diff --git a/CHANGELOG.md b/CHANGELOG.md index 358f2d4..0eed04a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Fixed — runtime resources ship inside the wheels + +- **Canonical algorithm sources are package data**: the six `algpseudocode` sources (`jacobi`, `newmark`, `pcg`, `radial_return_j2`, `radial_return_j2_kinematic`, `radial_return_j2_mixed`) moved from the repository-only `dev/algorithms/` to `algo2code/library/algorithms/` and are read via `importlib.resources`. Installed from PyPI, `mechdsl.lib.plasticity{,_kinematic,_mixed}` and `algo2code.library.radial_return_j2{,_kinematic,_mixed}` previously raised `FileNotFoundError` at import time (they walked up from `__file__` looking for the repo checkout); `mechdsl.solver.seam_solve` / `seam_integrate` had the same latent defect behind their Taichi import. +- **`algo2code.library.algorithm_source(name)` / `algorithm_source_path(name)` / `ALGORITHM_SOURCES`**: new accessors for the bundled sources; `mechdsl.solver.pcg_tex_path()` / `newmark_tex_path()` now resolve through them. +- **Regression test**: `packages/mechdsl-core/tests/test_wheel_import.py` builds both wheels, installs them into a fresh venv without the `[verify]` extra, and imports every module (missing `taichi` / `ti_runtime` is the only accepted skip); `packages/algo2code/tests/test_library_sources.py` checks the bundled sources in-process. + +### Changed — shipped source no longer carries internal planning references + +- Comments, docstrings, error messages and emitted-code comments in `mechdsl-core` and `algo2code` (and the bundled `.tex` algorithm sources) no longer cite internal plan/phase/task identifiers or cross-repo planning notes; the reasoning behind each comment is kept. Error messages keep their actionable content. No identifiers, behaviour or public API changed. Golden codegen snapshots were regenerated for the affected comment lines only. The benchmark registry's `task_id` values (`"P10-1"`..`"P10-9"`) are data keys matched by the committed perf baseline and are unchanged. + ## [0.2.1] - 2026-08-31 ### Changed — packaging for PyPI diff --git a/docs/algo2code/usage.md b/docs/algo2code/usage.md index d9382d8..26d9cf5 100644 --- a/docs/algo2code/usage.md +++ b/docs/algo2code/usage.md @@ -17,9 +17,11 @@ individual stages are exported too (`parse_algorithm`, `infer_types`, `generate_ ## The algorithm library -Canonical algorithms live in `algo2code.library`, each backed by a `.tex` source under -`dev/algorithms/`. Every module exposes the verbatim LaTeX as a constant, a getter, and a -convenience transpile wrapper. +Canonical algorithms live in `algo2code.library`, each backed by a `.tex` source bundled +as package data under `algo2code/library/algorithms/` (so they are available from an +installed wheel). Every module exposes the verbatim LaTeX as a constant, a getter, and a +convenience transpile wrapper; `algo2code.library.algorithm_source("pcg.tex")` reads any +bundled source by name. | Algorithm | Module | LaTeX constant | Wrapper | |---|---|---|---| diff --git a/packages/algo2code/src/algo2code/backends/taichi_codegen.py b/packages/algo2code/src/algo2code/backends/taichi_codegen.py index 9b0be82..91a8d71 100644 --- a/packages/algo2code/src/algo2code/backends/taichi_codegen.py +++ b/packages/algo2code/src/algo2code/backends/taichi_codegen.py @@ -18,9 +18,9 @@ each generated file — backward-compatible, no runtime dependency. - ``"ti_runtime"``: emit ``from ti_runtime import vector_ops as _v`` and call the shared primitives for ``dot``/``norm``/``copy``/``vec_add``; only - ``_matvec`` remains inlined (the matrix-free operator seam is P2-2's - concern). algo2code itself never imports ``ti_runtime`` — it only *emits* - the import line. + ``_matvec`` remains inlined (the matrix-free operator is injected through + the seam by the consumer). algo2code itself never imports ``ti_runtime`` — + it only *emits* the import line. Compatibility note (``"ti_runtime"`` mode): ``ti_runtime.vector_ops.dot`` uses ``x[I].dot(y[I])`` which requires @@ -29,7 +29,7 @@ the consumer passes scalar fields. Callers that use vector fields (e.g. mechdsl FEM drivers) are unaffected. This is a known field-model mismatch; resolving it (either by changing the emitted field model to ``ti.Vector.field`` - or by adding scalar variants to ti_runtime) is out of scope for P2-1. + or by adding scalar variants to ti_runtime) is out of scope for this backend. """ from __future__ import annotations @@ -177,7 +177,7 @@ def emit_kernels(self, runtime: str = RUNTIME_INLINE) -> str: In ``"ti_runtime"`` mode the shared primitives (dot/norm/copy/vec_add) are called via the ``_v`` alias imported from ``ti_runtime.vector_ops``. Only ``_matvec`` has no ti_runtime equivalent and remains inlined - (the matrix-free operator seam is P2-2's concern). + (the matrix-free operator is injected through the seam by the consumer). """ lines = [] if runtime == RUNTIME_TI_RUNTIME: @@ -394,8 +394,8 @@ def _vector_field_alloc(self, vec_arg: str | None) -> str: locals matching the *vector argument's* component layout (``.n``): the ``ti_runtime`` ``dot``/``norm2`` primitives use ``x[I].dot(y[I])``, which requires vector fields. This is what lets the matrix-free seam - (P2-2) run end-to-end against ``ti_runtime`` (resolving the field-model - mismatch P2-1 documented). With no vector argument the component count + run end-to-end against ``ti_runtime`` (resolving the field-model + mismatch documented above). With no vector argument the component count is unknown, so fall back to the scalar layout. """ if self.runtime == RUNTIME_TI_RUNTIME and vec_arg is not None: @@ -847,7 +847,8 @@ def generate_taichi(algo: Algorithm, runtime: str = RUNTIME_INLINE) -> str: ``"ti_runtime"`` Emit ``from ti_runtime import vector_ops as _v`` and call the shared primitives for dot/norm/copy/vec_add. Only ``_matvec`` - remains inlined (the matrix-free operator seam is P2-2's concern). + remains inlined (the matrix-free operator is injected through the + seam by the consumer). algo2code itself never imports ti_runtime — it only emits the import line. diff --git a/packages/algo2code/src/algo2code/expr_parser.py b/packages/algo2code/src/algo2code/expr_parser.py index 718e44a..ca82ca6 100644 --- a/packages/algo2code/src/algo2code/expr_parser.py +++ b/packages/algo2code/src/algo2code/expr_parser.py @@ -225,8 +225,7 @@ def parse_term(self) -> Expr: Division at the term level shares precedence with multiplication (left-associative) so ``a + b / c`` parses as ``a + (b / c)`` - rather than dropping the divisor. post_recovery_plan Phase 5 - parser fix. + rather than dropping the divisor. """ left = self.parse_signed_factor() while True: diff --git a/packages/algo2code/src/algo2code/library/__init__.py b/packages/algo2code/src/algo2code/library/__init__.py index 5bdabb6..527d322 100644 --- a/packages/algo2code/src/algo2code/library/__init__.py +++ b/packages/algo2code/src/algo2code/library/__init__.py @@ -6,14 +6,25 @@ here instead of re-typing or re-inventing the algorithm text — this is the canonical hand-off seam between ``algo2code`` and any consumer that wants a reference implementation of a textbook iterative solver. + +The verbatim ``.tex`` sources live in ``algo2code/library/algorithms/`` and +ship as package data; :func:`algorithm_source` reads any of them by name. """ from algo2code.library.pcg import ( PCG_ALGORITHM_LATEX, get_pcg_algorithm_latex, ) +from algo2code.library.sources import ( + ALGORITHM_SOURCES, + algorithm_source, + algorithm_source_path, +) __all__ = [ + "ALGORITHM_SOURCES", "PCG_ALGORITHM_LATEX", + "algorithm_source", + "algorithm_source_path", "get_pcg_algorithm_latex", ] diff --git a/dev/algorithms/jacobi.tex b/packages/algo2code/src/algo2code/library/algorithms/jacobi.tex similarity index 89% rename from dev/algorithms/jacobi.tex rename to packages/algo2code/src/algo2code/library/algorithms/jacobi.tex index bea46db..45c535d 100644 --- a/dev/algorithms/jacobi.tex +++ b/packages/algo2code/src/algo2code/library/algorithms/jacobi.tex @@ -1,4 +1,4 @@ -% Point-Jacobi preconditioner apply (PlanJune14 P4-1). +% Point-Jacobi preconditioner apply. % % Computes the Jacobi (diagonal) preconditioned vector: % @@ -9,8 +9,8 @@ % small positive guard against near-zero diagonal entries. All operands are % degree-of-freedom vectors (one value per node or per component). % -% Grammar note (2026-06-15, P4-1): -% ----------------------------------- +% Grammar note: +% ------------- % algo2code currently lowers vector/vector division (`z = r / d`) to the Python % expression `z = (r / d)`, which is not runnable for `ti.Vector.field` operands % (Taichi raises TypeError: unsupported operand type(s) for /: MatrixField / @@ -18,7 +18,7 @@ % callable-matvec, but has no `vec_ediv` (elementwise divide) primitive. % % The clean fix is a new `vec_ediv(z, r, d, eps)` primitive in ti_runtime -% vector_ops — exactly what P4-1 adds. The generated body below is therefore +% vector_ops. The generated body below is therefore % realised as a thin `@ti.data_oriented` adapter % (`mechdsl.solver.jacobi_preconditioner.GeneratedJacobiPreconditioner`) whose % `_apply` kernel calls `ti_runtime.vector_ops.ediv(z, r, d, eps)` — a diff --git a/dev/algorithms/newmark.tex b/packages/algo2code/src/algo2code/library/algorithms/newmark.tex similarity index 95% rename from dev/algorithms/newmark.tex rename to packages/algo2code/src/algo2code/library/algorithms/newmark.tex index 866e2c2..2aa9797 100644 --- a/dev/algorithms/newmark.tex +++ b/packages/algo2code/src/algo2code/library/algorithms/newmark.tex @@ -1,4 +1,4 @@ -% Newmark-beta implicit time integration — single step (PlanJune14 P6-1). +% Newmark-beta implicit time integration — single step. % % The canonical Newmark-beta step authored for the **all-Taichi on-device % time-integration seam path** (the temporal analogue of the matrix-free PCG in @@ -32,7 +32,7 @@ % average-acceleration / trapezoidal rule — unconditionally stable, no numerical % damping. HHT-alpha is a one-parameter generalisation (alpha-weighted residual); % it reuses the same predictor/corrector skeleton and is intentionally NOT -% authored here — Newmark-beta is the P6-1 deliverable, HHT is optional/next. +% authored here — Newmark-beta is the deliverable, HHT is optional/next. % % Seam contract (mirrors ti_runtime.seams.TimeIntegrationContext) % --------------------------------------------------------------- @@ -42,8 +42,8 @@ % The callable owns the mass / damping / stiffness / external-force data; the box % is mass-/material-agnostic, exactly like ``pcg.tex`` is operator-agnostic. % -% Grammar note (2026-06-15, P6-1) -% ------------------------------- +% Grammar note +% ------------ % This box transpiles **directly** through algo2code in ``runtime='ti_runtime'`` % mode with no grammar gap: the predictor/corrector updates are all % ``scalar*vector`` and ``vector +/- vector`` (fused ``_v.vec_add`` AXPBY calls), diff --git a/dev/algorithms/pcg.tex b/packages/algo2code/src/algo2code/library/algorithms/pcg.tex similarity index 91% rename from dev/algorithms/pcg.tex rename to packages/algo2code/src/algo2code/library/algorithms/pcg.tex index 0d051bf..643fb3c 100644 --- a/dev/algorithms/pcg.tex +++ b/packages/algo2code/src/algo2code/library/algorithms/pcg.tex @@ -1,7 +1,7 @@ -% Matrix-free Preconditioned Conjugate Gradient (PCG) — PlanJune14 P4-2. +% Matrix-free Preconditioned Conjugate Gradient (PCG). % -% The canonical PCG algorithm authored for the **all-Taichi on-device seam path** -% (Option 1): the operator ``A`` and the preconditioner ``M_inv`` are both +% The canonical PCG algorithm authored for the **all-Taichi on-device seam path**: +% the operator ``A`` and the preconditioner ``M_inv`` are both % *callables* (matrix-free), so this box transpiles via % ``algo2code.transpile(..., runtime='ti_runtime')`` into a body that solves % ``A x = b`` over ``ti.Vector.field`` DOF vectors without ever forming or storing @@ -12,7 +12,7 @@ % Identical in algorithm to the canonical PCG LaTeX % (``algo2code.library.pcg.PCG_ALGORITHM_LATEX`` / the hand twin % ``mechdsl.solver.import_adapter.Algo2CodePCGSolver``) and to the matrix-free -% ``_CALLABLE_PCG_LATEX`` proven runnable in P2-2. The *only* surface change vs +% ``_CALLABLE_PCG_LATEX`` proven runnable in runtime mode. The *only* surface change vs % the canonical box is the operator type: % % canonical: ``% args A:matrix, ... apply_M_inv:callable, ...`` @@ -21,7 +21,7 @@ % A matrix ``A`` lowers to a dense scalar-indexed ``_matvec`` (incompatible with % the ``ti.Vector.field`` layout the seam uses); a callable ``A`` lowers to the % in-place ``A(out, p)`` operator call — the ``ti_runtime`` ``apply_A(out, x)`` -% seam contract. The P2-2 runtime-mode guard requires exactly this: a callable +% seam contract. The runtime-mode guard requires exactly this: a callable % operator + a vector argument. % % Seam contract (mirrors ti_runtime.seams.LinearSolveContext) @@ -29,7 +29,7 @@ % A(out, x) — operator: out = A @ x (out FIRST) % M_inv(r, z) — preconditioner: z = M^{-1} r (out LAST) % -% This box productionizes the PJ-1 spike's hand-written ``pcg`` body +% This box productionizes the spike's hand-written ``pcg`` body % (``packages/mechdsl-core/tests/spike/svk_hex8_taichi.py``): the generated body % maps line-for-line to that hand-written PCG, but is *derived from LaTeX* rather % than hand-coded. diff --git a/dev/algorithms/radial_return_j2.tex b/packages/algo2code/src/algo2code/library/algorithms/radial_return_j2.tex similarity index 89% rename from dev/algorithms/radial_return_j2.tex rename to packages/algo2code/src/algo2code/library/algorithms/radial_return_j2.tex index 14d8364..abe0d68 100644 --- a/dev/algorithms/radial_return_j2.tex +++ b/packages/algo2code/src/algo2code/library/algorithms/radial_return_j2.tex @@ -1,4 +1,4 @@ -% post_recovery_plan Phase 5 (P5-1): J2 radial-return algpseudocode source. +% J2 radial-return algpseudocode source. % % Scalar Newton inner loop for J2 plasticity with power-law isotropic % hardening. Solves for the plastic multiplier increment dl given the @@ -14,11 +14,10 @@ % % This is the production algo2code source consumed by % ``mechdsl.lib.plasticity.radial_return`` via ``algo2code.transpile`` -% (post_recovery_plan Phase 5 P5-3 — algo2code expr_parser bugs that -% previously deferred direct emission landed in the same phase under -% P5-1's parser fix). Bit-equality with the imported reference path is -% asserted by -% ``packages/mechdsl-core/tests/test_j2_radial_return_parity.py`` (P5-4). +% (the algo2code expr_parser bugs that previously deferred direct emission +% were fixed alongside this source). Bit-equality with the imported reference +% path is asserted by +% ``packages/mechdsl-core/tests/test_j2_radial_return_parity.py``. % % Inputs % ------ diff --git a/dev/algorithms/radial_return_j2_kinematic.tex b/packages/algo2code/src/algo2code/library/algorithms/radial_return_j2_kinematic.tex similarity index 91% rename from dev/algorithms/radial_return_j2_kinematic.tex rename to packages/algo2code/src/algo2code/library/algorithms/radial_return_j2_kinematic.tex index 65ec698..82b95fc 100644 --- a/dev/algorithms/radial_return_j2_kinematic.tex +++ b/packages/algo2code/src/algo2code/library/algorithms/radial_return_j2_kinematic.tex @@ -1,5 +1,4 @@ -% constitutive_latex Phase 6 (P6-2): J2 kinematic linear-hardening -% radial-return algpseudocode source. +% J2 kinematic linear-hardening radial-return algpseudocode source. % % Scalar return-map inner solve for J2 plasticity with LINEAR KINEMATIC % hardening (Prager). The yield surface translates (back-stress beta) @@ -19,14 +18,14 @@ % % This scalar source is authored as the SAME fixed-iteration loop % structure as the isotropic power-law variant -% (``dev/algorithms/radial_return_j2.tex``) for transpile-pattern +% (``algo2code/library/algorithms/radial_return_j2.tex``) for transpile-pattern % consistency: the residual ``res = xi_eq - (3*mu + H_kin)*dl - sigy0`` % is linear in dl, so the Newton update (denominator ``3*mu + H_kin``) % converges in a single step and every subsequent iteration leaves dl % unchanged at the exact root (res = 0 => the closed form above). % Authoring it as a loop -% keeps P6-1/P6-2/P6-3 on one structural pattern that algo2code already -% transpiles end-to-end. +% keeps the isotropic, kinematic and mixed variants on one structural +% pattern that algo2code already transpiles end-to-end. % % Back-stress update (Prager, discrete) is performed in TENSOR space by % the mechdsl-core wrapper: diff --git a/dev/algorithms/radial_return_j2_mixed.tex b/packages/algo2code/src/algo2code/library/algorithms/radial_return_j2_mixed.tex similarity index 84% rename from dev/algorithms/radial_return_j2_mixed.tex rename to packages/algo2code/src/algo2code/library/algorithms/radial_return_j2_mixed.tex index 7e98d0f..1e42257 100644 --- a/dev/algorithms/radial_return_j2_mixed.tex +++ b/packages/algo2code/src/algo2code/library/algorithms/radial_return_j2_mixed.tex @@ -1,10 +1,10 @@ -% constitutive_latex Phase 6 (P6-3): J2 MIXED-hardening radial-return -% algpseudocode source. +% J2 MIXED-hardening radial-return algpseudocode source. % % Scalar return-map inner solve for J2 plasticity with MIXED hardening: % the yield surface BOTH translates (linear kinematic back-stress beta, -% Prager — from P6-2) AND expands (power-law isotropic radius growth -% sigma_y(alpha) = sigy0 + K*alpha^n — from P6-1), simultaneously. Yield +% Prager — as in the kinematic variant) AND expands (power-law isotropic +% radius growth sigma_y(alpha) = sigy0 + K*alpha^n — as in the isotropic +% variant), simultaneously. Yield % is measured on the RELATIVE (shifted) stress xi = dev(S) - beta against % the EXPANDING radius sigma_y(alpha): % @@ -14,10 +14,11 @@ % Because the isotropic part is a NONLINEAR power law in % alpha = alpha_old + dl, the discrete consistency condition is NONLINEAR % in the plastic-multiplier increment dl, so — unlike the pure-linear -% kinematic variant (P6-2, closed form) — this needs the scalar NEWTON -% loop of the isotropic variant (P6-1). The kinematic part contributes a -% LINEAR term (3*mu + H_kin)*dl exactly as in P6-2; the isotropic part -% contributes the nonlinear sigma_y(alpha_old + dl) exactly as in P6-1. +% kinematic variant (closed form) — this needs the scalar NEWTON +% loop of the isotropic variant. The kinematic part contributes a +% LINEAR term (3*mu + H_kin)*dl exactly as in the kinematic variant; the +% isotropic part contributes the nonlinear sigma_y(alpha_old + dl) exactly +% as in the isotropic variant. % % Consistency residual (root in dl): % @@ -29,7 +30,8 @@ % % so the Newton update is dl <- dl + r / (3*mu + H_kin + Hp) with % Hp = K*n*ap^(n-1) the isotropic slope d(sigma_y)/d(alpha). This reduces -% to P6-1 when H_kin = 0 (xi_eq == sigma_eq, beta == 0) and to P6-2 when +% to the isotropic variant when H_kin = 0 (xi_eq == sigma_eq, beta == 0) +% and to the kinematic variant when % K = 0 (sigma_y(alpha) == sigy0, constant radius, linear residual). % % The alpha->0 guard mirrors j2_power_law.py / radial_return_j2.tex: for @@ -44,7 +46,7 @@ % Python by ``mechdsl.lib.plasticity_mixed`` — the same division of labour % as the isotropic (``mechdsl.lib.plasticity``) and kinematic % (``mechdsl.lib.plasticity_kinematic``) variants. The flow normal and -% back-stress factors are identical to P6-2: +% back-stress factors are identical to the kinematic variant: % % nf = (3/2) * xi / ||xi||_eq (von Mises gradient) % dEp = dl * nf (plastic strain increment) diff --git a/packages/algo2code/src/algo2code/library/pcg.py b/packages/algo2code/src/algo2code/library/pcg.py index 9bb4eb8..cfd67d6 100644 --- a/packages/algo2code/src/algo2code/library/pcg.py +++ b/packages/algo2code/src/algo2code/library/pcg.py @@ -1,21 +1,18 @@ r"""Canonical Preconditioned Conjugate Gradient (PCG) algorithm in algpseudocode. This module is the **algo2code interface hook** consumed by -``mechdsl-core``'s ``Algo2CodePCGSolver`` adapter (Plan recovery task P6-1). +``mechdsl-core``'s ``Algo2CodePCGSolver`` adapter. It exposes: * :data:`PCG_ALGORITHM_LATEX` — the verbatim LaTeX source of the canonical - PCG algorithm, mirrored from - ``dev/tasks/recovery_plan_latex_contract/json/P6-1.json`` field - ``pcg_algorithm_latex.latex``. + PCG algorithm. * :func:`get_pcg_algorithm_latex` — accessor returning that text. Runtime invariant ----------------- This module imports **only stdlib**. ``algo2code`` is the runtime-free -sibling package of ``mechdsl-core`` (see ``.claude/CLAUDE.md`` / -``dev/design_docs/11-ALGO2CODE.md``); nothing here imports — directly or +sibling package of ``mechdsl-core``; nothing here imports — directly or transitively — ``mechdsl``. Consumers that want a runnable implementation own the translation step. diff --git a/packages/algo2code/src/algo2code/library/radial_return_j2.py b/packages/algo2code/src/algo2code/library/radial_return_j2.py index e2ec92b..7d41215 100644 --- a/packages/algo2code/src/algo2code/library/radial_return_j2.py +++ b/packages/algo2code/src/algo2code/library/radial_return_j2.py @@ -1,13 +1,12 @@ r"""Canonical J2 radial-return algorithm (algo2code interface hook). -post_recovery_plan Phase 5 (P5-1, P5-3). Mirrors the -``algo2code.library.pcg`` pattern but goes one step further: +Mirrors the ``algo2code.library.pcg`` pattern but goes one step further: - :data:`RADIAL_RETURN_J2_LATEX` exposes the verbatim algpseudocode. - :func:`get_radial_return_j2_latex` returns it (accessor symmetry with PCG). - :func:`transpile_radial_return_j2` runs ``algo2code.transpile`` on - the source and returns the emitted Python module text. Phase 5's + the source and returns the emitted Python module text. The parser fixes (multi-letter scratch identifiers; binary ``/`` in assignment LHS) and codegen fix (``n = b.shape[0]`` regression for scalar-only algorithms) make this work end-to-end. @@ -25,28 +24,11 @@ from __future__ import annotations -from pathlib import Path +from algo2code.library.sources import algorithm_source - -def _load_canonical_source() -> str: - """Read the canonical algpseudocode source from - ``dev/algorithms/radial_return_j2.tex``. - - Walks up from this module to the repo root, then reads the - file. Single source of truth — the LaTeX file is authoritative. - """ - here = Path(__file__).resolve() - for parent in here.parents: - candidate = parent / "dev" / "algorithms" / "radial_return_j2.tex" - if candidate.is_file(): - return candidate.read_text(encoding="utf-8") - raise FileNotFoundError( - "post_recovery_plan Phase 5 (P5-1): canonical algorithm source " - "not found. Expected at /dev/algorithms/radial_return_j2.tex." - ) - - -RADIAL_RETURN_J2_LATEX: str = _load_canonical_source() +# Single source of truth: the bundled ``algorithms/radial_return_j2.tex`` +# (package data shipped in the wheel) is authoritative. +RADIAL_RETURN_J2_LATEX: str = algorithm_source("radial_return_j2.tex") def get_radial_return_j2_latex() -> str: @@ -58,8 +40,8 @@ def transpile_radial_return_j2(backend: str = "taichi") -> str: """Transpile the canonical algpseudocode to ``backend`` and return the emitted Python module source. - post_recovery_plan Phase 5 lifted the deferral that previously - blocked this round-trip: the algo2code expr_parser now handles + The deferral that previously blocked this round-trip has been + lifted: the algo2code expr_parser handles multi-letter scratch identifiers (``sy``, ``Hp``, ``ap``) and binary ``/`` in assignment LHS contexts, and the Taichi codegen no longer emits ``n = b.shape[0]`` for scalar-only algorithms. diff --git a/packages/algo2code/src/algo2code/library/radial_return_j2_kinematic.py b/packages/algo2code/src/algo2code/library/radial_return_j2_kinematic.py index c0dfaa6..6f48483 100644 --- a/packages/algo2code/src/algo2code/library/radial_return_j2_kinematic.py +++ b/packages/algo2code/src/algo2code/library/radial_return_j2_kinematic.py @@ -1,6 +1,6 @@ r"""J2 kinematic linear-hardening radial-return (algo2code interface hook). -constitutive_latex Phase 6 (P6-2). Mirrors +Mirrors :mod:`algo2code.library.radial_return_j2` (the isotropic power-law variant) for the linear-kinematic (Prager) hardening model: @@ -29,29 +29,11 @@ from __future__ import annotations -from pathlib import Path +from algo2code.library.sources import algorithm_source - -def _load_canonical_source() -> str: - """Read the canonical algpseudocode source from - ``dev/algorithms/radial_return_j2_kinematic.tex``. - - Walks up from this module to the repo root, then reads the file. - Single source of truth — the LaTeX file is authoritative. - """ - here = Path(__file__).resolve() - for parent in here.parents: - candidate = parent / "dev" / "algorithms" / "radial_return_j2_kinematic.tex" - if candidate.is_file(): - return candidate.read_text(encoding="utf-8") - raise FileNotFoundError( - "constitutive_latex Phase 6 (P6-2): canonical kinematic algorithm " - "source not found. Expected at " - "/dev/algorithms/radial_return_j2_kinematic.tex." - ) - - -RADIAL_RETURN_J2_KINEMATIC_LATEX: str = _load_canonical_source() +# Single source of truth: the bundled ``algorithms/radial_return_j2_kinematic.tex`` +# (package data shipped in the wheel) is authoritative. +RADIAL_RETURN_J2_KINEMATIC_LATEX: str = algorithm_source("radial_return_j2_kinematic.tex") def get_radial_return_j2_kinematic_latex() -> str: diff --git a/packages/algo2code/src/algo2code/library/radial_return_j2_mixed.py b/packages/algo2code/src/algo2code/library/radial_return_j2_mixed.py index 1cfbc2c..02ac79d 100644 --- a/packages/algo2code/src/algo2code/library/radial_return_j2_mixed.py +++ b/packages/algo2code/src/algo2code/library/radial_return_j2_mixed.py @@ -1,6 +1,6 @@ r"""J2 mixed-hardening radial-return (algo2code interface hook). -constitutive_latex Phase 6 (P6-3). Mirrors +Mirrors :mod:`algo2code.library.radial_return_j2` (isotropic power-law) and :mod:`algo2code.library.radial_return_j2_kinematic` (linear kinematic) for the MIXED-hardening model — the yield surface BOTH translates @@ -31,29 +31,11 @@ from __future__ import annotations -from pathlib import Path +from algo2code.library.sources import algorithm_source - -def _load_canonical_source() -> str: - """Read the canonical algpseudocode source from - ``dev/algorithms/radial_return_j2_mixed.tex``. - - Walks up from this module to the repo root, then reads the file. - Single source of truth — the LaTeX file is authoritative. - """ - here = Path(__file__).resolve() - for parent in here.parents: - candidate = parent / "dev" / "algorithms" / "radial_return_j2_mixed.tex" - if candidate.is_file(): - return candidate.read_text(encoding="utf-8") - raise FileNotFoundError( - "constitutive_latex Phase 6 (P6-3): canonical mixed algorithm " - "source not found. Expected at " - "/dev/algorithms/radial_return_j2_mixed.tex." - ) - - -RADIAL_RETURN_J2_MIXED_LATEX: str = _load_canonical_source() +# Single source of truth: the bundled ``algorithms/radial_return_j2_mixed.tex`` +# (package data shipped in the wheel) is authoritative. +RADIAL_RETURN_J2_MIXED_LATEX: str = algorithm_source("radial_return_j2_mixed.tex") def get_radial_return_j2_mixed_latex() -> str: diff --git a/packages/algo2code/src/algo2code/library/sources.py b/packages/algo2code/src/algo2code/library/sources.py new file mode 100644 index 0000000..28ecb8a --- /dev/null +++ b/packages/algo2code/src/algo2code/library/sources.py @@ -0,0 +1,90 @@ +"""Access to the canonical algorithm sources bundled with ``algo2code``. + +The ``.tex`` files under ``algo2code/library/algorithms/`` are the single +source of truth for every algorithm the library exposes (and for the solver +seams in ``mechdsl-core`` that transpile them at runtime). They ship as +package data inside the wheel, so an installed ``algo2code`` can read them +without a source checkout — nothing here depends on the repository layout. + +Runtime invariant +----------------- + +This module imports **only stdlib**. +""" + +from __future__ import annotations + +from importlib import resources +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from importlib.resources.abc import Traversable + +#: Names of every bundled algorithm source, as accepted by :func:`algorithm_source`. +ALGORITHM_SOURCES: tuple[str, ...] = ( + "jacobi.tex", + "newmark.tex", + "pcg.tex", + "radial_return_j2.tex", + "radial_return_j2_kinematic.tex", + "radial_return_j2_mixed.tex", +) + + +def _algorithms_dir() -> Traversable: + return resources.files("algo2code.library").joinpath("algorithms") + + +def _resource(name: str) -> Traversable: + resource = _algorithms_dir().joinpath(name) + if not resource.is_file(): + try: + available = sorted( + p.name for p in _algorithms_dir().iterdir() if p.name.endswith(".tex") + ) + except (FileNotFoundError, NotADirectoryError): + available = [] + raise FileNotFoundError( + f"algo2code has no bundled algorithm source named {name!r} " + f"(expected it under algo2code/library/algorithms/). " + f"Available sources: {', '.join(available) or 'none'}. " + "If this is an installed package, the installation is missing its " + "package data — reinstall algo2code." + ) + return resource + + +def algorithm_source(name: str) -> str: + """Return the verbatim LaTeX text of the bundled algorithm source ``name``. + + Parameters + ---------- + name: + File name of the source, e.g. ``"pcg.tex"``. See + :data:`ALGORITHM_SOURCES` for the full list. + + Raises + ------ + FileNotFoundError + If no bundled source of that name exists. + """ + return _resource(name).read_text(encoding="utf-8") + + +def algorithm_source_path(name: str) -> Path: + """Return the on-disk path of the bundled algorithm source ``name``. + + Installed wheels are unpacked, so the resource is a regular file and the + returned path can be read directly. Prefer :func:`algorithm_source` when + only the text is needed; this accessor exists for callers that want to + show *where* the canonical source lives. + """ + return Path(str(_resource(name))) + + +__all__ = [ + "ALGORITHM_SOURCES", + "algorithm_source", + "algorithm_source_path", +] diff --git a/packages/algo2code/tests/test_library_sources.py b/packages/algo2code/tests/test_library_sources.py new file mode 100644 index 0000000..7acdfd4 --- /dev/null +++ b/packages/algo2code/tests/test_library_sources.py @@ -0,0 +1,46 @@ +"""The bundled algorithm sources are package data readable by name.""" + +from __future__ import annotations + +from importlib import resources + +import pytest + +from algo2code.library import ( + ALGORITHM_SOURCES, + algorithm_source, + algorithm_source_path, +) +from algo2code.library.radial_return_j2 import RADIAL_RETURN_J2_LATEX +from algo2code.library.radial_return_j2_kinematic import RADIAL_RETURN_J2_KINEMATIC_LATEX +from algo2code.library.radial_return_j2_mixed import RADIAL_RETURN_J2_MIXED_LATEX + + +@pytest.mark.parametrize("name", ALGORITHM_SOURCES) +def test_every_listed_source_loads(name: str) -> None: + text = algorithm_source(name) + assert text.strip(), f"{name} is empty" + assert "\\begin{algorithmic}" in text, f"{name} does not look like algpseudocode" + assert algorithm_source_path(name).is_file() + + +def test_listed_sources_match_bundled_files() -> None: + bundled = sorted( + entry.name + for entry in resources.files("algo2code.library").joinpath("algorithms").iterdir() + if entry.name.endswith(".tex") + ) + assert bundled == sorted(ALGORITHM_SOURCES) + + +def test_module_constants_are_the_bundled_text() -> None: + assert algorithm_source("radial_return_j2.tex") == RADIAL_RETURN_J2_LATEX + assert algorithm_source("radial_return_j2_kinematic.tex") == RADIAL_RETURN_J2_KINEMATIC_LATEX + assert algorithm_source("radial_return_j2_mixed.tex") == RADIAL_RETURN_J2_MIXED_LATEX + + +def test_unknown_source_is_an_actionable_error() -> None: + with pytest.raises(FileNotFoundError, match=r"no bundled algorithm source named 'nope.tex'"): + algorithm_source("nope.tex") + with pytest.raises(FileNotFoundError, match=r"Available sources: .*pcg\.tex"): + algorithm_source("nope.tex") diff --git a/packages/mechdsl-core/src/mechdsl/__init__.py b/packages/mechdsl-core/src/mechdsl/__init__.py index 8ec8127..55e61a6 100644 --- a/packages/mechdsl-core/src/mechdsl/__init__.py +++ b/packages/mechdsl-core/src/mechdsl/__init__.py @@ -5,9 +5,8 @@ The lower-level :func:`compile` is preserved as a programmatic API that takes a pre-built :class:`~mechdsl.ir.mechanics_ir.ProblemIR`. -See ``README.md`` Support tiers and -``dev/plans/recovery_plan_latex_contract.md`` Phase 2 (R1) for the recovery -context. +See ``README.md`` Support tiers for which entry points are MVP-stable and +which are experimental. """ from __future__ import annotations @@ -81,7 +80,7 @@ def compile_latex( frontend entry point that preserves directive-only behaviour while rejecting unsupported math-bearing sources before IR construction. 2. Adapts the resulting context dict to a :class:`ProblemIR` via - :meth:`ProblemIR.from_latex_semantics` (fgram P6-1). This is the + :meth:`ProblemIR.from_latex_semantics`. This is the LaTeX-semantic constructor: its MVP-stable *core* (dim, formulation, element type, material, boundaries) is identical to :meth:`ProblemIR.from_context`, so the emitted Taichi code is @@ -104,8 +103,7 @@ def compile_latex( ``mechdsl.codegen.moose_printer``) but they are not reachable through this façade and are not part of the stable contract. Reaching those surfaces requires calling the experimental printers directly; see - ``README.md`` Support tiers and - ``dev/plans/recovery_plan_latex_contract.md`` Phase 5 (R4). + ``README.md`` Support tiers. Parameters ---------- @@ -114,7 +112,7 @@ def compile_latex( profile: Compile profile selector. Only ``"mvp"`` is currently accepted; other profiles (e.g. extended LaTeX math grammar via NRPyLaTeX, - non-canonical backends) are deferred to later recovery phases. + non-canonical backends) are not yet available. energy_source: Optional self-contained nrpylatex strain-energy block (``% declare`` metric + ``EDD`` strain, then the scalar energy assignment). When @@ -141,9 +139,9 @@ def compile_latex( - **Dirichlet** — fixed displacement components, applied at solve time by the runtime adapter. - **Neumann** — surface tractions. When the directive carries a - numeric traction triple, post_recovery_plan P1-5 emits an - ``f_ext`` initialisation kernel (see ``f_ext_kernel`` in - *Returns* below) and the bundle is self-contained for that BC. + numeric traction triple, the compiler emits an ``f_ext`` + initialisation kernel (see ``f_ext_kernel`` in *Returns* below) + and the bundle is self-contained for that BC. Neumann BCs with a symbolic-string traction leave the kernel slot empty. @@ -164,9 +162,9 @@ def compile_latex( ArtifactBundle The same bundle :func:`compile` produces, ready for golden-file comparison or Taichi emission. When the parsed problem contains - one or more Neumann BCs with numeric traction (post_recovery_plan - P1-5), the bundle's optional ``f_ext_kernel`` field carries the - emitted ``init_f_ext_from_neumann_`` kernel source. The kernel + one or more Neumann BCs with numeric traction, the bundle's + optional ``f_ext_kernel`` field carries the emitted + ``init_f_ext_from_neumann_`` kernel source. The kernel signature is parametric in ``f_factor`` (= face_area / n_face_nodes) so the runtime mesh adapter supplies the per-node weighting at call time. Pure Dirichlet problems and Neumann BCs @@ -191,8 +189,7 @@ def compile_latex( raise ValueError( f"compile_latex profile={profile!r} is not supported. " f"Allowed profiles: {sorted(ALLOWED_PROFILES)}. " - "Broader profile support is planned for later recovery-plan " - "phases (see dev/plans/recovery_plan_latex_contract.md)." + "Broader profile support is planned." ) if energy_source is not None and energy_file is not None: diff --git a/packages/mechdsl-core/src/mechdsl/codegen/__init__.py b/packages/mechdsl-core/src/mechdsl/codegen/__init__.py index 924a544..62dfddb 100644 --- a/packages/mechdsl-core/src/mechdsl/codegen/__init__.py +++ b/packages/mechdsl-core/src/mechdsl/codegen/__init__.py @@ -4,8 +4,7 @@ --------------------- The MVP-stable canonical backend is :mod:`mechdsl.codegen.taichi_printer`. :mod:`mechdsl.codegen.mfem_printer` and :mod:`mechdsl.codegen.moose_printer` -are preserved in-tree as **experimental** surfaces (see -``dev/plans/recovery_plan_latex_contract.md`` Phase 5 (R4)). +are preserved in-tree as **experimental** surfaces. The ``__experimental__`` convention ----------------------------------- diff --git a/packages/mechdsl-core/src/mechdsl/codegen/anisotropic_emitter.py b/packages/mechdsl-core/src/mechdsl/codegen/anisotropic_emitter.py index f210e91..391a490 100644 --- a/packages/mechdsl-core/src/mechdsl/codegen/anisotropic_emitter.py +++ b/packages/mechdsl-core/src/mechdsl/codegen/anisotropic_emitter.py @@ -8,8 +8,8 @@ 1. a **data-dependent branch** — the fiber term is gated to tension by the Macaulay bracket ```` (active only when ``Ibar4 > 1``); and 2. a **per-element fiber-direction gather** — the fiber stress depends on a - direction ``a`` that is per-element field data (P5-1 - ``ProblemIR.fiber_field`` -> ``ElementIR.fiber_field``), not authored in the + direction ``a`` that is per-element field data + (``ProblemIR.fiber_field`` -> ``ElementIR.fiber_field``), not authored in the LaTeX, so the constitutive function gains a fiber-direction argument that the call site reads from the gathered field. diff --git a/packages/mechdsl-core/src/mechdsl/codegen/artifact.py b/packages/mechdsl-core/src/mechdsl/codegen/artifact.py index de829f9..27fed27 100644 --- a/packages/mechdsl-core/src/mechdsl/codegen/artifact.py +++ b/packages/mechdsl-core/src/mechdsl/codegen/artifact.py @@ -31,9 +31,9 @@ class ContractionPlan: contraction_path: Pairwise contraction order produced by *opt_einsum*. estimated_flops: Estimated floating-point operations for the contraction. tier: Einsum tier classification (1, 2, or 3) — the scheduling axis. - family: Contraction family name (P9-2) — the realisation axis. Stored + family: Contraction family name — the realisation axis. Stored as the enum *name* string for JSON round-trip safety. Defaults - to ``"FALLBACK"`` so bundles serialised before P9-2 deserialise + to ``"FALLBACK"`` so bundles serialised before the family axis existed deserialise cleanly; the Taichi/MFEM/MOOSE printers treat ``"FALLBACK"`` as the legacy emission path. """ @@ -87,22 +87,21 @@ class ArtifactBundle: The bundle captures: * The serialised ProblemIR (``problem_ir_dict``). * The canonical ElementIR contract-surface dict (``element_ir_dict``) — - added by recovery-plan P4-5 (R3.5) to reflect the post-P4-1 IR - ownership: ``ElementIR`` is the primary semantic carrier and the + reflects the IR ownership: ``ElementIR`` is the primary semantic carrier and the ``ContractionPlan`` tuple is the derived optimizer view. Defaults to - an empty dict so pre-P4-5 bundles round-trip cleanly. + an empty dict so older bundles round-trip cleanly. * Element IR metadata (``element_ir_summary``) — legacy summary kept - for back-compat with pre-P4-5 golden files. Excludes callable basis + for back-compat with older golden files. Excludes callable basis functions that cannot be serialised. * Optimised contraction plans (``contraction_plans``) — derived view. * The emitted Taichi source code (``emitted_source``). * Free-form ``metadata`` (commit hash, timestamps, compiler version, …). - Ownership hierarchy (post-P4-5): + Ownership hierarchy: problem_ir_dict ──primary semantic input element_ir_dict ──primary semantic carrier (ElementIR with - P4-1 contract enrichment) + contract enrichment) element_ir_summary──legacy summary, derived from element_ir_dict contraction_plans ──derived optimizer view over element_ir_dict @@ -237,8 +236,8 @@ def content_hash(self) -> str: def to_dict(self) -> dict[str, Any]: """Full serialisation to a JSON-compatible dict. - Always emits ``element_ir_dict`` (the P4-5 canonical contract - surface) so post-P4-5 consumers see the primary semantic carrier; + Always emits ``element_ir_dict`` (the canonical contract + surface) so consumers see the primary semantic carrier; legacy ``element_ir_summary`` rides alongside for back-compat. """ return { @@ -257,7 +256,7 @@ def from_dict(cls, d: dict[str, Any]) -> ArtifactBundle: Raises ``KeyError`` when required fields (``problem_ir_dict``, ``element_ir_summary``) are missing and ``TypeError`` on structural - problems. ``element_ir_dict`` is optional — pre-P4-5 bundles + problems. ``element_ir_dict`` is optional — older bundles rebuild with an empty dict so legacy goldens continue to round-trip. """ missing = [k for k in ("problem_ir_dict", "element_ir_summary") if k not in d] diff --git a/packages/mechdsl-core/src/mechdsl/codegen/einsum_optimizer.py b/packages/mechdsl-core/src/mechdsl/codegen/einsum_optimizer.py index 4f7dda4..51f1343 100644 --- a/packages/mechdsl-core/src/mechdsl/codegen/einsum_optimizer.py +++ b/packages/mechdsl-core/src/mechdsl/codegen/einsum_optimizer.py @@ -106,7 +106,7 @@ class ContractionResult: budget_detail: Human-readable budget summary. family: :class:`Family` classification — the *realisation* decision (which per-backend emitter owns this contraction). Orthogonal to - ``tier``. Added in P9-2. Defaults to :attr:`Family.FALLBACK` so + ``tier``. Defaults to :attr:`Family.FALLBACK` so hand-built ``ContractionResult`` instances (e.g. in unit tests that exercise the budget path) remain valid without having to thread a classification through every construction site. @@ -426,7 +426,7 @@ def family_emitters_enabled() -> bool: tier-only path. Any other non-empty value forces family dispatch. 2. Module-level :data:`FAMILY_EMITTERS_ENABLED` (default ``True``). - The env-var override exists so the P9-2 equivalence tests can exercise + The env-var override exists so the family-emitter equivalence tests can exercise the legacy path in a subprocess without restarting the interpreter. """ env = os.environ.get("MECHDSL_FAMILY_EMITTERS") diff --git a/packages/mechdsl-core/src/mechdsl/codegen/family_registry.py b/packages/mechdsl-core/src/mechdsl/codegen/family_registry.py index c230658..f6d0694 100644 --- a/packages/mechdsl-core/src/mechdsl/codegen/family_registry.py +++ b/packages/mechdsl-core/src/mechdsl/codegen/family_registry.py @@ -1,14 +1,13 @@ -"""Layer 4b — contraction-family registry (Phase 9 / Task P9-1). +"""Layer 4b — contraction-family registry. Authoritative families are defined in ``dev/design_docs/09-EINSUM-OPTIMISER.md §9``. This module is the executable mirror of §9.2-9.4 plus a minimal pattern-matching -classifier used by the P9-1 spec-completeness tests. +classifier used by the spec-completeness tests. -P9-2 will wire :func:`classify_einsum_string` into the -``optimize_contraction`` / ``plan_contraction`` flow and attach the -resulting :class:`Family` to :class:`ContractionResult`. P9-1 only -ships the registry. +:mod:`mechdsl.codegen.einsum_optimizer` wires :func:`classify_einsum_string` +into the ``optimize_contraction`` / ``plan_contraction`` flow and attaches +the resulting :class:`Family` to :class:`ContractionResult`. """ from __future__ import annotations @@ -203,9 +202,9 @@ def classify_einsum_string( ) -> Family: """Classify an einsum contraction into a named :class:`Family`. - This is the P9-1 design scaffold. Unrecognised strings resolve to - :attr:`Family.FALLBACK`; P9-2 will refine the pattern-matching - using operand shapes to split coarser families if needed. + Unrecognised strings resolve to :attr:`Family.FALLBACK`; the + pattern-matching may later be refined using operand shapes to split + coarser families if needed. """ _ = operand_shapes # reserved for future refinement if einsum_string in _EXACT_MATCH: diff --git a/packages/mechdsl-core/src/mechdsl/codegen/hex8_tables.py b/packages/mechdsl-core/src/mechdsl/codegen/hex8_tables.py index 14cceda..372b08a 100644 --- a/packages/mechdsl-core/src/mechdsl/codegen/hex8_tables.py +++ b/packages/mechdsl-core/src/mechdsl/codegen/hex8_tables.py @@ -162,7 +162,7 @@ def current_gradient_at_physical( The ``X_elem`` argument is retained for API symmetry with :func:`reference_gradient_at_physical` and for future callers that need - to compute both J0 and j in a single pass (e.g. the P1-4 tangent + to compute both J0 and j in a single pass (e.g. the UL tangent emission where the Jaumann material term needs J0 and the geometric stiffness term needs j). At ``F = I`` (``x_elem == X_elem``) the two helpers return byte-identical output. diff --git a/packages/mechdsl-core/src/mechdsl/codegen/hourglass.py b/packages/mechdsl-core/src/mechdsl/codegen/hourglass.py index 6fd63de..0753c31 100644 --- a/packages/mechdsl-core/src/mechdsl/codegen/hourglass.py +++ b/packages/mechdsl-core/src/mechdsl/codegen/hourglass.py @@ -208,8 +208,7 @@ def _hourglass_stiffness_scalar(V_e: float, mu: float, lambda_h: float) -> float The classical FB form has an hourglass coefficient times a shear modulus times a characteristic-area factor ``V_e^(2/3)`` (equivalent to - ``h^(d-2) vol`` with ``h ~ V_e^(1/3)`` in 3D, per the scope statement of - task P5-5): + ``h^(d-2) vol`` with ``h ~ V_e^(1/3)`` in 3D): epsilon = lambda_h * mu * V_e**(2/3) @@ -254,10 +253,9 @@ def flanagan_belytschko_force( where ``gamma_alpha`` are the geometry-projected hourglass vectors (FB 1981 eq. 2.33) and ``epsilon = lambda_h * mu * V_e^(2/3)`` is the - scalar hourglass stiffness (section 4 of the paper; see also the - scope statement of task P5-5 which specifies - ``k_h * lambda_h * h^(d-2) / vol_e`` — in 3D this is equivalent up - to the ``k_h * mu`` grouping). + scalar hourglass stiffness (section 4 of the paper; the equivalent + ``k_h * lambda_h * h^(d-2) / vol_e`` form differs only by the + ``k_h * mu`` grouping in 3D). The output has the sign convention of an internal resisting force: it acts to oppose motion along hourglass modes, so it should be diff --git a/packages/mechdsl-core/src/mechdsl/codegen/mfem_printer.py b/packages/mechdsl-core/src/mechdsl/codegen/mfem_printer.py index f455cf4..52a9f6b 100644 --- a/packages/mechdsl-core/src/mechdsl/codegen/mfem_printer.py +++ b/packages/mechdsl-core/src/mechdsl/codegen/mfem_printer.py @@ -1,7 +1,6 @@ """MFEM C++ printer: ArtifactBundle → standalone ``.cpp`` file. -Support tier: **experimental** (see ``README.md`` Support tiers and -``dev/plans/recovery_plan_latex_contract.md`` Phase 5 (R4)). +Support tier: **experimental** (see ``README.md`` Support tiers). The MVP-stable canonical compile path is Taichi only; this backend is preserved in tree but is not part of the contract surface. @@ -9,7 +8,7 @@ ``mfem::ParNonlinearForm`` and a custom ``mfem::NonlinearFormIntegrator`` subclass to solve the same problem that the Taichi printer emits. -The MVP scope (Plan B §8, task P8-1): +The MVP scope: * Hex8 only - other element types raise ``NotImplementedError``. * :class:`~mechdsl.ir.mechanics_ir.DynamicsMode.STATIC` only - explicit @@ -23,7 +22,7 @@ Parse-only verification: no local MFEM install is required. Tests use ``clang-format`` (or a structural fallback) - see ``packages/mechdsl-core/tests/test_mfem_printer.py`` - and the emitted app -is compiled against MFEM in CI (Plan B task P8-3). +is compiled against MFEM in CI. """ from __future__ import annotations @@ -144,7 +143,7 @@ def _emit_family_material_tangent_contraction_mfem(ctx: EmissionContext) -> None def _emit_family_fallback_mfem(ctx: EmissionContext) -> None: """MFEM fallback marker. Unused on the MVP Hex8 + SVK happy path.""" - ctx.emit("// (P9-2 fallback: no MFEM template override)") + ctx.emit("// (family fallback: no MFEM template override)") family_emitters: dict[Family, Callable[..., None]] = { @@ -439,7 +438,7 @@ def emit_force_integrator(ctx: EmissionContext, bundle: ArtifactBundle) -> None: Residual comes from ``AssembleElementVector`` and the consistent tangent from ``AssembleElementGrad``. The implementation is a deliberately simple - Hex8/SVK wiring — end-to-end compile + solve verification lands in P8-3. + Hex8/SVK wiring — end-to-end compile + solve verification is deferred. """ del bundle ctx.emit("// ---------------------------------------------------------------------") @@ -543,7 +542,7 @@ def emit_tangent_integrator(ctx: EmissionContext) -> None: accumulates ``elmat += w * det(J) * B^T C_eng B``. The small-strain ``B`` operator is used here; the geometric (initial- - stress) contribution is left to the base-class / P8-3 compile pass. + stress) contribution is left to the base-class / a later compile pass. All nested loops close before ``elmat`` is written, so every index used in the write is in scope. """ @@ -627,7 +626,7 @@ def emit_tangent_integrator(ctx: EmissionContext) -> None: ctx.emit("}") ctx.emit("") ctx.emit("// NOTE: geometric (initial-stress) tangent contribution is") - ctx.emit("// Deferred to Plan B §B8 P8-3 (full compile + verification pass).") + ctx.emit("// Deferred to the full compile + verification pass.") ctx.emit("}") ctx.emit("}") ctx.emit("") @@ -639,7 +638,7 @@ def emit_main(ctx: EmissionContext, bundle: ArtifactBundle) -> None: ctx.emit("// ---------------------------------------------------------------------") ctx.emit("// MPI-parallel driver. Uses mfem::ParNonlinearForm so the problem") ctx.emit("// distributes across ranks out of the box. Mesh + BC wiring is left") - ctx.emit("// deliberately skeletal — P8-3 extends it with a real mesh reader.") + ctx.emit("// deliberately skeletal — Deferred: a real mesh reader extends it.") ctx.emit("// ---------------------------------------------------------------------") ctx.emit("int main(int argc, char *argv[])") ctx.emit("{") @@ -671,9 +670,9 @@ def emit_main(ctx: EmissionContext, bundle: ArtifactBundle) -> None: ctx.emit("") ctx.emit("// NOTE: skeletal essential-BC setup — full boundary-condition") ctx.emit("// wiring (per-attribute Dirichlet tags, traction / Neumann loads,") - ctx.emit("// inhomogeneous prescribed displacements) is Deferred to Plan B") - ctx.emit("// §B8 P8-3. The present all-attributes-fixed path is sufficient") - ctx.emit("// for the parse-check + compile smoke-test in P8-3.") + ctx.emit("// inhomogeneous prescribed displacements) is Deferred to the full") + ctx.emit("// compile pass. The present all-attributes-fixed path is sufficient") + ctx.emit("// for the parse-check + compile smoke-test.") ctx.emit("Array ess_tdof_list;") ctx.emit("if (pmesh.bdr_attributes.Size()) {") with ctx.indent_block(): @@ -704,7 +703,7 @@ def emit_main(ctx: EmissionContext, bundle: ArtifactBundle) -> None: ctx.emit("newton.SetPrintLevel(1);") ctx.emit("") ctx.emit("// NOTE: zero RHS placeholder — external-load plumbing is Deferred") - ctx.emit("// to Plan B §B8 P8-3. Size the RHS to match the true-DOF vector so") + ctx.emit("// to the full compile pass. Size the RHS to match the true-DOF vector so") ctx.emit("// NewtonSolver::Mult sees a well-formed operand.") ctx.emit("Vector zero(U.Size());") ctx.emit("zero = 0.0;") diff --git a/packages/mechdsl-core/src/mechdsl/codegen/moose_printer.py b/packages/mechdsl-core/src/mechdsl/codegen/moose_printer.py index fffce0b..11c684b 100644 --- a/packages/mechdsl-core/src/mechdsl/codegen/moose_printer.py +++ b/packages/mechdsl-core/src/mechdsl/codegen/moose_printer.py @@ -1,11 +1,10 @@ """MOOSE backend code printer — ArtifactBundle -> ComputeStressBase subclass. -Support tier: **experimental** (see ``README.md`` Support tiers and -``dev/plans/recovery_plan_latex_contract.md`` Phase 5 (R4)). +Support tier: **experimental** (see ``README.md`` Support tiers). The MVP-stable canonical compile path is Taichi only; this backend is preserved in tree but is not part of the contract surface. -Plan B Phase 8, Task P8-2. Emits a MOOSE-flavoured C++ material class that +Emits a MOOSE-flavoured C++ material class that subclasses ``ComputeStressBase`` and maps MVP tensor types to MOOSE's ``RankTwoTensor`` / ``RankFourTensor``. The printer also fills a shipped ``moose_template/input_template.i`` deck so users get a ready-to-run tension @@ -138,7 +137,7 @@ def _emit_family_tangent_double_contraction_moose(ctx: EmissionContext) -> None: def _emit_family_fallback_moose(ctx: EmissionContext) -> None: """MOOSE fallback marker. Unused on the SVK Hex8 happy path.""" - ctx.emit("// (P9-2 fallback: no MOOSE template override)") + ctx.emit("// (family fallback: no MOOSE template override)") family_emitters: dict[Family, Callable[..., None]] = { @@ -460,7 +459,7 @@ def emit_cpp(bundle: ArtifactBundle) -> str: ctx.emit(f"void {class_name}::computeQpStress()") ctx.emit("{") with ctx.indent_block(): - ctx.emit("// Total-Lagrangian SVK stress update. Plan B Phase 8 MVP.") + ctx.emit("// Total-Lagrangian SVK stress update (MVP).") ctx.emit("// Reads _mechanical_strain[_qp] (a RankTwoTensor) and emits") ctx.emit("// _stress[_qp] = 2*mu*E + lambda*tr(E)*I as a RankTwoTensor.") ctx.emit("const RankTwoTensor E = _mechanical_strain[_qp];") diff --git a/packages/mechdsl-core/src/mechdsl/codegen/taichi_printer.py b/packages/mechdsl-core/src/mechdsl/codegen/taichi_printer.py index 7da70b3..c89698f 100644 --- a/packages/mechdsl-core/src/mechdsl/codegen/taichi_printer.py +++ b/packages/mechdsl-core/src/mechdsl/codegen/taichi_printer.py @@ -16,14 +16,14 @@ Uses ``ti.f64`` throughout. Physics indices -> ``ti.static`` in emitted code; mesh indices -> runtime loops. See ``dev/design_docs/07-CONVENTIONS.md``. -Recovery P5-4 — enriched IR consumption ---------------------------------------- +Enriched IR consumption +----------------------- The printer prefers :attr:`ArtifactBundle.element_ir_dict` (the canonical -post-P4-5 contract surface carrying ``ElementIR.to_dict()``) over the +contract surface carrying ``ElementIR.to_dict()``) over the legacy :attr:`ArtifactBundle.element_ir_summary` for element-identity fields (``element_type``, ``dim``, ``n_nodes``, ``n_quadrature_points``). The :func:`_ir_field` helper performs the priority lookup and falls back -to ``element_ir_summary`` when ``element_ir_dict`` is empty (pre-P4-5 +to ``element_ir_summary`` when ``element_ir_dict`` is empty (older bundles), keeping legacy callers byte-identical. When enrichment blocks (``geometry``, ``material_eval``, ``local_force``, @@ -34,8 +34,8 @@ remain stable; tests that exercise the auditability path opt-in explicitly. -P9-2 template-family dispatch ------------------------------ +Template-family dispatch +------------------------ Per-contraction shape choices (displacement-gradient scatter, force integration, tangent contractions) are routed through the module-level :data:`family_emitters` dict, keyed on :class:`Family`. Every contraction @@ -80,7 +80,7 @@ class EmissionContext: """Tracks state during code emission. The ``verbose`` flag (default ``False``) gates auditability comments - that surface enriched-IR contract fields (P5-4) in the emitted file's + that surface enriched-IR contract fields in the emitted file's docstring header. Defaults to off so existing golden snapshots remain byte-identical; tests / debugging callers may set it to ``True`` to inspect the contract surface that drove the emission. @@ -138,7 +138,7 @@ def _ir_field(bundle: ArtifactBundle, key: str, default: Any) -> Any: Parameters ---------- bundle : ArtifactBundle - The pipeline artifact bundle. Post-P4-5 bundles carry both + The pipeline artifact bundle. Enriched bundles carry both ``element_ir_dict`` (canonical) and ``element_ir_summary`` (legacy). key : str The field name to read. Note that ``element_ir_dict`` uses @@ -157,7 +157,7 @@ def _ir_field(bundle: ArtifactBundle, key: str, default: Any) -> Any: ------- Any The value from ``element_ir_dict[key]`` if present and the dict is - non-empty (post-P4-5 path); else from ``element_ir_summary[key]`` + non-empty (canonical path); else from ``element_ir_summary[key]`` (legacy back-compat); else *default*. Typed as ``Any`` because ``ArtifactBundle`` dict payloads are heterogeneous (``int`` for ``n_nodes``/``dim``, ``str`` for ``element_type``, nested ``dict`` @@ -173,12 +173,12 @@ def _ir_field(bundle: ArtifactBundle, key: str, default: Any) -> Any: def _ir_block(bundle: ArtifactBundle, block_name: str) -> dict[str, object] | None: - """Return one of the four P4-1 enrichment blocks, or ``None``. + """Return one of the four enrichment blocks, or ``None``. The enrichment blocks live under :attr:`ArtifactBundle.element_ir_dict` - (canonical) with a redundant copy under :attr:`element_ir_summary` - (P4-3). This helper prefers the canonical dict and falls back to the - summary when ``element_ir_dict`` is empty (pre-P4-5 bundles). + (canonical) with a redundant copy under :attr:`element_ir_summary`. + This helper prefers the canonical dict and falls back to the + summary when ``element_ir_dict`` is empty (older bundles). Parameters ---------- @@ -207,7 +207,7 @@ def _ir_block(bundle: ArtifactBundle, block_name: str) -> dict[str, object] | No def _n_quadrature_points(bundle: ArtifactBundle, default: int = 8) -> int: """Quadrature-point count, preferring the geometry block over summary. - Post-P4-5 bundles expose ``n_quad`` via ``element_ir_dict["geometry"]``; + Enriched bundles expose ``n_quad`` via ``element_ir_dict["geometry"]``; legacy bundles only expose ``n_quadrature_points`` on the summary. Both paths converge here so the printer's emit_constants only has one code path. @@ -228,8 +228,7 @@ def _is_plastic_material(model: str) -> bool: """Return True when *model* follows the plastic/damage history-field path. Models that need per-QP ``alpha`` history plus a radial-return - ``constitutive_update_plastic`` in the emitted kernel. Lemaitre (Plan B - phase B6, task P6-2) layers a damage update on top of the J2 power-law + ``constitutive_update_plastic`` in the emitted kernel. Lemaitre layers a damage update on top of the J2 power-law return map, so it shares the same ``alpha``/``plastic_strain`` plumbing plus its own ``damage_D``/``is_deleted`` fields (see :func:`mechdsl.solver.history_fields.create_lemaitre_history`). @@ -245,7 +244,7 @@ def _is_damage_material(model: str) -> bool: def _emits_generated_tangent_matvec(bundle: ArtifactBundle) -> bool: """True when the generated matrix-free tangent matvec ``@ti.kernel`` is emitted. - The runtime-q kernels (PlanJune14 WI-1) from + The runtime-q kernels from :func:`emit_svk_tangent_matvec_kernel` / :func:`emit_j2_tangent_matvec_kernel` are the SOLE consumers of the device-resident quadrature fields ``_GRAD_AT_QUAD_F`` / ``_QUAD_WEIGHTS_F``. They are emitted only for the @@ -253,7 +252,7 @@ def _emits_generated_tangent_matvec(bundle: ArtifactBundle) -> bool: in :func:`emit`, ~L3239/3247). Gating the quad-field emission on this same predicate keeps every other golden (Lemaitre, UL, derived, explicit) byte-identical — otherwise dead quad-field declarations leak into formulations - that never use them (the WI-1 regression the UL golden test caught). + that never use them (a regression the UL golden test caught). """ model = bundle.problem_ir_dict.get("material", {}).get("model", "svk") configuration = bundle.problem_ir_dict.get("configuration", "reference") @@ -493,7 +492,7 @@ def _emit_family_fallback_taichi(ctx: EmissionContext) -> None: contraction hits this path, but the entry exists so the dispatch table is total over :data:`FAMILIES`. """ - ctx.emit("# (P9-2 fallback: no template override — caller emits inline)") + ctx.emit("# (family fallback: no template override — caller emits inline)") # Family-emitter dispatch table. Keyed on :class:`Family`; each @@ -543,10 +542,10 @@ def _dispatch_family(family: Family, ctx: EmissionContext, *args: object) -> boo def emit_preamble(ctx: EmissionContext, bundle: ArtifactBundle) -> None: """Emit imports and ti.init. - Recovery P5-4: element identity fields are sourced from - ``element_ir_dict`` first (post-P4-5 canonical contract surface), + Element identity fields are sourced from + ``element_ir_dict`` first (the canonical contract surface), falling back to ``element_ir_summary`` for legacy bundles. When - ``ctx.verbose`` is true and the four P4-1 enrichment blocks are + ``ctx.verbose`` is true and the four enrichment blocks are populated, additional auditability comments surface ``material_eval`` / ``geometry`` / ``local_force`` / ``local_tangent`` fields in the docstring header. Verbose comments are gated to keep @@ -580,7 +579,7 @@ def emit_preamble(ctx: EmissionContext, bundle: ArtifactBundle) -> None: def _emit_enrichment_audit(ctx: EmissionContext, bundle: ArtifactBundle) -> None: - """Emit auditability comments for the four P4-1 enrichment blocks. + """Emit auditability comments for the four enrichment blocks. Called from :func:`emit_preamble` only when ``ctx.verbose`` is true. Each line is a ``# Field: value`` comment surfacing one fact from the @@ -598,7 +597,7 @@ def _emit_enrichment_audit(ctx: EmissionContext, bundle: ArtifactBundle) -> None return ctx.emit("") - ctx.emit("Enriched-IR contract surface (recovery P5-4 audit):") + ctx.emit("Enriched-IR contract surface (audit):") if material_eval is not None: if "stress_measure" in material_eval: ctx.emit(f" Stress measure : {material_eval['stress_measure']}") @@ -636,7 +635,7 @@ def emit_constants(ctx: EmissionContext, bundle: ArtifactBundle) -> None: Writes Hex8 quadrature tables as Python list literals that Taichi kernels can index into at compile time via ``ti.static``. - Recovery P5-4: ``n_nodes`` and ``dim`` come from + ``n_nodes`` and ``dim`` come from :func:`_ir_field` (prefers ``element_ir_dict`` over the legacy ``element_ir_summary``); the quadrature-point count comes from :func:`_n_quadrature_points`, which prefers the @@ -703,7 +702,7 @@ def emit_field_declarations(ctx: EmissionContext, bundle: ArtifactBundle) -> Non ctx.emit("elem_nodes = ti.field(dtype=ti.i32) # connectivity") if _emits_generated_tangent_matvec(bundle): ctx.emit("") - ctx.emit("# Quadrature tables as ti.fields (PlanJune14 WI-1): the matrix-free") + ctx.emit("# Quadrature tables as ti.fields: the matrix-free") ctx.emit("# tangent matvec kernels run the q-loop at RUNTIME (the ÷8 JIT-budget") ctx.emit("# lever), so they cannot index the Python-list constants GRAD_AT_QUAD /") ctx.emit("# QUAD_WEIGHTS by a runtime q. These device-resident copies are filled") @@ -1021,7 +1020,7 @@ def _emit_j2_constitutive(ctx: EmissionContext, params: dict[str, object]) -> No def _emit_lemaitre_constitutive(ctx: EmissionContext, params: dict[str, object]) -> None: """Emit Lemaitre damage-coupled J2 power-law constitutive update. - Plan B phase B6, task P6-2. Wraps :func:`_emit_j2_constitutive`'s + Wraps :func:`_emit_j2_constitutive`'s ``constitutive_update_plastic`` (which implements the undamaged radial return on the elastic predictor — i.e. the effective-stress field by strain equivalence). After the J2 step the wrapper evaluates the de @@ -1033,12 +1032,12 @@ def _emit_lemaitre_constitutive(ctx: EmissionContext, params: dict[str, object]) then forms the nominal PK2 stress ``sigma_nominal = (1 - D_new) * sigma_eff``. The returned ``alpha_new`` and ``D_new`` are written back by the caller. - Caveat (Gate B medium, P6-1 forward-warning): the tangent used by + Caveat: the tangent used by ``tangent_matvec`` is the **undamaged J2** algorithmic tangent, not a damage-aware consistent tangent. Under actively evolving damage Newton drops from super-linear to sub-linear convergence; this is acceptable - for P6-2/P6-3 because the notched bar runs quasi-statically with small - D increments per step. A damage-aware tangent is a Phase 10 V&V item. + for the notched-bar benchmark, which runs quasi-statically with small + D increments per step. A damage-aware tangent is future V&V work. """ ctx.emit("@ti.func") ctx.emit("def constitutive_update_lemaitre(") @@ -1279,13 +1278,13 @@ def emit_internal_force_kernel(ctx: EmissionContext, bundle: ArtifactBundle) -> # know the generated residual shape mutates across Newton steps only # by dropping elements (never by bringing them back). if is_damage: - ctx.emit("# NOTE (P6-2, Plan B phase B6): element deletion is one-way. Once") + ctx.emit("# NOTE: element deletion is one-way. Once") ctx.emit("# ``is_deleted[e] != 0`` the element contributes zero to both the") ctx.emit("# internal force and the tangent matvec. The residual-norm sum") ctx.emit("# therefore ranges only over active DOFs; Newton's convergence") ctx.emit("# tolerances are unaffected because deleted-element residual rows") ctx.emit("# are already zeroed here. Tangent uses the undamaged J2 tangent") - ctx.emit("# (see P6-1 forward-warning, Option A).") + ctx.emit("# (see the undamaged-tangent caveat in the Lemaitre model notes).") ctx.emit("") if is_damage: @@ -1321,7 +1320,7 @@ def emit_internal_force_kernel(ctx: EmissionContext, bundle: ArtifactBundle) -> ctx.emit("for e in range(n_elem):") with ctx.indent_block(): if is_damage: - ctx.emit("# P6-2 element deletion: skip elements flagged as failed") + ctx.emit("# Element deletion: skip elements flagged as failed") ctx.emit("if is_deleted[e] != 0:") with ctx.indent_block(): ctx.emit("continue") @@ -1467,7 +1466,7 @@ def emit_internal_force_kernel(ctx: EmissionContext, bundle: ArtifactBundle) -> # --- End of QP loop: damage may have advanced; check deletion --- if is_damage: ctx.emit("") - ctx.emit("# P6-2: flag element as deleted if any QP breaches D_crit.") + ctx.emit("# Flag element as deleted if any QP breaches D_crit.") ctx.emit("# One-way: once set, stays set (see module docstring note).") ctx.emit("elem_failed = 0") ctx.emit("for q_chk in ti.static(range(N_QP)):") @@ -1494,7 +1493,7 @@ def _emit_tl_tangent_qp_body( ) -> None: """Emit the Total Lagrangian QP-inner body for the tangent matvec. - Factored out of ``emit_tangent_matvec_kernel`` by P1-4 to enable TL/UL + Factored out of ``emit_tangent_matvec_kernel`` to enable TL/UL dispatch. ``derived_lines`` (when given) replaces the SVK/J2 material branch with the LaTeX-derived stress + ``dS = C_IJKL : dE`` lines from :func:`energy_emitter.emit_derived_tangent_matvec_body`, so a derived bundle @@ -1739,9 +1738,9 @@ def emit_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) -> ctx.emit("") ctx.emit("Notes") ctx.emit("-----") - ctx.emit("Lemaitre path (P6-2) uses the **undamaged J2** algorithmic tangent.") - ctx.emit("A damage-aware consistent tangent is a Phase 10 V&V item — see the") - ctx.emit("P6-1 forward-warning at ``symbolic/models/lemaitre.py:244-252``.") + ctx.emit("Lemaitre path uses the **undamaged J2** algorithmic tangent.") + ctx.emit("A damage-aware consistent tangent is future V&V work — see the") + ctx.emit("undamaged-tangent caveat in ``symbolic/models/lemaitre.py``.") ctx.emit("Under actively evolving damage Newton drops from super-linear to") ctx.emit("sub-linear convergence; acceptable for quasi-static notched-bar runs.") ctx.emit("Deleted elements (``is_deleted[e] != 0``) contribute zero to K @ v.") @@ -1802,7 +1801,7 @@ def emit_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) -> ctx.emit("for e in range(n_elem):") with ctx.indent_block(): if is_damage: - ctx.emit("# P6-2: deleted elements contribute zero to the tangent matvec") + ctx.emit("# Deleted elements contribute zero to the tangent matvec") ctx.emit("if is_deleted_np[e] != 0:") with ctx.indent_block(): ctx.emit("continue") @@ -1960,7 +1959,7 @@ def _emit_svk_consistent_tangent_A(ctx: EmissionContext) -> None: def _emit_optimised_matvec_contraction(ctx: EmissionContext, plan: ContractionPlan) -> None: - """Emit the P3-1 optimiser-recorded matvec contraction as ``ti.static`` loops. + """Emit the optimiser-recorded matvec contraction as ``ti.static`` loops. The matrix-free tangent contraction is ``qaI,qiIjJ,qbJ,bj->qai`` with the operand order ``(dN, A, dN, v)``. We do **not** hand-roll a contraction @@ -1980,14 +1979,14 @@ def _emit_optimised_matvec_contraction(ctx: EmissionContext, plan: ContractionPl keeps the contraction optimiser-derived rather than hand-written, while fitting the JIT budget (203 estimated lines, Tier 2). - Guard: P3-2's MVP path assumes the canonical 3-step SVK Hex8 path. If a + Guard: the MVP path assumes the canonical 3-step SVK Hex8 path. If a future ElementIR yields a different path the emitter fails loud rather than silently emitting a mismatched contraction. """ expected_path = [(2, 3), (1, 2), (0, 1)] if list(plan.contraction_path) != expected_path: raise NotImplementedError( - "P3-2 generated matrix-free SVK tangent expects the canonical " + "The generated matrix-free SVK tangent expects the canonical " f"opt_einsum path {expected_path} for '{plan.einsum_string}', got " f"{plan.contraction_path}. A path-generic emitter is planned for a " "later phase (multi-element support); for the Hex8 SVK MVP the " @@ -2042,20 +2041,20 @@ def _emit_optimised_matvec_contraction(ctx: EmissionContext, plan: ContractionPl def emit_svk_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) -> None: - """Emit the generated ``@ti.kernel`` matrix-free SVK tangent operator (P3-2). + """Emit the generated ``@ti.kernel`` matrix-free SVK tangent operator. Emits ``svk_tangent_matvec_apply(out, v, lam, mu)`` — a ``@ti.kernel`` that applies ``K(u)·v`` fully matrix-free over all elements. It recomputes the tangent per matvec (locked decision D-A; element tangents are never stored), forms the consistent two-point SVK tangent ``A(i,I,j,J)`` per quadrature point from the Tier-1 ``ti_runtime`` kinematics helpers, and - applies it to ``v`` via the **P3-1 optimiser-recorded contraction path** + applies it to ``v`` via the **optimiser-recorded contraction path** (consumed from :func:`build_tangent_matvec_plan`). The result scatters to the global ``out`` field via atomic add, targeting the ``ti_runtime`` ``apply_A(out, x)`` injection seam. Only emitted for the SVK / Total-Lagrangian / reference / named-model path - (see :func:`emit`). J2's algorithmic tangent (P5-1), the LaTeX-derived + (see :func:`emit`). J2's algorithmic tangent, the LaTeX-derived tangent, and the Updated-Lagrangian push-forward stay on the host ``tangent_matvec`` route for now. @@ -2074,10 +2073,10 @@ def emit_svk_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) plan = build_tangent_matvec_plan(element_ir) ctx.emit("# " + "=" * 70) - ctx.emit("# Generated matrix-free SVK tangent @ti.kernel (PlanJune14 P3-2)") + ctx.emit("# Generated matrix-free SVK tangent @ti.kernel") ctx.emit("#") ctx.emit("# Applies K(u)·v fully matrix-free (D-A: element tangents never stored),") - ctx.emit("# recomputed per matvec. The tangent contraction rides the P3-1") + ctx.emit("# recomputed per matvec. The tangent contraction rides the") ctx.emit( f"# opt_einsum ContractionPlan ('{plan.einsum_string}', path {list(plan.contraction_path)}," ) @@ -2103,7 +2102,7 @@ def emit_svk_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) ctx.emit("Taichi's implicit atomic add. ``u`` (current Newton iterate) and the") ctx.emit("mesh fields ``x_ref`` / ``elem_nodes`` are read live.") ctx.emit("") - ctx.emit("The tangent contraction is the P3-1 opt_einsum ContractionPlan for") + ctx.emit("The tangent contraction is the opt_einsum ContractionPlan for") ctx.emit(f"``{plan.einsum_string}`` (operands dNdX, A, dNdX, v), realised as the") ctx.emit(f"recorded path {list(plan.contraction_path)} (tier {plan.tier}, within") ctx.emit("the 512-line @ti.func JIT budget).") @@ -2125,7 +2124,7 @@ def emit_svk_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) with ctx.indent_block(): ctx.emit("X_elem[a, d] = x_ref[nid][d]") ctx.emit("") - ctx.emit("# Quadrature loop (RUNTIME — PlanJune14 WI-1 JIT-budget lever).") + ctx.emit("# Quadrature loop (RUNTIME — the JIT-budget lever).") ctx.emit("# Runtime q divides the per-QP unroll by N_QP (8); physics indices") ctx.emit("# i,I,j,J,a,d stay ti.static. The body can no longer index the") ctx.emit("# Python-list quad constants by a runtime q, so it reads the") @@ -2174,10 +2173,10 @@ def emit_svk_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) # --------------------------------------------------------------------------- -# PlanJune14 P5-1 — generated @ti.kernel matrix-free J2 algorithmic tangent +# Generated @ti.kernel matrix-free J2 algorithmic tangent # -# The dissipative-model counterpart of ``emit_svk_tangent_matvec_kernel`` -# (P3-2). J2 plasticity is dissipative, so the tangent is the **algorithmic +# The dissipative-model counterpart of ``emit_svk_tangent_matvec_kernel``. +# J2 plasticity is dissipative, so the tangent is the **algorithmic # consistent tangent** — the linearisation of the radial-return map, NOT # ∂²Ψ/∂E² (``.claude/rules/symbolic.md``). It is state-dependent: every matvec # re-runs the return map per quadrature point with the **on-device** history @@ -2193,10 +2192,10 @@ def emit_svk_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) # # (matching ``ref_hex8_plastic.element_tangent_matvec_plastic``: # ``dP = grad_v @ S + F @ (C_ep : dE)`` with ``dE = sym(F^T grad_v)``). Once -# ``A`` is formed the matvec rides the **same** P3-1 opt_einsum ContractionPlan +# ``A`` is formed the matvec rides the **same** opt_einsum ContractionPlan # (``qaI,qiIjJ,qbJ,bj->qai``) realised by ``_emit_optimised_matvec_contraction`` # — the contraction is path-driven and JIT-budget-checked exactly as for SVK -# (Tier 2, ~203 lines, primary PJ risk). The on-device ``C_ep`` assembly uses +# (Tier 2, ~203 lines). The on-device ``C_ep`` assembly uses # the closed Simo & Hughes §3.4 Box 3.5 form, byte-for-byte the algebra of # ``j2_power_law.assemble_j2_like_tangent`` / ``elastic_tangent``, so the # generated operator matches the reference algorithmic tangent to f64 precision. @@ -2400,7 +2399,7 @@ def _emit_j2_consistent_tangent_A(ctx: EmissionContext) -> None: def emit_j2_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) -> None: - """Emit the generated ``@ti.kernel`` matrix-free J2 tangent operator (P5-1). + """Emit the generated ``@ti.kernel`` matrix-free J2 tangent operator. Emits ``j2_tangent_matvec_apply(out, v, lam, mu, sigma_y0, K_hard, n_hard)`` — a ``@ti.kernel`` that applies the **J2 algorithmic consistent tangent** @@ -2413,7 +2412,7 @@ def emit_j2_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) stress ``S`` and the rank-4 algorithmic tangent ``C_ep`` (:func:`_emit_j2_algorithmic_tangent_C`), folds them into the consistent two-point tangent ``A(i,I,j,J)`` (:func:`_emit_j2_consistent_tangent_A`), - and applies it to ``v`` via the **same** P3-1 optimiser-recorded contraction + and applies it to ``v`` via the **same** optimiser-recorded contraction path as SVK (:func:`_emit_optimised_matvec_contraction`). The result scatters to the global ``out`` field via atomic add, targeting the ``ti_runtime`` ``apply_A(out, x)`` injection seam. @@ -2433,14 +2432,14 @@ def emit_j2_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) plan = build_tangent_matvec_plan(element_ir) ctx.emit("# " + "=" * 70) - ctx.emit("# Generated matrix-free J2 algorithmic-tangent @ti.kernel (PlanJune14 P5-1)") + ctx.emit("# Generated matrix-free J2 algorithmic-tangent @ti.kernel") ctx.emit("#") ctx.emit("# Applies the J2 ALGORITHMIC CONSISTENT TANGENT K(u)·v fully matrix-free") ctx.emit("# (D-A: element tangents never stored), recomputed per matvec. The tangent") ctx.emit("# is the linearisation of the radial-return map (NOT d2Psi/dE2 — J2 is") ctx.emit("# dissipative; see .claude/rules/symbolic.md). History alpha[e, q] is read") ctx.emit("# on-device (read-only); it advances only in compute_internal_force. The") - ctx.emit(f"# contraction rides the P3-1 opt_einsum ContractionPlan ('{plan.einsum_string}',") + ctx.emit(f"# contraction rides the opt_einsum ContractionPlan ('{plan.einsum_string}',") ctx.emit(f"# path {list(plan.contraction_path)}, tier {plan.tier}) — the same path as the") ctx.emit("# SVK kernel; kinematics use the Tier-1 ti_runtime @ti.func helpers.") ctx.emit("# " + "=" * 70) @@ -2468,7 +2467,7 @@ def emit_j2_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) ctx.emit("live; ``alpha`` is NOT written (history advances in compute_internal_force).") ctx.emit("") ctx.emit("The tangent is the J2 algorithmic consistent tangent (linearisation of") - ctx.emit("the radial-return map). The contraction is the P3-1 opt_einsum") + ctx.emit("the radial-return map). The contraction is the opt_einsum") ctx.emit(f"ContractionPlan for ``{plan.einsum_string}`` (operands dNdX, A, dNdX, v),") ctx.emit(f"realised as the recorded path {list(plan.contraction_path)} (tier {plan.tier}).") ctx.emit('"""') @@ -2489,7 +2488,7 @@ def emit_j2_tangent_matvec_kernel(ctx: EmissionContext, bundle: ArtifactBundle) with ctx.indent_block(): ctx.emit("X_elem[a, d] = x_ref[nid][d]") ctx.emit("") - ctx.emit("# Quadrature loop (RUNTIME — PlanJune14 WI-1 JIT-budget lever).") + ctx.emit("# Quadrature loop (RUNTIME — the JIT-budget lever).") ctx.emit("# Runtime q divides the per-QP unroll by N_QP (8); physics indices") ctx.emit("# i,I,j,J,K,M,a,d stay ti.static. The body can no longer index the") ctx.emit("# Python-list quad constants by a runtime q, so it reads the") @@ -2810,7 +2809,7 @@ def emit_newton_driver(ctx: EmissionContext, bundle: ArtifactBundle) -> None: def emit_explicit_driver(ctx: EmissionContext, bundle: ArtifactBundle) -> None: - """Emit central-difference explicit-dynamics driver (Plan B §B7, P7-1). + """Emit central-difference explicit-dynamics driver. Emits: * a Taichi velocity field ``v`` of shape ``(n_nodes, 3)`` storing the @@ -2827,7 +2826,7 @@ def emit_explicit_driver(ctx: EmissionContext, bundle: ArtifactBundle) -> None: with the deleted-element guard ``if is_deleted[e] != 0: continue`` baked into the internal-force accumulation so deleted elements - contribute zero inertia / internal force (Phase 6 carry-forward). + contribute zero inertia / internal force. Callers drive the time loop externally: compute ``f_int`` via the already-emitted ``compute_internal_force`` helper, then call @@ -2856,7 +2855,7 @@ def emit_explicit_driver(ctx: EmissionContext, bundle: ArtifactBundle) -> None: ctx.emit("@ti.kernel") ctx.emit("def advance_one_step(dt: ti.f64):") with ctx.indent_block(): - ctx.emit('"""One central-difference step (Plan B §B7, P7-1).') + ctx.emit('"""One central-difference step.') ctx.emit("") ctx.emit(" v^{n+1/2} = v^{n-1/2} + dt * M_inv * (f_ext - f_int)") ctx.emit(" u^{n+1} = u^{n} + dt * v^{n+1/2}") @@ -2986,7 +2985,7 @@ def emit_neumann_f_ext_kernel( when wiring up the Newton driver. Side effect: lines appended to ``ctx``. - Acceptance contract (post_recovery_plan P1-4): + Acceptance contract: 1. Generated source zeroes ``f_ext`` for every node before applying the Neumann contribution; nodes outside the tagged surface stay @@ -3003,7 +3002,7 @@ def emit_neumann_f_ext_kernel( ctx.emit("# " + "=" * 70) ctx.emit(f"# Neumann f_ext kernel for BC '{spec.bc_name}' (surface '{spec.surface_tag}')") - ctx.emit("# Emitted by post_recovery_plan P1-4 (Taichi printer Neumann emitter)") + ctx.emit("# Emitted by the Taichi printer Neumann emitter (literal-baked form)") ctx.emit("# " + "=" * 70) ctx.emit("") ctx.emit("@ti.kernel") @@ -3049,9 +3048,9 @@ def emit_neumann_f_ext_kernel_for_ir( """Emit a Neumann ``f_ext`` kernel parametric in face-area factor. Companion to :func:`emit_neumann_f_ext_kernel`. The literal-baked form - (P1-4) is right for golden tests where the lowering pre-distributes + is right for golden tests where the lowering pre-distributes the per-node force; this parametric form is right for the canonical - LaTeX façade (P1-5), which builds the kernel from the IR + LaTeX façade, which builds the kernel from the IR ``BoundaryCondition`` alone — no mesh, no face area available at compile time. @@ -3073,7 +3072,7 @@ def init_f_ext_from_neumann_( ctx.emit("# " + "=" * 70) ctx.emit(f"# Neumann f_ext kernel for BC '{bc_name}' (surface '{surface_tag}')") - ctx.emit("# Emitted by post_recovery_plan P1-5 (façade-driven, parametric form)") + ctx.emit("# Emitted by the Taichi printer Neumann emitter (façade-driven, parametric form)") ctx.emit("# " + "=" * 70) ctx.emit("") ctx.emit("@ti.kernel") @@ -3294,7 +3293,7 @@ def emit_main(ctx: EmissionContext, bundle: ArtifactBundle) -> None: ctx.emit("") ctx.emit("# Run Newton solver") if is_damage: - ctx.emit("# NOTE (P6-2): Lemaitre damage requires incremental load stepping") + ctx.emit("# NOTE: Lemaitre damage requires incremental load stepping") ctx.emit("# with alpha/D snapshot-rollback on non-convergence. The standalone") ctx.emit("# __main__ path does not implement this; use the newton_solve() API") ctx.emit("# directly with your own load-stepping driver. bc_values is") @@ -3405,12 +3404,12 @@ def emit(bundle: ArtifactBundle) -> str: emit_explicit_driver(ctx, bundle) else: emit_tangent_matvec_kernel(ctx, bundle) - # PlanJune14 P3-2: emit the generated matrix-free SVK tangent @ti.kernel - # *alongside* the host tangent_matvec, for the SVK / Total-Lagrangian / - # reference / named-model path only. The host route stays the default - # the emitted Newton driver drives (flipping the default is P4-3); the - # generated @ti.kernel is the optimiser-routed, ti_runtime-seam operator - # that downstream tasks (P4-3, P5-1, P7-2) consume. Gating on this exact + # Emit the generated matrix-free SVK tangent @ti.kernel *alongside* the + # host tangent_matvec, for the SVK / Total-Lagrangian / reference / + # named-model path only. The host route stays the default the emitted + # Newton driver drives; the generated @ti.kernel is the optimiser-routed, + # ti_runtime-seam operator that the seam solver path consumes. Gating on + # this exact # path keeps every other golden (Lemaitre, UL, derived, explicit) # byte-identical. configuration = bundle.problem_ir_dict.get("configuration", "reference") @@ -3454,8 +3453,7 @@ class TaichiCodegenFacade: The underlying module-level functions remain the implementation; they are importable and callable without going through this façade. This façade exists purely to satisfy the design-doc-aligned API shape - described in ``dev/design_docs/05-CODEGEN.md`` and the recovery plan - §Phase 5 (R4.3). + described in ``dev/design_docs/05-CODEGEN.md``. Usage ----- @@ -3493,7 +3491,7 @@ class TaichiCodegenFacade: facade.main(ctx, bundle) source = ctx.get_source() - # Verbose audit (recovery P5-4): + # Verbose audit: ctx = facade.make_context(verbose=True) facade.preamble(ctx, bundle) # ... ctx.code now includes enriched-IR audit block. @@ -3510,7 +3508,7 @@ def make_context(self, *, verbose: bool = False) -> EmissionContext: ---------- verbose : bool, default False When True, the emission pipeline appends an "Enriched-IR contract - surface (recovery P5-4 audit)" block to the emitted file's + surface (audit)" block to the emitted file's docstring whenever the bundle carries enrichment data. Default emission is byte-identical to legacy output. """ @@ -3545,11 +3543,11 @@ def tangent_matvec_kernel(self, ctx: EmissionContext, bundle: ArtifactBundle) -> return emit_tangent_matvec_kernel(ctx, bundle) def svk_tangent_matvec_kernel(self, ctx: EmissionContext, bundle: ArtifactBundle) -> None: - """Delegate to :func:`emit_svk_tangent_matvec_kernel` (P3-2 generated kernel).""" + """Delegate to :func:`emit_svk_tangent_matvec_kernel` (generated kernel).""" return emit_svk_tangent_matvec_kernel(ctx, bundle) def j2_tangent_matvec_kernel(self, ctx: EmissionContext, bundle: ArtifactBundle) -> None: - """Delegate to :func:`emit_j2_tangent_matvec_kernel` (P5-1 generated kernel).""" + """Delegate to :func:`emit_j2_tangent_matvec_kernel` (generated kernel).""" return emit_j2_tangent_matvec_kernel(ctx, bundle) def validate_mesh(self, ctx: EmissionContext) -> None: diff --git a/packages/mechdsl-core/src/mechdsl/frontend/__init__.py b/packages/mechdsl-core/src/mechdsl/frontend/__init__.py index 2605f80..47b75de 100644 --- a/packages/mechdsl-core/src/mechdsl/frontend/__init__.py +++ b/packages/mechdsl-core/src/mechdsl/frontend/__init__.py @@ -1,7 +1,6 @@ """LaTeX parsing, % mechanics directives, two-point tensor index resolution. -Public entry points (in tier order — see ``README.md`` Support tiers and -``dev/plans/recovery_plan_latex_contract.md`` Phase 2 / R1): +Public entry points (in tier order — see ``README.md`` Support tiers): **Canonical (MVP-stable)** @@ -49,7 +48,8 @@ def has_math_block(source: str) -> bool: """``True`` iff ``source`` contains at least one ``$...$`` math block. The math parser is invoked only when this is true - (parse-when-needed guard, plan §Phase 4 lines 226–227). + (parse-when-needed guard), so directive-only sources never pay + for ``nrpylatex``. """ return bool(_MATH_BLOCK_RE.search(source)) @@ -138,7 +138,7 @@ def parse_with_math(source: str) -> dict[str, Any]: The ``equations`` entry carries the per-equation semantics that :func:`mechdsl.frontend.math_parser.parse_math` extracts from each - ``$...$`` block (fgram Phase 4 ``EquationSemantics``). Entries are + ``$...$`` block (one ``EquationSemantics`` per equation). Entries are JSON-primitive dicts so the bundle round-trips through :meth:`mechdsl.ir.mechanics_ir.ProblemIR.from_latex_semantics` into the serialized IR's ``latex_semantics`` record without re-parsing the source. @@ -258,7 +258,7 @@ def build_context( a LaTeX source string and forwards through this function's machinery after parsing. ``build_context`` remains supported for tests and embedding scenarios that have no LaTeX source available — see ``README.md`` - Support tiers and ``dev/plans/recovery_plan_latex_contract.md`` Phase 2 (R1). + Support tiers. It returns the same context dict that the LaTeX parser would produce. Validates that inputs fall within the MVP-supported subset. ProblemIR @@ -269,7 +269,7 @@ def build_context( dim: Spatial dimension (3 for all MVP problems). cell_type: - Element type string. After Plan B phase B5 (task P5-6) this + Element type string. After Plan B phase B5 this accepts ``"hex8"``, ``"hex20"``, ``"tet4"``, and ``"tet10"`` — the full ElementFactory topology vocabulary. Unknown topologies and invalid ``(cell_type, integration, hourglass)`` triples raise @@ -291,16 +291,16 @@ def build_context( anisotropic models such as HGO. fiber_families: Optional fiber-direction families declared via the - ``% mechanics fiber`` directive (constitutive_latex P5-1) — a list of + ``% mechanics fiber`` directive — a list of ``{"direction": (x, y, z), ...}`` entries. Satisfies the anisotropic-requires-fiber requirement when ``fiber_data`` is absent. hourglass_coef: Dimensionless Flanagan-Belytschko hourglass control coefficient - used by reduced-integration elements (Plan B phase B5, task P5-5). + used by reduced-integration elements (Plan B phase B5). Defaults to ``0.05``; typical range is ``0.01 - 0.10``. integration: Integration rule selector, ``"full"`` (default) or ``"reduced"``. - Plan B phase B5 task P5-6 added the option; routed through + Added in Plan B phase B5; routed through :class:`mechdsl.ir.element_factory.ElementFactory` for combination validation. hourglass: @@ -377,8 +377,7 @@ def build_context( raise UnsupportedError( "material_type='hgo' requires fiber direction(s): either the " "fiber_data kwarg (per-element (a1, a2) unit-vector pairs, shape " - "(n_elem, 2, 3)) or the '% mechanics fiber --family' directive " - "(constitutive_latex P5-1)." + "(n_elem, 2, 3)) or the '% mechanics fiber --family' directive." ) ctx: dict = { diff --git a/packages/mechdsl-core/src/mechdsl/frontend/directives.py b/packages/mechdsl-core/src/mechdsl/frontend/directives.py index 924f092..83a7d6e 100644 --- a/packages/mechdsl-core/src/mechdsl/frontend/directives.py +++ b/packages/mechdsl-core/src/mechdsl/frontend/directives.py @@ -193,7 +193,7 @@ def _mech_cell(accum: dict[str, Any], args: ParsedArgs, line_no: int) -> None: """Handle ``% mechanics cell [--integration ] [--hourglass ]``. The ``--integration`` and ``--hourglass`` flags are Plan B phase B5 - additions (task P5-6). Both flow through to the accumulator under the + additions. Both flow through to the accumulator under the keys ``integration`` and ``hourglass`` respectively, where :func:`mechdsl.frontend.build_context` then dispatches them through :class:`mechdsl.ir.element_factory.ElementFactory` for combination @@ -451,7 +451,7 @@ def _mech_fiber(accum: dict[str, Any], args: ParsedArgs, line_no: int) -> None: two). The direction is a numeric 3-vector, comma- or space-separated. Accumulates ``{"direction": (x, y, z), "source_line": n}`` entries under ``fiber_families``, which the IR maps to a ``FiberFieldSpec`` carried through - ProblemIR -> Element IR (constitutive_latex P5-1). + ProblemIR -> Element IR. """ positional, options = args if positional: @@ -463,8 +463,7 @@ def _mech_fiber(accum: dict[str, Any], args: ParsedArgs, line_no: int) -> None: if "family" not in options: raise ParseError( f"line {line_no}: '% mechanics fiber' requires --family with a numeric " - '3-vector direction (e.g. --family "1, 0, 0"); anisotropic fiber ' - "plumbing is constitutive_latex Phase 5 (P5-1)." + '3-vector direction (e.g. --family "1, 0, 0").' ) raw = options["family"] tokens = [t for t in raw.replace(",", " ").split() if t] @@ -473,17 +472,17 @@ def _mech_fiber(accum: dict[str, Any], args: ParsedArgs, line_no: int) -> None: except ValueError as exc: raise ParseError( f"line {line_no}: '% mechanics fiber' --family must be a numeric " - f"3-vector (e.g. '1, 0, 0'); got {raw!r} (constitutive_latex P5-1)." + f"3-vector (e.g. '1, 0, 0'); got {raw!r}." ) from exc if len(direction) != 3: raise ParseError( f"line {line_no}: '% mechanics fiber' --family requires exactly 3 " - f"components, got {len(direction)} ({raw!r}) (constitutive_latex P5-1)." + f"components, got {len(direction)} ({raw!r})." ) if sum(c * c for c in direction) <= 0.0: raise ParseError( f"line {line_no}: '% mechanics fiber' --family must be a nonzero " - f"direction, got {raw!r} (constitutive_latex P5-1)." + f"direction, got {raw!r}." ) _append_entry( accum, diff --git a/packages/mechdsl-core/src/mechdsl/frontend/math_parser.py b/packages/mechdsl-core/src/mechdsl/frontend/math_parser.py index 3ba3b82..bf1e27f 100644 --- a/packages/mechdsl-core/src/mechdsl/frontend/math_parser.py +++ b/packages/mechdsl-core/src/mechdsl/frontend/math_parser.py @@ -1,8 +1,8 @@ """LaTeX-math frontend wrapping the ``nrpylatex`` parser. -post_recovery_plan Phase 4 (P4-1) wires ``nrpylatex`` (already declared in -pyproject.toml) into the frontend layer so ``$...$`` math blocks in -LaTeX source can flow into the symbolic stage. +This module wires ``nrpylatex`` (declared in pyproject.toml) into the +frontend layer so ``$...$`` math blocks in LaTeX source can flow into the +symbolic stage. Supported subset (nrpylatex 1.4.0) ---------------------------------- @@ -26,7 +26,7 @@ upper / lower placement) on the RHS, and every contracted index must appear exactly twice. ``nrpylatex`` raises ``GeneratorError`` on imbalanced expressions; ``parse_math`` re-raises as - :class:`MathParseError` with a Phase-4 pointer. + :class:`MathParseError` with a pointer to the accepted grammar. - Bound (contracted) indices must connect tensors of complementary position (one upper, one lower). Forms such as ``F^{kk}`` (two upper @@ -43,8 +43,8 @@ - Functions ``\\det``, ``\\sin``, etc. that require sympy intrinsics not registered with nrpylatex 1.4.0's parser raise ``SympifyError``; - these are caught and re-raised as :class:`MathParseError` Phase-4 to - signal the deferred extension surface. + these are caught and re-raised as :class:`MathParseError` to signal + that the construct is outside the supported subset. Index convention enforcement (07-CONVENTIONS) --------------------------------------------- @@ -58,10 +58,9 @@ Errors ------ -Every failure path in this module raises :class:`MathParseError` with -a message that includes the literal phrase -``"post_recovery_plan Phase 4"`` so the bridge surface is -traceable from the failure mode alone. +Every failure path in this module raises :class:`MathParseError`, so +callers can distinguish a math-grammar failure from a directive parse +error by exception type alone. """ from __future__ import annotations @@ -90,9 +89,7 @@ class MathParseError(RuntimeError): """Raised when nrpylatex cannot parse a math block, when the declared subset is exceeded, or when the post-parse index - convention check fails. Always carries the phrase - ``post_recovery_plan Phase 4`` so the failure surface is - traceable. + convention check fails. """ @@ -160,9 +157,9 @@ class MathParseResult: def _wrap_nrpylatex_error(exc: BaseException) -> MathParseError: return MathParseError( f"nrpylatex {type(exc).__name__}: {exc}. " - "Unsupported nrpylatex node — see post_recovery_plan Phase 4 " - "(packages/mechdsl-core/src/mechdsl/frontend/math_parser.py " - "supported-subset docstring) for the accepted grammar." + "Unsupported nrpylatex node — see the supported-subset docstring in " + "packages/mechdsl-core/src/mechdsl/frontend/math_parser.py " + "for the accepted grammar." ) @@ -241,14 +238,12 @@ def _validate_occurrence_manifold(occurrence: IndexedOccurrence) -> None: if symbol in _SPATIAL_TENSORS and "material" in roles: raise MathParseError( f"tensor {occurrence.source!r} mixes a material index into spatial tensor " - f"{symbol!r}; use a material stress/strain or an explicit two-point tensor. " - "post_recovery_plan Phase 4." + f"{symbol!r}; use a material stress/strain or an explicit two-point tensor." ) if symbol in _MATERIAL_TENSORS and "spatial" in roles: raise MathParseError( f"tensor {occurrence.source!r} mixes a spatial index into material tensor " - f"{symbol!r}; use F/P for two-point mappings per 07-CONVENTIONS. " - "post_recovery_plan Phase 4." + f"{symbol!r}; use F/P for two-point mappings per 07-CONVENTIONS." ) @@ -258,7 +253,8 @@ def _validate_supported_equation_source(source: str) -> None: if unsupported: raise MathParseError( f"unsupported LaTeX function(s) {unsupported} in preserved equation " - f"{source!r}. These are full-grammar phase work; post_recovery_plan Phase 4." + f"{source!r}. These are full-grammar phase work and are not yet accepted; " + "only polynomial / indexed tensor expressions are supported in math blocks." ) @@ -294,16 +290,15 @@ def _free_and_contracted_indices( else: raise MathParseError( f"index {index!r} appears {count} times in term {term!r}; " - "Einstein contractions must be pairwise before symbolic lowering. " - "post_recovery_plan Phase 4." + "Einstein contractions must be pairwise before symbolic lowering." ) term_free = {index for index, count in term_counts.items() if count == 1} if term_occurrences and term_free != expected_free: raise MathParseError( f"free-index mismatch in equation {source!r}: LHS has " - f"{sorted(expected_free)}, RHS term {term!r} has {sorted(term_free)}. " - "post_recovery_plan Phase 4." + f"{sorted(expected_free)}, RHS term {term!r} has {sorted(term_free)}; " + "every free index on the LHS must appear once in every RHS term." ) return tuple(sorted(rhs_free_union)), tuple(sorted(contracted)) @@ -424,11 +419,13 @@ def parse_math(latex_block: str) -> MathParseResult: Raises ------ MathParseError - For any nrpylatex parser/scanner/generator error, with the - Phase-4 pointer in the message. + For any nrpylatex parser/scanner/generator error, with a pointer + to the accepted grammar in the message. """ if not isinstance(latex_block, str): - raise MathParseError("parse_math requires a str — post_recovery_plan Phase 4.") + raise MathParseError( + f"parse_math requires a str LaTeX block, got {type(latex_block).__name__}." + ) equations = extract_equations(latex_block) try: @@ -505,7 +502,7 @@ def enforce_index_convention( raise MathParseError( f"tensor {name!r} appears with both spatial and material " f"indices on axis {axis} — mechdsl 07-CONVENTIONS forbids " - "this. post_recovery_plan Phase 4." + "this; declare a separate tensor for each index regime." ) if "spatial" in kinds: spatial_axes.append(axis) diff --git a/packages/mechdsl-core/src/mechdsl/frontend/parser.py b/packages/mechdsl-core/src/mechdsl/frontend/parser.py index 40e5ed4..4fc2cb8 100644 --- a/packages/mechdsl-core/src/mechdsl/frontend/parser.py +++ b/packages/mechdsl-core/src/mechdsl/frontend/parser.py @@ -2,8 +2,7 @@ This module is the **directive scanner** half of the frontend — see the parser-of-record vs adapter/normalizer/validator split documented in -``packages/mechdsl-core/src/mechdsl/frontend/ARCHITECTURE.md`` (introduced by -recovery-plan Phase 2 / R1.3 / task ``P2-3``). +``packages/mechdsl-core/src/mechdsl/frontend/ARCHITECTURE.md``. Responsibilities of this module: diff --git a/packages/mechdsl-core/src/mechdsl/frontend/two_point.py b/packages/mechdsl-core/src/mechdsl/frontend/two_point.py index 95d6b5f..fd3e8d3 100644 --- a/packages/mechdsl-core/src/mechdsl/frontend/two_point.py +++ b/packages/mechdsl-core/src/mechdsl/frontend/two_point.py @@ -2,8 +2,7 @@ This module is the **index validator** of the frontend — see ``packages/mechdsl-core/src/mechdsl/frontend/ARCHITECTURE.md`` for the -parser-of-record vs adapter/normalizer/validator split (recovery-plan -Phase 2 / R1.3 / task ``P2-3``). +parser-of-record vs adapter/normalizer/validator split. Resolves two-point tensor index families (lowercase ``i, j, k, l`` = spatial; uppercase ``I, J, K, L`` = material) and rejects mixed-tier diff --git a/packages/mechdsl-core/src/mechdsl/integration/__init__.py b/packages/mechdsl-core/src/mechdsl/integration/__init__.py index 98532dd..e73d7c8 100644 --- a/packages/mechdsl-core/src/mechdsl/integration/__init__.py +++ b/packages/mechdsl-core/src/mechdsl/integration/__init__.py @@ -5,13 +5,13 @@ machine-readable API and **never** triggers Taichi initialisation on import or during the emit/transpile paths. -Five entry points (implemented across tasks P1-1 … P1-4): +Five entry points: - :func:`capabilities` — declare supported profiles, backends, actions. - :func:`model_catalog` — enumerate all constitutive models with metadata. -- :func:`compile_from_sources` — wraps ``mechdsl.compile_latex`` (P1-2). -- :func:`transpile_algorithm` — wraps ``algo2code.transpile`` (P1-3). -- :func:`verify` — wraps the verify harness; Taichi-paying path (P1-4). +- :func:`compile_from_sources` — wraps ``mechdsl.compile_latex``. +- :func:`transpile_algorithm` — wraps ``algo2code.transpile``. +- :func:`verify` — wraps the verify harness; Taichi-paying path. **Taichi-free guarantee (Tier-1 contract):** Importing this module and calling :func:`capabilities` or diff --git a/packages/mechdsl-core/src/mechdsl/ir/element_factory.py b/packages/mechdsl-core/src/mechdsl/ir/element_factory.py index a74ec13..2455302 100644 --- a/packages/mechdsl-core/src/mechdsl/ir/element_factory.py +++ b/packages/mechdsl-core/src/mechdsl/ir/element_factory.py @@ -1,4 +1,4 @@ -"""Uniform element-construction API (Plan B §B5.2, task P5-6). +"""Uniform element-construction API (Plan B §B5.2). The :class:`ElementFactory` collapses the four element constructors (:func:`mechdsl.ir.element_ir.create_hex8_element_ir`, @@ -20,8 +20,9 @@ dataclass produced by the underlying constructor — no caching, no mutation. -See ``dev/design_docs/PLAN-B.md §B5.2`` for the API shape and -``dev/tasks/PLAN-B/json/P5-6.json`` for the supported-triple matrix. +The supported ``(topology, integration, hourglass)`` triples are the keys of +the dispatch table below; see ``dev/design_docs/PLAN-B.md §B5.2`` for the API +shape. """ from __future__ import annotations diff --git a/packages/mechdsl-core/src/mechdsl/ir/element_ir.py b/packages/mechdsl-core/src/mechdsl/ir/element_ir.py index fdb39a7..340f7a6 100644 --- a/packages/mechdsl-core/src/mechdsl/ir/element_ir.py +++ b/packages/mechdsl-core/src/mechdsl/ir/element_ir.py @@ -160,9 +160,9 @@ class GeometrySummary: """Per-element geometry summary — single source of truth for downstream. Captures the element's reference-cell volume, quadrature size, and - parametric dimension. Pre-P4-1 these were re-derived from - ``ElementIR.element_type`` everywhere that needed them; P4-1 promotes - them to a single dataclass so downstream consumers read one value. + parametric dimension. These used to be re-derived from + ``ElementIR.element_type`` everywhere that needed them; promoting them + to a single dataclass means downstream consumers read one value. """ n_quad: int @@ -276,9 +276,9 @@ class LocalForceDescriptor: Records the per-element force-vector size and a backend-agnostic contraction sketch. The descriptor is documentation-grade today — the Taichi printer and other emitters still write the actual contraction - inline. P4-1 promotes the layout to a first-class IR object so a future - P5-3 (codegen façade) can drive emission from this descriptor instead - of the printer's hard-coded knowledge. + inline. Promoting the layout to a first-class IR object lets a future + codegen façade drive emission from this descriptor instead of the + printer's hard-coded knowledge. """ n_dof: int # n_nodes * dim @@ -364,8 +364,7 @@ class ElementIR: element topology and integration rule; Plan B §B5.5 adds the hourglass control required to stabilise reduced Hex8. geometry, material_eval, local_force, local_tangent - Optional execution-contract enrichment introduced by recovery - Phase 4 / R3.1 / P4-1. All four default to ``None`` so legacy + Optional execution-contract enrichment. All four default to ``None`` so legacy callers continue constructing ``ElementIR`` without source changes. When populated, they expose the per-element geometry summary, constitutive evaluation contract, and local force / tangent layout @@ -519,7 +518,7 @@ def from_dict(cls, d: dict[str, Any]) -> ElementIR: """Deserialize the contract surface, rebuilding basis / quadrature from the recorded element_type via the canonical constructors. - Accepts both legacy dicts (without the P4-1 enrichment keys) and + Accepts both legacy dicts (without the execution-contract enrichment keys) and enriched dicts. Missing enrichment keys rebuild as ``None``. """ element_type = d["element_type"] @@ -631,8 +630,8 @@ def create_hex8_element_ir( configuration ``"reference"`` (default, Plan A) or ``"current"`` (Plan B §B1.3). The configuration determines which Jacobian (J0 vs j) downstream - emitters will select. P1-1 only carries the tag; P1-2 populates the - actual j-Jacobian slots. + emitters will select. The tag is carried here; the actual + j-Jacobian slots are populated by the kinematics lowering. integration_rule :attr:`IntegrationRule.FULL` (default, 2x2x2 = 8 Gauss points) or :attr:`IntegrationRule.REDUCED` (1-point centre, Plan B §B5.4). diff --git a/packages/mechdsl-core/src/mechdsl/ir/mechanics_ir.py b/packages/mechdsl-core/src/mechdsl/ir/mechanics_ir.py index f2a56c5..2e7fb95 100644 --- a/packages/mechdsl-core/src/mechdsl/ir/mechanics_ir.py +++ b/packages/mechdsl-core/src/mechdsl/ir/mechanics_ir.py @@ -65,8 +65,7 @@ class Configuration(Enum): class ElementType(Enum): """Element types known to the compiler. - Support tier (per ``README.md`` Support tiers and - ``dev/plans/recovery_plan_latex_contract.md`` Phase 1 (R0)): + Support tier (per ``README.md`` Support tiers): - **MVP-stable**: ``HEX8``. Only this element is part of the canonical compile path on the Taichi backend. @@ -83,7 +82,7 @@ class ElementType(Enum): class DynamicsMode(Enum): """Selector between implicit static and explicit dynamic time integration. - Plan B §B7 (task P7-1) introduces this enum so the generator can emit + Plan B §B7 introduces this enum so the generator can emit either a Newton–Raphson solve (:attr:`STATIC`, default) or a central- difference ``advance_one_step(dt)`` kernel (:attr:`EXPLICIT`). The choice is a compile-time property of :class:`ProblemIR` — runtime switching is @@ -169,14 +168,12 @@ def __post_init__(self) -> None: except (TypeError, ValueError) as exc: raise ValueError( f"BoundaryCondition(name={self.name!r}) traction sequence must " - f"contain numeric components; got {self.traction!r}. " - "Validation added in post_recovery_plan Phase 1 (P1-1)." + f"contain numeric components; got {self.traction!r}." ) from exc if len(tv) != 3: raise ValueError( f"BoundaryCondition(name={self.name!r}) traction vector must " - f"have length 3, got length {len(tv)}. " - "Validation added in post_recovery_plan Phase 1 (P1-1)." + f"have length 3, got length {len(tv)}." ) object.__setattr__(self, "traction", tv) # A Neumann BC with no traction has no physical content; reject it @@ -184,8 +181,7 @@ def __post_init__(self) -> None: if self.bc_type == BCType.NEUMANN and self.traction is None: raise ValueError( f"BoundaryCondition(name={self.name!r}, bc_type=neumann) requires " - "a traction specification (symbolic string or 3-vector). " - "Validation added in post_recovery_plan Phase 1 (P1-1)." + "a traction specification (symbolic string or 3-vector)." ) @property @@ -193,8 +189,8 @@ def effective_surface_tag(self) -> str: """Return ``surface_tag`` if set, otherwise the BC ``name``. Lowering passes use this property so callers do not need to repeat - the fallback rule. Introduced in post_recovery_plan Phase 1 (P1-1) - to give Phase 1 lowering a single, explicit surface identifier. + the fallback rule; it gives lowering a single, explicit surface + identifier. """ return self.surface_tag if self.surface_tag is not None else self.name @@ -340,7 +336,7 @@ def __post_init__(self) -> None: raise ValueError( f"FieldSpec(name={self.name!r}) kind={self.kind!r} is not supported; " f"allowed kinds are {sorted(ALLOWED_FIELD_KINDS)}. " - "Wider field-kind support is planned for recovery-plan task P3-5." + "Wider field-kind support is planned." ) def to_dict(self) -> dict[str, Any]: @@ -359,9 +355,9 @@ def from_dict(cls, d: dict[str, Any]) -> FieldSpec: class FiberFieldSpec: """Per-element fiber-orientation field data for anisotropic models (HGO). - constitutive_latex Phase 5 (P5-1). This is **field data**, deliberately - distinct from :class:`MaterialSpec` scalar params: it carries one direction - vector per fiber family (HGO uses two). Authored via the + This is **field data**, deliberately distinct from :class:`MaterialSpec` + scalar params: it carries one direction vector per fiber family (HGO uses + two). Authored via the ``% mechanics fiber --family "x,y,z"`` directive as constant (uniform) directions, or supplied programmatically through :func:`mechdsl.frontend.build_context` ``fiber_data``. Genuinely @@ -409,9 +405,9 @@ class DomainSpec: Kept intentionally permissive in the MVP: ``metadata`` is a free-form bag so callers can attach problem-specific keys (e.g. bounding box, - geometry source path) without rotating the schema. Phase 4 (R3) is the - place to introduce stricter typed domain descriptors if the bag turns - out to be too loose in practice. + geometry source path) without rotating the schema. Stricter typed domain + descriptors can be introduced later if the bag turns out to be too loose + in practice. The bag is exposed as a read-only ``MappingProxyType`` to honour the frozen-dataclass invariant — write attempts raise ``TypeError``. Callers @@ -514,7 +510,7 @@ def from_dict(cls, d: dict[str, Any]) -> ResidualContract: @dataclass(frozen=True) class LatexSemantics: - """Record of the LaTeX-derived semantics the compiler understood (fgram P5-1). + """Record of the LaTeX-derived semantics the compiler understood. This is the serializable explanation of *what the compiler read* from a LaTeX source: which fields were declared, which constitutive roles were @@ -528,7 +524,7 @@ class LatexSemantics: stays round-trip-safe. ``role`` values are taken verbatim from directive-tagged metadata (which - is authoritative per the Phase 4 handoff). A role of ``unknown`` / + is authoritative). A role of ``unknown`` / ``None`` from the equation classifier is downgraded to ``"auxiliary"`` rather than inferred into a constitutive role. """ @@ -1086,7 +1082,7 @@ def from_context(cls, ctx: dict[str, Any]) -> ProblemIR: boundary entries that may use ``region`` / ``face`` / ``dofs`` instead of ``name`` / ``components``. - Before P3-2, three private adapters reimplemented this mapping in + Previously, three private adapters reimplemented this mapping in :mod:`mechdsl` (used by :func:`compile_latex`), ``test_full_pipeline.py``, and ``test_formulation_switching.py``. This classmethod replaces all three so the adapter lives on the @@ -1149,7 +1145,7 @@ def _fiber_field_from_context(ctx: dict[str, Any]) -> FiberFieldSpec | None: def from_latex_semantics(cls, ctx: dict[str, Any]) -> ProblemIR: """Build a validated :class:`ProblemIR` from LaTeX-derived semantics. - This is the LaTeX-semantic constructor (fgram Phase 5). Unlike + This is the LaTeX-semantic constructor. Unlike :meth:`from_context`, which reads only the thin directive core (``dim`` / ``cell_type`` / ``formulation`` / ``material_type`` / ``boundaries``), this adapter additionally consumes the richer @@ -1163,7 +1159,7 @@ def from_latex_semantics(cls, ctx: dict[str, Any]) -> ProblemIR: :attr:`latex_semantics` so the bundle explains what the compiler understood. - Convergence (Phase 4 handoff): the MVP-stable *core* of the returned + Convergence guarantee: the MVP-stable *core* of the returned IR is identical to :meth:`from_context` for the same ``ctx`` — this adapter only *adds* enrichment, it never diverges on the core configuration. Symbolic / constitutive meaning is not re-derived here: @@ -1280,8 +1276,8 @@ def _latex_semantics_from_context( directive-tagged constitutive roles (authoritative), the weak-form label, and any equation roles from a ``$...$`` math block. Equation roles reported as ``unknown`` / ``None`` are downgraded to - ``"auxiliary"`` rather than inferred into a constitutive role (Phase 4 - handoff). Returns ``None`` when the context carries no LaTeX-derived + ``"auxiliary"`` rather than inferred into a constitutive role. + Returns ``None`` when the context carries no LaTeX-derived semantics at all, so legacy / programmatic IRs stay unannotated. """ constitutive = tuple( diff --git a/packages/mechdsl-core/src/mechdsl/lawgen/budgets.py b/packages/mechdsl-core/src/mechdsl/lawgen/budgets.py index fd56752..b5d5fbc 100644 --- a/packages/mechdsl-core/src/mechdsl/lawgen/budgets.py +++ b/packages/mechdsl-core/src/mechdsl/lawgen/budgets.py @@ -1,11 +1,11 @@ -"""Pre-emission JIT budget gate for the lawgen lowerer (Task P2-2). +"""Pre-emission JIT budget gate for the lawgen lowerer. Part of the MechDSL lawgen pipeline (YAML law spec → restricted SymPy → Taichi carrier). This module is the *gate* that stands between the deterministic lowerer -(:mod:`mechdsl.lawgen.sympy_to_taichi`, P2-1) and any Taichi emission -(Phase 3/4). It counts six quantities over a law's SymPy expressions and its +(:mod:`mechdsl.lawgen.sympy_to_taichi`) and any Taichi emission (the carrier and +test emitters). It counts six quantities over a law's SymPy expressions and its lowered source lines, and **fails loud** (R2) the moment any of them exceeds the matching :class:`~mechdsl.lawgen.contracts.TiconstitTarget` budget knob. No Taichi source is produced here — this is purely a pre-emission check, so an @@ -34,13 +34,13 @@ expression. ============================================ ================================ -Error contract (P3-1 — collect-all) ------------------------------------ +Error contract (collect-all) +---------------------------- :meth:`BudgetChecker.check_all` runs **all six** budget checks over **all** expressions/functions and accumulates *every* violation into a :class:`~mechdsl.lawgen.diagnostics.DiagnosticCollector`, then raises a single -:class:`~mechdsl.lawgen.diagnostics.LawgenError` carrying the whole batch (P3-1 -collect-all: the user sees every over-budget knob in one run, not just the first). +:class:`~mechdsl.lawgen.diagnostics.LawgenError` carrying the whole batch +(collect-all: the user sees every over-budget knob in one run, not just the first). Each budget violation still carries the structured ``" budget exceeded: > "`` text — built by :meth:`BudgetError.for_budget` — as the diagnostic's ``reason`` (so the measured @@ -50,14 +50,14 @@ :class:`mechdsl.lowering.fe_localise.LocalisationError` subclasses the broader ``UnsupportedError``. -Reusability for downstream phases (P2-3 / P2-4 / P3) ----------------------------------------------------- +Reusability for downstream stages +--------------------------------- The three counters are module-level pure functions (:func:`count_expr_ops`, :func:`count_piecewise_branches`, :func:`count_pow_symbolic_exponent`) so they are independently testable and can be reused outside :class:`BudgetChecker`. :class:`BudgetChecker` binds one :class:`TiconstitTarget` and exposes :meth:`BudgetChecker.check_all`, which -Phase 3/4 call *before* emission. +the emitters call *before* emission. """ from __future__ import annotations @@ -140,7 +140,7 @@ def count_expr_ops(expr: sp.Expr) -> int: tree. ``visual=False`` returns an integer (the default), which we coerce to a Python ``int`` so the value is JSON/log friendly and version-stable. - Risk note (P2-2): ``count_ops`` semantics can vary slightly across SymPy + Risk note: ``count_ops`` semantics can vary slightly across SymPy versions, so budget fixtures should use a low overridden limit on a fixed small expression rather than pinning an exact absolute count. """ @@ -172,7 +172,7 @@ def count_pow_symbolic_exponent(expr: sp.Expr) -> int: * Integer-literal exponents (``x**2``, ``x**-3``) are exempt — they lower to plain repeated multiplication and cost the JIT nothing extra. * Exact ``±1/2`` exponents (``sqrt(x)`` = ``Pow(x, S.Half)`` and - ``1/sqrt(x)`` = ``Pow(x, -S.Half)``) are exempt too. P2-1's printer + ``1/sqrt(x)`` = ``Pow(x, -S.Half)``) are exempt too. The printer (:meth:`mechdsl.lawgen.sympy_to_taichi.TaichiExprPrinter._print_Pow`) special-cases these to ``ti.sqrt`` / ``1/ti.sqrt`` — **not** ``ti.pow`` — so they do not incur the runtime-``ti.pow`` cost this budget guards. @@ -217,7 +217,7 @@ def _budget_diagnostic( The ``reason`` reuses :meth:`BudgetError.for_budget`'s exact message text (``" budget exceeded: > "``), so it always contains - **both the measured value and the limit** (P3-1 AC3). ``node`` is the budget + **both the measured value and the limit**. ``node`` is the budget knob name; ``fix`` is an actionable hint pointing at the two ways to clear a budget breach — simplify the law or raise the knob on the :class:`~mechdsl.lawgen.contracts.TiconstitTarget`. @@ -240,10 +240,10 @@ class BudgetChecker: The bound target supplies the six budget knobs, so a caller that constructs a :class:`TiconstitTarget` with a lowered/raised knob transparently overrides the module defaults (the defaults *are* the ``TiconstitTarget`` defaults — - there is a single source of truth, P1-1). + there is a single source of truth). - Usage (Phase 3/4) - ----------------- + Usage + ----- Construct with the emission target, then call :meth:`check_all` with the law's SymPy expressions and their lowered results *before* handing anything to the printer:: @@ -253,8 +253,8 @@ class BudgetChecker: :meth:`check_all` accumulates **every** budget violation (across all six knobs and all expressions/functions) and raises a single - :class:`~mechdsl.lawgen.diagnostics.LawgenError` carrying them all (P3-1 - collect-all); if nothing is over budget it returns ``None`` and emission may + :class:`~mechdsl.lawgen.diagnostics.LawgenError` carrying them all + (collect-all); if nothing is over budget it returns ``None`` and emission may proceed. """ @@ -262,7 +262,7 @@ def __init__(self, target: TiconstitTarget | None = None) -> None: """Bind the checker to a :class:`TiconstitTarget` (knobs = its fields). ``target=None`` uses a default :class:`TiconstitTarget`, i.e. the six - plan-frozen defaults; pass a customised target to override any knob. + built-in defaults; pass a customised target to override any knob. """ self.target: TiconstitTarget = target if target is not None else TiconstitTarget() @@ -273,7 +273,7 @@ def check_all( ) -> None: """Run all six budget checks; collect **all** violations, then raise once. - Collect-all (P3-1): every over-budget knob — across every expression and + Collect-all: every over-budget knob — across every expression and every function — is recorded as a :class:`~mechdsl.lawgen.diagnostics.LawgenDiagnostic`, and the whole batch is raised at the end as one diff --git a/packages/mechdsl-core/src/mechdsl/lawgen/carrier_emitter.py b/packages/mechdsl-core/src/mechdsl/lawgen/carrier_emitter.py index 0f72bc8..59a146e 100644 --- a/packages/mechdsl-core/src/mechdsl/lawgen/carrier_emitter.py +++ b/packages/mechdsl-core/src/mechdsl/lawgen/carrier_emitter.py @@ -1,12 +1,12 @@ -"""Spec-driven Taichi carrier-class emitter (Task P4-1). +"""Spec-driven Taichi carrier-class emitter. Part of the MechDSL lawgen pipeline (YAML law spec → restricted SymPy → Taichi carrier). -This is the *class emitter* the Phase-2 handoff (note 4) deferred to Phase 3/4: +This is the *class emitter* that sits downstream of the scalar lowerer: the piece that assembles a complete, self-contained Taichi module — a ``class `` with its ``@ti.func`` methods — from a -:class:`~mechdsl.lawgen.contracts.PlasticityCarrierSpec`. The Phase-2 lowerer +:class:`~mechdsl.lawgen.contracts.PlasticityCarrierSpec`. The lowerer (:func:`~mechdsl.lawgen.sympy_to_taichi.lower_expression`) turns a scalar ``sympy.Expr`` into Taichi expression *lines* that reference **bare** symbol names (``sigma0``, ``p``, ``n``); this module is what maps those bare names onto @@ -33,21 +33,21 @@ def __init__(self, params_dict, ti_type=ti.f64): ... # binds self. Design rules ------------ -* **Per-method lowering** (Phase-2 handoff note 2). Each ``get_*`` method is +* **Per-method lowering.** Each ``get_*`` method is lowered *independently* — the R/dR/H/dH/Q/dQ expressions are never lowered as one batch — so no cross-method CSE reorganises one method's structure into another's. This keeps each method's shape close to Cycle 0's hand-authored - method (the P4-2 equivalence gate is *numerical*, rtol=1e-10, so the derivative + method (the equivalence gate is *numerical*, rtol=1e-10, so the derivative reorganisation batching would introduce is unnecessary and undesirable). * **Auto-differentiated derivatives.** ``get_dR`` / ``get_dH`` / ``get_dQ`` are ``sympy.diff`` of the corresponding factor w.r.t. that factor's *primary* variable (R,dR → ``peeq``; H,dH → ``edot``; Q,dQ → ``T``), then lowered with the same guards. The generator never hand-writes a derivative — it differentiates the authored factor, so a factor and its derivative can never drift. -* **Frozen Phase-2 APIs, reused verbatim.** ``lower_expression(exprs, *, +* **Frozen lowerer APIs, reused verbatim.** ``lower_expression(exprs, *, guards=True, target=...)``, :class:`~mechdsl.lawgen.sympy_to_taichi.LoweredExpr`, and :meth:`~mechdsl.lawgen.budgets.BudgetChecker.check_all` are called by name, - never re-implemented. The budget gate runs (fail-loud via the P3-1 + never re-implemented. The budget gate runs (fail-loud via the collect-all :class:`~mechdsl.lawgen.diagnostics.LawgenError`) BEFORE any source is returned. * **Generated file imports Taichi + stdlib only.** The emitted module never imports ``mechdsl``, ``sympy``, ``ticonstit``, or NumerixWeave — it is a pure @@ -201,7 +201,7 @@ def _lower_rebound( The substitution is applied to the SymPy expression, so the lowerer sees ``self.sigma0`` / ``peeq`` as ordinary symbol names and prints them verbatim. Lowering is per-method (a single ``expr``), guards on (the production path the - P4-2 gate measures), and uses the emission ``target`` so its + numerical equivalence gate measures), and uses the emission ``target`` so its ``max_piecewise_branches`` knob gates any ``Piecewise``. """ rebound = expr.subs(rebind) @@ -428,8 +428,8 @@ def _assert_taichi_only(source: str, name: str) -> None: Scans every ``import`` / ``from ... import`` line for a forbidden top-level module (:data:`_FORBIDDEN_IMPORT_TOKENS`). The generated carrier is a pure Taichi runtime artifact (INV-DG-1): a stray ``import sympy`` / ``import - mechdsl`` would break the MechDSL↔NumerixWeave dependency DAG that P4-3's guard - enforces. Runs on the emitter's own output as a self-check before the source + mechdsl`` would break the MechDSL↔NumerixWeave dependency DAG that the cross-repo + import guard enforces. Runs on the emitter's own output as a self-check before the source is ever written, so a regression fails here rather than in the cross-repo DAG gate. """ diff --git a/packages/mechdsl-core/src/mechdsl/lawgen/cli.py b/packages/mechdsl-core/src/mechdsl/lawgen/cli.py index 84a4585..45ec93e 100644 --- a/packages/mechdsl-core/src/mechdsl/lawgen/cli.py +++ b/packages/mechdsl-core/src/mechdsl/lawgen/cli.py @@ -9,7 +9,7 @@ mode — prints the *emission plan* without writing any files. There is deliberately **no** lowering / codegen here. Turning the R/H/Q -expressions into Taichi source is Phase 2's job (``taichi_printer`` is not +expressions into Taichi source is the lowerer's job (``taichi_printer`` is not imported). Likewise there is no ``import ticonstit`` and no NumerixWeave import: the MechDSL↔NumerixWeave seam is committed artifacts only. The CLI depends only on :mod:`mechdsl.lawgen`, the standard library, :mod:`yaml`, and :mod:`sympy`. @@ -364,7 +364,7 @@ def format_emission_plan( ) -> str: """Render the human-readable emission plan for ``--dry-run``. - The exact line prefixes below are a stable contract: Phase 4's end-to-end + The exact line prefixes below are a stable contract: the end-to-end CLI test asserts against them. Keep them in sync if you change the format. """ paths = _planned_paths(out_dir, spec) @@ -485,7 +485,7 @@ def _emit( Byte-stability: the emitted carrier, manifest, and test are deterministic for a fixed spec (no timestamps / absolute paths embedded), so two runs produce - byte-identical output — the P4-1 determinism acceptance criterion. + byte-identical output — the determinism guarantee the generated artifacts carry. """ paths = _planned_paths(out_dir, spec) module = snake_case_module_name(spec.name) diff --git a/packages/mechdsl-core/src/mechdsl/lawgen/contracts.py b/packages/mechdsl-core/src/mechdsl/lawgen/contracts.py index 3787d8d..8b3f486 100644 --- a/packages/mechdsl-core/src/mechdsl/lawgen/contracts.py +++ b/packages/mechdsl-core/src/mechdsl/lawgen/contracts.py @@ -1,10 +1,10 @@ -"""Lawgen emission contracts — the seam between the CLI and the Phase 2 lowerer. +"""Lawgen emission contracts — the seam between the CLI and the lowerer. Part of the MechDSL lawgen pipeline (YAML law spec → restricted SymPy → Taichi carrier). This module defines the two frozen dataclasses that every downstream lowerer -task and the end-to-end test consume: +stage and the end-to-end test consume: * :class:`TiconstitTarget` — the ``ticonstit`` emission *target profile*: the frozen contract id, the generated-code package, the default Taichi scalar @@ -62,8 +62,8 @@ class TiconstitTarget: """The ``ticonstit`` emission target profile. Carries the frozen contract identity, the generated-code package, the - default Taichi scalar type, and the six JIT budget knobs the Phase 2 - lowerer enforces. + default Taichi scalar type, and the six JIT budget knobs the lowerer + enforces. ``ti_type_default`` defaults to ``"ti.f64"``: MechDSL's tension-positive conventions (``07-CONVENTIONS.md``) and its verification tolerances @@ -142,7 +142,7 @@ def _freeze_mapping(raw: Mapping[str, sp.Expr] | None) -> Mapping[str, sp.Expr]: @dataclass(frozen=True) class PlasticityCarrierSpec: - """A single plasticity carrier law, ready for Phase 2 lowering. + """A single plasticity carrier law, ready for lowering. Fields ------ @@ -161,16 +161,16 @@ class PlasticityCarrierSpec: strain rate ``edot``, temperature ``T``). These are the runtime inputs the generated carrier is a function of. monotone_check: - When ``True``, the P3-2 test emitter appends a monotonicity assertion to + When ``True``, the test emitter appends a monotonicity assertion to the generated test file — checking that the hardening ``R`` is non-decreasing in the accumulated plastic strain across the sample points. Defaults to ``False`` (no monotonicity block emitted), so every - existing construction site (the CLI, Phase-1 tests) that omits it keeps + existing construction site (the CLI, the contract tests) that omits it keeps working unchanged. It is a plain flag, not a SymPy object, so it is validated as a genuine ``bool`` in ``__post_init__``. The R/H/Q expressions and variable bindings are stored as SymPy objects (not - strings) because Phase 2 routes them through ``taichi_printer``, which + strings) because the lowerer routes them through ``taichi_printer``, which consumes SymPy expressions directly. """ diff --git a/packages/mechdsl-core/src/mechdsl/lawgen/diagnostics.py b/packages/mechdsl-core/src/mechdsl/lawgen/diagnostics.py index 84bbea2..5ada6c3 100644 --- a/packages/mechdsl-core/src/mechdsl/lawgen/diagnostics.py +++ b/packages/mechdsl-core/src/mechdsl/lawgen/diagnostics.py @@ -1,12 +1,12 @@ -"""Structured, collect-all diagnostics for the lawgen lowerer (Task P3-1). +"""Structured, collect-all diagnostics for the lawgen lowerer. Part of the MechDSL lawgen pipeline (YAML law spec → restricted SymPy → Taichi carrier). -This module is the *reporting* layer that stands beside the Phase-2 gates -(:mod:`mechdsl.lawgen.budgets`, P2-2; :mod:`mechdsl.lawgen.sympy_to_taichi`, -P2-4). Where Phase 2 fails on the **first** problem, P3-1 turns those checks into -**collect-all** ones: every unsupported SymPy node and every budget breach +This module is the *reporting* layer that stands beside the lowerer's gates +(:mod:`mechdsl.lawgen.budgets`; :mod:`mechdsl.lawgen.sympy_to_taichi`). +Where a fail-first gate stops at the **first** problem, this module turns those +checks into **collect-all** ones: every unsupported SymPy node and every budget breach produces a structured :class:`LawgenDiagnostic`, they are accumulated in a :class:`DiagnosticCollector`, and the whole batch is raised at once as a single :class:`LawgenError` so the user sees *every* problem in one run. @@ -26,9 +26,9 @@ its message, so a caller (or a test) can read every problem off the exception without re-running the compile. -Relationship to the Phase-2 error types ----------------------------------------- -:class:`LawgenError` subclasses :class:`NotImplementedError`. Phase 2's +Relationship to the lowerer's error types +----------------------------------------- +:class:`LawgenError` subclasses :class:`NotImplementedError`. The lowerer's fail-loud raises for unsupported nodes were ``NotImplementedError`` (the unmapped-function / non-exhaustive-``Piecewise`` path); making the collect-all aggregate a ``NotImplementedError`` keeps every ``except NotImplementedError`` / @@ -127,7 +127,7 @@ def render(self) -> str: class LawgenError(NotImplementedError): """Aggregate error carrying *every* diagnostic collected in one lowering run. - Subclasses :class:`NotImplementedError` so Phase 2's fail-loud contract for + Subclasses :class:`NotImplementedError` so the lowerer's fail-loud contract for unsupported nodes (which raised ``NotImplementedError``) is preserved: any existing ``except NotImplementedError`` / ``pytest.raises(NotImplementedError)`` still catches the aggregate, only now the payload is a structured, @@ -138,7 +138,7 @@ class LawgenError(NotImplementedError): * :attr:`diagnostics` — the tuple of :class:`LawgenDiagnostic` records. * ``args`` — the rendered message *plus* each diagnostic, so - ``LawgenError.args`` contains every problem (the P3-1 acceptance check). + ``LawgenError.args`` contains every problem. * ``str(err)`` — a header line naming the count, then every diagnostic's :meth:`LawgenDiagnostic.render` block. """ @@ -178,7 +178,7 @@ def _format(diagnostics: tuple[LawgenDiagnostic, ...]) -> str: class DiagnosticCollector: """Accumulates :class:`LawgenDiagnostic` records, then raises them all at once. - The collect-all counterpart to Phase 2's fail-first gates: a caller + The collect-all counterpart to the lowerer's fail-first gates: a caller :meth:`add`\\ s a diagnostic for *every* problem it finds and calls :meth:`raise_if_any` at the end, so a single run reports every problem rather than aborting on the first. diff --git a/packages/mechdsl-core/src/mechdsl/lawgen/guard_transforms.py b/packages/mechdsl-core/src/mechdsl/lawgen/guard_transforms.py index 678e69c..e26d4b0 100644 --- a/packages/mechdsl-core/src/mechdsl/lawgen/guard_transforms.py +++ b/packages/mechdsl-core/src/mechdsl/lawgen/guard_transforms.py @@ -1,4 +1,4 @@ -"""Numerical-guard injection for the SymPy → Taichi lowerer (Task P2-3). +"""Numerical-guard injection for the SymPy → Taichi lowerer. Part of the MechDSL lawgen pipeline (YAML law spec → restricted SymPy → Taichi carrier). Correctness-critical: the guards emitted here must match the @@ -43,7 +43,7 @@ * Swift ``(peeq + p0)**n`` and ``p0**n`` → **floor the base** with ``ti.max(base, 1e-12)`` then ``ti.pow`` — *only* when the exponent is - non-integer. An integer power needs no floor (and P2-4 inlines small-int + non-integer. An integer power needs no floor (and the printer inlines small-int powers to multiplication anyway). * ``exp(-b*peeq)`` is **UNGUARDED**, matching the Voce idiom (physical domain ``peeq >= 0`` ⇒ argument ``<= 0`` ⇒ ``exp in (0, 1]``, no overflow). Guarding @@ -140,7 +140,7 @@ def inject_guards(expr: sp.Expr) -> sp.Expr: Rules (see the module docstring for the golden anchoring): * ``Pow(base, 1/2)`` (``sqrt``) → positive-floor the base. - * ``Pow(base, non-negative integer)`` → **no** guard (P2-4 inlines). + * ``Pow(base, non-negative integer)`` → **no** guard (the printer inlines). * ``Pow(base, non-integer: fractional or symbolic, any sign)`` → positive base-floor via ``ti.pow`` (a fractional power's base must be positive). * ``Pow(base, negative integer)`` (``1/x``, ``1/x**2``) → sign-preserving @@ -205,7 +205,7 @@ def _guard_pow(base: sp.Expr, exp: sp.Expr) -> sp.Expr: (Gate-B Finding 2). 3. ``exp`` a negative integer (``x**-1``, ``x**-2``) → genuine reciprocal: sign-preserving denominator guard (unless ``base`` is a nonzero constant). - 4. ``exp`` a non-negative integer → no guard (exact; P2-4 inlines small ints). + 4. ``exp`` a non-negative integer → no guard (exact; the printer inlines small ints). """ # 1. sqrt: SymPy's ``Pow(x, 1/2)``. Floor the radicand; the printer routes # the half-exponent to ``ti.sqrt`` → ``ti.sqrt(ti.max(x, 1e-12))``. diff --git a/packages/mechdsl-core/src/mechdsl/lawgen/manifest.py b/packages/mechdsl-core/src/mechdsl/lawgen/manifest.py index afa3b90..8967e2b 100644 --- a/packages/mechdsl-core/src/mechdsl/lawgen/manifest.py +++ b/packages/mechdsl-core/src/mechdsl/lawgen/manifest.py @@ -1,12 +1,12 @@ -"""Manifest emitter — writes ``_manifest.json`` matching Cycle 0's schema (Task P3-3). +"""Manifest emitter — writes ``_manifest.json`` matching Cycle 0's schema. Part of the MechDSL lawgen pipeline (YAML law spec → restricted SymPy → Taichi carrier). :func:`emit_manifest` produces one *laws entry* — and, via :func:`write_manifest`, merges it into the ``{"_schema": {...}, "laws": [...]}`` file that -NumerixWeave's auto-register loop reads and that Task P4-2 relies on for -provenance (P4-2's own gate is run-to-run ``source_hash`` determinism, not a +NumerixWeave's auto-register loop reads and that provenance checks rely on +(the provenance gate is run-to-run ``source_hash`` determinism, not a whole-file byte-diff against Cycle 0). The authoritative schema is the real Cycle 0 ``libs/ticonstit/src/ticonstit/generated/_manifest.json`` in the NumerixWeave repo. Its ``laws[0]`` entry has **nine** fields, emitted in @@ -23,25 +23,25 @@ seam is committed artifacts only: the manifest we *emit* is compared against the real file by reading that file's **bytes** (in the tests), never by importing it. -``source_hash`` convention — hash the INPUT formula string (P4-2 reconciliation) --------------------------------------------------------------------------------- -# NOTE (P4-2): Cycle 0's ``_manifest.json`` defines ``source_hash`` as the -# SHA-256 of the **generator INPUT** — the canonical R formula string +``source_hash`` convention — hash the INPUT formula string +----------------------------------------------------------- +# NOTE: Cycle 0's ``_manifest.json`` defines ``source_hash`` as the SHA-256 of +# the **generator INPUT** — the canonical R formula string # (``"R = sigma0 + Q*(1-exp(-b*p)) + K*((p+p0)**n - p0**n)"`` → ``7b5af3a8…``), -# NOT the emitted-output-lines hash that P2-4's +# NOT the emitted-output-lines hash that # :func:`~mechdsl.lawgen.sympy_to_taichi.compute_source_hash` / # :attr:`~mechdsl.lawgen.sympy_to_taichi.LoweredExpr.source_hash` produces. # This module therefore hashes the input formula string via # :func:`compute_input_formula_hash` and deliberately does **not** consume the -# P2-4 emitted-lines hash for the manifest. P2-4's hash remains valid provenance -# of the *output*; the manifest fingerprints the *input* so NumerixWeave can -# verify the law was generated from the formula it claims. P4-1 supplies the -# exact canonical formula string (from the law yaml's ``R`` formula) — the -# canonical-string convention this module locks is: the formula string is hashed -# **verbatim, UTF-8, no normalisation** (no whitespace collapsing, no operator -# reordering). Cycle 0's value is reproduced exactly under this convention (see -# ``test_manifest.py`` / ``test_P3-3.py``), so P4-1 must pass the formula string -# with the same spelling the yaml carries. +# lowerer's emitted-lines hash for the manifest. That hash remains valid +# provenance of the *output*; the manifest fingerprints the *input* so +# NumerixWeave can verify the law was generated from the formula it claims. The +# carrier emitter supplies the exact canonical formula string (from the law +# yaml's ``R`` formula) — the canonical-string convention this module locks is: +# the formula string is hashed **verbatim, UTF-8, no normalisation** (no +# whitespace collapsing, no operator reordering). Cycle 0's value is reproduced +# exactly under this convention (see ``test_manifest.py``), so callers must +# pass the formula string with the same spelling the yaml carries. """ from __future__ import annotations @@ -104,8 +104,8 @@ def compute_input_formula_hash(formula: str) -> str: Parameters ---------- formula: - The canonical input formula string (P4-1 supplies it from the law yaml's - ``R`` formula). Must be a non-empty string. + The canonical input formula string (the carrier emitter supplies it from + the law yaml's ``R`` formula). Must be a non-empty string. Returns ------- @@ -128,7 +128,7 @@ def formula_matches_spec(input_formula: str, spec: PlasticityCarrierSpec) -> boo / ``parameters`` come from the ``spec``. Those two inputs are independent, so nothing structurally guarantees the hashed formula is the same law the spec describes — a stale or mistyped ``input_formula`` would fingerprint the *wrong* - law without complaint. This helper closes that gap for P4-1: parse the formula + law without complaint. This helper closes that gap: parse the formula (stripping an optional ``R =`` / ``R:=`` left-hand side), sympify the RHS, and check it is symbolically equal to ``spec.R``. @@ -137,8 +137,8 @@ def formula_matches_spec(input_formula: str, spec: PlasticityCarrierSpec) -> boo ``Q_inf`` — so for the *transitional* SwiftVoce law the formula variable names deliberately differ from ``spec.parameters`` and this check will return ``False``. That is exactly why :func:`emit_manifest`'s ``check_matches_spec`` - is **opt-in** (default off): turn it on once P4-1 authors a law whose formula - spelling matches the spec's own symbols, so a drifted formula fails loud. + is **opt-in** (default off): turn it on for a law whose formula spelling + matches the spec's own symbols, so a drifted formula fails loud. Returns ------- @@ -191,8 +191,8 @@ def _split_parameters( flat list): * **Explicit** — when ``required`` and/or ``optional`` are given, they are - used verbatim (order preserved). This is the path P4-1 takes: the law yaml - states which parameters are required vs optional. Every name in + used verbatim (order preserved). This is the path the carrier emitter + takes: the law yaml states which parameters are required vs optional. Every name in ``required``/``optional`` must appear in the spec's ``parameters`` (no inventing parameters the law does not declare), the two lists must be disjoint, and together they must be a **complete partition** — every @@ -205,7 +205,7 @@ def _split_parameters( carrier lists exactly the parameters its expressions reference, so absent an explicit yaml split every listed parameter is load-bearing (required). This keeps a spec-only call (tests, quick emission) valid while letting - P4-1 override with the real required/optional partition. + the carrier emitter override with the real required/optional partition. """ all_params = list(parameters) if required is None and optional is None: @@ -262,7 +262,7 @@ def emit_manifest( input_formula: The canonical generator-input formula string. Its SHA-256 (verbatim, UTF-8) is the entry ``source_hash`` — the INPUT-formula hash Cycle 0 uses, NOT the - P2-4 emitted-lines hash (see the module note). ``input_formula`` and ``spec`` + lowerer's emitted-lines hash (see the module note). ``input_formula`` and ``spec`` are independent inputs; pass ``check_matches_spec=True`` to assert the hashed formula actually is ``spec.R``. check_matches_spec: @@ -271,11 +271,11 @@ def emit_manifest( a stale/mistyped formula fingerprints the wrong law loudly instead of silently. Default ``False`` because, during the Cycle 0 transition, the published formula string deliberately spells parameters differently from the - spec's material card (``Q`` vs ``Q_inf``); P4-1 turns it on once a law's + spec's material card (``Q`` vs ``Q_inf``); turn it on for a law whose formula spelling matches its own symbols (see :func:`formula_matches_spec`). target_contract: The **runtime** contract the law implements (e.g. ``"VoceHardeningModel"``). - Supplied explicitly by P4-1 from the law yaml — this is deliberately NOT + Supplied explicitly by the caller from the law yaml — this is deliberately NOT ``TiconstitTarget.contract_id`` (the emission-contract id), which is a different thing. exports: @@ -285,7 +285,7 @@ def emit_manifest( (e.g. ``"swift_voce.py"``); its stem is the imported submodule. tests: Test file paths that pin the generated law (golden / FD-derivative / - guard-audit) — the paths P3-2's ``emit_tests`` writes. + guard-audit) — the paths the test emitter's ``emit_tests`` writes. kind: Registry bucket. Defaults to ``"plasticity"`` (the only Cycle 0 bucket). required, optional: @@ -360,7 +360,7 @@ def write_manifest( ``{required, optional}``), which is insertion-ordered, *not* alphabetical. ``sort_keys=True`` would gratuitously reorder the keys away from Cycle 0, so a structural diff against the hand-authored artifact would differ only in the - field *values* P4-1 supplies, never in key order. + field *values* the caller supplies, never in key order. Parameters ---------- diff --git a/packages/mechdsl-core/src/mechdsl/lawgen/sympy_to_taichi.py b/packages/mechdsl-core/src/mechdsl/lawgen/sympy_to_taichi.py index cd84bf9..40e8817 100644 --- a/packages/mechdsl-core/src/mechdsl/lawgen/sympy_to_taichi.py +++ b/packages/mechdsl-core/src/mechdsl/lawgen/sympy_to_taichi.py @@ -1,45 +1,42 @@ -"""Deterministic SymPy → Taichi scalar-expression lowerer (Task P2-1). +"""Deterministic SymPy → Taichi scalar-expression lowerer. Part of the MechDSL lawgen pipeline (YAML law spec → restricted SymPy → Taichi carrier). -This module is the foundation the rest of Phase 2 builds on: a *dedicated*, +This module is the foundation the rest of the lowerer builds on: a *dedicated*, idiomatic SymPy printer that turns a scalar ``sympy.Expr`` into a Taichi source string, plus a public :func:`lower_expression` entry point that applies deterministic common-subexpression elimination (CSE) before printing. -Why a bespoke printer (the R4 correction) ------------------------------------------ -The P1-3 reuse audit (``lawgen/REUSE.md``, Gate-B-verified) established that +Why a bespoke printer +--------------------- +The reuse audit (``lawgen/REUSE.md``) established that ``codegen/taichi_printer.py`` has **no** reusable scalar SymPy→Taichi printer — its surface is FEM ``ArtifactBundle`` emission with hard-coded literal Taichi. The only existing SymPy→Taichi code lives in ``codegen/energy_emitter.py`` and converts the expression to Python source (SymPy's ``py``-``code`` printer) then rewrites ``math.*`` to ``ti.*`` with a regex substitution over that string. That -is the **R4 anti-pattern** the plan forbids: brittle string surgery, no CSE, no +is the anti-pattern this module avoids: brittle string surgery, no CSE, no whitelist enforcement at the printer boundary. -So P2-1 adds a proper printer instead. It subclasses SymPy's +So this module adds a proper printer instead. It subclasses SymPy's :class:`~sympy.printing.str.StrPrinter` and overrides the relevant ``_print_*`` methods, so function/operator lowering happens *inside* SymPy's own dispatch — -no Python-source printing, no regex. Unmapped nodes fail loud (R2): a clear -``NotImplementedError`` naming the node and the phase that adds it, never silent -wrong code. - -Scope of this task ------------------- -Printer + deterministic CSE **only**. Deliberately out of scope (separate -tasks — clean seams are noted inline where each attaches): - -* **P2-2** — scalar-expression budget counting over the six ``TiconstitTarget`` - knobs. -* **P2-3** — numerical-guard injection (``ti.max(x, 1e-12)`` for log/sqrt, - ``ti.select`` for pow with symbolic exponent, guarded division). This printer - emits ``ti.pow``/``ti.log``/``ti.sqrt`` *unguarded*; P2-3 attaches a rewrite - pass upstream of :func:`lower_expression` (or wraps the mapped names). -* **P2-4** — ``Piecewise`` → nested ``ti.select`` lowering, small-integer ``Pow`` - inlining, and a ``source_hash`` provenance field on :class:`LoweredExpr` - (see the extension note on the dataclass). +no Python-source printing, no regex. Unmapped nodes fail loud: a clear +``NotImplementedError`` naming the node, never silent wrong code. + +Scope +----- +Printer + deterministic CSE, plus the pieces that attach at clean seams: + +* scalar-expression budget counting over the six ``TiconstitTarget`` knobs + (:mod:`mechdsl.lawgen.budgets`); +* numerical-guard injection (``ti.max(x, 1e-12)`` for log/sqrt, guarded + division). The printer itself emits ``ti.pow``/``ti.log``/``ti.sqrt`` + *unguarded*; :mod:`mechdsl.lawgen.guard_transforms` is a rewrite pass + upstream of printing whose marker nodes :class:`TaichiGuardedPrinter` renders; +* ``Piecewise`` → nested ``ti.select`` lowering, small-integer ``Pow`` inlining, + and a ``source_hash`` provenance field on :class:`LoweredExpr`. """ from __future__ import annotations @@ -173,14 +170,13 @@ class TaichiExprPrinter(StrPrinter): --------- * Symbols print as their name; ``Integer``/``Float``/``Rational`` print as numeric literals; ``Add``/``Mul`` and integer/general ``Pow`` print via - the base :class:`StrPrinter` (``x**n`` stays ``x**n`` for P2-1). + the base :class:`StrPrinter` (``x**n`` stays ``x**n`` unless inlined). * Whitelisted functions (:data:`MATH_TO_TAICHI`) print as their ``ti.*`` call, e.g. ``exp(-b*p)`` → ``ti.exp(-b*p)``. * ``sqrt(x)`` — SymPy's ``Pow(x, 1/2)`` — prints as ``ti.sqrt(x)`` rather than the base printer's literal ``sqrt(x)``. - * **Fail loud (R2):** any function/node this printer does not map raises - :class:`NotImplementedError` naming the node and the phase that adds it. - Nothing is emitted silently. + * **Fail loud:** any function/node this printer does not map raises + :class:`NotImplementedError` naming the node. Nothing is emitted silently. """ printmethod = "_taichi" @@ -254,8 +250,8 @@ def _print_Pow(self, expr: sp.Pow, rational: bool = False) -> str: call). We intercept the ``S.Half`` exponent and emit ``ti.sqrt(base)`` so the allowed-function table stays the single source of truth. - Small-integer inlining (P2-4) - ----------------------------- + Small-integer inlining + ---------------------- A ``Pow(base, n)`` with ``n`` a **non-negative** integer and ``n <= SMALL_INT_POW_LIMIT`` is inlined to repeated multiplication instead of a ``ti.pow`` call: ``x**2`` → ``x*x``, ``x**3`` → ``x*x*x``. Trivial powers @@ -275,7 +271,7 @@ def _print_Pow(self, expr: sp.Pow, rational: bool = False) -> str: which reports an inlined power at ``Mul`` precedence so ``_print_Mul`` wraps the denominator → ``a/(x*x)``. This holds on the guarded path too (``a/x**2`` → ``a/(*)``). This runs on the - already-guarded tree, so ``base`` carries any P2-3 guard injected. + already-guarded tree, so ``base`` carries any guard injected upstream. """ # Mirror SymPy's own idiom (``-expr.exp is S.Half``): negate then # identity-check, so an exact ``Rational(-1, 2)`` matches while a @@ -326,12 +322,12 @@ def _inline_small_int_pow(self, base: sp.Expr, n: int) -> str | None: return "*".join([printed] * n) def _print_Piecewise(self, expr: sp.Piecewise) -> str: - """Lower a ``Piecewise`` to a right-nested ``ti.select`` chain (P2-4). + """Lower a ``Piecewise`` to a right-nested ``ti.select`` chain. ``Piecewise((e1, c1), (e2, c2), ..., (en, True))`` → ``ti.select(c1, e1, ti.select(c2, e2, ... en))``. Each branch value and each condition is lowered recursively through this printer, so guards - injected inside a branch expression (P2-3) render normally and a nested + injected inside a branch expression render normally and a nested ``Piecewise`` becomes a nested ``ti.select``. Relational conditions (``x > 0``) print via the base :class:`StrPrinter` as Taichi-valid scalar comparisons. @@ -364,7 +360,7 @@ def _print_Piecewise(self, expr: sp.Piecewise) -> str: class TaichiGuardedPrinter(TaichiExprPrinter): - """A :class:`TaichiExprPrinter` that renders the P2-3 guard marker nodes. + """A :class:`TaichiExprPrinter` that renders the numerical-guard marker nodes. Pairs with :func:`mechdsl.lawgen.guard_transforms.inject_guards`: that pass wraps guardable sub-trees in :class:`~mechdsl.lawgen.guard_transforms.\ @@ -450,7 +446,7 @@ def _unsupported_function_names(expr: sp.Basic) -> list[str]: Collecting all unmapped names up front (rather than raising on the first one deep in the recursive printer) is what lets the caller report *every* unsupported node in a single - :class:`~mechdsl.lawgen.diagnostics.LawgenError` (P3-1 collect-all). + :class:`~mechdsl.lawgen.diagnostics.LawgenError` (collect-all). """ names = { type(fn).__name__ @@ -526,7 +522,7 @@ def _collect_piecewise_budget( ) -> None: """Add a budget diagnostic if ``expr``'s largest ``Piecewise`` exceeds the branch budget. - Reuses P2-2's :func:`~mechdsl.lawgen.budgets.count_piecewise_branches` and the + Reuses :func:`~mechdsl.lawgen.budgets.count_piecewise_branches` and the shared budget-diagnostic builder, so the ``reason`` carries the measured branch count AND the ``max_piecewise_branches`` limit. Runs in the same pre-pass, so an over-budget switch never reaches the printer and its @@ -564,7 +560,7 @@ class LoweredExpr: A deterministic SHA-256 provenance digest over the emitted lines (:func:`compute_source_hash`), computed automatically in ``__post_init__``. 64 lowercase hex chars. Every ``LoweredExpr`` carries - one — including instances constructed directly (P2-2's budget tests build + one — including instances constructed directly (the budget tests build ``LoweredExpr(temporaries=…, returns=…)`` without passing a hash). It is excluded from ``__init__`` (callers never supply it) and from equality (it is derived from ``temporaries``/``returns``, so two lowered results @@ -591,15 +587,15 @@ def compute_source_hash(temporaries: Sequence[str], returns: Sequence[str]) -> s line sequence — and therefore the hash — is fully deterministic for a given input and SymPy version. Returns 64 lowercase hex chars. - # NOTE (P4-2): this hashes the *emitted output lines*. Cycle 0's - # ``swift_voce.py`` header + ``_manifest.json`` instead define ``source_hash`` - # as the SHA-256 of the canonical *input formula string* (the generator - # INPUT, e.g. ``"R = sigma0 + Q*(1-exp(-b*p)) + K*((p+p0)**n - p0**n)"`` → - # ``7b5af3a8…``), NOT the emitted output. This P2-4 hash satisfies the task's - # determinism + 64-hex acceptance criteria but will NOT equal Cycle 0's hash. - # This function is kept small and separately named so P4-1/P4-2 can add or - # switch to an input-formula hash for manifest-matching without touching the - # lowerer. Do NOT reconcile the two definitions here — that is P4's call. + # NOTE: this hashes the *emitted output lines*. Cycle 0's ``swift_voce.py`` + # header + ``_manifest.json`` instead define ``source_hash`` as the SHA-256 + # of the canonical *input formula string* (the generator INPUT, e.g. + # ``"R = sigma0 + Q*(1-exp(-b*p)) + K*((p+p0)**n - p0**n)"`` → + # ``7b5af3a8…``), NOT the emitted output. This hash is deterministic and + # 64-hex but will NOT equal Cycle 0's hash. The manifest emitter + # (:mod:`mechdsl.lawgen.manifest`) owns the input-formula hash; this function + # is kept small and separately named so the two conventions stay independent + # without touching the lowerer. Do NOT reconcile the two definitions here. """ payload = "\n".join([*temporaries, *returns]) return hashlib.sha256(payload.encode()).hexdigest() @@ -623,7 +619,7 @@ def lower_expression( budget — accumulating a diagnostic per problem and raising them together as one :class:`~mechdsl.lawgen.diagnostics.LawgenError` *before* any code is produced (never fail-first, never a silent drop). - 3. **P2-3 guard injection** (when ``guards`` is true): rewrite each + 3. **Numerical-guard injection** (when ``guards`` is true): rewrite each expression with :func:`~mechdsl.lawgen.guard_transforms.inject_guards`, floring ``log``/``sqrt``/symbolic-``pow`` bases with ``ti.max(·, 1e-12)`` and guarding division denominators — reproducing Cycle 0's hand-authored @@ -649,19 +645,18 @@ def lower_expression( when ``guards`` is true, else a :class:`TaichiExprPrinter`. An explicit printer is used verbatim (the caller owns marker rendering then). guards: - When ``True`` (default), inject the P2-3 numerical guards and render them + When ``True`` (default), inject the numerical guards and render them with a guarded printer. Set ``False`` for the raw, unguarded lowering - (the P2-1 path — used by tests and by callers that guard elsewhere). - P2-4 / P4 depend on this flag: the guarded path is the one the P4-2 - equivalence gate measures. + (used by tests and by callers that guard elsewhere). The guarded path is + the production path — the one the numerical equivalence gate measures. target: The :class:`~mechdsl.lawgen.contracts.TiconstitTarget` whose ``max_piecewise_branches`` knob gates ``Piecewise`` lowering. Defaults to - a plain :class:`TiconstitTarget` (the plan-frozen default of 8 branches). + a plain :class:`TiconstitTarget` (the default of 8 branches). A ``Piecewise`` with more branches yields a budget diagnostic in the collect-all :class:`~mechdsl.lawgen.diagnostics.LawgenError` before any - ``ti.select`` is emitted (pre-emission fail-loud, reusing P2-2's counter - and error). + ``ti.select`` is emitted (pre-emission fail-loud, reusing the budget + module's counter and error). Returns ------- @@ -672,11 +667,11 @@ def lower_expression( Raises ------ LawgenError - Collect-all (P3-1): if any expression contains unsupported nodes, a + Collect-all: if any expression contains unsupported nodes, a non-exhaustive ``Piecewise``, or an over-budget ``Piecewise``, *every* such problem across *all* expressions is collected and raised together as one :class:`~mechdsl.lawgen.diagnostics.LawgenError` (a - ``NotImplementedError`` subclass, preserving the Phase-2 fail-loud + ``NotImplementedError`` subclass, preserving the fail-loud contract). Each diagnostic carries the offending node, a reason, and an actionable fix. No node is ever silently dropped (R2). TypeError diff --git a/packages/mechdsl-core/src/mechdsl/lawgen/test_emitter.py b/packages/mechdsl-core/src/mechdsl/lawgen/test_emitter.py index 09e5032..9094e2d 100644 --- a/packages/mechdsl-core/src/mechdsl/lawgen/test_emitter.py +++ b/packages/mechdsl-core/src/mechdsl/lawgen/test_emitter.py @@ -1,4 +1,4 @@ -"""Generated-tests emitter, one pytest file per scalar plasticity law (Task P3-2). +"""Generated-tests emitter, one pytest file per scalar plasticity law. Part of the MechDSL lawgen pipeline (YAML law spec → restricted SymPy → Taichi carrier). @@ -20,8 +20,8 @@ compares a central finite-difference derivative w.r.t. that factor's **own** primary variable against the *analytic* derivative (``sympy.diff``, lowered through the same ``lambdify`` path) at the sample points, to ``rtol = 1e-5`` - (standard FD precision — deliberately **not** the ``1e-10`` P4-2 equivalence - gate). A factor that is constant in its primary (e.g. a rate-independent + (standard FD precision — deliberately **not** the ``1e-10`` numerical + equivalence gate). A factor that is constant in its primary (e.g. a rate-independent ``H = 1``) has analytic derivative ``0`` and FD ``≈ 0``, so the general FD-vs-analytic check passes without any special-casing. * ``test_monotonicity`` — emitted **iff** ``spec.monotone_check`` is ``True``: @@ -174,7 +174,7 @@ def _ordered_symbol_names(spec: PlasticityCarrierSpec, primary_name: str) -> lis Primary variable first, then the remaining variable bindings, then the material parameters — each group in the spec's own declaration order and with duplicates removed. A stable order keeps the generated file byte-deterministic - (P3-3 lists these tests in the manifest) and lets the reference/FD/monotone + (the manifest lists these tests) and lets the reference/FD/monotone tests share one argument tuple. """ ordered: list[str] = [primary_name] @@ -449,7 +449,7 @@ def _render_header(spec: PlasticityCarrierSpec) -> str: monotone = "yes" if spec.monotone_check else "no" return ( f'"""Auto-generated tests for plasticity carrier {spec.name!r} ' - f"(MechDSL lawgen P3-2).\n\n" + f"(MechDSL lawgen).\n\n" f"DO NOT EDIT BY HAND — regenerate via mechdsl.lawgen.test_emitter.emit_tests.\n\n" f"Reference eval + per-factor FD-derivative (rtol={FD_RTOL!r}) self-consistency\n" f"checks over {N_SAMPLE_POINTS} sample points, with fixed placeholder parameters " diff --git a/packages/mechdsl-core/src/mechdsl/lib/plasticity.py b/packages/mechdsl-core/src/mechdsl/lib/plasticity.py index d2ffd50..fd99434 100644 --- a/packages/mechdsl-core/src/mechdsl/lib/plasticity.py +++ b/packages/mechdsl-core/src/mechdsl/lib/plasticity.py @@ -1,13 +1,13 @@ """Plasticity dispatch — algo2code-generated path with imported fallback. -post_recovery_plan Phase 5 (P5-3). The dispatcher's algo2code path now -genuinely consumes ``algo2code.transpile`` end-to-end: the canonical -algpseudocode at ``dev/algorithms/radial_return_j2.tex`` is transpiled +The dispatcher's algo2code path consumes ``algo2code.transpile`` +end-to-end: the canonical +algpseudocode at ``algo2code/library/algorithms/radial_return_j2.tex`` is transpiled to Python at module import time, exec'd into a namespace, and the resulting scalar Newton inner loop is invoked from inside the wrapper. The imported reference path remains as a feature-flagged fallback. -Phase 5 also landed the algo2code parser fixes (multi-letter scratch +This path relies on the algo2code parser fixes (multi-letter scratch identifiers and binary ``/`` in assignment LHS contexts) plus the Taichi codegen scalar-algorithm fix that previously blocked direct emission for radial-return. diff --git a/packages/mechdsl-core/src/mechdsl/lib/plasticity_kinematic.py b/packages/mechdsl-core/src/mechdsl/lib/plasticity_kinematic.py index c525508..a748424 100644 --- a/packages/mechdsl-core/src/mechdsl/lib/plasticity_kinematic.py +++ b/packages/mechdsl-core/src/mechdsl/lib/plasticity_kinematic.py @@ -1,13 +1,13 @@ """J2 linear kinematic (Prager) hardening — algo2code-orchestrated path. -constitutive_latex Phase 6 (P6-2). The dissipative kinematic-hardening +The dissipative kinematic-hardening return map: the yield surface *translates* (back-stress ``beta``) rather than *expands*. Yield is measured on the RELATIVE (shifted) stress ``xi = dev(S) - beta`` with a CONSTANT yield radius ``sigma_y0`` (no isotropic hardening — all hardening lives in ``beta``). The scalar plastic-multiplier solve is the ``algo2code``-transpiled -function from ``dev/algorithms/radial_return_j2_kinematic.tex`` (loaded +function from ``algo2code/library/algorithms/radial_return_j2_kinematic.tex`` (loaded via :func:`algo2code.library.radial_return_j2_kinematic.transpile_radial_return_j2_kinematic`). The surrounding tensor algebra (deviatoric split, relative stress, von Mises of ``xi``, stress reconstruction, plastic-strain and back-stress diff --git a/packages/mechdsl-core/src/mechdsl/lib/plasticity_mixed.py b/packages/mechdsl-core/src/mechdsl/lib/plasticity_mixed.py index 60b3b61..ff89935 100644 --- a/packages/mechdsl-core/src/mechdsl/lib/plasticity_mixed.py +++ b/packages/mechdsl-core/src/mechdsl/lib/plasticity_mixed.py @@ -1,21 +1,22 @@ """J2 mixed hardening (isotropic power-law + linear kinematic) — algo2code path. -constitutive_latex Phase 6 (P6-3). The dissipative MIXED-hardening +The dissipative MIXED-hardening return map: the yield surface BOTH *translates* (back-stress ``beta``, -Prager — from P6-2) AND *expands* (power-law radius -``sigma_y(alpha) = sigma_y0 + K*alpha^n`` — from P6-1), simultaneously. +Prager — as in the kinematic variant) AND *expands* (power-law radius +``sigma_y(alpha) = sigma_y0 + K*alpha^n`` — as in the isotropic variant), +simultaneously. Yield is measured on the RELATIVE (shifted) stress ``xi = dev(S) - beta`` against the EXPANDING radius ``sigma_y(alpha)``:: f = ||xi||_eq - sigma_y(alpha), sigma_y(alpha) = sigma_y0 + K*alpha^n The scalar plastic-multiplier solve is the ``algo2code``-transpiled -function from ``dev/algorithms/radial_return_j2_mixed.tex`` (loaded via +function from ``algo2code/library/algorithms/radial_return_j2_mixed.tex`` (loaded via :func:`algo2code.library.radial_return_j2_mixed.transpile_radial_return_j2_mixed`). Because the isotropic part is a nonlinear power law in ``alpha = alpha_old + dl`` the discrete consistency condition is NONLINEAR in ``dl``, so the scalar source runs the Newton loop of the -isotropic variant (P6-1) — with the kinematic ``(3*mu + H_kin)*dl`` +isotropic variant — with the kinematic ``(3*mu + H_kin)*dl`` linear term in the residual and the Prager modulus folded into the Newton denominator alongside the isotropic slope. The surrounding tensor algebra is orchestrated here in NumPy — the same division of labour as @@ -31,8 +32,8 @@ - deviatoric back-stress tensor ``beta`` (Prager translation); - plastic strain ``Ep`` (the elastic predictor uses ``E - Ep``). -Flow direction and tensor updates (identical convention to P6-2) ----------------------------------------------------------------- +Flow direction and tensor updates (identical convention to the kinematic variant) +--------------------------------------------------------------------------------- The flow normal is the von-Mises yield-function gradient ``nf = d||xi||_eq/dS = (3/2) * xi / ||xi||_eq``. With ``dEp = dl*nf`` the @@ -51,10 +52,9 @@ ---------- - ``H_kin = 0`` -> ``beta`` stays 0, ``xi == dev(S)``: the law reduces to - the isotropic power-law variant (P6-1). + the isotropic power-law variant. - ``K = 0`` -> ``sigma_y(alpha) == sigma_y0`` (constant radius), the - residual is linear: the law reduces to the linear kinematic variant - (P6-2). + residual is linear: the law reduces to the linear kinematic variant. """ from __future__ import annotations @@ -109,12 +109,12 @@ class J2MixedMaterial: Initial yield stress (> 0). K : float Isotropic power-law hardening modulus (>= 0). ``K = 0`` collapses - to pure kinematic hardening (P6-2). + to pure kinematic hardening. n : float Isotropic power-law hardening exponent (> 0). H_kin : float Linear kinematic (Prager) hardening modulus (>= 0). ``H_kin = 0`` - collapses to pure isotropic hardening (P6-1). + collapses to pure isotropic hardening. """ E: float diff --git a/packages/mechdsl-core/src/mechdsl/lowering/boundary.py b/packages/mechdsl-core/src/mechdsl/lowering/boundary.py index 4bcb639..317e64f 100644 --- a/packages/mechdsl-core/src/mechdsl/lowering/boundary.py +++ b/packages/mechdsl-core/src/mechdsl/lowering/boundary.py @@ -1,9 +1,8 @@ """Lower Neumann ``BoundaryCondition`` to per-node force contributions. Bridges the IR layer (``mechdsl.ir.mechanics_ir.BoundaryCondition``) to the -codegen-layer primitives in ``mechdsl.codegen.boundary_codegen``. Introduced -in post_recovery_plan Phase 1 (P1-3) so that directive-driven Neumann BCs -flow through the pipeline ``LaTeX → IR → lowering → codegen`` without a +codegen-layer primitives in ``mechdsl.codegen.boundary_codegen``, so that +directive-driven Neumann BCs flow through the pipeline ``LaTeX → IR → lowering → codegen`` without a manual numeric injection. Typical usage:: @@ -60,7 +59,7 @@ def resolve_traction_vector( ) -> NDArray: """Resolve a BC's traction spec to a length-3 numeric numpy array. - The IR allows ``traction`` to be either a numeric tuple (post P1-1 + The IR allows ``traction`` to be either a numeric tuple (the directive-driven form) or a symbolic string (legacy form like ``"t_bar"``). Numeric forms pass through; symbolic forms are looked up in ``traction_registry`` if supplied, otherwise an explicit error @@ -72,8 +71,7 @@ def resolve_traction_vector( # consistent error message. raise ValueError( f"BoundaryCondition(name={bc.name!r}) has no traction; " - "post_recovery_plan Phase 1 (P1-3) requires Neumann BCs to " - "carry a traction vector or registered symbol." + "Neumann BCs must carry a traction vector or a registered symbol." ) if isinstance(bc.traction, tuple): return np.asarray(bc.traction, dtype=np.float64) @@ -126,9 +124,8 @@ def per_node_contributions( Iterates the dense ``NeumannBC.force`` array and emits one entry per node whose force vector exceeds ``zero_tol`` in absolute value. With the default ``zero_tol=0.0`` only exactly-zero rows are dropped, so - nodes outside the tagged surface contribute nothing — matching the - P1-3 acceptance "non-tagged surface elements receive zero - contribution". + nodes outside the tagged surface contribute nothing ("non-tagged + surface elements receive zero contribution"). """ nbc = lower_neumann(bc, mesh, traction_registry) contribs: list[NodalForceContribution] = [] diff --git a/packages/mechdsl-core/src/mechdsl/lowering/einsum_extract.py b/packages/mechdsl-core/src/mechdsl/lowering/einsum_extract.py index bcd52d1..b7999f6 100644 --- a/packages/mechdsl-core/src/mechdsl/lowering/einsum_extract.py +++ b/packages/mechdsl-core/src/mechdsl/lowering/einsum_extract.py @@ -1,6 +1,6 @@ """Extract einsum strings from Element IR for the optimiser. -This is the canonical location for einsum extraction, per ``.claude/rules/ir.md``. +This is the canonical location for einsum extraction. The function is deterministic and runs once per localisation pass. """ @@ -153,7 +153,7 @@ def tangent_matvec_apply_spec(element_ir: ElementIR) -> EinsumSpec: that dict's three-key contract (``strain_displacement``, ``internal_force``, ``tangent_matvec``) is pinned by existing tests and golden artifacts. The matvec is an additional, optimiser-only view used - by the matrix-free codegen path (PlanJune14 P3-2). + by the matrix-free codegen path. Parameters ---------- @@ -213,7 +213,7 @@ def build_tangent_matvec_plan(element_ir: ElementIR) -> ContractionPlan: exactly as :func:`mechdsl.lowering.fe_localise.localise_and_optimize` does for the three standard specs. - The matrix-free tangent operator (PlanJune14 P3-2) *is* this optimised + The matrix-free tangent operator *is* this optimised contraction applied to ``v`` — the printer emits the recorded path rather than hand-rolling the contraction. diff --git a/packages/mechdsl-core/src/mechdsl/lowering/fe_localise.py b/packages/mechdsl-core/src/mechdsl/lowering/fe_localise.py index 3c5624c..b1234a9 100644 --- a/packages/mechdsl-core/src/mechdsl/lowering/fe_localise.py +++ b/packages/mechdsl-core/src/mechdsl/lowering/fe_localise.py @@ -25,15 +25,15 @@ class LocalisationError(UnsupportedError, ValueError): """Raised when ``localise()`` rejects a stable-path combination. - Recovery-plan P4-4 (R3.4) added this exception to give the lowering + This exception gives the lowering layer a single, named rejection class with a stable contract: every instance carries a clear phase pointer in its message naming the Plan-B phase that adds support for the rejected combination. Subclasses :class:`UnsupportedError` so callers that catch the broader rule-defined "outside supported subset" exception - (per ``.claude/rules/ir.md``) keep working. Also subclasses - :class:`ValueError` for back-compat with pre-P4-4 callers that caught + keep working. Also subclasses + :class:`ValueError` for back-compat with older callers that caught the bare ``ValueError`` raised by the old in-line rejection paths. """ @@ -79,17 +79,17 @@ class LocalisationError(UnsupportedError, ValueError): def _enrich_element_ir(legacy_ir: ElementIR, problem_ir: ProblemIR) -> ElementIR: - """Populate the P4-1 execution-contract dataclasses on an ``ElementIR``. + """Populate the execution-contract dataclasses on an ``ElementIR``. - Recovery-plan P4-3 (R3.3) makes the IR the *primary* output of lowering: + The IR is the *primary* output of lowering: the enriched ``ElementIR`` is built first and the einsum optimizer view - is derived from it. Pre-P4-3 ``localise()`` returned a bare ElementIR + is derived from it. Previously ``localise()`` returned a bare ElementIR (no contract dataclasses populated); this helper is the centralised derivation that ``localise()`` calls before extracting einsum specs. The four contract blocks are derived from the ``ProblemIR`` / ``ElementIR`` semantics — no Taichi-specific assumptions — so the - enrichment stays backend-agnostic per the Phase 4 constraint. + enrichment stays backend-agnostic. Parameters ---------- @@ -153,7 +153,7 @@ class EinsumSpec: .. note:: - Recovery-plan P4-2 (R3.2) demoted ``EinsumSpec`` from a primary + ``EinsumSpec`` was demoted from a primary semantic carrier to a *derived view* over :class:`ElementIR`. The canonical semantic source for element-level information is now ``ElementIR`` (see :mod:`mechdsl.ir.element_ir`); ``EinsumSpec`` @@ -186,7 +186,7 @@ class LocalisationResult: .. note:: - Recovery-plan P4-2 (R3.2) demoted ``LocalisationResult`` from "the + ``LocalisationResult`` was demoted from "the primary semantic carrier downstream consumers should read" to a derived bundle. The semantic source of truth is the ``element_ir`` field; ``einsum_specs`` is a derived optimizer view recomputed @@ -214,9 +214,9 @@ def from_element_ir( This makes the derived-view relationship explicit: callers build (or modify) ``ElementIR`` independently — possibly enriching it - with the P4-1 contract dataclasses — and then materialize the - optimizer view on demand. Pre-P4-2, ``localise()`` was the only - production path; ``from_element_ir`` is the post-P4-2 reverse + with the execution-contract dataclasses — and then materialize the + optimizer view on demand. Originally ``localise()`` was the only + production path; ``from_element_ir`` is the reverse constructor that lets test fixtures and downstream consumers derive the view without re-running the full ``ProblemIR -> ElementIR`` pass. @@ -225,7 +225,7 @@ def from_element_ir( ---------- element_ir The primary semantic carrier produced upstream. Optionally - carries P4-1 enrichment (geometry, material_eval, force / + carries the execution-contract enrichment (geometry, material_eval, force / tangent descriptors); this method does not require those fields to be populated. problem_ir @@ -251,7 +251,7 @@ def from_element_ir( def _check_stable_path_combo(problem_ir: ProblemIR) -> None: """Reject ``ProblemIR`` configurations the lowering pass cannot lower. - Recovery-plan P4-4 (R3.4) centralises the previously-scattered + This guard centralises the previously-scattered rejection logic into a single guard with a stable contract: 1. Every rejection raises :class:`LocalisationError` (a subclass of @@ -351,7 +351,7 @@ def localise(problem_ir: ProblemIR) -> LocalisationResult: def _contraction_result_to_plan(result: ContractionResult) -> ContractionPlan: """Convert a :class:`ContractionResult` to a :class:`ContractionPlan`. - P9-2 propagates the template-family classification from the optimiser + The template-family classification is propagated from the optimiser result into the serialisable plan. The family is stored as the enum *name* string so the artifact bundle stays JSON-round-trippable without a custom enum encoder. diff --git a/packages/mechdsl-core/src/mechdsl/solver/_seam_runtime.py b/packages/mechdsl-core/src/mechdsl/solver/_seam_runtime.py index 4d6e724..c1618f3 100644 --- a/packages/mechdsl-core/src/mechdsl/solver/_seam_runtime.py +++ b/packages/mechdsl-core/src/mechdsl/solver/_seam_runtime.py @@ -1,19 +1,18 @@ -"""Shared runtime plumbing for the generated-over-seam modules (PlanJune14). +"""Shared runtime plumbing for the generated-over-seam modules. -Both :mod:`mechdsl.solver.seam_solve` (generated PCG, P4-2) and -:mod:`mechdsl.solver.seam_integrate` (generated Newmark-β, P6-1) transpile a LaTeX +Both :mod:`mechdsl.solver.seam_solve` (generated PCG) and +:mod:`mechdsl.solver.seam_integrate` (generated Newmark-β) transpile a LaTeX algorithm box to runtime-mode Taichi source and import the generated driver. They share two concerns, factored here so neither feature module reaches across the other's (private) boundary: * :func:`strip_ti_init` — drop the module-level ``ti.init(...)`` line so the **caller** owns Taichi initialisation / the device. Re-init at import would free - the caller's already-allocated DOF fields (the P2-2 finding); stripping it makes + the caller's already-allocated DOF fields; stripping it makes the generated body device-agnostic. * :func:`import_generated_module` — write generated source to a real ``.py`` file and import it. Taichi needs ``inspect.getsource`` on the driver, so the code - cannot be ``exec``'d. Mirrors ``tests/_e2e_helpers._import_generated_module`` / - the P2-2 pattern. + cannot be ``exec``'d. Mirrors ``tests/_e2e_helpers._import_generated_module``. """ # NOTE: no ``from __future__ import annotations`` — kept consistent with the seam @@ -39,7 +38,7 @@ def strip_ti_init(code: str) -> str: """Drop the module-level ``ti.init(...)`` line(s) from generated *code*. The caller owns Taichi initialisation (device/arch); re-init here would free - already-allocated DOF fields (the P2-2 finding), so the result is + already-allocated DOF fields, so the result is device-agnostic — it runs on whatever arch the caller's ``ti.init`` selected. """ return "\n".join(line for line in code.splitlines() if not TI_INIT_LINE.match(line)) @@ -50,7 +49,7 @@ def import_generated_module(source: str, name: str) -> ModuleType: Taichi requires ``inspect.getsource`` on the driver, so the generated code must live in a real module file (not ``exec``'d). Mirrors - ``tests/_e2e_helpers._import_generated_module`` / the P2-2 pattern. + ``tests/_e2e_helpers._import_generated_module``. """ tmp = Path(tempfile.mkdtemp()) path = tmp / f"{name}.py" diff --git a/packages/mechdsl-core/src/mechdsl/solver/history_fields.py b/packages/mechdsl-core/src/mechdsl/solver/history_fields.py index c01723b..8cd5f2a 100644 --- a/packages/mechdsl-core/src/mechdsl/solver/history_fields.py +++ b/packages/mechdsl-core/src/mechdsl/solver/history_fields.py @@ -96,7 +96,7 @@ def create_lemaitre_history(n_elem: int, n_qp: int = 8) -> HistoryFields: """Create history fields for Lemaitre damage coupled to J2 power-law plasticity. Extends :func:`create_j2_history` with two additional fields specific to - the damage branch (Plan B phase B6, task P6-2): + the damage branch (Plan B phase B6): - ``"alpha"``: equivalent plastic strain ``(n_elem, n_qp)`` - ``"plastic_strain"``: plastic strain tensor in Voigt ``(n_elem, n_qp, 6)`` diff --git a/packages/mechdsl-core/src/mechdsl/solver/import_adapter.py b/packages/mechdsl-core/src/mechdsl/solver/import_adapter.py index 5b66bf8..2345c0f 100644 --- a/packages/mechdsl-core/src/mechdsl/solver/import_adapter.py +++ b/packages/mechdsl-core/src/mechdsl/solver/import_adapter.py @@ -251,9 +251,7 @@ class Algo2CodePCGSolver: The algorithm text lives in the sibling ``algo2code`` package (:func:`algo2code.library.pcg.get_pcg_algorithm_latex`) and is the - verbatim source mirrored from - ``dev/tasks/recovery_plan_latex_contract/json/P6-1.json`` - (``pcg_algorithm_latex.latex``). The Python body of :meth:`solve` + verbatim canonical source. The Python body of :meth:`solve` below is a line-by-line translation of that LaTeX. Why a hand-translated body and not ``algo2code.transpile`` output? @@ -264,18 +262,17 @@ class Algo2CodePCGSolver: ``\\State $pq = p^\\top q$``) tokenises as two single-letter tokens, so the expression parser interprets it as ``p * q`` and rejects it as an assignment target. Adding multi-letter identifier support is - tracked as follow-up work for ``algo2code``. Per the P6-1 directive - "do NOT modify the algorithm text", we (a) keep the canonical LaTeX + tracked as follow-up work for ``algo2code``. Because the algorithm + text must not be modified, we (a) keep the canonical LaTeX untouched in :mod:`algo2code.library.pcg` and (b) ship a verbatim - Python translation here. The two are kept in sync by Phase-6 tests. + Python translation here. The two are kept in sync by tests. Parameters ---------- precond_fn : Callable[[np.ndarray], np.ndarray] | None Optional ``M^{-1} v`` callable. When ``None``, the adapter binds - ``apply_M_inv = lambda v: v.copy()`` at construction time, exactly - as the P6-1 specification demands ("do not encode None-handling - in the LaTeX"). + ``apply_M_inv = lambda v: v.copy()`` at construction time, so that + no None-handling is encoded in the LaTeX. """ def __init__( @@ -410,30 +407,27 @@ def solve( def get_default_solver() -> LinearSolverInterface: """Return the default fallback solver — the imported ``ScipyCGSolver``. - Per recovery plan Phase 6 (R5.2) - ``dev/plans/recovery_plan_latex_contract.md`` line 318: the imported - solver remains the default fallback until the ``algo2code``-generated - PCG path (P6-1) is proven stable through P6-3's integration test and - broader regression. Do not flip this default until that work lands. + The imported solver remains the default fallback until the + ``algo2code``-generated PCG path is proven stable through its integration + test and broader regression. Do not flip this default until that work lands. - PlanJune14 P4-3 status - ---------------------- + Seam path status + ---------------- The all-Taichi on-device **seam** solve path (generated matrix-free PCG - via ``ti_runtime`` ``set_solver`` + the P3-2 generated tangent operator - via ``set_operator``, optionally the P4-1 generated Jacobi via - ``set_preconditioner``) is now *validated to <1e-10 against this fallback* - on the reference SVK patch (see ``tests/plan_tests/test_p4_3.py``) and is - **selectable** through :func:`make_seam_solver` (and the + via ``ti_runtime`` ``set_solver`` + the generated tangent operator + via ``set_operator``, optionally the generated Jacobi via + ``set_preconditioner``) is *validated to <1e-10 against this fallback* + on the reference SVK patch and is **selectable** through :func:`make_seam_solver` (and the ``mechdsl.solver.seam_solve`` entry points). It is intentionally a *different interface* — it drives a ``ti_runtime.LinearSolveContext`` over on-device ``ti.Vector.field`` DOF vectors, not the host-NumPy ``LinearSolverInterface`` matvec callback consumed by ``newton.py`` — so it is exposed as a distinct opt-in path rather than a ``build_solver`` mode. - Per the Option-1 architecture decision, this function's global default is + By design, this function's global default is **intentionally NOT flipped**: ``ScipyCGSolver`` stays the fallback default and the guarded global flip to the generated seam path is deferred until - broader regression coverage (Phase 5 J2 + Phase 7 governance) lands. + broader regression coverage lands. """ return ScipyCGSolver() @@ -449,8 +443,8 @@ def build_solver( ---------- mode ``"fallback"`` (default) — imported ``ScipyCGSolver`` (the stable - path). ``"generated"`` — ``Algo2CodePCGSolver``, opt-in until P6-3 - validates it. + path). ``"generated"`` — ``Algo2CodePCGSolver``, opt-in until its + integration test validates it. precond_fn Optional preconditioner callable forwarded to ``Algo2CodePCGSolver`` when ``mode == "generated"``. Ignored for the fallback mode @@ -492,12 +486,12 @@ def make_seam_solver( ) -> LinearSolveContext: """Construct the all-Taichi on-device **seam** linear solver (opt-in). - This is the selectable entry point for the PlanJune14 Phase-4 *Option 1* + This is the selectable entry point for the all-Taichi seam path: a generated matrix-free PCG (transpiled from - ``dev/algorithms/pcg.tex`` via ``algo2code`` in runtime mode and injected + ``algo2code/library/algorithms/pcg.tex`` via ``algo2code`` in runtime mode and injected through ``ti_runtime`` ``set_solver``) solving against an injected - matrix-free operator (e.g. the P3-2 generated SVK tangent) with an optional - generated preconditioner (e.g. the P4-1 Jacobi). + matrix-free operator (e.g. the generated SVK tangent) with an optional + generated preconditioner (e.g. the generated Jacobi). Unlike :func:`build_solver`, this path does **not** return a host-NumPy :class:`LinearSolverInterface`. It returns a bound @@ -511,11 +505,11 @@ def make_seam_solver( ---------- operator The matrix-free operator ``apply(out, x)`` (out FIRST) injected via - ``LinearSolveContext.set_operator`` — e.g. the P3-2 generated SVK + ``LinearSolveContext.set_operator`` — e.g. the generated SVK tangent ``apply_A``. preconditioner Optional ``ti_runtime.seams.PreconditionerBase`` injected via - ``set_preconditioner`` (e.g. the P4-1 ``GeneratedJacobiPreconditioner`` + ``set_preconditioner`` (e.g. the ``GeneratedJacobiPreconditioner`` or an ``IdentityPreconditioner``). When ``None`` the context keeps its default ``IdentityPreconditioner`` (unpreconditioned). diff --git a/packages/mechdsl-core/src/mechdsl/solver/integration.py b/packages/mechdsl-core/src/mechdsl/solver/integration.py index 3c8eafe..d515da0 100644 --- a/packages/mechdsl-core/src/mechdsl/solver/integration.py +++ b/packages/mechdsl-core/src/mechdsl/solver/integration.py @@ -1,11 +1,9 @@ """Solver integration layer — single mode-selector entry point. -Per recovery plan Phase 6 (R5.2) -``dev/plans/recovery_plan_latex_contract.md`` line 318: :func:`select_linear_solver` is the integration-layer hook that callers (tests, future Newton wiring) use to request the imported fallback or the ``algo2code``-generated PCG without reaching past the interface. The default -mode remains ``"fallback"`` until the generated path is stabilised by P6-3. +mode remains ``"fallback"`` until the generated path is stabilised. """ from __future__ import annotations diff --git a/packages/mechdsl-core/src/mechdsl/solver/jacobi_preconditioner.py b/packages/mechdsl-core/src/mechdsl/solver/jacobi_preconditioner.py index 7508094..6fe4f78 100644 --- a/packages/mechdsl-core/src/mechdsl/solver/jacobi_preconditioner.py +++ b/packages/mechdsl-core/src/mechdsl/solver/jacobi_preconditioner.py @@ -1,10 +1,10 @@ -"""Generated Jacobi (point-diagonal) preconditioner adapter (PlanJune14 P4-1). +"""Generated Jacobi (point-diagonal) preconditioner adapter. This module provides :class:`GeneratedJacobiPreconditioner`, which realises -the Jacobi ``M^{-1} r`` algorithm authored in ``dev/algorithms/jacobi.tex`` +the Jacobi ``M^{-1} r`` algorithm authored in ``algo2code/library/algorithms/jacobi.tex`` and injects it through the ``ti_runtime`` ``set_preconditioner`` seam. -Algorithm (from ``dev/algorithms/jacobi.tex``) +Algorithm (from ``algo2code/library/algorithms/jacobi.tex``) ---------------------------------------------- Given: r — residual vector (numerator, ti.Vector.field) @@ -54,7 +54,7 @@ class GeneratedJacobiPreconditioner(PreconditionerBase): """Point-Jacobi preconditioner: ``z_I = r_I / max(d_I, eps)``. - Realises the algorithm in ``dev/algorithms/jacobi.tex`` through the + Realises the algorithm in ``algo2code/library/algorithms/jacobi.tex`` through the ``ti_runtime`` ``PreconditionerBase`` seam. The hot-path apply calls ``ti_runtime.vector_ops.ediv`` — a ``@ti.kernel`` with no NumPy. diff --git a/packages/mechdsl-core/src/mechdsl/solver/lumped_mass.py b/packages/mechdsl-core/src/mechdsl/solver/lumped_mass.py index d5eeb04..0a3e21f 100644 --- a/packages/mechdsl-core/src/mechdsl/solver/lumped_mass.py +++ b/packages/mechdsl-core/src/mechdsl/solver/lumped_mass.py @@ -1,9 +1,9 @@ -"""Row-sum lumped mass for explicit dynamics (Plan B §B7, task P7-1). +"""Row-sum lumped mass for explicit dynamics (Plan B §B7). Support tier: **experimental**. Explicit dynamics is outside the canonical MVP-stable compile path (which is static / quasi-static only). See -``README.md`` Support tiers and ``dev/plans/recovery_plan_latex_contract.md`` -Phase 1 (R0). Code preserved in tree; not part of the contract surface. +``README.md`` Support tiers. Code preserved in tree; not part of the contract +surface. The consistent element mass matrix for a displacement-based FEM is diff --git a/packages/mechdsl-core/src/mechdsl/solver/mesh_io.py b/packages/mechdsl-core/src/mechdsl/solver/mesh_io.py index e3f6499..4020e80 100644 --- a/packages/mechdsl-core/src/mechdsl/solver/mesh_io.py +++ b/packages/mechdsl-core/src/mechdsl/solver/mesh_io.py @@ -411,7 +411,7 @@ def get_face_nodes(mesh: HexMesh, face_name: str) -> NDArray: def validate_mesh_against_contract(mesh: HexMesh, contract: MeshContract) -> None: """Reject a mesh that does not carry every region the IR contract requires. - Pre-P3-3 the "BC name == mesh boundary tag" assumption was scattered: + Previously the "BC name == mesh boundary tag" assumption was scattered: each consumer (assemblers, codegen runtime, BC compilers in :mod:`mechdsl.codegen.boundary_codegen`) re-derived it from ``mesh.boundary_tags[bc.name]`` lookups that surfaced as ``KeyError`` diff --git a/packages/mechdsl-core/src/mechdsl/solver/seam_integrate.py b/packages/mechdsl-core/src/mechdsl/solver/seam_integrate.py index f1f7e2b..6e2203c 100644 --- a/packages/mechdsl-core/src/mechdsl/solver/seam_integrate.py +++ b/packages/mechdsl-core/src/mechdsl/solver/seam_integrate.py @@ -1,7 +1,7 @@ -"""Generated Newmark-beta time integration over the ``ti_runtime`` seam (PlanJune14 P6-1). +"""Generated Newmark-beta time integration over the ``ti_runtime`` seam. The temporal twin of :mod:`mechdsl.solver.seam_solve`: it transpiles the -canonical Newmark-beta step authored in ``dev/algorithms/newmark.tex`` via +canonical Newmark-beta step authored in ``algo2code/library/algorithms/newmark.tex`` via ``algo2code`` in *runtime mode*, imports the generated ``newmark_step`` body, and binds it through the ``ti_runtime`` ``TimeIntegrationContext.set_integrator`` seam. The resulting integrator advances a dynamic state ``(u, v, a)`` over @@ -11,8 +11,8 @@ temporal analogue of the PCG operator ``A``). Proves the "any algorithm box" claim end-to-end: the *same* Seams & Bodies -pattern that drives the generated linear solver (P4) and the dissipative J2 model -(P5-1) also drives a generated *time integrator* — MechDSL owns the stable +pattern that drives the generated linear solver and the dissipative J2 model +also drives a generated *time integrator* — MechDSL owns the stable ``TimeIntegrationContext`` wrapper; algo2code generates the body. Build mechanism @@ -21,13 +21,13 @@ (:func:`~mechdsl.solver._seam_runtime.import_generated_module` + :func:`~mechdsl.solver._seam_runtime.strip_ti_init`; the caller owns Taichi init / the device), and caches the transpiled-and-imported module once per process so -repeated steps do not leak tempdirs / ``sys.modules`` entries (the P4-2 minor). +repeated steps do not leak tempdirs / ``sys.modules`` entries. Taichi note ----------- The generated module is written to a real ``.py`` file and imported (not -``exec``'d): Taichi needs ``inspect.getsource`` on the driver, mirroring the -``seam_solve`` / P2-2 pattern. +``exec``'d): Taichi needs ``inspect.getsource`` on the driver, mirroring +``seam_solve``. """ # NOTE: no ``from __future__ import annotations`` — downstream callers wire this @@ -42,36 +42,37 @@ from typing import TYPE_CHECKING from algo2code import transpile +from algo2code.library.sources import algorithm_source, algorithm_source_path from mechdsl.solver._seam_runtime import import_generated_module, strip_ti_init if TYPE_CHECKING: from ti_runtime.seams import TimeIntegrationContext -# Repo-relative path to the canonical Newmark algorithm source. This file lives -# at packages/mechdsl-core/src/mechdsl/solver/seam_integrate.py, so the repo root -# is five parents up (same depth as seam_solve.py). -_NEWMARK_TEX_PATH = Path(__file__).resolve().parents[5] / "dev" / "algorithms" / "newmark.tex" +# The canonical Newmark algorithm source ships as package data inside +# ``algo2code`` (``algo2code/library/algorithms/newmark.tex``), so it is +# available from an installed wheel without a source checkout. +_NEWMARK_TEX_NAME = "newmark.tex" def newmark_tex_path() -> Path: """Return the path to the canonical Newmark-beta LaTeX source.""" - return _NEWMARK_TEX_PATH + return algorithm_source_path(_NEWMARK_TEX_NAME) def transpile_seam_newmark() -> str: - """Transpile ``dev/algorithms/newmark.tex`` to runtime-mode Taichi source. + """Transpile ``algo2code/library/algorithms/newmark.tex`` to runtime-mode Taichi source. The module-level ``ti.init`` line is stripped: the **caller** owns Taichi initialisation (device/arch), so the generated body is device-agnostic — it runs on whatever arch the caller's ``ti.init`` selected. (Re-init here would - free already-allocated DOF fields — the P2-2 finding; shared with + free already-allocated DOF fields; shared with :func:`mechdsl.solver.seam_solve.transpile_seam_pcg`.) The emitted ``newmark_step`` body advances ``(u, v, a)`` one step with only ``ti_runtime.vector_ops`` AXPBY/copy calls plus the injected in-place acceleration solve ``solve_a(u_pred, v_pred, a_out)`` — no NumPy. """ - source = _NEWMARK_TEX_PATH.read_text(encoding="utf-8") + source = algorithm_source(_NEWMARK_TEX_NAME) code = transpile(source, backend="taichi", runtime="ti_runtime") return strip_ti_init(code) diff --git a/packages/mechdsl-core/src/mechdsl/solver/seam_solve.py b/packages/mechdsl-core/src/mechdsl/solver/seam_solve.py index d53a192..0c60f73 100644 --- a/packages/mechdsl-core/src/mechdsl/solver/seam_solve.py +++ b/packages/mechdsl-core/src/mechdsl/solver/seam_solve.py @@ -1,22 +1,22 @@ -"""Generated matrix-free PCG over the ``ti_runtime`` seams (PlanJune14 P4-2). +"""Generated matrix-free PCG over the ``ti_runtime`` seams. -This module is the **all-Taichi on-device seam path** (Option 1): it transpiles -the canonical matrix-free PCG authored in ``dev/algorithms/pcg.tex`` via +This module is the **all-Taichi on-device seam path**: it transpiles +the canonical matrix-free PCG authored in ``algo2code/library/algorithms/pcg.tex`` via ``algo2code`` in *runtime mode*, imports the generated ``pcg`` body, and binds it through the ``ti_runtime`` ``LinearSolveContext.set_solver`` seam. The resulting solver solves ``A x = b`` over ``ti.Vector.field`` DOF vectors with **no NumPy in the hot path** — the operator is the injected matrix-free ``apply_A`` (e.g. the -P3-2 generated SVK tangent), and the preconditioner is the injected +generated SVK tangent), and the preconditioner is the injected ``apply_preconditioner``. It deliberately does **not** route through the NumPy ``import_adapter.LinearSolverInterface`` / ``newton.py`` matvec callback: that is the host-NumPy path, whereas this is the on-device Taichi path the spike -(``tests/spike/svk_hex8_taichi.py``) demonstrates and PJ-1 gated. +(``tests/spike/svk_hex8_taichi.py``) demonstrates. Productionization template -------------------------- -The PJ-1 spike's hand-written :func:`pcg` body is the template. P4-2 makes that +The spike's hand-written :func:`pcg` body is the template. This module makes that body *generated*: :func:`build_seam_pcg` returns the transpiled-and-imported ``pcg`` callable; :func:`bind_generated_pcg_solver` wires it into a ``LinearSolveContext`` so ``ctx.solver.solve(b, x, tol, maxiter)`` runs the @@ -26,7 +26,7 @@ ----------- The generated module is written to a real ``.py`` file and imported (not ``exec``'d): Taichi needs ``inspect.getsource`` on the driver, mirroring -``tests/_e2e_helpers._import_generated_module`` and the P2-2 pattern. +``tests/_e2e_helpers._import_generated_module``. """ # NOTE: no ``from __future__ import annotations`` — downstream callers wire this @@ -40,35 +40,36 @@ from typing import TYPE_CHECKING, Any, cast from algo2code import transpile +from algo2code.library.sources import algorithm_source, algorithm_source_path from mechdsl.solver._seam_runtime import import_generated_module, strip_ti_init if TYPE_CHECKING: from ti_runtime.seams import LinearSolveContext -# Repo-relative path to the canonical PCG algorithm source. This file lives at -# packages/mechdsl-core/src/mechdsl/solver/seam_solve.py, so the repo root is -# five parents up. -_PCG_TEX_PATH = Path(__file__).resolve().parents[5] / "dev" / "algorithms" / "pcg.tex" +# The canonical PCG algorithm source ships as package data inside ``algo2code`` +# (``algo2code/library/algorithms/pcg.tex``), so it is available from an +# installed wheel without a source checkout. +_PCG_TEX_NAME = "pcg.tex" def pcg_tex_path() -> Path: """Return the path to the canonical matrix-free PCG LaTeX source.""" - return _PCG_TEX_PATH + return algorithm_source_path(_PCG_TEX_NAME) def transpile_seam_pcg() -> str: - """Transpile ``dev/algorithms/pcg.tex`` to runtime-mode Taichi source. + """Transpile ``algo2code/library/algorithms/pcg.tex`` to runtime-mode Taichi source. The module-level ``ti.init`` line is stripped: the **caller** owns Taichi initialisation, and therefore the device/arch. (Re-init here would free - already-allocated DOF fields — the P2-2 finding.) The generated body is thus + already-allocated DOF fields.) The generated body is thus device-agnostic — it runs on whatever arch the caller's ``ti.init`` selected. The emitted ``pcg`` body is matrix-free: it calls the injected operator ``A(out, x)`` and preconditioner ``M_inv(r, z)`` plus ``ti_runtime`` ``vector_ops`` primitives — no dense ``_matvec``, no NumPy. """ - source = _PCG_TEX_PATH.read_text(encoding="utf-8") + source = algorithm_source(_PCG_TEX_NAME) code = transpile(source, backend="taichi", runtime="ti_runtime") return strip_ti_init(code) diff --git a/packages/mechdsl-core/src/mechdsl/symbolic/anisotropic_energy.py b/packages/mechdsl-core/src/mechdsl/symbolic/anisotropic_energy.py index d73a357..7c1e200 100644 --- a/packages/mechdsl-core/src/mechdsl/symbolic/anisotropic_energy.py +++ b/packages/mechdsl-core/src/mechdsl/symbolic/anisotropic_energy.py @@ -4,7 +4,7 @@ (:func:`mechdsl.symbolic.energy.derive_from_energy`) derives an *isotropic* energy by differentiating a single ``Psi(C)`` w.r.t. the strain components. The Holzapfel-Gasser-Ogden (HGO) family adds **fiber** terms that (a) depend on a -per-element fiber direction ``a`` (P5-1 field data, not authored in LaTeX) and +per-element fiber direction ``a`` (per-element field data, not authored in LaTeX) and (b) are gated to tension only by the Macaulay bracket ```` — a per-fiber, non-smooth branch that does not map onto one differentiable ``Psi``. This module provides the fiber-aware derivation that handles both. diff --git a/packages/mechdsl-core/src/mechdsl/symbolic/bridge.py b/packages/mechdsl-core/src/mechdsl/symbolic/bridge.py index 8b4cd5f..8f6af9c 100644 --- a/packages/mechdsl-core/src/mechdsl/symbolic/bridge.py +++ b/packages/mechdsl-core/src/mechdsl/symbolic/bridge.py @@ -1,6 +1,6 @@ """Adapter from ``nrpylatex`` AST output to mechdsl symbolic types. -post_recovery_plan Phase 4 (P4-2). The bridge sits between the +The bridge sits between the :mod:`mechdsl.frontend.math_parser` output and the existing symbolic machinery in :mod:`mechdsl.symbolic`. It is the **only** module that imports ``nrpylatex`` types beyond the parser wrapper. @@ -24,11 +24,10 @@ budget counter in :mod:`mechdsl.codegen.einsum_optimizer` (≤ 512 unrolled lines per ``@ti.func``). - Full Taichi emission is wired in P3-3. ================ ============ ========================================= -Rank-1, rank-3, and rank > 4 raise :class:`BridgeError` with a -phase-pointer message. +Rank-1, rank-3, and rank > 4 raise :class:`BridgeError` naming the +supported subset. The bridge does **not** mutate or extend any existing symbolic type; it only constructs new dataclass nodes that downstream code can reason @@ -47,8 +46,8 @@ class BridgeError(RuntimeError): """Raised when an nrpylatex namespace entry cannot be mapped onto - a supported mechdsl symbolic node. Always carries the phrase - ``post_recovery_plan Phase 4`` so the bridge surface is traceable. + a supported mechdsl symbolic node. The message always names the + supported subset so the bridge surface is traceable. """ @@ -129,7 +128,7 @@ def convert( if not isinstance(indexed_symbol, nrpylatex.IndexedSymbol): raise BridgeError( f"convert expected nrpylatex.IndexedSymbol or Constant for {name!r}, got " - f"{type(indexed_symbol).__name__} — post_recovery_plan Phase 4." + f"{type(indexed_symbol).__name__}." ) rank = int(getattr(indexed_symbol, "rank", 0) or 0) @@ -173,7 +172,7 @@ def convert( f"convert: rank-{rank} tensor {name!r} is not supported. The bridge " "maps rank-0 (constant or scalar), rank-2 tensors, and rank-4 tangent " "moduli. Ranks 1, 3, and > 4 are not part of the mechdsl supported " - "subset — post_recovery_plan Phase 4." + "subset." ) @@ -199,13 +198,13 @@ def convert_equation(equation: EquationSemantics) -> SymbolicEquation: if not isinstance(equation, EquationSemantics): raise BridgeError( f"convert_equation expected EquationSemantics, got " - f"{type(equation).__name__}. Unsupported bridge node; " - "full grammar lowering is deferred to post_recovery_plan Phase 4." + f"{type(equation).__name__}. Only assignment equations produced by " + "parse_math are supported; full grammar lowering is not implemented." ) if not equation.lhs or not equation.rhs: raise BridgeError( "convert_equation requires assignment equations with both LHS and RHS. " - "Full grammar relation nodes are deferred to post_recovery_plan Phase 4." + "Full grammar relation nodes are not supported." ) return SymbolicEquation( lhs=equation.lhs, diff --git a/packages/mechdsl-core/src/mechdsl/symbolic/convected.py b/packages/mechdsl-core/src/mechdsl/symbolic/convected.py index 5a47cef..d73f005 100644 --- a/packages/mechdsl-core/src/mechdsl/symbolic/convected.py +++ b/packages/mechdsl-core/src/mechdsl/symbolic/convected.py @@ -10,7 +10,7 @@ (:func:`cylindrical_basis`, :func:`spherical_basis`) and basis-vector conveniences (:func:`metric_from_bases`, :func:`reciprocal_bases`, :func:`verify_biorthogonality`, :func:`christoffel_from_bases`) compose those -primitives rather than duplicating the math (absorbed from constkit, P1-2). +primitives rather than duplicating the math (absorbed from constkit). """ from __future__ import annotations diff --git a/packages/mechdsl-core/src/mechdsl/symbolic/energy.py b/packages/mechdsl-core/src/mechdsl/symbolic/energy.py index 0b684b4..8080060 100644 --- a/packages/mechdsl-core/src/mechdsl/symbolic/energy.py +++ b/packages/mechdsl-core/src/mechdsl/symbolic/energy.py @@ -29,8 +29,8 @@ to the identity, collapsing ``nrpylatex``'s symbolic ``gUU = inverse(gDD)`` to delta with no determinant denominators. -Named-invariant authoring (P2-1) --------------------------------- +Named-invariant authoring +------------------------- A strain energy may instead be authored in *named invariants* of the right Cauchy-Green tensor ``C = 2E + I`` — ``\\bar I_1``, ``\\bar I_2``, ``J``, … — so that ``nrpylatex`` never has to parse ``\\det`` / ``\\log`` (a frontend @@ -61,8 +61,8 @@ ``\\mathrm{Jdet}`` ``Jdet`` ``sqrt(det C)`` ``\\mathrm{Ibar1}`` ``Ibar1`` ``I1 * I3^{-1/3}`` ``\\mathrm{Ibar2}`` ``Ibar2`` ``I2 * I3^{-2/3}`` -``\\mathrm{I4f}`` ``I4f`` ``a·C·a`` (fiber a — Phase 5 / P5-1) -``\\mathrm{I5f}`` ``I5f`` ``a·C²·a`` (fiber a — Phase 5 / P5-1) +``\\mathrm{I4f}`` ``I4f`` ``a·C·a`` (fiber direction a) +``\\mathrm{I5f}`` ``I5f`` ``a·C²·a`` (fiber direction a) ============== ========================================== ============================ """ @@ -334,16 +334,16 @@ def _bind_invariants( if name in _FIBER_INVARIANTS: raise EnergyDerivationError( f"invariant {name!r}{loc} ({_FIBER_INVARIANTS[name]}) requires a " - "fiber direction; anisotropic I4/I5 fiber plumbing is planned for " - "Phase 5 (P5-1)." + "fiber direction; use the fiber-aware derivation in " + "mechdsl.symbolic.anisotropic_energy for I4/I5 energies." ) binder = _INVARIANT_REGISTRY.get(name) if binder is None: raise EnergyDerivationError( f"invariant {name!r}{loc} is not a supported named invariant; the " f"authoring contract covers {sorted(_INVARIANT_REGISTRY)} " - "(isotropic) and I4/I5 fiber invariants are planned for " - "Phase 5 (P5-1)." + "(isotropic); I4/I5 fiber invariants need the fiber-aware derivation " + "in mechdsl.symbolic.anisotropic_energy." ) if c is None: c = _c_from_strain(strain) diff --git a/packages/mechdsl-core/src/mechdsl/symbolic/kinematics.py b/packages/mechdsl-core/src/mechdsl/symbolic/kinematics.py index 4011c52..58f116d 100644 --- a/packages/mechdsl-core/src/mechdsl/symbolic/kinematics.py +++ b/packages/mechdsl-core/src/mechdsl/symbolic/kinematics.py @@ -84,7 +84,7 @@ def principal_stretches(self) -> list[sp.Expr]: """Principal stretches lambda_i = sqrt(eig_i(C)), sourced from invariants.py. Always three entries (one per spatial dimension, repeats included) in a - deterministic order — the contract the spectral models (Ogden, P4-2) need. + deterministic order — the contract the spectral models (Ogden) need. """ return _principal_stretches(self.F) diff --git a/packages/mechdsl-core/src/mechdsl/symbolic/models/__init__.py b/packages/mechdsl-core/src/mechdsl/symbolic/models/__init__.py index ccc7182..87ca264 100644 --- a/packages/mechdsl-core/src/mechdsl/symbolic/models/__init__.py +++ b/packages/mechdsl-core/src/mechdsl/symbolic/models/__init__.py @@ -1,7 +1,6 @@ """Constitutive models. -Support tier (per ``README.md`` Support tiers and -``dev/plans/recovery_plan_latex_contract.md`` Phase 1 (R0)): +Support tier (per ``README.md`` Support tiers): - **MVP-stable**: ``svk`` (St. Venant–Kirchhoff), ``j2_power_law`` (J2 plasticity with power-law hardening). Only these two are part of the canonical diff --git a/packages/mechdsl-core/src/mechdsl/symbolic/models/johnson_cook.py b/packages/mechdsl-core/src/mechdsl/symbolic/models/johnson_cook.py index 77a41d3..2e9bb14 100644 --- a/packages/mechdsl-core/src/mechdsl/symbolic/models/johnson_cook.py +++ b/packages/mechdsl-core/src/mechdsl/symbolic/models/johnson_cook.py @@ -575,7 +575,7 @@ def pk2_stress(self, E_strain: NDArray, **state: float) -> NDArray: return radial_return(self._mat, E_strain, alpha, T, dt).stress def material_tangent(self, E_strain: NDArray, **state: float) -> NDArray: - """Return algorithmic tangent (3,3,3,3) — elastic stub for P3-2.""" + """Return the algorithmic tangent (3,3,3,3) of the return map.""" alpha = state.get("alpha", 0.0) T = state.get("T", self._mat.T_ref) dt = state.get("dt", 1.0) diff --git a/packages/mechdsl-core/src/mechdsl/symbolic/models/lemaitre.py b/packages/mechdsl-core/src/mechdsl/symbolic/models/lemaitre.py index e0b81b6..0514772 100644 --- a/packages/mechdsl-core/src/mechdsl/symbolic/models/lemaitre.py +++ b/packages/mechdsl-core/src/mechdsl/symbolic/models/lemaitre.py @@ -18,7 +18,7 @@ Damage is only active once the accumulated plastic strain exceeds the threshold ``eps_D`` (the damage-nucleation strain). ``D`` is clamped below ``1 - 1e-6`` to keep the ``1 - D`` divisor away from singularity — final -element deletion at ``D >= D_crit`` is handled in Task P6-2. +element deletion at ``D >= D_crit`` is handled at the solver level. References ---------- @@ -240,7 +240,7 @@ class LemaitreReturnResult: ``stress`` is the **nominal** PK2 stress, i.e. ``(1 - D_new) * sigma_eff``. ``tangent`` is the undamaged J2 algorithmic tangent — a damage-aware - tangent is a P6-2 concern and is deliberately left unscaled here so that + tangent is out of scope here and is deliberately left unscaled so that at ``D = 0`` this model reproduces J2 power-law byte-for-byte. """ @@ -268,8 +268,8 @@ def lemaitre_return( ) -> LemaitreReturnResult: """Lemaitre effective-stress return mapping coupled to J2 power-law. - Algorithm (strain-equivalence, per task P6-1) - --------------------------------------------- + Algorithm (strain-equivalence) + ------------------------------ 1. Run the J2 power-law radial return on the (total) elastic predictor. Because the elastic predictor ``lambda tr(E) I + 2 mu E`` is built from the undamaged stiffness, the J2 result IS the effective stress field @@ -280,9 +280,9 @@ def lemaitre_return( 4. If the new plastic strain exceeds the damage threshold ``eps_D``, increment damage: ``D_new = D_n + (Y / S_d)^{s_d} * delta_lambda``. Otherwise leave ``D_new = D_n`` (guards ``D`` from accidentally - crossing the threshold mid-step is overkill for P6-1; in practice the + crossing the threshold mid-step is overkill here; in practice the step sizes in the solver stay below the yield-threshold gap). - 5. Clamp ``D_new <= 1 - 1e-6`` (element deletion is P6-2's job). + 5. Clamp ``D_new <= 1 - 1e-6`` (element deletion is the solver's job). 6. Nominal stress: ``sigma_nominal = (1 - D_new) * sigma_eff``. Parameters diff --git a/packages/mechdsl-core/src/mechdsl/symbolic/models/perzyna.py b/packages/mechdsl-core/src/mechdsl/symbolic/models/perzyna.py index d7a9046..6a193b2 100644 --- a/packages/mechdsl-core/src/mechdsl/symbolic/models/perzyna.py +++ b/packages/mechdsl-core/src/mechdsl/symbolic/models/perzyna.py @@ -165,8 +165,9 @@ def radial_return( Algorithmic tangent ------------------- - For P3-1 the tangent is **stubbed as the elastic tangent** when plastic. - P3-3 will replace this stub with the proper consistent tangent. + On the elastic branch the elastic tangent is returned; on the plastic + branch the consistent viscoplastic tangent is assembled from the + linearised return-map residual (see the inline derivation below). Parameters ---------- @@ -186,7 +187,7 @@ def radial_return( Returns ------- ReturnMappingResult - Updated stress, internal variables, and tangent (elastic stub for P3-1). + Updated stress, internal variables, and algorithmic tangent. """ if dt <= 0.0: raise ValueError(f"dt must be > 0, got {dt}") @@ -422,7 +423,7 @@ def pk2_stress(self, E_strain: NDArray, **state: float) -> NDArray: return radial_return(self._mat, E_strain, alpha, dt).stress def material_tangent(self, E_strain: NDArray, **state: float) -> NDArray: - """Return algorithmic tangent (3,3,3,3) — elastic stub for P3-1.""" + """Return the algorithmic tangent (3,3,3,3) of the return map.""" alpha = state.get("alpha", 0.0) dt = state.get("dt", 1.0) return radial_return(self._mat, E_strain, alpha, dt).tangent diff --git a/packages/mechdsl-core/src/mechdsl/symbolic/objective_rates.py b/packages/mechdsl-core/src/mechdsl/symbolic/objective_rates.py index 19f3c63..ba5f289 100644 --- a/packages/mechdsl-core/src/mechdsl/symbolic/objective_rates.py +++ b/packages/mechdsl-core/src/mechdsl/symbolic/objective_rates.py @@ -13,11 +13,11 @@ ============ ================================================================ This module exposes two layers of API: spatial tangent conversions (the -P1-5 contracted surface) and direct rate functions (a documented scope +contracted surface) and direct rate functions (a documented scope addition -- see "Out-of-scope additions" below). ================================================================ -Contracted scope -- Spatial tangent conversions (P1-5 deliverable) +Contracted scope -- Spatial tangent conversions ================================================================ Three functions, each consuming a 4th-order Lagrangian material tangent @@ -40,8 +40,8 @@ the function raises ``NotImplementedError`` with a pointer to the Plan B phase that will add support. -Downstream, P1-4 consumes these at emission time for the UL linearised -tangent term. The full-F Piola push-forward in ``truesdell_tangent`` +Downstream, the UL emitter consumes these at emission time for the UL +linearised tangent term. The full-F Piola push-forward in ``truesdell_tangent`` makes the conversion correct at any quadrature-point F, not just at the identity configuration. @@ -50,15 +50,15 @@ ================================================================ The three ``*_rate`` helpers below (``jaumann_rate``, ``truesdell_rate``, -``green_naghdi_rate``) are NOT part of the contracted P1-5 scope. They -exist because the P1-5 acceptance test "rigid rotation gives zero Cauchy +``green_naghdi_rate``) are NOT part of the contracted scope. They +exist because the acceptance test "rigid rotation gives zero Cauchy rate" is mathematically vacuous at the tangent level: under rigid rotation ``D = 0``, so any tangent contraction ``c : D`` is trivially zero -- it tests nothing. The rate functions are what actually carry the objective-rate invariance, and the rigid-rotation tests in ``tests/test_objective_rates.py`` exercise them directly. -These helpers also make convenient debugging primitives for the P1-7 +These helpers also make convenient debugging primitives for the TL-vs-UL equivalence verification when manual stress-rate inspection is needed. They are tiny, self-contained, and have no callers in the main emission path. @@ -249,7 +249,7 @@ def truesdell_tangent( sigma : Mat33 Current Cauchy stress. Not consumed by the Truesdell push-forward itself but retained in the signature for API symmetry with the - Jaumann and Green-Naghdi variants -- downstream callers (P1-4) can + Jaumann and Green-Naghdi variants -- downstream callers can dispatch on the rate name without knowing which tangent variant actually needs sigma. F : Mat33 | None, optional @@ -314,8 +314,8 @@ def jaumann_tangent( Returns ------- Tensor4 - Spatial Jaumann tangent ``c_Jau_ijkl``. P1-4 consumes this in the - UL linearised tangent kernel for the Jaumann-rate material term. + Spatial Jaumann tangent ``c_Jau_ijkl``. The UL emitter consumes this + in the UL linearised tangent kernel for the Jaumann-rate material term. """ c_tru = truesdell_tangent(C4, sigma, F=F) correction = _prandtl_reuss_correction(sigma) diff --git a/packages/mechdsl-core/src/mechdsl/verify/analytical.py b/packages/mechdsl-core/src/mechdsl/verify/analytical.py index 9696283..2be747b 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/analytical.py +++ b/packages/mechdsl-core/src/mechdsl/verify/analytical.py @@ -1,7 +1,7 @@ """Analytical solution library for verification benchmarks. -Provides four ground-truth reference functions used by Phase 3 and Phase 4 -verification harnesses: +Provides four ground-truth reference functions used by the patch-test and +codegen-equivalence verification harnesses: - patch_test_reference — constant Green-Lagrange strain → nodal displacements - rigid_body_reference — rigid body motion → nodal displacements diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/__init__.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/__init__.py index 14df2a1..c46726b 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/__init__.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/__init__.py @@ -1,44 +1,44 @@ -"""Benchmark harness for MechDSL physical verification (Phase 10). +"""Benchmark harness for MechDSL physical verification. Public API ---------- BenchmarkResult Frozen dataclass: displacements, newton_iters, wallclock_s, extras. CantileverParameters - P10-2: elastic cantilever benchmark setup for TL/UL x material x element matrix. + Elastic cantilever benchmark setup for TL/UL x material x element matrix. run_cantilever_benchmark - P10-2: cantilever tip displacement vs Euler-Bernoulli beam theory. + Cantilever tip displacement vs Euler-Bernoulli beam theory. CookMembraneParameters - P10-3: locked TL + J2 + Hex8 Cook's membrane benchmark setup. + Locked TL + J2 + Hex8 Cook's membrane benchmark setup. run_cook_membrane_benchmark - P10-3: Cook's membrane regression vs committed Hex8 reference. + Cook's membrane regression vs committed Hex8 reference. PlateWithHoleParameters - P10-5: Kirsch plate-with-hole benchmark setup for Hex8/Hex20. + Kirsch plate-with-hole benchmark setup for Hex8/Hex20. run_plate_with_hole_benchmark - P10-5: plate-with-hole stress concentration vs Kirsch K_t = 3. + Plate-with-hole stress concentration vs Kirsch K_t = 3. run_thick_cylinder_benchmark - P10-4: thick-walled internally-pressurised cylinder vs Lame solution. + Thick-walled internally-pressurised cylinder vs Lame solution. run_necking_bar_benchmark Necking bar benchmark (MVP plasticity reference). run_notched_bar_benchmark - P10-8: notched bar with Lemaitre damage; load-displacement history and + Notched bar with Lemaitre damage; load-displacement history and damage-field localisation. build_notched_bar_mesh - P10-8 public mesh builder (Hex8 rectangular bar + semi-circular notch). + Notched-bar public mesh builder (Hex8 rectangular bar + semi-circular notch). NotchedBarMesh - P10-8 mesh bundle (coords, connectivity, boundary sets, notch root). + Notched-bar mesh bundle (coords, connectivity, boundary sets, notch root). run_hgo_uniaxial - P10-9: HGO fiber-reinforced strip uniaxial stretch benchmark. + HGO fiber-reinforced strip uniaxial stretch benchmark. hgo_analytical_uniaxial_stress - P10-9: closed-form HGO uniaxial PK1 stress (reference curve). + Closed-form HGO uniaxial PK1 stress (reference curve). generate_strip_mesh - P10-9: rectangular Hex8 strip mesh generator. + Rectangular Hex8 strip mesh generator. fiber_direction_field - P10-9: per-element uniform fiber-direction field builder. + Per-element uniform fiber-direction field builder. TaylorImpactParameters - P8-1: public Taylor impact benchmark setup with smoke/nightly profiles. + Public Taylor impact benchmark setup with smoke/nightly profiles. run_taylor_impact_benchmark - P8-1: Taylor impact runner — wraps the Phase E7 internal explicit + Taylor impact runner — wraps the internal explicit runtime + JC return mapping + rigid-wall contact and packs Taylor metrics (final length, mushroom radius/diameter, peak PEEQ) into ``BenchmarkResult.extras``. diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_core.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_core.py index 097f60e..c0023f3 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_core.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_core.py @@ -1,18 +1,16 @@ """Shared benchmark harness core - BenchmarkResult dataclass and post-processing helpers. -This module is the common substrate for all P10 benchmark tasks: - - P10-4: thick cylinder (this module) - - P10-6: Cook's membrane - - P10-8: damage localisation - - P10-9: (reserved) +This module is the common substrate for the physical benchmarks (thick +cylinder, Cook's membrane, necking bar, notched bar with damage, HGO strip, +...). Design constraints ------------------ -- No Taichi in gate-C benchmarks. All computations use the NumPy ref solver. +- No Taichi in the CPU benchmarks. All computations use the NumPy ref solver. - BenchmarkResult is frozen; post-processing helpers return new arrays. - element_cauchy_stress evaluates SVK at the element centroid (xi=eta=zeta=0) - and returns 3x3 Cauchy stress sigma = (1/J) F S F^T. P10-6/8/9 will import - this directly. + and returns 3x3 Cauchy stress sigma = (1/J) F S F^T. The other benchmark + modules import this directly. """ from __future__ import annotations diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_elastic_solver.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_elastic_solver.py index 6781d4a..e1f58a0 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_elastic_solver.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_elastic_solver.py @@ -1,4 +1,4 @@ -"""Benchmark-local elastic helpers for Phase 10 cantilever prerequisites. +"""Benchmark-local elastic helpers for the cantilever benchmark. This module is intentionally internal to `mechdsl.verify.benchmarks`. It provides cheap prescribed-displacement elastic assembly checks for the diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_j2_solver.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_j2_solver.py index a909acb..64d70aa 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_j2_solver.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_j2_solver.py @@ -1,4 +1,4 @@ -"""Benchmark-local J2 helpers for Phase 10 prerequisite work. +"""Benchmark-local J2 helpers. The helpers in this module provide a small, numpy-based J2 assembly surface for benchmark enablement. They intentionally do not modify the symbolic material diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_meshes.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_meshes.py index 75df7a3..8aecb56 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_meshes.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_meshes.py @@ -1,4 +1,4 @@ -"""Benchmark-local mesh helpers for Phase 10 prerequisite work. +"""Benchmark-local mesh helpers. This module is intentionally geometry-only: it creates coordinates, connectivity, and boundary node sets, then validates element orientation. It @@ -54,7 +54,7 @@ @dataclass(frozen=True) class BenchmarkMesh: - """Geometry data shared by Phase 10 benchmark prerequisites.""" + """Geometry data shared by the benchmark harnesses.""" element_type: str coordinates: NDArray[np.float64] @@ -115,7 +115,7 @@ def structured_block_mesh( ny: int = 1, nz: int = 1, ) -> BenchmarkMesh: - """Build a structured block mesh for Phase 10 benchmark prerequisites.""" + """Build a structured block mesh for the benchmark harnesses.""" _validate_block_inputs(length=length, width=width, height=height, nx=nx, ny=ny, nz=nz) hex8 = _structured_hex8_block(length=length, width=width, height=height, nx=nx, ny=ny, nz=nz) diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_taylor_runtime.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_taylor_runtime.py index 904493f..124a826 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_taylor_runtime.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/_taylor_runtime.py @@ -1,4 +1,4 @@ -"""Benchmark-local explicit Taylor runtime (Phase 10 prerequisite — Phase E7). +"""Benchmark-local explicit Taylor runtime. This module is intentionally internal to ``mechdsl.verify.benchmarks``. It composes the explicit-dynamics building blocks (reduced-Hex8 internal force, @@ -7,14 +7,14 @@ Scope ----- -This is the runtime *enablement* phase (Plan E7). It deliberately does not -expose the public Taylor benchmark runner — that surface is owned by the -next phase (E8, ``run_taylor_impact_benchmark``). +This is the runtime *enablement* layer. It deliberately does not expose the +public Taylor benchmark runner — that surface is owned by +:mod:`mechdsl.verify.benchmarks.taylor_impact` (``run_taylor_impact_benchmark``). What is implemented here: - :class:`ExplicitTaylorState` — central-difference state container with - room for material-state arrays added by P7-2 (Johnson-Cook). + room for Johnson-Cook material-state arrays. - :class:`RigidWallSpec` — half-space rigid wall (point + outward normal). - :func:`init_taylor_runtime` — initial state allocator with optional initial velocity field. @@ -27,14 +27,15 @@ by the explicit loop to track cumulative hourglass energy. All operations are pure NumPy. Taichi codegen for the explicit loop is out -of scope for this prerequisite phase. +of scope for this module. Material model ~~~~~~~~~~~~~~ -P7-1 ships a simple Saint Venant-Kirchhoff (SVK) constitutive update at the -Hex8 centroid (one-point reduced quadrature). The Johnson-Cook integration -and per-quadrature-point state arrays are P7-2's responsibility — the state -container leaves a generic ``material_state`` slot for that work to attach +:func:`explicit_step` uses a simple Saint Venant-Kirchhoff (SVK) constitutive +update at the Hex8 centroid (one-point reduced quadrature). The Johnson-Cook +integration and per-quadrature-point state arrays live in the ``*_jc`` +variants (:func:`init_taylor_runtime_jc`, :func:`explicit_step_jc`) — the +state container leaves a generic ``material_state`` slot so they can attach arrays without restructuring the runtime. """ @@ -83,8 +84,8 @@ class ExplicitTaylorState: Mutability is intentional: the explicit loop updates arrays in place where possible to avoid per-step allocations. The container is a - *plain dataclass* (not frozen) so that P7-2 can attach Johnson-Cook - state arrays to ``material_state`` without restructuring the runtime. + *plain dataclass* (not frozen) so that the Johnson-Cook variant can attach + its state arrays to ``material_state`` without restructuring the runtime. Attributes ---------- @@ -114,11 +115,11 @@ class ExplicitTaylorState: hourglass_energy Running estimate of the work done by the hourglass force, ``sum f_HG · du`` accumulated across explicit steps. Used by the - AC-1 boundedness test. + hourglass-energy boundedness test. material_state - Free-form dict reserved for material-history arrays attached by - P7-2 (e.g. ``{"eqplas": ndarray, "back_stress": ndarray, ...}``). - Empty by default in P7-1. + Free-form dict reserved for material-history arrays attached by the + Johnson-Cook variant (e.g. ``{"eqplas": ndarray, "back_stress": + ndarray, ...}``). Empty by default for the SVK runtime. """ mesh: BenchmarkMesh @@ -152,7 +153,7 @@ class RigidWallSpec: Coefficient of restitution applied to the inward normal velocity component on impact. ``0.0`` (default) gives a perfectly inelastic wall — the simplest valid penetration-prevention contact law and - the only one P7-1 needs to demonstrate AC-2. + the only one the SVK runtime needs. """ point: NDArray[np.float64] @@ -264,7 +265,8 @@ def _assemble_internal_force( Nodal hourglass force from FB stabilisation. The two components are returned separately so the caller can attribute - work increments to internal vs hourglass energy, which AC-1 needs. + work increments to internal vs hourglass energy, which the hourglass-energy + boundedness check needs. """ f_int = np.zeros_like(u, dtype=np.float64) f_hg = np.zeros_like(u, dtype=np.float64) @@ -387,8 +389,9 @@ def explicit_step( E_hg <- E_hg + |f_HG . du| The energy bookkeeping uses the magnitude of ``f · du`` so the running - quantities are non-negative and directly usable by the AC-1 boundedness - test (a ratio test on dissipation budgets, not a signed work integral). + quantities are non-negative and directly usable by the hourglass-energy + boundedness test (a ratio test on dissipation budgets, not a signed work + integral). Parameters ---------- @@ -507,7 +510,7 @@ def hourglass_energy_increment(du: NDArray[np.float64], f_hg: NDArray[np.float64 """Incremental hourglass work ``|f_HG . du|`` for one explicit step. The absolute value keeps the diagnostic strictly non-negative across - sign-flipping oscillations (the AC-1 test uses it as a dissipation + sign-flipping oscillations (the boundedness test uses it as a dissipation *budget*, not as a signed work integral). Parameters diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/cantilever.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/cantilever.py index 0727b2a..5b81a31 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/cantilever.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/cantilever.py @@ -1,4 +1,4 @@ -"""Public cantilever benchmark harness for Phase 10 prerequisite closure.""" +"""Public cantilever benchmark harness.""" from __future__ import annotations diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/cook_membrane.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/cook_membrane.py index 70c448b..80ee068 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/cook_membrane.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/cook_membrane.py @@ -1,9 +1,9 @@ -"""de Souza Neto et al. (2008) Cook's membrane benchmark harness (P10-3). +"""de Souza Neto et al. (2008) Cook's membrane benchmark harness. This module wraps the existing Hex8 Cook's membrane regression in a reusable benchmark function and widens the original-scope matrix to `{TL, UL} x {Hex8, Tet10}`. Hex8 cells can use the injected handwritten -reference solve; Tet10 cells use the benchmark-local Phase 10 J2 assembly layer +reference solve; Tet10 cells use the benchmark-local J2 assembly layer with a prescribed load-face displacement smoke path. Reference @@ -176,7 +176,7 @@ def _run_hex8_reference( def _run_prescribed_matrix_cell(params: CookMembraneParameters) -> BenchmarkResult: - """Exercise a Cook matrix cell through Phase 10 mesh/J2 helper contracts.""" + """Exercise a Cook matrix cell through the benchmark-local mesh/J2 helper contracts.""" mesh = cook_membrane_mesh( params.element_type, diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/hgo_strip.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/hgo_strip.py index fcf8728..cb54ba9 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/hgo_strip.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/hgo_strip.py @@ -1,4 +1,4 @@ -"""Fiber-reinforced strip benchmark (TL x HGO x Hex8) — Task P10-9. +"""Fiber-reinforced strip benchmark (TL x HGO x Hex8). A unit (or small) rectangular Hex8 strip is loaded uniaxially either along the fiber direction (longitudinal) or perpendicular to it (transverse). The diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/necking_bar.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/necking_bar.py index 5351800..9f88a13 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/necking_bar.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/necking_bar.py @@ -1,21 +1,20 @@ -"""Simo and Hughes (1998) necking bar benchmark harness (P10-6). +"""Simo and Hughes (1998) necking bar benchmark harness. This module wraps the existing TL + J2 + Hex8 necking bar benchmark from `tests/test_benchmarks.py::TestNeckingBar` into a reusable harness function -that returns a `BenchmarkResult` consistent with the P10-4 pathfinder +that returns a `BenchmarkResult` consistent with the thick-cylinder benchmark (`mechdsl.verify.benchmarks._core.BenchmarkResult`). The default TL Hex8 path remains the committed golden regression. The UL Hex8 cell is enabled through the same injected reference solve and tagged as an updated-Lagrangian matrix cell, while lightweight prescribed-displacement smoke -support exercises the Phase 10 J2 assembly helper for finite history checks. +support exercises the benchmark-local J2 assembly helper for finite history checks. Reference --------- The load-displacement curve is compared against a committed golden file (`tests/golden/necking_bar_reference.npz`) generated from the same handwritten -plastic reference kernel. The 2% tolerance is the MVP acceptance criterion -from Simo and Hughes (1998), Chapter 4, and from -dev/design_docs/07-CONVENTIONS.md section 6. +plastic reference kernel. The 2% tolerance follows Simo and Hughes (1998), +Chapter 4, and the project's verification conventions. """ from __future__ import annotations @@ -290,7 +289,7 @@ def matvec(v_flat: NDArray, _u: NDArray = u) -> NDArray: def _run_prescribed_necking_smoke(params: NeckingBarParameters) -> BenchmarkResult: - """Run a cheap necking-bar history update through Phase 10 J2 helpers.""" + """Run a cheap necking-bar history update through the benchmark-local J2 helpers.""" raw_mesh = generate_necking_bar_mesh( params.nx, diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/notched_bar.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/notched_bar.py index 8d227a8..db65b98 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/notched_bar.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/notched_bar.py @@ -1,6 +1,6 @@ -"""Notched bar benchmark (P10-8): TL x Lemaitre damage x Hex8. +"""Notched bar benchmark: TL x Lemaitre damage x Hex8. -Extends the Phase 6 P6-3 notched-bar unit test into a full benchmark: +Extends the Lemaitre notched-bar acceptance unit test into a full benchmark: - Public mesh generator for a rectangular Hex8 bar with a semi-circular notch on the +y face. - Orchestrator that compiles a Lemaitre ProblemIR, applies displacement- @@ -17,26 +17,25 @@ quantitatively WITHOUT the original mesh -- the reference surfaces are always redrawn by hand. -Per the P10-8 task notes, when a literature reference is not directly -usable, we fall back to a **self-consistent** reference: we pick a fixed -mesh and parameter set (documented in ``_REFERENCE_*`` constants below), -execute the solve at a "reference" load-step resolution, record the -load-displacement samples, and then require subsequent runs at the same -mesh/parameter set to reproduce them within 10% (the tolerance called out -by the task JSON and by the Lemaitre & Desmorat textbook for damage -problems). The 10% tolerance detects regressions against our own +When a literature reference is not directly usable, we fall back to a +**self-consistent** reference: we pick a fixed mesh and parameter set +(documented in ``_REFERENCE_*`` constants below), execute the solve at a +"reference" load-step resolution, record the load-displacement samples, and +then require subsequent runs at the same mesh/parameter set to reproduce +them within 10% (the tolerance used by the Lemaitre & Desmorat textbook for +damage problems). The 10% tolerance detects regressions against our own baseline; it does NOT claim agreement with a specific published curve. -This choice is aligned with the task risk note: "Damage problems are -notoriously mesh-dependent. Mitigation: pick a reference that documents -its mesh and use the same density." Our self-consistent reference IS -that documented mesh. +This choice addresses a known risk: damage problems are notoriously +mesh-dependent, and the mitigation is to pick a reference that documents +its mesh and use the same density. Our self-consistent reference IS that +documented mesh. -Acceptance criteria (dev/tasks/PLAN-B/json/P10-8.json) ------------------------------------------------------- +Acceptance criteria +------------------- 1. Load-displacement curve within 10% of reference. 2. Damage localisation at the notch root (same invariant as the - Phase 6 unit test). + Lemaitre acceptance unit test). """ from __future__ import annotations @@ -173,7 +172,7 @@ def build_notched_bar_mesh( flat and the top face is pushed down by ``notch_depth``. This mirrors the ``_build_notched_bar_mesh`` helper in - ``tests/test_lemaitre_acceptance.py`` (the P6-3 unit test). The test- + ``tests/test_lemaitre_acceptance.py`` (the Lemaitre unit test). The test- tree helper is kept in place so existing tests do not need to be modified; both helpers produce identical coordinate arrays for the same inputs. @@ -286,7 +285,7 @@ def _newton_step_lemaitre( The generated kernel mutates ``alpha`` and ``damage_D`` in place; we snapshot them at entry and restore before every residual evaluation so intermediate Newton iterates do not corrupt the next step's starting - point. This mirrors the driver in the P6-3 unit test exactly (see + point. This mirrors the driver in the Lemaitre unit test exactly (see ``tests/test_lemaitre_acceptance.py`` for the same pattern). """ n_nodes = u.shape[0] @@ -391,8 +390,8 @@ def run_notched_bar_benchmark( - Right face (x=L): prescribed x-displacement ramped linearly from 0 to ``total_displacement`` over ``n_steps`` equal increments. All y- and z-components on the right face are left free (homogeneous Neumann) so - Poisson contraction is unconstrained -- matches the P6-3 unit-test - setup. + Poisson contraction is unconstrained -- matches the Lemaitre + unit-test setup. Load history extraction ----------------------- @@ -419,9 +418,9 @@ def run_notched_bar_benchmark( Scratch directory for the emitted Taichi module source. newton_tol, newton_max_iter : float, int Per-step Newton tolerance and iteration cap. Must accommodate the - P6-2 Gate B forward-warning that tangent_matvec uses the undamaged - J2 tangent (sub-linear convergence under active damage); defaults - match the P6-3 unit-test settings. + known limitation that tangent_matvec uses the undamaged J2 tangent + (sub-linear convergence under active damage); defaults match the + Lemaitre unit-test settings. module_name : str, optional Override for the generated module's importable name. Defaults to a uuid-based name so repeated runs in the same tmp_path do not diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/plate_with_hole.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/plate_with_hole.py index 6c983bc..5ec94bf 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/plate_with_hole.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/plate_with_hole.py @@ -1,4 +1,4 @@ -"""Kirsch plate-with-hole benchmark harness (P10-5). +"""Kirsch plate-with-hole benchmark harness. Implements a quarter-plate mesh with a circular hole centred at the origin, loaded by uniform tension on the far ``x = W`` boundary. The benchmark uses @@ -9,7 +9,7 @@ Scope ----- This harness is benchmark-local and bypasses the still-Hex8-only lowering / -codegen surface. It builds directly on the Phase 5 element basis/quadrature +codegen surface. It builds directly on the multi-element basis/quadrature support via :class:`mechdsl.ir.element_factory.ElementFactory`, which is enough for the benchmark's reference-style solve in the ``verify`` layer. """ diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/taylor_impact.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/taylor_impact.py index dc1d5c4..6402bf2 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/taylor_impact.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/taylor_impact.py @@ -1,8 +1,8 @@ -"""Public Taylor impact benchmark API (Phase 10 prerequisite — Phase E8 / P8-1). +"""Public Taylor impact benchmark API. -This module wraps the *internal* Phase E7 Taylor explicit runtime +This module wraps the *internal* Taylor explicit runtime (:mod:`mechdsl.verify.benchmarks._taylor_runtime`) into a public, deterministic -benchmark runner that mirrors the shape of every other Phase 10 runner +benchmark runner that mirrors the shape of every other benchmark runner (see :mod:`mechdsl.verify.benchmarks.cantilever`): - A frozen ``TaylorImpactParameters`` dataclass with ``smoke()`` and @@ -10,13 +10,13 @@ - ``run_taylor_impact_benchmark(params=...) -> BenchmarkResult`` that returns the shared :class:`BenchmarkResult` schema unchanged. Taylor-specific metrics (final length, mushroom radius, mushroom diameter, peak PEEQ) are - packed into ``BenchmarkResult.extras`` so AC-3 — *no shared benchmark - result schema changes are required* — is satisfied. + packed into ``BenchmarkResult.extras`` so *no shared benchmark result schema + changes are required*. The runtime building blocks (``init_taylor_runtime_jc``, ``explicit_step_jc``, ``apply_rigid_wall_contact``, ``final_length``, ``mushroom_radius``, -``extract_equivalent_plastic_strain``) are consumed verbatim — Phase E7's -public signatures must remain bit-for-bit unchanged. +``extract_equivalent_plastic_strain``) are consumed verbatim — the internal +runtime's public signatures must remain bit-for-bit unchanged. Bar geometry / impact convention -------------------------------- @@ -26,8 +26,8 @@ half-space is ``z >= wall_z`` (the bar). The impact velocity must be **strictly negative** (the bar moves toward the wall in the -z direction). -dt sizing (carry-forward Phase 6 wave-physics gate) ---------------------------------------------------- +dt sizing (explicit wave-physics stability gate) +----------------------------------------------- The explicit central-difference integrator requires ``dt <= dt_crit ≈ h_min / c`` where ``c = sqrt(E/rho)`` is the longitudinal wave speed and ``h_min`` is the smallest element edge. For the steel-like smoke @@ -208,7 +208,7 @@ def run_taylor_impact_benchmark( return mapping + reduced-Hex8 internal force + Flanagan-Belytschko hourglass control + frictionless rigid-wall contact) into a deterministic public runner. Taylor-specific metrics are packed into ``extras``; the - shared :class:`BenchmarkResult` schema is reused unchanged (AC-3). + shared :class:`BenchmarkResult` schema is reused unchanged. Parameters ---------- diff --git a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/thick_cylinder.py b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/thick_cylinder.py index 77eb75e..cf043ea 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/benchmarks/thick_cylinder.py +++ b/packages/mechdsl-core/src/mechdsl/verify/benchmarks/thick_cylinder.py @@ -1,4 +1,4 @@ -"""Thick-walled internally-pressurised cylinder benchmark (P10-4). +"""Thick-walled internally-pressurised cylinder benchmark. Implements: - Quarter-cylinder structured Hex8 mesh generator (theta in [0, pi/2]) diff --git a/packages/mechdsl-core/src/mechdsl/verify/mms_matrix.py b/packages/mechdsl-core/src/mechdsl/verify/mms_matrix.py index 8fd69f9..e39edfc 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/mms_matrix.py +++ b/packages/mechdsl-core/src/mechdsl/verify/mms_matrix.py @@ -1,4 +1,4 @@ -"""Matrix-capable MMS convergence helpers for Phase 10. +"""Matrix-capable MMS convergence helpers for the benchmark suite. This module intentionally does not modify ``run_mms_convergence(lam, mu, ...)``. It adds a separate result surface for the broader element/material MMS matrix. @@ -49,7 +49,7 @@ @dataclass(frozen=True) class MMSMatrixCase: - """One element/material entry in the Phase 10 MMS matrix.""" + """One element/material entry in the MMS matrix.""" element_type: MMSElementType material: MMSMaterial @@ -94,7 +94,7 @@ def by_id(self) -> dict[str, MMSConvergenceEntry]: def default_mms_matrix_cases() -> tuple[MMSMatrixCase, ...]: - """Return the planned Phase 10 MMS matrix entries.""" + """Return the default MMS matrix entries.""" return _DEFAULT_MATRIX @@ -107,7 +107,7 @@ def run_mms_convergence_matrix( A: float = 1.0e-3, tol: float = 0.1, ) -> MMSMatrixResult: - """Run the additive Phase 10 MMS matrix API. + """Run the additive MMS matrix API. The existing Hex8 SVK full-solve path remains available through the legacy API and is used for cases with ``policy='svk_full_solve'``. The default diff --git a/packages/mechdsl-core/src/mechdsl/verify/patch_test.py b/packages/mechdsl-core/src/mechdsl/verify/patch_test.py index 826763d..881d359 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/patch_test.py +++ b/packages/mechdsl-core/src/mechdsl/verify/patch_test.py @@ -5,11 +5,11 @@ - run_rigid_body_test() — rigid body motion test (internal force norm < 1e-12) - generate_irregular_mesh() — perturbed Hex8 mesh for patch test on irregular geometry -Design decision (Phase 3): +Design decision: These tests use the handwritten reference solver (ref_hex8_elastic.py), NOT the generated solver. The patch test and rigid body test validate fundamental mechanics correctness of the numerical method. Generated-vs-reference - comparison is Phase 4's job. + comparison is the job of the separate codegen equivalence tests. Patch test formulation: For a Hex8 element with trilinear shape functions, a constant Green-Lagrange diff --git a/packages/mechdsl-core/src/mechdsl/verify/perf/__init__.py b/packages/mechdsl-core/src/mechdsl/verify/perf/__init__.py index d42bf4b..9ee142b 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/perf/__init__.py +++ b/packages/mechdsl-core/src/mechdsl/verify/perf/__init__.py @@ -1,7 +1,7 @@ -"""Phase 9 P9-1: benchmark registry, smoke runners, and baseline comparison. +"""Benchmark registry, smoke runners, and baseline comparison. -The registry catalogues the nine public Phase 10 benchmark runners (P10-1 .. -P10-9) with smoke-sized parameter factories and metric extractors, and emits +The registry catalogues the nine public benchmark runners with smoke-sized +parameter factories and metric extractors, and emits a stable JSON-shaped comparison report against the committed local baseline. Public API @@ -11,7 +11,7 @@ metrics extracted from the runner result, and the per-benchmark default tolerance. :class:`BenchmarkRegistry` - Frozen catalogue of the nine Phase 10 specs. + Frozen catalogue of the nine benchmark specs. :func:`run_smoke_registry` Invoke every spec under its smoke profile and pack metrics + wallclock. :class:`MetricDelta`, :class:`BenchmarkComparison`, :class:`ComparisonReport` diff --git a/packages/mechdsl-core/src/mechdsl/verify/perf/baseline.py b/packages/mechdsl-core/src/mechdsl/verify/perf/baseline.py index 5737017..5715bf4 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/perf/baseline.py +++ b/packages/mechdsl-core/src/mechdsl/verify/perf/baseline.py @@ -1,9 +1,9 @@ """Smoke baseline loader and per-benchmark delta reporter. The committed baseline at ``packages/mechdsl-core/tests/golden/perf/ -baseline_smoke.json`` records, for every Phase 10 task in the registry, +baseline_smoke.json`` records, for every benchmark task in the registry, the smoke-run wallclock plus a small set of dimensionless metrics. Nightly -CI (P9-2) loads the baseline and calls :func:`compare_to_baseline` to flag +CI loads the baseline and calls :func:`compare_to_baseline` to flag per-benchmark, per-metric drift relative to the configured tolerance. The :class:`ComparisonReport` shape is stable and JSON round-trippable so diff --git a/packages/mechdsl-core/src/mechdsl/verify/perf/registry.py b/packages/mechdsl-core/src/mechdsl/verify/perf/registry.py index f3324b7..5455f9c 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/perf/registry.py +++ b/packages/mechdsl-core/src/mechdsl/verify/perf/registry.py @@ -1,9 +1,9 @@ """Benchmark registry, smoke profiles, and one-shot smoke run harness. -The registry catalogues the nine public Phase 10 benchmark runners and their -local smoke profiles. Every spec exposes: +The registry catalogues the nine public benchmark runners and their local +smoke profiles. Every spec exposes: -* ``task_id`` — canonical Phase 10 task id (``P10-1`` .. ``P10-9``). +* ``task_id`` — canonical benchmark task id (``P10-1`` .. ``P10-9``). * ``runner`` — the public runner callable from :mod:`mechdsl.verify.benchmarks` or :mod:`mechdsl.verify.mms_matrix`. * ``smoke_factory`` — zero-arg callable returning the kwargs dict consumed by @@ -16,16 +16,17 @@ ``dict[str, float]`` containing every key in ``metrics_keys`` (the ``wallclock_s`` slot is filled in by :func:`run_smoke_registry`). * ``tolerance_pct`` — default per-benchmark tolerance for baseline diffs. -* ``notes`` — free-form context (e.g. carry-forward constraints). +* ``notes`` — free-form context (e.g. known runner constraints). -Carry-forward constraints honoured here ---------------------------------------- -* P10-7 (Taylor): the spec uses :meth:`TaylorImpactParameters.smoke` only. +Runner constraints honoured here +-------------------------------- +* Taylor impact: the spec uses :meth:`TaylorImpactParameters.smoke` only. ``nightly()`` overruns the JC ``radial_return`` 50-iteration budget on the shipped 6x6x20 mesh, and PEEQ on long horizons (~16.6 at n_steps=200) is - unphysical (P8-2 carry-forward). The smoke profile (2x2x8 Hex8, 10 steps, - dt=2e-8 s, horizon=0.2 us) keeps PEEQ in the < ~3 range. -* P10-1 (MMS): the registry restricts the matrix to the cheap Hex8 SVK case + unphysical (a known limitation of the explicit JC runtime). The smoke + profile (2x2x8 Hex8, 10 steps, dt=2e-8 s, horizon=0.2 us) keeps PEEQ in + the < ~3 range. +* MMS: the registry restricts the matrix to the cheap Hex8 SVK case with three coarse mesh levels (4, 6, 8) so the local smoke run finishes in O(seconds) on a developer laptop. Convergence rates from the resulting ``MMSMatrixResult`` are recorded in the baseline alongside wallclock. @@ -75,12 +76,12 @@ @dataclass(frozen=True) class BenchmarkSpec: - """A single registry entry over one Phase 10 public benchmark runner. + """A single registry entry over one public benchmark runner. Attributes ---------- task_id - Canonical Phase 10 task id (``"P10-1"`` .. ``"P10-9"``). + Canonical benchmark task id (``"P10-1"`` .. ``"P10-9"``). runner The public runner callable from :mod:`mechdsl.verify.benchmarks` or :mod:`mechdsl.verify.mms_matrix`. Returns either a @@ -104,7 +105,7 @@ class BenchmarkSpec: Default per-benchmark tolerance for baseline diffs. Overridable via ``compare_to_baseline(per_benchmark_overrides=...)``. notes - Free-form context (e.g. ``"smoke profile only - P8-2 carry-forward"``). + Free-form context (e.g. ``"smoke profile only - nightly() overruns budget"``). """ task_id: str @@ -145,7 +146,7 @@ def __post_init__(self) -> None: class BenchmarkRegistry: - """Frozen registry of the nine Phase 10 public benchmark runners.""" + """Frozen registry of the nine public benchmark runners.""" def __init__(self, specs: tuple[BenchmarkSpec, ...]) -> None: seen: set[str] = set() @@ -158,7 +159,7 @@ def __init__(self, specs: tuple[BenchmarkSpec, ...]) -> None: @classmethod def default(cls) -> BenchmarkRegistry: - """Return the canonical registry covering P10-1 .. P10-9.""" + """Return the canonical registry covering all nine benchmark specs.""" return cls(_default_specs()) @@ -214,7 +215,7 @@ def run_smoke_registry( Defaults to :meth:`BenchmarkRegistry.default`. tmp_path Optional per-call ``tmp_path`` injected into runners that need it - (currently only P10-8 Notched bar). Ignored by other specs. + (currently only the notched-bar spec). Ignored by other specs. Returns ------- @@ -304,7 +305,7 @@ def _mms_matrix_adapter(result: MMSMatrixResult) -> dict[str, float]: # --------------------------------------------------------------------------- -# P10-1: cheap Hex8 SVK MMS matrix at three coarse levels. +# MMS smoke: cheap Hex8 SVK matrix at three coarse levels. _P10_1_SMOKE_CASES = (MMSMatrixCase("hex8", "svk", expected_l2_rate=2.0, expected_h1_rate=1.0),) _P10_1_SMOKE_LEVELS = (3, 4, 5) _P10_1_METRICS = ( @@ -411,7 +412,7 @@ def _p10_8_factory() -> dict[str, Any]: def _p10_9_factory() -> dict[str, Any]: # HGO uniaxial smoke: 1-element mesh, single mild stretch, fiber along x. - # Lazy import of the NumPy reference solver mirrors the P10-4 strategy. + # Lazy import of the NumPy reference solver mirrors the thick-cylinder factory. from tests.ref.ref_hex8_hgo import assemble_internal_force, solve_hgo return { @@ -518,8 +519,8 @@ def _default_specs() -> tuple[BenchmarkSpec, ...]: ), tolerance_pct=10.0, notes=( - "Taylor impact — smoke profile only (P8-2 carry-forward: nightly() " - "overruns JC return-map iter budget; long-horizon PEEQ is unphysical)." + "Taylor impact — smoke profile only (nightly() overruns the JC " + "return-map iteration budget; long-horizon PEEQ is unphysical)." ), ), BenchmarkSpec( diff --git a/packages/mechdsl-core/src/mechdsl/verify/perf/run_compare.py b/packages/mechdsl-core/src/mechdsl/verify/perf/run_compare.py index 3c64b8e..4b8784d 100644 --- a/packages/mechdsl-core/src/mechdsl/verify/perf/run_compare.py +++ b/packages/mechdsl-core/src/mechdsl/verify/perf/run_compare.py @@ -1,8 +1,8 @@ """Smoke-registry runner + baseline comparison CLI. This module is the canonical entry point invoked by nightly CI to enforce the -"no >10% regression vs the committed baseline" gate (P9-2 acceptance -criterion). It is also useful as a local dev verification step: +"no >10% regression vs the committed baseline" gate. It is also useful as a +local dev verification step: :: @@ -50,7 +50,7 @@ def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="python -m mechdsl.verify.perf.run_compare", description=( - "Run the Phase 10 smoke benchmark registry and compare against " + "Run the smoke benchmark registry and compare against " "the committed baseline. Exit non-zero on >tolerance regression." ), ) diff --git a/packages/mechdsl-core/tests/golden/boundary_neumann.ti.txt b/packages/mechdsl-core/tests/golden/boundary_neumann.ti.txt index 7d17b91..0cf26bd 100644 --- a/packages/mechdsl-core/tests/golden/boundary_neumann.ti.txt +++ b/packages/mechdsl-core/tests/golden/boundary_neumann.ti.txt @@ -1,6 +1,6 @@ # ====================================================================== # Neumann f_ext kernel for BC 'load' (surface 'z1') -# Emitted by post_recovery_plan P1-4 (Taichi printer Neumann emitter) +# Emitted by the Taichi printer Neumann emitter (literal-baked form) # ====================================================================== @ti.kernel diff --git a/packages/mechdsl-core/tests/golden/generated_elastic.py.golden b/packages/mechdsl-core/tests/golden/generated_elastic.py.golden index 4186857..89e9484 100644 --- a/packages/mechdsl-core/tests/golden/generated_elastic.py.golden +++ b/packages/mechdsl-core/tests/golden/generated_elastic.py.golden @@ -136,7 +136,7 @@ du = ti.Vector.field(3, dtype=ti.f64) # displacement increment Kv = ti.Vector.field(3, dtype=ti.f64) # tangent matvec result elem_nodes = ti.field(dtype=ti.i32) # connectivity -# Quadrature tables as ti.fields (PlanJune14 WI-1): the matrix-free +# Quadrature tables as ti.fields: the matrix-free # tangent matvec kernels run the q-loop at RUNTIME (the ÷8 JIT-budget # lever), so they cannot index the Python-list constants GRAD_AT_QUAD / # QUAD_WEIGHTS by a runtime q. These device-resident copies are filled @@ -328,10 +328,10 @@ def tangent_matvec(v_flat: np.ndarray, lam: float, mu: float) -> np.ndarray: return Kv.ravel() # ====================================================================== -# Generated matrix-free SVK tangent @ti.kernel (PlanJune14 P3-2) +# Generated matrix-free SVK tangent @ti.kernel # # Applies K(u)·v fully matrix-free (D-A: element tangents never stored), -# recomputed per matvec. The tangent contraction rides the P3-1 +# recomputed per matvec. The tangent contraction rides the # opt_einsum ContractionPlan ('qaI,qiIjJ,qbJ,bj->qai', path [(2, 3), (1, 2), (0, 1)], # tier 2). Kinematics use the Tier-1 ti_runtime @ti.func # helpers; the kernel targets the ti_runtime apply_A(out, x) seam. @@ -354,7 +354,7 @@ def svk_tangent_matvec_apply( Taichi's implicit atomic add. ``u`` (current Newton iterate) and the mesh fields ``x_ref`` / ``elem_nodes`` are read live. - The tangent contraction is the P3-1 opt_einsum ContractionPlan for + The tangent contraction is the opt_einsum ContractionPlan for ``qaI,qiIjJ,qbJ,bj->qai`` (operands dNdX, A, dNdX, v), realised as the recorded path [(2, 3), (1, 2), (0, 1)] (tier 2, within the 512-line @ti.func JIT budget). @@ -372,7 +372,7 @@ def svk_tangent_matvec_apply( for d in ti.static(range(DIM)): X_elem[a, d] = x_ref[nid][d] - # Quadrature loop (RUNTIME — PlanJune14 WI-1 JIT-budget lever). + # Quadrature loop (RUNTIME — the JIT-budget lever). # Runtime q divides the per-QP unroll by N_QP (8); physics indices # i,I,j,J,a,d stay ti.static. The body can no longer index the # Python-list quad constants by a runtime q, so it reads the diff --git a/packages/mechdsl-core/tests/golden/generated_plastic.py.golden b/packages/mechdsl-core/tests/golden/generated_plastic.py.golden index 9423c97..c0476e8 100644 --- a/packages/mechdsl-core/tests/golden/generated_plastic.py.golden +++ b/packages/mechdsl-core/tests/golden/generated_plastic.py.golden @@ -136,7 +136,7 @@ du = ti.Vector.field(3, dtype=ti.f64) # displacement increment Kv = ti.Vector.field(3, dtype=ti.f64) # tangent matvec result elem_nodes = ti.field(dtype=ti.i32) # connectivity -# Quadrature tables as ti.fields (PlanJune14 WI-1): the matrix-free +# Quadrature tables as ti.fields: the matrix-free # tangent matvec kernels run the q-loop at RUNTIME (the ÷8 JIT-budget # lever), so they cannot index the Python-list constants GRAD_AT_QUAD / # QUAD_WEIGHTS by a runtime q. These device-resident copies are filled @@ -419,14 +419,14 @@ def tangent_matvec(v_flat: np.ndarray, lam: float, mu: float, return Kv.ravel() # ====================================================================== -# Generated matrix-free J2 algorithmic-tangent @ti.kernel (PlanJune14 P5-1) +# Generated matrix-free J2 algorithmic-tangent @ti.kernel # # Applies the J2 ALGORITHMIC CONSISTENT TANGENT K(u)·v fully matrix-free # (D-A: element tangents never stored), recomputed per matvec. The tangent # is the linearisation of the radial-return map (NOT d2Psi/dE2 — J2 is # dissipative; see .claude/rules/symbolic.md). History alpha[e, q] is read # on-device (read-only); it advances only in compute_internal_force. The -# contraction rides the P3-1 opt_einsum ContractionPlan ('qaI,qiIjJ,qbJ,bj->qai', +# contraction rides the opt_einsum ContractionPlan ('qaI,qiIjJ,qbJ,bj->qai', # path [(2, 3), (1, 2), (0, 1)], tier 2) — the same path as the # SVK kernel; kinematics use the Tier-1 ti_runtime @ti.func helpers. # ====================================================================== @@ -453,7 +453,7 @@ def j2_tangent_matvec_apply( live; ``alpha`` is NOT written (history advances in compute_internal_force). The tangent is the J2 algorithmic consistent tangent (linearisation of - the radial-return map). The contraction is the P3-1 opt_einsum + the radial-return map). The contraction is the opt_einsum ContractionPlan for ``qaI,qiIjJ,qbJ,bj->qai`` (operands dNdX, A, dNdX, v), realised as the recorded path [(2, 3), (1, 2), (0, 1)] (tier 2). """ @@ -470,7 +470,7 @@ def j2_tangent_matvec_apply( for d in ti.static(range(DIM)): X_elem[a, d] = x_ref[nid][d] - # Quadrature loop (RUNTIME — PlanJune14 WI-1 JIT-budget lever). + # Quadrature loop (RUNTIME — the JIT-budget lever). # Runtime q divides the per-QP unroll by N_QP (8); physics indices # i,I,j,J,K,M,a,d stay ti.static. The body can no longer index the # Python-list quad constants by a runtime q, so it reads the diff --git a/packages/mechdsl-core/tests/test_compile_latex_docstring.py b/packages/mechdsl-core/tests/test_compile_latex_docstring.py index 75193fc..e00e38d 100644 --- a/packages/mechdsl-core/tests/test_compile_latex_docstring.py +++ b/packages/mechdsl-core/tests/test_compile_latex_docstring.py @@ -29,7 +29,7 @@ def test_compile_latex_docstring_mentions_boundary_condition() -> None: assert doc is not None, "compile_latex has no docstring" assert "BoundaryCondition" in doc, ( "compile_latex docstring lost its reference to BoundaryCondition — " - "the BC handoff contract paragraph must remain (post_recovery_plan P3-1)." + "the BC hand-off contract paragraph must remain." ) @@ -44,7 +44,7 @@ def test_compile_latex_docstring_describes_f_ext_caller_provisioning() -> None: lowered = doc.lower() assert any(token in lowered for token in CALLER_PROVISIONING_TOKENS), ( "compile_latex docstring must describe f_ext as caller-provisioned " - f"(any of {CALLER_PROVISIONING_TOKENS} expected post_recovery_plan P3-1)." + f"(any of {CALLER_PROVISIONING_TOKENS} expected)." ) @@ -56,7 +56,4 @@ def test_compile_latex_docstring_names_dirichlet_and_neumann() -> None: doc = inspect.getdoc(compile_latex) assert doc is not None, "compile_latex has no docstring" for kind in ("Dirichlet", "Neumann"): - assert kind in doc, ( - f"compile_latex docstring must name {kind} as a supported BC kind " - "(post_recovery_plan P3-1)." - ) + assert kind in doc, f"compile_latex docstring must name {kind} as a supported BC kind." diff --git a/packages/mechdsl-core/tests/test_j2_radial_return_parity.py b/packages/mechdsl-core/tests/test_j2_radial_return_parity.py index fade3b9..ed7acbb 100644 --- a/packages/mechdsl-core/tests/test_j2_radial_return_parity.py +++ b/packages/mechdsl-core/tests/test_j2_radial_return_parity.py @@ -10,7 +10,7 @@ The plan (line 267-268) requires "tolerance derived from imported-path baseline, not absolute zero". For Phase 5 the algo2code path's runtime body is a verbatim translation of the imported implementation (the -algo2code parser bugs documented in ``dev/algorithms/radial_return_j2.tex`` +algo2code parser bugs documented in ``algo2code/library/algorithms/radial_return_j2.tex`` defer direct emission), so the parity is bit-equal *today*. The tolerance constant ``BASELINE_TOL`` is set conservatively to ``1e-12`` — a slack against the imported Newton tolerance — so a diff --git a/packages/mechdsl-core/tests/test_mfem_printer.py b/packages/mechdsl-core/tests/test_mfem_printer.py index ef2f481..7fa8305 100644 --- a/packages/mechdsl-core/tests/test_mfem_printer.py +++ b/packages/mechdsl-core/tests/test_mfem_printer.py @@ -317,9 +317,9 @@ def test_tangent_references_full_voigt_structure(self, svk_source: str) -> None: @pytest.mark.unit def test_emitted_source_carries_deferral_markers(self, svk_source: str) -> None: - """Simplified sections must cite the plan phase that extends them.""" - assert "P8-3" in svk_source - assert "Plan B" in svk_source + """Simplified sections must say they are deferred to a later pass.""" + assert "Deferred" in svk_source + assert "compile" in svk_source # Must not accidentally emit the banned draft markers (spelled out so # the check itself does not trip). for marker in ("TO" + "DO", "FIX" + "ME", "XX" + "X"): diff --git a/packages/mechdsl-core/tests/test_nrpylatex_round_trip.py b/packages/mechdsl-core/tests/test_nrpylatex_round_trip.py index e054fb0..0c6ffc1 100644 --- a/packages/mechdsl-core/tests/test_nrpylatex_round_trip.py +++ b/packages/mechdsl-core/tests/test_nrpylatex_round_trip.py @@ -137,7 +137,7 @@ def test_unsupported_full_grammar_node_fails_with_phase_pointer() -> None: with pytest.raises(MathParseError) as excinfo: parse_math(unsupported) assert "full-grammar phase" in str(excinfo.value) - assert "post_recovery_plan Phase 4" in str(excinfo.value) + assert "not yet accepted" in str(excinfo.value) @pytest.mark.docs diff --git a/packages/mechdsl-core/tests/test_wheel_import.py b/packages/mechdsl-core/tests/test_wheel_import.py new file mode 100644 index 0000000..b52fbe7 --- /dev/null +++ b/packages/mechdsl-core/tests/test_wheel_import.py @@ -0,0 +1,160 @@ +"""Wheel-install regression: every non-Taichi module must import from a clean venv. + +Builds the ``mechdsl-core`` and ``algo2code`` wheels from this checkout, installs +them (and only them — no ``[verify]`` extra, no source tree on ``sys.path``) into a +fresh virtual environment, and imports every module of both packages there. + +This guards the packaging contract that runtime resources ship inside the wheel: +an earlier release located the canonical algorithm ``.tex`` sources by walking up +from ``__file__`` to a repository-only directory, so ``mechdsl.lib.plasticity*`` +and ``algo2code.library.radial_return_j2*`` raised ``FileNotFoundError`` at +import time for anyone installing from PyPI. + +Modules whose only failure is a missing ``taichi`` / ``ti_runtime`` (the +``[verify]`` extra) are expected skips; any other exception is a failure. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +import textwrap +import zipfile +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[3] +PACKAGES = ("mechdsl-core", "algo2code") +TOP_LEVEL_MODULES = ("mechdsl", "algo2code") +BUNDLED_TEX = ( + "jacobi.tex", + "newmark.tex", + "pcg.tex", + "radial_return_j2.tex", + "radial_return_j2_kinematic.tex", + "radial_return_j2_mixed.tex", +) + +# Runs inside the clean venv. Enumerates modules from the installed package +# directory (rather than ``pkgutil.walk_packages``) so a broken ``__init__`` in +# one subpackage cannot hide its siblings. +_IMPORT_WALK = textwrap.dedent( + """ + import importlib + import sys + from pathlib import Path + + EXPECTED_MISSING = {"taichi", "ti_runtime"} + + def enumerate_modules(pkg_name): + pkg = importlib.import_module(pkg_name) + root = Path(pkg.__file__).parent + for py in sorted(root.rglob("*.py")): + parts = list(py.relative_to(root).with_suffix("").parts) + if parts[-1] == "__init__": + parts = parts[:-1] + yield ".".join([pkg_name, *parts]) + + failures = [] + skipped = [] + imported = [] + for top in sys.argv[1:]: + for mod in enumerate_modules(top): + try: + importlib.import_module(mod) + imported.append(mod) + except ModuleNotFoundError as exc: + root = (exc.name or "").split(".")[0] + if root in EXPECTED_MISSING: + skipped.append(f"{mod} (missing {exc.name})") + else: + failures.append(f"{mod}: {type(exc).__name__}: {exc}") + except Exception as exc: # noqa: BLE001 - report everything + failures.append(f"{mod}: {type(exc).__name__}: {exc}") + + # The J2 return-map sources must be readable from the installed wheel. + from algo2code.library import ALGORITHM_SOURCES, algorithm_source + from algo2code.library.radial_return_j2 import RADIAL_RETURN_J2_LATEX + from algo2code.library.radial_return_j2_kinematic import RADIAL_RETURN_J2_KINEMATIC_LATEX + from algo2code.library.radial_return_j2_mixed import RADIAL_RETURN_J2_MIXED_LATEX + + for name, text in { + "RADIAL_RETURN_J2_LATEX": RADIAL_RETURN_J2_LATEX, + "RADIAL_RETURN_J2_KINEMATIC_LATEX": RADIAL_RETURN_J2_KINEMATIC_LATEX, + "RADIAL_RETURN_J2_MIXED_LATEX": RADIAL_RETURN_J2_MIXED_LATEX, + **{n: algorithm_source(n) for n in ALGORITHM_SOURCES}, + }.items(): + if not text.strip(): + failures.append(f"{name} is empty") + + print(f"imported={len(imported)} skipped={len(skipped)} failed={len(failures)}") + for line in skipped: + print("SKIP", line) + for line in failures: + print("FAIL", line) + sys.exit(1 if failures else 0) + """ +) + + +def _run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + return subprocess.run(cmd, check=True, capture_output=True, text=True, **kwargs) # type: ignore[call-overload] + + +@pytest.fixture(scope="module") +def built_wheels(tmp_path_factory: pytest.TempPathFactory) -> dict[str, Path]: + uv = shutil.which("uv") + if uv is None: + pytest.skip("uv not on PATH; the wheel-install regression needs it to build and install") + dist = tmp_path_factory.mktemp("dist") + wheels: dict[str, Path] = {} + for pkg in PACKAGES: + _run([uv, "build", "--wheel", "--out-dir", str(dist)], cwd=REPO_ROOT / "packages" / pkg) + matches = sorted(dist.glob(f"{pkg.replace('-', '_')}-*.whl")) + assert len(matches) == 1, f"expected exactly one {pkg} wheel, found {matches}" + wheels[pkg] = matches[0] + return wheels + + +@pytest.mark.integration +def test_algorithm_sources_are_wheel_package_data(built_wheels: dict[str, Path]) -> None: + """The canonical ``.tex`` sources must be inside the algo2code wheel.""" + with zipfile.ZipFile(built_wheels["algo2code"]) as whl: + names = set(whl.namelist()) + missing = [t for t in BUNDLED_TEX if f"algo2code/library/algorithms/{t}" not in names] + assert not missing, f"algo2code wheel is missing bundled algorithm sources: {missing}" + + +@pytest.mark.integration +def test_every_non_taichi_module_imports_from_clean_venv( + built_wheels: dict[str, Path], tmp_path: Path +) -> None: + uv = shutil.which("uv") + assert uv is not None # the fixture skipped otherwise + venv = tmp_path / "venv" + py_version = f"{sys.version_info.major}.{sys.version_info.minor}" + _run([uv, "venv", "--python", py_version, str(venv)]) + python = venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + assert python.exists(), f"venv interpreter not found at {python}" + + # Only the two wheels and their declared dependencies — no [verify] extra. + _run([uv, "pip", "install", "--python", str(python), *map(str, built_wheels.values())]) + + script = tmp_path / "import_walk.py" + script.write_text(_IMPORT_WALK, encoding="utf-8") + # ``-I`` (isolated) plus a cwd outside the repo: nothing from the source + # checkout can leak onto sys.path, so only the installed wheels are exercised. + proc = subprocess.run( + [str(python), "-I", str(script), *TOP_LEVEL_MODULES], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + assert proc.returncode == 0, ( + f"import walk over installed wheels failed:\n{proc.stdout}\n{proc.stderr}" + ) + assert "FAIL" not in proc.stdout, proc.stdout diff --git a/packages/ti-runtime/src/ti_runtime/seams.py b/packages/ti-runtime/src/ti_runtime/seams.py index e95934c..75a563b 100644 --- a/packages/ti-runtime/src/ti_runtime/seams.py +++ b/packages/ti-runtime/src/ti_runtime/seams.py @@ -160,7 +160,7 @@ class Integrator: The body advances ``(u, v, a)`` one step in place, calling the injected acceleration solve plus the :mod:`ti_runtime.vector_ops` primitives — the - shape ``dev/algorithms/newmark.tex`` transpiles to. + shape ``algo2code/library/algorithms/newmark.tex`` transpiles to. """ def __init__(self) -> None: