fenicsx-beat 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fenicsx_beat-0.0.1/LICENSE +21 -0
- fenicsx_beat-0.0.1/PKG-INFO +129 -0
- fenicsx_beat-0.0.1/README.md +83 -0
- fenicsx_beat-0.0.1/pyproject.toml +151 -0
- fenicsx_beat-0.0.1/setup.cfg +4 -0
- fenicsx_beat-0.0.1/src/beat/__init__.py +32 -0
- fenicsx_beat-0.0.1/src/beat/base_model.py +268 -0
- fenicsx_beat-0.0.1/src/beat/conductivities.py +118 -0
- fenicsx_beat-0.0.1/src/beat/ecg.py +167 -0
- fenicsx_beat-0.0.1/src/beat/geometry.py +218 -0
- fenicsx_beat-0.0.1/src/beat/monodomain_model.py +98 -0
- fenicsx_beat-0.0.1/src/beat/monodomain_solver.py +92 -0
- fenicsx_beat-0.0.1/src/beat/odesolver.py +256 -0
- fenicsx_beat-0.0.1/src/beat/postprocess.py +57 -0
- fenicsx_beat-0.0.1/src/beat/single_cell.py +156 -0
- fenicsx_beat-0.0.1/src/beat/stimulation.py +74 -0
- fenicsx_beat-0.0.1/src/beat/units.py +10 -0
- fenicsx_beat-0.0.1/src/beat/utils.py +322 -0
- fenicsx_beat-0.0.1/src/fenicsx_beat.egg-info/PKG-INFO +129 -0
- fenicsx_beat-0.0.1/src/fenicsx_beat.egg-info/SOURCES.txt +26 -0
- fenicsx_beat-0.0.1/src/fenicsx_beat.egg-info/dependency_links.txt +1 -0
- fenicsx_beat-0.0.1/src/fenicsx_beat.egg-info/requires.txt +38 -0
- fenicsx_beat-0.0.1/src/fenicsx_beat.egg-info/top_level.txt +1 -0
- fenicsx_beat-0.0.1/tests/test_monodomain.py +147 -0
- fenicsx_beat-0.0.1/tests/test_monodomain_solver.py +217 -0
- fenicsx_beat-0.0.1/tests/test_odesolver.py +42 -0
- fenicsx_beat-0.0.1/tests/test_stimulation.py +105 -0
- fenicsx_beat-0.0.1/tests/test_utils.py +146 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Henrik Finsberg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fenicsx-beat
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Library to run cardiac EP simulations
|
|
5
|
+
Author-email: Henrik Finsberg <henriknf@simula.no>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://finsberg.github.io/fenicsx-beat
|
|
8
|
+
Project-URL: Documentation, https://finsberg.github.io/fenicsx-beat
|
|
9
|
+
Project-URL: Source, https://github.com/finsberg/fenicsx-beat
|
|
10
|
+
Project-URL: Tracker, https://github.com/finsberg/fenicsx-beat/issues
|
|
11
|
+
Keywords: cardiac,electrophysiology
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
Requires-Dist: scipy
|
|
17
|
+
Requires-Dist: scifem>=0.2.14
|
|
18
|
+
Requires-Dist: pint
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest; extra == "test"
|
|
21
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
24
|
+
Provides-Extra: pypi
|
|
25
|
+
Requires-Dist: twine; extra == "pypi"
|
|
26
|
+
Requires-Dist: build; extra == "pypi"
|
|
27
|
+
Provides-Extra: demos
|
|
28
|
+
Requires-Dist: cardiac-geometriesx; extra == "demos"
|
|
29
|
+
Requires-Dist: fenicsx-ldrb; extra == "demos"
|
|
30
|
+
Requires-Dist: gotranx; extra == "demos"
|
|
31
|
+
Provides-Extra: docs
|
|
32
|
+
Requires-Dist: jupyter-book; extra == "docs"
|
|
33
|
+
Requires-Dist: jupytext; extra == "docs"
|
|
34
|
+
Requires-Dist: fenicsx-beat[demos]; extra == "docs"
|
|
35
|
+
Requires-Dist: ipywidgets; extra == "docs"
|
|
36
|
+
Requires-Dist: imageio; extra == "docs"
|
|
37
|
+
Requires-Dist: pyvista[jupyter]; extra == "docs"
|
|
38
|
+
Provides-Extra: all
|
|
39
|
+
Requires-Dist: fenicsx-beat[test]; extra == "all"
|
|
40
|
+
Requires-Dist: fenicsx-beat[docs]; extra == "all"
|
|
41
|
+
Requires-Dist: fenicsx-beat[pypi]; extra == "all"
|
|
42
|
+
Requires-Dist: fenicsx-beat[dev]; extra == "all"
|
|
43
|
+
Requires-Dist: fenicsx-beat[demos]; extra == "all"
|
|
44
|
+
Provides-Extra: testpaths
|
|
45
|
+
Requires-Dist: tests; extra == "testpaths"
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
# fenicsx-beat
|
|
50
|
+
Cardiac electrophysiology simulator in FEniCSx
|
|
51
|
+
|
|
52
|
+
- Source code: https://github.com/finsberg/fenicsx-beat
|
|
53
|
+
- Documentation: https://finsberg.github.io/fenicsx-beat
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
You can install the library with pip
|
|
58
|
+
```
|
|
59
|
+
python3 -m pip install fenicsx-beat
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## Getting started
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from mpi4py import MPI
|
|
67
|
+
import dolfinx
|
|
68
|
+
import ufl
|
|
69
|
+
import beat
|
|
70
|
+
|
|
71
|
+
comm = MPI.COMM_WORLD
|
|
72
|
+
mesh = dolfinx.mesh.create_unit_square(comm, 10, 10, dolfinx.cpp.mesh.CellType.triangle)
|
|
73
|
+
time = dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(0.0))
|
|
74
|
+
# Create stimulus
|
|
75
|
+
stim_expr = ufl.conditional(ufl.And(ufl.ge(time, 0.0), ufl.le(time, 0.5)),200.0, 0.0)
|
|
76
|
+
stim_marker = 1
|
|
77
|
+
cells = dolfinx.mesh.locate_entities(mesh, mesh.topology.dim, lambda x: np.logical_and(x[0] <= 0.5, x[1] <= 0.5))
|
|
78
|
+
stim_tags = dolfinx.mesh.meshtags(
|
|
79
|
+
mesh,
|
|
80
|
+
mesh.topology.dim,
|
|
81
|
+
cells,
|
|
82
|
+
np.full(len(cells), stim_marker, dtype=np.int32),
|
|
83
|
+
)
|
|
84
|
+
dx = ufl.Measure("dx", domain=mesh, subdomain_data=stim_tags)
|
|
85
|
+
I_s = beat.Stimulus(expr=stim_expr, dZ=dx, marker=stim_marker)
|
|
86
|
+
# Create PDE model
|
|
87
|
+
pde = beat.MonodomainModel(time=time, mesh=mesh, M=0.01, I_s=I_s, dx=dx)
|
|
88
|
+
# Define scheme to solve ODE
|
|
89
|
+
def fun(t, states, parameters, dt):
|
|
90
|
+
v, s = states
|
|
91
|
+
a, b = parameters
|
|
92
|
+
values = np.zeros_like(states)
|
|
93
|
+
values[0] = v - a * s * dt
|
|
94
|
+
values[1] = s + b * v * dt
|
|
95
|
+
return values
|
|
96
|
+
# Define ODE solver
|
|
97
|
+
ode_space = dolfinx.fem.functionspace(mesh, ("P", 1))
|
|
98
|
+
parameters = np.array([1.0, 1.0])
|
|
99
|
+
init_states = np.array([0.0, 0.0])
|
|
100
|
+
ode = beat.odesolver.DolfinODESolver(
|
|
101
|
+
v_ode=dolfinx.fem.Function(ode_space),
|
|
102
|
+
v_pde=pde.state,
|
|
103
|
+
fun=fun,
|
|
104
|
+
init_states=states,
|
|
105
|
+
parameters=parameters,
|
|
106
|
+
num_states=2,
|
|
107
|
+
v_index=1,
|
|
108
|
+
)
|
|
109
|
+
# Combine PDE and ODE solver
|
|
110
|
+
solver = beat.MonodomainSplittingSolver(pde=pde, ode=ode)
|
|
111
|
+
# Solve
|
|
112
|
+
T = 5.0
|
|
113
|
+
t = 0.0
|
|
114
|
+
i = 0
|
|
115
|
+
while t < T:
|
|
116
|
+
v = solver.pde.state.x.array
|
|
117
|
+
solver.step((t, t + dt))
|
|
118
|
+
i += 1
|
|
119
|
+
t += dt
|
|
120
|
+
```
|
|
121
|
+

