# Conditions and physics

A Physics member picks what the solver backend, Elmer today, solves, and conditions supply the sources and constraints of that solve. Each condition knows its physics and whether it attaches to a body or a boundary, and it rides on an EntityTag that selects bodies by their tags. One domain can carry conditions for several physics: a solve reads the conditions of its own physics and leaves the rest inert.

## Drive a magnet and measure its force

```python
import femforge as ff

magnet = ff.EntityTag(
    selector={ff.TagKey.NAME: "Magnet"},
    conditions=[
        ff.Magnetization(direction=ff.Direction(0, 0, 1)),
        ff.ForceComponent(
            label="MagnetForce",
            calculate_magnetic_torque=True,
            torque_origin=ff.Vec3(0, 0, 0),
            torque_axis=ff.Direction(0, 0, 1),
        ),
    ],
)

config = ff.SimulationConfig(
    mesh=ff.MeshConfig(step="magnet_in_air.step"),
    domain=ff.DomainConfig(materials=[ff.materials.Air(), ff.materials.N52()], tags=[magnet]),
    physics=ff.PhysicsSelection(selected=ff.Physics.MAGNETOSTATICS),
)
```

The STEP holds an air box and a body named `Magnet` tagged `Material=N52`. The `Magnetization` sets the magnet's direction, and the N52 remanence sets its strength. The `ForceComponent` lumps the magnetic force and torque on the magnet into one Elmer Component named `MagnetForce`. The result reads them back as `result.component_forces["MagnetForce"]` and `result.component_torques["MagnetForce"]`. See [Results](<https://docs.femforge.io/user-guide/results/index.md>).

## Physics

| Member | Solves | Preset solver stack | Frames |
| --- | --- | --- | --- |
| `MAGNETOSTATICS` | The static magnetic field of magnets and steel. | `MagnetoDynamicsSolver`, `MagnetoDynamicsCalcFieldsSolver`, `SaveScalarsSolver`, `ResultOutputSolver` | `Solid`; `Axisymmetric` as `MAGNETOSTATICS_2D` |
| `MAGNETOSTATICS_2D` | Axisymmetric magnetostatics on the r-z half-plane, with `CurrentDensity` coil drives. | `MagnetoDynamics2DSolver`, `MagnetoDynamicsCalcFieldsSolver`, `SaveScalarsSolver`, `ResultOutputSolver` | `Axisymmetric` only |
| `THERMAL` | Steady-state heat conduction. | `HeatSolver`, `SaveScalarsSolver` (peak temperature), `ResultOutputSolver` | `Solid`, `Axisymmetric` |
| `ELECTROSTATICS` | The electrostatic potential and field. | `ElectrostaticsSolver`, `ResultOutputSolver` | `Solid` |
| `CURRENT_CONDUCTION` | The static current density driven by electrode potentials. | `StatCurrentSolver`, `SaveScalarsSolver`, `ResultOutputSolver` | `Solid` |
| `JOULE_HEATING` | Current conduction coupled to heat conduction: the temperature its Joule losses produce. | `StatCurrentSolver`, `HeatSolver`, `SaveScalarsSolver`, `ResultOutputSolver` | `Solid` |
| `COIL_MAGNETOSTATICS` | Current conduction chained into magnetostatics: the field of a coil's solved current distribution. | `CoilCurrentSolver`, `MagnetoDynamicsSolver`, `MagnetoDynamicsCalcFieldsSolver`, `SaveScalarsSolver`, `ResultOutputSolver` | `Solid` |
| `LINEAR_ELASTICITY` | Linear elastic stress and strain. | `StressSolver`, `ResultOutputSolver` | `Solid` |

`LINEAR_ELASTICITY` is a scaffold: no condition targets it yet, and the material model has no Poisson ratio.

