Skip to content

Commit c05462a

Browse files
refactor: refactor default ep material handling (#1246)
Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent 0871462 commit c05462a

File tree

18 files changed

+726
-281
lines changed

18 files changed

+726
-281
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a material factory module

examples/simulator/ep-mechanics-simulator-fullheart.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050

5151
from ansys.health.heart.examples import get_preprocessed_fullheart
5252
import ansys.health.heart.models as models
53-
import ansys.health.heart.settings.material.ep_material as ep_materials
53+
import ansys.health.heart.settings.material.ep_material_factory as ep_material_factory
5454
from ansys.health.heart.settings.material.material import ISO, Mat295
5555
from ansys.health.heart.simulator import DynaSettings, EPMechanicsSimulator
5656

@@ -133,7 +133,9 @@
133133
ring.meca_material = stiff_iso
134134

135135
# Assign the default EP material
136-
ring.ep_material = ep_materials.Active()
136+
ring.ep_material = ep_material_factory.get_default_myocardium_material(
137+
simulator.settings.electrophysiology.analysis.solvertype
138+
)
137139

138140
# plot the mesh
139141
simulator.model.plot_mesh()

src/ansys/health/heart/exceptions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
"""Custom exceptions for PyAnsys Heart."""
2424

25+
from typing import Literal
26+
2527

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

7779
class MissingEnvironmentVariableError(EnvironmentError):
7880
"""Exception raised when a required environment variable is missing."""
81+
82+
83+
class MissingMaterialError(ValueError):
84+
"""Exception raised when a required material is missing in the model."""
85+
86+
def __init__(self, part_name: str, material_type: Literal["EP", "Mechanical"]):
87+
super().__init__(f"Part {part_name} has no {material_type} material assigned.")

src/ansys/health/heart/settings/defaults/electrophysiology.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,49 @@
4848
"sigma_fiber": Quantity(0.5, "mS/mm"), # mS/mm
4949
"sigma_sheet": Quantity(0.1, "mS/mm"), # mS/mm
5050
"sigma_sheet_normal": Quantity(0.1, "mS/mm"), # mS/mm
51-
"sigma_passive": Quantity(1.0, "mS/mm"), # mS/mm
51+
"sigma_passive": Quantity(1.0, "mS/mm"), # mS/mm: use for passive conduction (e.g. blood)
5252
"beta": Quantity(140, "1/mm"),
5353
"cm": Quantity(0.01, "uF/mm^2"), # uF/mm^2
54-
"lambda": Quantity(0.2, "dimensionless"),
55-
"percent_endo": Quantity(0.17, "dimensionless"),
56-
"percent_mid": Quantity(0.41, "dimensionless"),
54+
# These are not really material properties?
55+
"lambda": Quantity(0.2, "dimensionless"), # activate extracellular potential solve
56+
"percent_endo": Quantity(0.17, "dimensionless"), # thickness of endocardial layer
57+
"percent_mid": Quantity(0.41, "dimensionless"), # thickness of midmyocardial layer
5758
},
5859
"beam": {
5960
"velocity": Quantity(1, "mm/ms"), # mm/ms in case of eikonal model
6061
"sigma": Quantity(1, "mS/mm"), # mS/mm
6162
"beta": Quantity(140, "1/mm"),
6263
"cm": Quantity(0.001, "uF/mm^2"), # uF/mm^2
6364
"lambda": Quantity(0.2, "dimensionless"),
64-
"pmjrestype": Quantity(1),
65-
"pmjres": Quantity(0.001, "1/mS"), # 1/mS
6665
},
6766
}
6867

