gotranx 2.4.0__tar.gz → 2.6.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 (97) hide show
  1. {gotranx-2.4.0/src/gotranx.egg-info → gotranx-2.6.0}/PKG-INFO +2 -1
  2. {gotranx-2.4.0 → gotranx-2.6.0}/pyproject.toml +5 -3
  3. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/cli/__init__.py +14 -0
  4. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/cli/gotran2cytozoo.py +5 -0
  5. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/cli/utils.py +55 -20
  6. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/codegen/base.py +6 -4
  7. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/codegen/c.py +1 -0
  8. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/codegen/cytozoo.py +26 -3
  9. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/myokit.py +29 -14
  10. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/singularities.py +289 -21
  11. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/templates/c.py +4 -2
  12. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/templates/cytozoo.py +32 -12
  13. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/templates/julia.py +4 -2
  14. {gotranx-2.4.0 → gotranx-2.6.0/src/gotranx.egg-info}/PKG-INFO +2 -1
  15. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx.egg-info/SOURCES.txt +2 -0
  16. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx.egg-info/requires.txt +1 -0
  17. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_cli.py +47 -0
  18. gotranx-2.6.0/tests/test_julia_runs.py +116 -0
  19. gotranx-2.6.0/tests/test_missing_variables.py +372 -0
  20. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_myokit.py +31 -0
  21. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_singularities.py +131 -0
  22. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_singularity_models.py +79 -0
  23. {gotranx-2.4.0 → gotranx-2.6.0}/LICENSE +0 -0
  24. {gotranx-2.4.0 → gotranx-2.6.0}/README.md +0 -0
  25. {gotranx-2.4.0 → gotranx-2.6.0}/setup.cfg +0 -0
  26. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/__init__.py +0 -0
  27. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/__main__.py +0 -0
  28. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/_enum.py +0 -0
  29. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/atoms.py +0 -0
  30. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/cli/cellml2ode.py +0 -0
  31. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/cli/gotran2c.py +0 -0
  32. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/cli/gotran2julia.py +0 -0
  33. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/cli/gotran2md.py +0 -0
  34. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/cli/gotran2mtk.py +0 -0
  35. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/cli/gotran2py.py +0 -0
  36. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/cli/gotran2ufl.py +0 -0
  37. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/cli/ode2cellml.py +0 -0
  38. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/codegen/__init__.py +0 -0
  39. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/codegen/jax.py +0 -0
  40. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/codegen/julia.py +0 -0
  41. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/codegen/markdown.py +0 -0
  42. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/codegen/mtk.py +0 -0
  43. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/codegen/ode.py +0 -0
  44. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/codegen/python.py +0 -0
  45. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/codegen/ufl.py +0 -0
  46. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/exceptions.py +0 -0
  47. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/expressions.py +0 -0
  48. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/linearization.py +0 -0
  49. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/load.py +0 -0
  50. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/ode.lark +0 -0
  51. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/ode.py +0 -0
  52. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/ode_component.py +0 -0
  53. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/parser.py +0 -0
  54. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/py.typed +0 -0
  55. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/save.py +0 -0
  56. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/schemes.py +0 -0
  57. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/sympytools.py +0 -0
  58. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/templates/__init__.py +0 -0
  59. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/templates/jax.py +0 -0
  60. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/templates/markdown.py +0 -0
  61. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/templates/mtk.py +0 -0
  62. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/templates/python.py +0 -0
  63. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/templates/ufl.py +0 -0
  64. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/transformer.py +0 -0
  65. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx/units.py +0 -0
  66. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx.egg-info/dependency_links.txt +0 -0
  67. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx.egg-info/entry_points.txt +0 -0
  68. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx.egg-info/not-zip-safe +0 -0
  69. {gotranx-2.4.0 → gotranx-2.6.0}/src/gotranx.egg-info/top_level.txt +0 -0
  70. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_atoms.py +0 -0
  71. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_benchmarks.py +0 -0
  72. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_c_codegen.py +0 -0
  73. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_comments.py +0 -0
  74. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_conditionals.py +0 -0
  75. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_cytozoo_codegen.py +0 -0
  76. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_deep_linearization_models.py +0 -0
  77. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_demos.py +0 -0
  78. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_determinism.py +0 -0
  79. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_expressions.py +0 -0
  80. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_julia_codegen.py +0 -0
  81. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_linearization.py +0 -0
  82. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_load.py +0 -0
  83. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_markdown_codegen.py +0 -0
  84. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_min_max.py +0 -0
  85. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_mtk_codegen.py +0 -0
  86. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_ode.py +0 -0
  87. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_ode_component.py +0 -0
  88. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_operations.py +0 -0
  89. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_python_codegen.py +0 -0
  90. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_save.py +0 -0
  91. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_schemes.py +0 -0
  92. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_subodes.py +0 -0
  93. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_sympytools.py +0 -0
  94. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_transformer_assignments.py +0 -0
  95. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_transformer_parameters.py +0 -0
  96. {gotranx-2.4.0 → gotranx-2.6.0}/tests/test_transformer_states.py +0 -0
  97. {gotranx-2.4.0 → gotranx-2.6.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.4.0
3
+ Version: 2.6.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.4.0"
7
+ version = "2.6.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.4.0"
177
+ current_version = "2.6.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 find_pyproject_toml_config() -> Path | None:
33
- """Find the pyproject.toml file."""
34
- from black.files import find_pyproject_toml
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
- path = find_pyproject_toml((str(Path.cwd()),))
37
- if path is None:
38
- return None
39
- return Path(path)
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
- # First try to use tomllib which is part of stdlib
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 += ["missing_variables"]
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 += ["missing_variables"]
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 += ["missing_variables"]
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 += ["missing_variables"]
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
- return [p.name for p in self.ode.parameters]
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(name, len(self.ode.parameters)),
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(name, self._potential_index(), len(monitors)),
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))
@@ -4,18 +4,14 @@ import functools
4
4
  import re
