gotranx 2.3.0__tar.gz → 2.5.0__tar.gz
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.
- {gotranx-2.3.0/src/gotranx.egg-info → gotranx-2.5.0}/PKG-INFO +2 -1
- {gotranx-2.3.0 → gotranx-2.5.0}/pyproject.toml +5 -3
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/cli/__init__.py +14 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/cli/gotran2cytozoo.py +5 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/cli/utils.py +55 -20
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/codegen/base.py +6 -4
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/codegen/c.py +1 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/codegen/cytozoo.py +26 -3
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/codegen/python.py +10 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/myokit.py +68 -15
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/ode.lark +4 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/singularities.py +44 -14
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/templates/c.py +4 -2
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/templates/cytozoo.py +32 -12
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/templates/julia.py +4 -2
- {gotranx-2.3.0 → gotranx-2.5.0/src/gotranx.egg-info}/PKG-INFO +2 -1
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx.egg-info/SOURCES.txt +3 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx.egg-info/requires.txt +1 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_benchmarks.py +17 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_cli.py +47 -0
- gotranx-2.5.0/tests/test_julia_runs.py +116 -0
- gotranx-2.5.0/tests/test_min_max.py +184 -0
- gotranx-2.5.0/tests/test_missing_variables.py +372 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_myokit.py +31 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_singularities.py +48 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/LICENSE +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/README.md +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/setup.cfg +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/__init__.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/__main__.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/_enum.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/atoms.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/cli/cellml2ode.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/cli/gotran2c.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/cli/gotran2julia.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/cli/gotran2md.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/cli/gotran2mtk.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/cli/gotran2py.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/cli/gotran2ufl.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/cli/ode2cellml.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/codegen/__init__.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/codegen/jax.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/codegen/julia.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/codegen/markdown.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/codegen/mtk.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/codegen/ode.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/codegen/ufl.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/exceptions.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/expressions.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/linearization.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/load.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/ode.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/ode_component.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/parser.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/py.typed +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/save.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/schemes.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/sympytools.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/templates/__init__.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/templates/jax.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/templates/markdown.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/templates/mtk.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/templates/python.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/templates/ufl.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/transformer.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx/units.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx.egg-info/dependency_links.txt +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx.egg-info/entry_points.txt +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx.egg-info/not-zip-safe +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/src/gotranx.egg-info/top_level.txt +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_atoms.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_c_codegen.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_comments.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_conditionals.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_cytozoo_codegen.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_deep_linearization_models.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_demos.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_determinism.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_expressions.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_julia_codegen.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_linearization.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_load.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_markdown_codegen.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_mtk_codegen.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_ode.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_ode_component.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_operations.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_python_codegen.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_save.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_schemes.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_singularity_models.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_subodes.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_sympytools.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_transformer_assignments.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_transformer_parameters.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_transformer_states.py +0 -0
- {gotranx-2.3.0 → gotranx-2.5.0}/tests/test_ufl_codegen.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gotranx
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: A declarative language describing ordinary differential equations
|
|
5
5
|
Author-email: Henrik Finsberg <henriknf@simula.no>
|
|
6
6
|
License: MIT
|
|
@@ -12,6 +12,7 @@ Requires-Python: >=3.9
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: attrs
|
|
15
|
+
Requires-Dist: black
|
|
15
16
|
Requires-Dist: lark
|
|
16
17
|
Requires-Dist: pint
|
|
17
18
|
Requires-Dist: rich-click
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gotranx"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.5.0"
|
|
8
8
|
description = "A declarative language describing ordinary differential equations"
|
|
9
9
|
authors = [{name = "Henrik Finsberg", email = "henriknf@simula.no"}]
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -17,6 +17,7 @@ urls = {Homepage = "https://github.com/finsberg/gotranx"}
|
|
|
17
17
|
requires-python = ">=3.9"
|
|
18
18
|
dependencies = [
|
|
19
19
|
"attrs",
|
|
20
|
+
"black",
|
|
20
21
|
"lark",
|
|
21
22
|
"pint",
|
|
22
23
|
"rich-click",
|
|
@@ -97,7 +98,8 @@ test = "pytest"
|
|
|
97
98
|
testpaths = ["tests"]
|
|
98
99
|
markers = [
|
|
99
100
|
"benchmark: mark a test as a benchmark.",
|
|
100
|
-
"example: mark a test as an example."
|
|
101
|
+
"example: mark a test as an example.",
|
|
102
|
+
"julia: mark a test that runs generated code in Julia."
|
|
101
103
|
]
|
|
102
104
|
|
|
103
105
|
|
|
@@ -172,7 +174,7 @@ tag = true
|
|
|
172
174
|
sign_tags = false
|
|
173
175
|
tag_name = "v{new_version}"
|
|
174
176
|
tag_message = "Bump version: {current_version} → {new_version}"
|
|
175
|
-
current_version = "2.
|
|
177
|
+
current_version = "2.5.0"
|
|
176
178
|
|
|
177
179
|
|
|
178
180
|
[[tool.bumpversion.files]]
|
|
@@ -669,6 +669,18 @@ def ode2cytozoo(
|
|
|
669
669
|
None, "--model-name", help="Julia struct name (default: the ODE name in PascalCase)"
|
|
670
670
|
),
|
|
671
671
|
v_name: str = typer.Option("v", "--v-name", help="State holding the transmembrane potential"),
|
|
672
|
+
remove_component: typing.Optional[typing.List[str]] = typer.Option(
|
|
673
|
+
None,
|
|
674
|
+
"--remove-component",
|
|
675
|
+
help="Generate the model without this component (repeatable). What the rest "
|
|
676
|
+
"reads from it becomes an input: a trailing writable parameter, NaN until set",
|
|
677
|
+
),
|
|
678
|
+
keep_component: typing.Optional[str] = typer.Option(
|
|
679
|
+
None,
|
|
680
|
+
"--keep-component",
|
|
681
|
+
help="Generate only this component. What it reads from the other components "
|
|
682
|
+
"becomes an input: a trailing writable parameter, NaN until set",
|
|
683
|
+
),
|
|
672
684
|
remove_unused: bool = typer.Option(False, "--remove-unused", help="Remove unused variables"),
|
|
673
685
|
remove_singularities: bool = typer.Option(
|
|
674
686
|
True,
|
|
@@ -720,6 +732,8 @@ def ode2cytozoo(
|
|
|
720
732
|
remove_unused=remove_unused,
|
|
721
733
|
remove_singularities=remove_singularities,
|
|
722
734
|
verbose=verbose,
|
|
735
|
+
remove_component=remove_component,
|
|
736
|
+
keep_component=keep_component,
|
|
723
737
|
)
|
|
724
738
|
|
|
725
739
|
|
|
@@ -6,6 +6,7 @@ import structlog
|
|
|
6
6
|
from ..codegen.cytozoo import CytoZooCodeGenerator
|
|
7
7
|
from ..load import load_ode
|
|
8
8
|
from ..ode import ODE
|
|
9
|
+
from .utils import select_components
|
|
9
10
|
|
|
10
11
|
logger = structlog.get_logger()
|
|
11
12
|
|
|
@@ -87,6 +88,7 @@ def get_code(
|
|
|
87
88
|
"import CytoZoo: num_monitors, monitor_names, monitor_values!",
|
|
88
89
|
f"const NUM_STATES = {len(ode.states)};",
|
|
89
90
|
f"const NUM_PARAMS = {len(ode.parameters)};",
|
|
91
|
+
*([f"const NUM_MISSING = {len(ode.missing_variables)};"] if ode.missing_variables else []),
|
|
90
92
|
codegen.initial_parameter_values(),
|
|
91
93
|
codegen.initial_state_values(),
|
|
92
94
|
codegen.rhs(),
|
|
@@ -104,10 +106,13 @@ def main(
|
|
|
104
106
|
remove_unused: bool = False,
|
|
105
107
|
remove_singularities: bool = True,
|
|
106
108
|
verbose: bool = False,
|
|
109
|
+
remove_component: list[str] | None = None,
|
|
110
|
+
keep_component: str | None = None,
|
|
107
111
|
) -> None:
|
|
108
112
|
loglevel = logging.DEBUG if verbose else logging.INFO
|
|
109
113
|
structlog.configure(wrapper_class=structlog.make_filtering_bound_logger(loglevel))
|
|
110
114
|
ode = load_ode(fname, remove_singularities=remove_singularities)
|
|
115
|
+
ode = select_components(ode, remove=remove_component, keep=keep_component)
|
|
111
116
|
code = get_code(
|
|
112
117
|
ode,
|
|
113
118
|
model_name=model_name,
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
7
|
from ..codegen import CodeGenerator
|
|
8
|
+
from ..ode import ODE
|
|
8
9
|
from ..schemes import Scheme, get_scheme
|
|
9
10
|
|
|
10
11
|
|
|
@@ -29,14 +30,34 @@ def add_schemes(
|
|
|
29
30
|
return comp
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
def _toml_loads(text: str) -> dict[str, Any]:
|
|
34
|
+
try:
|
|
35
|
+
import tomllib # Python >= 3.11
|
|
36
|
+
except ImportError:
|
|
37
|
+
import toml as tomllib # type: ignore
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
return tomllib.loads(text)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def find_pyproject_toml_config(start: Path | None = None) -> Path | None:
|
|
43
|
+
"""The nearest `pyproject.toml` with a `[tool.gotranx]` table, searching from
|
|
44
|
+
`start` (by default the current directory) up to the root of the file system.
|
|
45
|
+
|
|
46
|
+
A `pyproject.toml` that cannot be parsed is returned too, so that
|
|
47
|
+
`read_config` reports it rather than skipping it silently.
|
|
48
|
+
"""
|
|
49
|
+
start = Path.cwd() if start is None else start
|
|
50
|
+
for directory in (start, *start.parents):
|
|
51
|
+
path = directory / "pyproject.toml"
|
|
52
|
+
if not path.is_file():
|
|
53
|
+
continue
|
|
54
|
+
try:
|
|
55
|
+
config = _toml_loads(path.read_text())
|
|
56
|
+
except Exception:
|
|
57
|
+
return path
|
|
58
|
+
if "gotranx" in config.get("tool", {}):
|
|
59
|
+
return path
|
|
60
|
+
return None
|
|
40
61
|
|
|
41
62
|
|
|
42
63
|
def read_config(path: Path | None) -> dict[str, Any]:
|
|
@@ -50,20 +71,8 @@ def read_config(path: Path | None) -> dict[str, Any]:
|
|
|
50
71
|
if path is None:
|
|
51
72
|
return {}
|
|
52
73
|
|
|
53
|
-
# Try to read the configuration file
|
|
54
74
|
try:
|
|
55
|
-
|
|
56
|
-
import tomllib as toml
|
|
57
|
-
except ImportError:
|
|
58
|
-
# If tomllib is not available, try to use toml
|
|
59
|
-
try:
|
|
60
|
-
import toml # type: ignore
|
|
61
|
-
except ImportError:
|
|
62
|
-
typer.echo("Please install 'tomllib' or 'toml' to read configuration files")
|
|
63
|
-
return {}
|
|
64
|
-
|
|
65
|
-
try:
|
|
66
|
-
config = toml.loads(Path(path).read_text())
|
|
75
|
+
config = _toml_loads(Path(path).read_text())
|
|
67
76
|
except Exception:
|
|
68
77
|
typer.echo(f"Could not read configuration file {path}")
|
|
69
78
|
return {}
|
|
@@ -109,3 +118,29 @@ def validate_scheme(scheme: list[Scheme] | list[str]) -> list[Scheme]:
|
|
|
109
118
|
else:
|
|
110
119
|
lst.append(s)
|
|
111
120
|
return lst
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def select_components(ode: ODE, remove: list[str] | None = None, keep: str | None = None) -> ODE:
|
|
124
|
+
"""The part of `ode` to generate code for: without the components in `remove`
|
|
125
|
+
(`ode - ode.get_component(name)` for each), or only the component `keep`
|
|
126
|
+
(`ode.get_component(keep).to_ode()`). Names the parts share become missing
|
|
127
|
+
variables of the result."""
|
|
128
|
+
if remove and keep is not None:
|
|
129
|
+
raise typer.BadParameter("give --remove-component or --keep-component, not both")
|
|
130
|
+
names = [c.name for c in ode.components]
|
|
131
|
+
for name in [*(remove or []), *([keep] if keep is not None else [])]:
|
|
132
|
+
if name not in names:
|
|
133
|
+
raise typer.BadParameter(
|
|
134
|
+
f"no component {name!r} in {ode.name}; it has {', '.join(names)}",
|
|
135
|
+
param_hint="--remove-component" if keep is None else "--keep-component",
|
|
136
|
+
)
|
|
137
|
+
if keep is not None:
|
|
138
|
+
return ode.get_component(keep).to_ode()
|
|
139
|
+
if not remove:
|
|
140
|
+
return ode
|
|
141
|
+
# `ode - component` is named "<ode> - <component>", which is not a valid default
|
|
142
|
+
# model name (ode2cytozoo derives the struct name from it).
|
|
143
|
+
name = f"{ode.name}_without_{'_'.join(remove)}"
|
|
144
|
+
for comp in remove:
|
|
145
|
+
ode = ode - ode.get_component(comp)
|
|
146
|
+
return ODE(components=ode.components, t=ode.t, name=name, comments=ode.comments)
|
|
@@ -106,6 +106,8 @@ class Shape(str, Enum):
|
|
|
106
106
|
|
|
107
107
|
class CodeGenerator(abc.ABC):
|
|
108
108
|
variable_prefix = ""
|
|
109
|
+
#: How a function declares its missing-variables argument (C needs a type).
|
|
110
|
+
missing_variables_argument = "missing_variables"
|
|
109
111
|
|
|
110
112
|
def __init__(
|
|
111
113
|
self,
|
|
@@ -321,7 +323,7 @@ class CodeGenerator(abc.ABC):
|
|
|
321
323
|
|
|
322
324
|
arguments = rhs.arguments
|
|
323
325
|
if self._missing_variables:
|
|
324
|
-
arguments += [
|
|
326
|
+
arguments += [self.missing_variables_argument]
|
|
325
327
|
|
|
326
328
|
values_lst = []
|
|
327
329
|
index = 0
|
|
@@ -383,7 +385,7 @@ class CodeGenerator(abc.ABC):
|
|
|
383
385
|
|
|
384
386
|
arguments = rhs.arguments
|
|
385
387
|
if self._missing_variables:
|
|
386
|
-
arguments += [
|
|
388
|
+
arguments += [self.missing_variables_argument]
|
|
387
389
|
|
|
388
390
|
values_lst = []
|
|
389
391
|
index = 0
|
|
@@ -429,7 +431,7 @@ class CodeGenerator(abc.ABC):
|
|
|
429
431
|
|
|
430
432
|
arguments = rhs.arguments
|
|
431
433
|
if self._missing_variables:
|
|
432
|
-
arguments += [
|
|
434
|
+
arguments += [self.missing_variables_argument]
|
|
433
435
|
|
|
434
436
|
values_lst = []
|
|
435
437
|
N = len(values)
|
|
@@ -495,7 +497,7 @@ class CodeGenerator(abc.ABC):
|
|
|
495
497
|
|
|
496
498
|
arguments = rhs.arguments
|
|
497
499
|
if self._missing_variables:
|
|
498
|
-
arguments += [
|
|
500
|
+
arguments += [self.missing_variables_argument]
|
|
499
501
|
|
|
500
502
|
dt = sympy.Symbol("dt")
|
|
501
503
|
eqs = f(
|
|
@@ -81,6 +81,7 @@ class GotranCCodePrinter(C99CodePrinter):
|
|
|
81
81
|
|
|
82
82
|
class CCodeGenerator(CodeGenerator):
|
|
83
83
|
variable_prefix = "const double "
|
|
84
|
+
missing_variables_argument = "const double *__restrict missing_variables"
|
|
84
85
|
|
|
85
86
|
def __init__(
|
|
86
87
|
self, ode: ODE, format: Format = Format.clang_format, remove_unused: bool = False
|
|
@@ -68,7 +68,10 @@ class CytoZooCodeGenerator(JuliaCodeGenerator):
|
|
|
68
68
|
return [s.name for s in self.ode.sorted_states()]
|
|
69
69
|
|
|
70
70
|
def _parameter_name_list(self) -> list[str]:
|
|
71
|
-
|
|
71
|
+
# The missing variables follow the parameters, in missing_index order, because
|
|
72
|
+
# the adapter stores them in the parameter vector (templates.cytozoo.model_struct).
|
|
73
|
+
missing = sorted(self._missing_variables, key=self._missing_variables.__getitem__)
|
|
74
|
+
return [p.name for p in self.ode.parameters] + missing
|
|
72
75
|
|
|
73
76
|
def _monitor_name_list(self) -> list[str]:
|
|
74
77
|
# Mirror codegen.base.CodeGenerator.monitor_index (base.py:186) exactly:
|
|
@@ -142,18 +145,38 @@ class CytoZooCodeGenerator(JuliaCodeGenerator):
|
|
|
142
145
|
)
|
|
143
146
|
return "\n".join(lines)
|
|
144
147
|
|
|
148
|
+
def _missing_variables_assignments(self) -> str:
|
|
149
|
+
"""Every missing-variable local, converted to the state's element type.
|
|
150
|
+
|
|
151
|
+
The missing variables are trailing entries of the parameter vector (see
|
|
152
|
+
`templates.cytozoo.model_struct`), so they are converted like the parameters:
|
|
153
|
+
a model built in Float64 and evaluated on Float32 states would otherwise
|
|
154
|
+
compute every term that reads one in Float64.
|
|
155
|
+
"""
|
|
156
|
+
if not self._missing_variables:
|
|
157
|
+
return ""
|
|
158
|
+
lines = [
|
|
159
|
+
f"{name} = TYPE(missing_variables[{index + 1}])"
|
|
160
|
+
for name, index in self._missing_variables.items()
|
|
161
|
+
]
|
|
162
|
+
return "\n".join(["", self._comment("Assign missing variables"), *lines, ""])
|
|
163
|
+
|
|
145
164
|
def adapter(self) -> str:
|
|
146
165
|
"""The struct, name tuples, index lookups and interface methods."""
|
|
147
166
|
name = self._model_name
|
|
148
167
|
monitors = self._monitor_name_list()
|
|
149
168
|
parts = [
|
|
150
|
-
templates.cytozoo.model_struct(
|
|
169
|
+
templates.cytozoo.model_struct(
|
|
170
|
+
name, len(self.ode.parameters), len(self._missing_variables)
|
|
171
|
+
),
|
|
151
172
|
templates.cytozoo.name_tuples(
|
|
152
173
|
name, self._state_name_list(), self._parameter_name_list(), monitors
|
|
153
174
|
),
|
|
154
175
|
templates.cytozoo.index_lookups(
|
|
155
176
|
name, self._state_name_list(), self._parameter_name_list()
|
|
156
177
|
),
|
|
157
|
-
templates.cytozoo.interface_methods(
|
|
178
|
+
templates.cytozoo.interface_methods(
|
|
179
|
+
name, self._potential_index(), len(monitors), len(self._missing_variables)
|
|
180
|
+
),
|
|
158
181
|
]
|
|
159
182
|
return self._format("\n".join(parts))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
import functools
|
|
2
3
|
import typing
|
|
3
4
|
from enum import Enum
|
|
4
5
|
from sympy.printing.pycode import PythonCodePrinter
|
|
@@ -46,6 +47,15 @@ class GotranPythonCodePrinter(PythonCodePrinter):
|
|
|
46
47
|
def _print_Float(self, flt):
|
|
47
48
|
return self._print(str(float(flt)))
|
|
48
49
|
|
|
50
|
+
def _print_Min(self, expr):
|
|
51
|
+
# numpy.min(a, b) would read b as an axis; numpy.minimum takes two arrays
|
|
52
|
+
args = [self._print(arg) for arg in expr.args]
|
|
53
|
+
return functools.reduce(lambda x, y: f"numpy.minimum({x}, {y})", args)
|
|
54
|
+
|
|
55
|
+
def _print_Max(self, expr):
|
|
56
|
+
args = [self._print(arg) for arg in expr.args]
|
|
57
|
+
return functools.reduce(lambda x, y: f"numpy.maximum({x}, {y})", args)
|
|
58
|
+
|
|
49
59
|
def _print_Piecewise(self, expr):
|
|
50
60
|
result = []
|
|
51
61
|
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from collections import defaultdict
|
|
3
|
+
import functools
|
|
3
4
|
import re
|
|
4
5
|
import warnings
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
from typing import TYPE_CHECKING, overload
|
|
7
9
|
from collections.abc import Sequence
|
|
8
10
|
|
|
9
11
|
import sympy as sp
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
if TYPE_CHECKING:
|
|
12
14
|
import myokit
|
|
13
|
-
import myokit.formats.sympy
|
|
14
|
-
import myokit.lib.guess
|
|
15
|
-
import myokit.formats.cellml
|
|
16
|
-
except ImportError:
|
|
17
|
-
myokit = None
|
|
18
15
|
|
|
19
16
|
from . import atoms
|
|
20
17
|
from .ode_component import BaseComponent, MyokitComponent
|
|
@@ -24,9 +21,24 @@ from .exceptions import GotranxImportError
|
|
|
24
21
|
reserved_names = {name for name in dir(sp) if not name.startswith("_")}
|
|
25
22
|
|
|
26
23
|
|
|
24
|
+
def _import_myokit() -> ModuleType:
|
|
25
|
+
"""myokit, with the submodules used here, imported by the functions that need it.
|
|
26
|
+
|
|
27
|
+
Importing myokit imports numpy and lxml, which only reading and writing CellML
|
|
28
|
+
and `.mmt` files needs, so `import gotranx` leaves it alone.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
import myokit
|
|
32
|
+
import myokit.formats.cellml
|
|
33
|
+
import myokit.formats.sympy
|
|
34
|
+
import myokit.lib.guess
|
|
35
|
+
except ImportError as e:
|
|
36
|
+
raise GotranxImportError("myokit") from e
|
|
37
|
+
return myokit
|
|
38
|
+
|
|
39
|
+
|
|
27
40
|
def assert_has_myokit():
|
|
28
|
-
|
|
29
|
-
raise GotranxImportError("myokit")
|
|
41
|
+
_import_myokit()
|
|
30
42
|
|
|
31
43
|
|
|
32
44
|
_INVALID_NAME_CHARS = re.compile(r"\W+")
|
|
@@ -223,7 +235,7 @@ def mmt_to_gotran(filename: str | Path) -> ODE:
|
|
|
223
235
|
gotranx.ode.ODE
|
|
224
236
|
The gotran ODE
|
|
225
237
|
"""
|
|
226
|
-
|
|
238
|
+
myokit = _import_myokit()
|
|
227
239
|
model, protocol, _ = myokit.load(filename)
|
|
228
240
|
|
|
229
241
|
return myokit_to_gotran(model, protocol=protocol)
|
|
@@ -244,7 +256,7 @@ def myokit_to_gotran(model: "myokit.Model", protocol=None) -> ODE:
|
|
|
244
256
|
gotranx.ode.ODE
|
|
245
257
|
The gotran ODE
|
|
246
258
|
"""
|
|
247
|
-
|
|
259
|
+
myokit = _import_myokit()
|
|
248
260
|
# Embed protocol
|
|
249
261
|
if protocol is not None:
|
|
250
262
|
model = model.clone()
|
|
@@ -346,6 +358,47 @@ def myokit_to_gotran(model: "myokit.Model", protocol=None) -> ODE:
|
|
|
346
358
|
)
|
|
347
359
|
|
|
348
360
|
|
|
361
|
+
def _sympy_reader(model: "myokit.Model") -> "myokit.formats.sympy.SymPyExpressionReader":
|
|
362
|
+
"""myokit's sympy expression reader, extended to read ``Min``, ``Max`` and n-ary
|
|
363
|
+
``And``/``Or``.
|
|
364
|
+
|
|
365
|
+
myokit has no min or max, and its ``And`` and ``Or`` take exactly two operands where sympy's
|
|
366
|
+
take any number. Rewriting ``Min``/``Max`` as sympy ``Piecewise`` first does not work: a
|
|
367
|
+
``Piecewise`` nested in a condition, as in the clamp ``Min(Max(x, 0), 1)``, becomes an ``ITE``,
|
|
368
|
+
which the reader cannot read either, and four or more arguments produce an n-ary ``And``. So
|
|
369
|
+
each ``Min``/``Max`` is read into nested myokit ``If`` expressions, and ``And``/``Or`` are
|
|
370
|
+
folded into nested binary ones.
|
|
371
|
+
"""
|
|
372
|
+
_import_myokit()
|
|
373
|
+
# An import statement rather than `myokit = _import_myokit()`, so that mypy can
|
|
374
|
+
# resolve the base class below.
|
|
375
|
+
import myokit.formats.sympy
|
|
376
|
+
|
|
377
|
+
class Reader(myokit.formats.sympy.SymPyExpressionReader):
|
|
378
|
+
def _build_op_map(self):
|
|
379
|
+
op_map = super()._build_op_map()
|
|
380
|
+
op_map[sp.Min] = self._ex_min
|
|
381
|
+
op_map[sp.Max] = self._ex_max
|
|
382
|
+
return op_map
|
|
383
|
+
|
|
384
|
+
def _fold(self, e, combine):
|
|
385
|
+
return functools.reduce(combine, [self.ex(arg) for arg in e.args])
|
|
386
|
+
|
|
387
|
+
def _ex_min(self, e):
|
|
388
|
+
return self._fold(e, lambda a, b: myokit.If(myokit.LessEqual(a, b), a, b))
|
|
389
|
+
|
|
390
|
+
def _ex_max(self, e):
|
|
391
|
+
return self._fold(e, lambda a, b: myokit.If(myokit.MoreEqual(a, b), a, b))
|
|
392
|
+
|
|
393
|
+
def _ex_and(self, e):
|
|
394
|
+
return self._fold(e, myokit.And)
|
|
395
|
+
|
|
396
|
+
def _ex_or(self, e):
|
|
397
|
+
return self._fold(e, myokit.Or)
|
|
398
|
+
|
|
399
|
+
return Reader(model=model)
|
|
400
|
+
|
|
401
|
+
|
|
349
402
|
def gotran_to_myokit(ode: ODE, time_component="engine", time_unit="s") -> "myokit.Model":
|
|
350
403
|
"""Convert a gotran ODE to myokit model
|
|
351
404
|
|
|
@@ -359,7 +412,7 @@ def gotran_to_myokit(ode: ODE, time_component="engine", time_unit="s") -> "myoki
|
|
|
359
412
|
myokit.Model
|
|
360
413
|
The myokit model
|
|
361
414
|
"""
|
|
362
|
-
|
|
415
|
+
myokit = _import_myokit()
|
|
363
416
|
model = myokit.Model(ode.name)
|
|
364
417
|
model.meta["author"] = "GotranX API"
|
|
365
418
|
comp = model.add_component(time_component)
|
|
@@ -420,7 +473,7 @@ def gotran_to_myokit(ode: ODE, time_component="engine", time_unit="s") -> "myoki
|
|
|
420
473
|
var.set_unit(to_myokit_unit(intermediate.unit_str))
|
|
421
474
|
global_var_map[intermediate.symbol] = sp.Symbol(var.qname())
|
|
422
475
|
|
|
423
|
-
sympy_reader =
|
|
476
|
+
sympy_reader = _sympy_reader(model)
|
|
424
477
|
# Then we can add expressions
|
|
425
478
|
for component in ode.components:
|
|
426
479
|
comp = model[component_name_map[component.name]]
|
|
@@ -468,7 +521,7 @@ def cellml_to_gotran(filename: str | Path) -> ODE:
|
|
|
468
521
|
gotranx.ode.ODE
|
|
469
522
|
The gotran ODE
|
|
470
523
|
"""
|
|
471
|
-
|
|
524
|
+
myokit = _import_myokit()
|
|
472
525
|
myokit_model = myokit.formats.cellml.CellMLImporter().model(filename)
|
|
473
526
|
return myokit_to_gotran(myokit_model)
|
|
474
527
|
|
|
@@ -483,6 +536,6 @@ def gotran_to_cellml(ode: ODE, filename: str | Path) -> None:
|
|
|
483
536
|
filename : str | Path
|
|
484
537
|
The filename of the cellml file
|
|
485
538
|
"""
|
|
486
|
-
|
|
539
|
+
myokit = _import_myokit()
|
|
487
540
|
model = gotran_to_myokit(ode)
|
|
488
541
|
myokit.formats.cellml.CellMLExporter().model(filename, model, version="1.0")
|
|
@@ -55,6 +55,8 @@ variable: VARIABLE
|
|
|
55
55
|
| ABSL
|
|
56
56
|
| FLOOR
|
|
57
57
|
| MOD
|
|
58
|
+
| MIN
|
|
59
|
+
| MAX
|
|
58
60
|
|
|
59
61
|
?logicalfunc: logicalfuncname "(" expression ("," expression)* (",")? ")"
|
|
60
62
|
?logicalfuncname: CONTINUOUSCONDITIONAL
|
|
@@ -93,6 +95,8 @@ OR: "Or"
|
|
|
93
95
|
EQ: "Eq"
|
|
94
96
|
NOT: "Not"
|
|
95
97
|
MOD: "Mod"
|
|
98
|
+
MIN: "Min"
|
|
99
|
+
MAX: "Max"
|
|
96
100
|
|
|
97
101
|
|
|
98
102
|
PI: "pi"
|
|
@@ -300,7 +300,7 @@ def _depending_on(
|
|
|
300
300
|
"""Cached front for :func:`_compute_depending_on`.
|
|
301
301
|
|
|
302
302
|
Called once per (assignment, variable) pair against the same model, so
|
|
303
|
-
recomputing the
|
|
303
|
+
recomputing the dependents every time dominated the scan. The cache holds
|
|
304
304
|
only the most recent ``definitions`` object and compares by identity, so it
|
|
305
305
|
can never serve a stale answer for a different model.
|
|
306
306
|
"""
|
|
@@ -313,25 +313,47 @@ def _depending_on(
|
|
|
313
313
|
return set(by_seeds[key])
|
|
314
314
|
|
|
315
315
|
|
|
316
|
+
_USERS_CACHE: list = []
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _users(
|
|
320
|
+
definitions: Mapping[sympy.Symbol, sympy.Expr],
|
|
321
|
+
) -> dict[sympy.Symbol, list[sympy.Symbol]]:
|
|
322
|
+
"""Every symbol mapped to the symbols whose definitions use it.
|
|
323
|
+
|
|
324
|
+
The reverse of ``definitions``' dependency graph. Built once per
|
|
325
|
+
``definitions`` object and cached by identity, as :func:`_depending_on`
|
|
326
|
+
caches its answers, since a model asks for the dependents of every state.
|
|
327
|
+
"""
|
|
328
|
+
if not _USERS_CACHE or _USERS_CACHE[0] is not definitions:
|
|
329
|
+
users: dict[sympy.Symbol, list[sympy.Symbol]] = {}
|
|
330
|
+
for symbol, definition in definitions.items():
|
|
331
|
+
for used in definition.free_symbols:
|
|
332
|
+
users.setdefault(used, []).append(symbol)
|
|
333
|
+
_USERS_CACHE[:] = [definitions, users]
|
|
334
|
+
return _USERS_CACHE[1]
|
|
335
|
+
|
|
336
|
+
|
|
316
337
|
def _compute_depending_on(
|
|
317
338
|
definitions: Mapping[sympy.Symbol, sympy.Expr],
|
|
318
339
|
seeds: set[sympy.Symbol],
|
|
319
340
|
) -> set[sympy.Symbol]:
|
|
320
341
|
"""Symbols in ``definitions`` that transitively depend on any of ``seeds``.
|
|
321
342
|
|
|
322
|
-
|
|
323
|
-
|
|
343
|
+
Walks the reverse dependency graph (:func:`_users`) out from the seeds, so
|
|
344
|
+
the cost is linear in the graph whatever order ``definitions`` is in. A
|
|
345
|
+
fixed-point sweep over ``definitions``, which is not guaranteed to be
|
|
346
|
+
topologically sorted, cost a pass per level of dependency for every set of
|
|
347
|
+
seeds, which made the scan quadratic in the size of a model.
|
|
324
348
|
"""
|
|
349
|
+
users = _users(definitions)
|
|
325
350
|
dependent: set[sympy.Symbol] = set()
|
|
326
|
-
|
|
327
|
-
while
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (definition.free_symbols & seeds) or (definition.free_symbols & dependent):
|
|
333
|
-
dependent.add(symbol)
|
|
334
|
-
changed = True
|
|
351
|
+
stack = list(seeds)
|
|
352
|
+
while stack:
|
|
353
|
+
for user in users.get(stack.pop(), ()):
|
|
354
|
+
if user not in dependent:
|
|
355
|
+
dependent.add(user)
|
|
356
|
+
stack.append(user)
|
|
335
357
|
return dependent
|
|
336
358
|
|
|
337
359
|
|
|
@@ -441,7 +463,15 @@ def _numeric(expr: sympy.Expr, defaults: Mapping[sympy.Symbol, float]) -> float
|
|
|
441
463
|
None means "not usable as a guard location or as a check point": complex,
|
|
442
464
|
infinite, NaN, or still carrying a free symbol ``defaults`` does not cover.
|
|
443
465
|
"""
|
|
444
|
-
|
|
466
|
+
# Only the expression's own symbols: converting every default in the model
|
|
467
|
+
# on every call made evaluating all of a model's defaults quadratic.
|
|
468
|
+
substituted = expr.xreplace(
|
|
469
|
+
{
|
|
470
|
+
symbol: sympy.Float(defaults[symbol])
|
|
471
|
+
for symbol in expr.free_symbols
|
|
472
|
+
if symbol in defaults
|
|
473
|
+
}
|
|
474
|
+
)
|
|
445
475
|
try:
|
|
446
476
|
value = complex(substituted.evalf())
|
|
447
477
|
except (TypeError, ValueError, AttributeError):
|
|
@@ -1081,7 +1111,7 @@ def default_values(
|
|
|
1081
1111
|
"""
|
|
1082
1112
|
values = dict(base)
|
|
1083
1113
|
for symbol, definition in definitions.items():
|
|
1084
|
-
if not definition.free_symbols <=
|
|
1114
|
+
if not definition.free_symbols <= values.keys():
|
|
1085
1115
|
continue
|
|
1086
1116
|
try:
|
|
1087
1117
|
value = _numeric(definition, values)
|
|
@@ -45,11 +45,13 @@ def method(
|
|
|
45
45
|
values: str,
|
|
46
46
|
return_name: None = None,
|
|
47
47
|
num_return_values: int = 0,
|
|
48
|
+
missing_variables: str = "",
|
|
48
49
|
**kwargs,
|
|
49
50
|
):
|
|
50
51
|
indent_states = indent(states, " ")
|
|
51
52
|
indent_parameters = indent(parameters, " ")
|
|
52
53
|
indent_values = indent(values, " ")
|
|
54
|
+
indent_missing_variables = indent(missing_variables, " ")
|
|
53
55
|
return dedent(
|
|
54
56
|
f"""
|
|
55
57
|
void {name}({args}){{
|
|
@@ -59,7 +61,7 @@ void {name}({args}){{
|
|
|
59
61
|
|
|
60
62
|
// Assign parameters
|
|
61
63
|
{indent_parameters}
|
|
62
|
-
|
|
64
|
+
{indent_missing_variables}
|
|
63
65
|
// Assign expressions
|
|
64
66
|
{indent_values}
|
|
65
67
|
}}
|
|
@@ -102,4 +104,4 @@ def monitor_index(data: dict[str, int]) -> str:
|
|
|
102
104
|
|
|
103
105
|
|
|
104
106
|
def missing_index(data: dict[str, int]) -> str:
|
|
105
|
-
return method_index(data, "
|
|
107
|
+
return method_index(data, "missing")
|