68+
"""Material settings."""
69+
default_myocardium_material_eikonal = {
70+
"sigma_fiber": Quantity(0.7, "mm/ms"), # mm/ms in case of eikonal model
71+
"sigma_sheet": Quantity(0.2, "mm/ms"), # mm/ms in case of eikonal model
72+
"sigma_sheet_normal": Quantity(0.2, "mm/ms"), # mm/ms in case of eikonal model
73+
"beta": Quantity(140, "1/mm"),
74+
"cm": Quantity(0.01, "uF/mm^2"), # uF/mm^2
75+
"lambda": Quantity(0.2, "dimensionless"),
76+
"percent_endo": Quantity(0.17, "dimensionless"),
77+
"percent_mid": Quantity(0.41, "dimensionless"),
78+
}
79+
default_beam_material_eikonal = {
80+
"sigma": Quantity(1, "mm/ms"), # mm/ms in case of eikonal model
81+
"beta": Quantity(140, "1/mm"),
82+
"cm": Quantity(0.001, "uF/mm^2"), # uF/mm^2
83+
"lambda": Quantity(0.2, "dimensionless"),
84+
}
85+
86+
# Create monodomain defaults by copying eikonal and changing relevant fields
87+
default_beam_material_monodomain = default_beam_material_eikonal.copy()
88+
default_beam_material_monodomain["sigma"] = Quantity(1, "mS/mm") # mS/mm
89+
default_myocardium_material_monodomain = default_myocardium_material_eikonal.copy()
90+
default_myocardium_material_monodomain["sigma_fiber"] = Quantity(0.5, "mS/mm") # mS/mm
91+
default_myocardium_material_monodomain["sigma_sheet"] = Quantity(0.1, "mS/mm") # mS/mm
92+
default_myocardium_material_monodomain["sigma_sheet_normal"] = Quantity(0.1, "mS/mm") # mS/mm
93+
6994
"""Stimulation settings."""
7095
stimulation = {
7196
"stimdefaults": {

src/ansys/health/heart/settings/defaults/purkinje.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@
3434
"nsplit": Quantity(4, "dimensionless"),
3535
"pmjtype": Quantity(2, "dimensionless"),
3636
"pmjradius": Quantity(1.5, "dimensionless"),
37+
"pmjrestype": Quantity(1, "dimensionless"),
38+
"pmjres": Quantity(0.001, "1/mS"), # 1/mS
3739
}

src/ansys/health/heart/settings/material/ep_material.py

Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,30 @@
2222

2323
"""EP material module."""
2424

25-
from typing import Literal, Optional
25+
from enum import Enum
26+
from typing import Optional
2627

2728
from pydantic import BaseModel, Field, model_validator
2829

29-
from ansys.health.heart.settings.defaults import electrophysiology as ep_defaults
3030
from ansys.health.heart.settings.material.cell_models import Tentusscher
3131

3232

33-
class EPMaterialModel(BaseModel):
33+
class EPSolverType(Enum):
34+
"""Enumeration of EP solver types."""
35+
36+
MONODOMAIN = "Monodomain"
37+
EIKONAL = "Eikonal"
38+
REACTION_EIKONAL = "ReactionEikonal"
39+
40+
41+
class EPMaterialModel(BaseModel): # EM MAT 003
3442
"""Base class for all EP material models."""
3543

3644
sigma_fiber: Optional[float] = None
3745
sigma_sheet: Optional[float] = None
3846
sigma_sheet_normal: Optional[float] = None
39-
beta: Optional[float] = ep_defaults.material["myocardium"]["beta"].m
40-
cm: Optional[float] = ep_defaults.material["myocardium"]["cm"].m
47+
beta: Optional[float] = None
48+
cm: Optional[float] = None
4149
lambda_: Optional[float] = None
4250

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

5361

54-
class Insulator(BaseModel):
62+
class Insulator(BaseModel): # EM MAT 001
5563
"""Insulator material."""
5664

5765
sigma_fiber: float = 0.0
5866
cm: float = 0.0
5967
beta: float = 0.0
6068

6169

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

65-
solver_type: Literal["Monodomain", "Eikonal", "Reaction-Eikonal"] = ep_defaults.analysis[
66-
"solvertype"
67-
]
68-
6974
sigma_fiber: Optional[float] = None
7075
sigma_sheet: Optional[float] = None
7176
sigma_sheet_normal: Optional[float] = None
7277

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

75-
# NOTE: complicated logic and conditional default values. Should split into different classes
76-
@model_validator(mode="after")
77-
def check_sigmas(self):
78-
"""Conditional validation of sigmas."""
79-
if self.solver_type == "Monodomain":
80-
if self.sigma_fiber is None:
81-
self.sigma_fiber = ep_defaults.material["myocardium"]["sigma_fiber"].m
82-
if self.sigma_sheet is None:
83-
self.sigma_sheet = ep_defaults.material["myocardium"]["sigma_sheet"].m
84-
if self.sigma_sheet_normal is None:
85-
self.sigma_sheet_normal = ep_defaults.material["myocardium"]["sigma_sheet_normal"].m
86-
elif self.solver_type == "Eikonal" or self.solver_type == "ReactionEikonal":
87-
if self.sigma_fiber is None:
88-
self.sigma_fiber = ep_defaults.material["myocardium"]["velocity_fiber"].m
89-
if self.sigma_sheet is None:
90-
self.sigma_sheet = ep_defaults.material["myocardium"]["velocity_sheet"].m
91-
if self.sigma_sheet_normal is None:
92-
self.sigma_sheet_normal = ep_defaults.material["myocardium"][
93-
"velocity_sheet_normal"
94-
].m
95-
96-
return self
97-
9880

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

102-
sigma_fiber: float = ep_defaults.material["beam"]["sigma"].m
84+
sigma_fiber: float = 1.0
10385
# TODO: replace by TentusscherEndo
10486
cell_model: Tentusscher = Tentusscher()
105-
pmjres: float = ep_defaults.material["beam"]["pmjres"].m
87+
pmjres: float = 0.001
10688

10789

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

111-
sigma_fiber: float = ep_defaults.material["myocardium"]["sigma_fiber"].m
112-
sigma_sheet: Optional[float] = None
113-
sigma_sheet_normal: Optional[float] = None
94+
# sigma_fiber: Optional[float] = None
95+
# sigma_sheet: Optional[float] = None
96+
# sigma_sheet_normal: Optional[float] = None
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Copyright (C) 2023 - 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
"""Factory for creating EP material models."""
24+
25+
from typing import Literal
26+
27+
from pint import Quantity
28+
29+
from ansys.health.heart.settings.material.ep_material import (
30+
Active,
31+
ActiveBeam,
32+
EPSolverType,
33+
Passive,
34+
)
35+
36+
37+
def get_default_myocardium_material(
38+
ep_solver_type: EPSolverType | Literal["Monodomain", "Eikonal", "ReactionEikonal"],
39+
) -> Active:
40+
"""Get the default myocardium material for a solver type.
41+
42+
Parameters
43+
----------
44+
ep_solver_type : EPSolverType | str
45+
The type of EP solver to select appropriate defaults for.
46+
47+
Returns
48+
-------
49+
Active
50+
The default myocardium EP material model.
51+
"""
52+
if isinstance(ep_solver_type, str):
53+
ep_solver_type = EPSolverType(ep_solver_type)
54+
55+
# import defaults depending on solver type.
56+
if ep_solver_type in (EPSolverType.REACTION_EIKONAL, EPSolverType.EIKONAL):
57+
from ansys.health.heart.settings.defaults.electrophysiology import (
58+
default_myocardium_material_eikonal as defaults,
59+
)
60+
if ep_solver_type == EPSolverType.MONODOMAIN:
61+
from ansys.health.heart.settings.defaults.electrophysiology import (
62+
default_myocardium_material_monodomain as defaults,
63+
)
64+
65+
# Remove units from default Quantity values.
66+
defaults = {k: (v.m if isinstance(v, Quantity) else v) for k, v in defaults.items()}
67+
68+
return Active(**defaults)
69+
70+
71+
def get_passive_material(
72+
ep_solver_type: EPSolverType | Literal["Monodomain", "Eikonal", "ReactionEikonal"],
73+
) -> Passive:
74+
"""Get the default passive material for a solver type.
75+
76+
Parameters
77+
----------
78+
ep_solver_type : EPSolverType | str
79+
The type of EP solver to select appropriate defaults for.
80+
81+
Returns
82+
-------
83+
Passive
84+
The default passive EP material model.
85+
"""
86+
if isinstance(ep_solver_type, str):
87+
ep_solver_type = EPSolverType(ep_solver_type)
88+
89+
# import defaults depending on solver type.
90+
if ep_solver_type in (EPSolverType.REACTION_EIKONAL, EPSolverType.EIKONAL):
91+
from ansys.health.heart.settings.defaults.electrophysiology import (
92+
default_myocardium_material_eikonal as defaults,
93+
)
94+
if ep_solver_type == EPSolverType.MONODOMAIN:
95+
from ansys.health.heart.settings.defaults.electrophysiology import (
96+
default_myocardium_material_monodomain as defaults,
97+
)
98+
99+
# Remove units from default Quantity values.
100+
defaults = {k: (v.m if isinstance(v, Quantity) else v) for k, v in defaults.items()}
101+
102+
del defaults["sigma_sheet"]
103+
del defaults["sigma_sheet_normal"]
104+
105+
return Passive(**defaults)
106+
107+
108+
def get_default_conduction_system_material(
109+
ep_solver_type: EPSolverType | Literal["Monodomain", "Eikonal", "ReactionEikonal"],
110+
) -> ActiveBeam:
111+
"""Get the default conduction-system (beam) material for a solver type.
112+
113+
Parameters
114+
----------
115+
ep_solver_type : EPSolverType | str
116+
The type of EP solver to select appropriate defaults for.
117+
118+
Returns
119+
-------
120+
ActiveBeam
121+
The default conduction system material.
122+
"""
123+
if isinstance(ep_solver_type, str):
124+
ep_solver_type = EPSolverType(ep_solver_type)
125+
126+
# import defaults depending on solver type.
127+
if ep_solver_type in (EPSolverType.REACTION_EIKONAL, EPSolverType.EIKONAL):
128+
from ansys.health.heart.settings.defaults.electrophysiology import (
129+
default_beam_material_eikonal as defaults,
130+
)
131+
elif ep_solver_type == EPSolverType.MONODOMAIN:
132+
from ansys.health.heart.settings.defaults.electrophysiology import (
133+
default_beam_material_monodomain as defaults,
134+
)
135+
136+
# Remove units from default Quantity values.
137+
defaults = {k: (v.m if isinstance(v, Quantity) else v) for k, v in defaults.items()}
138+
139+
return ActiveBeam(**defaults)

0 commit comments

Comments
 (0)