circulation 0.2.0__tar.gz → 0.3.0__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.
Files changed (24) hide show
  1. {circulation-0.2.0/src/circulation.egg-info → circulation-0.3.0}/PKG-INFO +1 -1
  2. {circulation-0.2.0 → circulation-0.3.0}/pyproject.toml +2 -2
  3. circulation-0.3.0/src/circulation/__init__.py +11 -0
  4. {circulation-0.2.0 → circulation-0.3.0}/src/circulation/base.py +8 -4
  5. {circulation-0.2.0 → circulation-0.3.0}/src/circulation/bestel.py +4 -4
  6. {circulation-0.2.0 → circulation-0.3.0}/src/circulation/regazzoni2020.py +12 -12
  7. circulation-0.3.0/src/circulation/windkessel.py +120 -0
  8. {circulation-0.2.0 → circulation-0.3.0/src/circulation.egg-info}/PKG-INFO +1 -1
  9. {circulation-0.2.0 → circulation-0.3.0}/src/circulation.egg-info/SOURCES.txt +3 -1
  10. {circulation-0.2.0 → circulation-0.3.0}/tests/test_models.py +12 -0
  11. circulation-0.3.0/tests/test_regazzoni_steady_state.py +15 -0
  12. circulation-0.2.0/src/circulation/__init__.py +0 -3
  13. {circulation-0.2.0 → circulation-0.3.0}/LICENSE +0 -0
  14. {circulation-0.2.0 → circulation-0.3.0}/README.md +0 -0
  15. {circulation-0.2.0 → circulation-0.3.0}/setup.cfg +0 -0
  16. {circulation-0.2.0 → circulation-0.3.0}/src/circulation/log.py +0 -0
  17. {circulation-0.2.0 → circulation-0.3.0}/src/circulation/time_varying_elastance.py +0 -0
  18. {circulation-0.2.0 → circulation-0.3.0}/src/circulation/units.py +0 -0
  19. {circulation-0.2.0 → circulation-0.3.0}/src/circulation/zenker.py +0 -0
  20. {circulation-0.2.0 → circulation-0.3.0}/src/circulation.egg-info/dependency_links.txt +0 -0
  21. {circulation-0.2.0 → circulation-0.3.0}/src/circulation.egg-info/requires.txt +0 -0
  22. {circulation-0.2.0 → circulation-0.3.0}/src/circulation.egg-info/top_level.txt +0 -0
  23. {circulation-0.2.0 → circulation-0.3.0}/tests/test_bestel.py +0 -0
  24. {circulation-0.2.0 → circulation-0.3.0}/tests/test_initialization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: circulation
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Closed loop blood circulation model in FEniCS
5
5
  Author-email: Henrik Finsberg <henriknf@simula.no>
6
6
  License-Expression: MIT
@@ -3,7 +3,7 @@ requires = ["setuptools>=61.0.0", "wheel"]
3
3
 
4
4
  [project]
5
5
  name = "circulation"
6
- version = "0.2.0"
6
+ version = "0.3.0"
7
7
  description = "Closed loop blood circulation model in FEniCS"
8
8
  authors = [{ name = "Henrik Finsberg", email = "henriknf@simula.no" }]
9
9
  license = "MIT"
@@ -116,7 +116,7 @@ tag = true
116
116
  sign_tags = false
117
117
  tag_name = "v{new_version}"
118
118
  tag_message = "Bump version: {current_version} → {new_version}"
119
- current_version = "0.2.0"
119
+ current_version = "0.3.0"
120
120
 
121
121
 
122
122
  [[tool.bumpversion.files]]
@@ -0,0 +1,11 @@
1
+ from . import base, log, regazzoni2020, zenker, units, time_varying_elastance, windkessel
2
+
3
+ __all__ = [
4
+ "base",
5
+ "log",
6
+ "regazzoni2020",
7
+ "zenker",
8
+ "units",
9
+ "time_varying_elastance",
10
+ "windkessel",
11
+ ]
@@ -267,10 +267,14 @@ class CirculationModel(ABC):
267
267
  unit_R: float = 1.0,
268
268
  unit_p: float = 1.0,
269
269
  ) -> Callable[[float, float], float]:
270
- return lambda w, v: unit_R * 10.0 ** (
271
- np.log10(Rmin / unit_R)
272
- + (np.log10(Rmax / unit_R) - np.log10(Rmin / unit_R))
273
- * smooth_heavyside((v - w) / unit_p)
270
+ return lambda w, v: (
271
+ unit_R
272
+ * 10.0
273
+ ** (
274
+ np.log10(Rmin / unit_R)
275
+ + (np.log10(Rmax / unit_R) - np.log10(Rmin / unit_R))
276
+ * smooth_heavyside((v - w) / unit_p)
277
+ )
274
278
  )
275
279
 
276
280
  def times_n_beats(self, dt: float, n: int = 1) -> np.ndarray:
@@ -106,8 +106,8 @@ class BestelActivation:
106
106
 
107
107
  # print(f"Solving active stress model with parameters: {pprint.pformat(params)}")
108
108
 
109
- f = (
110
- lambda t: 0.25
109
+ f = lambda t: (
110
+ 0.25
111
111
  * (1 + math.tanh((t - ps["t_sys"]) / ps["gamma"]))
112
112
  * (1 - math.tanh((t - ps["t_dias"]) / ps["gamma"]))
113
113
  )
@@ -286,8 +286,8 @@ class BestelPressure:
286
286
 
287
287
  def __call__(self, t, p):
288
288
  ps = self.parameters
289
- f = (
290
- lambda t: 0.25
289
+ f = lambda t: (
290
+ 0.25
291
291
  * (1 + math.tanh((t - ps["t_sys_pre"]) / ps["gamma"]))
292
292
  * (1 - math.tanh((t - ps["t_dias_pre"]) / ps["gamma"]))
293
293
  )
@@ -229,18 +229,18 @@ class Regazzoni2020(base.CirculationModel):
229
229
  @staticmethod
230
230
  def default_initial_conditions() -> dict[str, float]:
231
231
  return {
232
- "V_LA": 80.0 * mL,
233
- "V_LV": 110.0 * mL,
234
- "V_RA": 80.0 * mL,
235
- "V_RV": 110.0 * mL,
236
- "p_AR_SYS": 70.0 * mmHg,
237
- "p_VEN_SYS": 28.32334306787334 * mmHg,
238
- "p_AR_PUL": 25.0 * mmHg,
239
- "p_VEN_PUL": 20.0 * mmHg,
240
- "Q_AR_SYS": 0.0 * mL / s,
241
- "Q_VEN_SYS": 0.0 * mL / s,
242
- "Q_AR_PUL": 0.0 * mL / s,
243
- "Q_VEN_PUL": 0.0 * mL / s,
232
+ "V_LA": 87.183 * mL,
233
+ "V_LV": 118.520 * mL,
234
+ "V_RA": 86.833 * mL,
235
+ "V_RV": 166.177 * mL,
236
+ "p_AR_SYS": 87.675 * mmHg,
237
+ "p_VEN_SYS": 35.898 * mmHg,
238
+ "p_AR_PUL": 19.545 * mmHg,
239
+ "p_VEN_PUL": 15.004 * mmHg,
240
+ "Q_AR_SYS": 71.104 * mL / s,
241
+ "Q_VEN_SYS": 94.039 * mL / s,
242
+ "Q_AR_PUL": 94.084 * mL / s,
243
+ "Q_VEN_PUL": 473.279 * mL / s,
244
244
  }
245
245
 
246
246
  @staticmethod
@@ -0,0 +1,120 @@
1
+ from . import base
2
+ from . import units
3
+
4
+ mL = units.ureg("mL")
5
+ mmHg = units.ureg("mmHg")
6
+ s = units.ureg("s")
7
+
8
+
9
+ class ThreeElementWindkessel(base.CirculationModel):
10
+ """
11
+ A 0D model of the Left Ventricle coupled to a 3-Element Windkessel system.
12
+
13
+ The 3-Element Windkessel (WK3) models the systemic arterial tree using:
14
+ - R_c: Characteristic resistance (proximal aortic impedance)
15
+ - C: Arterial compliance (vessel elasticity)
16
+ - R_p: Peripheral resistance (microcirculation)
17
+ """
18
+
19
+ def __init__(self, parameters=None, p_LV_func=None, **kwargs):
20
+ super().__init__(parameters, **kwargs)
21
+
22
+ if p_LV_func is not None:
23
+ self.p_LV_func = p_LV_func
24
+ self._E_LV = lambda t: 1.0 # Dummy function, not used
25
+ else:
26
+ # Use default time-varying elastance model if no custom mechanics are provided
27
+ self._E_LV = self.time_varying_elastance(**self.parameters["chambers"]["LV"])
28
+ self.p_LV_func = lambda V, t: (
29
+ self._E_LV(t) * (V - self.parameters["chambers"]["LV"]["V0"])
30
+ )
31
+
32
+ self._initialize()
33
+
34
+ @property
35
+ def HR(self) -> float:
36
+ return self.parameters["HR"]
37
+
38
+ @staticmethod
39
+ def default_parameters():
40
+ return {
41
+ "HR": 1.25 * units.ureg("Hz"), # Heart rate in Hz (75 BPM)
42
+ "R_c": 0.05 * mmHg * s / mL, # Characteristic resistance (mmHg*s/mL)
43
+ "R_p": 1.05 * mmHg * s / mL, # Peripheral resistance (mmHg*s/mL)
44
+ "C": 1.3 * mL / mmHg, # Arterial compliance (mL/mmHg)
45
+ "P_venous": 8.0 * mmHg, # Preload / Left Atrial Pressure (mmHg)
46
+ "R_mitral": 0.01 * mmHg * s / mL, # Mitral valve resistance
47
+ "R_aortic": 0.01 * mmHg * s / mL, # Aortic valve resistance
48
+ "chambers": {
49
+ "LV": {
50
+ "EA": 2.5 * mmHg / mL, # Max elastance (mmHg/mL)
51
+ "EB": 0.08 * mmHg / mL, # Min elastance (mmHg/mL)
52
+ "TC": 0.25 * s, # Contraction duration (s)
53
+ "TR": 0.15 * s, # Relaxation duration (s)
54
+ "tC": 0.0 * s, # Time of contraction onset (s)
55
+ "V0": 15.0 * mL, # Unstressed volume (mL)
56
+ }
57
+ },
58
+ }
59
+
60
+ @staticmethod
61
+ def default_initial_conditions() -> dict:
62
+ return {
63
+ "V_LV": 140.0 * mL, # Initial left ventricular volume
64
+ "p_c": 80.0 * mmHg, # Pressure in the arterial compliance capacitor
65
+ }
66
+
67
+ @staticmethod
68
+ def state_names() -> list:
69
+ return ["V_LV", "p_c"]
70
+
71
+ @staticmethod
72
+ def var_names() -> list:
73
+ return ["p_LV", "p_ao", "Q_in", "Q_out", "Q_p"]
74
+
75
+ def update_static_variables(self, t, y):
76
+ V_LV = y[0]
77
+ p_c = y[1]
78
+
79
+ p_LV = self.p_LV_func(V_LV, t)
80
+
81
+ P_venous = self.parameters["P_venous"]
82
+ R_mitral = self.parameters["R_mitral"]
83
+ R_aortic = self.parameters["R_aortic"]
84
+ R_c = self.parameters["R_c"]
85
+ R_p = self.parameters["R_p"]
86
+
87
+ # 1. Mitral Flow (Diastolic Filling)
88
+ Q_in = max(0.0, (P_venous - p_LV) / R_mitral)
89
+
90
+ # 2. Aortic Flow (Systolic Ejection)
91
+ # In a WK3 model, aortic pressure P_ao = p_c + Q_out * R_c
92
+ # If the valve is open (p_LV > p_ao), we substitute P_ao and solve for Q_out
93
+ if p_LV > p_c:
94
+ Q_out = (p_LV - p_c) / (R_aortic + R_c)
95
+ else:
96
+ Q_out = 0.0
97
+
98
+ # 3. Pressures and Peripheral Flow
99
+ p_ao = p_c + Q_out * R_c
100
+ Q_p = p_c / R_p # Discharge to venous pool (assumed 0 mmHg pressure drop reference)
101
+
102
+ var = self._get_var(t)
103
+ var[0] = p_LV
104
+ var[1] = p_ao
105
+ var[2] = Q_in
106
+ var[3] = Q_out
107
+ var[4] = Q_p
108
+ return var
109
+
110
+ def rhs(self, t, y):
111
+ var = self.update_static_variables(t, y)
112
+ Q_in = var[2]
113
+ Q_out = var[3]
114
+ Q_p = var[4]
115
+ C = self.parameters["C"]
116
+
117
+ self.dy[0] = Q_in - Q_out
118
+ self.dy[1] = (Q_out - Q_p) / C
119
+
120
+ return self.dy
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: circulation
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Closed loop blood circulation model in FEniCS
5
5
  Author-email: Henrik Finsberg <henriknf@simula.no>
6
6
  License-Expression: MIT
@@ -8,6 +8,7 @@ src/circulation/log.py
8
8
  src/circulation/regazzoni2020.py
9
9
  src/circulation/time_varying_elastance.py
10
10
  src/circulation/units.py
11
+ src/circulation/windkessel.py
11
12
  src/circulation/zenker.py
12
13
  src/circulation.egg-info/PKG-INFO
13
14
  src/circulation.egg-info/SOURCES.txt
@@ -16,4 +17,5 @@ src/circulation.egg-info/requires.txt
16
17
  src/circulation.egg-info/top_level.txt
17
18
  tests/test_bestel.py
18
19
  tests/test_initialization.py
19
- tests/test_models.py
20
+ tests/test_models.py
21
+ tests/test_regazzoni_steady_state.py
@@ -24,3 +24,15 @@ def test_Regazzoni2020():
24
24
  for k, v in model.default_initial_conditions().items():
25
25
  assert k in results
26
26
  assert results[k][0] == v.magnitude
27
+
28
+
29
+ def test_ThreeElementWindkessel():
30
+ model = circulation.windkessel.ThreeElementWindkessel()
31
+ results = model.solve(T=1.0, dt=1e-3, dt_eval=0.1)
32
+
33
+ for k, v in results.items():
34
+ assert len(v) == len(results["time"]), k
35
+
36
+ for k, v in model.default_initial_conditions().items():
37
+ assert k in results
38
+ assert results[k][0] == v.magnitude
@@ -0,0 +1,15 @@
1
+ import numpy as np
2
+ from circulation import regazzoni2020
3
+
4
+
5
+ def test_regazzoni_steady_state():
6
+ """
7
+ Test that the default initial state is close to the limit cycle.
8
+ We run the model for ten beats and check that the state hasn't drifted much.
9
+ """
10
+ model = regazzoni2020.Regazzoni2020()
11
+ initial_state = model.state.copy()
12
+ model.solve(num_beats=10)
13
+ final_state = model.state
14
+
15
+ np.testing.assert_allclose(final_state, initial_state, rtol=1e-2, verbose=True)
@@ -1,3 +0,0 @@
1
- from . import base, log, regazzoni2020, zenker, units, time_varying_elastance
2
-
3
- __all__ = ["base", "log", "regazzoni2020", "zenker", "units", "time_varying_elastance"]
File without changes
File without changes
File without changes