EffectModel contract#
Added in version v0.2: The mime.effects package ships the pilot of the EffectModel contract —
the Protocol surface, the registry, the Experiment composition + validation,
and the HydrodynamicModel family. The MagneticModel family and
cross-effect coupling implementation land in v0.3.
The EffectModel abstraction is a common builder pattern for environment
effects that deliver force/torque to a body. The hydrodynamic family (fluid
drag) is the first concrete instance; the magnetic family follows in v0.3,
and acoustic / electric / thermal slots are designed-in for post-1.0. The
load-bearing insight (from ADR-2026-EFFECT-MODEL) is that the fluid-node
contract was never really a fluid abstraction — it is a builder for
“a subgraph that emits force/torque to a body”, and fluids are just the first
case.
This page documents the v0.2 pilot. The full design rationale, the decision
log, and the post-1.0 extension path (Nelson-group eMNS / OctoMag coil arrays)
live in ADR-2026-EFFECT-MODEL.md.
Why#
A microrobotics experiment composes several physical effects on one body —
hydrodynamic drag, a magnetic moment in an applied field, possibly acoustic
streaming. Today each is wired by hand into a GraphManager with bespoke
edge helpers. The EffectModel contract makes effects composable and
swappable: attach a HydrodynamicModel.LBM and a (future)
MagneticModel.PointDipole to an Experiment, call build(), and get a
validated graph — with the option to swap the LBM backend for FVM, Stokeslet
or DefectCorrection across the same edges without touching the rest of the
graph.
Composability is a first-class v1.0 goal: the four-way hydrodynamic swap, the magnetic-model swap, and multi-effect composition on one body are part of the 1.0.0 definition-of-done.
Protocol surface#
mime.effects.protocol defines the contract (after six rounds of design
iteration in the ADR):
class EffectModel(Protocol):
@property
def coupling_ports(self) -> dict[str, EdgeSpec]: ... # cross-effect output ports
def applicable_regime(self) -> Regime: ... # advisory regime metadata
def required_body_properties(self) -> set[str]: ... # per-physics body sections
def required_medium_properties(self) -> set[str]: ... # medium properties
def build(self, gm, *, body, medium) -> EffectHandle: ... # materialise the subgraph
coupling_portsis an instance property, computable from constructor arguments alone —build()validation (pass 2) needs the EdgeSpecs before any subgraph exists. Empty for effects with no cross-coupling.build(gm, *, body, medium)materialises the effect’s nodes and edges onto theGraphManager. Called once perExperiment.build(); the graph is static for the lifetime of the run (no mid-run mutation).EffectHandleis a pure-introspection return value — the names of the nodes the effect added.
SourcedEffectModel[S] is the sub-protocol for effects with external sources
(magnetic dipoles, coils, acoustic transducers). S is a covariant
TypeVar bound to Source because it appears only in return position
(sources), so SourcedEffectModel[MagneticSource] is usable where
SourcedEffectModel[Source] is expected. The hydrodynamic pilot has no
sources; the type exists so the magnetic family slots in without a refactor.
BaseEffectModel is a convenience base supplying the no-op declarations
(empty coupling_ports, empty required-property sets) so concrete models
override only what they use.
Regimes#
Regime is a polymorphic ABC — each physics family ships its own subclass
with its own check(), and the family-agnostic caller just invokes
model.applicable_regime().check(...) (no central isinstance dispatch).
HydrodynamicRegime(re_range) emits a RegimeWarning when the medium’s
Reynolds number falls outside the validated creeping-flow range. Regimes
are advisory only — they surface warnings, never gate execution, because
cross-domain regime classifications are research-active and baking a gate in
would lock in a wrong classification.
Registry#
from mime.effects import register_effect, list_registered_effects, get_effect
@register_effect("HydrodynamicModel.LBM")
class LBM(_HydrodynamicEffect): ...
get_effect("HydrodynamicModel.LBM") # -> the class
list_registered_effects() # -> {name: class}
The registry is the single source of truth for YAML type: string
resolution, MICROROBOTICA’s effect-picker dropdowns, and serialisation
round-trips. A decorator (not importlib auto-discovery) keeps the surface
auditable and avoids arbitrary-code config files.
Body and Medium#
Body and Medium are fat dataclasses (not Protocols) — at v1.0 scale
(≤ 5 families, ≤ 25 fields total) the simpler approach wins, with a written
refactor tripwire in the ADR.
Body(name, node, properties)—nameis the body’s node name in the graph (effects wiredrag_force/drag_torque/magnetic_*edges into it);nodeis the body node itself (added to the graph once byExperiment.build);propertiesholds per-physics property sections, e.g.{"hydrodynamic": {...}}.Medium(properties)— the surrounding-fluid properties, e.g.{"density": 1060.0, "viscosity": 1.2e-3, "reynolds_number": 1e-3}.
Experiment composition and the six-pass build()#
Experiment formalises the existing directory convention and adds the effect
composition surface:
exp = Experiment(name="ar4_helical_drive",
mime_version_min="0.2.0")
exp.set_body(Body(name="rigid_body", node=rigid, properties={"hydrodynamic": {}}))
exp.set_medium(Medium(properties={"density": 1060.0, "viscosity": 1.2e-3}))
exp.attach(HydrodynamicModel.LBM(lbm_node, dx_physical=dx, dt_physical=dt))
gm, handles = exp.build()
build() runs six ordered passes, cheap checks first; each pass assumes
its predecessors succeeded, and each failure raises a specific typed error so
the validation promise is testable rather than aspirational:
# |
Pass |
On failure |
|---|---|---|
1 |
Effect-reference resolution — every |
|
2 |
Port existence + type compatibility — source port ∈ |
|
3 |
Body/Medium property presence — every required body section and medium property is present. |
|
4 |
Regime check — advisories collected onto |
(warnings only) |
5 |
Per-effect |
|
6 |
|
MADDENING errors pass through |
Load-time version validation#
The Experiment carries version-and-provenance metadata as concrete fields:
mime_version_min, mime_version_max, asset_paths, asset_hashes,
benchmark_refs, citation. Version compatibility is validated at
construction (load) time, before build() — a clean-install reproducer
gets a clear IncompatibleMimeVersionError (“your MIME is too old / too
new”) immediately, not after graph construction.
HydrodynamicModel family#
HydrodynamicModel is a namespace of four swappable backends, each adapting
an existing fluid node over the shared FLUID_NODE_CONTRACT.md drag
interface:
Backend |
Wraps |
Edges |
|---|---|---|
|
|
lattice→SI drag transforms ( |
|
|
SI ( |
|
|
SI generic edges (see below) |
|
|
SI generic edges |
Each is an adapter — it wraps a pre-constructed fluid node (backend
parameters belong on the node) and wires its drag_force / drag_torque
into the body. The LBM and Stokeslet backends use bespoke edge helpers; the
generic SI path (FVM / DefectCorrection) wires the forward drag edges plus
the body_* back-edges the node declares — it introspects the node’s
boundary_input_spec() and maps the body’s outputs (velocity →
body_velocity, angular_velocity → body_angular_velocity, etc.) onto only
the contract inputs that backend consumes. Because the fluid nodes emit the
contract names, swapping one backend for another across the same body edges
compiles clean — the interchangeability guarantee, exercised in
tests/effects/test_effect_model.py.
Concept proof — swap on free-space Stokes drag#
tests/verification/test_effectmodel_stokes_drag_swap.py runs the surface
end to end: a kinematic sphere is composed with a backend through
Experiment, driven at a prescribed velocity, and its drag read; swapping the
single attach() line runs a genuinely different solver across the identical
body/edges. The Stokeslet backend reproduces the analytical free-space
Stokes drag magnitude F = 6πμaV to ≈0.4%; the FVM backend — a full
Navier–Stokes + IBM solver — runs through the same swap and produces a finite
drag (confined sphere-in-a-pipe, order-of-magnitude at the pilot’s coarse 8³
grid). How far apart the backends are, why, and whether the gap closes with
resolution is recorded in
the drag-swap resolution note.
Two of the findings this exercise first surfaced are resolved in the pilot:
Graph-external inputs (was E6a). The body’s prescribed velocity is now injected through
Experiment.add_external_input(node, field, shape)+step()— theExperimentsurface composes external inputs (registered on theGraphManagerbeforecompile()), so noset_node_stateharness is needed. (Full FSI coupling-group composition is still v0.3.)Load-time version validation fires for an incompatible
mime_version_min.
A third finding is now resolved in the adapter (was E6g): the raw fluid
nodes do not share a drag sign convention — IBLBMFluidNode and the
standalone StokesletFluidNode report +R·(motion) (the reaction / force on
the fluid), while FVMFluidNode reports the force on the body (opposing).
Fed as-is to a body that adds the drag, the reaction sign is
anti-dissipative — a de-Boer UMR diverges when given IBLBM’s +R·ω with
the omega_max clamp removed. The HydrodynamicModel adapter therefore
normalizes every backend to the contract sign (force on the body) via a
per-backend native_drag_sign (IBLBM / Stokeslet flip, FVM unchanged), so a
swapped backend always delivers dissipative drag —
test_backends_deliver_force_on_body asserts the delivered drag opposes the
motion for both runnable backends. Remaining: node-level sign reconciliation
(so non-adapter consumers like the hand-built de Boer graph don’t lean on the
clamp) and confirming DefectCorrection’s sign on first real use.
What’s deferred to v0.3#
Per the ADR scope boundaries, the v0.2 pilot deliberately excludes:
MagneticModelfamily —PointDipole/UniformFieldrefactor of the existing magnetic chain (perMAGNETIC_NODE_AUDIT.md).SourceInputProvider—ConstantInput | NodeFieldRef | ExternalInputRefcarried on each source for its required external inputs (pose, coil current, …).Cross-effect coupling implementation — the ports are designed-in, but the additive operator-splitting / strong-coupling implementation behind them is v0.3 (the realistic cross-coupling cases — acoustic streaming, MHD — are post-1.0).
Full
make_experiment(params) -> Experimentmigration of the de Boer and de Jongh experiments — those experiments’ magnetic actuation chains are not yet EffectModels, so they cannot be expressed purely through the pilot.Acoustic / electric / thermal-radiation model slots — designed-in, unimplemented (acoustic contingent on MADDENING multi-rate verification).
References#
ADR-2026-EFFECT-MODEL.md— the accepted design, decision log, and post-1.0 extension path.MAGNETIC_NODE_AUDIT.md— the magnetic-family audit feeding v0.3.src/mime/nodes/environment/FLUID_NODE_CONTRACT.md— the shared fluid-node interface theHydrodynamicModelbackends build on.