Skip to content

Materials

The material property model, the concrete materials and the tag-keyed registry the domain config resolves.

femforge.materials

Material library: the property model every material fills in, the concrete materials, and the tag-keyed registry the domain config resolves against.

Modules:

  • air –

    Dry air as a material.

  • copper –

    Annealed copper with a temperature-dependent conductivity.

  • neodymium –

    Sintered NdFeB magnets with temperature-dependent remanence and coercivity.

  • pcb –

    FR4, the glass-epoxy laminate of printed circuit boards.

  • properties –

    The material property model: four property groups whose fields map onto Elmer material keywords.

  • registry –

    The single material registry, keyed by each material's tag — the one source both the CAD/mesh stage and the sif writer read.

  • steel –

    Carbon and stainless steels.

Classes:

  • MaterialProperties –

    A named material: its registry tag and its mechanical, thermal, magnetic and electrical properties.

  • MechanicalProperties –

    The mechanical properties of a material.

  • ThermalProperties –

    The thermal properties of a material.

  • MagneticProperties –

    The magnetic properties of a material.

  • ElectricalProperties –

    The electrical properties of a material.

  • Air –

    Dry air at room temperature.

  • Copper –

    Annealed copper whose resistivity rises linearly with temperature.

  • FR4 –

    FR4 glass-epoxy laminate, the substrate of printed circuit boards.

  • Neodymium –

    Sintered NdFeB magnet; grades supply B_r and H_cb at the reference temperature, authored as Expressions so a hot magnet's weakened drive falls out of the operating point.

  • N42 –

    N42 neodymium magnet.

  • N52 –

    N52 neodymium magnet.

  • Steel_1080 –

    1080 High-carbon steel.

  • Steel_316 –

    316 austenitic stainless steel, annealed.

Functions:

MaterialProperties pydantic-model

Bases: _PropertyModel

A named material: its registry tag and its mechanical, thermal, magnetic and electrical properties.

Fields:

name pydantic-field

name: str = ''

The display name.

tag pydantic-field

tag: str = ''

The registry key that a body's material tag names.

mechanical pydantic-field

The mechanical properties.

thermal pydantic-field

The thermal properties.

magnetic pydantic-field

The magnetic properties.

electrical pydantic-field

The electrical properties.

is_magnet property

is_magnet: bool

Whether the material has a remanence, which marks it as a permanent magnet.

to_elmer

to_elmer(*, at: Mapping[str, Quantity], runtime: frozenset[str] = frozenset(), backend: Backend = 'matc') -> dict[str, float | str]

The sif keyword/value pairs of all four property groups.

Parameters:

  • at (Mapping[str, Quantity]) –

    The operating point the other properties are evaluated at, each stripped to a bare SI float.

  • runtime (frozenset[str], default: frozenset() ) –

    The parameters Elmer supplies at solve time; a property of only these emits as a MATC/Lua expression or table.

  • backend (Backend, default: 'matc' ) –

    The sif expression language for runtime properties.

Source code in src/femforge/materials/properties.py
def to_elmer(self, *, at: Mapping[str, Quantity], runtime: frozenset[str] = frozenset(), backend: Backend = "matc") -> dict[str, float | str]:
    """The sif keyword/value pairs of all four property groups.

    Args:
        at: The operating point the other properties are evaluated at, each stripped to a bare SI float.
        runtime: The parameters Elmer supplies at solve time; a property of only these emits as a MATC/Lua expression or table.
        backend: The sif expression language for runtime properties.
    """
    data: dict[str, float | str] = {}
    data.update(self.mechanical.to_elmer(at=at, runtime=runtime, backend=backend))
    data.update(self.thermal.to_elmer(at=at, runtime=runtime, backend=backend))
    data.update(self.magnetic.to_elmer(at=at, runtime=runtime, backend=backend))
    data.update(self.electrical.to_elmer(at=at, runtime=runtime, backend=backend))
    return data

is_conductor

