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 ``temperatureField`` file in ``0/`` 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 ----------------------- .. mermaid:: flowchart LR CASE[("Case directory
system/
constant/
0/")] REGISTRY[["Plugin registry
(@PluginSystem.register)"]] CORE[["Core specs
(register_core_models)"]] CASE -->|"per-model
@spec.detect"| PLUGIN["Plugin path
optional models"] CASE -->|"detect_and_create()
reads file once"| CORE_PATH["Core-spec path
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()``. The solver discovers them at startup with ``.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 ``@detect`` predicates 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 ~~~~~~~~~~~~~ .. mermaid:: flowchart TD START["I want to register a model"] Q1{"Is it mandatory
(some variant
must run)?"} Q2{"Do third parties
need to ship
their own?"} CORE["Core spec
register_core_models([...])"] PLUGIN["Plugin registry
@PluginSystem.register +
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 ``@detect`` 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 ``@detect`` has 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 a ``temperatureField`` in ``0/``; the solver does not know it exists until ``detect_models`` finds it. - ``PressureVelocityAlgorithm`` — core-spec path. The solver registers ``PIMPLE``, ``SIMPLE``, and ``PISO`` specs at construction; LOAD picks one by reading ``system/fvSolution``.