modelbase2 0.1.78__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.78.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.78.dist-info/METADATA +0 -44
  57. modelbase2-0.1.78.dist-info/RECORD +0 -22
  58. {modelbase2-0.1.78.dist-info → modelbase2-0.2.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,656 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import inspect
5
+ import re
6
+ import textwrap
7
+ from datetime import UTC, datetime
8
+ from typing import TYPE_CHECKING, Any, cast
9
+
10
+ import dill
11
+ import libsbml
12
+ import numpy as np
13
+
14
+ from modelbase2.sbml._data import AtomicUnit, Compartment
15
+ from modelbase2.types import Derived
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable
19
+ from pathlib import Path
20
+
21
+ from modelbase2.model import Model
22
+
23
+
24
+ __all__ = [
25
+ "BINARY",
26
+ "DocstringRemover",
27
+ "IdentifierReplacer",
28
+ "NARY",
29
+ "RE_LAMBDA_ALGEBRAIC_MODULE_FUNC",
30
+ "RE_LAMBDA_FUNC",
31
+ "RE_LAMBDA_RATE_FUNC",
32
+ "RE_TO_SBML",
33
+ "SBML_DOT",
34
+ "UNARY",
35
+ "write",
36
+ ]
37
+
38
+ RE_LAMBDA_FUNC = re.compile(r".*(lambda)(.+?):(.*?)")
39
+ RE_LAMBDA_RATE_FUNC = re.compile(r".*(lambda)(.+?):(.*?),")
40
+ RE_LAMBDA_ALGEBRAIC_MODULE_FUNC = re.compile(r".*(lambda)(.+?):(.*[\(\[].+[\)\]]),")
41
+ RE_TO_SBML = re.compile(r"([^0-9_a-zA-Z])")
42
+
43
+ SBML_DOT = "__SBML_DOT__"
44
+
45
+
46
+ UNARY = {
47
+ "sqrt": libsbml.AST_FUNCTION_ROOT,
48
+ "remainder": libsbml.AST_FUNCTION_REM,
49
+ "abs": libsbml.AST_FUNCTION_ABS,
50
+ "ceil": libsbml.AST_FUNCTION_CEILING,
51
+ "sin": libsbml.AST_FUNCTION_SIN,
52
+ "cos": libsbml.AST_FUNCTION_COS,
53
+ "tan": libsbml.AST_FUNCTION_TAN,
54
+ "arcsin": libsbml.AST_FUNCTION_ARCSIN,
55
+ "arccos": libsbml.AST_FUNCTION_ARCCOS,
56
+ "arctan": libsbml.AST_FUNCTION_ARCTAN,
57
+ "sinh": libsbml.AST_FUNCTION_SINH,
58
+ "cosh": libsbml.AST_FUNCTION_COSH,
59
+ "tanh": libsbml.AST_FUNCTION_TANH,
60
+ "arcsinh": libsbml.AST_FUNCTION_ARCSINH,
61
+ "arccosh": libsbml.AST_FUNCTION_ARCCOSH,
62
+ "arctanh": libsbml.AST_FUNCTION_ARCTANH,
63
+ "log": libsbml.AST_FUNCTION_LN,
64
+ "log10": libsbml.AST_FUNCTION_LOG,
65
+ }
66
+
67
+ BINARY = {
68
+ "power": libsbml.AST_POWER,
69
+ }
70
+
71
+ NARY = {
72
+ "max": libsbml.AST_FUNCTION_MAX,
73
+ "min": libsbml.AST_FUNCTION_MIN,
74
+ }
75
+
76
+
77
+ class IdentifierReplacer(ast.NodeTransformer):
78
+ def __init__(self, mapping: dict[str, str]) -> None:
79
+ self.mapping = mapping
80
+
81
+ def visit_Name(self, node: ast.Name) -> ast.Name: # noqa: N802
82
+ return ast.Name(
83
+ id=self.mapping.get(node.id, node.id),
84
+ ctx=node.ctx,
85
+ )
86
+
87
+
88
+ class DocstringRemover(ast.NodeTransformer):
89
+ def visit_Expr(self, node: ast.Expr) -> ast.Expr | None: # noqa: N802
90
+ if isinstance(const := node.value, ast.Constant) and isinstance(
91
+ const.value, str
92
+ ):
93
+ return None
94
+ return node
95
+
96
+
97
+ def _convert_unaryop(node: ast.UnaryOp) -> libsbml.ASTNode:
98
+ operand = _convert_node(node.operand)
99
+
100
+ match node.op:
101
+ case ast.USub():
102
+ op = libsbml.AST_MINUS
103
+ case ast.Not():
104
+ op = libsbml.AST_LOGICAL_NOT
105
+ case _:
106
+ raise NotImplementedError(type(node.op))
107
+
108
+ sbml_node = libsbml.ASTNode(op)
109
+ sbml_node.addChild(operand)
110
+ return sbml_node
111
+
112
+
113
+ def _convert_binop(node: ast.BinOp) -> libsbml.ASTNode:
114
+ left = _convert_node(node.left)
115
+ right = _convert_node(node.right)
116
+
117
+ match node.op:
118
+ case ast.Mult():
119
+ op = libsbml.AST_TIMES
120
+ case ast.Add():
121
+ op = libsbml.AST_PLUS
122
+ case ast.Sub():
123
+ op = libsbml.AST_MINUS
124
+ case ast.Div():
125
+ op = libsbml.AST_DIVIDE
126
+ case ast.Pow():
127
+ op = libsbml.AST_POWER
128
+ case ast.FloorDiv():
129
+ op = libsbml.AST_FUNCTION_QUOTIENT
130
+ case _:
131
+ raise NotImplementedError(type(node.op))
132
+
133
+ sbml_node = libsbml.ASTNode(op)
134
+ sbml_node.addChild(left)
135
+ sbml_node.addChild(right)
136
+ return sbml_node
137
+
138
+
139
+ def _convert_attribute(node: ast.Attribute) -> libsbml.ASTNode:
140
+ parent = cast(ast.Name, node.value).id
141
+ attr = node.attr
142
+
143
+ if parent in ("math", "np", "numpy"):
144
+ if attr == "e":
145
+ return libsbml.ASTNode(libsbml.AST_CONSTANT_E)
146
+ if attr == "pi":
147
+ return libsbml.ASTNode(libsbml.AST_CONSTANT_PI)
148
+ if attr == "inf":
149
+ sbml_node = libsbml.ASTNode(libsbml.AST_REAL)
150
+ sbml_node.setValue(np.inf)
151
+ return sbml_node
152
+ if attr == "nan":
153
+ sbml_node = libsbml.ASTNode(libsbml.AST_REAL)
154
+ sbml_node.setValue(np.nan)
155
+ return sbml_node
156
+
157
+ msg = f"{parent}.{attr}"
158
+ raise NotImplementedError(msg)
159
+
160
+
161
+ def _convert_constant(node: ast.Constant) -> libsbml.ASTNode:
162
+ value = node.value
163
+ if isinstance(value, bool):
164
+ if value:
165
+ return libsbml.ASTNode(libsbml.AST_CONSTANT_TRUE)
166
+ return libsbml.ASTNode(libsbml.AST_CONSTANT_FALSE)
167
+
168
+ sbml_node = libsbml.ASTNode(libsbml.AST_REAL)
169
+ sbml_node.setValue(value)
170
+ return sbml_node
171
+
172
+
173
+ def _convert_ifexp(node: ast.IfExp) -> libsbml.ASTNode:
174
+ condition = _convert_node(node.test)
175
+ true = _convert_node(node.body)
176
+ false = _convert_node(node.orelse)
177
+
178
+ sbml_node = libsbml.ASTNode(libsbml.AST_FUNCTION_PIECEWISE)
179
+ sbml_node.addChild(condition)
180
+ sbml_node.addChild(true)
181
+ sbml_node.addChild(false)
182
+ return sbml_node
183
+
184
+
185
+ def _convert_direct_call(node: ast.Call) -> libsbml.ASTNode:
186
+ func = cast(ast.Name, node.func).id
187
+
188
+ if (typ := UNARY.get(func)) is not None:
189
+ sbml_node = libsbml.ASTNode(typ)
190
+ sbml_node.addChild(_convert_node(node.args[0]))
191
+ return sbml_node
192
+ if (typ := BINARY.get(func)) is not None:
193
+ sbml_node = libsbml.ASTNode(typ)
194
+ sbml_node.addChild(_convert_node(node.args[0]))
195
+ sbml_node.addChild(_convert_node(node.args[1]))
196
+ return sbml_node
197
+ if (typ := NARY.get(func)) is not None:
198
+ sbml_node = libsbml.ASTNode(typ)
199
+ for arg in node.args:
200
+ sbml_node.addChild(_convert_node(arg))
201
+ return sbml_node
202
+
203
+ # General function call
204
+ sbml_node = libsbml.ASTNode(libsbml.AST_FUNCTION)
205
+ for arg in node.args:
206
+ sbml_node.addChild(_convert_node(arg))
207
+ return sbml_node
208
+
209
+
210
+ def _convert_library_call(node: ast.Call) -> libsbml.ASTNode:
211
+ func = cast(ast.Attribute, node.func)
212
+ parent = cast(ast.Name, func.value).id
213
+ attr = func.attr
214
+
215
+ if parent in ("math", "np", "numpy"):
216
+ if (typ := UNARY.get(attr)) is not None:
217
+ sbml_node = libsbml.ASTNode(typ)
218
+ sbml_node.addChild(_convert_node(node.args[0]))
219
+ return sbml_node
220
+ if (typ := BINARY.get(attr)) is not None:
221
+ sbml_node = libsbml.ASTNode(typ)
222
+ sbml_node.addChild(_convert_node(node.args[0]))
223
+ sbml_node.addChild(_convert_node(node.args[1]))
224
+ return sbml_node
225
+ if (typ := NARY.get(attr)) is not None:
226
+ sbml_node = libsbml.ASTNode(typ)
227
+ for arg in node.args:
228
+ sbml_node.addChild(_convert_node(arg))
229
+ return sbml_node
230
+
231
+ # General library call
232
+ sbml_node = libsbml.ASTNode(libsbml.AST_FUNCTION)
233
+ for arg in node.args:
234
+ sbml_node.addChild(_convert_node(arg))
235
+ return sbml_node
236
+
237
+
238
+ def _convert_call(node: ast.Call) -> libsbml.ASTNode:
239
+ func = node.func
240
+ if isinstance(func, ast.Name):
241
+ return _convert_direct_call(node)
242
+ if isinstance(func, ast.Attribute):
243
+ return _convert_library_call(node)
244
+
245
+ msg = f"Unknown call type: {type(func)}"
246
+ raise NotImplementedError(msg)
247
+
248
+
249
+ def _convert_compare(node: ast.Compare) -> libsbml.ASTNode:
250
+ # FIXME: handle cases such as x < y < z
251
+
252
+ left = _convert_node(node.left)
253
+ right = _convert_node(node.comparators[0])
254
+
255
+ match node.ops[0]:
256
+ case ast.Eq():
257
+ op = libsbml.AST_RELATIONAL_EQ
258
+ case ast.NotEq():
259
+ op = libsbml.AST_RELATIONAL_NEQ
260
+ case ast.Lt():
261
+ op = libsbml.AST_RELATIONAL_LT
262
+ case ast.LtE():
263
+ op = libsbml.AST_RELATIONAL_LEQ
264
+ case ast.Gt():
265
+ op = libsbml.AST_RELATIONAL_GT
266
+ case ast.GtE():
267
+ op = libsbml.AST_RELATIONAL_GEQ
268
+ case _:
269
+ raise NotImplementedError(type(node.ops[0]))
270
+
271
+ sbml_node = libsbml.ASTNode(op)
272
+ sbml_node.addChild(left)
273
+ sbml_node.addChild(right)
274
+ return sbml_node
275
+
276
+
277
+ def _convert_node(node: ast.stmt | ast.expr) -> libsbml.ASTNode:
278
+ match node:
279
+ case ast.Return(value):
280
+ if value is None:
281
+ msg = "Model function cannot return `None`"
282
+ raise ValueError(msg)
283
+ return _convert_node(value)
284
+ case ast.UnaryOp():
285
+ return _convert_unaryop(node)
286
+ case ast.BinOp():
287
+ return _convert_binop(node)
288
+ case ast.Name(id):
289
+ sbml_node = libsbml.ASTNode(libsbml.AST_NAME)
290
+ sbml_node.setName(id)
291
+ return sbml_node
292
+ case ast.Constant():
293
+ return _convert_constant(node)
294
+ case ast.Attribute():
295
+ return _convert_attribute(node)
296
+ case ast.IfExp():
297
+ return _convert_ifexp(node)
298
+ case ast.Call():
299
+ return _convert_call(node)
300
+ case ast.Compare():
301
+ return _convert_compare(node)
302
+ case _:
303
+ raise NotImplementedError(type(node))
304
+
305
+
306
+ def _handle_body(stmts: list[ast.stmt]) -> libsbml.ASTNode:
307
+ code = libsbml.ASTNode()
308
+ for stmt in stmts:
309
+ code = _convert_node(stmt)
310
+ return code
311
+
312
+
313
+ def _tree_to_sbml(
314
+ tree: ast.FunctionDef, args: list[str] | None = None
315
+ ) -> libsbml.ASTNode:
316
+ DocstringRemover().visit(tree)
317
+ if args is not None:
318
+ fn_args = [i.arg for i in tree.args.args]
319
+ argmap = dict(zip(fn_args, args, strict=True))
320
+ IdentifierReplacer(argmap).visit(tree)
321
+ return _handle_body(tree.body)
322
+
323
+
324
+ def _sbmlify_fn(fn: Callable, user_args: list[str]) -> libsbml.ASTNode:
325
+ try:
326
+ source = inspect.getsource(fn)
327
+ except OSError: # could not get source code
328
+ source = dill.source.getsource(fn)
329
+
330
+ tree = ast.parse(textwrap.dedent(source))
331
+ if not isinstance(fn_def := tree.body[0], ast.FunctionDef):
332
+ msg = "Not a function"
333
+ raise TypeError(msg)
334
+
335
+ return _tree_to_sbml(fn_def, args=user_args)
336
+
337
+
338
+ ##########################################################################
339
+ # SBML functions
340
+ ##########################################################################
341
+
342
+
343
+ def _escape_non_alphanumeric(re_sub: Any) -> str:
344
+ """Convert a non-alphanumeric charactor to a string representation of its ascii number."""
345
+ return f"__{ord(re_sub.group(0))}__"
346
+
347
+
348
+ def _convert_id_to_sbml(id_: str, prefix: str) -> str:
349
+ """Add prefix if id startswith number."""
350
+ new_id = RE_TO_SBML.sub(_escape_non_alphanumeric, id_).replace(".", SBML_DOT)
351
+ if not new_id[0].isalpha():
352
+ return f"{prefix}_{new_id}"
353
+ return new_id
354
+
355
+
356
+ def _create_sbml_document() -> libsbml.SBMLDocument:
357
+ """Create an sbml document, into which sbml information can be written.
358
+
359
+ Returns:
360
+ doc : libsbml.Document
361
+
362
+ """
363
+ # SBML namespaces
364
+ sbml_ns = libsbml.SBMLNamespaces(3, 2)
365
+ sbml_ns.addPackageNamespace("fbc", 2)
366
+ # SBML document
367
+ doc = libsbml.SBMLDocument(sbml_ns)
368
+ doc.setPackageRequired("fbc", flag=False)
369
+ doc.setSBOTerm("SBO:0000004")
370
+ return doc
371
+
372
+
373
+ def _create_sbml_model(
374
+ *,
375
+ model_name: str,
376
+ doc: libsbml.SBMLDocument,
377
+ extent_units: str,
378
+ substance_units: str,
379
+ time_units: str,
380
+ ) -> libsbml.Model:
381
+ """Create an sbml model.
382
+
383
+ Args:
384
+ model_name: Name of the model.
385
+ doc: libsbml.Document
386
+ extent_units: Units for the extent of reactions.
387
+ substance_units: Units for the amount of substances.
388
+ time_units: Units for time.
389
+
390
+ Returns:
391
+ sbml_model : libsbml.Model
392
+
393
+ """
394
+ name = f"{model_name}_{datetime.now(UTC).date().strftime('%Y-%m-%d')}"
395
+ sbml_model = doc.createModel()
396
+ sbml_model.setId(_convert_id_to_sbml(id_=name, prefix="MODEL"))
397
+ sbml_model.setName(_convert_id_to_sbml(id_=name, prefix="MODEL"))
398
+ sbml_model.setTimeUnits(time_units)
399
+ sbml_model.setExtentUnits(extent_units)
400
+ sbml_model.setSubstanceUnits(substance_units)
401
+ sbml_model_fbc = sbml_model.getPlugin("fbc")
402
+ sbml_model_fbc.setStrict(True)
403
+ return sbml_model
404
+
405
+
406
+ def _create_sbml_units(
407
+ *,
408
+ units: dict[str, AtomicUnit],
409
+ sbml_model: libsbml.Model,
410
+ ) -> None:
411
+ """Create sbml units out of the meta_info.
412
+
413
+ Args:
414
+ units: Dictionary of units to use in the SBML file.
415
+ sbml_model : libsbml Model
416
+
417
+ """
418
+ for unit_id, unit in units.items():
419
+ sbml_definition = sbml_model.createUnitDefinition()
420
+ sbml_definition.setId(unit_id)
421
+ sbml_unit = sbml_definition.createUnit()
422
+ sbml_unit.setKind(unit.kind)
423
+ sbml_unit.setExponent(unit.exponent)
424
+ sbml_unit.setScale(unit.scale)
425
+ sbml_unit.setMultiplier(unit.multiplier)
426
+
427
+
428
+ def _create_sbml_compartments(
429
+ *,
430
+ compartments: dict[str, Compartment],
431
+ sbml_model: libsbml.Model,
432
+ ) -> None:
433
+ for compartment_id, compartment in compartments.items():
434
+ sbml_compartment = sbml_model.createCompartment()
435
+ sbml_compartment.setId(compartment_id)
436
+ sbml_compartment.setName(compartment.name)
437
+ sbml_compartment.setConstant(compartment.is_constant)
438
+ sbml_compartment.setSize(compartment.size)
439
+ sbml_compartment.setSpatialDimensions(compartment.dimensions)
440
+ sbml_compartment.setUnits(compartment.units)
441
+
442
+
443
+ def _create_sbml_variables(
444
+ *,
445
+ model: Model,
446
+ sbml_model: libsbml.Model,
447
+ ) -> None:
448
+ """Create the variables for the sbml model.
449
+
450
+ Args:
451
+ model: Model instance to export.
452
+ sbml_model : libsbml.Model
453
+
454
+ """
455
+ for name, value in model.variables.items():
456
+ cpd = sbml_model.createSpecies()
457
+ cpd.setId(_convert_id_to_sbml(id_=name, prefix="CPD"))
458
+
459
+ cpd.setConstant(False)
460
+ cpd.setBoundaryCondition(False)
461
+ cpd.setHasOnlySubstanceUnits(False)
462
+ cpd.setInitialAmount(float(value))
463
+
464
+
465
+ def _create_sbml_derived_variables(*, model: Model, sbml_model: libsbml.Model) -> None:
466
+ for name, dv in model.derived_variables.items():
467
+ sbml_ar = sbml_model.createAssignmentRule()
468
+ sbml_ar.setId(_convert_id_to_sbml(id_=name, prefix="AR"))
469
+ sbml_ar.setName(_convert_id_to_sbml(id_=name, prefix="AR"))
470
+ sbml_ar.setVariable(_convert_id_to_sbml(id_=name, prefix="AR"))
471
+ sbml_ar.setMath(_sbmlify_fn(dv.fn, dv.args))
472
+
473
+
474
+ def _create_derived_parameter(
475
+ sbml_model: libsbml.Model,
476
+ name: str,
477
+ dp: Derived,
478
+ ) -> None:
479
+ """Create a derived parameter for the sbml model."""
480
+ ar = sbml_model.createAssignmentRule()
481
+ ar.setId(_convert_id_to_sbml(id_=name, prefix="AR"))
482
+ ar.setName(_convert_id_to_sbml(id_=name, prefix="AR"))
483
+ ar.setVariable(_convert_id_to_sbml(id_=name, prefix="AR"))
484
+ ar.setMath(_sbmlify_fn(dp.fn, dp.args))
485
+
486
+
487
+ def _create_sbml_parameters(
488
+ *,
489
+ model: Model,
490
+ sbml_model: libsbml.Model,
491
+ ) -> None:
492
+ """Create the parameters for the sbml model.
493
+
494
+ Args:
495
+ model: Model instance to export.
496
+ sbml_model : libsbml.Model
497
+
498
+ """
499
+ for parameter_id, value in model.parameters.items():
500
+ k = sbml_model.createParameter()
501
+ k.setId(_convert_id_to_sbml(id_=parameter_id, prefix="PAR"))
502
+ k.setConstant(True)
503
+ k.setValue(float(value))
504
+
505
+
506
+ def _create_sbml_derived_parameters(*, model: Model, sbml_model: libsbml.Model) -> None:
507
+ for name, dp in model.derived_parameters.items():
508
+ _create_derived_parameter(sbml_model, name, dp)
509
+
510
+
511
+ def _create_sbml_reactions(
512
+ *,
513
+ model: Model,
514
+ sbml_model: libsbml.Model,
515
+ ) -> None:
516
+ """Create the reactions for the sbml model."""
517
+ for name, rxn in model.reactions.items():
518
+ sbml_rxn = sbml_model.createReaction()
519
+ sbml_rxn.setId(_convert_id_to_sbml(id_=name, prefix="RXN"))
520
+ sbml_rxn.setName(name)
521
+ sbml_rxn.setFast(False)
522
+
523
+ for compound_id, factor in rxn.stoichiometry.items():
524
+ match factor:
525
+ case float() | int():
526
+ sref = (
527
+ sbml_rxn.createReactant()
528
+ if factor < 0
529
+ else sbml_rxn.createProduct()
530
+ )
531
+ sref.setSpecies(_convert_id_to_sbml(id_=compound_id, prefix="CPD"))
532
+ sref.setStoichiometry(abs(factor))
533
+ sref.setConstant(False)
534
+ case Derived():
535
+ # SBML uses species references for derived stoichiometries
536
+ # So we need to create a assignment rule and then refer to it
537
+ reference = f"{compound_id}ref"
538
+ _create_derived_parameter(sbml_model, reference, factor)
539
+
540
+ sref = sbml_rxn.createReactant()
541
+ sref.setId(_convert_id_to_sbml(id_=reference, prefix="CPD"))
542
+ sref.setSpecies(_convert_id_to_sbml(id_=compound_id, prefix="CPD"))
543
+ case _:
544
+ msg = f"Stoichiometry type {type(factor)} not supported"
545
+ raise NotImplementedError(msg)
546
+ for compound_id in rxn.get_modifiers(model):
547
+ sref = sbml_rxn.createModifier()
548
+ sref.setSpecies(_convert_id_to_sbml(id_=compound_id, prefix="CPD"))
549
+
550
+ sbml_rxn.createKineticLaw().setMath(_sbmlify_fn(rxn.fn, rxn.args))
551
+
552
+
553
+ def _model_to_sbml(
554
+ model: Model,
555
+ *,
556
+ model_name: str,
557
+ units: dict[str, AtomicUnit],
558
+ extent_units: str,
559
+ substance_units: str,
560
+ time_units: str,
561
+ compartments: dict[str, Compartment],
562
+ ) -> libsbml.SBMLDocument:
563
+ """Export model to sbml."""
564
+ doc = _create_sbml_document()
565
+ sbml_model = _create_sbml_model(
566
+ model_name=model_name,
567
+ doc=doc,
568
+ extent_units=extent_units,
569
+ substance_units=substance_units,
570
+ time_units=time_units,
571
+ )
572
+ _create_sbml_units(units=units, sbml_model=sbml_model)
573
+ _create_sbml_compartments(compartments=compartments, sbml_model=sbml_model)
574
+ # Actual model components
575
+ _create_sbml_parameters(model=model, sbml_model=sbml_model)
576
+ _create_sbml_derived_parameters(model=model, sbml_model=sbml_model)
577
+ _create_sbml_variables(model=model, sbml_model=sbml_model)
578
+ _create_sbml_derived_variables(model=model, sbml_model=sbml_model)
579
+ _create_sbml_reactions(model=model, sbml_model=sbml_model)
580
+ return doc
581
+
582
+
583
+ def _default_compartments(
584
+ compartments: dict[str, Compartment] | None,
585
+ ) -> dict[str, Compartment]:
586
+ if compartments is None:
587
+ return {
588
+ "c": Compartment(
589
+ name="cytosol",
590
+ dimensions=3,
591
+ size=1,
592
+ units="litre",
593
+ is_constant=True,
594
+ )
595
+ }
596
+ return compartments
597
+
598
+
599
+ def _default_model_name(model_name: str | None) -> str:
600
+ if model_name is None:
601
+ return "model"
602
+ return model_name
603
+
604
+
605
+ def _default_units(units: dict[str, AtomicUnit] | None) -> dict[str, AtomicUnit]:
606
+ if units is None:
607
+ return {
608
+ "per_second": AtomicUnit(
609
+ kind=libsbml.UNIT_KIND_SECOND,
610
+ exponent=-1,
611
+ scale=0,
612
+ multiplier=1,
613
+ )
614
+ }
615
+ return units
616
+
617
+
618
+ def write(
619
+ model: Model,
620
+ file: Path,
621
+ *,
622
+ model_name: str | None = None,
623
+ units: dict[str, AtomicUnit] | None = None,
624
+ compartments: dict[str, Compartment] | None = None,
625
+ extent_units: str = "mole",
626
+ substance_units: str = "mole",
627
+ time_units: str = "second",
628
+ ) -> Path:
629
+ """Export a metabolic model to an SBML file.
630
+
631
+ Args:
632
+ model: Model instance to export.
633
+ file: Name of the SBML file to create.
634
+ model_name: Name of the model.
635
+ units: Dictionary of units to use in the SBML file (default: None).
636
+ compartments: Dictionary of compartments to use in the SBML file (default: None).
637
+ extent_units: Units for the extent of reactions (default: "mole").
638
+ substance_units: Units for the amount of substances (default: "mole").
639
+ time_units: Units for time (default: "second").
640
+
641
+ Returns:
642
+ str | None: None if the export is successful.
643
+
644
+ """
645
+ doc = _model_to_sbml(
646
+ model=model,
647
+ model_name=_default_model_name(model_name),
648
+ units=_default_units(units),
649
+ extent_units=extent_units,
650
+ substance_units=substance_units,
651
+ time_units=time_units,
652
+ compartments=_default_compartments(compartments),
653
+ )
654
+
655
+ libsbml.writeSBMLToFile(doc, str(file))
656
+ return file