|
| 1 | +# # Problem 2: Inflation of a ventricle |
| 2 | +# In the second problem we will solve the inflation of a ventricle. First we import the necessary libraries |
| 3 | +# |
| 4 | + |
| 5 | +from pathlib import Path |
| 6 | +from mpi4py import MPI |
| 7 | +from dolfinx import log |
| 8 | +import dolfinx |
| 9 | +import numpy as np |
| 10 | +import math |
| 11 | +import cardiac_geometries |
| 12 | +import fenicsx_pulse |
| 13 | + |
| 14 | +# Next we will create the geometry and save it in the folder called `lv_ellipsoid`. |
| 15 | + |
| 16 | +geodir = Path("lv_ellipsoid") |
| 17 | +if not geodir.exists(): |
| 18 | + cardiac_geometries.mesh.lv_ellipsoid( |
| 19 | + outdir=geodir, |
| 20 | + r_short_endo=7.0, |
| 21 | + r_short_epi=10.0, |
| 22 | + r_long_endo=17.0, |
| 23 | + r_long_epi=20.0, |
| 24 | + mu_apex_endo = -math.pi, |
| 25 | + mu_base_endo = -math.acos(5 / 17), |
| 26 | + mu_apex_epi = -math.pi, |
| 27 | + mu_base_epi = -math.acos(5 / 20), |
| 28 | + ) |
| 29 | + |
| 30 | +# If the folder already exist, then we just load the geometry |
| 31 | + |
| 32 | +geo = cardiac_geometries.geometry.Geometry.from_folder( |
| 33 | + comm=MPI.COMM_WORLD, |
| 34 | + folder=geodir, |
| 35 | +) |
| 36 | + |
| 37 | +# Now, lets convert the geometry to a `fenicsx_pulse.Geometry` object. |
| 38 | + |
| 39 | +geometry = fenicsx_pulse.Geometry.from_cardiac_geometries(geo, metadata={"quadrature_degree": 4}) |
| 40 | + |
| 41 | + |
| 42 | +# The material model used in this benchmark is the {py:class}`Guccione <fenicsx_pulse.material_models.guccione.Guccione>` model. |
| 43 | + |
| 44 | +material_params = { |
| 45 | + "C": dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(10.0)), |
| 46 | + "bf": dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(1.0)), |
| 47 | + "bt": dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(1.0)), |
| 48 | + "bfs": dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(1.0)), |
| 49 | +} |
| 50 | +material = fenicsx_pulse.Guccione(**material_params) |
| 51 | + |
| 52 | + |
| 53 | +# There are now active contraction, so we choose a pure passive model |
| 54 | + |
| 55 | +active_model = fenicsx_pulse.active_model.Passive() |
| 56 | + |
| 57 | +# and the model should be incompressible |
| 58 | + |
| 59 | +comp_model = fenicsx_pulse.Incompressible() |
| 60 | + |
| 61 | + |
| 62 | +# and assembles the `CardiacModel` |
| 63 | + |
| 64 | +model = fenicsx_pulse.CardiacModel( |
| 65 | + material=material, |
| 66 | + active=active_model, |
| 67 | + compressibility=comp_model, |
| 68 | +) |
| 69 | + |
| 70 | + |
| 71 | +# Next we need to apply some boundary conditions. For the Dirichlet BC we can supply a function that takes as input the state space and returns a list of `DirichletBC`. We will fix the base in all directions. Note that the displacement is in the first subspace since we use an incompressible model. The hydrostatic pressure is in the second subspace |
| 72 | + |
| 73 | +def dirichlet_bc( |
| 74 | + state_space: dolfinx.fem.FunctionSpace, |
| 75 | +) -> list[dolfinx.fem.bcs.DirichletBC]: |
| 76 | + V, _ = state_space.sub(0).collapse() |
| 77 | + facets = geometry.facet_tags.find( |
| 78 | + geo.markers["BASE"][0], |
| 79 | + ) # Specify the marker used on the boundary |
| 80 | + geometry.mesh.topology.create_connectivity( |
| 81 | + geometry.mesh.topology.dim - 1, |
| 82 | + geometry.mesh.topology.dim, |
| 83 | + ) |
| 84 | + dofs = dolfinx.fem.locate_dofs_topological((state_space.sub(0), V), 2, facets) |
| 85 | + u_fixed = dolfinx.fem.Function(V) |
| 86 | + u_fixed.x.array[:] = 0.0 |
| 87 | + return [dolfinx.fem.dirichletbc(u_fixed, dofs, state_space.sub(0))] |
| 88 | + |
| 89 | + |
| 90 | +# We apply a traction in endocardium |
| 91 | + |
| 92 | +traction = dolfinx.fem.Constant(geometry.mesh, dolfinx.default_scalar_type(0.0)) |
| 93 | +neumann = fenicsx_pulse.NeumannBC(traction=traction, marker=geo.markers["ENDO"][0]) |
| 94 | + |
| 95 | +# and finally combine all the boundary conditions |
| 96 | + |
| 97 | +bcs = fenicsx_pulse.BoundaryConditions(dirichlet=(dirichlet_bc,), neumann=(neumann,)) |
| 98 | + |
| 99 | +# and create a Mixed problem |
| 100 | + |
| 101 | +problem = fenicsx_pulse.MechanicsProblemMixed(model=model, geometry=geometry, bcs=bcs) |
| 102 | + |
| 103 | +# Now we can solve the problem |
| 104 | +log.set_log_level(log.LogLevel.INFO) |
| 105 | + |
| 106 | +problem.solve() |
| 107 | + |
| 108 | +# Now step up the pressure to 10 kPa starting with an increment of 1 kPa |
| 109 | +target_value = 10.0 |
| 110 | +incr = 1.0 |
| 111 | + |
| 112 | +# Here we use a continuation strategy to speed up the convergence |
| 113 | + |
| 114 | +use_continuation = True |
| 115 | + |
| 116 | +old_states = [problem.state.copy()] |
| 117 | +old_tractions = [traction.value.copy()] |
| 118 | + |
| 119 | +while traction.value < target_value: |
| 120 | + value = min(traction.value + incr, target_value) |
| 121 | + print(f"Solving problem for traction={value}") |
| 122 | + |
| 123 | + if use_continuation and len(old_tractions) > 1: |
| 124 | + d = (value - old_tractions[-2]) / (old_tractions[-1] - old_tractions[-2]) |
| 125 | + problem.state.x.array[:] = (1 - d) * old_states[-2].x.array + d * old_states[-1].x.array |
| 126 | + |
| 127 | + traction.value = value |
| 128 | + |
| 129 | + try: |
| 130 | + nit, converged = problem.solve() |
| 131 | + except RuntimeError: |
| 132 | + |
| 133 | + # Reset state and half the increment |
| 134 | + traction.value = old_tractions[-1] |
| 135 | + problem.state.x.array[:] = old_states[-1].x.array |
| 136 | + incr *= 0.5 |
| 137 | + else: |
| 138 | + if nit < 3: |
| 139 | + # Increase increment |
| 140 | + incr *= 1.5 |
| 141 | + old_states.append(problem.state.copy()) |
| 142 | + old_tractions.append(traction.value.copy()) |
| 143 | + |
| 144 | +log.set_log_level(log.LogLevel.INFO) |
| 145 | + |
| 146 | +# Now save the displacement to a file that we can view in Paraview |
| 147 | + |
| 148 | +u = problem.state.sub(0).collapse() |
| 149 | +with dolfinx.io.VTXWriter(geometry.mesh.comm, "problem2.bp", [u], engine="BP4") as vtx: |
| 150 | + vtx.write(0.0) |
| 151 | + |
| 152 | + |
| 153 | +try: |
| 154 | + import pyvista |
| 155 | +except ImportError: |
| 156 | + print("Pyvista is not installed") |
| 157 | +else: |
| 158 | + pyvista.start_xvfb() |
| 159 | + V = dolfinx.fem.functionspace(geometry.mesh, ("Lagrange", 1, (geometry.mesh.geometry.dim,))) |
| 160 | + uh = dolfinx.fem.Function(V) |
| 161 | + uh.interpolate(u) |
| 162 | + # Create plotter and pyvista grid |
| 163 | + p = pyvista.Plotter() |
| 164 | + topology, cell_types, geometry = dolfinx.plot.vtk_mesh(V) |
| 165 | + grid = pyvista.UnstructuredGrid(topology, cell_types, geometry) |
| 166 | + |
| 167 | + # Attach vector values to grid and warp grid by vector |
| 168 | + grid["u"] = uh.x.array.reshape((geometry.shape[0], 3)) |
| 169 | + actor_0 = p.add_mesh(grid, style="wireframe", color="k") |
| 170 | + warped = grid.warp_by_vector("u", factor=1.5) |
| 171 | + actor_1 = p.add_mesh(warped, show_edges=True) |
| 172 | + p.show_axes() |
| 173 | + if not pyvista.OFF_SCREEN: |
| 174 | + p.show() |
| 175 | + else: |
| 176 | + figure_as_array = p.screenshot("problem2.png") |
| 177 | + |
| 178 | + |
| 179 | +# FIXME: Need to figure out how to evaluate the displacement at the apex |
| 180 | +# geometry.mesh.topology.create_connectivity(0, geometry.mesh.topology.dim) |
| 181 | +# apex_endo = geo.vfun.find(geo.markers["ENDOPT"][0]) |
| 182 | +# endo_apex_coord = geo.mesh.geometry.x[apex_endo] |
| 183 | + |
| 184 | +# dofs_endo_apex = dolfinx.fem.locate_dofs_topological(problem.state_space.sub(0), 0, apex_endo) |
| 185 | +# u_endo_apex = u.x.array[dofs_endo_apex] |
| 186 | + |
| 187 | +# endo_apex_pos = endo_apex_coord + u_endo_apex |
| 188 | + |
| 189 | +# print(f"\nGet longitudinal position of endocardial apex: {endo_apex_pos[0, 0]:4f} mm") |
| 190 | + |
| 191 | +# apex_epi = geo.vfun.find(geo.markers["EPIPT"][0]) |
| 192 | +# epi_apex_coord = geo.mesh.geometry.x[apex_epi] |
| 193 | + |
| 194 | +# geometry.mesh.topology.create_connectivity(0, geometry.mesh.topology.dim) |
| 195 | +# dofs_epi_apex = dolfinx.fem.locate_dofs_topological(problem.state_space.sub(0), 0, apex_epi) |
| 196 | +# u_epi_apex = u.x.array[dofs_epi_apex] |
| 197 | + |
| 198 | +# epi_apex_pos = epi_apex_coord + u_epi_apex |
| 199 | + |
| 200 | +# print(f"\nGet longitudinal position of epicardial apex: {epi_apex_pos[0, 0]:.4f} mm") |
0 commit comments