11import copy
22import uuid
33import warnings
4+ from types import SimpleNamespace
45from collections import defaultdict
56from dataclasses import dataclass
67from typing import Any , ClassVar
@@ -100,10 +101,19 @@ def _initialize_model(self) -> None:
100101 self .model .parameters ()[key ].value = param_dict ["value" ]
101102
102103 if param_dict .get ("bounds" ) is not None :
104+ raw_bounds = param_dict ["bounds" ]
105+ lower , upper = float (raw_bounds [0 ]), float (raw_bounds [1 ])
106+ if lower > upper :
107+ lower , upper = upper , lower
108+
103109 self .model .parameters ()[key ].fixed = False
104- self .model .parameters ()[key ].bounds = bumps .bounds .Bounded (
105- * param_dict ["bounds" ]
106- )
110+ # Newer bumps expects an iterable (min, max) tuple. Keep a
111+ # compatibility fallback for environments that still expect
112+ # a Bounded instance.
113+ try :
114+ self .model .parameters ()[key ].bounds = (lower , upper )
115+ except Exception :
116+ self .model .parameters ()[key ].bounds = bumps .bounds .Bounded (lower , upper )
107117 else :
108118 self .model .parameters ()[key ].fixed = True
109119
@@ -163,6 +173,17 @@ def fit(self, fit_method: dict[str, Any] | None = None) -> Any:
163173 "xtol" : 1.5e-6 ,
164174 "verbose" : False ,
165175 }
176+
177+ # If there are no free parameters, treat this as a fixed-parameter
178+ # model evaluation and skip optimizer invocation (which can fail with
179+ # empty parameter vectors in newer bumps versions).
180+ if len (self .problem .labels ()) == 0 :
181+ self .results = SimpleNamespace (x = np .array ([], dtype = float ), dx = np .array ([], dtype = float ))
182+ self .fit_params = {}
183+ self .model_I = self ()
184+ self .model_q = self .data .x [self .data .mask == 0 ]
185+ self .model_cov = np .zeros ((0 , 0 ), dtype = float )
186+ return self .results
166187
167188 # Try to fit with provided method, fall back to defaults if it fails
168189 try :
@@ -183,7 +204,36 @@ def fit(self, fit_method: dict[str, Any] | None = None) -> Any:
183204 self .fit_params = dict (zip (self .problem .labels (), self .problem .getp ()))
184205 self .model_I = self (params = self .fit_params )
185206 self .model_q = self .data .x [self .data .mask == 0 ]
186- self .model_cov = self .problem .cov ()
207+ n_params = len (self .problem .labels ())
208+
209+ # Bumps covariance API differs across versions. Prefer direct problem
210+ # covariance if available, then fit-result covariance, and finally fall
211+ # back to a diagonal covariance to keep downstream reporting robust.
212+ cov_matrix = None
213+ if hasattr (self .problem , "cov" ):
214+ try :
215+ cov_matrix = self .problem .cov ()
216+ except Exception :
217+ cov_matrix = None
218+
219+ if cov_matrix is None and self .results is not None :
220+ for attr_name in ("cov" , "covariance" ):
221+ if hasattr (self .results , attr_name ):
222+ attr = getattr (self .results , attr_name )
223+ try :
224+ cov_matrix = attr () if callable (attr ) else attr
225+ except Exception :
226+ cov_matrix = None
227+ if cov_matrix is not None :
228+ break
229+
230+ if cov_matrix is None :
231+ cov_matrix = np .eye (n_params , dtype = float )
232+
233+ cov_matrix = np .asarray (cov_matrix , dtype = float )
234+ if cov_matrix .shape != (n_params , n_params ):
235+ cov_matrix = np .eye (n_params , dtype = float )
236+ self .model_cov = cov_matrix
187237
188238 return self .results
189239
@@ -201,10 +251,17 @@ def get_fit_params(self) -> dict[str, dict[str, Any]]:
201251 params = copy .deepcopy (self .init_params )
202252
203253 # Update with fitted values and errors
254+ dx = getattr (self .results , "dx" , None )
204255 for idx , param_name in enumerate (self .problem .labels ()):
256+ err_val = np .nan
257+ if dx is not None :
258+ try :
259+ err_val = float (dx [idx ])
260+ except Exception :
261+ err_val = np .nan
205262 params [param_name ] = {
206263 "value" : self .results .x [idx ],
207- "error" : self . results . dx [ idx ]
264+ "error" : err_val
208265 }
209266
210267 # Remove bounds from the output parameters
@@ -514,6 +571,7 @@ def _calculate_probabilities(self) -> np.ndarray:
514571 Array of model probabilities
515572 """
516573 all_probabilities = []
574+ eps = 1e-300
517575
518576 for sample_results in self .fit_results :
519577 # Calculate log likelihood for each model
@@ -522,20 +580,63 @@ def _calculate_probabilities(self) -> np.ndarray:
522580 for model in sample_results :
523581 # Get model parameters
524582 chisq = model ["chisq" ]
525- cov_matrix = np .array (model ["cov" ])
526- n_params = len (cov_matrix )
583+ cov_matrix = np .asarray (model .get ("cov" , []), dtype = float )
584+
585+ # Normalize covariance shape across bumps/scipy versions.
586+ if cov_matrix .ndim == 0 :
587+ cov_matrix = cov_matrix .reshape (1 , 1 )
588+ elif cov_matrix .ndim == 1 :
589+ if cov_matrix .size == 0 :
590+ cov_matrix = np .zeros ((0 , 0 ), dtype = float )
591+ else :
592+ cov_matrix = np .diag (cov_matrix )
593+ elif cov_matrix .ndim > 2 :
594+ cov_matrix = np .squeeze (cov_matrix )
595+ if cov_matrix .ndim != 2 :
596+ cov_matrix = np .zeros ((0 , 0 ), dtype = float )
597+
598+ if cov_matrix .shape [0 ] != cov_matrix .shape [1 ]:
599+ # Fall back to smallest square representation.
600+ n = min (cov_matrix .shape [0 ], cov_matrix .shape [1 ]) if cov_matrix .ndim == 2 else 0
601+ cov_matrix = cov_matrix [:n , :n ] if n > 0 else np .zeros ((0 , 0 ), dtype = float )
602+
603+ n_params = int (cov_matrix .shape [0 ])
527604
528605 # Calculate log marginal likelihood
606+ if n_params == 0 :
607+ log_det_term = 0.0
608+ else :
609+ try :
610+ sign , logdet = np .linalg .slogdet (cov_matrix )
611+ if sign <= 0 or not np .isfinite (logdet ):
612+ log_det_term = np .log (eps )
613+ else :
614+ log_det_term = float (logdet )
615+ except Exception :
616+ log_det_term = np .log (eps )
617+
529618 log_likelihood = (
530619 - chisq +
531- 0.5 * np . log ( np . linalg . det ( cov_matrix )) +
620+ 0.5 * log_det_term +
532621 0.5 * n_params * np .log (2 * np .pi )
533622 )
534623
535624 log_likelihoods .append (log_likelihood )
536625
537626 # Convert to probabilities
538- likelihoods = np .exp (log_likelihoods )
627+ finite = np .asarray (log_likelihoods , dtype = float )
628+ finite [~ np .isfinite (finite )] = - np .inf
629+ max_ll = np .max (finite )
630+ if not np .isfinite (max_ll ):
631+ probabilities = np .ones (len (finite ), dtype = float ) / max (len (finite ), 1 )
632+ all_probabilities .append (probabilities )
633+ continue
634+
635+ likelihoods = np .exp (finite - max_ll )
636+ if not np .any (np .isfinite (likelihoods )) or float (np .sum (likelihoods )) <= 0 :
637+ probabilities = np .ones (len (finite ), dtype = float ) / max (len (finite ), 1 )
638+ all_probabilities .append (probabilities )
639+ continue
539640 probabilities = likelihoods / np .sum (likelihoods )
540641
541642 all_probabilities .append (probabilities )
@@ -826,35 +927,65 @@ def _calculate_remote(self, dataset):
826927 """
827928 # Create client connection
828929 self .construct_clients ()
829-
830- # Send dataset to the server
831- db_uuid = self .AutoSAS_client .deposit_obj (obj = dataset )
832930
833931 if self .q_min or self .q_max :
834932 self .AutoSAS_client .set_config (
835933 q_min = self .q_min ,
836934 q_max = self .q_max
837935 )
838936
937+ entry_ids = []
938+ for key in ("tiled_entry_ids" , "autosas_tiled_entry_ids" , "entry_ids" ):
939+ candidate = dataset .attrs .get (key )
940+ if isinstance (candidate , (list , tuple )):
941+ entry_ids = [str (v ) for v in candidate if str (v ).strip ()]
942+ if entry_ids :
943+ break
944+
945+ if entry_ids :
946+ context_kw = {
947+ "entry_ids" : entry_ids ,
948+ "concat_dim" : self .sample_dim ,
949+ "sample_dim" : self .sample_dim ,
950+ "q_variable" : self .q_variable ,
951+ "sas_variable" : self .sas_variable ,
952+ "sas_err_variable" : self .sas_err_variable ,
953+ "sas_resolution_variable" : self .resolution ,
954+ }
955+ else :
956+ context_kw = {
957+ "dataset_dict" : dataset .to_dict (data = True ),
958+ "sample_dim" : self .sample_dim ,
959+ "q_variable" : self .q_variable ,
960+ "sas_variable" : self .sas_variable ,
961+ "sas_err_variable" : self .sas_err_variable ,
962+ "sas_resolution_variable" : self .resolution ,
963+ }
964+
839965 # Initialize the input data for fitting
840966 self .AutoSAS_client .enqueue (
841967 task_name = "set_sasdata" ,
842- db_uuid = db_uuid ,
843- sample_dim = self .sample_dim ,
844- q_variable = self .q_variable ,
845- sas_variable = self .sas_variable ,
846- sas_err_variable = self .sas_err_variable ,
968+ ** context_kw ,
847969 )
848970
849971 # Run the fitting
850- fit_calc_id = self .AutoSAS_client .enqueue (
972+ fit_result = self .AutoSAS_client .enqueue (
851973 task_name = "fit_models" ,
852974 fit_method = self .fit_method ,
853975 interactive = True
854976 )['return_val' ]
855977
856- # Retrieve the results
857- autosas_fit = self .AutoSAS_client .retrieve_obj (uid = fit_calc_id , delete = False )
978+ if isinstance (fit_result , xr .Dataset ):
979+ autosas_fit = fit_result
980+ else :
981+ fit_calc_id = fit_result
982+ # Retrieve the results from Tiled-backed fit records.
983+ autosas_fit = self .AutoSAS_client .enqueue (
984+ task_name = "get_fit_dataset" ,
985+ fit_uuid = fit_calc_id ,
986+ fit_task_name = "fit_models" ,
987+ interactive = True ,
988+ )["return_val" ]
858989
859990 # Rename variables and dimensions to match our naming convention
860991 autosas_fit = autosas_fit .rename_vars ({
@@ -1440,4 +1571,3 @@ def calculate(self, dataset):
14401571 dims = [self .sample_dim ]
14411572 )
14421573 return self
1443-
0 commit comments