Source code for pops.time.program

"""pops.time.Program -- the compiled time-program authoring class (builder-mode IR).

A ``Program`` BUILDS a typed SSA IR for one time step; Python never executes a numerical stage.
The C++ lowering (``emit_cpp_program``) is a thin delegator to
``pops.codegen.program_codegen`` (lazy import), so this package keeps a strictly acyclic
graph (it imports only ``pops.ir`` / ``pops.model`` and never ``pops.codegen`` / ``_pops`` at
module scope). The class is composed from focused authoring mixins.

cf. docs/sphinx/reference/time-program.md (Phase 8) and the ADC-399 epic.
"""
from pops.time.program_authoring import _ProgramAuthoring
from pops.time.program_core import _ProgramCore
from pops.time.program_inspect import _ProgramInspect
from pops.time.program_local import _ProgramLocal
from pops.time.program_passes import _ProgramPasses
from pops.time.program_solve import _ProgramSolve
from pops.time.values import _Coeff, Value  # noqa: F401  (Value used by mixins via prog ref)


[docs] class Program(_ProgramCore, _ProgramLocal, _ProgramSolve, _ProgramAuthoring, _ProgramPasses, _ProgramInspect): """A compiled time program (builder mode). Holds the SSA value list and the committed blocks. The Python object only BUILDS the IR; it is never executed numerically during ``sim.step``. Authoring methods come from the mixins; C++ emission is delegated to ``pops.codegen.program_codegen`` via :meth:`emit_cpp_program`. """ def __init__(self, name): self.name = name # De-stringing is the ONE public path (Spec 5 sec.15, ADC-479 criteria 23 + 27): the public # P.call requires a typed operator handle and the public P.rhs requires the typed terms= list # (the legacy string operator name / flux=/sources= form is REFUSED). The byte-identical # builders survive ONLY as the internal _call / _rhs_legacy, which the typed front doors and # the pops.lib.time macros lower through -- no opt-in flag, no second public path. self._values = [] self._next_id = 0 self._commits = {} # block -> State value self._recording = [] # stack of sub-block lists (a control-flow body); see _new / while_ self._histories = {} # name -> max declared lag (multistep histories; ADC-406a) # OPTIONAL dt bound (spec s18 / ADC-417): a recorded scalar sub-program (cfl -> Scalar) the # generated .so exports as pops_program_dt_bound; None = no bound (the native CFL is used). self._dt_bound = None # (block, scalar_value) once set; the block is the scalar sub-block self.dt = _Coeff({1: 1.0}) # symbolic time step; participates in coefficient arithmetic # OPTIONAL bound operator registry (Spec 2, operator-first): set by bind_operators so P.call # can resolve and type-check operators at build time. None = legacy PDE-shortcut-only Program. self._registry = None # Per-emit scratch names of coupled_rate blocks, keyed by (coupled node id, block): the # coupled_rate kernel fills them and each coupled_rate_out projection aliases its block's # scratch (ADC-457). Populated during _emit_op; harmless to keep across emits (keys are unique # per node id). self._coupled_scratch = {} def __str__(self): """Short, deterministic, array-free summary -- never the full SSA IR. Prints the program name, the op count and the committed block names (Spec 5 sec.12.1): a one-line header, not a node-by-node dump. """ return "Program(name=%r, ops=%d, blocks=%s)" % ( self.name, len(self._values), sorted(self._commits)) # --- C++ codegen (lowering to a problem.so source) lives in pops.codegen; the authoring # Program delegates via a LAZY import so pops.time stays free of any codegen/_pops edge. ---
[docs] def emit_cpp_program(self, model=None): """Generate the C++ source of a problem.so implementing this Program (codegen). Thin authoring entry point: delegates to the free function :func:`pops.codegen.program_codegen.emit_cpp_program`, imported lazily so the ``pops.time`` package never imports ``pops.codegen`` / ``_pops`` at module scope. See the codegen function for the full lowering contract. """ from pops.codegen import program_codegen as _pcg return _pcg.emit_cpp_program(self, model=model)
def _check_lowerable(self, model=None): """Raise if the IR uses a construct the codegen cannot lower (delegates to :func:`pops.codegen.program_codegen._check_lowerable`, lazy import). """ from pops.codegen import program_codegen as _pcg return _pcg._check_lowerable(self, model) def _check_schedules_lowerable(self): """Raise if a node carries a schedule the codegen cannot lower (delegates to :func:`pops.codegen.program_codegen._check_schedules_lowerable`, lazy import). """ from pops.codegen import program_codegen as _pcg return _pcg._check_schedules_lowerable(self) def _emit_body(self, model=None): """Lower the install-function body to ``(prelude, body)`` C++ (delegates to :func:`pops.codegen.program_codegen._emit_body`, lazy import). Exposed for the codegen tests that assert the body shape directly. """ from pops.codegen import program_codegen as _pcg return _pcg._emit_body(self, model)
[docs] class CompiledTime: """Record of a compiled `Program`'s macro-step cadence (`substeps` / `stride`). A compiled Program OWNS the whole step body: it is installed via `sim.install_program` and driven by `sim.step(dt)`. Its cadence is applied to the System with `sim.set_program_cadence(substeps, stride)` (call it after `install_program`); a `CompiledTime` just records those values. The compiled program is NOT attached via `sim.add_equation(time=CompiledTime(...))` -- that path is rejected with an explicit error (the transport policy passed to `add_equation` is a native `pops.Explicit`/etc.; the compiled program is installed separately). `substeps` and `stride` are wired (ADC-411) as a SYSTEM-level orchestration AROUND the opaque program closure (`System.set_program_cadence`, mirroring the native per-block advance loop): `substeps=n` runs the program n times over `eff_dt/n`; `stride=M` runs the whole program once per M macro-steps with `eff_dt = M*dt` (GLOBAL hold-then-catch-up, the clock still ticks every macro-step). Two semantic limits to keep in mind (cf. system_stepper.hpp): - `substeps > 1` is bit-exact vs native `pops.Explicit(substeps=n)` ONLY for an UNCOUPLED / transport-only program: `program_step_(h)` re-runs the WHOLE program (its `solve_fields` included), whereas native substeps subdivides ONLY the transport (solve_fields runs once). - `stride` here is GLOBAL (a compiled program is one whole-system closure), so it equals native per-block stride only for a single-block system (or all blocks sharing the stride). A NUMERIC `cfl` (e.g. `cfl=0.5`) is wired: it is applied at RUNTIME by `sim.run(cfl=...)`, whose `cfl` defaults to the installed cadence's `cfl` when the caller passes none, so a bare `sim.run(t_end)` after `bind(..., cadence=CompiledTime(cfl=0.5))` advances at `cfl=0.5` (the per-block CFL `dt` is computed in adc_cpp and drives the installed Program). A self-computed `cfl` SUB-PROGRAM (`cfl="program"`) is still deferred (the Program would export its own dt bound); it fails loud rather than being silently ignored.""" def __init__(self, substeps=1, stride=1, cfl="default"): if not isinstance(substeps, int) or substeps < 1: raise ValueError("CompiledTime: substeps must be a positive int (got %r)" % (substeps,)) if not isinstance(stride, int) or stride < 1: raise ValueError("CompiledTime: stride must be a positive int (got %r)" % (stride,)) # A numeric cfl is wired (applied at runtime via sim.run(cfl=)); only a non-numeric, # non-"default" cfl (e.g. cfl="program", a self-computed dt sub-program) is still deferred. if cfl != "default" and not isinstance(cfl, (int, float)): raise NotImplementedError( "CompiledTime: a self-computed cfl sub-program (cfl=%r) is deferred (ADC-401 Phase " "2c); pass a numeric cfl=<value> (applied at runtime by sim.run(cfl=)) or an explicit " "dt to sim.step(dt)" % (cfl,)) self.substeps = substeps self.stride = stride self.cfl = cfl self.kind = "compiled" def __repr__(self): return "CompiledTime(substeps=%d, stride=%d, cfl=%r)" % (self.substeps, self.stride, self.cfl)