Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f1203f2
refactor: add a material factory module
mhoeijm Sep 23, 2025
ee11e27
chore: adding changelog file 1246.miscellaneous.md [dependabot-skip]
pyansys-ci-bot Sep 23, 2025
cdcdefb
refactor: remove methods from settings
mhoeijm Sep 23, 2025
d567f5d
Merge branch 'refactor/refactor-default-material-management' of https…
mhoeijm Sep 23, 2025
a9f335a
refactor: remove test
mhoeijm Sep 23, 2025
2082df2
feat: add validator method for materials
mhoeijm Sep 23, 2025
422ea17
Merge branch 'main' into refactor/refactor-default-material-management
mhoeijm Sep 23, 2025
84f0e95
Merge branch 'main' into refactor/refactor-default-material-management
mhoeijm Sep 29, 2025
dee91f3
Merge branch 'refactor/refactor-default-material-management' of https…
mhoeijm Sep 30, 2025
1cb0270
feat: add method for getting default materials
mhoeijm Sep 30, 2025
f1a2293
feat: add ep material factory for default materials
mhoeijm Oct 2, 2025
52c31fa
feat: add tests for material factory
mhoeijm Oct 2, 2025
5683306
refactor: ep solver type to ep_material
mhoeijm Oct 2, 2025
1b19563
refactor: add passive material factory method
mhoeijm Oct 3, 2025
1337880
refactor: update defaults
mhoeijm Oct 3, 2025
33c1731
refactor: use factory methods
mhoeijm Oct 3, 2025
84d1391
refactor: use factory methods
mhoeijm Oct 3, 2025
a4a9dc6
refactor: use factory methods
mhoeijm Oct 3, 2025
410a7b9
refactor: fix tests
mhoeijm Oct 3, 2025
51c58d4
Merge branch 'main' into refactor/refactor-default-material-management
mhoeijm Oct 3, 2025
4ff96a5
refactor: default material for fiber generation
mhoeijm Oct 3, 2025
62a12f5
refactor: naming of reaction eikonal
mhoeijm Oct 3, 2025
2be4fd8
refactor: remove solvertype from material model
mhoeijm Oct 8, 2025
9d73aa1
refactor: remove solvertype from material model
mhoeijm Oct 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/changelog/1246.miscellaneous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a material factory module
6 changes: 4 additions & 2 deletions examples/simulator/ep-mechanics-simulator-fullheart.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@

from ansys.health.heart.examples import get_preprocessed_fullheart
import ansys.health.heart.models as models
import ansys.health.heart.settings.material.ep_material as ep_materials
import ansys.health.heart.settings.material.ep_material_factory as ep_material_factory
from ansys.health.heart.settings.material.material import ISO, Mat295
from ansys.health.heart.simulator import DynaSettings, EPMechanicsSimulator

Expand Down Expand Up @@ -133,7 +133,9 @@
ring.meca_material = stiff_iso

# Assign the default EP material
ring.ep_material = ep_materials.Active()
ring.ep_material = ep_material_factory.get_default_myocardium_material(
simulator.settings.electrophysiology.analysis.solvertype
)

# plot the mesh
simulator.model.plot_mesh()
Expand Down
9 changes: 9 additions & 0 deletions src/ansys/health/heart/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

"""Custom exceptions for PyAnsys Heart."""

from typing import Literal


class LSDYNATerminationError(BaseException):
"""Exception raised when ``Normal Termination`` is not found in the LS-DYNA logs."""
Expand Down Expand Up @@ -76,3 +78,10 @@ class WSLNotFoundError(FileNotFoundError):

class MissingEnvironmentVariableError(EnvironmentError):
"""Exception raised when a required environment variable is missing."""


class MissingMaterialError(ValueError):
"""Exception raised when a required material is missing in the model."""

def __init__(self, part_name: str, material_type: Literal["EP", "Mechanical"]):
super().__init__(f"Part {part_name} has no {material_type} material assigned.")
38 changes: 34 additions & 4 deletions src/ansys/health/heart/settings/defaults/electrophysiology.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,54 @@
"sigma_fiber": Quantity(0.5, "mS/mm"), # mS/mm
"sigma_sheet": Quantity(0.1, "mS/mm"), # mS/mm
"sigma_sheet_normal": Quantity(0.1, "mS/mm"), # mS/mm
"sigma_passive": Quantity(1.0, "mS/mm"), # mS/mm
"sigma_passive": Quantity(1.0, "mS/mm"), # mS/mm: use for passive conduction (e.g. blood)
"beta": Quantity(140, "1/mm"),
"cm": Quantity(0.01, "uF/mm^2"), # uF/mm^2
"lambda": Quantity(0.2, "dimensionless"),
"percent_endo": Quantity(0.17, "dimensionless"),
"percent_mid": Quantity(0.41, "dimensionless"),
# These are not really material properties?
"lambda": Quantity(0.2, "dimensionless"), # activate extracellular potential solve
"percent_endo": Quantity(0.17, "dimensionless"), # thickness of endocardial layer
"percent_mid": Quantity(0.41, "dimensionless"), # thickness of midmyocardial layer
},
"beam": {
"velocity": Quantity(1, "mm/ms"), # mm/ms in case of eikonal model
"sigma": Quantity(1, "mS/mm"), # mS/mm
"beta": Quantity(140, "1/mm"),
"cm": Quantity(0.001, "uF/mm^2"), # uF/mm^2
# These are not really material properties?
"lambda": Quantity(0.2, "dimensionless"),
"pmjrestype": Quantity(1),
"pmjres": Quantity(0.001, "1/mS"), # 1/mS
},
}

