Skip to content

Commit f74012f

Browse files
Merge pull request #34 from finitewave/develop
Develop
2 parents 4a05bef + f60a02f commit f74012f

20 files changed

Lines changed: 887 additions & 982 deletions

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
# Changelog
22

3+
## [0.9.3] May 2026
4+
5+
### Added
6+
7+
- Initial conditions support. Model variables can now be initialized either as scalars (uniform across the tissue)
8+
or as arrays (node-wise values). All initial condition fields use the `init_*` prefix.
9+
See the **change_initial_conditions.py** example.
10+
11+
- Courant–Friedrichs–Lewy (CFL) condition check to warn about potential instability of the user-defined
12+
numerical parameters.
13+
14+
- Propagation tests for validating model conduction velocity.
15+
16+
- **Reentry and Spiral waves** tutorial.
17+
18+
### Fixes
19+
20+
- Adjusted diffusion coefficients (`D_model`) in Fenton–Karma and Mitchell–Schaeffer models
21+
to ensure physiological conduction velocities.
22+
23+
- Removed the unnecessary variable `irel` in the Courtemanche model (now treated as a parameter).
24+
25+
- Standardized variable and parameter names in the ten-Tusscher–Panfilov 2006 model.
26+
27+
- Fixed an API issue and a potential deadlock in AnimationBuilder2D/3D.
28+
29+
330
## [0.9.0] March 2026
431

