Skip to content

Commit 6921097

Browse files
committed
Add references to other demos
1 parent d9609e4 commit 6921097

1 file changed

Lines changed: 84 additions & 58 deletions

File tree

demo/time_dependent/complete_cycle.py

Lines changed: 84 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# # Complete Multiscale Simulation with Prestressing
22
#
33
# This comprehensive demo illustrates a complete cardiac mechanics pipeline involving:
4-
# 1. **Geometry**: Generating a Bi-Ventricular (BiV) mesh from the UK Biobank Atlas, rotating it, and generating fiber fields using LDRB.
4+
# 1. **Geometry**: Generating a Bi-Ventricular (BiV) mesh from the UK Biobank Atlas, rotating it, and generating fiber fields using LDRB, which is similar to what is implemented in [rotated BiV demo](../boundary_conditions/ukb_bcs.py). In addition we show how to generate additional fields such as longitudinal and circumferential fields for computing e.g longitudinal strain, similar to the [additional data demo in `caridac-geometriesx`](https://computationalphysiology.github.io/cardiac-geometriesx/demos/additional_data.html)
55
# 2. **0D Circulation**: Running a 0D closed-loop circulation model (Regazzoni) to establish physiological pressure traces.
6-
# 3. **Prestressing**: Solving the Inverse Elasticity Problem (IEP) to find the unloaded reference configuration that matches the atlas geometry at End-Diastole (ED).
6+
# 3. **Prestressing**: Solving the Inverse Elasticity Problem (IEP) to find the unloaded reference configuration that matches the atlas geometry at End-Diastole (ED). This is similar to what is impemtented in [the BiV prestress demo](../prestress/prestress_biv.py)
77
# 4. **Inflation**: Ramping the unloaded mesh back to the End-Diastolic state to initialize the dynamic simulation.
8-
# 5. **Multiscale Coupling**: Running a forward simulation coupled to the 0D circulation model.
8+
# 5. **Multiscale Coupling**: Running a forward simulation coupled to the 0D circulation model, which is similar to what is implemented in the [time dependent BiV problem](time_dependent_land_circ_biv.py)
99
# 6. **Post-processing**: Computing Fiber Stress and Fiber Strain.
1010
#
1111
# ---
@@ -62,7 +62,7 @@ def filter(self, record):
6262
return 1 if self.comm.rank == 0 else 0
6363

6464

65-
outdir = Path("results_biv_complete_cycle")
65+
outdir = Path("results_biv_complete_cycle2")
6666
outdir.mkdir(parents=True, exist_ok=True)
6767
geodir = outdir / "geometry"
6868

@@ -83,18 +83,6 @@ def run_0D(outdir):
8383
logger.info("Running 0D circulation model to steady state...")
8484
model = Regazzoni2020()
8585
history = model.solve(num_beats=10)
86-
87-
if comm.rank == 0:
88-
fig, ax = plt.subplots(2, 2, sharex=True, sharey="row", figsize=(10, 5))
89-
ax[0, 0].plot(history["V_LV"], history["p_LV"])
90-
ax[0, 0].set_xlabel("V (LV) [mL]")
91-
ax[0, 0].set_ylabel("p (LV) [mmHg]")
92-
ax[0, 0].set_title("All beats")
93-
ax[0, 1].plot(history["V_LV"][-1000:], history["p_LV"][-1000:])
94-
ax[0, 1].set_title("Last beat")
95-
fig.savefig(outdir / "0D_circulation_pv.png")
96-
plt.close(fig)
97-
9886
state = dict(zip(model.state_names(), model.state))
9987
np.save(outdir / "state.npy", state, allow_pickle=True)
10088
np.save(outdir / "history.npy", history, allow_pickle=True)
@@ -106,6 +94,27 @@ def run_0D(outdir):
10694
comm.Barrier()
10795
history = np.load(outdir / "history.npy", allow_pickle=True).item()
10896

97+
98+
# Let us plot the results from the circulation model
99+
100+
if comm.rank == 0:
101+
fig, ax = plt.subplots(2, 2, sharex=True, sharey="row", figsize=(10, 5))
102+
ax[0, 0].plot(history["V_LV"], history["p_LV"])
103+
ax[0, 0].set_xlabel("V (LV) [mL]")
104+
ax[0, 0].set_ylabel("p (LV) [mmHg]")
105+
ax[0, 0].set_title("All beats")
106+
ax[0, 1].plot(history["V_LV"][-1000:], history["p_LV"][-1000:])
107+
ax[0, 1].set_title("Last beat")
108+
ax[1, 0].plot(history["V_RV"], history["p_RV"])
109+
ax[1, 0].set_xlabel("V (RV) [mL]")
110+
ax[1, 0].set_ylabel("p (RV) [mmHg]")
111+
ax[1, 1].plot(history["V_RV"][-1000:], history["p_RV"][-1000:])
112+
fig.savefig(outdir / "0D_circulation_pv.png")
113+
114+
if comm.rank == 0:
115+
plt.close(fig)
116+
117+
109118
# ## 3. Activation Model (Synthetic)
110119
#
111120
# We use the Blanco time-varying elastance function for a synthetic activation trace.
@@ -123,24 +132,36 @@ def get_activation(t):
123132
)(t)
124133

125134

135+
# Let us plot the resulting activation trace
136+
137+
if comm.rank == 0:
138+
fig, ax = plt.subplots()
139+
t = np.linspace(0, 1, 100)
140+
ax.plot(t, get_activation(t))
141+
fig.savefig(outdir / "activation.png")
142+
143+
if comm.rank == 0:
144+
plt.close(fig)
145+
146+
126147
# ## 4. Geometry Generation & Rotation
127-
# # We generate the BiV geometry from the UK Biobank Atlas, rotate it to align the base normal with the x-axis,
148+
# We generate the BiV geometry from the UK Biobank Atlas, rotate it to align the base normal with the x-axis,
128149
# and generate fiber fields using LDRB. The fibers are based on the fiber orientation angles from
129150
# https://doi.org/10.1002/cnm.3185. We also generate AHA segments for later analysis.
130151
# Additional data such as fibers in DG 1 space are also stored for post-processing.
131152
# This is useful if we want to compute stress/strain at intermediate points later on.
132153
# The fibers used for the mechanics simulation are in a quadrature space to avoid interpolation errors.
133154

134155

135-
if not (geodir / "mesh.xdmf").exists() and comm.rank == 0:
156+
if not (geodir / "geometry.bp").exists():
136157
logger.info("Generating and processing geometry...")
137158
mode = -1
138159
std = 0
139160
char_length = 10.0
140161

141162
geo = cardiac_geometries.mesh.ukb(
142163
outdir=geodir,
143-
comm=MPI.COMM_SELF,
164+
comm=comm,
144165
mode=mode,
145166
std=std,
146167
case="ED",
@@ -152,11 +173,7 @@ def get_activation(t):
152173
# Rotate Mesh (Base Normal -> X-axis)
153174
geo = geo.rotate(target_normal=[1.0, 0.0, 0.0], base_marker="BASE")
154175

155-
# Generate Fibers (LDRB)
156-
system = ldrb.dolfinx_ldrb(
157-
mesh=geo.mesh,
158-
ffun=geo.ffun,
159-
markers=cardiac_geometries.mesh.transform_markers(geo.markers, clipped=True),
176+
fiber_angles = dict(
160177
alpha_endo_lv=60,
161178
alpha_epi_lv=-60,
162179
alpha_endo_rv=90,
@@ -165,6 +182,14 @@ def get_activation(t):
165182
beta_epi_lv=20,
166183
beta_endo_rv=0,
167184
beta_epi_rv=20,
185+
)
186+
187+
# Generate Fibers (LDRB)
188+
system = ldrb.dolfinx_ldrb(
189+
mesh=geo.mesh,
190+
ffun=geo.ffun,
191+
markers=cardiac_geometries.mesh.transform_markers(geo.markers, clipped=True),
192+
**fiber_angles,
168193
fiber_space="Quadrature_6",
169194
)
170195

@@ -190,8 +215,7 @@ def get_activation(t):
190215
mesh=geo.mesh,
191216
ffun=geo.ffun,
192217
markers=cardiac_geometries.mesh.transform_markers(geo.markers, clipped=True),
193-
alpha_endo_lv=60,
194-
alpha_epi_lv=-60,
218+
**fiber_angles,
195219
fiber_space=fiber_space,
196220
)
197221

@@ -384,38 +408,6 @@ def dirichlet_bc(V: dolfinx.fem.FunctionSpace):
384408
parameters={"mesh_unit": mesh_unit, "u_space": "P_2"},
385409
)
386410

387-
# ## 8. Inflation (Reference -> End-Diastole)
388-
#
389-
# We now ramp the volumes from the unloaded state back to the target ED volumes.
390-
# This establishes the correct initial condition (stress/strain) for the time-dependent loop.
391-
392-
logger.info("Inflating to End-Diastolic Target...")
393-
ramp_steps = 10
394-
for i in range(ramp_steps):
395-
factor = (i + 1) / ramp_steps
396-
397-
# Interpolate volume
398-
current_lvv = lvv_unloaded + factor * (lvv_target - lvv_unloaded)
399-
current_rvv = rvv_unloaded + factor * (rvv_target - rvv_unloaded)
400-
401-
lv_volume.value = current_lvv
402-
rv_volume.value = current_rvv
403-
404-
problem.solve()
405-
406-
# Log pressures to ensure we are reaching target
407-
plv = problem.cavity_pressures[0].x.array[0] * 1e-3
408-
prv = problem.cavity_pressures[1].x.array[0] * 1e-3
409-
if comm.rank == 0:
410-
logger.info(f"Inflation Step {i + 1}/{ramp_steps}: pLV={plv:.2f} kPa, pRV={prv:.2f} kPa")
411-
412-
# Store old values for time-stepping and handling if solver fails
413-
414-
problem.old_Ta = Ta.value.value.copy() # type: ignore
415-
problem.old_lv_volume = lv_volume.value.copy() # type: ignore
416-
problem.old_rv_volume = rv_volume.value.copy() # type: ignore
417-
418-
# ## 9. Post-Processing Setup (Fiber Stress/Strain Only)
419411
# We set up functions to compute fiber stress and fiber strain during the simulation for post-processing.
420412

421413
W = dolfinx.fem.functionspace(geometry.mesh, ("DG", 1))
@@ -445,6 +437,7 @@ def dirichlet_bc(V: dolfinx.fem.FunctionSpace):
445437
ufl.inner(E * f0_map, f0_map), W.element.interpolation_points,
446438
)
447439

440+
448441
# VTX Writers for visualization in ParaView
449442

450443
vtx = dolfinx.io.VTXWriter(
@@ -456,10 +449,43 @@ def dirichlet_bc(V: dolfinx.fem.FunctionSpace):
456449
[fiber_stress, fiber_strain],
457450
engine="BP4",
458451
)
452+
453+
# ## 8. Inflation (Reference -> End-Diastole)
454+
#
455+
# We now ramp the volumes from the unloaded state back to the target ED volumes.
456+
# This establishes the correct initial condition (stress/strain) for the time-dependent loop.
457+
458+
logger.info("Inflating to End-Diastolic Target...")
459+
ramp_steps = 10
460+
for i in range(ramp_steps):
461+
factor = (i + 1) / ramp_steps
462+
463+
# Interpolate volume
464+
current_lvv = lvv_unloaded + factor * (lvv_target - lvv_unloaded)
465+
current_rvv = rvv_unloaded + factor * (rvv_target - rvv_unloaded)
466+
467+
lv_volume.value = current_lvv
468+
rv_volume.value = current_rvv
469+
470+
problem.solve()
471+
472+
# Log pressures to ensure we are reaching target
473+
plv = problem.cavity_pressures[0].x.array[0] * 1e-3
474+
prv = problem.cavity_pressures[1].x.array[0] * 1e-3
475+
if comm.rank == 0:
476+
logger.info(f"Inflation Step {i + 1}/{ramp_steps}: pLV={plv:.2f} kPa, pRV={prv:.2f} kPa")
477+
478+
459479
vtx.write(0.0)
460480
vtx_stress.write(0.0)
461481

462482

483+
# Store old values for time-stepping and handling if solver fails
484+
485+
problem.old_Ta = Ta.value.value.copy() # type: ignore
486+
problem.old_lv_volume = lv_volume.value.copy() # type: ignore
487+
problem.old_rv_volume = rv_volume.value.copy() # type: ignore
488+
463489
# ## 10. Multiscale Coupling Loop
464490

465491

0 commit comments

Comments
 (0)