Skip to content

ode2sbml.parser

parser

Parser for Python ODE functions → ODEModel IR.

Supports three input conventions: A: scipy-style function (ast-parsed) B: dict-of-formulas style C: SymPy symbolic system

from_function(func, state_vars, param_names, initial_conditions, parameter_values, parameter_units=None, compartment='cell', compartment_size=1.0, model_id='model', model_name='ODE Model', notes='')

Parse a scipy-style ODE function into an :class:ODEModel.

The function must have signature func(t, y, p) or func(t, y) where y is the state vector and p is the parameter vector.

Source code in ode2sbml/parser.py
def from_function(
    func: Callable,
    state_vars: List[str],
    param_names: List[str],
    initial_conditions: Dict[str, float],
    parameter_values: Dict[str, float],
    parameter_units: Optional[Dict[str, str]] = None,
    compartment: str = "cell",
    compartment_size: float = 1.0,
    model_id: str = "model",
    model_name: str = "ODE Model",
    notes: str = "",
) -> ODEModel:
    """
    Parse a scipy-style ODE function into an :class:`ODEModel`.

    The function must have signature ``func(t, y, p)`` or ``func(t, y)``
    where ``y`` is the state vector and ``p`` is the parameter vector.
    """
    src = inspect.getsource(func)
    src = textwrap.dedent(src)
    tree = ast.parse(src)

    func_def = next(
        node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)
    )
    visitor = _ODEFunctionVisitor()
    visitor.visit(func_def)

    # Map y[i] → state_vars[i],  p[i] → param_names[i]
    y_arg = func_def.args.args[1].arg  # second arg is state vector
    p_arg = func_def.args.args[2].arg if len(func_def.args.args) > 2 else None

    # Substitute y[i] and p[i] references using exact-token regex to avoid
    # substring collisions (e.g. y[1] being matched inside y[10]).
    def _sub(expr: str) -> str:
        for i, sv in enumerate(state_vars):
            expr = re.sub(rf"\b{re.escape(y_arg)}\[{i}\]", sv, expr)
        if p_arg:
            for i, pn in enumerate(param_names):
                expr = re.sub(rf"\b{re.escape(p_arg)}\[{i}\]", pn, expr)
        return expr

    # Build substituted assignment map
    assignments: Dict[str, str] = {}
    for name, expr in visitor.assignments.items():
        assignments[name] = _sub(expr)

    # Identify ODE derivatives (names in return + prefixed with 'd')
    # Heuristic: the returned variable names are the derivatives
    deriv_names = visitor.return_names or [f"d{sv}" for sv in state_vars]

    rate_rules: List[RateRule] = []
    assignment_rules: List[AssignmentRule] = []

    for sv, dname in zip(state_vars, deriv_names):
        if dname in assignments:
            formula = assignments.pop(dname)
            # Try to simplify with sympy
            formula = _simplify_formula(formula, state_vars, param_names)
            rate_rules.append(RateRule(variable=sv, formula=formula))

    # Remaining assignments that are not state var unpackings become AssignmentRules
    for name, expr in assignments.items():
        # Skip no-op/self-referential assignments that arise from tuple-unpacking
        # after substitution, e.g. "x, y = y" → {"x": "x", "y": "y"}.
        if expr == name:
            continue
        # Skip unpack stubs where the rhs is still y_arg[i] or p_arg[i]
        if expr.startswith(f"{y_arg}["):
            continue
        if p_arg is not None and expr.startswith(f"{p_arg}["):
            continue
        # Skip any residual subscript references like "y[i]" or "p[i]"
        if "[" in expr and expr.split("[", 1)[0] in (y_arg, p_arg or ""):
            continue
        # Do not create AssignmentRules for core state variables or parameters;
        # they already participate in rate rules / parameter definitions.
        if name in state_vars or name in param_names:
            continue
        formula = _simplify_formula(expr, state_vars, param_names)
        assignment_rules.append(AssignmentRule(variable=name, formula=formula))

    comp = Compartment(id=compartment, size=compartment_size)
    species_list = [
        Species(
            id=sv,
            compartment=compartment,
            initial_value=initial_conditions.get(sv, 0.0),
        )
        for sv in state_vars
    ]

    _punits = parameter_units or {}
    param_list = [
        Parameter(
            id=pn,
            value=parameter_values.get(pn, 1.0),
            units=_punits.get(pn, "dimensionless"),
        )
        for pn in param_names
    ]

    return ODEModel(
        id=model_id,
        name=model_name,
        compartments=[comp],
        species=species_list,
        parameters=param_list,
        rate_rules=rate_rules,
        assignment_rules=assignment_rules,
        notes=notes,
    )

from_dict(model_dict, model_id='model', model_name='ODE Model', notes='')

Parse a dict-of-formulas specification into an :class:ODEModel.

Expected keys: - species: {name: initial_value} - parameters: {name: value} - odes: {species_name: formula_string} - compartments: {name: size} (optional, defaults to {"cell": 1.0}) - assignment_rules: {name: formula_string} (optional) - events: list of {trigger, assignments, delay} (optional)