is_conductor(at: Mapping[str, Quantity]) -> bool

Whether this material conducts current for solver purposes: σ(at) ≥ CONDUCTOR_CONDUCTIVITY_FLOOR.

Source code in src/femforge/materials/properties.py
def is_conductor(self, at: Mapping[str, Quantity]) -> bool:
    """Whether this material conducts current for solver purposes: σ(at) ≥ ``CONDUCTOR_CONDUCTIVITY_FLOOR``."""
    sigma = self.electrical.conductivity
    if sigma is None:
        return False
    return float(sigma(**at).to(U.S / U.m).magnitude) >= CONDUCTOR_CONDUCTIVITY_FLOOR

print_tree

print_tree(tree: Tree | None = None) -> Tree

A rich tree of this material's properties, added under tree when one is given.

Source code in src/femforge/materials/properties.py
def print_tree(self, tree: Tree | None = None) -> Tree:
    """A rich tree of this material's properties, added under ``tree`` when one is given."""
    if tree is None:
        tree = Tree(COLORS.H1(self.name))
    else:
        tree = tree.add(COLORS.H1(self.name))
    for key in ("name", "tag", "mechanical", "thermal", "magnetic", "electrical"):
        value = getattr(self, key)  # pyright: ignore[reportAny]  # reflection over model fields
        if isinstance(value, (MechanicalProperties, ThermalProperties, MagneticProperties, ElectricalProperties)):
            subtree = tree.add(COLORS.H2(title(key)))
            for subkey, subvalue in value.__dict__.items():  # pyright: ignore[reportAny]  # __dict__ values are Any
                prop = COLORS.Prop(title(subkey), f"{subvalue}")
                subtree.add(prop)
        else:
            tree.add(COLORS.Prop(title(key), f"{value}"))
    return tree

MechanicalProperties pydantic-model

Bases: _PropertyModel

The mechanical properties of a material.

Fields:

density pydantic-field

density: Optional[_DensityPF] = None

ρ [kg/m³]: Mass density.

elastic_modulus pydantic-field

elastic_modulus: Optional[_PressurePF] = None

