modelbase2 0.5.0__py3-none-any.whl → 0.7.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.
modelbase2/__init__.py CHANGED
@@ -50,6 +50,7 @@ __all__ = [
50
50
  "distributions",
51
51
  "experimental",
52
52
  "fit",
53
+ "fns",
53
54
  "make_protocol",
54
55
  "mc",
55
56
  "mca",
@@ -75,6 +76,7 @@ from . import (
75
76
  distributions,
76
77
  experimental,
77
78
  fit,
79
+ fns,
78
80
  mc,
79
81
  mca,
80
82
  nnarchitectures,
@@ -1,10 +1,10 @@
1
1
  """Module to export models as code."""
2
2
 
3
3
  import ast
4
- import inspect
5
4
  import warnings
6
5
  from collections.abc import Callable, Generator, Iterable, Iterator
7
6
 
7
+ from modelbase2.experimental.source_tools import get_fn_ast, get_fn_source
8
8
  from modelbase2.model import Model
9
9
  from modelbase2.types import Derived
10
10
 
@@ -15,7 +15,6 @@ __all__ = [
15
15
  "conditional_join",
16
16
  "generate_model_code_py",
17
17
  "generate_modelbase_code",
18
- "get_fn_source",
19
18
  "handle_fn",
20
19
  ]
21
20
 
@@ -55,28 +54,9 @@ class ReturnRemover(ast.NodeTransformer):
55
54
  return node.value
56
55
 
57
56
 
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
57
  def handle_fn(fn: Callable, args: list[str]) -> str:
78
58
  """Get the source code of a function, removing docstrings and return statements."""
79
- tree = get_fn_source(fn)
59
+ tree = get_fn_ast(fn)
80
60
 
81
61
  argmap = dict(zip([i.arg for i in tree.args.args], args, strict=True))
82
62
  tree = DocstringRemover().visit(tree)
@@ -118,31 +98,41 @@ def generate_modelbase_code(model: Model) -> str:
118
98
 
119
99
  # Derived
120
100
  derived_source = []
121
- for k, v in model.derived.items():
122
- fn = v.fn
101
+ for k, rxn in model.derived.items():
102
+ fn = rxn.fn
123
103
  fn_name = fn.__name__
124
- functions[fn_name] = inspect.getsource(fn)
104
+ functions[fn_name] = get_fn_source(fn)
125
105
 
126
106
  derived_source.append(
127
- f""".add_derived(
107
+ f""" .add_derived(
128
108
  "{k}",
129
109
  fn={fn_name},
130
- args={v.args},
110
+ args={rxn.args},
131
111
  )"""
132
112
  )
133
113
 
134
114
  # Reactions
135
115
  reactions_source = []
136
- for k, v in model.reactions.items():
137
- fn = v.fn
116
+ for k, rxn in model.reactions.items():
117
+ fn = rxn.fn
138
118
  fn_name = fn.__name__
139
- functions[fn_name] = inspect.getsource(fn)
119
+ functions[fn_name] = get_fn_source(fn)
120
+ stoichiometry: list[str] = []
121
+ for var, stoich in rxn.stoichiometry.items():
122
+ if isinstance(stoich, Derived):
123
+ functions[fn_name] = get_fn_source(fn)
124
+ args = ", ".join(f'"{k}"' for k in stoich.args)
125
+ stoich = ( # noqa: PLW2901
126
+ f"""Derived(name="{var}", fn={fn.__name__}, args=[{args}])"""
127
+ )
128
+ stoichiometry.append(f""""{var}": {stoich}""")
129
+
140
130
  reactions_source.append(
141
131
  f""" .add_reaction(
142
132
  "{k}",
143
133
  fn={fn_name},
144
- args={v.args},
145
- stoichiometry={v.stoichiometry},
134
+ args={rxn.args},
135
+ stoichiometry={{{",".join(stoichiometry)}}},
146
136
  )"""
147
137
  )
148
138
 
@@ -178,25 +168,32 @@ def generate_modelbase_code(model: Model) -> str:
178
168
 
179
169
  def generate_model_code_py(model: Model) -> str:
180
170
  """Transform the model into a single function, inlining the function calls."""
171
+ source = [
172
+ "from collections.abc import Iterable\n",
173
+ "from modelbase2.types import Float\n",
174
+ "def model(t: Float, y: Float) -> Iterable[Float]:",
175
+ ]
176
+
181
177
  # Variables
182
178
  variables = model.variables
183
- variable_source = " {} = y".format(", ".join(variables))
179
+ if len(variables) > 0:
180
+ source.append(" {} = y".format(", ".join(variables)))
184
181
 
185
182
  # Parameters
186
- parameter_source = "\n".join(f" {k} = {v}" for k, v in model.parameters.items())
183
+ parameters = model.parameters
184
+ if len(parameters) > 0:
185
+ source.append("\n".join(f" {k} = {v}" for k, v in model.parameters.items()))
187
186
 
188
187
  # Derived
189
- derived_source = []
190
188
  for name, derived in model.derived.items():
191
- derived_source.append(f" {name} = {handle_fn(derived.fn, derived.args)}")
189
+ source.append(f" {name} = {handle_fn(derived.fn, derived.args)}")
192
190
 
193
191
  # Reactions
194
- reactions = []
195
192
  for name, rxn in model.reactions.items():
196
- reactions.append(f" {name} = {handle_fn(rxn.fn, rxn.args)}")
193
+ source.append(f" {name} = {handle_fn(rxn.fn, rxn.args)}")
197
194
 
198
195
  # Stoichiometries
199
- stoichiometries = {}
196
+ stoich_srcs = {}
200
197
  for rxn_name, rxn in model.reactions.items():
201
198
  for cpd_name, factor in rxn.stoichiometry.items():
202
199
  if isinstance(factor, Derived):
@@ -207,10 +204,9 @@ def generate_model_code_py(model: Model) -> str:
207
204
  src = f"- {rxn_name}"
208
205
  else:
209
206
  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(
207
+ stoich_srcs.setdefault(cpd_name, []).append(src)
208
+ for variable, stoich in stoich_srcs.items():
209
+ source.append(
214
210
  f" d{variable}dt = {conditional_join(stoich, lambda x: x.startswith('-'), ' ', ' + ')}"
215
211
  )
216
212
 
@@ -221,19 +217,14 @@ def generate_model_code_py(model: Model) -> str:
221
217
  stacklevel=1,
222
218
  )
223
219
 
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
- ]
220
+ # Return
221
+ if len(variables) > 0:
222
+ source.append(
223
+ " return {}".format(
224
+ ", ".join(f"d{i}dt" for i in variables),
225
+ ),
226
+ )
227
+ else:
228
+ source.append(" return ()")
238
229
 
239
230
  return "\n".join(source)
@@ -1,7 +1,7 @@
1
1
  """Diffing utilities for comparing models."""
2
2
 
3
+ from collections.abc import Mapping
3
4
  from dataclasses import dataclass, field
4
- from typing import Mapping
5
5
 
6
6
  from modelbase2.model import Model
7
7
  from modelbase2.types import Derived
@@ -128,9 +128,8 @@ def _soft_eq_stoichiometries(
128
128
  return False
129
129
  if v1.args != v2.args:
130
130
  return False
131
- else:
132
- if v1 != v2:
133
- return False
131
+ elif v1 != v2:
132
+ return False
134
133
 
135
134
  return True
136
135
 
@@ -0,0 +1,30 @@
1
+ """Tools for working with python source files."""
2
+
3
+ import ast
4
+ import inspect
5
+ import textwrap
6
+ from collections.abc import Callable
7
+
8
+ import dill
9
+
10
+ __all__ = [
11
+ "get_fn_ast",
12
+ "get_fn_source",
13
+ ]
14
+
15
+
16
+ def get_fn_source(fn: Callable) -> str:
17
+ """Get the string representation of a function."""
18
+ try:
19
+ return inspect.getsource(fn)
20
+ except OSError: # could not get source code
21
+ return dill.source.getsource(fn)
22
+
23
+
24
+ def get_fn_ast(fn: Callable) -> ast.FunctionDef:
25
+ """Get the source code of a function as an AST."""
26
+ tree = ast.parse(textwrap.dedent(get_fn_source(fn)))
27
+ if not isinstance(fn_def := tree.body[0], ast.FunctionDef):
28
+ msg = "Not a function"
29
+ raise TypeError(msg)
30
+ return fn_def
@@ -2,22 +2,43 @@
2
2
 
3
3
  import ast
4
4
  import inspect
5
- import textwrap
6
5
  from collections.abc import Callable
7
6
  from dataclasses import dataclass
7
+ from types import ModuleType
8
8
  from typing import Any, cast
9
9
 
10
10
  import sympy
11
11
 
12
+ from modelbase2.experimental.source_tools import get_fn_ast
12
13
  from modelbase2.model import Model
13
14
 
14
- __all__ = ["Context", "SymbolicModel", "model_fn_to_sympy", "to_symbolic_model"]
15
+ __all__ = [
16
+ "Context",
17
+ "SymbolicModel",
18
+ "model_fn_to_sympy",
19
+ "to_symbolic_model",
20
+ ]
15
21
 
16
22
 
17
23
  @dataclass
18
24
  class Context:
19
25
  symbols: dict[str, sympy.Symbol | sympy.Expr]
20
26
  caller: Callable
27
+ parent_module: ModuleType | None
28
+
29
+ def updated(
30
+ self,
31
+ symbols: dict[str, sympy.Symbol | sympy.Expr] | None = None,
32
+ caller: Callable | None = None,
33
+ parent_module: ModuleType | None = None,
34
+ ) -> "Context":
35
+ return Context(
36
+ symbols=self.symbols if symbols is None else symbols,
37
+ caller=self.caller if caller is None else caller,
38
+ parent_module=self.parent_module
39
+ if parent_module is None
40
+ else parent_module,
41
+ )
21
42
 
22
43
 
23
44
  @dataclass
@@ -27,68 +48,21 @@ class SymbolicModel:
27
48
  eqs: list[sympy.Expr]
28
49
 
29
50
 
30
- def to_symbolic_model(model: Model) -> SymbolicModel:
31
- cache = model._create_cache() # noqa: SLF001
32
-
33
- variables = dict(
34
- zip(model.variables, sympy.symbols(list(model.variables)), strict=True)
35
- )
36
- parameters = dict(
37
- zip(model.parameters, sympy.symbols(list(model.parameters)), strict=True)
38
- )
39
- symbols = variables | parameters
40
-
41
- for k, v in model.derived.items():
42
- symbols[k] = model_fn_to_sympy(v.fn, [symbols[i] for i in v.args])
43
-
44
- rxns = {
45
- k: model_fn_to_sympy(v.fn, [symbols[i] for i in v.args])
46
- for k, v in model.reactions.items()
47
- }
48
-
49
- eqs: dict[str, sympy.Expr] = {}
50
- for cpd, stoich in cache.stoich_by_cpds.items():
51
- for rxn, stoich_value in stoich.items():
52
- eqs[cpd] = (
53
- eqs.get(cpd, sympy.Float(0.0)) + sympy.Float(stoich_value) * rxns[rxn] # type: ignore
54
- )
55
-
56
- for cpd, dstoich in cache.dyn_stoich_by_cpds.items():
57
- for rxn, der in dstoich.items():
58
- eqs[cpd] = eqs.get(cpd, sympy.Float(0.0)) + model_fn_to_sympy(
59
- der.fn,
60
- [symbols[i] for i in der.args] * rxns[rxn], # type: ignore
61
- ) # type: ignore
62
-
63
- return SymbolicModel(
64
- variables=variables,
65
- parameters=parameters,
66
- eqs=[eqs[i] for i in cache.var_names],
67
- )
68
-
69
-
70
51
  def model_fn_to_sympy(
71
52
  fn: Callable, model_args: list[sympy.Symbol | sympy.Expr] | None = None
72
53
  ) -> sympy.Expr:
73
- source = textwrap.dedent(inspect.getsource(fn))
74
-
75
- if not isinstance(fn_def := ast.parse(source).body[0], ast.FunctionDef):
76
- msg = "Expected a function definition"
77
- raise TypeError(msg)
78
-
54
+ fn_def = get_fn_ast(fn)
79
55
  fn_args = [str(arg.arg) for arg in fn_def.args.args]
80
-
81
56
  sympy_expr = _handle_fn_body(
82
57
  fn_def.body,
83
58
  ctx=Context(
84
59
  symbols={name: sympy.Symbol(name) for name in fn_args},
85
60
  caller=fn,
61
+ parent_module=inspect.getmodule(fn),
86
62
  ),
87
63
  )
88
-
89
64
  if model_args is not None:
90
65
  sympy_expr = sympy_expr.subs(dict(zip(fn_args, model_args, strict=True)))
91
-
92
66
  return cast(sympy.Expr, sympy_expr)
93
67
 
94
68
 
@@ -215,18 +189,45 @@ def _handle_binop(node: ast.BinOp, ctx: Context) -> sympy.Expr:
215
189
 
216
190
 
217
191
  def _handle_call(node: ast.Call, ctx: Context) -> sympy.Expr:
218
- if not isinstance(callee := node.func, ast.Name):
219
- msg = "Only function calls with names are supported"
220
- raise TypeError(msg)
192
+ # direct call, e.g. mass_action(x, k1)
193
+ if isinstance(callee := node.func, ast.Name):
194
+ fn_name = str(callee.id)
195
+ fns = dict(inspect.getmembers(ctx.parent_module, predicate=callable))
196
+
197
+ return model_fn_to_sympy(
198
+ fns[fn_name],
199
+ model_args=[_handle_expr(i, ctx) for i in node.args],
200
+ )
201
+
202
+ # search for fn in other namespace
203
+ if isinstance(attr := node.func, ast.Attribute):
204
+ imports = dict(inspect.getmembers(ctx.parent_module, inspect.ismodule))
205
+
206
+ # Single level, e.g. fns.mass_action(x, k1)
207
+ if isinstance(module_name := attr.value, ast.Name):
208
+ return _handle_call(
209
+ ast.Call(func=ast.Name(attr.attr), args=node.args),
210
+ ctx=ctx.updated(parent_module=imports[module_name.id]),
211
+ )
221
212
 
222
- fn_name = str(callee.id)
223
- parent_module = inspect.getmodule(ctx.caller)
224
- fns = dict(inspect.getmembers(parent_module, predicate=callable))
213
+ # Multiple levels, e.g. modelbase2.fns.mass_action(x, k1)
214
+ if isinstance(inner_attr := attr.value, ast.Attribute):
215
+ if not isinstance(module_name := inner_attr.value, ast.Name):
216
+ msg = f"Unknown target kind {module_name}"
217
+ raise NotImplementedError(msg)
218
+ return _handle_call(
219
+ ast.Call(
220
+ func=ast.Attribute(
221
+ value=ast.Name(inner_attr.attr),
222
+ attr=attr.attr,
223
+ ),
224
+ args=node.args,
225
+ ),
226
+ ctx=ctx.updated(parent_module=imports[module_name.id]),
227
+ )
225
228
 
226
- return model_fn_to_sympy(
227
- fns[fn_name],
228
- model_args=[_handle_expr(i, ctx) for i in node.args],
229
- )
229
+ msg = f"Onsupported function type {node.func}"
230
+ raise NotImplementedError(msg)
230
231
 
231
232
 
232
233
  def _handle_name(node: ast.Name, ctx: Context) -> sympy.Symbol | sympy.Expr:
@@ -284,3 +285,43 @@ def _handle_expr(node: ast.expr, ctx: Context) -> sympy.Expr:
284
285
 
285
286
  msg = f"Expression type {type(node).__name__} not implemented"
286
287
  raise NotImplementedError(msg)
288
+
289
+
290
+ def to_symbolic_model(model: Model) -> SymbolicModel:
291
+ cache = model._create_cache() # noqa: SLF001
292
+
293
+ variables = dict(
294
+ zip(model.variables, sympy.symbols(list(model.variables)), strict=True)
295
+ )
296
+ parameters = dict(
297
+ zip(model.parameters, sympy.symbols(list(model.parameters)), strict=True)
298
+ )
299
+ symbols = variables | parameters
300
+
301
+ for k, v in model.derived.items():
302
+ symbols[k] = model_fn_to_sympy(v.fn, [symbols[i] for i in v.args])
303
+
304
+ rxns = {
305
+ k: model_fn_to_sympy(v.fn, [symbols[i] for i in v.args])
306
+ for k, v in model.reactions.items()
307
+ }
308
+
309
+ eqs: dict[str, sympy.Expr] = {}
310
+ for cpd, stoich in cache.stoich_by_cpds.items():
311
+ for rxn, stoich_value in stoich.items():
312
+ eqs[cpd] = (
313
+ eqs.get(cpd, sympy.Float(0.0)) + sympy.Float(stoich_value) * rxns[rxn] # type: ignore
314
+ )
315
+
316
+ for cpd, dstoich in cache.dyn_stoich_by_cpds.items():
317
+ for rxn, der in dstoich.items():
318
+ eqs[cpd] = eqs.get(cpd, sympy.Float(0.0)) + model_fn_to_sympy(
319
+ der.fn,
320
+ [symbols[i] for i in der.args] * rxns[rxn], # type: ignore
321
+ ) # type: ignore
322
+
323
+ return SymbolicModel(
324
+ variables=variables,
325
+ parameters=parameters,
326
+ eqs=[eqs[i] for i in cache.var_names],
327
+ )
@@ -2,16 +2,21 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import ast
6
- import inspect
7
5
  from dataclasses import dataclass
8
6
  from typing import TYPE_CHECKING, cast
9
7
 
10
8
  import latexify
11
9
 
10
+ from modelbase2.experimental.source_tools import get_fn_ast
12
11
  from modelbase2.types import Derived, RateFn
13
12
 
14
- __all__ = ["TexExport", "TexReaction", "default_init", "get_model_tex_diff", "to_tex"]
13
+ __all__ = [
14
+ "TexExport",
15
+ "TexReaction",
16
+ "default_init",
17
+ "get_model_tex_diff",
18
+ "to_tex",
19
+ ]
15
20
 
16
21
  if TYPE_CHECKING:
17
22
  from collections.abc import Callable, Mapping
@@ -67,9 +72,7 @@ def _escape_non_math(s: str) -> str:
67
72
 
68
73
 
69
74
  def _fn_to_latex(fn: Callable, arg_names: list[str]) -> str:
70
- code = inspect.getsource(fn)
71
- src = cast(ast.Module, ast.parse(code))
72
- fn_def = cast(ast.FunctionDef, src.body[0])
75
+ fn_def = get_fn_ast(fn)
73
76
  args: list[str] = [i.arg for i in fn_def.args.args]
74
77
  arg_mapping: dict[str, str] = dict(zip(args, arg_names, strict=True))
75
78
  return cast(
@@ -329,7 +332,9 @@ class TexExport:
329
332
  parameters={gls.get(k, k): v for k, v in self.parameters.items()},
330
333
  variables={gls.get(k, k): v for k, v in self.variables.items()},
331
334
  derived={
332
- gls.get(k, k): Derived(fn=v.fn, args=[gls.get(i, i) for i in v.args])
335
+ gls.get(k, k): Derived(
336
+ name=k, fn=v.fn, args=[gls.get(i, i) for i in v.args]
337
+ )
333
338
  for k, v in self.derived.items()
334
339
  },
335
340
  reactions={
modelbase2/fit.py CHANGED
@@ -19,7 +19,14 @@ from scipy.optimize import minimize
19
19
 
20
20
  from modelbase2.integrators import DefaultIntegrator
21
21
  from modelbase2.simulator import Simulator
22
- from modelbase2.types import Array, ArrayLike, Callable, IntegratorProtocol, cast
22
+ from modelbase2.types import (
23
+ Array,
24
+ ArrayLike,
25
+ Callable,
26
+ IntegratorProtocol,
27
+ cast,
28
+ unwrap,
29
+ )
23
30
 
24
31
  __all__ = [
25
32
  "InitialGuess",
@@ -117,7 +124,7 @@ def _steady_state_residual(
117
124
  float: Root mean square error between model and data
118
125
 
119
126
  """
120
- c_ss, v_ss = (
127
+ c_ss, v_ss = unwrap(
121
128
  Simulator(
122
129
  model.update_parameters(
123
130
  dict(
@@ -132,7 +139,7 @@ def _steady_state_residual(
132
139
  integrator=integrator,
133
140
  )
134
141
  .simulate_to_steady_state()
135
- .get_full_concs_and_fluxes()
142
+ .get_result()
136
143
  )
137
144
  if c_ss is None or v_ss is None:
138
145
  return cast(float, np.inf)
@@ -171,7 +178,7 @@ def _time_course_residual(
171
178
  integrator=integrator,
172
179
  )
173
180
  .simulate_time_course(data.index) # type: ignore
174
- .get_full_concs_and_fluxes()
181
+ .get_result()
175
182
  )
176
183
  if c_ss is None or v_ss is None:
177
184
  return cast(float, np.inf)
modelbase2/fns.py CHANGED
@@ -8,6 +8,7 @@ if TYPE_CHECKING:
8
8
  from modelbase2.types import Float
9
9
 
10
10
  __all__ = [
11
+ "add",
11
12
  "constant",
12
13
  "diffusion_1s_1p",
13
14
  "div",
@@ -75,6 +76,11 @@ def twice(x: Float) -> Float:
75
76
  return x * 2
76
77
 
77
78
 
79
+ def add(x: Float, y: Float) -> Float:
80
+ """Proportional function."""
81
+ return x + y
82
+
83
+
78
84
  def proportional(x: Float, y: Float) -> Float:
79
85
  """Proportional function."""
80
86
  return x * y
@@ -22,7 +22,7 @@ with contextlib.redirect_stderr(open(os.devnull, "w")): # noqa: PTH123
22
22
  if TYPE_CHECKING:
23
23
  from collections.abc import Callable
24
24
 
25
- from modelbase2.types import ArrayLike
25
+ from modelbase2.types import Array, ArrayLike
26
26
 
27
27
 
28
28
  @dataclass
@@ -77,7 +77,7 @@ class Assimulo:
77
77
  *,
78
78
  t_end: float,
79
79
  steps: int | None = None,
80
- ) -> tuple[ArrayLike | None, ArrayLike | None]:
80
+ ) -> tuple[Array | None, ArrayLike | None]:
81
81
  """Integrate the ODE system.
82
82
 
83
83
  Args:
@@ -100,7 +100,7 @@ class Assimulo:
100
100
  self,
101
101
  *,
102
102
  time_points: ArrayLike,
103
- ) -> tuple[ArrayLike | None, ArrayLike | None]:
103
+ ) -> tuple[Array | None, ArrayLike | None]:
104
104
  """Integrate the ODE system over a time course.
105
105
 
106
106
  Args:
@@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, cast
14
14
  import numpy as np
15
15
  import scipy.integrate as spi
16
16
 
17
- from modelbase2.types import ArrayLike, Float
17
+ from modelbase2.types import Array, ArrayLike
18
18
 
19
19
  if TYPE_CHECKING:
20
20
  from collections.abc import Callable
@@ -66,7 +66,7 @@ class Scipy:
66
66
  *,
67
67
  t_end: float,
68
68
  steps: int | None = None,
69
- ) -> tuple[ArrayLike | None, ArrayLike | None]:
69
+ ) -> tuple[Array | None, ArrayLike | None]:
70
70
  """Integrate the ODE system.
71
71
 
72
72
  Args:
@@ -87,7 +87,7 @@ class Scipy:
87
87
 
88
88
  def integrate_time_course(
89
89
  self, *, time_points: ArrayLike
90
- ) -> tuple[ArrayLike | None, ArrayLike | None]:
90
+ ) -> tuple[Array | None, ArrayLike | None]:
91
91
  """Integrate the ODE system over a time course.
92
92
 
93
93
  Args:
@@ -97,7 +97,6 @@ class Scipy:
97
97
  tuple[ArrayLike, ArrayLike]: Tuple containing the time points and the integrated values.
98
98
 
99
99
  """
100
-
101
100
  y = spi.odeint(
102
101
  func=self.rhs,
103
102
  y0=self.y0,
@@ -108,7 +107,7 @@ class Scipy:
108
107
  )
109
108
  self.t0 = time_points[-1]
110
109
  self.y0 = y[-1, :]
111
- return time_points, y
110
+ return np.array(time_points, dtype=float), y
112
111
 
113
112
  def integrate_to_steady_state(
114
113
  self,
@@ -287,10 +287,12 @@ class LinearLabelMapper:
287
287
  stoichiometry = {}
288
288
  if substrate != "EXT":
289
289
  stoichiometry[substrate] = Derived(
290
- _neg_one_div, [substrate.split("__")[0]]
290
+ name=substrate, fn=_neg_one_div, args=[substrate.split("__")[0]]
291
291
  )
292
292
  if product != "EXT":
293
- stoichiometry[product] = Derived(_one_div, [product.split("__")[0]])
293
+ stoichiometry[product] = Derived(
294
+ name=product, fn=_one_div, args=[product.split("__")[0]]
295
+ )
294
296
 
295
297
  m.add_reaction(
296
298
  name=f"{rxn_name}__{i}",