Three-stage initialization ========================== The three-stage init pipeline exists to **populate the Context that the core abstraction reads from**. Operations described in :doc:`operations-and-the-dag` cannot run until ``ctx["fields.U"]``, ``ctx["models.turbulence"]``, and the rest are in place; they also cannot run until cross-model dependencies (for example Boussinesq flipping ``use_boussinesq=True`` on the pressure-velocity algorithm) are wired. LOAD / RESOLVE / BUILD is the pipeline that delivers both, in the right order, before the first operation fires. .. seealso:: For the lifecycle overview that places this page in context, see :doc:`solver-structure`. For what happens *after* BUILD finishes, see :doc:`operations-and-the-dag`. When a NeoFOAM solver starts, initialization is split into three explicit stages plus an implicit verification step: 1. **LOAD** — read configuration files, detect active models, instantiate ``ModelRuntime`` objects. No interaction between models. No mesh, no field allocation. 2. **RESOLVE** — wire inter-model dependencies through ``ConfigContext``; models adapt their configuration to what other models exist. 3. **VERIFY** *(implicit)* — check that every active operation's ``@fvSchemes.add`` / ``@fvSolution.add`` requirements are satisfied by the loaded ``system/fvSchemes`` / ``system/fvSolution``. Raises before any allocation. 4. **BUILD** — produce ``InitStep`` objects that describe how to create runtime objects (fields, operators, models). The framework topologically sorts them and executes them in dependency order, producing the ``Context``. .. mermaid:: sequenceDiagram autonumber participant Caller participant Init as StagedInit participant Files as Case files
(system/, constant/, 0/) participant Models as ModelRuntime(s) participant Cfg as ConfigContext participant Verify as Verifier participant Ctx as Context rect rgb(227, 242, 253) Note over Caller,Models: LOAD — read files, instantiate models in isolation Caller->>Init: run() / validate() Init->>Files: read fvSchemes, fvSolution,
turbulenceProperties, … Files-->>Init: raw config dicts Init->>Models: instantiate per active model Models-->>Init: loaded configs
(no cross-model interaction) end rect rgb(227, 242, 253) Note over Init,Cfg: RESOLVE — models adapt configs to peers Init->>Cfg: register loaded models by name Init->>Models: @resolve(config_context) Models->>Cfg: read peer configs,
mutate own (e.g. use_boussinesq=True) end rect rgb(255, 243, 224) Note over Init,Verify: VERIFY (implicit) — fail fast before BUILD Init->>Verify: collect @fvSchemes.add /
@fvSolution.add requirements Verify->>Files: cross-check against
system/fvSchemes, system/fvSolution Verify-->>Init: list[VerificationError] alt validate() only Init-->>Caller: errors (skip BUILD) end end rect rgb(232, 245, 233) Note over Init,Ctx: BUILD — produce InitSteps and execute them Init->>Models: @build → list[InitStep] Init->>Init: topological_sort(init_steps) Init->>Ctx: execute steps
(allocate mesh, fields, operators) Ctx-->>Caller: Context (solver-ready) end What makes this hard -------------------- OpenFOAM's solver ``main()`` reads the dictionary, allocates the mesh, and constructs models in one pass — validation means running the solver. That works when the only consumers are "the solver" and "the case author who is about to run the solver." Three forces push against the single-pass design once the surface goes Python: - **Cross-model dependencies are order-sensitive.** The Boussinesq model needs to flip ``use_boussinesq=True`` on whichever pressure-velocity algorithm was selected — but only *after* the algorithm is loaded and *before* either of them allocates fields. With a single init step, the ordering rules end up encoded inside whichever model happens to run last, and adding a new model means revisiting that ordering. - **Field allocation cycles.** ``p_rgh`` (Boussinesq) depends on ``mesh`` and ``p`` and ``rhok``; ``rhok`` depends on ``T``; ``T`` depends on ``mesh``. The dependency graph is fine, but it has to be *expressed* somewhere, and "construct everything in the right order" buries that expression inside imperative code. - **Validation that needs the resolved config but not the allocated fields.** A user wants to know "does my case satisfy the solver's requirements?" without paying the cost of mesh I/O. That implies LOAD + RESOLVE need to be runnable independently of BUILD — exactly what OpenFOAM's "validate by running" pattern cannot offer. Mechanism: what each stage cannot do ------------------------------------ The constraints below are easy to break by accident, and the framework relies on them holding: .. list-table:: :header-rows: 1 :widths: 15 35 50 * - Stage - Forbidden - Why it matters * - **LOAD** - Depending on other models' loaded state - Each ``@load`` runs in isolation; the framework provides no guaranteed ordering between models. Cross-model wiring belongs in RESOLVE. * - **RESOLVE** - Allocating fields or touching the mesh - No mesh exists yet, no ``InitStep`` has run. RESOLVE is config-only — it edits ``ConfigContext`` and returns. * - **BUILD** - Reading configuration files - It receives the resolved config from RESOLVE; reading files at this point bypasses verification and breaks the schema-introspection contract. Hooked into ``incompressibleFluid`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The three stages are not framework abstractions floating in the abstract — they are exactly the three decorators in ``src/neofoam/solver/incompressibleFluid/create_fields.py``: .. list-table:: :header-rows: 1 :widths: 25 30 45 * - Decorator in ``create_fields.py`` - Stage - What ``incompressibleFluid`` does * - ``@init.load`` - LOAD - ``PressureVelocityAlgorithm.detect_and_create()`` reads ``system/fvSolution`` to pick PIMPLE / SIMPLE / PISO; ``incompressibleFluidModel.detect_models()`` walks the plugin registry; ``FvSchemesConfig`` / ``FvSolutionConfig`` are loaded for verification. Returns a ``LoadResult``. * - ``@init.resolve`` - RESOLVE - Each loaded model's ``run_resolve(config_context)`` is called. This is where Boussinesq flips ``use_boussinesq=True`` on the pressure-velocity algorithm, for example. * - ``@init.build`` - BUILD - ``InitializerBuilder`` produces ``InitStep``\ s for the time/ mesh, the pressure-velocity algorithm, ``laminarTransport``, ``turbulence``, and the optional models. The framework topologically sorts the steps and runs them, populating ``Context.fields`` and ``Context.models``. Reading those three decorators side-by-side is the easiest way to see the lifecycle on real code; everything else on this page is the rationale for the split. Trade-offs: why three, not two or four -------------------------------------- **Why not two (LOAD + BUILD)?** Without RESOLVE, every cross-model dependency has to fit inside one of the two stages. LOAD cannot contain it (a model cannot see configs that other models have not loaded yet); BUILD cannot contain it cleanly (by then field allocations are already happening). Splitting LOAD and BUILD with a RESOLVE step in the middle is what gives "configs influence other configs" a place to live. **Why not four (separate VERIFY)?** Verification is a check, not an action — it does not produce state, it just rejects bad input. Hiding it as an automatic step between RESOLVE and BUILD means the user-facing surface stays at three stages while the framework still gets to fail fast. The honest cost is that solver authors and model authors have to know which stage their code lives in, and respect the constraints. A single constructor is harder to misuse and harder to extend; three stages is easier to extend and easier to misuse. The framework's lint and tests are what keep the constraints honest. When this matters in practice ----------------------------- With the constraints above in place, three user-visible capabilities become cheap: - ``StagedInit.validate()`` runs LOAD + RESOLVE + VERIFY without BUILD. Case validation costs file I/O plus config wiring — no mesh, no fields. - ``StagedInit.solver_inputs()`` introspects every config class without running anything. See :doc:`schema-introspection`. - ``StagedInit.scheme_inputs()`` introspects every required scheme without parsing a case. In a single-stage init, each of those would require parsing the entire pipeline. In a two-stage init, only the first would be possible.