PyAntiGen 2.1.6__tar.gz → 2.2.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 (123) hide show
  1. {pyantigen-2.1.6 → pyantigen-2.2.0}/PKG-INFO +5 -1
  2. {pyantigen-2.1.6 → pyantigen-2.2.0}/PyAntiGen.egg-info/PKG-INFO +5 -1
  3. {pyantigen-2.1.6 → pyantigen-2.2.0}/PyAntiGen.egg-info/SOURCES.txt +10 -0
  4. {pyantigen-2.1.6 → pyantigen-2.2.0}/PyAntiGen.egg-info/entry_points.txt +1 -0
  5. {pyantigen-2.1.6 → pyantigen-2.2.0}/PyAntiGen.egg-info/requires.txt +7 -0
  6. {pyantigen-2.1.6 → pyantigen-2.2.0}/PyAntiGen.egg-info/scm_file_list.json +22 -0
  7. pyantigen-2.2.0/PyAntiGen.egg-info/scm_version.json +8 -0
  8. pyantigen-2.2.0/docs/BACKENDS.md +60 -0
  9. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/_version.py +3 -3
  10. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Analytic_gradient.py +206 -21
  11. pyantigen-2.2.0/pyantigen/engine/Backend.py +308 -0
  12. pyantigen-2.2.0/pyantigen/engine/Fim.py +75 -0
  13. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Fit_cache.py +7 -0
  14. pyantigen-2.2.0/pyantigen/engine/Fit_lm.py +93 -0
  15. pyantigen-2.2.0/pyantigen/engine/Floors.py +93 -0
  16. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Model_optimize.py +14 -0
  17. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Optimize.py +350 -4
  18. pyantigen-2.2.0/pyantigen/engine/Profile.py +451 -0
  19. pyantigen-2.2.0/pyantigen/engine/Profile_amici.py +262 -0
  20. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Profile_checkpoint.py +25 -1
  21. pyantigen-2.2.0/pyantigen/engine/Profile_lm.py +292 -0
  22. pyantigen-2.2.0/pyantigen/engine/Profile_record.py +466 -0
  23. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Segmented.py +8 -0
  24. pyantigen-2.2.0/pyantigen/setup_env.py +72 -0
  25. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/Model_run.py +15 -0
  26. pyantigen-2.2.0/pyantigen/template/Example/Modules/Solver_settings.py +33 -0
  27. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyproject.toml +3 -0
  28. pyantigen-2.1.6/PyAntiGen.egg-info/scm_version.json +0 -8
  29. pyantigen-2.1.6/pyantigen/template/Example/Modules/Solver_settings.py +0 -16
  30. {pyantigen-2.1.6 → pyantigen-2.2.0}/.github/workflows/publish.yml +0 -0
  31. {pyantigen-2.1.6 → pyantigen-2.2.0}/.github/workflows/tests.yml +0 -0
  32. {pyantigen-2.1.6 → pyantigen-2.2.0}/.gitignore +0 -0
  33. {pyantigen-2.1.6 → pyantigen-2.2.0}/.vscode/launch.json +0 -0
  34. {pyantigen-2.1.6 → pyantigen-2.2.0}/LICENSE +0 -0
  35. {pyantigen-2.1.6 → pyantigen-2.2.0}/MANIFEST.in +0 -0
  36. {pyantigen-2.1.6 → pyantigen-2.2.0}/PyAntiGen.egg-info/dependency_links.txt +0 -0
  37. {pyantigen-2.1.6 → pyantigen-2.2.0}/PyAntiGen.egg-info/top_level.txt +0 -0
  38. {pyantigen-2.1.6 → pyantigen-2.2.0}/README.md +0 -0
  39. {pyantigen-2.1.6 → pyantigen-2.2.0}/docs/ANALYTIC_GRADIENT.md +0 -0
  40. {pyantigen-2.1.6 → pyantigen-2.2.0}/docs/MULTISTART.md +0 -0
  41. {pyantigen-2.1.6 → pyantigen-2.2.0}/docs/PROTOCOL_LAYER.md +0 -0
  42. {pyantigen-2.1.6 → pyantigen-2.2.0}/docs/V2_DESIGN.md +0 -0
  43. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/.agents/skills/module_generation/SKILL.md +0 -0
  44. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/.agents/skills/ode_conversion_antimony/SKILL.md +0 -0
  45. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/.agents/skills/ode_extraction_pipeline/SKILL.md +0 -0
  46. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/.gitignore +0 -0
  47. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/__init__.py +0 -0
  48. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/cli.py +0 -0
  49. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Anchor_cache.py +0 -0
  50. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Deadline.py +0 -0
  51. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Differential_evolution.py +0 -0
  52. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Evaluator.py +0 -0
  53. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Event_times.py +0 -0
  54. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Fast_profile.py +0 -0
  55. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Identifiability.py +0 -0
  56. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Model_cache.py +0 -0
  57. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Model_simulate.py +0 -0
  58. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Multistart.py +0 -0
  59. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Nelder_mead.py +0 -0
  60. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Noise_floor.py +0 -0
  61. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Petab_export.py +0 -0
  62. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Preequil_cache.py +0 -0
  63. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Profile_plan.py +0 -0
  64. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/README.md +0 -0
  65. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Results.py +0 -0
  66. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Scaled_amici.py +0 -0
  67. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Sensitivity_analysis.py +0 -0
  68. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Simulate.py +0 -0
  69. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/Stop_rules.py +0 -0
  70. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/engine/__init__.py +0 -0
  71. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/AntimonyGen.py +0 -0
  72. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/RxnDict_to_antimony.py +0 -0
  73. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/TelluriumGen.py +0 -0
  74. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/__init__.py +0 -0
  75. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/antimony_utils.py +0 -0
  76. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/data_interpolation.py +0 -0
  77. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/input_schedule.py +0 -0
  78. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/isotopomer_tools.py +0 -0
  79. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/model_generation.py +0 -0
  80. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/models.py +0 -0
  81. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/module_base.py +0 -0
  82. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/pyantigen.py +0 -0
  83. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/rate_laws.py +0 -0
  84. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/reaction_creation.py +0 -0
  85. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/generate/scaled_time.py +0 -0
  86. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/__init__.py +0 -0
  87. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/assay.py +0 -0
  88. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/describe.py +0 -0
  89. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/design.py +0 -0
  90. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/lower_v1.py +0 -0
  91. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/optimization.py +0 -0
  92. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/params.py +0 -0
  93. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/quantity.py +0 -0
  94. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/reagent.py +0 -0
  95. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/refs.py +0 -0
  96. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/remarks.py +0 -0
  97. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/serialize.py +0 -0
  98. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/study/validate.py +0 -0
  99. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/AntiGen_paths.py +0 -0
  100. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/Flipflop_reference.py +0 -0
  101. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/Model_generate.py +0 -0
  102. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/Modules/Data.py +0 -0
  103. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/Modules/Events.py +0 -0
  104. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/Modules/Experiment.py +0 -0
  105. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/Modules/Loss_config.py +0 -0
  106. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/Modules/Observed_species.py +0 -0
  107. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/Modules/Optimizer_settings.py +0 -0
  108. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/Modules/Plots.py +0 -0
  109. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/Modules/Update_opt_parameters.py +0 -0
  110. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/Modules/Update_parameters.py +0 -0
  111. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/optimizations/Example1_ADneg.json +0 -0
  112. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/optimizations/Example1_ADpos.json +0 -0
  113. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/optimizations/Example3_joint.json +0 -0
  114. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/optimizations/Example4_flipflop.json +0 -0
  115. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/optimizations/example.py +0 -0
  116. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/studies/example.json +0 -0
  117. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/studies/example.py +0 -0
  118. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/Example/studies/flipflop.json +0 -0
  119. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/data/ADneg.csv +0 -0
  120. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/data/ADpos.csv +0 -0
  121. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/data/Flipflop.csv +0 -0
  122. {pyantigen-2.1.6 → pyantigen-2.2.0}/pyantigen/template/data/make_flipflop_data.py +0 -0
  123. {pyantigen-2.1.6 → pyantigen-2.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAntiGen
3
- Version: 2.1.6
3
+ Version: 2.2.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
@@ -28,6 +28,10 @@ Requires-Dist: pypesto
28
28
  Requires-Dist: SALib
29
29
  Provides-Extra: test
30
30
  Requires-Dist: pytest; extra == "test"
31
+ Provides-Extra: amici
32
+ Requires-Dist: amici>=1.1.0; sys_platform != "win32" and extra == "amici"
33
+ Requires-Dist: cmake; sys_platform != "win32" and extra == "amici"
34
+ Requires-Dist: swig; sys_platform != "win32" and extra == "amici"
31
35
  Dynamic: license-file
32
36
 
33
37
  # PyAntiGen
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAntiGen
3
- Version: 2.1.6
3
+ Version: 2.2.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
@@ -28,6 +28,10 @@ Requires-Dist: pypesto
28
28
  Requires-Dist: SALib
29
29
  Provides-Extra: test
30
30
  Requires-Dist: pytest; extra == "test"
31
+ Provides-Extra: amici
32
+ Requires-Dist: amici>=1.1.0; sys_platform != "win32" and extra == "amici"
33
+ Requires-Dist: cmake; sys_platform != "win32" and extra == "amici"
34
+ Requires-Dist: swig; sys_platform != "win32" and extra == "amici"
31
35
  Dynamic: license-file
32
36
 
33
37
  # PyAntiGen
@@ -15,6 +15,7 @@ PyAntiGen.egg-info/scm_file_list.json
15
15
  PyAntiGen.egg-info/scm_version.json
16
16
  PyAntiGen.egg-info/top_level.txt
17
17
  docs/ANALYTIC_GRADIENT.md
18
+ docs/BACKENDS.md
18
19
  docs/MULTISTART.md
19
20
  docs/PROTOCOL_LAYER.md
20
21
  docs/V2_DESIGN.md
@@ -22,17 +23,22 @@ pyantigen/.gitignore
22
23
  pyantigen/__init__.py
23
24
  pyantigen/_version.py
24
25
  pyantigen/cli.py
26
+ pyantigen/setup_env.py
25
27
  pyantigen/.agents/skills/module_generation/SKILL.md
26
28
  pyantigen/.agents/skills/ode_conversion_antimony/SKILL.md
27
29
  pyantigen/.agents/skills/ode_extraction_pipeline/SKILL.md
28
30
  pyantigen/engine/Analytic_gradient.py
29
31
  pyantigen/engine/Anchor_cache.py
32
+ pyantigen/engine/Backend.py
30
33
  pyantigen/engine/Deadline.py
31
34
  pyantigen/engine/Differential_evolution.py
32
35
  pyantigen/engine/Evaluator.py
33
36
  pyantigen/engine/Event_times.py
34
37
  pyantigen/engine/Fast_profile.py
38
+ pyantigen/engine/Fim.py
35
39
  pyantigen/engine/Fit_cache.py
40
+ pyantigen/engine/Fit_lm.py
41
+ pyantigen/engine/Floors.py
36
42
  pyantigen/engine/Identifiability.py
37
43
  pyantigen/engine/Model_cache.py
38
44
  pyantigen/engine/Model_optimize.py
@@ -43,8 +49,12 @@ pyantigen/engine/Noise_floor.py
43
49
  pyantigen/engine/Optimize.py
44
50
  pyantigen/engine/Petab_export.py
45
51
  pyantigen/engine/Preequil_cache.py
52
+ pyantigen/engine/Profile.py
53
+ pyantigen/engine/Profile_amici.py
46
54
  pyantigen/engine/Profile_checkpoint.py
55
+ pyantigen/engine/Profile_lm.py
47
56
  pyantigen/engine/Profile_plan.py
57
+ pyantigen/engine/Profile_record.py
48
58
  pyantigen/engine/README.md
49
59
  pyantigen/engine/Results.py
50
60
  pyantigen/engine/Scaled_amici.py
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  pyantigen-create = pyantigen.cli:create_project
3
+ pyantigen-setup = pyantigen.setup_env:main
@@ -8,5 +8,12 @@ matplotlib
8
8
  pypesto
9
9
  SALib
10
10
 
11
+ [amici]
12
+
13
+ [amici:sys_platform != "win32"]
14
+ amici>=1.1.0
15
+ cmake
16
+ swig
17
+
11
18
  [test]
12
19
  pytest
@@ -8,6 +8,7 @@
8
8
  "MANIFEST.in",
9
9
  "README.md",
10
10
  "docs/ANALYTIC_GRADIENT.md",
11
+ "docs/BACKENDS.md",
11
12
  "docs/MULTISTART.md",
12
13
  "docs/PROTOCOL_LAYER.md",
13
14
  "docs/V2_DESIGN.md",
@@ -19,12 +20,16 @@
19
20
  "pyantigen/cli.py",
20
21
  "pyantigen/engine/Analytic_gradient.py",
21
22
  "pyantigen/engine/Anchor_cache.py",
23
+ "pyantigen/engine/Backend.py",
22
24
  "pyantigen/engine/Deadline.py",
23
25
  "pyantigen/engine/Differential_evolution.py",
24
26
  "pyantigen/engine/Evaluator.py",
25
27
  "pyantigen/engine/Event_times.py",
26
28
  "pyantigen/engine/Fast_profile.py",
29
+ "pyantigen/engine/Fim.py",
27
30
  "pyantigen/engine/Fit_cache.py",
31
+ "pyantigen/engine/Fit_lm.py",
32
+ "pyantigen/engine/Floors.py",
28
33
  "pyantigen/engine/Identifiability.py",
29
34
  "pyantigen/engine/Model_cache.py",
30
35
  "pyantigen/engine/Model_optimize.py",
@@ -35,8 +40,12 @@
35
40
  "pyantigen/engine/Optimize.py",
36
41
  "pyantigen/engine/Petab_export.py",
37
42
  "pyantigen/engine/Preequil_cache.py",
43
+ "pyantigen/engine/Profile.py",
44
+ "pyantigen/engine/Profile_amici.py",
38
45
  "pyantigen/engine/Profile_checkpoint.py",
46
+ "pyantigen/engine/Profile_lm.py",
39
47
  "pyantigen/engine/Profile_plan.py",
48
+ "pyantigen/engine/Profile_record.py",
40
49
  "pyantigen/engine/README.md",
41
50
  "pyantigen/engine/Results.py",
42
51
  "pyantigen/engine/Scaled_amici.py",
@@ -60,6 +69,7 @@
60
69
  "pyantigen/generate/rate_laws.py",
61
70
  "pyantigen/generate/reaction_creation.py",
62
71
  "pyantigen/generate/scaled_time.py",
72
+ "pyantigen/setup_env.py",
63
73
  "pyantigen/study/__init__.py",
64
74
  "pyantigen/study/assay.py",
65
75
  "pyantigen/study/describe.py",
@@ -109,14 +119,20 @@
109
119
  "tests/engine/test_analytic_gradient.py",
110
120
  "tests/engine/test_analytic_gradient_amici.py",
111
121
  "tests/engine/test_anchor_cache.py",
122
+ "tests/engine/test_backend.py",
112
123
  "tests/engine/test_better_point.py",
124
+ "tests/engine/test_block_title.py",
113
125
  "tests/engine/test_de_fit.py",
114
126
  "tests/engine/test_deadline.py",
115
127
  "tests/engine/test_differential_evolution.py",
116
128
  "tests/engine/test_event_parameter_reset.py",
117
129
  "tests/engine/test_event_times.py",
118
130
  "tests/engine/test_fast_profile.py",
131
+ "tests/engine/test_fim.py",
119
132
  "tests/engine/test_fit_cache.py",
133
+ "tests/engine/test_fit_lm.py",
134
+ "tests/engine/test_fixed_sigma.py",
135
+ "tests/engine/test_floors.py",
120
136
  "tests/engine/test_hessian_steps.py",
121
137
  "tests/engine/test_model_cache.py",
122
138
  "tests/engine/test_multistart_candidates.py",
@@ -127,10 +143,15 @@
127
143
  "tests/engine/test_multistart_triage.py",
128
144
  "tests/engine/test_nelder_mead.py",
129
145
  "tests/engine/test_point_identity.py",
146
+ "tests/engine/test_profile.py",
147
+ "tests/engine/test_profile_amici.py",
130
148
  "tests/engine/test_profile_grid_open.py",
149
+ "tests/engine/test_profile_lm.py",
131
150
  "tests/engine/test_profile_quadratic.py",
151
+ "tests/engine/test_profile_record.py",
132
152
  "tests/engine/test_profile_records.py",
133
153
  "tests/engine/test_profile_stream.py",
154
+ "tests/engine/test_protocol_profile.py",
134
155
  "tests/engine/test_results_csv_policy.py",
135
156
  "tests/engine/test_schedule_bounds.py",
136
157
  "tests/engine/test_search_mode.py",
@@ -138,6 +159,7 @@
138
159
  "tests/engine/test_segment_sensitivity.py",
139
160
  "tests/engine/test_segmented.py",
140
161
  "tests/engine/test_sigma_floor.py",
162
+ "tests/engine/test_sigma_key_fingerprint.py",
141
163
  "tests/engine/test_slice_screen.py",
142
164
  "tests/engine/test_slice_window.py",
143
165
  "tests/engine/test_solver_fingerprint.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.2.0",
3
+ "distance": 0,
4
+ "node": "g48927540482fa68669cb63e24ba6437f15b2669f",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-28"
8
+ }
@@ -0,0 +1,60 @@
1
+ # Solver backends: RoadRunner and AMICI
2
+
3
+ | | RoadRunner | AMICI |
4
+ |---|---|---|
5
+ | install | core dependency (`pip install pyantigen`) | `pip install "pyantigen[amici]"`, then `pyantigen-setup` |
6
+ | platforms | Linux, macOS, Windows | Linux, macOS; **Windows only through WSL** |
7
+ | start-up | seconds | each model is compiled to C++ once (minutes to tens of minutes), then cached |
8
+ | run time | slower; no sensitivities | much faster per evaluation, with exact sensitivities |
9
+ | use for | simulate, quick fits | multistart, profile likelihood, FIM |
10
+
11
+ AMICI needs g++ >= 10.1 (or Apple clang), cmake and swig. `pyantigen-setup` checks all of these and
12
+ prints the fix for anything missing (it never uses sudo). `pyantigen-setup --install-amici` pip-installs
13
+ the package; it refuses on native Windows and on a cluster login node.
14
+
15
+ ## Choosing
16
+
17
+ Per stage (`simulate`, `fit`, `multistart`, `profile`, `fim`), first match wins:
18
+
19
+ 1. `Model_run.py --backend roadrunner|amici|auto`
20
+ 2. environment variable `PYANTIGEN_BACKEND` (reaches spawned workers)
21
+ 3. `BACKEND` in the project's `Modules/Solver_settings.py`
22
+ 4. `amici`, if the optimization spec already sets `optimizer_kwargs["analytic_gradient"]`
23
+ 5. the stage default: RoadRunner for `simulate` and `fit`, AMICI for `multistart`, `profile`, `fim`
24
+
25
+ `auto` at any level means no preference. A backend named at levels 1-4 that cannot run here stops the run
26
+ with the reason and the fix. Only the stage default falls back to RoadRunner, with a note.
27
+
28
+ `--backend amici` on `--optimize` switches on `analytic_gradient` for a spec that lacks it;
29
+ `--backend roadrunner` removes it. `--simulate` is always RoadRunner.
30
+
31
+ ## Compiled-model cache
32
+
33
+ `Solver_settings.AMICI_CACHE_DIR`, else `PYANTIGEN_AMICI_CACHE`, else a per-user cache directory
34
+ (`~/.cache/pyantigen/amici_models`, `~/Library/Caches/...` on macOS). On a cluster, point it at group or
35
+ scratch storage: a home directory has a small quota and compiled models are large.
36
+
37
+ ## Not covered yet
38
+
39
+ This layer chooses and checks the backend. The multistart, profile and FIM stages still run from the
40
+ project's `tools/` scripts; moving them into the engine is the next phase. BLAS is not checked.
41
+
42
+ ## Sigma rule
43
+
44
+ How a data-floored block gets its sigma is a separate setting from the backend, but the AMICI long stages
45
+ depend on it.
46
+
47
+ | `sigma_floor` | sigma used | loss | for |
48
+ |---|---|---|---|
49
+ | `"fixed"` | the LOESS floor, as a known sigma, calibrated once | `const + 0.5 r.r` | AMICI multistart, profile, FIM |
50
+ | `"cap"` (engine default) | `min(sigma_hat, floor)`, re-estimated at every point | concentrated NLL | older RoadRunner fits; to be retired |
51
+
52
+ Precedence: the spec's `optimizer_kwargs["sigma_floor"]`, then `Solver_settings.SIGMA_FLOOR`, then the stage:
53
+ multistart, profile and FIM on AMICI get `"fixed"`. `"cap"` there is an error, because the residual form of
54
+ the loss (Levenberg-Marquardt, `J^T J` as the FIM) needs known sigmas.
55
+
56
+ In fixed mode the floors are calibrated once, at the first point evaluated (normally x0), and are not
57
+ recalibrated when a fit ends. `pyantigen.engine.Floors` saves them to a file and returns a sha256 stamp to put
58
+ on every result, loads them back into worker processes, and compares two calibrations. Recalibrating at the
59
+ optimum is a deliberate second pass: `Floors.calibrate`, refit, `Floors.compare`, and accept when
60
+ `Floors.stable` holds.
@@ -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.1.6'
22
- __version_tuple__ = version_tuple = (2, 1, 6)
21
+ __version__ = version = '2.2.0'
22
+ __version_tuple__ = version_tuple = (2, 2, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g917168907'
24
+ __commit_id__ = commit_id = 'g489275404'
@@ -32,6 +32,8 @@ engine's parameter updates is what AMICI is given.
32
32
  AMICI is optional; on Windows it needs WSL. Nothing here imports it until a
33
33
  gradient is built.
34
34
  """
35
+ import contextlib
36
+ import fcntl
35
37
  import hashlib
36
38
  import json
37
39
  import os
@@ -63,7 +65,9 @@ class SimTable:
63
65
  self._index = {c: i for i, c in enumerate(self.colnames)}
64
66
 
65
67
  def __getitem__(self, key):
66
- return self.data[:, self._index[key]]
68
+ if isinstance(key, str):
69
+ return self.data[:, self._index[key]]
70
+ return self.data[key] # numpy-style, as RoadRunner's result array allows: table[:, j], table[i, j]
67
71
 
68
72
  def __len__(self):
69
73
  return self.data.shape[0]
@@ -134,6 +138,74 @@ class LossGradient:
134
138
  raise AnalyticGradientError("the loss is not finite")
135
139
  return v
136
140
 
141
+ def residual_vector(self, items, p_lin):
142
+ """``(r, const)``: the scaled residuals ``sqrt(w) (y - yhat) / sigma`` of every datum, and ``sum n ln sigma``.
143
+
144
+ With every block's sigma known (``PYANTIGEN_SIGMA_FLOOR=fixed`` and declared sigmas) the loss is exactly
145
+ ``const + 0.5 * r.r``, a nonlinear least-squares problem. Raises if any block still profiles its sigma.
146
+ """
147
+ from pyantigen.engine import Optimize as O
148
+ p_lin = np.asarray(p_lin, dtype=float)
149
+ p_dict = dict(zip(self.param_names, p_lin.tolist()))
150
+ sink = []
151
+ O._RESIDUAL_SINK = sink
152
+ try:
153
+ blocks = O.collect_loss_blocks(items, self.groups, self.replicates, self.param_names, p_lin, p_dict,
154
+ frozen_sigmas=self.frozen_sigmas)
155
+ finally:
156
+ O._RESIDUAL_SINK = None
157
+ sig, const = {}, 0.0
158
+ for key, v in blocks.items():
159
+ sse, n, ks, fl = O._unpack_block(v)
160
+ if n <= 0:
161
+ continue
162
+ s, _cost, state = O._block_sigma_resolution(sse, n, ks, fl)
163
+ if state != "declared":
164
+ raise AnalyticGradientError(
165
+ f"block {key} profiles its sigma ({state}): the residual form needs every sigma known "
166
+ "(set PYANTIGEN_SIGMA_FLOOR=fixed)")
167
+ sig[key] = s
168
+ const += n * np.log(max(s, 1e-300))
169
+ parts = [v / sig[key] for key, v in sink if key in sig and v.size]
170
+ if not parts:
171
+ raise AnalyticGradientError("the loss has no data to score")
172
+ return np.concatenate(parts), float(const)
173
+
174
+ def residual_jacobian(self, items, directions, p_lin):
175
+ """``(r, J, const)``: residuals, ``J[i, k] = d r_i / d theta_k`` (linear units), and ``sum n ln sigma``.
176
+
177
+ The derivative is taken the way :meth:`gradient` takes the loss's: central differences of the residual
178
+ vector along the sensitivity tables (Richardson), plus a partial for each parameter the loss names itself.
179
+ ``J.T @ r`` is then the gradient of ``const + 0.5 r.r`` and ``J.T @ J`` its Gauss-Newton Hessian.
180
+ """
181
+ p_lin = np.asarray(p_lin, dtype=float)
182
+ K = len(self.param_names)
183
+ r0, const = self.residual_vector(items, p_lin)
184
+ J = np.zeros((r0.size, K))
185
+
186
+ def res(it, p):
187
+ r, _c = self.residual_vector(it, p)
188
+ if r.size != r0.size:
189
+ raise AnalyticGradientError("the number of scored data changed between shifted evaluations")
190
+ return r
191
+
192
+ for k in range(K):
193
+ for units, h in self._step_groups(items, directions, k):
194
+ def central(x, units=units, k=k):
195
+ return (res(self._shift_units(items, directions, units, k, x), p_lin)
196
+ - res(self._shift_units(items, directions, units, k, -x), p_lin)) / (2.0 * x)
197
+ J[:, k] += (4.0 * central(h / 2.0) - central(h)) / 3.0
198
+ if k in self.explicit:
199
+ d = 1e-6 * max(abs(p_lin[k]), 1e-8)
200
+
201
+ def partial(s, k=k):
202
+ up, dn = p_lin.copy(), p_lin.copy()
203
+ up[k] += s
204
+ dn[k] -= s
205
+ return (res(items, up) - res(items, dn)) / (2.0 * s)
206
+ J[:, k] += (4.0 * partial(d / 2.0) - partial(d)) / 3.0
207
+ return r0, J, const
208
+
137
209
  @staticmethod
138
210
  def _shift(items, directions, k, h):
139
211
  out = {}
@@ -143,10 +215,12 @@ class LossGradient:
143
215
  out[sim] = {**item, "results": table}
144
216
  return out
145
217
 
146
- def _step(self, items, directions, k):
218
+ def _step(self, items, directions, k, sims=None):
147
219
  """The step along direction k that moves the smallest-scale entry by ``rel_step`` of itself."""
148
220
  ratio = 0.0
149
221
  for sim, item in items.items():
222
+ if sims is not None and sim not in sims:
223
+ continue
150
224
  D = directions.get(sim)
151
225
  if D is None:
152
226
  continue
@@ -156,6 +230,68 @@ class LossGradient:
156
230
  ratio = max(ratio, float(np.nanmax(np.abs(D[:, :, k]) / scale)) if D.size else 0.0)
157
231
  return self.rel_step / ratio if ratio > 0 else None
158
232
 
233
+ # One finite-difference step for the whole loss is set by the table entry with the largest relative sensitivity in ANY
234
+ # column of ANY simulation. A column the loss never reads (45 states in a typical table) can have sensitivities of 1e12
235
+ # that fix a step far too small for the column it does read, and the central difference of that is lost in rounding:
236
+ # on aggregation_silk the Jacobian of one arm changed by a relative 1e-3 between two points 1e-9 apart while its residuals
237
+ # changed by 1e-10 (the gradient jumped by 1.9). The chain rule allows a step per (simulation, column) unit:
238
+ # dL/dtheta_k = sum_u dL/dR_u . D_u,k, each term by shifting only that column. Units whose steps are within SPLIT_RATIO
239
+ # of each other are shifted together, because splitting everything into single columns turns offsetting partial
240
+ # derivatives (mass balances) into rounding error. Only columns that are not read could be left out, and a probe for
241
+ # "read" columns proved unreliable (it missed columns on silk_appfull, error 18.7), so every column with a non-zero
242
+ # sensitivity is a unit. Measured (silk_appfull optimum, aggregation_silk traced points): SPLIT_RATIO from 3 to 1e5 gives
243
+ # the old gradient on silk_appfull (error vs forward differences 0.021) and no jump on aggregation_silk (3e-4, was 1.9);
244
+ # 1e7 is already too coarse there (8e-3).
245
+ SPLIT_RATIO = 1e3
246
+
247
+ def _unit_steps(self, items, directions, k):
248
+ """``{(sim, column): h}``: the step that moves that column's most sensitive entry by ``rel_step`` of itself."""
249
+ steps = {}
250
+ for sim, item in items.items():
251
+ D = directions.get(sim)
252
+ if D is None or not D.size:
253
+ continue
254
+ R = item["results"].data
255
+ colmax = np.nanmax(np.abs(R), axis=0)
256
+ for c in range(R.shape[1]):
257
+ Dc = D[:, c, k]
258
+ if not np.any(Dc):
259
+ continue
260
+ scale = np.maximum(np.abs(R[:, c]), 1e-6 * colmax[c] + 1e-300)
261
+ ratio = float(np.nanmax(np.abs(Dc) / scale))
262
+ if ratio > 0:
263
+ steps[(sim, c)] = self.rel_step / ratio
264
+ return steps
265
+
266
+ def _step_groups(self, items, directions, k, read=None, p_lin=None):
267
+ """``[(units, h)]``: (simulation, column) units that share a step. The smallest remaining step serves every unit
268
+ within SPLIT_RATIO of it; the rest are grouped the same way. (``read`` and ``p_lin`` are accepted and ignored.)"""
269
+ remaining = self._unit_steps(items, directions, k)
270
+ groups = []
271
+ while remaining:
272
+ h0 = min(remaining.values())
273
+ joint = {u for u, h in remaining.items() if h <= max(self.SPLIT_RATIO, 1.0) * h0} # always includes the smallest
274
+ groups.append((joint, h0))
275
+ for u in joint:
276
+ del remaining[u]
277
+ return groups
278
+
279
+ @staticmethod
280
+ def _shift_units(items, directions, units, k, h):
281
+ from collections import defaultdict
282
+ by_sim = defaultdict(list)
283
+ for sim, c in units:
284
+ by_sim[sim].append(c)
285
+ out = dict(items)
286
+ for sim, cols in by_sim.items():
287
+ table = items[sim]["results"]
288
+ data = table.data.copy()
289
+ D = directions[sim]
290
+ for c in cols:
291
+ data[:, c] += h * D[:, c, k]
292
+ out[sim] = {**items[sim], "results": type(table)(table.colnames, data)}
293
+ return out
294
+
159
295
  def gradient(self, items, directions, p_lin):
160
296
  """``(nll, grad)`` at *p_lin*.
161
297
 
@@ -167,11 +303,10 @@ class LossGradient:
167
303
  f0 = self.value(items, p_lin)
168
304
  g = np.zeros(K)
169
305
  for k in range(K):
170
- h = self._step(items, directions, k)
171
- if h is not None:
172
- def central(s, k=k):
173
- return (self.value(self._shift(items, directions, k, s), p_lin)
174
- - self.value(self._shift(items, directions, k, -s), p_lin)) / (2.0 * s)
306
+ for units, h in self._step_groups(items, directions, k):
307
+ def central(x, units=units, k=k):
308
+ return (self.value(self._shift_units(items, directions, units, k, x), p_lin)
309
+ - self.value(self._shift_units(items, directions, units, k, -x), p_lin)) / (2.0 * x)
175
310
  g[k] += (4.0 * central(h / 2.0) - central(h)) / 3.0
176
311
  if k in self.explicit: # the loss names this parameter itself
177
312
  d = 1e-6 * max(abs(p_lin[k]), 1e-8)
@@ -263,6 +398,40 @@ class ParameterReplay:
263
398
  # AMICI
264
399
  # ---------------------------------------------------------------------------------------
265
400
 
401
+ @contextlib.contextmanager
402
+ def _build_lock(directory):
403
+ """Hold an exclusive lock on one model's build, across processes (a Slurm array compiles the same model at once).
404
+
405
+ Whoever gets the lock first compiles; the rest wait, then find the finished module and import it. The lock file
406
+ sits beside the model directory, and ``flock`` is released if the holder dies.
407
+ """
408
+ os.makedirs(os.path.dirname(directory) or ".", exist_ok=True)
409
+ with open(directory + ".lock", "a") as fh:
410
+ fcntl.flock(fh, fcntl.LOCK_EX)
411
+ try:
412
+ yield
413
+ finally:
414
+ fcntl.flock(fh, fcntl.LOCK_UN)
415
+
416
+
417
+ def _model_directory(cache_dir, name, tag):
418
+ """Where a compiled model lives: the simulation's name made safe for a path and a shell, plus the model's hash.
419
+
420
+ A name such as ``Aducanumab 1 mg/kg (Q4W)`` would otherwise nest a directory at the slash and break the build's
421
+ shell command at the parentheses.
422
+ """
423
+ return os.path.join(cache_dir, re.sub(r"\W", "_", name) + f"_{tag}")
424
+
425
+
426
+ def _never_reached(param_names, lost_per_sim):
427
+ """Fitted parameters that reach no plain parameter of the model in any simulation.
428
+
429
+ A parameter one arm does not use (another drug's binding rate) is ``lost`` there by design; only one that no arm
430
+ reaches would silently get a zero gradient.
431
+ """
432
+ return [p for p in param_names if lost_per_sim and all(p in lost for lost in lost_per_sim)]
433
+
434
+
266
435
  def _tracked_window(settings):
267
436
  blocks = settings["simulation_blocks"]
268
437
  blocks = list(blocks.values()) if isinstance(blocks, dict) else list(blocks)
@@ -399,12 +568,9 @@ class _SimGradient:
399
568
  f"{name}: a hook sets species initial values that depend on the fitted parameters "
400
569
  f"({sorted(set(found) | set(direct))}); their sensitivities are not available")
401
570
  direct = found
402
- lost = [p for p, hit in zip(param_names, reach.any(axis=0)) if not hit]
403
- if lost:
404
- raise AnalyticGradientError(
405
- f"{name}: fitted parameter(s) {lost} reach no plain parameter of the model (a species "
406
- "initial value, or a parameter with an initial assignment?): the analytic gradient "
407
- "would be zero for them")
571
+ # a parameter this arm does not use (another drug's binding rate) has a zero column here by design;
572
+ # AmiciGradient raises if no arm at all is reached by it
573
+ self.lost = [p for p, hit in zip(param_names, reach.any(axis=0)) if not hit]
408
574
  self.free = [p for p, used in zip(plain, reach.any(axis=1)) if used]
409
575
 
410
576
  columns = [c for c in replicate["Observed_species"](r) if c != "time"]
@@ -418,16 +584,18 @@ class _SimGradient:
418
584
  outputs = concentration_outputs(sbml, usable)
419
585
  tag = hashlib.sha256(json.dumps([scaled.sbml, sorted(self.free), list(outputs.items())],
420
586
  sort_keys=True).encode()).hexdigest()[:16]
421
- directory = os.path.join(cache_dir, f"{name}_{tag}")
587
+ directory = _model_directory(cache_dir, name, tag)
422
588
  module_name = re.sub(r"\W", "_", f"pag_{name}_{tag}")
423
589
  import amici
424
- if os.path.exists(os.path.join(directory, module_name, "__init__.py")):
425
- module = amici.import_model_module(module_name, directory)
426
- else:
427
- if verbose:
428
- print(f" [amici] compiling {name}: {len(self.free)} sensitivity parameter(s), "
429
- f"{len(usable)} output(s) -> {directory}", flush=True)
430
- module = compile_model(scaled, module_name, directory, free=self.free, outputs=outputs)
590
+ init = os.path.join(directory, module_name, "__init__.py")
591
+ with _build_lock(directory): # an __init__.py seen without the lock may belong to a build still in progress
592
+ if os.path.exists(init):
593
+ module = amici.import_model_module(module_name, directory)
594
+ else:
595
+ if verbose:
596
+ print(f" [amici] compiling {name}: {len(self.free)} sensitivity parameter(s), "
597
+ f"{len(usable)} output(s) -> {directory}", flush=True)
598
+ module = compile_model(scaled, module_name, directory, free=self.free, outputs=outputs)
431
599
  self.runner = AmiciSegmentRunner(scaled, schedule, module, states=[], rtol=rtol, atol=atol,
432
600
  max_steps=max_steps)
433
601
  self.t_start, self.t_first, self.t_last = _tracked_window(settings)
@@ -571,6 +739,12 @@ class AmiciGradient:
571
739
  model_text=model_text, paths=paths, cache_dir=cache_dir, rtol=settings["rtol"],
572
740
  atol=settings["atol"], max_steps=settings["max_steps"], verbose=settings["verbose"],
573
741
  seed=settings["seed"], extra_times=(settings["extra_times"] or {}).get(name, ()))
742
+ never = _never_reached(self.param_names, [sim.lost for sim in self.sims.values()])
743
+ if never:
744
+ raise AnalyticGradientError(
745
+ f"fitted parameter(s) {never} reach no plain parameter of the model in any arm (a species "
746
+ "initial value, or a parameter with an initial assignment?): the analytic gradient "
747
+ "would be zero for them")
574
748
  self.n_calls = 0
575
749
 
576
750
  @staticmethod
@@ -605,6 +779,17 @@ class AmiciGradient:
605
779
  self.n_calls += 1
606
780
  return self.loss.gradient(items, dirs, p_lin)
607
781
 
782
+ def linear_residual_jacobian(self, p_lin):
783
+ """``(r, J, const)`` on AMICI's own simulation; see :meth:`LossGradient.residual_jacobian`."""
784
+ items, dirs = {}, {}
785
+ for name, sim in self.sims.items():
786
+ table, D = sim.tables(p_lin, self.groups, self.replicates, self.models)
787
+ items[name] = {"results": table, "data": self.models[name]["df_dict"],
788
+ "replicate": self.replicates[name]}
789
+ dirs[name] = D
790
+ self.n_calls += 1
791
+ return self.loss.residual_jacobian(items, dirs, p_lin)
792
+
608
793
  def __call__(self, x_opt):
609
794
  """The gradient in optimizer space (a log10 parameter's derivative is taken with respect to log10 p)."""
610
795
  from pyantigen.engine.Optimize import _to_linear