Model discovery: plugin vs core spec¶
incompressibleFluid picks up models in two different ways. They
co-exist and serve distinct purposes; this page explains the design
rationale and when to use which.
What makes this hard¶
OpenFOAM has effectively one mechanism for “what model is active”:
read a string from a dictionary, look it up in a runtime-selection
table, instantiate. Selection, instantiation, and configuration are
all collapsed into the same addToRunTimeSelectionTable call.
That is fine for a model whose decision rule is “the user wrote a name in a dictionary.” It does not fit cleanly when the decision rule is either of:
“this model is optional, and its presence is signalled by the case layout” (e.g. a
temperatureFieldfile in0/enables Boussinesq).“exactly one of the following must run, and the choice depends on a case file the solver has already read” (e.g. PIMPLE vs SIMPLE vs PISO comes from
system/fvSolution).
NeoFOAM separates is this model active? (case-driven) from how do I instantiate it? (Python type), and each axis warrants its own mechanism.
The two discovery paths¶
flowchart LR
CASE[("Case directory<br/>system/<br/>constant/<br/>0/")]
REGISTRY[["Plugin registry<br/>(@PluginSystem.register)"]]
CORE[["Core specs<br/>(register_core_models)"]]
CASE -->|"per-model<br/>@spec.detect"| PLUGIN["Plugin path<br/>optional models"]
CASE -->|"detect_and_create()<br/>reads file once"| CORE_PATH["Core-spec path<br/>required component"]
REGISTRY --> PLUGIN
CORE --> CORE_PATH
PLUGIN -->|"appended to"| OPTIONAL["LoadResult.optional_models"]
CORE_PATH -->|"added to"| COREMODELS["LoadResult.core_models"]
style CASE fill:#E3F2FD
style REGISTRY fill:#FFF3E0
style CORE fill:#FFF3E0
style PLUGIN fill:#E8F5E9
style CORE_PATH fill:#E8F5E9
style OPTIONAL fill:#F3E5F5
style COREMODELS fill:#F3E5F5
1. Plugin registry — for optional, externally-pluggable physics.
Models register themselves with a base class via
@PluginSystem.register plus
Model("...").register_with(<interface>). The solver discovers them
at startup with <interface>.detect_models(case_dir=...); each
model’s own @spec.detect decides whether it should activate.
This is the right mechanism when:
The solver works fine without the model.
The decision to enable the model belongs to the case (file presence, dictionary key, etc.) rather than to a hard-coded list.
Third-party packages might want to ship their own implementations.
In incompressibleFluid, optional physics like boussinesq and
spalart_allmaras use this mechanism. The call site is
incompressibleFluidModel.detect_models() in create_fields.py.
2. Core specs — for required components with case-driven variant selection.
The solver registers a known list of specs via
StagedInit.register_core_models([...]). At LOAD time, exactly one
of them is selected — typically by reading a case file directly — and
added to LoadResult.core_models. The solver pulls the chosen spec
out of state.core_models by index when building its execution
graph.
This is the right mechanism when:
The component is mandatory; some variant has to run.
The variants are known at solver authoring time (PIMPLE, SIMPLE, PISO).
Selection depends on a case file that must be read by something authoritative, not by N independent
@detectpredicates racing each other.
In incompressibleFluid, the pressure-velocity algorithm uses this
mechanism. PressureVelocityAlgorithm.detect_and_create() reads
system/fvSolution exactly once and returns the right spec; the
execution_graph step then pulls it out of
self.state.core_models[0].
Decision tree¶
flowchart TD
START["I want to register a model"]
Q1{"Is it mandatory<br/>(some variant<br/>must run)?"}
Q2{"Do third parties<br/>need to ship<br/>their own?"}
CORE["Core spec<br/>register_core_models([...])"]
PLUGIN["Plugin registry<br/>@PluginSystem.register +<br/>register_with(...)"]
START --> Q1
Q1 -->|"yes"| CORE
Q1 -->|"no"| Q2
Q2 -->|"yes"| PLUGIN
Q2 -->|"no"| PLUGIN
style CORE fill:#E8F5E9
style PLUGIN fill:#E8F5E9
If the answer to the first question is yes, you need a core spec — plugins cannot enforce “exactly one of N must activate.” Otherwise, a plugin is the right tool whether or not third parties are involved.
A third path exists¶
The framework also supports a YAML manifest (models.yaml lists
type + name + inline config per entry; loaded via
load_manifest(manifest_path, case_dir, registry_name)). It is the
right tool when the same model type appears multiple times with
different parameters (e.g. several heat sources).
incompressibleFluid does not use it; the manifest path is
exercised in test/framework/integration/ and is the natural
mechanism for future multi-source / multi-zone solvers.
Trade-offs¶
The two paths trade off differently along two axes:
Axis |
Plugin |
Core specs |
|---|---|---|
Mandatory? |
No (opt-in) |
Yes (one of N) |
Selection |
Per-model |
Solver decides |
Pinning everything to one mechanism would compromise on at least one axis:
Plugin only: mandatory components have no clean way to express “exactly one of these must activate”; each
@detecthas to know about the others.Core specs only: third parties cannot ship optional physics without modifying the solver.
The cost of two mechanisms is exactly what it sounds like — it is a choice for the solver author, and that choice is part of the solver’s design. The mitigation is the decision tree above: required and case-decided → core specs; optional and case-detected → plugin.
When this matters in practice¶
The two mechanisms map to two concrete pieces of in-tree code:
boussinesq— plugin path. Activated by atemperatureFieldin0/; the solver does not know it exists untildetect_modelsfinds it.PressureVelocityAlgorithm— core-spec path. The solver registersPIMPLE,SIMPLE, andPISOspecs at construction; LOAD picks one by readingsystem/fvSolution.