Skip to content

Physical

The physics domain model: region tags and their materials, boundary and body conditions, and the property functions their values come from.

femforge.physical

The physics domain model: per-region tags and their materials, the boundary/body conditions, and the property functions those values are written as.

Modules:

  • conditions –

    Conditions attached to a region, each carrying its physics, its body or boundary target and its own Elmer sif emission.

  • domain –

    The simulation domain: the materials and per-region tag overrides shared by the mesh and FEA backends.

  • expressions –

    Typed expression AST over bare SI magnitudes for variable-dependent material properties: evaluates in Python, renders to Elmer MATC or Lua, serializes to JSON.

  • property_functions –

    Material properties as pint-aware callables (Static, Calibration, ClosedForm, Expression) behind the PropertyFunction protocol.

  • units –

    The shared pint unit registry, its typed quantity facades and the dimensionality-constrained quantity field types.

Classes:

  • CoilDrive –

    A driven coil under COIL_MAGNETOSTATICS (body target): bodies sharing this condition become one massive CoilSolver Component normalized to current.

  • CoilTerminal –

    One end of an open coil's terminal pair, Elmer's Coil Start or Coil End marker (COIL_MAGNETOSTATICS, boundary target).

  • Condition –

    Base condition: subclasses pin physics/target defaults and implement to_elmer. Never instantiated directly (no kind discriminator).

  • ConditionTarget –

    Whether a condition attaches to a 3D body or a 2D boundary.

  • Convection –

    Convective (Newton-cooling) boundary condition (thermal, boundary target); external_only — a film BC on an interior interface would bypass the conduction path.

  • CurrentDensity –

    Uniform signed azimuthal current density J = N·I/A over a winding window (axisymmetric magnetostatics, body target), the homogenized stranded-coil drive.

  • FixedPotential –

    Dirichlet electric-potential boundary condition (current conduction, boundary target), one electrode of a phase's terminal pair.

  • FixedTemperature –

    Dirichlet temperature boundary condition (thermal, boundary target).

  • ForceComponent –

    An Elmer Component lumping CalcFields' magnetic force and torque over the bodies this condition's tag matches (magnetic physics, body target).

  • HeatFlux –

    Prescribed heat flux boundary condition (thermal, boundary target).

  • HeatSource –

    Volumetric heat source in W/m³ (thermal, body target).

  • JouleHeat –

    Joule heat source body force (coupled current->thermal, body target): HeatSolve integrates StatCurrentSolve's loss density as the heat source.

  • Magnetization –

    Permanent-magnet body force (magnetostatics, body target).

  • DomainConfig –

    The materials the mesher matches to STEP parts, plus the per-region EntityTag overrides (conditions / mesh refinement).

  • EntityTag –

    A per-region override matched to a mesh entity by its authored tags; carries the region's self-describing conditions.

  • Add –

    The sum of two subexpressions.

  • Const –

    A literal SI magnitude.

  • Div –

    The quotient of two subexpressions.

  • ExprNode –

    Base node: shared config + Python operator composition, so laws are authored as plain arithmetic.

  • Mul –

    The product of two subexpressions.

  • Neg –

    The negation of a subexpression.

  • Param –

    A named parameter (e.g. temperature), bound at evaluation/render time.

  • Pow –

    One subexpression raised to the power of another.

  • Sub –

    The difference of two subexpressions.

  • BasePropertyFunction –

    Pydantic base implementing parameter checking once; subclasses declare parameters / result_dimensionality and implement _evaluate.

  • Calibration –

    A property defined by sample points and interpolated between them (1-D only for now).

  • CalibrationPoint –

    One measured sample: input quantities by name and the resulting output.

  • ClosedForm –

    A property defined by an explicit Python callable, pint-in / pint-out; the callable is code, not JSON-serializable.

  • Expression –

    A property defined by a serializable typed expression AST (physical.expressions) — ClosedForm as data instead of code.

  • PropertyDimensionError –

    A supplied parameter had the wrong dimensionality.

  • PropertyError –

    Base class for every property-function error.

  • PropertyFunction –

    A material property as a function of zero or more named pint quantities, returning a pint Quantity.

  • PropertyFunctionField –

    Annotated metadata for a PropertyFunction constrained to dimensionality.

  • PropertyParameterError –

    A required parameter was not supplied when evaluating a property.

  • PropertyRangeError –

    A parameter fell outside the calibrated range and extrapolation is off.

  • Static –

    A constant property: zero parameters, always returns value.

  • Quantity –

    A pint quantity with a float magnitude.

  • Unit –

    A pint unit, typed so that arithmetic with units and numbers keeps its type.

  • VectorQuantity –

    A pint quantity with a Vec3 magnitude.

Functions:

  • conditions_for –

    The conditions on tags that belong to physics and attach to target.

  • as_expr –

    Wrap a bare number into Const; pass nodes through (the operator-composition coercion point).

  • evaluate –

    Module-level convenience mirroring expr.evaluate.

  • render –

    Backend source for expr with parameter references substituted (e.g. {"temperature": "tx(0)"}).

  • property_function_type –

    A PropertyFunction constrained to dimensionality, usable as a Pydantic field.

  • quantity –

    Statically-typed face of U.Quantity(...): a float magnitude yields Quantity, a Vec3 yields VectorQuantity.

Attributes:

  • ConditionUnion –

    Any concrete condition, discriminated by its kind.

  • Expr –

    Any expression node, discriminated by its op.

  • U –

    The shared pint unit registry, as in 5 * U.mm or U.Quantity(1.2, "T").

ConditionUnion module-attribute

Any concrete condition, discriminated by its kind.

Expr module-attribute

Expr = Union[Const, Param, Add, Sub, Mul, Div, Pow, Neg]

Any expression node, discriminated by its op.

U module-attribute

U = cast('_TypedRegistry', pint.UnitRegistry(autoconvert_offset_to_baseunit=True))