Select a physics with PhysicsSelection. You never select `MAGNETOSTATICS_2D` yourself: select `MAGNETOSTATICS` with `frame=ff.Axisymmetric()` and the config solves it as `MAGNETOSTATICS_2D`. A physics with no form in the frame raises when you build the config. See [Frames](<https://docs.femforge.io/user-guide/frames/index.md>).

## Conditions

| Class | Physics | Target | What it sets |
| --- | --- | --- | --- |
| Magnetization | `MAGNETOSTATICS` | body | The magnetization direction. The magnitude B\_r/μ₀ comes from the material. |
| CurrentDensity | `MAGNETOSTATICS_2D` | body | `value` \[A/m²\]: a uniform signed azimuthal current density, the homogenized drive of a stranded coil. |
| FixedTemperature | `THERMAL` | boundary | `value` \[K\]: a fixed temperature. |
| HeatFlux | `THERMAL` | boundary | `value` \[W/m²\]: a heat flux through the surface. |
| Convection | `THERMAL` | outer shell | `coefficient` \[W/(m²·K)\] and `ext_temperature` \[K\]: Newton cooling to an ambient. |
| HeatSource | `THERMAL` | body | `power_density` \[W/m³\]. With `temperature_coefficient` and `reference_temperature`, the source scales linearly with the local temperature. |
| FixedPotential | `CURRENT_CONDUCTION` | boundary | `value` \[V\]: an electrode potential. The run also integrates the electrode current. |
| JouleHeat | `JOULE_HEATING` | body | Heats the body with its solved Joule losses. It takes no value. |
| CoilDrive | `COIL_MAGNETOSTATICS` | body | `current` \[A\]: the total current of a massive coil. Bodies that share one `CoilDrive` instance form one coil. |
| CoilTerminal | `COIL_MAGNETOSTATICS` | outer shell | `role`: the coil's `start` or `end` electrode. Positive current flows from start to end. |
| ForceComponent | `MAGNETOSTATICS` | body | Lumps the magnetic force, and optionally the torque about `torque_origin` and `torque_axis`, over the matched bodies. |

Every value is a pint quantity of the dimensionality in brackets. Build it with `ff.quantity(300, "kelvin")` or `300 * ff.U.K`. See [Materials](<https://docs.femforge.io/user-guide/materials/#units>).

Reverse a coil by swapping its terminal roles, and keep `current` positive.

## Where conditions land

A body condition applies to every body its tag matches:

- `Magnetization` goes into the magnet's own Material block, where the magnetic solver reads it.
- `CoilDrive` and `ForceComponent` become Elmer Components. One condition instance gathers every body it matches into one Component.
- The rest become one Body Force block per body.

You never tag a surface. A boundary condition on a body's tag lands on every boundary group that body borders, both its interfaces with other bodies and its outer shell. `Convection` and `CoilTerminal` land on the outer shell only (`<BODY>__EXT`): a film coefficient on an interior interface would bypass the conduction path, and a coil terminal is the electrode's exposed end face. When several conditions reach one surface, they merge into one boundary block.

For example, with a copper body named `Trace` on an FR4 body named `Board`:

| The `Board` tag carries | Lands on `BOARD__TRACE` | Lands on `BOARD__EXT` |
| --- | --- | --- |
| `FixedTemperature` | yes | yes |
| `Convection` | no | yes |

Some conditions need no authoring:

- The magnetic physics pin the potential to zero on every outer shell, a far-field condition.
- An axisymmetric solve pins the potential to zero on the symmetry axis (`<BODY>__AXIS`), which is exact there.
- Every magnet body gets its own force Component, named after its body group. An authored `ForceComponent` that matches the magnet replaces it.

## Which conditions a physics reads

A coupled physics also reads the conditions of the physics it couples, so the same tags drive the standalone and the coupled solves.

| Solved physics | Reads the conditions of |
| --- | --- |
| `JOULE_HEATING` | `JOULE_HEATING`, `CURRENT_CONDUCTION`, `THERMAL` |
| `COIL_MAGNETOSTATICS` | `COIL_MAGNETOSTATICS`, `MAGNETOSTATICS` |
| `MAGNETOSTATICS_2D` | `MAGNETOSTATICS_2D`, `MAGNETOSTATICS` |
| Any other | Its own |

`COIL_MAGNETOSTATICS` leaves `FixedPotential` inert: it sources the coil from `CoilDrive` and `CoilTerminal`.

## Checks before the solve

The config rejects a condition its frame cannot solve as soon as you build or assign it:

- `CurrentDensity` needs `frame=ff.Axisymmetric()`.
- An axisymmetric `Magnetization` must point along the symmetry axis.
- An axisymmetric `ForceComponent` cannot ask for torque.

The sif writer checks the rest before it writes:

| Problem | Result |
| --- | --- |
| A material lacks a keyword its solvers need | Error |
| A magnet has no `Magnetization` with a direction | Error |
| `THERMAL`, `ELECTROSTATICS`, `CURRENT_CONDUCTION` or `JOULE_HEATING` resolves no boundary condition | Warning: the solve is likely ill-posed |
| `COIL_MAGNETOSTATICS` finds no `CoilDrive` | Warning: the coil has no source |
| A force Component exists but the stack cannot compute or export it | Warning |

## Customize the solver stack

Each physics has a preset: a typed PhysicsConfig that holds the ordered solvers, the Simulation settings, the constants and the operating point. `config.resolved_stack()` returns the stack the run solves. To change it, call customize\_physics:

```python
import femforge as ff

config = ff.SimulationConfig(
    mesh=ff.MeshConfig(step="magnet_in_air.step"),
    physics=ff.PhysicsSelection(selected=ff.Physics.MAGNETOSTATICS),
)

stack = config.customize_physics()
solver = stack.solver(ff.elmer.MagnetoDynamicsSolver)
solver.linear_system_max_iterations = 1000
stack.simulation.max_output_level = 3
```

`customize_physics()` copies the preset into `config.physics.overrides` once and returns it, so later calls edit the same stack. Every field is a typed sif keyword, validated when you assign it.

- The overrides are part of the config, so they enter the run identity and the manifest. Overrides equal to the preset leave the identity unchanged.
- Set the frame before you customize. A stack customized for one frame raises under the other, since the coordinate system differs.
- Every solver and the Simulation settings take `extra_keywords` for a sif keyword femforge has no field for. GenericSolver adds a whole solver block femforge has no model for.

The solver models and their keyword tables live in the [Elmer solvers API](<https://docs.femforge.io/reference/elmer-config/index.md>).

## Reference

- [Physical API](<https://docs.femforge.io/reference/physical/index.md>): the conditions, `EntityTag` and `DomainConfig`.
- [Elmer API](<https://docs.femforge.io/reference/elmer/index.md>): `Physics` and the backends.
- [Elmer solvers API](<https://docs.femforge.io/reference/elmer-config/index.md>): `PhysicsSelection`, `PhysicsConfig`, the presets and every solver.
