Skip to content

Commit 24327d4

Browse files
authored
Merge pull request #92 from usnistgov/autosas_web_ui
AutoSAS UI
2 parents fad7a67 + e473934 commit 24327d4

File tree

9 files changed

+6935
-72
lines changed

9 files changed

+6935
-72
lines changed

.github/workflows/test-integration.yaml

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,7 @@ jobs:
1717

1818
- name: Build and start stack
1919
run: |
20-
docker compose up -d --build
21-
- name: Wait for services to become healthy
22-
run: |
23-
set -euo pipefail
24-
tiled_id=$(docker compose ps -q tiled)
25-
agent_id=$(docker compose ps -q agent)
26-
for i in $(seq 1 30); do
27-
th=$(docker inspect --format '{{.State.Health.Status}}' "$tiled_id") || true
28-
ah=$(docker inspect --format '{{.State.Health.Status}}' "$agent_id") || true
29-
if [ "$th" = "healthy" ] && [ "$ah" = "healthy" ]; then
30-
echo "Services healthy"; exit 0
31-
fi
32-
sleep 2
33-
done
34-
echo "Services did not become healthy" >&2
35-
docker compose ps
36-
exit 1
20+
docker compose up -d --build --wait --wait-timeout 240
3721
3822
- name: Smoke test Tiled API
3923
run: |
@@ -57,4 +41,4 @@ jobs:
5741
5842
- name: Teardown
5943
if: always()
60-
run: docker compose down -v
44+
run: docker compose down -v

AFL/double_agent/AutoSAS.py

Lines changed: 151 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import copy
22
import uuid
33
import warnings
4+
from types import SimpleNamespace
45
from collections import defaultdict
56
from dataclasses import dataclass
67
from 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

Comments
 (0)