The shared pint unit registry, as in 5 * U.mm or U.Quantity(1.2, "T").

CoilDrive pydantic-model

Bases: Condition

A driven coil under COIL_MAGNETOSTATICS (body target): bodies sharing this condition become one massive CoilSolver Component normalized to current.

Reverse the drive by swapping the terminal roles, not with a negative current.

Fields:

kind pydantic-field

kind: Literal['coil_drive'] = 'coil_drive'

Discriminator that names this condition type in a serialized config.

physics pydantic-field

The physics whose solve this condition feeds.

target pydantic-field

Whether the condition attaches to a body or a boundary.

current pydantic-field

current: Current

I_coil [A]: The total coil current CoilSolver normalizes this coil's solved distribution to.

to_elmer

to_elmer() -> dict[str, float | str]

The Desired Coil Current and the massive Coil Type.

Source code in src/femforge/physical/conditions.py
def to_elmer(self) -> dict[str, float | str]:
    """The ``Desired Coil Current`` and the massive ``Coil Type``."""
    out: dict[str, float | str] = super().to_elmer()
    # Massive: one solid conductor whose current distribution is the solved conduction field (the PCB winding),
    # as opposed to "stranded" (a wound bundle with imposed uniform fill).
    out["Coil Type"] = '"massive"'
    return out

CoilTerminal pydantic-model

Bases: Condition

One end of an open coil's terminal pair, Elmer's Coil Start or Coil End marker (COIL_MAGNETOSTATICS, boundary target).

The electrode's end face must lie on the domain boundary (GeometryConfig.terminate_electrodes); positive coil current flows from start to end.

Fields:

kind pydantic-field

kind: Literal['coil_terminal'] = 'coil_terminal'

Discriminator that names this condition type in a serialized config.

physics pydantic-field

The physics whose solve this condition feeds.

target pydantic-field

Whether the condition attaches to a body or a boundary.

role pydantic-field

role: Literal['start', 'end']

Which end of the coil this electrode is; CoilSolver's normalized current flows start -> end.

to_elmer

to_elmer() -> dict[str, float | str]

The Coil Start or Coil End flag for this role.

Source code in src/femforge/physical/conditions.py
def to_elmer(self) -> dict[str, float | str]:
    """The ``Coil Start`` or ``Coil End`` flag for this ``role``."""
    return {"Coil Start" if self.role == "start" else "Coil End": "Logical True"}

Condition pydantic-model

Bases: BaseModel

Base condition: subclasses pin physics/target defaults and implement to_elmer. Never instantiated directly (no kind discriminator).

Config:

  • arbitrary_types_allowed: True
  • use_attribute_docstrings: True

Fields:

external_only class-attribute

external_only: bool = False

Boundary-target conditions with this set land only on a body's external (domain-boundary) surface groups, not its interfaces — e.g. a coil terminal is the electrode's exposed end face, not its interior contacts.

physics pydantic-field

physics: Physics

The physics whose solve this condition feeds.

target pydantic-field

Whether the condition attaches to a body or a boundary.

to_elmer

to_elmer(*args: Any, **kwargs: Any) -> dict[str, float | str]

This condition's Elmer sif keyword dict, reflected from each field's ElmerKeyword metadata.

Source code in src/femforge/physical/conditions.py
def to_elmer(self, *args: Any, **kwargs: Any) -> dict[str, float | str]:  # pyright: ignore[reportExplicitAny, reportAny]  # variadic: subclass to_elmer signatures diverge (Magnetization takes a magnitude)
    """This condition's Elmer sif keyword dict, reflected from each field's ``ElmerKeyword`` metadata."""
    out: dict[str, float | str] = {}
    for name, field in type(self).model_fields.items():
        spec: ElmerKeyword | None = _elmer_keyword(field)
        if spec is None:
            continue
        out[spec.key] = _si(getattr(self, name), spec.unit)  # pyright: ignore[reportAny]  # getattr is Any
    return out

check_frame

check_frame(frame: Frame) -> None

Raise when this condition cannot drive a frame solve.

Source code in src/femforge/physical/conditions.py
def check_frame(self, frame: Frame) -> None:
    """Raise when this condition cannot drive a ``frame`` solve."""

ConditionTarget

Bases: StrEnum

Whether a condition attaches to a 3D body or a 2D boundary.

Attributes:

  • BODY –

    Attach to a body.

  • BOUNDARY –

    Attach to a boundary surface.

BODY class-attribute instance-attribute

BODY = 'body'

Attach to a body.

BOUNDARY class-attribute instance-attribute

BOUNDARY = 'boundary'

Attach to a boundary surface.

Convection pydantic-model

Bases: Condition

Convective (Newton-cooling) boundary condition (thermal, boundary target); external_only — a film BC on an interior interface would bypass the conduction path.

Fields:

kind pydantic-field

kind: Literal['convection'] = 'convection'

Discriminator that names this condition type in a serialized config.

physics pydantic-field

physics: Physics = Physics.THERMAL

The physics whose solve this condition feeds.

target pydantic-field

Whether the condition attaches to a body or a boundary.

coefficient pydantic-field

coefficient: HeatTransferCoefficient

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

ext_temperature pydantic-field

ext_temperature: Temperature

T_amb [K]: External (ambient) temperature the boundary exchanges with.

CurrentDensity pydantic-model

Bases: Condition

Uniform signed azimuthal current density J = N·I/A over a winding window (axisymmetric magnetostatics, body target), the homogenized stranded-coil drive.

Requires frame=Axisymmetric().

Fields:

kind pydantic-field

kind: Literal['current_density'] = 'current_density'

Discriminator that names this condition type in a serialized config.

physics pydantic-field

The physics whose solve this condition feeds.

target pydantic-field

Whether the condition attaches to a body or a boundary.

value pydantic-field

value: CurrentDensity

J [A/m²]: Azimuthal current density (signed) imposed uniformly over the body.

check_frame

check_frame(frame: Frame) -> None

Raise unless frame is axisymmetric.

Source code in src/femforge/physical/conditions.py
def check_frame(self, frame: Frame) -> None:
    """Raise unless ``frame`` is axisymmetric."""
    if isinstance(frame, Solid):
        raise ValueError("CurrentDensity is the axisymmetric coil drive; it requires frame=Axisymmetric().")

FixedPotential pydantic-model

Bases: Condition

Dirichlet electric-potential boundary condition (current conduction, boundary target), one electrode of a phase's terminal pair.

Fields:

kind pydantic-field

kind: Literal['fixed_potential'] = 'fixed_potential'

Discriminator that names this condition type in a serialized config.

physics pydantic-field

The physics whose solve this condition feeds.

target pydantic-field

Whether the condition attaches to a body or a boundary.

value pydantic-field

value: Voltage

V [V]: Electric potential imposed on the electrode boundary; the keyword is the solver variable's name (Elmer Dirichlet semantics), single-sourced via the enum.

to_elmer

to_elmer() -> dict[str, float | str]

The electrode potential plus the Save Scalars flag that makes SaveScalars integrate the electrode current.

Source code in src/femforge/physical/conditions.py
def to_elmer(self) -> dict[str, float | str]:
    """The electrode potential plus the ``Save Scalars`` flag that makes SaveScalars integrate the electrode current."""
    out: dict[str, float | str] = super().to_elmer()
    # Mark the electrode for SaveScalars boundary integration (the per-phase current measurement).
    out["Save Scalars"] = "Logical True"
    return out

FixedTemperature pydantic-model

Bases: Condition

Dirichlet temperature boundary condition (thermal, boundary target).

Fields:

kind pydantic-field

kind: Literal['fixed_temperature'] = 'fixed_temperature'

Discriminator that names this condition type in a serialized config.

physics pydantic-field

physics: Physics = Physics.THERMAL

The physics whose solve this condition feeds.

target pydantic-field

Whether the condition attaches to a body or a boundary.

value pydantic-field

value: Temperature

T [K]: Fixed temperature imposed on the boundary.

ForceComponent pydantic-model

Bases: Condition

An Elmer Component lumping CalcFields' magnetic force and torque over the bodies this condition's tag matches (magnetic physics, body target).

Fields:

kind pydantic-field

kind: Literal['force_component'] = 'force_component'

Discriminator that names this condition type in a serialized config.

physics pydantic-field

The physics whose solve this condition feeds.

target pydantic-field

Whether the condition attaches to a body or a boundary.

label pydantic-field

label: str | None = None

The Component's Name; None uses the authoring tag's name.

calculate_magnetic_force pydantic-field

calculate_magnetic_force: bool = True

Lump the nodal magnetic force over the master bodies.

calculate_magnetic_torque pydantic-field

calculate_magnetic_torque: bool = False

Also lump the magnetic torque (requires torque_origin/torque_axis).

torque_origin pydantic-field

torque_origin: Vec3Field | None = None

r_0 [m]: Torque pivot point.

torque_axis pydantic-field

torque_axis: DirectionField | None = None

Torque axis direction.

to_elmer

to_elmer() -> dict[str, float | str]

The CalcFields force and torque flags, plus the torque origin and axis when set.

Source code in src/femforge/physical/conditions.py
def to_elmer(self) -> dict[str, float | str]:
    """The CalcFields force and torque flags, plus the torque origin and axis when set."""
    out: dict[str, float | str] = {}
    if self.calculate_magnetic_force:
        out["Calculate Magnetic Force"] = "Logical True"
    if self.calculate_magnetic_torque:
        out["Calculate Magnetic Torque"] = "Logical True"
    if self.torque_origin is not None:
        out["Torque Origin(3)"] = f"Real {self.torque_origin.x:.6g} {self.torque_origin.y:.6g} {self.torque_origin.z:.6g}"
    if self.torque_axis is not None:
        out["Torque Axis(3)"] = f"Real {self.torque_axis.x:.6g} {self.torque_axis.y:.6g} {self.torque_axis.z:.6g}"
    return out

check_frame

check_frame(frame: Frame) -> None

Raise when torque is requested under an axisymmetric frame.

Source code in src/femforge/physical/conditions.py
def check_frame(self, frame: Frame) -> None:
    """Raise when torque is requested under an axisymmetric ``frame``."""
    torque: bool = self.calculate_magnetic_torque or self.torque_origin is not None or self.torque_axis is not None

    if torque and isinstance(frame, Axisymmetric):
        raise ValueError("ForceComponent torque has no axisymmetric form; drop calculate_magnetic_torque, torque_origin and torque_axis under frame=Axisymmetric().")

HeatFlux pydantic-model

Bases: Condition

Prescribed heat flux boundary condition (thermal, boundary target).

Fields:

kind pydantic-field

kind: Literal['heat_flux'] = 'heat_flux'

Discriminator that names this condition type in a serialized config.

physics pydantic-field

physics: Physics = Physics.THERMAL

The physics whose solve this condition feeds.

target pydantic-field

Whether the condition attaches to a body or a boundary.

value pydantic-field

value: HeatFlux

q_s [W/m²]: Heat flux through the boundary.

HeatSource pydantic-model

Bases: Condition

Volumetric heat source in W/m³ (thermal, body target).

With temperature_coefficient set, the source scales linearly with the local temperature, the σ(T) Joule feedback of a prescribed-current winding.

Fields:

kind pydantic-field

kind: Literal['heat_source'] = 'heat_source'

Discriminator that names this condition type in a serialized config.

physics pydantic-field

physics: Physics = Physics.THERMAL

The physics whose solve this condition feeds.

target pydantic-field

Whether the condition attaches to a body or a boundary.

power_density pydantic-field

power_density: PowerDensity

q [W/m³]: Heat generation density at reference_temperature when a coefficient is set, else the constant source.

temperature_coefficient pydantic-field

temperature_coefficient: float | None = None

