finitewave 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. finitewave/README.md +20 -0
  2. finitewave/__init__.py +126 -0
  3. finitewave/core/__init__.py +8 -0
  4. finitewave/core/command/__init__.py +2 -0
  5. finitewave/core/command/command.py +52 -0
  6. finitewave/core/command/command_sequence.py +59 -0
  7. finitewave/core/exception/__init__.py +1 -0
  8. finitewave/core/exception/exceptions.py +46 -0
  9. finitewave/core/fibrosis/__init__.py +1 -0
  10. finitewave/core/fibrosis/fibrosis_pattern.py +51 -0
  11. finitewave/core/model/__init__.py +1 -0
  12. finitewave/core/model/cardiac_model.py +315 -0
  13. finitewave/core/model/ionic_kernel_generator.py +173 -0
  14. finitewave/core/state/__init__.py +2 -0
  15. finitewave/core/state/state_loader.py +80 -0
  16. finitewave/core/state/state_saver.py +122 -0
  17. finitewave/core/stencil/__init__.py +1 -0
  18. finitewave/core/stencil/stencil.py +46 -0
  19. finitewave/core/stimulation/__init__.py +4 -0
  20. finitewave/core/stimulation/stim.py +56 -0
  21. finitewave/core/stimulation/stim_current.py +44 -0
  22. finitewave/core/stimulation/stim_sequence.py +77 -0
  23. finitewave/core/stimulation/stim_voltage.py +49 -0
  24. finitewave/core/tissue/__init__.py +1 -0
  25. finitewave/core/tissue/cardiac_tissue.py +119 -0
  26. finitewave/core/tracker/__init__.py +2 -0
  27. finitewave/core/tracker/tracker.py +101 -0
  28. finitewave/core/tracker/tracker_sequence.py +64 -0
  29. finitewave/cpuwave/__init__.py +26 -0
  30. finitewave/cpuwave/exception/__init__.py +1 -0
  31. finitewave/cpuwave/exception/exceptions_2d.py +37 -0
  32. finitewave/cpuwave/fibrosis/__init__.py +2 -0
  33. finitewave/cpuwave/fibrosis/diffuse_pattern.py +82 -0
  34. finitewave/cpuwave/fibrosis/structural_pattern.py +122 -0
  35. finitewave/cpuwave/model/__init__.py +8 -0
  36. finitewave/cpuwave/model/_kernel_builder.py +46 -0
  37. finitewave/cpuwave/model/_registry.py +68 -0
  38. finitewave/cpuwave/model/aliev_panfilov.py +181 -0
  39. finitewave/cpuwave/model/barkley.py +157 -0
  40. finitewave/cpuwave/model/bueno_orovio.py +252 -0
  41. finitewave/cpuwave/model/courtemanche.py +553 -0
  42. finitewave/cpuwave/model/fenton_karma.py +211 -0
  43. finitewave/cpuwave/model/luo_rudy_91.py +186 -0
  44. finitewave/cpuwave/model/mitchell_schaeffer.py +184 -0
  45. finitewave/cpuwave/model/ten_tusscher_panfilov_2006.py +470 -0
  46. finitewave/cpuwave/stencil/__init__.py +5 -0
  47. finitewave/cpuwave/stencil/sten2D/__init__.py +3 -0
  48. finitewave/cpuwave/stencil/sten2D/asymmetric_stencil_2d.py +433 -0
  49. finitewave/cpuwave/stencil/sten2D/isotropic_stencil_2d.py +204 -0
  50. finitewave/cpuwave/stencil/sten2D/symmetric_stencil_2d.py +250 -0
  51. finitewave/cpuwave/stencil/sten3D/__init__.py +2 -0
  52. finitewave/cpuwave/stencil/sten3D/asymmetric_stencil_3d.py +458 -0
  53. finitewave/cpuwave/stencil/sten3D/isotropic_stencil_3d.py +149 -0
  54. finitewave/cpuwave/stimulation/__init__.py +5 -0
  55. finitewave/cpuwave/stimulation/stim_current_area.py +100 -0
  56. finitewave/cpuwave/stimulation/stim_current_coord.py +104 -0
  57. finitewave/cpuwave/stimulation/stim_current_matrix.py +68 -0
  58. finitewave/cpuwave/stimulation/stim_voltage_coord.py +85 -0
  59. finitewave/cpuwave/stimulation/stim_voltage_matrix.py +51 -0
  60. finitewave/cpuwave/tracker/__init__.py +33 -0
  61. finitewave/cpuwave/tracker/action_potential_tracker.py +79 -0
  62. finitewave/cpuwave/tracker/activation_time_tracker.py +75 -0
  63. finitewave/cpuwave/tracker/animation_tracker.py +194 -0
  64. finitewave/cpuwave/tracker/ecg_tracker.py +171 -0
  65. finitewave/cpuwave/tracker/local_activation_time_tracker.py +101 -0
  66. finitewave/cpuwave/tracker/period_tracker.py +91 -0
  67. finitewave/cpuwave/tracker/spiral_wave_core_tracker.py +174 -0
  68. finitewave/cpuwave/tracker/variables_tracker.py +118 -0
  69. finitewave/tools/__init__.py +5 -0
  70. finitewave/tools/animation_2d_builder.py +132 -0
  71. finitewave/tools/animation_3d_builder.py +152 -0
  72. finitewave/tools/velocity_2d_calculation.py +115 -0
  73. finitewave/tools/velocity_3d_calculation.py +97 -0
  74. finitewave/tools/vis_mesh_builder_3d.py +118 -0
  75. finitewave-0.9.0.dist-info/METADATA +417 -0
  76. finitewave-0.9.0.dist-info/RECORD +78 -0
  77. finitewave-0.9.0.dist-info/WHEEL +4 -0
  78. finitewave-0.9.0.dist-info/licenses/LICENSE +21 -0
finitewave/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # Finitewave
2
+
3
+ Package for a wide range of tasks in modeling cardiac electrophysiology using
4
+ finite-difference methods.
5
+
6
+ ## Package structure
7
+
8
+ ### core
9
+
10
+ Base classes subpackage. Use this subpackage to create your own implementation
11
+ and incorporate it in the package logic.
12
+
13
+ ### cpuwave
14
+
15
+ Ready-to-use implementation for cardiac modeling problems. Contains prepared models,
16
+ tissue generation methods, optimized numerical schemes and specialized tools.
17
+
18
+ ### tools
19
+
20
+ Additional methods to treat the results and make different visual representations.
finitewave/__init__.py ADDED
@@ -0,0 +1,126 @@
1
+
2
+ """
3
+ finitewave
4
+ ==========
5
+
6
+ A Python package for simulating cardiac electrophysiology in 2D and 3D using
7
+ the finite difference method.
8
+
9
+ This package provides a set of tools for simulating cardiac electrophysiology
10
+ in 2D and 3D using the finite difference method. The package includes classes
11
+ for creating cardiac tissue models, tracking electrical activity, and
12
+ visualizing simulation results. The package is designed to be flexible and
13
+ extensible, allowing users to create custom models and trackers for their
14
+ specific research needs.
15
+
16
+ """
17
+
18
+ from finitewave.core import (
19
+ Command,
20
+ CommandSequence,
21
+ FibrosisPattern,
22
+ CardiacModel,
23
+ StateLoader,
24
+ StateSaver,
25
+ StateSaverCollection,
26
+ Stencil,
27
+ StimCurrent,
28
+ StimSequence,
29
+ StimVoltage,
30
+ Stim,
31
+ CardiacTissue,
32
+ Tracker,
33
+ TrackerSequence
34
+ )
35
+
36
+ from finitewave.cpuwave import (
37
+ # IncorrectWeightsModeError2D,
38
+ DiffusePattern,
39
+ StructuralPattern,
40
+ AlievPanfilov,
41
+ Barkley,
42
+ MitchellSchaeffer,
43
+ FentonKarma,
44
+ BuenoOrovio,
45
+ LuoRudy91,
46
+ TenTusscherPanfilov2006,
47
+ Courtemanche,
48
+ AsymmetricStencil2D,
49
+ # SymmetricStencil2D,
50
+ IsotropicStencil2D,
51
+ StimCurrentArea,
52
+ StimCurrentCoord,
53
+ StimVoltageCoord,
54
+ StimCurrentMatrix,
55
+ StimVoltageMatrix,
56
+ ActionPotentialTracker,
57
+ ActivationTimeTracker,
58
+ AnimationTracker,
59
+ ECGTracker,
60
+ LocalActivationTimeTracker,
61
+ PeriodTracker,
62
+ # PeriodAnimationTracker,
63
+ SpiralWaveCoreTracker,
64
+ VariablesTracker,
65
+ )
66
+
67
+ from finitewave.tools import (
68
+ Animation2DBuilder,
69
+ Animation3DBuilder,
70
+ VisMeshBuilder3D,
71
+ Velocity2DCalculation,
72
+ Velocity3DCalculation,
73
+ )
74
+
75
+
76
+ # compatibility with older versions:
77
+
78
+ CardiacTissue2D = CardiacTissue
79
+ CardiacTissue3D = CardiacTissue
80
+
81
+ AlievPanfilov2D = AlievPanfilov
82
+ AlievPanfilov3D = AlievPanfilov
83
+ Barkley2D = Barkley
84
+ Barkley3D = Barkley
85
+ MitchellSchaeffer2D = MitchellSchaeffer
86
+ MitchellSchaeffer3D = MitchellSchaeffer
87
+ FentonKarma2D = FentonKarma
88
+ FentonKarma3D = FentonKarma
89
+ BuenoOrovio2D = BuenoOrovio
90
+ BuenoOrovio3D = BuenoOrovio
91
+ LuoRudy912D = LuoRudy91
92
+ LuoRudy913D = LuoRudy91
93
+ TP062D = TenTusscherPanfilov2006
94
+ TP063D = TenTusscherPanfilov2006
95
+ Courtemanche2D = Courtemanche
96
+ Courtemanche3D = Courtemanche
97
+
98
+ StimCurrentArea2D = StimCurrentArea
99
+ StimCurrentArea3D = StimCurrentArea
100
+ StimCurrentCoord2D = StimCurrentCoord
101
+ StimCurrentCoord3D = StimCurrentCoord
102
+ StimVoltageCoord2D = StimVoltageCoord
103
+ StimVoltageCoord3D = StimVoltageCoord
104
+ StimCurrentMatrix2D = StimCurrentMatrix
105
+ StimCurrentMatrix3D = StimCurrentMatrix
106
+ StimVoltageMatrix2D = StimVoltageMatrix
107
+ StimVoltageMatrix3D = StimVoltageMatrix
108
+
109
+ ActionPotential2DTracker = ActionPotentialTracker
110
+ ActionPotential3DTracker = ActionPotentialTracker
111
+ ActivationTime2DTracker = ActivationTimeTracker
112
+ ActivationTime3DTracker = ActivationTimeTracker
113
+ Animation2DTracker = AnimationTracker
114
+ Animation3DTracker = AnimationTracker
115
+ ECG2DTracker = ECGTracker
116
+ ECG3DTracker = ECGTracker
117
+ LocalActivationTime2DTracker = LocalActivationTimeTracker
118
+ LocalActivationTime3DTracker = LocalActivationTimeTracker
119
+ Period2DTracker = PeriodTracker
120
+ Period3DTracker = PeriodTracker
121
+ SpiralWaveCore2DTracker = SpiralWaveCoreTracker
122
+ SpiralWaveCore3DTracker = SpiralWaveCoreTracker
123
+ Variables2DTracker = VariablesTracker
124
+ Variables3DTracker = VariablesTracker
125
+ MultiVariable2DTracker = VariablesTracker
126
+ MultiVariable3DTracker = VariablesTracker
@@ -0,0 +1,8 @@
1
+ from finitewave.core.command import Command, CommandSequence
2
+ from finitewave.core.fibrosis import FibrosisPattern
3
+ from finitewave.core.model import CardiacModel
4
+ from finitewave.core.state import StateLoader, StateSaver, StateSaverCollection
5
+ from finitewave.core.stencil import Stencil
6
+ from finitewave.core.stimulation import StimCurrent, StimSequence, StimVoltage, Stim
7
+ from finitewave.core.tissue import CardiacTissue
8
+ from finitewave.core.tracker import Tracker, TrackerSequence
@@ -0,0 +1,2 @@
1
+ from finitewave.core.command.command import Command
2
+ from finitewave.core.command.command_sequence import CommandSequence
@@ -0,0 +1,52 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class Command(ABC):
5
+ """Base class for a command to be executed during a simulation.
6
+
7
+ Attributes
8
+ ----------
9
+ t : float
10
+ The time at which the command should be executed.
11
+
12
+ passed : bool
13
+ Flag indicating whether the command has been executed.
14
+ """
15
+
16
+ def __init__(self, time=None):
17
+ """
18
+ Initializes a Command instance with the specified execution time.
19
+
20
+ Parameters
21
+ ----------
22
+ time : float
23
+ The time at which the command should be executed.
24
+ """
25
+ self.t = time
26
+ self.passed = False
27
+
28
+ @abstractmethod
29
+ def execute(self, model):
30
+ """
31
+ Abstract method for executing the command. This method should be
32
+ implemented by subclasses to define the specific behavior of the
33
+ command.
34
+
35
+ Parameters
36
+ ----------
37
+ model : CardiacModel
38
+ The cardiac model instance on which the command will be executed.
39
+ """
40
+ pass
41
+
42
+ def update_status(self, model):
43
+ """
44
+ Marks the command as executed.
45
+
46
+ Parameters
47
+ ----------
48
+ model : CardiacModel
49
+ The cardiac model instance on which the command was executed
50
+ """
51
+ self.passed = model.t >= self.t
52
+ return self.passed
@@ -0,0 +1,59 @@
1
+
2
+
3
+ class CommandSequence:
4
+ """Manages a sequence of commands to be executed during a simulation.
5
+
6
+ Attributes
7
+ ----------
8
+ sequence : list
9
+ A list of ``Command`` instances representing the sequence of commands
10
+ to be executed.
11
+
12
+ model : CardiacModel
13
+ The cardiac model instance on which commands will be executed.
14
+ """
15
+
16
+ def __init__(self):
17
+ self.sequence = []
18
+ self.model = None
19
+
20
+ def initialize(self, model):
21
+ """
22
+ Initializes the CommandSequence with the specified model and resets
23
+ the execution status of all commands.
24
+
25
+ Parameters
26
+ ----------
27
+ model : CardiacModel
28
+ The cardiac model instance to be used for command execution.
29
+ """
30
+ self.model = model
31
+ for command in self.sequence:
32
+ command.passed = False
33
+
34
+ def add_command(self, command):
35
+ """
36
+ Adds a ``Command`` instance to the sequence.
37
+
38
+ Parameters
39
+ ----------
40
+ command : Command
41
+ The command instance to be added to the sequence.
42
+ """
43
+ self.sequence.append(command)
44
+
45
+ def remove_commands(self):
46
+ """
47
+ Clears the sequence of all commands.
48
+ """
49
+ self.sequence = []
50
+
51
+ def execute_next(self):
52
+ """
53
+ Executes commands whose time has arrived and which have not been
54
+ executed yet.
55
+ """
56
+ for command in self.sequence:
57
+ if not command.passed and command.update_status(self.model):
58
+ command.execute(self.model)
59
+
@@ -0,0 +1 @@
1
+ from finitewave.core.exception.exceptions import IncorrectNumberOfWeights
@@ -0,0 +1,46 @@
1
+
2
+
3
+ class IncorrectWeightsShapeError(Exception):
4
+ def __init__(self, *args: object) -> None:
5
+ super().__init__(*args)
6
+
7
+
8
+ class IncorrectNumberOfWeights(Exception):
9
+ """Exception raised for errors in the shape of weights in the
10
+ ``CardiacTissue`` class.
11
+
12
+ This exception is used to indicate that the shape of weights provided does
13
+ not match the expected dimensions. It includes details about the incorrect
14
+ shape and the expected shapes.
15
+
16
+ Parameters
17
+ ----------
18
+ number_of_weights : int
19
+ The number of weights in the incorrect shape.
20
+
21
+ n1 : int
22
+ The target number of weights in one dimension.
23
+
24
+ n2 : int
25
+ The target number of weights in another dimension.
26
+ """
27
+
28
+ def __init__(self, number_of_weights, n1, n2):
29
+ """
30
+ Initializes the ``IncorrectNumberOfWeights`` with details about the
31
+ incorrect shape and expected dimensions.
32
+
33
+ Parameters
34
+ ----------
35
+ number_of_weights : int
36
+ The number of weights in the incorrect shape.
37
+
38
+ n1 : int
39
+ The target number of weights in one dimension.
40
+
41
+ n2 : int
42
+ The target number of weights in another dimension.
43
+ """
44
+ self.message = (f"Number of weights provided ({number_of_weights})" +
45
+ f"does not match the expected {n1} or {n2}.")
46
+ super().__init__(self.message)
@@ -0,0 +1 @@
1
+ from finitewave.core.fibrosis.fibrosis_pattern import FibrosisPattern
@@ -0,0 +1,51 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class FibrosisPattern(ABC):
5
+ """Abstract base class for generating and applying fibrosis patterns to
6
+ cardiac tissue.
7
+
8
+ This class defines an interface for creating fibrosis patterns and applying
9
+ them to cardiac tissue models. Subclasses must implement the ``generate``
10
+ method to define specific patterns. The ``apply`` method uses the generated
11
+ pattern to modify the mesh of the cardiac tissue.
12
+ """
13
+ def __init__(self):
14
+ pass
15
+
16
+ @abstractmethod
17
+ def generate(self, shape=None, mesh=None):
18
+ """
19
+ Generates a fibrosis pattern for the given shape and optionally based
20
+ on the provided mesh.
21
+
22
+ Parameters
23
+ ----------
24
+ shape : tuple
25
+ The shape of the mesh (e.g., (ni, nj) or (ni, nj, nk)).
26
+ mesh : numpy.ndarray, optional
27
+ The existing mesh to base the pattern on. Default is None.
28
+
29
+ Returns
30
+ -------
31
+ numpy.ndarray
32
+ A new mesh array with the applied fibrosis pattern.
33
+ """
34
+
35
+ def apply(self, cardiac_tissue):
36
+ """
37
+ Applies the generated fibrosis pattern to the specified cardiac tissue
38
+ object.
39
+
40
+ This method calls the ``generate`` method to create the pattern and
41
+ then updates the ``mesh`` attribute of the ``cardiac_tissue`` object
42
+ with the generated pattern.
43
+
44
+ Parameters
45
+ ----------
46
+ cardiac_tissue : CardiacTissue
47
+ The cardiac tissue object to which the fibrosis pattern will be
48
+ applied. The ``mesh`` attribute of this object will be updated with
49
+ the generated pattern.
50
+ """
51
+ cardiac_tissue.mesh = self.generate(mesh=cardiac_tissue.mesh)
@@ -0,0 +1 @@
1
+ from finitewave.core.model.cardiac_model import CardiacModel
@@ -0,0 +1,315 @@
1
+ from abc import ABC, abstractmethod
2
+ import copy
3
+ import warnings
4
+ from tqdm import tqdm
5
+ import numpy as np
6
+ import numba
7
+
8
+
9
+ class CardiacModel(ABC):
10
+ """
11
+ Base class for electrophysiological models.
12
+
13
+ This class serves as the base for implementing various cardiac models.
14
+ It provides methods for initializing the model, running simulations,
15
+ and managing the state of the simulation.
16
+
17
+ Attributes
18
+ ----------
19
+ cardiac_tissue : CardiacTissue
20
+ The tissue object that represents the cardiac tissue in the simulation.
21
+ stim_sequence : StimSequence
22
+ The sequence of stimuli applied to the cardiac tissue.
23
+ tracker_sequence : TrackerSequence
24
+ The sequence of trackers used to monitor the simulation.
25
+ command_sequence : CommandSequence
26
+ The sequence of commands to execute during the simulation.
27
+ state_loader : StateLoader
28
+ The object responsible for loading the state of the simulation.
29
+ state_saver : StateSaver
30
+ The object responsible for saving the state of the simulation.
31
+ stencil : Stencil
32
+ The stencil used for numerical computations.
33
+ u : ndarray
34
+ Array representing the action potential (mV) across the tissue.
35
+ u_new : ndarray
36
+ Array for storing the updated action potential values.
37
+ dt : float
38
+ Time step for the simulation.
39
+ dr : float
40
+ Spatial step for the simulation.
41
+ t_max : float
42
+ Maximum time for the simulation (model units).
43
+ t : float
44
+ Current time in the simulation (model units).
45
+ step : int
46
+ Current step or iteration in the simulation.
47
+ D_model : float
48
+ Model-specific diffusion coefficient.
49
+ prog_bar : bool
50
+ Whether to display a progress bar during simulation.
51
+ npfloat : type
52
+ The floating-point type used for numerical computations.
53
+ state_vars : list
54
+ List of state variables to save and load during simulation.
55
+ """
56
+ def __init__(self):
57
+ self.meta = {}
58
+ self.cardiac_tissue = None
59
+ self.stim_sequence = None
60
+ self.tracker_sequence = None
61
+ self.command_sequence = None
62
+ self.state_loader = None
63
+ self.state_saver = None
64
+ self.stencil = None
65
+
66
+ self.observers = []
67
+ self._buffs = [] # observer buffers
68
+
69
+ self.diffusion_kernel = None
70
+ self.ionic_kernel = None
71
+
72
+ self.u = np.ndarray
73
+ self.u_new = np.ndarray
74
+ self.weights = np.ndarray
75
+ self.dt = 0.
76
+ self.dr = 0.
77
+ self.t_max = 0.
78
+ self.t = 0
79
+ self.step = 0
80
+ self.D_model = 1.
81
+
82
+ self.prog_bar = True
83
+ self.npfloat = np.float64
84
+ self.state_vars = []
85
+
86
+ @abstractmethod
87
+ def run_ionic_kernel(self):
88
+ """
89
+ Abstract method for running the ionic kernel. Must be implemented by
90
+ subclasses.
91
+ """
92
+ pass
93
+
94
+ def initialize(self):
95
+ """
96
+ Initializes the model for simulation. Sets up arrays, computes weights,
97
+ and initializes stimuli, trackers, and commands.
98
+ """
99
+ self.u = np.zeros_like(self.cardiac_tissue.mesh, dtype=self.npfloat)
100
+ self.u_new = self.u.copy()
101
+ self.step = 0
102
+ self.t = 0
103
+
104
+ self.compute_weights()
105
+ self.diffusion_kernel = self.stencil.select_diffusion_kernel()
106
+
107
+ if self.stim_sequence:
108
+ self.stim_sequence.initialize(self)
109
+
110
+ if self.tracker_sequence:
111
+ self.tracker_sequence.initialize(self)
112
+
113
+ if self.command_sequence:
114
+ self.command_sequence.initialize(self)
115
+
116
+ if self.state_loader:
117
+ self.state_loader.initialize(self)
118
+
119
+ if self.state_saver:
120
+ self.state_saver.initialize(self)
121
+
122
+ def compute_weights(self):
123
+ """
124
+ Computes the weights for the stencil.
125
+ """
126
+ self.cardiac_tissue.compute_myo_indexes()
127
+
128
+ if self.stencil is None:
129
+ self.stencil = self.select_stencil(self.cardiac_tissue)
130
+
131
+ self.weights = self.stencil.compute_weights(self, self.cardiac_tissue)
132
+
133
+ def run(self, initialize=True, num_of_threads=None):
134
+ """
135
+ Runs the simulation loop. Handles stimuli, diffusion, ionic kernel
136
+ updates, and tracking.
137
+
138
+ Parameters
139
+ ----------
140
+ initialize : bool, optional
141
+ Whether to (re)initialize the model before running the simulation.
142
+ Default is True.
143
+ """
144
+ if initialize:
145
+ self.initialize()
146
+
147
+ numba.set_num_threads(numba.config.NUMBA_NUM_THREADS)
148
+
149
+ if num_of_threads is not None:
150
+ if num_of_threads > numba.config.NUMBA_NUM_THREADS:
151
+ warnings.warn(
152
+ f"Selected number of threads ({num_of_threads}) exceeds the available threads ({numba.config.NUMBA_NUM_THREADS}). "
153
+ f"Using the maximum available threads instead."
154
+ )
155
+ num_of_theads = min(num_of_threads, numba.config.NUMBA_NUM_THREADS)
156
+ numba.set_num_threads(num_of_theads)
157
+
158
+ if self.t_max < self.t:
159
+ raise ValueError("t_max must be greater than current t.")
160
+
161
+ if self.state_loader:
162
+ self.state_loader.load()
163
+
164
+ iters = int(np.ceil((self.t_max - self.t) / self.dt))
165
+ bar_desc = f"Running {self.__class__.__name__}"
166
+
167
+ for _ in tqdm(range(iters), total=iters, desc=bar_desc,
168
+ disable=not self.prog_bar):
169
+
170
+ if self.stim_sequence:
171
+ self.stim_sequence.stimulate_next()
172
+
173
+ self.run_diffusion_kernel()
174
+ self.run_ionic_kernel()
175
+
176
+ if self.tracker_sequence:
177
+ self.tracker_sequence.tracker_next()
178
+
179
+ self.t += self.dt
180
+ self.step += 1
181
+ self.u_new, self.u = self.u, self.u_new
182
+
183
+ if self.command_sequence:
184
+ self.command_sequence.execute_next()
185
+
186
+ if self.state_saver:
187
+ self.state_saver.save()
188
+
189
+ if self.check_termination():
190
+ if self.state_saver:
191
+ self.state_saver.save()
192
+ break
193
+
194
+ def check_termination(self):
195
+ """
196
+ Checks whether the simulation should terminate based on the current
197
+ time. The ``CommandSequence`` may change the ``t_max`` value during
198
+ execution to control the simulation duration.
199
+
200
+ Returns
201
+ -------
202
+ bool
203
+ True if the simulation should terminate, False otherwise.
204
+ """
205
+ max_iters = int(np.ceil(self.t_max / self.dt))
206
+ return (self.t > self.t_max) or (self.step > max_iters)
207
+
208
+ def run_diffusion_kernel(self):
209
+ """
210
+ Executes the diffusion kernel computation using the current parameters
211
+ and tissue weights.
212
+ """
213
+ self.diffusion_kernel(self.u_new, self.u, self.weights,
214
+ self.cardiac_tissue.myo_indexes)
215
+
216
+ @abstractmethod
217
+ def select_stencil(self, cardiac_tissue):
218
+ """
219
+ Selects the appropriate stencil based on the cardiac tissue properties.
220
+
221
+ Parameters
222
+ ----------
223
+ cardiac_tissue : CardiacTissue
224
+ The tissue object representing the cardiac tissue.
225
+
226
+ Returns
227
+ -------
228
+ Stencil
229
+ The stencil object to use for diffusion computations.
230
+ """
231
+ pass
232
+
233
+ def clone(self):
234
+ """
235
+ Creates a deep copy of the current model instance.
236
+
237
+ Returns
238
+ -------
239
+ CardiacModel
240
+ A deep copy of the current CardiacModel instance.
241
+ """
242
+ return copy.deepcopy(self)
243
+
244
+ def _initialize_variables_and_parameters(self, ops):
245
+ self.default_parameters = ops.get_parameters()
246
+ self.default_variables = ops.get_variables()
247
+
248
+ self.state_vars = self.default_variables.keys()
249
+ self.state_pars = list(self.default_parameters.keys())
250
+
251
+ # expose parameters as direct attributes (scalar or array)
252
+ for name, value in self.default_parameters.items():
253
+ setattr(self, name, value)
254
+
255
+ # 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)
262
+
263
+ def _allocate_state_arrays(self):
264
+ # allocate state arrays
265
+ for name in self.default_variables.keys():
266
+ init_val = getattr(self, f"init_{name}")
267
+ setattr(self, name, init_val * np.ones_like(self.u, dtype=self.npfloat))
268
+ if name == 'u':
269
+ self.u_new = self.u.copy()
270
+
271
+ # validate parameter fields shapes if they are arrays
272
+ tissue_shape = self.cardiac_tissue.mesh.shape
273
+ for name in self.default_parameters.keys():
274
+ par = getattr(self, name)
275
+ if isinstance(par, np.ndarray):
276
+ if par.shape != tissue_shape:
277
+ raise ValueError(
278
+ f"param '{name}' shape {par.shape} != tissue shape {tissue_shape}"
279
+ )
280
+
281
+ def _initialize_kernel(self, kernel, exclude_params=[]):
282
+ gen = kernel()
283
+ self._kernel_args_order = gen.args_order[:]
284
+
285
+ # args_order: state vars first, then all parameters (stable order for call site)
286
+ param_names = list(self.default_parameters.keys())
287
+ var_names = list(self.default_variables.keys())
288
+
289
+ # Tell generator which names are arrays vs scalars (for indexing decisions)
290
+ for name in var_names:
291
+ gen.arrays.append(name)
292
+
293
+ for name in param_names:
294
+ if name in exclude_params: # computed internally
295
+ continue
296
+ par = getattr(self, name)
297
+ if np.isscalar(par):
298
+ gen.scalars.append(name)
299
+ elif isinstance(par, np.ndarray):
300
+ gen.arrays.append(name)
301
+
302
+ return gen
303
+
304
+ def _form_and_verify_observers(self):
305
+ buffs = []
306
+ for obs in self.observers:
307
+ name = obs["name"]
308
+ try:
309
+ buffs.append(getattr(self, name))
310
+ except AttributeError as e:
311
+ raise AttributeError(
312
+ f"Observer buffer '{name}' not found on model. "
313
+ f"Create it before initialize(), e.g.: model.{name} = np.zeros(...)."
314
+ ) from e
315
+ return buffs