-
Notifications
You must be signed in to change notification settings - Fork 0
feat: CellML to Cubie adapter layer added #221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
633a96c
Initial plan
Copilot f0a9054
chore: initialize cellml import testing implementation plan
Copilot 02daf3b
feat: implement and test CellML model import functionality
Copilot 590dd5b
fix: address reviewer feedback - validation, simplification, test imp…
Copilot ad02da1
chore: delete old review report and generate new review
Copilot 4b83412
docs: apply all review edits - enhance docstrings and rename fixture
Copilot fa63cbe
chore: final review complete - all edits applied, ready to merge
Copilot 5b33ae1
chore: remove planning files, keep only mergeable source code
Copilot 4c83476
feat: extract algebraic equations from CellML models as observables
Copilot 3821eb1
feat: return initialized SymbolicODE from load_cellml_model, add buil…
Copilot 28055fb
fix: address review comments - remove slash, add params/observables, …
Copilot 723f86a
Merge branch 'main' into copilot/test-import-cellml-model
ccam80 ed96f4c
fix: add cellmlmanip to CI dependencies
ccam80 115a254
fix: update dockerfile uv install to use venv
ccam80 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,4 +10,5 @@ attrs | |
| scipy | ||
| cupy-cuda12x | ||
| pandas | ||
| sympy | ||
| sympy | ||
| cellmlmanip | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,38 +1,255 @@ | ||
| """Minimal CellML parsing helpers using ``cellmlmanip``. | ||
|
|
||
| This wrapper is heavily inspired by | ||
| :mod:`chaste_codegen.model_with_conversions` from the chaste-codegen project | ||
| (MIT licence). Only a tiny subset required for basic model loading is | ||
| implemented here. | ||
| """ | ||
| This module provides functionality to import CellML models into CuBIE's | ||
| symbolic ODE framework. It wraps the cellmlmanip library to load | ||
| CellML files and convert them directly into SymbolicODE objects. | ||
|
|
||
| The implementation is inspired by | ||
| :mod:`chaste_codegen.model_with_conversions` from the chaste-codegen | ||
| project (MIT licence). Only a minimal subset required for basic model | ||
| loading is implemented here. | ||
|
|
||
| Examples | ||
| -------- | ||
| Basic CellML model loading workflow: | ||
|
|
||
| >>> from cubie.odesystems.symbolic.parsing.cellml import ( | ||
| ... load_cellml_model | ||
| ... ) | ||
| >>> | ||
| >>> # Load a CellML model file - returns initialized SymbolicODE | ||
| >>> ode_system = load_cellml_model("cardiac_model.cellml") | ||
| >>> | ||
| >>> # The model is ready to use with solve_ivp | ||
| >>> print(f"Model has {ode_system.num_states} states") | ||
| >>> print(f"Model has {len(ode_system.indices.observables)} observables") | ||
|
|
||
| Notes | ||
| ----- | ||
| The cellmlmanip dependency is optional. Install with: | ||
|
|
||
| from __future__ import annotations | ||
| pip install cellmlmanip | ||
|
|
||
| CellML models can be obtained from the Physiome Model Repository: | ||
| https://models.physiomeproject.org/ | ||
|
|
||
| See Also | ||
| -------- | ||
| load_cellml_model : Main function for loading CellML files | ||
| """ | ||
|
|
||
| try: # pragma: no cover - optional dependency | ||
| import cellmlmanip # type: ignore | ||
| except Exception: # pragma: no cover | ||
| cellmlmanip = None # type: ignore | ||
|
|
||
| import sympy as sp | ||
| from pathlib import Path | ||
| import numpy as np | ||
| from typing import Optional, List | ||
| import re | ||
|
|
||
| from cubie._utils import PrecisionDType | ||
|
|
||
| def load_cellml_model(path: str) -> tuple[list[sp.Symbol], list[sp.Eq]]: | ||
| """Load a CellML model and extract states and derivatives. | ||
|
|
||
| def _sanitize_symbol_name(name: str) -> str: | ||
| """Sanitize CellML symbol names for Python identifiers. | ||
|
|
||
| CellML uses $ for namespacing and allows names starting with _ | ||
| followed by numbers. We need to convert these to valid Python | ||
| identifiers. | ||
| """ | ||
| # Replace $ with _ | ||
| name = name.replace('$', '_') | ||
|
|
||
| # Replace . with _ | ||
| name = name.replace('.', '_') | ||
|
|
||
| # If name starts with _, check if next char is a digit | ||
| # If so, prepend with 'var_' to make it valid | ||
| if name.startswith('_') and len(name) > 1 and name[1].isdigit(): | ||
| name = 'var' + name | ||
|
|
||
| # Ensure name doesn't start with a digit | ||
| if name and name[0].isdigit(): | ||
| name = 'var_' + name | ||
|
|
||
| # Replace any remaining invalid characters with _ | ||
| name = re.sub(r'[^a-zA-Z0-9_]', '_', name) | ||
|
|
||
| return name | ||
|
|
||
|
|
||
| def load_cellml_model( | ||
| path: str, | ||
| precision: PrecisionDType = np.float32, | ||
| name: Optional[str] = None, | ||
| parameters: Optional[List[str]] = None, | ||
| observables: Optional[List[str]] = None, | ||
| ): | ||
| """Load a CellML model and return an initialized SymbolicODE system. | ||
|
|
||
| This function uses the cellmlmanip library to parse CellML files | ||
| and converts them into a ready-to-use SymbolicODE system with all | ||
| differential equations and algebraic constraints properly configured. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| path | ||
| Filesystem path to the CellML source file. | ||
| path : str | ||
| Filesystem path to the CellML source file. Must have .cellml | ||
| extension and be a valid CellML 1.0 or 1.1 model file. | ||
| precision : numpy dtype, optional | ||
| Target floating-point precision for compiled kernels. | ||
| Default is np.float32. | ||
| name : str, optional | ||
| Identifier for the generated system. If None, uses the | ||
| filename without extension. | ||
| parameters : list of str, optional | ||
| List of symbol names to assign as parameters. Otherwise, | ||
| these symbols become constants or anonymous auxiliaries. | ||
| observables : list of str, optional | ||
| List of symbol names to assign as observables. Otherwise, | ||
| these symbols become anonymous auxiliaries. | ||
|
|
||
| Returns | ||
| ------- | ||
| tuple[list[sympy.Symbol], list[sympy.Eq]] | ||
| States and differential equations defined by the model. | ||
| SymbolicODE | ||
| Fully initialized ODE system ready for use with solve_ivp. | ||
| State variables are configured with initial values from the | ||
| CellML model, and algebraic equations are set up according | ||
| to the parameters and observables specifications. | ||
|
|
||
| Raises | ||
| ------ | ||
| ImportError | ||
| If cellmlmanip is not installed. Install with: | ||
| pip install cellmlmanip | ||
| TypeError | ||
| If path is not a string. | ||
| FileNotFoundError | ||
| If the specified CellML file does not exist. | ||
| ValueError | ||
| If the file does not have .cellml extension. | ||
|
|
||
| Examples | ||
| -------- | ||
| Load a CellML model and run a simulation: | ||
|
|
||
| >>> from cubie import load_cellml_model, solve_ivp | ||
| >>> import numpy as np | ||
| >>> | ||
| >>> # Load the model | ||
| >>> ode_system = load_cellml_model("beeler_reuter_model_1977.cellml") | ||
| >>> | ||
| >>> # Set up simulation | ||
| >>> t_span = (0.0, 100.0) | ||
| >>> initial_states = np.ones(ode_system.num_states, dtype=np.float32) | ||
| >>> | ||
| >>> # Run simulation | ||
| >>> result = solve_ivp(ode_system, t_span, initial_states) | ||
|
|
||
| Notes | ||
| ----- | ||
| - Differential equations become state equations in the ODE system | ||
| - Algebraic equations become observables or anonymous auxiliaries | ||
| - State variables are converted from sympy.Dummy to sympy.Symbol | ||
| - Initial values from CellML are preserved in the ODE system | ||
| - Supports CellML 1.0 and 1.1 formats | ||
| - CellML models from Physiome repository are compatible | ||
| - The cellmlmanip library handles the complex CellML XML parsing | ||
| """ | ||
| if cellmlmanip is None: # pragma: no cover | ||
| raise ImportError("cellmlmanip is required for CellML parsing") | ||
|
|
||
| # Validate input type | ||
| if not isinstance(path, str): | ||
| raise TypeError( | ||
| f"path must be a string, got {type(path).__name__}" | ||
| ) | ||
|
|
||
| # Validate file existence | ||
| path_obj = Path(path) | ||
| if not path_obj.exists(): | ||
| raise FileNotFoundError(f"CellML file not found: {path}") | ||
|
|
||
| # Validate file extension | ||
| if not path.endswith('.cellml'): | ||
| raise ValueError( | ||
| f"File must have .cellml extension, got: {path}" | ||
| ) | ||
|
|
||
| # Use filename as default name if not provided | ||
| if name is None: | ||
| name = path_obj.stem | ||
|
|
||
| model = cellmlmanip.load_model(path) | ||
| states = list(model.get_state_variables()) | ||
| derivatives = list(model.get_derivatives()) | ||
| equations = [eq for eq in model.equations if eq.lhs in derivatives] | ||
| return states, equations | ||
| raw_states = list(model.get_state_variables()) | ||
| raw_derivatives = list(model.get_derivatives()) | ||
|
|
||
| # Extract initial values from CellML model | ||
| initial_values = {} | ||
|
|
||
| # Convert Dummy symbols to regular Symbols with sanitized names | ||
| # cellmlmanip returns Dummy symbols but we need regular Symbols | ||
| states = [] | ||
| dummy_to_symbol = {} | ||
| for raw_state in raw_states: | ||
| clean_name = _sanitize_symbol_name(raw_state.name) | ||
| symbol = sp.Symbol(clean_name) | ||
| dummy_to_symbol[raw_state] = symbol | ||
| states.append(symbol) | ||
|
|
||
| # Get initial value if available | ||
| if hasattr(raw_state, 'initial_value') and raw_state.initial_value is not None: | ||
| initial_values[clean_name] = float(raw_state.initial_value) | ||
|
|
||
| # Also convert any other Dummy symbols in the model equations | ||
| for eq in model.equations: | ||
| for atom in eq.atoms(sp.Dummy): | ||
| if atom not in dummy_to_symbol: | ||
| clean_name = _sanitize_symbol_name(atom.name) | ||
| dummy_to_symbol[atom] = sp.Symbol(clean_name) | ||
|
|
||
| # Filter differential equations and algebraic equations separately | ||
| differential_equations = [] | ||
| algebraic_equations = [] | ||
|
|
||
| for eq in model.equations: | ||
| eq_substituted = eq.subs(dummy_to_symbol) | ||
| if eq.lhs in raw_derivatives: | ||
| differential_equations.append(eq_substituted) | ||
| else: | ||
| algebraic_equations.append(eq_substituted) | ||
|
|
||
| # Convert equations to string format for SymbolicODE.create() | ||
| dxdt_strings = [] | ||
| for eq in differential_equations: | ||
| # Get the state variable from the derivative | ||
| state_var = eq.lhs.args[0] | ||
| # Format as "dstate_name = rhs" (no slash) | ||
| dxdt_str = f"d{state_var.name} = {eq.rhs}" | ||
| dxdt_strings.append(dxdt_str) | ||
|
|
||
| # Convert algebraic equations to strings | ||
| # These will be included in the equations list | ||
| all_equations = dxdt_strings.copy() | ||
| for eq in algebraic_equations: | ||
| # Format as "lhs = rhs" | ||
| obs_str = f"{eq.lhs} = {eq.rhs}" | ||
| all_equations.append(obs_str) | ||
|
|
||
| # Import here to avoid circular import with codegen modules | ||
| # cellml is imported by parsing/__init__.py which is imported | ||
| # during SymbolicODE initialization, creating a circular dependency | ||
| from cubie.odesystems.symbolic.symbolicODE import SymbolicODE | ||
|
|
||
| # Create and return the SymbolicODE system | ||
| return SymbolicODE.create( | ||
| dxdt=all_equations, | ||
| states=initial_values if initial_values else None, | ||
| parameters=parameters, | ||
| observables=observables, | ||
| name=name, | ||
| precision=precision, | ||
| strict=False, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <model name="basic_ode" xmlns="http://www.cellml.org/cellml/1.0#"> | ||
| <component name="main"> | ||
| <variable name="time" units="dimensionless" public_interface="out"/> | ||
| <variable name="x" units="dimensionless" initial_value="1.0"/> | ||
| <variable name="a" units="dimensionless" initial_value="0.5"/> | ||
| <math xmlns="http://www.w3.org/1998/Math/MathML"> | ||
| <apply> | ||
| <eq/> | ||
| <apply> | ||
| <diff/> | ||
| <bvar><ci>time</ci></bvar> | ||
| <ci>x</ci> | ||
| </apply> | ||
| <apply> | ||
| <times/> | ||
| <apply><minus/><ci>a</ci></apply> | ||
| <ci>x</ci> | ||
| </apply> | ||
| </apply> | ||
| </math> | ||
| </component> | ||
| </model> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.