gotranx 2.1.0__tar.gz → 2.2.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 (90) hide show
  1. {gotranx-2.1.0/src/gotranx.egg-info → gotranx-2.2.0}/PKG-INFO +2 -1
  2. {gotranx-2.1.0 → gotranx-2.2.0}/pyproject.toml +3 -2
  3. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/codegen/ufl.py +22 -0
  4. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/ode.py +4 -1
  5. {gotranx-2.1.0 → gotranx-2.2.0/src/gotranx.egg-info}/PKG-INFO +2 -1
  6. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx.egg-info/SOURCES.txt +1 -0
  7. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx.egg-info/requires.txt +1 -0
  8. gotranx-2.2.0/tests/test_determinism.py +89 -0
  9. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_ufl_codegen.py +78 -0
  10. {gotranx-2.1.0 → gotranx-2.2.0}/LICENSE +0 -0
  11. {gotranx-2.1.0 → gotranx-2.2.0}/README.md +0 -0
  12. {gotranx-2.1.0 → gotranx-2.2.0}/setup.cfg +0 -0
  13. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/__init__.py +0 -0
  14. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/__main__.py +0 -0
  15. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/_enum.py +0 -0
  16. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/atoms.py +0 -0
  17. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/cli/__init__.py +0 -0
  18. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/cli/cellml2ode.py +0 -0
  19. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/cli/gotran2c.py +0 -0
  20. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/cli/gotran2julia.py +0 -0
  21. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/cli/gotran2md.py +0 -0
  22. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/cli/gotran2mtk.py +0 -0
  23. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/cli/gotran2py.py +0 -0
  24. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/cli/gotran2ufl.py +0 -0
  25. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/cli/ode2cellml.py +0 -0
  26. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/cli/utils.py +0 -0
  27. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/codegen/__init__.py +0 -0
  28. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/codegen/base.py +0 -0
  29. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/codegen/c.py +0 -0
  30. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/codegen/jax.py +0 -0
  31. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/codegen/julia.py +0 -0
  32. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/codegen/markdown.py +0 -0
  33. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/codegen/mtk.py +0 -0
  34. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/codegen/ode.py +0 -0
  35. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/codegen/python.py +0 -0
  36. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/exceptions.py +0 -0
  37. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/expressions.py +0 -0
  38. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/linearization.py +0 -0
  39. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/load.py +0 -0
  40. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/myokit.py +0 -0
  41. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/ode.lark +0 -0
  42. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/ode_component.py +0 -0
  43. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/parser.py +0 -0
  44. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/py.typed +0 -0
  45. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/save.py +0 -0
  46. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/schemes.py +0 -0
  47. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/singularities.py +0 -0
  48. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/sympytools.py +0 -0
  49. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/templates/__init__.py +0 -0
  50. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/templates/c.py +0 -0
  51. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/templates/jax.py +0 -0
  52. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/templates/julia.py +0 -0
  53. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/templates/markdown.py +0 -0
  54. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/templates/mtk.py +0 -0
  55. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/templates/python.py +0 -0
  56. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/templates/ufl.py +0 -0
  57. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/transformer.py +0 -0
  58. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx/units.py +0 -0
  59. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx.egg-info/dependency_links.txt +0 -0
  60. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx.egg-info/entry_points.txt +0 -0
  61. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx.egg-info/not-zip-safe +0 -0
  62. {gotranx-2.1.0 → gotranx-2.2.0}/src/gotranx.egg-info/top_level.txt +0 -0
  63. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_atoms.py +0 -0
  64. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_benchmarks.py +0 -0
  65. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_c_codegen.py +0 -0
  66. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_cli.py +0 -0
  67. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_comments.py +0 -0
  68. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_conditionals.py +0 -0
  69. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_deep_linearization_models.py +0 -0
  70. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_demos.py +0 -0
  71. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_expressions.py +0 -0
  72. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_julia_codegen.py +0 -0
  73. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_linearization.py +0 -0
  74. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_load.py +0 -0
  75. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_markdown_codegen.py +0 -0
  76. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_mtk_codegen.py +0 -0
  77. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_myokit.py +0 -0
  78. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_ode.py +0 -0
  79. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_ode_component.py +0 -0
  80. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_operations.py +0 -0
  81. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_python_codegen.py +0 -0
  82. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_save.py +0 -0
  83. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_schemes.py +0 -0
  84. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_singularities.py +0 -0
  85. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_singularity_models.py +0 -0
  86. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_subodes.py +0 -0
  87. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_sympytools.py +0 -0
  88. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_transformer_assignments.py +0 -0
  89. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_transformer_parameters.py +0 -0
  90. {gotranx-2.1.0 → gotranx-2.2.0}/tests/test_transformer_states.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gotranx
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: A declarative language describing ordinary differential equations
5
5
  Author-email: Henrik Finsberg <henriknf@simula.no>
