From b082f0d29b3a1f1d8d30e9c1d43375ac2f15219b Mon Sep 17 00:00:00 2001 From: Goran Jelic-Cizmek Date: Mon, 30 Sep 2024 14:29:28 +0200 Subject: [PATCH 1/7] Allow diffing implicit functions in `differentiate2c` Uses finite differences --- python/nmodl/ode.py | 18 +++++++++++++++--- test/unit/ode/test_ode.py | 10 ++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/python/nmodl/ode.py b/python/nmodl/ode.py index 3fe769e596..66b3a752e2 100644 --- a/python/nmodl/ode.py +++ b/python/nmodl/ode.py @@ -643,15 +643,27 @@ def differentiate2c(expression, dependent_var, vars, prev_expressions=None): # differentiate w.r.t. x diff = expr.diff(x).simplify() + # could be something generic like f'(x), in which case we use finite differences + if needs_finite_differences(diff): + diff = ( + transform_expression(diff, discretize_derivative) + .subs({finite_difference_step_variable(x): 1e-3}) + .evalf() + ) + + # the codegen method does not like undefined function calls, so we extract + # them here + custom_fcts = {str(f.func): str(f.func) for f in diff.atoms(sp.Function)} + # try to simplify expression in terms of existing variables # ignore any exceptions here, since we already have a valid solution # so if this further simplification step fails the error is not fatal try: # if expression is equal to one of the supplied vars, replace with this var # can do a simple string comparison here since a var cannot be further simplified - diff_as_string = sp.ccode(diff) + diff_as_string = sp.ccode(diff, user_functions=custom_fcts) for v in sympy_vars: - if diff_as_string == sp.ccode(sympy_vars[v]): + if diff_as_string == sp.ccode(sympy_vars[v], user_functions=custom_fcts): diff = sympy_vars[v] # or if equal to rhs of one of the supplied equations, replace with lhs @@ -672,4 +684,4 @@ def differentiate2c(expression, dependent_var, vars, prev_expressions=None): pass # return result as C code in NEURON format - return sp.ccode(diff.evalf()) + return sp.ccode(diff.evalf(), user_functions=custom_fcts) diff --git a/test/unit/ode/test_ode.py b/test/unit/ode/test_ode.py index 387cfb801f..0d5e7f628a 100644 --- a/test/unit/ode/test_ode.py +++ b/test/unit/ode/test_ode.py @@ -100,6 +100,16 @@ def test_differentiate2c(): "g", ) + assert _equivalent( + differentiate2c( + "-f(x)", + "x", + {}, + ), + "1000.0*f(x - 0.00050000000000000001) - 1000.0*f(x + 0.00050000000000000001)", + {"x"}, + ) + def test_integrate2c(): From 565fa03c1d9647badc146e82b24117ab7f8b3272 Mon Sep 17 00:00:00 2001 From: Goran Jelic-Cizmek Date: Tue, 1 Oct 2024 11:02:57 +0200 Subject: [PATCH 2/7] Better testing --- test/unit/ode/test_ode.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/test/unit/ode/test_ode.py b/test/unit/ode/test_ode.py index 0d5e7f628a..21ee8a88f9 100644 --- a/test/unit/ode/test_ode.py +++ b/test/unit/ode/test_ode.py @@ -4,6 +4,7 @@ # SPDX-License-Identifier: Apache-2.0 from nmodl.ode import differentiate2c, integrate2c +import numpy as np import sympy as sp @@ -100,15 +101,29 @@ def test_differentiate2c(): "g", ) - assert _equivalent( - differentiate2c( - "-f(x)", - "x", - {}, - ), - "1000.0*f(x - 0.00050000000000000001) - 1000.0*f(x + 0.00050000000000000001)", - {"x"}, + result = differentiate2c( + "-f(x)", + "x", + {}, ) + # instead of comparing the expression as a string, we convert the string + # back to an expression and insert various functions + for function in [sp.sin, sp.exp, sp.tanh]: + for value in np.linspace(-5, 5, 100): + np.testing.assert_allclose( + float( + sp.sympify(result) + .subs(sp.Function("f"), function) + .subs({"x": value}) + .evalf() + ), + float( + -sp.Derivative(function("x")) + .as_finite_difference(1e-3) + .subs({"x": value}) + .evalf() + ), + ) def test_integrate2c(): From 6bd6aed914cfc87bdf1a2b54ec39c6bc8fc3c654 Mon Sep 17 00:00:00 2001 From: Goran Jelic-Cizmek Date: Tue, 1 Oct 2024 15:45:22 +0200 Subject: [PATCH 3/7] Add suggestions from code review --- test/unit/ode/test_ode.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/test/unit/ode/test_ode.py b/test/unit/ode/test_ode.py index 21ee8a88f9..e3a25b06e5 100644 --- a/test/unit/ode/test_ode.py +++ b/test/unit/ode/test_ode.py @@ -107,23 +107,22 @@ def test_differentiate2c(): {}, ) # instead of comparing the expression as a string, we convert the string - # back to an expression and insert various functions - for function in [sp.sin, sp.exp, sp.tanh]: - for value in np.linspace(-5, 5, 100): - np.testing.assert_allclose( - float( - sp.sympify(result) - .subs(sp.Function("f"), function) - .subs({"x": value}) - .evalf() - ), - float( - -sp.Derivative(function("x")) - .as_finite_difference(1e-3) - .subs({"x": value}) - .evalf() - ), - ) + # back to an expression and compare with an explicit function + for value in np.linspace(-5, 5, 100): + np.testing.assert_allclose( + float( + sp.sympify(result) + .subs(sp.Function("f"), sp.sin) + .subs({"x": value}) + .evalf() + ), + float( + -sp.Derivative(sp.sin("x")) + .as_finite_difference(1e-3) + .subs({"x": value}) + .evalf() + ), + ) def test_integrate2c(): From 0eba407672a868fdcc24a5c66e84fa7d175ca3ba Mon Sep 17 00:00:00 2001 From: Goran Jelic-Cizmek Date: Wed, 2 Oct 2024 11:33:14 +0200 Subject: [PATCH 4/7] Add `stepsize` param to `differentiate2c` --- python/nmodl/ode.py | 14 ++++++++++++-- test/unit/ode/test_ode.py | 8 ++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/python/nmodl/ode.py b/python/nmodl/ode.py index 66b3a752e2..e40cb47c62 100644 --- a/python/nmodl/ode.py +++ b/python/nmodl/ode.py @@ -568,7 +568,13 @@ def forwards_euler2c(diff_string, dt_var, vars, function_calls): return f"{sp.ccode(x)} = {sp.ccode(solution, user_functions=custom_fcts)}" -def differentiate2c(expression, dependent_var, vars, prev_expressions=None): +def differentiate2c( + expression, + dependent_var, + vars, + prev_expressions=None, + stepsize=1e-3, +): """Analytically differentiate supplied expression, return solution as C code. Expression should be of the form "f(x)", where "x" is @@ -595,11 +601,15 @@ def differentiate2c(expression, dependent_var, vars, prev_expressions=None): vars: set of all other variables used in expression, e.g. {"a", "b", "c"} prev_expressions: time-ordered list of preceeding expressions to evaluate & substitute, e.g. ["b = x + c", "a = 12*b"] + stepsize: in case an analytic expression is not possible, finite differences are used; + this argument sets the step size Returns: string containing analytic derivative of expression (including any substitutions of variables from supplied prev_expressions) w.r.t. dependent_var as C code. """ + if stepsize <= 0: + raise ValueError("arg `stepsize` must be > 0") prev_expressions = prev_expressions or [] # every symbol (a.k.a variable) that SymPy # is going to manipulate needs to be declared @@ -647,7 +657,7 @@ def differentiate2c(expression, dependent_var, vars, prev_expressions=None): if needs_finite_differences(diff): diff = ( transform_expression(diff, discretize_derivative) - .subs({finite_difference_step_variable(x): 1e-3}) + .subs({finite_difference_step_variable(x): stepsize}) .evalf() ) diff --git a/test/unit/ode/test_ode.py b/test/unit/ode/test_ode.py index e3a25b06e5..390c938f9a 100644 --- a/test/unit/ode/test_ode.py +++ b/test/unit/ode/test_ode.py @@ -5,6 +5,7 @@ from nmodl.ode import differentiate2c, integrate2c import numpy as np +import pytest import sympy as sp @@ -123,6 +124,13 @@ def test_differentiate2c(): .evalf() ), ) + with pytest.raises(ValueError): + differentiate2c( + "-f(x)", + "x", + {}, + stepsize=-1, + ) def test_integrate2c(): From c1e7fd3bb9657d07f5d567e435d1893e4a8d06c2 Mon Sep 17 00:00:00 2001 From: Goran Jelic-Cizmek Date: Wed, 2 Oct 2024 12:45:44 +0200 Subject: [PATCH 5/7] Try Python 3.9 maybe? --- .github/workflows/nmodl-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nmodl-ci.yml b/.github/workflows/nmodl-ci.yml index 09a2a4d20d..3d981241cc 100644 --- a/.github/workflows/nmodl-ci.yml +++ b/.github/workflows/nmodl-ci.yml @@ -16,7 +16,7 @@ on: env: CTEST_PARALLEL_LEVEL: 1 - PYTHON_VERSION: 3.8 + PYTHON_VERSION: 3.9 DESIRED_CMAKE_VERSION: 3.15.0 jobs: From 34371ccfb22159b697714355c26dd7248b7a5c5d Mon Sep 17 00:00:00 2001 From: Goran Jelic-Cizmek Date: Wed, 2 Oct 2024 14:50:58 +0200 Subject: [PATCH 6/7] Stop using numpy to please MacOS dual-arch build --- test/unit/ode/test_ode.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/test/unit/ode/test_ode.py b/test/unit/ode/test_ode.py index 390c938f9a..df5f6c4f0a 100644 --- a/test/unit/ode/test_ode.py +++ b/test/unit/ode/test_ode.py @@ -4,7 +4,6 @@ # SPDX-License-Identifier: Apache-2.0 from nmodl.ode import differentiate2c, integrate2c -import numpy as np import pytest import sympy as sp @@ -109,20 +108,22 @@ def test_differentiate2c(): ) # instead of comparing the expression as a string, we convert the string # back to an expression and compare with an explicit function - for value in np.linspace(-5, 5, 100): - np.testing.assert_allclose( + size = 100 + for index in range(size): + a, b = -5, 5 + value = (b - a) * index / size + a + pytest.approx( float( sp.sympify(result) .subs(sp.Function("f"), sp.sin) .subs({"x": value}) .evalf() - ), - float( - -sp.Derivative(sp.sin("x")) - .as_finite_difference(1e-3) - .subs({"x": value}) - .evalf() - ), + ) + ) == float( + -sp.Derivative(sp.sin("x")) + .as_finite_difference(1e-3) + .subs({"x": value}) + .evalf() ) with pytest.raises(ValueError): differentiate2c( From 030d086476229bedd666644010aece74ec8b580d Mon Sep 17 00:00:00 2001 From: JCGoran Date: Wed, 2 Oct 2024 17:21:22 +0200 Subject: [PATCH 7/7] Bring back Python 3.8 --- .github/workflows/nmodl-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nmodl-ci.yml b/.github/workflows/nmodl-ci.yml index 3d981241cc..09a2a4d20d 100644 --- a/.github/workflows/nmodl-ci.yml +++ b/.github/workflows/nmodl-ci.yml @@ -16,7 +16,7 @@ on: env: CTEST_PARALLEL_LEVEL: 1 - PYTHON_VERSION: 3.9 + PYTHON_VERSION: 3.8 DESIRED_CMAKE_VERSION: 3.15.0 jobs: