circulation 0.1.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.
@@ -0,0 +1,3 @@
1
+ from . import base, log, regazzoni2020, zenkur, units, time_varying_elastance
2
+
3
+ __all__ = ["base", "log", "regazzoni2020", "zenkur", "units", "time_varying_elastance"]
circulation/base.py ADDED
@@ -0,0 +1,208 @@
1
+ from __future__ import annotations
2
+ from typing import Callable, Any
3
+ from abc import ABC, abstractmethod
4
+ import json
5
+ from collections import defaultdict
6
+ import numpy as np
7
+ import time
8
+ import logging
9
+ from rich.table import Table
10
+
11
+ from . import units
12
+ from . import time_varying_elastance
13
+ from . import log
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def smooth_heavyside(x):
20
+ return np.arctan(np.pi / 2 * x * 200) * 1 / np.pi + 0.5
21
+
22
+
23
+ def remove_units(parameters: dict[str, Any]) -> dict[str, Any]:
24
+ d = {}
25
+ for k, v in parameters.items():
26
+ if isinstance(v, units.pint.Quantity):
27
+ d[k] = v.magnitude
28
+ elif isinstance(v, dict):
29
+ d[k] = remove_units(v)
30
+ else:
31
+ d[k] = v
32
+ return d
33
+
34
+
35
+ class CirculationModel(ABC):
36
+ def __init__(
37
+ self,
38
+ parameters: dict[str, Any] | None = None,
39
+ add_units: bool = False,
40
+ callback: Callable[[float], None] | None = None,
41
+ verbose: bool = False,
42
+ ):
43
+ self.parameters = type(self).default_parameters()
44
+ if parameters is not None:
45
+ self.parameters.update(parameters)
46
+ if not add_units:
47
+ self.parameters = remove_units(self.parameters)
48
+ self._add_units = add_units
49
+
50
+ if callback is not None:
51
+ assert callable(callback), "callback must be callable"
52
+
53
+ self.callback = callback
54
+ else:
55
+ self.callback = lambda t: None
56
+ self._verbose = verbose
57
+
58
+ def _initialize(self):
59
+ self.var = {}
60
+ self.state = type(self).default_initial_conditions()
61
+ self.update_state()
62
+ self.update_static_variables(0.0)
63
+
64
+ @property
65
+ def THB(self):
66
+ if self._add_units:
67
+ return (1 / self.parameters["BPM"]).to(units.ureg("s"))
68
+
69
+ return 60.0 / self.parameters["BPM"]
70
+
71
+ @staticmethod
72
+ @abstractmethod
73
+ def default_parameters() -> dict[str, Any]: ...
74
+
75
+ @abstractmethod
76
+ def update_static_variables(self, t: float):
77
+ pass
78
+
79
+ def update_state(self, state: dict[str, float] | None = None):
80
+ if state is not None:
81
+ self.state.update(state)
82
+
83
+ if not self._add_units:
84
+ self.state = remove_units(self.state)
85
+
86
+ @staticmethod
87
+ @abstractmethod
88
+ def default_initial_conditions() -> dict[str, float]: ...
89
+
90
+ def time_varying_elastance(self, EA, EB, tC, TC, TR, **kwargs):
91
+ return time_varying_elastance.blanco_ventricle(
92
+ EA=EA,
93
+ EB=EB,
94
+ tC=tC,
95
+ TC=TC,
96
+ TR=TR,
97
+ THB=self.THB,
98
+ )
99
+
100
+ def flux_through_valve(self, p1, p2, R):
101
+ return (p1 - p2) / R(p1, p2)
102
+
103
+ def _R(
104
+ self,
105
+ Rmin: float,
106
+ Rmax: float,
107
+ unit_R: float = 1.0,
108
+ unit_p: float = 1.0,
109
+ ) -> Callable[[float, float], float]:
110
+ return lambda w, v: unit_R * 10.0 ** (
111
+ np.log10(Rmin / unit_R)
112
+ + (np.log10(Rmax / unit_R) - np.log10(Rmin / unit_R))
113
+ * smooth_heavyside((v - w) / unit_p)
114
+ )
115
+
116
+ @abstractmethod
117
+ def step(self, t: float, dt: float) -> None: ...
118
+
119
+ def solve(
120
+ self,
121
+ T: float | None = None,
122
+ num_cycles: int | None = None,
123
+ initial_state: dict[str, float] | None = None,
124
+ dt: float = 1e-3,
125
+ dt_eval: float | None = None,
126
+ ):
127
+ logger.info("Running circulation model")
128
+ if T is None:
129
+ assert num_cycles is not None, "Please provide num_cycles or T"
130
+ T = self.THB * num_cycles
131
+
132
+ initial_state = initial_state or dict()
133
+
134
+ if dt_eval is None:
135
+ output_every_n_steps = 1
136
+ else:
137
+ output_every_n_steps = np.round(dt_eval / dt)
138
+
139
+ self.update_state(state=initial_state)
140
+ self.initialize_output()
141
+ t = 0.0
142
+ if self._add_units:
143
+ t *= units.ureg("s")
144
+ dt *= units.ureg("s")
145
+
146
+ self.store(t)
147
+
148
+ time_start = time.time()
149
+
150
+ i = 0
151
+ while t < T:
152
+ self.callback(t)
153
+ self.step(t, dt)
154
+ if i % output_every_n_steps == 0:
155
+ self.store(t)
156
+ if self._verbose:
157
+ self.print_info()
158
+ t += dt
159
+ i += 1
160
+
161
+ duration = time.time() - time_start
162
+
163
+ logger.info("Done running circulation model in elapsed time %1.4f s" % duration)
164
+ return self.results
165
+
166
+ def initialize_output(self):
167
+ self.results = defaultdict(list)
168
+
169
+ def store(self, t):
170
+ get = lambda x: x if not self._add_units else x.magnitude
171
+
172
+ self.results["time"].append(get(t))
173
+ for k, v in self.state.items():
174
+ self.results[k].append(get(v))
175
+ for k, v in self.var.items():
176
+ self.results[k].append(get(v))
177
+
178
+ def save_state(self, filename):
179
+ with open(filename, mode="w", newline="") as outfile:
180
+ json.dump(self.state, outfile, indent=2)
181
+
182
+ @property
183
+ def volumes(self) -> dict[str, float]:
184
+ return {}
185
+
186
+ @property
187
+ def pressures(self) -> dict[str, float]:
188
+ return {}
189
+
190
+ @property
191
+ def flows(self) -> dict[str, float]:
192
+ return {}
193
+
194
+ def print_info(self):
195
+ msg = []
196
+ for attr, title in [
197
+ (self.volumes, "Volumes"),
198
+ (self.pressures, "Pressures"),
199
+ (self.flows, "Flows"),
200
+ ]:
201
+ table = Table(title=title)
202
+ row = []
203
+ for k, v in attr.items():
204
+ table.add_column(k)
205
+ row.append(f"{v:.3f}")
206
+ table.add_row(*row)
207
+ msg.append(f"\n{log.log_table(table)}")
208
+ logger.info("".join(msg))
circulation/log.py ADDED
@@ -0,0 +1,24 @@
1
+ import logging
2
+ from rich.logging import RichHandler
3
+ from rich.console import Console
4
+ from rich.text import Text
5
+
6
+
7
+ def log_table(rich_table):
8
+ """Generate an ascii formatted presentation of a Rich table
9
+ Eliminates any column styling
10
+ """
11
+ console = Console(width=150)
12
+ with console.capture() as capture:
13
+ console.print(rich_table)
14
+ return Text.from_ansi(capture.get())
15
+
16
+
17
+ def setup_logging(level=logging.DEBUG, comm=None):
18
+ handlers = [RichHandler(console=Console(width=200))]
19
+ if comm is not None:
20
+ handlers[0].addFilter(lambda record: 1 if comm.rank == 0 else 0)
21
+ logging.basicConfig(
22
+ level=level,
23
+ handlers=handlers,
24
+ )
circulation/lv_only.py ADDED
@@ -0,0 +1,115 @@
1
+ import numpy as np
2
+ import json
3
+ import time
4
+ from collections import defaultdict
5
+ # from scipy.integrate import RK45, solve_ivp
6
+
7
+
8
+ from . import base
9
+ from . import units
10
+
11
+ mL = units.ureg("mL")
12
+ mmHg = units.ureg("mmHg")
13
+ s = units.ureg("s")
14
+
15
+
16
+ class LVOnly(base.CirculationModel):
17
+ """
18
+ 0D model of the left ventricle only.
19
+
20
+ """
21
+
22
+ def __init__(self, parameters: dict[str, float] | None = None, add_units=False):
23
+ super().__init__(parameters, add_units=add_units)
24
+
25
+ ############ Chambers
26
+ chambers = self.parameters["chambers"]
27
+
28
+ self.E_LV = self.time_varying_elastance(**chambers["LV"])
29
+ self.p_LV_func = lambda V, t: self.E_LV(t) * (V - chambers["LV"]["V0"])
30
+ self.var = {}
31
+
32
+ @staticmethod
33
+ def default_parameters() -> base.CirculcationModelParams:
34
+ return {
35
+ "BPM": 75.0 * units.ureg("1/minutes"),
36
+ "chambers": {
37
+ "LV": {
38
+ "EA": 2.75 * mmHg / mL,
39
+ "EB": 0.008 * mmHg / mL,
40
+ "TC": 0.34 * s,
41
+ "TR": 0.17 * s,
42
+ "tC": 0.00 * s,
43
+ "V0": -9.0 * mL,
44
+ },
45
+ },
46
+ "circulation": {
47
+ "SYS": {
48
+ "Rao": 0.5 * mmHg * s / mL,
49
+ "R_sys": 2.5 * mmHg * s / mL,
50
+ "p_dia": 10.0 * mmHg,
51
+ "C_sys": 0.1 * mL / mmHg,
52
+ "Pp": 4.5 * mmHg,
53
+ },
54
+ },
55
+ }
56
+
57
+ @staticmethod
58
+ def default_initial_conditions() -> dict[str, float]:
59
+ return {
60
+ "V_LV": 100.0 * mL,
61
+ "p_ao": 70.0 * mmHg,
62
+ "dp_ao_dt": 0.0 * mmHg / s,
63
+ }
64
+
65
+ def update_static_variables(self, t):
66
+ self.var["p_LV"] = self.p_LV_func(self.state["V_LV"], t)
67
+
68
+ def step(self, t, dt):
69
+ self.update_static_variables(t)
70
+
71
+ p_ao = self.state["p_ao"]
72
+ dp_ao_dt = self.state["dp_ao_dt"]
73
+ p_LV = self.var["p_LV"]
74
+
75
+ C_sys = self.parameters["circulation"]["SYS"]["C_sys"]
76
+ R_ao = self.parameters["circulation"]["SYS"]["Rao"]
77
+ R_sys = self.parameters["circulation"]["SYS"]["R_sys"]
78
+ p_dia = self.parameters["circulation"]["SYS"]["p_dia"]
79
+
80
+ Q = (p_LV - p_ao) / R_ao
81
+ Q_R = (p_ao - p_dia) / R_sys
82
+ Q_C = C_sys * dp_ao_dt
83
+
84
+ dQ_C_dt = (Q - Q_R - Q_C) / C_sys
85
+ d2p_ao_dt2 = dQ_C_dt
86
+
87
+ self.state["p_ao"] += dt * dp_ao_dt
88
+ self.state["dp_ao_dt"] += dt * d2p_ao_dt2
89
+ if p_LV > p_ao:
90
+ self.state["V_LV"] += -dt * Q
91
+
92
+ def print_info(self):
93
+ C_VEN_SYS = self.parameters["circulation"]["SYS"]["C_VEN"]
94
+ C_AR_SYS = self.parameters["circulation"]["SYS"]["C_AR"]
95
+
96
+ print("V_LV = %4.2f mL" % self.state["V_LV"])
97
+ print("V_AR_SYS = %4.2f mL" % (C_AR_SYS * self.state["p_AR_SYS"]))
98
+ print("V_VEN_SYS = %4.2f mL" % (C_VEN_SYS * self.state["p_VEN_SYS"]))
99
+
100
+ V_tot_heart = (
101
+ self.state["V_LA"]
102
+ + self.state["V_LV"]
103
+ + self.state["V_RA"]
104
+ + self.state["V_RV"]
105
+ )
106
+ V_tot_SYS = (
107
+ C_AR_SYS * self.state["p_AR_SYS"] + C_VEN_SYS * self.state["p_VEN_SYS"]
108
+ )
109
+
110
+ V_tot = V_tot_heart + V_tot_SYS
111
+ print("======================")
112
+ print("V (heart) = %4.2f mL" % V_tot_heart)
113
+ print("V (SYS) = %4.2f mL" % V_tot_SYS)
114
+ print("======================")
115
+ print("V = %4.2f mL" % V_tot)
@@ -0,0 +1,166 @@
1
+ import numpy as np
2
+ import json
3
+ import time
4
+ from collections import defaultdict
5
+ # from scipy.integrate import RK45, solve_ivp
6
+
7
+
8
+ from . import base
9
+ from . import units
10
+
11
+ mL = units.ureg("mL")
12
+ mmHg = units.ureg("mmHg")
13
+ s = units.ureg("s")
14
+
15
+
16
+ class LVOnly(base.CirculationModel):
17
+ """
18
+ 0D model of the left ventricle only.
19
+
20
+ """
21
+
22
+ def __init__(self, parameters: dict[str, float] | None = None, add_units=False):
23
+ super().__init__(parameters, add_units=add_units)
24
+
25
+ ############ Chambers
26
+ chambers = self.parameters["chambers"]
27
+
28
+ self.E_LV = self.time_varying_elastance(**chambers["LV"])
29
+
30
+ ############ Valves
31
+ valves = self.parameters["valves"]
32
+
33
+ if self._add_units:
34
+ unit_R = 1 * mmHg * s / mL
35
+ unit_p = 1 * mmHg
36
+ else:
37
+ unit_R = 1
38
+ unit_p = 1
39
+
40
+ self.R_AV = self._R(
41
+ valves["AV"]["Rmin"], valves["AV"]["Rmax"], unit_R=unit_R, unit_p=unit_p
42
+ )
43
+ self.R_MV = self._R(
44
+ valves["MV"]["Rmin"], valves["MV"]["Rmax"], unit_R=unit_R, unit_p=unit_p
45
+ )
46
+
47
+ ############ PV relationships
48
+ self.p_LV_func = lambda V, t: self.E_LV(t) * (V - chambers["LV"]["V0"])
49
+ self.var = {}
50
+
51
+ @staticmethod
52
+ def default_parameters() -> base.CirculcationModelParams:
53
+ return {
54
+ "BPM": 75.0 * units.ureg("1/minutes"),
55
+ "chambers": {
56
+ "LV": {
57
+ "EA": 2.75 * mmHg / mL,
58
+ "EB": 0.08 * mmHg / mL,
59
+ "TC": 0.34 * s,
60
+ "TR": 0.17 * s,
61
+ "tC": 0.00 * s,
62
+ "V0": 5.0 * mL,
63
+ },
64
+ },
65
+ "valves": {
66
+ "MV": {
67
+ "Rmin": 0.0075 * mmHg * s / mL,
68
+ "Rmax": 75006.2 * mmHg * s / mL,
69
+ },
70
+ "AV": {
71
+ "Rmin": 0.0075 * mmHg * s / mL,
72
+ "Rmax": 75006.2 * mmHg * s / mL,
73
+ },
74
+ },
75
+ "circulation": {
76
+ "SYS": {
77
+ "R_AR": 0.8 * mmHg * s / mL,
78
+ "C_AR": 1.2 * mL / mmHg,
79
+ "R_VEN": 0.26 * mmHg * s / mL,
80
+ "C_VEN": 60.0 * mL / mmHg,
81
+ "L_AR": 5e-3 * mmHg * s**2 / mL,
82
+ "L_VEN": 5e-4 * mmHg * s**2 / mL,
83
+ },
84
+ },
85
+ }
86
+
87
+ @staticmethod
88
+ def default_initial_conditions() -> dict[str, float]:
89
+ return {
90
+ "V_LA": 65.0 * mL,
91
+ "V_LV": 120.0 * mL,
92
+ "V_RA": 65.0 * mL,
93
+ "V_RV": 145.0 * mL,
94
+ "p_AR_SYS": 80.0 * mmHg,
95
+ "p_VEN_SYS": 30.0 * mmHg,
96
+ "p_AR_PUL": 35.0 * mmHg,
97
+ "p_VEN_PUL": 24.0 * mmHg,
98
+ "Q_AR_SYS": 0.0 * mL / s,
99
+ "Q_VEN_SYS": 0.0 * mL / s,
100
+ "Q_AR_PUL": 0.0 * mL / s,
101
+ "Q_VEN_PUL": 0.0 * mL / s,
102
+ }
103
+
104
+ def update_static_variables(self, t):
105
+ self.var["p_LV"] = self.p_LV_func(self.state["V_LV"], t)
106
+ self.var["Q_AV"] = self.flux_through_valve(
107
+ self.var["p_LV"], self.state["p_AR_SYS"], self.R_AV
108
+ )
109
+ self.var["Q_MV"] = self.flux_through_valve(
110
+ self.state["p_VEN_SYS"], self.var["p_LV"], self.R_MV
111
+ )
112
+
113
+ def step(self, t, dt):
114
+ self.update_static_variables(t)
115
+
116
+ Q_VEN_SYS = self.state["Q_VEN_SYS"]
117
+ Q_AR_SYS = self.state["Q_AR_SYS"]
118
+ Q_AV = self.var["Q_AV"]
119
+ Q_MV = self.var["Q_MV"]
120
+
121
+ p_LV = self.var["p_LV"]
122
+ p_AR_SYS = self.state["p_AR_SYS"]
123
+ p_VEN_SYS = self.state["p_VEN_SYS"]
124
+
125
+ C_VEN_SYS = self.parameters["circulation"]["SYS"]["C_VEN"]
126
+ C_AR_SYS = self.parameters["circulation"]["SYS"]["C_AR"]
127
+ R_AR_SYS = self.parameters["circulation"]["SYS"]["R_AR"]
128
+ R_VEN_SYS = self.parameters["circulation"]["SYS"]["R_VEN"]
129
+ L_AR_SYS = self.parameters["circulation"]["SYS"]["L_AR"]
130
+ L_VEN_SYS = self.parameters["circulation"]["SYS"]["L_VEN"]
131
+
132
+ self.state["V_LV"] += dt * (Q_MV - Q_AV)
133
+
134
+ self.state["p_AR_SYS"] += dt * (Q_AV - Q_AR_SYS) / C_AR_SYS
135
+ self.state["p_VEN_SYS"] += dt * (Q_AR_SYS - Q_VEN_SYS) / C_VEN_SYS
136
+ self.state["Q_AR_SYS"] += (
137
+ -dt * (R_AR_SYS * Q_AR_SYS + p_VEN_SYS - p_AR_SYS) / L_AR_SYS
138
+ )
139
+ self.state["Q_VEN_SYS"] += (
140
+ -dt * (R_VEN_SYS * Q_VEN_SYS + p_LV - p_VEN_SYS) / L_VEN_SYS
141
+ )
142
+
143
+ def print_info(self):
144
+ C_VEN_SYS = self.parameters["circulation"]["SYS"]["C_VEN"]
145
+ C_AR_SYS = self.parameters["circulation"]["SYS"]["C_AR"]
146
+
147
+ print("V_LV = %4.2f mL" % self.state["V_LV"])
148
+ print("V_AR_SYS = %4.2f mL" % (C_AR_SYS * self.state["p_AR_SYS"]))
149
+ print("V_VEN_SYS = %4.2f mL" % (C_VEN_SYS * self.state["p_VEN_SYS"]))
150
+
151
+ V_tot_heart = (
152
+ self.state["V_LA"]
153
+ + self.state["V_LV"]
154
+ + self.state["V_RA"]
155
+ + self.state["V_RV"]
156
+ )
157
+ V_tot_SYS = (
158
+ C_AR_SYS * self.state["p_AR_SYS"] + C_VEN_SYS * self.state["p_VEN_SYS"]
159
+ )
160
+
161
+ V_tot = V_tot_heart + V_tot_SYS
162
+ print("======================")
163
+ print("V (heart) = %4.2f mL" % V_tot_heart)
164
+ print("V (SYS) = %4.2f mL" % V_tot_SYS)
165
+ print("======================")
166
+ print("V = %4.2f mL" % V_tot)