532
### Added

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ plt.tight_layout()
147147
plt.show()
148148
```
149149

150+
![Alt text](images/quick_start_output.png)
151+
150152
Now, let's move on to a detailed explanation.
151153

152154
## Table of Contents

Tutorials/Anisotropy2D.ipynb

Lines changed: 0 additions & 477 deletions
This file was deleted.

Tutorials/Reentry and Spiral waves.ipynb

Lines changed: 427 additions & 0 deletions
Large diffs are not rendered by default.

Tutorials/SpiralWaves2D.ipynb

Lines changed: 0 additions & 416 deletions
This file was deleted.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
Change initial conditions in the Luo-Rudy model
3+
======================================
4+
5+
Overview:
6+
---------
7+
This example demonstrates how to set spatially varying initial conditions
8+
in the Luo-Rudy 1991 cardiac model. Here we create a non-conductive obstacle in the center of the tissue
9+
and initialize an anatomical reentry by keeping the right half of the tissue refractory.
10+
11+
Simulation Setup:
12+
-----------------
13+
- Tissue Grid: A 256×256 cardiac tissue domain.
14+
- Non-conductive Obstacle: A circular non-conductive region is created in the center of
15+
the tissue to simulate an anatomical obstacle.
16+
- Stimulation: A localized stimulus is applied to the left half of the upper border.
17+
- Initial Conditions: The right half of the tissue is initialized in a refractory state by setting the gating
18+
variables 'h' and 'j' to 0, while the left half is initialized to 0.9 (non-refractory).
19+
- Time and Space Resolution:
20+
- Temporal step (dt): 0.01
21+
- Spatial resolution (dr): 0.4
22+
- Total simulation time (t_max): 200
23+
24+
Execution:
25+
----------
26+
1. Create a 2D cardiac tissue grid and define a non-conductive obstacle.
27+
2. Apply a stimulus to the left half of the upper border.
28+
3. Set up and initialize the Luo-Rudy 1991 model with spatially varying initial conditions.
29+
4. Run the simulation to observe wave propagation and interaction with the obstacle.
30+
5. Visualize the final membrane potential distribution, highlighting the non-conductive region.
31+
32+
How to use initial conditions:
33+
------------------------------
34+
Initial conditions can be set by defining attributes that start with 'init_'
35+
followed by the name of the model variable. In this example, we set 'init_h' and 'init_j'
36+
to create a refractory region in the right half of the tissue. This allows us to control
37+
the initial state of the tissue and observe how it affects wave propagation.
38+
"""
39+
40+
41+
import numpy as np
42+
import matplotlib.pyplot as plt
43+
import finitewave as fw
44+
45+
import numpy as np
46+
import matplotlib.pyplot as plt
47+
import finitewave as fw
48+
49+
50+
n = 300
51+
tissue = fw.CardiacTissue([n, n])
52+
53+
# Create a circular non-conductive obstacle in the center
54+
x, y = np.meshgrid(np.arange(n), np.arange(n), indexing="ij")
55+
56+
cx, cy = n // 2, n // 2
57+
radius = 70
58+
59+
obstacle = (x - cx) ** 2 + (y - cy) ** 2 < radius ** 2
60+
61+
tissue.mesh[obstacle] = 0 # non-conductive
62+
63+
64+
stim_sequence = fw.StimSequence()
65+
66+
# Stimulate only the left half of the upper border
67+
stim_sequence.add_stim(
68+
fw.StimVoltageCoord(
69+
time=0,
70+
volt_value=20,
71+
x1=0, x2=n // 2,
72+
y1=0, y2=n // 2,
73+
)
74+
)
75+
76+
model = fw.LuoRudy91()
77+
model.dt = 0.01
78+
model.dr = 0.5
79+
model.t_max = 200
80+
81+
model.cardiac_tissue = tissue
82+
model.stim_sequence = stim_sequence
83+
84+
# Here we use initial conditions to keep the right half of the tissue refractory,
85+
# which will prevent wave propagation in that region until it is released.
86+
#
87+
# Variables starts with init_ are used as initial conditions for the corresponding model variables.
88+
# You can set them to scalar values (all elements will have the same initial value) or to spatially varying arrays.
89+
# In this case, we set 'h' and 'j' to arrays that are 0 in the left half and 1 in the right half,
90+
# which corresponds to the refractory state in the Luo-Rudy model.
91+
model.init_h = np.full_like(tissue.mesh, 0.9, dtype=np.float64)
92+
model.init_h[n // 2:, :] = 0.0
93+
model.init_j = np.full_like(tissue.mesh, 0.9, dtype=np.float64)
94+
model.init_j[n // 2:, :] = 0.0
95+
96+
model.run()
97+
98+
# Show the potential map at the end of calculations.
99+
# We also overlay the obstacle in gray to visualize its location relative to the wave propagation.
100+
fig, axs = plt.subplots(ncols=1)
101+
plt.rcParams["axes.titley"] = -0.25
102+
obstacle_overlay = np.ma.masked_where(~obstacle, obstacle)
103+
axs.imshow(model.u)
104+
axs.imshow(obstacle_overlay, cmap='gray', alpha=1.0, vmin=0, vmax=1)
105+
plt.tight_layout()
106+
plt.show()

finitewave/core/model/cardiac_model.py

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ def run(self, initialize=True, num_of_threads=None):
141141
Whether to (re)initialize the model before running the simulation.
142142
Default is True.
143143
"""
144+
self._check_cfl_condition()
144145
if initialize:
145146
self.initialize()
146147

@@ -241,6 +242,52 @@ def clone(self):
241242
"""
242243
return copy.deepcopy(self)
243244

245+
def reset_variables_to_defaults(self):
246+
"""
247+
Resets the state variables to their default initial conditions.
248+
"""
249+
for name, value in self.default_variables.items():
250+
setattr(self, f"init_{name}", value)
251+
252+
def reset_parameters_to_defaults(self):
253+
"""
254+
Resets the parameters to their default values.
255+
"""
256+
for name, value in self.default_parameters.items():
257+
setattr(self, name, value)
258+
259+
def _check_cfl_condition(self, safety_factor=0.75):
260+
"""
261+
Checks the CFL stability condition for the explicit diffusion scheme and issues a warning
262+
if it is likely to be violated.
263+
"""
264+
if self.D_model is None:
265+
return
266+
267+
if self.D_model <= 0:
268+
return
269+
270+
dt_limit = self.dr**2 / (2 * self.cardiac_tissue.dimensions * self.D_model)
271+
dt_recommended = safety_factor * dt_limit
272+
273+
if self.dt > dt_limit:
274+
warnings.warn(
275+
(
276+
"The selected time step may violate the CFL stability "
277+
"condition for the explicit diffusion scheme.\n\n"
278+
f"Current values:\n"
279+
f" dt = {self.dt}\n"
280+
f" dr = {self.dr}\n"
281+
f" D_max = {self.D_model}\n"
282+
f" dimensions = {self.cardiac_tissue.dimensions}\n\n"
283+
f"Estimated stable dt <= {dt_limit:.6g}\n"
284+
f"Recommended dt <= {dt_recommended:.6g}\n\n"
285+
"Consider decreasing dt, increasing dr, or decreasing D."
286+
),
287+
RuntimeWarning,
288+
stacklevel=2
289+
)
290+
244291
def _initialize_variables_and_parameters(self, ops):
245292
self.default_parameters = ops.get_parameters()
246293
self.default_variables = ops.get_variables()
@@ -249,22 +296,19 @@ def _initialize_variables_and_parameters(self, ops):
249296
self.state_pars = list(self.default_parameters.keys())
250297

251298
# expose parameters as direct attributes (scalar or array)
252-
for name, value in self.default_parameters.items():
253-
setattr(self, name, value)
299+
self.reset_parameters_to_defaults()
254300

255301
# expose initial conditions as init_*
256-
for name, value in self.default_variables.items():
257-
setattr(self, f"init_{name}", value)
258-
259-
# declare arrays (optional, for readability/debug)
260-
for name in self.default_variables.keys():
261-
setattr(self, name, np.ndarray)
302+
self.reset_variables_to_defaults()
262303

263304
def _allocate_state_arrays(self):
264305
# allocate state arrays
265306
for name in self.default_variables.keys():
266307
init_val = getattr(self, f"init_{name}")
267-
setattr(self, name, init_val * np.ones_like(self.u, dtype=self.npfloat))
308+
if isinstance(init_val, np.ndarray):
309+
setattr(self, name, init_val)
310+
else:
311+
setattr(self, name, init_val * np.ones_like(self.u, dtype=self.npfloat))
268312
if name == 'u':
269313
self.u_new = self.u.copy()
270314

finitewave/cpuwave/model/courtemanche.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def __init__(self):
3131
"u", "nai", "ki", "cai",
3232
"m", "h", "j",
3333
"oa", "oi", "ua", "ui", "xr", "xs", "d", "f", "fca",
34-
"urel", "vrel", "wrel", "irel",
34+
"urel", "vrel", "wrel",
3535
"caup", "carel",
3636
"nao", "ko", "cao", "R", "T", "F", "Cm",
3737
"gna", "gk1", "gto", "gcal", "gnab", "gcab",
@@ -67,7 +67,6 @@ def generate_body(self) -> str:
6767
6868
urel_old = {model['urel']}
6969
vrel_old = {model['vrel']}
70-
irel_old = {model['irel']}
7170
wrel_old = {model['wrel']}
7271
7372
caup_old = {model['caup']}
@@ -200,7 +199,13 @@ def generate_body(self) -> str:
200199
{model['ipcamax']}, cai_old, {model['Cm']}
201200
)
202201
203-
Fn = calc_Fn(irel_old, ical, inaca, {model['F']}, {model['Vrel']})
202+
irel = calc_irel(
203+
urel_old, vrel_old, wrel_old,
204+
{model['krel']},
205+
carel_old, cai_old
206+
)
207+
208+
Fn = calc_Fn(irel, ical, inaca, {model['F']}, {model['Vrel']})
204209
205210
tau_urel = calc_tau_urel()
206211
urel_inf = calc_urel_inf(Fn)
@@ -214,12 +219,6 @@ def generate_body(self) -> str:
214219
wrel_inf = calc_wrel_inf(u_old)
215220
wrel_new = calc_gating_variable_rush_larsen(wrel_old, wrel_inf, tau_wrel, dt)
216221
217-
irel_new = calc_irel(
218-
urel_old, vrel_old, irel_old, wrel_old,
219-
{model['krel']},
220-
carel_old, cai_old
221-
)
222-
223222
itr = calc_itr(caup_old, carel_old)
224223
iup = calc_iup({model['iupmax']}, cai_old, {model['kup']})
225224
iupleak = calc_iupleak(caup_old, {model['caupmax']}, {model['iupmax']})
@@ -241,15 +240,15 @@ def generate_body(self) -> str:
241240
242241
dcai = calc_dcai(
243242
cai_old, inaca, ipca, ical, ibca,
244-
iup, iupleak, irel_old,
243+
iup, iupleak, irel,
245244
{model['Vrel']}, {model['Vup']},
246245
{model['trpnmax']}, {model['kmtrpn']},
247246
{model['cmdnmax']}, {model['kmcmdn']},
248247
{model['F']}, {model['Vj']}
249248
)
250249
251250
dcarel = calc_dcarel(
252-
carel_old, itr, irel_old,
251+
carel_old, itr, irel,
253252
{model['csqnmax']}, {model['kmcsqn']}
254253
)
255254
@@ -270,7 +269,6 @@ def generate_body(self) -> str:
270269
271270
{model['urel']} = urel_new
272271
{model['vrel']} = vrel_new
273-
{model['irel']} = irel_new
274272
{model['wrel']} = wrel_new
275273
276274
{model['caup']} = caup_old + dt * dcaup

finitewave/cpuwave/model/fenton_karma.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def __init__(self):
130130
Initializes the Fenton-Karma instance with default parameters.
131131
"""
132132
super().__init__()
133-
self.D_model = 1.
133+
self.D_model = 0.1
134134
self.npfloat = 'float64'
135135

136136
self._initialize_variables_and_parameters(ops)

0 commit comments

Comments
 (0)