diff --git a/python/sdist/amici/de_export.py b/python/sdist/amici/de_export.py index 37958edf97..be5a70e5cc 100644 --- a/python/sdist/amici/de_export.py +++ b/python/sdist/amici/de_export.py @@ -1,13 +1,15 @@ """ C++ Export ---------- -This module provides all necessary functionality specify a DE model and -generate executable C++ simulation code. The user generally won't have to -directly call any function from this module as this will be done by +This module provides all necessary functionality to specify a differential +equation model and generate executable C++ simulation code. +The user generally won't have to directly call any function from this module +as this will be done by :py:func:`amici.pysb_import.pysb2amici`, :py:func:`amici.sbml_import.SbmlImporter.sbml2amici` and :py:func:`amici.petab_import.import_model`. """ +from __future__ import annotations import contextlib import copy import itertools @@ -22,8 +24,6 @@ TYPE_CHECKING, Callable, Literal, - Optional, - Union, ) from collections.abc import Sequence import numpy as np @@ -40,7 +40,6 @@ splines, ) from ._codegen.template import apply_template -from .constants import SymbolId from .cxxcodeprinter import ( AmiciCxxCodePrinter, get_switch_statement, @@ -51,7 +50,6 @@ ObservableTransformation, SBMLException, amici_time_symbol, - generate_flux_symbol, smart_subs_dict, strip_pysb, toposort_symbols, @@ -70,7 +68,7 @@ ) if TYPE_CHECKING: - from . import sbml_import + from .splines import AbstractSpline # Template for model simulation main.cpp file @@ -497,25 +495,6 @@ def var_in_function_signature(name: str, varname: str, ode: bool) -> bool: ) -# defines the type of some attributes in DEModel -symbol_to_type = { - SymbolId.SPECIES: DifferentialState, - SymbolId.ALGEBRAIC_STATE: AlgebraicState, - SymbolId.ALGEBRAIC_EQUATION: AlgebraicEquation, - SymbolId.PARAMETER: Parameter, - SymbolId.FIXED_PARAMETER: Constant, - SymbolId.OBSERVABLE: Observable, - SymbolId.EVENT_OBSERVABLE: EventObservable, - SymbolId.SIGMAY: SigmaY, - SymbolId.SIGMAZ: SigmaZ, - SymbolId.LLHY: LogLikelihoodY, - SymbolId.LLHZ: LogLikelihoodZ, - SymbolId.LLHRZ: LogLikelihoodRZ, - SymbolId.EXPRESSION: Expression, - SymbolId.EVENT: Event, -} - - class DEModel: """ Defines a Differential Equation as set of ModelQuantities. @@ -639,8 +618,8 @@ class DEModel: def __init__( self, - verbose: Optional[Union[bool, int]] = False, - simplify: Optional[Callable] = _default_simplify, + verbose: bool | int | None = False, + simplify: Callable | None = _default_simplify, cache_simplify: bool = False, ): """ @@ -672,7 +651,7 @@ def __init__( self._expressions: list[Expression] = [] self._conservation_laws: list[ConservationLaw] = [] self._events: list[Event] = [] - self._splines = [] + self._splines: list[AbstractSpline] = [] self._symboldim_funs: dict[str, Callable[[], int]] = { "sx": self.num_states_solver, "v": self.num_states_solver, @@ -683,19 +662,15 @@ def __init__( } self._eqs: dict[ str, - Union[ - sp.Matrix, - sp.SparseMatrix, - list[Union[sp.Matrix, sp.SparseMatrix]], - ], + (sp.Matrix | sp.SparseMatrix | list[sp.Matrix | sp.SparseMatrix]), ] = dict() - self._sparseeqs: dict[str, Union[sp.Matrix, list[sp.Matrix]]] = dict() + self._sparseeqs: dict[str, sp.Matrix | list[sp.Matrix]] = dict() self._vals: dict[str, list[sp.Expr]] = dict() self._names: dict[str, list[str]] = dict() - self._syms: dict[str, Union[sp.Matrix, list[sp.Matrix]]] = dict() - self._sparsesyms: dict[str, Union[list[str], list[list[str]]]] = dict() - self._colptrs: dict[str, Union[list[int], list[list[int]]]] = dict() - self._rowvals: dict[str, Union[list[int], list[list[int]]]] = dict() + self._syms: dict[str, sp.Matrix | list[sp.Matrix]] = dict() + self._sparsesyms: dict[str, list[str] | list[list[str]]] = dict() + self._colptrs: dict[str, list[int] | list[list[int]]] = dict() + self._rowvals: dict[str, list[int] | list[list[int]]] = dict() self._equation_prototype: dict[str, Callable] = { "total_cl": self.conservation_laws, @@ -726,7 +701,7 @@ def __init__( "k": self.constants, } self._total_derivative_prototypes: dict[ - str, dict[str, Union[str, list[str]]] + str, dict[str, str | list[str]] ] = { "sroot": { "eq": "root", @@ -742,7 +717,7 @@ def __init__( def cached_simplify( expr: sp.Expr, - _simplified: dict[str, sp.Expr] = {}, + _simplified: dict[str, sp.Expr] = {}, # noqa B006 _simplify: Callable = simplify, ) -> sp.Expr: """Speed up expression simplification with caching. @@ -769,7 +744,7 @@ def cached_simplify( return _simplified[expr_str] self._simplify = cached_simplify - self._x0_fixedParameters_idx: Union[None, Sequence[int]] + self._x0_fixedParameters_idx: None | Sequence[int] self._w_recursion_depth: int = 0 self._has_quadratic_nllh: bool = True set_log_level(logger, verbose) @@ -838,188 +813,6 @@ def states(self) -> list[State]: """Get all states.""" return self._differential_states + self._algebraic_states - @log_execution_time("importing SbmlImporter", logger) - def import_from_sbml_importer( - self, - si: "sbml_import.SbmlImporter", - compute_cls: Optional[bool] = True, - ) -> None: - """ - Imports a model specification from a - :class:`amici.sbml_import.SbmlImporter` instance. - - :param si: - imported SBML model - :param compute_cls: - whether to compute conservation laws - """ - - # add splines as expressions to the model - # saved for later substituting into the fluxes - spline_subs = {} - - for ispl, spl in enumerate(si.splines): - spline_expr = spl.ode_model_symbol(si) - spline_subs[spl.sbml_id] = spline_expr - self.add_component( - Expression( - identifier=spl.sbml_id, - name=str(spl.sbml_id), - value=spline_expr, - ) - ) - self._splines = si.splines - - # get symbolic expression from SBML importers - symbols = copy.copy(si.symbols) - - # assemble fluxes and add them as expressions to the model - assert len(si.flux_ids) == len(si.flux_vector) - fluxes = [ - generate_flux_symbol(ir, name=flux_id) - for ir, flux_id in enumerate(si.flux_ids) - ] - - # correct time derivatives for compartment changes - def transform_dxdt_to_concentration(species_id, dxdt): - """ - Produces the appropriate expression for the first derivative of a - species with respect to time, for species that reside in - compartments with a constant volume, or a volume that is defined by - an assignment or rate rule. - - :param species_id: - The identifier of the species (generated in "sbml_import.py"). - - :param dxdt: - The element-wise product of the row in the stoichiometric - matrix that corresponds to the species (row x_index) and the - flux (kinetic laws) vector. Ignored in the case of rate rules. - """ - # The derivation of the below return expressions can be found in - # the documentation. They are found by rearranging - # $\frac{d}{dt} (vx) = Sw$ for $\frac{dx}{dt}$, where $v$ is the - # vector of species compartment volumes, $x$ is the vector of - # species concentrations, $S$ is the stoichiometric matrix, and $w$ - # is the flux vector. The conditional below handles the cases of - # species in (i) compartments with a rate rule, (ii) compartments - # with an assignment rule, and (iii) compartments with a constant - # volume, respectively. - species = si.symbols[SymbolId.SPECIES][species_id] - - comp = species["compartment"] - if comp in si.symbols[SymbolId.SPECIES]: - dv_dt = si.symbols[SymbolId.SPECIES][comp]["dt"] - xdot = (dxdt - dv_dt * species_id) / comp - return xdot - elif comp in si.compartment_assignment_rules: - v = si.compartment_assignment_rules[comp] - - # we need to flatten out assignments in the compartment in - # order to ensure that we catch all species dependencies - v = smart_subs_dict( - v, si.symbols[SymbolId.EXPRESSION], "value" - ) - dv_dt = v.diff(amici_time_symbol) - # we may end up with a time derivative of the compartment - # volume due to parameter rate rules - comp_rate_vars = [ - p - for p in v.free_symbols - if p in si.symbols[SymbolId.SPECIES] - ] - for var in comp_rate_vars: - dv_dt += ( - v.diff(var) * si.symbols[SymbolId.SPECIES][var]["dt"] - ) - dv_dx = v.diff(species_id) - xdot = (dxdt - dv_dt * species_id) / (dv_dx * species_id + v) - return xdot - elif comp in si.symbols[SymbolId.ALGEBRAIC_STATE]: - raise SBMLException( - f"Species {species_id} is in a compartment {comp} that is" - f" defined by an algebraic equation. This is not" - f" supported." - ) - else: - v = si.compartments[comp] - - if v == 1.0: - return dxdt - - return dxdt / v - - # create dynamics without respecting conservation laws first - dxdt = smart_multiply( - si.stoichiometric_matrix, MutableDenseMatrix(fluxes) - ) - for ix, ((species_id, species), formula) in enumerate( - zip(symbols[SymbolId.SPECIES].items(), dxdt) - ): - # rate rules and amount species don't need to be updated - if "dt" in species: - continue - if species["amount"]: - species["dt"] = formula - else: - species["dt"] = transform_dxdt_to_concentration( - species_id, formula - ) - - # create all basic components of the DE model and add them. - for symbol_name in symbols: - # transform dict of lists into a list of dicts - args = ["name", "identifier"] - - if symbol_name == SymbolId.SPECIES: - args += ["dt", "init"] - elif symbol_name == SymbolId.ALGEBRAIC_STATE: - args += ["init"] - else: - args += ["value"] - - if symbol_name == SymbolId.EVENT: - args += ["state_update", "initial_value"] - elif symbol_name == SymbolId.OBSERVABLE: - args += ["transformation"] - elif symbol_name == SymbolId.EVENT_OBSERVABLE: - args += ["event"] - - comp_kwargs = [ - { - "identifier": var_id, - **{k: v for k, v in var.items() if k in args}, - } - for var_id, var in symbols[symbol_name].items() - ] - - for comp_kwarg in comp_kwargs: - self.add_component(symbol_to_type[symbol_name](**comp_kwarg)) - - # add fluxes as expressions, this needs to happen after base - # expressions from symbols have been parsed - for flux_id, flux in zip(fluxes, si.flux_vector): - # replace splines inside fluxes - flux = flux.subs(spline_subs) - self.add_component( - Expression(identifier=flux_id, name=str(flux_id), value=flux) - ) - - # process conservation laws - if compute_cls: - si.process_conservation_laws(self) - - # fill in 'self._sym' based on prototypes and components in ode_model - self.generate_basic_variables() - self._has_quadratic_nllh = all( - llh["dist"] - in ["normal", "lin-normal", "log-normal", "log10-normal"] - for llh in si.symbols[SymbolId.LLHY].values() - ) - - # substitute SBML-rateOf constructs - self._process_sbml_rate_of() - def _process_sbml_rate_of(self) -> None: """Substitute any SBML-rateOf constructs in the model equations""" rate_of_func = sp.core.function.UndefinedFunction("rateOf") @@ -1160,7 +953,7 @@ def get_rate(symbol: sp.Symbol): # ) def add_component( - self, component: ModelQuantity, insert_first: Optional[bool] = False + self, component: ModelQuantity, insert_first: bool | None = False ) -> None: """ Adds a new ModelQuantity to the model. @@ -1276,6 +1069,24 @@ def add_conservation_law( self.add_component(cl) self._differential_states[ix].set_conservation_law(cl) + def add_spline(self, spline: AbstractSpline, spline_expr: sp.Expr) -> None: + """Add a spline to the model. + + :param spline: + Spline instance to be added + :param spline_expr: + Sympy function representation of `spline` from + ``spline.ode_model_symbol()``. + """ + self._splines.append(spline) + self.add_component( + Expression( + identifier=spline.sbml_id, + name=str(spline.sbml_id), + value=spline_expr, + ) + ) + def get_observable_transformations(self) -> list[ObservableTransformation]: """ List of observable transformations @@ -1459,9 +1270,7 @@ def sparseeq(self, name) -> sp.Matrix: self._generate_sparse_symbol(name) return self._sparseeqs[name] - def colptrs( - self, name: str - ) -> Union[list[sp.Number], list[list[sp.Number]]]: + def colptrs(self, name: str) -> list[sp.Number] | list[list[sp.Number]]: """ Returns (and constructs if necessary) the column pointers for a sparsified symbolic variable. @@ -1478,9 +1287,7 @@ def colptrs( self._generate_sparse_symbol(name) return self._colptrs[name] - def rowvals( - self, name: str - ) -> Union[list[sp.Number], list[list[sp.Number]]]: + def rowvals(self, name: str) -> list[sp.Number] | list[list[sp.Number]]: """ Returns (and constructs if necessary) the row values for a sparsified symbolic variable. @@ -1534,8 +1341,7 @@ def free_symbols(self) -> set[sp.Basic]: """ return set( chain.from_iterable( - state.get_free_symbols() - for state in self.states() + self.algebraic_equations() + state.get_free_symbols() for state in self.states() ) ) @@ -2426,8 +2232,8 @@ def _multiplication( name: str, x: str, y: str, - transpose_x: Optional[bool] = False, - sign: Optional[int] = 1, + transpose_x: bool | None = False, + sign: int | None = 1, ): """ Creates a new symbolic variable according to a multiplication @@ -2629,7 +2435,7 @@ def _get_unique_root( self, root_found: sp.Expr, roots: list[Event], - ) -> Union[sp.Symbol, None]: + ) -> sp.Symbol | None: """ Collects roots of Heaviside functions and events and stores them in the roots list. It checks for redundancy to not store symbolically @@ -2803,13 +2609,13 @@ class DEExporter: def __init__( self, de_model: DEModel, - outdir: Optional[Union[Path, str]] = None, - verbose: Optional[Union[bool, int]] = False, - assume_pow_positivity: Optional[bool] = False, - compiler: Optional[str] = None, - allow_reinit_fixpar_initcond: Optional[bool] = True, - generate_sensitivity_code: Optional[bool] = True, - model_name: Optional[str] = "model", + outdir: Path | str | None = None, + verbose: bool | int | None = False, + assume_pow_positivity: bool | None = False, + compiler: str | None = None, + allow_reinit_fixpar_initcond: bool | None = True, + generate_sensitivity_code: bool | None = True, + model_name: str | None = "model", ): """ Generate AMICI C++ files for the DE provided to the constructor. @@ -3901,7 +3707,7 @@ def _write_module_setup(self) -> None: template_data, ) - def set_paths(self, output_dir: Optional[Union[str, Path]] = None) -> None: + def set_paths(self, output_dir: str | Path | None = None) -> None: """ Set output paths for the model and create if necessary diff --git a/python/sdist/amici/de_model.py b/python/sdist/amici/de_model.py index c20509407a..cb0c066e4d 100644 --- a/python/sdist/amici/de_model.py +++ b/python/sdist/amici/de_model.py @@ -13,6 +13,7 @@ generate_measurement_symbol, generate_regularization_symbol, ) +from .constants import SymbolId __all__ = [ "ConservationLaw", @@ -732,3 +733,22 @@ def get_trigger_time(self) -> sp.Float: "This event does not trigger at a fixed timepoint." ) return self._t_root[0] + + +# defines the type of some attributes in DEModel +symbol_to_type = { + SymbolId.SPECIES: DifferentialState, + SymbolId.ALGEBRAIC_STATE: AlgebraicState, + SymbolId.ALGEBRAIC_EQUATION: AlgebraicEquation, + SymbolId.PARAMETER: Parameter, + SymbolId.FIXED_PARAMETER: Constant, + SymbolId.OBSERVABLE: Observable, + SymbolId.EVENT_OBSERVABLE: EventObservable, + SymbolId.SIGMAY: SigmaY, + SymbolId.SIGMAZ: SigmaZ, + SymbolId.LLHY: LogLikelihoodY, + SymbolId.LLHZ: LogLikelihoodZ, + SymbolId.LLHRZ: LogLikelihoodRZ, + SymbolId.EXPRESSION: Expression, + SymbolId.EVENT: Event, +} diff --git a/python/sdist/amici/sbml_import.py b/python/sdist/amici/sbml_import.py index 11ffb16599..eb0b739186 100644 --- a/python/sdist/amici/sbml_import.py +++ b/python/sdist/amici/sbml_import.py @@ -32,7 +32,8 @@ DEExporter, DEModel, ) -from .sympy_utils import smart_is_zero_matrix +from .de_model import symbol_to_type, Expression +from .sympy_utils import smart_is_zero_matrix, smart_multiply from .import_utils import ( RESERVED_SYMBOLS, _check_unsupported_functions, @@ -50,10 +51,12 @@ symbol_with_assumptions, toposort_symbols, _default_simplify, + generate_flux_symbol, ) from .logging import get_logger, log_execution_time, set_log_level from .sbml_utils import SBMLException, _parse_logical_operators from .splines import AbstractSpline +from sympy.matrices.dense import MutableDenseMatrix SymbolicFormula = dict[sp.Symbol, sp.Expr] @@ -180,7 +183,7 @@ def __init__( self.species_assignment_rules: SymbolicFormula = {} self.parameter_assignment_rules: SymbolicFormula = {} self.initial_assignments: SymbolicFormula = {} - self.splines = [] + self.splines: list[AbstractSpline] = [] self._reset_symbols() @@ -534,9 +537,96 @@ def _build_ode_model( simplify=simplify, cache_simplify=cache_simplify, ) - ode_model.import_from_sbml_importer( - self, compute_cls=compute_conservation_laws + + ode_model._has_quadratic_nllh = all( + llh["dist"] + in ["normal", "lin-normal", "log-normal", "log10-normal"] + for llh in self.symbols[SymbolId.LLHY].values() + ) + + # add splines as expressions to the model + # saved for later substituting into the fluxes + spline_subs = {} + for ispl, spl in enumerate(self.splines): + spline_expr = spl.ode_model_symbol(self) + spline_subs[spl.sbml_id] = spline_expr + ode_model.add_spline(spl, spline_expr) + + # assemble fluxes and add them as expressions to the model + assert len(self.flux_ids) == len(self.flux_vector) + fluxes = [ + generate_flux_symbol(ir, name=flux_id) + for ir, flux_id in enumerate(self.flux_ids) + ] + + # create dynamics without respecting conservation laws first + dxdt = smart_multiply( + self.stoichiometric_matrix, MutableDenseMatrix(fluxes) ) + # correct time derivatives for compartment changes + for ix, ((species_id, species), formula) in enumerate( + zip(self.symbols[SymbolId.SPECIES].items(), dxdt) + ): + # rate rules and amount species don't need to be updated + if "dt" in species: + continue + if species["amount"]: + species["dt"] = formula + else: + species["dt"] = self._transform_dxdt_to_concentration( + species_id, formula + ) + + # create all basic components of the DE model and add them. + for symbol_name in self.symbols: + # transform dict of lists into a list of dicts + args = ["name", "identifier"] + + if symbol_name == SymbolId.SPECIES: + args += ["dt", "init"] + elif symbol_name == SymbolId.ALGEBRAIC_STATE: + args += ["init"] + else: + args += ["value"] + + if symbol_name == SymbolId.EVENT: + args += ["state_update", "initial_value"] + elif symbol_name == SymbolId.OBSERVABLE: + args += ["transformation"] + elif symbol_name == SymbolId.EVENT_OBSERVABLE: + args += ["event"] + + comp_kwargs = [ + { + "identifier": var_id, + **{k: v for k, v in var.items() if k in args}, + } + for var_id, var in self.symbols[symbol_name].items() + ] + + for comp_kwarg in comp_kwargs: + ode_model.add_component( + symbol_to_type[symbol_name](**comp_kwarg) + ) + + # add fluxes as expressions, this needs to happen after base + # expressions from symbols have been parsed + for flux_id, flux in zip(fluxes, self.flux_vector): + # replace splines inside fluxes + flux = flux.subs(spline_subs) + ode_model.add_component( + Expression(identifier=flux_id, name=str(flux_id), value=flux) + ) + + if compute_conservation_laws: + self._process_conservation_laws(ode_model) + + # fill in 'self._sym' based on prototypes and components in ode_model + ode_model.generate_basic_variables() + + # substitute SBML-rateOf constructs + ode_model._process_sbml_rate_of() + return ode_model @log_execution_time("importing SBML", logger) @@ -550,7 +640,7 @@ def _process_sbml( :param constant_parameters: SBML Ids identifying constant parameters - :param hardcode_parameters: + :param hardcode_symbols: Parameter IDs to be replaced by their values in the generated model. """ if not self._discard_annotations: @@ -1974,12 +2064,12 @@ def _make_initial( return sym_math - def process_conservation_laws(self, ode_model) -> None: + def _process_conservation_laws(self, ode_model: DEModel) -> None: """ Find conservation laws in reactions and species. :param ode_model: - ODEModel object with basic definitions + :class:`DEModel` object with basic definitions """ conservation_laws = [] @@ -2526,6 +2616,74 @@ def is_rate_rule_target(self, element: sbml.SBase) -> bool: a = self.sbml.getRateRuleByVariable(element.getId()) return a is not None and self._sympy_from_sbml_math(a) is not None + def _transform_dxdt_to_concentration( + self, species_id: sp.Symbol, dxdt: sp.Expr + ) -> sp.Expr: + """ + Produces the appropriate expression for the first derivative of a + species with respect to time, for species that reside in + compartments with a constant volume, or a volume that is defined by + an assignment or rate rule. + + :param species_id: + The identifier of the species (generated in "sbml_import.py"). + + :param dxdt: + The element-wise product of the row in the stoichiometric + matrix that corresponds to the species (row x_index) and the + flux (kinetic laws) vector. Ignored in the case of rate rules. + """ + # The derivation of the below return expressions can be found in + # the documentation. They are found by rearranging + # $\frac{d}{dt} (vx) = Sw$ for $\frac{dx}{dt}$, where $v$ is the + # vector of species compartment volumes, $x$ is the vector of + # species concentrations, $S$ is the stoichiometric matrix, and $w$ + # is the flux vector. The conditional below handles the cases of + # species in (i) compartments with a rate rule, (ii) compartments + # with an assignment rule, and (iii) compartments with a constant + # volume, respectively. + species = self.symbols[SymbolId.SPECIES][species_id] + + comp = species["compartment"] + if comp in self.symbols[SymbolId.SPECIES]: + dv_dt = self.symbols[SymbolId.SPECIES][comp]["dt"] + xdot = (dxdt - dv_dt * species_id) / comp + return xdot + elif comp in self.compartment_assignment_rules: + v = self.compartment_assignment_rules[comp] + + # we need to flatten out assignments in the compartment in + # order to ensure that we catch all species dependencies + v = smart_subs_dict(v, self.symbols[SymbolId.EXPRESSION], "value") + dv_dt = v.diff(amici_time_symbol) + # we may end up with a time derivative of the compartment + # volume due to parameter rate rules + comp_rate_vars = [ + p + for p in v.free_symbols + if p in self.symbols[SymbolId.SPECIES] + ] + for var in comp_rate_vars: + dv_dt += ( + v.diff(var) * self.symbols[SymbolId.SPECIES][var]["dt"] + ) + dv_dx = v.diff(species_id) + xdot = (dxdt - dv_dt * species_id) / (dv_dx * species_id + v) + return xdot + elif comp in self.symbols[SymbolId.ALGEBRAIC_STATE]: + raise SBMLException( + f"Species {species_id} is in a compartment {comp} that is" + f" defined by an algebraic equation. This is not" + f" supported." + ) + else: + v = self.compartments[comp] + + if v == 1.0: + return dxdt + + return dxdt / v + def _check_lib_sbml_errors( sbml_doc: sbml.SBMLDocument, show_warnings: bool = False diff --git a/python/sdist/amici/splines.py b/python/sdist/amici/splines.py index d55a78137b..ea0cd0e06d 100644 --- a/python/sdist/amici/splines.py +++ b/python/sdist/amici/splines.py @@ -52,6 +52,7 @@ pretty_xml, sbml_mathml, ) +from .constants import SymbolId logger = get_logger(__name__, logging.WARNING) @@ -569,8 +570,6 @@ def check_if_valid(self, importer: sbml_import.SbmlImporter) -> None: # the AMICI spline implementation. # If found, they should be checked for here # until (if at all) they are accounted for. - from .de_export import SymbolId - fixed_parameters: list[sp.Symbol] = list( importer.symbols[SymbolId.FIXED_PARAMETER].keys() ) @@ -1345,8 +1344,6 @@ def _from_annotation( def parameters(self, importer: sbml_import.SbmlImporter) -> set[sp.Symbol]: """Returns the SBML parameters used by this spline""" - from .de_export import SymbolId - return self._parameters().intersection( set(importer.symbols[SymbolId.PARAMETER].keys()) ) @@ -1659,7 +1656,6 @@ def check_if_valid(self, importer: sbml_import.SbmlImporter) -> None: for spline grid points, values, ... contain species symbols. """ # TODO this is very much a draft - from .de_export import SymbolId species: list[sp.Symbol] = list(importer.symbols[SymbolId.SPECIES]) for d in self.derivatives_at_nodes: