PyAntiGen 1.0.11__tar.gz → 1.0.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. {pyantigen-1.0.11 → pyantigen-1.0.12}/PKG-INFO +1 -1
  2. {pyantigen-1.0.11 → pyantigen-1.0.12}/PyAntiGen.egg-info/PKG-INFO +1 -1
  3. pyantigen-1.0.12/PyAntiGen.egg-info/scm_version.json +8 -0
  4. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/_version.py +3 -3
  5. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Evaluator.py +26 -0
  6. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Model_optimize.py +20 -206
  7. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Optimize.py +454 -889
  8. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Simulate.py +18 -1
  9. pyantigen-1.0.11/PyAntiGen.egg-info/scm_version.json +0 -8
  10. {pyantigen-1.0.11 → pyantigen-1.0.12}/.github/workflows/publish.yml +0 -0
  11. {pyantigen-1.0.11 → pyantigen-1.0.12}/.gitignore +0 -0
  12. {pyantigen-1.0.11 → pyantigen-1.0.12}/.vscode/launch.json +0 -0
  13. {pyantigen-1.0.11 → pyantigen-1.0.12}/LICENSE +0 -0
  14. {pyantigen-1.0.11 → pyantigen-1.0.12}/MANIFEST.in +0 -0
  15. {pyantigen-1.0.11 → pyantigen-1.0.12}/PyAntiGen.egg-info/SOURCES.txt +0 -0
  16. {pyantigen-1.0.11 → pyantigen-1.0.12}/PyAntiGen.egg-info/dependency_links.txt +0 -0
  17. {pyantigen-1.0.11 → pyantigen-1.0.12}/PyAntiGen.egg-info/entry_points.txt +0 -0
  18. {pyantigen-1.0.11 → pyantigen-1.0.12}/PyAntiGen.egg-info/requires.txt +0 -0
  19. {pyantigen-1.0.11 → pyantigen-1.0.12}/PyAntiGen.egg-info/scm_file_list.json +0 -0
  20. {pyantigen-1.0.11 → pyantigen-1.0.12}/PyAntiGen.egg-info/top_level.txt +0 -0
  21. {pyantigen-1.0.11 → pyantigen-1.0.12}/README.md +0 -0
  22. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/.agents/skills/module_generation/SKILL.md +0 -0
  23. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/.agents/skills/ode_conversion_antimony/SKILL.md +0 -0
  24. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/.agents/skills/ode_extraction_pipeline/SKILL.md +0 -0
  25. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/.gitignore +0 -0
  26. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/AntimonyGen.py +0 -0
  27. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/RxnDict_to_antimony.py +0 -0
  28. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/TelluriumGen.py +0 -0
  29. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/__init__.py +0 -0
  30. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/antimony_utils.py +0 -0
  31. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/cli.py +0 -0
  32. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/data_interpolation.py +0 -0
  33. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/isotopomer_tools.py +0 -0
  34. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/model_generation.py +0 -0
  35. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/models.py +0 -0
  36. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/module_base.py +0 -0
  37. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/pyantigen.py +0 -0
  38. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/rate_laws.py +0 -0
  39. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/reaction_creation.py +0 -0
  40. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/AntiGen_paths.py +0 -0
  41. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Anchor_cache.py +0 -0
  42. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Deadline.py +0 -0
  43. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Event_times.py +0 -0
  44. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Fast_profile.py +0 -0
  45. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Fit_cache.py +0 -0
  46. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Identifiability.py +0 -0
  47. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Model_simulate.py +0 -0
  48. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Nuisance_sensitivity.py +0 -0
  49. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Petab_export.py +0 -0
  50. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Preequil_cache.py +0 -0
  51. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Profile_checkpoint.py +0 -0
  52. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Results.py +0 -0
  53. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Engine/Sensitivity_analysis.py +0 -0
  54. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Flipflop_reference.py +0 -0
  55. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Model_generate.py +0 -0
  56. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Model_run.py +0 -0
  57. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Modules/Data.py +0 -0
  58. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Modules/Events.py +0 -0
  59. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Modules/Experiment.py +0 -0
  60. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Modules/Loss_config.py +0 -0
  61. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Modules/Observed_species.py +0 -0
  62. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Modules/Optimizer_settings.py +0 -0
  63. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Modules/Plots.py +0 -0
  64. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Modules/Solver_settings.py +0 -0
  65. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Modules/Update_opt_parameters.py +0 -0
  66. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/Example/Modules/Update_parameters.py +0 -0
  67. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/data/ADneg.csv +0 -0
  68. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/data/ADpos.csv +0 -0
  69. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/data/Flipflop.csv +0 -0
  70. {pyantigen-1.0.11 → pyantigen-1.0.12}/framework/template/data/make_flipflop_data.py +0 -0
  71. {pyantigen-1.0.11 → pyantigen-1.0.12}/pyproject.toml +0 -0
  72. {pyantigen-1.0.11 → pyantigen-1.0.12}/setup.cfg +0 -0
  73. {pyantigen-1.0.11 → pyantigen-1.0.12}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAntiGen
3
- Version: 1.0.11
3
+ Version: 1.0.12
4
4
  Summary: Short one-line description of what PyAntiGen does
5
5
  Author: Open Source Contributor
6
6
  Author-email: Don <you@example.com>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAntiGen
3
- Version: 1.0.11
3
+ Version: 1.0.12
4
4
  Summary: Short one-line description of what PyAntiGen does
5
5
  Author: Open Source Contributor
6
6
  Author-email: Don <you@example.com>
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.0.12",
3
+ "distance": 0,
4
+ "node": "g3450403b38a37a7b62ac7644e0d82e8592310347",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-16"
8
+ }
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.0.11'
22
- __version_tuple__ = version_tuple = (1, 0, 11)
21
+ __version__ = version = '1.0.12'
22
+ __version_tuple__ = version_tuple = (1, 0, 12)
23
23
 
24
- __commit_id__ = commit_id = 'gdaaaf88e9'
24
+ __commit_id__ = commit_id = 'g3450403b3'
@@ -131,6 +131,17 @@ class EvalSpec:
131
131
  # in the parent, which runs the invariance check once; a worker must never
132
132
  # make that call on its own, or 40 of them would each re-derive it.
133
133
  preequil_cache: bool = False
134
+ # Modules.utils.noise_floor.export_cache() snapshot, taken in the parent
135
+ # AFTER its own calibration (see Engine.Optimize.run_optimization_from_groups,
136
+ # clear_cache() + the post-optimum re-evaluation). Workers seed their own
137
+ # (otherwise empty, since spawn shares no memory) floor cache from this in
138
+ # _init_worker, so every worker scores every floored observable against
139
+ # the SAME calibrated sigma the parent settled on, rather than each one
140
+ # independently calibrating against whatever parameter vector it happens
141
+ # to be handed first -- an arbitrary profile-grid point or Sobol sample,
142
+ # not the converged optimum. Same reasoning as fixed_sigmas above, one
143
+ # mechanism down: compute once where it's meaningful, ship the answer.
144
+ floor_cache: dict = field(default_factory=dict)
134
145
  # Reserved for future use by the profile grid (Stage 2).
135
146
  meta: dict = field(default_factory=dict)
136
147
 
@@ -149,8 +160,16 @@ def _init_worker(spec_blob):
149
160
  from framework.TelluriumGen import TelluriumGen
150
161
  from Engine.Event_times import attach_event_times
151
162
  from Engine.Optimize import OptRoadRunnerProxy
163
+ from Modules.utils.noise_floor import seed_cache
152
164
 
153
165
  spec = _serializer.loads(spec_blob)
166
+ # Before any task runs: this worker's own Modules.utils.noise_floor
167
+ # module was just re-imported fresh (spawn shares no memory with the
168
+ # parent), so its floor cache starts empty. Seed it from the parent's
169
+ # already-calibrated snapshot so every worker agrees with the parent --
170
+ # and with each other -- on every floored observable's sigma, instead of
171
+ # each recalibrating independently against whichever task it draws first.
172
+ seed_cache(spec.floor_cache)
154
173
  models = {}