Source code in ode2sbml/parser.py
def from_dict(
    model_dict: Dict[str, Any],
    model_id: str = "model",
    model_name: str = "ODE Model",
    notes: str = "",
) -> ODEModel:
    """
    Parse a dict-of-formulas specification into an :class:`ODEModel`.

    Expected keys:
      - ``species``: {name: initial_value}
      - ``parameters``: {name: value}
      - ``odes``: {species_name: formula_string}
      - ``compartments``: {name: size}  (optional, defaults to ``{"cell": 1.0}``)
      - ``assignment_rules``: {name: formula_string}  (optional)
      - ``events``: list of {trigger, assignments, delay}  (optional)
    """
    species_init: Dict[str, float] = model_dict.get("species", {})
    param_vals: Dict[str, float] = model_dict.get("parameters", {})
    odes: Dict[str, str] = model_dict.get("odes", {})
    comps_raw: Dict[str, float] = model_dict.get("compartments", {"cell": 1.0})
    ar_raw: Dict[str, str] = model_dict.get("assignment_rules", {})
    ev_raw: List[Dict] = model_dict.get("events", [])

    state_ids = list(species_init.keys())
    param_ids = list(param_vals.keys())

    compartments = [Compartment(id=cid, size=sz) for cid, sz in comps_raw.items()]
    default_comp = list(comps_raw.keys())[0] if comps_raw else "cell"

    species_list = [
        Species(id=sid, compartment=default_comp, initial_value=iv)
        for sid, iv in species_init.items()
    ]
    _punits: Dict[str, str] = model_dict.get("parameter_units", {})  # ← ADD
    param_list = [
        Parameter(id=pid, value=pv, units=_punits.get(pid, "dimensionless"))
        for pid, pv in param_vals.items()
    ]
    rate_rules = [
        RateRule(
            variable=sid,
            formula=_simplify_formula(formula, state_ids, param_ids),
        )
        for sid, formula in odes.items()
    ]
    assignment_rules = [
        AssignmentRule(
            variable=name,
            formula=_simplify_formula(formula, state_ids, param_ids),
        )
        for name, formula in ar_raw.items()
    ]
    events = [
        EventTrigger(
            trigger_formula=ev.get("trigger", "false"),
            assignments=ev.get("assignments", {}),
            delay=ev.get("delay", 0.0),
        )
        for ev in ev_raw
    ]

    return ODEModel(
        id=model_id,
        name=model_name,
        compartments=compartments,
        species=species_list,
        parameters=param_list,
        rate_rules=rate_rules,
        assignment_rules=assignment_rules,
        events=events,
        notes=notes,
    )

from_sympy(odes, params, inits, compartment='cell', compartment_size=1.0, species_units=None, parameter_units=None, model_id='model', model_name='ODE Model', assignment_rules=None, events=None, notes='')

Parse a SymPy symbolic ODE system into an :class:ODEModel.

Parameters:

Name Type Description Default
odes Dict[Symbol, Expr]

Mapping from species symbol to its rate expression.

required
params Dict[Symbol, float]

Mapping from parameter symbol to its nominal value.

required
inits Dict[Symbol, float]

Mapping from species symbol to initial condition.

required
Source code in ode2sbml/parser.py
def from_sympy(
    odes: Dict[sp.Symbol, sp.Expr],
    params: Dict[sp.Symbol, float],
    inits: Dict[sp.Symbol, float],
    compartment: str = "cell",
    compartment_size: float = 1.0,
    species_units: Optional[Dict[str, str]] = None,
    parameter_units: Optional[Dict[str, str]] = None,
    model_id: str = "model",
    model_name: str = "ODE Model",
    assignment_rules: Optional[Dict[sp.Symbol, sp.Expr]] = None,
    events: Optional[List[EventTrigger]] = None,
    notes: str = "",
) -> ODEModel:
    """
    Parse a SymPy symbolic ODE system into an :class:`ODEModel`.

    Parameters
    ----------
    odes:
        Mapping from species symbol to its rate expression.
    params:
        Mapping from parameter symbol to its nominal value.
    inits:
        Mapping from species symbol to initial condition.
    """
    parameter_units = parameter_units or {}
    species_units = species_units or {}

    state_syms = list(odes.keys())

    comp = Compartment(id=compartment, size=compartment_size)
    species_list = [
        Species(
            id=str(sym),
            compartment=compartment,
            initial_value=float(inits.get(sym, 0.0)),
            units=species_units.get(str(sym), "substance"),
        )
        for sym in state_syms
    ]
    param_list = [
        Parameter(
            id=str(sym),
            value=float(val),
            units=parameter_units.get(str(sym), "dimensionless"),
        )
        for sym, val in params.items()
    ]
    rate_rules = [
        RateRule(variable=str(sym), formula=str(expr)) for sym, expr in odes.items()
    ]
    ar_list: List[AssignmentRule] = []
    if assignment_rules:
        ar_list = [
            AssignmentRule(variable=str(sym), formula=str(expr))
            for sym, expr in assignment_rules.items()
        ]

    return ODEModel(
        id=model_id,
        name=model_name,
        compartments=[comp],
        species=species_list,
        parameters=param_list,
        rate_rules=rate_rules,
        assignment_rules=ar_list,
        events=events or [],
        notes=notes,
    )