α [1/K]: Linear coefficient in q(T) = power_density·(1 + α·(T − reference_temperature)); None emits the constant.

reference_temperature pydantic-field

reference_temperature: Temperature | None = None

T_ref [K]: The temperature power_density holds at; required with temperature_coefficient.

to_elmer

to_elmer() -> dict[str, float | str]

The Volumetric Heat Source keyword, a MATC expression in temperature when temperature_coefficient is set.

Source code in src/femforge/physical/conditions.py
def to_elmer(self) -> dict[str, float | str]:
    """The ``Volumetric Heat Source`` keyword, a MATC expression in temperature when ``temperature_coefficient`` is set."""
    out: dict[str, float | str] = super().to_elmer()
    if self.temperature_coefficient is not None:
        if self.reference_temperature is None:
            raise ValueError("temperature_coefficient requires reference_temperature (the temperature power_density holds at).")
        source_at_reference: float = _si(self.power_density, U.W / U.m**3)
        reference_kelvin: float = _si(self.reference_temperature, U.K)
        out["Volumetric Heat Source"] = f'Variable Temperature\n    Real MATC "{source_at_reference:.9g}*(1.0+{self.temperature_coefficient:.9g}*(tx(0)-{reference_kelvin:.9g}))"'
    return out

JouleHeat pydantic-model

Bases: Condition

Joule heat source body force (coupled current->thermal, body target): HeatSolve integrates StatCurrentSolve's loss density as the heat source.

Carries no value — the magnitude is the solved current field itself.

Fields:

kind pydantic-field

kind: Literal['joule_heat'] = 'joule_heat'

Discriminator that names this condition type in a serialized config.

physics pydantic-field

The physics whose solve this condition feeds.

target pydantic-field

Whether the condition attaches to a body or a boundary.

to_elmer

to_elmer() -> dict[str, float | str]

The Joule Heat flag.

Source code in src/femforge/physical/conditions.py
def to_elmer(self) -> dict[str, float | str]:
    """The ``Joule Heat`` flag."""
    return {"Joule Heat": "Logical True"}

Magnetization pydantic-model

Bases: Condition

Permanent-magnet body force (magnetostatics, body target).

The direction is per-region and lives here; the magnitude |M| = Bᵣ/μ₀ is a material property passed into to_elmer.

Fields:

kind pydantic-field

kind: Literal['magnetization'] = 'magnetization'

Discriminator that names this condition type in a serialized config.

physics pydantic-field

The physics whose solve this condition feeds.

target pydantic-field

Whether the condition attaches to a body or a boundary.

direction pydantic-field

direction: DirectionField | None

Magnetization direction in the authored model's frame, None for an unmagnetized body; magnitude comes from the material.

to_elmer

to_elmer(magnitude: float, frame: Frame) -> dict[str, float | str]

The Magnetization components of magnitude (A/m) along the direction, in frame's solve coordinates.

Source code in src/femforge/physical/conditions.py
def to_elmer(self, magnitude: float, frame: Frame) -> dict[str, float | str]:
    """The ``Magnetization`` components of ``magnitude`` (A/m) along the direction, in ``frame``'s solve coordinates."""
    unit: Vec3 | None = self.direction

    if unit is None:
        raise ValueError("A Magnetization without a direction has no components to emit.")

    match frame:
        case Solid():
            m: Vec3 = unit * magnitude
            return {
                "Magnetization 1": f"{m.x:.6g}",
                "Magnetization 2": f"{m.y:.6g}",
                "Magnetization 3": f"{m.z:.6g}",
            }
        case Axisymmetric():
            along: float | None = frame.axis.along(unit)

            if along is None:
                raise ValueError(f"Magnetization direction {self.direction} is not along the symmetry axis {frame.axis.direction}; a transverse magnetization is not axisymmetric.")

            return {
                "Magnetization 1": "0",
                "Magnetization 2": f"{along * magnitude:.6g}",
            }

check_frame

check_frame(frame: Frame) -> None

Raise when an axisymmetric frame meets a direction off its symmetry axis.

Source code in src/femforge/physical/conditions.py
def check_frame(self, frame: Frame) -> None:
    """Raise when an axisymmetric ``frame`` meets a direction off its symmetry axis."""
    # A missing direction is left to Sim.validate, which reports it as a missing direction rather than a frame mismatch.
    if not isinstance(frame, Axisymmetric) or self.direction is None:
        return

    if frame.axis.along(self.direction) is None:
        raise ValueError(f"Magnetization direction {self.direction} is not along the symmetry axis {frame.axis.direction}; a transverse magnetization is not axisymmetric.")

DomainConfig pydantic-model

Bases: ConfigArea

The materials the mesher matches to STEP parts, plus the per-region EntityTag overrides (conditions / mesh refinement).

Fields:

materials pydantic-field

The candidate materials; the mesher matches each STEP entity to one by tag.

tags pydantic-field

tags: list[EntityTag]

The per-region overrides, matched to mesh entities by name; each carries its conditions and optional mesh_size.

check_frame

check_frame(frame: Frame) -> None

Raise when a condition here cannot drive a frame solve.

Source code in src/femforge/physical/domain.py
def check_frame(self, frame: Frame) -> None:
    """Raise when a condition here cannot drive a ``frame`` solve."""
    for tag in self.tags:
        for condition in tag.conditions:
            condition.check_frame(frame)

EntityTag pydantic-model

Bases: ConfigArea

A per-region override matched to a mesh entity by its authored tags; carries the region's self-describing conditions.

Fields:

selector pydantic-field

selector: TagDict

Compound-key selector: a mesh entity matches when its tag dict contains ALL of these key=value pairs (an empty selector matches every entity).

conditions pydantic-field

conditions: list[ConditionUnion]

Composable, self-describing conditions (body forces / boundary conditions) this region carries.

mesh_size pydantic-field

mesh_size: float | None = None