155
174
  t0 = time.time()
156
175
  for sim_name, replicate in spec.replicates.items():
@@ -1125,6 +1144,7 @@ def build_eval_spec(
1125
1144
  ):
1126
1145
  """Convenience constructor mirroring the spec-route local variables."""
1127
1146
  from Engine.Event_times import without_event_times
1147
+ from Modules.utils.noise_floor import export_cache
1128
1148
 
1129
1149
  return EvalSpec(
1130
1150
  model_text=model_text,
@@ -1150,6 +1170,12 @@ def build_eval_spec(
1150
1170
  for_inference=bool(for_inference),
1151
1171
  concentrated=bool(concentrated),
1152
1172
  preequil_cache=bool(preequil_cache),
1173
+ # Captured HERE, at spec-build time -- called in the parent after its
1174
+ # own clear_cache()-and-recalibrate pass (see run_optimization_from_
1175
+ # groups), so this snapshot is the same calibration the parent's own
1176
+ # subsequent diagnostics use, not whatever was cached earlier in the
1177
+ # run (e.g. during the live optimize()).
1178
+ floor_cache=export_cache(),
1153
1179
  )
1154
1180
 
1155
1181
 
@@ -5,8 +5,8 @@ High-level optimization setup. Two entry points:
5
5
  Original flat-dict path: experiments is a plain dict of treatment dicts.
6
6
 
7
7
  setup_optimization_from_groups(settings, optimization_settings, EXPERIMENT_dict)
8
- Group-aware path: uses Experiment.opt_groups (derived from each replicate's
9
- Opt_group key) to define which replicates contribute to the objective.
8
+ Which replicates contribute to the objective is read off the
9
+ Optimization spec: each group's loss_elements name their simulations.
10
10
  """
11
11
  import os
12
12
  import pandas as pd
@@ -1166,15 +1166,6 @@ def setup_optimization(settings, optimization_settings, experiment_dict):
1166
1166
  # Group-aware entry point
1167
1167
  # ---------------------------------------------------------------------------
1168
1168
 
1169
- def _is_per_group_settings(optimization_settings):
1170
- """Return True when optimization_settings contains per-group sub-dicts
1171
- (e.g. the PK block keyed by drug name), False for a flat shared dict."""
1172
- return any(
1173
- isinstance(v, dict) and "param_names" in v
1174
- for v in optimization_settings.values()
1175
- )
1176
-
1177
-
1178
1169
  def setup_optimization_from_groups(settings, optimization_settings, EXPERIMENT_dict):
1179
1170
  """
1180
1171
  Run parameter optimization using Experiment.opt_groups.
@@ -1183,13 +1174,9 @@ def setup_optimization_from_groups(settings, optimization_settings, EXPERIMENT_d
1183
1174
  Replicates whose Loss_config is ``no_optimization()`` are simulated at the
1184
1175
  end with the optimal parameters for use by the plot function.
1185
1176
 
1186
- Flat mode — optimization_settings has top-level param_names/x0/bounds:
1187
- one optimization is run, summing NLL across all active groups.
1188
-
1189
- Per-group mode — optimization_settings has per-group sub-dicts each
1190
- containing param_names/x0/bounds (e.g. the PK block keyed by drug):
1191
- one independent optimization is run per group, results accumulated,
1192
- then plot_function is called once.
1177
+ optimization_settings must be an Optimization spec. The flat and
1178
+ per-group dict modes were removed on 2026-09-09; run one invocation per
1179
+ fit instead.
1193
1180
  """
1194
1181
  MODEL_NAME = settings.get("MODEL_NAME", AntiGen_paths.MODEL_NAME)
1195
1182
  model_text, paths = AntimonyGen(MODEL_NAME, repo_root=REPO_ROOT)
@@ -1253,201 +1240,28 @@ def setup_optimization_from_groups(settings, optimization_settings, EXPERIMENT_d
1253
1240
  _shutdown_evaluator(opt)
1254
1241
  return opt
1255
1242
 
1256
- if _is_per_group_settings(optimization_settings):
1257
- # ── Per-group mode ────────────────────────────────────────────────
1258
- all_results = {}
1259
- group_optimizations = {}
1260
- allowed_groups = optimization_settings.get("group_names") # None = run all
1261
- # Replicates whose Opt_group is not among any per-group sub-dict key
1262
- # are treated as passive (plot-only) and kept in all_results regardless
1263
- # of which sub-group is being optimized.
1264
- known_opt_groups = {
1265
- name for name, val in optimization_settings.items()
1266
- if isinstance(val, dict) and val.get("param_names")
1267
- }
1268
- for group_name, group_settings in optimization_settings.items():
1269
- if not isinstance(group_settings, dict) or not group_settings.get("param_names"):
1270
- continue
1271
- if allowed_groups is not None and group_name not in allowed_groups:
1272
- continue
1273
- param_names = group_settings["param_names"]
1274
- method = group_settings.get("method", "Nelder-Mead")
1275
-
1276
- try:
1277
- opt = run_optimization_from_groups(
1278
- model_text, paths, experiment,
1279
- param_names=param_names,
1280
- x0=group_settings["x0"],
1281
- bounds=group_settings.get("bounds"),
1282
- group_names=[group_name],
1283
- method=method,
1284
- optimizer_kwargs=group_settings.get("optimizer_kwargs", {}),
1285
- wald_analysis=group_settings.get("wald_analysis", False),
1286
- slice_analysis=group_settings.get("slice_analysis", False),
1287
- profile_likelihood_analysis=group_settings.get("profile_likelihood_analysis", False),
1288
- fast_profile_likelihood_analysis=group_settings.get("fast_profile_likelihood_analysis", False),
1289
- sobol_analysis=group_settings.get("sobol_analysis", False),
1290
- sobol_kwargs={"N": group_settings.get("sobol_N", 128), "mode": group_settings.get("sobol_mode", "loss")},
1291
- fit_mode=group_settings.get("fit_mode", settings.get("fit_mode")),
1292
- n_workers=group_settings.get("n_workers", settings.get("n_workers")),
1293
- )
1294
- except Exception as e:
1295
- print(f"Warning: optimization for '{group_name}' failed: {e}")
1296
- continue
1297
-
1298
- if opt.get("results_dict"):
1299
- filtered_results = {}
1300
- for req_id, item in opt["results_dict"].items():
1301
- item_group = item.get("replicate", {}).get("Opt_group")
1302
- if item_group == group_name or item_group not in known_opt_groups:
1303
- filtered_results[req_id] = item
1304
- all_results.update(filtered_results)
1305
-
1306
- group_optimizations[group_name] = opt
1307
-
1308
- # Run analysis plots first so profile_ci is populated before CSV write
1309
- if group_settings.get("slice_analysis") and opt.get("stats", {}).get("likelihood_slice"):
1310
- _save_likelihood_slice_plot(opt, param_names, paths["plot_path"],
1311
- MODEL_NAME, tag=group_name)
1312
- if (group_settings.get("profile_likelihood_analysis") or group_settings.get("fast_profile_likelihood_analysis")) and opt.get("stats", {}).get("profile_likelihood"):
1313
- _save_profile_likelihood_plot(
1314
- opt, param_names, paths["plot_path"], MODEL_NAME,
1315
- tag=group_name,
1316
- profile_kwargs=_profile_kwargs(group_settings))
1317
- if group_settings.get("sobol_analysis") and opt.get("stats", {}).get("sobol"):
1318
- from Engine.Sensitivity_analysis import save_sobol_plot
1319
- save_sobol_plot(opt.get("stats", {}).get("sobol"), paths["plot_path"],
1320
- MODEL_NAME, tag=group_name)
1321
-
1322
- print(f"\nOptimization Summary for {group_name}:")
1323
- print("-" * 85)
1324
- print(f"{'Parameter':<30} | {'Optimized Value':<15} | {'Wald SE':<15} | {'Wald 95% CI':<20}")
1325
- print("-" * 85)
1326
- x_vals = opt.get("x", [])
1327
- se = opt.get("stats", {}).get("wald_se")
1328
- ci = opt.get("stats", {}).get("wald_ci")
1329
- for i, p_name in enumerate(param_names):
1330
- val = x_vals[i] if i < len(x_vals) else float('nan')
1331
- std_err = se[i] if se is not None and i < len(se) else "N/A"
1332
- std_err_str = f"{std_err:.4g}" if isinstance(std_err, (int, float)) else std_err
1333
- conf_int = ci[i] if ci is not None and i < len(ci) else ("N/A", "N/A")
1334
- if isinstance(conf_int, tuple) and len(conf_int) == 2:
1335
- if isinstance(conf_int[0], (int, float)) and isinstance(conf_int[1], (int, float)):
1336
- conf_int_str = f"[{conf_int[0]:.4g}, {conf_int[1]:.4g}]"
1337
- else:
1338
- conf_int_str = f"[{conf_int[0]}, {conf_int[1]}]"
1339
- else:
1340
- conf_int_str = str(conf_int)
1341
- print(f"{p_name:<30} | {val:<15.4g} | {std_err_str:<15} | {conf_int_str:<20}")
1342
- print("-" * 85)
1343
- print(f"Final Objective Value (NLL): {opt.get('fun', 'N/A'):.6g}\n")
1344
-
1345
- corr_matrix = opt.get("stats", {}).get("wald_correlation")
1346
- if corr_matrix is not None:
1347
- print(f"\nWald Correlation Matrix:")
1348
- print("-" * 85)
1349
- header_str = f"{'':<25} | " + " | ".join(f"{p[:10]:<10}" for p in param_names)
1350
- print(header_str)
1351
- print("-" * 85)
1352
- for i, p_row in enumerate(param_names):
1353
- row_str = f"{p_row[:25]:<25} | "
1354
- row_vals = []
1355
- for j in range(len(param_names)):
1356
- if i < len(corr_matrix) and j < len(corr_matrix[i]):
1357
- row_vals.append(f"{corr_matrix[i][j]:<10.4g}")
1358
- else:
1359
- row_vals.append(f"{'N/A':<10}")
1360
- row_str += " | ".join(row_vals)
1361
- print(row_str)
1362
- print("-" * 85)
1363
- print()
1364
-
1365
- csv_path = os.path.join(
1366
- paths["plot_path"],
1367
- f"{MODEL_NAME}_{group_name}_optimization_results.csv",
1368
- )
1369
- log_optimization_results(opt, param_names, csv_path,
1370
- model_name=MODEL_NAME, experiment_id=group_name,
1371
- method=method)
1372
-
1373
- if optimization_settings.get("petab_export"):
1374
- _write_petab_archive(paths, MODEL_NAME, experiment,
1375
- optimization_settings, group_optimizations)
1376
-
1377
- if all_results and plot_function:
1378
- plot_function(paths, all_results)
1379
- for _o in group_optimizations.values():
1380
- _shutdown_evaluator(_o)
1381
- return group_optimizations
1382
-
1383
- else:
1384
- # ── Flat (shared) mode ────────────────────────────────────────────
1385
- param_names = optimization_settings["param_names"]
1386
- x0 = optimization_settings["x0"]
1387
- bounds = optimization_settings.get("bounds")
1388
- method = optimization_settings.get("method", "Nelder-Mead")
1389
- opt_kwargs = optimization_settings.get("optimizer_kwargs", {})
1390
- group_names = optimization_settings.get("group_names", None)
1391
-
1392
- if not param_names:
1393
- print("Error: No parameters to optimize. Set param_names and x0 in optimization_settings.")
1394
- return
1395
-
1396
- opt = run_optimization_from_groups(
1397
- model_text, paths, experiment,
1398
- param_names=param_names,
1399
- x0=x0,
1400
- bounds=bounds,
1401
- group_names=group_names,
1402
- method=method,
1403
- optimizer_kwargs=opt_kwargs,
1404
- wald_analysis=optimization_settings.get("wald_analysis", False),
1405
- slice_analysis=optimization_settings.get("slice_analysis", False),
1406
- profile_likelihood_analysis=optimization_settings.get("profile_likelihood_analysis", False),
1407
- fast_profile_likelihood_analysis=optimization_settings.get("fast_profile_likelihood_analysis", False),
1408
- sobol_analysis=optimization_settings.get("sobol_analysis", False),
1409
- sobol_kwargs={"N": optimization_settings.get("sobol_N", 128), "mode": optimization_settings.get("sobol_mode", "loss")},
1410
- fit_mode=optimization_settings.get("fit_mode", settings.get("fit_mode")),
1411
- n_workers=optimization_settings.get("n_workers", settings.get("n_workers")),
1412
- )
1413
-
1414
- groups_tag = "_".join(opt.get("groups", ["ALL"]))
1415
-
1416
- # Run analysis plots first so profile_ci is populated before CSV write
1417
- if optimization_settings.get("slice_analysis") and opt["stats"].get("likelihood_slice"):
1418
- _save_likelihood_slice_plot(opt, param_names, paths["plot_path"],
1419
- MODEL_NAME, tag=groups_tag)
1420
- if (optimization_settings.get("profile_likelihood_analysis") or optimization_settings.get("fast_profile_likelihood_analysis")) and opt["stats"].get("profile_likelihood"):
1421
- _save_profile_likelihood_plot(
1422
- opt, param_names, paths["plot_path"], MODEL_NAME,
1423
- tag=groups_tag,
1424
- profile_kwargs=_profile_kwargs(optimization_settings))
1425
- if optimization_settings.get("sobol_analysis") and opt["stats"].get("sobol"):
1426
- from Engine.Sensitivity_analysis import save_sobol_plot
1427
- save_sobol_plot(opt["stats"]["sobol"], paths["plot_path"],
1428
- MODEL_NAME, tag=groups_tag)
1429
-
1430
- csv_path = os.path.join(
1431
- paths["plot_path"],
1432
- f"{MODEL_NAME}_{groups_tag}_optimization_results.csv",
1433
- )
1434
- log_optimization_results(opt, param_names, csv_path,
1435
- model_name=MODEL_NAME, experiment_id=groups_tag, method=method)
1243
+ raise TypeError(
1244
+ "setup_optimization_from_groups needs an Optimization spec from "
1245
+ "Modules/Optimizer_settings.py; got "
1246
+ f"{type(optimization_settings).__name__}. The per-group and flat "
1247
+ "dict routes were removed on 2026-09-09: nothing used them, and they "
1248
+ "selected replicates by a per-replicate 'Opt_group' key that no longer "
1249
+ "exists. To run several fits, run several invocations."
1250
+ )
1436
1251
 
1437
- if optimization_settings.get("petab_export"):
1438
- _write_petab_archive(paths, MODEL_NAME, experiment,
1439
- optimization_settings, {"__flat__": opt})
1440
-
1441
- if opt.get("results_dict") is not None and plot_function:
1442
- plot_function(paths, opt["results_dict"])
1443
- _shutdown_evaluator(opt)
1444
- return opt
1445
1252
 
1446
1253
 
1447
1254
  def _write_petab_archive(paths, model_name, experiment,
1448
1255
  optimization_settings, group_optimizations):
1449
1256
  """Write a PEtab v2 archive to results/<model>/petab/<expid>/.
1450
1257
 
1258
+ NOT CALLED as of 2026-09-09. Its only caller was the per-group dict route,
1259
+ removed that day along with the flat route because nothing could reach
1260
+ either. The function is kept because PEtab export is a feature rather than
1261
+ plumbing, and rewiring it to the spec route is a small job: it wants a
1262
+ {group_name: opt} mapping, which for a spec fit is a single entry. Delete
1263
+ it if PEtab export is not wanted.
1264
+
1451
1265
  ``expid`` is built from the sorted union of optimized group names so
1452
1266
  successive runs against different groups land in distinct subdirs.
1453
1267
  """