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.
Files changed (94) hide show
  1. {gotranx-2.2.0/src/gotranx.egg-info → gotranx-2.3.0}/PKG-INFO +7 -2
  2. {gotranx-2.2.0 → gotranx-2.3.0}/README.md +6 -1
  3. {gotranx-2.2.0 → gotranx-2.3.0}/pyproject.toml +2 -2
  4. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/__init__.py +65 -1
  5. gotranx-2.3.0/src/gotranx/cli/gotran2cytozoo.py +121 -0
  6. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/__init__.py +3 -0
  7. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/base.py +12 -0
  8. gotranx-2.3.0/src/gotranx/codegen/cytozoo.py +159 -0
  9. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/julia.py +23 -9
  10. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/exceptions.py +28 -0
  11. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/__init__.py +2 -1
  12. gotranx-2.3.0/src/gotranx/templates/cytozoo.py +123 -0
  13. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/julia.py +19 -4
  14. {gotranx-2.2.0 → gotranx-2.3.0/src/gotranx.egg-info}/PKG-INFO +7 -2
  15. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx.egg-info/SOURCES.txt +4 -0
  16. gotranx-2.3.0/tests/test_cytozoo_codegen.py +488 -0
  17. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_determinism.py +34 -0
  18. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_julia_codegen.py +65 -1
  19. {gotranx-2.2.0 → gotranx-2.3.0}/LICENSE +0 -0
  20. {gotranx-2.2.0 → gotranx-2.3.0}/setup.cfg +0 -0
  21. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/__init__.py +0 -0
  22. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/__main__.py +0 -0
  23. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/_enum.py +0 -0
  24. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/atoms.py +0 -0
  25. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/cellml2ode.py +0 -0
  26. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/gotran2c.py +0 -0
  27. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/gotran2julia.py +0 -0
  28. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/gotran2md.py +0 -0
  29. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/gotran2mtk.py +0 -0
  30. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/gotran2py.py +0 -0
  31. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/gotran2ufl.py +0 -0
  32. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/ode2cellml.py +0 -0
  33. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/cli/utils.py +0 -0
  34. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/c.py +0 -0
  35. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/jax.py +0 -0
  36. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/markdown.py +0 -0
  37. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/mtk.py +0 -0
  38. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/ode.py +0 -0
  39. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/python.py +0 -0
  40. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/codegen/ufl.py +0 -0
  41. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/expressions.py +0 -0
  42. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/linearization.py +0 -0
  43. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/load.py +0 -0
  44. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/myokit.py +0 -0
  45. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/ode.lark +0 -0
  46. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/ode.py +0 -0
  47. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/ode_component.py +0 -0
  48. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/parser.py +0 -0
  49. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/py.typed +0 -0
  50. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/save.py +0 -0
  51. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/schemes.py +0 -0
  52. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/singularities.py +0 -0
  53. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/sympytools.py +0 -0
  54. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/c.py +0 -0
  55. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/jax.py +0 -0
  56. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/markdown.py +0 -0
  57. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/mtk.py +0 -0
  58. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/python.py +0 -0
  59. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/templates/ufl.py +0 -0
  60. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/transformer.py +0 -0
  61. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx/units.py +0 -0
  62. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx.egg-info/dependency_links.txt +0 -0
  63. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx.egg-info/entry_points.txt +0 -0
  64. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx.egg-info/not-zip-safe +0 -0
  65. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx.egg-info/requires.txt +0 -0
  66. {gotranx-2.2.0 → gotranx-2.3.0}/src/gotranx.egg-info/top_level.txt +0 -0
  67. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_atoms.py +0 -0
  68. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_benchmarks.py +0 -0
  69. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_c_codegen.py +0 -0
  70. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_cli.py +0 -0
  71. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_comments.py +0 -0
  72. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_conditionals.py +0 -0
  73. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_deep_linearization_models.py +0 -0
  74. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_demos.py +0 -0
  75. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_expressions.py +0 -0
  76. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_linearization.py +0 -0
  77. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_load.py +0 -0
  78. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_markdown_codegen.py +0 -0
  79. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_mtk_codegen.py +0 -0
  80. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_myokit.py +0 -0
  81. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_ode.py +0 -0
  82. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_ode_component.py +0 -0
  83. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_operations.py +0 -0
  84. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_python_codegen.py +0 -0
  85. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_save.py +0 -0
  86. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_schemes.py +0 -0
  87. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_singularities.py +0 -0
  88. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_singularity_models.py +0 -0
  89. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_subodes.py +0 -0
  90. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_sympytools.py +0 -0
  91. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_transformer_assignments.py +0 -0
  92. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_transformer_parameters.py +0 -0
  93. {gotranx-2.2.0 → gotranx-2.3.0}/tests/test_transformer_states.py +0 -0
  94. {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.2.0
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. Now you can solve the ode using the following code snippet
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. Now you can solve the ode using the following code snippet
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.2.0"
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.2.0"
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::TYPE",
95
- "p": "parameters::AbstractVector{TYPE}",
104
+ "t": "t::TIME",
105
+ "p": "parameters::AbstractVector{PARAM}",
96
106
  }
97
- values = ["values::AbstractVector{TYPE}"]
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::TYPE",
131
- "d": "dt::TYPE",
132
- "p": "parameters::AbstractVector{TYPE}",
144
+ "t": "t::TIME",
145
+ "d": "dt::DT",
146
+ "p": "parameters::AbstractVector{PARAM}",
133
147
  }
134
- values = ["values::AbstractVector{TYPE}"]
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"]