h [m]: Target mesh size for this region, such as a finer air gap; None inherits MeshConfig.global_mesh_size.

name property

name: str

Stable human-readable stem shared with gmsh/Elmer names: the uppercased part name for a lone Name selector, else sorted KeyValue pairs (CurrentDirEntry_Phase2).

matches

matches(tags: Mapping[str, str]) -> bool

Whether an entity's tag dict satisfies every selector pair.

Source code in src/femforge/physical/domain.py
def matches(self, tags: Mapping[str, str]) -> bool:
    """Whether an entity's tag dict satisfies every selector pair."""
    return all(tags.get(key) == value for key, value in self.selector.items())

print_tree

print_tree(tree: Tree) -> None

Add this tag and its conditions to tree.

Source code in src/femforge/physical/domain.py
def print_tree(self, tree: Tree) -> None:
    """Add this tag and its conditions to ``tree``."""
    report: Tree = tree.add(COLORS.H2(f"Tag: {self.name}"))
    for condition in self.conditions:
        report.add(COLORS.Prop(type(condition).__name__, f"{condition}"))

Add pydantic-model

Bases: _Binary

The sum of two subexpressions.

Fields:

  • left ('Expr')
  • right ('Expr')
  • op (Literal['add'])
  • _symbol (str)

op pydantic-field

op: Literal['add'] = 'add'

Discriminator that names this node type in serialized JSON.

evaluate

evaluate(env: Mapping[str, float]) -> float

The sum of both operands.

Source code in src/femforge/physical/expressions.py
def evaluate(self, env: Mapping[str, float]) -> float:
    """The sum of both operands."""
    return self.left.evaluate(env) + self.right.evaluate(env)

Const pydantic-model

Bases: ExprNode

A literal SI magnitude.

Fields:

op pydantic-field

op: Literal['const'] = 'const'

Discriminator that names this node type in serialized JSON.

value pydantic-field

value: float

c [?]: The literal magnitude, in SI units.

evaluate

evaluate(env: Mapping[str, float]) -> float

The literal value.

Source code in src/femforge/physical/expressions.py
def evaluate(self, env: Mapping[str, float]) -> float:
    """The literal value."""
    return self.value

render

render(refs: Mapping[str, str]) -> str

The literal at full float precision.

Source code in src/femforge/physical/expressions.py
def render(self, refs: Mapping[str, str]) -> str:
    """The literal at full float precision."""
    # repr keeps full float precision; MATC and Lua both read e-notation.
    return repr(self.value)

parameters

parameters() -> frozenset[str]

The empty set: a literal references no parameter.

Source code in src/femforge/physical/expressions.py
def parameters(self) -> frozenset[str]:
    """The empty set: a literal references no parameter."""
    return frozenset()

Div pydantic-model

Bases: _Binary

The quotient of two subexpressions.

Fields:

  • left ('Expr')
  • right ('Expr')
  • op (Literal['div'])
  • _symbol (str)

op pydantic-field

op: Literal['div'] = 'div'

Discriminator that names this node type in serialized JSON.

evaluate

evaluate(env: Mapping[str, float]) -> float

The left operand divided by the right one.

Source code in src/femforge/physical/expressions.py
def evaluate(self, env: Mapping[str, float]) -> float:
    """The left operand divided by the right one."""
    return self.left.evaluate(env) / self.right.evaluate(env)

ExprNode pydantic-model

Bases: BaseModel

Base node: shared config + Python operator composition, so laws are authored as plain arithmetic.

Config:

  • frozen: True
  • use_attribute_docstrings: True

evaluate

evaluate(env: Mapping[str, float]) -> float

This subtree's value with each parameter bound to a bare SI float.

Source code in src/femforge/physical/expressions.py
def evaluate(self, env: Mapping[str, float]) -> float:
    """This subtree's value with each parameter bound to a bare SI float."""
    raise NotImplementedError

render

render(refs: Mapping[str, str]) -> str

This subtree as backend source, with each parameter name replaced by its reference (e.g. tx(0)).

Source code in src/femforge/physical/expressions.py
def render(self, refs: Mapping[str, str]) -> str:
    """This subtree as backend source, with each parameter name replaced by its reference (e.g. ``tx(0)``)."""
    raise NotImplementedError

parameters

parameters() -> frozenset[str]

Every parameter name referenced in this subtree.

Source code in src/femforge/physical/expressions.py
def parameters(self) -> frozenset[str]:
    """Every parameter name referenced in this subtree."""
    raise NotImplementedError

Mul pydantic-model

Bases: _Binary

The product of two subexpressions.

Fields:

  • left ('Expr')
  • right ('Expr')
  • op (Literal['mul'])
  • _symbol (str)

op pydantic-field

op: Literal['mul'] = 'mul'

Discriminator that names this node type in serialized JSON.

evaluate

evaluate(env: Mapping[str, float]) -> float

The product of both operands.

Source code in src/femforge/physical/expressions.py
def evaluate(self, env: Mapping[str, float]) -> float:
    """The product of both operands."""
    return self.left.evaluate(env) * self.right.evaluate(env)

Neg pydantic-model

Bases: ExprNode

The negation of a subexpression.

Fields:

op pydantic-field

op: Literal['neg'] = 'neg'

Discriminator that names this node type in serialized JSON.

operand pydantic-field

operand: 'Expr'

The negated subexpression.

evaluate

evaluate(env: Mapping[str, float]) -> float

The negated operand.

Source code in src/femforge/physical/expressions.py
def evaluate(self, env: Mapping[str, float]) -> float:
    """The negated operand."""
    return -self.operand.evaluate(env)

render

render(refs: Mapping[str, str]) -> str

The negation as (-operand).

Source code in src/femforge/physical/expressions.py
def render(self, refs: Mapping[str, str]) -> str:
    """The negation as ``(-operand)``."""
    return f"(-{self.operand.render(refs)})"

parameters

parameters() -> frozenset[str]

The operand's parameters.

Source code in src/femforge/physical/expressions.py
def parameters(self) -> frozenset[str]:
    """The operand's parameters."""
    return self.operand.parameters()

Param pydantic-model

Bases: ExprNode

A named parameter (e.g. temperature), bound at evaluation/render time.

Fields:

op pydantic-field

op: Literal['param'] = 'param'

Discriminator that names this node type in serialized JSON.

name pydantic-field

name: str

The parameter name that env and refs bind.

evaluate

evaluate(env: Mapping[str, float]) -> float

The value env binds to this parameter.

Source code in src/femforge/physical/expressions.py
def evaluate(self, env: Mapping[str, float]) -> float:
    """The value ``env`` binds to this parameter."""
    if self.name not in env:
        raise KeyError(f"expression parameter {self.name!r} not bound; supplied: {sorted(env)}")
    return env[self.name]

render

render(refs: Mapping[str, str]) -> str

The backend reference refs binds to this parameter.

Source code in src/femforge/physical/expressions.py
def render(self, refs: Mapping[str, str]) -> str:
    """The backend reference ``refs`` binds to this parameter."""
    if self.name not in refs:
        raise KeyError(f"expression parameter {self.name!r} has no backend reference; supplied: {sorted(refs)}")
    return refs[self.name]

parameters

parameters() -> frozenset[str]

This parameter's name.

Source code in src/femforge/physical/expressions.py
def parameters(self) -> frozenset[str]:
    """This parameter's name."""
    return frozenset({self.name})

Pow pydantic-model

Bases: ExprNode

One subexpression raised to the power of another.

Fields:

op pydantic-field

op: Literal['pow'] = 'pow'

Discriminator that names this node type in serialized JSON.

base pydantic-field

base: 'Expr'

The base.

exponent pydantic-field

exponent: 'Expr'

The exponent.

evaluate

evaluate(env: Mapping[str, float]) -> float

The base raised to the exponent.

Source code in src/femforge/physical/expressions.py
def evaluate(self, env: Mapping[str, float]) -> float:
    """The base raised to the exponent."""
    # math.pow keeps the operation float-typed (the ** operator's stub admits complex).
    return math.pow(self.base.evaluate(env), self.exponent.evaluate(env))

render

render(refs: Mapping[str, str]) -> str

The power as (base ^ exponent).

Source code in src/femforge/physical/expressions.py
def render(self, refs: Mapping[str, str]) -> str:
    """The power as ``(base ^ exponent)``."""
    # ^ is exponentiation in both MATC and Lua.
    return f"({self.base.render(refs)} ^ {self.exponent.render(refs)})"

parameters

parameters() -> frozenset[str]

The parameters of the base and the exponent.

Source code in src/femforge/physical/expressions.py
def parameters(self) -> frozenset[str]:
    """The parameters of the base and the exponent."""
    return self.base.parameters() | self.exponent.parameters()

Sub pydantic-model

Bases: _Binary

The difference of two subexpressions.

Fields:

  • left ('Expr')
  • right ('Expr')
  • op (Literal['sub'])
  • _symbol (str)

op pydantic-field

op: Literal['sub'] = 'sub'

Discriminator that names this node type in serialized JSON.

evaluate

evaluate(env: Mapping[str, float]) -> float

The left operand minus the right one.

Source code in src/femforge/physical/expressions.py
def evaluate(self, env: Mapping[str, float]) -> float:
    """The left operand minus the right one."""
    return self.left.evaluate(env) - self.right.evaluate(env)

BasePropertyFunction pydantic-model

Bases: BaseModel

Pydantic base implementing parameter checking once; subclasses declare parameters / result_dimensionality and implement _evaluate.

Extra keyword arguments are ignored, so an operating point may carry more quantities than a property needs.

Config:

  • arbitrary_types_allowed: True
  • use_attribute_docstrings: True

parameters property

parameters: Mapping[str, str]

Required parameter name to expected dimensionality, as a pint string.

result_dimensionality property

result_dimensionality: str

Dimensionality of the returned quantity.

Calibration pydantic-model

Bases: BasePropertyFunction

A property defined by sample points and interpolated between them (1-D only for now).

Point inputs are validated against param_dims at construction; out-of-range raises unless extrapolate=True.

Fields:

Validators:

  • _validate_points

kind pydantic-field

kind: Literal['calibration'] = 'calibration'

Discriminator that names this property kind in serialized JSON.

param_dims pydantic-field

param_dims: dict[str, str]

The single parameter name and its expected dimensionality, as a pint string.

points pydantic-field

The samples to interpolate between.

method pydantic-field

method: Literal['linear', 'nearest', 'cubic'] = 'linear'

Interpolation between samples: linear, nearest, or monotone cubic (PCHIP).

extrapolate pydantic-field

extrapolate: bool = False

Clamp to the end samples outside the calibrated range instead of raising.

parameters property

parameters: Mapping[str, str]

param_dims as the call schema.

result_dimensionality property

result_dimensionality: str

The dimensionality of the sample outputs.

CalibrationPoint pydantic-model

Bases: BaseModel

One measured sample: input quantities by name and the resulting output.

Config:

  • arbitrary_types_allowed: True
  • use_attribute_docstrings: True

Fields:

Validators:

inputs pydantic-field

inputs: dict[str, Quantity]

[?]: The input quantities, keyed by parameter name.

output pydantic-field

output: Quantity

[?]: The property value measured at inputs.

ClosedForm pydantic-model

Bases: BasePropertyFunction

A property defined by an explicit Python callable, pint-in / pint-out; the callable is code, not JSON-serializable.

Fields:

param_dims pydantic-field

param_dims: dict[str, str]

Parameter name to expected dimensionality, as a pint string.

result_dim pydantic-field

result_dim: str

The dimensionality the callable must return, as a pint string.

expression pydantic-field

expression: Callable[..., Quantity]

The callable, given the declared parameters as keyword quantities.

parameters property

parameters: Mapping[str, str]

