mxlpy 0.22.0__py3-none-any.whl → 0.23.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.
mxlpy/sbml/_import.py CHANGED
@@ -1,582 +1,104 @@
1
1
  from __future__ import annotations
2
2
 
3
- import logging
4
- import math # noqa: F401 # models might need it
5
3
  import re
6
- from collections import defaultdict
7
- from dataclasses import dataclass, field
8
- from pathlib import Path
9
- from typing import TYPE_CHECKING, Self
4
+ import sys
5
+ import unicodedata
6
+ from importlib import util
7
+ from typing import TYPE_CHECKING
10
8
 
11
- import libsbml
12
- import numpy as np # noqa: F401 # models might need it
9
+ import pysbml
13
10
  import sympy
14
11
 
15
- from mxlpy.model import Dependency, Model, _sort_dependencies
16
- from mxlpy.paths import default_tmp_dir
17
- from mxlpy.sbml._data import (
18
- AtomicUnit,
19
- Compartment,
20
- CompositeUnit,
21
- Compound,
22
- Derived,
23
- Function,
24
- Parameter,
25
- Reaction,
12
+ from mxlpy.meta.codegen_mxlpy import (
13
+ SymbolicFn,
14
+ SymbolicParameter,
15
+ SymbolicReaction,
16
+ SymbolicRepr,
17
+ SymbolicVariable,
18
+ generate_mxlpy_code_from_symbolic_repr,
26
19
  )
27
- from mxlpy.sbml._mathml import parse_sbml_math
28
- from mxlpy.sbml._name_conversion import _name_to_py
29
- from mxlpy.sbml._unit_conversion import get_operator_mappings, get_unit_conversion
20
+ from mxlpy.paths import default_tmp_dir
30
21
  from mxlpy.types import unwrap
31
22
 
23
+ __all__ = ["free_symbols", "import_from_path", "read", "valid_filename"]
24
+
32
25
  if TYPE_CHECKING:
33
26
  from collections.abc import Callable
27
+ from pathlib import Path
34
28
 
35
- _LOGGER = logging.getLogger(__name__)
36
-
37
- __all__ = [
38
- "INDENT",
39
- "OPERATOR_MAPPINGS",
40
- "Parser",
41
- "UNIT_CONVERSION",
42
- "import_from_path",
43
- "read",
44
- "valid_filename",
45
- ]
46
-
47
- UNIT_CONVERSION = get_unit_conversion()
48
- OPERATOR_MAPPINGS = get_operator_mappings()
49
- INDENT = " "
50
-
51
-
52
- def _nan_to_zero(value: float) -> float:
53
- return 0 if str(value) == "nan" else value
54
-
55
-
56
- @dataclass(slots=True)
57
- class Parser:
58
- # Collections
59
- boundary_species: set[str] = field(default_factory=set)
60
- # Parsed stuff
61
- atomic_units: dict[str, AtomicUnit] = field(default_factory=dict)
62
- composite_units: dict[str, CompositeUnit] = field(default_factory=dict)
63
- compartments: dict[str, Compartment] = field(default_factory=dict)
64
- parameters: dict[str, Parameter] = field(default_factory=dict)
65
- variables: dict[str, Compound] = field(default_factory=dict)
66
- derived: dict[str, Derived] = field(default_factory=dict)
67
- initial_assignment: dict[str, Derived] = field(default_factory=dict)
68
- functions: dict[str, Function] = field(default_factory=dict)
69
- reactions: dict[str, Reaction] = field(default_factory=dict)
70
-
71
- def parse(self, file: str | Path) -> Self:
72
- if not Path(file).exists():
73
- msg = "Model file does not exist"
74
- raise OSError(msg)
75
- doc = libsbml.readSBMLFromFile(str(file))
76
-
77
- # Check for unsupported packages
78
- for i in range(doc.num_plugins):
79
- if doc.getPlugin(i).getPackageName() == "comp":
80
- msg = "No support for comp package"
81
- raise NotImplementedError(msg)
82
-
83
- sbml_model = doc.getModel()
84
- if sbml_model is None:
85
- return self
86
-
87
- if bool(sbml_model.getConversionFactor()):
88
- msg = "Conversion factors are currently not supported"
89
- raise NotImplementedError(msg)
90
-
91
- self.parse_functions(sbml_model)
92
- self.parse_units(sbml_model)
93
- self.parse_compartments(sbml_model)
94
- self.parse_variables(sbml_model)
95
- self.parse_parameters(sbml_model)
96
- self.parse_initial_assignments(sbml_model)
97
- self.parse_rules(sbml_model)
98
- self.parse_constraints(sbml_model)
99
- self.parse_reactions(sbml_model)
100
- self.parse_events(sbml_model)
101
-
102
- # Modifications
103
- self._convert_substance_amount_to_concentration()
104
- # self._rename_species_references()
105
- return self
106
-
107
- ###############################################################################
108
- # PARSING STAGE
109
- ###############################################################################
110
-
111
- def parse_constraints(self, sbml_model: libsbml.Model) -> None:
112
- if len(sbml_model.getListOfConstraints()) > 0:
113
- msg = "mxlpy does not support model constraints. "
114
- raise NotImplementedError(msg)
115
-
116
- def parse_events(self, sbml_model: libsbml.Model) -> None:
117
- if len(sbml_model.getListOfEvents()) > 0:
118
- msg = (
119
- "mxlpy does not current support events. "
120
- "Check the file for how to integrate properly."
121
- )
122
- raise NotImplementedError(msg)
123
-
124
- def parse_units(self, sbml_model: libsbml.Model) -> None:
125
- for unit_definition in sbml_model.getListOfUnitDefinitions():
126
- composite_id = unit_definition.getId()
127
- local_units = []
128
- for unit in unit_definition.getListOfUnits():
129
- atomic_unit = AtomicUnit(
130
- kind=UNIT_CONVERSION[unit.getKind()],
131
- scale=unit.getScale(),
132
- exponent=unit.getExponent(),
133
- multiplier=unit.getMultiplier(),
134
- )
135
- local_units.append(atomic_unit.kind)
136
- self.atomic_units[atomic_unit.kind] = atomic_unit
137
- self.composite_units[composite_id] = CompositeUnit(
138
- sbml_id=composite_id,
139
- units=local_units,
140
- )
141
-
142
- def parse_compartments(self, sbml_model: libsbml.Model) -> None:
143
- for compartment in sbml_model.getListOfCompartments():
144
- sbml_id = _name_to_py(compartment.getId())
145
- size = compartment.getSize()
146
- if str(size) == "nan":
147
- size = 0
148
- self.compartments[sbml_id] = Compartment(
149
- name=compartment.getName(),
150
- dimensions=compartment.getSpatialDimensions(),
151
- size=size,
152
- units=compartment.getUnits(),
153
- is_constant=compartment.getConstant(),
154
- )
155
-
156
- def parse_parameters(self, sbml_model: libsbml.Model) -> None:
157
- for parameter in sbml_model.getListOfParameters():
158
- self.parameters[_name_to_py(parameter.getId())] = Parameter(
159
- value=_nan_to_zero(parameter.getValue()),
160
- is_constant=parameter.getConstant(),
161
- )
162
-
163
- def parse_variables(self, sbml_model: libsbml.Model) -> None:
164
- for compound in sbml_model.getListOfSpecies():
165
- compound_id = _name_to_py(compound.getId())
166
- if bool(compound.getConversionFactor()):
167
- msg = "Conversion factors are not implemented. "
168
- raise NotImplementedError(msg)
169
-
170
- # NOTE: What the shit is this?
171
- initial_amount = compound.getInitialAmount()
172
- if str(initial_amount) == "nan":
173
- initial_amount = compound.getInitialConcentration()
174
- is_concentration = str(initial_amount) != "nan"
175
- else:
176
- is_concentration = False
177
-
178
- has_boundary_condition = compound.getBoundaryCondition()
179
- if has_boundary_condition:
180
- self.boundary_species.add(compound_id)
181
-
182
- self.variables[compound_id] = Compound(
183
- compartment=compound.getCompartment(),
184
- initial_amount=_nan_to_zero(initial_amount),
185
- substance_units=compound.getSubstanceUnits(),
186
- has_only_substance_units=compound.getHasOnlySubstanceUnits(),
187
- has_boundary_condition=has_boundary_condition,
188
- is_constant=compound.getConstant(),
189
- is_concentration=is_concentration,
190
- )
191
-
192
- def parse_functions(self, sbml_model: libsbml.Model) -> None:
193
- for func in sbml_model.getListOfFunctionDefinitions():
194
- func_name = func.getName()
195
- sbml_id = func.getId()
196
- if sbml_id is None or sbml_id == "":
197
- sbml_id = func_name
198
- elif func_name is None or func_name == "":
199
- func_name = sbml_id
200
- func_name = _name_to_py(func_name)
201
-
202
- if (node := func.getMath()) is None:
203
- continue
204
- body, args = parse_sbml_math(node=node)
205
-
206
- self.functions[func_name] = Function(
207
- body=body,
208
- args=args,
209
- )
210
-
211
- ###############################################################################
212
- # Different kinds of derived values
213
- ###############################################################################
214
-
215
- def parse_initial_assignments(self, sbml_model: libsbml.Model) -> None:
216
- for assignment in sbml_model.getListOfInitialAssignments():
217
- name = _name_to_py(assignment.getSymbol())
218
-
219
- node = assignment.getMath()
220
- if node is None:
221
- msg = f"Unusable math for {name}"
222
- _LOGGER.warning(msg)
223
- continue
224
-
225
- body, args = parse_sbml_math(node)
226
- self.initial_assignment[name] = Derived(
227
- body=body,
228
- args=args,
229
- )
230
-
231
- def _parse_algebraic_rule(self, rule: libsbml.AlgebraicRule) -> None:
232
- msg = f"Algebraic rules are not implemented for {rule.getId()}"
233
- raise NotImplementedError(msg)
234
-
235
- def _parse_assignment_rule(self, rule: libsbml.AssignmentRule) -> None:
236
- if (node := rule.getMath()) is None:
237
- return
238
-
239
- name: str = _name_to_py(rule.getId())
240
- body, args = parse_sbml_math(node=node)
241
-
242
- self.derived[name] = Derived(
243
- body=body,
244
- args=args,
245
- )
246
-
247
- def _parse_rate_rule(self, rule: libsbml.RateRule) -> None:
248
- msg = f"Skipping rate rule {rule.getId()}"
249
- raise NotImplementedError(msg)
250
-
251
- def parse_rules(self, sbml_model: libsbml.Model) -> None:
252
- """Parse rules and separate them by type."""
253
- for rule in sbml_model.getListOfRules():
254
- if rule.element_name == "algebraicRule":
255
- self._parse_algebraic_rule(rule=rule)
256
- elif rule.element_name == "assignmentRule":
257
- self._parse_assignment_rule(rule=rule)
258
- elif rule.element_name == "rateRule":
259
- self._parse_rate_rule(rule=rule)
260
- else:
261
- msg = "Unknown rate type"
262
- raise ValueError(msg)
29
+ from mxlpy.model import Model
263
30
 
264
- def _parse_local_parameters(
265
- self, reaction_id: str, kinetic_law: libsbml.KineticLaw
266
- ) -> dict[str, str]:
267
- """Parse local parameters."""
268
- parameters_to_update = {}
269
- for parameter in kinetic_law.getListOfLocalParameters():
270
- old_id = _name_to_py(parameter.getId())
271
- if old_id in self.parameters:
272
- new_id = f"{reaction_id}__{old_id}"
273
- parameters_to_update[old_id] = new_id
274
- else:
275
- new_id = old_id
276
- self.parameters[new_id] = Parameter(
277
- value=_nan_to_zero(parameter.getValue()),
278
- is_constant=parameter.getConstant(),
279
- )
280
- # Some models apparently also write local parameters in this
281
- for parameter in kinetic_law.getListOfParameters():
282
- old_id = _name_to_py(parameter.getId())
283
- if old_id in self.parameters:
284
- new_id = f"{reaction_id}__{old_id}"
285
- parameters_to_update[old_id] = new_id
286
- else:
287
- new_id = old_id
288
- self.parameters[new_id] = Parameter(
289
- value=_nan_to_zero(parameter.getValue()),
290
- is_constant=parameter.getConstant(),
291
- )
292
- return parameters_to_update
293
-
294
- def parse_reactions(self, sbml_model: libsbml.Model) -> None:
295
- for reaction in sbml_model.getListOfReactions():
296
- sbml_id = _name_to_py(reaction.getId())
297
- kinetic_law = reaction.getKineticLaw()
298
- if kinetic_law is None:
299
- continue
300
- parameters_to_update = self._parse_local_parameters(
301
- reaction_id=sbml_id,
302
- kinetic_law=kinetic_law,
303
- )
304
-
305
- node = reaction.getKineticLaw().getMath()
306
- # FIXME: convert substance amount to concentration here
307
- body, args = parse_sbml_math(node=node)
308
-
309
- # Update parameter references
310
- for old, new in parameters_to_update.items():
311
- pat = re.compile(f"({old})" + r"\b")
312
- body = pat.sub(new, body)
313
31
 
314
- for i, arg in enumerate(args):
315
- args[i] = parameters_to_update.get(arg, arg)
316
-
317
- dynamic_stoichiometry: dict[str, str] = {}
318
- parsed_reactants: defaultdict[str, int] = defaultdict(int)
319
- for substrate in reaction.getListOfReactants():
320
- species = _name_to_py(substrate.getSpecies())
321
- if (ref := substrate.getId()) != "":
322
- dynamic_stoichiometry[species] = ref
323
- self.parameters[ref] = Parameter(0.0, is_constant=False)
324
-
325
- elif species not in self.boundary_species:
326
- factor = substrate.getStoichiometry()
327
- if str(factor) == "nan":
328
- msg = f"Cannot parse stoichiometry: {factor}"
329
- raise ValueError(msg)
330
- parsed_reactants[species] -= factor
331
-
332
- parsed_products: defaultdict[str, int] = defaultdict(int)
333
- for product in reaction.getListOfProducts():
334
- species = _name_to_py(product.getSpecies())
335
- if (ref := product.getId()) != "":
336
- dynamic_stoichiometry[species] = ref
337
- self.parameters[ref] = Parameter(0.0, is_constant=False)
338
- elif species not in self.boundary_species:
339
- factor = product.getStoichiometry()
340
- if str(factor) == "nan":
341
- msg = f"Cannot parse stoichiometry: {factor}"
342
- raise ValueError(msg)
343
- parsed_products[species] += factor
344
-
345
- # Combine stoichiometries
346
- # Hint: you can't just combine the dictionaries, as you have cases like
347
- # S1 + S2 -> 2S2, which have to be combined to S1 -> S2
348
- stoichiometries = dict(parsed_reactants)
349
- for species, value in parsed_products.items():
350
- if species in stoichiometries:
351
- stoichiometries[species] = stoichiometries[species] + value
352
- else:
353
- stoichiometries[species] = value
354
-
355
- self.reactions[sbml_id] = Reaction(
356
- body=body,
357
- stoichiometry=stoichiometries | dynamic_stoichiometry,
358
- args=args,
359
- )
32
+ def free_symbols(expr: sympy.Expr) -> list[str]:
33
+ return [i.name for i in expr.free_symbols if isinstance(i, sympy.Symbol)]
360
34
 
361
- def _convert_substance_amount_to_concentration(
362
- self,
363
- ) -> None:
364
- """Convert substance amount to concentration if has_only_substance_units is false.
365
35
 
366
- The compounds in the test are supplied in mole if has_only_substance_units is false.
367
- In that case, the reaction equation has to be reformed like this:
368
- k1 * S1 * compartment -> k1 * S1
369
- or in other words the species have to be divided by the compartment to get
370
- concentration units.
371
- """
372
- for reaction in self.reactions.values():
373
- function_body = reaction.body
374
- removed_compartments = set()
375
- for arg in reaction.args:
376
- # the parsed species part is important to not
377
- # introduce conversion on things that aren't species
378
- if (species := self.variables.get(arg, None)) is None:
379
- continue
380
-
381
- if not species.has_only_substance_units:
382
- compartment = species.compartment
383
- if compartment is not None:
384
- if self.compartments[compartment].dimensions == 0:
385
- continue
386
- if species.is_concentration:
387
- if compartment not in removed_compartments:
388
- pattern = f"({compartment})" + r"\b"
389
- repl = f"({compartment} / {compartment})"
390
- function_body = re.sub(pattern, repl, function_body)
391
- removed_compartments.add(compartment)
392
- else:
393
- # \b is word boundary
394
- pattern = f"({arg})" + r"\b"
395
- repl = f"({arg} / {compartment})"
396
- function_body = re.sub(pattern, repl, function_body)
397
- if compartment not in reaction.args:
398
- reaction.args.append(compartment)
399
-
400
- # Simplify the function
401
- try:
402
- reaction.body = str(sympy.parse_expr(function_body))
403
- except AttributeError:
404
- # E.g. when math.factorial is called
405
- # FIXME: do the sympy conversion before?
406
- reaction.body = function_body
407
-
408
-
409
- def _handle_fn(name: str, body: str, args: list[str]) -> Callable[..., float]:
410
- func_args = ", ".join(args)
411
- func_str = "\n".join(
412
- [
413
- f"def {name}({func_args}):",
414
- f"{INDENT}return {body}",
415
- "",
416
- ]
417
- )
418
- try:
419
- exec(func_str, globals(), None) # noqa: S102
420
- except SyntaxError as e:
421
- msg = f"Invalid function definition: {func_str}"
422
- raise SyntaxError(msg) from e
423
- python_func = globals()[name]
424
- python_func.__source__ = func_str
425
- return python_func # type: ignore
426
-
427
-
428
- def _codegen_fn(name: str, body: str, args: list[str]) -> str:
429
- func_args = ", ".join(args)
430
- return "\n".join(
431
- [
432
- f"def {name}({func_args}):",
433
- f"{INDENT}return {body}",
434
- "",
435
- ]
436
- )
437
-
438
-
439
- def _codegen_initial_assignment(
36
+ def _transform_stoichiometry(
440
37
  k: str,
441
- sbml: Parser,
442
- parameters: dict[str, float],
443
- ) -> str:
444
- if k in parameters:
445
- return f"m.update_parameter('{k}', _init_{k}(*(args[i] for i in {sbml.initial_assignment[k].args})) )"
446
-
447
- ass = sbml.initial_assignment[k]
448
- fn = f"_init_{k}(*(args[i] for i in {ass.args}))"
449
-
450
- if (species := sbml.variables.get(k)) is not None:
451
- compartment = species.compartment
452
- if compartment is not None and (
453
- not species.is_concentration
454
- or (species.has_only_substance_units and species.is_concentration)
455
- ):
456
- size = 1 if (c := sbml.compartments.get(compartment)) is None else c.size
457
- if size != 0:
458
- fn = f"{fn} * {size}"
459
- elif k in sbml.compartments:
460
- pass
461
- else:
462
- msg = "Initial assignment targeting unknown type"
463
- raise NotImplementedError(msg)
464
-
465
- return f"m.update_variable('{k}', {fn})"
466
-
467
-
468
- def _codgen(name: str, sbml: Parser) -> Path:
469
- import itertools as it
470
-
471
- functions = {
472
- k: _codegen_fn(k, body=v.body, args=v.args)
473
- for k, v in it.chain(
474
- sbml.functions.items(),
475
- sbml.derived.items(),
476
- sbml.reactions.items(),
477
- {f"_init_{k}": v for k, v in sbml.initial_assignment.items()}.items(),
38
+ v: pysbml.transform.data.Expr,
39
+ ) -> SymbolicFn | str | sympy.Float:
40
+ if isinstance(v, sympy.Float):
41
+ return v
42
+ if isinstance(v, sympy.Symbol):
43
+ return v.name
44
+
45
+ return SymbolicFn(k, expr=v, args=free_symbols(v))
46
+
47
+
48
+ def _codegen(name: str, model: pysbml.transform.data.Model) -> Path:
49
+ sym = SymbolicRepr()
50
+ for key, var in model.variables.items():
51
+ sym.variables[key] = SymbolicVariable(
52
+ value=var.value,
53
+ unit=var.unit,
478
54
  )
479
- }
480
-
481
- parameters = {
482
- k: v.value for k, v in sbml.parameters.items() if k not in sbml.derived
483
- }
484
- variables = {
485
- k: v.initial_amount for k, v in sbml.variables.items() if k not in sbml.derived
486
- }
487
- for k, v in sbml.compartments.items():
488
- if k in sbml.derived:
489
- continue
490
- if v.is_constant:
491
- parameters[k] = v.size
492
- else:
493
- variables[k] = v.size
494
55
 
495
- # Ensure non-zero value for initial assignments
496
- # EXPLAIN: we need to do this for the first round of get_args to work
497
- # otherwise we run into a ton of DivisionByZero errors.
498
- # Since the values are overwritte afterwards, it doesn't really matter anyways
499
- for k in sbml.initial_assignment:
500
- if k in parameters and parameters[k] == 0:
501
- parameters[k] = 1
502
- if k in variables and variables[k] == 0:
503
- variables[k] = 1
56
+ for key, par in model.parameters.items():
57
+ sym.parameters[key] = SymbolicParameter(value=par.value, unit=par.unit)
504
58
 
505
- derived_str = "\n ".join(
506
- f"m.add_derived('{k}', fn={k}, args={v.args})" for k, v in sbml.derived.items()
507
- )
508
- rxn_str = "\n ".join(
509
- f"m.add_reaction('{k}', fn={k}, args={rxn.args}, stoichiometry={rxn.stoichiometry})"
510
- for k, rxn in sbml.reactions.items()
511
- )
512
-
513
- functions_str = "\n\n".join(functions.values())
514
-
515
- parameters_str = f"m.add_parameters({parameters})" if len(parameters) > 0 else ""
516
- variables_str = f"m.add_variables({variables})" if len(variables) > 0 else ""
517
-
518
- # Initial assignments
519
- initial_assignment_order = _sort_dependencies(
520
- available=set(sbml.initial_assignment)
521
- ^ set(parameters)
522
- ^ set(variables)
523
- ^ set(sbml.derived)
524
- | {"time"},
525
- elements=[
526
- Dependency(name=k, required=set(v.args), provided={k})
527
- for k, v in sbml.initial_assignment.items()
528
- ],
529
- )
59
+ for key, der in model.derived.items():
60
+ sym.derived[key] = SymbolicFn(fn_name=key, expr=der, args=free_symbols(der))
530
61
 
531
- if len(initial_assignment_order) > 0:
532
- initial_assignment_source = "\n ".join(
533
- _codegen_initial_assignment(k, sbml, parameters=parameters)
534
- for k in initial_assignment_order
62
+ for key, rxn in model.reactions.items():
63
+ sym.reactions[key] = SymbolicReaction(
64
+ fn=SymbolicFn(fn_name=key, expr=rxn.expr, args=free_symbols(rxn.expr)),
65
+ stoichiometry={
66
+ k: _transform_stoichiometry(k, v) for k, v in rxn.stoichiometry.items()
67
+ },
535
68
  )
536
- else:
537
- initial_assignment_source = ""
538
-
539
- file = f"""
540
- import math
541
-
542
- import numpy as np
543
- import scipy
544
-
545
- from mxlpy import Model
546
-
547
- {functions_str}
69
+ for key, der in model.initial_assignments.items():
70
+ if key in model.parameters:
71
+ sym.parameters[key].value = SymbolicFn(
72
+ fn_name=key, expr=der, args=free_symbols(der)
73
+ )
74
+ elif key in model.variables:
75
+ sym.variables[key].value = SymbolicFn(
76
+ fn_name=key, expr=der, args=free_symbols(der)
77
+ )
548
78
 
549
- def get_model() -> Model:
550
- m = Model()
551
- {parameters_str}
552
- {variables_str}
553
- {derived_str}
554
- {rxn_str}
555
- args = m.get_args()
556
- {initial_assignment_source}
557
- return m
558
- """
559
79
  path = default_tmp_dir(None, remove_old_cache=False) / f"{name}.py"
560
80
  with path.open("w+") as f:
561
- f.write(file)
81
+ f.write(
82
+ generate_mxlpy_code_from_symbolic_repr(
83
+ sym,
84
+ imports=[
85
+ "import math",
86
+ "import scipy",
87
+ ],
88
+ )
89
+ )
562
90
  return path
563
91
 
564
92
 
565
93
  def import_from_path(module_name: str, file_path: Path) -> Callable[[], Model]:
566
- import sys
567
- from importlib import util
568
-
569
94
  spec = unwrap(util.spec_from_file_location(module_name, file_path))
570
95
  module = util.module_from_spec(spec)
571
96
  sys.modules[module_name] = module
572
97
  unwrap(spec.loader).exec_module(module)
573
- return module.get_model
98
+ return module.create_model
574
99
 
575
100
 
576
101
  def valid_filename(value: str) -> str:
577
- import re
578
- import unicodedata
579
-
580
102
  value = (
581
103
  unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
582
104
  )
@@ -595,8 +117,7 @@ def read(file: Path) -> Model:
595
117
  Model: Imported model instance.
596
118
 
597
119
  """
598
- name = valid_filename(file.stem)
599
- sbml = Parser().parse(file=file)
600
-
601
- model_fn = import_from_path(name, _codgen(name, sbml))
120
+ model = pysbml.load_and_transform_model(file)
121
+ out_name = valid_filename(file.stem)
122
+ model_fn = import_from_path(out_name, _codegen(out_name, model))
602
123
  return model_fn()