|
|
122
|
+
|
|
123
|
+
See more examples in the [documentation](https://finsberg.github.io/fenicsx-beat)
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
MIT
|
|
127
|
+
|
|
128
|
+
## Need help or having issues
|
|
129
|
+
Please submit an [issue](https://github.com/finsberg/fenicsx-beat/issues)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# fenicsx-beat
|
|
4
|
+
Cardiac electrophysiology simulator in FEniCSx
|
|
5
|
+
|
|
6
|
+
- Source code: https://github.com/finsberg/fenicsx-beat
|
|
7
|
+
- Documentation: https://finsberg.github.io/fenicsx-beat
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
You can install the library with pip
|
|
12
|
+
```
|
|
13
|
+
python3 -m pip install fenicsx-beat
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Getting started
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from mpi4py import MPI
|
|
21
|
+
import dolfinx
|
|
22
|
+
import ufl
|
|
23
|
+
import beat
|
|
24
|
+
|
|
25
|
+
comm = MPI.COMM_WORLD
|
|
26
|
+
mesh = dolfinx.mesh.create_unit_square(comm, 10, 10, dolfinx.cpp.mesh.CellType.triangle)
|
|
27
|
+
time = dolfinx.fem.Constant(mesh, dolfinx.default_scalar_type(0.0))
|
|
28
|
+
# Create stimulus
|
|
29
|
+
stim_expr = ufl.conditional(ufl.And(ufl.ge(time, 0.0), ufl.le(time, 0.5)),200.0, 0.0)
|
|
30
|
+
stim_marker = 1
|
|
31
|
+
cells = dolfinx.mesh.locate_entities(mesh, mesh.topology.dim, lambda x: np.logical_and(x[0] <= 0.5, x[1] <= 0.5))
|
|
32
|
+
stim_tags = dolfinx.mesh.meshtags(
|
|
33
|
+
mesh,
|
|
34
|
+
mesh.topology.dim,
|
|
35
|
+
cells,
|
|
36
|
+
np.full(len(cells), stim_marker, dtype=np.int32),
|
|
37
|
+
)
|
|
38
|
+
dx = ufl.Measure("dx", domain=mesh, subdomain_data=stim_tags)
|
|
39
|
+
I_s = beat.Stimulus(expr=stim_expr, dZ=dx, marker=stim_marker)
|
|
40
|
+
# Create PDE model
|
|
41
|
+
pde = beat.MonodomainModel(time=time, mesh=mesh, M=0.01, I_s=I_s, dx=dx)
|
|
42
|
+
# Define scheme to solve ODE
|
|
43
|
+
def fun(t, states, parameters, dt):
|
|
44
|
+
v, s = states
|
|
45
|
+
a, b = parameters
|
|
46
|
+
values = np.zeros_like(states)
|
|
47
|
+
values[0] = v - a * s * dt
|
|
48
|
+
values[1] = s + b * v * dt
|
|
49
|
+
return values
|
|
50
|
+
# Define ODE solver
|
|
51
|
+
ode_space = dolfinx.fem.functionspace(mesh, ("P", 1))
|
|
52
|
+
parameters = np.array([1.0, 1.0])
|
|
53
|
+
init_states = np.array([0.0, 0.0])
|
|
54
|
+
ode = beat.odesolver.DolfinODESolver(
|
|
55
|
+
v_ode=dolfinx.fem.Function(ode_space),
|
|
56
|
+
v_pde=pde.state,
|
|
57
|
+
fun=fun,
|
|
58
|
+
init_states=states,
|
|
59
|
+
parameters=parameters,
|
|
60
|
+
num_states=2,
|
|
61
|
+
v_index=1,
|
|
62
|
+
)
|
|
63
|
+
# Combine PDE and ODE solver
|
|
64
|
+
solver = beat.MonodomainSplittingSolver(pde=pde, ode=ode)
|
|
65
|
+
# Solve
|
|
66
|
+
T = 5.0
|
|
67
|
+
t = 0.0
|
|
68
|
+
i = 0
|
|
69
|
+
while t < T:
|
|
70
|
+
v = solver.pde.state.x.array
|
|
71
|
+
solver.step((t, t + dt))
|
|
72
|
+
i += 1
|
|
73
|
+
t += dt
|
|
74
|
+
```
|
|
75
|
+

|
|
76
|
+
|
|
77
|
+
See more examples in the [documentation](https://finsberg.github.io/fenicsx-beat)
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
MIT
|
|
81
|
+
|
|
82
|
+
## Need help or having issues
|
|
83
|
+
Please submit an [issue](https://github.com/finsberg/fenicsx-beat/issues)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
[build-system] # Require setuptool version due to https://github.com/pypa/setuptools/issues/2938
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
|
|
4
|
+
[project]
|
|
5
|
+
name = "fenicsx-beat"
|
|
6
|
+
version = "0.0.1"
|
|
7
|
+
description = "Library to run cardiac EP simulations"
|
|
8
|
+
authors = [{name = "Henrik Finsberg", email = "henriknf@simula.no"}]
|
|
9
|
+
license = {text = "MIT"}
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
keywords = ["cardiac", "electrophysiology"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"numpy",
|
|
15
|
+
"scipy",
|
|
16
|
+
"scifem>=0.2.14",
|
|
17
|
+
"pint",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://finsberg.github.io/fenicsx-beat"
|
|
22
|
+
Documentation = "https://finsberg.github.io/fenicsx-beat"
|
|
23
|
+
Source = "https://github.com/finsberg/fenicsx-beat"
|
|
24
|
+
Tracker = "https://github.com/finsberg/fenicsx-beat/issues"
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
test = [
|
|
28
|
+
"pytest",
|
|
29
|
+
"pytest-cov",
|
|
30
|
+
]
|
|
31
|
+
dev = [
|
|
32
|
+
"pre-commit",
|
|
33
|
+
]
|
|
34
|
+
pypi = [
|
|
35
|
+
"twine",
|
|
36
|
+
"build"
|
|
37
|
+
]
|
|
38
|
+
demos = [
|
|
39
|
+
"cardiac-geometriesx",
|
|
40
|
+
"fenicsx-ldrb",
|
|
41
|
+
"gotranx"
|
|
42
|
+
]
|
|
43
|
+
docs = [
|
|
44
|
+
"jupyter-book",
|
|
45
|
+
"jupytext",
|
|
46
|
+
"fenicsx-beat[demos]",
|
|
47
|
+
"ipywidgets",
|
|
48
|
+
"imageio",
|
|
49
|
+
"pyvista[jupyter]",
|
|
50
|
+
]
|
|
51
|
+
all = [
|
|
52
|
+
"fenicsx-beat[test]",
|
|
53
|
+
"fenicsx-beat[docs]",
|
|
54
|
+
"fenicsx-beat[pypi]",
|
|
55
|
+
"fenicsx-beat[dev]",
|
|
56
|
+
"fenicsx-beat[demos]"
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
testpaths = [
|
|
61
|
+
"tests"
|
|
62
|
+
]
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
# Exclude a variety of commonly ignored directories.
|
|
65
|
+
exclude = [
|
|
66
|
+
"demo/*.py",
|
|
67
|
+
".bzr",
|
|
68
|
+
".direnv",
|
|
69
|
+
".eggs",
|
|
70
|
+
".git",
|
|
71
|
+
".hg",
|
|
72
|
+
".mypy_cache",
|
|
73
|
+
".nox",
|
|
74
|
+
".pants.d",
|
|
75
|
+
".pytype",
|
|
76
|
+
".ruff_cache",
|
|
77
|
+
".svn",
|
|
78
|
+
".tox",
|
|
79
|
+
".venv",
|
|
80
|
+
"__pypackages__",
|
|
81
|
+
"_build",
|
|
82
|
+
"buck-out",
|
|
83
|
+
"build",
|
|
84
|
+
"dist",
|
|
85
|
+
"node_modules",
|
|
86
|
+
"venv",
|
|
87
|
+
"demos",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# Same as Black.
|
|
91
|
+
line-length = 100
|
|
92
|
+
|
|
93
|
+
# Allow unused variables when underscore-prefixed.
|
|
94
|
+
|
|
95
|
+
# Assume Python 3.10.
|
|
96
|
+
target-version = "py310"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
[tool.ruff.lint]
|
|
100
|
+
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
|
|
101
|
+
select = ["E", "F", "I"]
|
|
102
|
+
ignore = ["E402", "E741", "E743", "E731"]
|
|
103
|
+
|
|
104
|
+
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
|
105
|
+
fixable = ["A", "B", "C", "D", "E", "F", "I"]
|
|
106
|
+
unfixable = []
|
|
107
|
+
|
|
108
|
+
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
109
|
+
|
|
110
|
+
[tool.ruff.lint.mccabe]
|
|
111
|
+
# Unlike Flake8, default to a complexity level of 10.
|
|
112
|
+
max-complexity = 10
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
[tool.ruff.lint.isort]
|
|
116
|
+
known-third-party = [
|
|
117
|
+
"basix",
|
|
118
|
+
"dolfinx",
|
|
119
|
+
"ffcx",
|
|
120
|
+
"ufl",
|
|
121
|
+
"gmsh",
|
|
122
|
+
"numpy",
|
|
123
|
+
"pytest",
|
|
124
|
+
]
|
|
125
|
+
section-order = [
|
|
126
|
+
"future",
|
|
127
|
+
"standard-library",
|
|
128
|
+
"mpi",
|
|
129
|
+
"third-party",
|
|
130
|
+
"first-party",
|
|
131
|
+
"local-folder",
|
|
132
|
+
]
|
|
133
|
+
[tool.ruff.lint.isort.sections]
|
|
134
|
+
"mpi" = ["mpi4py", "petsc4py"]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
[tool.bumpversion]
|
|
138
|
+
allow_dirty = false
|
|
139
|
+
commit = true
|
|
140
|
+
message = "Bump version: {current_version} → {new_version}"
|
|
141
|
+
tag = true
|
|
142
|
+
sign_tags = false
|
|
143
|
+
tag_name = "v{new_version}"
|
|
144
|
+
tag_message = "Bump version: {current_version} → {new_version}"
|
|
145
|
+
current_version = "0.0.1"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
[[tool.bumpversion.files]]
|
|
149
|
+
filename = "pyproject.toml"
|
|
150
|
+
search = 'version = "{current_version}"'
|
|
151
|
+
replace = 'version = "{new_version}"'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from . import (
|
|
2
|
+
base_model,
|
|
3
|
+
conductivities,
|
|
4
|
+
ecg,
|
|
5
|
+
geometry,
|
|
6
|
+
monodomain_model,
|
|
7
|
+
monodomain_solver,
|
|
8
|
+
odesolver,
|
|
9
|
+
single_cell,
|
|
10
|
+
stimulation,
|
|
11
|
+
utils,
|
|
12
|
+
)
|
|
13
|
+
from .monodomain_model import MonodomainModel
|
|
14
|
+
from .monodomain_solver import MonodomainSplittingSolver
|
|
15
|
+
from .stimulation import Stimulus
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"monodomain_model",
|
|
19
|
+
"odesolver",
|
|
20
|
+
"base_model",
|
|
21
|
+
"MonodomainModel",
|
|
22
|
+
"monodomain_solver",
|
|
23
|
+
"MonodomainSplittingSolver",
|
|
24
|
+
"utils",
|
|
25
|
+
"conductivities",
|
|
26
|
+
"stimulation",
|
|
27
|
+
"geometry",
|
|
28
|
+
"single_cell",
|
|
29
|
+
"stimulation",
|
|
30
|
+
"ecg",
|
|
31
|
+
"Stimulus",
|
|
32
|
+
]
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import logging
|
|
5
|
+
from enum import Enum, auto
|
|
6
|
+
from typing import Any, Literal, NamedTuple, Sequence
|
|
7
|
+
|
|
8
|
+
from petsc4py import PETSc
|
|
9
|
+
|
|
10
|
+
import dolfinx
|
|
11
|
+
import dolfinx.fem.petsc
|
|
12
|
+
import ufl
|
|
13
|
+
from ufl.core.expr import Expr
|
|
14
|
+
|
|
15
|
+
from .stimulation import Stimulus
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Status(str, Enum):
|
|
21
|
+
OK = auto()
|
|
22
|
+
NOT_CONVERGING = auto()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Results(NamedTuple):
|
|
26
|
+
state: dolfinx.fem.Function
|
|
27
|
+
status: Status
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _transform_I_s(
|
|
31
|
+
I_s: Stimulus | Sequence[Stimulus] | ufl.Coefficient | None,
|
|
32
|
+
dZ: ufl.Measure,
|
|
33
|
+
) -> list[Stimulus]:
|
|
34
|
+
if I_s is None:
|
|
35
|
+
return [Stimulus(expr=ufl.zero(), dZ=dZ)]
|
|
36
|
+
if isinstance(I_s, Stimulus):
|
|
37
|
+
return [I_s]
|
|
38
|
+
if isinstance(I_s, ufl.core.expr.Expr):
|
|
39
|
+
return [Stimulus(expr=I_s, dZ=dZ)]
|
|
40
|
+
|
|
41
|
+
# FIXME: Might need more checks here
|
|
42
|
+
return list(I_s)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BaseModel:
|
|
46
|
+
"""
|
|
47
|
+
Base class for models.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
time : dolfinx.fem.Constant
|
|
52
|
+
The current time
|
|
53
|
+
mesh : dolfinx.mesh.Mesh
|
|
54
|
+
The mesh
|
|
55
|
+
dx : ufl.Measure, optional
|
|
56
|
+
The measure for the spatial domain, by default None
|
|
57
|
+
params : dict, optional
|
|
58
|
+
Parameters for the model, by default None
|
|
59
|
+
I_s : Stimulus | Sequence[Stimulus] | ufl.Coefficient, optional
|
|
60
|
+
The stimulus, by default None
|
|
61
|
+
jit_options : dict, optional
|
|
62
|
+
JIT options, by default None
|
|
63
|
+
form_compiler_options : dict, optional
|
|
64
|
+
Form compiler options, by default None
|
|
65
|
+
petsc_options : dict, optional
|
|
66
|
+
PETSc options, by default None
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
time: dolfinx.fem.Constant,
|
|
73
|
+
mesh: dolfinx.mesh.Mesh,
|
|
74
|
+
dx: ufl.Measure | None = None,
|
|
75
|
+
params: dict[str, Any] | None = None,
|
|
76
|
+
I_s: Stimulus | Sequence[Stimulus] | ufl.Coefficient | None = None,
|
|
77
|
+
jit_options: dict[str, Any] | None = None,
|
|
78
|
+
form_compiler_options: dict[str, Any] | None = None,
|
|
79
|
+
petsc_options: dict[str, Any] | None = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
self._mesh = mesh
|
|
82
|
+
self.time = time
|
|
83
|
+
self.dx = dx or ufl.dx(domain=mesh)
|
|
84
|
+
|
|
85
|
+
self.parameters = type(self).default_parameters()
|
|
86
|
+
if params is not None:
|
|
87
|
+
self.parameters.update(params)
|
|
88
|
+
|
|
89
|
+
self._I_s = _transform_I_s(I_s, dZ=self.dx)
|
|
90
|
+
|
|
91
|
+
self._setup_state_space()
|
|
92
|
+
|
|
93
|
+
self._timestep = dolfinx.fem.Constant(mesh, self.parameters["default_timestep"])
|
|
94
|
+
a, L = self.variational_forms(self._timestep)
|
|
95
|
+
self._solver = dolfinx.fem.petsc.LinearProblem(
|
|
96
|
+
a,
|
|
97
|
+
L,
|
|
98
|
+
u=self.state,
|
|
99
|
+
form_compiler_options=form_compiler_options,
|
|
100
|
+
jit_options=jit_options,
|
|
101
|
+
petsc_options=petsc_options,
|
|
102
|
+
)
|
|
103
|
+
dolfinx.fem.petsc.assemble_matrix(self._solver.A, self._solver.a) # type: ignore
|
|
104
|
+
self._solver.A.assemble()
|
|
105
|
+
|
|
106
|
+
@abc.abstractmethod
|
|
107
|
+
def _setup_state_space(self) -> None: ...
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
@abc.abstractmethod
|
|
111
|
+
def state(self) -> dolfinx.fem.Function: ...
|
|
112
|
+
|
|
113
|
+
@abc.abstractmethod
|
|
114
|
+
def assign_previous(self) -> None: ...
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def default_parameters(
|
|
118
|
+
solver_type: Literal["iterative", "direct"] = "direct",
|
|
119
|
+
) -> dict[str, Any]:
|
|
120
|
+
if solver_type == "iterative":
|
|
121
|
+
petsc_options = {
|
|
122
|
+
"ksp_type": "cg",
|
|
123
|
+
# "pc_type": "hypre",
|
|
124
|
+
"pc_type": "petsc_amg",
|
|
125
|
+
"pc_hypre_type": "boomeramg",
|
|
126
|
+
# "ksp_norm_type": "unpreconditioned",
|
|
127
|
+
# "ksp_atol": 1e-15,
|
|
128
|
+
# "ksp_rtol": 1e-10,
|
|
129
|
+
# "ksp_max_it": 10_000,
|
|
130
|
+
# "ksp_error_if_not_converged": False,
|
|
131
|
+
}
|
|
132
|
+
else:
|
|
133
|
+
petsc_options = {
|
|
134
|
+
"ksp_type": "preonly",
|
|
135
|
+
"pc_type": "lu",
|
|
136
|
+
"pc_factor_mat_solver_type": "mumps",
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
"theta": 0.5,
|
|
140
|
+
"degree": 1,
|
|
141
|
+
"family": "Lagrange",
|
|
142
|
+
"default_timestep": 1.0,
|
|
143
|
+
"jit_options": {},
|
|
144
|
+
"form_compiler_options": {},
|
|
145
|
+
"petsc_options": petsc_options,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@abc.abstractmethod
|
|
149
|
+
def variational_forms(self, dt: Expr | float) -> tuple[ufl.Form, ufl.Form]:
|
|
150
|
+
"""Create the variational forms corresponding to the given
|
|
151
|
+
discretization of the given system of equations.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
dt : Expr | float
|
|
156
|
+
The time step
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
tuple[ufl.Form, ufl.Form]
|
|
161
|
+
The variational form and the precondition
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
...
|
|
165
|
+
|
|
166
|
+
def _update_matrices(self):
|
|
167
|
+
"""
|
|
168
|
+
Re-assemble matrix.
|
|
169
|
+
"""
|
|
170
|
+
self._solver.A.zeroEntries()
|
|
171
|
+
dolfinx.fem.petsc.assemble_matrix(self._solver.A, self._solver.a) # type: ignore
|
|
172
|
+
self._solver.A.assemble()
|
|
173
|
+
|
|
174
|
+
def _update_rhs(self):
|
|
175
|
+
"""
|
|
176
|
+
Re-assemble RHS vector
|
|
177
|
+
"""
|
|
178
|
+
with self._solver.b.localForm() as b_loc:
|
|
179
|
+
b_loc.set(0)
|
|
180
|
+
dolfinx.fem.petsc.assemble_vector(self._solver.b, self._solver.L)
|
|
181
|
+
self._solver.b.ghostUpdate(
|
|
182
|
+
addv=PETSc.InsertMode.ADD,
|
|
183
|
+
mode=PETSc.ScatterMode.REVERSE,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def step(self, interval):
|
|
187
|
+
"""
|
|
188
|
+
Perform a single time step.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
interval : tuple[float, float]
|
|
193
|
+
The time interval (T0, T)
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
# timer = dolfin.Timer("PDE Step")
|
|
198
|
+
|
|
199
|
+
# Extract interval and thus time-step
|
|
200
|
+
(t0, t1) = interval
|
|
201
|
+
dt = t1 - t0
|
|
202
|
+
theta = self.parameters["theta"]
|
|
203
|
+
t = t0 + theta * dt
|
|
204
|
+
self.time.value = t
|
|
205
|
+
|
|
206
|
+
# Update matrix and linear solvers etc as needed
|
|
207
|
+
timestep_unchanged = abs(dt - float(self._timestep)) < 1.0e-12
|
|
208
|
+
if not timestep_unchanged:
|
|
209
|
+
self._timestep.value = dt
|
|
210
|
+
self._update_matrices()
|
|
211
|
+
|
|
212
|
+
self._update_rhs()
|
|
213
|
+
# Solve linear system and update ghost values in the solution
|
|
214
|
+
|
|
215
|
+
self._solver.solver.solve(self._solver.b, self.state.x.petsc_vec)
|
|
216
|
+
self.state.x.scatter_forward()
|
|
217
|
+
|
|
218
|
+
def _G_stim(self, w):
|
|
219
|
+
return sum([i.expr * w * i.dz for i in self._I_s])
|
|
220
|
+
|
|
221
|
+
def solve(
|
|
222
|
+
self,
|
|
223
|
+
interval: tuple[float, float],
|
|
224
|
+
dt: float | None = None,
|
|
225
|
+
) -> Results:
|
|
226
|
+
"""
|
|
227
|
+
Solve on the given time interval.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
interval : tuple[float, float]
|
|
232
|
+
The time interval (T0, T)
|
|
233
|
+
dt : float, optional
|
|
234
|
+
The time step, by default None
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
Results
|
|
239
|
+
The results of the solution
|
|
240
|
+
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
# Initial set-up
|
|
244
|
+
# Solve on entire interval if no interval is given.
|
|
245
|
+
(T0, T) = interval
|
|
246
|
+
if dt is None:
|
|
247
|
+
dt = T - T0
|
|
248
|
+
t0 = T0
|
|
249
|
+
t1 = T0 + dt
|
|
250
|
+
|
|
251
|
+
# Step through time steps until at end time
|
|
252
|
+
while True:
|
|
253
|
+
logger.info("Solving on t = (%g, %g)" % (t0, t1))
|
|
254
|
+
self.step((t0, t1))
|
|
255
|
+
|
|
256
|
+
# Yield solutions
|
|
257
|
+
# yield (t0, t1), self.solution_fields()
|
|
258
|
+
|
|
259
|
+
# Break if this is the last step
|
|
260
|
+
if (t1 + dt) > (T + 1e-12):
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
self.assign_previous()
|
|
264
|
+
|
|
265
|
+
t0 = t1
|
|
266
|
+
t1 = t0 + dt
|
|
267
|
+
|
|
268
|
+
return Results(state=self.state, status=Status.OK)
|