gotranx 2.2.0__tar.gz → 2.3.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.2.0/src/gotranx.egg-info → gotranx-2.3.0}/PKG-INFO +7 -2
- {gotranx-2.2.0 → gotranx-2.3.0}/README.md +6 -1
- {gotranx-2.2.0 → gotranx-2.3.0}/pyproject.toml +2 -2
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/__init__.py +65 -1
- gotranx-2.3.0/src/gotranx/cli/gotran2cytozoo.py +121 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/__init__.py +3 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/base.py +12 -0
- gotranx-2.3.0/src/gotranx/codegen/cytozoo.py +159 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/julia.py +23 -9
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/exceptions.py +28 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/__init__.py +2 -1
- gotranx-2.3.0/src/gotranx/templates/cytozoo.py +123 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/julia.py +19 -4
- {gotranx-2.2.0 → gotranx-2.3.0/src/gotranx.egg-info}/PKG-INFO +7 -2
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx.egg-info/SOURCES.txt +4 -0
- gotranx-2.3.0/tests/test_cytozoo_codegen.py +488 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_determinism.py +34 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_julia_codegen.py +65 -1
- {gotranx-2.2.0 → gotranx-2.3.0}/LICENSE +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/setup.cfg +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/__init__.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/__main__.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/_enum.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/atoms.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/cellml2ode.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/gotran2c.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/gotran2julia.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/gotran2md.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/gotran2mtk.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/gotran2py.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/gotran2ufl.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/ode2cellml.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/utils.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/c.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/jax.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/markdown.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/mtk.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/ode.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/python.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/ufl.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/expressions.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/linearization.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/load.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/myokit.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/ode.lark +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/ode.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/ode_component.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/parser.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/py.typed +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/save.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/schemes.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/singularities.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/sympytools.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/c.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/jax.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/markdown.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/mtk.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/python.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/ufl.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/transformer.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/units.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx.egg-info/dependency_links.txt +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx.egg-info/entry_points.txt +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx.egg-info/not-zip-safe +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx.egg-info/requires.txt +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx.egg-info/top_level.txt +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_atoms.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_benchmarks.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_c_codegen.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_cli.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_comments.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_conditionals.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_deep_linearization_models.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_demos.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_expressions.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_linearization.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_load.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_markdown_codegen.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_mtk_codegen.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_myokit.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_ode.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_ode_component.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_operations.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_python_codegen.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_save.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_schemes.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_singularities.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_singularity_models.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_subodes.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_sympytools.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_transformer_assignments.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_transformer_parameters.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_transformer_states.py +0 -0
- {gotranx-2.2.0 → gotranx-2.3.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.3.0
|
|
4
4
|
Summary: A declarative language describing ordinary differential equations
|
|
5
5
|
Author-email: Henrik Finsberg <henriknf@simula.no>
|
|
6
6
|
License: MIT
|
|
@@ -116,7 +116,12 @@ with the initial conditions $x(0) = 1$ and $y(0) = 0$ and the parameter $a$ with
|
|
|
116
116
|
```
|
|
117
117
|
gotranx ode2py file.ode --scheme explicit_euler -o file.py
|
|
118
118
|
```
|
|
119
|
-
which will create a file `file.py` containing functions for solving the ODE.
|
|
119
|
+
which will create a file `file.py` containing functions for solving the ODE.
|
|
120
|
+
The same ODE can be turned into C (`ode2c`), Julia (`ode2julia`), a
|
|
121
|
+
ModelingToolkit system (`ode2mtk`), UFL (`ode2ufl`), Markdown (`ode2md`),
|
|
122
|
+
CellML (`ode2cellml`) or a [CytoZoo](https://github.com/finsberg/CytoZoo.jl)
|
|
123
|
+
cell-model adapter (`ode2cytozoo` -- whose output needs a CytoZoo providing
|
|
124
|
+
`resolve_parameter`, not yet released). Now you can solve the ode using the following code snippet
|
|
120
125
|
|
|
121
126
|
```python
|
|
122
127
|
import file as model
|
|
@@ -57,7 +57,12 @@ with the initial conditions $x(0) = 1$ and $y(0) = 0$ and the parameter $a$ with
|
|
|
57
57
|
```
|
|
58
58
|
gotranx ode2py file.ode --scheme explicit_euler -o file.py
|
|
59
59
|
```
|
|
60
|
-
which will create a file `file.py` containing functions for solving the ODE.
|
|
60
|
+
which will create a file `file.py` containing functions for solving the ODE.
|
|
61
|
+
The same ODE can be turned into C (`ode2c`), Julia (`ode2julia`), a
|
|
62
|
+
ModelingToolkit system (`ode2mtk`), UFL (`ode2ufl`), Markdown (`ode2md`),
|
|
63
|
+
CellML (`ode2cellml`) or a [CytoZoo](https://github.com/finsberg/CytoZoo.jl)
|
|
64
|
+
cell-model adapter (`ode2cytozoo` -- whose output needs a CytoZoo providing
|
|
65
|
+
`resolve_parameter`, not yet released). Now you can solve the ode using the following code snippet
|
|
61
66
|
|
|
62
67
|
```python
|
|
63
68
|
import file as model
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gotranx"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.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"}
|
|
@@ -172,7 +172,7 @@ tag = true
|
|
|
172
172
|
sign_tags = false
|
|
173
173
|
tag_name = "v{new_version}"
|
|
174
174
|
tag_message = "Bump version: {current_version} → {new_version}"
|
|
175
|
-
current_version = "2.
|
|
175
|
+
current_version = "2.3.0"
|
|
176
176
|
|
|
177
177
|
|
|
178
178
|
[[tool.bumpversion.files]]
|
|
@@ -8,7 +8,7 @@ import typer
|
|
|
8
8
|
from ..schemes import Scheme, get_scheme
|
|
9
9
|
from ..codegen import PythonFormat, CFormat
|
|
10
10
|
from ..codegen.base import Shape
|
|
11
|
-
from . import gotran2c, gotran2py, gotran2julia, gotran2md, gotran2mtk, gotran2ufl
|
|
11
|
+
from . import gotran2c, gotran2py, gotran2julia, gotran2cytozoo, gotran2md, gotran2mtk, gotran2ufl
|
|
12
12
|
from . import utils
|
|
13
13
|
|
|
14
14
|
app = typer.Typer()
|
|
@@ -659,6 +659,70 @@ def ode2julia(
|
|
|
659
659
|
)
|
|
660
660
|
|
|
661
661
|
|
|
662
|
+
@app.command()
|
|
663
|
+
def ode2cytozoo(
|
|
664
|
+
fname: typing.Optional[Path] = typer.Argument(
|
|
665
|
+
None, exists=True, file_okay=True, dir_okay=False, readable=True, resolve_path=True
|
|
666
|
+
),
|
|
667
|
+
outname: typing.Optional[str] = typer.Option(None, "-o", "--outname", help="Output name"),
|
|
668
|
+
model_name: typing.Optional[str] = typer.Option(
|
|
669
|
+
None, "--model-name", help="Julia struct name (default: the ODE name in PascalCase)"
|
|
670
|
+
),
|
|
671
|
+
v_name: str = typer.Option("v", "--v-name", help="State holding the transmembrane potential"),
|
|
672
|
+
remove_unused: bool = typer.Option(False, "--remove-unused", help="Remove unused variables"),
|
|
673
|
+
remove_singularities: bool = typer.Option(
|
|
674
|
+
True,
|
|
675
|
+
"--remove-singularities/--no-remove-singularities",
|
|
676
|
+
help=(
|
|
677
|
+
"Replace a small neighborhood of every removable singularity "
|
|
678
|
+
"(e.g. x/(exp(x) - 1) at x = 0) with a truncated Taylor series"
|
|
679
|
+
),
|
|
680
|
+
),
|
|
681
|
+
version: bool = typer.Option(
|
|
682
|
+
None,
|
|
683
|
+
"--version",
|
|
684
|
+
callback=version_callback,
|
|
685
|
+
is_eager=True,
|
|
686
|
+
help="Show version",
|
|
687
|
+
),
|
|
688
|
+
license: bool = typer.Option(
|
|
689
|
+
None,
|
|
690
|
+
"--license",
|
|
691
|
+
callback=license_callback,
|
|
692
|
+
is_eager=True,
|
|
693
|
+
help="Show license",
|
|
694
|
+
),
|
|
695
|
+
config: typing.Optional[Path] = typer.Option(
|
|
696
|
+
None,
|
|
697
|
+
"-c",
|
|
698
|
+
"--config",
|
|
699
|
+
help="Read configuration options from a configuration file",
|
|
700
|
+
),
|
|
701
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
|
702
|
+
):
|
|
703
|
+
if fname is None:
|
|
704
|
+
return typer.echo("No file specified")
|
|
705
|
+
|
|
706
|
+
config_data = utils.read_config(config)
|
|
707
|
+
verbose = config_data.get("verbose", verbose)
|
|
708
|
+
remove_singularities = utils.validate_remove_singularities(
|
|
709
|
+
config_data.get("remove_singularities", remove_singularities)
|
|
710
|
+
)
|
|
711
|
+
cytozoo_config = config_data.get("cytozoo", {})
|
|
712
|
+
model_name = cytozoo_config.get("model_name", model_name)
|
|
713
|
+
v_name = cytozoo_config.get("v_name", v_name)
|
|
714
|
+
|
|
715
|
+
gotran2cytozoo.main(
|
|
716
|
+
fname=fname,
|
|
717
|
+
outname=outname,
|
|
718
|
+
model_name=model_name,
|
|
719
|
+
v_name=v_name,
|
|
720
|
+
remove_unused=remove_unused,
|
|
721
|
+
remove_singularities=remove_singularities,
|
|
722
|
+
verbose=verbose,
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
|
|
662
726
|
@app.command()
|
|
663
727
|
def ode2mtk(
|
|
664
728
|
fname: typing.Optional[Path] = typer.Argument(
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import logging
|
|
4
|
+
import structlog
|
|
5
|
+
|
|
6
|
+
from ..codegen.cytozoo import CytoZooCodeGenerator
|
|
7
|
+
from ..load import load_ode
|
|
8
|
+
from ..ode import ODE
|
|
9
|
+
|
|
10
|
+
logger = structlog.get_logger()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _banner(source: str) -> str:
|
|
14
|
+
"""The provenance header every generated module carries.
|
|
15
|
+
|
|
16
|
+
Hand-written headers in the consuming repo are deleted the moment anyone runs
|
|
17
|
+
the documented regeneration command, so the generator has to emit its own.
|
|
18
|
+
"""
|
|
19
|
+
# Deferred: gotranx/__init__.py imports this package, so a module-level
|
|
20
|
+
# import of __version__ would be circular.
|
|
21
|
+
from .. import __version__
|
|
22
|
+
|
|
23
|
+
return "\n".join(
|
|
24
|
+
[
|
|
25
|
+
f"# Generated by gotranx {__version__} from {source} -- do not hand-edit.",
|
|
26
|
+
"# Regenerate with `gotranx ode2cytozoo` rather than editing this file.",
|
|
27
|
+
"# Requires a CytoZoo providing `resolve_parameter`; no released CytoZoo",
|
|
28
|
+
"# has it yet, so this module needs a development checkout of CytoZoo.",
|
|
29
|
+
"# Top-level names here (`rhs`, `NUM_STATES`, ...) are unqualified, so",
|
|
30
|
+
"# loading several generated models in one session needs each wrapped in",
|
|
31
|
+
"# its own `module`.",
|
|
32
|
+
]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_code(
|
|
37
|
+
ode: ODE,
|
|
38
|
+
model_name: str | None = None,
|
|
39
|
+
v_name: str = "v",
|
|
40
|
+
remove_unused: bool = False,
|
|
41
|
+
source: Path | str | None = None,
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Generate the CytoZoo cell-model adapter for the ODE
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
ode : gotranx.ode.ODE
|
|
48
|
+
The ODE
|
|
49
|
+
model_name : str | None, optional
|
|
50
|
+
Name of the generated Julia struct, by default None, which uses the
|
|
51
|
+
ODE's own name in PascalCase. Must be a valid Julia identifier
|
|
52
|
+
v_name : str, optional
|
|
53
|
+
Name of the state holding the transmembrane potential, by default "v"
|
|
54
|
+
remove_unused : bool, optional
|
|
55
|
+
Remove unused variables, by default False
|
|
56
|
+
source : pathlib.Path | str | None, optional
|
|
57
|
+
The source `.ode` file, named in the provenance banner. Only its
|
|
58
|
+
basename is emitted, by default None, which falls back to the ODE's
|
|
59
|
+
name. An absolute path would otherwise leak one machine's layout into
|
|
60
|
+
a committed file
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
str
|
|
65
|
+
The CytoZoo adapter code
|
|
66
|
+
|
|
67
|
+
Raises
|
|
68
|
+
------
|
|
69
|
+
gotranx.exceptions.InvalidModelName
|
|
70
|
+
If the model name is not a valid Julia identifier
|
|
71
|
+
gotranx.exceptions.MissingPotentialState
|
|
72
|
+
If no state is named `v_name`
|
|
73
|
+
"""
|
|
74
|
+
codegen = CytoZooCodeGenerator(
|
|
75
|
+
ode, model_name=model_name, v_name=v_name, remove_unused=remove_unused, type_stable=True
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
source_name = Path(source).name if source is not None else f"{ode.name}.ode"
|
|
79
|
+
|
|
80
|
+
comp = [
|
|
81
|
+
_banner(source_name),
|
|
82
|
+
"import CytoZoo",
|
|
83
|
+
"using CytoZoo: AbstractCardiacCellModel, SpatialContext, resolve_parameter",
|
|
84
|
+
"import CytoZoo: num_states, num_parameters, transmembrane_potential_index",
|
|
85
|
+
"import CytoZoo: default_initial_state, state_index, parameter_index",
|
|
86
|
+
"import CytoZoo: state_names, parameter_names",
|
|
87
|
+
"import CytoZoo: num_monitors, monitor_names, monitor_values!",
|
|
88
|
+
f"const NUM_STATES = {len(ode.states)};",
|
|
89
|
+
f"const NUM_PARAMS = {len(ode.parameters)};",
|
|
90
|
+
codegen.initial_parameter_values(),
|
|
91
|
+
codegen.initial_state_values(),
|
|
92
|
+
codegen.rhs(),
|
|
93
|
+
codegen.monitor_values(),
|
|
94
|
+
codegen.adapter(),
|
|
95
|
+
]
|
|
96
|
+
return codegen._format("\n".join(comp))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def main(
|
|
100
|
+
fname: Path,
|
|
101
|
+
outname: Path | str | None = None,
|
|
102
|
+
model_name: str | None = None,
|
|
103
|
+
v_name: str = "v",
|
|
104
|
+
remove_unused: bool = False,
|
|
105
|
+
remove_singularities: bool = True,
|
|
106
|
+
verbose: bool = False,
|
|
107
|
+
) -> None:
|
|
108
|
+
loglevel = logging.DEBUG if verbose else logging.INFO
|
|
109
|
+
structlog.configure(wrapper_class=structlog.make_filtering_bound_logger(loglevel))
|
|
110
|
+
ode = load_ode(fname, remove_singularities=remove_singularities)
|
|
111
|
+
code = get_code(
|
|
112
|
+
ode,
|
|
113
|
+
model_name=model_name,
|
|
114
|
+
v_name=v_name,
|
|
115
|
+
remove_unused=remove_unused,
|
|
116
|
+
source=fname,
|
|
117
|
+
)
|
|
118
|
+
out = fname if outname is None else Path(outname)
|
|
119
|
+
out_name = out.with_suffix(suffix=".jl")
|
|
120
|
+
out_name.write_text(code)
|
|
121
|
+
logger.info(f"Wrote {out_name}")
|
|
@@ -10,6 +10,7 @@ from .base import CodeGenerator, Func, RHSArgument, SchemeArgument
|
|
|
10
10
|
from .ode import GotranODECodePrinter, BaseGotranODECodePrinter
|
|
11
11
|
from .julia import JuliaCodeGenerator, GotranJuliaCodePrinter
|
|
12
12
|
from .mtk import MTKCodeGenerator
|
|
13
|
+
from .cytozoo import CytoZooCodeGenerator, MissingPotentialState # noqa: F401
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
15
16
|
"base",
|
|
@@ -32,4 +33,6 @@ __all__ = [
|
|
|
32
33
|
"GotranJuliaCodePrinter",
|
|
33
34
|
"JaxCodeGenerator",
|
|
34
35
|
"MTKCodeGenerator",
|
|
36
|
+
"CytoZooCodeGenerator",
|
|
37
|
+
"MissingPotentialState",
|
|
35
38
|
]
|
|
@@ -218,6 +218,7 @@ class CodeGenerator(abc.ABC):
|
|
|
218
218
|
state_names=[s.name for s in self.ode.sorted_states()],
|
|
219
219
|
state_values=[self.printer.doprint(s.value) for s in self.ode.sorted_states()],
|
|
220
220
|
name=name,
|
|
221
|
+
**self._init_value_kwargs(),
|
|
221
222
|
)
|
|
222
223
|
return self._format(code)
|
|
223
224
|
|
|
@@ -247,9 +248,20 @@ class CodeGenerator(abc.ABC):
|
|
|
247
248
|
parameter_names=[s.name for s in self.ode.parameters],
|
|
248
249
|
parameter_values=[self.printer.doprint(s.value) for s in self.ode.parameters],
|
|
249
250
|
name=name,
|
|
251
|
+
**self._init_value_kwargs(),
|
|
250
252
|
)
|
|
251
253
|
return self._format(code)
|
|
252
254
|
|
|
255
|
+
def _init_value_kwargs(self) -> dict[str, typing.Any]:
|
|
256
|
+
"""Extra keyword arguments for the `init_*_values` templates.
|
|
257
|
+
|
|
258
|
+
Empty for every backend whose initial-value functions need no signature
|
|
259
|
+
of their own. The Julia backend overrides it, because under
|
|
260
|
+
`--type-stable` the emitted bodies refer to `TYPE` and so the functions
|
|
261
|
+
need a type parameter to bind it.
|
|
262
|
+
"""
|
|
263
|
+
return {}
|
|
264
|
+
|
|
253
265
|
def _state_assignments(self, states: sympy.IndexedBase, remove_unused: bool) -> str:
|
|
254
266
|
return "\n".join(
|
|
255
267
|
self._doprint(state.symbol, states[i], use_variable_prefix=True)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import structlog
|
|
4
|
+
|
|
5
|
+
from ..ode import ODE
|
|
6
|
+
from .. import templates
|
|
7
|
+
from .. import atoms
|
|
8
|
+
from ..exceptions import InvalidModelName, MissingPotentialState
|
|
9
|
+
from .base import RHSArgument
|
|
10
|
+
from .julia import JuliaCodeGenerator
|
|
11
|
+
|
|
12
|
+
logger = structlog.get_logger()
|
|
13
|
+
|
|
14
|
+
# Both exceptions live in gotranx.exceptions alongside every other GotranxError,
|
|
15
|
+
# so `except exceptions.GotranxError` catches them. They stay importable from
|
|
16
|
+
# here, which is where they are raised and where callers already import them.
|
|
17
|
+
__all__ = ["CytoZooCodeGenerator", "InvalidModelName", "MissingPotentialState"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _pascal_case(name: str) -> str:
|
|
21
|
+
return "".join(
|
|
22
|
+
part[:1].upper() + part[1:] for part in name.replace("-", "_").split("_") if part
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CytoZooCodeGenerator(JuliaCodeGenerator):
|
|
27
|
+
"""Emit a CytoZoo cell-model adapter.
|
|
28
|
+
|
|
29
|
+
Subclasses the Julia generator so every expression, every `TYPE(...)`-wrapped
|
|
30
|
+
literal and every assignment block is shared rather than reimplemented. What
|
|
31
|
+
differs is the RHS signature (Task 5), the parameter assignments (Task 5) and
|
|
32
|
+
the adapter block below.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
ode: ODE,
|
|
38
|
+
model_name: str | None = None,
|
|
39
|
+
v_name: str = "v",
|
|
40
|
+
remove_unused: bool = False,
|
|
41
|
+
type_stable: bool = True,
|
|
42
|
+
) -> None:
|
|
43
|
+
super().__init__(ode, remove_unused=remove_unused, type_stable=type_stable)
|
|
44
|
+
name = model_name or _pascal_case(ode.name or "Model")
|
|
45
|
+
# `_pascal_case` only splits on `-`/`_`, so an ODE named `1962_noble`,
|
|
46
|
+
# `tentusscher panfilov` or `my.model` -- and any `--model-name` the
|
|
47
|
+
# user passes -- can still start with a digit or carry a space or a
|
|
48
|
+
# dot. Each emits `struct <invalid>{T <: AbstractVector}`, a Julia
|
|
49
|
+
# parse error hundreds of lines away from its cause.
|
|
50
|
+
if not name.isidentifier():
|
|
51
|
+
raise InvalidModelName(name)
|
|
52
|
+
self._model_name = name
|
|
53
|
+
self._v_name = v_name
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def template(self):
|
|
57
|
+
return templates.julia
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def model_name(self) -> str:
|
|
61
|
+
return self._model_name
|
|
62
|
+
|
|
63
|
+
def _state_name_list(self) -> list[str]:
|
|
64
|
+
# self.ode.states is name-sorted; sorted_states() is the dependency order
|
|
65
|
+
# that fills `u`, which is what state_index()/_state_assignments() in
|
|
66
|
+
# codegen/base.py actually use (base.py:171, base.py:268). Using the
|
|
67
|
+
# name-sorted list here would silently mislabel every state slot.
|
|
68
|
+
return [s.name for s in self.ode.sorted_states()]
|
|
69
|
+
|
|
70
|
+
def _parameter_name_list(self) -> list[str]:
|
|
71
|
+
return [p.name for p in self.ode.parameters]
|
|
72
|
+
|
|
73
|
+
def _monitor_name_list(self) -> list[str]:
|
|
74
|
+
# Mirror codegen.base.CodeGenerator.monitor_index (base.py:186) exactly:
|
|
75
|
+
# one pass over sorted_assignments() in encounter order, not two
|
|
76
|
+
# name-sorted blocks concatenated. Monitor slots must match monitor_index.
|
|
77
|
+
return [
|
|
78
|
+
a.name
|
|
79
|
+
for a in self.ode.sorted_assignments(remove_unused=False)
|
|
80
|
+
if isinstance(a, (atoms.Intermediate, atoms.StateDerivative))
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
def _potential_index(self) -> int:
|
|
84
|
+
names = self._state_name_list()
|
|
85
|
+
try:
|
|
86
|
+
return names.index(self._v_name) + 1
|
|
87
|
+
except ValueError:
|
|
88
|
+
raise MissingPotentialState(
|
|
89
|
+
v_name=self._v_name, ode_name=self.ode.name, state_names=names
|
|
90
|
+
) from None
|
|
91
|
+
|
|
92
|
+
def _rhs_arguments(self, order=None, const_states: bool = True):
|
|
93
|
+
"""The Julia signature plus the two spatial arguments.
|
|
94
|
+
|
|
95
|
+
`_cz_`-prefixed so an .ode declaring a parameter called `x` or `overrides`
|
|
96
|
+
still generates valid code.
|
|
97
|
+
|
|
98
|
+
Typing the overrides follows the house pattern CytoZoo's own models use
|
|
99
|
+
(docs/src/guides/implementing_a_model.md, "Adding Spatial Support"):
|
|
100
|
+
giving them their own type parameter makes the method specialize on the
|
|
101
|
+
override NamedTuple's type, so the `Nothing` case compiles the whole
|
|
102
|
+
spatial branch away instead of leaving a runtime check behind. Julia
|
|
103
|
+
does not specialize on an untyped argument that is only passed along.
|
|
104
|
+
|
|
105
|
+
The guide spells that type parameter `F`, which cannot be used verbatim
|
|
106
|
+
here: `F` is Faraday's constant in every cardiac model, and the emitted
|
|
107
|
+
body assigns a local `F = TYPE(resolve_parameter(...))` for it, which
|
|
108
|
+
Julia rejects with "local variable name \"F\" conflicts with a static
|
|
109
|
+
parameter". The generated name is `_cz_F` for the same reason the
|
|
110
|
+
arguments are `_cz_`-prefixed, and for the same reason the element type
|
|
111
|
+
is `TYPE` rather than `T`.
|
|
112
|
+
"""
|
|
113
|
+
func = super()._rhs_arguments(
|
|
114
|
+
order if order is not None else RHSArgument.tsp, const_states=const_states
|
|
115
|
+
)
|
|
116
|
+
post = func.post_function_signature
|
|
117
|
+
if post:
|
|
118
|
+
# " where {TYPE, TIME, PARAM, OUT}" -> " where {TYPE, TIME, PARAM, OUT, _cz_F}"
|
|
119
|
+
post = post.rstrip().removesuffix("}") + ", _cz_F}"
|
|
120
|
+
else:
|
|
121
|
+
post = " where {_cz_F}"
|
|
122
|
+
return func._replace(
|
|
123
|
+
arguments=list(func.arguments) + ["_cz_x", "_cz_overrides::_cz_F"],
|
|
124
|
+
post_function_signature=post,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def _parameter_assignments(self, parameters) -> str:
|
|
128
|
+
"""Every parameter local resolves against the spatial overrides.
|
|
129
|
+
|
|
130
|
+
`resolve_parameter` is CytoZoo public API. It is @inline and `name in names`
|
|
131
|
+
folds against the override NamedTuple's type parameter, so a parameter with no
|
|
132
|
+
override costs nothing at runtime; the ::Nothing method returns the fallback
|
|
133
|
+
directly.
|
|
134
|
+
"""
|
|
135
|
+
lines = []
|
|
136
|
+
for i, param in enumerate(self.ode.parameters):
|
|
137
|
+
if not self._condition(param.name):
|
|
138
|
+
continue
|
|
139
|
+
lines.append(
|
|
140
|
+
f"{param.name} = TYPE(resolve_parameter("
|
|
141
|
+
f"parameters[{i + 1}], _cz_overrides, Val(:{param.name}), _cz_x, t))"
|
|
142
|
+
)
|
|
143
|
+
return "\n".join(lines)
|
|
144
|
+
|
|
145
|
+
def adapter(self) -> str:
|
|
146
|
+
"""The struct, name tuples, index lookups and interface methods."""
|
|
147
|
+
name = self._model_name
|
|
148
|
+
monitors = self._monitor_name_list()
|
|
149
|
+
parts = [
|
|
150
|
+
templates.cytozoo.model_struct(name, len(self.ode.parameters)),
|
|
151
|
+
templates.cytozoo.name_tuples(
|
|
152
|
+
name, self._state_name_list(), self._parameter_name_list(), monitors
|
|
153
|
+
),
|
|
154
|
+
templates.cytozoo.index_lookups(
|
|
155
|
+
name, self._state_name_list(), self._parameter_name_list()
|
|
156
|
+
),
|
|
157
|
+
templates.cytozoo.interface_methods(name, self._potential_index(), len(monitors)),
|
|
158
|
+
]
|
|
159
|
+
return self._format("\n".join(parts))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
import typing
|
|
2
3
|
import structlog
|
|
3
4
|
from sympy.printing.julia import JuliaCodePrinter
|
|
4
5
|
from sympy.codegen.ast import Assignment
|
|
@@ -73,6 +74,9 @@ class JuliaCodeGenerator(CodeGenerator):
|
|
|
73
74
|
def template(self):
|
|
74
75
|
return templates.julia
|
|
75
76
|
|
|
77
|
+
def _init_value_kwargs(self) -> dict[str, typing.Any]:
|
|
78
|
+
return {"type_stable": self._printer._type_stable}
|
|
79
|
+
|
|
76
80
|
def imports(self) -> str:
|
|
77
81
|
return ""
|
|
78
82
|
# return self._format(
|
|
@@ -89,13 +93,19 @@ class JuliaCodeGenerator(CodeGenerator):
|
|
|
89
93
|
) -> Func:
|
|
90
94
|
value = RHSArgument.get_value(order)
|
|
91
95
|
if self._printer._type_stable:
|
|
96
|
+
# Each argument gets its OWN type parameter. Tying them to a single
|
|
97
|
+
# `TYPE` means no AD-based stiff solver in the SciML stack can call
|
|
98
|
+
# the emitted `rhs`: such a solver differentiates with respect to the
|
|
99
|
+
# states only, calling `f(du, u, p, t)` with `Dual` `u`/`du` and the
|
|
100
|
+
# original `Float64` `p` and `t`, a combination no single-parameter
|
|
101
|
+
# method matches.
|
|
92
102
|
argument_dict = {
|
|
93
103
|
"s": "states::AbstractVector{TYPE}",
|
|
94
|
-
"t": "t::
|
|
95
|
-
"p": "parameters::AbstractVector{
|
|
104
|
+
"t": "t::TIME",
|
|
105
|
+
"p": "parameters::AbstractVector{PARAM}",
|
|
96
106
|
}
|
|
97
|
-
values = ["values::AbstractVector{
|
|
98
|
-
post_function_signature = " where {TYPE}"
|
|
107
|
+
values = ["values::AbstractVector{OUT}"]
|
|
108
|
+
post_function_signature = " where {TYPE, TIME, PARAM, OUT}"
|
|
99
109
|
else:
|
|
100
110
|
argument_dict = {
|
|
101
111
|
"s": "states",
|
|
@@ -125,14 +135,18 @@ class JuliaCodeGenerator(CodeGenerator):
|
|
|
125
135
|
) -> Func:
|
|
126
136
|
value = SchemeArgument.get_value(order)
|
|
127
137
|
if self._printer._type_stable:
|
|
138
|
+
# Own type parameter per argument, for the reason given in
|
|
139
|
+
# `_rhs_arguments`. A stepper is called with the same mixture of
|
|
140
|
+
# differentiated states and undifferentiated parameters, time and
|
|
141
|
+
# step size.
|
|
128
142
|
argument_dict = {
|
|
129
143
|
"s": "states::AbstractVector{TYPE}",
|
|
130
|
-
"t": "t::
|
|
131
|
-
"d": "dt::
|
|
132
|
-
"p": "parameters::AbstractVector{
|
|
144
|
+
"t": "t::TIME",
|
|
145
|
+
"d": "dt::DT",
|
|
146
|
+
"p": "parameters::AbstractVector{PARAM}",
|
|
133
147
|
}
|
|
134
|
-
values = ["values::AbstractVector{
|
|
135
|
-
post_function_signature = " where {TYPE}"
|
|
148
|
+
values = ["values::AbstractVector{OUT}"]
|
|
149
|
+
post_function_signature = " where {TYPE, TIME, DT, PARAM, OUT}"
|
|
136
150
|
else:
|
|
137
151
|
argument_dict = {
|
|
138
152
|
"s": "states",
|
|
@@ -125,3 +125,31 @@ class ResolveExpressionError(GotranxError, ValueError):
|
|
|
125
125
|
|
|
126
126
|
def __str__(self) -> str:
|
|
127
127
|
return f"Unable to resolve expression for {self.name!r}"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class MissingPotentialState(GotranxError, ValueError):
|
|
132
|
+
v_name: str
|
|
133
|
+
ode_name: str
|
|
134
|
+
state_names: list[str]
|
|
135
|
+
|
|
136
|
+
def __str__(self) -> str:
|
|
137
|
+
return (
|
|
138
|
+
f"no state named {self.v_name!r} in ODE {self.ode_name!r}; "
|
|
139
|
+
f"pass v_name=<state> (CLI: --v-name) naming the transmembrane potential. "
|
|
140
|
+
f"States are: {', '.join(self.state_names)}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class InvalidModelName(GotranxError, ValueError):
|
|
146
|
+
name: str
|
|
147
|
+
|
|
148
|
+
def __str__(self) -> str:
|
|
149
|
+
return (
|
|
150
|
+
f"Model name {self.name!r} is not a valid Julia identifier, so the "
|
|
151
|
+
"generated `struct` would not parse. A valid name starts with a letter "
|
|
152
|
+
"or underscore and contains only letters, digits and underscores -- "
|
|
153
|
+
"e.g. 'ORdmmLand'. Pass a different name with model_name= "
|
|
154
|
+
"(CLI: --model-name)."
|
|
155
|
+
)
|
|
@@ -8,6 +8,7 @@ from . import julia
|
|
|
8
8
|
from . import mtk
|
|
9
9
|
from . import markdown
|
|
10
10
|
from . import ufl
|
|
11
|
+
from . import cytozoo
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class Template(typing.Protocol):
|
|
@@ -197,4 +198,4 @@ class Template(typing.Protocol):
|
|
|
197
198
|
"""
|
|
198
199
|
|
|
199
200
|
|
|
200
|
-
__all__ = ["c", "python", "jax", "julia", "markdown", "mtk", "Template", "ufl"]
|
|
201
|
+
__all__ = ["c", "python", "jax", "julia", "markdown", "mtk", "Template", "ufl", "cytozoo"]
|