PyAntiGen 2.0.3__tar.gz → 2.1.0__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 (105) hide show
  1. {pyantigen-2.0.3 → pyantigen-2.1.0}/PKG-INFO +1 -1
  2. {pyantigen-2.0.3 → pyantigen-2.1.0}/PyAntiGen.egg-info/PKG-INFO +1 -1
  3. {pyantigen-2.0.3 → pyantigen-2.1.0}/PyAntiGen.egg-info/SOURCES.txt +2 -0
  4. {pyantigen-2.0.3 → pyantigen-2.1.0}/PyAntiGen.egg-info/scm_file_list.json +8 -0
  5. pyantigen-2.1.0/PyAntiGen.egg-info/scm_version.json +8 -0
  6. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/_version.py +3 -3
  7. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Evaluator.py +33 -4
  8. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Fit_cache.py +53 -1
  9. pyantigen-2.1.0/pyantigen/engine/Model_cache.py +75 -0
  10. pyantigen-2.1.0/pyantigen/engine/Multistart.py +502 -0
  11. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Optimize.py +720 -49
  12. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Preequil_cache.py +5 -2
  13. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Results.py +38 -4
  14. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Simulate.py +83 -16
  15. pyantigen-2.0.3/PyAntiGen.egg-info/scm_version.json +0 -8
  16. {pyantigen-2.0.3 → pyantigen-2.1.0}/.github/workflows/publish.yml +0 -0
  17. {pyantigen-2.0.3 → pyantigen-2.1.0}/.github/workflows/tests.yml +0 -0
  18. {pyantigen-2.0.3 → pyantigen-2.1.0}/.gitignore +0 -0
  19. {pyantigen-2.0.3 → pyantigen-2.1.0}/.vscode/launch.json +0 -0
  20. {pyantigen-2.0.3 → pyantigen-2.1.0}/LICENSE +0 -0
  21. {pyantigen-2.0.3 → pyantigen-2.1.0}/MANIFEST.in +0 -0
  22. {pyantigen-2.0.3 → pyantigen-2.1.0}/PyAntiGen.egg-info/dependency_links.txt +0 -0
  23. {pyantigen-2.0.3 → pyantigen-2.1.0}/PyAntiGen.egg-info/entry_points.txt +0 -0
  24. {pyantigen-2.0.3 → pyantigen-2.1.0}/PyAntiGen.egg-info/requires.txt +0 -0
  25. {pyantigen-2.0.3 → pyantigen-2.1.0}/PyAntiGen.egg-info/top_level.txt +0 -0
  26. {pyantigen-2.0.3 → pyantigen-2.1.0}/README.md +0 -0
  27. {pyantigen-2.0.3 → pyantigen-2.1.0}/docs/MULTISTART.md +0 -0
  28. {pyantigen-2.0.3 → pyantigen-2.1.0}/docs/PROTOCOL_LAYER.md +0 -0
  29. {pyantigen-2.0.3 → pyantigen-2.1.0}/docs/V2_DESIGN.md +0 -0
  30. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/.agents/skills/module_generation/SKILL.md +0 -0
  31. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/.agents/skills/ode_conversion_antimony/SKILL.md +0 -0
  32. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/.agents/skills/ode_extraction_pipeline/SKILL.md +0 -0
  33. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/.gitignore +0 -0
  34. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/__init__.py +0 -0
  35. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/cli.py +0 -0
  36. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Anchor_cache.py +0 -0
  37. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Deadline.py +0 -0
  38. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Differential_evolution.py +0 -0
  39. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Event_times.py +0 -0
  40. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Fast_profile.py +0 -0
  41. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Identifiability.py +0 -0
  42. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Model_optimize.py +0 -0
  43. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Model_simulate.py +0 -0
  44. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Nelder_mead.py +0 -0
  45. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Noise_floor.py +0 -0
  46. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Petab_export.py +0 -0
  47. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Profile_checkpoint.py +0 -0
  48. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Profile_plan.py +0 -0
  49. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/README.md +0 -0
  50. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/Sensitivity_analysis.py +0 -0
  51. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/engine/__init__.py +0 -0
  52. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/AntimonyGen.py +0 -0
  53. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/RxnDict_to_antimony.py +0 -0
  54. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/TelluriumGen.py +0 -0
  55. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/__init__.py +0 -0
  56. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/antimony_utils.py +0 -0
  57. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/data_interpolation.py +0 -0
  58. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/isotopomer_tools.py +0 -0
  59. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/model_generation.py +0 -0
  60. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/models.py +0 -0
  61. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/module_base.py +0 -0
  62. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/pyantigen.py +0 -0
  63. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/rate_laws.py +0 -0
  64. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/generate/reaction_creation.py +0 -0
  65. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/__init__.py +0 -0
  66. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/assay.py +0 -0
  67. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/describe.py +0 -0
  68. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/design.py +0 -0
  69. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/lower_v1.py +0 -0
  70. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/optimization.py +0 -0
  71. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/params.py +0 -0
  72. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/quantity.py +0 -0
  73. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/reagent.py +0 -0
  74. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/refs.py +0 -0
  75. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/remarks.py +0 -0
  76. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/serialize.py +0 -0
  77. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/study/validate.py +0 -0
  78. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/AntiGen_paths.py +0 -0
  79. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Flipflop_reference.py +0 -0
  80. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Model_generate.py +0 -0
  81. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Model_run.py +0 -0
  82. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Data.py +0 -0
  83. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Events.py +0 -0
  84. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Experiment.py +0 -0
  85. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Loss_config.py +0 -0
  86. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Observed_species.py +0 -0
  87. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Optimizer_settings.py +0 -0
  88. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Plots.py +0 -0
  89. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Solver_settings.py +0 -0
  90. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Update_opt_parameters.py +0 -0
  91. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/Modules/Update_parameters.py +0 -0
  92. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/optimizations/Example1_ADneg.json +0 -0
  93. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/optimizations/Example1_ADpos.json +0 -0
  94. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/optimizations/Example3_joint.json +0 -0
  95. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/optimizations/Example4_flipflop.json +0 -0
  96. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/optimizations/example.py +0 -0
  97. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/studies/example.json +0 -0
  98. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/studies/example.py +0 -0
  99. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/Example/studies/flipflop.json +0 -0
  100. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/data/ADneg.csv +0 -0
  101. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/data/ADpos.csv +0 -0
  102. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/data/Flipflop.csv +0 -0
  103. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyantigen/template/data/make_flipflop_data.py +0 -0
  104. {pyantigen-2.0.3 → pyantigen-2.1.0}/pyproject.toml +0 -0
  105. {pyantigen-2.0.3 → pyantigen-2.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAntiGen
3
- Version: 2.0.3
3
+ Version: 2.1.0
4
4
  Summary: Declarative generation, simulation and identifiability analysis of compartmental Antimony/SBML models
5
5
  Author-email: Don Elbert <elbert5770@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAntiGen
3
- Version: 2.0.3
3
+ Version: 2.1.0
4
4
  Summary: Declarative generation, simulation and identifiability analysis of compartmental Antimony/SBML models
5
5
  Author-email: Don Elbert <elbert5770@gmail.com>
6
6
  License-Expression: MIT
@@ -32,8 +32,10 @@ pyantigen/engine/Event_times.py
32
32
  pyantigen/engine/Fast_profile.py
33
33
  pyantigen/engine/Fit_cache.py
34
34
  pyantigen/engine/Identifiability.py
35
+ pyantigen/engine/Model_cache.py
35
36
  pyantigen/engine/Model_optimize.py
36
37
  pyantigen/engine/Model_simulate.py
38
+ pyantigen/engine/Multistart.py
37
39
  pyantigen/engine/Nelder_mead.py
38
40
  pyantigen/engine/Noise_floor.py
39
41
  pyantigen/engine/Optimize.py
@@ -24,8 +24,10 @@
24
24
  "pyantigen/engine/Fast_profile.py",
25
25
  "pyantigen/engine/Fit_cache.py",
26
26
  "pyantigen/engine/Identifiability.py",
27
+ "pyantigen/engine/Model_cache.py",
27
28
  "pyantigen/engine/Model_optimize.py",
28
29
  "pyantigen/engine/Model_simulate.py",
30
+ "pyantigen/engine/Multistart.py",
29
31
  "pyantigen/engine/Nelder_mead.py",
30
32
  "pyantigen/engine/Noise_floor.py",
31
33
  "pyantigen/engine/Optimize.py",
@@ -104,14 +106,20 @@
104
106
  "tests/engine/test_fast_profile.py",
105
107
  "tests/engine/test_fit_cache.py",
106
108
  "tests/engine/test_hessian_steps.py",
109
+ "tests/engine/test_model_cache.py",
110
+ "tests/engine/test_multistart_candidates.py",
111
+ "tests/engine/test_multistart_gradient.py",
112
+ "tests/engine/test_multistart_resume.py",
107
113
  "tests/engine/test_multistart_sampler.py",
108
114
  "tests/engine/test_multistart_stage.py",
115
+ "tests/engine/test_multistart_triage.py",
109
116
  "tests/engine/test_nelder_mead.py",
110
117
  "tests/engine/test_point_identity.py",
111
118
  "tests/engine/test_profile_grid_open.py",
112
119
  "tests/engine/test_profile_quadratic.py",
113
120
  "tests/engine/test_profile_records.py",
114
121
  "tests/engine/test_profile_stream.py",
122
+ "tests/engine/test_results_csv_policy.py",
115
123
  "tests/engine/test_search_mode.py",
116
124
  "tests/engine/test_sigma_floor.py",
117
125
  "tests/engine/test_slice_screen.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.1.0",
3
+ "distance": 0,
4
+ "node": "g664f039a120684d736966bef028ad1f83320d9bc",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-06"
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 = '2.0.3'
22
- __version_tuple__ = version_tuple = (2, 0, 3)
21
+ __version__ = version = '2.1.0'
22
+ __version_tuple__ = version_tuple = (2, 1, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g0218ed4d7'
24
+ __commit_id__ = commit_id = 'g664f039a1'
@@ -239,14 +239,40 @@ def _worker_nll(x, frozen_sigmas=None):
239
239
  )
240
240
 
241
241
 
242
- def _eval_task(x, frozen_sigmas=None):
242
+ def _eval_context(eval_mode):
243
+ """Context for one task: the solver limits an ``eval_mode`` asks for.
244
+
245
+ ``eval_mode`` is a plain dict so it pickles cheaply:
246
+
247
+ {"retry_limits": (max_attempts, max_depth), "relax": factor}
248
+
249
+ Applied around ONE task and undone after it, never at worker start-up: both
250
+ settings are process-global, and the same worker goes on to serve profile
251
+ points and diagnostics that must run exactly as configured. Absent or empty,
252
+ this does nothing.
253
+ """
254
+ from contextlib import ExitStack
255
+
256
+ stack = ExitStack()
257
+ if eval_mode:
258
+ from pyantigen.engine.Simulate import relaxed_tolerances, retry_limits
259
+ limits = eval_mode.get("retry_limits")
260
+ if limits:
261
+ stack.enter_context(retry_limits(limits[0], limits[1]))
262
+ if eval_mode.get("relax"):
263
+ stack.enter_context(relaxed_tolerances(eval_mode["relax"]))
264
+ return stack
265
+
266
+
267
+ def _eval_task(x, frozen_sigmas=None, eval_mode=None):
243
268
  """Evaluate one parameter vector. Never raises across the pool boundary."""
244
269
  if _WORKER["spec"] is None:
245
270
  return (FAILURE_VALUE, "worker-not-initialized", 0.0)
246
271
 
247
272
  t0 = time.time()
248
273
  try:
249
- val = _worker_nll(x, frozen_sigmas=frozen_sigmas)
274
+ with _eval_context(eval_mode):
275
+ val = _worker_nll(x, frozen_sigmas=frozen_sigmas)
250
276
  _WORKER["n_evals"] += 1
251
277
  status = "ok" if np.isfinite(val) and val < FAILURE_VALUE else "sentinel"
252
278
  return (float(val), status, time.time() - t0)
@@ -791,9 +817,12 @@ class ParallelEvaluator:
791
817
  # -- evaluation --------------------------------------------------------
792
818
 
793
819
  def evaluate_batch(self, xs, label=None, heartbeat_s=_HEARTBEAT_SECONDS,
794
- frozen_sigmas=None):
820
+ frozen_sigmas=None, eval_mode=None):
795
821
  """Evaluate every parameter vector in *xs*; return losses in input order.
796
822
 
823
+ ``eval_mode`` ({"retry_limits": (attempts, depth), "relax": factor})
824
+ runs each evaluation under those solver limits; see :func:`_eval_context`.
825
+
797
826
  Uses submit/wait, not map -- see profile_batch's docstring for the
798
827
  general reasoning. map() (the previous implementation here) returns
799
828
  nothing until the WHOLE batch is done, so one slow straggler among
@@ -833,7 +862,7 @@ class ParallelEvaluator:
833
862
  flush=True)
834
863
 
835
864
  try:
836
- futures = {self._pool.submit(_eval_task, x, frozen_sigmas): i
865
+ futures = {self._pool.submit(_eval_task, x, frozen_sigmas, eval_mode): i
837
866
  for i, x in enumerate(xs)}
838
867
  except RuntimeError as exc:
839
868
  if "bootstrapping phase" in str(exc):
@@ -161,7 +161,7 @@ def fit_fingerprint(param_names, x0_lin, bounds_lin, scales, groups, model_text,
161
161
  # spec a default stage does not invalidate the fits already cached.
162
162
  if int(n_starts or 1) <= 1:
163
163
  for k in ("multistart_method", "multistart_optimizer_kwargs",
164
- "multistart_limits"):
164
+ "multistart_limits", "multistart_triage", "multistart_gradient"):
165
165
  kwargs.pop(k, None)
166
166
 
167
167
  key = {
@@ -387,6 +387,58 @@ class FitCache:
387
387
  data["complete"] = block
388
388
  return self._write(data)
389
389
 
390
+ def save_stage(self, state):
391
+ """Checkpoint of the multi-start stage in progress.
392
+
393
+ Written after every candidate scored and every local fit finished, so a
394
+ kill costs the evaluation or fit in flight and nothing before it. Kept
395
+ under its own key: the stage is not the fit, and the fit's partial must
396
+ not appear until the stage is over (see ``_persist_stage_result``). Must
397
+ already be plain JSON data.
398
+ """
399
+ if not self.enabled:
400
+ return False
401
+ data = self._read() or {}
402
+ data["multistart_stage"] = state
403
+ return self._write(data)
404
+
405
+ def load_stage(self):
406
+ """The stage checkpoint :meth:`save_stage` kept, or None."""
407
+ data = self._read()
408
+ block = data.get("multistart_stage") if data else None
409
+ return block if isinstance(block, dict) else None
410
+
411
+ def clear_stage(self):
412
+ """Drop the stage checkpoint, once the stage's result is on record."""
413
+ if not self.enabled:
414
+ return False
415
+ data = self._read()
416
+ if not data or "multistart_stage" not in data:
417
+ return False
418
+ data.pop("multistart_stage")
419
+ return self._write(data)
420
+
421
+ def save_multistart(self, report):
422
+ """Keep the multi-start stage's report with the fit it produced.
423
+
424
+ Stored beside the ``complete``/``partial`` records, not inside them, so it
425
+ survives both: a relaunch that resumes the standard fit skips the stage,
426
+ and one that reuses a finished fit skips both, yet the result written at
427
+ the end should still say how the starting point was found. *report* must
428
+ already be plain JSON data (see ``Multistart.json_safe``).
429
+ """
430
+ if not self.enabled:
431
+ return False
432
+ data = self._read() or {}
433
+ data["multistart"] = report
434
+ return self._write(data)
435
+
436
+ def load_multistart(self):
437
+ """The report :meth:`save_multistart` kept, or None."""
438
+ data = self._read()
439
+ block = data.get("multistart") if data else None
440
+ return block if isinstance(block, dict) else None
441
+
390
442
  def save_partial(self, x_lin, fun, n_evals=None, force=False, nm_state=None,
391
443
  interval=None, de_state=None, cal_x_lin=None):
