From b31fd039a257dc5b646659f5a8eccd4d1615d3a6 Mon Sep 17 00:00:00 2001 From: Thomas Baumann <39156931+brownbaerchen@users.noreply.github.com> Date: Thu, 7 Mar 2024 16:32:47 -0500 Subject: [PATCH 1/8] Made aborting the step at growing residual optional (#405) --- .../convergence_controller_classes/adaptivity.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pySDC/implementations/convergence_controller_classes/adaptivity.py b/pySDC/implementations/convergence_controller_classes/adaptivity.py index b2bbee55c2..75a4faaa9f 100644 --- a/pySDC/implementations/convergence_controller_classes/adaptivity.py +++ b/pySDC/implementations/convergence_controller_classes/adaptivity.py @@ -208,6 +208,7 @@ def setup(self, controller, params, description, **kwargs): 'residual_max_tol': 1e9, 'maxiter': description['sweeper_params'].get('maxiter', 99), 'interpolate_between_restarts': True, + 'abort_at_growing_residual': True, **super().setup(controller, params, description, **kwargs), } if defaults['restol_rel']: @@ -232,7 +233,12 @@ def determine_restart(self, controller, S, **kwargs): self.trigger_restart_upon_nonconvergence(S) elif self.get_local_error_estimate(controller, S, **kwargs) > self.params.e_tol: S.status.restart = True - elif S.status.time_size == 1 and self.res_last_iter < S.levels[0].status.residual and S.status.iter > 0: + elif ( + S.status.time_size == 1 + and self.res_last_iter < S.levels[0].status.residual + and S.status.iter > 0 + and self.params.abort_at_growing_residual + ): self.trigger_restart_upon_nonconvergence(S) elif S.levels[0].status.residual > self.params.residual_max_tol: self.trigger_restart_upon_nonconvergence(S) From 82a6b739af533c5635826f35e23e0e54458e4fc0 Mon Sep 17 00:00:00 2001 From: Lisa Wimmer <68507897+lisawim@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:21:21 +0100 Subject: [PATCH 2/8] `pySDC`-build-in `LagrangeApproximation` class in `SwitchEstimator` (#406) * SE now uses LagrangeApproximation class + removed Lagrange class in SE * Removed log message again (not corresponding to PR) --- pySDC/core/Lagrange.py | 16 +++++- pySDC/projects/PinTSimE/switch_estimator.py | 64 +-------------------- 2 files changed, 18 insertions(+), 62 deletions(-) diff --git a/pySDC/core/Lagrange.py b/pySDC/core/Lagrange.py index 6ef2d09653..3733eee1a0 100644 --- a/pySDC/core/Lagrange.py +++ b/pySDC/core/Lagrange.py @@ -88,7 +88,7 @@ class LagrangeApproximation(object): The associated barycentric weights """ - def __init__(self, points): + def __init__(self, points, fValues=None): points = np.asarray(points).ravel() diffs = points[:, None] - points[None, :] @@ -110,6 +110,20 @@ def analytic(diffs): self.points = points self.weights = weights + # Store function values if provided + if fValues is not None: + fValues = np.asarray(fValues) + if fValues.shape != points.shape: + raise ValueError(f'fValues {fValues.shape} has not the correct shape: {points.shape}') + self.fValues = fValues + + def __call__(self, t): + assert self.fValues is not None, "cannot evaluate polynomial without fValues" + t = np.asarray(t) + values = self.getInterpolationMatrix(t.ravel()).dot(self.fValues) + values.shape = t.shape + return values + @property def n(self): return self.points.size diff --git a/pySDC/projects/PinTSimE/switch_estimator.py b/pySDC/projects/PinTSimE/switch_estimator.py index 3c438c134a..8da86a9ee6 100644 --- a/pySDC/projects/PinTSimE/switch_estimator.py +++ b/pySDC/projects/PinTSimE/switch_estimator.py @@ -4,6 +4,7 @@ from pySDC.core.Collocation import CollBase from pySDC.core.ConvergenceController import ConvergenceController, Status from pySDC.implementations.convergence_controller_classes.check_convergence import CheckConvergence +from pySDC.core.Lagrange import LagrangeApproximation class SwitchEstimator(ConvergenceController): @@ -274,23 +275,8 @@ def get_switch(t_interp, state_function, m_guess): Time point of found event. """ - LagrangeInterpolator = LagrangeInterpolation(t_interp, state_function) - - def p(t): - """ - Simplifies the call of the interpolant. - - Parameters - ---------- - t : float - Time t at which the interpolant is called. - - Returns - ------- - p(t) : float - The value of the interpolated function at time t. - """ - return LagrangeInterpolator.eval(t) + LagrangeInterpolation = LagrangeApproximation(points=t_interp, fValues=state_function) + p = lambda t: LagrangeInterpolation.__call__(t) def fprime(t): """ @@ -385,47 +371,3 @@ def newton(x0, p, fprime, newton_tol, newton_maxiter): root = x0 return root - - -class LagrangeInterpolation(object): - def __init__(self, ti, yi): - """Initialization routine""" - self.ti = np.asarray(ti) - self.yi = np.asarray(yi) - self.n = len(ti) - - def get_Lagrange_polynomial(self, t, i): - """ - Computes the basis of the i-th Lagrange polynomial. - - Parameters - ---------- - t : float - Time where the polynomial is computed at. - i : int - Index of the Lagrange polynomial - - Returns - ------- - product : float - The product of the bases. - """ - product = np.prod([(t - self.ti[k]) / (self.ti[i] - self.ti[k]) for k in range(self.n) if k != i]) - return product - - def eval(self, t): - """ - Evaluates the Lagrange interpolation at time t. - - Parameters - ---------- - t : float - Time where interpolation is computed. - - Returns - ------- - p : float - Value of interpolant at time t. - """ - p = np.sum([self.yi[i] * self.get_Lagrange_polynomial(t, i) for i in range(self.n)]) - return p From 745b02735a405a18ea18e3291c0f01c18637cd99 Mon Sep 17 00:00:00 2001 From: Thibaut Lunet Date: Wed, 27 Mar 2024 12:55:03 +0100 Subject: [PATCH 3/8] version bump --- CITATION.cff | 4 ++-- docs/source/conf.py | 2 +- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index e5b39fdac0..8616ff5c07 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -21,9 +21,9 @@ authors: orcid: https://orcid.org/0000-0002-8869-0784 affiliation: "Hamburg University of Technology, Institute of Mathematics, 21073 Hamburg, Germany" -version: 5.4.2 +version: 5.4.3 doi: 10.5281/zenodo.594191 -date-released: 2024-02-08 +date-released: 2024-03-27 keywords: - "parallel-in-time" - "spectral deferred corrections" diff --git a/docs/source/conf.py b/docs/source/conf.py index f0999afc4f..5687b3ea35 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -72,7 +72,7 @@ # The short X.Y version. version = '5.4' # The full version, including alpha/beta/rc tags. -release = '5.4.2' +release = '5.4.3' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/pyproject.toml b/pyproject.toml index 1dc503ea2c..cc9d1ea8e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = 'pySDC' -version = '5.4.2' +version = '5.4.3' description = 'A Python implementation of spectral deferred correction methods and the likes' license = {text = "BSD-2-Clause"} readme = 'README.md' From d77e43164ebf5f4965e537baacc107441b710c26 Mon Sep 17 00:00:00 2001 From: Thomas Baumann <39156931+brownbaerchen@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:00:10 +0100 Subject: [PATCH 4/8] Added hook for logging to file (#410) --- pySDC/implementations/hooks/log_solution.py | 79 +++++++++++++++++++ pySDC/tests/test_hooks/test_log_to_file.py | 85 +++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 pySDC/tests/test_hooks/test_log_to_file.py diff --git a/pySDC/implementations/hooks/log_solution.py b/pySDC/implementations/hooks/log_solution.py index f6a1980558..d6c85b8e43 100644 --- a/pySDC/implementations/hooks/log_solution.py +++ b/pySDC/implementations/hooks/log_solution.py @@ -1,4 +1,7 @@ from pySDC.core.Hooks import hooks +import pickle +import os +import numpy as np class LogSolution(hooks): @@ -63,3 +66,79 @@ def post_iteration(self, step, level_number): type='u', value=L.uend, ) + + +class LogToFile(hooks): + r""" + Hook for logging the solution to file after the step using pickle. + + Please configure the hook to your liking by manipulating class attributes. + You must set a custom path to a directory like so: + + ``` + LogToFile.path = '/my/directory/' + ``` + + Keep in mind that the hook will overwrite files without warning! + You can give a custom file name by setting the ``file_name`` class attribute and give a custom way of rendering the + index associated with individual files by giving a different lambda function ``format_index`` class attribute. This + lambda should accept one index and return one string. + + You can also give a custom ``logging_condition`` lambda, accepting the current level if you want to log selectively. + + Importantly, you may need to change ``process_solution``. By default, this will return a numpy view of the solution. + Of course, if you are not using numpy, you need to change this. Again, this is a lambda accepting the level. + + After the fact, you can use the classmethod `get_path` to get the path to a certain data or the `load` function to + directly load the solution at a given index. Just configure the hook like you did when you recorded the data + beforehand. + + Finally, be aware that using this hook with MPI parallel runs may lead to different tasks overwriting files. Make + sure to give a different `file_name` for each task that writes files. + """ + + path = None + file_name = 'solution' + logging_condition = lambda L: True + process_solution = lambda L: {'t': L.time + L.dt, 'u': L.uend.view(np.ndarray)} + format_index = lambda index: f'{index:06d}' + + def __init__(self): + super().__init__() + self.counter = 0 + + if self.path is None: + raise ValueError('Please set a path for logging as the class attribute `LogToFile.path`!') + + if os.path.isfile(self.path): + raise ValueError( + f'{self.path!r} is not a valid path to log to because a file of the same name exists. Please supply a directory' + ) + + if not os.path.isdir(self.path): + os.mkdir(self.path) + + def post_step(self, step, level_number): + if level_number > 0: + return None + + L = step.levels[level_number] + + if type(self).logging_condition(L): + path = self.get_path(self.counter) + data = type(self).process_solution(L) + + with open(path, 'wb') as file: + pickle.dump(data, file) + + self.counter += 1 + + @classmethod + def get_path(cls, index): + return f'{cls.path}/{cls.file_name}_{cls.format_index(index)}.pickle' + + @classmethod + def load(cls, index): + path = cls.get_path(index) + with open(path, 'rb') as file: + return pickle.load(file) diff --git a/pySDC/tests/test_hooks/test_log_to_file.py b/pySDC/tests/test_hooks/test_log_to_file.py new file mode 100644 index 0000000000..0f0d48f0e2 --- /dev/null +++ b/pySDC/tests/test_hooks/test_log_to_file.py @@ -0,0 +1,85 @@ +import pytest + + +def run(hook, Tend=0): + from pySDC.implementations.problem_classes.TestEquation_0D import testequation0d + from pySDC.implementations.sweeper_classes.generic_implicit import generic_implicit + from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI + + level_params = {'dt': 1.0e-1} + + sweeper_params = { + 'num_nodes': 1, + 'quad_type': 'GAUSS', + } + + description = { + 'level_params': level_params, + 'sweeper_class': generic_implicit, + 'problem_class': testequation0d, + 'sweeper_params': sweeper_params, + 'problem_params': {}, + 'step_params': {'maxiter': 1}, + } + + controller_params = { + 'hook_class': hook, + 'logger_level': 30, + } + controller = controller_nonMPI(1, controller_params, description) + if Tend > 0: + prob = controller.MS[0].levels[0].prob + u0 = prob.u_exact(0) + + _, stats = controller.run(u0, 0, Tend) + return stats + + +@pytest.mark.base +def test_errors(): + from pySDC.implementations.hooks.log_solution import LogToFile + import os + + with pytest.raises(ValueError): + run(LogToFile) + + LogToFile.path = os.getcwd() + run(LogToFile) + + path = f'{os.getcwd()}/tmp' + LogToFile.path = path + run(LogToFile) + os.path.isdir(path) + + with pytest.raises(ValueError): + LogToFile.path = __file__ + run(LogToFile) + + +@pytest.mark.base +def test_logging(): + from pySDC.implementations.hooks.log_solution import LogToFile, LogSolution + from pySDC.helpers.stats_helper import get_sorted + import os + import pickle + import numpy as np + + path = f'{os.getcwd()}/tmp' + LogToFile.path = path + Tend = 2 + + stats = run([LogToFile, LogSolution], Tend=Tend) + u = get_sorted(stats, type='u') + + u_file = [] + for i in range(len(u)): + data = LogToFile.load(i) + u_file += [(data['t'], data['u'])] + + for us, uf in zip(u, u_file): + assert us[0] == uf[0] + assert np.allclose(us[1], uf[1]) + + +if __name__ == '__main__': + test_logging() From 303635173af6be81dce4909e918fa854760c97cd Mon Sep 17 00:00:00 2001 From: Giacomo Rosilho de Souza Date: Mon, 1 Apr 2024 12:13:04 +0200 Subject: [PATCH 5/8] Monodomain project (#407) * addded some classes from oldexplicit_stabilized branch. Mainly, the problems description, datatype classes, explicit stabilized classes. Tested for IMEX on simple problems * added implicit,explicit,exponential integrator (in electrophysiology aka Rush-Larsen) * added exponential imex and mES, added parabolic_system in vec format * added new stabilized integrators using multirate, splitting and exponential approaches * before adding exponential_runge_kutta as underlying method, instead of the traditional collocation methods * added first order exponential runge kutta as underlying collocation method. To be generalized to higher order * generalized exponential runge kutta to higher order. Added exponential multirate stabilized method using exponential RK but must tbe checked properly * fixed a few things * optimized a few things * renamed project ExplicitStabilized to Monodomain * removed deprecated problems * fixed some renaming issues * did refactoring of code and put in Monodomain_NEW * removed old code and renamed new code * added finite difference discretization * added many things, cant remember * old convergence_controller * addded some classes from oldexplicit_stabilized branch. Mainly, the problems description, datatype classes, explicit stabilized classes. Tested for IMEX on simple problems * added implicit,explicit,exponential integrator (in electrophysiology aka Rush-Larsen) * added exponential imex and mES, added parabolic_system in vec format * added new stabilized integrators using multirate, splitting and exponential approaches * before adding exponential_runge_kutta as underlying method, instead of the traditional collocation methods * added first order exponential runge kutta as underlying collocation method. To be generalized to higher order * generalized exponential runge kutta to higher order. Added exponential multirate stabilized method using exponential RK but must tbe checked properly * fixed a few things * optimized a few things * renamed project ExplicitStabilized to Monodomain * removed deprecated problems * fixed some renaming issues * did refactoring of code and put in Monodomain_NEW * removed old code and renamed new code * added finite difference discretization * added many things, cant remember * added smooth TTP model for conv test, added DCT for 2D and 3D problems * added plot stuff and run scripts * fixed controller to original * removed explicit stabilized files * fixed other files * removed obsolete splittings from ionic models * removed old sbatch scripts * removed mass transfer and sweeper * fixed something * removed my base transfer * removed hook class pde * removed FD files * fixed some calls to FD stuff * removed FEM FEniCSx files * renamed FD_Vector to DCT_Vector * added hook for output and visualization script * removed plot scripts * removed run scripts, except convergence * removed convergence experiments script * fixed TestODE * added stability test in run_TestODE * added stability test in run_TestODE * added stability test in run_TestODE * removed obsolete stuff in TestODE * removed unneeded stuff from run_MonodomainODE * cleaned a bit run_MonodomainODE * removed utils/ * added few comments, cleaned a bit * removed schedule from workflow * restored tutorial step 7 A which I has modified time ago * run black on monodomain project * fixed a formatting thing * reformatted everything with black * Revert "revert formatted with black" This reverts commit 82c82e9eb5396854c4892e1667b13975df3fb6bb. * added environment file for monodomain project, started to add stuff in workflow * added first test * added package tqdm to monodomain environment * added new TestODE using DCT_vectors instead of myfloat, moved phi_eval_lists from MonodomainODE to the sweeper * deleted old TestODE and myfloat stuff * renamed TestODEnew to TestODE * cleaned a bit * added stability, convergence and iterations tests. Changed a bit other scripts as needed * reactivated other tests in workflow * removed my tests temporarly * added monodomain marker to project pyproject.toml * changed files and function names for tests * fixed convergence test * made one test a bit shorter * added test for SDC on HH and fixed missing feature in SDC imex sweeper for monodomain * reformatted with correct black options * fixed a lint error * another lint error * adding tests with plot * modified convergence test * added test iterations in parallel * removed plot from tests * added plots without writing to file * added write to file * simplified plot * new plot * fixed plot in iterations parallel * added back all tests and plots * cleaned a bit * added README * fixed readme * modified comments in controllers * try to compute phi every step * removed my controllers, check u changed before comuting phis * enabled postprocessing in pipeline * added comments to data_type classes, removed unnecessary methods * added comments to hooks * added comments to the problem classes * added comments to the run scripts * added comments to sweepers and transfer classes * fixed the readme * decommented if in pipeline * removed recv_mprobe option * changed back some stuff outiside of monodomain project * same * again * fixed Thomas hints * removed old unneeded move coverage folders * fixed previously missed Thomas comments * begin change datatype * changed run_Monodomain * added prints * fixed prints * mod print * mod print * mod print * mod print * rading init val * rading init val * removed prints * removed prints * checking longer time * checking longer time * fixed call phi eval * trying 2D * trying 2D * new_data type passing tests * removed coverage folders * optmized phi eval lists * before changing phi type * changed eval phi lists * polished a bit * before switch indeces * reformatted phi computaiton to its traspose * before changing Q * optimized integral of exp terms * changed interfate to c++ code * moved definition of dtype u f * tests passed after code refactoring --- .github/workflows/ci_pipeline.yml | 49 +- docs/source/index.rst | 1 + docs/source/projects/monodomain.rst | 1 + pySDC/projects/Monodomain/README.rst | 94 +++ .../Monodomain/datatype_classes/my_mesh.py | 5 + .../Monodomain/etc/environment-monodomain.yml | 24 + .../Monodomain/hooks/HookClass_pde.py | 34 ++ .../hooks/HookClass_post_iter_info.py | 34 ++ .../problem_classes/MonodomainODE.py | 408 +++++++++++++ .../Monodomain/problem_classes/TestODE.py | 119 ++++ .../ionicmodels/cpp/__init__.py | 5 + .../ionicmodels/cpp/bindings_definitions.cpp | 83 +++ .../ionicmodels/cpp/bistable.h | 88 +++ .../ionicmodels/cpp/compilation_command.txt | 8 + .../ionicmodels/cpp/courtemanche.h | 575 ++++++++++++++++++ .../ionicmodels/cpp/hodgkinhuxley.h | 177 ++++++ .../ionicmodels/cpp/ionicmodel.h | 61 ++ .../ionicmodels/cpp/tentusscher.h | 542 +++++++++++++++++ .../ionicmodels/cpp/tentusscher_smooth.h | 550 +++++++++++++++++ .../space_discretizazions/Parabolic_DCT.py | 340 +++++++++++ .../run_scripts/run_MonodomainODE.py | 418 +++++++++++++ .../run_scripts/run_MonodomainODE_cli.py | 135 ++++ .../Monodomain/run_scripts/run_TestODE.py | 301 +++++++++ .../imexexp_1st_order.py | 301 +++++++++ .../runge_kutta/imexexp_1st_order.py | 145 +++++ .../TransferVectorOfDCTVectors.py | 40 ++ .../transfer_classes/Transfer_DCT_Vector.py | 70 +++ .../Monodomain/utils/data_management.py | 107 ++++ .../visualization/show_monodomain_sol.py | 99 +++ .../test_monodomain_convergence.py | 193 ++++++ .../test_monodomain_iterations.py | 120 ++++ .../test_monodomain_iterations_parallel.py | 273 +++++++++ .../test_monodomain_stability_domain.py | 68 +++ pyproject.toml | 1 + 34 files changed, 5468 insertions(+), 1 deletion(-) create mode 100644 docs/source/projects/monodomain.rst create mode 100644 pySDC/projects/Monodomain/README.rst create mode 100644 pySDC/projects/Monodomain/datatype_classes/my_mesh.py create mode 100644 pySDC/projects/Monodomain/etc/environment-monodomain.yml create mode 100644 pySDC/projects/Monodomain/hooks/HookClass_pde.py create mode 100644 pySDC/projects/Monodomain/hooks/HookClass_post_iter_info.py create mode 100644 pySDC/projects/Monodomain/problem_classes/MonodomainODE.py create mode 100644 pySDC/projects/Monodomain/problem_classes/TestODE.py create mode 100644 pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/__init__.py create mode 100644 pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/bindings_definitions.cpp create mode 100644 pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/bistable.h create mode 100644 pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/compilation_command.txt create mode 100644 pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/courtemanche.h create mode 100644 pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/hodgkinhuxley.h create mode 100644 pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/ionicmodel.h create mode 100644 pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/tentusscher.h create mode 100644 pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/tentusscher_smooth.h create mode 100644 pySDC/projects/Monodomain/problem_classes/space_discretizazions/Parabolic_DCT.py create mode 100644 pySDC/projects/Monodomain/run_scripts/run_MonodomainODE.py create mode 100644 pySDC/projects/Monodomain/run_scripts/run_MonodomainODE_cli.py create mode 100644 pySDC/projects/Monodomain/run_scripts/run_TestODE.py create mode 100644 pySDC/projects/Monodomain/sweeper_classes/exponential_runge_kutta/imexexp_1st_order.py create mode 100644 pySDC/projects/Monodomain/sweeper_classes/runge_kutta/imexexp_1st_order.py create mode 100644 pySDC/projects/Monodomain/transfer_classes/TransferVectorOfDCTVectors.py create mode 100644 pySDC/projects/Monodomain/transfer_classes/Transfer_DCT_Vector.py create mode 100644 pySDC/projects/Monodomain/utils/data_management.py create mode 100644 pySDC/projects/Monodomain/visualization/show_monodomain_sol.py create mode 100644 pySDC/tests/test_projects/test_monodomain/test_monodomain_convergence.py create mode 100644 pySDC/tests/test_projects/test_monodomain/test_monodomain_iterations.py create mode 100644 pySDC/tests/test_projects/test_monodomain/test_monodomain_iterations_parallel.py create mode 100644 pySDC/tests/test_projects/test_monodomain/test_monodomain_stability_domain.py diff --git a/.github/workflows/ci_pipeline.yml b/.github/workflows/ci_pipeline.yml index 0c2dc97638..6f645caa94 100644 --- a/.github/workflows/ci_pipeline.yml +++ b/.github/workflows/ci_pipeline.yml @@ -144,7 +144,53 @@ jobs: path: | data_libpressio coverage_libpressio_3.10.dat - + + user_monodomain_tests_linux: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash -l {0} + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install Conda environment with Micromamba + uses: mamba-org/setup-micromamba@v1 + with: + environment-file: "pySDC/projects/Monodomain/etc/environment-monodomain.yml" + create-args: >- + python=3.10 + + - name: Compile C++ ionic models + env: + IONIC_MODELS_PATH: "pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp" + run: | + c++ -O3 -Wall -shared -std=c++11 -fPIC -fvisibility=hidden $(python3 -m pybind11 --includes) ${IONIC_MODELS_PATH}/bindings_definitions.cpp -o ${IONIC_MODELS_PATH}/ionicmodels$(python3-config --extension-suffix) + + - name: Run pytest for CPU stuff + run: | + echo "print('Loading sitecustomize.py...') + import coverage + coverage.process_startup() " > sitecustomize.py + coverage run -m pytest --continue-on-collection-errors -v --durations=0 pySDC/tests -m monodomain + + - name: Make coverage report + run: | + mv data data_monodomain + coverage combine + mv .coverage coverage_monodomain_3.10.dat + + - name: Uploading artifacts + uses: actions/upload-artifact@v3 + with: + name: cpu-test-artifacts + path: | + data_monodomain + coverage_monodomain_3.10.dat + + # user_cpu_tests_macos: # runs-on: macos-12 # @@ -206,6 +252,7 @@ jobs: - lint - user_cpu_tests_linux - user_libpressio_tests + - user_monodomain_tests_linux # - wait_for_gitlab defaults: diff --git a/docs/source/index.rst b/docs/source/index.rst index dc7836e744..8029dfa7c3 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -52,6 +52,7 @@ Projects projects/DAE.rst projects/compression.rst projects/second_order.rst + projects/monodomain.rst API documentation diff --git a/docs/source/projects/monodomain.rst b/docs/source/projects/monodomain.rst new file mode 100644 index 0000000000..91c21b7b7a --- /dev/null +++ b/docs/source/projects/monodomain.rst @@ -0,0 +1 @@ +.. include:: /../../pySDC/projects/Monodomain/README.rst \ No newline at end of file diff --git a/pySDC/projects/Monodomain/README.rst b/pySDC/projects/Monodomain/README.rst new file mode 100644 index 0000000000..fd0e4d344d --- /dev/null +++ b/pySDC/projects/Monodomain/README.rst @@ -0,0 +1,94 @@ +Exponential SDC for the Monodomain Equation in Cardiac Electrophysiology +============================================================================== +This project implements the exponential spectral deferred correction (ESDC) method for the monodomain equation in cardiac electrophysiology. +The method proposed here is an adaptation of the `ESDC method proposed by T. Buvoli `_ to the monodomain equation. +In particular, the implicit-explicit Rush-Larsen method is used as correction scheme. Moreover, not all model components have exponential terms, therefore the resulting method is an hybrid between ESDC and the standard SDC method. + +Monodomain equation +------------------- +The monodomain equation models the electrical activity in the heart. It is a reaction-diffusion equation coupled with an ordinary differential equation for the ionic model and is given by + +.. math:: + \begin{align} + \chi (C_m\frac{\partial V}{\partial t}+I_{ion}(V,z_E,z_e, t)) &= \nabla \cdot (D \nabla V) & \quad \text{in } &\Omega \times (0,T), \\ + \frac{\partial z_E}{\partial t} &= g_E(V,z_E,z_e) & \quad \text{in } &\Omega \times (0,T), \\ + \frac{\partial z_e}{\partial t} &= \Lambda_e(V)(z_e-z_{e,\infty}(V)) & \quad \text{in } &\Omega \times (0,T), \\ + \end{align} + +plus the boundary conditions, where :math:`V(t,x)\in\mathbb{R}` is the transmembrane potential and :math:`z_E(t,x)\in\mathbb{R}^n`, :math:`z_e(t,x)\in\mathbb{R}^m` are the ionic model state variables. +The ionic model right-hand side :math:`g_E` is a general nonlinear term, while :math:`\Lambda_e` is a diagonal matrix. The typical range for the number of unknowns :math:`N=1+n+m` is :math:`N\in [4,50]` and depends on the ionic model of choice. + +Spatial discretization yields a system of ODEs which can be written in compact form as + +.. math:: + \mathbf y'=f_I(\mathbf y)+f_E(\mathbf y)+f_e(\mathbf y), + +where :math:`\mathbf y(t)\in\mathbb{R}^{M N}` is the vector of unknowns and :math:`M` the number of mesh nodes. +Concerning the right-hand sides, :math:`f_I` is a linear term for the discrete diffusion, :math:`f_E` is a nonlinear but non-stiff term for :math:`I_{ion},g_E`, and :math:`f_e` is a severely stiff term for :math:`\Lambda_e(V)(z_e-z_{e,\infty}(V))`. + +The standard (serial) way of integrating the monodomain equation is by using a splitting method, where :math:`f_I` is integrated implicitly, :math:`f_E` explicitly, and :math:`f_e` using the exponential Euler method (which is inexpensive due to the diagonal structure of :math:`\Lambda_e`). We denote this method as IMEXEXP. + +The ESDC method for the monodomain equation +------------------------------------------- +A possible way to parallelize the integration of the monodomain equation is by employing the SDC method in combination with the IMEXEXP approach for the correction scheme (preconditioner). +However, this approach is unstable due to the severe stiffness of :math:`f_e`. +Therefore we propose a hybrid method, where we employ SDC for the :math:`f_I,f_E` terms and ESDC for the :math:`f_e` term. For the correcttion scheme we still use the IMEXEXP method. +The resulting method can be seen as a particular case of ESDC and will be denoted by ESDC in the next figures, for simplicity. + +Running the code +---------------- +Due to their complexity, ionic models are coded in C++ and wrapped to Python. Therefore, before running any example you need to compile the ionic models by running the following command in the root folder: + +.. code-block:: + + export IONIC_MODELS_PATH=pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp + c++ -O3 -Wall -shared -std=c++11 -fPIC -fvisibility=hidden $(python3 -m pybind11 --includes) ${IONIC_MODELS_PATH}/bindings_definitions.cpp -o ${IONIC_MODELS_PATH}/ionicmodels$(python3-config --extension-suffix) + +Then an example can be run: + +.. code-block:: + + cd pySDC/projects/Monodomain/run_scripts + mpirun -n 4 python run_MonodomainODE_cli.py --dt 0.05 --end_time 0.2 --num_nodes 6,3 --domain_name cube_1D --refinements 0 --ionic_model_name TTP --truly_time_parallel --n_time_ranks 4 + +Stability +--------- +We display here the stability domain of the ESDC and SDC methods, both with IMEXEXP as correction scheme, applied to the test problem + +.. math:: + y'=\lambda_I y+\lambda_E y+\lambda_e y, + +with :math:`\lambda_I,\lambda_E,\lambda_e` representing :math:`f_I,f_E,f_e`, respectively. +We fix :math:`\lambda_E=-1` and vary the stiff terms :math:`\lambda_I,\lambda_e` only. We see that the ESDC method is stable for all tested values of :math:`\lambda_I,\lambda_e`, while SDC is not. + +.. image:: ../../../data/stability_domain_IMEXEXP_EXPRK.png + :scale: 60 % +.. image:: ../../../data/stability_domain_IMEXEXP.png + :scale: 60 % + +Convergence +----------- +Here we verify convergence of the ESDC method for the monodomain equation. +We fix the number of collocation nodes to :math:`m=6` and perform a convergence experiment fixing the number of sweeps to either :math:`k=3` or :math:`k=6`. +We use the ten Tusscher-Panfilov ionic model, which is employed in practical applications. +We see that we gain one order of accuracy per sweep, as expected. + +.. image:: ../../../data/convergence_ESDC_fixed_iter.png + :scale: 100 % + + +Iterations +---------- +Here we consider three methods: + +* ESDC: with :math:`m=6` collocation nodes. +* MLESDC: This is a multilevel version of ESDC with :math:`m=6` collocation nodes on the fine level and :math:`m=3` nodes on the coarse level. +* PFASST: Combination of the PFASST parallelization method with MLESDC, using 24 processors. + +We display the number of iterations required by each method to reach a given tolerance and the residual at convergence. As ionic model we use again the ten Tusscher-Panfilov model. +We see that PFASST requires a reasonalbly small number of iterations, comparable to the serial counterparts ESDC and MLESDC. + +.. image:: ../../../data/niter_VS_time.png + :scale: 100 % +.. image:: ../../../data/res_VS_time.png + :scale: 100 % \ No newline at end of file diff --git a/pySDC/projects/Monodomain/datatype_classes/my_mesh.py b/pySDC/projects/Monodomain/datatype_classes/my_mesh.py new file mode 100644 index 0000000000..d07079bea0 --- /dev/null +++ b/pySDC/projects/Monodomain/datatype_classes/my_mesh.py @@ -0,0 +1,5 @@ +from pySDC.implementations.datatype_classes.mesh import MultiComponentMesh + + +class imexexp_mesh(MultiComponentMesh): + components = ['impl', 'expl', 'exp'] diff --git a/pySDC/projects/Monodomain/etc/environment-monodomain.yml b/pySDC/projects/Monodomain/etc/environment-monodomain.yml new file mode 100644 index 0000000000..3fe8c09c3a --- /dev/null +++ b/pySDC/projects/Monodomain/etc/environment-monodomain.yml @@ -0,0 +1,24 @@ +name: pySDC_monodomain +channels: + - conda-forge + - defaults +dependencies: + - python + - numpy + - scipy>=0.17.1 + - matplotlib>=3.0 + - sympy>=1.0 + - numba>=0.35 + - dill>=0.2.6 + - pytest + - pytest-benchmark + - pytest-timeout + - pytest-order + - coverage[toml] + - sphinx + - numdifftools + - pybind11 + - mpi4py + - mpich + - tqdm + - pymp-pypi diff --git a/pySDC/projects/Monodomain/hooks/HookClass_pde.py b/pySDC/projects/Monodomain/hooks/HookClass_pde.py new file mode 100644 index 0000000000..e4940ee153 --- /dev/null +++ b/pySDC/projects/Monodomain/hooks/HookClass_pde.py @@ -0,0 +1,34 @@ +from pySDC.core.Hooks import hooks + + +class pde_hook(hooks): + """ + Hook class to write the solution to file. + """ + + def __init__(self): + super(pde_hook, self).__init__() + + def pre_run(self, step, level_number): + """ + Overwrite default routine called before time-loop starts + It calls the default routine and then writes the initial value to file. + """ + super(pde_hook, self).pre_run(step, level_number) + + L = step.levels[level_number] + P = L.prob + if level_number == 0 and L.time == P.t0: + P.write_solution(L.u[0], P.t0) + + def post_step(self, step, level_number): + """ + Overwrite default routine called after each step. + It calls the default routine and then writes the solution to file. + """ + super(pde_hook, self).post_step(step, level_number) + + if level_number == 0: + L = step.levels[level_number] + P = L.prob + P.write_solution(L.uend, L.time + L.dt) diff --git a/pySDC/projects/Monodomain/hooks/HookClass_post_iter_info.py b/pySDC/projects/Monodomain/hooks/HookClass_post_iter_info.py new file mode 100644 index 0000000000..6cbb98100a --- /dev/null +++ b/pySDC/projects/Monodomain/hooks/HookClass_post_iter_info.py @@ -0,0 +1,34 @@ +import time +from pySDC.core.Hooks import hooks + + +class post_iter_info_hook(hooks): + """ + Hook class to write additional iteration information to the command line. + It is used to print the final residual, after u[0] has been updated with the new value from the previous step. + This residual is the one used to check the convergence of the iteration and when running in parallel is different from + the one printed at IT_FINE. + """ + + def __init__(self): + super(post_iter_info_hook, self).__init__() + + def post_iteration(self, step, level_number): + """ + Overwrite default routine called after each iteration. + It calls the default routine and then writes the residual to the command line. + We call this the residual at IT_END. + """ + super().post_iteration(step, level_number) + self.__t1_iteration = time.perf_counter() + + L = step.levels[level_number] + + self.logger.info( + "Process %2i on time %8.6f at stage %15s: ----------- Iteration: %2i --------------- " "residual: %12.8e", + step.status.slot, + L.time, + "IT_END", + step.status.iter, + L.status.residual, + ) diff --git a/pySDC/projects/Monodomain/problem_classes/MonodomainODE.py b/pySDC/projects/Monodomain/problem_classes/MonodomainODE.py new file mode 100644 index 0000000000..ff22af7f18 --- /dev/null +++ b/pySDC/projects/Monodomain/problem_classes/MonodomainODE.py @@ -0,0 +1,408 @@ +from pathlib import Path +import logging +import numpy as np +from pySDC.core.Problem import ptype +from pySDC.implementations.datatype_classes.mesh import mesh +from pySDC.projects.Monodomain.datatype_classes.my_mesh import imexexp_mesh +from pySDC.projects.Monodomain.problem_classes.space_discretizazions.Parabolic_DCT import Parabolic_DCT +import pySDC.projects.Monodomain.problem_classes.ionicmodels.cpp as ionicmodels + + +class MonodomainODE(ptype): + """ + A class for the discretization of the Monodomain equation. The Monodomain equation is a parabolic PDE composed of + a reaction-diffusion equation coupled with an ODE system. The unknowns are the potential V and the ionic model variables (g_1,...,g_N). + The reaction-diffusion equation is discretized in another class, where any spatial discretization can be used. + The ODE system is the ionic model, which doesn't need spatial discretization, being a system of ODEs. + + + + Attributes: + ----------- + parabolic: The parabolic problem class used to discretize the reaction-diffusion equation + ionic_model: The ionic model used to discretize the ODE system. This is a wrapper around the actual ionic model, which is written in C++. + size: The number of variables in the ionic model + vector_type: The type of vector used to store a single unknown (e.g. V). This data type depends on spatial discretization, hence on the parabolic class. + dtype_u: The type of vector used to store all the unknowns (V,g_1,...,g_N). This is a vector of vector_type. + dtype_f: The type of vector used to store the right-hand side of the ODE system stemming from the monodomain equation. This is a vector of vector_type. + output_folder: The folder where the solution is written to file + t0: The initial simulation time. This is 0.0 by default but can be changed by the user in order to skip the initial stimulus. + Tend: The duration of the simulation. + output_V_only: If True, only the potential V is written to file. If False, all the ionic model variables are written to file. + read_init_val: If True, the initial value is read from file. If False, the initial value is at equilibrium. + """ + + dtype_u = mesh + dtype_f = mesh + + def __init__(self, **problem_params): + self.logger = logging.getLogger("step") + + self.parabolic = Parabolic_DCT(**problem_params) + + self.define_ionic_model(problem_params["ionic_model_name"]) + + self.init = ((self.size, *self.parabolic.init[0]), self.parabolic.init[1], self.parabolic.init[2]) + + # invoke super init + super(MonodomainODE, self).__init__(self.init) + # store all problem params dictionary values as attributes + self._makeAttributeAndRegister(*problem_params.keys(), localVars=problem_params, readOnly=True) + + self.define_stimulus() + + # initial and end time + self.t0 = 0.0 + self.Tend = 50.0 if self.end_time < 0.0 else self.end_time + + # init output stuff + self.output_folder = ( + Path(self.output_root) + / Path(self.parabolic.domain_name) + / Path(self.parabolic.mesh_name) + / Path(self.ionic_model_name) + ) + self.parabolic.init_output(self.output_folder) + + def init_exp_extruded(self, new_dim_shape): + # The info needed to initialize a new vector of size (M,N) where M is the number of variables in the + # ionic model with exponential terms and N is the number of dofs in the mesh. + # The vector is further extruded to additional dimensions with shape new_dim_shape. + return ((*new_dim_shape, len(self.rhs_exp_indeces), self.init[0][1]), self.init[1], self.init[2]) + + def write_solution(self, uh, t): + # write solution to file, only the potential V=uh[0], not the ionic model variables + self.parabolic.write_solution(uh[0], t) + + def write_reference_solution(self, uh, all=False): + # write solution to file, only the potential V=uh[0] or all variables if all=True + self.parabolic.write_reference_solution(uh, list(range(self.size)) if all else [0]) + + def read_reference_solution(self, uh, ref_file_name, all=False): + # read solution from file, only the potential V=uh[0] or all variables if all=True + # returns true if read was successful, false else + return self.parabolic.read_reference_solution(uh, list(range(self.size)) if all else [0], ref_file_name) + + def initial_value(self): + # Create initial value. Every variable is constant in space + u0 = self.dtype_u(self.init) + init_vals = self.ionic_model.initial_values() + for i in range(self.size): + u0[i][:] = init_vals[i] + + # overwrite the initial value with solution from file if desired + if self.read_init_val: + read_ok = self.read_reference_solution(u0, self.init_val_name, True) + assert read_ok, "ERROR: Could not read initial value from file." + + return u0 + + def compute_errors(self, uh): + """ + Compute L2 error of uh[0] (potential V) + Args: + uh (VectorOfVectors): solution as vector of vectors + + Returns: + computed (bool): if error computation was successful + error (float): L2 error + rel_error (float): relative L2 error + """ + ref_sol_V = self.dtype_u(init=self.init, val=0.0) + read_ok = self.read_reference_solution(ref_sol_V, self.ref_sol, False) + if read_ok: + error_L2, rel_error_L2 = self.parabolic.compute_errors(uh[0], ref_sol_V[0]) + + print(f"L2-errors: {error_L2}") + print(f"Relative L2-errors: {rel_error_L2}") + + return True, error_L2, rel_error_L2 + else: + return False, 0.0, 0.0 + + def define_ionic_model(self, ionic_model_name): + self.scale_Iion = 0.01 # used to convert currents in uA/cm^2 to uA/mm^2 + # scale_im is applied to the rhs of the ionic model, so that the rhs is in units of mV/ms + self.scale_im = self.scale_Iion / self.parabolic.Cm + + if ionic_model_name in ["HodgkinHuxley", "HH"]: + self.ionic_model = ionicmodels.HodgkinHuxley(self.scale_im) + elif ionic_model_name in ["Courtemanche1998", "CRN"]: + self.ionic_model = ionicmodels.Courtemanche1998(self.scale_im) + elif ionic_model_name in ["TenTusscher2006_epi", "TTP"]: + self.ionic_model = ionicmodels.TenTusscher2006_epi(self.scale_im) + elif ionic_model_name in ["TTP_S", "TTP_SMOOTH"]: + self.ionic_model = ionicmodels.TenTusscher2006_epi_smooth(self.scale_im) + elif ionic_model_name in ["BiStable", "BS"]: + self.ionic_model = ionicmodels.BiStable(self.scale_im) + else: + raise Exception("Unknown ionic model.") + + self.size = self.ionic_model.size + + def define_stimulus(self): + + stim_dur = 2.0 + if "cuboid" in self.parabolic.domain_name: + self.stim_protocol = [[0.0, stim_dur]] # list of stimuli times and stimuli durations + self.stim_intensities = [50.0] # list of stimuli intensities + self.stim_centers = [[0.0, 0.0, 0.0]] # list of stimuli centers + r = 1.5 + self.stim_radii = [[r, r, r]] * len( + self.stim_protocol + ) # list of stimuli radii in the three directions (x,y,z) + elif "cube" in self.parabolic.domain_name: + self.stim_protocol = [[0.0, 2.0], [1000.0, 10.0]] + self.stim_intensities = [50.0, 80.0] + centers = [[0.0, 50.0, 50.0], [58.5, 0.0, 50.0]] + self.stim_centers = [centers[i] for i in range(len(self.stim_protocol))] + self.stim_radii = [[1.0, 50.0, 50.0], [1.5, 60.0, 50.0]] + else: + raise Exception("Unknown domain name.") + + self.stim_protocol = np.array(self.stim_protocol) + self.stim_protocol[:, 0] -= self.init_time # shift stimulus times by the initial time + + # index of the last stimulus applied. The value -1 means no stimulus has been applied yet. + self.last_stim_index = -1 + + def eval_f(self, u, t, fh=None): + if fh is None: + fh = self.dtype_f(init=self.init, val=0.0) + + # eval ionic model rhs on u and put result in fh. All indices of the vector of vector fh must be computed (list(range(self.size)) + self.eval_expr(self.ionic_model.f, u, fh, list(range(self.size)), False) + # apply stimulus + fh[0] += self.Istim(t) + + # apply diffusion + self.parabolic.add_disc_laplacian(u[0], fh[0]) + + return fh + + def Istim(self, t): + tol = 1e-8 + for i, (stim_time, stim_dur) in enumerate(self.stim_protocol): + # Look for which stimulus to apply at the current time t by checking the stimulus protocol: + # Check if t is in the interval [stim_time, stim_time+stim_dur] with a tolerance tol + # and apply the corresponding stimulus + if (t + stim_dur * tol >= stim_time) and (t + stim_dur * tol < stim_time + stim_dur): + # if the stimulus is not the same as the last one applied, update the last_stim_index and the space_stim vector + if i != self.last_stim_index: + self.last_stim_index = i + # get the vector of zeros and ones defining the stimulus region + self.space_stim = self.parabolic.stim_region(self.stim_centers[i], self.stim_radii[i]) + # scale by the stimulus intensity and apply the change of units + self.space_stim *= self.scale_im * self.stim_intensities[i] + return self.space_stim + + return self.parabolic.zero_stim_vec + + def eval_expr(self, expr, u, fh, indeces, zero_untouched_indeces=True): + # evaluate the expression expr on u and put the result in fh + # Here expr is a wrapper on a C++ function that evaluates the rhs of the ionic model (or part of it) + if expr is not None: + expr(u, fh) + + # indeces is a list of integers indicating which variables are modified by the expression expr. + # This information is known a priori. Here we use it to zero the variables that are not modified by expr (if zero_untouched_indeces is True) + if zero_untouched_indeces: + non_indeces = [i for i in range(self.size) if i not in indeces] + for i in non_indeces: + fh[i][:] = 0.0 + + +class MultiscaleMonodomainODE(MonodomainODE): + """ + The multiscale version of the MonodomainODE problem. This class is used to solve the monodomain equation with a multirate solver. + The main difference with respect to the MonodomainODE class is that the right-hand side of the ODE system is split into three parts: + - impl: The discrete Laplacian. This is a stiff term threated implicitly by time integrators. + - expl: The non stiff term of the ionic models, threated explicitly by time integrators. + - exp: The very stiff but diagonal terms of the ionic models, threated exponentially by time integrators. + """ + + dtype_f = imexexp_mesh + + def __init__(self, **problem_params): + super(MultiscaleMonodomainODE, self).__init__(**problem_params) + + self.define_splittings() + + self.constant_lambda_and_phi = False + + def define_splittings(self): + """ + This function defines the splittings used in the problem. + The im_* variables are meant for internal use, the rhs_* for external use (i.e. in the sweeper). + The *_args and *_indeces are list of integers. + The *_args are list of variables that are needed to evaluate a function plus the variables that are modified by the function. + The *_indeces are the list of variables that are modified by the function (subset of args). + Example: for f(x_0,x_1,x_2,x_3,x_4)=f(x_0,x_2,x_4)=(y_0,y_1,0,0,y_4) we have + f_args=[0,1,2,4]=([0,2,4] union [0,1,4]) since f needs x_0,x_2,x_4 and y_0,y_1,y_4 are effective outputs of the function (others are zero). + f_indeces=[0,1,4] since only y_0,y_1,y_4 are outputs of the function, y_2,y_3 are zero + + The ionic model has many variables (say M) and each variable has the same number of dofs as the mesh (say N). + Therefore the problem has size N*M and quickly becomes very large. Thanks to args and indeces we can: + - avoid to copy the whole vector M*N of variables when we only need a subset, for instance 2*N + - avoid unnecessary operations on the whole vector, for instance update only the variables that are effective outputs of a function (indeces), + and so on. + + Yeah, it's a bit a mess, but helpful. + """ + # define nonstiff term (explicit part) + # the wrapper to c++ expression that evaluates the nonstiff part of the ionic model + self.im_f_nonstiff = self.ionic_model.f_expl + # the args of f_expl + self.im_nonstiff_args = self.ionic_model.f_expl_args + # the indeces of f_expl + self.im_nonstiff_indeces = self.ionic_model.f_expl_indeces + + # define stiff term (implicit part) + self.im_f_stiff = None # no stiff part coming from ionic model to be threated implicitly. Indeed, all the stiff terms are diagonal and are threated exponentially. + self.im_stiff_args = [] + self.im_stiff_indeces = [] + + # define exp term (eponential part) + # the exponential term is defined by f_exp(u)= lmbda(u)*(u-yinf(u)), hence we only need to define lmbda and yinf + # the wrapper to c++ expression that evaluates lmbda(u) + self.im_lmbda_exp = self.ionic_model.lmbda_exp + # the wrapper to c++ expression that evaluates lmbda(u) and yinf(u) + self.im_lmbda_yinf_exp = self.ionic_model.lmbda_yinf_exp + # the args of lmbda and yinf (they are the same) + self.im_exp_args = self.ionic_model.f_exp_args + # the indeces of lmbda and yinf + self.im_exp_indeces = self.ionic_model.f_exp_indeces + + # the spectral radius of the jacobian of non stiff term. We use a bound + self.rho_nonstiff_cte = self.ionic_model.rho_f_expl() + + self.rhs_stiff_args = self.im_stiff_args + self.rhs_stiff_indeces = self.im_stiff_indeces + # Add the potential V index 0 to the rhs_stiff_args and rhs_stiff_indeces. + # Indeed V is used to compute the Laplacian and is affected by the Laplacian, which is the implicit part of the problem. + if 0 not in self.rhs_stiff_args: + self.rhs_stiff_args = [0] + self.rhs_stiff_args + if 0 not in self.rhs_stiff_indeces: + self.rhs_stiff_indeces = [0] + self.rhs_stiff_indeces + + self.rhs_nonstiff_args = self.im_nonstiff_args + self.rhs_nonstiff_indeces = self.im_nonstiff_indeces + # Add the potential V index 0 to the rhs_nonstiff_indeces. Indeed V is affected by the stimulus, which is a non stiff term. + if 0 not in self.rhs_nonstiff_indeces: + self.rhs_nonstiff_indeces = [0] + self.rhs_nonstiff_indeces + + self.im_non_exp_indeces = [i for i in range(self.size) if i not in self.im_exp_indeces] + + self.rhs_exp_args = self.im_exp_args + self.rhs_exp_indeces = self.im_exp_indeces + + self.rhs_non_exp_indeces = self.im_non_exp_indeces + + # a vector of ones, useful + self.one = self.dtype_u(init=self.init, val=1.0) + + # some space to store lmbda and yinf + self.lmbda = self.dtype_u(init=self.init, val=0.0) + self.yinf = self.dtype_u(init=self.init, val=0.0) + + def solve_system(self, rhs, factor, u0, t, u_sol=None): + """ + Solve the system u_sol[0] = (M-factor*A)^{-1} * M * rhs[0] + and sets u_sol[i] = rhs[i] for i>0 (as if A=0 for i>0) + + Arguments: + rhs (dtype_u): right-hand side + factor (float): factor multiplying the Laplacian + u0 (dtype_u): initial guess + t (float): current time + u_sol (dtype_u, optional): some space to store the solution. If None, a new space is allocated. Can be the same as rhs. + """ + if u_sol is None: + u_sol = self.dtype_u(init=self.init, val=0.0) + + self.parabolic.solve_system(rhs[0], factor, u0[0], t, u_sol[0]) + + if rhs is not u_sol: + for i in range(1, self.size): + u_sol[i][:] = rhs[i][:] + + return u_sol + + def eval_f(self, u, t, eval_impl=True, eval_expl=True, eval_exp=True, fh=None, zero_untouched_indeces=True): + """ + Evaluates the right-hand side terms. + + Arguments: + u (dtype_u): the current solution + t (float): the current time + eval_impl (bool, optional): if True, evaluates the implicit part of the right-hand side. Default is True. + eval_expl (bool, optional): if True, evaluates the explicit part of the right-hand side. Default is True. + eval_exp (bool, optional): if True, evaluates the exponential part of the right-hand side. Default is True. + fh (dtype_f, optional): space to store the right-hand side. If None, a new space is allocated. Default is None. + zero_untouched_indeces (bool, optional): if True, the variables that are not modified by the right-hand side are zeroed. Default is True. + """ + + if fh is None: + fh = self.dtype_f(init=self.init, val=0.0) + + if eval_expl: + fh.expl = self.eval_f_nonstiff(u, t, fh.expl, zero_untouched_indeces) + + if eval_impl: + fh.impl = self.eval_f_stiff(u, t, fh.impl, zero_untouched_indeces) + + if eval_exp: + fh.exp = self.eval_f_exp(u, t, fh.exp, zero_untouched_indeces) + + return fh + + def eval_f_nonstiff(self, u, t, fh_nonstiff, zero_untouched_indeces=True): + # eval ionic model nonstiff terms + self.eval_expr(self.im_f_nonstiff, u, fh_nonstiff, self.im_nonstiff_indeces, zero_untouched_indeces) + + if not zero_untouched_indeces and 0 not in self.im_nonstiff_indeces: + fh_nonstiff[0][:] = 0.0 + + # apply stimulus + fh_nonstiff[0] += self.Istim(t) + + return fh_nonstiff + + def eval_f_stiff(self, u, t, fh_stiff, zero_untouched_indeces=True): + # eval ionic model stiff terms + self.eval_expr(self.im_f_stiff, u, fh_stiff, self.im_stiff_indeces, zero_untouched_indeces) + + if not zero_untouched_indeces and 0 not in self.im_stiff_indeces: + fh_stiff[0][:] = 0.0 + + # apply diffusion + self.parabolic.add_disc_laplacian(u[0], fh_stiff[0]) + + return fh_stiff + + def eval_f_exp(self, u, t, fh_exp, zero_untouched_indeces=True): + # eval ionic model exp terms f_exp(u)= lmbda(u)*(u-yinf(u) + self.eval_lmbda_yinf_exp(u, self.lmbda, self.yinf) + for i in self.im_exp_indeces: + fh_exp[i][:] = self.lmbda[i] * (u[i] - self.yinf[i]) + + if zero_untouched_indeces: + fh_exp[self.im_non_exp_indeces] = 0.0 + + return fh_exp + + def lmbda_eval(self, u, t, lmbda=None): + if lmbda is None: + lmbda = self.dtype_u(init=self.init, val=0.0) + + self.eval_lmbda_exp(u, lmbda) + + lmbda[self.im_non_exp_indeces] = 0.0 + + return lmbda + + def eval_lmbda_yinf_exp(self, u, lmbda, yinf): + self.im_lmbda_yinf_exp(u, lmbda, yinf) + + def eval_lmbda_exp(self, u, lmbda): + self.im_lmbda_exp(u, lmbda) diff --git a/pySDC/projects/Monodomain/problem_classes/TestODE.py b/pySDC/projects/Monodomain/problem_classes/TestODE.py new file mode 100644 index 0000000000..62d4bf01b6 --- /dev/null +++ b/pySDC/projects/Monodomain/problem_classes/TestODE.py @@ -0,0 +1,119 @@ +import logging +import numpy as np +from pySDC.core.Problem import ptype +from pySDC.core.Common import RegisterParams +from pySDC.implementations.datatype_classes.mesh import mesh +from pySDC.projects.Monodomain.datatype_classes.my_mesh import imexexp_mesh + + +""" +Here we define the problems classes for the multirate Dahlquist test equation y'=lambda_I*y + lambda_E*y + lambda_e*y +Things are done so that it is compatible witht the sweepers. +""" + + +class Parabolic(RegisterParams): + def __init__(self, **problem_params): + self._makeAttributeAndRegister(*problem_params.keys(), localVars=problem_params, readOnly=True) + self.shape = (1,) + self.init = ((1,), None, np.dtype("float64")) + + +class TestODE(ptype): + def __init__(self, **problem_params): + self.logger = logging.getLogger("step") + + self.parabolic = Parabolic(**problem_params) + self.size = 1 # one state variable + self.init = ((self.size, *self.parabolic.init[0]), self.parabolic.init[1], self.parabolic.init[2]) + + # invoke super init + super(TestODE, self).__init__(self.init) + # store all problem params dictionary values as attributes + self._makeAttributeAndRegister(*problem_params.keys(), localVars=problem_params, readOnly=True) + + # initial and end time + self.t0 = 0.0 + self.Tend = 1.0 if self.end_time < 0.0 else self.end_time + + # set lambdas, if not provided by user + if not hasattr(self, 'lmbda_laplacian'): + self.lmbda_laplacian = -5.0 + if not hasattr(self, 'lmbda_gating'): + self.lmbda_gating = -10.0 + if not hasattr(self, 'lmbda_others'): + self.lmbda_others = -1.0 + + self.dtype_u = mesh + self.dtype_f = mesh + + def init_exp_extruded(self, new_dim_shape): + return ((*new_dim_shape, 1, self.init[0][1]), self.init[1], self.init[2]) + + def initial_value(self): + u0 = self.dtype_u(self.init, val=1.0) + + return u0 + + def eval_f(self, u, t, fh=None): + if fh is None: + fh = self.dtype_f(init=self.init, val=0.0) + + fh[0] = (self.lmbda_laplacian + self.lmbda_gating + self.lmbda_others) * u[0] + + return fh + + +class MultiscaleTestODE(TestODE): + def __init__(self, **problem_params): + super(MultiscaleTestODE, self).__init__(**problem_params) + + self.dtype_f = imexexp_mesh + + self.rhs_stiff_indeces = [0] + self.rhs_stiff_args = [0] + self.rhs_nonstiff_indeces = [0] + self.rhs_nonstiff_args = [0] + self.rhs_exp_args = [0] + self.rhs_exp_indeces = [0] + self.rhs_non_exp_indeces = [] + + self.constant_lambda_and_phi = True + + self.one = self.dtype_u(init=self.init, val=1.0) + + def solve_system(self, rhs, factor, u0, t, u_sol=None): + if u_sol is None: + u_sol = self.dtype_u(init=self.init, val=0.0) + + u_sol[0] = rhs[0] / (1 - factor * self.lmbda_laplacian) + + return u_sol + + def eval_f(self, u, t, eval_impl=True, eval_expl=True, eval_exp=True, fh=None, zero_untouched_indeces=True): + + if fh is None: + fh = self.dtype_f(init=self.init, val=0.0) + + if eval_expl: + fh.expl[0] = self.lmbda_others * u[0] + + if eval_impl: + fh.impl[0] = self.lmbda_laplacian * u[0] + + if eval_exp: + fh.exp[0] = self.lmbda_gating * u[0] + + return fh + + def eval_lmbda_yinf_exp(self, u, lmbda, yinf): + lmbda[0] = self.lmbda_gating + yinf[0] = 0.0 + + def lmbda_eval(self, u, t, lmbda=None): + if lmbda is None: + lmbda = self.dtype_u(init=self.init, val=0.0) + + lmbda[0] = self.lmbda_gating + + return lmbda diff --git a/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/__init__.py b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/__init__.py new file mode 100644 index 0000000000..5d5baf10ed --- /dev/null +++ b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/__init__.py @@ -0,0 +1,5 @@ +from pySDC.projects.Monodomain.problem_classes.ionicmodels.cpp.ionicmodels import HodgkinHuxley +from pySDC.projects.Monodomain.problem_classes.ionicmodels.cpp.ionicmodels import Courtemanche1998 +from pySDC.projects.Monodomain.problem_classes.ionicmodels.cpp.ionicmodels import TenTusscher2006_epi +from pySDC.projects.Monodomain.problem_classes.ionicmodels.cpp.ionicmodels import TenTusscher2006_epi_smooth +from pySDC.projects.Monodomain.problem_classes.ionicmodels.cpp.ionicmodels import BiStable diff --git a/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/bindings_definitions.cpp b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/bindings_definitions.cpp new file mode 100644 index 0000000000..e7c3a8685a --- /dev/null +++ b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/bindings_definitions.cpp @@ -0,0 +1,83 @@ +#include + +#include +#include +#include + +namespace py = pybind11; + +#include "ionicmodel.h" +#include "hodgkinhuxley.h" +#include "courtemanche.h" +#include "tentusscher.h" +#include "tentusscher_smooth.h" +#include "bistable.h" + +PYBIND11_MODULE(ionicmodels, m) +{ + m.doc() = ""; + + // A class to represent the ionic models. + py::class_ IonicModelPy(m, "IonicModel"); + IonicModelPy.def(py::init()); + IonicModelPy.def_property_readonly("f_expl_args", &IonicModel::get_f_expl_args, py::return_value_policy::copy); + IonicModelPy.def_property_readonly("f_exp_args", &IonicModel::get_f_exp_args, py::return_value_policy::copy); + IonicModelPy.def_property_readonly("f_expl_indeces", &IonicModel::get_f_expl_indeces, py::return_value_policy::copy); + IonicModelPy.def_property_readonly("f_exp_indeces", &IonicModel::get_f_exp_indeces, py::return_value_policy::copy); + IonicModelPy.def_property_readonly("size", &IonicModel::get_size, py::return_value_policy::copy); + + // A very simple ionic model with one variable one. It is used for testing purposes. With this model the + // monodomain equation reduces to a reaction-diffusion equation with one variable. + py::class_ BiStablePy(m, "BiStable"); + BiStablePy.def(py::init()); + BiStablePy.def("initial_values", &BiStable::initial_values, py::return_value_policy::copy); + BiStablePy.def("f", &BiStable::f); + BiStablePy.def("f_expl", &BiStable::f_expl); + BiStablePy.def("lmbda_exp", &BiStable::lmbda_exp); + BiStablePy.def("lmbda_yinf_exp", &BiStable::lmbda_yinf_exp); + BiStablePy.def("rho_f_expl", &BiStable::rho_f_expl); + + // The Hodgkin-Huxley ionic model. A model with 4 variables, smooth, nonstiff. Still an academic model. + py::class_ HodgkinHuxleyPy(m, "HodgkinHuxley"); + HodgkinHuxleyPy.def(py::init()); + HodgkinHuxleyPy.def("initial_values", &HodgkinHuxley::initial_values, py::return_value_policy::copy); + HodgkinHuxleyPy.def("f", &HodgkinHuxley::f); + HodgkinHuxleyPy.def("f_expl", &HodgkinHuxley::f_expl); + HodgkinHuxleyPy.def("lmbda_exp", &HodgkinHuxley::lmbda_exp); + HodgkinHuxleyPy.def("lmbda_yinf_exp", &HodgkinHuxley::lmbda_yinf_exp); + HodgkinHuxleyPy.def("rho_f_expl", &HodgkinHuxley::rho_f_expl); + + // The Courtemanche ionic model. A model with 21 variables, mildly stiff. It is a realistic model for the human atrial cells. + py::class_ Courtemanche1998Py(m, "Courtemanche1998"); + Courtemanche1998Py.def(py::init()); + Courtemanche1998Py.def("initial_values", &Courtemanche1998::initial_values, py::return_value_policy::copy); + Courtemanche1998Py.def("f", &Courtemanche1998::f); + Courtemanche1998Py.def("f_expl", &Courtemanche1998::f_expl); + Courtemanche1998Py.def("lmbda_exp", &Courtemanche1998::lmbda_exp); + Courtemanche1998Py.def("lmbda_yinf_exp", &Courtemanche1998::lmbda_yinf_exp); + Courtemanche1998Py.def("rho_f_expl", &Courtemanche1998::rho_f_expl); + + // The TenTusscher ionic model. A model with 20 variables, very stiff. It is a realistic model for the human ventricular cells. + py::class_ TenTusscher2006_epiPy(m, "TenTusscher2006_epi"); + TenTusscher2006_epiPy.def(py::init()); + TenTusscher2006_epiPy.def("initial_values", &TenTusscher2006_epi::initial_values, py::return_value_policy::copy); + TenTusscher2006_epiPy.def("f", &TenTusscher2006_epi::f); + TenTusscher2006_epiPy.def("f_expl", &TenTusscher2006_epi::f_expl); + TenTusscher2006_epiPy.def("lmbda_exp", &TenTusscher2006_epi::lmbda_exp); + TenTusscher2006_epiPy.def("lmbda_yinf_exp", &TenTusscher2006_epi::lmbda_yinf_exp); + TenTusscher2006_epiPy.def("rho_f_expl", &TenTusscher2006_epi::rho_f_expl); + + // A smoothed version TenTusscher ionic model. Indeed, in the right-hand side of the original model there are if-else clauses which are not differentiable. + // This model is a smoothed version of the original model, where the if-else clauses are removed by keeping the 'else' part of the clauses. + // The model is no more exact, from a physiological viewpoint, but the qualitative behavior is preserved. For instance it remains very stiff and + // action potentials are still propagated. Moreover, it is now differentiable. + // We use this model for convergence experiments only. + py::class_ TenTusscher2006_epi_smoothPy(m, "TenTusscher2006_epi_smooth"); + TenTusscher2006_epi_smoothPy.def(py::init()); + TenTusscher2006_epi_smoothPy.def("initial_values", &TenTusscher2006_epi_smooth::initial_values, py::return_value_policy::copy); + TenTusscher2006_epi_smoothPy.def("f", &TenTusscher2006_epi_smooth::f); + TenTusscher2006_epi_smoothPy.def("f_expl", &TenTusscher2006_epi_smooth::f_expl); + TenTusscher2006_epi_smoothPy.def("lmbda_exp", &TenTusscher2006_epi_smooth::lmbda_exp); + TenTusscher2006_epi_smoothPy.def("lmbda_yinf_exp", &TenTusscher2006_epi_smooth::lmbda_yinf_exp); + TenTusscher2006_epi_smoothPy.def("rho_f_expl", &TenTusscher2006_epi_smooth::rho_f_expl); +} \ No newline at end of file diff --git a/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/bistable.h b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/bistable.h new file mode 100644 index 0000000000..1e799636fa --- /dev/null +++ b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/bistable.h @@ -0,0 +1,88 @@ +#include +#include + +#include +#include +#include + +#include "ionicmodel.h" + +#ifndef BISTABLE +#define BISTABLE + +class BiStable : public IonicModel +{ +public: + BiStable(const double scale_); + ~BiStable(){}; + void f(py::array_t &y, py::array_t &fy); + void f_expl(py::array_t &y, py::array_t &fy); + void lmbda_exp(py::array_t &y_list, py::array_t &lmbda_list); + void lmbda_yinf_exp(py::array_t &y_list, py::array_t &lmbda_list, py::array_t &yinf_list); + py::list initial_values(); + double rho_f_expl(); + +private: + double V_th, V_depol, V_rest, a; +}; + +BiStable::BiStable(const double scale_) + : IonicModel(scale_) +{ + size = 1; + + // Set values of constants + V_th = -57.6; + V_depol = 30.; + V_rest = -85.; + a = 1.4e-3; + + assign(f_expl_args, {0}); + assign(f_exp_args, {}); + assign(f_expl_indeces, {0}); + assign(f_exp_indeces, {}); +} + +py::list BiStable::initial_values() +{ + py::list y0(size); + y0[0] = -85.0; + + return y0; +} + +void BiStable::f(py::array_t &y_list, py::array_t &fy_list) +{ + double *y_ptrs[size]; + double *fy_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(fy_list, fy_ptrs, N, n_dofs); + + // Remember to scale the first variable!!! + for (unsigned j = 0; j < n_dofs; j++) + fy_ptrs[0][j] = -scale * a * (y_ptrs[0][j] - V_th) * (y_ptrs[0][j] - V_depol) * (y_ptrs[0][j] - V_rest); +} + +void BiStable::f_expl(py::array_t &y_list, py::array_t &fy_list) +{ + this->f(y_list, fy_list); +} + +void BiStable::lmbda_exp(py::array_t &y_list, py::array_t &lmbda_list) +{ + return; +} + +void BiStable::lmbda_yinf_exp(py::array_t &y_list, py::array_t &lmbda_list, py::array_t &yinf_list) +{ + return; +} + +double BiStable::rho_f_expl() +{ + return 20.; +} + +#endif \ No newline at end of file diff --git a/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/compilation_command.txt b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/compilation_command.txt new file mode 100644 index 0000000000..695d087148 --- /dev/null +++ b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/compilation_command.txt @@ -0,0 +1,8 @@ +Linux: +c++ -O3 -Wall -shared -std=c++11 -fPIC -fvisibility=hidden $(python3 -m pybind11 --includes) bindings_definitions.cpp -o ionicmodels$(python3-config --extension-suffix) + +Daint: +c++ -march=haswell -O3 -Wall -shared -std=c++11 -fPIC -fvisibility=hidden $(python3 -m pybind11 --includes) bindings_definitions.cpp -o ionicmodels$(python3-config --extension-suffix) + +Mac: +c++ -O3 -Wall -shared -std=c++11 -undefined dynamic_lookup $(python3 -m pybind11 --includes) bindings_definitions.cpp -o ionicmodels$(python3-config --extension-suffix) \ No newline at end of file diff --git a/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/courtemanche.h b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/courtemanche.h new file mode 100644 index 0000000000..b73ea0fc50 --- /dev/null +++ b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/courtemanche.h @@ -0,0 +1,575 @@ +#include +#include + +#include +#include +#include + +#include "ionicmodel.h" + +#ifndef COURTEMANCHE +#define COURTEMANCHE + +class Courtemanche1998 : public IonicModel +{ +public: + Courtemanche1998(const double scale_); + ~Courtemanche1998(){}; + void f(py::array_t &y, py::array_t &fy); + void f_expl(py::array_t &y, py::array_t &fy); + void lmbda_exp(py::array_t &y_list, py::array_t &lmbda_list); + void lmbda_yinf_exp(py::array_t &y_list, py::array_t &lmbda_list, py::array_t &yinf_list); + py::list initial_values(); + double rho_f_expl(); + +private: + double AC_CMDN_max, AC_CSQN_max, AC_Km_CMDN, AC_Km_CSQN, AC_Km_TRPN, AC_TRPN_max, AC_I_up_max, AC_K_up, AC_tau_f_Ca, AC_Ca_o, AC_K_o, AC_Na_o; + double AC_tau_tr, AC_Ca_up_max, AC_K_rel, AC_tau_u, AC_g_Ca_L, AC_I_NaCa_max, AC_K_mCa, AC_K_mNa, AC_K_sat, AC_Na_Ca_exchanger_current_gamma; + double AC_g_B_Ca, AC_g_B_K, AC_g_B_Na, AC_g_Na, AC_V_cell, AC_V_i, AC_V_rel, AC_V_up, AC_Cm, AC_F, AC_R, AC_T, AC_g_Kr, AC_i_CaP_max, AC_g_Ks, AC_Km_K_o; + double AC_Km_Na_i, AC_i_NaK_max, AC_sigma, AC_g_K1, AC_K_Q10, AC_g_to; +}; + +Courtemanche1998::Courtemanche1998(const double scale_) + : IonicModel(scale_) +{ + size = 21; + + AC_CMDN_max = 0.05; + AC_CSQN_max = 10.0; + AC_Km_CMDN = 0.00238; + AC_Km_CSQN = 0.8; + AC_Km_TRPN = 0.0005; + AC_TRPN_max = 0.07; + AC_I_up_max = 0.005; + AC_K_up = 0.00092; + AC_tau_f_Ca = 2.0; + AC_Ca_o = 1.8; + AC_K_o = 5.4; + AC_Na_o = 140.0; + AC_tau_tr = 180.0; + AC_Ca_up_max = 15.0; + AC_K_rel = 30.0; + AC_tau_u = 8.0; + AC_g_Ca_L = 0.12375; + AC_I_NaCa_max = 1600.0; + AC_K_mCa = 1.38; + AC_K_mNa = 87.5; + AC_K_sat = 0.1; + AC_Na_Ca_exchanger_current_gamma = 0.35; + AC_g_B_Ca = 0.001131; + AC_g_B_K = 0.0; + AC_g_B_Na = 6.74437500000000015e-04; + AC_g_Na = 7.8; + AC_V_cell = 20100.0; + AC_V_i = AC_V_cell * 0.68; + AC_V_rel = 0.0048 * AC_V_cell; + AC_V_up = 0.0552 * AC_V_cell; + AC_Cm = 1.0; // 100.0; + AC_F = 96.4867; + AC_R = 8.3143; + AC_T = 310.0; + AC_g_Kr = 2.94117649999999994e-02; + AC_i_CaP_max = 0.275; + AC_g_Ks = 1.29411759999999987e-01; + AC_Km_K_o = 1.5; + AC_Km_Na_i = 10.0; + AC_i_NaK_max = 5.99338739999999981e-01; + AC_sigma = 1.0 / 7.0 * (exp(AC_Na_o / 67.3) - 1.0); + AC_g_K1 = 0.09; + AC_K_Q10 = 3.0; + AC_g_to = 0.1652; + + assign(f_expl_args, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}); + assign(f_exp_args, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 15}); + assign(f_expl_indeces, {0, 12, 13, 14, 16, 17, 18, 19, 20}); + assign(f_exp_indeces, {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 15}); +} + +py::list Courtemanche1998::initial_values() +{ + py::list y0(size); + y0[0] = -81.18; + y0[1] = 0.002908; + y0[2] = 0.9649; + y0[3] = 0.9775; + y0[4] = 0.03043; + y0[5] = 0.9992; + y0[6] = 0.004966; + y0[7] = 0.9986; + y0[8] = 3.296e-05; + y0[9] = 0.01869; + y0[10] = 0.0001367; + y0[11] = 0.9996; + y0[12] = 0.7755; + y0[13] = 2.35e-112; + y0[14] = 1.0; + y0[15] = 0.9992; + y0[16] = 11.17; + y0[17] = 0.0001013; + y0[18] = 139.0; + y0[19] = 1.488; + y0[20] = 1.488; + + return y0; +} + +void Courtemanche1998::f(py::array_t &y_list, py::array_t &fy_list) +{ + double *y_ptrs[size]; + double *fy_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(fy_list, fy_ptrs, N, n_dofs); + + double y[size]; + // For linear in gating var terms + double AV_tau_w, AV_w_infinity, AV_d_infinity, AV_tau_d, AV_f_infinity, AV_tau_f, AV_alpha_h, AV_beta_h, AV_h_inf, AV_tau_h, AV_alpha_j, AV_beta_j, AV_j_inf, AV_tau_j; + double AV_alpha_m, AV_beta_m, AV_m_inf, AV_tau_m, AV_alpha_xr, AV_beta_xr, AV_xr_infinity, AV_tau_xr, AV_alpha_xs, AV_beta_xs, AV_xs_infinity; + double AV_tau_xs, AV_alpha_oa, AV_beta_oa, AV_oa_infinity, AV_tau_oa, AV_oi_infinity; + double AV_alpha_oi, AV_beta_oi, AV_tau_oi, AV_alpha_ua, AV_beta_ua, AV_ua_infinity, AV_tau_ua, AV_alpha_ui, AV_beta_ui, AV_ui_infinity, AV_tau_ui; + // for nonlinear + double AV_f_Ca_infinity, AV_i_tr, AV_i_up_leak, AV_i_rel, AV_i_up, AV_i_CaP, AV_f_NaK, AV_i_NaK, AV_E_K, AV_i_K1, AV_i_to, AV_g_Kur, AV_i_Kur; + double AV_i_Ca_L, AV_i_NaCa, AV_E_Ca, AV_i_B_K, AV_E_Na, AV_i_Kr, AV_i_Ks, AV_Fn, AV_i_B_Ca, AV_i_B_Na, AV_i_Na, AV_u_infinity, AV_tau_v, AV_v_infinity, AV_B1, AV_B2; + // Remember to scale the first variable!!! + for (unsigned j = 0; j < n_dofs; j++) + { + for (unsigned i = 0; i < size; i++) + y[i] = y_ptrs[i][j]; + + // # Linear (in the gating variables) terms + + // #/* Ca_release_current_from_JSR_w_gate */ + + AV_tau_w = abs(NV_Ith_S(y, 0) - 7.9) < 1e-10 ? 6.0 * 0.2 / 1.3 : 6.0 * (1.0 - exp((-(NV_Ith_S(y, 0) - 7.9)) / 5.0)) / ((1.0 + 0.3 * exp((-(NV_Ith_S(y, 0) - 7.9)) / 5.0)) * 1.0 * (NV_Ith_S(y, 0) - 7.9)); + AV_w_infinity = 1.0 - pow(1.0 + exp((-(NV_Ith_S(y, 0) - 40.0)) / 17.0), (-1.0)); + fy_ptrs[15][j] = (AV_w_infinity - NV_Ith_S(y, 15)) / AV_tau_w; + + // #/* L_type_Ca_channel_d_gate */ + AV_d_infinity = pow(1.0 + exp((NV_Ith_S(y, 0) + 10.0) / (-8.0)), (-1.0)); + AV_tau_d = abs(NV_Ith_S(y, 0) + 10.0) < 1e-10 ? 4.579 / (1.0 + exp((NV_Ith_S(y, 0) + 10.0) / (-6.24))) : (1.0 - exp((NV_Ith_S(y, 0) + 10.0) / (-6.24))) / (0.035 * (NV_Ith_S(y, 0) + 10.0) * (1.0 + exp((NV_Ith_S(y, 0) + 10.0) / (-6.24)))); + fy_ptrs[10][j] = (AV_d_infinity - NV_Ith_S(y, 10)) / AV_tau_d; + + // #/* L_type_Ca_channel_f_gate */ + AV_f_infinity = exp((-(NV_Ith_S(y, 0) + 28.0)) / 6.9) / (1.0 + exp((-(NV_Ith_S(y, 0) + 28.0)) / 6.9)); + AV_tau_f = 9.0 * pow(0.0197 * exp((-pow(0.0337, 2.0)) * pow(NV_Ith_S(y, 0) + 10.0, 2.0)) + 0.02, (-1.0)); + fy_ptrs[11][j] = (AV_f_infinity - NV_Ith_S(y, 11)) / AV_tau_f; + + // #/* fast_sodium_current_h_gate */ + AV_alpha_h = NV_Ith_S(y, 0) < (-40.0) ? 0.135 * exp((NV_Ith_S(y, 0) + 80.0) / (-6.8)) : 0.0; + AV_beta_h = NV_Ith_S(y, 0) < (-40.0) ? 3.56 * exp(0.079 * NV_Ith_S(y, 0)) + 310000.0 * exp(0.35 * NV_Ith_S(y, 0)) : 1.0 / (0.13 * (1.0 + exp((NV_Ith_S(y, 0) + 10.66) / (-11.1)))); + AV_h_inf = AV_alpha_h / (AV_alpha_h + AV_beta_h); + AV_tau_h = 1.0 / (AV_alpha_h + AV_beta_h); + fy_ptrs[2][j] = (AV_h_inf - NV_Ith_S(y, 2)) / AV_tau_h; + + // #/* fast_sodium_current_j_gate */ + AV_alpha_j = NV_Ith_S(y, 0) < (-40.0) ? ((-127140.0) * exp(0.2444 * NV_Ith_S(y, 0)) - 3.474e-05 * exp((-0.04391) * NV_Ith_S(y, 0))) * (NV_Ith_S(y, 0) + 37.78) / (1.0 + exp(0.311 * (NV_Ith_S(y, 0) + 79.23))) : 0.0; + AV_beta_j = NV_Ith_S(y, 0) < (-40.0) ? 0.1212 * exp((-0.01052) * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1378) * (NV_Ith_S(y, 0) + 40.14))) : 0.3 * exp((-2.535e-07) * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1) * (NV_Ith_S(y, 0) + 32.0))); + AV_j_inf = AV_alpha_j / (AV_alpha_j + AV_beta_j); + AV_tau_j = 1.0 / (AV_alpha_j + AV_beta_j); + fy_ptrs[3][j] = (AV_j_inf - NV_Ith_S(y, 3)) / AV_tau_j; + + // #/* fast_sodium_current_m_gate */ + AV_alpha_m = NV_Ith_S(y, 0) == (-47.13) ? 3.2 : 0.32 * (NV_Ith_S(y, 0) + 47.13) / (1.0 - exp((-0.1) * (NV_Ith_S(y, 0) + 47.13))); + AV_beta_m = 0.08 * exp((-NV_Ith_S(y, 0)) / 11.0); + AV_m_inf = AV_alpha_m / (AV_alpha_m + AV_beta_m); + AV_tau_m = 1.0 / (AV_alpha_m + AV_beta_m); + fy_ptrs[1][j] = (AV_m_inf - NV_Ith_S(y, 1)) / AV_tau_m; + + // #/* rapid_delayed_rectifier_K_current_xr_gate */ + AV_alpha_xr = abs(NV_Ith_S(y, 0) + 14.1) < 1e-10 ? 0.0015 : 0.0003 * (NV_Ith_S(y, 0) + 14.1) / (1.0 - exp((NV_Ith_S(y, 0) + 14.1) / (-5.0))); + AV_beta_xr = abs(NV_Ith_S(y, 0) - 3.3328) < 1e-10 ? 3.78361180000000004e-04 : 7.38980000000000030e-05 * (NV_Ith_S(y, 0) - 3.3328) / (exp((NV_Ith_S(y, 0) - 3.3328) / 5.1237) - 1.0); + AV_xr_infinity = pow(1.0 + exp((NV_Ith_S(y, 0) + 14.1) / (-6.5)), (-1.0)); + AV_tau_xr = pow(AV_alpha_xr + AV_beta_xr, (-1.0)); + fy_ptrs[8][j] = (AV_xr_infinity - NV_Ith_S(y, 8)) / AV_tau_xr; + + // #/* slow_delayed_rectifier_K_current_xs_gate */ + AV_alpha_xs = abs(NV_Ith_S(y, 0) - 19.9) < 1e-10 ? 0.00068 : 4e-05 * (NV_Ith_S(y, 0) - 19.9) / (1.0 - exp((NV_Ith_S(y, 0) - 19.9) / (-17.0))); + AV_beta_xs = abs(NV_Ith_S(y, 0) - 19.9) < 1e-10 ? 0.000315 : 3.5e-05 * (NV_Ith_S(y, 0) - 19.9) / (exp((NV_Ith_S(y, 0) - 19.9) / 9.0) - 1.0); + AV_xs_infinity = pow(1.0 + exp((NV_Ith_S(y, 0) - 19.9) / (-12.7)), (-0.5)); + AV_tau_xs = 0.5 * pow(AV_alpha_xs + AV_beta_xs, (-1.0)); + fy_ptrs[9][j] = (AV_xs_infinity - NV_Ith_S(y, 9)) / AV_tau_xs; + + // #/* transient_outward_K_current_oa_gate */ + AV_alpha_oa = 0.65 * pow(exp((NV_Ith_S(y, 0) - (-10.0)) / (-8.5)) + exp((NV_Ith_S(y, 0) - (-10.0) - 40.0) / (-59.0)), (-1.0)); + AV_beta_oa = 0.65 * pow(2.5 + exp((NV_Ith_S(y, 0) - (-10.0) + 72.0) / 17.0), (-1.0)); + AV_oa_infinity = pow(1.0 + exp((NV_Ith_S(y, 0) - (-10.0) + 10.47) / (-17.54)), (-1.0)); + AV_tau_oa = pow(AV_alpha_oa + AV_beta_oa, (-1.0)) / AC_K_Q10; + fy_ptrs[4][j] = (AV_oa_infinity - NV_Ith_S(y, 4)) / AV_tau_oa; + + // #/* transient_outward_K_current_oi_gate */ + AV_alpha_oi = pow(18.53 + 1.0 * exp((NV_Ith_S(y, 0) - (-10.0) + 103.7) / 10.95), (-1.0)); + AV_beta_oi = pow(35.56 + 1.0 * exp((NV_Ith_S(y, 0) - (-10.0) - 8.74) / (-7.44)), (-1.0)); + AV_oi_infinity = pow(1.0 + exp((NV_Ith_S(y, 0) - (-10.0) + 33.1) / 5.3), (-1.0)); + AV_tau_oi = pow(AV_alpha_oi + AV_beta_oi, (-1.0)) / AC_K_Q10; + fy_ptrs[5][j] = (AV_oi_infinity - NV_Ith_S(y, 5)) / AV_tau_oi; + + // #/* ultrarapid_delayed_rectifier_K_current_ua_gate */ + AV_alpha_ua = 0.65 * pow(exp((NV_Ith_S(y, 0) - (-10.0)) / (-8.5)) + exp((NV_Ith_S(y, 0) - (-10.0) - 40.0) / (-59.0)), (-1.0)); + AV_beta_ua = 0.65 * pow(2.5 + exp((NV_Ith_S(y, 0) - (-10.0) + 72.0) / 17.0), (-1.0)); + AV_ua_infinity = pow(1.0 + exp((NV_Ith_S(y, 0) - (-10.0) + 20.3) / (-9.6)), (-1.0)); + AV_tau_ua = pow(AV_alpha_ua + AV_beta_ua, (-1.0)) / AC_K_Q10; + fy_ptrs[6][j] = (AV_ua_infinity - NV_Ith_S(y, 6)) / AV_tau_ua; + + // #/* ultrarapid_delayed_rectifier_K_current_ui_gate */ + AV_alpha_ui = pow(21.0 + 1.0 * exp((NV_Ith_S(y, 0) - (-10.0) - 195.0) / (-28.0)), (-1.0)); + AV_beta_ui = 1.0 / exp((NV_Ith_S(y, 0) - (-10.0) - 168.0) / (-16.0)); + AV_ui_infinity = pow(1.0 + exp((NV_Ith_S(y, 0) - (-10.0) - 109.45) / 27.48), (-1.0)); + AV_tau_ui = pow(AV_alpha_ui + AV_beta_ui, (-1.0)) / AC_K_Q10; + fy_ptrs[7][j] = (AV_ui_infinity - NV_Ith_S(y, 7)) / AV_tau_ui; + + // # Non Linear (in the gating variables) terms + + // #/* L_type_Ca_channel_f_Ca_gate */ + AV_f_Ca_infinity = pow(1.0 + NV_Ith_S(y, 17) / 0.00035, (-1.0)); + fy_ptrs[12][j] = (AV_f_Ca_infinity - NV_Ith_S(y, 12)) / AC_tau_f_Ca; + + // #/* transfer_current_from_NSR_to_JSR */ + AV_i_tr = (NV_Ith_S(y, 20) - NV_Ith_S(y, 19)) / AC_tau_tr; + + // #/* Ca_leak_current_by_the_NSR */ + AV_i_up_leak = AC_I_up_max * NV_Ith_S(y, 20) / AC_Ca_up_max; + + // #/* Ca_release_current_from_JSR */ + AV_i_rel = AC_K_rel * pow(NV_Ith_S(y, 13), 2.0) * NV_Ith_S(y, 14) * NV_Ith_S(y, 15) * (NV_Ith_S(y, 19) - NV_Ith_S(y, 17)); + + // #/* intracellular_ion_concentrations */ + fy_ptrs[19][j] = (AV_i_tr - AV_i_rel) * pow(1.0 + AC_CSQN_max * AC_Km_CSQN / pow(NV_Ith_S(y, 19) + AC_Km_CSQN, 2.0), (-1.0)); + + // #/* Ca_uptake_current_by_the_NSR */ + AV_i_up = AC_I_up_max / (1.0 + AC_K_up / NV_Ith_S(y, 17)); + fy_ptrs[20][j] = AV_i_up - (AV_i_up_leak + AV_i_tr * AC_V_rel / AC_V_up); + + // #/* sarcolemmal_calcium_pump_current */ + AV_i_CaP = AC_Cm * AC_i_CaP_max * NV_Ith_S(y, 17) / (0.0005 + NV_Ith_S(y, 17)); + + // #/* sodium_potassium_pump */ + AV_f_NaK = pow(1.0 + 0.1245 * exp((-0.1) * AC_F * NV_Ith_S(y, 0) / (AC_R * AC_T)) + 0.0365 * AC_sigma * exp((-AC_F) * NV_Ith_S(y, 0) / (AC_R * AC_T)), (-1.0)); + AV_i_NaK = AC_Cm * AC_i_NaK_max * AV_f_NaK * 1.0 / (1.0 + pow(AC_Km_Na_i / NV_Ith_S(y, 16), 1.5)) * AC_K_o / (AC_K_o + AC_Km_K_o); + + // #/* time_independent_potassium_current */ + AV_E_K = AC_R * AC_T / AC_F * log(AC_K_o / NV_Ith_S(y, 18)); + AV_i_K1 = AC_Cm * AC_g_K1 * (NV_Ith_S(y, 0) - AV_E_K) / (1.0 + exp(0.07 * (NV_Ith_S(y, 0) + 80.0))); + + // #/* transient_outward_K_current */ + AV_i_to = AC_Cm * AC_g_to * pow(NV_Ith_S(y, 4), 3.0) * NV_Ith_S(y, 5) * (NV_Ith_S(y, 0) - AV_E_K); + + // #/* ultrarapid_delayed_rectifier_K_current */ + AV_g_Kur = 0.005 + 0.05 / (1.0 + exp((NV_Ith_S(y, 0) - 15.0) / (-13.0))); + AV_i_Kur = AC_Cm * AV_g_Kur * pow(NV_Ith_S(y, 6), 3.0) * NV_Ith_S(y, 7) * (NV_Ith_S(y, 0) - AV_E_K); + + // #/* *remaining* */ + AV_i_Ca_L = AC_Cm * AC_g_Ca_L * NV_Ith_S(y, 10) * NV_Ith_S(y, 11) * NV_Ith_S(y, 12) * (NV_Ith_S(y, 0) - 65.0); + AV_i_NaCa = AC_Cm * AC_I_NaCa_max * (exp(AC_Na_Ca_exchanger_current_gamma * AC_F * NV_Ith_S(y, 0) / (AC_R * AC_T)) * pow(NV_Ith_S(y, 16), 3.0) * AC_Ca_o - exp((AC_Na_Ca_exchanger_current_gamma - 1.0) * AC_F * NV_Ith_S(y, 0) / (AC_R * AC_T)) * pow(AC_Na_o, 3.0) * NV_Ith_S(y, 17)) / ((pow(AC_K_mNa, 3.0) + pow(AC_Na_o, 3.0)) * (AC_K_mCa + AC_Ca_o) * (1.0 + AC_K_sat * exp((AC_Na_Ca_exchanger_current_gamma - 1.0) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)))); + AV_E_Ca = AC_R * AC_T / (2.0 * AC_F) * log(AC_Ca_o / NV_Ith_S(y, 17)); + AV_i_B_K = AC_Cm * AC_g_B_K * (NV_Ith_S(y, 0) - AV_E_K); + AV_E_Na = AC_R * AC_T / AC_F * log(AC_Na_o / NV_Ith_S(y, 16)); + AV_i_Kr = AC_Cm * AC_g_Kr * NV_Ith_S(y, 8) * (NV_Ith_S(y, 0) - AV_E_K) / (1.0 + exp((NV_Ith_S(y, 0) + 15.0) / 22.4)); + AV_i_Ks = AC_Cm * AC_g_Ks * pow(NV_Ith_S(y, 9), 2.0) * (NV_Ith_S(y, 0) - AV_E_K); + AV_Fn = 1000.0 * (1e-15 * AC_V_rel * AV_i_rel - 1e-15 / (2.0 * AC_F) * (0.5 * AV_i_Ca_L - 0.2 * AV_i_NaCa)); + AV_i_B_Ca = AC_Cm * AC_g_B_Ca * (NV_Ith_S(y, 0) - AV_E_Ca); + AV_i_B_Na = AC_Cm * AC_g_B_Na * (NV_Ith_S(y, 0) - AV_E_Na); + AV_i_Na = AC_Cm * AC_g_Na * pow(NV_Ith_S(y, 1), 3.0) * NV_Ith_S(y, 2) * NV_Ith_S(y, 3) * (NV_Ith_S(y, 0) - AV_E_Na); + fy_ptrs[18][j] = (2.0 * AV_i_NaK - (AV_i_K1 + AV_i_to + AV_i_Kur + AV_i_Kr + AV_i_Ks + AV_i_B_K)) / (AC_V_i * AC_F); + AV_u_infinity = pow(1.0 + exp((-(AV_Fn - 3.41749999999999983e-13)) / 1.367e-15), (-1.0)); + AV_tau_v = 1.91 + 2.09 * pow(1.0 + exp((-(AV_Fn - 3.41749999999999983e-13)) / 1.367e-15), (-1.0)); + AV_v_infinity = 1.0 - pow(1.0 + exp((-(AV_Fn - 6.835e-14)) / 1.367e-15), (-1.0)); + fy_ptrs[16][j] = ((-3.0) * AV_i_NaK - (3.0 * AV_i_NaCa + AV_i_B_Na + AV_i_Na)) / (AC_V_i * AC_F); + + fy_ptrs[0][j] = scale * (-(AV_i_Na + AV_i_K1 + AV_i_to + AV_i_Kur + AV_i_Kr + AV_i_Ks + AV_i_B_Na + AV_i_B_Ca + AV_i_NaK + AV_i_CaP + AV_i_NaCa + AV_i_Ca_L)) / AC_Cm; + fy_ptrs[13][j] = (AV_u_infinity - NV_Ith_S(y, 13)) / AC_tau_u; + fy_ptrs[14][j] = (AV_v_infinity - NV_Ith_S(y, 14)) / AV_tau_v; + + AV_B1 = (2.0 * AV_i_NaCa - (AV_i_CaP + AV_i_Ca_L + AV_i_B_Ca)) / (2.0 * AC_V_i * AC_F) + (AC_V_up * (AV_i_up_leak - AV_i_up) + AV_i_rel * AC_V_rel) / AC_V_i; + AV_B2 = 1.0 + AC_TRPN_max * AC_Km_TRPN / pow(NV_Ith_S(y, 17) + AC_Km_TRPN, 2.0) + AC_CMDN_max * AC_Km_CMDN / pow(NV_Ith_S(y, 17) + AC_Km_CMDN, 2.0); + fy_ptrs[17][j] = AV_B1 / AV_B2; + } +} + +void Courtemanche1998::f_expl(py::array_t &y_list, py::array_t &fy_list) +{ + double *y_ptrs[size]; + double *fy_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(fy_list, fy_ptrs, N, n_dofs); + + double y[size]; + // for nonlinear + double AV_f_Ca_infinity, AV_i_tr, AV_i_up_leak, AV_i_rel, AV_i_up, AV_i_CaP, AV_f_NaK, AV_i_NaK, AV_E_K, AV_i_K1, AV_i_to, AV_g_Kur, AV_i_Kur; + double AV_i_Ca_L, AV_i_NaCa, AV_E_Ca, AV_i_B_K, AV_E_Na, AV_i_Kr, AV_i_Ks, AV_Fn, AV_i_B_Ca, AV_i_B_Na, AV_i_Na, AV_u_infinity, AV_tau_v, AV_v_infinity, AV_B1, AV_B2; + // Remember to scale the first variable!!! + for (unsigned j = 0; j < n_dofs; j++) + { + for (unsigned i = 0; i < size; i++) + y[i] = y_ptrs[i][j]; + + // #/* L_type_Ca_channel_f_Ca_gate */ + AV_f_Ca_infinity = pow(1.0 + NV_Ith_S(y, 17) / 0.00035, (-1.0)); + fy_ptrs[12][j] = (AV_f_Ca_infinity - NV_Ith_S(y, 12)) / AC_tau_f_Ca; + + // #/* transfer_current_from_NSR_to_JSR */ + AV_i_tr = (NV_Ith_S(y, 20) - NV_Ith_S(y, 19)) / AC_tau_tr; + + // #/* Ca_leak_current_by_the_NSR */ + AV_i_up_leak = AC_I_up_max * NV_Ith_S(y, 20) / AC_Ca_up_max; + + // #/* Ca_release_current_from_JSR */ + AV_i_rel = AC_K_rel * pow(NV_Ith_S(y, 13), 2.0) * NV_Ith_S(y, 14) * NV_Ith_S(y, 15) * (NV_Ith_S(y, 19) - NV_Ith_S(y, 17)); + + // #/* intracellular_ion_concentrations */ + fy_ptrs[19][j] = (AV_i_tr - AV_i_rel) * pow(1.0 + AC_CSQN_max * AC_Km_CSQN / pow(NV_Ith_S(y, 19) + AC_Km_CSQN, 2.0), (-1.0)); + + // #/* Ca_uptake_current_by_the_NSR */ + AV_i_up = AC_I_up_max / (1.0 + AC_K_up / NV_Ith_S(y, 17)); + fy_ptrs[20][j] = AV_i_up - (AV_i_up_leak + AV_i_tr * AC_V_rel / AC_V_up); + + // #/* sarcolemmal_calcium_pump_current */ + AV_i_CaP = AC_Cm * AC_i_CaP_max * NV_Ith_S(y, 17) / (0.0005 + NV_Ith_S(y, 17)); + + // #/* sodium_potassium_pump */ + AV_f_NaK = pow(1.0 + 0.1245 * exp((-0.1) * AC_F * NV_Ith_S(y, 0) / (AC_R * AC_T)) + 0.0365 * AC_sigma * exp((-AC_F) * NV_Ith_S(y, 0) / (AC_R * AC_T)), (-1.0)); + AV_i_NaK = AC_Cm * AC_i_NaK_max * AV_f_NaK * 1.0 / (1.0 + pow(AC_Km_Na_i / NV_Ith_S(y, 16), 1.5)) * AC_K_o / (AC_K_o + AC_Km_K_o); + + // #/* time_independent_potassium_current */ + AV_E_K = AC_R * AC_T / AC_F * log(AC_K_o / NV_Ith_S(y, 18)); + AV_i_K1 = AC_Cm * AC_g_K1 * (NV_Ith_S(y, 0) - AV_E_K) / (1.0 + exp(0.07 * (NV_Ith_S(y, 0) + 80.0))); + + // #/* transient_outward_K_current */ + AV_i_to = AC_Cm * AC_g_to * pow(NV_Ith_S(y, 4), 3.0) * NV_Ith_S(y, 5) * (NV_Ith_S(y, 0) - AV_E_K); + + // #/* ultrarapid_delayed_rectifier_K_current */ + AV_g_Kur = 0.005 + 0.05 / (1.0 + exp((NV_Ith_S(y, 0) - 15.0) / (-13.0))); + AV_i_Kur = AC_Cm * AV_g_Kur * pow(NV_Ith_S(y, 6), 3.0) * NV_Ith_S(y, 7) * (NV_Ith_S(y, 0) - AV_E_K); + + // #/* *remaining* */ + AV_i_Ca_L = AC_Cm * AC_g_Ca_L * NV_Ith_S(y, 10) * NV_Ith_S(y, 11) * NV_Ith_S(y, 12) * (NV_Ith_S(y, 0) - 65.0); + AV_i_NaCa = AC_Cm * AC_I_NaCa_max * (exp(AC_Na_Ca_exchanger_current_gamma * AC_F * NV_Ith_S(y, 0) / (AC_R * AC_T)) * pow(NV_Ith_S(y, 16), 3.0) * AC_Ca_o - exp((AC_Na_Ca_exchanger_current_gamma - 1.0) * AC_F * NV_Ith_S(y, 0) / (AC_R * AC_T)) * pow(AC_Na_o, 3.0) * NV_Ith_S(y, 17)) / ((pow(AC_K_mNa, 3.0) + pow(AC_Na_o, 3.0)) * (AC_K_mCa + AC_Ca_o) * (1.0 + AC_K_sat * exp((AC_Na_Ca_exchanger_current_gamma - 1.0) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)))); + AV_E_Ca = AC_R * AC_T / (2.0 * AC_F) * log(AC_Ca_o / NV_Ith_S(y, 17)); + AV_i_B_K = AC_Cm * AC_g_B_K * (NV_Ith_S(y, 0) - AV_E_K); + AV_E_Na = AC_R * AC_T / AC_F * log(AC_Na_o / NV_Ith_S(y, 16)); + AV_i_Kr = AC_Cm * AC_g_Kr * NV_Ith_S(y, 8) * (NV_Ith_S(y, 0) - AV_E_K) / (1.0 + exp((NV_Ith_S(y, 0) + 15.0) / 22.4)); + AV_i_Ks = AC_Cm * AC_g_Ks * pow(NV_Ith_S(y, 9), 2.0) * (NV_Ith_S(y, 0) - AV_E_K); + AV_Fn = 1000.0 * (1e-15 * AC_V_rel * AV_i_rel - 1e-15 / (2.0 * AC_F) * (0.5 * AV_i_Ca_L - 0.2 * AV_i_NaCa)); + AV_i_B_Ca = AC_Cm * AC_g_B_Ca * (NV_Ith_S(y, 0) - AV_E_Ca); + AV_i_B_Na = AC_Cm * AC_g_B_Na * (NV_Ith_S(y, 0) - AV_E_Na); + AV_i_Na = AC_Cm * AC_g_Na * pow(NV_Ith_S(y, 1), 3.0) * NV_Ith_S(y, 2) * NV_Ith_S(y, 3) * (NV_Ith_S(y, 0) - AV_E_Na); + fy_ptrs[18][j] = (2.0 * AV_i_NaK - (AV_i_K1 + AV_i_to + AV_i_Kur + AV_i_Kr + AV_i_Ks + AV_i_B_K)) / (AC_V_i * AC_F); + AV_u_infinity = pow(1.0 + exp((-(AV_Fn - 3.41749999999999983e-13)) / 1.367e-15), (-1.0)); + AV_tau_v = 1.91 + 2.09 * pow(1.0 + exp((-(AV_Fn - 3.41749999999999983e-13)) / 1.367e-15), (-1.0)); + AV_v_infinity = 1.0 - pow(1.0 + exp((-(AV_Fn - 6.835e-14)) / 1.367e-15), (-1.0)); + fy_ptrs[16][j] = ((-3.0) * AV_i_NaK - (3.0 * AV_i_NaCa + AV_i_B_Na + AV_i_Na)) / (AC_V_i * AC_F); + + fy_ptrs[0][j] = scale * (-(AV_i_Na + AV_i_K1 + AV_i_to + AV_i_Kur + AV_i_Kr + AV_i_Ks + AV_i_B_Na + AV_i_B_Ca + AV_i_NaK + AV_i_CaP + AV_i_NaCa + AV_i_Ca_L)) / AC_Cm; + fy_ptrs[13][j] = (AV_u_infinity - NV_Ith_S(y, 13)) / AC_tau_u; + fy_ptrs[14][j] = (AV_v_infinity - NV_Ith_S(y, 14)) / AV_tau_v; + + AV_B1 = (2.0 * AV_i_NaCa - (AV_i_CaP + AV_i_Ca_L + AV_i_B_Ca)) / (2.0 * AC_V_i * AC_F) + (AC_V_up * (AV_i_up_leak - AV_i_up) + AV_i_rel * AC_V_rel) / AC_V_i; + AV_B2 = 1.0 + AC_TRPN_max * AC_Km_TRPN / pow(NV_Ith_S(y, 17) + AC_Km_TRPN, 2.0) + AC_CMDN_max * AC_Km_CMDN / pow(NV_Ith_S(y, 17) + AC_Km_CMDN, 2.0); + fy_ptrs[17][j] = AV_B1 / AV_B2; + } +} + +void Courtemanche1998::lmbda_yinf_exp(py::array_t &y_list, py::array_t &lmbda_list, py::array_t &yinf_list) +{ + double *y_ptrs[size]; + double *lmbda_ptrs[size]; + double *yinf_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(lmbda_list, lmbda_ptrs, N, n_dofs); + get_raw_data(yinf_list, yinf_ptrs, N, n_dofs); + + double y[size]; + double AV_tau_w, AV_tau_d, AV_tau_f, AV_alpha_h, AV_beta_h, AV_tau_h, AV_alpha_j, AV_beta_j, AV_tau_j; + double AV_alpha_m, AV_beta_m, AV_tau_m, AV_alpha_xr, AV_beta_xr, AV_tau_xr, AV_alpha_xs, AV_beta_xs; + double AV_tau_xs, AV_alpha_oa, AV_beta_oa, AV_tau_oa; + double AV_alpha_oi, AV_beta_oi, AV_tau_oi, AV_alpha_ua, AV_beta_ua, AV_tau_ua, AV_alpha_ui, AV_beta_ui, AV_tau_ui; + // Remember to scale the first variable!!! + for (unsigned j = 0; j < n_dofs; j++) + { + for (unsigned i = 0; i < size; i++) + y[i] = y_ptrs[i][j]; + + // # Linear (in the gating variables) terms + + // #/* Ca_release_current_from_JSR_w_gate */ + + AV_tau_w = abs(NV_Ith_S(y, 0) - 7.9) < 1e-10 ? 6.0 * 0.2 / 1.3 : 6.0 * (1.0 - exp((-(NV_Ith_S(y, 0) - 7.9)) / 5.0)) / ((1.0 + 0.3 * exp((-(NV_Ith_S(y, 0) - 7.9)) / 5.0)) * 1.0 * (NV_Ith_S(y, 0) - 7.9)); + lmbda_ptrs[15][j] = -1. / AV_tau_w; + yinf_ptrs[15][j] = 1.0 - pow(1.0 + exp((-(NV_Ith_S(y, 0) - 40.0)) / 17.0), (-1.0)); + + // #/* L_type_Ca_channel_d_gate */ + yinf_ptrs[10][j] = pow(1.0 + exp((NV_Ith_S(y, 0) + 10.0) / (-8.0)), (-1.0)); + AV_tau_d = abs(NV_Ith_S(y, 0) + 10.0) < 1e-10 ? 4.579 / (1.0 + exp((NV_Ith_S(y, 0) + 10.0) / (-6.24))) : (1.0 - exp((NV_Ith_S(y, 0) + 10.0) / (-6.24))) / (0.035 * (NV_Ith_S(y, 0) + 10.0) * (1.0 + exp((NV_Ith_S(y, 0) + 10.0) / (-6.24)))); + lmbda_ptrs[10][j] = -1. / AV_tau_d; + + // #/* L_type_Ca_channel_f_gate */ + yinf_ptrs[11][j] = exp((-(NV_Ith_S(y, 0) + 28.0)) / 6.9) / (1.0 + exp((-(NV_Ith_S(y, 0) + 28.0)) / 6.9)); + AV_tau_f = 9.0 * pow(0.0197 * exp((-pow(0.0337, 2.0)) * pow(NV_Ith_S(y, 0) + 10.0, 2.0)) + 0.02, (-1.0)); + lmbda_ptrs[11][j] = -1. / AV_tau_f; + + // #/* fast_sodium_current_h_gate */ + AV_alpha_h = NV_Ith_S(y, 0) < (-40.0) ? 0.135 * exp((NV_Ith_S(y, 0) + 80.0) / (-6.8)) : 0.0; + AV_beta_h = NV_Ith_S(y, 0) < (-40.0) ? 3.56 * exp(0.079 * NV_Ith_S(y, 0)) + 310000.0 * exp(0.35 * NV_Ith_S(y, 0)) : 1.0 / (0.13 * (1.0 + exp((NV_Ith_S(y, 0) + 10.66) / (-11.1)))); + yinf_ptrs[2][j] = AV_alpha_h / (AV_alpha_h + AV_beta_h); + AV_tau_h = 1.0 / (AV_alpha_h + AV_beta_h); + lmbda_ptrs[2][j] = -1. / AV_tau_h; + + // #/* fast_sodium_current_j_gate */ + AV_alpha_j = NV_Ith_S(y, 0) < (-40.0) ? ((-127140.0) * exp(0.2444 * NV_Ith_S(y, 0)) - 3.474e-05 * exp((-0.04391) * NV_Ith_S(y, 0))) * (NV_Ith_S(y, 0) + 37.78) / (1.0 + exp(0.311 * (NV_Ith_S(y, 0) + 79.23))) : 0.0; + AV_beta_j = NV_Ith_S(y, 0) < (-40.0) ? 0.1212 * exp((-0.01052) * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1378) * (NV_Ith_S(y, 0) + 40.14))) : 0.3 * exp((-2.535e-07) * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1) * (NV_Ith_S(y, 0) + 32.0))); + yinf_ptrs[3][j] = AV_alpha_j / (AV_alpha_j + AV_beta_j); + AV_tau_j = 1.0 / (AV_alpha_j + AV_beta_j); + lmbda_ptrs[3][j] = -1. / AV_tau_j; + + // #/* fast_sodium_current_m_gate */ + AV_alpha_m = NV_Ith_S(y, 0) == (-47.13) ? 3.2 : 0.32 * (NV_Ith_S(y, 0) + 47.13) / (1.0 - exp((-0.1) * (NV_Ith_S(y, 0) + 47.13))); + AV_beta_m = 0.08 * exp((-NV_Ith_S(y, 0)) / 11.0); + yinf_ptrs[1][j] = AV_alpha_m / (AV_alpha_m + AV_beta_m); + AV_tau_m = 1.0 / (AV_alpha_m + AV_beta_m); + lmbda_ptrs[1][j] = -1. / AV_tau_m; + + // #/* rapid_delayed_rectifier_K_current_xr_gate */ + AV_alpha_xr = abs(NV_Ith_S(y, 0) + 14.1) < 1e-10 ? 0.0015 : 0.0003 * (NV_Ith_S(y, 0) + 14.1) / (1.0 - exp((NV_Ith_S(y, 0) + 14.1) / (-5.0))); + AV_beta_xr = abs(NV_Ith_S(y, 0) - 3.3328) < 1e-10 ? 3.78361180000000004e-04 : 7.38980000000000030e-05 * (NV_Ith_S(y, 0) - 3.3328) / (exp((NV_Ith_S(y, 0) - 3.3328) / 5.1237) - 1.0); + yinf_ptrs[8][j] = pow(1.0 + exp((NV_Ith_S(y, 0) + 14.1) / (-6.5)), (-1.0)); + AV_tau_xr = pow(AV_alpha_xr + AV_beta_xr, (-1.0)); + lmbda_ptrs[8][j] = -1. / AV_tau_xr; + + // #/* slow_delayed_rectifier_K_current_xs_gate */ + AV_alpha_xs = abs(NV_Ith_S(y, 0) - 19.9) < 1e-10 ? 0.00068 : 4e-05 * (NV_Ith_S(y, 0) - 19.9) / (1.0 - exp((NV_Ith_S(y, 0) - 19.9) / (-17.0))); + AV_beta_xs = abs(NV_Ith_S(y, 0) - 19.9) < 1e-10 ? 0.000315 : 3.5e-05 * (NV_Ith_S(y, 0) - 19.9) / (exp((NV_Ith_S(y, 0) - 19.9) / 9.0) - 1.0); + yinf_ptrs[9][j] = pow(1.0 + exp((NV_Ith_S(y, 0) - 19.9) / (-12.7)), (-0.5)); + AV_tau_xs = 0.5 * pow(AV_alpha_xs + AV_beta_xs, (-1.0)); + lmbda_ptrs[9][j] = -1. / AV_tau_xs; + + // #/* transient_outward_K_current_oa_gate */ + AV_alpha_oa = 0.65 * pow(exp((NV_Ith_S(y, 0) - (-10.0)) / (-8.5)) + exp((NV_Ith_S(y, 0) - (-10.0) - 40.0) / (-59.0)), (-1.0)); + AV_beta_oa = 0.65 * pow(2.5 + exp((NV_Ith_S(y, 0) - (-10.0) + 72.0) / 17.0), (-1.0)); + yinf_ptrs[4][j] = pow(1.0 + exp((NV_Ith_S(y, 0) - (-10.0) + 10.47) / (-17.54)), (-1.0)); + AV_tau_oa = pow(AV_alpha_oa + AV_beta_oa, (-1.0)) / AC_K_Q10; + lmbda_ptrs[4][j] = -1. / AV_tau_oa; + + // #/* transient_outward_K_current_oi_gate */ + AV_alpha_oi = pow(18.53 + 1.0 * exp((NV_Ith_S(y, 0) - (-10.0) + 103.7) / 10.95), (-1.0)); + AV_beta_oi = pow(35.56 + 1.0 * exp((NV_Ith_S(y, 0) - (-10.0) - 8.74) / (-7.44)), (-1.0)); + yinf_ptrs[5][j] = pow(1.0 + exp((NV_Ith_S(y, 0) - (-10.0) + 33.1) / 5.3), (-1.0)); + AV_tau_oi = pow(AV_alpha_oi + AV_beta_oi, (-1.0)) / AC_K_Q10; + lmbda_ptrs[5][j] = -1. / AV_tau_oi; + + // #/* ultrarapid_delayed_rectifier_K_current_ua_gate */ + AV_alpha_ua = 0.65 * pow(exp((NV_Ith_S(y, 0) - (-10.0)) / (-8.5)) + exp((NV_Ith_S(y, 0) - (-10.0) - 40.0) / (-59.0)), (-1.0)); + AV_beta_ua = 0.65 * pow(2.5 + exp((NV_Ith_S(y, 0) - (-10.0) + 72.0) / 17.0), (-1.0)); + yinf_ptrs[6][j] = pow(1.0 + exp((NV_Ith_S(y, 0) - (-10.0) + 20.3) / (-9.6)), (-1.0)); + AV_tau_ua = pow(AV_alpha_ua + AV_beta_ua, (-1.0)) / AC_K_Q10; + lmbda_ptrs[6][j] = -1. / AV_tau_ua; + + // #/* ultrarapid_delayed_rectifier_K_current_ui_gate */ + AV_alpha_ui = pow(21.0 + 1.0 * exp((NV_Ith_S(y, 0) - (-10.0) - 195.0) / (-28.0)), (-1.0)); + AV_beta_ui = 1.0 / exp((NV_Ith_S(y, 0) - (-10.0) - 168.0) / (-16.0)); + yinf_ptrs[7][j] = pow(1.0 + exp((NV_Ith_S(y, 0) - (-10.0) - 109.45) / 27.48), (-1.0)); + AV_tau_ui = pow(AV_alpha_ui + AV_beta_ui, (-1.0)) / AC_K_Q10; + lmbda_ptrs[7][j] = -1. / AV_tau_ui; + } +} + +void Courtemanche1998::lmbda_exp(py::array_t &y_list, py::array_t &lmbda_list) +{ + double *y_ptrs[size]; + double *lmbda_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(lmbda_list, lmbda_ptrs, N, n_dofs); + + double y[size]; + double AV_tau_w, AV_tau_d, AV_tau_f, AV_alpha_h, AV_beta_h, AV_tau_h, AV_alpha_j, AV_beta_j, AV_tau_j; + double AV_alpha_m, AV_beta_m, AV_tau_m, AV_alpha_xr, AV_beta_xr, AV_tau_xr, AV_alpha_xs, AV_beta_xs; + double AV_tau_xs, AV_alpha_oa, AV_beta_oa, AV_tau_oa; + double AV_alpha_oi, AV_beta_oi, AV_tau_oi, AV_alpha_ua, AV_beta_ua, AV_tau_ua, AV_alpha_ui, AV_beta_ui, AV_tau_ui; + // Remember to scale the first variable!!! + for (unsigned j = 0; j < n_dofs; j++) + { + for (unsigned i = 0; i < size; i++) + y[i] = y_ptrs[i][j]; + + // # Linear (in the gating variables) terms + + // #/* Ca_release_current_from_JSR_w_gate */ + + AV_tau_w = abs(NV_Ith_S(y, 0) - 7.9) < 1e-10 ? 6.0 * 0.2 / 1.3 : 6.0 * (1.0 - exp((-(NV_Ith_S(y, 0) - 7.9)) / 5.0)) / ((1.0 + 0.3 * exp((-(NV_Ith_S(y, 0) - 7.9)) / 5.0)) * 1.0 * (NV_Ith_S(y, 0) - 7.9)); + lmbda_ptrs[15][j] = -1. / AV_tau_w; + + // #/* L_type_Ca_channel_d_gate */ + AV_tau_d = abs(NV_Ith_S(y, 0) + 10.0) < 1e-10 ? 4.579 / (1.0 + exp((NV_Ith_S(y, 0) + 10.0) / (-6.24))) : (1.0 - exp((NV_Ith_S(y, 0) + 10.0) / (-6.24))) / (0.035 * (NV_Ith_S(y, 0) + 10.0) * (1.0 + exp((NV_Ith_S(y, 0) + 10.0) / (-6.24)))); + lmbda_ptrs[10][j] = -1. / AV_tau_d; + + // #/* L_type_Ca_channel_f_gate */ + AV_tau_f = 9.0 * pow(0.0197 * exp((-pow(0.0337, 2.0)) * pow(NV_Ith_S(y, 0) + 10.0, 2.0)) + 0.02, (-1.0)); + lmbda_ptrs[11][j] = -1. / AV_tau_f; + + // #/* fast_sodium_current_h_gate */ + AV_alpha_h = NV_Ith_S(y, 0) < (-40.0) ? 0.135 * exp((NV_Ith_S(y, 0) + 80.0) / (-6.8)) : 0.0; + AV_beta_h = NV_Ith_S(y, 0) < (-40.0) ? 3.56 * exp(0.079 * NV_Ith_S(y, 0)) + 310000.0 * exp(0.35 * NV_Ith_S(y, 0)) : 1.0 / (0.13 * (1.0 + exp((NV_Ith_S(y, 0) + 10.66) / (-11.1)))); + AV_tau_h = 1.0 / (AV_alpha_h + AV_beta_h); + lmbda_ptrs[2][j] = -1. / AV_tau_h; + + // #/* fast_sodium_current_j_gate */ + AV_alpha_j = NV_Ith_S(y, 0) < (-40.0) ? ((-127140.0) * exp(0.2444 * NV_Ith_S(y, 0)) - 3.474e-05 * exp((-0.04391) * NV_Ith_S(y, 0))) * (NV_Ith_S(y, 0) + 37.78) / (1.0 + exp(0.311 * (NV_Ith_S(y, 0) + 79.23))) : 0.0; + AV_beta_j = NV_Ith_S(y, 0) < (-40.0) ? 0.1212 * exp((-0.01052) * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1378) * (NV_Ith_S(y, 0) + 40.14))) : 0.3 * exp((-2.535e-07) * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1) * (NV_Ith_S(y, 0) + 32.0))); + AV_tau_j = 1.0 / (AV_alpha_j + AV_beta_j); + lmbda_ptrs[3][j] = -1. / AV_tau_j; + + // #/* fast_sodium_current_m_gate */ + AV_alpha_m = NV_Ith_S(y, 0) == (-47.13) ? 3.2 : 0.32 * (NV_Ith_S(y, 0) + 47.13) / (1.0 - exp((-0.1) * (NV_Ith_S(y, 0) + 47.13))); + AV_beta_m = 0.08 * exp((-NV_Ith_S(y, 0)) / 11.0); + AV_tau_m = 1.0 / (AV_alpha_m + AV_beta_m); + lmbda_ptrs[1][j] = -1. / AV_tau_m; + + // #/* rapid_delayed_rectifier_K_current_xr_gate */ + AV_alpha_xr = abs(NV_Ith_S(y, 0) + 14.1) < 1e-10 ? 0.0015 : 0.0003 * (NV_Ith_S(y, 0) + 14.1) / (1.0 - exp((NV_Ith_S(y, 0) + 14.1) / (-5.0))); + AV_beta_xr = abs(NV_Ith_S(y, 0) - 3.3328) < 1e-10 ? 3.78361180000000004e-04 : 7.38980000000000030e-05 * (NV_Ith_S(y, 0) - 3.3328) / (exp((NV_Ith_S(y, 0) - 3.3328) / 5.1237) - 1.0); + AV_tau_xr = pow(AV_alpha_xr + AV_beta_xr, (-1.0)); + lmbda_ptrs[8][j] = -1. / AV_tau_xr; + + // #/* slow_delayed_rectifier_K_current_xs_gate */ + AV_alpha_xs = abs(NV_Ith_S(y, 0) - 19.9) < 1e-10 ? 0.00068 : 4e-05 * (NV_Ith_S(y, 0) - 19.9) / (1.0 - exp((NV_Ith_S(y, 0) - 19.9) / (-17.0))); + AV_beta_xs = abs(NV_Ith_S(y, 0) - 19.9) < 1e-10 ? 0.000315 : 3.5e-05 * (NV_Ith_S(y, 0) - 19.9) / (exp((NV_Ith_S(y, 0) - 19.9) / 9.0) - 1.0); + AV_tau_xs = 0.5 * pow(AV_alpha_xs + AV_beta_xs, (-1.0)); + lmbda_ptrs[9][j] = -1. / AV_tau_xs; + + // #/* transient_outward_K_current_oa_gate */ + AV_alpha_oa = 0.65 * pow(exp((NV_Ith_S(y, 0) - (-10.0)) / (-8.5)) + exp((NV_Ith_S(y, 0) - (-10.0) - 40.0) / (-59.0)), (-1.0)); + AV_beta_oa = 0.65 * pow(2.5 + exp((NV_Ith_S(y, 0) - (-10.0) + 72.0) / 17.0), (-1.0)); + AV_tau_oa = pow(AV_alpha_oa + AV_beta_oa, (-1.0)) / AC_K_Q10; + lmbda_ptrs[4][j] = -1. / AV_tau_oa; + + // #/* transient_outward_K_current_oi_gate */ + AV_alpha_oi = pow(18.53 + 1.0 * exp((NV_Ith_S(y, 0) - (-10.0) + 103.7) / 10.95), (-1.0)); + AV_beta_oi = pow(35.56 + 1.0 * exp((NV_Ith_S(y, 0) - (-10.0) - 8.74) / (-7.44)), (-1.0)); + AV_tau_oi = pow(AV_alpha_oi + AV_beta_oi, (-1.0)) / AC_K_Q10; + lmbda_ptrs[5][j] = -1. / AV_tau_oi; + + // #/* ultrarapid_delayed_rectifier_K_current_ua_gate */ + AV_alpha_ua = 0.65 * pow(exp((NV_Ith_S(y, 0) - (-10.0)) / (-8.5)) + exp((NV_Ith_S(y, 0) - (-10.0) - 40.0) / (-59.0)), (-1.0)); + AV_beta_ua = 0.65 * pow(2.5 + exp((NV_Ith_S(y, 0) - (-10.0) + 72.0) / 17.0), (-1.0)); + AV_tau_ua = pow(AV_alpha_ua + AV_beta_ua, (-1.0)) / AC_K_Q10; + lmbda_ptrs[6][j] = -1. / AV_tau_ua; + + // #/* ultrarapid_delayed_rectifier_K_current_ui_gate */ + AV_alpha_ui = pow(21.0 + 1.0 * exp((NV_Ith_S(y, 0) - (-10.0) - 195.0) / (-28.0)), (-1.0)); + AV_beta_ui = 1.0 / exp((NV_Ith_S(y, 0) - (-10.0) - 168.0) / (-16.0)); + AV_tau_ui = pow(AV_alpha_ui + AV_beta_ui, (-1.0)) / AC_K_Q10; + lmbda_ptrs[7][j] = -1. / AV_tau_ui; + } +} + +double Courtemanche1998::rho_f_expl() +{ + return 7.5; +} + +#endif \ No newline at end of file diff --git a/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/hodgkinhuxley.h b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/hodgkinhuxley.h new file mode 100644 index 0000000000..0b178308ef --- /dev/null +++ b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/hodgkinhuxley.h @@ -0,0 +1,177 @@ +#include +#include + +#include +#include +#include + +#include "ionicmodel.h" + +#ifndef HODGKINHUXLEY +#define HODGKINHUXLEY + +class HodgkinHuxley : public IonicModel +{ +public: + HodgkinHuxley(const double scale_); + ~HodgkinHuxley(){}; + void f(py::array_t &y, py::array_t &fy); + void f_expl(py::array_t &y, py::array_t &fy); + void lmbda_exp(py::array_t &y_list, py::array_t &lmbda_list); + void lmbda_yinf_exp(py::array_t &y_list, py::array_t &lmbda_list, py::array_t &yinf_list); + py::list initial_values(); + double rho_f_expl(); + +private: + double AC_g_L, AC_Cm, AC_E_R, AC_E_K, AC_g_K, AC_E_Na, AC_g_Na, AC_E_L; +}; + +HodgkinHuxley::HodgkinHuxley(const double scale_) + : IonicModel(scale_) +{ + size = 4; + + // Set values of constants + AC_g_L = 0.3; + AC_Cm = 1.0; + AC_E_R = -75.0; + AC_E_K = AC_E_R - 12.0; + AC_g_K = 36.0; + AC_E_Na = AC_E_R + 115.0; + AC_g_Na = 120.0; + AC_E_L = AC_E_R + 10.613; + + assign(f_expl_args, {0, 1, 2, 3}); + assign(f_exp_args, {0, 1, 2, 3}); + assign(f_expl_indeces, {0}); + assign(f_exp_indeces, {1, 2, 3}); +} + +py::list HodgkinHuxley::initial_values() +{ + py::list y0(size); + y0[0] = -75.0; + y0[1] = 0.05; + y0[2] = 0.595; + y0[3] = 0.317; + + return y0; +} + +void HodgkinHuxley::f(py::array_t &y_list, py::array_t &fy_list) +{ + double *y_ptrs[size]; + double *fy_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(fy_list, fy_ptrs, N, n_dofs); + + double AV_alpha_n, AV_beta_n, AV_alpha_h, AV_beta_h, AV_alpha_m, AV_beta_m, AV_i_K, AV_i_Na, AV_i_L; + // Remember to scale the first variable!!! + for (unsigned j = 0; j < n_dofs; j++) + { + double y[4] = {y_ptrs[0][j], y_ptrs[1][j], y_ptrs[2][j], y_ptrs[3][j]}; + + AV_alpha_n = (-0.01) * (y[0] + 65.0) / (exp((-(y[0] + 65.0)) / 10.0) - 1.0); + AV_beta_n = 0.125 * exp((y[0] + 75.0) / 80.0); + fy_ptrs[3][j] = AV_alpha_n * (1.0 - y[3]) - AV_beta_n * y[3]; + + AV_alpha_h = 0.07 * exp((-(y[0] + 75.0)) / 20.0); + AV_beta_h = 1.0 / (exp((-(y[0] + 45.0)) / 10.0) + 1.0); + fy_ptrs[2][j] = AV_alpha_h * (1.0 - y[2]) - AV_beta_h * y[2]; + + AV_alpha_m = (-0.1) * (y[0] + 50.0) / (exp((-(y[0] + 50.0)) / 10.0) - 1.0); + AV_beta_m = 4.0 * exp((-(y[0] + 75.0)) / 18.0); + fy_ptrs[1][j] = AV_alpha_m * (1.0 - y[1]) - AV_beta_m * y[1]; + + AV_i_K = AC_g_K * pow(y[3], 4.0) * (y[0] - AC_E_K); + AV_i_Na = AC_g_Na * pow(y[1], 3.0) * y[2] * (y[0] - AC_E_Na); + AV_i_L = AC_g_L * (y[0] - AC_E_L); + fy_ptrs[0][j] = -scale * (AV_i_Na + AV_i_K + AV_i_L); + } +} + +void HodgkinHuxley::f_expl(py::array_t &y_list, py::array_t &fy_list) +{ + double *y_ptrs[size]; + double *fy_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(fy_list, fy_ptrs, N, n_dofs); + + double AV_i_K, AV_i_Na, AV_i_L; + // Remember to scale the first variable!!! + for (unsigned j = 0; j < n_dofs; j++) + { + AV_i_K = AC_g_K * pow(y_ptrs[3][j], 4.0) * (y_ptrs[0][j] - AC_E_K); + AV_i_Na = AC_g_Na * pow(y_ptrs[1][j], 3.0) * y_ptrs[2][j] * (y_ptrs[0][j] - AC_E_Na); + AV_i_L = AC_g_L * (y_ptrs[0][j] - AC_E_L); + fy_ptrs[0][j] = -scale * (AV_i_Na + AV_i_K + AV_i_L); + } +} + +void HodgkinHuxley::lmbda_exp(py::array_t &y_list, py::array_t &lmbda_list) +{ + double *y_ptrs[size]; + double *lmbda_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(lmbda_list, lmbda_ptrs, N, n_dofs); + + double AV_alpha_n, AV_beta_n, AV_alpha_h, AV_beta_h, AV_alpha_m, AV_beta_m; + for (unsigned j = 0; j < n_dofs; j++) + { + AV_alpha_n = (-0.01) * (y_ptrs[0][j] + 65.0) / (exp((-(y_ptrs[0][j] + 65.0)) / 10.0) - 1.0); + AV_beta_n = 0.125 * exp((y_ptrs[0][j] + 75.0) / 80.0); + lmbda_ptrs[3][j] = -(AV_alpha_n + AV_beta_n); + + AV_alpha_h = 0.07 * exp((-(y_ptrs[0][j] + 75.0)) / 20.0); + AV_beta_h = 1.0 / (exp((-(y_ptrs[0][j] + 45.0)) / 10.0) + 1.0); + lmbda_ptrs[2][j] = -(AV_alpha_h + AV_beta_h); + + AV_alpha_m = (-0.1) * (y_ptrs[0][j] + 50.0) / (exp((-(y_ptrs[0][j] + 50.0)) / 10.0) - 1.0); + AV_beta_m = 4.0 * exp((-(y_ptrs[0][j] + 75.0)) / 18.0); + lmbda_ptrs[1][j] = -(AV_alpha_m + AV_beta_m); + } +} + +void HodgkinHuxley::lmbda_yinf_exp(py::array_t &y_list, py::array_t &lmbda_list, py::array_t &yinf_list) +{ + double *y_ptrs[size]; + double *lmbda_ptrs[size]; + double *yinf_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(lmbda_list, lmbda_ptrs, N, n_dofs); + get_raw_data(yinf_list, yinf_ptrs, N, n_dofs); + + double AV_alpha_n, AV_beta_n, AV_alpha_h, AV_beta_h, AV_alpha_m, AV_beta_m; + for (unsigned j = 0; j < n_dofs; j++) + { + AV_alpha_n = (-0.01) * (y_ptrs[0][j] + 65.0) / (exp((-(y_ptrs[0][j] + 65.0)) / 10.0) - 1.0); + AV_beta_n = 0.125 * exp((y_ptrs[0][j] + 75.0) / 80.0); + lmbda_ptrs[3][j] = -(AV_alpha_n + AV_beta_n); + yinf_ptrs[3][j] = -AV_alpha_n / lmbda_ptrs[3][j]; + + AV_alpha_h = 0.07 * exp((-(y_ptrs[0][j] + 75.0)) / 20.0); + AV_beta_h = 1.0 / (exp((-(y_ptrs[0][j] + 45.0)) / 10.0) + 1.0); + lmbda_ptrs[2][j] = -(AV_alpha_h + AV_beta_h); + yinf_ptrs[2][j] = -AV_alpha_h / lmbda_ptrs[2][j]; + + AV_alpha_m = (-0.1) * (y_ptrs[0][j] + 50.0) / (exp((-(y_ptrs[0][j] + 50.0)) / 10.0) - 1.0); + AV_beta_m = 4.0 * exp((-(y_ptrs[0][j] + 75.0)) / 18.0); + lmbda_ptrs[1][j] = -(AV_alpha_m + AV_beta_m); + yinf_ptrs[1][j] = -AV_alpha_m / lmbda_ptrs[1][j]; + } +} + +double HodgkinHuxley::rho_f_expl() +{ + return 40.; +} + +#endif \ No newline at end of file diff --git a/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/ionicmodel.h b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/ionicmodel.h new file mode 100644 index 0000000000..0cdee8648a --- /dev/null +++ b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/ionicmodel.h @@ -0,0 +1,61 @@ +#ifndef IONICMODEL +#define IONICMODEL + +// The ionic model codes are partially generated by myokit, which uses this NV_Ith_S function to access the elements of the array in their data type. +// In our implementation we use the [] operator to access the elements of the array so we define this function as a wrapper and avoid changing the original code. +double inline NV_Ith_S(double *y, unsigned i) +{ + return y[i]; +} + +double inline phi_f_from_lmbda_yinf(double y, double lmbda, double yinf, double dt) +{ + return ((exp(dt * lmbda) - 1.) / dt) * (y - yinf); +} + +double inline phi_f_from_tau_yinf(double y, double tau, double yinf, double dt) +{ + return ((exp(-dt / tau) - 1.) / dt) * (y - yinf); +} + +void get_raw_data(py::array_t &x, double **array_ptrs, size_t &N, size_t &n_dofs) +{ + auto r = x.unchecked<2>(); + N = r.shape(0); + n_dofs = r.shape(1); + for (py::ssize_t i = 0; i < r.shape(0); i++) + array_ptrs[i] = (double *)r.data(i, 0); +}; + +void assign(py::list l, std::initializer_list a) +{ + for (auto a_el : a) + l.append(a_el); +}; + +class IonicModel +{ +public: + IonicModel(const double scale_); + + py::list f_expl_args; + py::list f_exp_args; + py::list f_expl_indeces; + py::list f_exp_indeces; + py::list get_f_expl_args() { return f_expl_args; }; + py::list get_f_exp_args() { return f_exp_args; }; + py::list get_f_expl_indeces() { return f_expl_indeces; }; + py::list get_f_exp_indeces() { return f_exp_indeces; }; + size_t get_size() { return size; }; + +protected: + double scale; + size_t size; +}; + +IonicModel::IonicModel(const double scale_) +{ + scale = scale_; +} + +#endif \ No newline at end of file diff --git a/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/tentusscher.h b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/tentusscher.h new file mode 100644 index 0000000000..2b6ea5eace --- /dev/null +++ b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/tentusscher.h @@ -0,0 +1,542 @@ +#include +#include + +#include +#include +#include + +#include "ionicmodel.h" + +#ifndef TENTUSSCHER +#define TENTUSSCHER + +class TenTusscher2006_epi : public IonicModel +{ +public: + TenTusscher2006_epi(const double scale_); + ~TenTusscher2006_epi(){}; + void f(py::array_t &y, py::array_t &fy); + void f_expl(py::array_t &y, py::array_t &fy); + void lmbda_exp(py::array_t &y_list, py::array_t &lmbda_list); + void lmbda_yinf_exp(py::array_t &y_list, py::array_t &lmbda_list, py::array_t &yinf_list); + py::list initial_values(); + double rho_f_expl(); + +private: + double AC_Cm, AC_K_pCa, AC_g_pCa, AC_g_CaL, AC_g_bca, AC_Buf_c, AC_Buf_sr, AC_Buf_ss, AC_Ca_o, AC_EC, AC_K_buf_c, AC_K_buf_sr, AC_K_buf_ss, AC_K_up, AC_V_leak, AC_V_rel, AC_V_sr, AC_V_ss, AC_V_xfer, AC_Vmax_up, AC_k1_prime, AC_k2_prime, AC_k3, AC_k4, AC_max_sr, AC_min_sr, AC_g_Na, AC_g_K1, AC_F, AC_R, AC_T, AC_V_c, AC_stim_amplitude, AC_K_o, AC_g_pK, AC_g_Kr, AC_P_kna, AC_g_Ks, AC_g_bna, AC_K_NaCa, AC_K_sat, AC_Km_Ca, AC_Km_Nai, AC_alpha, AC_sodium_calcium_exchanger_current_gamma, AC_Na_o, AC_K_mNa, AC_K_mk, AC_P_NaK, AC_g_to; +}; + +TenTusscher2006_epi::TenTusscher2006_epi(const double scale_) + : IonicModel(scale_) +{ + size = 19; + + AC_Cm = 1.0; // 185.0; + AC_K_pCa = 0.0005; + AC_g_pCa = 0.1238; + AC_g_CaL = 0.0398; + AC_g_bca = 0.000592; + AC_Buf_c = 0.2; + AC_Buf_sr = 10.0; + AC_Buf_ss = 0.4; + AC_Ca_o = 2.0; + AC_EC = 1.5; + AC_K_buf_c = 0.001; + AC_K_buf_sr = 0.3; + AC_K_buf_ss = 0.00025; + AC_K_up = 0.00025; + AC_V_leak = 0.00036; + AC_V_rel = 0.102; + AC_V_sr = 1094.0; + AC_V_ss = 54.68; + AC_V_xfer = 0.0038; + AC_Vmax_up = 0.006375; + AC_k1_prime = 0.15; + AC_k2_prime = 0.045; + AC_k3 = 0.06; + AC_k4 = 0.005; + AC_max_sr = 2.5; + AC_min_sr = 1.0; + AC_g_Na = 14.838; + AC_g_K1 = 5.405; + AC_F = 96.485; + AC_R = 8.314; + AC_T = 310.0; + AC_V_c = 16404.0; + AC_stim_amplitude = (-52.0); + AC_K_o = 5.4; + AC_g_pK = 0.0146; + AC_g_Kr = 0.153; + AC_P_kna = 0.03; + AC_g_Ks = 0.392; + AC_g_bna = 0.00029; + AC_K_NaCa = 1000.0; + AC_K_sat = 0.1; + AC_Km_Ca = 1.38; + AC_Km_Nai = 87.5; + AC_alpha = 2.5; + AC_sodium_calcium_exchanger_current_gamma = 0.35; + AC_Na_o = 140.0; + AC_K_mNa = 40.0; + AC_K_mk = 1.0; + AC_P_NaK = 2.724; + AC_g_to = 0.294; + + assign(f_expl_args, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}); + assign(f_exp_args, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15}); + assign(f_expl_indeces, {0, 13, 14, 15, 16, 17, 18}); + assign(f_exp_indeces, {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}); +} + +py::list TenTusscher2006_epi::initial_values() +{ + py::list y0(size); + y0[0] = -85.23; + y0[1] = 0.00621; + y0[2] = 0.4712; + y0[3] = 0.0095; + y0[4] = 0.00172; + y0[5] = 0.7444; + y0[6] = 0.7045; + y0[7] = 3.373e-05; + y0[8] = 0.7888; + y0[9] = 0.9755; + y0[10] = 0.9953; + y0[11] = 0.999998; + y0[12] = 2.42e-08; + y0[13] = 0.000126; + y0[14] = 3.64; + y0[15] = 0.00036; + y0[16] = 0.9073; + y0[17] = 8.604; + y0[18] = 136.89; + + return y0; +} + +void TenTusscher2006_epi::f(py::array_t &y_list, py::array_t &fy_list) +{ + double *y_ptrs[size]; + double *fy_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(fy_list, fy_ptrs, N, n_dofs); + + double y[size]; + // # needed for linear in gating variables + double AV_alpha_d, AV_beta_d, AV_d_inf, AV_gamma_d, AV_tau_d, AV_f2_inf, AV_tau_f2, AV_fCass_inf, AV_tau_fCass, AV_f_inf, AV_tau_f; + double AV_alpha_h, AV_beta_h, AV_h_inf, AV_tau_h, AV_alpha_j, AV_beta_j, AV_j_inf, AV_tau_j, AV_alpha_m, AV_beta_m, AV_m_inf, AV_tau_m; + double AV_alpha_xr1, AV_beta_xr1, AV_xr1_inf, AV_tau_xr1, AV_alpha_xr2, AV_beta_xr2, AV_xr2_inf, AV_tau_xr2, AV_alpha_xs, AV_beta_xs, AV_xs_inf, AV_tau_xs; + double AV_r_inf, AV_tau_r, AV_s_inf, AV_tau_s; + // # needed for nonlinear in gating variables + double AV_f_JCa_i_free, AV_f_JCa_sr_free, AV_f_JCa_ss_free, AV_i_leak, AV_i_up, AV_i_xfer, AV_kcasr, AV_k1, AV_k2, AV_O, AV_i_rel, AV_ddt_Ca_sr_total; + double AV_E_Ca, AV_E_K, AV_i_NaK, AV_i_to, AV_i_p_Ca, AV_i_CaL, AV_i_b_Ca, AV_alpha_K1, AV_beta_K1, AV_i_p_K, AV_i_Kr, AV_E_Ks, AV_E_Na, AV_i_NaCa; + double AV_ddt_Ca_i_total, AV_ddt_Ca_ss_total, AV_i_Na, AV_i_K1, AV_xK1_inf, AV_i_Ks, AV_i_b_Na; + + for (unsigned j = 0; j < n_dofs; j++) + { + for (unsigned i = 0; i < size; i++) + y[i] = y_ptrs[i][j]; + + // # Linear in gating variables + + // # /* L_type_Ca_current_d_gate */ + AV_alpha_d = 1.4 / (1.0 + exp(((-35.0) - NV_Ith_S(y, 0)) / 13.0)) + 0.25; + AV_beta_d = 1.4 / (1.0 + exp((NV_Ith_S(y, 0) + 5.0) / 5.0)); + AV_d_inf = 1.0 / (1.0 + exp(((-8.0) - NV_Ith_S(y, 0)) / 7.5)); + AV_gamma_d = 1.0 / (1.0 + exp((50.0 - NV_Ith_S(y, 0)) / 20.0)); + AV_tau_d = 1.0 * AV_alpha_d * AV_beta_d + AV_gamma_d; + fy_ptrs[7][j] = (AV_d_inf - NV_Ith_S(y, 7)) / AV_tau_d; + + // # /* L_type_Ca_current_f2_gate */ + AV_f2_inf = 0.67 / (1.0 + exp((NV_Ith_S(y, 0) + 35.0) / 7.0)) + 0.33; + AV_tau_f2 = 562.0 * exp((-pow(NV_Ith_S(y, 0) + 27.0, 2.0)) / 240.0) + 31.0 / (1.0 + exp((25.0 - NV_Ith_S(y, 0)) / 10.0)) + 80.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 10.0)); + fy_ptrs[9][j] = (AV_f2_inf - NV_Ith_S(y, 9)) / AV_tau_f2; + + // # /* L_type_Ca_current_fCass_gate */ + AV_fCass_inf = 0.6 / (1.0 + pow(NV_Ith_S(y, 15) / 0.05, 2.0)) + 0.4; + AV_tau_fCass = 80.0 / (1.0 + pow(NV_Ith_S(y, 15) / 0.05, 2.0)) + 2.0; + fy_ptrs[10][j] = (AV_fCass_inf - NV_Ith_S(y, 10)) / AV_tau_fCass; + + // # /* L_type_Ca_current_f_gate */ + AV_f_inf = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) + 20.0) / 7.0)); + AV_tau_f = 1102.5 * exp((-pow(NV_Ith_S(y, 0) + 27.0, 2.0)) / 225.0) + 200.0 / (1.0 + exp((13.0 - NV_Ith_S(y, 0)) / 10.0)) + 180.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 10.0)) + 20.0; + fy_ptrs[8][j] = (AV_f_inf - NV_Ith_S(y, 8)) / AV_tau_f; + + // # /* fast_sodium_current_h_gate */ + AV_alpha_h = NV_Ith_S(y, 0) < (-40.0) ? 0.057 * exp((-(NV_Ith_S(y, 0) + 80.0)) / 6.8) : 0.0; + AV_beta_h = NV_Ith_S(y, 0) < (-40.0) ? 2.7 * exp(0.079 * NV_Ith_S(y, 0)) + 310000.0 * exp(0.3485 * NV_Ith_S(y, 0)) : 0.77 / (0.13 * (1.0 + exp((NV_Ith_S(y, 0) + 10.66) / (-11.1)))); + AV_h_inf = 1.0 / pow(1.0 + exp((NV_Ith_S(y, 0) + 71.55) / 7.43), 2.0); + AV_tau_h = 1.0 / (AV_alpha_h + AV_beta_h); + fy_ptrs[5][j] = (AV_h_inf - NV_Ith_S(y, 5)) / AV_tau_h; + + // # /* fast_sodium_current_j_gate */ + AV_alpha_j = NV_Ith_S(y, 0) < (-40.0) ? ((-25428.0) * exp(0.2444 * NV_Ith_S(y, 0)) - 6.948e-06 * exp((-0.04391) * NV_Ith_S(y, 0))) * (NV_Ith_S(y, 0) + 37.78) / 1.0 / (1.0 + exp(0.311 * (NV_Ith_S(y, 0) + 79.23))) : 0.0; + AV_beta_j = NV_Ith_S(y, 0) < (-40.0) ? 0.02424 * exp((-0.01052) * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1378) * (NV_Ith_S(y, 0) + 40.14))) : 0.6 * exp(0.057 * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1) * (NV_Ith_S(y, 0) + 32.0))); + AV_j_inf = 1.0 / pow(1.0 + exp((NV_Ith_S(y, 0) + 71.55) / 7.43), 2.0); + AV_tau_j = 1.0 / (AV_alpha_j + AV_beta_j); + fy_ptrs[6][j] = (AV_j_inf - NV_Ith_S(y, 6)) / AV_tau_j; + + // # /* fast_sodium_current_m_gate */ + AV_alpha_m = 1.0 / (1.0 + exp(((-60.0) - NV_Ith_S(y, 0)) / 5.0)); + AV_beta_m = 0.1 / (1.0 + exp((NV_Ith_S(y, 0) + 35.0) / 5.0)) + 0.1 / (1.0 + exp((NV_Ith_S(y, 0) - 50.0) / 200.0)); + AV_m_inf = 1.0 / pow(1.0 + exp(((-56.86) - NV_Ith_S(y, 0)) / 9.03), 2.0); + AV_tau_m = 1.0 * AV_alpha_m * AV_beta_m; + fy_ptrs[4][j] = (AV_m_inf - NV_Ith_S(y, 4)) / AV_tau_m; + + // # /* rapid_time_dependent_potassium_current_Xr1_gate */ + AV_alpha_xr1 = 450.0 / (1.0 + exp(((-45.0) - NV_Ith_S(y, 0)) / 10.0)); + AV_beta_xr1 = 6.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 11.5)); + AV_xr1_inf = 1.0 / (1.0 + exp(((-26.0) - NV_Ith_S(y, 0)) / 7.0)); + AV_tau_xr1 = 1.0 * AV_alpha_xr1 * AV_beta_xr1; + fy_ptrs[1][j] = (AV_xr1_inf - NV_Ith_S(y, 1)) / AV_tau_xr1; + + // # /* rapid_time_dependent_potassium_current_Xr2_gate */ + AV_alpha_xr2 = 3.0 / (1.0 + exp(((-60.0) - NV_Ith_S(y, 0)) / 20.0)); + AV_beta_xr2 = 1.12 / (1.0 + exp((NV_Ith_S(y, 0) - 60.0) / 20.0)); + AV_xr2_inf = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) + 88.0) / 24.0)); + AV_tau_xr2 = 1.0 * AV_alpha_xr2 * AV_beta_xr2; + fy_ptrs[2][j] = (AV_xr2_inf - NV_Ith_S(y, 2)) / AV_tau_xr2; + + // # /* slow_time_dependent_potassium_current_Xs_gate */ + AV_alpha_xs = 1400.0 / sqrt(1.0 + exp((5.0 - NV_Ith_S(y, 0)) / 6.0)); + AV_beta_xs = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) - 35.0) / 15.0)); + AV_xs_inf = 1.0 / (1.0 + exp(((-5.0) - NV_Ith_S(y, 0)) / 14.0)); + AV_tau_xs = 1.0 * AV_alpha_xs * AV_beta_xs + 80.0; + fy_ptrs[3][j] = (AV_xs_inf - NV_Ith_S(y, 3)) / AV_tau_xs; + + // # /* transient_outward_current_r_gate */ + AV_r_inf = 1.0 / (1.0 + exp((20.0 - NV_Ith_S(y, 0)) / 6.0)); + AV_tau_r = 9.5 * exp((-pow(NV_Ith_S(y, 0) + 40.0, 2.0)) / 1800.0) + 0.8; + fy_ptrs[12][j] = (AV_r_inf - NV_Ith_S(y, 12)) / AV_tau_r; + + // # /* transient_outward_current_s_gate */ + AV_s_inf = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) + 20.0) / 5.0)); + AV_tau_s = 85.0 * exp((-pow(NV_Ith_S(y, 0) + 45.0, 2.0)) / 320.0) + 5.0 / (1.0 + exp((NV_Ith_S(y, 0) - 20.0) / 5.0)) + 3.0; + fy_ptrs[11][j] = (AV_s_inf - NV_Ith_S(y, 11)) / AV_tau_s; + + // # Non linear in gating variables + + // # /* calcium_dynamics */ + AV_f_JCa_i_free = 1.0 / (1.0 + AC_Buf_c * AC_K_buf_c / pow(NV_Ith_S(y, 13) + AC_K_buf_c, 2.0)); + AV_f_JCa_sr_free = 1.0 / (1.0 + AC_Buf_sr * AC_K_buf_sr / pow(NV_Ith_S(y, 14) + AC_K_buf_sr, 2.0)); + AV_f_JCa_ss_free = 1.0 / (1.0 + AC_Buf_ss * AC_K_buf_ss / pow(NV_Ith_S(y, 15) + AC_K_buf_ss, 2.0)); + AV_i_leak = AC_V_leak * (NV_Ith_S(y, 14) - NV_Ith_S(y, 13)); + AV_i_up = AC_Vmax_up / (1.0 + pow(AC_K_up, 2.0) / pow(NV_Ith_S(y, 13), 2.0)); + AV_i_xfer = AC_V_xfer * (NV_Ith_S(y, 15) - NV_Ith_S(y, 13)); + AV_kcasr = AC_max_sr - (AC_max_sr - AC_min_sr) / (1.0 + pow(AC_EC / NV_Ith_S(y, 14), 2.0)); + AV_k1 = AC_k1_prime / AV_kcasr; + AV_k2 = AC_k2_prime * AV_kcasr; + AV_O = AV_k1 * pow(NV_Ith_S(y, 15), 2.0) * NV_Ith_S(y, 16) / (AC_k3 + AV_k1 * pow(NV_Ith_S(y, 15), 2.0)); + fy_ptrs[16][j] = (-AV_k2) * NV_Ith_S(y, 15) * NV_Ith_S(y, 16) + AC_k4 * (1.0 - NV_Ith_S(y, 16)); + AV_i_rel = AC_V_rel * AV_O * (NV_Ith_S(y, 14) - NV_Ith_S(y, 15)); + AV_ddt_Ca_sr_total = AV_i_up - (AV_i_rel + AV_i_leak); + fy_ptrs[14][j] = AV_ddt_Ca_sr_total * AV_f_JCa_sr_free; + + // # /* reversal_potentials */ + AV_E_Ca = 0.5 * AC_R * AC_T / AC_F * log(AC_Ca_o / NV_Ith_S(y, 13)); + AV_E_K = AC_R * AC_T / AC_F * log(AC_K_o / NV_Ith_S(y, 18)); + + // # /* sodium_potassium_pump_current */ + AV_i_NaK = AC_P_NaK * AC_K_o / (AC_K_o + AC_K_mk) * NV_Ith_S(y, 17) / (NV_Ith_S(y, 17) + AC_K_mNa) / (1.0 + 0.1245 * exp((-0.1) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)) + 0.0353 * exp((-NV_Ith_S(y, 0)) * AC_F / (AC_R * AC_T))); + + // # /* transient_outward_current */ + AV_i_to = AC_g_to * NV_Ith_S(y, 12) * NV_Ith_S(y, 11) * (NV_Ith_S(y, 0) - AV_E_K); + + // # /* calcium_pump_current */ + AV_i_p_Ca = AC_g_pCa * NV_Ith_S(y, 13) / (NV_Ith_S(y, 13) + AC_K_pCa); + + // # /* *remaining* */ + AV_i_CaL = AC_g_CaL * NV_Ith_S(y, 7) * NV_Ith_S(y, 8) * NV_Ith_S(y, 9) * NV_Ith_S(y, 10) * 4.0 * (NV_Ith_S(y, 0) - 15.0) * pow(AC_F, 2.0) / (AC_R * AC_T) * (0.25 * NV_Ith_S(y, 15) * exp(2.0 * (NV_Ith_S(y, 0) - 15.0) * AC_F / (AC_R * AC_T)) - AC_Ca_o) / (exp(2.0 * (NV_Ith_S(y, 0) - 15.0) * AC_F / (AC_R * AC_T)) - 1.0); + AV_i_b_Ca = AC_g_bca * (NV_Ith_S(y, 0) - AV_E_Ca); + AV_alpha_K1 = 0.1 / (1.0 + exp(0.06 * (NV_Ith_S(y, 0) - AV_E_K - 200.0))); + AV_beta_K1 = (3.0 * exp(0.0002 * (NV_Ith_S(y, 0) - AV_E_K + 100.0)) + exp(0.1 * (NV_Ith_S(y, 0) - AV_E_K - 10.0))) / (1.0 + exp((-0.5) * (NV_Ith_S(y, 0) - AV_E_K))); + AV_i_p_K = AC_g_pK * (NV_Ith_S(y, 0) - AV_E_K) / (1.0 + exp((25.0 - NV_Ith_S(y, 0)) / 5.98)); + AV_i_Kr = AC_g_Kr * sqrt(AC_K_o / 5.4) * NV_Ith_S(y, 1) * NV_Ith_S(y, 2) * (NV_Ith_S(y, 0) - AV_E_K); + AV_E_Ks = AC_R * AC_T / AC_F * log((AC_K_o + AC_P_kna * AC_Na_o) / (NV_Ith_S(y, 18) + AC_P_kna * NV_Ith_S(y, 17))); + AV_E_Na = AC_R * AC_T / AC_F * log(AC_Na_o / NV_Ith_S(y, 17)); + AV_i_NaCa = AC_K_NaCa * (exp(AC_sodium_calcium_exchanger_current_gamma * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)) * pow(NV_Ith_S(y, 17), 3.0) * AC_Ca_o - exp((AC_sodium_calcium_exchanger_current_gamma - 1.0) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)) * pow(AC_Na_o, 3.0) * NV_Ith_S(y, 13) * AC_alpha) / ((pow(AC_Km_Nai, 3.0) + pow(AC_Na_o, 3.0)) * (AC_Km_Ca + AC_Ca_o) * (1.0 + AC_K_sat * exp((AC_sodium_calcium_exchanger_current_gamma - 1.0) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)))); + AV_ddt_Ca_i_total = (-(AV_i_b_Ca + AV_i_p_Ca - 2.0 * AV_i_NaCa)) * AC_Cm / (2.0 * AC_V_c * AC_F) + (AV_i_leak - AV_i_up) * AC_V_sr / AC_V_c + AV_i_xfer; + AV_ddt_Ca_ss_total = (-AV_i_CaL) * AC_Cm / (2.0 * AC_V_ss * AC_F) + AV_i_rel * AC_V_sr / AC_V_ss - AV_i_xfer * AC_V_c / AC_V_ss; + AV_i_Na = AC_g_Na * pow(NV_Ith_S(y, 4), 3.0) * NV_Ith_S(y, 5) * NV_Ith_S(y, 6) * (NV_Ith_S(y, 0) - AV_E_Na); + AV_xK1_inf = AV_alpha_K1 / (AV_alpha_K1 + AV_beta_K1); + AV_i_Ks = AC_g_Ks * pow(NV_Ith_S(y, 3), 2.0) * (NV_Ith_S(y, 0) - AV_E_Ks); + AV_i_b_Na = AC_g_bna * (NV_Ith_S(y, 0) - AV_E_Na); + fy_ptrs[13][j] = AV_ddt_Ca_i_total * AV_f_JCa_i_free; + fy_ptrs[15][j] = AV_ddt_Ca_ss_total * AV_f_JCa_ss_free; + AV_i_K1 = AC_g_K1 * AV_xK1_inf * sqrt(AC_K_o / 5.4) * (NV_Ith_S(y, 0) - AV_E_K); + fy_ptrs[17][j] = (-(AV_i_Na + AV_i_b_Na + 3.0 * AV_i_NaK + 3.0 * AV_i_NaCa)) / (AC_V_c * AC_F) * AC_Cm; + fy_ptrs[0][j] = scale * (-(AV_i_K1 + AV_i_to + AV_i_Kr + AV_i_Ks + AV_i_CaL + AV_i_NaK + AV_i_Na + AV_i_b_Na + AV_i_NaCa + AV_i_b_Ca + AV_i_p_K + AV_i_p_Ca)); + fy_ptrs[18][j] = (-(AV_i_K1 + AV_i_to + AV_i_Kr + AV_i_Ks + AV_i_p_K - 2.0 * AV_i_NaK)) / (AC_V_c * AC_F) * AC_Cm; + } +} + +void TenTusscher2006_epi::f_expl(py::array_t &y_list, py::array_t &fy_list) +{ + double *y_ptrs[size]; + double *fy_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(fy_list, fy_ptrs, N, n_dofs); + + double y[size]; + double AV_f_JCa_i_free, AV_f_JCa_sr_free, AV_f_JCa_ss_free, AV_i_leak, AV_i_up, AV_i_xfer, AV_kcasr, AV_k1, AV_k2, AV_O, AV_i_rel, AV_ddt_Ca_sr_total; + double AV_E_Ca, AV_E_K, AV_i_NaK, AV_i_to, AV_i_p_Ca, AV_i_CaL, AV_i_b_Ca, AV_alpha_K1, AV_beta_K1, AV_i_p_K, AV_i_Kr, AV_E_Ks, AV_E_Na, AV_i_NaCa; + double AV_ddt_Ca_i_total, AV_ddt_Ca_ss_total, AV_i_Na, AV_i_K1, AV_xK1_inf, AV_i_Ks, AV_i_b_Na; + + for (unsigned j = 0; j < n_dofs; j++) + { + + for (unsigned i = 0; i < size; i++) + y[i] = y_ptrs[i][j]; + + AV_f_JCa_i_free = 1.0 / (1.0 + AC_Buf_c * AC_K_buf_c / pow(NV_Ith_S(y, 13) + AC_K_buf_c, 2.0)); + AV_f_JCa_sr_free = 1.0 / (1.0 + AC_Buf_sr * AC_K_buf_sr / pow(NV_Ith_S(y, 14) + AC_K_buf_sr, 2.0)); + AV_f_JCa_ss_free = 1.0 / (1.0 + AC_Buf_ss * AC_K_buf_ss / pow(NV_Ith_S(y, 15) + AC_K_buf_ss, 2.0)); + AV_i_leak = AC_V_leak * (NV_Ith_S(y, 14) - NV_Ith_S(y, 13)); + AV_i_up = AC_Vmax_up / (1.0 + pow(AC_K_up, 2.0) / pow(NV_Ith_S(y, 13), 2.0)); + AV_i_xfer = AC_V_xfer * (NV_Ith_S(y, 15) - NV_Ith_S(y, 13)); + AV_kcasr = AC_max_sr - (AC_max_sr - AC_min_sr) / (1.0 + pow(AC_EC / NV_Ith_S(y, 14), 2.0)); + AV_k1 = AC_k1_prime / AV_kcasr; + AV_k2 = AC_k2_prime * AV_kcasr; + AV_O = AV_k1 * pow(NV_Ith_S(y, 15), 2.0) * NV_Ith_S(y, 16) / (AC_k3 + AV_k1 * pow(NV_Ith_S(y, 15), 2.0)); + fy_ptrs[16][j] = (-AV_k2) * NV_Ith_S(y, 15) * NV_Ith_S(y, 16) + AC_k4 * (1.0 - NV_Ith_S(y, 16)); + AV_i_rel = AC_V_rel * AV_O * (NV_Ith_S(y, 14) - NV_Ith_S(y, 15)); + AV_ddt_Ca_sr_total = AV_i_up - (AV_i_rel + AV_i_leak); + fy_ptrs[14][j] = AV_ddt_Ca_sr_total * AV_f_JCa_sr_free; + + // # /* reversal_potentials */ + AV_E_Ca = 0.5 * AC_R * AC_T / AC_F * log(AC_Ca_o / NV_Ith_S(y, 13)); + AV_E_K = AC_R * AC_T / AC_F * log(AC_K_o / NV_Ith_S(y, 18)); + + // # /* sodium_potassium_pump_current */ + AV_i_NaK = AC_P_NaK * AC_K_o / (AC_K_o + AC_K_mk) * NV_Ith_S(y, 17) / (NV_Ith_S(y, 17) + AC_K_mNa) / (1.0 + 0.1245 * exp((-0.1) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)) + 0.0353 * exp((-NV_Ith_S(y, 0)) * AC_F / (AC_R * AC_T))); + + // # /* transient_outward_current */ + AV_i_to = AC_g_to * NV_Ith_S(y, 12) * NV_Ith_S(y, 11) * (NV_Ith_S(y, 0) - AV_E_K); + + // # /* calcium_pump_current */ + AV_i_p_Ca = AC_g_pCa * NV_Ith_S(y, 13) / (NV_Ith_S(y, 13) + AC_K_pCa); + + // # /* *remaining* */ + AV_i_CaL = AC_g_CaL * NV_Ith_S(y, 7) * NV_Ith_S(y, 8) * NV_Ith_S(y, 9) * NV_Ith_S(y, 10) * 4.0 * (NV_Ith_S(y, 0) - 15.0) * pow(AC_F, 2.0) / (AC_R * AC_T) * (0.25 * NV_Ith_S(y, 15) * exp(2.0 * (NV_Ith_S(y, 0) - 15.0) * AC_F / (AC_R * AC_T)) - AC_Ca_o) / (exp(2.0 * (NV_Ith_S(y, 0) - 15.0) * AC_F / (AC_R * AC_T)) - 1.0); + AV_i_b_Ca = AC_g_bca * (NV_Ith_S(y, 0) - AV_E_Ca); + AV_alpha_K1 = 0.1 / (1.0 + exp(0.06 * (NV_Ith_S(y, 0) - AV_E_K - 200.0))); + AV_beta_K1 = (3.0 * exp(0.0002 * (NV_Ith_S(y, 0) - AV_E_K + 100.0)) + exp(0.1 * (NV_Ith_S(y, 0) - AV_E_K - 10.0))) / (1.0 + exp((-0.5) * (NV_Ith_S(y, 0) - AV_E_K))); + AV_i_p_K = AC_g_pK * (NV_Ith_S(y, 0) - AV_E_K) / (1.0 + exp((25.0 - NV_Ith_S(y, 0)) / 5.98)); + AV_i_Kr = AC_g_Kr * sqrt(AC_K_o / 5.4) * NV_Ith_S(y, 1) * NV_Ith_S(y, 2) * (NV_Ith_S(y, 0) - AV_E_K); + AV_E_Ks = AC_R * AC_T / AC_F * log((AC_K_o + AC_P_kna * AC_Na_o) / (NV_Ith_S(y, 18) + AC_P_kna * NV_Ith_S(y, 17))); + AV_E_Na = AC_R * AC_T / AC_F * log(AC_Na_o / NV_Ith_S(y, 17)); + AV_i_NaCa = AC_K_NaCa * (exp(AC_sodium_calcium_exchanger_current_gamma * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)) * pow(NV_Ith_S(y, 17), 3.0) * AC_Ca_o - exp((AC_sodium_calcium_exchanger_current_gamma - 1.0) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)) * pow(AC_Na_o, 3.0) * NV_Ith_S(y, 13) * AC_alpha) / ((pow(AC_Km_Nai, 3.0) + pow(AC_Na_o, 3.0)) * (AC_Km_Ca + AC_Ca_o) * (1.0 + AC_K_sat * exp((AC_sodium_calcium_exchanger_current_gamma - 1.0) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)))); + AV_ddt_Ca_i_total = (-(AV_i_b_Ca + AV_i_p_Ca - 2.0 * AV_i_NaCa)) * AC_Cm / (2.0 * AC_V_c * AC_F) + (AV_i_leak - AV_i_up) * AC_V_sr / AC_V_c + AV_i_xfer; + AV_ddt_Ca_ss_total = (-AV_i_CaL) * AC_Cm / (2.0 * AC_V_ss * AC_F) + AV_i_rel * AC_V_sr / AC_V_ss - AV_i_xfer * AC_V_c / AC_V_ss; + AV_i_Na = AC_g_Na * pow(NV_Ith_S(y, 4), 3.0) * NV_Ith_S(y, 5) * NV_Ith_S(y, 6) * (NV_Ith_S(y, 0) - AV_E_Na); + AV_xK1_inf = AV_alpha_K1 / (AV_alpha_K1 + AV_beta_K1); + AV_i_Ks = AC_g_Ks * pow(NV_Ith_S(y, 3), 2.0) * (NV_Ith_S(y, 0) - AV_E_Ks); + AV_i_b_Na = AC_g_bna * (NV_Ith_S(y, 0) - AV_E_Na); + fy_ptrs[13][j] = AV_ddt_Ca_i_total * AV_f_JCa_i_free; + fy_ptrs[15][j] = AV_ddt_Ca_ss_total * AV_f_JCa_ss_free; + AV_i_K1 = AC_g_K1 * AV_xK1_inf * sqrt(AC_K_o / 5.4) * (NV_Ith_S(y, 0) - AV_E_K); + fy_ptrs[17][j] = (-(AV_i_Na + AV_i_b_Na + 3.0 * AV_i_NaK + 3.0 * AV_i_NaCa)) / (AC_V_c * AC_F) * AC_Cm; + fy_ptrs[0][j] = scale * (-(AV_i_K1 + AV_i_to + AV_i_Kr + AV_i_Ks + AV_i_CaL + AV_i_NaK + AV_i_Na + AV_i_b_Na + AV_i_NaCa + AV_i_b_Ca + AV_i_p_K + AV_i_p_Ca)); + fy_ptrs[18][j] = (-(AV_i_K1 + AV_i_to + AV_i_Kr + AV_i_Ks + AV_i_p_K - 2.0 * AV_i_NaK)) / (AC_V_c * AC_F) * AC_Cm; + } +} + +void TenTusscher2006_epi::lmbda_yinf_exp(py::array_t &y_list, py::array_t &lmbda_list, py::array_t &yinf_list) +{ + double *y_ptrs[size]; + double *lmbda_ptrs[size]; + double *yinf_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(lmbda_list, lmbda_ptrs, N, n_dofs); + get_raw_data(yinf_list, yinf_ptrs, N, n_dofs); + + double y[size]; + double AV_alpha_d, AV_beta_d, AV_gamma_d, AV_tau_d, AV_tau_f2, AV_tau_fCass, AV_tau_f; + double AV_alpha_h, AV_beta_h, AV_tau_h, AV_alpha_j, AV_beta_j, AV_tau_j, AV_alpha_m, AV_beta_m, AV_tau_m; + double AV_alpha_xr1, AV_beta_xr1, AV_tau_xr1, AV_alpha_xr2, AV_beta_xr2, AV_tau_xr2, AV_alpha_xs, AV_beta_xs, AV_tau_xs; + double AV_tau_r, AV_tau_s; + + // Remember to scale the first variable!!! + for (unsigned j = 0; j < n_dofs; j++) + { + + for (unsigned i = 0; i < size; i++) + y[i] = y_ptrs[i][j]; + + // # /* L_type_Ca_current_d_gate */ + AV_alpha_d = 1.4 / (1.0 + exp(((-35.0) - NV_Ith_S(y, 0)) / 13.0)) + 0.25; + AV_beta_d = 1.4 / (1.0 + exp((NV_Ith_S(y, 0) + 5.0) / 5.0)); + yinf_ptrs[7][j] = 1.0 / (1.0 + exp(((-8.0) - NV_Ith_S(y, 0)) / 7.5)); + AV_gamma_d = 1.0 / (1.0 + exp((50.0 - NV_Ith_S(y, 0)) / 20.0)); + AV_tau_d = 1.0 * AV_alpha_d * AV_beta_d + AV_gamma_d; + lmbda_ptrs[7][j] = -1. / AV_tau_d; + + // # /* L_type_Ca_current_f2_gate */ + yinf_ptrs[9][j] = 0.67 / (1.0 + exp((NV_Ith_S(y, 0) + 35.0) / 7.0)) + 0.33; + AV_tau_f2 = 562.0 * exp((-pow(NV_Ith_S(y, 0) + 27.0, 2.0)) / 240.0) + 31.0 / (1.0 + exp((25.0 - NV_Ith_S(y, 0)) / 10.0)) + 80.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 10.0)); + lmbda_ptrs[9][j] = -1. / AV_tau_f2; + + // # /* L_type_Ca_current_fCass_gate */ + yinf_ptrs[10][j] = 0.6 / (1.0 + pow(NV_Ith_S(y, 15) / 0.05, 2.0)) + 0.4; + AV_tau_fCass = 80.0 / (1.0 + pow(NV_Ith_S(y, 15) / 0.05, 2.0)) + 2.0; + lmbda_ptrs[10][j] = -1. / AV_tau_fCass; + + // # /* L_type_Ca_current_f_gate */ + yinf_ptrs[8][j] = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) + 20.0) / 7.0)); + AV_tau_f = 1102.5 * exp((-pow(NV_Ith_S(y, 0) + 27.0, 2.0)) / 225.0) + 200.0 / (1.0 + exp((13.0 - NV_Ith_S(y, 0)) / 10.0)) + 180.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 10.0)) + 20.0; + lmbda_ptrs[8][j] = -1. / AV_tau_f; + + // # /* fast_sodium_current_h_gate */ + AV_alpha_h = NV_Ith_S(y, 0) < (-40.0) ? 0.057 * exp((-(NV_Ith_S(y, 0) + 80.0)) / 6.8) : 0.0; + AV_beta_h = NV_Ith_S(y, 0) < (-40.0) ? 2.7 * exp(0.079 * NV_Ith_S(y, 0)) + 310000.0 * exp(0.3485 * NV_Ith_S(y, 0)) : 0.77 / (0.13 * (1.0 + exp((NV_Ith_S(y, 0) + 10.66) / (-11.1)))); + yinf_ptrs[5][j] = 1.0 / pow(1.0 + exp((NV_Ith_S(y, 0) + 71.55) / 7.43), 2.0); + AV_tau_h = 1.0 / (AV_alpha_h + AV_beta_h); + lmbda_ptrs[5][j] = -1. / AV_tau_h; + + // # /* fast_sodium_current_j_gate */ + AV_alpha_j = NV_Ith_S(y, 0) < (-40.0) ? ((-25428.0) * exp(0.2444 * NV_Ith_S(y, 0)) - 6.948e-06 * exp((-0.04391) * NV_Ith_S(y, 0))) * (NV_Ith_S(y, 0) + 37.78) / 1.0 / (1.0 + exp(0.311 * (NV_Ith_S(y, 0) + 79.23))) : 0.0; + AV_beta_j = NV_Ith_S(y, 0) < (-40.0) ? 0.02424 * exp((-0.01052) * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1378) * (NV_Ith_S(y, 0) + 40.14))) : 0.6 * exp(0.057 * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1) * (NV_Ith_S(y, 0) + 32.0))); + yinf_ptrs[6][j] = 1.0 / pow(1.0 + exp((NV_Ith_S(y, 0) + 71.55) / 7.43), 2.0); + AV_tau_j = 1.0 / (AV_alpha_j + AV_beta_j); + lmbda_ptrs[6][j] = -1. / AV_tau_j; + + // # /* fast_sodium_current_m_gate */ + AV_alpha_m = 1.0 / (1.0 + exp(((-60.0) - NV_Ith_S(y, 0)) / 5.0)); + AV_beta_m = 0.1 / (1.0 + exp((NV_Ith_S(y, 0) + 35.0) / 5.0)) + 0.1 / (1.0 + exp((NV_Ith_S(y, 0) - 50.0) / 200.0)); + yinf_ptrs[4][j] = 1.0 / pow(1.0 + exp(((-56.86) - NV_Ith_S(y, 0)) / 9.03), 2.0); + AV_tau_m = 1.0 * AV_alpha_m * AV_beta_m; + lmbda_ptrs[4][j] = -1. / AV_tau_m; + + // # /* rapid_time_dependent_potassium_current_Xr1_gate */ + AV_alpha_xr1 = 450.0 / (1.0 + exp(((-45.0) - NV_Ith_S(y, 0)) / 10.0)); + AV_beta_xr1 = 6.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 11.5)); + yinf_ptrs[1][j] = 1.0 / (1.0 + exp(((-26.0) - NV_Ith_S(y, 0)) / 7.0)); + AV_tau_xr1 = 1.0 * AV_alpha_xr1 * AV_beta_xr1; + lmbda_ptrs[1][j] = -1. / AV_tau_xr1; + + // # /* rapid_time_dependent_potassium_current_Xr2_gate */ + AV_alpha_xr2 = 3.0 / (1.0 + exp(((-60.0) - NV_Ith_S(y, 0)) / 20.0)); + AV_beta_xr2 = 1.12 / (1.0 + exp((NV_Ith_S(y, 0) - 60.0) / 20.0)); + yinf_ptrs[2][j] = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) + 88.0) / 24.0)); + AV_tau_xr2 = 1.0 * AV_alpha_xr2 * AV_beta_xr2; + lmbda_ptrs[2][j] = -1. / AV_tau_xr2; + + // # /* slow_time_dependent_potassium_current_Xs_gate */ + AV_alpha_xs = 1400.0 / sqrt(1.0 + exp((5.0 - NV_Ith_S(y, 0)) / 6.0)); + AV_beta_xs = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) - 35.0) / 15.0)); + yinf_ptrs[3][j] = 1.0 / (1.0 + exp(((-5.0) - NV_Ith_S(y, 0)) / 14.0)); + AV_tau_xs = 1.0 * AV_alpha_xs * AV_beta_xs + 80.0; + lmbda_ptrs[3][j] = -1. / AV_tau_xs; + + // # /* transient_outward_current_r_gate */ + yinf_ptrs[12][j] = 1.0 / (1.0 + exp((20.0 - NV_Ith_S(y, 0)) / 6.0)); + AV_tau_r = 9.5 * exp((-pow(NV_Ith_S(y, 0) + 40.0, 2.0)) / 1800.0) + 0.8; + lmbda_ptrs[12][j] = -1. / AV_tau_r; + + // # /* transient_outward_current_s_gate */ + yinf_ptrs[11][j] = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) + 20.0) / 5.0)); + AV_tau_s = 85.0 * exp((-pow(NV_Ith_S(y, 0) + 45.0, 2.0)) / 320.0) + 5.0 / (1.0 + exp((NV_Ith_S(y, 0) - 20.0) / 5.0)) + 3.0; + lmbda_ptrs[11][j] = -1. / AV_tau_s; + } +} + +void TenTusscher2006_epi::lmbda_exp(py::array_t &y_list, py::array_t &lmbda_list) +{ + double *y_ptrs[size]; + double *lmbda_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(lmbda_list, lmbda_ptrs, N, n_dofs); + + double y[size]; + double AV_alpha_d, AV_beta_d, AV_gamma_d, AV_tau_d, AV_tau_f2, AV_tau_fCass, AV_tau_f; + double AV_alpha_h, AV_beta_h, AV_tau_h, AV_alpha_j, AV_beta_j, AV_tau_j, AV_alpha_m, AV_beta_m, AV_tau_m; + double AV_alpha_xr1, AV_beta_xr1, AV_tau_xr1, AV_alpha_xr2, AV_beta_xr2, AV_tau_xr2, AV_alpha_xs, AV_beta_xs, AV_tau_xs; + double AV_tau_r, AV_tau_s; + + // Remember to scale the first variable!!! + + for (unsigned j = 0; j < n_dofs; j++) + { + + for (unsigned i = 0; i < size; i++) + y[i] = y_ptrs[i][j]; + + // # /* L_type_Ca_current_d_gate */ + AV_alpha_d = 1.4 / (1.0 + exp(((-35.0) - NV_Ith_S(y, 0)) / 13.0)) + 0.25; + AV_beta_d = 1.4 / (1.0 + exp((NV_Ith_S(y, 0) + 5.0) / 5.0)); + AV_gamma_d = 1.0 / (1.0 + exp((50.0 - NV_Ith_S(y, 0)) / 20.0)); + AV_tau_d = 1.0 * AV_alpha_d * AV_beta_d + AV_gamma_d; + lmbda_ptrs[7][j] = -1. / AV_tau_d; + + // # /* L_type_Ca_current_f2_gate */ + AV_tau_f2 = 562.0 * exp((-pow(NV_Ith_S(y, 0) + 27.0, 2.0)) / 240.0) + 31.0 / (1.0 + exp((25.0 - NV_Ith_S(y, 0)) / 10.0)) + 80.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 10.0)); + lmbda_ptrs[9][j] = -1. / AV_tau_f2; + + // # /* L_type_Ca_current_fCass_gate */ + AV_tau_fCass = 80.0 / (1.0 + pow(NV_Ith_S(y, 15) / 0.05, 2.0)) + 2.0; + lmbda_ptrs[10][j] = -1. / AV_tau_fCass; + + // # /* L_type_Ca_current_f_gate */ + AV_tau_f = 1102.5 * exp((-pow(NV_Ith_S(y, 0) + 27.0, 2.0)) / 225.0) + 200.0 / (1.0 + exp((13.0 - NV_Ith_S(y, 0)) / 10.0)) + 180.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 10.0)) + 20.0; + lmbda_ptrs[8][j] = -1. / AV_tau_f; + + // # /* fast_sodium_current_h_gate */ + AV_alpha_h = NV_Ith_S(y, 0) < (-40.0) ? 0.057 * exp((-(NV_Ith_S(y, 0) + 80.0)) / 6.8) : 0.0; + AV_beta_h = NV_Ith_S(y, 0) < (-40.0) ? 2.7 * exp(0.079 * NV_Ith_S(y, 0)) + 310000.0 * exp(0.3485 * NV_Ith_S(y, 0)) : 0.77 / (0.13 * (1.0 + exp((NV_Ith_S(y, 0) + 10.66) / (-11.1)))); + AV_tau_h = 1.0 / (AV_alpha_h + AV_beta_h); + lmbda_ptrs[5][j] = -1. / AV_tau_h; + + // # /* fast_sodium_current_j_gate */ + AV_alpha_j = NV_Ith_S(y, 0) < (-40.0) ? ((-25428.0) * exp(0.2444 * NV_Ith_S(y, 0)) - 6.948e-06 * exp((-0.04391) * NV_Ith_S(y, 0))) * (NV_Ith_S(y, 0) + 37.78) / 1.0 / (1.0 + exp(0.311 * (NV_Ith_S(y, 0) + 79.23))) : 0.0; + AV_beta_j = NV_Ith_S(y, 0) < (-40.0) ? 0.02424 * exp((-0.01052) * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1378) * (NV_Ith_S(y, 0) + 40.14))) : 0.6 * exp(0.057 * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1) * (NV_Ith_S(y, 0) + 32.0))); + AV_tau_j = 1.0 / (AV_alpha_j + AV_beta_j); + lmbda_ptrs[6][j] = -1. / AV_tau_j; + + // # /* fast_sodium_current_m_gate */ + AV_alpha_m = 1.0 / (1.0 + exp(((-60.0) - NV_Ith_S(y, 0)) / 5.0)); + AV_beta_m = 0.1 / (1.0 + exp((NV_Ith_S(y, 0) + 35.0) / 5.0)) + 0.1 / (1.0 + exp((NV_Ith_S(y, 0) - 50.0) / 200.0)); + AV_tau_m = 1.0 * AV_alpha_m * AV_beta_m; + lmbda_ptrs[4][j] = -1. / AV_tau_m; + + // # /* rapid_time_dependent_potassium_current_Xr1_gate */ + AV_alpha_xr1 = 450.0 / (1.0 + exp(((-45.0) - NV_Ith_S(y, 0)) / 10.0)); + AV_beta_xr1 = 6.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 11.5)); + AV_tau_xr1 = 1.0 * AV_alpha_xr1 * AV_beta_xr1; + lmbda_ptrs[1][j] = -1. / AV_tau_xr1; + + // # /* rapid_time_dependent_potassium_current_Xr2_gate */ + AV_alpha_xr2 = 3.0 / (1.0 + exp(((-60.0) - NV_Ith_S(y, 0)) / 20.0)); + AV_beta_xr2 = 1.12 / (1.0 + exp((NV_Ith_S(y, 0) - 60.0) / 20.0)); + AV_tau_xr2 = 1.0 * AV_alpha_xr2 * AV_beta_xr2; + lmbda_ptrs[2][j] = -1. / AV_tau_xr2; + + // # /* slow_time_dependent_potassium_current_Xs_gate */ + AV_alpha_xs = 1400.0 / sqrt(1.0 + exp((5.0 - NV_Ith_S(y, 0)) / 6.0)); + AV_beta_xs = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) - 35.0) / 15.0)); + AV_tau_xs = 1.0 * AV_alpha_xs * AV_beta_xs + 80.0; + lmbda_ptrs[3][j] = -1. / AV_tau_xs; + + // # /* transient_outward_current_r_gate */ + AV_tau_r = 9.5 * exp((-pow(NV_Ith_S(y, 0) + 40.0, 2.0)) / 1800.0) + 0.8; + lmbda_ptrs[12][j] = -1. / AV_tau_r; + + // # /* transient_outward_current_s_gate */ + AV_tau_s = 85.0 * exp((-pow(NV_Ith_S(y, 0) + 45.0, 2.0)) / 320.0) + 5.0 / (1.0 + exp((NV_Ith_S(y, 0) - 20.0) / 5.0)) + 3.0; + lmbda_ptrs[11][j] = -1. / AV_tau_s; + } +} + +double TenTusscher2006_epi::rho_f_expl() +{ + return 6.5; +} + +#endif \ No newline at end of file diff --git a/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/tentusscher_smooth.h b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/tentusscher_smooth.h new file mode 100644 index 0000000000..7469e94d2c --- /dev/null +++ b/pySDC/projects/Monodomain/problem_classes/ionicmodels/cpp/tentusscher_smooth.h @@ -0,0 +1,550 @@ +#include +#include + +#include +#include +#include + +#include "ionicmodel.h" + +#ifndef TENTUSSCHER_SMOOTH +#define TENTUSSCHER_SMOOTH + +/* +The original TenTusscher2006_epi model has if clauses in the right hand side, which makes it non-smooth. +This is the original TenTusscher2006_epi model, but where if clauses are removed in order to obtain a smooth right hand side. +This model is used only for convergence tests with high order methods, since with the original one the relative error (typically) stagnates at 1e-8. +*/ + +class TenTusscher2006_epi_smooth : public IonicModel +{ +public: + TenTusscher2006_epi_smooth(const double scale_); + ~TenTusscher2006_epi_smooth(){}; + void f(py::array_t &y, py::array_t &fy); + void f_expl(py::array_t &y, py::array_t &fy); + void lmbda_exp(py::array_t &y_list, py::array_t &lmbda_list); + void lmbda_yinf_exp(py::array_t &y_list, py::array_t &lmbda_list, py::array_t &yinf_list); + py::list initial_values(); + double rho_f_expl(); + +private: + double AC_Cm, AC_K_pCa, AC_g_pCa, AC_g_CaL, AC_g_bca, AC_Buf_c, AC_Buf_sr, AC_Buf_ss, AC_Ca_o, AC_EC, AC_K_buf_c, AC_K_buf_sr, AC_K_buf_ss, AC_K_up, AC_V_leak, AC_V_rel, AC_V_sr, AC_V_ss, AC_V_xfer, AC_Vmax_up, AC_k1_prime, AC_k2_prime, AC_k3, AC_k4, AC_max_sr, AC_min_sr, AC_g_Na, AC_g_K1, AC_F, AC_R, AC_T, AC_V_c, AC_stim_amplitude, AC_K_o, AC_g_pK, AC_g_Kr, AC_P_kna, AC_g_Ks, AC_g_bna, AC_K_NaCa, AC_K_sat, AC_Km_Ca, AC_Km_Nai, AC_alpha, AC_sodium_calcium_exchanger_current_gamma, AC_Na_o, AC_K_mNa, AC_K_mk, AC_P_NaK, AC_g_to; +}; + +TenTusscher2006_epi_smooth::TenTusscher2006_epi_smooth(const double scale_) + : IonicModel(scale_) +{ + size = 19; + + AC_Cm = 1.0; // 185.0; + AC_K_pCa = 0.0005; + AC_g_pCa = 0.1238; + AC_g_CaL = 0.0398; + AC_g_bca = 0.000592; + AC_Buf_c = 0.2; + AC_Buf_sr = 10.0; + AC_Buf_ss = 0.4; + AC_Ca_o = 2.0; + AC_EC = 1.5; + AC_K_buf_c = 0.001; + AC_K_buf_sr = 0.3; + AC_K_buf_ss = 0.00025; + AC_K_up = 0.00025; + AC_V_leak = 0.00036; + AC_V_rel = 0.102; + AC_V_sr = 1094.0; + AC_V_ss = 54.68; + AC_V_xfer = 0.0038; + AC_Vmax_up = 0.006375; + AC_k1_prime = 0.15; + AC_k2_prime = 0.045; + AC_k3 = 0.06; + AC_k4 = 0.005; + AC_max_sr = 2.5; + AC_min_sr = 1.0; + AC_g_Na = 14.838; + AC_g_K1 = 5.405; + AC_F = 96.485; + AC_R = 8.314; + AC_T = 310.0; + AC_V_c = 16404.0; + AC_stim_amplitude = (-52.0); + AC_K_o = 5.4; + AC_g_pK = 0.0146; + AC_g_Kr = 0.153; + AC_P_kna = 0.03; + AC_g_Ks = 0.392; + AC_g_bna = 0.00029; + AC_K_NaCa = 1000.0; + AC_K_sat = 0.1; + AC_Km_Ca = 1.38; + AC_Km_Nai = 87.5; + AC_alpha = 2.5; + AC_sodium_calcium_exchanger_current_gamma = 0.35; + AC_Na_o = 140.0; + AC_K_mNa = 40.0; + AC_K_mk = 1.0; + AC_P_NaK = 2.724; + AC_g_to = 0.294; + + assign(f_expl_args, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}); + assign(f_exp_args, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15}); + assign(f_expl_indeces, {0, 13, 14, 15, 16, 17, 18}); + assign(f_exp_indeces, {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}); +} + +py::list TenTusscher2006_epi_smooth::initial_values() +{ + py::list y0(size); + y0[0] = -85.23; + y0[1] = 0.00621; + y0[2] = 0.4712; + y0[3] = 0.0095; + y0[4] = 0.00172; + y0[5] = 0.7444; + y0[6] = 0.7045; + y0[7] = 3.373e-05; + y0[8] = 0.7888; + y0[9] = 0.9755; + y0[10] = 0.9953; + y0[11] = 0.999998; + y0[12] = 2.42e-08; + y0[13] = 0.000126; + y0[14] = 3.64; + y0[15] = 0.00036; + y0[16] = 0.9073; + y0[17] = 8.604; + y0[18] = 136.89; + + return y0; +} + +void TenTusscher2006_epi_smooth::f(py::array_t &y_list, py::array_t &fy_list) +{ + double *y_ptrs[size]; + double *fy_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(fy_list, fy_ptrs, N, n_dofs); + + double y[size]; + // # needed for linear in gating variables + double AV_alpha_d, AV_beta_d, AV_d_inf, AV_gamma_d, AV_tau_d, AV_f2_inf, AV_tau_f2, AV_fCass_inf, AV_tau_fCass, AV_f_inf, AV_tau_f; + double AV_alpha_h, AV_beta_h, AV_h_inf, AV_tau_h, AV_alpha_j, AV_beta_j, AV_j_inf, AV_tau_j, AV_alpha_m, AV_beta_m, AV_m_inf, AV_tau_m; + double AV_alpha_xr1, AV_beta_xr1, AV_xr1_inf, AV_tau_xr1, AV_alpha_xr2, AV_beta_xr2, AV_xr2_inf, AV_tau_xr2, AV_alpha_xs, AV_beta_xs, AV_xs_inf, AV_tau_xs; + double AV_r_inf, AV_tau_r, AV_s_inf, AV_tau_s; + // # needed for nonlinear in gating variables + double AV_f_JCa_i_free, AV_f_JCa_sr_free, AV_f_JCa_ss_free, AV_i_leak, AV_i_up, AV_i_xfer, AV_kcasr, AV_k1, AV_k2, AV_O, AV_i_rel, AV_ddt_Ca_sr_total; + double AV_E_Ca, AV_E_K, AV_i_NaK, AV_i_to, AV_i_p_Ca, AV_i_CaL, AV_i_b_Ca, AV_alpha_K1, AV_beta_K1, AV_i_p_K, AV_i_Kr, AV_E_Ks, AV_E_Na, AV_i_NaCa; + double AV_ddt_Ca_i_total, AV_ddt_Ca_ss_total, AV_i_Na, AV_i_K1, AV_xK1_inf, AV_i_Ks, AV_i_b_Na; + // Remember to scale the first variable!!! + for (unsigned j = 0; j < n_dofs; j++) + { + for (unsigned i = 0; i < size; i++) + y[i] = y_ptrs[i][j]; + + // # Linear in gating variables + + // # /* L_type_Ca_current_d_gate */ + AV_alpha_d = 1.4 / (1.0 + exp(((-35.0) - NV_Ith_S(y, 0)) / 13.0)) + 0.25; + AV_beta_d = 1.4 / (1.0 + exp((NV_Ith_S(y, 0) + 5.0) / 5.0)); + AV_d_inf = 1.0 / (1.0 + exp(((-8.0) - NV_Ith_S(y, 0)) / 7.5)); + AV_gamma_d = 1.0 / (1.0 + exp((50.0 - NV_Ith_S(y, 0)) / 20.0)); + AV_tau_d = 1.0 * AV_alpha_d * AV_beta_d + AV_gamma_d; + fy_ptrs[7][j] = (AV_d_inf - NV_Ith_S(y, 7)) / AV_tau_d; + + // # /* L_type_Ca_current_f2_gate */ + AV_f2_inf = 0.67 / (1.0 + exp((NV_Ith_S(y, 0) + 35.0) / 7.0)) + 0.33; + AV_tau_f2 = 562.0 * exp((-pow(NV_Ith_S(y, 0) + 27.0, 2.0)) / 240.0) + 31.0 / (1.0 + exp((25.0 - NV_Ith_S(y, 0)) / 10.0)) + 80.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 10.0)); + fy_ptrs[9][j] = (AV_f2_inf - NV_Ith_S(y, 9)) / AV_tau_f2; + + // # /* L_type_Ca_current_fCass_gate */ + AV_fCass_inf = 0.6 / (1.0 + pow(NV_Ith_S(y, 15) / 0.05, 2.0)) + 0.4; + AV_tau_fCass = 80.0 / (1.0 + pow(NV_Ith_S(y, 15) / 0.05, 2.0)) + 2.0; + fy_ptrs[10][j] = (AV_fCass_inf - NV_Ith_S(y, 10)) / AV_tau_fCass; + + // # /* L_type_Ca_current_f_gate */ + AV_f_inf = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) + 20.0) / 7.0)); + AV_tau_f = 1102.5 * exp((-pow(NV_Ith_S(y, 0) + 27.0, 2.0)) / 225.0) + 200.0 / (1.0 + exp((13.0 - NV_Ith_S(y, 0)) / 10.0)) + 180.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 10.0)) + 20.0; + fy_ptrs[8][j] = (AV_f_inf - NV_Ith_S(y, 8)) / AV_tau_f; + + // # /* fast_sodium_current_h_gate */ + AV_alpha_h = 0.0; + AV_beta_h = 0.77 / (0.13 * (1.0 + exp((NV_Ith_S(y, 0) + 10.66) / (-11.1)))); + AV_h_inf = 1.0 / pow(1.0 + exp((NV_Ith_S(y, 0) + 71.55) / 7.43), 2.0); + AV_tau_h = 1.0 / (AV_alpha_h + AV_beta_h); + fy_ptrs[5][j] = (AV_h_inf - NV_Ith_S(y, 5)) / AV_tau_h; + + // # /* fast_sodium_current_j_gate */ + AV_alpha_j = 0.0; + AV_beta_j = 0.6 * exp(0.057 * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1) * (NV_Ith_S(y, 0) + 32.0))); + AV_j_inf = 1.0 / pow(1.0 + exp((NV_Ith_S(y, 0) + 71.55) / 7.43), 2.0); + AV_tau_j = 1.0 / (AV_alpha_j + AV_beta_j); + fy_ptrs[6][j] = (AV_j_inf - NV_Ith_S(y, 6)) / AV_tau_j; + + // # /* fast_sodium_current_m_gate */ + AV_alpha_m = 1.0 / (1.0 + exp(((-60.0) - NV_Ith_S(y, 0)) / 5.0)); + AV_beta_m = 0.1 / (1.0 + exp((NV_Ith_S(y, 0) + 35.0) / 5.0)) + 0.1 / (1.0 + exp((NV_Ith_S(y, 0) - 50.0) / 200.0)); + AV_m_inf = 1.0 / pow(1.0 + exp(((-56.86) - NV_Ith_S(y, 0)) / 9.03), 2.0); + AV_tau_m = 1.0 * AV_alpha_m * AV_beta_m; + fy_ptrs[4][j] = (AV_m_inf - NV_Ith_S(y, 4)) / AV_tau_m; + + // # /* rapid_time_dependent_potassium_current_Xr1_gate */ + AV_alpha_xr1 = 450.0 / (1.0 + exp(((-45.0) - NV_Ith_S(y, 0)) / 10.0)); + AV_beta_xr1 = 6.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 11.5)); + AV_xr1_inf = 1.0 / (1.0 + exp(((-26.0) - NV_Ith_S(y, 0)) / 7.0)); + AV_tau_xr1 = 1.0 * AV_alpha_xr1 * AV_beta_xr1; + fy_ptrs[1][j] = (AV_xr1_inf - NV_Ith_S(y, 1)) / AV_tau_xr1; + + // # /* rapid_time_dependent_potassium_current_Xr2_gate */ + AV_alpha_xr2 = 3.0 / (1.0 + exp(((-60.0) - NV_Ith_S(y, 0)) / 20.0)); + AV_beta_xr2 = 1.12 / (1.0 + exp((NV_Ith_S(y, 0) - 60.0) / 20.0)); + AV_xr2_inf = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) + 88.0) / 24.0)); + AV_tau_xr2 = 1.0 * AV_alpha_xr2 * AV_beta_xr2; + fy_ptrs[2][j] = (AV_xr2_inf - NV_Ith_S(y, 2)) / AV_tau_xr2; + + // # /* slow_time_dependent_potassium_current_Xs_gate */ + AV_alpha_xs = 1400.0 / sqrt(1.0 + exp((5.0 - NV_Ith_S(y, 0)) / 6.0)); + AV_beta_xs = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) - 35.0) / 15.0)); + AV_xs_inf = 1.0 / (1.0 + exp(((-5.0) - NV_Ith_S(y, 0)) / 14.0)); + AV_tau_xs = 1.0 * AV_alpha_xs * AV_beta_xs + 80.0; + fy_ptrs[3][j] = (AV_xs_inf - NV_Ith_S(y, 3)) / AV_tau_xs; + + // # /* transient_outward_current_r_gate */ + AV_r_inf = 1.0 / (1.0 + exp((20.0 - NV_Ith_S(y, 0)) / 6.0)); + AV_tau_r = 9.5 * exp((-pow(NV_Ith_S(y, 0) + 40.0, 2.0)) / 1800.0) + 0.8; + fy_ptrs[12][j] = (AV_r_inf - NV_Ith_S(y, 12)) / AV_tau_r; + + // # /* transient_outward_current_s_gate */ + AV_s_inf = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) + 20.0) / 5.0)); + AV_tau_s = 85.0 * exp((-pow(NV_Ith_S(y, 0) + 45.0, 2.0)) / 320.0) + 5.0 / (1.0 + exp((NV_Ith_S(y, 0) - 20.0) / 5.0)) + 3.0; + fy_ptrs[11][j] = (AV_s_inf - NV_Ith_S(y, 11)) / AV_tau_s; + + // # Non linear in gating variables + + // # /* calcium_dynamics */ + AV_f_JCa_i_free = 1.0 / (1.0 + AC_Buf_c * AC_K_buf_c / pow(NV_Ith_S(y, 13) + AC_K_buf_c, 2.0)); + AV_f_JCa_sr_free = 1.0 / (1.0 + AC_Buf_sr * AC_K_buf_sr / pow(NV_Ith_S(y, 14) + AC_K_buf_sr, 2.0)); + AV_f_JCa_ss_free = 1.0 / (1.0 + AC_Buf_ss * AC_K_buf_ss / pow(NV_Ith_S(y, 15) + AC_K_buf_ss, 2.0)); + AV_i_leak = AC_V_leak * (NV_Ith_S(y, 14) - NV_Ith_S(y, 13)); + AV_i_up = AC_Vmax_up / (1.0 + pow(AC_K_up, 2.0) / pow(NV_Ith_S(y, 13), 2.0)); + AV_i_xfer = AC_V_xfer * (NV_Ith_S(y, 15) - NV_Ith_S(y, 13)); + AV_kcasr = AC_max_sr - (AC_max_sr - AC_min_sr) / (1.0 + pow(AC_EC / NV_Ith_S(y, 14), 2.0)); + AV_k1 = AC_k1_prime / AV_kcasr; + AV_k2 = AC_k2_prime * AV_kcasr; + AV_O = AV_k1 * pow(NV_Ith_S(y, 15), 2.0) * NV_Ith_S(y, 16) / (AC_k3 + AV_k1 * pow(NV_Ith_S(y, 15), 2.0)); + fy_ptrs[16][j] = (-AV_k2) * NV_Ith_S(y, 15) * NV_Ith_S(y, 16) + AC_k4 * (1.0 - NV_Ith_S(y, 16)); + AV_i_rel = AC_V_rel * AV_O * (NV_Ith_S(y, 14) - NV_Ith_S(y, 15)); + AV_ddt_Ca_sr_total = AV_i_up - (AV_i_rel + AV_i_leak); + fy_ptrs[14][j] = AV_ddt_Ca_sr_total * AV_f_JCa_sr_free; + + // # /* reversal_potentials */ + AV_E_Ca = 0.5 * AC_R * AC_T / AC_F * log(AC_Ca_o / NV_Ith_S(y, 13)); + AV_E_K = AC_R * AC_T / AC_F * log(AC_K_o / NV_Ith_S(y, 18)); + + // # /* sodium_potassium_pump_current */ + AV_i_NaK = AC_P_NaK * AC_K_o / (AC_K_o + AC_K_mk) * NV_Ith_S(y, 17) / (NV_Ith_S(y, 17) + AC_K_mNa) / (1.0 + 0.1245 * exp((-0.1) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)) + 0.0353 * exp((-NV_Ith_S(y, 0)) * AC_F / (AC_R * AC_T))); + + // # /* transient_outward_current */ + AV_i_to = AC_g_to * NV_Ith_S(y, 12) * NV_Ith_S(y, 11) * (NV_Ith_S(y, 0) - AV_E_K); + + // # /* calcium_pump_current */ + AV_i_p_Ca = AC_g_pCa * NV_Ith_S(y, 13) / (NV_Ith_S(y, 13) + AC_K_pCa); + + // # /* *remaining* */ + AV_i_CaL = AC_g_CaL * NV_Ith_S(y, 7) * NV_Ith_S(y, 8) * NV_Ith_S(y, 9) * NV_Ith_S(y, 10) * 4.0 * (NV_Ith_S(y, 0) - 15.0) * pow(AC_F, 2.0) / (AC_R * AC_T) * (0.25 * NV_Ith_S(y, 15) * exp(2.0 * (NV_Ith_S(y, 0) - 15.0) * AC_F / (AC_R * AC_T)) - AC_Ca_o) / (exp(2.0 * (NV_Ith_S(y, 0) - 15.0) * AC_F / (AC_R * AC_T)) - 1.0); + AV_i_b_Ca = AC_g_bca * (NV_Ith_S(y, 0) - AV_E_Ca); + AV_alpha_K1 = 0.1 / (1.0 + exp(0.06 * (NV_Ith_S(y, 0) - AV_E_K - 200.0))); + AV_beta_K1 = (3.0 * exp(0.0002 * (NV_Ith_S(y, 0) - AV_E_K + 100.0)) + exp(0.1 * (NV_Ith_S(y, 0) - AV_E_K - 10.0))) / (1.0 + exp((-0.5) * (NV_Ith_S(y, 0) - AV_E_K))); + AV_i_p_K = AC_g_pK * (NV_Ith_S(y, 0) - AV_E_K) / (1.0 + exp((25.0 - NV_Ith_S(y, 0)) / 5.98)); + AV_i_Kr = AC_g_Kr * sqrt(AC_K_o / 5.4) * NV_Ith_S(y, 1) * NV_Ith_S(y, 2) * (NV_Ith_S(y, 0) - AV_E_K); + AV_E_Ks = AC_R * AC_T / AC_F * log((AC_K_o + AC_P_kna * AC_Na_o) / (NV_Ith_S(y, 18) + AC_P_kna * NV_Ith_S(y, 17))); + AV_E_Na = AC_R * AC_T / AC_F * log(AC_Na_o / NV_Ith_S(y, 17)); + AV_i_NaCa = AC_K_NaCa * (exp(AC_sodium_calcium_exchanger_current_gamma * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)) * pow(NV_Ith_S(y, 17), 3.0) * AC_Ca_o - exp((AC_sodium_calcium_exchanger_current_gamma - 1.0) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)) * pow(AC_Na_o, 3.0) * NV_Ith_S(y, 13) * AC_alpha) / ((pow(AC_Km_Nai, 3.0) + pow(AC_Na_o, 3.0)) * (AC_Km_Ca + AC_Ca_o) * (1.0 + AC_K_sat * exp((AC_sodium_calcium_exchanger_current_gamma - 1.0) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)))); + AV_ddt_Ca_i_total = (-(AV_i_b_Ca + AV_i_p_Ca - 2.0 * AV_i_NaCa)) * AC_Cm / (2.0 * AC_V_c * AC_F) + (AV_i_leak - AV_i_up) * AC_V_sr / AC_V_c + AV_i_xfer; + AV_ddt_Ca_ss_total = (-AV_i_CaL) * AC_Cm / (2.0 * AC_V_ss * AC_F) + AV_i_rel * AC_V_sr / AC_V_ss - AV_i_xfer * AC_V_c / AC_V_ss; + AV_i_Na = AC_g_Na * pow(NV_Ith_S(y, 4), 3.0) * NV_Ith_S(y, 5) * NV_Ith_S(y, 6) * (NV_Ith_S(y, 0) - AV_E_Na); + AV_xK1_inf = AV_alpha_K1 / (AV_alpha_K1 + AV_beta_K1); + AV_i_Ks = AC_g_Ks * pow(NV_Ith_S(y, 3), 2.0) * (NV_Ith_S(y, 0) - AV_E_Ks); + AV_i_b_Na = AC_g_bna * (NV_Ith_S(y, 0) - AV_E_Na); + fy_ptrs[13][j] = AV_ddt_Ca_i_total * AV_f_JCa_i_free; + fy_ptrs[15][j] = AV_ddt_Ca_ss_total * AV_f_JCa_ss_free; + AV_i_K1 = AC_g_K1 * AV_xK1_inf * sqrt(AC_K_o / 5.4) * (NV_Ith_S(y, 0) - AV_E_K); + fy_ptrs[17][j] = (-(AV_i_Na + AV_i_b_Na + 3.0 * AV_i_NaK + 3.0 * AV_i_NaCa)) / (AC_V_c * AC_F) * AC_Cm; + fy_ptrs[0][j] = scale * (-(AV_i_K1 + AV_i_to + AV_i_Kr + AV_i_Ks + AV_i_CaL + AV_i_NaK + AV_i_Na + AV_i_b_Na + AV_i_NaCa + AV_i_b_Ca + AV_i_p_K + AV_i_p_Ca)); + fy_ptrs[18][j] = (-(AV_i_K1 + AV_i_to + AV_i_Kr + AV_i_Ks + AV_i_p_K - 2.0 * AV_i_NaK)) / (AC_V_c * AC_F) * AC_Cm; + } +} + +void TenTusscher2006_epi_smooth::f_expl(py::array_t &y_list, py::array_t &fy_list) +{ + double *y_ptrs[size]; + double *fy_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(fy_list, fy_ptrs, N, n_dofs); + + double y[size]; + // # needed for nonlinear in gating variables + double AV_f_JCa_i_free, AV_f_JCa_sr_free, AV_f_JCa_ss_free, AV_i_leak, AV_i_up, AV_i_xfer, AV_kcasr, AV_k1, AV_k2, AV_O, AV_i_rel, AV_ddt_Ca_sr_total; + double AV_E_Ca, AV_E_K, AV_i_NaK, AV_i_to, AV_i_p_Ca, AV_i_CaL, AV_i_b_Ca, AV_alpha_K1, AV_beta_K1, AV_i_p_K, AV_i_Kr, AV_E_Ks, AV_E_Na, AV_i_NaCa; + double AV_ddt_Ca_i_total, AV_ddt_Ca_ss_total, AV_i_Na, AV_i_K1, AV_xK1_inf, AV_i_Ks, AV_i_b_Na; + // Remember to scale the first variable!!! + for (unsigned j = 0; j < n_dofs; j++) + { + for (unsigned i = 0; i < size; i++) + y[i] = y_ptrs[i][j]; + + // # Non linear in gating variables + + // # /* calcium_dynamics */ + AV_f_JCa_i_free = 1.0 / (1.0 + AC_Buf_c * AC_K_buf_c / pow(NV_Ith_S(y, 13) + AC_K_buf_c, 2.0)); + AV_f_JCa_sr_free = 1.0 / (1.0 + AC_Buf_sr * AC_K_buf_sr / pow(NV_Ith_S(y, 14) + AC_K_buf_sr, 2.0)); + AV_f_JCa_ss_free = 1.0 / (1.0 + AC_Buf_ss * AC_K_buf_ss / pow(NV_Ith_S(y, 15) + AC_K_buf_ss, 2.0)); + AV_i_leak = AC_V_leak * (NV_Ith_S(y, 14) - NV_Ith_S(y, 13)); + AV_i_up = AC_Vmax_up / (1.0 + pow(AC_K_up, 2.0) / pow(NV_Ith_S(y, 13), 2.0)); + AV_i_xfer = AC_V_xfer * (NV_Ith_S(y, 15) - NV_Ith_S(y, 13)); + AV_kcasr = AC_max_sr - (AC_max_sr - AC_min_sr) / (1.0 + pow(AC_EC / NV_Ith_S(y, 14), 2.0)); + AV_k1 = AC_k1_prime / AV_kcasr; + AV_k2 = AC_k2_prime * AV_kcasr; + AV_O = AV_k1 * pow(NV_Ith_S(y, 15), 2.0) * NV_Ith_S(y, 16) / (AC_k3 + AV_k1 * pow(NV_Ith_S(y, 15), 2.0)); + fy_ptrs[16][j] = (-AV_k2) * NV_Ith_S(y, 15) * NV_Ith_S(y, 16) + AC_k4 * (1.0 - NV_Ith_S(y, 16)); + AV_i_rel = AC_V_rel * AV_O * (NV_Ith_S(y, 14) - NV_Ith_S(y, 15)); + AV_ddt_Ca_sr_total = AV_i_up - (AV_i_rel + AV_i_leak); + fy_ptrs[14][j] = AV_ddt_Ca_sr_total * AV_f_JCa_sr_free; + + // # /* reversal_potentials */ + AV_E_Ca = 0.5 * AC_R * AC_T / AC_F * log(AC_Ca_o / NV_Ith_S(y, 13)); + AV_E_K = AC_R * AC_T / AC_F * log(AC_K_o / NV_Ith_S(y, 18)); + + // # /* sodium_potassium_pump_current */ + AV_i_NaK = AC_P_NaK * AC_K_o / (AC_K_o + AC_K_mk) * NV_Ith_S(y, 17) / (NV_Ith_S(y, 17) + AC_K_mNa) / (1.0 + 0.1245 * exp((-0.1) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)) + 0.0353 * exp((-NV_Ith_S(y, 0)) * AC_F / (AC_R * AC_T))); + + // # /* transient_outward_current */ + AV_i_to = AC_g_to * NV_Ith_S(y, 12) * NV_Ith_S(y, 11) * (NV_Ith_S(y, 0) - AV_E_K); + + // # /* calcium_pump_current */ + AV_i_p_Ca = AC_g_pCa * NV_Ith_S(y, 13) / (NV_Ith_S(y, 13) + AC_K_pCa); + + // # /* *remaining* */ + AV_i_CaL = AC_g_CaL * NV_Ith_S(y, 7) * NV_Ith_S(y, 8) * NV_Ith_S(y, 9) * NV_Ith_S(y, 10) * 4.0 * (NV_Ith_S(y, 0) - 15.0) * pow(AC_F, 2.0) / (AC_R * AC_T) * (0.25 * NV_Ith_S(y, 15) * exp(2.0 * (NV_Ith_S(y, 0) - 15.0) * AC_F / (AC_R * AC_T)) - AC_Ca_o) / (exp(2.0 * (NV_Ith_S(y, 0) - 15.0) * AC_F / (AC_R * AC_T)) - 1.0); + AV_i_b_Ca = AC_g_bca * (NV_Ith_S(y, 0) - AV_E_Ca); + AV_alpha_K1 = 0.1 / (1.0 + exp(0.06 * (NV_Ith_S(y, 0) - AV_E_K - 200.0))); + AV_beta_K1 = (3.0 * exp(0.0002 * (NV_Ith_S(y, 0) - AV_E_K + 100.0)) + exp(0.1 * (NV_Ith_S(y, 0) - AV_E_K - 10.0))) / (1.0 + exp((-0.5) * (NV_Ith_S(y, 0) - AV_E_K))); + AV_i_p_K = AC_g_pK * (NV_Ith_S(y, 0) - AV_E_K) / (1.0 + exp((25.0 - NV_Ith_S(y, 0)) / 5.98)); + AV_i_Kr = AC_g_Kr * sqrt(AC_K_o / 5.4) * NV_Ith_S(y, 1) * NV_Ith_S(y, 2) * (NV_Ith_S(y, 0) - AV_E_K); + AV_E_Ks = AC_R * AC_T / AC_F * log((AC_K_o + AC_P_kna * AC_Na_o) / (NV_Ith_S(y, 18) + AC_P_kna * NV_Ith_S(y, 17))); + AV_E_Na = AC_R * AC_T / AC_F * log(AC_Na_o / NV_Ith_S(y, 17)); + AV_i_NaCa = AC_K_NaCa * (exp(AC_sodium_calcium_exchanger_current_gamma * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)) * pow(NV_Ith_S(y, 17), 3.0) * AC_Ca_o - exp((AC_sodium_calcium_exchanger_current_gamma - 1.0) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)) * pow(AC_Na_o, 3.0) * NV_Ith_S(y, 13) * AC_alpha) / ((pow(AC_Km_Nai, 3.0) + pow(AC_Na_o, 3.0)) * (AC_Km_Ca + AC_Ca_o) * (1.0 + AC_K_sat * exp((AC_sodium_calcium_exchanger_current_gamma - 1.0) * NV_Ith_S(y, 0) * AC_F / (AC_R * AC_T)))); + AV_ddt_Ca_i_total = (-(AV_i_b_Ca + AV_i_p_Ca - 2.0 * AV_i_NaCa)) * AC_Cm / (2.0 * AC_V_c * AC_F) + (AV_i_leak - AV_i_up) * AC_V_sr / AC_V_c + AV_i_xfer; + AV_ddt_Ca_ss_total = (-AV_i_CaL) * AC_Cm / (2.0 * AC_V_ss * AC_F) + AV_i_rel * AC_V_sr / AC_V_ss - AV_i_xfer * AC_V_c / AC_V_ss; + AV_i_Na = AC_g_Na * pow(NV_Ith_S(y, 4), 3.0) * NV_Ith_S(y, 5) * NV_Ith_S(y, 6) * (NV_Ith_S(y, 0) - AV_E_Na); + AV_xK1_inf = AV_alpha_K1 / (AV_alpha_K1 + AV_beta_K1); + AV_i_Ks = AC_g_Ks * pow(NV_Ith_S(y, 3), 2.0) * (NV_Ith_S(y, 0) - AV_E_Ks); + AV_i_b_Na = AC_g_bna * (NV_Ith_S(y, 0) - AV_E_Na); + fy_ptrs[13][j] = AV_ddt_Ca_i_total * AV_f_JCa_i_free; + fy_ptrs[15][j] = AV_ddt_Ca_ss_total * AV_f_JCa_ss_free; + AV_i_K1 = AC_g_K1 * AV_xK1_inf * sqrt(AC_K_o / 5.4) * (NV_Ith_S(y, 0) - AV_E_K); + fy_ptrs[17][j] = (-(AV_i_Na + AV_i_b_Na + 3.0 * AV_i_NaK + 3.0 * AV_i_NaCa)) / (AC_V_c * AC_F) * AC_Cm; + fy_ptrs[0][j] = scale * (-(AV_i_K1 + AV_i_to + AV_i_Kr + AV_i_Ks + AV_i_CaL + AV_i_NaK + AV_i_Na + AV_i_b_Na + AV_i_NaCa + AV_i_b_Ca + AV_i_p_K + AV_i_p_Ca)); + fy_ptrs[18][j] = (-(AV_i_K1 + AV_i_to + AV_i_Kr + AV_i_Ks + AV_i_p_K - 2.0 * AV_i_NaK)) / (AC_V_c * AC_F) * AC_Cm; + } +} + +void TenTusscher2006_epi_smooth::lmbda_yinf_exp(py::array_t &y_list, py::array_t &lmbda_list, py::array_t &yinf_list) +{ + double *y_ptrs[size]; + double *lmbda_ptrs[size]; + double *yinf_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(lmbda_list, lmbda_ptrs, N, n_dofs); + get_raw_data(yinf_list, yinf_ptrs, N, n_dofs); + + double y[size]; + double AV_alpha_d, AV_beta_d, AV_gamma_d, AV_tau_d, AV_tau_f2, AV_tau_fCass, AV_tau_f; + double AV_alpha_h, AV_beta_h, AV_tau_h, AV_alpha_j, AV_beta_j, AV_tau_j, AV_alpha_m, AV_beta_m, AV_tau_m; + double AV_alpha_xr1, AV_beta_xr1, AV_tau_xr1, AV_alpha_xr2, AV_beta_xr2, AV_tau_xr2, AV_alpha_xs, AV_beta_xs, AV_tau_xs; + double AV_tau_r, AV_tau_s; + // Remember to scale the first variable!!! + for (unsigned j = 0; j < n_dofs; j++) + { + for (unsigned i = 0; i < size; i++) + y[i] = y_ptrs[i][j]; + + // # Linear in gating variables + + // # /* L_type_Ca_current_d_gate */ + AV_alpha_d = 1.4 / (1.0 + exp(((-35.0) - NV_Ith_S(y, 0)) / 13.0)) + 0.25; + AV_beta_d = 1.4 / (1.0 + exp((NV_Ith_S(y, 0) + 5.0) / 5.0)); + yinf_ptrs[7][j] = 1.0 / (1.0 + exp(((-8.0) - NV_Ith_S(y, 0)) / 7.5)); + AV_gamma_d = 1.0 / (1.0 + exp((50.0 - NV_Ith_S(y, 0)) / 20.0)); + AV_tau_d = 1.0 * AV_alpha_d * AV_beta_d + AV_gamma_d; + lmbda_ptrs[7][j] = -1. / AV_tau_d; + + // # /* L_type_Ca_current_f2_gate */ + yinf_ptrs[9][j] = 0.67 / (1.0 + exp((NV_Ith_S(y, 0) + 35.0) / 7.0)) + 0.33; + AV_tau_f2 = 562.0 * exp((-pow(NV_Ith_S(y, 0) + 27.0, 2.0)) / 240.0) + 31.0 / (1.0 + exp((25.0 - NV_Ith_S(y, 0)) / 10.0)) + 80.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 10.0)); + lmbda_ptrs[9][j] = -1. / AV_tau_f2; + + // # /* L_type_Ca_current_fCass_gate */ + yinf_ptrs[10][j] = 0.6 / (1.0 + pow(NV_Ith_S(y, 15) / 0.05, 2.0)) + 0.4; + AV_tau_fCass = 80.0 / (1.0 + pow(NV_Ith_S(y, 15) / 0.05, 2.0)) + 2.0; + lmbda_ptrs[10][j] = -1. / AV_tau_fCass; + + // # /* L_type_Ca_current_f_gate */ + yinf_ptrs[8][j] = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) + 20.0) / 7.0)); + AV_tau_f = 1102.5 * exp((-pow(NV_Ith_S(y, 0) + 27.0, 2.0)) / 225.0) + 200.0 / (1.0 + exp((13.0 - NV_Ith_S(y, 0)) / 10.0)) + 180.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 10.0)) + 20.0; + lmbda_ptrs[8][j] = -1. / AV_tau_f; + + // # /* fast_sodium_current_h_gate */ + AV_alpha_h = 0.0; + AV_beta_h = 0.77 / (0.13 * (1.0 + exp((NV_Ith_S(y, 0) + 10.66) / (-11.1)))); + yinf_ptrs[5][j] = 1.0 / pow(1.0 + exp((NV_Ith_S(y, 0) + 71.55) / 7.43), 2.0); + AV_tau_h = 1.0 / (AV_alpha_h + AV_beta_h); + lmbda_ptrs[5][j] = -1. / AV_tau_h; + + // # /* fast_sodium_current_j_gate */ + AV_alpha_j = 0.0; + AV_beta_j = 0.6 * exp(0.057 * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1) * (NV_Ith_S(y, 0) + 32.0))); + yinf_ptrs[6][j] = 1.0 / pow(1.0 + exp((NV_Ith_S(y, 0) + 71.55) / 7.43), 2.0); + AV_tau_j = 1.0 / (AV_alpha_j + AV_beta_j); + lmbda_ptrs[6][j] = -1. / AV_tau_j; + + // # /* fast_sodium_current_m_gate */ + AV_alpha_m = 1.0 / (1.0 + exp(((-60.0) - NV_Ith_S(y, 0)) / 5.0)); + AV_beta_m = 0.1 / (1.0 + exp((NV_Ith_S(y, 0) + 35.0) / 5.0)) + 0.1 / (1.0 + exp((NV_Ith_S(y, 0) - 50.0) / 200.0)); + yinf_ptrs[4][j] = 1.0 / pow(1.0 + exp(((-56.86) - NV_Ith_S(y, 0)) / 9.03), 2.0); + AV_tau_m = 1.0 * AV_alpha_m * AV_beta_m; + lmbda_ptrs[4][j] = -1. / AV_tau_m; + + // # /* rapid_time_dependent_potassium_current_Xr1_gate */ + AV_alpha_xr1 = 450.0 / (1.0 + exp(((-45.0) - NV_Ith_S(y, 0)) / 10.0)); + AV_beta_xr1 = 6.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 11.5)); + yinf_ptrs[1][j] = 1.0 / (1.0 + exp(((-26.0) - NV_Ith_S(y, 0)) / 7.0)); + AV_tau_xr1 = 1.0 * AV_alpha_xr1 * AV_beta_xr1; + lmbda_ptrs[1][j] = -1. / AV_tau_xr1; + + // # /* rapid_time_dependent_potassium_current_Xr2_gate */ + AV_alpha_xr2 = 3.0 / (1.0 + exp(((-60.0) - NV_Ith_S(y, 0)) / 20.0)); + AV_beta_xr2 = 1.12 / (1.0 + exp((NV_Ith_S(y, 0) - 60.0) / 20.0)); + yinf_ptrs[2][j] = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) + 88.0) / 24.0)); + AV_tau_xr2 = 1.0 * AV_alpha_xr2 * AV_beta_xr2; + lmbda_ptrs[2][j] = -1. / AV_tau_xr2; + + // # /* slow_time_dependent_potassium_current_Xs_gate */ + AV_alpha_xs = 1400.0 / sqrt(1.0 + exp((5.0 - NV_Ith_S(y, 0)) / 6.0)); + AV_beta_xs = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) - 35.0) / 15.0)); + yinf_ptrs[3][j] = 1.0 / (1.0 + exp(((-5.0) - NV_Ith_S(y, 0)) / 14.0)); + AV_tau_xs = 1.0 * AV_alpha_xs * AV_beta_xs + 80.0; + lmbda_ptrs[3][j] = -1. / AV_tau_xs; + + // # /* transient_outward_current_r_gate */ + yinf_ptrs[12][j] = 1.0 / (1.0 + exp((20.0 - NV_Ith_S(y, 0)) / 6.0)); + AV_tau_r = 9.5 * exp((-pow(NV_Ith_S(y, 0) + 40.0, 2.0)) / 1800.0) + 0.8; + lmbda_ptrs[12][j] = -1. / AV_tau_r; + + // # /* transient_outward_current_s_gate */ + yinf_ptrs[11][j] = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) + 20.0) / 5.0)); + AV_tau_s = 85.0 * exp((-pow(NV_Ith_S(y, 0) + 45.0, 2.0)) / 320.0) + 5.0 / (1.0 + exp((NV_Ith_S(y, 0) - 20.0) / 5.0)) + 3.0; + lmbda_ptrs[11][j] = -1. / AV_tau_s; + } +} + +void TenTusscher2006_epi_smooth::lmbda_exp(py::array_t &y_list, py::array_t &lmbda_list) +{ + double *y_ptrs[size]; + double *lmbda_ptrs[size]; + size_t N; + size_t n_dofs; + get_raw_data(y_list, y_ptrs, N, n_dofs); + get_raw_data(lmbda_list, lmbda_ptrs, N, n_dofs); + + double y[size]; + double AV_alpha_d, AV_beta_d, AV_gamma_d, AV_tau_d, AV_tau_f2, AV_tau_fCass, AV_tau_f; + double AV_alpha_h, AV_beta_h, AV_tau_h, AV_alpha_j, AV_beta_j, AV_tau_j, AV_alpha_m, AV_beta_m, AV_tau_m; + double AV_alpha_xr1, AV_beta_xr1, AV_tau_xr1, AV_alpha_xr2, AV_beta_xr2, AV_tau_xr2, AV_alpha_xs, AV_beta_xs, AV_tau_xs; + double AV_tau_r, AV_tau_s; + // Remember to scale the first variable!!! + for (unsigned j = 0; j < n_dofs; j++) + { + for (unsigned i = 0; i < size; i++) + y[i] = y_ptrs[i][j]; + + // # Linear in gating variables + + // # /* L_type_Ca_current_d_gate */ + AV_alpha_d = 1.4 / (1.0 + exp(((-35.0) - NV_Ith_S(y, 0)) / 13.0)) + 0.25; + AV_beta_d = 1.4 / (1.0 + exp((NV_Ith_S(y, 0) + 5.0) / 5.0)); + AV_gamma_d = 1.0 / (1.0 + exp((50.0 - NV_Ith_S(y, 0)) / 20.0)); + AV_tau_d = 1.0 * AV_alpha_d * AV_beta_d + AV_gamma_d; + lmbda_ptrs[7][j] = -1. / AV_tau_d; + + // # /* L_type_Ca_current_f2_gate */ + AV_tau_f2 = 562.0 * exp((-pow(NV_Ith_S(y, 0) + 27.0, 2.0)) / 240.0) + 31.0 / (1.0 + exp((25.0 - NV_Ith_S(y, 0)) / 10.0)) + 80.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 10.0)); + lmbda_ptrs[9][j] = -1. / AV_tau_f2; + + // # /* L_type_Ca_current_fCass_gate */ + AV_tau_fCass = 80.0 / (1.0 + pow(NV_Ith_S(y, 15) / 0.05, 2.0)) + 2.0; + lmbda_ptrs[10][j] = -1. / AV_tau_fCass; + + // # /* L_type_Ca_current_f_gate */ + AV_tau_f = 1102.5 * exp((-pow(NV_Ith_S(y, 0) + 27.0, 2.0)) / 225.0) + 200.0 / (1.0 + exp((13.0 - NV_Ith_S(y, 0)) / 10.0)) + 180.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 10.0)) + 20.0; + lmbda_ptrs[8][j] = -1. / AV_tau_f; + + // # /* fast_sodium_current_h_gate */ + AV_alpha_h = 0.0; + AV_beta_h = 0.77 / (0.13 * (1.0 + exp((NV_Ith_S(y, 0) + 10.66) / (-11.1)))); + AV_tau_h = 1.0 / (AV_alpha_h + AV_beta_h); + lmbda_ptrs[5][j] = -1. / AV_tau_h; + + // # /* fast_sodium_current_j_gate */ + AV_alpha_j = 0.0; + AV_beta_j = 0.6 * exp(0.057 * NV_Ith_S(y, 0)) / (1.0 + exp((-0.1) * (NV_Ith_S(y, 0) + 32.0))); + AV_tau_j = 1.0 / (AV_alpha_j + AV_beta_j); + lmbda_ptrs[6][j] = -1. / AV_tau_j; + + // # /* fast_sodium_current_m_gate */ + AV_alpha_m = 1.0 / (1.0 + exp(((-60.0) - NV_Ith_S(y, 0)) / 5.0)); + AV_beta_m = 0.1 / (1.0 + exp((NV_Ith_S(y, 0) + 35.0) / 5.0)) + 0.1 / (1.0 + exp((NV_Ith_S(y, 0) - 50.0) / 200.0)); + AV_tau_m = 1.0 * AV_alpha_m * AV_beta_m; + lmbda_ptrs[4][j] = -1. / AV_tau_m; + + // # /* rapid_time_dependent_potassium_current_Xr1_gate */ + AV_alpha_xr1 = 450.0 / (1.0 + exp(((-45.0) - NV_Ith_S(y, 0)) / 10.0)); + AV_beta_xr1 = 6.0 / (1.0 + exp((NV_Ith_S(y, 0) + 30.0) / 11.5)); + AV_tau_xr1 = 1.0 * AV_alpha_xr1 * AV_beta_xr1; + lmbda_ptrs[1][j] = -1. / AV_tau_xr1; + + // # /* rapid_time_dependent_potassium_current_Xr2_gate */ + AV_alpha_xr2 = 3.0 / (1.0 + exp(((-60.0) - NV_Ith_S(y, 0)) / 20.0)); + AV_beta_xr2 = 1.12 / (1.0 + exp((NV_Ith_S(y, 0) - 60.0) / 20.0)); + AV_tau_xr2 = 1.0 * AV_alpha_xr2 * AV_beta_xr2; + lmbda_ptrs[2][j] = -1. / AV_tau_xr2; + + // # /* slow_time_dependent_potassium_current_Xs_gate */ + AV_alpha_xs = 1400.0 / sqrt(1.0 + exp((5.0 - NV_Ith_S(y, 0)) / 6.0)); + AV_beta_xs = 1.0 / (1.0 + exp((NV_Ith_S(y, 0) - 35.0) / 15.0)); + AV_tau_xs = 1.0 * AV_alpha_xs * AV_beta_xs + 80.0; + lmbda_ptrs[3][j] = -1. / AV_tau_xs; + + // # /* transient_outward_current_r_gate */ + AV_tau_r = 9.5 * exp((-pow(NV_Ith_S(y, 0) + 40.0, 2.0)) / 1800.0) + 0.8; + lmbda_ptrs[12][j] = -1. / AV_tau_r; + + // # /* transient_outward_current_s_gate */ + AV_tau_s = 85.0 * exp((-pow(NV_Ith_S(y, 0) + 45.0, 2.0)) / 320.0) + 5.0 / (1.0 + exp((NV_Ith_S(y, 0) - 20.0) / 5.0)) + 3.0; + lmbda_ptrs[11][j] = -1. / AV_tau_s; + } +} + +double TenTusscher2006_epi_smooth::rho_f_expl() +{ + return 6.5; +} + +#endif \ No newline at end of file diff --git a/pySDC/projects/Monodomain/problem_classes/space_discretizazions/Parabolic_DCT.py b/pySDC/projects/Monodomain/problem_classes/space_discretizazions/Parabolic_DCT.py new file mode 100644 index 0000000000..20156b95c7 --- /dev/null +++ b/pySDC/projects/Monodomain/problem_classes/space_discretizazions/Parabolic_DCT.py @@ -0,0 +1,340 @@ +import numpy as np +import scipy as sp +from pySDC.core.Common import RegisterParams +from pySDC.implementations.datatype_classes.mesh import mesh +from pathlib import Path +import os + + +class Parabolic_DCT(RegisterParams): + """ + A class for the spatial discretization of the parabolic part of the monodomain equation. + Here we discretize the spatial domain with a uniform mesh and use the discrete cosine transform (DCT) + to discretize the Laplacian operator. The DCT is a real-to-real type of Fourier transform that is well suited for + Neumann boundary conditions. + + Parameters: + ----------- + problem_params: dict containing the problem parameters + + Attributes: + ----------- + chi: float + Surface-to-volume ratio of the cell membrane + Cm: float + Membrane capacitance + si_l: float + Longitudinal intracellular conductivity + se_l: float + Longitudinal extracellular conductivity + si_t: float + Transversal intracellular conductivity + se_t: float + Transversal extracellular conductivity + sigma_l: float + Longitudinal conductivity + sigma_t: float + Transversal conductivity + diff_l: float + Longitudinal diffusion coefficient + diff_t: float + Transversal diffusion coefficient + diff: tuple of floats + Tuple containing the diffusion coefficients + dom_size: tuple of floats + Tuple containing the domain size + n_elems: tuple of ints + Tuple containing the number of elements in each direction + refinements: int + Number of refinements with respect to a baseline mesh. Can be both positive (to get finer meshes) and negative (to get coarser meshes). + grids: tuple of 1D arrays + Tuple containing the grid points in each direction + dx: tuple of floats + Tuple containing the grid spacings in each direction + shape: tuple of ints + Tuple containing the number of grid points in each direction. Same as n_elems with with reversed order. + n_dofs: int + Total number of degrees of freedom + dim: int + Dimension of the spatial domain. Can be 1, 2 or 3. + init: tuple + Shape of the mesh, None and data type of the mesh (np.double) + mesh_name: str + Name of the mesh. Can be cube_ND, cubdoid_ND, cubdoid_ND_smaller, cubdoid_ND_small, cubdoid_ND_large, cubdoid_ND_very_large. Where N=1,2,3. + diff_dct: array + Array containing the discrete Laplacian operator + output_folder: Path + Path to the output folder + output_file_path: Path + Path to the output file + output_file: str + Name of the output file + enable_output: bool + If True, the solution is written to file. Else not. + t_out: list + List containing the output times + order: int + Order of the spatial discretization. Can be 2 or 4 + zero_stim_vec: float + Used to apply zero stimili. + """ + + def __init__(self, **problem_params): + self._makeAttributeAndRegister(*problem_params.keys(), localVars=problem_params, readOnly=True) + + self.define_domain() + self.define_coefficients() + self.define_diffusion() + self.define_stimulus() + + def __del__(self): + if self.enable_output: + # Close the output file + self.output_file.close() + # Save the output times and the grid points + with open( + self.output_file_path.parent / Path(self.output_file_name + '_txyz').with_suffix(".npy"), 'wb' + ) as f: + np.save(f, np.array(self.t_out)) + xyz = self.grids + for i in range(self.dim): + np.save(f, xyz[i]) + + @property + def mesh_name(self): + return "ref_" + str(self.refinements) + + def define_coefficients(self): + self.chi = 140.0 # mm^-1 + self.Cm = 0.01 # uF/mm^2 + self.si_l = 0.17 # mS/mm + self.se_l = 0.62 # mS/mm + self.si_t = 0.019 # mS/mm + self.se_t = 0.24 # mS/mm + + if "cube" in self.domain_name: + # For this domain we use isotropic conductivities + self.si_t = self.si_l + self.se_t = self.se_l + + self.sigma_l = self.si_l * self.se_l / (self.si_l + self.se_l) + self.sigma_t = self.si_t * self.se_t / (self.si_t + self.se_t) + self.diff_l = self.sigma_l / self.chi / self.Cm + self.diff_t = self.sigma_t / self.chi / self.Cm + + if self.dim == 1: + self.diff = (self.diff_l,) + elif self.dim == 2: + self.diff = (self.diff_l, self.diff_t) + else: + self.diff = (self.diff_l, self.diff_t, self.diff_t) + + def define_domain(self): + if "cube" in self.domain_name: + self.dom_size = (100.0, 100.0, 100.0) + self.dim = int(self.domain_name[5]) + else: # cuboid + if "smaller" in self.domain_name: + self.dom_size = (10.0, 4.5, 2.0) + elif "small" in self.domain_name: + self.dom_size = (5.0, 3.0, 1.0) + elif "very_large" in self.domain_name: + self.dom_size = (280.0, 112.0, 48.0) + elif "large" in self.domain_name: + self.dom_size = (60.0, 21.0, 9.0) + else: + self.dom_size = (20.0, 7.0, 3.0) + self.dim = int(self.domain_name[7]) + + self.dom_size = self.dom_size[: self.dim] + self.n_elems = [int(2 ** np.round(np.log2(5.0 * L * 2**self.refinements))) for L in self.dom_size] + self.grids, self.dx = self.get_grids_dx(self.dom_size, self.n_elems) + + self.shape = tuple(np.flip([x.size for x in self.grids])) + self.n_dofs = int(np.prod(self.shape)) + self.init = ((self.n_dofs,), None, np.dtype('float64')) + + def define_diffusion(self): + N = self.n_elems + dx = self.dx + dim = len(N) + if self.order == 2: + diff_dct = self.diff[0] * (2.0 * np.cos(np.pi * np.arange(N[0]) / N[0]) - 2.0) / dx[0] ** 2 + if dim >= 2: + diff_dct = ( + diff_dct[None, :] + + self.diff[1] + * np.array((2.0 * np.cos(np.pi * np.arange(N[1]) / N[1]) - 2.0) / dx[1] ** 2)[:, None] + ) + if dim >= 3: + diff_dct = ( + diff_dct[None, :, :] + + self.diff[2] + * np.array((2.0 * np.cos(np.pi * np.arange(N[2]) / N[2]) - 2.0) / dx[2] ** 2)[:, None, None] + ) + elif self.order == 4: + diff_dct = ( + self.diff[0] + * ( + (-1.0 / 6.0) * np.cos(2.0 * np.pi * np.arange(N[0]) / N[0]) + + (8.0 / 3.0) * np.cos(np.pi * np.arange(N[0]) / N[0]) + - 2.5 + ) + / dx[0] ** 2 + ) + if dim >= 2: + diff_dct = ( + diff_dct[None, :] + + self.diff[1] + * np.array( + ( + (-1.0 / 6.0) * np.cos(2.0 * np.pi * np.arange(N[1]) / N[1]) + + (8.0 / 3.0) * np.cos(np.pi * np.arange(N[1]) / N[1]) + - 2.5 + ) + / dx[1] ** 2 + )[:, None] + ) + if dim >= 3: + diff_dct = ( + diff_dct[None, :, :] + + self.diff[2] + * np.array( + ( + (-1.0 / 6.0) * np.cos(2.0 * np.pi * np.arange(N[2]) / N[2]) + + (8.0 / 3.0) * np.cos(np.pi * np.arange(N[2]) / N[2]) + - 2.5 + ) + / dx[2] ** 2 + )[:, None, None] + ) + else: + raise NotImplementedError("Only order 2 and 4 are implemented for Parabolic_DCT.") + + self.diff_dct = diff_dct + + def grids_from_x(self, x): + dim = len(x) + if dim == 1: + return (x[0],) + elif dim == 2: + return (x[0][None, :], x[1][:, None]) + elif dim == 3: + return (x[0][None, None, :], x[1][None, :, None], x[2][:, None, None]) + + def get_grids_dx(self, dom_size, N): + # The grid points are the midpoints of the elements, hence x_{n+1/2}=(n+1/2)*dx + # This is needed for the DCT. + x = [np.linspace(0, dom_size[i], 2 * N[i] + 1) for i in range(len(N))] + x = [xi[1::2] for xi in x] + dx = [xi[1] - xi[0] for xi in x] + return self.grids_from_x(x), dx + + def define_stimulus(self): + self.zero_stim_vec = 0.0 + # all remaining stimulus parameters are set in MonodomainODE + + def solve_system(self, rhs, factor, u0, t, u_sol): + """ + Solve the linear system: u_sol = (I - factor * A)^{-1} rhs + + Arguments: + ---------- + rhs: mesh + The right-hand side of the linear system + factor: float + The factor in the linear system multiplying the Laplacian + u0: mesh + The initial guess for the solution. Not used here since we use a direct solver. + t: float + The current time. Not used here since the Laplacian is time-independent. + u_sol: mesh + The vector to store the solution in. + """ + rhs_hat = sp.fft.dctn(rhs.reshape(self.shape)) + u_sol_hat = rhs_hat / (1.0 - factor * self.diff_dct) + u_sol[:] = sp.fft.idctn(u_sol_hat).ravel() + + return u_sol + + def add_disc_laplacian(self, uh, res): + """ + Add the discrete Laplacian operator to res: res += A * uh + """ + res[:] += sp.fft.idctn(self.diff_dct * sp.fft.dctn(uh.reshape(self.shape))).ravel() + + def init_output(self, output_folder): + # Initialize the output parameters and file + self.output_folder = output_folder + self.output_file_path = self.output_folder / Path(self.output_file_name).with_suffix(".npy") + if self.enable_output: + if self.output_file_path.is_file(): + os.remove(self.output_file_path) + if not self.output_folder.is_dir(): + os.makedirs(self.output_folder) + self.output_file = open(self.output_file_path, 'wb') + self.t_out = [] + + def write_solution(self, uh, t): + # Write the solution to file. Meant for visualization, hence we print the time stamp too. + # Usually we only write the first component of the solution, hence the electric potential V and not the ionic model state variables. + if self.enable_output: + np.save(self.output_file, uh.reshape(self.shape)) + self.t_out.append(t) + + def write_reference_solution(self, uh, indeces): + """ + Write the solution to file. This is meant to print solutions that will be used as reference solutions + and compute errors or as well solutions that will be used as initial values for other simulations. + Here we write the model variables listed in indeces. + """ + if self.output_file_path.is_file(): + os.remove(self.output_file_path) + if not self.output_file_path.parent.is_dir(): + os.makedirs(self.output_file_path.parent) + with open(self.output_file_path, 'wb') as file: + [np.save(file, uh[i].reshape(self.shape)) for i in indeces] + + def read_reference_solution(self, uh, indeces, ref_file_name): + """ + Read a reference solution from file. It can be used to set the initial values of a simulation + or to compute errors. + We read the model variables listed in indeces. + """ + if ref_file_name == "": + return False + ref_sol_path = Path(self.output_folder) / Path(ref_file_name).with_suffix(".npy") + if ref_sol_path.is_file(): + with open(ref_sol_path, 'rb') as f: + for i in indeces: + uh[i][:] = np.load(f).ravel() + return True + else: + return False + + def stim_region(self, stim_center, stim_radius): + """ + Define the region where the stimulus is applied, given the center and the radius of the stimulus. + Returns a vector of the same size as the grid, with 1s inside the stimulus region and 0s outside. + """ + grids = self.grids + coord_inside_stim_box = [] + for i in range(len(grids)): + coord_inside_stim_box.append(abs(grids[i] - stim_center[i]) < stim_radius[i]) + + inside_stim_box = True + for i in range(len(grids)): + inside_stim_box = np.logical_and(inside_stim_box, coord_inside_stim_box[i]) + + stim = mesh(self.init) + stim[:] = inside_stim_box.ravel().astype(float) + + return stim + + def compute_errors(self, uh, ref_sol): + # Compute L2 errors with respect to the reference solution + error_L2 = np.linalg.norm(uh - ref_sol) + sol_norm_L2 = np.linalg.norm(ref_sol) + rel_error_L2 = error_L2 / sol_norm_L2 + + return error_L2, rel_error_L2 diff --git a/pySDC/projects/Monodomain/run_scripts/run_MonodomainODE.py b/pySDC/projects/Monodomain/run_scripts/run_MonodomainODE.py new file mode 100644 index 0000000000..c1aa47db27 --- /dev/null +++ b/pySDC/projects/Monodomain/run_scripts/run_MonodomainODE.py @@ -0,0 +1,418 @@ +from pathlib import Path +import numpy as np +from mpi4py import MPI +import logging +import os + +from pySDC.core.Errors import ParameterError + +from pySDC.projects.Monodomain.problem_classes.MonodomainODE import MultiscaleMonodomainODE +from pySDC.projects.Monodomain.hooks.HookClass_pde import pde_hook +from pySDC.projects.Monodomain.hooks.HookClass_post_iter_info import post_iter_info_hook + +from pySDC.helpers.stats_helper import get_sorted + +from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI +from pySDC.implementations.controller_classes.controller_MPI import controller_MPI + +from pySDC.projects.Monodomain.sweeper_classes.exponential_runge_kutta.imexexp_1st_order import ( + imexexp_1st_order as imexexp_1st_order_ExpRK, +) +from pySDC.projects.Monodomain.sweeper_classes.runge_kutta.imexexp_1st_order import imexexp_1st_order + +from pySDC.projects.Monodomain.transfer_classes.TransferVectorOfDCTVectors import TransferVectorOfDCTVectors + +from pySDC.projects.Monodomain.utils.data_management import database + + +def set_logger(controller_params): + logging.basicConfig(level=controller_params["logger_level"]) + hooks_logger = logging.getLogger("hooks") + hooks_logger.setLevel(controller_params["logger_level"]) + + +def get_controller(controller_params, description, time_comm, n_time_ranks, truly_time_parallel): + if truly_time_parallel: + controller = controller_MPI(controller_params=controller_params, description=description, comm=time_comm) + else: + controller = controller_nonMPI( + num_procs=n_time_ranks, controller_params=controller_params, description=description + ) + return controller + + +def print_dofs_stats(time_rank, controller, P, uinit): + tot_dofs = uinit.size + mesh_dofs = uinit.shape[1] + if time_rank == 0: + controller.logger.info(f"Total dofs: {tot_dofs}, mesh dofs = {mesh_dofs}") + + +def get_P_data(controller, truly_time_parallel): + if truly_time_parallel: + P = controller.S.levels[0].prob + else: + P = controller.MS[0].levels[0].prob + # set time parameters + t0 = P.t0 + Tend = P.Tend + uinit = P.initial_value() + return t0, Tend, uinit, P + + +def get_comms(n_time_ranks, truly_time_parallel): + if truly_time_parallel: + time_comm = MPI.COMM_WORLD + time_rank = time_comm.Get_rank() + assert time_comm.Get_size() == n_time_ranks, "Number of time ranks does not match the number of MPI ranks" + else: + time_comm = None + time_rank = 0 + return time_comm, time_rank + + +def get_base_transfer_params(finter): + base_transfer_params = dict() + base_transfer_params["finter"] = finter + return base_transfer_params + + +def get_controller_params(problem_params, n_time_ranks): + controller_params = dict() + controller_params["predict_type"] = "pfasst_burnin" if n_time_ranks > 1 else None + controller_params["log_to_file"] = False + controller_params["fname"] = problem_params["output_root"] + "controller" + controller_params["logger_level"] = 20 + controller_params["dump_setup"] = False + if n_time_ranks == 1: + controller_params["hook_class"] = [post_iter_info_hook, pde_hook] + else: + controller_params["hook_class"] = [post_iter_info_hook] + return controller_params + + +def get_description( + integrator, problem_params, sweeper_params, level_params, step_params, base_transfer_params, space_transfer_class +): + description = dict() + + problem = MultiscaleMonodomainODE + + if integrator == "IMEXEXP": + # implicit-explicit-exponential integrators in the preconditioner and standard SDC + description["sweeper_class"] = imexexp_1st_order + elif integrator == "IMEXEXP_EXPRK": + # implicit-explicit-exponential integrators in the preconditioner and exponential SDC + description["sweeper_class"] = imexexp_1st_order_ExpRK + else: + raise ParameterError("Unknown integrator.") + + description["problem_class"] = problem + description["problem_params"] = problem_params + description["sweeper_params"] = sweeper_params + description["level_params"] = level_params + description["step_params"] = step_params + description["base_transfer_params"] = base_transfer_params + description["space_transfer_class"] = space_transfer_class + + return description + + +def get_step_params(maxiter): + step_params = dict() + step_params["maxiter"] = maxiter + return step_params + + +def get_level_params(dt, nsweeps, restol, n_time_ranks): + # initialize level parameters + level_params = dict() + level_params["restol"] = restol + level_params["dt"] = dt + level_params["nsweeps"] = nsweeps + level_params["residual_type"] = "full_rel" + level_params["parallel"] = n_time_ranks > 1 + + return level_params + + +def get_sweeper_params(num_nodes, skip_residual_computation): + # initialize sweeper parameters + sweeper_params = dict() + sweeper_params["initial_guess"] = "spread" + sweeper_params["quad_type"] = "RADAU-RIGHT" + sweeper_params["num_nodes"] = num_nodes + sweeper_params["QI"] = "IE" + if skip_residual_computation: + sweeper_params["skip_residual_computation"] = ("IT_FINE", "IT_COARSE", "IT_DOWN", "IT_UP") + + return sweeper_params + + +def get_space_tranfer_params(): + + space_transfer_class = TransferVectorOfDCTVectors + + return space_transfer_class + + +def get_problem_params( + domain_name, + refinements, + ionic_model_name, + read_init_val, + init_time, + enable_output, + end_time, + order, + output_root, + output_file_name, + ref_sol, +): + # initialize problem parameters + problem_params = dict() + problem_params["order"] = order # order of the spatial discretization + problem_params["refinements"] = refinements # number of refinements with respect to a baseline + problem_params["domain_name"] = ( + domain_name # name of the domain: cube_1D, cube_2D, cube_3D, cuboid_1D, cuboid_2D, cuboid_3D, cuboid_1D_small, cuboid_2D_small, cuboid_3D_small + ) + problem_params["ionic_model_name"] = ( + ionic_model_name # name of the ionic model: HH, CRN, TTP, TTP_SMOOTH for Hodgkin-Huxley, Courtemanche-Ramirez-Nattel, Ten Tusscher-Panfilov and a smoothed version of Ten Tusscher-Panfilov + ) + problem_params["read_init_val"] = ( + read_init_val # read the initial value from file (True) or initiate an action potential with a stimulus (False) + ) + problem_params["init_time"] = ( + init_time # stimulus happpens at t=0 and t=1000 and lasts 2ms. If init_time>2 nothing happens up to t=1000. If init_time>1002 nothing happens, never. + ) + problem_params["init_val_name"] = "init_val_DCT" # name of the file containing the initial value + problem_params["enable_output"] = ( + enable_output # activate or deactivate output (that can be visualized with visualization/show_monodomain_sol.py) + ) + problem_params["output_V_only"] = ( + True # output only the transmembrane potential (V) and not the ionic model variables + ) + executed_file_dir = os.path.dirname(os.path.realpath(__file__)) + problem_params["output_root"] = ( + executed_file_dir + "/../../../../data/" + output_root + ) # output root folder. A hierarchy of folders is created in this folder, as root/domain_name/ref_+str(refinements)/ionic_model_name. Initial values are put here + problem_params["output_file_name"] = output_file_name + problem_params["ref_sol"] = ref_sol # reference solution file name + problem_params["end_time"] = end_time + Path(problem_params["output_root"]).mkdir(parents=True, exist_ok=True) + + return problem_params + + +def setup_and_run( + integrator, + num_nodes, + skip_residual_computation, + num_sweeps, + max_iter, + dt, + restol, + domain_name, + refinements, + order, + ionic_model_name, + read_init_val, + init_time, + enable_output, + write_as_reference_solution, + write_all_variables, + output_root, + output_file_name, + ref_sol, + end_time, + truly_time_parallel, + n_time_ranks, + finter, + write_database, +): + + # get time communicator + time_comm, time_rank = get_comms(n_time_ranks, truly_time_parallel) + + # get time integration parameters + # set maximum number of iterations in ESDC/MLESDC/PFASST + step_params = get_step_params(maxiter=max_iter) + # set number of collocation nodes in each level + sweeper_params = get_sweeper_params(num_nodes=num_nodes, skip_residual_computation=skip_residual_computation) + # set step size, number of sweeps per iteration, and residual tolerance for the stopping criterion + level_params = get_level_params( + dt=dt, + nsweeps=num_sweeps, + restol=restol, + n_time_ranks=n_time_ranks, + ) + + # fix enable output to that only finest level has output + n_levels = max(len(refinements), len(num_nodes)) + enable_output = [enable_output] + [False] * (n_levels - 1) + # get problem parameters + problem_params = get_problem_params( + domain_name=domain_name, + refinements=refinements, + ionic_model_name=ionic_model_name, + read_init_val=read_init_val, + init_time=init_time, + enable_output=enable_output, + end_time=end_time, + order=order, + output_root=output_root, + output_file_name=output_file_name, + ref_sol=ref_sol, + ) + + space_transfer_class = get_space_tranfer_params() + + # get remaining prams + base_transfer_params = get_base_transfer_params(finter) + controller_params = get_controller_params(problem_params, n_time_ranks) + description = get_description( + integrator, + problem_params, + sweeper_params, + level_params, + step_params, + base_transfer_params, + space_transfer_class, + ) + set_logger(controller_params) + controller = get_controller(controller_params, description, time_comm, n_time_ranks, truly_time_parallel) + + # get PDE data + t0, Tend, uinit, P = get_P_data(controller, truly_time_parallel) + + # print dofs stats + print_dofs_stats(time_rank, controller, P, uinit) + + # run + uend, stats = controller.run(u0=uinit, t0=t0, Tend=Tend) + + # write reference solution, to be used later for error computation + if write_as_reference_solution: + P.write_reference_solution(uend, write_all_variables) + + # compute errors, if a reference solution is found + error_availabe, error_L2, rel_error_L2 = P.compute_errors(uend) + + # get some stats + iter_counts = get_sorted(stats, type="niter", sortby="time") + residuals = get_sorted(stats, type="residual_post_iteration", sortby="time") + if time_comm is not None: + iter_counts = time_comm.gather(iter_counts, root=0) + residuals = time_comm.gather(residuals, root=0) + if time_rank == 0: + iter_counts = [item for sublist in iter_counts for item in sublist] + residuals = [item for sublist in residuals for item in sublist] + iter_counts = time_comm.bcast(iter_counts, root=0) + residuals = time_comm.bcast(residuals, root=0) + + iter_counts.sort() + times = [item[0] for item in iter_counts] + niters = [item[1] for item in iter_counts] + + residuals.sort() + residuals_new = [residuals[0][1]] + t = residuals[0][0] + for i in range(1, len(residuals)): + if residuals[i][0] > t + dt / 2.0: + residuals_new.append(residuals[i][1]) + t = residuals[i][0] + residuals = residuals_new + + avg_niters = np.mean(niters) + if time_rank == 0: + controller.logger.info("Mean number of iterations: %4.2f" % avg_niters) + controller.logger.info( + "Std and var for number of iterations: %4.2f -- %4.2f" % (float(np.std(niters)), float(np.var(niters))) + ) + + if write_database and time_rank == 0: + errors = dict() + errors["error_L2"] = error_L2 + errors["rel_error_L2"] = rel_error_L2 + iters_info = dict() + iters_info["avg_niters"] = avg_niters + iters_info["times"] = times + iters_info["niters"] = niters + iters_info["residuals"] = residuals + file_name = P.output_folder / Path(P.output_file_name) + if file_name.with_suffix('.db').is_file(): + os.remove(file_name.with_suffix('.db')) + data_man = database(file_name) + data_man.write_dictionary("errors", errors) + data_man.write_dictionary("iters_info", iters_info) + + return error_L2, rel_error_L2, avg_niters, times, niters, residuals + + +def main(): + # define sweeper parameters + # integrator = "IMEXEXP" + integrator = "IMEXEXP_EXPRK" + num_nodes = [4] + num_sweeps = [1] + + # set step parameters + max_iter = 100 + + # set level parameters + dt = 0.05 + restol = 5e-8 + + # set problem parameters + domain_name = "cube_2D" + refinements = [-1] + order = 4 # 2 or 4 + ionic_model_name = "TTP" + read_init_val = True + init_time = 3.0 + enable_output = False + write_as_reference_solution = False + write_all_variables = False + write_database = False + end_time = 0.05 + output_root = "results_tmp" + output_file_name = "ref_sol" if write_as_reference_solution else "monodomain" + ref_sol = "ref_sol" + skip_residual_computation = False + + finter = False + + # set time parallelism to True or emulated (False) + truly_time_parallel = False + n_time_ranks = 1 + + error_L2, rel_error_L2, avg_niters, times, niters, residuals = setup_and_run( + integrator, + num_nodes, + skip_residual_computation, + num_sweeps, + max_iter, + dt, + restol, + domain_name, + refinements, + order, + ionic_model_name, + read_init_val, + init_time, + enable_output, + write_as_reference_solution, + write_all_variables, + output_root, + output_file_name, + ref_sol, + end_time, + truly_time_parallel, + n_time_ranks, + finter, + write_database, + ) + + +if __name__ == "__main__": + main() diff --git a/pySDC/projects/Monodomain/run_scripts/run_MonodomainODE_cli.py b/pySDC/projects/Monodomain/run_scripts/run_MonodomainODE_cli.py new file mode 100644 index 0000000000..9fca058c78 --- /dev/null +++ b/pySDC/projects/Monodomain/run_scripts/run_MonodomainODE_cli.py @@ -0,0 +1,135 @@ +import argparse +from pySDC.projects.Monodomain.run_scripts.run_MonodomainODE import setup_and_run + + +def list_of_ints(arg): + arg = arg.replace(' ', '') + arg = arg.replace('_', '-') + arg = arg.split(',') + return list(map(int, arg)) + + +# This is to run the MonodomainODE example from the command line +# Pretty much all the parameters can be defined from the command line + +# For the refinements, it is possible to set negative values, which yield a mesh coarser than the baseline. +# To do so in the command line use an underscore _ insteaf of a minus sign -. +# For example, to solve a 3 level example with meshes refinements 1, 0 and -1, use the option --refinements 1,0,_1 + + +def main(): + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + # define sweeper parameters + parser.add_argument("--integrator", default="IMEXEXP_EXPRK", type=str, help="sweeper name") + parser.add_argument( + "--num_nodes", + default="4", + type=list_of_ints, + help="list of ints (as '5,3', i.e. no brackets): number of collocation nodes per level", + ) + parser.add_argument("--num_sweeps", default="1", type=list_of_ints, help="list of ints: number of sweeps per level") + parser.add_argument( + "--skip_res", + default=False, + action=argparse.BooleanOptionalAction, + help="compute residual only when really needed", + ) + # set step parameters + parser.add_argument("--max_iter", default=100, type=int, help="maximal number of iterations") + # set level parameters + parser.add_argument("--dt", default=0.05, type=float, help="step size") + parser.add_argument("--restol", default=5e-8, type=float, help="residual tolerance") + # problem args + parser.add_argument( + "--domain_name", default="cuboid_2D_small", type=str, help="cuboid_2D, cuboid_3D, truncated_ellipsoid,..." + ) + parser.add_argument( + "--refinements", + default="0", + type=list_of_ints, + help="list of ints: number of refinements per level, with respect to a baseline mesh (negative values yield coarser meshes). For negative values use _ instead of -.", + ) + parser.add_argument( + "--order", default="4", type=list_of_ints, help="list of ints: order of FEM or FD discretization" + ) + parser.add_argument("--ionic_model_name", default="TTP", type=str, help="ionic_model: HH, CRN, TTP") + parser.add_argument( + "--read_init_val", default=False, action=argparse.BooleanOptionalAction, help="read the initial value from file" + ) + parser.add_argument("--init_time", default=0.0, type=float, help="duration of stimulus. -1 means default") + parser.add_argument( + "--enable_output", + default=False, + action=argparse.BooleanOptionalAction, + help="activate or deactivate xdmf output: True or False", + ) + parser.add_argument( + "--write_as_reference_solution", + default=False, + action=argparse.BooleanOptionalAction, + help="write as reference solution: True or False", + ) + parser.add_argument( + "--write_all_variables", + default=False, + action=argparse.BooleanOptionalAction, + help="when write_as_reference_solution=True, write write all variables (True) or only potential V (False)", + ) + parser.add_argument("--end_time", default=1.0, type=float, help="end time. If negative, a default one is used") + parser.add_argument("--output_file_name", default="monodomain", type=str, help="output file name") + parser.add_argument("--ref_sol", default="ref_sol", type=str, help="reference solution file name") + parser.add_argument("--output_root", default="results_tmp/", type=str, help="output root folder") + parser.add_argument( + "--finter", + default=False, + action=argparse.BooleanOptionalAction, + help="in prolong, re-evaluate f (false) or interpolate (true)", + ) + # controller args + parser.add_argument( + "--truly_time_parallel", + default=False, + action=argparse.BooleanOptionalAction, + help="truly time parallel or emulated", + ) + parser.add_argument("--n_time_ranks", default=1, type=int, help="number of time ranks") + + parser.add_argument( + "--write_database", + default=True, + action=argparse.BooleanOptionalAction, + help="save some simulation results in a database", + ) + + args = parser.parse_args() + + error_L2, rel_error_L2, avg_niters, times, niters, residuals = setup_and_run( + args.integrator, + args.num_nodes, + args.skip_res, + args.num_sweeps, + args.max_iter, + args.dt, + args.restol, + args.domain_name, + args.refinements, + args.order, + args.ionic_model_name, + args.read_init_val, + args.init_time, + args.enable_output, + args.write_as_reference_solution, + args.write_all_variables, + args.output_root, + args.output_file_name, + args.ref_sol, + args.end_time, + args.truly_time_parallel, + args.n_time_ranks, + args.finter, + args.write_database, + ) + + +if __name__ == "__main__": + main() diff --git a/pySDC/projects/Monodomain/run_scripts/run_TestODE.py b/pySDC/projects/Monodomain/run_scripts/run_TestODE.py new file mode 100644 index 0000000000..6b27b1aec2 --- /dev/null +++ b/pySDC/projects/Monodomain/run_scripts/run_TestODE.py @@ -0,0 +1,301 @@ +from pathlib import Path +import numpy as np +import logging +import os + +from tqdm import tqdm + +from pySDC.core.Errors import ParameterError + +from pySDC.projects.Monodomain.problem_classes.TestODE import MultiscaleTestODE +from pySDC.projects.Monodomain.transfer_classes.TransferVectorOfDCTVectors import TransferVectorOfDCTVectors + +from pySDC.projects.Monodomain.hooks.HookClass_post_iter_info import post_iter_info_hook + +from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI + +from pySDC.projects.Monodomain.sweeper_classes.exponential_runge_kutta.imexexp_1st_order import ( + imexexp_1st_order as imexexp_1st_order_ExpRK, +) +from pySDC.projects.Monodomain.sweeper_classes.runge_kutta.imexexp_1st_order import imexexp_1st_order + +""" +Run the multirate Dahlquist test equation and plot the stability domain of the method. +We vary only the exponential term and the stiff term, while the non stiff term is kept constant (to allow 2D plots). +""" + + +def set_logger(controller_params): + logging.basicConfig(level=controller_params["logger_level"]) + hooks_logger = logging.getLogger("hooks") + hooks_logger.setLevel(controller_params["logger_level"]) + + +def get_controller(controller_params, description, n_time_ranks): + controller = controller_nonMPI(num_procs=n_time_ranks, controller_params=controller_params, description=description) + return controller + + +def get_P_data(controller): + P = controller.MS[0].levels[0].prob + # set time parameters + t0 = P.t0 + Tend = P.Tend + uinit = P.initial_value() + return t0, Tend, uinit, P + + +def get_base_transfer_params(): + base_transfer_params = dict() + base_transfer_params["finter"] = False + return base_transfer_params + + +def get_controller_params(output_root, logger_level): + controller_params = dict() + controller_params["predict_type"] = "pfasst_burnin" + controller_params["log_to_file"] = False + controller_params["fname"] = output_root + "controller" + controller_params["logger_level"] = logger_level + controller_params["dump_setup"] = False + controller_params["hook_class"] = [post_iter_info_hook] + return controller_params + + +def get_description( + integrator, problem_params, sweeper_params, level_params, step_params, base_transfer_params, space_transfer_class +): + description = dict() + + problem = MultiscaleTestODE + + if integrator == "IMEXEXP": + description["sweeper_class"] = imexexp_1st_order + elif integrator == "IMEXEXP_EXPRK": + description["sweeper_class"] = imexexp_1st_order_ExpRK + else: + raise ParameterError("Unknown integrator.") + + description["problem_class"] = problem + description["problem_params"] = problem_params + description["sweeper_params"] = sweeper_params + description["level_params"] = level_params + description["step_params"] = step_params + description["base_transfer_params"] = base_transfer_params + description["space_transfer_class"] = space_transfer_class + return description + + +def get_step_params(maxiter): + step_params = dict() + step_params["maxiter"] = maxiter + return step_params + + +def get_level_params(dt, nsweeps, restol): + # initialize level parameters + level_params = dict() + level_params["restol"] = restol + level_params["dt"] = dt + level_params["nsweeps"] = nsweeps + level_params["residual_type"] = "full_rel" + return level_params + + +def get_sweeper_params(num_nodes): + # initialize sweeper parameters + sweeper_params = dict() + sweeper_params["initial_guess"] = "spread" + sweeper_params["quad_type"] = "RADAU-RIGHT" + sweeper_params["num_nodes"] = num_nodes + sweeper_params["QI"] = "IE" + + return sweeper_params + + +def get_output_root(): + executed_file_dir = os.path.dirname(os.path.realpath(__file__)) + output_root = executed_file_dir + "/../../../../data/Monodomain/results_tmp" + return output_root + + +def get_problem_params(lmbda_laplacian, lmbda_gating, lmbda_others, end_time): + # initialize problem parameters + problem_params = dict() + problem_params["output_file_name"] = "monodomain" + problem_params["output_root"] = get_output_root() + problem_params["end_time"] = end_time + problem_params["lmbda_laplacian"] = lmbda_laplacian + problem_params["lmbda_gating"] = lmbda_gating + problem_params["lmbda_others"] = lmbda_others + Path(problem_params["output_root"]).mkdir(parents=True, exist_ok=True) + return problem_params + + +def plot_stability_domain(lmbda_laplacian_list, lmbda_gating_list, R, integrator, num_nodes, n_time_ranks): + import matplotlib.pyplot as plt + from matplotlib.colors import LogNorm + import pySDC.helpers.plot_helper as plt_helper + + plt_helper.setup_mpl() + + # fig, ax = plt_helper.newfig(textwidth=400, scale=0.89, ratio=0.5) + # fig, ax = plt_helper.newfig(textwidth=238.96, scale=0.89) + fig, ax = plt_helper.plt.subplots( + figsize=plt_helper.figsize(textwidth=400, scale=1.0, ratio=0.78), layout='constrained' + ) + + fs_label = 14 + fs_ticks = 12 + fs_title = 16 + X, Y = np.meshgrid(lmbda_gating_list, lmbda_laplacian_list) + R = np.abs(R) + CS = ax.contourf(X, Y, R, cmap=plt.cm.viridis, levels=np.logspace(-6, 0, 13), norm=LogNorm()) + ax.plot(lmbda_gating_list, 0 * lmbda_gating_list, 'k--', linewidth=1.0) + ax.plot(0 * lmbda_laplacian_list, lmbda_laplacian_list, 'k--', linewidth=1.0) + ax.contour(CS, levels=CS.levels, colors='black') + ax.set_xlabel(r'$z_{e}$', fontsize=fs_label, labelpad=-5) + ax.set_ylabel(r'$z_{I}$', fontsize=fs_label, labelpad=-10) + ax.tick_params(axis='x', labelsize=fs_ticks) + ax.tick_params(axis='y', labelsize=fs_ticks) + if len(num_nodes) == 1 and n_time_ranks == 1: + prefix = "" + elif len(num_nodes) > 1 and n_time_ranks == 1: + prefix = "ML" + elif len(num_nodes) > 1 and n_time_ranks > 1: + prefix = "PFASST " + if integrator == "IMEXEXP": + ax.set_title(prefix + "SDC stability domain", fontsize=fs_title) + elif integrator == "IMEXEXP_EXPRK": + ax.set_title(prefix + "ESDC stability domain", fontsize=fs_title) + ax.yaxis.tick_right() + ax.yaxis.set_label_position("right") + cbar = fig.colorbar(CS) + cbar.ax.set_ylabel(r'$|R(z_e,z_{I})|$', fontsize=fs_label, labelpad=-20) + cbar.set_ticks([cbar.vmin, cbar.vmax]) # keep only the ticks at the ends + cbar.ax.tick_params(labelsize=fs_ticks) + # plt_helper.plt.show() + plt_helper.savefig("data/stability_domain_" + integrator, save_pdf=False, save_pgf=False, save_png=True) + + +def main(integrator, dl, l_min, openmp, n_time_ranks, end_time, num_nodes, check_stability): + + # get time integration parameters + # set maximum number of iterations in SDC/ESDC/MLSDC/etc + step_params = get_step_params(maxiter=5) + # set number of collocation nodes in each level + sweeper_params = get_sweeper_params(num_nodes=num_nodes) + # set step size, number of sweeps per iteration, and residual tolerance for the stopping criterion + level_params = get_level_params(dt=1.0, nsweeps=[1], restol=5e-8) + # set space transfer parameters + # space_transfer_class = Transfer_myfloat + space_transfer_class = TransferVectorOfDCTVectors + base_transfer_params = get_base_transfer_params() + controller_params = get_controller_params(get_output_root(), logger_level=40) + + # set stability test parameters + lmbda_others = -1.0 # the non stiff term + lmbda_laplacian_min = l_min # the stiff term + lmbda_laplacian_max = 0.0 + lmbda_gating_min = l_min # the exponential term + lmbda_gating_max = 0.0 + + # define the grid for the stability domain + n_lmbda_laplacian = np.round((lmbda_laplacian_max - lmbda_laplacian_min) / dl).astype(int) + 1 + n_lmbda_gating = np.round((lmbda_gating_max - lmbda_gating_min) / dl).astype(int) + 1 + lmbda_laplacian_list = np.linspace(lmbda_laplacian_min, lmbda_laplacian_max, n_lmbda_laplacian) + lmbda_gating_list = np.linspace(lmbda_gating_min, lmbda_gating_max, n_lmbda_gating) + + if not openmp: + R = np.zeros((n_lmbda_laplacian, n_lmbda_gating)) + for i in tqdm(range(n_lmbda_gating)): + for j in range(n_lmbda_laplacian): + lmbda_gating = lmbda_gating_list[i] + lmbda_laplacian = lmbda_laplacian_list[j] + + problem_params = get_problem_params( + lmbda_laplacian=lmbda_laplacian, + lmbda_gating=lmbda_gating, + lmbda_others=lmbda_others, + end_time=end_time, + ) + description = get_description( + integrator, + problem_params, + sweeper_params, + level_params, + step_params, + base_transfer_params, + space_transfer_class, + ) + set_logger(controller_params) + controller = get_controller(controller_params, description, n_time_ranks) + + t0, Tend, uinit, P = get_P_data(controller) + uend, stats = controller.run(u0=uinit, t0=t0, Tend=Tend) + + R[j, i] = abs(uend) + else: + import pymp + + R = pymp.shared.array((n_lmbda_laplacian, n_lmbda_gating), dtype=float) + with pymp.Parallel(12) as p: + for i in tqdm(p.range(0, n_lmbda_gating)): + for j in range(n_lmbda_laplacian): + lmbda_gating = lmbda_gating_list[i] + lmbda_laplacian = lmbda_laplacian_list[j] + + problem_params = get_problem_params( + lmbda_laplacian=lmbda_laplacian, + lmbda_gating=lmbda_gating, + lmbda_others=lmbda_others, + end_time=end_time, + ) + description = get_description( + integrator, + problem_params, + sweeper_params, + level_params, + step_params, + base_transfer_params, + space_transfer_class, + ) + set_logger(controller_params) + controller = get_controller(controller_params, description, n_time_ranks) + + t0, Tend, uinit, P = get_P_data(controller) + uend, stats = controller.run(u0=uinit, t0=t0, Tend=Tend) + + R[j, i] = abs(uend) + + plot_stability_domain(lmbda_laplacian_list, lmbda_gating_list, R, integrator, num_nodes, n_time_ranks) + + if check_stability: + assert ( + np.max(np.abs(R.ravel())) <= 1.0 + ), "The maximum absolute value of the stability function is greater than 1.0." + + +if __name__ == "__main__": + # Plot stability for exponential SDC coupled with the implicit-explicit-exponential integrator as preconditioner + main( + integrator="IMEXEXP_EXPRK", + dl=2, + l_min=-100, + openmp=True, + n_time_ranks=1, + end_time=1.0, + num_nodes=[5, 3], + check_stability=True, # check that the stability function is bounded by 1.0 + ) + # Plot stability for standard SDC coupled with the implicit-explicit-exponential integrator as preconditioner + main( + integrator="IMEXEXP", + dl=2, + l_min=-100, + openmp=True, + n_time_ranks=1, + end_time=1.0, + num_nodes=[5, 3], + check_stability=False, # do not check for stability since we already know that the method is not stable + ) diff --git a/pySDC/projects/Monodomain/sweeper_classes/exponential_runge_kutta/imexexp_1st_order.py b/pySDC/projects/Monodomain/sweeper_classes/exponential_runge_kutta/imexexp_1st_order.py new file mode 100644 index 0000000000..25c0813c14 --- /dev/null +++ b/pySDC/projects/Monodomain/sweeper_classes/exponential_runge_kutta/imexexp_1st_order.py @@ -0,0 +1,301 @@ +import numpy as np + +from pySDC.core.Sweeper import sweeper +from pySDC.core.Errors import CollocationError, ParameterError +from pySDC.core.Collocation import CollBase +import numdifftools.fornberg as fornberg +import scipy + + +class imexexp_1st_order(sweeper): + """ + Custom sweeper class, implements Sweeper.py + + First-order IMEXEXP sweeper using implicit/explicit/exponential Euler as base integrator + In the cardiac electrphysiology community this is known as Rush-Larsen scheme. + + The underlying intergrator is exponential Runge-Kutta, leading to exponential SDC (ESDC). + """ + + def __init__(self, params): + """ + Initialization routine for the custom sweeper + + Args: + params: parameters for the sweeper + """ + + if "QI" not in params: + params["QI"] = "IE" + + # call parent's initialization routine + super(imexexp_1st_order, self).__init__(params) + + # IMEX integration matrices + self.QI = self.get_Qdelta_implicit(coll=self.coll, qd_type=self.params.QI) + self.delta = np.diagonal(self.QI)[1:] + + # Compute weights w such that PiQ^(k)(0) = sum_{j=0}^{M-1} w[k,j]*Q[j], k=0,...,M-1 + # Used to express the derivatives of a polynomial in x=0 in terms of the values of the polynomial at the collocation nodes + M = self.coll.num_nodes + c = self.coll.nodes + self.w = fornberg.fd_weights_all(c, 0.0, M - 1).transpose() + + # Define the quadature rule for the evaluation of the phi_i(z) functions. Indeed, we evaluate them as integrals in order to avoid round off errors. + phi_num_nodes = 5 # seems to be enough in most cases + self.phi_coll = CollBase(num_nodes=phi_num_nodes, tleft=0, tright=1, node_type='LEGENDRE', quad_type='GAUSS') + + def phi_eval(self, factors, indeces, phi, lmbda): + """ + Evaluate the phi_k functions at the points factors[i]*lmbda, for all k in indeces + + Arguments: + factors: list of factors to multiply lmbda with. + indeces: list of indeces k for the phi_k functions. Since we use the integral formulation, k=0 is not allowed (not needed neither). + phi: an instance of mesh with shape (len(factors),len(indeces),*lmbda.shape) (i.e., some space to store the results) + it will filled as: phi[i,k][:] = phi_{indeces[k]}(factor[i]*lmbda[:]) + lmbda: dtype_u: the value of lmbda + """ + + assert 0 not in indeces, "phi_0 is not implemented, since the integral definition is not valid for k=0." + + # the quadrature rule used to evaluate the phi functions as integrals. This is not the same as the one used in the ESDC method!!!! + c = self.phi_coll.nodes + b = self.phi_coll.weights + + k = np.array(indeces) + km1_fac = scipy.special.factorial(k - 1) # (k-1)! + + # Here we use the quadrature rule to approximate the integral + # phi_{k}(factor[i]*lmbda[:,:])= \int_0^1 e^{(1-s)*factor[i]*lambda[:,:]}*s^{k-1}/(k-1)! ds + + # First, compute e^((1-c[j])*factor[i]*lmbda[:]) for nodes c[j] on the quadrature rule and all factors[i] + exp_terms = np.exp(((1.0 - c[None, :, None, None]) * factors[:, None, None, None]) * lmbda[None, None, :, :]) + # Then, compute the terms c[j]^{k-1}/(k-1)! for all nodes c[j] and all k and multiply with the weights b[j] + wgt_tmp = (b[:, None] * c[:, None] ** (k[None, :] - 1)) / km1_fac[None, :] + # Finally, compute the integral by summing over the quadrature nodes + phi[:] = np.sum(wgt_tmp[None, :, :, None, None] * exp_terms[:, :, None, :, :], axis=1) + + def compute_lambda_phi_Qmat_exp(self): + + if not hasattr(self, "u_old"): + # make some space for the old value of u[0] + self.u_old = self.level.prob.dtype_u(init=self.level.prob.init, val=0.0) + + # everything that is computed in this if statement depends on u[0] only + # To save computations we recompute that only if u[0] has changed. + # Also, we check only for the first component u[0][0] of u[0] to save more computations. + # Remember that u[0][0] is a vector representing the electric potential on the whole mesh and is enough to check if the whole u[0] has changed. + if not np.allclose(self.u_old[0], self.level.u[0][0], rtol=1e-10, atol=1e-10): + + self.u_old[:] = self.level.u[0] + + L = self.level + P = L.prob + M = self.coll.num_nodes + c = self.coll.nodes + + # compute lambda(u) of the exponential term f_exp(u)=lmbda(u)*(u-y_inf(u)) + # and select only the indeces with exponential terms (others are zeros) + self.lmbda = P.lmbda_eval(L.u[0], L.time)[P.rhs_exp_indeces] + + if not hasattr(self, "phi"): + # make some space + self.phi = P.dtype_u(init=P.init_exp_extruded((M, M)), val=0.0) + self.phi_one = P.dtype_u(init=P.init_exp_extruded((M, 1)), val=0.0) + + # evaluate the phi_k(dt*c_i*lambda) functions at the collocation nodes c_i for k=1,...,M + self.phi_eval(L.dt * c, list(range(1, M + 1)), self.phi, self.lmbda) + # evaluates phi_1(dt*delta_i*lambda) for delta_i = c_i - c_{i-1} + self.phi_eval(L.dt * self.delta, [1], self.phi_one, self.lmbda) + + # compute weight for the integration of \int_0^ci exp(dt*(ci-r)lmbda)*PiQ(r)dr, + # where PiQ(r) is a polynomial interpolating some nodal values Q(c_i)=Q[i]. + # The integral of PiQ will be approximated as: + # \int_0^ci exp(dt*(ci-r)lmbda)*PiQ(r)dr ~= \sum_{j=0}^{M-1} Qmat_exp[i,j]*Q[j] + + k = np.arange(0, M) + wgt_tmp = self.w[None, :, :] * c[:, None, None] ** (k[None, None, :] + 1) + self.Qmat_exp = np.sum(wgt_tmp[:, :, :, None, None] * self.phi[:, None, :, :, :], axis=2) + + def integrate(self): + """ + Integrates the right-hand side (here impl + expl + exp) using exponential Runge-Kutta + + Returns: + list of dtype_u: containing the integral as values + """ + + # get current level and problem description + L = self.level + P = L.prob + M = self.coll.num_nodes + + self.compute_lambda_phi_Qmat_exp() + + if not hasattr(self, "Q"): + self.Q = P.dtype_u(init=P.init_exp_extruded((M,)), val=0.0) + + for k in range(M): + self.Q[k][:] = L.f[k + 1].exp[P.rhs_exp_indeces] + self.lmbda * ( + L.u[0][P.rhs_exp_indeces] - L.u[k + 1][P.rhs_exp_indeces] + ) + + # integrate RHS over all collocation nodes + me = [P.dtype_u(init=P.init, val=0.0) for _ in range(M)] + for m in range(1, M + 1): + for j in range(1, M + 1): + me[m - 1][P.rhs_stiff_indeces] += self.coll.Qmat[m, j] * L.f[j].impl[P.rhs_stiff_indeces] + me[m - 1][P.rhs_nonstiff_indeces] += self.coll.Qmat[m, j] * L.f[j].expl[P.rhs_nonstiff_indeces] + me[m - 1][P.rhs_exp_indeces] += np.sum(self.Qmat_exp[m - 1] * self.Q, axis=0) + + me[m - 1] *= L.dt + + return me + + def update_nodes(self): + """ + Update the u- and f-values at the collocation nodes -> corresponds to a single sweep over all nodes + + Returns: + None + """ + + # get current level and problem description + L = self.level + P = L.prob + + # only if the level has been touched before + assert L.status.unlocked + + # get number of collocation nodes for easier access + M = self.coll.num_nodes + + integral = self.integrate() + for m in range(M): + if L.tau[m] is not None: + integral[m] += L.tau[m] + for i in range(1, M): + integral[M - i] -= integral[M - i - 1] + + # prepare the integral term + for m in range(M): + integral[m][P.rhs_stiff_indeces] += -L.dt * self.delta[m] * L.f[m + 1].impl[P.rhs_stiff_indeces] + integral[m][P.rhs_nonstiff_indeces] += -L.dt * self.delta[m] * L.f[m].expl[P.rhs_nonstiff_indeces] + integral[m][P.rhs_exp_indeces] += ( + -L.dt + * self.delta[m] + * self.phi_one[m][0] + * (L.f[m].exp[P.rhs_exp_indeces] + self.lmbda * (L.u[0][P.rhs_exp_indeces] - L.u[m][P.rhs_exp_indeces])) + ) + + # do the sweep + for m in range(M): + + tmp = L.u[m] + integral[m] + tmp[P.rhs_exp_indeces] += ( + L.dt + * self.delta[m] + * self.phi_one[m][0] + * (L.f[m].exp[P.rhs_exp_indeces] + self.lmbda * (L.u[0][P.rhs_exp_indeces] - L.u[m][P.rhs_exp_indeces])) + ) + tmp[P.rhs_nonstiff_indeces] += L.dt * self.delta[m] * L.f[m].expl[P.rhs_nonstiff_indeces] + + # implicit solve with prefactor stemming from QI + L.u[m + 1] = P.solve_system( + tmp, L.dt * self.QI[m + 1, m + 1], L.u[m + 1], L.time + L.dt * self.coll.nodes[m], L.u[m + 1] + ) + + # update function values + P.eval_f(L.u[m + 1], L.time + L.dt * self.coll.nodes[m], fh=L.f[m + 1]) + + # indicate presence of new values at this level + L.status.updated = True + + return None + + def compute_end_point(self): + """ + Compute u at the right point of the interval + + The value uend computed here is a full evaluation of the Picard formulation unless do_full_update==False + + Returns: + None + """ + + # get current level and problem description + L = self.level + P = L.prob + + # check if Mth node is equal to right point and do_coll_update is false, perform a simple copy + if self.coll.right_is_node and not self.params.do_coll_update: + # a copy is sufficient + L.uend = P.dtype_u(L.u[-1]) + else: + raise CollocationError("This option is not implemented yet.") + + return None + + def rel_norm(self, a, b): + norms = [] + for i in range(len(a)): + norms.append(np.linalg.norm(a[i]) / np.linalg.norm(b[i])) + return np.average(norms) + + def compute_residual(self, stage=''): + """ + Computation of the residual using the collocation matrix Q + + Args: + stage (str): The current stage of the step the level belongs to + """ + + # get current level and problem description + L = self.level + + # Check if we want to skip the residual computation to gain performance + # Keep in mind that skipping any residual computation is likely to give incorrect outputs of the residual! + if stage in self.params.skip_residual_computation: + L.status.residual = 0.0 if L.status.residual is None else L.status.residual + return None + + # check if there are new values (e.g. from a sweep) + # assert L.status.updated + + # compute the residual for each node + + # build QF(u) + res_norm = [] + rel_res_norm = [] + res = self.integrate() + for m in range(self.coll.num_nodes): + res[m] += L.u[0] + res[m] -= L.u[m + 1] + # add tau if associated + if L.tau[m] is not None: + res[m] += L.tau[m] + # use abs function from data type here + res_norm.append(abs(res[m])) + # the different components of the monodomain equation have very different magnitude therefore we use a tailored relative norm here to avoid the cancellation of the smaller components + rel_res_norm.append(self.rel_norm(res[m], L.u[0])) + + # find maximal residual over the nodes + if L.params.residual_type == 'full_abs': + L.status.residual = max(res_norm) + elif L.params.residual_type == 'last_abs': + L.status.residual = res_norm[-1] + elif L.params.residual_type == 'full_rel': + L.status.residual = max(rel_res_norm) + elif L.params.residual_type == 'last_rel': + L.status.residual = rel_res_norm[-1] + else: + raise ParameterError( + f'residual_type = {L.params.residual_type} not implemented, choose ' + f'full_abs, last_abs, full_rel or last_rel instead' + ) + + # indicate that the residual has seen the new values + L.status.updated = False + + return None diff --git a/pySDC/projects/Monodomain/sweeper_classes/runge_kutta/imexexp_1st_order.py b/pySDC/projects/Monodomain/sweeper_classes/runge_kutta/imexexp_1st_order.py new file mode 100644 index 0000000000..fbd6d45341 --- /dev/null +++ b/pySDC/projects/Monodomain/sweeper_classes/runge_kutta/imexexp_1st_order.py @@ -0,0 +1,145 @@ +import numpy as np + +from pySDC.core.Sweeper import sweeper +from pySDC.core.Errors import CollocationError + + +class imexexp_1st_order(sweeper): + """ + Custom sweeper class, implements Sweeper.py + + First-order IMEXEXP sweeper using implicit/explicit/exponential Euler as base integrator + In the cardiac electrphysiology community this is known as Rush-Larsen scheme. + + """ + + def __init__(self, params): + """ + Initialization routine for the custom sweeper + + Args: + params: parameters for the sweeper + """ + + if "QI" not in params: + params["QI"] = "IE" + + # call parent's initialization routine + super(imexexp_1st_order, self).__init__(params) + + # IMEX integration matrices + self.QI = self.get_Qdelta_implicit(coll=self.coll, qd_type=self.params.QI) + self.delta = np.diagonal(self.QI)[1:] + + def eval_phi_f_exp(self, u, factor): + """ + Evaluates the exponential part of the right-hand side f_exp(u)=lambda(u)*(u-y_inf(u)) multiplied by the exponential factor phi_1(factor*lambda) + Since phi_1(z)=(e^z-1)/z then phi_1(factor*lambda) * f_exp(u) = ((e^(factor*lambda)-1)/factor) *(u-y_inf(u)) + """ + L = self.level + P = L.prob + self.lmbda = P.dtype_u(init=P.init, val=0.0) + self.yinf = P.dtype_u(init=P.init, val=0.0) + P.eval_lmbda_yinf_exp(u, self.lmbda, self.yinf) + phi_f_exp = P.dtype_u(init=P.init, val=0.0) + for i in P.rhs_exp_indeces: + phi_f_exp[i][:] = u[i] - self.yinf[i][:] + phi_f_exp[i][:] *= (np.exp(factor * self.lmbda[i]) - 1.0) / factor + + return phi_f_exp + + def integrate(self): + """ + Integrates the right-hand side (here impl + expl + exp) + + Returns: + list of dtype_u: containing the integral as values + """ + + # get current level and problem description + L = self.level + + me = [] + + # integrate RHS over all collocation nodes + for m in range(1, self.coll.num_nodes + 1): + me.append(L.dt * self.coll.Qmat[m, 1] * (L.f[1].impl + L.f[1].expl + L.f[1].exp)) + for j in range(2, self.coll.num_nodes + 1): + me[m - 1] += L.dt * self.coll.Qmat[m, j] * (L.f[j].impl + L.f[j].expl + L.f[j].exp) + + return me + + def update_nodes(self): + """ + Update the u- and f-values at the collocation nodes -> corresponds to a single sweep over all nodes + + Returns: + None + """ + + # get current level and problem description + L = self.level + P = L.prob + + # only if the level has been touched before + assert L.status.unlocked + + # get number of collocation nodes for easier access + M = self.coll.num_nodes + + integral = self.integrate() + for m in range(M): + if L.tau[m] is not None: + integral[m] += L.tau[m] + for i in range(1, M): + integral[M - i] -= integral[M - i - 1] + + # do the sweep + for m in range(M): + integral[m] -= ( + L.dt + * self.delta[m] + * (L.f[m].expl + L.f[m + 1].impl + self.eval_phi_f_exp(L.u[m], L.dt * self.delta[m])) + ) + for m in range(M): + rhs = ( + L.u[m] + + integral[m] + + L.dt * self.delta[m] * (L.f[m].expl + self.eval_phi_f_exp(L.u[m], L.dt * self.delta[m])) + ) + + # implicit solve with prefactor stemming from QI + L.u[m + 1] = P.solve_system(rhs, L.dt * self.delta[m], L.u[m + 1], L.time + L.dt * self.coll.nodes[m]) + + # update function values + L.f[m + 1] = P.eval_f(L.u[m + 1], L.time + L.dt * self.coll.nodes[m]) + + # indicate presence of new values at this level + L.status.updated = True + + return None + + def compute_end_point(self): + """ + Compute u at the right point of the interval + + The value uend computed here is a full evaluation of the Picard formulation unless do_full_update==False + + Returns: + None + """ + + # get current level and problem description + L = self.level + P = L.prob + + # check if Mth node is equal to right point and do_coll_update is false, perform a simple copy + if self.coll.right_is_node and not self.params.do_coll_update: + # a copy is sufficient + L.uend = P.dtype_u(L.u[-1]) + else: + raise CollocationError( + "In this sweeper we expect the right point to be a collocation node and do_coll_update==False" + ) + + return None diff --git a/pySDC/projects/Monodomain/transfer_classes/TransferVectorOfDCTVectors.py b/pySDC/projects/Monodomain/transfer_classes/TransferVectorOfDCTVectors.py new file mode 100644 index 0000000000..3a21390c81 --- /dev/null +++ b/pySDC/projects/Monodomain/transfer_classes/TransferVectorOfDCTVectors.py @@ -0,0 +1,40 @@ +from pySDC.core.SpaceTransfer import space_transfer +from pySDC.projects.Monodomain.transfer_classes.Transfer_DCT_Vector import DCT_to_DCT +from pySDC.implementations.datatype_classes.mesh import mesh +from pySDC.projects.Monodomain.datatype_classes.my_mesh import imexexp_mesh + + +class TransferVectorOfDCTVectors(space_transfer): + """ + This implementation can restrict and prolong VectorOfVectors + """ + + def __init__(self, fine_prob, coarse_prob, params): + # invoke super initialization + super(TransferVectorOfDCTVectors, self).__init__(fine_prob, coarse_prob, params) + + self.DCT_to_DCT = DCT_to_DCT(fine_prob, coarse_prob, params) + + def restrict(self, F): + + u_coarse = mesh(self.coarse_prob.init) + + for i in range(self.coarse_prob.size): + u_coarse[i][:] = self.DCT_to_DCT.restrict(F[i]) + + return u_coarse + + def prolong(self, G): + + if isinstance(G, imexexp_mesh): + u_fine = imexexp_mesh(self.fine_prob.init) + for i in range(self.fine_prob.size): + u_fine.impl[i][:] = self.DCT_to_DCT.prolong(G.impl[i]) + u_fine.expl[i][:] = self.DCT_to_DCT.prolong(G.expl[i]) + u_fine.exp[i][:] = self.DCT_to_DCT.prolong(G.exp[i]) + elif isinstance(G, mesh): + u_fine = mesh(self.fine_prob.init) + for i in range(self.fine_prob.size): + u_fine[i][:] = self.DCT_to_DCT.prolong(G[i]) + + return u_fine diff --git a/pySDC/projects/Monodomain/transfer_classes/Transfer_DCT_Vector.py b/pySDC/projects/Monodomain/transfer_classes/Transfer_DCT_Vector.py new file mode 100644 index 0000000000..f0bf2ca98b --- /dev/null +++ b/pySDC/projects/Monodomain/transfer_classes/Transfer_DCT_Vector.py @@ -0,0 +1,70 @@ +import scipy.fft as fft + +from pySDC.core.SpaceTransfer import space_transfer +from pySDC.implementations.datatype_classes.mesh import mesh + + +class DCT_to_DCT(space_transfer): + """ + Class to transfer data between two meshes using DCT. + Restriction is performed by zeroing out high frequency modes, while prolongation is done by zero-padding. + + Arguments: + ---------- + fine_prob: fine problem + coarse_prob: coarse problem + params: parameters for the transfer operators + """ + + def __init__(self, fine_prob, coarse_prob, params): + + # invoke super initialization + super(DCT_to_DCT, self).__init__(fine_prob, coarse_prob, params) + + self.norm = "forward" + + self.fine_shape = self.fine_prob.parabolic.shape + self.coarse_shape = self.coarse_prob.parabolic.shape + + if self.fine_shape == self.coarse_shape: + self.same_grid = True + else: + self.same_grid = False + + def restrict(self, F): + """ + Restriction opeartor + Args: + F: the fine level data (easier to access than via the fine attribute) + """ + + G = mesh(self.coarse_prob.parabolic.init) + + if self.same_grid: + G[:] = F + else: + + G[:] = fft.idctn( + fft.dctn(F.reshape(self.fine_shape), norm=self.norm), s=self.coarse_shape, norm=self.norm + ).ravel() + + return G + + def prolong(self, G): + """ + Prolongation opeartor + Args: + G: the coarse level data (easier to access than via the coarse attribute) + """ + + F = mesh(self.fine_prob.parabolic.init) + + if self.same_grid: + F[:] = G + else: + + F[:] = fft.idctn( + fft.dctn(G.reshape(self.coarse_shape), norm=self.norm), s=self.fine_shape, norm=self.norm + ).ravel() + + return F diff --git a/pySDC/projects/Monodomain/utils/data_management.py b/pySDC/projects/Monodomain/utils/data_management.py new file mode 100644 index 0000000000..970c1ab6e7 --- /dev/null +++ b/pySDC/projects/Monodomain/utils/data_management.py @@ -0,0 +1,107 @@ +import sqlite3 +import json +import os + + +class database: + def __init__(self, name): + self.name = name + path = os.path.dirname(self.name) + if path != "" and not os.path.exists(path): + os.makedirs(path) + self.conn = sqlite3.connect(f'{self.name}.db') + self.cursor = self.conn.cursor() + + # def write_arrays(self, table, arrays, columns_names=None): + # if not isinstance(arrays, list): + # arrays = [arrays] + # n = len(arrays) + # if columns_names is None: + # columns_names = ["val_" + str(i) for i in range(n)] + + # self.cursor.execute(f'DROP TABLE IF EXISTS {table}') + # self.cursor.execute(f'CREATE TABLE {table} ({self._convert_list_str_to_arg(columns_names)})') + # self.cursor.execute(f'INSERT INTO {table} VALUES ({self._convert_list_str_to_arg(["?"]*n)})', arrays) + # self.conn.commit() + + def write_dictionary(self, table, dic): + self.cursor.execute(f'DROP TABLE IF EXISTS {table}') + self.cursor.execute(f'CREATE TABLE {table} (dic TEXT)') + self.cursor.execute(f'INSERT INTO {table} VALUES (?)', [json.dumps(dic)]) + self.conn.commit() + + # def read_arrays(self, table, columns_names=None, dtype=np.double): + # if columns_names is None: + # self.cursor.execute(f"SELECT * FROM {table}") + # else: + # self.cursor.execute(f"SELECT {self._convert_list_str_to_arg(columns_names)} FROM {table}") + # result = self.cursor.fetchone() + # result_new = list() + # for res in result: + # result_new.append(np.frombuffer(res, dtype=dtype)) + # if len(result_new) > 1: + # return result_new + # else: + # return result_new[0] + + def read_dictionary(self, table): + self.cursor.execute(f"SELECT dic FROM {table}") + (json_dic,) = self.cursor.fetchone() + return json.loads(json_dic) + + def _convert_list_str_to_arg(self, str_list): + return str(str_list).replace("'", "").replace("[", "").replace("]", "") + + def __del__(self): + self.conn.close() + + +# def main(): +# data = database("test") +# a = np.array([1.0, 2.0, 3.0]) +# b = np.array([10.0, 11.0, 12.0]) +# data.write_arrays("ab_table", [a, b], ['a', 'b']) +# data.write_arrays("a_table", [a], ['a']) +# data.write_arrays("b_table", b, 'b') +# data.write_arrays("ab_table_noname", [a, b]) +# data.write_arrays("b_table_noname", b) + +# a_new, b_new = data.read_arrays("ab_table", ['a', 'b']) +# print(a) +# print(a_new) +# print(b) +# print(b_new) + +# a_new = data.read_arrays("a_table", "a") +# print(a) +# print(a_new) +# b_new = data.read_arrays("b_table", "b") +# print(b) +# print(b_new) + +# a_new, b_new = data.read_arrays("ab_table_noname") +# print(a) +# print(a_new) +# print(b) +# print(b_new) + +# b_new = data.read_arrays("b_table_noname") +# print(b) +# print(b_new) + +# dic = {"name": "Giacomo", "age": 33} +# data.write_dictionary("dic_table", dic) +# dic_new = data.read_dictionary("dic_table") +# print(dic) +# print(dic_new) + +# data_read = database("test") +# a_new, b_new = data_read.read_arrays("ab_table", ['a', 'b']) +# print(a) +# print(a_new) +# print(b) +# print(b_new) + + +# if __name__ == "__main__": +# main() diff --git a/pySDC/projects/Monodomain/visualization/show_monodomain_sol.py b/pySDC/projects/Monodomain/visualization/show_monodomain_sol.py new file mode 100644 index 0000000000..47fba6aede --- /dev/null +++ b/pySDC/projects/Monodomain/visualization/show_monodomain_sol.py @@ -0,0 +1,99 @@ +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +from pathlib import Path +import os + +# Script for displaying the solution of the monodomain equation + +executed_file_dir = os.path.dirname(os.path.realpath(__file__)) +output_root = executed_file_dir + "/../../../../data/Monodomain/results_tmp/" +domain_name = "cube_1D" +refinements = 2 +ionic_model = "TTP" +file_name = "monodomain" +file_path = Path(output_root + domain_name + "/" + "ref_" + str(refinements) + "/" + ionic_model + "/" + file_name) + + +# no need to modifiy below this line +# ------------------------------------------------------------------------------ +if not file_path.with_suffix(".npy").is_file(): + print(f"File {str(file_path)} does not exist") + exit() + +with open(str(file_path) + "_txyz.npy", "rb") as file: + t = np.load(file, allow_pickle=True) +n_dt = t.size +print(f"t_end = {t[-1]}, n_dt = {n_dt}") + +V = [] +with open(file_path.with_suffix(".npy"), "rb") as file: + for _ in range(n_dt): + V.append(np.load(file, allow_pickle=True)) + +Vmin = np.min(V[0].flatten()) +Vmax = np.max(V[0].flatten()) +for Vi in V: + Vmin = min(Vmin, np.min(Vi.flatten())) + Vmax = max(Vmax, np.max(Vi.flatten())) + +Vmin = 1.1 * max(Vmin, -100) +Vmax = 1.1 * min(Vmax, 200) +print(f"Vmin = {Vmin}, Vmax = {Vmax}") + +dim = len(V[0].shape) + +with open(str(file_path) + "_txyz.npy", "rb") as file: + t = np.load(file, allow_pickle=True) + xyz = [] + for _ in range(dim): + xyz.append(np.load(file, allow_pickle=True)) + +if dim == 1: + fig, ax = plt.subplots() + ax.set(ylim=[Vmin, Vmax], xlabel="x [mm]", ylabel="V [mV]") + line = ax.plot(xyz[0], V[0])[0] +elif dim == 2: + fig, ax = plt.subplots() + ax.set(xlabel="x [mm]", ylabel="y [mm]") + ax.set_aspect(aspect="equal") + line = ax.pcolormesh(xyz[0], xyz[1], V[0], cmap=plt.cm.jet, vmin=Vmin, vmax=Vmax) + fig.colorbar(line) +elif dim == 3: + Z, Y, X = np.meshgrid(xyz[2].ravel(), xyz[1].ravel(), xyz[0].ravel(), indexing="ij") + kw = {"vmin": Vmin, "vmax": Vmax, "levels": np.linspace(Vmin, Vmax, 10)} + # fig, ax = plt.subplots(projection="3d") + fig = plt.figure(figsize=(5, 4)) + ax = fig.add_subplot(111, projection="3d") + A = ax.contourf(V[0][:, :, 0], Y[:, :, 0], Z[:, :, 0], zdir="x", offset=0, **kw) + B = ax.contourf(X[:, 0, :], V[0][:, 0, :], Z[:, 0, :], zdir="y", offset=0, **kw) + C = ax.contourf(X[0, :, :], Y[0, :, :], V[0][0, :, :], zdir="z", offset=0, **kw) + # D = ax.contourf(V[0][:, :, -1], Y[:, :, -1], Z[:, :, -1], zdir="x", offset=X.max(), **kw) + + xmin, xmax = X.min(), X.max() + ymin, ymax = Y.min(), Y.max() + zmin, zmax = Z.min(), Z.max() + ax.set(xlim=[xmin, xmax], ylim=[ymin, ymax], zlim=[zmin, zmax]) + ax.set(xlabel="x [mm]", ylabel="y [mm]", zlabel="z [mm]") + fig.colorbar(A, ax=ax, fraction=0.02, pad=0.1, label="V [mV]") + + +def plot_V(k): + ax.set_title(f"V at t = {t[k]:.3f} [ms]") + if dim == 1: + line.set_ydata(V[k]) + return line + elif dim == 2: + line.set_array(V[k].flatten()) + return line + elif dim == 3: + A = ax.contourf(V[k][:, :, 0], Y[:, :, 0], Z[:, :, 0], zdir="x", offset=0, **kw) + B = ax.contourf(X[:, 0, :], V[k][:, 0, :], Z[:, 0, :], zdir="y", offset=0, **kw) + C = ax.contourf(X[0, :, :], Y[0, :, :], V[k][0, :, :], zdir="z", offset=0, **kw) + # D = ax.contourf(V[k][:, :, -1], Y[:, :, -1], Z[:, :, -1], zdir="x", offset=X.max(), **kw) + return A, B, C + + +anim = animation.FuncAnimation(fig=fig, func=plot_V, interval=1, frames=n_dt, repeat=False) +plt.show() +# anim.save(file_path.with_suffix(".gif")) diff --git a/pySDC/tests/test_projects/test_monodomain/test_monodomain_convergence.py b/pySDC/tests/test_projects/test_monodomain/test_monodomain_convergence.py new file mode 100644 index 0000000000..571d6a7e25 --- /dev/null +++ b/pySDC/tests/test_projects/test_monodomain/test_monodomain_convergence.py @@ -0,0 +1,193 @@ +import pytest + + +def run_monodomain_convergence( + dt_max, n_dt, expected_convergence_rate, convergence_rate_tolerance, compute_init_val, compute_ref_sol, **opts +): + from pySDC.projects.Monodomain.run_scripts.run_MonodomainODE import setup_and_run + + opts["num_sweeps"] = [1] + + dt_list = [dt_max / 2**i for i in range(n_dt)] + + # skip residual computation at coarser levels (if any) + opts["skip_residual_computation"] = True + + # interpolate or recompute rhs on fine level + opts["finter"] = False + + # set time parallelism to True or emulated (False) + opts["truly_time_parallel"] = False + + # set monodomain parameters + opts["domain_name"] = "cuboid_1D_small" # small problem for this pytest + opts["refinements"] = [0] + opts["order"] = 2 # 2 or 4 + opts["ionic_model_name"] = ( + "TTP_SMOOTH" # a smoothed ionic model, the original TTP model has (very small) discontinuities due if-else statements in its implementation + ) + opts["enable_output"] = False + opts["write_database"] = False + + opts["output_root"] = "results_convergence" + + # save some values for later + opts_bak = opts.copy() + + # In order to initiate an action potential the monodomain problem needs a stimulus. In our code the stimulus is a step function. + # Due to its non smoothness we dont want to use it in the convergence test. Therefore we first generate an initial value, + # using the step function, and then we use this initial value as the initial value for the convergence test. In that way the non smooth + # stimulus is not used in the convergence test. + + # First, compute an initial value for the convergence test. + opts["dt"] = 0.1 + opts["restol"] = 5e-8 # residual tolerance, doesn't need to be very small for the initial value + opts["read_init_val"] = False + opts["init_time"] = 0.0 + opts["end_time"] = 3.0 + opts["write_as_reference_solution"] = True # write the initial value + opts["write_all_variables"] = True # write all variables, not only the potential + opts["output_file_name"] = "init_val_DCT" + opts["ref_sol"] = "" + if compute_init_val: + print("Computing initial value for the convergence test...") + err, rel_err, avg_niters, times, niters, residuals = setup_and_run(**opts) + + # Second, compute a reference solution for the convergence test. + opts["dt"] = dt_list[-1] / 4.0 + opts["restol"] = 1e-14 # residual tolerance, very small to no pollute convergence + opts["read_init_val"] = True + opts["init_time"] = 3.0 # start at t0=3 + opts["end_time"] = opts_bak["end_time"] # end at t = t0+end_time + opts["write_as_reference_solution"] = True # write as reference solution + opts["write_all_variables"] = ( + False # write only the potential. The other ionic model variables are not taken in account in the convergence test. + ) + opts["output_file_name"] = "ref_sol" + if compute_ref_sol: + print("Computing reference solution for the convergence test...") + err, rel_err, avg_niters, times, niters, residuals = setup_and_run(**opts) + + # Third, run the convergence test + opts["write_as_reference_solution"] = False + opts["write_all_variables"] = False + opts["ref_sol"] = "ref_sol" + + print("Running convergence test...") + rel_err = [0.0] * n_dt + for i, dt in enumerate(dt_list): + print(f"Iteration {i} of {n_dt}...") + opts["dt"] = dt + opts["output_file_name"] = "monodomain_dt_" + str(dt).replace(".", "p") + err, rel_err[i], avg_niters, times, niters, residuals = setup_and_run(**opts) + + import numpy as np + + rates = np.zeros(n_dt - 1) + for i in range(n_dt - 1): + rates[i] = np.log(rel_err[i] / rel_err[i + 1]) / np.log(dt_list[i] / dt_list[i + 1]) + + print("\nConvergence test results") + print(f"Relative errors: {rel_err}") + print(f"Rates: {rates}") + + assert np.all(rates > expected_convergence_rate - convergence_rate_tolerance), "ERROR: convergence rate is too low!" + + return dt_list, rel_err + + +@pytest.mark.monodomain +def test_monodomain_convergence_ESDC_TTP(): + max_iter_6_dt, max_iter_6_rel_err = run_monodomain_convergence( + dt_max=0.2, + n_dt=5, + expected_convergence_rate=6.0, + convergence_rate_tolerance=1.0, + compute_init_val=True, + compute_ref_sol=True, + integrator="IMEXEXP_EXPRK", + num_nodes=[6], + max_iter=6, + n_time_ranks=1, + end_time=0.2, + ) + + max_iter_3_dt, max_iter_3_rel_err = run_monodomain_convergence( + dt_max=0.2, + n_dt=5, + expected_convergence_rate=3.0, + convergence_rate_tolerance=0.5, + compute_init_val=False, + compute_ref_sol=False, + integrator="IMEXEXP_EXPRK", + num_nodes=[6], + max_iter=3, + n_time_ranks=1, + end_time=0.2, + ) + + import numpy as np + + max_iter_3_dt = np.array(max_iter_3_dt) + max_iter_3_rel_err = np.array(max_iter_3_rel_err) + max_iter_6_dt = np.array(max_iter_6_dt) + max_iter_6_rel_err = np.array(max_iter_6_rel_err) + + import pySDC.helpers.plot_helper as plt_helper + + plt_helper.setup_mpl() + plt_helper.newfig(textwidth=238.96, scale=0.89) + + lw = 1.5 + colors = ["C0", "C1", "C2", "C3", "C4"] + markers = ["o", "x", "s", "D", "^"] + + plt_helper.plt.loglog( + max_iter_3_dt, + max_iter_3_rel_err, + label="$k=3$", + lw=lw, + linestyle="-", + color=colors[0], + marker=markers[0], + markerfacecolor="none", + markeredgewidth=1.2, + markersize=7.5, + ) + plt_helper.plt.loglog( + max_iter_6_dt, + max_iter_6_rel_err, + label="$k=6$", + lw=lw, + linestyle="-", + color=colors[1], + marker=markers[1], + markerfacecolor="none", + markeredgewidth=1.2, + markersize=7.5, + ) + plt_helper.plt.loglog( + max_iter_3_dt, + 0.1 * np.min(max_iter_3_rel_err) * (max_iter_3_dt / max_iter_3_dt[-1]) ** 3, + linewidth=2, + linestyle="--", + color="k", + label=r"$\mathcal{{O}}(\Delta t^3)$", + ) + plt_helper.plt.loglog( + max_iter_6_dt, + 0.1 * np.min(max_iter_6_rel_err) * (max_iter_6_dt / max_iter_6_dt[-1]) ** 6, + linewidth=2, + linestyle="-", + color="k", + label=r"$\mathcal{{O}}(\Delta t^6)$", + ) + plt_helper.plt.legend(loc="lower right", ncol=1) + plt_helper.plt.ylabel('rel. err.') + plt_helper.plt.xlabel(r"$\Delta t$") + plt_helper.plt.grid() + plt_helper.savefig("data/convergence_ESDC_fixed_iter", save_pdf=False, save_pgf=False, save_png=True) + + +if __name__ == "__main__": + test_monodomain_convergence_ESDC_TTP() diff --git a/pySDC/tests/test_projects/test_monodomain/test_monodomain_iterations.py b/pySDC/tests/test_projects/test_monodomain/test_monodomain_iterations.py new file mode 100644 index 0000000000..e588d5b2fe --- /dev/null +++ b/pySDC/tests/test_projects/test_monodomain/test_monodomain_iterations.py @@ -0,0 +1,120 @@ +import pytest + + +def check_iterations(expected_avg_niters, **opts): + from pySDC.projects.Monodomain.run_scripts.run_MonodomainODE import setup_and_run + + # define sweeper parameters + opts["integrator"] = "IMEXEXP_EXPRK" + opts["num_sweeps"] = [1] + + # set step parameters + opts["max_iter"] = 100 + + # set level parameters + opts["dt"] = 0.025 + + opts["restol"] = 5e-8 # residual tolerance + + # skip residual computation at coarser levels (if any) + opts["skip_residual_computation"] = True + + # interpolate or recompute rhs on fine level + opts["finter"] = True + + # set time parallelism to True or emulated (False) + opts["truly_time_parallel"] = False + + # set monodomain parameters + opts["order"] = 4 # 2 or 4 + opts["enable_output"] = False + opts["write_database"] = False + + opts["output_root"] = "results_iterations_pytest" + + opts["read_init_val"] = False + opts["init_time"] = 0.0 + opts["end_time"] = 2.0 + opts["write_as_reference_solution"] = False + opts["write_all_variables"] = False + opts["output_file_name"] = "monodomain" + opts["ref_sol"] = "" + + err, rel_err, avg_niters, times, niters, residuals = setup_and_run(**opts) + + print(f"Got average number of iterations {avg_niters}, expected was {expected_avg_niters}") + + assert avg_niters == pytest.approx( + expected_avg_niters, rel=0.1 + ), f"Average number of iterations {avg_niters} too different from the expected {expected_avg_niters}" + + +# Many of the following are commented since they test features already tested in other tests +# If you reactivate them the number of expected iterations should be updated + + +@pytest.mark.monodomain +def test_monodomain_iterations_ESDC_BS(): + check_iterations( + domain_name="cuboid_2D_small", + num_nodes=[6, 3], + refinements=[0, -1], + ionic_model_name="BS", + n_time_ranks=4, + expected_avg_niters=3.3209876543209877, + ) + + +# @pytest.mark.monodomain +# def test_monodomain_iterations_MLESDC_BS(): +# check_iterations(num_nodes=[6, 3], ionic_model_name="BS", expected_avg_niters=2.03125) + + +@pytest.mark.monodomain +def test_monodomain_iterations_ESDC_HH(): + check_iterations( + domain_name="cuboid_2D_small", + num_nodes=[6, 3], + refinements=[0, -1], + ionic_model_name="HH", + n_time_ranks=2, + expected_avg_niters=3.074074074074074, + ) + + +# @pytest.mark.monodomain +# def test_monodomain_iterations_MLESDC_HH(): +# check_iterations(num_nodes=[6, 3], ionic_model_name="HH", expected_avg_niters=2.80625) + + +@pytest.mark.monodomain +def test_monodomain_iterations_ESDC_CRN(): + check_iterations( + domain_name="cube_1D", + num_nodes=[6], + refinements=[0], + ionic_model_name="CRN", + n_time_ranks=1, + expected_avg_niters=3.382716, + ) + + +# @pytest.mark.monodomain +# def test_monodomain_iterations_MLESDC_CRN(): +# check_iterations(num_nodes=[6, 3], ionic_model_name="CRN", expected_avg_niters=2.3625) + + +# @pytest.mark.monodomain +# def test_monodomain_iterations_ESDC_TTP(): +# check_iterations(num_nodes=[6], ionic_model_name="TTP", expected_avg_niters=3.60625) + + +# @pytest.mark.monodomain +# def test_monodomain_iterations_MLESDC_TTP(): +# check_iterations(num_nodes=[6, 3], ionic_model_name="TTP", expected_avg_niters=2.90625) + + +# if __name__ == "__main__": +# test_monodomain_iterations_ESDC_BS() +# test_monodomain_iterations_ESDC_HH() +# test_monodomain_iterations_ESDC_CRN() diff --git a/pySDC/tests/test_projects/test_monodomain/test_monodomain_iterations_parallel.py b/pySDC/tests/test_projects/test_monodomain/test_monodomain_iterations_parallel.py new file mode 100644 index 0000000000..feab5126e9 --- /dev/null +++ b/pySDC/tests/test_projects/test_monodomain/test_monodomain_iterations_parallel.py @@ -0,0 +1,273 @@ +import pytest +import os +import subprocess + + +def plot_iter_info(iters_info_list, labels_list, key1, key2, logy, xlabel, ylabel, ymin, ymax, title, output_file_name): + + markers = ["o", "x", "s", "D", "v", "^", "<", ">", "p", "h", "H", "*", "+", "X", "d", "|", "_"] + colors = ["C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9"] + + import pySDC.helpers.plot_helper as plt_helper + + plt_helper.setup_mpl() + plt_helper.newfig(textwidth=238.96, scale=0.89) + + lw = 1.5 + colors = ["C0", "C1", "C2", "C3", "C4"] + markers = ["o", "x", "s", "D", "^"] + + if logy: + plt_helper.plt.yscale("log", base=10) + + for i, (iters_info, label) in enumerate(zip(iters_info_list, labels_list)): + plt_helper.plt.plot( + iters_info[key1], + iters_info[key2], + label=label, + lw=lw, + color=colors[i], + marker=markers[i], + markerfacecolor="none", + markeredgewidth=1.2, + markersize=7.5, + ) + + if ymin is not None and ymax is not None: + plt_helper.plt.set_ylim([ymin, ymax]) + + plt_helper.plt.legend(loc="lower right", ncol=1) + plt_helper.plt.ylabel(ylabel) + plt_helper.plt.xlabel(xlabel) + plt_helper.plt.title(title) + plt_helper.plt.grid() + plt_helper.savefig("data/" + output_file_name, save_pdf=False, save_pgf=False, save_png=True) + + +def options_command(options): + cmd = "" + for key, val in options.items(): + if type(val) is list: + opt = key + if type(val[0]) is int: + arg = ",".join([str(v).replace("-", "_") for v in val]) + else: + arg = ",".join([map(str, val)]) + elif type(val) is bool: + if not val: + opt = "no-" + key + else: + opt = key + arg = "" + else: + opt = key + arg = str(val) + cmd = cmd + " --" + opt + (" " + arg if arg != "" else "") + return cmd + + +def generate_initial_value(ionic_model_name): + from pySDC.projects.Monodomain.run_scripts.run_MonodomainODE import setup_and_run + + opts = dict() + + # define sweeper parameters + opts["integrator"] = "IMEXEXP_EXPRK" + opts["num_nodes"] = [5] + opts["num_sweeps"] = [1] + + # set step parameters + opts["max_iter"] = 100 + + # set level parameters + opts["dt"] = 0.1 + + opts["restol"] = 5e-8 # residual tolerance + + opts["truly_time_parallel"] = False + opts["n_time_ranks"] = 1 + + # skip residual computation at coarser levels (if any) + opts["skip_residual_computation"] = True + + # interpolate or recompute rhs on fine level + opts["finter"] = False + + # set monodomain parameters + opts["domain_name"] = "cuboid_1D_small" + opts["ionic_model_name"] = ionic_model_name + opts["refinements"] = [0] + opts["order"] = 4 # 2 or 4 + + opts["enable_output"] = False + opts["write_database"] = False + + opts["output_root"] = "results_iterations_parallel" + + opts["read_init_val"] = False + opts["init_time"] = 0.0 + opts["end_time"] = 6.0 + opts["write_as_reference_solution"] = True + opts["write_all_variables"] = True + opts["output_file_name"] = "init_val_DCT" + opts["ref_sol"] = "" + + err, rel_err, avg_niters, times, niters, residuals = setup_and_run(**opts) + + +def check_iterations_parallel(expected_avg_niters, **options): + # define sweeper parameters + + options["num_sweeps"] = [1] + + # set step parameters + options["max_iter"] = 100 + options["dt"] = 0.025 + + # set level parameters + options["restol"] = 5e-8 + + options["end_time"] = 0.6 + + # set problem parameters + options["domain_name"] = "cuboid_1D_small" + options["refinements"] = [0] + options["order"] = 4 + options["read_init_val"] = True + options["init_time"] = 3.0 + options["enable_output"] = False + options["write_as_reference_solution"] = False + options["write_all_variables"] = False + options["output_file_name"] = "monodomain" + options["output_root"] = "results_iterations_parallel" + options["skip_res"] = True + options["finter"] = False + options["write_database"] = True + + my_env = os.environ.copy() + my_env['PYTHONPATH'] = '.:../../../..' + my_env['COVERAGE_PROCESS_START'] = 'pyproject.toml' + cwd = "pySDC/projects/Monodomain/run_scripts" + + # base_python_command = "coverage run -p run_MonodomainODE_cli.py" + base_python_command = "coverage run -p " + cwd + "/run_MonodomainODE_cli.py" + cmd = f"mpirun -n {options['n_time_ranks']} " + base_python_command + " " + options_command(options) + + print(f"Running command: {cmd}") + + process = subprocess.Popen( + args=cmd.split(), + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=my_env, + cwd=".", + ) + + while True: + output = process.stdout.readline() + if output == '' and process.poll() is not None: + break + if output: + print(output.strip()) + + process.wait() + + assert ( + process.returncode == 0 + ), f"ERROR: did not get return code 0, got {process.returncode} with {options['n_time_ranks']} processes" + + # read the generated data + executed_file_dir = os.path.dirname(os.path.realpath(__file__)) + file_name = ( + executed_file_dir + + "/../../../../data/" + + options["output_root"] + + "/" + + options["domain_name"] + + "/ref_" + + str(options["refinements"][0]) + + "/" + + options["ionic_model_name"] + + "/" + + options["output_file_name"] + ) + from pySDC.projects.Monodomain.utils.data_management import database + + data_man = database(file_name) + # errors = data_man.read_dictionary("errors") + iters_info = data_man.read_dictionary("iters_info") + + print(f"Got average number of iterations {iters_info['avg_niters']}, expected was {expected_avg_niters}") + + assert iters_info['avg_niters'] == pytest.approx( + expected_avg_niters, rel=0.1 + ), f"Average number of iterations {iters_info['avg_niters']} too different from the expected {expected_avg_niters}" + + return iters_info + + +@pytest.mark.monodomain +def test_monodomain_iterations_ESDC_MLESDC_PFASST(): + + generate_initial_value(ionic_model_name="TTP") + + ESDC_iters_info = check_iterations_parallel( + integrator="IMEXEXP_EXPRK", + num_nodes=[8], + ionic_model_name="TTP", + truly_time_parallel=True, + n_time_ranks=1, + expected_avg_niters=3.58333, + ) + + MLESDC_iters_info = check_iterations_parallel( + integrator="IMEXEXP_EXPRK", + num_nodes=[8, 4], + ionic_model_name="TTP", + truly_time_parallel=True, + n_time_ranks=1, + expected_avg_niters=2.0, + ) + + PFASST_iters_info = check_iterations_parallel( + integrator="IMEXEXP_EXPRK", + num_nodes=[8, 4], + ionic_model_name="TTP", + truly_time_parallel=True, + n_time_ranks=24, + expected_avg_niters=3.0, + ) + + iters_info_list = [ESDC_iters_info, MLESDC_iters_info, PFASST_iters_info] + labels_list = ["ESDC", "MLESDC", "PFASST"] + plot_iter_info( + iters_info_list, + labels_list, + key1='times', + key2='niters', + logy=False, + xlabel="$t$", + ylabel=r"\# iter", + ymin=None, + ymax=None, + title="Number of iterations", + output_file_name="niter_VS_time", + ) + plot_iter_info( + iters_info_list, + labels_list, + key1='times', + key2='residuals', + logy=True, + xlabel="$t$", + ylabel="residual", + ymin=None, + ymax=None, + title="Residual over time", + output_file_name="res_VS_time", + ) + + +if __name__ == "__main__": + test_monodomain_iterations_ESDC_MLESDC_PFASST() diff --git a/pySDC/tests/test_projects/test_monodomain/test_monodomain_stability_domain.py b/pySDC/tests/test_projects/test_monodomain/test_monodomain_stability_domain.py new file mode 100644 index 0000000000..73fb53e719 --- /dev/null +++ b/pySDC/tests/test_projects/test_monodomain/test_monodomain_stability_domain.py @@ -0,0 +1,68 @@ +import pytest + + +@pytest.mark.monodomain +def test_monodomain_stability_ESDC(): + from pySDC.projects.Monodomain.run_scripts.run_TestODE import main + + main( + integrator="IMEXEXP_EXPRK", + dl=2, + l_min=-100, + openmp=True, + n_time_ranks=1, + end_time=1.0, + num_nodes=[5], + check_stability=True, + ) + + # This is to generate the image only, we do not check for stabiltiy since we already know that + # SDC is unstable for this problem + main( + integrator="IMEXEXP", + dl=2, + l_min=-100, + openmp=True, + n_time_ranks=1, + end_time=1.0, + num_nodes=[5], + check_stability=False, + ) + + +# @pytest.mark.monodomain +# def test_monodomain_stability_MLESDC(): +# from pySDC.projects.Monodomain.run_scripts.run_TestODE import main + +# main( +# integrator="IMEXEXP_EXPRK", +# dl=2, +# l_min=-100, +# openmp=True, +# n_time_ranks=1, +# end_time=1.0, +# num_nodes=[5, 3], +# check_stability=True, +# ) + + +# @pytest.mark.monodomain +# def test_monodomain_stability_PFASST(): +# from pySDC.projects.Monodomain.run_scripts.run_TestODE import main + +# main( +# integrator="IMEXEXP_EXPRK", +# dl=2, +# l_min=-100, +# openmp=True, +# n_time_ranks=4, +# end_time=1.0, +# num_nodes=[5, 3], +# check_stability=True, +# ) + + +# if __name__ == "__main__": +# test_monodomain_stability_ESDC() +# test_monodomain_stability_MLESDC() +# test_monodomain_stability_PFASST() diff --git a/pyproject.toml b/pyproject.toml index cc9d1ea8e9..37f2be3aad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ markers = [ 'benchmark: tests for benchmarking', 'cupy: tests for cupy on GPUs', 'libpressio: tests using the libpressio library', + 'monodomain: tests the monodomain project, which requires previous compilation of c++ code', ] timeout = 300 From ea1ed4844206ea4d01a8142e7f9f70ac51951ab8 Mon Sep 17 00:00:00 2001 From: Thomas Baumann <39156931+brownbaerchen@users.noreply.github.com> Date: Mon, 1 Apr 2024 13:47:23 +0200 Subject: [PATCH 6/8] Generic MPI FFT class (#408) * Added generic MPIFFT problem class * Fixes * Generalized to `xp` in preparation for GPUs * Fixes * Ported Allen-Cahn to generic MPI FFT implementation --- .../problem_classes/AllenCahn_MPIFFT.py | 122 ++---------- .../problem_classes/Brusselator.py | 106 +++-------- .../NonlinearSchroedinger_MPIFFT.py | 147 ++------------- .../generic_MPIFFT_Laplacian.py | 177 ++++++++++++++++++ .../test_AC/test_simple_forcing.py | 14 +- 5 files changed, 246 insertions(+), 320 deletions(-) create mode 100644 pySDC/implementations/problem_classes/generic_MPIFFT_Laplacian.py diff --git a/pySDC/implementations/problem_classes/AllenCahn_MPIFFT.py b/pySDC/implementations/problem_classes/AllenCahn_MPIFFT.py index 17f770d218..f0aa47d89e 100644 --- a/pySDC/implementations/problem_classes/AllenCahn_MPIFFT.py +++ b/pySDC/implementations/problem_classes/AllenCahn_MPIFFT.py @@ -1,15 +1,11 @@ import numpy as np from mpi4py import MPI -from mpi4py_fft import PFFT - -from pySDC.core.Errors import ProblemError -from pySDC.core.Problem import ptype -from pySDC.implementations.datatype_classes.mesh import mesh, imex_mesh +from pySDC.implementations.problem_classes.generic_MPIFFT_Laplacian import IMEX_Laplacian_MPIFFT from mpi4py_fft import newDistArray -class allencahn_imex(ptype): +class allencahn_imex(IMEX_Laplacian_MPIFFT): r""" Example implementing the :math:`N`-dimensional Allen-Cahn equation with periodic boundary conditions :math:`u \in [0, 1]^2` @@ -64,68 +60,21 @@ class allencahn_imex(ptype): .. [1] https://mpi4py-fft.readthedocs.io/en/latest/ """ - dtype_u = mesh - dtype_f = imex_mesh - def __init__( self, - nvars=None, eps=0.04, radius=0.25, - spectral=None, dw=0.0, - L=1.0, init_type='circle', - comm=MPI.COMM_WORLD, + **kwargs, ): - """Initialization routine""" - - if nvars is None: - nvars = (128, 128) - - if not (isinstance(nvars, tuple) and len(nvars) > 1): - raise ProblemError('Need at least two dimensions') - - # Creating FFT structure - ndim = len(nvars) - axes = tuple(range(ndim)) - self.fft = PFFT(comm, list(nvars), axes=axes, dtype=np.float64, collapse=True) - - # get test data to figure out type and dimensions - tmp_u = newDistArray(self.fft, spectral) - - # invoke super init, passing the communicator and the local dimensions as init - super().__init__(init=(tmp_u.shape, comm, tmp_u.dtype)) - self._makeAttributeAndRegister( - 'nvars', 'eps', 'radius', 'spectral', 'dw', 'L', 'init_type', 'comm', localVars=locals(), readOnly=True - ) - - L = np.array([self.L] * ndim, dtype=float) - - # get local mesh - X = np.ogrid[self.fft.local_slice(False)] - N = self.fft.global_shape() - for i in range(len(N)): - X[i] = X[i] * L[i] / N[i] - self.X = [np.broadcast_to(x, self.fft.shape(False)) for x in X] - - # get local wavenumbers and Laplace operator - s = self.fft.local_slice() - N = self.fft.global_shape() - k = [np.fft.fftfreq(n, 1.0 / n).astype(int) for n in N[:-1]] - k.append(np.fft.rfftfreq(N[-1], 1.0 / N[-1]).astype(int)) - K = [ki[si] for ki, si in zip(k, s)] - Ks = np.meshgrid(*K, indexing='ij', sparse=True) - Lp = 2 * np.pi / L - for i in range(ndim): - Ks[i] = (Ks[i] * Lp[i]).astype(float) - K = [np.broadcast_to(k, self.fft.shape(True)) for k in Ks] - K = np.array(K).astype(float) - self.K2 = np.sum(K * K, 0, dtype=float) - - # Need this for diagnostics - self.dx = self.L / nvars[0] - self.dy = self.L / nvars[1] + kwargs['L'] = kwargs.get('L', 1.0) + super().__init__(alpha=1.0, dtype=np.dtype('float'), **kwargs) + self._makeAttributeAndRegister('eps', 'radius', 'dw', 'init_type', localVars=locals(), readOnly=True) + + def _eval_explicit_part(self, u, t, f_expl): + f_expl[:] = -2.0 / self.eps**2 * u * (1.0 - u) * (1.0 - 2.0 * u) - 6.0 * self.dw * u * (1.0 - u) + return f_expl def eval_f(self, u, t): """ @@ -146,56 +95,24 @@ def eval_f(self, u, t): f = self.dtype_f(self.init) + f.impl[:] = self._eval_Laplacian(u, f.impl) + if self.spectral: f.impl = -self.K2 * u if self.eps > 0: tmp = self.fft.backward(u) - tmpf = -2.0 / self.eps**2 * tmp * (1.0 - tmp) * (1.0 - 2.0 * tmp) - 6.0 * self.dw * tmp * (1.0 - tmp) - f.expl[:] = self.fft.forward(tmpf) + tmp[:] = self._eval_explicit_part(tmp, t, tmp) + f.expl[:] = self.fft.forward(tmp) else: - u_hat = self.fft.forward(u) - lap_u_hat = -self.K2 * u_hat - f.impl[:] = self.fft.backward(lap_u_hat, f.impl) if self.eps > 0: - f.expl = -2.0 / self.eps**2 * u * (1.0 - u) * (1.0 - 2.0 * u) - 6.0 * self.dw * u * (1.0 - u) + f.expl[:] = self._eval_explicit_part(u, t, f.expl) + self.work_counters['rhs']() return f - def solve_system(self, rhs, factor, u0, t): - """ - Simple FFT solver for the diffusion part. - - Parameters - ---------- - rhs : dtype_f - Right-hand side for the linear system. - factor : float - Abbrev. for the node-to-node stepsize (or any other factor required). - u0 : dtype_u - Initial guess for the iterative solver (not used here so far). - t : float - Current time (e.g. for time-dependent BCs). - - Returns - ------- - me : dtype_u - The solution as mesh. - """ - - if self.spectral: - me = rhs / (1.0 + factor * self.K2) - - else: - me = self.dtype_u(self.init) - rhs_hat = self.fft.forward(rhs) - rhs_hat /= 1.0 + factor * self.K2 - me[:] = self.fft.backward(rhs_hat) - - return me - def u_exact(self, t): r""" Routine to compute the exact solution at time :math:`t`. @@ -289,8 +206,9 @@ def eval_f(self, u, t): f = self.dtype_f(self.init) + f.impl[:] = self._eval_Laplacian(u, f.impl) + if self.spectral: - f.impl = -self.K2 * u tmp = newDistArray(self.fft, False) tmp[:] = self.fft.backward(u, tmp) @@ -324,9 +242,6 @@ def eval_f(self, u, t): f.expl[:] = self.fft.forward(tmpf) else: - u_hat = self.fft.forward(u) - lap_u_hat = -self.K2 * u_hat - f.impl[:] = self.fft.backward(lap_u_hat, f.impl) if self.eps > 0: f.expl = -2.0 / self.eps**2 * u * (1.0 - u) * (1.0 - 2.0 * u) @@ -353,4 +268,5 @@ def eval_f(self, u, t): f.expl -= 6.0 * dw * u * (1.0 - u) + self.work_counters['rhs']() return f diff --git a/pySDC/implementations/problem_classes/Brusselator.py b/pySDC/implementations/problem_classes/Brusselator.py index d802e9a7f9..946e676781 100644 --- a/pySDC/implementations/problem_classes/Brusselator.py +++ b/pySDC/implementations/problem_classes/Brusselator.py @@ -1,15 +1,10 @@ import numpy as np from mpi4py import MPI -from mpi4py_fft import PFFT -from pySDC.core.Errors import ProblemError -from pySDC.core.Problem import ptype, WorkCounter -from pySDC.implementations.datatype_classes.mesh import mesh, imex_mesh +from pySDC.implementations.problem_classes.generic_MPIFFT_Laplacian import IMEX_Laplacian_MPIFFT -from mpi4py_fft import newDistArray - -class Brusselator(ptype): +class Brusselator(IMEX_Laplacian_MPIFFT): r""" Two-dimensional Brusselator from [1]_. This is a reaction-diffusion equation with non-autonomous source term: @@ -27,68 +22,29 @@ class Brusselator(ptype): .. [1] https://link.springer.com/book/10.1007/978-3-642-05221-7 """ - dtype_u = mesh - dtype_f = imex_mesh - - def __init__(self, nvars=None, alpha=0.1, comm=MPI.COMM_WORLD): + def __init__(self, alpha=0.1, **kwargs): """Initialization routine""" - nvars = (128,) * 2 if nvars is None else nvars - L = 1.0 - - if not (isinstance(nvars, tuple) and len(nvars) > 1): - raise ProblemError('Need at least two dimensions') - - # Create FFT structure - self.ndim = len(nvars) - axes = tuple(range(self.ndim)) - self.fft = PFFT( - comm, - list(nvars), - axes=axes, - dtype=np.float64, - collapse=True, - backend='fftw', - ) - - # get test data to figure out type and dimensions - tmp_u = newDistArray(self.fft, False) + super().__init__(spectral=False, L=1.0, dtype='d', alpha=alpha, **kwargs) # prepare the array with two components - shape = (2,) + tmp_u.shape + shape = (2,) + (self.init[0]) self.iU = 0 self.iV = 1 + self.init = (shape, self.comm, np.dtype('float')) + + def _eval_explicit_part(self, u, t, f_expl): + iU, iV = self.iU, self.iV + x, y = self.X[0], self.X[1] - super().__init__(init=(shape, comm, tmp_u.dtype)) - self._makeAttributeAndRegister('nvars', 'alpha', 'L', 'comm', localVars=locals(), readOnly=True) - - L = np.array([self.L] * self.ndim, dtype=float) - - # get local mesh for distributed FFT - X = np.ogrid[self.fft.local_slice(False)] - N = self.fft.global_shape() - for i in range(len(N)): - X[i] = X[i] * L[i] / N[i] - self.X = [np.broadcast_to(x, self.fft.shape(False)) for x in X] - - # get local wavenumbers and Laplace operator - s = self.fft.local_slice() - N = self.fft.global_shape() - k = [np.fft.fftfreq(n, 1.0 / n).astype(int) for n in N[:-1]] - k.append(np.fft.rfftfreq(N[-1], 1.0 / N[-1]).astype(int)) - K = [ki[si] for ki, si in zip(k, s)] - Ks = np.meshgrid(*K, indexing='ij', sparse=True) - Lp = 2 * np.pi / L - for i in range(self.ndim): - Ks[i] = (Ks[i] * Lp[i]).astype(float) - K = [np.broadcast_to(k, self.fft.shape(True)) for k in Ks] - K = np.array(K).astype(float) - self.K2 = np.sum(K * K, 0, dtype=float) - - # Need this for diagnostics - self.dx = self.L / nvars[0] - self.dy = self.L / nvars[1] - - self.work_counters['rhs'] = WorkCounter() + # evaluate time independent part + f_expl[iU, ...] = 1.0 + u[iU] ** 2 * u[iV] - 4.4 * u[iU] + f_expl[iV, ...] = 3.4 * u[iU] - u[iU] ** 2 * u[iV] + + # add time-dependent part + if t >= 1.1: + mask = (x - 0.3) ** 2 + (y - 0.6) ** 2 <= 0.1**2 + f_expl[iU][mask] += 5.0 + return f_expl def eval_f(self, u, t): """ @@ -106,25 +62,13 @@ def eval_f(self, u, t): f : dtype_f The right-hand side of the problem. """ - iU, iV = self.iU, self.iV - x, y = self.X[0], self.X[1] - f = self.dtype_f(self.init) # evaluate Laplacian to be solved implicitly for i in [self.iU, self.iV]: - u_hat = self.fft.forward(u[i, ...]) - lap_u_hat = -self.alpha * self.K2 * u_hat - f.impl[i, ...] = self.fft.backward(lap_u_hat, f.impl[i, ...]) + f.impl[i, ...] = self._eval_Laplacian(u[i], f.impl[i]) - # evaluate time independent part - f.expl[iU, ...] = 1.0 + u[iU] ** 2 * u[iV] - 4.4 * u[iU] - f.expl[iV, ...] = 3.4 * u[iU] - u[iU] ** 2 * u[iV] - - # add time-dependent part - if t >= 1.1: - mask = (x - 0.3) ** 2 + (y - 0.6) ** 2 <= 0.1**2 - f.expl[iU][mask] += 5.0 + f.expl[:] = self._eval_explicit_part(u, t, f.expl) self.work_counters['rhs']() @@ -153,9 +97,7 @@ def solve_system(self, rhs, factor, u0, t): me = self.dtype_u(self.init) for i in [self.iU, self.iV]: - rhs_hat = self.fft.forward(rhs[i, ...]) - rhs_hat /= 1.0 + factor * self.K2 * self.alpha - me[i, ...] = self.fft.backward(rhs_hat, me[i, ...]) + me[i, ...] = self._invert_Laplacian(me[i], factor, rhs[i]) return me @@ -184,8 +126,8 @@ def u_exact(self, t, u_init=None, t_init=None): me = self.dtype_u(self.init, val=0.0) if t == 0: - me[iU, ...] = 22.0 * y * (1 - y / self.L) ** (3.0 / 2.0) / self.L - me[iV, ...] = 27.0 * x * (1 - x / self.L) ** (3.0 / 2.0) / self.L + me[iU, ...] = 22.0 * y * (1 - y / self.L[0]) ** (3.0 / 2.0) / self.L[0] + me[iV, ...] = 27.0 * x * (1 - x / self.L[0]) ** (3.0 / 2.0) / self.L[0] else: def eval_rhs(t, u): diff --git a/pySDC/implementations/problem_classes/NonlinearSchroedinger_MPIFFT.py b/pySDC/implementations/problem_classes/NonlinearSchroedinger_MPIFFT.py index 66c96c84ae..70c628ec1a 100644 --- a/pySDC/implementations/problem_classes/NonlinearSchroedinger_MPIFFT.py +++ b/pySDC/implementations/problem_classes/NonlinearSchroedinger_MPIFFT.py @@ -1,18 +1,14 @@ import numpy as np -from scipy.optimize import newton_krylov, root +from scipy.optimize import newton_krylov from scipy.optimize.nonlin import NoConvergence -import scipy.sparse as sp -from mpi4py import MPI -from mpi4py_fft import PFFT from pySDC.core.Errors import ProblemError -from pySDC.core.Problem import ptype, WorkCounter -from pySDC.implementations.datatype_classes.mesh import mesh, imex_mesh +from pySDC.core.Problem import WorkCounter +from pySDC.implementations.problem_classes.generic_MPIFFT_Laplacian import IMEX_Laplacian_MPIFFT +from pySDC.implementations.datatype_classes.mesh import mesh -from mpi4py_fft import newDistArray - -class nonlinearschroedinger_imex(ptype): +class nonlinearschroedinger_imex(IMEX_Laplacian_MPIFFT): r""" Example implementing the :math:`N`-dimensional nonlinear Schrödinger equation with periodic boundary conditions @@ -51,130 +47,17 @@ class nonlinearschroedinger_imex(ptype): Journal of Parallel and Distributed Computing (2019). """ - dtype_u = mesh - dtype_f = imex_mesh - - def __init__(self, nvars=None, spectral=False, L=2 * np.pi, c=1.0, comm=MPI.COMM_WORLD): + def __init__(self, c=1.0, **kwargs): """Initialization routine""" - - if nvars is None: - nvars = (128, 128) - - if not L == 2.0 * np.pi: - raise ProblemError(f'Setup not implemented, L has to be 2pi, got {L}') + super().__init__(L=2 * np.pi, alpha=1j, dtype='D', **kwargs) if not (c == 0.0 or c == 1.0): raise ProblemError(f'Setup not implemented, c has to be 0 or 1, got {c}') + self._makeAttributeAndRegister('c', localVars=locals(), readOnly=True) - if not (isinstance(nvars, tuple) and len(nvars) > 1): - raise ProblemError('Need at least two dimensions') - - # Creating FFT structure - self.ndim = len(nvars) - axes = tuple(range(self.ndim)) - self.fft = PFFT(comm, list(nvars), axes=axes, dtype=np.complex128, collapse=True) - - # get test data to figure out type and dimensions - tmp_u = newDistArray(self.fft, spectral) - - L = np.array([L] * self.ndim, dtype=float) - - # invoke super init, passing the communicator and the local dimensions as init - super(nonlinearschroedinger_imex, self).__init__(init=(tmp_u.shape, comm, tmp_u.dtype)) - self._makeAttributeAndRegister('nvars', 'spectral', 'L', 'c', 'comm', localVars=locals(), readOnly=True) - - # get local mesh - X = np.ogrid[self.fft.local_slice(False)] - N = self.fft.global_shape() - for i in range(len(N)): - X[i] = X[i] * self.L[i] / N[i] - self.X = [np.broadcast_to(x, self.fft.shape(False)) for x in X] - - # get local wavenumbers and Laplace operator - s = self.fft.local_slice() - N = self.fft.global_shape() - k = [np.fft.fftfreq(n, 1.0 / n).astype(int) for n in N] - K = [ki[si] for ki, si in zip(k, s)] - Ks = np.meshgrid(*K, indexing='ij', sparse=True) - Lp = 2 * np.pi / self.L - for i in range(self.ndim): - Ks[i] = (Ks[i] * Lp[i]).astype(float) - K = [np.broadcast_to(k, self.fft.shape(True)) for k in Ks] - K = np.array(K).astype(float) - self.K2 = np.sum(K * K, 0, dtype=float) - - # Need this for diagnostics - self.dx = self.L / nvars[0] - self.dy = self.L / nvars[1] - - # work counters - self.work_counters['rhs'] = WorkCounter() - - def eval_f(self, u, t): - """ - Routine to evaluate the right-hand side of the problem. - - Parameters - ---------- - u : dtype_u - Current values of the numerical solution. - t : float - Current time at which the numerical solution is computed. - - Returns - ------- - f : dtype_f - The right-hand side of the problem. - """ - - f = self.dtype_f(self.init) - - if self.spectral: - f.impl = -self.K2 * 1j * u - tmp = self.fft.backward(u) - tmpf = self.ndim * self.c * 2j * np.absolute(tmp) ** 2 * tmp - f.expl[:] = self.fft.forward(tmpf) - - else: - u_hat = self.fft.forward(u) - lap_u_hat = -self.K2 * 1j * u_hat - f.impl[:] = self.fft.backward(lap_u_hat, f.impl) - f.expl = self.ndim * self.c * 2j * np.absolute(u) ** 2 * u - - self.work_counters['rhs']() - return f - - def solve_system(self, rhs, factor, u0, t): - """ - Simple FFT solver for the diffusion part. - - Parameters - ---------- - rhs : dtype_f - Right-hand side for the linear system. - factor : float - Abbrev. for the node-to-node stepsize (or any other factor required). - u0 : dtype_u - Initial guess for the iterative solver (not used here so far). - t : float - Current time (e.g. for time-dependent BCs). - - Returns - ------- - me : dtype_u - The solution as mesh. - """ - - if self.spectral: - me = rhs / (1.0 + factor * self.K2 * 1j) - - else: - me = self.dtype_u(self.init) - rhs_hat = self.fft.forward(rhs) - rhs_hat /= 1.0 + factor * self.K2 * 1j - me[:] = self.fft.backward(rhs_hat) - - return me + def _eval_explicit_part(self, u, t, f_expl): + f_expl[:] = self.ndim * self.c * 2j * self.xp.absolute(u) ** 2 * u + return f_expl def u_exact(self, t, **kwargs): r""" @@ -198,9 +81,9 @@ def u_exact(self, t, **kwargs): def nls_exact_1D(t, x, c): ae = 1.0 / np.sqrt(2.0) * np.exp(1j * t) if c != 0: - u = ae * ((np.cosh(t) + 1j * np.sinh(t)) / (np.cosh(t) - 1.0 / np.sqrt(2.0) * np.cos(x)) - 1.0) + u = ae * ((np.cosh(t) + 1j * np.sinh(t)) / (np.cosh(t) - 1.0 / np.sqrt(2.0) * self.xp.cos(x)) - 1.0) else: - u = np.sin(x) * np.exp(-t * 1j) + u = self.xp.sin(x) * np.exp(-t * 1j) return u @@ -261,13 +144,13 @@ def eval_f(self, u, t): if self.spectral: tmp = self.fft.backward(u) - tmpf = self.ndim * self.c * 2j * np.absolute(tmp) ** 2 * tmp + tmpf = self.ndim * self.c * 2j * self.xp.absolute(tmp) ** 2 * tmp f[:] = -self.K2 * 1j * u + self.fft.forward(tmpf) else: u_hat = self.fft.forward(u) lap_u_hat = -self.K2 * 1j * u_hat - f[:] = self.fft.backward(lap_u_hat) + self.ndim * self.c * 2j * np.absolute(u) ** 2 * u + f[:] = self.fft.backward(lap_u_hat) + self.ndim * self.c * 2j * self.xp.absolute(u) ** 2 * u self.work_counters['rhs']() return f diff --git a/pySDC/implementations/problem_classes/generic_MPIFFT_Laplacian.py b/pySDC/implementations/problem_classes/generic_MPIFFT_Laplacian.py new file mode 100644 index 0000000000..9e81378b07 --- /dev/null +++ b/pySDC/implementations/problem_classes/generic_MPIFFT_Laplacian.py @@ -0,0 +1,177 @@ +import numpy as np +from mpi4py import MPI +from mpi4py_fft import PFFT + +from pySDC.core.Errors import ProblemError +from pySDC.core.Problem import ptype, WorkCounter +from pySDC.implementations.datatype_classes.mesh import mesh, imex_mesh + +from mpi4py_fft import newDistArray + + +class IMEX_Laplacian_MPIFFT(ptype): + r""" + Generic base class for IMEX problems using a spectral method to solve the Laplacian implicitly and a possible rest + explicitly. The FFTs are done with``mpi4py-fft`` [1]_. + + Parameters + ---------- + nvars : tuple, optional + Spatial resolution + spectral : bool, optional + If True, the solution is computed in spectral space. + L : float, optional + Denotes the period of the function to be approximated for the Fourier transform. + alpha : float, optional + Multiplicative factor before the Laplacian + comm : MPI.COMM_World + Communicator for parallelisation. + + Attributes + ---------- + fft : PFFT + Object for parallel FFT transforms. + X : mesh-grid + Grid coordinates in real space. + K2 : matrix + Laplace operator in spectral space. + + References + ---------- + .. [1] Lisandro Dalcin, Mikael Mortensen, David E. Keyes. Fast parallel multidimensional FFT using advanced MPI. + Journal of Parallel and Distributed Computing (2019). + """ + + dtype_u = mesh + dtype_f = imex_mesh + + xp = np + + def __init__(self, nvars=None, spectral=False, L=2 * np.pi, alpha=1.0, comm=MPI.COMM_WORLD, dtype='d'): + """Initialization routine""" + + if nvars is None: + nvars = (128, 128) + + if not (isinstance(nvars, tuple) and len(nvars) > 1): + raise ProblemError('Need at least two dimensions for distributed FFTs') + + # Creating FFT structure + self.ndim = len(nvars) + axes = tuple(range(self.ndim)) + self.fft = PFFT(comm, list(nvars), axes=axes, dtype=dtype, collapse=True) + + # get test data to figure out type and dimensions + tmp_u = newDistArray(self.fft, spectral) + + L = np.array([L] * self.ndim, dtype=float) + + # invoke super init, passing the communicator and the local dimensions as init + super().__init__(init=(tmp_u.shape, comm, tmp_u.dtype)) + self._makeAttributeAndRegister('nvars', 'spectral', 'L', 'alpha', 'comm', localVars=locals(), readOnly=True) + + # get local mesh + X = self.xp.ogrid[self.fft.local_slice(False)] + N = self.fft.global_shape() + for i in range(len(N)): + X[i] = X[i] * self.L[i] / N[i] + self.X = [self.xp.broadcast_to(x, self.fft.shape(False)) for x in X] + + # get local wavenumbers and Laplace operator + s = self.fft.local_slice() + N = self.fft.global_shape() + k = [self.xp.fft.fftfreq(n, 1.0 / n).astype(int) for n in N] + K = [ki[si] for ki, si in zip(k, s)] + Ks = self.xp.meshgrid(*K, indexing='ij', sparse=True) + Lp = 2 * np.pi / self.L + for i in range(self.ndim): + Ks[i] = (Ks[i] * Lp[i]).astype(float) + K = [self.xp.broadcast_to(k, self.fft.shape(True)) for k in Ks] + K = self.xp.array(K).astype(float) + self.K2 = self.xp.sum(K * K, 0, dtype=float) # Laplacian in spectral space + + # Need this for diagnostics + self.dx = self.L[0] / nvars[0] + self.dy = self.L[1] / nvars[1] + + # work counters + self.work_counters['rhs'] = WorkCounter() + + def eval_f(self, u, t): + """ + Routine to evaluate the right-hand side of the problem. + + Parameters + ---------- + u : dtype_u + Current values of the numerical solution. + t : float + Current time at which the numerical solution is computed. + + Returns + ------- + f : dtype_f + The right-hand side of the problem. + """ + + f = self.dtype_f(self.init) + + f.impl[:] = self._eval_Laplacian(u, f.impl) + + if self.spectral: + tmp = self.fft.backward(u) + tmp[:] = self._eval_explicit_part(tmp, t, tmp) + f.expl[:] = self.fft.forward(tmp) + + else: + f.expl[:] = self._eval_explicit_part(u, t, f.expl) + + self.work_counters['rhs']() + return f + + def _eval_Laplacian(self, u, f_impl): + if self.spectral: + f_impl[:] = -self.alpha * self.K2 * u + else: + u_hat = self.fft.forward(u) + lap_u_hat = -self.alpha * self.K2 * u_hat + f_impl[:] = self.fft.backward(lap_u_hat, f_impl) + return f_impl + + def _eval_explicit_part(self, u, t, f_expl): + return f_expl + + def solve_system(self, rhs, factor, u0, t): + """ + Simple FFT solver for the diffusion part. + + Parameters + ---------- + rhs : dtype_f + Right-hand side for the linear system. + factor : float + Abbrev. for the node-to-node stepsize (or any other factor required). + u0 : dtype_u + Initial guess for the iterative solver (not used here so far). + t : float + Current time (e.g. for time-dependent BCs). + + Returns + ------- + me : dtype_u + The solution as mesh. + """ + me = self.dtype_u(self.init) + me[:] = self._invert_Laplacian(me, factor, rhs) + + return me + + def _invert_Laplacian(self, me, factor, rhs): + if self.spectral: + me[:] = rhs / (1.0 + factor * self.alpha * self.K2) + + else: + rhs_hat = self.fft.forward(rhs) + rhs_hat /= 1.0 + factor * self.alpha * self.K2 + me[:] = self.fft.backward(rhs_hat) + return me diff --git a/pySDC/tests/test_projects/test_AC/test_simple_forcing.py b/pySDC/tests/test_projects/test_AC/test_simple_forcing.py index 0c2a243ce9..c44589d5ca 100644 --- a/pySDC/tests/test_projects/test_AC/test_simple_forcing.py +++ b/pySDC/tests/test_projects/test_AC/test_simple_forcing.py @@ -5,10 +5,18 @@ @pytest.mark.mpi4py -def test_main_serial(): - from pySDC.projects.AllenCahn_Bayreuth.run_simple_forcing_verification import main, visualize_radii +@pytest.mark.parametrize('spectral', [True, False]) +@pytest.mark.parametrize('name', ['AC-test-noforce', 'AC-test-constforce', 'AC-test-timeforce']) +def test_main_serial(name, spectral): + from pySDC.projects.AllenCahn_Bayreuth.run_simple_forcing_verification import run_simulation + + run_simulation(name=name, spectral=spectral, nprocs_space=None) + + +@pytest.mark.mpi4py +def test_visualize_radii(): + from pySDC.projects.AllenCahn_Bayreuth.run_simple_forcing_verification import visualize_radii - main() visualize_radii() From 98c3d99a3a2f24ac636610ddd28d3d41e9143e6e Mon Sep 17 00:00:00 2001 From: Thomas Baumann <39156931+brownbaerchen@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:08:28 +0200 Subject: [PATCH 7/8] Ported Gray-Scott to generic MPI FFT (#412) * Ported Gray-Scott to generic MPI FFT class * `np` -> `xp` * Reverted poor changes --- .../problem_classes/AllenCahn_MPIFFT.py | 24 +- .../problem_classes/GrayScott_MPIFFT.py | 341 ++++++++---------- .../generic_MPIFFT_Laplacian.py | 22 +- .../test_problems/test_GrayScottMPIFFT.py | 58 +++ 4 files changed, 225 insertions(+), 220 deletions(-) create mode 100644 pySDC/tests/test_problems/test_GrayScottMPIFFT.py diff --git a/pySDC/implementations/problem_classes/AllenCahn_MPIFFT.py b/pySDC/implementations/problem_classes/AllenCahn_MPIFFT.py index f0aa47d89e..577a7e2646 100644 --- a/pySDC/implementations/problem_classes/AllenCahn_MPIFFT.py +++ b/pySDC/implementations/problem_classes/AllenCahn_MPIFFT.py @@ -133,18 +133,18 @@ def u_exact(self, t): if self.init_type == 'circle': r2 = (self.X[0] - 0.5) ** 2 + (self.X[1] - 0.5) ** 2 if self.spectral: - tmp = 0.5 * (1.0 + np.tanh((self.radius - np.sqrt(r2)) / (np.sqrt(2) * self.eps))) + tmp = 0.5 * (1.0 + self.xp.tanh((self.radius - self.xp.sqrt(r2)) / (np.sqrt(2) * self.eps))) me[:] = self.fft.forward(tmp) else: - me[:] = 0.5 * (1.0 + np.tanh((self.radius - np.sqrt(r2)) / (np.sqrt(2) * self.eps))) + me[:] = 0.5 * (1.0 + self.xp.tanh((self.radius - self.xp.sqrt(r2)) / (np.sqrt(2) * self.eps))) elif self.init_type == 'circle_rand': ndim = len(me.shape) - L = int(self.L) + L = int(self.L[0]) # get random radii for circles/spheres - np.random.seed(1) + self.xp.random.seed(1) lbound = 3.0 * self.eps ubound = 0.5 - self.eps - rand_radii = (ubound - lbound) * np.random.random_sample(size=tuple([L] * ndim)) + lbound + rand_radii = (ubound - lbound) * self.xp.random.random_sample(size=tuple([L] * ndim)) + lbound # distribute circles/spheres tmp = newDistArray(self.fft, False) if ndim == 2: @@ -153,14 +153,14 @@ def u_exact(self, t): # build radius r2 = (self.X[0] + i - L + 0.5) ** 2 + (self.X[1] + j - L + 0.5) ** 2 # add this blob, shifted by 1 to avoid issues with adding up negative contributions - tmp += np.tanh((rand_radii[i, j] - np.sqrt(r2)) / (np.sqrt(2) * self.eps)) + 1 + tmp += self.xp.tanh((rand_radii[i, j] - np.sqrt(r2)) / (np.sqrt(2) * self.eps)) + 1 # normalize to [0,1] tmp *= 0.5 - assert np.all(tmp <= 1.0) + assert self.xp.all(tmp <= 1.0) if self.spectral: me[:] = self.fft.forward(tmp) else: - me[:] = tmp[:] + self.xp.copyto(me, tmp) else: raise NotImplementedError('type of initial value not implemented, got %s' % self.init_type) @@ -219,14 +219,14 @@ def eval_f(self, u, t): tmpf = self.dtype_f(self.init, val=0.0) # build sum over RHS without driving force - Rt_local = float(np.sum(self.fft.backward(f.impl) + tmpf)) + Rt_local = float(self.xp.sum(self.fft.backward(f.impl) + tmpf)) if self.comm is not None: Rt_global = self.comm.allreduce(sendobj=Rt_local, op=MPI.SUM) else: Rt_global = Rt_local # build sum over driving force term - Ht_local = float(np.sum(6.0 * tmp * (1.0 - tmp))) + Ht_local = float(self.xp.sum(6.0 * tmp * (1.0 - tmp))) if self.comm is not None: Ht_global = self.comm.allreduce(sendobj=Ht_local, op=MPI.SUM) else: @@ -247,14 +247,14 @@ def eval_f(self, u, t): f.expl = -2.0 / self.eps**2 * u * (1.0 - u) * (1.0 - 2.0 * u) # build sum over RHS without driving force - Rt_local = float(np.sum(f.impl + f.expl)) + Rt_local = float(self.xp.sum(f.impl + f.expl)) if self.comm is not None: Rt_global = self.comm.allreduce(sendobj=Rt_local, op=MPI.SUM) else: Rt_global = Rt_local # build sum over driving force term - Ht_local = float(np.sum(6.0 * u * (1.0 - u))) + Ht_local = float(self.xp.sum(6.0 * u * (1.0 - u))) if self.comm is not None: Ht_global = self.comm.allreduce(sendobj=Ht_local, op=MPI.SUM) else: diff --git a/pySDC/implementations/problem_classes/GrayScott_MPIFFT.py b/pySDC/implementations/problem_classes/GrayScott_MPIFFT.py index 0eaa5c109a..293b1c846e 100644 --- a/pySDC/implementations/problem_classes/GrayScott_MPIFFT.py +++ b/pySDC/implementations/problem_classes/GrayScott_MPIFFT.py @@ -1,16 +1,16 @@ -import numpy as np import scipy.sparse as sp from mpi4py import MPI from mpi4py_fft import PFFT from pySDC.core.Errors import ProblemError -from pySDC.core.Problem import ptype +from pySDC.core.Problem import ptype, WorkCounter from pySDC.implementations.datatype_classes.mesh import mesh, imex_mesh, comp2_mesh +from pySDC.implementations.problem_classes.generic_MPIFFT_Laplacian import IMEX_Laplacian_MPIFFT from mpi4py_fft import newDistArray -class grayscott_imex_diffusion(ptype): +class grayscott_imex_diffusion(IMEX_Laplacian_MPIFFT): r""" The Gray-Scott system [1]_ describes a reaction-diffusion process of two substances :math:`u` and :math:`v`, where they diffuse over time. During the reaction :math:`u` is used up with overall decay rate :math:`B`, @@ -71,68 +71,21 @@ class grayscott_imex_diffusion(ptype): .. [3] https://www.chebfun.org/examples/pde/GrayScott.html """ - dtype_u = mesh - dtype_f = imex_mesh + def __init__(self, Du=1.0, Dv=0.01, A=0.09, B=0.086, **kwargs): + kwargs['L'] = 2.0 + super().__init__(dtype='d', alpha=1.0, x0=-kwargs['L'] / 2.0, **kwargs) - def __init__(self, nvars=None, Du=1.0, Dv=0.01, A=0.09, B=0.086, spectral=None, L=2.0, comm=MPI.COMM_WORLD): - """Initialization routine""" - nvars = (127, 127) if nvars is None else nvars - if not (isinstance(nvars, tuple) and len(nvars) > 1): - raise ProblemError('Need at least two dimensions') - - # Creating FFT structure - self.ndim = len(nvars) - axes = tuple(range(self.ndim)) - self.fft = PFFT( - comm, - list(nvars), - axes=axes, - dtype=np.float64, - collapse=True, - backend='fftw', - ) - - # get test data to figure out type and dimensions - tmp_u = newDistArray(self.fft, spectral) - - # add two components to contain field and temperature - self.ncomp = 2 - sizes = tmp_u.shape + (self.ncomp,) - - # invoke super init, passing the communicator and the local dimensions as init - super().__init__(init=(sizes, comm, tmp_u.dtype)) - self._makeAttributeAndRegister( - 'nvars', 'Du', 'Dv', 'A', 'B', 'spectral', 'L', 'comm', localVars=locals(), readOnly=True - ) - - L = np.array([self.L] * self.ndim, dtype=float) - - # get local mesh - X = np.ogrid[self.fft.local_slice(False)] - N = self.fft.global_shape() - for i in range(len(N)): - X[i] = -L[i] / 2 + (X[i] * L[i] / N[i]) - self.X = [np.broadcast_to(x, self.fft.shape(False)) for x in X] - - # get local wavenumbers and Laplace operator - s = self.fft.local_slice() - N = self.fft.global_shape() - k = [np.fft.fftfreq(n, 1.0 / n).astype(int) for n in N[:-1]] - k.append(np.fft.rfftfreq(N[-1], 1.0 / N[-1]).astype(int)) - K = [ki[si] for ki, si in zip(k, s)] - Ks = np.meshgrid(*K, indexing='ij', sparse=True) - Lp = 2 * np.pi / L - for i in range(self.ndim): - Ks[i] = (Ks[i] * Lp[i]).astype(float) - K = [np.broadcast_to(k, self.fft.shape(True)) for k in Ks] - K = np.array(K).astype(float) - self.K2 = np.sum(K * K, 0, dtype=float) - self.Ku = -self.K2 * self.Du - self.Kv = -self.K2 * self.Dv - - # Need this for diagnostics - self.dx = self.L / nvars[0] - self.dy = self.L / nvars[1] + # prepare the array with two components + shape = (2,) + (self.init[0]) + self.iU = 0 + self.iV = 1 + self.init = (shape, self.comm, self.xp.dtype('float')) + + self._makeAttributeAndRegister('Du', 'Dv', 'A', 'B', localVars=locals(), readOnly=True) + + # prepare "Laplacians" + self.Ku = -self.Du * self.K2 + self.Kv = -self.Dv * self.K2 def eval_f(self, u, t): """ @@ -154,27 +107,28 @@ def eval_f(self, u, t): f = self.dtype_f(self.init) if self.spectral: - f.impl[..., 0] = self.Ku * u[..., 0] - f.impl[..., 1] = self.Kv * u[..., 1] + f.impl[0, ...] = self.Ku * u[0, ...] + f.impl[1, ...] = self.Kv * u[1, ...] tmpu = newDistArray(self.fft, False) tmpv = newDistArray(self.fft, False) - tmpu[:] = self.fft.backward(u[..., 0], tmpu) - tmpv[:] = self.fft.backward(u[..., 1], tmpv) + tmpu[:] = self.fft.backward(u[0, ...], tmpu) + tmpv[:] = self.fft.backward(u[1, ...], tmpv) tmpfu = -tmpu * tmpv**2 + self.A * (1 - tmpu) tmpfv = tmpu * tmpv**2 - self.B * tmpv - f.expl[..., 0] = self.fft.forward(tmpfu) - f.expl[..., 1] = self.fft.forward(tmpfv) + f.expl[0, ...] = self.fft.forward(tmpfu) + f.expl[1, ...] = self.fft.forward(tmpfv) else: - u_hat = self.fft.forward(u[..., 0]) + u_hat = self.fft.forward(u[0, ...]) lap_u_hat = self.Ku * u_hat - f.impl[..., 0] = self.fft.backward(lap_u_hat, f.impl[..., 0]) - u_hat = self.fft.forward(u[..., 1]) + f.impl[0, ...] = self.fft.backward(lap_u_hat, f.impl[0, ...]) + u_hat = self.fft.forward(u[1, ...]) lap_u_hat = self.Kv * u_hat - f.impl[..., 1] = self.fft.backward(lap_u_hat, f.impl[..., 1]) - f.expl[..., 0] = -u[..., 0] * u[..., 1] ** 2 + self.A * (1 - u[..., 0]) - f.expl[..., 1] = u[..., 0] * u[..., 1] ** 2 - self.B * u[..., 1] + f.impl[1, ...] = self.fft.backward(lap_u_hat, f.impl[1, ...]) + f.expl[0, ...] = -u[0, ...] * u[1, ...] ** 2 + self.A * (1 - u[0, ...]) + f.expl[1, ...] = u[0, ...] * u[1, ...] ** 2 - self.B * u[1, ...] + self.work_counters['rhs']() return f def solve_system(self, rhs, factor, u0, t): @@ -200,16 +154,16 @@ def solve_system(self, rhs, factor, u0, t): me = self.dtype_u(self.init) if self.spectral: - me[..., 0] = rhs[..., 0] / (1.0 - factor * self.Ku) - me[..., 1] = rhs[..., 1] / (1.0 - factor * self.Kv) + me[0, ...] = rhs[0, ...] / (1.0 - factor * self.Ku) + me[1, ...] = rhs[1, ...] / (1.0 - factor * self.Kv) else: - rhs_hat = self.fft.forward(rhs[..., 0]) + rhs_hat = self.fft.forward(rhs[0, ...]) rhs_hat /= 1.0 - factor * self.Ku - me[..., 0] = self.fft.backward(rhs_hat, me[..., 0]) - rhs_hat = self.fft.forward(rhs[..., 1]) + me[0, ...] = self.fft.backward(rhs_hat, me[0, ...]) + rhs_hat = self.fft.forward(rhs[1, ...]) rhs_hat /= 1.0 - factor * self.Kv - me[..., 1] = self.fft.backward(rhs_hat, me[..., 1]) + me[1, ...] = self.fft.backward(rhs_hat, me[1, ...]) return me @@ -234,19 +188,13 @@ def u_exact(self, t): # This assumes that the box is [-L/2, L/2]^2 if self.spectral: - tmp = 1.0 - np.exp(-80.0 * ((self.X[0] + 0.05) ** 2 + (self.X[1] + 0.02) ** 2)) - me[..., 0] = self.fft.forward(tmp) - tmp = np.exp(-80.0 * ((self.X[0] - 0.05) ** 2 + (self.X[1] - 0.02) ** 2)) - me[..., 1] = self.fft.forward(tmp) + tmp = 1.0 - self.xp.exp(-80.0 * ((self.X[0] + 0.05) ** 2 + (self.X[1] + 0.02) ** 2)) + me[0, ...] = self.fft.forward(tmp) + tmp = self.xp.exp(-80.0 * ((self.X[0] - 0.05) ** 2 + (self.X[1] - 0.02) ** 2)) + me[1, ...] = self.fft.forward(tmp) else: - me[..., 0] = 1.0 - np.exp(-80.0 * ((self.X[0] + 0.05) ** 2 + (self.X[1] + 0.02) ** 2)) - me[..., 1] = np.exp(-80.0 * ((self.X[0] - 0.05) ** 2 + (self.X[1] - 0.02) ** 2)) - - # tmpu = np.load('data/u_0001.npy') - # tmpv = np.load('data/v_0001.npy') - # - # me[..., 0] = self.fft.forward(tmpu) - # me[..., 1] = self.fft.forward(tmpv) + me[0, ...] = 1.0 - self.xp.exp(-80.0 * ((self.X[0] + 0.05) ** 2 + (self.X[1] + 0.02) ** 2)) + me[1, ...] = self.xp.exp(-80.0 * ((self.X[0] - 0.05) ** 2 + (self.X[1] - 0.02) ** 2)) return me @@ -272,10 +220,8 @@ class grayscott_imex_linear(grayscott_imex_diffusion): part is computed in an explicit way). """ - def __init__(self, nvars=None, Du=1.0, Dv=0.01, A=0.09, B=0.086, spectral=None, L=2.0, comm=MPI.COMM_WORLD): - """Initialization routine""" - nvars = (127, 127) if nvars is None else nvars - super().__init__(nvars, Du, Dv, A, B, spectral, L, comm) + def __init__(self, **kwargs): + super().__init__(**kwargs) self.Ku -= self.A self.Kv -= self.B @@ -299,27 +245,28 @@ def eval_f(self, u, t): f = self.dtype_f(self.init) if self.spectral: - f.impl[..., 0] = self.Ku * u[..., 0] - f.impl[..., 1] = self.Kv * u[..., 1] + f.impl[0, ...] = self.Ku * u[0, ...] + f.impl[1, ...] = self.Kv * u[1, ...] tmpu = newDistArray(self.fft, False) tmpv = newDistArray(self.fft, False) - tmpu[:] = self.fft.backward(u[..., 0], tmpu) - tmpv[:] = self.fft.backward(u[..., 1], tmpv) + tmpu[:] = self.fft.backward(u[0, ...], tmpu) + tmpv[:] = self.fft.backward(u[1, ...], tmpv) tmpfu = -tmpu * tmpv**2 + self.A tmpfv = tmpu * tmpv**2 - f.expl[..., 0] = self.fft.forward(tmpfu) - f.expl[..., 1] = self.fft.forward(tmpfv) + f.expl[0, ...] = self.fft.forward(tmpfu) + f.expl[1, ...] = self.fft.forward(tmpfv) else: - u_hat = self.fft.forward(u[..., 0]) + u_hat = self.fft.forward(u[0, ...]) lap_u_hat = self.Ku * u_hat - f.impl[..., 0] = self.fft.backward(lap_u_hat, f.impl[..., 0]) - u_hat = self.fft.forward(u[..., 1]) + f.impl[0, ...] = self.fft.backward(lap_u_hat, f.impl[0, ...]) + u_hat = self.fft.forward(u[1, ...]) lap_u_hat = self.Kv * u_hat - f.impl[..., 1] = self.fft.backward(lap_u_hat, f.impl[..., 1]) - f.expl[..., 0] = -u[..., 0] * u[..., 1] ** 2 + self.A - f.expl[..., 1] = u[..., 0] * u[..., 1] ** 2 + f.impl[1, ...] = self.fft.backward(lap_u_hat, f.impl[1, ...]) + f.expl[0, ...] = -u[0, ...] * u[1, ...] ** 2 + self.A + f.expl[1, ...] = u[0, ...] * u[1, ...] ** 2 + self.work_counters['rhs']() return f @@ -387,22 +334,18 @@ class grayscott_mi_diffusion(grayscott_imex_diffusion): def __init__( self, - nvars=None, - Du=1.0, - Dv=0.01, - A=0.09, - B=0.086, - spectral=None, newton_maxiter=100, newton_tol=1e-12, - L=2.0, - comm=MPI.COMM_WORLD, + **kwargs, ): """Initialization routine""" - nvars = (127, 127) if nvars is None else nvars - super().__init__(nvars, Du, Dv, A, B, spectral, L, comm) + super().__init__(**kwargs) # This may not run in parallel yet.. assert self.comm.Get_size() == 1 + self.work_counters['newton'] = WorkCounter() + self.Ku = -self.Du * self.K2 + self.Kv = -self.Dv * self.K2 + self._makeAttributeAndRegister('newton_maxiter', 'newton_tol', localVars=locals(), readOnly=False) def eval_f(self, u, t): """ @@ -424,27 +367,28 @@ def eval_f(self, u, t): f = self.dtype_f(self.init) if self.spectral: - f.comp1[..., 0] = self.Ku * u[..., 0] - f.comp1[..., 1] = self.Kv * u[..., 1] + f.comp1[0, ...] = self.Ku * u[0, ...] + f.comp1[1, ...] = self.Kv * u[1, ...] tmpu = newDistArray(self.fft, False) tmpv = newDistArray(self.fft, False) - tmpu[:] = self.fft.backward(u[..., 0], tmpu) - tmpv[:] = self.fft.backward(u[..., 1], tmpv) + tmpu[:] = self.fft.backward(u[0, ...], tmpu) + tmpv[:] = self.fft.backward(u[1, ...], tmpv) tmpfu = -tmpu * tmpv**2 + self.A * (1 - tmpu) tmpfv = tmpu * tmpv**2 - self.B * tmpv - f.comp2[..., 0] = self.fft.forward(tmpfu) - f.comp2[..., 1] = self.fft.forward(tmpfv) + f.comp2[0, ...] = self.fft.forward(tmpfu) + f.comp2[1, ...] = self.fft.forward(tmpfv) else: - u_hat = self.fft.forward(u[..., 0]) + u_hat = self.fft.forward(u[0, ...]) lap_u_hat = self.Ku * u_hat - f.comp1[..., 0] = self.fft.backward(lap_u_hat, f.comp1[..., 0]) - u_hat = self.fft.forward(u[..., 1]) + f.comp1[0, ...] = self.fft.backward(lap_u_hat, f.comp1[0, ...]) + u_hat = self.fft.forward(u[1, ...]) lap_u_hat = self.Kv * u_hat - f.comp1[..., 1] = self.fft.backward(lap_u_hat, f.comp1[..., 1]) - f.comp2[..., 0] = -u[..., 0] * u[..., 1] ** 2 + self.A * (1 - u[..., 0]) - f.comp2[..., 1] = u[..., 0] * u[..., 1] ** 2 - self.B * u[..., 1] + f.comp1[1, ...] = self.fft.backward(lap_u_hat, f.comp1[1, ...]) + f.comp2[0, ...] = -u[0, ...] * u[1, ...] ** 2 + self.A * (1 - u[0, ...]) + f.comp2[1, ...] = u[0, ...] * u[1, ...] ** 2 - self.B * u[1, ...] + self.work_counters['rhs']() return f def solve_system_1(self, rhs, factor, u0, t): @@ -468,7 +412,7 @@ def solve_system_1(self, rhs, factor, u0, t): The solution as mesh. """ - me = super(grayscott_mi_diffusion, self).solve_system(rhs, factor, u0, t) + me = super().solve_system(rhs, factor, u0, t) return me def solve_system_2(self, rhs, factor, u0, t): @@ -496,18 +440,18 @@ def solve_system_2(self, rhs, factor, u0, t): if self.spectral: tmpu = newDistArray(self.fft, False) tmpv = newDistArray(self.fft, False) - tmpu[:] = self.fft.backward(u[..., 0], tmpu) - tmpv[:] = self.fft.backward(u[..., 1], tmpv) + tmpu[:] = self.fft.backward(u[0, ...], tmpu) + tmpv[:] = self.fft.backward(u[1, ...], tmpv) tmprhsu = newDistArray(self.fft, False) tmprhsv = newDistArray(self.fft, False) - tmprhsu[:] = self.fft.backward(rhs[..., 0], tmprhsu) - tmprhsv[:] = self.fft.backward(rhs[..., 1], tmprhsv) + tmprhsu[:] = self.fft.backward(rhs[0, ...], tmprhsu) + tmprhsv[:] = self.fft.backward(rhs[1, ...], tmprhsv) else: - tmpu = u[..., 0] - tmpv = u[..., 1] - tmprhsu = rhs[..., 0] - tmprhsv = rhs[..., 1] + tmpu = u[0, ...] + tmpv = u[1, ...] + tmprhsu = rhs[0, ...] + tmprhsv = rhs[1, ...] # start newton iteration n = 0 @@ -519,7 +463,7 @@ def solve_system_2(self, rhs, factor, u0, t): tmpgv = tmpv - tmprhsv - factor * (tmpu * tmpv**2 - self.B * tmpv) # if g is close to 0, then we are done - res = max(np.linalg.norm(tmpgu, np.inf), np.linalg.norm(tmpgv, np.inf)) + res = max(self.xp.linalg.norm(tmpgu, self.xp.inf), self.xp.linalg.norm(tmpgv, self.xp.inf)) if res < self.newton_tol: break @@ -530,17 +474,19 @@ def solve_system_2(self, rhs, factor, u0, t): dg11 = 1 - factor * (2 * tmpu * tmpv - self.B) # interleave and unravel to put into sparse matrix - dg00I = np.ravel(np.kron(dg00, np.array([1, 0]))) - dg01I = np.ravel(np.kron(dg01, np.array([1, 0]))) - dg10I = np.ravel(np.kron(dg10, np.array([1, 0]))) - dg11I = np.ravel(np.kron(dg11, np.array([0, 1]))) + dg00I = self.xp.ravel(self.xp.kron(dg00, self.xp.array([1, 0]))) + dg01I = self.xp.ravel(self.xp.kron(dg01, self.xp.array([1, 0]))) + dg10I = self.xp.ravel(self.xp.kron(dg10, self.xp.array([1, 0]))) + dg11I = self.xp.ravel(self.xp.kron(dg11, self.xp.array([0, 1]))) # put into sparse matrix dg = sp.diags(dg00I, offsets=0) + sp.diags(dg11I, offsets=0) dg += sp.diags(dg01I, offsets=1, shape=dg.shape) + sp.diags(dg10I, offsets=-1, shape=dg.shape) # interleave g terms to apply inverse to it - g = np.kron(tmpgu.flatten(), np.array([1, 0])) + np.kron(tmpgv.flatten(), np.array([0, 1])) + g = self.xp.kron(tmpgu.flatten(), self.xp.array([1, 0])) + self.xp.kron( + tmpgv.flatten(), self.xp.array([0, 1]) + ) # invert dg matrix b = sp.linalg.spsolve(dg, g) # update real space vectors @@ -549,24 +495,23 @@ def solve_system_2(self, rhs, factor, u0, t): # increase iteration count n += 1 + self.work_counters['newton']() - if np.isnan(res) and self.stop_at_nan: + if self.xp.isnan(res) and self.stop_at_nan: raise ProblemError('Newton got nan after %i iterations, aborting...' % n) - elif np.isnan(res): + elif self.xp.isnan(res): self.logger.warning('Newton got nan after %i iterations...' % n) if n == self.newton_maxiter: self.logger.warning('Newton did not converge after %i iterations, error is %s' % (n, res)) - # self.newton_ncalls += 1 - # self.newton_itercount += n me = self.dtype_u(self.init) if self.spectral: - me[..., 0] = self.fft.forward(tmpu) - me[..., 1] = self.fft.forward(tmpv) + me[0, ...] = self.fft.forward(tmpu) + me[1, ...] = self.fft.forward(tmpv) else: - me[..., 0] = tmpu - me[..., 1] = tmpv + me[0, ...] = tmpu + me[1, ...] = tmpv return me @@ -595,22 +540,18 @@ class grayscott_mi_linear(grayscott_imex_linear): def __init__( self, - nvars=None, - Du=1.0, - Dv=0.01, - A=0.09, - B=0.086, - spectral=None, newton_maxiter=100, newton_tol=1e-12, - L=2.0, - comm=MPI.COMM_WORLD, + **kwargs, ): """Initialization routine""" - nvars = (127, 127) if nvars is None else nvars - super().__init__(nvars, Du, Dv, A, B, spectral, L, comm) + super().__init__(**kwargs) # This may not run in parallel yet.. assert self.comm.Get_size() == 1 + self.work_counters['newton'] = WorkCounter() + self.Ku = -self.Du * self.K2 - self.A + self.Kv = -self.Dv * self.K2 - self.B + self._makeAttributeAndRegister('newton_maxiter', 'newton_tol', localVars=locals(), readOnly=False) def eval_f(self, u, t): """ @@ -632,27 +573,28 @@ def eval_f(self, u, t): f = self.dtype_f(self.init) if self.spectral: - f.comp1[..., 0] = self.Ku * u[..., 0] - f.comp1[..., 1] = self.Kv * u[..., 1] + f.comp1[0, ...] = self.Ku * u[0, ...] + f.comp1[1, ...] = self.Kv * u[1, ...] tmpu = newDistArray(self.fft, False) tmpv = newDistArray(self.fft, False) - tmpu[:] = self.fft.backward(u[..., 0], tmpu) - tmpv[:] = self.fft.backward(u[..., 1], tmpv) + tmpu[:] = self.fft.backward(u[0, ...], tmpu) + tmpv[:] = self.fft.backward(u[1, ...], tmpv) tmpfu = -tmpu * tmpv**2 + self.A tmpfv = tmpu * tmpv**2 - f.comp2[..., 0] = self.fft.forward(tmpfu) - f.comp2[..., 1] = self.fft.forward(tmpfv) + f.comp2[0, ...] = self.fft.forward(tmpfu) + f.comp2[1, ...] = self.fft.forward(tmpfv) else: - u_hat = self.fft.forward(u[..., 0]) + u_hat = self.fft.forward(u[0, ...]) lap_u_hat = self.Ku * u_hat - f.comp1[..., 0] = self.fft.backward(lap_u_hat, f.comp1[..., 0]) - u_hat = self.fft.forward(u[..., 1]) + f.comp1[0, ...] = self.fft.backward(lap_u_hat, f.comp1[0, ...]) + u_hat = self.fft.forward(u[1, ...]) lap_u_hat = self.Kv * u_hat - f.comp1[..., 1] = self.fft.backward(lap_u_hat, f.comp1[..., 1]) - f.comp2[..., 0] = -u[..., 0] * u[..., 1] ** 2 + self.A - f.comp2[..., 1] = u[..., 0] * u[..., 1] ** 2 + f.comp1[1, ...] = self.fft.backward(lap_u_hat, f.comp1[1, ...]) + f.comp2[0, ...] = -u[0, ...] * u[1, ...] ** 2 + self.A + f.comp2[1, ...] = u[0, ...] * u[1, ...] ** 2 + self.work_counters['rhs']() return f def solve_system_1(self, rhs, factor, u0, t): @@ -704,18 +646,18 @@ def solve_system_2(self, rhs, factor, u0, t): if self.spectral: tmpu = newDistArray(self.fft, False) tmpv = newDistArray(self.fft, False) - tmpu[:] = self.fft.backward(u[..., 0], tmpu) - tmpv[:] = self.fft.backward(u[..., 1], tmpv) + tmpu[:] = self.fft.backward(u[0, ...], tmpu) + tmpv[:] = self.fft.backward(u[1, ...], tmpv) tmprhsu = newDistArray(self.fft, False) tmprhsv = newDistArray(self.fft, False) - tmprhsu[:] = self.fft.backward(rhs[..., 0], tmprhsu) - tmprhsv[:] = self.fft.backward(rhs[..., 1], tmprhsv) + tmprhsu[:] = self.fft.backward(rhs[0, ...], tmprhsu) + tmprhsv[:] = self.fft.backward(rhs[1, ...], tmprhsv) else: - tmpu = u[..., 0] - tmpv = u[..., 1] - tmprhsu = rhs[..., 0] - tmprhsv = rhs[..., 1] + tmpu = u[0, ...] + tmpv = u[1, ...] + tmprhsu = rhs[0, ...] + tmprhsv = rhs[1, ...] # start newton iteration n = 0 @@ -727,7 +669,7 @@ def solve_system_2(self, rhs, factor, u0, t): tmpgv = tmpv - tmprhsv - factor * (tmpu * tmpv**2) # if g is close to 0, then we are done - res = max(np.linalg.norm(tmpgu, np.inf), np.linalg.norm(tmpgv, np.inf)) + res = max(self.xp.linalg.norm(tmpgu, self.xp.inf), self.xp.linalg.norm(tmpgv, self.xp.inf)) if res < self.newton_tol: break @@ -738,17 +680,19 @@ def solve_system_2(self, rhs, factor, u0, t): dg11 = 1 - factor * (2 * tmpu * tmpv) # interleave and unravel to put into sparse matrix - dg00I = np.ravel(np.kron(dg00, np.array([1, 0]))) - dg01I = np.ravel(np.kron(dg01, np.array([1, 0]))) - dg10I = np.ravel(np.kron(dg10, np.array([1, 0]))) - dg11I = np.ravel(np.kron(dg11, np.array([0, 1]))) + dg00I = self.xp.ravel(self.xp.kron(dg00, self.xp.array([1, 0]))) + dg01I = self.xp.ravel(self.xp.kron(dg01, self.xp.array([1, 0]))) + dg10I = self.xp.ravel(self.xp.kron(dg10, self.xp.array([1, 0]))) + dg11I = self.xp.ravel(self.xp.kron(dg11, self.xp.array([0, 1]))) # put into sparse matrix dg = sp.diags(dg00I, offsets=0) + sp.diags(dg11I, offsets=0) dg += sp.diags(dg01I, offsets=1, shape=dg.shape) + sp.diags(dg10I, offsets=-1, shape=dg.shape) # interleave g terms to apply inverse to it - g = np.kron(tmpgu.flatten(), np.array([1, 0])) + np.kron(tmpgv.flatten(), np.array([0, 1])) + g = self.xp.kron(tmpgu.flatten(), self.xp.array([1, 0])) + self.xp.kron( + tmpgv.flatten(), self.xp.array([0, 1]) + ) # invert dg matrix b = sp.linalg.spsolve(dg, g) # update real-space vectors @@ -757,22 +701,21 @@ def solve_system_2(self, rhs, factor, u0, t): # increase iteration count n += 1 + self.work_counters['newton']() - if np.isnan(res) and self.stop_at_nan: + if self.xp.isnan(res) and self.stop_at_nan: raise ProblemError('Newton got nan after %i iterations, aborting...' % n) - elif np.isnan(res): + elif self.xp.isnan(res): self.logger.warning('Newton got nan after %i iterations...' % n) if n == self.newton_maxiter: self.logger.warning('Newton did not converge after %i iterations, error is %s' % (n, res)) - # self.newton_ncalls += 1 - # self.newton_itercount += n me = self.dtype_u(self.init) if self.spectral: - me[..., 0] = self.fft.forward(tmpu) - me[..., 1] = self.fft.forward(tmpv) + me[0, ...] = self.fft.forward(tmpu) + me[1, ...] = self.fft.forward(tmpv) else: - me[..., 0] = tmpu - me[..., 1] = tmpv + me[0, ...] = tmpu + me[1, ...] = tmpv return me diff --git a/pySDC/implementations/problem_classes/generic_MPIFFT_Laplacian.py b/pySDC/implementations/problem_classes/generic_MPIFFT_Laplacian.py index 9e81378b07..eb3b0bffa2 100644 --- a/pySDC/implementations/problem_classes/generic_MPIFFT_Laplacian.py +++ b/pySDC/implementations/problem_classes/generic_MPIFFT_Laplacian.py @@ -47,7 +47,7 @@ class IMEX_Laplacian_MPIFFT(ptype): xp = np - def __init__(self, nvars=None, spectral=False, L=2 * np.pi, alpha=1.0, comm=MPI.COMM_WORLD, dtype='d'): + def __init__(self, nvars=None, spectral=False, L=2 * np.pi, alpha=1.0, comm=MPI.COMM_WORLD, dtype='d', x0=0.0): """Initialization routine""" if nvars is None: @@ -68,13 +68,15 @@ def __init__(self, nvars=None, spectral=False, L=2 * np.pi, alpha=1.0, comm=MPI. # invoke super init, passing the communicator and the local dimensions as init super().__init__(init=(tmp_u.shape, comm, tmp_u.dtype)) - self._makeAttributeAndRegister('nvars', 'spectral', 'L', 'alpha', 'comm', localVars=locals(), readOnly=True) + self._makeAttributeAndRegister( + 'nvars', 'spectral', 'L', 'alpha', 'comm', 'x0', localVars=locals(), readOnly=True + ) # get local mesh X = self.xp.ogrid[self.fft.local_slice(False)] N = self.fft.global_shape() for i in range(len(N)): - X[i] = X[i] * self.L[i] / N[i] + X[i] = x0 + (X[i] * L[i] / N[i]) self.X = [self.xp.broadcast_to(x, self.fft.shape(False)) for x in X] # get local wavenumbers and Laplace operator @@ -129,12 +131,13 @@ def eval_f(self, u, t): self.work_counters['rhs']() return f - def _eval_Laplacian(self, u, f_impl): + def _eval_Laplacian(self, u, f_impl, alpha=None): + alpha = alpha if alpha else self.alpha if self.spectral: - f_impl[:] = -self.alpha * self.K2 * u + f_impl[:] = -alpha * self.K2 * u else: u_hat = self.fft.forward(u) - lap_u_hat = -self.alpha * self.K2 * u_hat + lap_u_hat = -alpha * self.K2 * u_hat f_impl[:] = self.fft.backward(lap_u_hat, f_impl) return f_impl @@ -166,12 +169,13 @@ def solve_system(self, rhs, factor, u0, t): return me - def _invert_Laplacian(self, me, factor, rhs): + def _invert_Laplacian(self, me, factor, rhs, alpha=None): + alpha = alpha if alpha else self.alpha if self.spectral: - me[:] = rhs / (1.0 + factor * self.alpha * self.K2) + me[:] = rhs / (1.0 + factor * alpha * self.K2) else: rhs_hat = self.fft.forward(rhs) - rhs_hat /= 1.0 + factor * self.alpha * self.K2 + rhs_hat /= 1.0 + factor * alpha * self.K2 me[:] = self.fft.backward(rhs_hat) return me diff --git a/pySDC/tests/test_problems/test_GrayScottMPIFFT.py b/pySDC/tests/test_problems/test_GrayScottMPIFFT.py new file mode 100644 index 0000000000..63ed4fd5b0 --- /dev/null +++ b/pySDC/tests/test_problems/test_GrayScottMPIFFT.py @@ -0,0 +1,58 @@ +import pytest + + +@pytest.mark.mpi4py +@pytest.mark.parametrize('name', ['imex_diffusion', 'imex_linear', 'mi_diffusion', 'mi_linear']) +@pytest.mark.parametrize('spectral', [True, False]) +def test_GrayScottMPIFFT(name, spectral): + """ + Test the implementation of the Gray-Scott problem by doing an Euler step forward and then an explicit Euler step + backward to compute something akin to an error. We check that the "local error" has order 2. + + Keep + """ + if name == 'imex_diffusion': + from pySDC.implementations.problem_classes.GrayScott_MPIFFT import grayscott_imex_diffusion as problem_class + elif name == 'imex_linear': + from pySDC.implementations.problem_classes.GrayScott_MPIFFT import grayscott_imex_linear as problem_class + elif name == 'mi_diffusion': + from pySDC.implementations.problem_classes.GrayScott_MPIFFT import grayscott_mi_diffusion as problem_class + elif name == 'mi_linear': + from pySDC.implementations.problem_classes.GrayScott_MPIFFT import grayscott_mi_linear as problem_class + import numpy as np + + prob = problem_class(spectral=spectral, nvars=(127,) * 2) + + dts = np.logspace(-3, -7, 15) + errors = [] + + for dt in dts: + + u0 = prob.u_exact(0) + f0 = prob.eval_f(u0, 0) + + # do an IMEX or multi implicit Euler step forward + if 'solve_system_2' in dir(prob): + _u = prob.solve_system_1(u0, dt, u0, 0) + u1 = prob.solve_system_2(_u, dt, _u, 0) + else: + u1 = prob.solve_system(u0 + dt * f0.expl, dt, u0, 0) + + # do an explicit Euler step backward + f1 = prob.eval_f(u1, dt) + u02 = u1 - dt * (np.sum(f1, axis=0)) + errors += [abs(u0 - u02)] + + errors = np.array(errors) + dts = np.array(dts) + order = np.log(errors[1:] / errors[:-1]) / np.log(dts[1:] / dts[:-1]) + mean_order = np.median(order) + + assert np.isclose(np.median(order), 2, atol=1e-2), f'Expected order 2, but got {mean_order}' + assert prob.work_counters['rhs'].niter == len(errors) * 2 + if 'newton' in prob.work_counters.keys(): + assert prob.work_counters['newton'].niter > 0 + + +if __name__ == '__main__': + test_GrayScottMPIFFT('imex_diffusion', False) From b02181b7aabfb0f9b9d59df89ebb408eb730d2ce Mon Sep 17 00:00:00 2001 From: Daniel Ruprecht Date: Wed, 3 Apr 2024 15:22:18 +0200 Subject: [PATCH 8/8] Update README.md (#413) Added the ExaOcean grant identified and the "Supported by the European Union - NextGenerationEU." clause that they would like us to display. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 270a8d5662..9df0201c10 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ The JU receives support from the European Union's Horizon 2020 research and innovation programme and Belgium, France, Germany, and Switzerland. This project also received funding from the [German Federal Ministry of Education and Research](https://www.bmbf.de/bmbf/en/home/home_node.html) -(BMBF) grant 16HPC047. The project also received help from the +(BMBF) grants 16HPC047 and 16ME0679K. Supported by the European Union - NextGenerationEU. The project also received help from the [Helmholtz Platform for Research Software Engineering - Preparatory Study (HiRSE_PS)](https://www.helmholtz-hirse.de/).