modelbase2 0.1.79__py3-none-any.whl → 0.2.0__py3-none-any.whl

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 (58) hide show
  1. modelbase2/__init__.py +138 -26
  2. modelbase2/distributions.py +306 -0
  3. modelbase2/experimental/__init__.py +17 -0
  4. modelbase2/experimental/codegen.py +239 -0
  5. modelbase2/experimental/diff.py +227 -0
  6. modelbase2/experimental/notes.md +4 -0
  7. modelbase2/experimental/tex.py +521 -0
  8. modelbase2/fit.py +284 -0
  9. modelbase2/fns.py +185 -0
  10. modelbase2/integrators/__init__.py +19 -0
  11. modelbase2/integrators/int_assimulo.py +146 -0
  12. modelbase2/integrators/int_scipy.py +147 -0
  13. modelbase2/label_map.py +610 -0
  14. modelbase2/linear_label_map.py +301 -0
  15. modelbase2/mc.py +548 -0
  16. modelbase2/mca.py +280 -0
  17. modelbase2/model.py +1621 -0
  18. modelbase2/npe.py +343 -0
  19. modelbase2/parallel.py +171 -0
  20. modelbase2/parameterise.py +28 -0
  21. modelbase2/paths.py +36 -0
  22. modelbase2/plot.py +829 -0
  23. modelbase2/sbml/__init__.py +14 -0
  24. modelbase2/sbml/_data.py +77 -0
  25. modelbase2/sbml/_export.py +656 -0
  26. modelbase2/sbml/_import.py +585 -0
  27. modelbase2/sbml/_mathml.py +691 -0
  28. modelbase2/sbml/_name_conversion.py +52 -0
  29. modelbase2/sbml/_unit_conversion.py +74 -0
  30. modelbase2/scan.py +616 -0
  31. modelbase2/scope.py +96 -0
  32. modelbase2/simulator.py +635 -0
  33. modelbase2/surrogates/__init__.py +32 -0
  34. modelbase2/surrogates/_poly.py +66 -0
  35. modelbase2/surrogates/_torch.py +249 -0
  36. modelbase2/surrogates.py +316 -0
  37. modelbase2/types.py +352 -11
  38. modelbase2-0.2.0.dist-info/METADATA +81 -0
  39. modelbase2-0.2.0.dist-info/RECORD +42 -0
  40. {modelbase2-0.1.79.dist-info → modelbase2-0.2.0.dist-info}/WHEEL +1 -1
  41. modelbase2/core/__init__.py +0 -29
  42. modelbase2/core/algebraic_module_container.py +0 -130
  43. modelbase2/core/constant_container.py +0 -113
  44. modelbase2/core/data.py +0 -109
  45. modelbase2/core/name_container.py +0 -29
  46. modelbase2/core/reaction_container.py +0 -115
  47. modelbase2/core/utils.py +0 -28
  48. modelbase2/core/variable_container.py +0 -24
  49. modelbase2/ode/__init__.py +0 -13
  50. modelbase2/ode/integrator.py +0 -80
  51. modelbase2/ode/mca.py +0 -270
  52. modelbase2/ode/model.py +0 -470
  53. modelbase2/ode/simulator.py +0 -153
  54. modelbase2/utils/__init__.py +0 -0
  55. modelbase2/utils/plotting.py +0 -372
  56. modelbase2-0.1.79.dist-info/METADATA +0 -44
  57. modelbase2-0.1.79.dist-info/RECORD +0 -22
  58. {modelbase2-0.1.79.dist-info → modelbase2-0.2.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,239 @@
1
+ """Module to export models as code."""
2
+
3
+ import ast
4
+ import inspect
5
+ import warnings
6
+ from collections.abc import Callable, Generator, Iterable, Iterator
7
+
8
+ from modelbase2.model import Model
9
+ from modelbase2.types import Derived
10
+
11
+ __all__ = [
12
+ "DocstringRemover",
13
+ "IdentifierReplacer",
14
+ "ReturnRemover",
15
+ "conditional_join",
16
+ "generate_model_code_py",
17
+ "generate_modelbase_code",
18
+ "get_fn_source",
19
+ "handle_fn",
20
+ ]
21
+
22
+
23
+ class IdentifierReplacer(ast.NodeTransformer):
24
+ """Replace identifiers in an AST."""
25
+
26
+ def __init__(self, mapping: dict[str, str]) -> None:
27
+ """Initialize the transformer with a mapping."""
28
+ self.mapping = mapping
29
+
30
+ def visit_Name(self, node: ast.Name) -> ast.Name: # noqa: N802
31
+ """Replace the identifier with the mapped value."""
32
+ return ast.Name(
33
+ id=self.mapping.get(node.id, node.id),
34
+ ctx=node.ctx,
35
+ )
36
+
37
+
38
+ class DocstringRemover(ast.NodeTransformer):
39
+ """Remove docstrings from an AST."""
40
+
41
+ def visit_Expr(self, node: ast.Expr) -> ast.Expr | None: # noqa: N802
42
+ """Remove docstrings."""
43
+ if isinstance(const := node.value, ast.Constant) and isinstance(
44
+ const.value, str
45
+ ):
46
+ return None
47
+ return node
48
+
49
+
50
+ class ReturnRemover(ast.NodeTransformer):
51
+ """Remove return statements from an AST."""
52
+
53
+ def visit_Return(self, node: ast.Return) -> ast.expr | None: # noqa: N802
54
+ """Remove return statements."""
55
+ return node.value
56
+
57
+
58
+ def get_fn_source(fn: Callable) -> ast.FunctionDef:
59
+ """Get the source code of a function as an AST."""
60
+ import inspect
61
+ import textwrap
62
+
63
+ import dill
64
+
65
+ try:
66
+ source = inspect.getsource(fn)
67
+ except OSError: # could not get source code
68
+ source = dill.source.getsource(fn)
69
+
70
+ tree = ast.parse(textwrap.dedent(source))
71
+ if not isinstance(fn_def := tree.body[0], ast.FunctionDef):
72
+ msg = "Not a function"
73
+ raise TypeError(msg)
74
+ return fn_def
75
+
76
+
77
+ def handle_fn(fn: Callable, args: list[str]) -> str:
78
+ """Get the source code of a function, removing docstrings and return statements."""
79
+ tree = get_fn_source(fn)
80
+
81
+ argmap = dict(zip([i.arg for i in tree.args.args], args, strict=True))
82
+ tree = DocstringRemover().visit(tree)
83
+ tree = IdentifierReplacer(argmap).visit(tree)
84
+ tree = ReturnRemover().visit(tree)
85
+ return ast.unparse(tree.body)
86
+
87
+
88
+ def conditional_join[T](
89
+ iterable: Iterable[T],
90
+ question: Callable[[T], bool],
91
+ true_pat: str,
92
+ false_pat: str,
93
+ ) -> str:
94
+ """Join an iterable, applying a pattern to each element based on a condition."""
95
+
96
+ def inner(it: Iterator[T]) -> Generator[str, None, None]:
97
+ yield str(next(it))
98
+ while True:
99
+ try:
100
+ el = next(it)
101
+ if question(el):
102
+ yield f"{true_pat}{el}"
103
+ else:
104
+ yield f"{false_pat}{el}"
105
+ except StopIteration:
106
+ break
107
+
108
+ return "".join(inner(iter(iterable)))
109
+
110
+
111
+ def generate_modelbase_code(model: Model) -> str:
112
+ """Generate a modelbase model from a model."""
113
+ functions = {}
114
+
115
+ # Variables and parameters
116
+ variables = model.variables
117
+ parameters = model.parameters
118
+
119
+ # Derived
120
+ derived_source = []
121
+ for k, v in model.derived.items():
122
+ fn = v.fn
123
+ fn_name = fn.__name__
124
+ functions[fn_name] = inspect.getsource(fn)
125
+
126
+ derived_source.append(
127
+ f""".add_derived(
128
+ "{k}",
129
+ fn={fn_name},
130
+ args={v.args},
131
+ )"""
132
+ )
133
+
134
+ # Reactions
135
+ reactions_source = []
136
+ for k, v in model.reactions.items():
137
+ fn = v.fn
138
+ fn_name = fn.__name__
139
+ functions[fn_name] = inspect.getsource(fn)
140
+ reactions_source.append(
141
+ f""" .add_reaction(
142
+ "{k}",
143
+ fn={fn_name},
144
+ args={v.args},
145
+ stoichiometry={v.stoichiometry},
146
+ )"""
147
+ )
148
+
149
+ # Surrogates
150
+ if len(model._surrogates) > 0: # noqa: SLF001
151
+ warnings.warn(
152
+ "Generating code for Surrogates not yet supported.",
153
+ stacklevel=1,
154
+ )
155
+
156
+ # Combine all the sources
157
+ functions_source = "\n".join(functions.values())
158
+ source = [
159
+ "from modelbase2 import Model\n",
160
+ functions_source,
161
+ "def create_model() -> Model:",
162
+ " return (",
163
+ " Model()",
164
+ ]
165
+ if len(parameters) > 0:
166
+ source.append(f" .add_parameters({parameters})")
167
+ if len(variables) > 0:
168
+ source.append(f" .add_variables({variables})")
169
+ if len(derived_source) > 0:
170
+ source.append("\n".join(derived_source))
171
+ if len(reactions_source) > 0:
172
+ source.append("\n".join(reactions_source))
173
+
174
+ source.append(" )")
175
+
176
+ return "\n".join(source)
177
+
178
+
179
+ def generate_model_code_py(model: Model) -> str:
180
+ """Transform the model into a single function, inlining the function calls."""
181
+ # Variables
182
+ variables = model.variables
183
+ variable_source = " {} = y".format(", ".join(variables))
184
+
185
+ # Parameters
186
+ parameter_source = "\n".join(f" {k} = {v}" for k, v in model.parameters.items())
187
+
188
+ # Derived
189
+ derived_source = []
190
+ for name, derived in model.derived.items():
191
+ derived_source.append(f" {name} = {handle_fn(derived.fn, derived.args)}")
192
+
193
+ # Reactions
194
+ reactions = []
195
+ for name, rxn in model.reactions.items():
196
+ reactions.append(f" {name} = {handle_fn(rxn.fn, rxn.args)}")
197
+
198
+ # Stoichiometries
199
+ stoichiometries = {}
200
+ for rxn_name, rxn in model.reactions.items():
201
+ for cpd_name, factor in rxn.stoichiometry.items():
202
+ if isinstance(factor, Derived):
203
+ src = f"{handle_fn(factor.fn, factor.args)} * {rxn_name}"
204
+ elif factor == 1:
205
+ src = rxn_name
206
+ elif factor == -1:
207
+ src = f"- {rxn_name}"
208
+ else:
209
+ src = f"{factor} * {rxn_name}"
210
+ stoichiometries.setdefault(cpd_name, []).append(src)
211
+ stoich_source = []
212
+ for variable, stoich in stoichiometries.items():
213
+ stoich_source.append(
214
+ f" d{variable}dt = {conditional_join(stoich, lambda x: x.startswith("-"), " ", " + ")}"
215
+ )
216
+
217
+ # Surrogates
218
+ if len(model._surrogates) > 0: # noqa: SLF001
219
+ warnings.warn(
220
+ "Generating code for Surrogates not yet supported.",
221
+ stacklevel=1,
222
+ )
223
+
224
+ # Combine all the sources
225
+ source = [
226
+ "from collections.abc import Iterable\n",
227
+ "from modelbase2.types import Float\n",
228
+ "def model(t: Float, y: Float) -> Iterable[Float]:",
229
+ variable_source,
230
+ parameter_source,
231
+ *derived_source,
232
+ *reactions,
233
+ *stoich_source,
234
+ " return {}".format(
235
+ ", ".join(f"d{i}dt" for i in variables),
236
+ ),
237
+ ]
238
+
239
+ return "\n".join(source)
@@ -0,0 +1,227 @@
1
+ """Diffing utilities for comparing models."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Mapping
5
+
6
+ from modelbase2.model import Model
7
+ from modelbase2.types import Derived
8
+
9
+ __all__ = ["DerivedDiff", "ModelDiff", "ReactionDiff", "model_diff", "soft_eq"]
10
+
11
+
12
+ @dataclass
13
+ class DerivedDiff:
14
+ """Difference between two derived variables."""
15
+
16
+ args1: list[str] = field(default_factory=list)
17
+ args2: list[str] = field(default_factory=list)
18
+
19
+
20
+ @dataclass
21
+ class ReactionDiff:
22
+ """Difference between two reactions."""
23
+
24
+ args1: list[str] = field(default_factory=list)
25
+ args2: list[str] = field(default_factory=list)
26
+ stoichiometry1: dict[str, float | Derived] = field(default_factory=dict)
27
+ stoichiometry2: dict[str, float | Derived] = field(default_factory=dict)
28
+
29
+
30
+ @dataclass
31
+ class ModelDiff:
32
+ """Difference between two models."""
33
+
34
+ missing_parameters: set[str] = field(default_factory=set)
35
+ missing_variables: set[str] = field(default_factory=set)
36
+ missing_reactions: set[str] = field(default_factory=set)
37
+ missing_surrogates: set[str] = field(default_factory=set)
38
+ missing_readouts: set[str] = field(default_factory=set)
39
+ missing_derived: set[str] = field(default_factory=set)
40
+ different_parameters: dict[str, tuple[float, float]] = field(default_factory=dict)
41
+ different_variables: dict[str, tuple[float, float]] = field(default_factory=dict)
42
+ different_reactions: dict[str, ReactionDiff] = field(default_factory=dict)
43
+ different_surrogates: dict[str, ReactionDiff] = field(default_factory=dict)
44
+ different_readouts: dict[str, DerivedDiff] = field(default_factory=dict)
45
+ different_derived: dict[str, DerivedDiff] = field(default_factory=dict)
46
+
47
+ def __str__(self) -> str:
48
+ """Return a human-readable string representation of the diff."""
49
+ content = ["Model Diff", "----------"]
50
+
51
+ # Parameters
52
+ if self.missing_parameters:
53
+ content.append(
54
+ "Missing Parameters: {}".format(", ".join(self.missing_parameters))
55
+ )
56
+ if self.different_parameters:
57
+ content.append("Different Parameters:")
58
+ for k, (v1, v2) in self.different_parameters.items():
59
+ content.append(f" {k}: {v1} != {v2}")
60
+
61
+ # Variables
62
+ if self.missing_variables:
63
+ content.append(
64
+ "Missing Variables: {}".format(", ".join(self.missing_variables))
65
+ )
66
+ if self.different_variables:
67
+ content.append("Different Variables:")
68
+ for k, (v1, v2) in self.different_variables.items():
69
+ content.append(f" {k}: {v1} != {v2}")
70
+
71
+ # Derived
72
+ if self.missing_derived:
73
+ content.append(
74
+ "Missing Derived: {}".format(", ".join(self.missing_derived))
75
+ )
76
+ if self.different_derived:
77
+ content.append("Different Derived:")
78
+ for k, diff in self.different_derived.items():
79
+ content.append(f" {k}:")
80
+ if diff.args1 != diff.args2:
81
+ content.append(f" Args: {diff.args1} != {diff.args2}")
82
+
83
+ # Reactions
84
+ if self.missing_reactions:
85
+ content.append(
86
+ "Missing Reactions: {}".format(", ".join(self.missing_reactions))
87
+ )
88
+ if self.different_reactions:
89
+ content.append("Different Reactions:")
90
+ for k, diff in self.different_reactions.items():
91
+ content.append(f" {k}:")
92
+ if diff.args1 != diff.args2:
93
+ content.append(f" Args: {diff.args1} != {diff.args2}")
94
+ if diff.stoichiometry1 != diff.stoichiometry2:
95
+ content.append(
96
+ f" Stoichiometry: {diff.stoichiometry1} != {diff.stoichiometry2}"
97
+ )
98
+
99
+ # Surrogates
100
+ if self.missing_surrogates:
101
+ content.append(
102
+ "Missing Surrogates: {}".format(", ".join(self.missing_surrogates))
103
+ )
104
+ if self.different_surrogates:
105
+ content.append("Different Surrogates:")
106
+ for k, diff in self.different_surrogates.items():
107
+ content.append(f" {k}:")
108
+ if diff.args1 != diff.args2:
109
+ content.append(f" Args: {diff.args1} != {diff.args2}")
110
+ if diff.stoichiometry1 != diff.stoichiometry2:
111
+ content.append(
112
+ f" Stoichiometry: {diff.stoichiometry1} != {diff.stoichiometry2}"
113
+ )
114
+ return "\n".join(content)
115
+
116
+
117
+ def _soft_eq_stoichiometries(
118
+ s1: Mapping[str, float | Derived], s2: Mapping[str, float | Derived]
119
+ ) -> bool:
120
+ """Check if two stoichiometries are equal, ignoring the functions."""
121
+ if s1.keys() != s2.keys():
122
+ return False
123
+
124
+ for k, v1 in s1.items():
125
+ v2 = s2[k]
126
+ if isinstance(v1, Derived):
127
+ if not isinstance(v2, Derived):
128
+ return False
129
+ if v1.args != v2.args:
130
+ return False
131
+ else:
132
+ if v1 != v2:
133
+ return False
134
+
135
+ return True
136
+
137
+
138
+ def soft_eq(m1: Model, m2: Model) -> bool:
139
+ """Check if two models are equal, ignoring the functions."""
140
+ if m1._parameters != m2._parameters: # noqa: SLF001
141
+ return False
142
+ if m1._variables != m2._variables: # noqa: SLF001
143
+ return False
144
+ for k, d1 in m1._derived.items(): # noqa: SLF001
145
+ if (d2 := m2._derived.get(k)) is None: # noqa: SLF001
146
+ return False
147
+ if d1.args != d2.args:
148
+ return False
149
+ for k, r1 in m1._readouts.items(): # noqa: SLF001
150
+ if (r2 := m2._readouts.get(k)) is None: # noqa: SLF001
151
+ return False
152
+ if r1.args != r2.args:
153
+ return False
154
+ for k, v1 in m1._reactions.items(): # noqa: SLF001
155
+ if (v2 := m2._reactions.get(k)) is None: # noqa: SLF001
156
+ return False
157
+ if v1.args != v2.args:
158
+ return False
159
+ if not _soft_eq_stoichiometries(v1.stoichiometry, v2.stoichiometry):
160
+ return False
161
+ for k, s1 in m1._surrogates.items(): # noqa: SLF001
162
+ if (s2 := m2._surrogates.get(k)) is None: # noqa: SLF001
163
+ return False
164
+ if s1.args != s2.args:
165
+ return False
166
+ if s1.stoichiometries != s2.stoichiometries:
167
+ return False
168
+ return True
169
+
170
+
171
+ def model_diff(m1: Model, m2: Model) -> ModelDiff:
172
+ """Compute the difference between two models."""
173
+ diff = ModelDiff()
174
+
175
+ for k, v1 in m1._parameters.items(): # noqa: SLF001
176
+ if (v2 := m2._parameters.get(k)) is None: # noqa: SLF001
177
+ diff.missing_parameters.add(k)
178
+ elif v1 != v2:
179
+ diff.different_parameters[k] = (v1, v2)
180
+
181
+ for k, v1 in m1._variables.items(): # noqa: SLF001
182
+ if (v2 := m2._variables.get(k)) is None: # noqa: SLF001
183
+ diff.missing_variables.add(k)
184
+ elif v1 != v2:
185
+ diff.different_variables[k] = (v1, v2)
186
+
187
+ for k, v1 in m1._readouts.items(): # noqa: SLF001
188
+ if (v2 := m2._readouts.get(k)) is None: # noqa: SLF001
189
+ diff.missing_readouts.add(k)
190
+ elif v1.args != v2.args:
191
+ diff.different_readouts[k] = DerivedDiff(v1.args, v2.args)
192
+
193
+ for k, v1 in m1._derived.items(): # noqa: SLF001
194
+ if (v2 := m2._derived.get(k)) is None: # noqa: SLF001
195
+ diff.missing_derived.add(k)
196
+ elif v1.args != v2.args:
197
+ diff.different_derived[k] = DerivedDiff(v1.args, v2.args)
198
+
199
+ for k, v1 in m1._reactions.items(): # noqa: SLF001
200
+ if (v2 := m2._reactions.get(k)) is None: # noqa: SLF001
201
+ diff.missing_reactions.add(k)
202
+ else:
203
+ if v1.args != v2.args:
204
+ rxn_diff: ReactionDiff = diff.different_reactions.get(k, ReactionDiff())
205
+ rxn_diff.args1 = v1.args
206
+ rxn_diff.args2 = v2.args
207
+ diff.different_reactions[k] = rxn_diff
208
+ if v1.stoichiometry != v2.stoichiometry:
209
+ rxn_diff = diff.different_reactions.get(k, ReactionDiff())
210
+ rxn_diff.stoichiometry1 = dict(v1.stoichiometry)
211
+ rxn_diff.stoichiometry2 = dict(v2.stoichiometry)
212
+ diff.different_reactions[k] = rxn_diff
213
+
214
+ for k, v1 in m1._surrogates.items(): # noqa: SLF001
215
+ if (v2 := m2._surrogates.get(k)) is None: # noqa: SLF001
216
+ diff.missing_surrogates.add(k)
217
+ else:
218
+ if v1.args != v2.args:
219
+ rxn_diff = diff.different_surrogates.get(k, ReactionDiff())
220
+ rxn_diff.args1 = v1.args
221
+ rxn_diff.args2 = v2.args
222
+ if v1.stoichiometries != v2.stoichiometries:
223
+ rxn_diff = diff.different_surrogates.get(k, ReactionDiff())
224
+ rxn_diff.stoichiometry1 = dict(v1.stoichiometries) # type: ignore
225
+ rxn_diff.stoichiometry2 = dict(v2.stoichiometries) # type: ignore
226
+
227
+ return diff
@@ -0,0 +1,4 @@
1
+ # Developer notes
2
+
3
+ - Complete function expansion (e.g. inlining) other functions into the current function isn't possible in Python, as we can't expand statements
4
+ - Same is true for assignment replacement, this only works for expressions