diff --git a/CHANGELOG.md b/CHANGELOG.md index de49dbe01..b9e1ebb5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning ### Changed +- Canonical authoring now keeps one explicit projection/construction route: use + `pops.physics.Model.lower()` for advanced Module inspection and + `MomentModel.build()` for recorded moment specifications. The duplicate facade aliases were + removed instead of deprecated. +- Private uniform and AMR Python runtime wrappers no longer expose `add_block`; native-brick and + compiled-package installation share the existing type-dispatched `add_equation` seam used below + `pops.bind`, while public authoring remains `Case.block(...)`. - Strict AMR checkpoint payload v7 now persists the accepted shared-interface flux audit together with Program clocks, histories, tagging state, conservative ledger and synchronization report. Restart validates every fragment's topology epoch, level pair, exact clock window, resolved diff --git a/docs/CODE_DOCUMENTATION_CONVENTION.md b/docs/CODE_DOCUMENTATION_CONVENTION.md index 8274e7205..af5680d64 100644 --- a/docs/CODE_DOCUMENTATION_CONVENTION.md +++ b/docs/CODE_DOCUMENTATION_CONVENTION.md @@ -118,7 +118,7 @@ Prefer: |---|---|---| | Folder/file | Architectural role, boundaries | `runtime/System` orchestrates, does not contain the physics formulas. | | Class | Usage, contract, invariants, constraints | `AmrSystem` orchestrates a common AMR hierarchy. | -| Public method | User/API contract, `@param`, `@return`, `@throws` if useful | `add_block` validates resolved block metadata. | +| Public method | User/API contract, `@param`, `@return`, `@throws` if useful | `Case.block` validates resolved block metadata. | | Complex block | Why the order of operations matters | Poisson then aux then RHS. | | Line | Rare, only bug/trick | `local_size()==0` MPI guard. | diff --git a/docs/design/pybind-binding-audit.md b/docs/design/pybind-binding-audit.md index d9ad932c0..95ad9077f 100644 --- a/docs/design/pybind-binding-audit.md +++ b/docs/design/pybind-binding-audit.md @@ -1,7 +1,9 @@ # Pybind and native component boundary This note defines the final binding boundary. Python authoring never selects a native algorithm with -a string and never calls `System.add_block`. A typed descriptor contributes a versioned +a string, and the private Python runtime wrappers do not expose `System.add_block` or +`AmrSystem.add_block`; `pops.bind` uses their single type-dispatched `add_equation` seam. A typed +descriptor contributes a versioned `ComponentManifest`; resolution authenticates its small interfaces and produces an immutable route identity. Pybind materializes the already-resolved plan and does not reinterpret scientific intent. diff --git a/python/pops/codegen/program_codegen.py b/python/pops/codegen/program_codegen.py index 9851836c5..23ff7cc64 100644 --- a/python/pops/codegen/program_codegen.py +++ b/python/pops/codegen/program_codegen.py @@ -144,7 +144,8 @@ def emit_cpp_program( the blocks are first declared via ``T.state``). The .so also exports its block NAMES in that order (``pops_program_block_count`` / ``pops_program_block_name``); ``System::install_program`` binds them to the instantiated System blocks BY NAME (Spec 3 criterion 23, ADC-457), so the - System blocks (``sim.add_equation`` / ``sim.add_block``) may be added in ANY order -- a Program + System blocks (through the private ``sim.add_equation`` install seam) may be added in ANY + order -- a Program block whose name has no instantiated System block fails loud (``Program requires block instance '', but simulation did not instantiate it``). A block declared but never committed is a READ-ONLY block (allowed; e.g. a passive field whose charge couples the others through the shared diff --git a/python/pops/moments/hierarchy.py b/python/pops/moments/hierarchy.py index ecb158cdf..49e9df167 100644 --- a/python/pops/moments/hierarchy.py +++ b/python/pops/moments/hierarchy.py @@ -282,10 +282,6 @@ def build(self, name: Any = "moments", *, frame: Any = None) -> Any: self._apply_poisson(m, registered) return m - def check(self, name: Any = "moments") -> Any: - """Alias of :meth:`build` (build + the engine's own validation on construction).""" - return self.build(name) - # --- internals ---------------------------------------------------------- def _apply_poisson(self, m: Any, registered: dict[str, Any]) -> None: """Author ``-laplacian(phi) == eps * M00`` and its gradient outputs.""" diff --git a/python/pops/numerics/reconstruction/__init__.py b/python/pops/numerics/reconstruction/__init__.py index 9f2c881d1..3bbf14e47 100644 --- a/python/pops/numerics/reconstruction/__init__.py +++ b/python/pops/numerics/reconstruction/__init__.py @@ -124,7 +124,8 @@ def _weno5(name: str, epsilon: Any = None) -> Any: ``None`` (the default) keeps the native ``kWenoEpsilon`` literal -- the descriptor options are unchanged (omit-when-default) and the emitted stencil is bit-identical. A finite positive value - is carried in the descriptor options and threaded to the native ``Weno5::eps`` by ``add_block``. + is carried in the descriptor options and threaded to the native ``Weno5::eps`` by the + private ``add_equation`` installation seam. On AMR, descriptor availability is conditional on the resolved coarse/fine authority: it must certify order 5 and ghost depth 3. The builtin capability family selects its conservative order-5 route from that resolved requirement; an insufficient external provider is refused diff --git a/python/pops/physics/board.py b/python/pops/physics/board.py index 7246baf8d..b0732c0d1 100644 --- a/python/pops/physics/board.py +++ b/python/pops/physics/board.py @@ -1162,14 +1162,9 @@ def lower(self) -> Any: compiled = pops.compile(resolved) ``pops.compile`` captures the operator-first Module and validates ONCE internally; ``lower`` - (and its ``to_module`` alias) stay ADVANCED / inspection-only. Identical to :pyattr:`module`.""" + stays ADVANCED / inspection-only and is identical to :pyattr:`module`.""" return self.module - # Spec 5 sec.11 alias: physics.Model.to_module() == physics.Model.lower(). ADVANCED / inspection only - # (ADC-557): the standard case.block(model=m) -> pops.compile flow captures the Module itself; - # neither is REQUIRED (pops.compile does the lowering once, internally). - to_module = lower - # --- introspection --- # --- internals --- diff --git a/python/pops/runtime/_amr_system.py b/python/pops/runtime/_amr_system.py index 1e4c3319f..045d2d903 100644 --- a/python/pops/runtime/_amr_system.py +++ b/python/pops/runtime/_amr_system.py @@ -4,7 +4,7 @@ ``_amr_system_equation`` (add_equation + named-aux), ``_amr_system_io`` (private accepted-state codec and restore transaction), ``_amr_system_program`` (compiled time-Program install / params / transaction) and ``_amr_system_install`` (the ``pops.bind`` install seam + field-solver / aux helpers) -mixins; this module composes them and keeps the constructor plus native block/coupling glue. +mixins; this module composes them and keeps the constructor plus coupling glue. """ from __future__ import annotations @@ -15,19 +15,12 @@ from pops.runtime import _threading from pops.runtime._lifecycle import ( FROZEN_STRUCTURAL as _FROZEN_STRUCTURAL, + RETIRED_NATIVE_PASSTHROUGH as _RETIRED_NATIVE_PASSTHROUGH, freeze_error as _freeze_error, guard_assembling as _guard_assembling, _LifecycleMixin, ) from pops.runtime._numeric import native_real -from pops.runtime._engine_descriptors import Spatial, Explicit -from pops.runtime.defaults import ( - NEWTON_DEFAULT_ABS_TOL, - NEWTON_DEFAULT_DAMPING, - NEWTON_DEFAULT_FD_EPS, - NEWTON_DEFAULT_MAX_ITERS, - NEWTON_DEFAULT_REL_TOL, -) from pops.runtime._amr_system_equation import _AmrSystemEquation from pops.runtime._amr_system_install import _AmrSystemInstall from pops.runtime._amr_system_io import _AmrSystemIO @@ -294,94 +287,6 @@ def coarse_total_boxes(self) -> Any: """ return self._s.coarse_total_boxes() - def add_block(self, name: Any, model: Any, spatial: Any = None, time: Any = None) -> Any: - """Installs an evolved block composed of NATIVE BRICKS on the shared AMR hierarchy. - - Low-level runtime seam. The documented PUBLIC path is the typed ``pops.Case`` assembly - resolved with ``pops.resolve(case, layout=...)``, compiled with ``pops.compile(plan)`` and - wired by ``pops.bind`` (which calls this internally); ``add_block`` stays private. - - Refined counterpart of System.add_block. Every block count uses the same AmrRuntime engine; - subsequent blocks are co-located on the shared hierarchy and contribute to the summed - system-Poisson right-hand side. - In multi-block the name indexes set_density(name) / mass(name) / density(name). The arguments - are marshaled to the C++ facade (AmrSystem::add_block), which validates the block against the model. - For a compiled DSL model (.so) or a dispatch on the model type, use add_equation. - - @param name unique name of the block. - @param model private ``ModelSpec`` engine value composed from native bricks. - @param spatial private engine adapter lowered from ``pops.numerics.FiniteVolume(...)`` - (default minmod + rusanov + conservative). The native seam accepts limiter tokens - none / minmod / vanleer / weno5, Riemann fluxes rusanov / hll / hllc / roe, and - conservative / primitive variables. This low-level WENO5 stencil route is not an AMR - availability guarantee: a resolved Case also requires an owner-qualified coarse/fine - provider certified for order 5 and ghost depth 3. The native catalogue contains that - provider and resolves it from the reconstruction requirements; no lower-order - coarse/fine fallback is permitted. - @param time private engine policy. Public authoring uses an explicit ``pops.Program`` or a - ``pops.lib.time`` factory. The installed typed Program is the sole time authority. - Until the AMR target provides a typed local implicit primitive, non-empty partial masks, - non-default Newton controls, and Newton diagnostics fail closed. The spatial runtime - never stores them or manufactures an implicit step/report. - spatial.positivity_floor > 0 (ADC-259) floors the Density-role face states AND the - coarse-fine fine ghost means to >= floor on the AMR transport (Zhang-Shu, parity with the - uniform System). Guarantee = face / ghost-state Density positivity only (order-1 fallback), - NOT updated-mean nor pressure positivity. A model without a Density role rejects it at the - first step. The COMPILED .so path carries it too now (ADC-322): a loader regenerated against - the current headers marshals the floor (add_equation on a CompiledModel, add_native_block). - """ - _guard_assembling(self, "add_block") # frozen once pops.bind completes (ADC-592) - spatial = spatial if spatial is not None else Spatial() - time = time if time is not None else Explicit() - # positivity_floor (ADC-259) IS now wired on the AMR transport (Density-role face states + - # C/F fine ghost means). Threaded to AmrSystem::add_block below; the compiled .so path carries - # it too (ADC-322, regenerated loader). The C++ side rejects it on a model without a Density role. - spatial_options: dict[str, bool | float] = { - "wave_speed_cache": bool(getattr(spatial, "wave_speed_cache", False)), - } - if getattr(spatial, "weno_epsilon", None) is not None: - spatial_options["weno_epsilon"] = native_real( - spatial.weno_epsilon, where="AmrSystem.add_block.weno_epsilon" - ) - # Forward the complete authoring request to the native contract. Cadence remains meaningful - # to Program/CFL normalization; unsupported partial masks and non-default Newton requests - # fail closed there instead of becoming inert spatial-runtime state. - self._s.add_block( - name, - model, - spatial.limiter, - spatial.flux, - spatial.recon, - time.kind, - getattr(time, "substeps", 1), - getattr(time, "stride", 1), - getattr(time, "implicit_vars", []), - getattr(time, "implicit_roles", []), - getattr(time, "newton_max_iters", NEWTON_DEFAULT_MAX_ITERS), - native_real( - getattr(time, "newton_rel_tol", NEWTON_DEFAULT_REL_TOL), - where="AmrSystem.add_block.newton_rel_tol", - ), - native_real( - getattr(time, "newton_abs_tol", NEWTON_DEFAULT_ABS_TOL), - where="AmrSystem.add_block.newton_abs_tol", - ), - native_real( - getattr(time, "newton_fd_eps", NEWTON_DEFAULT_FD_EPS), - where="AmrSystem.add_block.newton_fd_eps", - ), - native_real( - getattr(time, "newton_damping", NEWTON_DEFAULT_DAMPING), - where="AmrSystem.add_block.newton_damping", - ), - getattr(time, "newton_diagnostics", False), - native_real( - getattr(spatial, "positivity_floor", 0.0), - where="AmrSystem.add_block.positivity_floor", - ), - **spatial_options, - ) - def field(self, name: Any) -> Any: """Return the solved potential of a NAMED elliptic field as a ``(ny, nx)`` array. @@ -496,6 +401,11 @@ def program_report(self) -> Any: return build_program_report(self) def __getattr__(self, attr: Any) -> Any: + if attr in _RETIRED_NATIVE_PASSTHROUGH: + raise AttributeError( + "AmrSystem.%s is not an authoring route; declare the block with " + "pops.Case.block(...)" % attr + ) # RUNTIME FREEZE (ADC-592): once bound, refuse a native STRUCTURAL setter reached through the # passthrough (install_program / ...) with the bind-vocabulary # RuntimeError, so the bypass is closed even under a prebuilt .so whose C++ setters are not yet diff --git a/python/pops/runtime/_amr_system_contract.py b/python/pops/runtime/_amr_system_contract.py index fe26a7caa..db24af70e 100644 --- a/python/pops/runtime/_amr_system_contract.py +++ b/python/pops/runtime/_amr_system_contract.py @@ -43,7 +43,6 @@ def set_history_persistence(self, *args: Any, **kwargs: Any) -> Any: ... def last_restart_regrid_receipt(self) -> Any: ... def add_equation(self, *args: Any, **kwargs: Any) -> Any: ... - def add_block(self, *args: Any, **kwargs: Any) -> Any: ... def set_poisson(self, *args: Any, **kwargs: Any) -> Any: ... def _set_poisson_native(self, *args: Any, **kwargs: Any) -> Any: ... def set_density(self, *args: Any, **kwargs: Any) -> Any: ... diff --git a/python/pops/runtime/_amr_system_equation.py b/python/pops/runtime/_amr_system_equation.py index 14ae748ef..dc8d7745f 100644 --- a/python/pops/runtime/_amr_system_equation.py +++ b/python/pops/runtime/_amr_system_equation.py @@ -98,10 +98,11 @@ def add_equation( Dispatch: - - a private ``ModelSpec`` -> add_block (native bricks composed on the hierarchy); + - a private ``ModelSpec`` -> the native ``AmrSystem::add_block`` ABI (bricks composed on + the hierarchy); - a CompiledModel(backend='production', target='amr_system') installs a package whose loader inlines add_compiled_model(AmrSystem&), so the block runs - the SAME AMR hierarchy as add_block (conservative reflux, regrid), ZERO-COPY. + the same AMR hierarchy as the native-brick ABI (conservative reflux, regrid), ZERO-COPY. The ``time`` value carried by a block is immutable Program-authoring metadata, not an executable method in the AMR spatial runtime. The compiled ``pops.Program`` installed after @@ -110,7 +111,7 @@ def add_equation( Newton controls, or diagnostics fails closed until a typed implicit Program primitive exists. It never reaches a private backward-Euler/Newton engine. ``recon="primitive"`` and fluxes ``roe`` / ``hllc`` use the same compiled spatial dispatch as - ``add_block``. The low-level dispatch also contains the WENO5-Z stencil and its three-cell + the native-brick branch. The low-level dispatch also contains the WENO5-Z stencil and its three-cell halo, but the resolved Case route accepts it only when the owner-qualified coarse/fine provider certifies order 5 and ghost depth 3. The native catalogue resolves that provider from the reconstruction requirements and never lowers the coarse/fine interface order @@ -118,7 +119,7 @@ def add_equation( MULTIRATE CADENCE (stride) and PARTIAL IMEX MASK (implicit_vars / implicit_roles): - - private ``ModelSpec`` path: FORWARDED to ``AmrSystem::add_block``. Cadence remains part of + - private ``ModelSpec`` path: forwarded to ``AmrSystem::add_block``. Cadence remains part of Program/CFL normalization; non-empty masks and non-default Newton requests fail closed until the AMR target exposes their typed Program primitive; - CompiledModel production path (.so): explicitly REJECTED (ValueError). The flat ABI of the @@ -150,7 +151,7 @@ def add_equation( where="AmrSystem.add_equation.substeps", ) - # --- ModelSpec: native bricks composed -> add_block (existing path) --- + # --- ModelSpec: native bricks composed through the sole Python dispatch seam --- # Forward the complete authoring request to the native contract. Unsupported masks and # Newton controls are rejected there rather than retained by the spatial runtime. if isinstance(model, ModelSpec): diff --git a/python/pops/runtime/_bricks_model.py b/python/pops/runtime/_bricks_model.py index 71addd4f8..7cceb5824 100644 --- a/python/pops/runtime/_bricks_model.py +++ b/python/pops/runtime/_bricks_model.py @@ -185,7 +185,8 @@ def Model(state: Any, transport: Any, source: Any, elliptic: Any) -> Any: Validates the state <-> transport consistency (Scalar with ExB; compressible FluidState with CompressibleFlux; isothermal with IsothermalFlux) and carries the parameters into the spec. - The returned ``ModelSpec`` is the BOUNDED LEGACY BRIDGE for the native ``add_block`` path (a + The returned ``ModelSpec`` is the bounded private bridge for the native-ABI branch of + ``add_equation`` (a flat C++ POD of brick tags + parameters); it is NOT the target representation. The target representation of a model is the operator-first ``pops.model.Module`` (compiled to a Problem) and its self-describing ``ModuleManifest`` (ADC-585). The POD remains an explicitly private diff --git a/python/pops/runtime/_bricks_scheme.py b/python/pops/runtime/_bricks_scheme.py index 7ee99202b..4c614db09 100644 --- a/python/pops/runtime/_bricks_scheme.py +++ b/python/pops/runtime/_bricks_scheme.py @@ -138,8 +138,8 @@ class Spatial: ``pops.numerics.reconstruction.FirstOrder()`` -> none, ``.limiters.Minmod()`` / ``.VanLeer()``, ``.WENO5()`` / ``.WENO5Z()`` -> weno5, ``.MUSCL(limiter=...)`` -> its limiter. weno5 = WENO5-Z, order 5 in smooth regions, 5-point stencil (3 ghosts), oscillation-free - capture near a front; only the native ``add_block`` path exposes it (the compiled .so paths - allocate 2 ghosts -> explicit rejection). + capture near a front; only the private native-``ModelSpec`` branch of ``add_equation`` + exposes it (the compiled .so paths allocate 2 ghosts -> explicit rejection). - ``flux``: a ``pops.numerics.riemann`` descriptor lowering to "rusanov" | "hll" | "hllc" | "roe". Rusanov() = minimal generic (requires only max_wave_speed, any model). diff --git a/python/pops/runtime/_lifecycle.py b/python/pops/runtime/_lifecycle.py index 03f3c3e40..b599b35f6 100644 --- a/python/pops/runtime/_lifecycle.py +++ b/python/pops/runtime/_lifecycle.py @@ -39,7 +39,7 @@ # structural after bind: only BindSchema may populate them. State/field/clock data remain mutable. FROZEN_STRUCTURAL = frozenset({ # blocks / field problems / aux LAYOUT - "add_block", "add_equation", "_install_native_block", + "add_equation", "_install_native_block", "set_poisson", "set_epsilon_field", "set_epsilon_anisotropic_field", "set_reaction_field", "set_aux_field_halo_component", "set_electron_temperature_from", "register_elliptic_field", "set_block_elliptic_field", "set_compiled_block", @@ -54,13 +54,18 @@ "set_program_params", }) +# Native facades still expose this ABI entry, but it is no longer a Python runtime-authoring +# spelling. Keep it out of ``__getattr__`` in both assembling and bound phases so deleting the +# duplicate mixin methods cannot accidentally reveal the C++ method as a compatibility fallback. +RETIRED_NATIVE_PASSTHROUGH = frozenset({"add_block"}) + def freeze_error(what: Any) -> Any: """The precise :class:`RuntimeError` for a structural mutation attempted after ``pops.bind``. @p what names the refused operation (a method / attribute name). The message speaks the BIND vocabulary and points at the assembly path (``pops.Case`` + ``pops.compile`` + ``pops.bind``); - it NEVER recommends a legacy setter as the remedy (no ``add_block`` / ``set_poisson`` / + it NEVER recommends a legacy setter as the remedy (no ``add_equation`` / ``set_poisson`` / ``install_program`` as an alternative), so it cannot be read as a validation bypass. """ @@ -75,7 +80,7 @@ def freeze_error(what: Any) -> Any: def guard_assembling(engine: Any, what: Any) -> Any: """Raise :func:`freeze_error` when @p engine is already bound (the Python-layer structural guard). - Called at the TOP of each Python-implemented structural method (add_block / add_equation / + Called at the TOP of each Python-implemented structural method (add_equation / set_poisson / set_disc_domain / _install_compiled / ...). Enforces the freeze at the Python layer WITHOUT the native ``mark_bound`` (bypass-proof on a prebuilt ``.so``): it reads the engine's ``_lifecycle`` flag, defaulting to ``assembling`` (so an engine constructed @@ -196,5 +201,11 @@ def last_restart_identity(self) -> Any: return getattr(self, "_last_restart_identity", None) -__all__ = ["FROZEN_STRUCTURAL", "freeze_error", "guard_assembling", "derive_lifecycle_state", - "_LifecycleMixin"] +__all__ = [ + "FROZEN_STRUCTURAL", + "RETIRED_NATIVE_PASSTHROUGH", + "freeze_error", + "guard_assembling", + "derive_lifecycle_state", + "_LifecycleMixin", +] diff --git a/python/pops/runtime/_system.py b/python/pops/runtime/_system.py index a9346686a..b5c529136 100644 --- a/python/pops/runtime/_system.py +++ b/python/pops/runtime/_system.py @@ -17,7 +17,11 @@ from pops._bootstrap import AmrSystemConfig # noqa: F401 (re-exported via this module) from pops.runtime import _threading from pops.runtime._lifecycle import ( - FROZEN_STRUCTURAL as _FROZEN_STRUCTURAL, freeze_error as _freeze_error, _LifecycleMixin) + FROZEN_STRUCTURAL as _FROZEN_STRUCTURAL, + RETIRED_NATIVE_PASSTHROUGH as _RETIRED_NATIVE_PASSTHROUGH, + freeze_error as _freeze_error, + _LifecycleMixin, +) from pops.runtime._amr_system import AmrSystem # noqa: F401 (re-exported via this module) from pops.runtime._system_aux_state import _SystemAuxState from pops.runtime._system_diagnostics import _SystemDiagnostics @@ -76,11 +80,11 @@ class System(_SystemInstall, _SystemUnifiedInstall, _SystemAuxState, Low-level runtime. The documented PUBLIC path is the typed ``pops.Case`` assembly lowered by ``pops.compile`` and wired by ``pops.bind`` -> ``pops.run(sim, ...)``; the per-step native methods - (and ``add_block`` / ``add_equation`` / ``set_poisson``) + (and ``add_equation`` / ``set_poisson``) are the low-level seam ``pops.bind`` builds on and the tests use, not the recommended front door. - ``add_block`` takes a private native ``ModelSpec`` plus private spatial and time adapters. + ``add_equation`` dispatches a private native ``ModelSpec`` or a compiled production package. Public authoring uses ``pops.Model`` through ``pops.Case``; discretization and reusable integration Programs live in ``pops.numerics`` and ``pops.lib.time`` respectively. Everything else (set_poisson, set_density, step, step_cfl, diagnostics, @@ -271,6 +275,11 @@ def __getattr__(self, attr: Any) -> Any: "with no AMR hierarchy. Declare layout=AMR(...) on the pops.Case for a refined run " "(its sim.amr returns an AmrRuntimeView), or pops.inspect(layout) for the " "static authoring report.") + if attr in _RETIRED_NATIVE_PASSTHROUGH: + raise AttributeError( + "System.%s is not an authoring route; declare the block with pops.Case.block(...)" + % attr + ) # RUNTIME FREEZE (ADC-592): once bound, refuse a native STRUCTURAL setter reached through the # passthrough (instance.install_program / ...) with the bind-vocabulary # RuntimeError -- NOT AttributeError -- so the bypass is closed even under a prebuilt .so whose diff --git a/python/pops/runtime/_system_contract.py b/python/pops/runtime/_system_contract.py index f62b9caa4..a3b1258cb 100644 --- a/python/pops/runtime/_system_contract.py +++ b/python/pops/runtime/_system_contract.py @@ -40,7 +40,6 @@ class _System: _execution_context: Any def add_equation(self, *args: Any, **kwargs: Any) -> Any: ... - def add_block(self, *args: Any, **kwargs: Any) -> Any: ... def set_poisson(self, *args: Any, **kwargs: Any) -> Any: ... def _set_poisson_native(self, *args: Any, **kwargs: Any) -> Any: ... def set_state(self, *args: Any, **kwargs: Any) -> Any: ... diff --git a/python/pops/runtime/_system_install.py b/python/pops/runtime/_system_install.py index b33ae6b92..5ee08e396 100644 --- a/python/pops/runtime/_system_install.py +++ b/python/pops/runtime/_system_install.py @@ -1,7 +1,7 @@ -"""System install mixin (Spec-4 PR-F): block/equation/coupling installation. +"""System install mixin (Spec-4 PR-F): equation/coupling installation. -Holds the densest part of :class:`pops.runtime._system.System`: ``add_block`` / -``add_equation`` (direct native versus compiled production-package installation), +Holds the densest part of :class:`pops.runtime._system.System`: ``add_equation`` +(direct native versus compiled production-package installation), ``add_background``, ``add_elliptic_model`` and ``add_coupling``. Mixed into ``System`` via inheritance; methods operate on ``self._s`` (the compiled facade) and ``self._aux_field_index``. """ @@ -42,60 +42,16 @@ class _SystemInstall(_System): - """Block/equation/coupling installation methods of System.""" - - def add_block(self, name: Any, model: Any, spatial: Any = None, time: Any = None, - evolve: bool = True) -> Any: - """Installs an evolved block composed of NATIVE BRICKS on the shared system Poisson. - - Low-level runtime seam. The documented PUBLIC path is the typed - ``pops.Case(...).block(...)`` assembly passed through ``pops.resolve`` / ``pops.compile`` - and wired by ``pops.bind`` (which calls this method internally); ``add_block`` stays for that seam, - the native/AMR runtime, and the tests. - - Installs a private ``ModelSpec`` composed from native bricks. Public ``pops.Model`` - authoring enters through ``pops.Case`` and the lifecycle. For a compiled production model - or automatic dispatch on the engine value type, use add_equation. Arguments reach the C++ facade - (System::add_block), which validates the block (names / roles / implicit mask) against the model. - - @param name unique block name; indexes set_density(name) / mass(name) / density(name). - @param model private ``ModelSpec`` engine value. - @param spatial private engine adapter lowered from ``pops.numerics.FiniteVolume(...)`` - (default minmod + rusanov + conservative). Carries the limiter (none / minmod / - vanleer / weno5 -- - weno5 is exposed ONLY by this native path), the Riemann flux (rusanov / hll / hllc / - roe) and the reconstructed variables (conservative / primitive). positivity_floor is read - here (Zhang-Shu positivity limiter). - @param time private engine policy. Public authoring uses an explicit ``pops.Program`` or a - ``pops.lib.time`` factory. The lowered policy carries cadence, any implicit mask and - local Newton options; these values are forwarded as-is to C++. - @param evolve True (default) = block advances; False = frozen field (background) which still - contributes to the right-hand side of the system Poisson. - """ - _guard_assembling(self, "add_block") # frozen once pops.bind completes (ADC-592) - spatial = spatial if spatial is not None else Spatial() - time = time if time is not None else Explicit() - # Native ABI conversion happens here; descriptors above this seam stay exact. - rel_tol, abs_tol, fd_eps, damping, positivity_floor = native_block_scalars( - time, spatial, where="System.add_block") - self._s.add_block(name, model, spatial.limiter, spatial.flux, spatial.recon, time.kind, - getattr(time, "substeps", 1), evolve, getattr(time, "stride", 1), - getattr(time, "implicit_vars", []), getattr(time, "implicit_roles", []), - getattr(time, "newton_max_iters", NEWTON_DEFAULT_MAX_ITERS), - rel_tol, abs_tol, fd_eps, - getattr(time, "newton_diagnostics", False), - damping, - positivity_floor, - getattr(spatial, "wave_speed_cache", False), **_weno_kwargs(spatial)) + """Equation/coupling installation methods of System.""" def add_equation(self, name: Any, model: Any, spatial: Any = None, time: Any = None, substeps: Any = None, names: Any = None, evolve: bool = True, stride: Any = None, _bind_params: Any = None) -> Any: """Install a native model or one compiled production package. - Low-level runtime seam. The documented PUBLIC path is the typed + Sole Python block-installation seam below ``pops.bind``. The documented PUBLIC path is the typed ``pops.Case(...).block(...)`` assembly passed through ``pops.resolve`` / ``pops.compile`` - and wired by ``pops.bind``; ``add_equation`` stays private to the native/AMR runtime. + and wired by ``pops.bind``; ``add_equation`` stays private to the native runtime. A ``ModelSpec`` uses the direct native brick path. A ``CompiledModel`` must be a production package; its complete resolved BindSchema vector is provided privately by @@ -267,9 +223,10 @@ def add_equation(self, name: Any, model: Any, spatial: Any = None, time: Any = N def add_background(self, name: Any, model: Any, density: Any, spatial: Any = None) -> Any: """FROZEN species (not advanced): a fixed background that contributes to the system Poisson (and, - later, to coupled sources). density: n*n array. Equivalent to add_block(evolve=False) then - set_density (freeze ADC-592 enforced by the delegated, guarded add_block).""" - self.add_block(name, model, spatial=spatial, evolve=False) + later, to coupled sources). density: n*n array. Uses the same type-dispatched + ``add_equation(evolve=False)`` installation seam as evolved blocks, then sets density. + """ + self.add_equation(name, model, spatial=spatial, evolve=False) self.set_density(name, density) def set_poisson(self, rhs: Any = "charge_density", solver: Any = None, diff --git a/python/pops/runtime/_system_install_lowering.py b/python/pops/runtime/_system_install_lowering.py index 5f9153726..ce78c1c7c 100644 --- a/python/pops/runtime/_system_install_lowering.py +++ b/python/pops/runtime/_system_install_lowering.py @@ -61,10 +61,10 @@ def _lower_bc(bc: Any) -> Any: def _weno_kwargs(spatial): """ADC-645: WENO5(epsilon=...) rides along the Spatial; None (the default) forwards NOTHING so - the native add_block keeps its kWenoEpsilon default (byte-identical historical call).""" + the native ABI keeps its kWenoEpsilon default (byte-identical historical call).""" weps = getattr(spatial, "weno_epsilon", None) return {} if weps is None else { - "weno_epsilon": native_real(weps, where="System.add_block.weno_epsilon")} + "weno_epsilon": native_real(weps, where="System.add_equation.weno_epsilon")} def _mg_kwargs(rel_tol, max_cycles, min_coarse, pre_smooth, post_smooth, bottom_sweeps, diff --git a/tests/python/architecture/test_final_public_api.py b/tests/python/architecture/test_final_public_api.py index 1d3e8257d..6026b000d 100644 --- a/tests/python/architecture/test_final_public_api.py +++ b/tests/python/architecture/test_final_public_api.py @@ -334,6 +334,7 @@ def test_physics_has_no_competing_model_facade() -> None: model = pops.Model("single_public_model") assert not hasattr(model, "dsl") assert not hasattr(model, "compile") + assert not hasattr(model, "to_module") for retired_module in ( "pops.physics.facade", "pops.physics.model", @@ -344,3 +345,11 @@ def test_physics_has_no_competing_model_facade() -> None: ): with pytest.raises(ModuleNotFoundError): importlib.import_module(retired_module) + + +def test_moment_model_has_one_model_construction_route() -> None: + from pops import moments + + specification = moments.CartesianVelocityMoments(order=2) + assert callable(specification.build) + assert not hasattr(specification, "check") diff --git a/tests/python/architecture/test_no_legacy_runtime_routes.py b/tests/python/architecture/test_no_legacy_runtime_routes.py index 856e87bc1..41cdfd4a9 100644 --- a/tests/python/architecture/test_no_legacy_runtime_routes.py +++ b/tests/python/architecture/test_no_legacy_runtime_routes.py @@ -269,6 +269,46 @@ def test_case_has_one_registration_spelling_per_authority() -> None: assert hasattr(case, "consumers") and not hasattr(case, "output") +def test_native_runtime_wrappers_do_not_restore_add_block_through_passthrough() -> None: + from pops.runtime._lifecycle import RETIRED_NATIVE_PASSTHROUGH + + assert RETIRED_NATIVE_PASSTHROUGH == frozenset({"add_block"}) + + for relative in ( + "runtime/_system_install.py", + "runtime/_amr_system.py", + "runtime/_system_contract.py", + "runtime/_amr_system_contract.py", + ): + source = (PACKAGE / relative).read_text(encoding="utf-8") + tree = ast.parse(source, filename=relative) + assert not any( + isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + and node.name == "add_block" + for node in ast.walk(tree) + ), relative + + for relative in ("runtime/_system.py", "runtime/_amr_system.py"): + source = (PACKAGE / relative).read_text(encoding="utf-8") + tree = ast.parse(source, filename=relative) + passthrough = next( + node + for node in ast.walk(tree) + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + and node.name == "__getattr__" + ) + assert any( + isinstance(node, ast.Name) and node.id == "_RETIRED_NATIVE_PASSTHROUGH" + for node in ast.walk(passthrough) + ), relative + assert any( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "AttributeError" + for node in ast.walk(passthrough) + ), relative + + def test_amr_has_one_checkpoint_output_and_tagging_authority_path() -> None: from pops import amr as authoring_amr import pops.mesh as public_mesh diff --git a/tests/python/integration/native_loader/test_compile_module_trace.py b/tests/python/integration/native_loader/test_compile_module_trace.py index 6b68907e8..daa6fafc6 100644 --- a/tests/python/integration/native_loader/test_compile_module_trace.py +++ b/tests/python/integration/native_loader/test_compile_module_trace.py @@ -2,7 +2,7 @@ """ADC-557 real-compiler acceptance: the standard flow lowers the final model once. A final ``pops.physics.Model`` compiled through the internal ``compile_problem`` seam (no -manual ``m.to_module()``) yields a handle that carries the operator-first Module as the lowered-module +manual ``m.lower()``) yields a handle that carries the operator-first Module as the lowered-module trace (``compiled.inspect()``) and a compile-time ``module_hash`` for drift detection. The bounded native ``ModelSpec`` bridge is rejected before compilation because it has no canonical Module authority; a missing trace can therefore never be fabricated. diff --git a/tests/python/unit/codegen/test_module_lowering.py b/tests/python/unit/codegen/test_module_lowering.py index 82ff7e347..5cf380580 100644 --- a/tests/python/unit/codegen/test_module_lowering.py +++ b/tests/python/unit/codegen/test_module_lowering.py @@ -11,7 +11,7 @@ 1 a raw Module with a bodyless codegen operator raises the SAME error through ``lower_and_validate`` as through ``_module_to_model`` (one validation path); 2 a facade Model resolves to its operator-first Module (``source_module``) with NO manual - ``to_module()`` / ``lower()`` and carries a ``module_hash``; + ``lower()`` and carries a ``module_hash``; 3 a facade dependency error is remapped, citing the model name / states / operators; 4 the emit model of a facade Model is BYTE-IDENTICAL through ``lower_and_validate`` vs direct. @@ -85,7 +85,7 @@ def test_one_validation_bodyless_operator_same_error(): assert direct == via_lower, "the SAME error text is raised via both entries (no divergence)" -# --- 2: a facade Model resolves to its operator-first Module with no manual to_module ----------- +# --- 2: a facade Model resolves to its operator-first Module with no manual lower() ------------- def test_facade_model_carries_operator_first_module(): m = _facade_model() diff --git a/tests/python/unit/descriptors/test_moments_descriptors.py b/tests/python/unit/descriptors/test_moments_descriptors.py index ca5694efe..756b593cb 100644 --- a/tests/python/unit/descriptors/test_moments_descriptors.py +++ b/tests/python/unit/descriptors/test_moments_descriptors.py @@ -140,6 +140,10 @@ def test_handles_are_not_descriptors(): def test_moment_model_has_no_transport_noop_surface(): specification = moments.CartesianVelocityMoments(order=2) assert not hasattr(specification, "add_transport") + assert callable(specification.build) + assert not hasattr( + specification, "check" + ), "MomentModel.build() is the sole model-construction route" def test_moment_transport_blocks_follow_the_canonical_directional_chains(): diff --git a/tests/python/unit/physics/test_fv_hll_minmod.py b/tests/python/unit/physics/test_fv_hll_minmod.py index 01eab9506..1ddc2b844 100644 --- a/tests/python/unit/physics/test_fv_hll_minmod.py +++ b/tests/python/unit/physics/test_fv_hll_minmod.py @@ -91,7 +91,7 @@ def gaussian(n): chk("hllc" in str(e), f"erreur explicite : {e}") # --- 4. AmrSystem : hll + minmod accepte (alignement de surface System/AMR) ------ -print("== AmrSystem : add_block(riemann='hll') accepte sur isotherme ==") +print("== AmrSystem : add_equation(riemann='hll') accepte sur isotherme ==") amr = AmrSystem(n=32, L=1.0, periodicity=(True, True), regrid_every=0) amr.set_poisson(rhs="charge_density", solver="geometric_mg", bc=Periodic()) amr_rho0 = gaussian(32) diff --git a/tests/python/unit/physics/test_wave_speed_cache.py b/tests/python/unit/physics/test_wave_speed_cache.py index fbc0e0a66..53f5d0e68 100644 --- a/tests/python/unit/physics/test_wave_speed_cache.py +++ b/tests/python/unit/physics/test_wave_speed_cache.py @@ -66,12 +66,12 @@ def make_sim(cache, riemann=None, limiter=None, time=None): riemann = riemann if riemann is not None else HLL() limiter = limiter if limiter is not None else FirstOrder() sim = System(n=N, L=1.0, periodicity=(True, True)) - sim.add_block("ions", - Model(state=FluidState("isothermal", cs2=CS2), - transport=IsothermalFlux(), source=NoSource(), - elliptic=BackgroundDensity(alpha=1.0, n0=1.0)), - spatial=Spatial(limiter=limiter, flux=riemann, wave_speed_cache=cache), - time=time if time is not None else Explicit()) + sim.add_equation("ions", + Model(state=FluidState("isothermal", cs2=CS2), + transport=IsothermalFlux(), source=NoSource(), + elliptic=BackgroundDensity(alpha=1.0, n0=1.0)), + spatial=Spatial(limiter=limiter, flux=riemann, wave_speed_cache=cache), + time=time if time is not None else Explicit()) return sim @@ -98,12 +98,12 @@ def make_sim(cache, riemann=None, limiter=None, time=None): print("== (2) defaut inchange : sans wave_speed_cache == cache OFF ==") s_def = System(n=N, L=1.0, periodicity=(True, True)) -s_def.add_block("ions", - Model(state=FluidState("isothermal", cs2=CS2), - transport=IsothermalFlux(), source=NoSource(), - elliptic=BackgroundDensity(alpha=1.0, n0=1.0)), - spatial=Spatial(limiter=FirstOrder(), flux=HLL()), - time=Explicit()) +s_def.add_equation("ions", + Model(state=FluidState("isothermal", cs2=CS2), + transport=IsothermalFlux(), source=NoSource(), + elliptic=BackgroundDensity(alpha=1.0, n0=1.0)), + spatial=Spatial(limiter=FirstOrder(), flux=HLL()), + time=Explicit()) s_def.set_state("ions", U0) install_forward_euler_program(s_def) for _ in range(20): @@ -136,13 +136,13 @@ def make_disc_sim_then_mode(): def make_mode_then_cache(): sim = System(n=N, L=1.0, periodicity=(True, True)) sim.set_disc_domain(DiscDomain(center=(0.5, 0.5), radius=0.3, mode=CutCell())) - sim.add_block("ions", - Model(state=FluidState("isothermal", cs2=CS2), - transport=IsothermalFlux(), source=NoSource(), - elliptic=BackgroundDensity(alpha=1.0, n0=1.0)), - spatial=Spatial(limiter=FirstOrder(), flux=HLL(), - wave_speed_cache=True), # doit lever (mode disque actif) - time=Explicit()) + sim.add_equation("ions", + Model(state=FluidState("isothermal", cs2=CS2), + transport=IsothermalFlux(), source=NoSource(), + elliptic=BackgroundDensity(alpha=1.0, n0=1.0)), + spatial=Spatial(limiter=FirstOrder(), flux=HLL(), + wave_speed_cache=True), # doit lever (mode disque actif) + time=Explicit()) msg = err_msg(make_disc_sim_then_mode) @@ -150,10 +150,10 @@ def make_mode_then_cache(): f"cache puis set_disc_domain(staircase) rejete ({msg[:60]}...)") msg = err_msg(make_mode_then_cache) chk("wave_speed_cache" in msg and ("cutcell" in msg or "staircase" in msg), - f"set_disc_domain(cutcell) puis add_block(cache) rejete ({msg[:60]}...)") + f"set_disc_domain(cutcell) puis add_equation(cache) rejete ({msg[:60]}...)") print("== (6) garde backend compile : cache + add_equation(modele .so) -> erreur ==") -# Le cache n'est cable que sur le chemin natif compose (add_block). Le package de production ne +# Le cache n'est cable que sur le chemin natif compose de add_equation. Le package de production ne # transporte pas le flag : il serait ignore en silence. On verifie le rejet avant le dlopen. from pops.codegen.loader import CompiledModel # noqa: E402 diff --git a/tests/python/unit/runtime/test_board_multispecies.py b/tests/python/unit/runtime/test_board_multispecies.py index 733ade7c0..b67c73537 100644 --- a/tests/python/unit/runtime/test_board_multispecies.py +++ b/tests/python/unit/runtime/test_board_multispecies.py @@ -321,8 +321,7 @@ def test_multispecies_lowers_to_a_multiblock_module(): assert not hasattr(m, "compile"), "physics.Model must not expose a direct compile()" module = m.lower() assert isinstance(module, _model_pkg.Module), "physics.Model.lower() returns a pops.model.Module" - assert isinstance(m.to_module(), _model_pkg.Module), "to_module() returns a Module too" - assert type(m).to_module is type(m).lower, "to_module() is the lower() alias" + assert not hasattr(m, "to_module"), "lower() is the sole explicit Module projection" def test_multispecies_check_rejects_an_undeclared_coupled_coordinate(): @@ -464,7 +463,7 @@ def test_local_transform_promotion_preserves_the_first_species_declaration(): electrons = m.species("electrons", state=["ne"]) transform = m.local_transform( "repair_electrons", (electrons["ne"] + 1.0,), on=electrons) - ions = m.species("ions", state=["ni"]) + m.species("ions", state=["ni"]) module = m.module electron_space = module.state_spaces()["electrons"] ion_space = module.state_spaces()["ions"] diff --git a/tests/python/unit/runtime/test_cutcell_thresholds.py b/tests/python/unit/runtime/test_cutcell_thresholds.py index f8c85268d..f659e033e 100644 --- a/tests/python/unit/runtime/test_cutcell_thresholds.py +++ b/tests/python/unit/runtime/test_cutcell_thresholds.py @@ -55,20 +55,20 @@ def test_transport_mask_thresholds_require_typed_mask(): # --- runtime tier (needs _pops) ---------------------------------------------- pops = pytest.importorskip("pops") -from pops.runtime._engine_descriptors import ( +from pops.runtime._engine_descriptors import ( # noqa: E402 ChargeDensity, FluidState, IsothermalFlux, Model, NoSource, Spatial, ) -from pops.runtime._system import System # ADC-545 advanced runtime seam +from pops.runtime._system import System # noqa: E402 # ADC-545 advanced runtime seam def _sim(): sim = System(n=16, L=1.0, periodicity=(False, False)) - sim.add_block("ion", Model(FluidState.isothermal(cs2=0.7), IsothermalFlux(), - NoSource(), ChargeDensity(charge=1.0)), - # The native embedded-boundary facade currently provides a geometry-aware - # first-order reconstruction. Higher-order neighbor stencils are rejected - # instead of reading inactive cells. - spatial=Spatial(none=True)) + sim.add_equation("ion", Model(FluidState.isothermal(cs2=0.7), IsothermalFlux(), + NoSource(), ChargeDensity(charge=1.0)), + # The native embedded-boundary facade currently provides a geometry-aware + # first-order reconstruction. Higher-order neighbor stencils are rejected + # instead of reading inactive cells. + spatial=Spatial(none=True)) return sim