5
5
  import warnings
6
6
  from pathlib import Path
7
- from typing import overload
7
+ from types import ModuleType
8
+ from typing import TYPE_CHECKING, overload
8
9
  from collections.abc import Sequence
9
10
 
10
11
  import sympy as sp
11
12
 
12
- try:
13
+ if TYPE_CHECKING:
13
14
  import myokit
14
- import myokit.formats.sympy
15
- import myokit.lib.guess
16
- import myokit.formats.cellml
17
- except ImportError:
18
- myokit = None
19
15
 
20
16
  from . import atoms
21
17
  from .ode_component import BaseComponent, MyokitComponent
@@ -25,9 +21,24 @@ from .exceptions import GotranxImportError
25
21
  reserved_names = {name for name in dir(sp) if not name.startswith("_")}
26
22
 
27
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
+
28
40
  def assert_has_myokit():
29
- if myokit is None:
30
- raise GotranxImportError("myokit")
41
+ _import_myokit()
31
42
 
32
43
 
33
44
  _INVALID_NAME_CHARS = re.compile(r"\W+")
@@ -224,7 +235,7 @@ def mmt_to_gotran(filename: str | Path) -> ODE:
224
235
  gotranx.ode.ODE
225
236
  The gotran ODE
226
237
  """
227
- assert_has_myokit()
238
+ myokit = _import_myokit()
228
239
  model, protocol, _ = myokit.load(filename)
229
240
 
230
241
  return myokit_to_gotran(model, protocol=protocol)
@@ -245,7 +256,7 @@ def myokit_to_gotran(model: "myokit.Model", protocol=None) -> ODE:
245
256
  gotranx.ode.ODE
246
257
  The gotran ODE
247
258
  """
248
- assert_has_myokit()
259
+ myokit = _import_myokit()
249
260
  # Embed protocol
250
261
  if protocol is not None:
251
262
  model = model.clone()
@@ -358,6 +369,10 @@ def _sympy_reader(model: "myokit.Model") -> "myokit.formats.sympy.SymPyExpressio
358
369
  each ``Min``/``Max`` is read into nested myokit ``If`` expressions, and ``And``/``Or`` are
359
370
  folded into nested binary ones.
360
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
361
376
 
362
377
  class Reader(myokit.formats.sympy.SymPyExpressionReader):
363
378
  def _build_op_map(self):
@@ -397,7 +412,7 @@ def gotran_to_myokit(ode: ODE, time_component="engine", time_unit="s") -> "myoki
397
412
  myokit.Model
398
413
  The myokit model
399
414
  """
400
- assert_has_myokit()
415
+ myokit = _import_myokit()
401
416
  model = myokit.Model(ode.name)
402
417
  model.meta["author"] = "GotranX API"
403
418
  comp = model.add_component(time_component)
@@ -506,7 +521,7 @@ def cellml_to_gotran(filename: str | Path) -> ODE:
506
521
  gotranx.ode.ODE
507
522
  The gotran ODE
508
523
  """
509
- assert_has_myokit()
524
+ myokit = _import_myokit()
510
525
  myokit_model = myokit.formats.cellml.CellMLImporter().model(filename)
511
526
  return myokit_to_gotran(myokit_model)
512
527
 
@@ -521,6 +536,6 @@ def gotran_to_cellml(ode: ODE, filename: str | Path) -> None:
521
536
  filename : str | Path
522
537
  The filename of the cellml file
523
538
  """
524
- assert_has_myokit()
539
+ myokit = _import_myokit()
525
540
  model = gotran_to_myokit(ode)
526
541
  myokit.formats.cellml.CellMLExporter().model(filename, model, version="1.0")