gotranx 2.0.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 (91) hide show
  1. {gotranx-2.0.0/src/gotranx.egg-info → gotranx-2.2.0}/PKG-INFO +2 -1
  2. {gotranx-2.0.0 → gotranx-2.2.0}/pyproject.toml +3 -2
  3. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/atoms.py +0 -120
  4. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/cli/__init__.py +85 -1
  5. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/cli/gotran2c.py +2 -1
  6. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/cli/gotran2julia.py +2 -1
  7. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/cli/gotran2md.py +2 -1
  8. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/cli/gotran2mtk.py +2 -1
  9. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/cli/gotran2py.py +2 -1
  10. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/cli/gotran2ufl.py +2 -1
  11. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/cli/ode2cellml.py +4 -1
  12. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/cli/utils.py +12 -0
  13. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/codegen/ufl.py +22 -0
  14. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/load.py +18 -3
  15. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/ode.py +86 -7
  16. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/ode_component.py +0 -25
  17. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/schemes.py +6 -1
  18. gotranx-2.2.0/src/gotranx/singularities.py +1092 -0
  19. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/sympytools.py +81 -0
  20. {gotranx-2.0.0 → gotranx-2.2.0/src/gotranx.egg-info}/PKG-INFO +2 -1
  21. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx.egg-info/SOURCES.txt +4 -0
  22. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx.egg-info/requires.txt +1 -0
  23. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_atoms.py +0 -77
  24. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_cli.py +67 -0
  25. gotranx-2.2.0/tests/test_determinism.py +89 -0
  26. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_python_codegen.py +21 -16
  27. gotranx-2.2.0/tests/test_singularities.py +479 -0
  28. gotranx-2.2.0/tests/test_singularity_models.py +305 -0
  29. gotranx-2.2.0/tests/test_sympytools.py +177 -0
  30. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_ufl_codegen.py +78 -0
  31. gotranx-2.0.0/tests/test_sympytools.py +0 -98
  32. {gotranx-2.0.0 → gotranx-2.2.0}/LICENSE +0 -0
  33. {gotranx-2.0.0 → gotranx-2.2.0}/README.md +0 -0
  34. {gotranx-2.0.0 → gotranx-2.2.0}/setup.cfg +0 -0
  35. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/__init__.py +0 -0
  36. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/__main__.py +0 -0
  37. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/_enum.py +0 -0
  38. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/cli/cellml2ode.py +0 -0
  39. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/codegen/__init__.py +0 -0
  40. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/codegen/base.py +0 -0
  41. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/codegen/c.py +0 -0
  42. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/codegen/jax.py +0 -0
  43. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/codegen/julia.py +0 -0
  44. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/codegen/markdown.py +0 -0
  45. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/codegen/mtk.py +0 -0
  46. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/codegen/ode.py +0 -0
  47. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/codegen/python.py +0 -0
  48. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/exceptions.py +0 -0
  49. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/expressions.py +0 -0
  50. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/linearization.py +0 -0
  51. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/myokit.py +0 -0
  52. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/ode.lark +0 -0
  53. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/parser.py +0 -0
  54. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/py.typed +0 -0
  55. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/save.py +0 -0
  56. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/templates/__init__.py +0 -0
  57. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/templates/c.py +0 -0
  58. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/templates/jax.py +0 -0
  59. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/templates/julia.py +0 -0
  60. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/templates/markdown.py +0 -0
  61. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/templates/mtk.py +0 -0
  62. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/templates/python.py +0 -0
  63. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/templates/ufl.py +0 -0
  64. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/transformer.py +0 -0
  65. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx/units.py +0 -0
  66. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx.egg-info/dependency_links.txt +0 -0
  67. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx.egg-info/entry_points.txt +0 -0
  68. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx.egg-info/not-zip-safe +0 -0
  69. {gotranx-2.0.0 → gotranx-2.2.0}/src/gotranx.egg-info/top_level.txt +0 -0
  70. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_benchmarks.py +0 -0
  71. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_c_codegen.py +0 -0
  72. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_comments.py +0 -0
  73. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_conditionals.py +0 -0
  74. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_deep_linearization_models.py +0 -0
  75. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_demos.py +0 -0
  76. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_expressions.py +0 -0
  77. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_julia_codegen.py +0 -0
  78. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_linearization.py +0 -0
  79. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_load.py +0 -0
  80. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_markdown_codegen.py +0 -0
  81. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_mtk_codegen.py +0 -0
  82. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_myokit.py +0 -0
  83. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_ode.py +0 -0
  84. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_ode_component.py +0 -0
  85. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_operations.py +0 -0
  86. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_save.py +0 -0
  87. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_schemes.py +0 -0
  88. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_subodes.py +0 -0
  89. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_transformer_assignments.py +0 -0
  90. {gotranx-2.0.0 → gotranx-2.2.0}/tests/test_transformer_parameters.py +0 -0
  91. {gotranx-2.0.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.0.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.0.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.0.0"
175
+ current_version = "2.2.0"
175
176
 
176
177
 
177
178
  [[tool.bumpversion.files]]
@@ -192,53 +192,6 @@ class Expression:
192
192
  return build_expression(self.tree, symbols=symbols)
193
193
 
194
194
 
195
- @attr.s(frozen=True, kw_only=True, slots=True)
196
- class Singularity:
197
- symbol: sp.Symbol = attr.ib()
198
- value: float | sp.core.Number = attr.ib()
199
- replacement: sp.Expr = attr.ib()
200
-
201
- @property
202
- def is_infinite(self):
203
- """Return True if the replacement is potentially infinite"""
204
- return self.replacement.has(sp.oo) or self.replacement.has(-sp.oo)
205
-
206
-
207
- def remove_singularities(expr: sp.Expr, singularities: frozenset[Singularity]) -> sp.Expr:
208
- """Remove singularities from an expression recursively using Conditionals
209
-
210
- Parameters
211
- ----------
212
- expr : sp.Expr
213
- The expression
214
- singularities : frozenset[Singularity]
215
- The singularities
216
-
217
- Returns
218
- -------
219
- sp.Expr
220
- The expression with singularities removed
221
- """
222
- from .sympytools import Conditional
223
-
224
- exprs = [
225
- Conditional(
226
- cond=sp.Eq(singularity.symbol, singularity.value),
227
- true_value=singularity.replacement,
228
- false_value=expr,
229
- )
230
- for singularity in singularities
231
- if not singularity.is_infinite
232
- ]
233
- if len(exprs) == 0:
234
- logger.debug("No singularities to remove")
235
- return expr
236
-
237
- new_expr = sp.piecewise_fold(sum(exprs))
238
- logger.debug("Removing singularities", new_expr=new_expr)
239
- return new_expr
240
-
241
-
242
195
  @attr.s(frozen=True, kw_only=True, slots=True)
243
196
  class Assignment(Atom):
244
197
  """Assignments are object of the form `name = value`."""
@@ -266,79 +219,6 @@ class Assignment(Atom):
266
219
  return True
267
220
  return False
268
221
 
269
- def singularities(self, lookup: dict[str, Atom]) -> frozenset[Singularity]:
270
- """Check if the expression has any singularities
271
- and return a list of singularities"""
272
- from sympy import singularities, limit
273
-
274
- singularity_list: set[Singularity] = set()
275
- if self.value is None:
276
- return frozenset(singularity_list)
277
- if self.expr == 0:
278
- logger.warning(
279
- f"Expression {self.name} is zero. Maybe you forgot to resolve the expression?"
280
- )
281
- return frozenset(singularity_list)
282
-
283
- for dep in self.value.dependencies:
284
- try:
285
- var = lookup[dep]
286
- except KeyError:
287
- continue
288
-
289
- if not var.is_stateful(lookup):
290
- continue
291
-
292
- values = singularities(self.expr, var.symbol)
293
-
294
- if not values:
295
- continue
296
-
297
- if not isinstance(values, sp.sets.sets.FiniteSet):
298
- continue
299
-
300
- for value in values:
301
- singularity_list.add(
302
- Singularity(
303
- symbol=var.symbol,
304
- value=value,
305
- replacement=limit(self.expr, var.symbol, value),
306
- )
307
- )
308
- return frozenset(singularity_list)
309
-
310
- def remove_singularities(self, lookup: dict[str, Atom]) -> Assignment:
311
- """Remove singularities from the assignment
312
-
313
- Parameters
314
- ----------
315
- lookup : dict[str, Atom]
316
- A lookup table for atoms
317
-
318
- Returns
319
- -------
320
- Assignment
321
- A new assignment with singularities removed
322
- """
323
- logger.debug("Removing singularities")
324
- if singularities := self.singularities(lookup):
325
- logger.debug("Singularities found", singularities=singularities)
326
- new_expr = remove_singularities(self.expr, singularities)
327
- return type(self)(
328
- name=self.name,
329
- value=self.value,
330
- components=self.components,
331
- unit_str=self.unit_str,
332
- unit=self.unit,
333
- expr=new_expr,
334
- symbol=self.symbol,
335
- description=self.description,
336
- comment=self.comment,
337
- )
338
-
339
- # Do not make a copy if no singularities
340
- return self
341
-
342
222
  def resolve_expression(self, symbols: dict[str, sp.Symbol]) -> Assignment:
343
223
  """Resolve the expression of the assignment by
344
224
  building the sympy expression from the expression tree
@@ -78,6 +78,14 @@ def convert(
78
78
  "--remove-unused",
79
79
  help="Remove unused variables",
80
80
  ),
81
+ remove_singularities: bool = typer.Option(
82
+ True,
83
+ "--remove-singularities/--no-remove-singularities",
84
+ help=(
85
+ "Replace a small neighborhood of every removable singularity "
86
+ "(e.g. x/(exp(x) - 1) at x = 0) with a truncated Taylor series"
87
+ ),
88
+ ),
81
89
  jax: bool = typer.Option(
82
90
  False,
83
91
  "--jax",
@@ -148,6 +156,7 @@ def convert(
148
156
  outname=outname,
149
157
  scheme=scheme,
150
158
  remove_unused=remove_unused,
159
+ remove_singularities=remove_singularities,
151
160
  verbose=verbose,
152
161
  stiff_states=stiff_states,
153
162
  delta=delta,
@@ -160,6 +169,7 @@ def convert(
160
169
  outname=outname,
161
170
  scheme=scheme,
162
171
  remove_unused=remove_unused,
172
+ remove_singularities=remove_singularities,
163
173
  verbose=verbose,
164
174
  stiff_states=stiff_states,
165
175
  delta=delta,
@@ -302,6 +312,14 @@ def ode2py(
302
312
  "--remove-unused",
303
313
  help="Remove unused variables",
304
314
  ),
315
+ remove_singularities: bool = typer.Option(
316
+ True,
317
+ "--remove-singularities/--no-remove-singularities",
318
+ help=(
319
+ "Replace a small neighborhood of every removable singularity "
320
+ "(e.g. x/(exp(x) - 1) at x = 0) with a truncated Taylor series"
321
+ ),
322
+ ),
305
323
  version: bool = typer.Option(
306
324
  None,
307
325
  "--version",
@@ -376,6 +394,9 @@ def ode2py(
376
394
  delta = config_data.get("delta", delta)
377
395
  stiff_states = config_data.get("stiff_states", stiff_states)
378
396
  cse = utils.validate_cse(config_data.get("cse", cse))
397
+ remove_singularities = utils.validate_remove_singularities(
398
+ config_data.get("remove_singularities", remove_singularities)
399
+ )
379
400
  scheme = config_data.get("scheme", scheme)
380
401
  shape = Shape(config_data.get("shape", shape))
381
402
  scheme = utils.validate_scheme(scheme)
@@ -388,6 +409,7 @@ def ode2py(
388
409
  outname=outname,
389
410
  scheme=scheme,
390
411
  remove_unused=remove_unused,
412
+ remove_singularities=remove_singularities,
391
413
  verbose=verbose,
392
414
  stiff_states=stiff_states,
393
415
  delta=delta,
@@ -425,6 +447,14 @@ def ode2c(
425
447
  "--remove-unused",
426
448
  help="Remove unused variables",
427
449
  ),
450
+ remove_singularities: bool = typer.Option(
451
+ True,
452
+ "--remove-singularities/--no-remove-singularities",
453
+ help=(
454
+ "Replace a small neighborhood of every removable singularity "
455
+ "(e.g. x/(exp(x) - 1) at x = 0) with a truncated Taylor series"
456
+ ),
457
+ ),
428
458
  version: bool = typer.Option(
429
459
  None,
430
460
  "--version",
@@ -487,6 +517,9 @@ def ode2c(
487
517
  delta = config_data.get("delta", delta)
488
518
  stiff_states = config_data.get("stiff_states", stiff_states)
489
519
  cse = utils.validate_cse(config_data.get("cse", cse))
520
+ remove_singularities = utils.validate_remove_singularities(
521
+ config_data.get("remove_singularities", remove_singularities)
522
+ )
490
523
  scheme = config_data.get("scheme", scheme)
491
524
  scheme = utils.validate_scheme(scheme)
492
525
  c_config = config_data.get("c", {})
@@ -499,6 +532,7 @@ def ode2c(
499
532
  outname=outname,
500
533
  scheme=scheme,
501
534
  remove_unused=remove_unused,
535
+ remove_singularities=remove_singularities,
502
536
  verbose=verbose,
503
537
  stiff_states=stiff_states,
504
538
  delta=delta,
@@ -528,6 +562,14 @@ def ode2julia(
528
562
  "--remove-unused",
529
563
  help="Remove unused variables",
530
564
  ),
565
+ remove_singularities: bool = typer.Option(
566
+ True,
567
+ "--remove-singularities/--no-remove-singularities",
568
+ help=(
569
+ "Replace a small neighborhood of every removable singularity "
570
+ "(e.g. x/(exp(x) - 1) at x = 0) with a truncated Taylor series"
571
+ ),
572
+ ),
531
573
  version: bool = typer.Option(
532
574
  None,
533
575
  "--version",
@@ -595,6 +637,9 @@ def ode2julia(
595
637
  delta = config_data.get("delta", delta)
596
638
  stiff_states = config_data.get("stiff_states", stiff_states)
597
639
  cse = utils.validate_cse(config_data.get("cse", cse))
640
+ remove_singularities = utils.validate_remove_singularities(
641
+ config_data.get("remove_singularities", remove_singularities)
642
+ )
598
643
  scheme = config_data.get("scheme", scheme)
599
644
  scheme = utils.validate_scheme(scheme)
600
645
  # c_config = config_data.get("c", {})
@@ -605,6 +650,7 @@ def ode2julia(
605
650
  outname=outname,
606
651
  scheme=scheme,
607
652
  remove_unused=remove_unused,
653
+ remove_singularities=remove_singularities,
608
654
  verbose=verbose,
609
655
  stiff_states=stiff_states,
610
656
  delta=delta,
@@ -635,6 +681,14 @@ def ode2mtk(
635
681
  "--remove-unused",
636
682
  help="Remove unused variables",
637
683
  ),
684
+ remove_singularities: bool = typer.Option(
685
+ True,
686
+ "--remove-singularities/--no-remove-singularities",
687
+ help=(
688
+ "Replace a small neighborhood of every removable singularity "
689
+ "(e.g. x/(exp(x) - 1) at x = 0) with a truncated Taylor series"
690
+ ),
691
+ ),
638
692
  version: bool = typer.Option(
639
693
  None,
640
694
  "--version",
@@ -668,10 +722,14 @@ def ode2mtk(
668
722
  config_data = utils.read_config(config)
669
723
  verbose = config_data.get("verbose", verbose)
670
724
  remove_unused = config_data.get("remove_unused", remove_unused)
725
+ remove_singularities = utils.validate_remove_singularities(
726
+ config_data.get("remove_singularities", remove_singularities)
727
+ )
671
728
  gotran2mtk.main(
672
729
  fname=fname,
673
730
  outname=outname,
674
731
  remove_unused=remove_unused,
732
+ remove_singularities=remove_singularities,
675
733
  verbose=verbose,
676
734
  )
677
735
 
@@ -750,11 +808,25 @@ def ode2md(
750
808
  "--pdf",
751
809
  help="Generate PDF output",
752
810
  ),
811
+ remove_singularities: bool = typer.Option(
812
+ True,
813
+ "--remove-singularities/--no-remove-singularities",
814
+ help=(
815
+ "Replace a small neighborhood of every removable singularity "
816
+ "(e.g. x/(exp(x) - 1) at x = 0) with a truncated Taylor series"
817
+ ),
818
+ ),
753
819
  ):
754
820
  if fname is None:
755
821
  return typer.echo("No file specified")
756
822
 
757
- gotran2md.main(fname=fname, outname=outname, verbose=verbose, pdf=pdf)
823
+ gotran2md.main(
824
+ fname=fname,
825
+ outname=outname,
826
+ verbose=verbose,
827
+ pdf=pdf,
828
+ remove_singularities=remove_singularities,
829
+ )
758
830
 
759
831
 
760
832
  @app.command()
@@ -779,6 +851,14 @@ def ode2ufl(
779
851
  "--remove-unused",
780
852
  help="Remove unused variables",
781
853
  ),
854
+ remove_singularities: bool = typer.Option(
855
+ True,
856
+ "--remove-singularities/--no-remove-singularities",
857
+ help=(
858
+ "Replace a small neighborhood of every removable singularity "
859
+ "(e.g. x/(exp(x) - 1) at x = 0) with a truncated Taylor series"
860
+ ),
861
+ ),
782
862
  version: bool = typer.Option(
783
863
  None,
784
864
  "--version",
@@ -847,6 +927,9 @@ def ode2ufl(
847
927
  delta = config_data.get("delta", delta)
848
928
  stiff_states = config_data.get("stiff_states", stiff_states)
849
929
  cse = utils.validate_cse(config_data.get("cse", cse))
930
+ remove_singularities = utils.validate_remove_singularities(
931
+ config_data.get("remove_singularities", remove_singularities)
932
+ )
850
933
  scheme = config_data.get("scheme", scheme)
851
934
  shape = Shape(config_data.get("shape", shape))
852
935
  scheme = utils.validate_scheme(scheme)
@@ -858,6 +941,7 @@ def ode2ufl(
858
941
  outname=outname,
859
942
  scheme=scheme,
860
943
  remove_unused=remove_unused,
944
+ remove_singularities=remove_singularities,
861
945
  verbose=verbose,
862
946
  stiff_states=stiff_states,
863
947
  delta=delta,
@@ -97,6 +97,7 @@ def main(
97
97
  outname: Path | str | None = None,
98
98
  scheme: list[Scheme] | None = None,
99
99
  remove_unused: bool = False,
100
+ remove_singularities: bool = True,
100
101
  format: Format = Format.clang_format,
101
102
  verbose: bool = False,
102
103
  missing_values: dict[str, int] | None = None,
@@ -108,7 +109,7 @@ def main(
108
109
  structlog.configure(
109
110
  wrapper_class=structlog.make_filtering_bound_logger(loglevel),
110
111
  )
111
- ode = load_ode(fname)
112
+ ode = load_ode(fname, remove_singularities=remove_singularities)
112
113
  code = get_code(
113
114
  ode,
114
115
  scheme=scheme,
@@ -97,6 +97,7 @@ def main(
97
97
  outname: Path | str | None = None,
98
98
  scheme: list[Scheme] | None = None,
99
99
  remove_unused: bool = False,
100
+ remove_singularities: bool = True,
100
101
  verbose: bool = False,
101
102
  missing_values: dict[str, int] | None = None,
102
103
  delta: float = 1e-8,
@@ -108,7 +109,7 @@ def main(
108
109
  structlog.configure(
109
110
  wrapper_class=structlog.make_filtering_bound_logger(loglevel),
110
111
  )
111
- ode = load_ode(fname)
112
+ ode = load_ode(fname, remove_singularities=remove_singularities)
112
113
  code = get_code(
113
114
  ode,
114
115
  scheme=scheme,
@@ -14,13 +14,14 @@ def main(
14
14
  outname: Path | str | None = None,
15
15
  verbose: bool = False,
16
16
  pdf: bool = False,
17
+ remove_singularities: bool = True,
17
18
  ) -> None:
18
19
  loglevel = logging.DEBUG if verbose else logging.INFO
19
20
  structlog.configure(
20
21
  wrapper_class=structlog.make_filtering_bound_logger(loglevel),
21
22
  )
22
23
 
23
- ode = load_ode(fname)
24
+ ode = load_ode(fname, remove_singularities=remove_singularities)
24
25
  generator = MarkdownGenerator(ode)
25
26
  code = generator.generate()
26
27
 
@@ -24,13 +24,14 @@ def main(
24
24
  fname: Path,
25
25
  outname: Path | str | None = None,
26
26
  remove_unused: bool = False,
27
+ remove_singularities: bool = True,
27
28
  verbose: bool = False,
28
29
  ) -> None:
29
30
  loglevel = logging.DEBUG if verbose else logging.INFO
30
31
  structlog.configure(
31
32
  wrapper_class=structlog.make_filtering_bound_logger(loglevel),
32
33
  )
33
- ode = load_ode(fname)
34
+ ode = load_ode(fname, remove_singularities=remove_singularities)
34
35
  code = get_code(
35
36
  ode,
36
37
  remove_unused=remove_unused,
@@ -119,6 +119,7 @@ def main(
119
119
  format: Format = Format.black,
120
120
  scheme: list[Scheme] | None = None,
121
121
  remove_unused: bool = False,
122
+ remove_singularities: bool = True,
122
123
  verbose: bool = True,
123
124
  stiff_states: list[str] | None = None,
124
125
  cse: bool = True,
@@ -132,7 +133,7 @@ def main(
132
133
  wrapper_class=structlog.make_filtering_bound_logger(loglevel),
133
134
  )
134
135
 
135
- ode = load_ode(fname)
136
+ ode = load_ode(fname, remove_singularities=remove_singularities)
136
137
 
137
138
  code = get_code(
138
139
  ode,
@@ -72,6 +72,7 @@ def main(
72
72
  format: Format = Format.black,
73
73
  scheme: list[Scheme] | None = None,
74
74
  remove_unused: bool = False,
75
+ remove_singularities: bool = True,
75
76
  verbose: bool = True,
76
77
  stiff_states: list[str] | None = None,
77
78
  cse: bool = True,
@@ -84,7 +85,7 @@ def main(
84
85
  wrapper_class=structlog.make_filtering_bound_logger(loglevel),
85
86
  )
86
87
 
87
- ode = load_ode(fname)
88
+ ode = load_ode(fname, remove_singularities=remove_singularities)
88
89
 
89
90
  code = get_code(
90
91
  ode,
@@ -20,7 +20,10 @@ def main(
20
20
  )
21
21
  assert fname.suffix == ".ode", f"File {fname} must be an ode file"
22
22
  assert fname.exists(), f"File {fname} does not exist"
23
- ode = load_ode(fname)
23
+ # A model export, not code generation: writing gotranx's float guard
24
+ # windows into an interchange format would bake a numerical artifact
25
+ # into someone else's model.
26
+ ode = load_ode(fname, remove_singularities=False)
24
27
  logger.info(f"Converting {fname} to CellML")
25
28
  cellml_file = fname.with_suffix(".cellml") if outname is None else Path(outname)
26
29
  gotran_to_cellml(ode, filename=cellml_file)
@@ -89,6 +89,18 @@ def validate_cse(cse: Any) -> bool:
89
89
  return cse
90
90
 
91
91
 
92
+ def validate_remove_singularities(remove_singularities: Any) -> bool:
93
+ """Reject a non-boolean `remove_singularities`, which can only reach here
94
+ from a config file -- where a string such as "false" is truthy and would
95
+ silently do the opposite of what it says."""
96
+ if not isinstance(remove_singularities, bool):
97
+ raise typer.BadParameter(
98
+ f"remove_singularities must be true or false, got {remove_singularities!r}",
99
+ param_hint="remove_singularities",
100
+ )
101
+ return remove_singularities
102
+
103
+
92
104
  def validate_scheme(scheme: list[Scheme] | list[str]) -> list[Scheme]:
93
105
  lst = []
94
106
  for s in scheme:
@@ -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)
@@ -12,7 +12,7 @@ from .transformer import TreeToODE, LarkODE
12
12
  logger = get_logger()
13
13
 
14
14
 
15
- def ode_from_string(text: str, name="ode") -> ODE:
15
+ def ode_from_string(text: str, name="ode", *, remove_singularities: bool = True) -> ODE:
16
16
  """Create an ODE from a string
17
17
 
18
18
  Parameters
@@ -21,6 +21,13 @@ def ode_from_string(text: str, name="ode") -> ODE:
21
21
  The string to parse
22
22
  name : str, optional
23
23
  Name of the ODE, by default "ode"
24
+ remove_singularities : bool, optional
25
+ Replace a neighborhood of every removable singularity with a
26
+ truncated Taylor series, by default True. Pass False to get the
27
+ expressions exactly as written, which will divide by zero wherever the
28
+ model has a removable pole -- numpy warns and returns nan there, numba
29
+ crashes -- and whose derivative, which the Rush-Larsen schemes form, is
30
+ wrong by an O(1) amount with a possibly wrong sign well before that.
24
31
 
25
32
  Returns
26
33
  -------
@@ -37,18 +44,24 @@ def ode_from_string(text: str, name="ode") -> ODE:
37
44
  name=name,
38
45
  comments=result.comments,
39
46
  )
47
+ if remove_singularities:
48
+ ode = ode.remove_singularities()
40
49
  logger.info(f"Num states {ode.num_states}")
41
50
  logger.info(f"Num parameters {ode.num_parameters}")
42
51
  return ode
43
52
 
44
53
 
45
- def load_ode(path: str | Path) -> ODE:
54
+ def load_ode(path: str | Path, *, remove_singularities: bool = True) -> ODE:
46
55
  """Load an ODE from a file
47
56
 
48
57
  Parameters
49
58
  ----------
50
59
  path : str | Path
51
60
  Path to the file
61
+ remove_singularities : bool, optional
62
+ Replace a neighborhood of every removable singularity with a
63
+ truncated Taylor series, by default True. See
64
+ :func:`ode_from_string`.
52
65
 
53
66
  Returns
54
67
  -------
@@ -67,4 +80,6 @@ def load_ode(path: str | Path) -> ODE:
67
80
  if not fname.is_file():
68
81
  raise exceptions.ODEFileNotFound(fname)
69
82
 
70
- return ode_from_string(fname.read_text(), name=fname.stem)
83
+ return ode_from_string(
84
+ fname.read_text(), name=fname.stem, remove_singularities=remove_singularities
85
+ )