"""Material settings."""
default_myocardium_material_eikonal = {
"sigma_fiber": Quantity(0.7, "mm/ms"), # mm/ms in case of eikonal model
"sigma_sheet": Quantity(0.2, "mm/ms"), # mm/ms in case of eikonal model
"sigma_sheet_normal": Quantity(0.2, "mm/ms"), # mm/ms in case of eikonal model
"beta": Quantity(140, "1/mm"),
"cm": Quantity(0.01, "uF/mm^2"), # uF/mm^2
"lambda": Quantity(0.2, "dimensionless"),
"percent_endo": Quantity(0.17, "dimensionless"),
"percent_mid": Quantity(0.41, "dimensionless"),
}
default_beam_material_eikonal = {
"sigma": Quantity(1, "mm/ms"), # mm/ms in case of eikonal model
"beta": Quantity(140, "1/mm"),
"cm": Quantity(0.001, "uF/mm^2"), # uF/mm^2
"lambda": Quantity(0.2, "dimensionless"),
"pmjrestype": Quantity(1),
"pmjres": Quantity(0.001, "1/mS"), # 1/mS
}

# Create monodomain defaults by copying eikonal and changing relevant fields
default_beam_material_monodomain = default_beam_material_eikonal.copy()
default_beam_material_monodomain["sigma"] = Quantity(1, "mS/mm") # mS/mm
default_myocardium_material_monodomain = default_myocardium_material_eikonal.copy()
default_myocardium_material_monodomain["sigma_fiber"] = Quantity(0.5, "mS/mm") # mS/mm
default_myocardium_material_monodomain["sigma_sheet"] = Quantity(0.1, "mS/mm") # mS/mm
default_myocardium_material_monodomain["sigma_sheet_normal"] = Quantity(0.1, "mS/mm") # mS/mm

"""Stimulation settings."""
stimulation = {
"stimdefaults": {
Expand Down
59 changes: 21 additions & 38 deletions src/ansys/health/heart/settings/material/ep_material.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,30 @@

"""EP material module."""

from typing import Literal, Optional
from enum import Enum
from typing import Optional

from pydantic import BaseModel, Field, model_validator

from ansys.health.heart.settings.defaults import electrophysiology as ep_defaults
from ansys.health.heart.settings.material.cell_models import Tentusscher


class EPMaterialModel(BaseModel):
class EPSolverType(Enum):
"""Enumeration of EP solver types."""

MONODOMAIN = "Monodomain"
EIKONAL = "Eikonal"
REACTION_EIKONAL = "ReactionEikonal"


class EPMaterialModel(BaseModel): # EM MAT 003
"""Base class for all EP material models."""

sigma_fiber: Optional[float] = None
sigma_sheet: Optional[float] = None
sigma_sheet_normal: Optional[float] = None
beta: Optional[float] = ep_defaults.material["myocardium"]["beta"].m
cm: Optional[float] = ep_defaults.material["myocardium"]["cm"].m
beta: Optional[float] = None
cm: Optional[float] = None
lambda_: Optional[float] = None

@model_validator(mode="after")
Expand All @@ -51,63 +59,38 @@ def check_inputs(self):
return self


class Insulator(BaseModel):
class Insulator(BaseModel): # EM MAT 001
"""Insulator material."""

sigma_fiber: float = 0.0
cm: float = 0.0
beta: float = 0.0


# solver type should be managed from global settings and not in material settings.
class Active(EPMaterialModel):
"""Hold data for EP material."""

solver_type: Literal["Monodomain", "Eikonal", "Reaction-Eikonal"] = ep_defaults.analysis[
"solvertype"
]

sigma_fiber: Optional[float] = None
sigma_sheet: Optional[float] = None
sigma_sheet_normal: Optional[float] = None

cell_model: Tentusscher = Field(default_factory=lambda: Tentusscher())

# NOTE: complicated logic and conditional default values. Should split into different classes
@model_validator(mode="after")
def check_sigmas(self):
"""Conditional validation of sigmas."""
if self.solver_type == "Monodomain":
if self.sigma_fiber is None:
self.sigma_fiber = ep_defaults.material["myocardium"]["sigma_fiber"].m
if self.sigma_sheet is None:
self.sigma_sheet = ep_defaults.material["myocardium"]["sigma_sheet"].m
if self.sigma_sheet_normal is None:
self.sigma_sheet_normal = ep_defaults.material["myocardium"]["sigma_sheet_normal"].m
elif self.solver_type == "Eikonal" or self.solver_type == "ReactionEikonal":
if self.sigma_fiber is None:
self.sigma_fiber = ep_defaults.material["myocardium"]["velocity_fiber"].m
if self.sigma_sheet is None:
self.sigma_sheet = ep_defaults.material["myocardium"]["velocity_sheet"].m
if self.sigma_sheet_normal is None:
self.sigma_sheet_normal = ep_defaults.material["myocardium"][
"velocity_sheet_normal"
].m

return self


class ActiveBeam(Active):
"""Hold data for beam active EP material."""

sigma_fiber: float = ep_defaults.material["beam"]["sigma"].m
sigma_fiber: float = 1.0
# TODO: replace by TentusscherEndo
cell_model: Tentusscher = Tentusscher()
pmjres: float = ep_defaults.material["beam"]["pmjres"].m
pmjres: float = 0.001


# A Passive material model is actually the same as the EPMaterialModel
class Passive(EPMaterialModel):
"""Hold data for EP passive material."""

sigma_fiber: float = ep_defaults.material["myocardium"]["sigma_fiber"].m
sigma_sheet: Optional[float] = None
sigma_sheet_normal: Optional[float] = None
# sigma_fiber: Optional[float] = None
# sigma_sheet: Optional[float] = None
# sigma_sheet_normal: Optional[float] = None
140 changes: 140 additions & 0 deletions src/ansys/health/heart/settings/material/ep_material_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Copyright (C) 2023 - 2025 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""Factory for creating EP material models."""

from typing import Literal

from pint import Quantity

from ansys.health.heart.settings.material.ep_material import (
Active,
ActiveBeam,
EPMaterialModel,
EPSolverType,
Passive,
)


def get_default_myocardium_material(
ep_solver_type: EPSolverType | Literal["Monodomain", "Eikonal", "ReactionEikonal"],
) -> Active:
"""Get the default myocardium material for a solver type.

Parameters
----------
ep_solver_type : EPSolverType | str
The type of EP solver to select appropriate defaults for.

Returns
-------
Active
The default myocardium EP material model.
"""
if isinstance(ep_solver_type, str):
ep_solver_type = EPSolverType(ep_solver_type)

# import defaults depending on solver type.
if ep_solver_type in (EPSolverType.REACTION_EIKONAL, EPSolverType.EIKONAL):
from ansys.health.heart.settings.defaults.electrophysiology import (
default_myocardium_material_eikonal as defaults,
)
if ep_solver_type == EPSolverType.MONODOMAIN:
from ansys.health.heart.settings.defaults.electrophysiology import (
default_myocardium_material_monodomain as defaults,
)

# Remove units from default Quantity values.
defaults = {k: (v.m if isinstance(v, Quantity) else v) for k, v in defaults.items()}

return Active(**defaults)


def get_passive_material(
ep_solver_type: EPSolverType | Literal["Monodomain", "Eikonal", "ReactionEikonal"],
) -> Passive:
"""Get the default passive material for a solver type.

Parameters
----------
ep_solver_type : EPSolverType | str
The type of EP solver to select appropriate defaults for.

Returns
-------
Passive
The default passive EP material model.
"""
if isinstance(ep_solver_type, str):
ep_solver_type = EPSolverType(ep_solver_type)

# import defaults depending on solver type.
if ep_solver_type in (EPSolverType.REACTION_EIKONAL, EPSolverType.EIKONAL):
from ansys.health.heart.settings.defaults.electrophysiology import (
default_myocardium_material_eikonal as defaults,
)
if ep_solver_type == EPSolverType.MONODOMAIN:
from ansys.health.heart.settings.defaults.electrophysiology import (
default_myocardium_material_monodomain as defaults,
)

# Remove units from default Quantity values.
defaults = {k: (v.m if isinstance(v, Quantity) else v) for k, v in defaults.items()}

del defaults["sigma_sheet"]
del defaults["sigma_sheet_normal"]

return EPMaterialModel(**defaults)


def get_default_conduction_system_material(
ep_solver_type: EPSolverType | Literal["Monodomain", "Eikonal", "ReactionEikonal"],
) -> ActiveBeam:
"""Get the default conduction-system (beam) material for a solver type.

Parameters
----------
ep_solver_type : EPSolverType | str
The type of EP solver to select appropriate defaults for.

Returns
-------
ActiveBeam
The default conduction system material.
"""
if isinstance(ep_solver_type, str):
ep_solver_type = EPSolverType(ep_solver_type)

# import defaults depending on solver type.
if ep_solver_type in (EPSolverType.REACTION_EIKONAL, EPSolverType.EIKONAL):
from ansys.health.heart.settings.defaults.electrophysiology import (
default_beam_material_eikonal as defaults,
)
elif ep_solver_type == EPSolverType.MONODOMAIN:
from ansys.health.heart.settings.defaults.electrophysiology import (
default_beam_material_monodomain as defaults,
)

# Remove units from default Quantity values.
defaults = {k: (v.m if isinstance(v, Quantity) else v) for k, v in defaults.items()}

return ActiveBeam(**defaults)
Loading
Loading