392
444
  """Record the best point so far, at most once per *interval*.
@@ -0,0 +1,75 @@
1
+ """Per-model caches, stored on the RoadRunner instance without touching ``__getattr__``.
2
+
3
+ The engine keeps a few values on each RoadRunner so they are computed once per
4
+ model rather than once per evaluation (the state keys, the available symbols,
5
+ the parameter baseline, the configured tolerance). The obvious way to read one
6
+ back -- ``try: r._x`` / ``except AttributeError``, or ``getattr(r, "_x", None)`` --
7
+ is a trap, because RoadRunner defines::
8
+
9
+ def __getattr__(self, name):
10
+ if name != "this" and name in self._getIds(SelectionRecord_ALL):
11
+ return self[name]
12
+ raise AttributeError(name)
13
+
14
+ ``__getattr__`` runs only after normal lookup has failed, so every *miss* builds
15
+ the list of every selection id the model has, to learn that the name is not one
16
+ of them. That list is roughly quadratic in model size: about a millisecond on a
17
+ 25-species model, over a second on 400 species, and on a model with ~9,000 ODEs
18
+ ~70 million ids, about 18 minutes and 16 GB per miss. A model pays it once per
19
+ cache slot per instance, which is why small models never notice and a large one
20
+ spends an hour before its first result.
21
+
22
+ Reading through the instance ``__dict__`` never reaches ``__getattr__``, so a
23
+ miss costs a dictionary lookup whatever the model size. Writing needs no care:
24
+ RoadRunner defines no ``__setattr__``, so ``r._x = v`` is an ordinary
25
+ instance-dict write.
26
+
27
+ Use :func:`get` and :func:`put` for every engine-private attribute on a
28
+ RoadRunner. ``tests/engine/test_model_cache.py`` fails if any of the engine's own
29
+ cache reads causes a ``_getIds`` call.
30
+ """
31
+
32
+
33
+ def _target(r):
34
+ """The object whose ``__dict__`` holds the caches.
35
+
36
+ ``OptRoadRunnerProxy`` wraps a RoadRunner in ``_r`` and forwards attribute
37
+ writes to it, so a cache written through the proxy lives on the RoadRunner.
38
+ Reading ``vars(proxy)`` would look in the proxy's own dict instead, which is
39
+ never where the value is.
40
+ """
41
+ try:
42
+ d = vars(r)
43
+ except TypeError: # no __dict__ (slots, builtins): nothing cached
44
+ return None
45
+ inner = d.get("_r")
46
+ if inner is not None and "_opt_param_names" in d:
47
+ return inner
48
+ return r
49
+
50
+
51
+ def get(r, name, default=None):
52
+ """The value cached on *r* under *name*, or *default*. Never calls ``__getattr__``."""
53
+ t = _target(r)
54
+ if t is None:
55
+ return default
56
+ try:
57
+ return vars(t).get(name, default)
58
+ except TypeError:
59
+ return default
60
+
61
+
62
+ def put(r, name, value):
63
+ """Cache *value* on *r* under *name*. Returns whether it was stored.
64
+
65
+ A target that refuses attributes is tolerated, as the call sites always did:
66
+ the value is then recomputed next time instead of reused.
67
+ """
68
+ t = _target(r)
69
+ if t is None:
70
+ return False
71
+ try:
72
+ vars(t)[name] = value
73
+ except Exception:
74
+ return False
75
+ return True