param_dims as the call schema.

result_dimensionality property

result_dimensionality: str

The declared result_dim.

Expression pydantic-model

Bases: BasePropertyFunction

A property defined by a serializable typed expression AST (physical.expressions) — ClosedForm as data instead of code.

body computes the result's SI magnitude in result_units from parameters bound as SI-base magnitudes, matching Elmer's tx.

Fields:

Validators:

  • _validate_body_parameters

kind pydantic-field

kind: Literal['expression'] = 'expression'

Discriminator that names this property kind in serialized JSON.

param_dims pydantic-field

param_dims: dict[str, str]

Parameter name to expected dimensionality, as a pint string.

result_units pydantic-field

result_units: str

The pint units of the magnitude body computes.

body pydantic-field

body: Expr

The law over parameters bound as SI-base magnitudes.

parameters property

parameters: Mapping[str, str]

param_dims as the call schema.

result_dimensionality property

result_dimensionality: str

The dimensionality of result_units.

PropertyDimensionError

PropertyDimensionError(fn: 'PropertyFunction', name: str, expected: str, got: object)

Bases: PropertyError

A supplied parameter had the wrong dimensionality.

Parameters:

  • fn ('PropertyFunction') –

    The property that rejected the call.

  • name (str) –

    The offending parameter, or <result> for a closed form's return value.

  • expected (str) –

    The dimensionality the parameter needs.

  • got (object) –

    The dimensionality, or the type, that was supplied.

Source code in src/femforge/physical/property_functions.py
def __init__(self, fn: "PropertyFunction", name: str, expected: str, got: object) -> None:
    """Record which parameter of ``fn`` had the wrong dimensionality.

    Args:
        fn: The property that rejected the call.
        name: The offending parameter, or ``<result>`` for a closed form's return value.
        expected: The dimensionality the parameter needs.
        got: The dimensionality, or the type, that was supplied.
    """
    self.fn: PropertyFunction = fn
    self.name: str = name
    self.expected: str = expected
    self.got: object = got
    super().__init__(f"{type(fn).__name__} parameter {name!r} expected {expected or 'dimensionless'}, got {got}.")

PropertyError

Bases: Exception

Base class for every property-function error.

PropertyFunction

Bases: Protocol

A material property as a function of zero or more named pint quantities, returning a pint Quantity.

Methods:

  • __call__ –

    Evaluate. Raises a typed error if a required parameter is missing or has the wrong dimensionality.

Attributes:

  • parameters (Mapping[str, str]) –

    Required parameter name -> expected dimensionality (pint string); this IS the call schema.

  • result_dimensionality (str) –

    Dimensionality of the returned quantity, e.g. '[power]/[length]/[temperature]'.

parameters property

parameters: Mapping[str, str]

Required parameter name -> expected dimensionality (pint string); this IS the call schema.

result_dimensionality property

result_dimensionality: str

Dimensionality of the returned quantity, e.g. '[power]/[length]/[temperature]'.

__call__

__call__(**kwargs: Quantity) -> Quantity

Evaluate. Raises a typed error if a required parameter is missing or has the wrong dimensionality.

Source code in src/femforge/physical/property_functions.py
def __call__(self, **kwargs: Quantity) -> Quantity:
    """Evaluate. Raises a typed error if a required parameter is missing or has the wrong dimensionality."""
    ...

PropertyFunctionField

PropertyFunctionField(dimensionality: str)

Annotated metadata for a PropertyFunction constrained to dimensionality.

Coerces bare quantities/strings into Static, validates result dimensionality, and exports quantity_type's unit-annotated JSON schema.

Source code in src/femforge/physical/property_functions.py
def __init__(self, dimensionality: str) -> None:
    """Constrain the field to properties of ``dimensionality``."""
    self.dimensionality = dimensionality

PropertyParameterError

PropertyParameterError(fn: 'PropertyFunction', missing: set[str])

Bases: PropertyError

A required parameter was not supplied when evaluating a property.

Source code in src/femforge/physical/property_functions.py
def __init__(self, fn: "PropertyFunction", missing: set[str]) -> None:
    """Name the parameters of ``fn`` the call left out."""
    self.fn: PropertyFunction = fn
    self.missing: set[str] = set(missing)
    wanted: str = ", ".join(f"{name}=<{dim or 'dimensionless'}>" for name, dim in fn.parameters.items())
    names: str = ", ".join(sorted(self.missing))
    super().__init__(f"{type(fn).__name__} requires [{wanted}]; not supplied: {names}.")

PropertyRangeError

PropertyRangeError(fn: 'PropertyFunction', name: str, value: object, low: object, high: object)

Bases: PropertyError

A parameter fell outside the calibrated range and extrapolation is off.

Parameters:

  • fn ('PropertyFunction') –

    The calibration that rejected the call.

  • name (str) –

    The out-of-range parameter.

  • value (object) –

    The supplied value.

  • low (object) –

    The lowest calibrated value.

  • high (object) –

    The highest calibrated value.

Source code in src/femforge/physical/property_functions.py
def __init__(self, fn: "PropertyFunction", name: str, value: object, low: object, high: object) -> None:
    """Record a parameter of ``fn`` that fell outside the calibrated range.

    Args:
        fn: The calibration that rejected the call.
        name: The out-of-range parameter.
        value: The supplied value.
        low: The lowest calibrated value.
        high: The highest calibrated value.
    """
    self.fn: PropertyFunction = fn
    self.name: str = name
    super().__init__(f"{type(fn).__name__} parameter {name!r}={value} is outside the calibrated range [{low}, {high}]; set extrapolate=True to allow.")

Static pydantic-model

Bases: BasePropertyFunction

A constant property: zero parameters, always returns value.

Authored as Static(value="8.7 W/(m*K)"); ignores any operating-point arguments.

Fields:

Validators:

  • _coerce_value → value

value pydantic-field

value: Quantity

[?]: The constant returned at every operating point.