6
6
  License: MIT
@@ -47,6 +47,7 @@ Requires-Dist: pytest; extra == "test"
47
47
  Requires-Dist: pytest-cov; extra == "test"
48
48
  Requires-Dist: jax; extra == "test"
49
49
  Requires-Dist: cmake; extra == "test"
50
+ Requires-Dist: fenics-ufl; extra == "test"
50
51
  Requires-Dist: gotranx[formatters]; extra == "test"
51
52
  Provides-Extra: benchmark
52
53
  Requires-Dist: pytest-benchmark>=4.0.0; extra == "benchmark"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gotranx"
7
- version = "2.1.0"
7
+ version = "2.2.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"}
@@ -62,6 +62,7 @@ test = [
62
62
  "pytest-cov",
63
63
  "jax",
64
64
  "cmake",
65
+ "fenics-ufl",
65
66
  "gotranx[formatters]",
66
67
  ]
67
68
  benchmark = [
@@ -171,7 +172,7 @@ tag = true
171
172
  sign_tags = false
172
173
  tag_name = "v{new_version}"
173
174
  tag_message = "Bump version: {current_version} → {new_version}"
174
- current_version = "2.1.0"
175
+ current_version = "2.2.0"
175
176
 
176
177
 
177
178
  [[tool.bumpversion.files]]
@@ -74,6 +74,28 @@ class UFLPrinter(GotranPythonCodePrinter):
74
74
 
75
75
  return "".join(result)
76
76
 
77
+ # Comparisons are printed as calls to the ufl functions, not as Python
78
+ # operators. With two numbers as operands (say a parameter passed as a
79
+ # float) an operator gives a Python bool, which ufl.conditional, ufl.And
80
+ # and ufl.Or refuse; the functions convert both operands with ufl.as_ufl.
81
+ # `==` and `!=` never give a condition: UFL keeps them for structural
82
+ # equality of expressions.
83
+ _comparisons = {
84
+ "==": "ufl.eq",
85
+ "!=": "ufl.ne",
86
+ "<": "ufl.lt",
87
+ "<=": "ufl.le",
88
+ ">": "ufl.gt",
89
+ ">=": "ufl.ge",
90
+ }
91
+
92
+ def _print_Relational(self, expr):
93
+ func = self._comparisons[expr.rel_op]
94
+ return f"{func}({self._print(expr.lhs)}, {self._print(expr.rhs)})"
95
+
96
+ # The Python printer's own _print_Equality would otherwise take precedence
97
+ _print_Equality = _print_Relational
98
+
77
99
  def _print_And(self, expr):
78
100
  args = [self._print(arg) for arg in expr.args]
79
101
  return functools.reduce(lambda x, y: f"ufl.And({x}, {y})", args)
@@ -280,7 +280,10 @@ def sort_assignments(
280
280
  "Try to save the ODE to an .ode file first and load it again"
281
281
  )
282
282
  raise exceptions.GotranxError(msg)
283
- sorter.add(assignment.name, *assignment.value.dependencies)
283
+ # sorted(): dependencies is a frozenset[str], and graphlib breaks ties in
284
+ # insertion order, so an unsorted splat leaks PYTHONHASHSEED into the
285
+ # emitted order of states, monitors and intermediates.
286
+ sorter.add(assignment.name, *sorted(assignment.value.dependencies))
284
287
 
285
288
  static_order = tuple(sorter.static_order())
286
289
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gotranx
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: A declarative language describing ordinary differential equations
5
5
  Author-email: Henrik Finsberg <henriknf@simula.no>
6
6
  License: MIT
@@ -47,6 +47,7 @@ Requires-Dist: pytest; extra == "test"
47
47
  Requires-Dist: pytest-cov; extra == "test"
48
48
  Requires-Dist: jax; extra == "test"
49
49
  Requires-Dist: cmake; extra == "test"
50
+ Requires-Dist: fenics-ufl; extra == "test"
50
51
  Requires-Dist: gotranx[formatters]; extra == "test"
51
52
  Provides-Extra: benchmark
52
53
  Requires-Dist: pytest-benchmark>=4.0.0; extra == "benchmark"
@@ -64,6 +64,7 @@ tests/test_comments.py
64
64
  tests/test_conditionals.py
65
65
  tests/test_deep_linearization_models.py
66
66
  tests/test_demos.py
67
+ tests/test_determinism.py
67
68
  tests/test_expressions.py
68
69
  tests/test_julia_codegen.py
69
70
  tests/test_linearization.py
@@ -52,4 +52,5 @@ pytest
52
52
  pytest-cov
53
53
  jax
54
54
  cmake
55
+ fenics-ufl
55
56
  gotranx[formatters]
@@ -0,0 +1,89 @@
1
+ """Generated output must not depend on Python's per-process hash seed.
2
+
3
+ ``sort_assignments`` feeds ``graphlib.TopologicalSorter``, which breaks ties in
4
+ insertion order. Any set of strings iterated on the way in therefore leaks
5
+ ``PYTHONHASHSEED`` into the emitted order of states, monitors and intermediates,
6
+ and from there into the generated code.
7
+
8
+ The seed must be set before the interpreter starts, so every check here shells
9
+ out to a fresh interpreter rather than running in-process.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import subprocess
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ import pytest
21
+
22
+ here = Path(__file__).parent
23
+
24
+ # Models with enough independent branches for a tie-break to be observable.
25
+ # Small models have no ties to break, so they cannot expose this. These two produce a
26
+ # distinct order on every seed tried.
27
+ MODELS = ["ToRORd_dyn_chloride.ode", "ORdmm_Land.ode"]
28
+ SEEDS = ("1", "2", "7")
29
+
30
+ _ORDER_SCRIPT = """
31
+ import json, sys
32
+ import gotranx
33
+
34
+ ode = gotranx.load_ode(sys.argv[1])
35
+ # Marker-delimited: gotranx logs to stdout, so the payload cannot be the whole stream.
36
+ print("<<<ORDER>>>" + json.dumps({
37
+ "assignments": [a.name for a in ode.sorted_assignments()],
38
+ "states": [s.name for s in ode.sorted_states()],
39
+ }))
40
+ """
41
+
42
+ _CODEGEN_SCRIPT = """
43
+ import sys
44
+ import gotranx
45
+
46
+ ode = gotranx.load_ode(sys.argv[1])
47
+ from gotranx.codegen import JuliaCodeGenerator
48
+ codegen = JuliaCodeGenerator(ode)
49
+ sys.stdout.write("<<<CODE>>>" + codegen.rhs() + codegen.monitor_index() + codegen.state_index())
50
+ """
51
+
52
+
53
+ def _run(script: str, odefile: Path, seed: str, marker: str) -> str:
54
+ env = {**os.environ, "PYTHONHASHSEED": seed}
55
+ out = subprocess.run(
56
+ [sys.executable, "-c", script, str(odefile)],
57
+ env=env,
58
+ capture_output=True,
59
+ text=True,
60
+ check=True,
61
+ )
62
+ assert marker in out.stdout, f"marker missing; stderr was:\n{out.stderr}"
63
+ return out.stdout.split(marker, 1)[1]
64
+
65
+
66
+ @pytest.mark.parametrize("model", MODELS)
67
+ def test_assignment_and_state_order_are_independent_of_hash_seed(model):
68
+ odefile = here / "odefiles" / model
69
+ orders = [json.loads(_run(_ORDER_SCRIPT, odefile, s, "<<<ORDER>>>")) for s in SEEDS]
70
+
71
+ for seed, other in zip(SEEDS[1:], orders[1:]):
72
+ assert other["assignments"] == orders[0]["assignments"], (
73
+ f"assignment order under PYTHONHASHSEED={seed} differs from {SEEDS[0]}"
74
+ )
75
+ assert other["states"] == orders[0]["states"], (
76
+ f"state order under PYTHONHASHSEED={seed} differs from {SEEDS[0]}"
77
+ )
78
+
79
+
80
+ @pytest.mark.parametrize("model", MODELS)
81
+ def test_generated_code_is_independent_of_hash_seed(model):
82
+ """The end-to-end property users actually depend on."""
83
+ odefile = here / "odefiles" / model
84
+ code = [_run(_CODEGEN_SCRIPT, odefile, s, "<<<CODE>>>") for s in SEEDS]
85
+
86
+ for seed, other in zip(SEEDS[1:], code[1:]):
87
+ assert other == code[0], (
88
+ f"generated rhs under PYTHONHASHSEED={seed} differs from {SEEDS[0]}"
89
+ )
@@ -1,7 +1,10 @@
1
+ import math
1
2
  import pytest
2
3
  from pathlib import Path
3
4
  import gotranx
5
+ from gotranx.cli import gotran2py, gotran2ufl
4
6
  from gotranx.codegen.ufl import UFLCodeGenerator
7
+ from gotranx.schemes import Scheme
5
8
 
6
9
  here = Path(__file__).parent.absolute()
7
10
 
@@ -117,3 +120,78 @@ def test_ufl_codegen_ordmm_land():
117
120
  # generated successfully as state array index mappings
118
121
  assert "hL = states[" in code
119
122
  assert "a = states[" in code
123
+
124
+
125
+ def _generated(module, ode, scheme=None):
126
+ code = module.get_code(ode, scheme=scheme)
127
+ namespace: dict = {}
128
+ exec(compile(code, f"<{module.__name__}>", "exec"), namespace)
129
+ return namespace
130
+
131
+
132
+ def test_ufl_generalized_rush_larsen_with_float_parameters():
133
+ """The generated UFL scheme accepts parameters that are plain Python floats.
134
+
135
+ The linearized rate of ``x`` is the parameter ``-k``, so the scheme's
136
+ zero-division guard ``|dx_dt_linearized| > delta`` compares two numbers.
137
+ Printed as ``>`` that is a Python ``bool``, which ``ufl.Or`` refuses.
138
+ """
139
+ pytest.importorskip("ufl")
140
+ from ufl.core.expr import Expr
141
+
142
+ ode = gotranx.load.ode_from_string(
143
+ """
144
+ parameters(k=2.0, c=1.0)
145
+ states(x=0.2)
146
+ dx_dt = c - k * x
147
+ """
148
+ )
149
+ scheme = [Scheme.generalized_rush_larsen]
150
+ ufl_code = _generated(gotran2ufl, ode, scheme)
151
+ py_code = _generated(gotran2py, ode, scheme)
152
+
153
+ states = py_code["init_state_values"]()
154
+ parameters = py_code["init_parameter_values"]()
155
+ dt = 0.1
156
+ expected = py_code["generalized_rush_larsen"](states, 0.0, dt, parameters)
157
+
158
+ values = ufl_code["generalized_rush_larsen"](
159
+ [float(s) for s in states], 0.0, dt, [float(p) for p in parameters]
160
+ )
161
+
162
+ assert isinstance(values[0], Expr)
163
+ assert float(values[0]) == pytest.approx(expected[0], rel=1e-12)
164
+ # The scheme is exact for a linear ODE: x(dt) = c/k + (x0 - c/k) exp(-k dt)
165
+ assert float(values[0]) == pytest.approx(0.5 - 0.3 * math.exp(-0.2), rel=1e-12)
166
+
167
+
168
+ @pytest.mark.parametrize("b", [1.0, 2.0])
169
+ @pytest.mark.parametrize(
170
+ "condition", ["Lt(a, b)", "Le(a, b)", "Gt(a, b)", "Ge(a, b)", "Eq(a, b)", "Not(Eq(a, b))"]
171
+ )
172
+ def test_ufl_relational_with_float_operands(condition, b):
173
+ """Every relational prints to a UFL condition, whatever its operands are.
174
+
175
+ Comparing two Python numbers gives a Python ``bool``, which
176
+ ``ufl.conditional`` refuses. ``==`` and ``!=`` never give a UFL condition,
177
+ even on UFL operands, since UFL reserves them for structural equality.
178
+ """
179
+ pytest.importorskip("ufl")
180
+
181
+ ode = gotranx.load.ode_from_string(
182
+ f"""
183
+ parameters(a=1.0, b=2.0)
184
+ states(x=0.0)
185
+ dx_dt = Conditional({condition}, 1.0, -1.0)
186
+ """
187
+ )
188
+ ufl_code = _generated(gotran2ufl, ode)
189
+ py_code = _generated(gotran2py, ode)
190
+
191
+ states = py_code["init_state_values"]()
192
+ parameters = py_code["init_parameter_values"](b=b)
193
+ expected = py_code["rhs"](0.0, states, parameters)
194
+
195
+ values = ufl_code["rhs"](0.0, [float(s) for s in states], [float(p) for p in parameters])
196
+
197
+ assert float(values[0]) == expected[0]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes