Source code for simcardems.geometry

import abc
import json
from pathlib import Path
from typing import Any
from typing import Callable
from typing import Dict
from typing import NamedTuple
from typing import Optional
from typing import Tuple
from typing import Union

import dolfin
import pulse
from cardiac_geometries.geometry import Geometry
from cardiac_geometries.geometry import H5Path
from cardiac_geometries.geometry import MeshTypes
from mpi4py import MPI

from . import utils

logger = utils.getLogger(__name__)


[docs] class StimulusDomain(NamedTuple): domain: dolfin.MeshFunction marker: int
[docs] def load_geometry( mesh_path: utils.PathLike, schema_path: Optional[utils.PathLike] = None, stimulus_domain: Optional[Union[StimulusDomain, Callable[[dolfin.Mesh], StimulusDomain]]] = None, ) -> "BaseGeometry": from .slabgeometry import SlabGeometry from .lvgeometry import LeftVentricularGeometry if mesh_path == "": # Use default slab geometry return SlabGeometry() if schema_path is None: schema_path = Path(mesh_path).with_suffix(".json") geo = Geometry.from_file( fname=mesh_path, schema_path=schema_path, schema=BaseGeometry.default_schema(), ) info = getattr(geo, "info", None) if info is None: raise RuntimeError("Unable to load info from geometry") mesh_type = info.get("mesh_type") if mesh_type is None: raise RuntimeError("Unable to get mesh type from info") if mesh_type == MeshTypes.slab.value: return SlabGeometry.from_geometry(geo, stimulus_domain=stimulus_domain) elif mesh_type == MeshTypes.lv_ellipsoid.value: return LeftVentricularGeometry.from_geometry( geo, stimulus_domain=stimulus_domain, ) raise RuntimeError(f"Unknown mesh type {mesh_type!r}")
[docs] def refine_mesh( mesh: dolfin.Mesh, num_refinements: int, redistribute: bool = False, ) -> dolfin.Mesh: dolfin.parameters["refinement_algorithm"] = "plaza_with_parent_facets" for i in range(num_refinements): logger.info(f"Performing refinement {i+1}") mesh = dolfin.refine(mesh, redistribute=redistribute) return mesh
[docs] class BaseGeometry(abc.ABC): """Abstract geometry base class""" def __init__( self, parameters: Optional[Dict[str, Any]] = None, stimulus_domain: Optional[Union[StimulusDomain, Callable[[dolfin.Mesh], StimulusDomain]]] = None, mechanics_mesh: Optional[dolfin.Mesh] = None, ep_mesh: Optional[dolfin.Mesh] = None, microstructure: Optional[pulse.Microstructure] = None, microstructure_ep: Optional[pulse.Microstructure] = None, ffun: Optional[dolfin.MeshFunction] = None, ffun_ep: Optional[dolfin.MeshFunction] = None, markers: Optional[Dict[str, Tuple[int, int]]] = None, outdir: Optional[utils.PathLike] = None, ) -> None: self.markers = type(self).default_markers() if markers is not None: self.markers.update(markers) self.outdir = outdir # type: ignore self.parameters = type(self).default_parameters() if parameters is not None: self.parameters.update(parameters) self.mechanics_mesh = mechanics_mesh self.ffun = ffun self.ep_mesh = ep_mesh self.stimulus_domain = self._handle_stimulus_domain(stimulus_domain) self.ffun_ep = ffun_ep self.microstructure = microstructure self.microstructure_ep = microstructure_ep self.validate() def __eq__(self, __o: object) -> bool: if not isinstance(__o, type(self)): return NotImplemented # TODO: We might add more checks here return self.parameters == __o.parameters def _handle_stimulus_domain( self, stimulus_domain: Optional[Union[StimulusDomain, Callable[[dolfin.Mesh], StimulusDomain]]], ) -> StimulusDomain: if stimulus_domain is None: return type(self).default_stimulus_domain(self.ep_mesh) if isinstance(stimulus_domain, StimulusDomain): return stimulus_domain assert callable(stimulus_domain) return stimulus_domain(self.ep_mesh)
[docs] def comm(self) -> MPI.Comm: return self.mesh.mpi_comm()
[docs] def validate(self): pass
[docs] @staticmethod @abc.abstractmethod def default_markers() -> Dict[str, Tuple[int, int]]: ...
[docs] @staticmethod @abc.abstractmethod def default_parameters() -> Dict[str, Any]: ...
[docs] @staticmethod def default_stimulus_domain(mesh: dolfin.Mesh) -> StimulusDomain: # Default is to stimulate the entire tissue marker = 1 domain = dolfin.MeshFunction("size_t", mesh, mesh.topology().dim()) domain.set_all(marker) return StimulusDomain(domain=domain, marker=marker)
[docs] @staticmethod def default_schema() -> Dict[str, H5Path]: return { "mesh": H5Path( h5group="/geometry/mesh/mechanics", is_mesh=True, ), "ep_mesh": H5Path( h5group="/geometry/mesh/ep", is_mesh=True, ), "ffun": H5Path( h5group="/geometry/meshfunctions/ffun", is_meshfunction=True, dim=2, mesh_key="mesh", ), "ffun_ep": H5Path( h5group="/geometry/meshfunctions/ffun_ep", is_meshfunction=True, dim=2, mesh_key="ep_mesh", ), "f0": H5Path( h5group="/geometry/microstructure/mechanics/f0", is_function=True, mesh_key="mesh", ), "s0": H5Path( h5group="/geometry/microstructure/mechanics/s0", is_function=True, mesh_key="mesh", ), "n0": H5Path( h5group="/geometry/microstructure/mechanics/n0", is_function=True, mesh_key="mesh", ), "f0_ep": H5Path( h5group="/geometry/microstructure/ep/f0", is_function=True, mesh_key="ep_mesh", ), "s0_ep": H5Path( h5group="/geometry/microstructure/ep/s0", is_function=True, mesh_key="ep_mesh", ), "n0_ep": H5Path( h5group="/geometry/microstructure/ep/n0", is_function=True, mesh_key="ep_mesh", ), "info": H5Path( h5group="/geometry/info", is_dolfin=False, ), "markers": H5Path( h5group="/geometry/markers", is_dolfin=False, ), }
@abc.abstractmethod def _default_microstructure( self, mesh: dolfin.Mesh, ffun: dolfin.MeshFunction, ) -> pulse.Microstructure: ... @abc.abstractmethod def _default_ffun(self, mesh: dolfin.Mesh) -> dolfin.MeshFunction: ... @abc.abstractmethod def _default_mesh(self) -> dolfin.Mesh: ... @property def facet_normal(self) -> dolfin.FacetNormal: return dolfin.FacetNormal(self.mesh) @property def mesh(self) -> dolfin.Mesh: # FIXME: This should be optional return self.mechanics_mesh @property def dx(self): """Return the volume measure using self.mesh""" return dolfin.dx(domain=self.mesh) @property def ds(self): """Return the surface measure of exterior facets using self.mesh as domain and self.ffun as subdomain_data """ return dolfin.ds(domain=self.mesh, subdomain_data=self.ffun) @property def num_refinements(self) -> int: return self.parameters["num_refinements"]
[docs] def dump( self, fname: utils.PathLike, schema_path: Optional[utils.PathLike] = None, unlink: bool = True, ): path = Path(fname) schema = type(self).default_schema() kwargs = {k: getattr(self, k) for k in schema if k != "info"} kwargs["info"] = self.parameters logger.debug("Instantiating geometry") geo = Geometry(**kwargs, schema=schema) if schema_path is None: schema_path = path.with_suffix(".json") logger.debug("Save geo") geo.save(path, schema_path=schema_path, unlink=unlink) logger.info(f"Saved geometry to {fname}")
def _get_microstructure_if_None( self, mesh: dolfin.Mesh, ffun: dolfin.MeshFunction, label: str, ) -> pulse.Microstructure: microstructure = self._default_microstructure(mesh=mesh, ffun=ffun) if self.outdir is not None: path = self.outdir / f"microstructure{label}.h5" with dolfin.HDF5File( mesh.mpi_comm(), path.as_posix(), "w", ) as h5file: h5file.write(microstructure.f0, "f0") h5file.write(microstructure.s0, "s0") h5file.write(microstructure.n0, "n0") return microstructure @property def microstructure_ep(self) -> pulse.Microstructure: return self._microstructure_ep # type: ignore @microstructure_ep.setter def microstructure_ep(self, microstructure: Optional[pulse.Microstructure]) -> None: if microstructure is None or microstructure.f0 is None: microstructure = self._interpolate_microstructure() self._microstructure_ep = microstructure def _interpolate_microstructure(self) -> pulse.Microstructure: element = self.f0.ufl_element() if element.family() == "Quadrature": return self._default_microstructure(mesh=self.ep_mesh, ffun=self.ffun_ep) else: V = dolfin.FunctionSpace(self.ep_mesh, element) try: f0 = dolfin.interpolate(self.f0, V) except RuntimeError: logger.info( "Extrapolate fibers in order to interpolate from mechanics to ep mesh", ) self.f0.set_allow_extrapolation(True) self.s0.set_allow_extrapolation(True) self.n0.set_allow_extrapolation(True) f0 = dolfin.interpolate(self.f0, V) s0 = dolfin.interpolate(self.s0, V) n0 = dolfin.interpolate(self.n0, V) return pulse.Microstructure(f0=f0, s0=s0, n0=n0) @property def microstructure(self) -> pulse.Microstructure: return self._microstructure # type: ignore @microstructure.setter def microstructure(self, microstructure: Optional[pulse.Microstructure]) -> None: if microstructure is None: microstructure = self._get_microstructure_if_None( mesh=self.mechanics_mesh, ffun=self.ffun, label="", ) self._microstructure = microstructure @property def f0(self) -> dolfin.Function: return self.microstructure.f0 @property def s0(self) -> dolfin.Function: return self.microstructure.s0 @property def n0(self) -> dolfin.Function: return self.microstructure.n0 @property def f0_ep(self) -> dolfin.Function: return self.microstructure_ep.f0 @property def s0_ep(self) -> dolfin.Function: return self.microstructure_ep.s0 @property def n0_ep(self) -> dolfin.Function: return self.microstructure_ep.n0 @property def mechanics_mesh(self) -> dolfin.Mesh: return self._mechanics_mesh # type: ignore @mechanics_mesh.setter def mechanics_mesh(self, mesh: Optional[dolfin.Mesh]) -> None: if mesh is None: mesh = self._default_mesh() if self.outdir is not None: mesh_path = self.outdir / "mesh.xdmf" with dolfin.XDMFFile(mesh_path.as_posix()) as f: f.write(mesh) self._mechanics_mesh = mesh @property def ffun(self) -> dolfin.MeshFunction: return self._ffun # type: ignore @ffun.setter def ffun(self, ffun: Optional[dolfin.MeshFunction]) -> None: if ffun is None: self._ffun = self._default_ffun( self.mechanics_mesh, ) if self.outdir is not None: ffun_path = self.outdir / "ffun.xdmf" with dolfin.XDMFFile(ffun_path.as_posix()) as f: f.write(self.ffun) else: self._ffun = ffun @property def ep_mesh(self) -> dolfin.Mesh: return self._ep_mesh # type: ignore @ep_mesh.setter def ep_mesh(self, mesh: Optional[dolfin.Mesh]) -> None: if mesh is None: self._ep_mesh = refine_mesh( mesh=self.mechanics_mesh, num_refinements=self.parameters["num_refinements"], ) if self.outdir is not None: mesh_path = self.outdir / "ep_mesh.xdmf" with dolfin.XDMFFile(mesh_path.as_posix()) as f: f.write(mesh) else: self._ep_mesh = mesh @property def ffun_ep(self) -> dolfin.MeshFunction: return self._ffun_ep # type: ignore @ffun_ep.setter def ffun_ep(self, ffun: Optional[dolfin.MeshFunction]) -> None: if ffun is None: self._ffun_ep = dolfin.adapt(self.ffun, self.ep_mesh) else: self._ffun_ep = ffun @property def outdir(self) -> Optional[Path]: return self._outdir @outdir.setter def outdir(self, folder: Optional[utils.PathLike]): if folder is None: self._outdir = None else: self._outdir = Path(folder) @property def marker_functions(self): return pulse.MarkerFunctions(ffun=self.ffun)
[docs] @classmethod def from_files( cls, mesh_path: utils.PathLike, ffun_path: Optional[utils.PathLike] = None, marker_path: Optional[utils.PathLike] = None, parameter_path: Optional[utils.PathLike] = None, microstructure_path: Optional[utils.PathLike] = None, **kwargs, ): markers = cls.default_markers() if marker_path is not None: markers.update(json.loads(Path(marker_path).read_text())) parameters = cls.default_parameters() if parameter_path is not None: parameters.update(json.loads(Path(parameter_path).read_text())) mesh = dolfin.Mesh() with dolfin.XDMFFile(Path(mesh_path).as_posix()) as f: f.read(mesh) if ffun_path is not None: ffun = dolfin.MeshFunction("size_t", mesh, 2) with dolfin.XDMFFile(Path(ffun_path).as_posix()) as f: f.read(ffun) else: ffun = None fiber_space = parameters.get("fiber_space") if fiber_space is not None and microstructure_path is not None: family, degree = fiber_space.split("_") logger.debug("Set up microstructure") V_f = dolfin.VectorFunctionSpace(mesh, family, int(degree)) f0 = dolfin.Function(V_f) s0 = dolfin.Function(V_f) n0 = dolfin.Function(V_f) with dolfin.HDF5File( mesh.mpi_comm(), Path(microstructure_path).as_posix(), "r", ) as h5file: h5file.read(f0, "f0") h5file.read(s0, "s0") h5file.read(n0, "n0") microstructure = pulse.Microstructure(f0=f0, s0=s0, n0=n0) else: microstructure = None return cls( mechanics_mesh=mesh, ffun=ffun, markers=markers, parameters=parameters, microstructure=microstructure, **kwargs, )
[docs] @classmethod def from_geometry(cls, geo: Geometry, **kwargs): for attr_geo, attr_simcardems in [ ("info", "parameters"), ("mesh", "mechanics_mesh"), ("ep_mesh", "ep_mesh"), ("markers", "markers"), ("ffun", "ffun"), ("ffun_ep", "ffun_ep"), ]: attr = getattr(geo, attr_geo, None) if attr is not None: kwargs[attr_simcardems] = attr micro_kwargs = {} for f in ["f0", "s0", "n0"]: attr = getattr(geo, f, None) if attr is not None: micro_kwargs[f] = attr if micro_kwargs: kwargs["microstructure"] = pulse.Microstructure(**micro_kwargs) micro_ep_kwargs = {} for f in ["f0_ep", "s0_ep", "n0_ep"]: attr = getattr(geo, f, None) if attr is not None: micro_ep_kwargs[f.rstrip("_ep")] = attr if micro_kwargs: kwargs["microstructure_ep"] = pulse.Microstructure(**micro_ep_kwargs) return cls(**kwargs)