parameters property

parameters: Mapping[str, str]

No parameters: a constant ignores the operating point.

result_dimensionality property

result_dimensionality: str

The dimensionality of value.

Quantity

Bases: PlainQuantity[float]

A pint quantity with a float magnitude.

Methods:

  • to –

    This quantity converted to the units other names.

to

to(other: object = None, *contexts: object, **ctx_kwargs: object) -> Quantity

This quantity converted to the units other names.

Parameters:

  • other (object, default: None ) –

    The target units, as a unit, a quantity or a unit string.

  • contexts (object, default: () ) –

    The pint contexts, by name or object, that enable the conversion.

  • ctx_kwargs (object, default: {} ) –

    The parameters those contexts take.

Source code in src/femforge/physical/units.py
def to(self, other: object = None, *contexts: object, **ctx_kwargs: object) -> Quantity:
    """This quantity converted to the units ``other`` names.

    Args:
        other: The target units, as a unit, a quantity or a unit string.
        contexts: The pint contexts, by name or object, that enable the conversion.
        ctx_kwargs: The parameters those contexts take.
    """
    ...

Unit

Bases: PlainUnit

A pint unit, typed so that arithmetic with units and numbers keeps its type.

VectorQuantity

Bases: PlainQuantity[Vec3]

A pint quantity with a Vec3 magnitude.

Methods:

  • to –

    This vector quantity converted to the units other names.

to

to(other: object = None, *contexts: object, **ctx_kwargs: object) -> VectorQuantity

This vector quantity converted to the units other names.

Parameters:

  • other (object, default: None ) –

    The target units, as a unit, a quantity or a unit string.

  • contexts (object, default: () ) –

    The pint contexts, by name or object, that enable the conversion.

  • ctx_kwargs (object, default: {} ) –

    The parameters those contexts take.

Source code in src/femforge/physical/units.py
def to(self, other: object = None, *contexts: object, **ctx_kwargs: object) -> VectorQuantity:
    """This vector quantity converted to the units ``other`` names.

    Args:
        other: The target units, as a unit, a quantity or a unit string.
        contexts: The pint contexts, by name or object, that enable the conversion.
        ctx_kwargs: The parameters those contexts take.
    """
    ...

conditions_for

conditions_for(tags: Iterable[EntityTag], physics: Physics | Iterable[Physics], target: ConditionTarget) -> list[Condition]

The conditions on tags that belong to physics and attach to target.

Parameters:

Source code in src/femforge/physical/domain.py
def conditions_for(tags: Iterable["EntityTag"], physics: Physics | Iterable[Physics], target: ConditionTarget) -> list[Condition]:
    """The conditions on ``tags`` that belong to ``physics`` and attach to ``target``.

    Args:
        tags: The region overrides that match the region.
        physics: One [`Physics`][femforge.elmer.physics.Physics], or several, such as a coupled preset's constituents.
        target: Whether to collect body or boundary conditions.
    """
    wanted: frozenset[Physics] = frozenset({physics}) if isinstance(physics, Physics) else frozenset(physics)
    matching: list[Condition] = []
    for tag in tags:
        for condition in tag.conditions:
            if condition.physics in wanted and condition.target == target:
                matching.append(condition)
    return matching

as_expr

as_expr(value: 'ExprNode | float') -> 'Expr'

Wrap a bare number into Const; pass nodes through (the operator-composition coercion point).

Source code in src/femforge/physical/expressions.py
def as_expr(value: "ExprNode | float") -> "Expr":
    """Wrap a bare number into [`Const`][]; pass nodes through (the operator-composition coercion point)."""
    if isinstance(value, ExprNode):
        return value  # pyright: ignore[reportReturnType]  # every concrete node is a union member; ExprNode itself is never instantiated
    return Const(value=float(value))

evaluate

evaluate(expr: 'Expr', env: Mapping[str, float]) -> float

Module-level convenience mirroring expr.evaluate.

Source code in src/femforge/physical/expressions.py
def evaluate(expr: "Expr", env: Mapping[str, float]) -> float:
    """Module-level convenience mirroring ``expr.evaluate``."""
    return expr.evaluate(env)

render

render(expr: 'Expr', *, refs: Mapping[str, str]) -> str

Backend source for expr with parameter references substituted (e.g. {"temperature": "tx(0)"}).

Source code in src/femforge/physical/expressions.py
def render(expr: "Expr", *, refs: Mapping[str, str]) -> str:
    """Backend source for ``expr`` with parameter references substituted (e.g. ``{"temperature": "tx(0)"}``)."""
    return expr.render(refs)

property_function_type

property_function_type(dimensionality: str) -> object

A PropertyFunction constrained to dimensionality, usable as a Pydantic field.

Source code in src/femforge/physical/property_functions.py
def property_function_type(dimensionality: str) -> object:
    """A [`PropertyFunction`][] constrained to ``dimensionality``, usable as a Pydantic field."""
    return Annotated[PropertyFunction, PropertyFunctionField(dimensionality)]

quantity

quantity(value: float, units: Unit | str | None = None) -> Quantity
quantity(value: Vec3, units: Unit | str | None = None) -> VectorQuantity
quantity(value: float | Vec3, units: Unit | str | None = None) -> Quantity | VectorQuantity

Statically-typed face of U.Quantity(...): a float magnitude yields Quantity, a Vec3 yields VectorQuantity.

Prefer a string for composite units ("newton * meter").

Source code in src/femforge/physical/units.py
def quantity(value: float | Vec3, units: Unit | str | None = None) -> Quantity | VectorQuantity:
    """
    Statically-typed face of ``U.Quantity(...)``: a ``float`` magnitude yields [`Quantity`][], a Vec3 yields [`VectorQuantity`][].

    Prefer a string for composite units (``"newton * meter"``).
    """
    return cast("Quantity | VectorQuantity", U.Quantity(value, units))  # pyright: ignore[reportCallIssue, reportArgumentType]