E [Pa]: Elastic (Young's) modulus.

yield_strength pydantic-field

yield_strength: Optional[_PressurePF] = None

σ_y [Pa]: Yield strength.

ultimate_tensile_strength pydantic-field

ultimate_tensile_strength: Optional[_PressurePF] = None

σ_u [Pa]: Ultimate tensile strength.

ThermalProperties pydantic-model

Bases: _PropertyModel

The thermal properties of a material.

Fields:

conductivity pydantic-field

conductivity: Optional[_ThermalConductivityPF] = None

k [W/(m·K)]: Thermal conductivity.

specific_heat_capacity pydantic-field

specific_heat_capacity: Optional[_SpecificHeatPF] = None

c_p [J/(kg·K)]: Specific heat capacity.

convective_heat_transfer_coefficient pydantic-field

convective_heat_transfer_coefficient: Optional[_HeatTransferCoefficientPF] = None

h [W/(m²·K)]: Convective heat transfer coefficient.

glass_transition_temperature pydantic-field

glass_transition_temperature: Optional[_TemperaturePF] = None

T_g [K]: Glass transition temperature.

MagneticProperties pydantic-model

Bases: _PropertyModel

The magnetic properties of a material.

Fields:

rel_permeability pydantic-field

rel_permeability: Optional[_DimensionlessPF] = None

μ_r [-]: Relative permeability.

remanence pydantic-field

remanence: Optional[_FluxDensityPF] = None

B_r [T]: Remanent flux density.

coercivity pydantic-field

coercivity: Optional[_FieldStrengthPF] = None

H_c [A/m]: Coercivity.

bh_curve pydantic-field

bh_curve: Optional[_FieldStrengthPF] = None

H(B) [A/m]: Magnetization curve, a 1-D Calibration over flux_density emitted as Elmer's H-B Curve table.

to_elmer

to_elmer(*, at: Mapping[str, Quantity], runtime: frozenset[str] = frozenset(), backend: Backend = 'matc') -> dict[str, float | str]

The sif keyword/value pairs of the magnetic fields, plus the H-B Curve table when bh_curve is set.

Parameters:

  • at (Mapping[str, Quantity]) –

    The operating point the other properties are evaluated at, each stripped to a bare SI float.

  • runtime (frozenset[str], default: frozenset() ) –

    The parameters Elmer supplies at solve time; a property of only these emits as a MATC/Lua expression or table.

  • backend (Backend, default: 'matc' ) –

    The sif expression language for runtime properties.

Source code in src/femforge/materials/properties.py
def to_elmer(self, *, at: Mapping[str, Quantity], runtime: frozenset[str] = frozenset(), backend: Backend = "matc") -> dict[str, float | str]:
    """The sif keyword/value pairs of the magnetic fields, plus the ``H-B Curve`` table when ``bh_curve`` is set.

    Args:
        at: The operating point the other properties are evaluated at, each stripped to a bare SI float.
        runtime: The parameters Elmer supplies at solve time; a property of only these emits as a MATC/Lua expression or table.
        backend: The sif expression language for runtime properties.
    """
    out: dict[str, float | str] = super().to_elmer(at=at, runtime=runtime, backend=backend)
    if self.bh_curve is not None:
        out[ElmerMaterialKeyword.H_B_CURVE] = _hb_curve_value(self.bh_curve)
    return out

magnetization_magnitude

magnetization_magnitude(*, at: Mapping[str, Quantity]) -> Optional[float]

|M| = Bᵣ/μ₀ in A/m, evaluating the remanence at operating point at; None for a non-magnet (no remanence).

Source code in src/femforge/materials/properties.py
def magnetization_magnitude(self, *, at: Mapping[str, Quantity]) -> Optional[float]:
    """|M| = Bᵣ/μ₀ in A/m, evaluating the remanence at operating point ``at``; None for a non-magnet (no remanence)."""
    br: Optional[float] = _si(self.remanence, U.T, at)
    if br is None:
        return None
    return br / PERMEABILITY_OF_VACUUM

ElectricalProperties pydantic-model

Bases: _PropertyModel

The electrical properties of a material.

Fields:

conductivity pydantic-field

conductivity: Optional[_ElectricalConductivityPF] = None

σ [S/m]: Electrical conductivity.

resistivity pydantic-field

resistivity: Optional[_ResistivityPF] = None

ρ_e [Ω·m]: Electrical resistivity.

permittivity pydantic-field

permittivity: Optional[_PermittivityPF] = None

ε [F/m]: Absolute permittivity, emitted to Elmer as the relative ε/ε₀.

permeability pydantic-field

permeability: Optional[_PermeabilityPF] = None

μ [H/m]: Absolute magnetic permeability.

Air pydantic-model

Air()

Bases: MaterialProperties

Dry air at room temperature.

Fields:

Source code in src/femforge/materials/air.py
def __init__(self):
    """Fill in the properties of dry air at room temperature."""
    super().__init__()
    self.name: str = "Air"
    self.tag: str = "Air"
    self.mechanical.density = Static(value=1.225 * U.kg / U.m**3)

    self.thermal.conductivity = Static(value=0.0257 * U.W / (U.m * U.K))
    self.thermal.specific_heat_capacity = Static(value=1005 * U.J / (U.kg * U.K))
    self.thermal.convective_heat_transfer_coefficient = Static(value=10 * U.W / (U.m**2 * U.K))

    self.magnetic.rel_permeability = Static(value=1.0 * U.dimensionless)

    self.electrical.conductivity = Static(value=1e-14 * U.S / U.m)
    self.electrical.resistivity = Static(value=1e14 * U.ohm * U.m)
    self.electrical.permittivity = Static(value=8.854e-12 * U.F / U.m)
    self.electrical.permeability = Static(value=1.2566370614e-6 * U.H / U.m)

Copper pydantic-model

Copper()

Bases: MaterialProperties

Annealed copper whose resistivity rises linearly with temperature.

Fields:

Source code in src/femforge/materials/copper.py
def __init__(self):
    """Fill in the properties of annealed copper."""
    super().__init__()
    self.name: str = "Copper"
    self.tag: str = "Copper"
    self.mechanical.density = Static(value=8960 * U.kg / U.m**3)

    self.thermal.conductivity = Static(value=401 * U.W / (U.m * U.K))
    self.thermal.specific_heat_capacity = Static(value=385 * U.J / (U.kg * U.K))
    # Natural convection placeholder, matching the other materials; the surface film coefficient is a BC property, not a bulk one.
    self.thermal.convective_heat_transfer_coefficient = Static(value=10 * U.W / (U.m**2 * U.K))

    self.magnetic.rel_permeability = Static(value=1.0 * U.dimensionless)

    self.electrical.conductivity = Expression(
        param_dims={"temperature": DIM_TEMPERATURE},
        result_units="S/m",
        body=CONDUCTIVITY_AT_REFERENCE / (1.0 + RESISTIVITY_TEMPERATURE_COEFFICIENT * (Param(name="temperature") - REFERENCE_TEMPERATURE)),
    )
    self.electrical.resistivity = Static(value=1.68e-8 * U.ohm * U.m)
    self.electrical.permittivity = Static(value=8.854e-12 * U.F / U.m)
    self.electrical.permeability = Static(value=1.2566370614e-6 * U.H / U.m)

FR4 pydantic-model

FR4()

Bases: MaterialProperties

FR4 glass-epoxy laminate, the substrate of printed circuit boards.

Fields:

Source code in src/femforge/materials/pcb.py
def __init__(self):
    """Fill in the properties of FR4."""
    super().__init__()
    self.name: str = "FR4"
    self.tag: str = "FR4"

    self.mechanical.density = Static(value=1900 * U.kg / U.m**3)
    self.mechanical.elastic_modulus = Static(value=20e9 * U.Pa)
    self.mechanical.yield_strength = Static(value=60e6 * U.Pa)
    self.mechanical.ultimate_tensile_strength = Static(value=100e6 * U.Pa)

    self.thermal.conductivity = Static(value=0.3 * U.W / (U.m * U.K))
    self.thermal.specific_heat_capacity = Static(value=1200 * U.J / (U.kg * U.K))
    self.thermal.convective_heat_transfer_coefficient = Static(value=10 * U.W / (U.m**2 * U.K))
    self.thermal.glass_transition_temperature = Static(value=(130 * U.degC).to(U.K))

    self.magnetic.rel_permeability = Static(value=1.0 * U.dimensionless)

    self.electrical.conductivity = Static(value=1e-10 * U.S / U.m)
    self.electrical.resistivity = Static(value=1e10 * U.ohm * U.m)
    self.electrical.permittivity = Static(value=4.5 * 8.854e-12 * U.F / U.m)
    self.electrical.permeability = Static(value=1e-6 * U.H / U.m)

Neodymium pydantic-model

Neodymium(*, name: str, tag: str, remanence_at_reference: float, coercivity_at_reference: float)

Bases: MaterialProperties

Sintered NdFeB magnet; grades supply B_r and H_cb at the reference temperature, authored as Expressions so a hot magnet's weakened drive falls out of the operating point.

Parameters:

  • name (str) –

    The display name.

  • tag (str) –

    The registry key that a body's material tag names.

  • remanence_at_reference (float) –

    The remanence B_r at the reference temperature, in T.

  • coercivity_at_reference (float) –

    The coercivity H_cb at the reference temperature, in A/m.

Config:

  • arbitrary_types_allowed: True
  • validate_assignment: True
  • use_attribute_docstrings: True

Fields:

Source code in src/femforge/materials/neodymium.py
def __init__(self, *, name: str, tag: str, remanence_at_reference: float, coercivity_at_reference: float):
    """Fill in the NdFeB properties around a grade's remanence and coercivity.

    Args:
        name: The display name.
        tag: The registry key that a body's ``material`` tag names.
        remanence_at_reference: The remanence B_r at the reference temperature, in T.
        coercivity_at_reference: The coercivity H_cb at the reference temperature, in A/m.
    """
    super().__init__()

    self.name: str = name
    self.tag: str = tag

    self.mechanical.density = Static(value=7500 * U.kg / U.m**3)
    self.mechanical.elastic_modulus = Static(value=160e9 * U.Pa)
    # Sintered NdFeB is brittle: it fractures at ~80 MPa in tension with no yield plateau, so yield_strength is left unset.
    self.mechanical.ultimate_tensile_strength = Static(value=80e6 * U.Pa)

    self.thermal.conductivity = Static(value=8.7 * U.W / (U.m * U.K))
    self.thermal.specific_heat_capacity = Static(value=440 * U.J / (U.kg * U.K))
    self.thermal.convective_heat_transfer_coefficient = Static(value=10 * U.W / (U.m**2 * U.K))

    self.magnetic.rel_permeability = Static(value=1.05 * U.dimensionless)
    self.magnetic.remanence = Expression(
        param_dims={"temperature": DIM_TEMPERATURE},
        result_units="T",
        body=remanence_at_reference * (1.0 + _REMANENCE_TEMPERATURE_COEFFICIENT * (Param(name="temperature") - _REFERENCE_TEMPERATURE)),
    )
    self.magnetic.coercivity = Expression(
        param_dims={"temperature": DIM_TEMPERATURE},
        result_units="A/m",
        body=coercivity_at_reference * (1.0 + _COERCIVITY_TEMPERATURE_COEFFICIENT * (Param(name="temperature") - _REFERENCE_TEMPERATURE)),
    )

    self.electrical.conductivity = Static(value=7.14e5 * U.S / U.m)
    self.electrical.resistivity = Static(value=1.4e-6 * U.ohm * U.m)
    self.electrical.permittivity = Static(value=8.854e-12 * U.F / U.m)
    self.electrical.permeability = Static(value=1.3196e-6 * U.H / U.m)

N42 pydantic-model

N42()

Bases: Neodymium

N42 neodymium magnet.

Fields:

Source code in src/femforge/materials/neodymium.py
def __init__(self):
    """Fill in the N42 grade."""
    super().__init__(name="Neodymium Magnet (N42)", tag="N42", remanence_at_reference=1.30, coercivity_at_reference=915e3)

N52 pydantic-model

N52()

Bases: Neodymium

N52 neodymium magnet.

Fields:

Source code in src/femforge/materials/neodymium.py
def __init__(self):
    """Fill in the N52 grade."""
    super().__init__(name="Neodymium Magnet (N52)", tag="N52", remanence_at_reference=1.48, coercivity_at_reference=955e3)

Steel_1080 pydantic-model

Steel_1080(rel_permeability: float | None = None)

Bases: MaterialProperties

1080 High-carbon steel.

Fields:

Source code in src/femforge/materials/steel.py
def __init__(self, rel_permeability: float | None = None):
    """``rel_permeability`` None emits the nonlinear H-B curve; a value freezes a linear Static μr."""
    super().__init__()

    self.name: str = "High-Carbon Steel (AISI 1080, annealed)"
    self.tag: str = "Steel1080"

    self.mechanical.density = Static(value=7850 * U.kg / U.m**3)
    self.mechanical.elastic_modulus = Static(value=205e9 * U.Pa)
    self.mechanical.yield_strength = Static(value=375e6 * U.Pa)
    self.mechanical.ultimate_tensile_strength = Static(value=615e6 * U.Pa)

    self.thermal.conductivity = Static(value=47.7 * U.W / (U.m * U.K))
    self.thermal.specific_heat_capacity = Static(value=470 * U.J / (U.kg * U.K))
    self.thermal.convective_heat_transfer_coefficient = Static(value=10 * U.W / (U.m**2 * U.K))

    if rel_permeability is None:
        self.magnetic.bh_curve = Calibration(
            param_dims={"flux_density": DIM_FLUX_DENSITY},
            points=[CalibrationPoint(inputs={"flux_density": b * U.T}, output=h * U.A / U.m) for h, b in _BH_CURVE],
            method="cubic",
        )
    else:
        self.magnetic.rel_permeability = Static(value=rel_permeability * U.dimensionless)
    # Soft-magnetic in this condition, so no remanence is authored: a remanence is what marks a material as a
    # permanent magnet (MaterialProperties.is_magnet) and would give the flux path a spurious magnetization.
    self.magnetic.coercivity = Static(value=400 * U.A / U.m)

    self.electrical.conductivity = Static(value=5.56e6 * U.S / U.m)
    self.electrical.resistivity = Static(value=1.8e-7 * U.ohm * U.m)
    self.electrical.permittivity = Static(value=8.854e-12 * U.F / U.m)

Steel_316 pydantic-model

Steel_316()

Bases: MaterialProperties

316 austenitic stainless steel, annealed.

Fields:

Source code in src/femforge/materials/steel.py
def __init__(self):
    """Fill in the properties of annealed 316 stainless steel."""
    super().__init__()

    self.name: str = "Stainless Steel (AISI 316, annealed)"
    self.tag: str = "Steel316"

    self.mechanical.density = Static(value=8000 * U.kg / U.m**3)
    self.mechanical.elastic_modulus = Static(value=193e9 * U.Pa)
    self.mechanical.yield_strength = Static(value=290e6 * U.Pa)
    self.mechanical.ultimate_tensile_strength = Static(value=580e6 * U.Pa)

    self.thermal.conductivity = Static(value=16.3 * U.W / (U.m * U.K))
    self.thermal.specific_heat_capacity = Static(value=500 * U.J / (U.kg * U.K))
    self.thermal.convective_heat_transfer_coefficient = Static(value=10 * U.W / (U.m**2 * U.K))

    # Annealed austenite is effectively non-magnetic (mu_r ~ 1.008); cold work raises it via strain-induced
    # martensite, which this model does not capture.
    self.magnetic.rel_permeability = Static(value=1.008 * U.dimensionless)

    self.electrical.conductivity = Static(value=1.35e6 * U.S / U.m)
    self.electrical.resistivity = Static(value=7.4e-7 * U.ohm * U.m)
    self.electrical.permittivity = Static(value=8.854e-12 * U.F / U.m)

material

material(tag: str) -> MaterialProperties

Construct a fresh registered material by its tag (e.g. "N52").

Source code in src/femforge/materials/registry.py
def material(tag: str) -> MaterialProperties:
    """Construct a fresh registered material by its tag (e.g. ``"N52"``)."""
    try:
        return _FACTORIES[tag]()
    except KeyError:
        raise KeyError(f"unknown material tag {tag!r}; registered: {available_materials()}") from None

all_materials

all_materials() -> list[MaterialProperties]

A fresh instance of every registered material.

Source code in src/femforge/materials/registry.py
def all_materials() -> list[MaterialProperties]:
    """A fresh instance of every registered material."""
    return [factory() for factory in _FACTORIES.values()]

available_materials

available_materials() -> list[str]

The tags of every registered material.

Source code in src/femforge/materials/registry.py
def available_materials() -> list[str]:
    """The tags of every registered material."""
    return list(_FACTORIES)

register_material

register_material(tag: str, factory: type[MaterialProperties]) -> None

Add or replace the material registered under tag.

Source code in src/femforge/materials/registry.py
def register_material(tag: str, factory: type[MaterialProperties]) -> None:
    """Add or replace the material registered under ``tag``."""
    _FACTORIES[tag] = factory