Source code for cardiac_geometries.geometry

import json
import logging
import shutil
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from mpi4py import MPI

import adios2
import adios4dolfinx
import dolfinx
import numpy as np
import ufl
from packaging.version import Version

from . import utils

logger = logging.getLogger(__name__)


[docs] @dataclass class Geometry: mesh: dolfinx.mesh.Mesh markers: dict[str, tuple[int, int]] = field(default_factory=dict) cfun: dolfinx.mesh.MeshTags | None = None ffun: dolfinx.mesh.MeshTags | None = None efun: dolfinx.mesh.MeshTags | None = None vfun: dolfinx.mesh.MeshTags | None = None f0: dolfinx.fem.Function | None = None s0: dolfinx.fem.Function | None = None n0: dolfinx.fem.Function | None = None info: dict[str, Any] = field(default_factory=dict)
[docs] def save(self, path: str | Path) -> None: """Save the geometry to a file using adios4dolfinx. Parameters ---------- path : str | Path The path to the file where the geometry will be saved. The file will be created if it does not exist, or overwritten if it does. """ path = Path(path) shutil.rmtree(path, ignore_errors=True) self.mesh.comm.barrier() adios4dolfinx.write_mesh(mesh=self.mesh, filename=path) if Version(np.__version__) <= Version("2.11") or Version(adios2.__version__) >= Version( "2.10.2" ): # This is broken in adios < 2.10.2 and numpy >= 2.11 adios4dolfinx.write_attributes( comm=self.mesh.comm, filename=path, name="markers", attributes={k: np.array(v, dtype=np.uint8) for k, v in self.markers.items()}, ) if self.cfun is not None: adios4dolfinx.write_meshtags( meshtags=self.cfun, mesh=self.mesh, filename=path, meshtag_name="Cell tags", ) if self.ffun is not None: adios4dolfinx.write_meshtags( meshtags=self.ffun, mesh=self.mesh, filename=path, meshtag_name="Facet tags", ) # if self.efun is not None: # adios4dolfinx.write_meshtags( # meshtags=self.efun, # mesh=self.mesh, # filename=path, # meshtag_name="Edge tags", # ) # if self.vfun is not None: # adios4dolfinx.write_meshtags( # meshtags=self.vfun, # mesh=self.mesh, # filename=path, # meshtag_name="Vertex tags", # ) if self.f0 is not None: el = self.f0.ufl_element() arr = utils.element2array(el) if Version(np.__version__) <= Version("2.11") or Version(adios2.__version__) >= Version( "2.10.2" ): # This is broken in adios for numpy >= 2.11 adios4dolfinx.write_attributes( comm=self.mesh.comm, filename=path, name="function_space", attributes={k: arr for k in ("f0", "s0", "n0")}, ) adios4dolfinx.write_function(u=self.f0, filename=path, name="f0") if self.s0 is not None: adios4dolfinx.write_function(u=self.s0, filename=path, name="s0") if self.n0 is not None: adios4dolfinx.write_function(u=self.n0, filename=path, name="n0") self.mesh.comm.barrier()
@property def dx(self): """Volume measure for the mesh using the cell function `cfun` if it exists as subdomain data. """ return ufl.Measure("dx", domain=self.mesh, subdomain_data=self.cfun) @property def ds(self): """Surface measure for the mesh using the facet function `ffun` if it exists as subdomain data. """ return ufl.Measure("ds", domain=self.mesh, subdomain_data=self.ffun) @property def facet_normal(self) -> ufl.FacetNormal: """Facet normal vector for the mesh.""" return ufl.FacetNormal(self.mesh)
[docs] def refine(self, n=1, outdir: Path | None = None) -> "Geometry": """ Refine the mesh and transfer the meshtags to new geometry. Also regenerate fibers if `self.info` is found. If `self.info` is not found, it currently raises a NotImplementedError, however fiber could be interpolated from the old mesh to the new mesh but this will result in a loss of information about the fiber orientation. Parameters ---------- n : int, optional Number of times to refine the mesh, by default 1 outdir : Path | None, optional Output directory to save the refined mesh and meshtags, by default None in which case the mesh is not saved. Returns ------- Geometry A new Geometry object with the refined mesh and updated meshtags. Raises ------ NotImplementedError If `self.info` is not found, indicating that fiber interpolation after refinement is not implemented yet. """ mesh = self.mesh cfun = self.cfun ffun = self.ffun for _ in range(n): new_mesh, parent_cell, parent_facet = dolfinx.mesh.refine( mesh, partitioner=None, option=dolfinx.mesh.RefinementOption.parent_cell_and_facet, ) new_mesh.name = mesh.name mesh = new_mesh new_mesh.topology.create_entities(1) new_mesh.topology.create_connectivity(2, 3) if cfun is not None: new_cfun = dolfinx.mesh.transfer_meshtag(cfun, new_mesh, parent_cell, parent_facet) new_cfun.name = cfun.name cfun = new_cfun else: new_cfun = None if ffun is not None: new_ffun = dolfinx.mesh.transfer_meshtag(ffun, new_mesh, parent_cell, parent_facet) new_ffun.name = ffun.name ffun = new_ffun else: new_ffun = None if outdir is not None: outdir = Path(outdir) outdir.mkdir(parents=True, exist_ok=True) with dolfinx.io.XDMFFile(new_mesh.comm, outdir / "mesh.xdmf", "w") as xdmf: xdmf.write_mesh(new_mesh) xdmf.write_meshtags( cfun, new_mesh.geometry, geometry_xpath=f"/Xdmf/Domain/Grid[@Name='{new_mesh.name}']/Geometry", ) mesh.topology.create_connectivity(2, 3) xdmf.write_meshtags( ffun, new_mesh.geometry, geometry_xpath=f"/Xdmf/Domain/Grid[@Name='{new_mesh.name}']/Geometry", ) if self.info is not None: info = self.info.copy() info["refinement"] = n info.pop("outdir", None) if outdir is not None: info["outdir"] = str(outdir) from .fibers import generate_fibers f0, s0, n0 = generate_fibers(mesh=new_mesh, ffun=new_ffun, markers=self.markers, **info) else: info = None # Interpolate fibers raise NotImplementedError( "Interpolating fibers after refinement is not implemented yet." ) return Geometry( mesh=new_mesh, markers=self.markers, cfun=new_cfun, ffun=new_ffun, f0=f0, s0=s0, n0=n0, )
[docs] @classmethod def from_file( cls, comm: MPI.Intracomm, path: str | Path, function_space_data: dict[str, np.ndarray] | None = None, ) -> "Geometry": """Read geometry from a file using adios4dolfinx. Parameters ---------- comm : MPI.Intracomm The MPI communicator to use for reading the mesh. path : str | Path The path to the file containing the geometry data. function_space_data : dict[str, np.ndarray] | None, optional A dictionary containing function space data for the functions to be read. If None, it will be read from the file. Returns ------- Geometry An instance of the Geometry class containing the mesh, markers, and functions. """ path = Path(path) mesh = adios4dolfinx.read_mesh(comm=comm, filename=path) markers = adios4dolfinx.read_attributes(comm=comm, filename=path, name="markers") tags = {} for name, meshtag_name in ( ("cfun", "Cell tags"), ("ffun", "Facet tags"), ("efun", "Edge tags"), ("vfun", "Vertex tags"), ): try: tags[name] = adios4dolfinx.read_meshtags( mesh=mesh, meshtag_name=meshtag_name, filename=path ) except KeyError: tags[name] = None functions = {} if function_space_data is None: function_space_data = adios4dolfinx.read_attributes( comm=comm, filename=path, name="function_space" ) assert isinstance(function_space_data, dict), "function_space_data must be a dictionary" for name, el in function_space_data.items(): element = utils.array2element(el) V = dolfinx.fem.functionspace(mesh, element) f = dolfinx.fem.Function(V, name=name) try: adios4dolfinx.read_function(u=f, filename=path, name=name) except KeyError: continue else: functions[name] = f return cls( mesh=mesh, markers=markers, **functions, **tags, )
[docs] @classmethod def from_folder(cls, comm: MPI.Intracomm, folder: str | Path) -> "Geometry": """Read geometry from a folder containing mesh and markers files. Parameters ---------- comm : MPI.Intracomm The MPI communicator to use for reading the mesh and markers. folder : str | Path The path to the folder containing the geometry data. The folder should contain the following files: - mesh.xdmf: The mesh file in XDMF format. - markers.json: A JSON file containing markers. - microstructure.json: A JSON file containing microstructure data (optional). - microstructure.bp: A BP file containing microstructure functions (optional). - info.json: A JSON file containing additional information (optional). Returns ------- Geometry An instance of the Geometry class containing the mesh, markers, and functions. Raises ------ ValueError If the required mesh file is not found in the specified folder. """ folder = Path(folder) logger.info(f"Reading geometry from {folder}") # Read mesh if (folder / "mesh.xdmf").exists(): logger.debug("Reading mesh") mesh, tags = utils.read_mesh(comm=comm, filename=folder / "mesh.xdmf") else: raise ValueError("No mesh file found") # Read markers if (folder / "markers.json").exists(): logger.debug("Reading markers") if comm.rank == 0: markers = json.loads((folder / "markers.json").read_text()) else: markers = {} markers = comm.bcast(markers, root=0) else: markers = {} if (folder / "microstructure.json").exists(): if comm.rank == 0: microstructure = json.loads((folder / "microstructure.json").read_text()) else: microstructure = {} microstructure = comm.bcast(microstructure, root=0) else: microstructure = {} functions = {} microstructure_path = folder / "microstructure.bp" if microstructure_path.exists(): logger.debug("Reading microstructure") # function_space = adios4dolfinx.read_attributes( # comm=MPI.COMM_WORLD, filename=microstructure_path, name="function_space" # ) for name, el in microstructure.items(): logger.debug(f"Reading {name}") element = utils.array2element(el) V = dolfinx.fem.functionspace(mesh, element) f = dolfinx.fem.Function(V, name=name) try: adios4dolfinx.read_function(u=f, filename=microstructure_path, name=name) except KeyError: continue else: functions[name] = f if (folder / "info.json").exists(): logger.debug("Reading info") if comm.rank == 0: info = json.loads((folder / "info.json").read_text()) else: info = {} info = comm.bcast(info, root=0) else: info = {} return cls( mesh=mesh, markers=markers, info=info, **functions, **tags, )