@@ -187,6 +187,7 @@ def on_fit_failed(self, error_message: str) -> None:
187187 """
188188 self ._result = None
189189 self ._results = []
190+ self ._prepared = None
190191 self ._fit_error_message = error_message
191192 self ._running = False
192193 self ._finished = True
@@ -248,11 +249,14 @@ def _ordered_experiments(self) -> list:
248249 'Bayesian sampling of polarized experiments is not supported yet.'
249250 )
250251
251- def _has_polarized_experiments (self ) -> bool :
252- """Whether any loaded experiment carries per-channel (polarized) data."""
253- return any (
254- getattr (experiment , 'available_channels' , None ) is not None for experiment in self ._ordered_experiments ()
255- )
252+ def _has_polarized_experiments (self , experiments : Optional [list ] = None ) -> bool :
253+ """Whether any of `experiments` (by default every loaded one) carries per-channel data."""
254+ experiments = self ._ordered_experiments () if experiments is None else experiments
255+ return any (getattr (experiment , 'available_channels' , None ) is not None for experiment in experiments )
256+
257+ def _included_experiments (self ) -> list :
258+ """The experiments whose Fit box is ticked, in order."""
259+ return [experiment for experiment in self ._ordered_experiments () if getattr (experiment , 'include_in_fit' , True )]
256260
257261 # ------------------------------------------------------------------
258262 # Inequality constraints (BUMPS penalties)
@@ -377,11 +381,13 @@ def _fail_before_start(self, message: str) -> None:
377381 self ._finished = True
378382 self ._show_results_dialog = True
379383
380- def record_on_project (self , results : Optional [list ], status : str = 'completed' ) -> None :
381- """Record the outcome of the worker's run on the project (``Project.last_fit``),
382- which the results dialog and the summary read."""
384+ def record_on_project (self , results : list ) -> None :
385+ """Record the completed run on the project (``Project.last_fit``), which the results
386+ dialog and the summary read. The prepared run is used up: a later failure, or a
387+ sampling run, cannot be recorded against it."""
383388 if self ._prepared is not None :
384- self ._project_lib .record_fit (self ._prepared , results , status )
389+ prepared , self ._prepared = self ._prepared , None
390+ self ._project_lib .record_fit (prepared , results )
385391
386392 @property
387393 def _run (self ):
@@ -399,6 +405,7 @@ def fit_dataset_rows(self) -> list[dict]:
399405 {
400406 'name' : name if channel is None else f'{ name } ({ channel } )' ,
401407 'points' : entry ['objective_n_points' ],
408+ 'measuredPoints' : entry ['classical_n_points' ],
402409 'chi2' : entry ['objective_chi2' ],
403410 'chi2PerPoint' : entry ['objective_chi2_per_point' ],
404411 'share' : entry ['share_of_objective' ],
@@ -430,25 +437,23 @@ def fit_classical_reduced_chi2(self) -> float | None:
430437 # Bayesian sampling helpers
431438 # ------------------------------------------------------------------
432439
433- def collect_all_experiments_datagroup (self ) -> 'sc.DataGroup' :
440+ def collect_all_experiments_datagroup (self , experiments : Optional [ list ] = None ) -> 'sc.DataGroup' :
434441 """Build the scipp DataGroup required by reflectometry-lib ``MultiFitter.mcmc_sample()``.
435442
436- Scope decision (see issue #319): Bayesian sampling deliberately runs over
437- **all** experiments, mirroring the classical fit path (``prepare_threaded_fit``)
438- which also fits every experiment. The experiment selection currently affects
439- only plotting, not the fit/sampling scope. If per-selection sampling is ever
440- wanted, filter ``self._ordered_experiments()`` here and rename accordingly.
443+ Sampling covers the experiments whose Fit box is ticked, like a classical fit;
444+ the selection only affects plotting.
441445
446+ :param experiments: The experiments to sample, by default the included ones.
442447 :return: DataGroup with reflectivity coords and data.
443448 :rtype: sc.DataGroup
444449 """
445450 import numpy as np
446451 import scipp as sc
447452
448- if self ._has_polarized_experiments ():
453+ experiments = self ._included_experiments () if experiments is None else experiments
454+ if self ._has_polarized_experiments (experiments ):
449455 raise ValueError (self ._POLARIZED_SAMPLE_MESSAGE )
450456
451- experiments = self ._ordered_experiments ()
452457 coords = {}
453458 data = {}
454459 for i , experiment in enumerate (experiments ):
@@ -483,17 +488,26 @@ def prepare_threaded_sample(self, minimizers_logic: 'Minimizers') -> tuple:
483488 try :
484489 from easyreflectometry .fitting import MultiFitter
485490
486- experiments = self ._ordered_experiments ()
487- if not experiments :
491+ if not self ._ordered_experiments ():
488492 self ._fail_before_start ('No experiments to sample' )
489493 return None , None
494+ experiments = self ._included_experiments ()
495+ if not experiments :
496+ self ._fail_before_start ('No experiment to sample: tick Fit for at least one experiment.' )
497+ return None , None
498+ unbound = [experiment .name for experiment in experiments if experiment .model is None ]
499+ if unbound :
500+ names = ', ' .join (f"'{ name } '" for name in unbound )
501+ self ._fail_before_start (f'{ names } cannot be sampled: no model is assigned.' )
502+ return None , None
490503
491504 constraints_error = self .inequality_constraints_error (minimizers_logic )
492505 if constraints_error :
493506 logger .warning ('Sampling refused: %s' , constraints_error )
494507 self ._fail_before_start (constraints_error )
495508 return None , None
496509
510+ # The library adds the free parameters of excluded models that these follow.
497511 models = [experiment .model for experiment in experiments ]
498512 multi_fitter = MultiFitter (* models )
499513 # Sampled with a snapshot of the project's settings (a BUMPS minimizer
@@ -502,7 +516,7 @@ def prepare_threaded_sample(self, minimizers_logic: 'Minimizers') -> tuple:
502516 # controls stay editable meanwhile.
503517 multi_fitter .settings = copy .deepcopy (self ._project_lib .fit_settings )
504518
505- data_group = self .collect_all_experiments_datagroup ()
519+ data_group = self .collect_all_experiments_datagroup (experiments )
506520 return multi_fitter , data_group
507521 except Exception as e :
508522 self ._fail_before_start (f'Error preparing sampling: { e } ' )
@@ -511,6 +525,7 @@ def prepare_threaded_sample(self, minimizers_logic: 'Minimizers') -> tuple:
511525
512526 def prepare_for_threaded_sample (self ) -> None :
513527 """Set running flags and sampling progress message before launching the worker."""
528+ self ._prepared = None # a sampling run is not recorded as a classical one
514529 self .reset_stop_flag ()
515530 self ._running = True
516531 self ._finished = False
0 commit comments