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