diff --git a/_bmad-output/implementation-artifacts/0-2-phantom-gradient-regularization-standard.md b/_bmad-output/implementation-artifacts/0-2-phantom-gradient-regularization-standard.md new file mode 100644 index 0000000..90c5bee --- /dev/null +++ b/_bmad-output/implementation-artifacts/0-2-phantom-gradient-regularization-standard.md @@ -0,0 +1,285 @@ +--- +story_key: 0-2-phantom-gradient-regularization-standard +epic: 0 +story: 2 +source: _bmad-output/planning-artifacts/epics-solver-hp.md +baseline_commit: f88cd7f7d83c8c25bf8d4262742fb9736c3bf7c7 +depends_on: 0-1-jacobian-killing-pattern-audit-fd-test-harness +--- + +# Story 0.2: Phantom-Gradient Regularization Standard + +Status: review + + + + +## Story + +As a component developer, +I want a standardized C¹ regularization toolkit (smooth max/min/clamp with documented ε) generalizing the existing `flow_regularization.rs` pattern, +so that fixes are uniform and derivative-safe instead of ad-hoc. + +## Acceptance Criteria + +1. **Core C¹ toolkit is complete, tested, and derivative-safe.** + - **Given** the `entropyk-core` crate + - **When** the toolkit lands (or is completed if already present) + - **Then** `smooth_max` / `smooth_min` / `smooth_clamp` (C¹ or smoother, parameterized sharpness/width) are available alongside existing `smooth_abs`, with **derivative-continuity unit tests** + - **And** ε / width selection guidance is documented (physical scale of the blended region) + +2. **`flow_regularization.rs` relationship is resolved without physics churn.** + - **Given** the existing `crates/components/src/heat_exchanger/flow_regularization.rs` + - **When** reviewed + - **Then** it is either unified into the toolkit **or** documented as the **reference implementation** for mass-flow activity / zero-flow blending + - **And** no existing component behavior changes in this story (**toolkit + docs only**) + +## Tasks / Subtasks + +- [x] **Task 1: Inventory what already exists — do not reinvent** (AC: #1, #2) + - [x] 1.1 Confirm primitives in `crates/core/src/smoothing.rs`: `smooth_max`, `smooth_min`, `smooth_abs` (+ derivative), `smooth_clamp`, plus `smoothstep` / `cubic_blend` family. + - [x] 1.2 Confirm HX-specific adapters in `flow_regularization.rs` already call `smooth_abs` — keep them in `entropyk-components` (domain-specific, not core). + - [x] 1.3 Gap check (expected at story creation): + | Gap | Expected action | + |---|---| + | No public `smooth_clamp_derivative` | **Add** analytic ∂/∂x (+ FD unit test) — required by Stories 0.3/0.4 Jacobian paths | + | `smooth_max` / `smooth_min` derivatives only inline in tests | **Add** public `smooth_max_da` / `smooth_min_da` (or named `*_derivative` w.r.t. first arg) + FD tests | + | No dedicated C¹ join tests for `smooth_clamp` | **Add** FD slope continuity at the four junctions (`lo`, `lo+width`, `hi-width`, `hi`) | + | No general ε/width guidance doc | **Add** rustdoc module section + short markdown standard (see Task 3) | + | `flow_regularization` not formally declared “reference” | **Document** — do **not** move into core | + +- [x] **Task 2: Complete the toolkit APIs in `entropyk-core`** (AC: #1) + - [x] 2.1 Add analytic derivatives needed by consumers: + - `smooth_clamp_derivative(x, lo, hi, width) -> f64` + - `smooth_max` / `smooth_min` first-arg (or both-arg) derivatives — match existing formulas already verified in unit tests + - [x] 2.2 Keep APIs pure `f64`, `#[inline]`, zero-alloc, no new crate deps. + - [x] 2.3 Guard invalid parameters without panic: `width <= 0`, `hi < lo`, `k <= 0`, `eps <= 0` — document degenerate behavior (hard clamp / hard max / hard abs) and cover with tests. Prefer non-panicking fallbacks consistent with existing `normalize` zero-width handling. + - [x] 2.4 Re-export path unchanged: `entropyk_core::smoothing::*` via existing `pub mod smoothing` in `crates/core/src/lib.rs`. No facade/`entropyk` re-export required unless already present. + +- [x] **Task 3: Document the Phantom-Gradient Regularization Standard** (AC: #1, #2) + - [x] 3.1 Add a focused doc (preferred path): `docs/components/phantom-gradient-regularization.md` **OR** expand `docs/components/flow-regularization.md` with a top-level “Standard” section that links to `smoothing.rs`. Prefer a **dedicated** short doc if the flow doc stays HX-specific. + - [x] 3.2 Document **ε / width selection guidance** with physical meaning: + + | Primitive | Parameter | Meaning | Rule of thumb | + |---|---|---|---| + | `smooth_abs(x, eps)` | `eps` | value at `x=0`; blend half-width | ~1% of characteristic \|x\| scale, or smaller if kink is far from operating region | + | `smooth_max` / `smooth_min` | `k` | overshoot at equality = `k/2` | `k` ≪ \|a−b\| in the region you care about; never so large it shifts equilibrium | + | `smooth_clamp` | `width` | transition length at each bound | `width ≤ (hi−lo)/2`; pick so Newton steps near bound still see ∂/∂x ∈ (0,1) over a physically small band (e.g. opening width `1e-3`…`1e-2` for [0,1]) | + | `flow_activity` | `m_eps` | mass-flow activity scale | keep `DEFAULT_M_EPS_KG_S = 1e-4` as HX reference; do not change the constant in this story | + + - [x] 3.3 Explicit decision (write it into the doc): + - **Canonical general toolkit:** `entropyk_core::smoothing` + - **Reference specialized pattern:** `heat_exchanger::flow_regularization` (activity / duty / transport blend) — **not** duplicated into core + - **Banned in residual/Jacobian paths on Newton unknowns:** bare `.clamp(` / hard `.max(0)` / early `Ok(0.0)` that zeros informative derivatives (fixes deferred to 0.3/0.4) + - [x] 3.4 Cross-link from `docs/audits/jacobian-killing-pattern-audit.md` deferred table: mark Story 0.2 standard as landed when done (update the “deferred” row only — do not re-audit). + - [x] 3.5 English technical docs; keep code comments English. + +- [x] **Task 4: Derivative-continuity unit tests** (AC: #1) + - [x] 4.1 Extend `crates/core/src/smoothing.rs` `#[cfg(test)]`: + - FD match for `smooth_clamp_derivative` across interior, both ramps, and far exterior. + - Slope continuity of `smooth_clamp` across junctions (sample FD slope just inside/outside joins; assert small jump). + - FD match for new `smooth_max` / `smooth_min` derivative helpers. + - [x] 4.2 Do **not** require `check_jacobian_health` for this story — that harness is component-level (Story 0.1). Core unit FD helpers already in `smoothing.rs` tests are the right layer. + - [x] 4.3 Keep using relative/absolute tolerances consistent with existing tests (`~1e-5` FD match for analytic derivatives). + +- [x] **Task 5: Hard scope boundary — toolkit only** (AC: #2) + - [x] 5.1 **DO NOT** modify production residual/Jacobian physics in: + - `valve_flow.rs` + - `isenthalpic_expansion_valve.rs` + - `heat_exchanger/two_phase_dp.rs` + - `heat_exchanger/condenser.rs` + - any other component clamp site + - [x] 5.2 **DO NOT** change `DEFAULT_M_EPS_KG_S` / `DEFAULT_M_SCALE_KG_S` or HX residual formulas. + - [x] 5.3 **DO NOT** change `n_equations()` anywhere. + - [x] 5.4 Allowed touch list is essentially: `smoothing.rs` (+ tests), docs under `docs/components/` and a small audit cross-link, optional rustdoc polish on `flow_regularization.rs` module docs pointing to the standard. + +- [x] **Task 6: Validation** (AC: #1, #2) + - [x] 6.1 `cargo test -p entropyk-core` green (includes new smoothing tests / doctests). + - [x] 6.2 `cargo test -p entropyk-components` green — proves no accidental physics regressions from doc-only / non-touch policy. + - [x] 6.3 Diff review: production component residual files should show **zero** physics diffs attributable to this story. + +## Dev Notes + +### Context & Positioning + +- **Epic 0** — Physical Regularization & Jacobian Health. Prerequisite for primary KPI **NFR11** (≥95% first-try convergence). Robustness before speed. +- **FR19** — informative non-zero C¹ Jacobians across the full physical domain via standardized phantom-gradient regularization. +- **FR20** — FD harness (already delivered in Story 0.1); CI gate is Story **0.5**. +- **This story owns the standard + toolkit completeness.** Stories **0.3** (valve/EXV) and **0.4** (HX quality/actuators/MSH) **consume** `smooth_clamp` / `smooth_max` / `smooth_abs` to fix P0 killers. +- Source of truth for solver-HP scope: `_bmad-output/planning-artifacts/epics-solver-hp.md` (supersedes legacy PRD/architecture for this epic). + +### CRITICAL: Toolkit already largely exists — complete, don't recreate + +```text +CANONICAL LOCATION (reuse): + crates/core/src/smoothing.rs + smooth_max(a, b, k) + smooth_min(a, b, k) + smooth_abs(x, eps) + smooth_abs_derivative + smooth_clamp(x, lo, hi, width) // value exists; derivative API missing + smoothstep / smootherstep / cubic_blend / quintic_blend (+ derivatives) + +HX REFERENCE PATTERN (keep in components — do not move to core): + crates/components/src/heat_exchanger/flow_regularization.rs + flow_activity / effective_duty / blend_transport_residual + uses entropyk_core::smoothing::smooth_abs + +VERIFICATION HARNESS (Story 0.1 — do not reimplement): + crates/components/src/jacobian_fd.rs + docs/audits/jacobian-killing-pattern-audit.md +``` + +**Anti-patterns to prevent:** +- Creating a second `smoothing` / `regularization` module under `components/` +- Copy-pasting `tanh` / `LogSumExp` soft-max when `smooth_max` already exists +- “Unifying” by moving `flow_activity` into core (wrong layer — HX residual units / m_scale) +- Replacing component clamps in this story (that is 0.3/0.4) +- Calling `JacobianMatrix::numerical` (forward FD Newton fallback) for anything here + +### Architecture Compliance + +- **Exact analytic Jacobians** (`project-context.md` / AGENTS.md): production assembly stays analytic; FD only in tests. +- **C¹ residual policy:** any clamp/branch on a Newton unknown must use toolkit primitives so ∂R/∂x stays informative and continuous. +- **DoF invariant:** regularization never changes `n_equations()`. +- **Zero panic** in library code; public APIs need rustdoc + examples. +- Hot path: pure functions, no allocation, no dynamic dispatch. +- Layering: `entropyk-core` must not depend on `entropyk-components`. + +### Technical Requirements + +- **Language:** Rust edition 2021, crate `entropyk-core` (+ docs only elsewhere). +- **Deps:** no new crates. Existing workspace/`approx` patterns for tests as already used in core. +- **`smooth_clamp` construction (already implemented — preserve semantics):** + ```rust + // Lower ramp then upper ramp via smoothstep — output always in [lo, hi], C¹ joins. + let lower = lo + (x - lo) * smoothstep(lo, lo + width, x); + hi + (lower - hi) * smoothstep(hi, hi - width, lower) + ``` + Derivative must follow the chain rule through both ramps; unit-test against central FD. +- **`smooth_max` analytic ∂/∂a (already proven in tests):** + ```text + 0.5 * (1 + (a-b) / sqrt((a-b)² + k²)) + ``` +- Prefer exporting named derivative functions over forcing callers to re-derive formulas (Story 0.3 will clamp EXV opening in both residual and Jacobian). + +### Current toolkit gaps (verified 2026-07-18) + +| Item | Status | +|---|---| +| `smooth_max` / `smooth_min` / `smooth_abs` / `smooth_clamp` values | Present | +| `smooth_abs_derivative` | Present | +| Public `smooth_clamp_derivative` | **Missing — add** | +| Public `smooth_max`/`smooth_min` derivatives | **Missing — add** (formulas exist in tests) | +| FD C¹ junction tests for `smooth_clamp` | **Missing — add** (current test only saturates/bounds) | +| General ε/width selection doc | **Missing — add** (`flow-regularization.md` covers HX m_eps only) | +| Formal “reference vs unify” decision for `flow_regularization` | **Missing — document as reference** | + +### Downstream contract (write APIs for these consumers) + +| Future story | Will need | +|---|---| +| 0.3 Valve / EXV | `smooth_clamp` + derivative for opening ∈ [0,1]; smooth ΔP blend (may use `smooth_max`/`smooth_abs`) | +| 0.4 HX | `smooth_clamp` for quality / fan / flood; reuse `flow_regularization` patterns | +| 0.5 CI gate | Assumes toolkit stable; no API churn after this story | + +Recommended default widths for docs/examples (not applied to components here): +- Opening / quality ∈ [0,1]: `width = 1e-3` … `1e-2` +- Fan actuator ∈ [0, 1.5]: `width ≈ 1e-2` +- Flood level ∈ [0, 0.98]: `width ≈ 1e-2` (keep `width ≤ (hi−lo)/2`) + +### File Structure Requirements + +| File | Action | +|---|---| +| `crates/core/src/smoothing.rs` | **UPDATE** — add missing derivative APIs + C¹/FD tests; expand module rustdoc with ε guidance | +| `crates/core/src/lib.rs` | **UPDATE only if** export changes needed (likely unchanged) | +| `docs/components/phantom-gradient-regularization.md` | **CREATE** (preferred) — standard + ε table + reference decision | +| `docs/components/flow-regularization.md` | **UPDATE** — link to standard; declare HX module as specialized reference | +| `crates/components/src/heat_exchanger/flow_regularization.rs` | **UPDATE docs only** (optional module-doc pointer) — **no formula changes** | +| `docs/audits/jacobian-killing-pattern-audit.md` | **UPDATE** — mark 0.2 standard row as done / link to new doc | +| `valve_flow.rs` / EXV / `two_phase_dp.rs` / `condenser.rs` | **DO NOT CHANGE** physics | + +### Testing Requirements + +- Unit tests live next to primitives in `smoothing.rs` (existing pattern with local `fd` helper). +- Must prove: + 1. Analytic derivatives match central FD. + 2. `smooth_clamp` slope is continuous at bound joins (C¹). + 3. Degenerate parameters do not panic. +- Run: + ```bash + cargo test -p entropyk-core + cargo test -p entropyk-components + ``` +- Do not flip Story 0.1 detection tests to “must be clean” — killers remain until 0.3/0.4. + +### Previous Story Intelligence + +From **`0-1-jacobian-killing-pattern-audit-fd-test-harness`** (status: review): + +- Shared harness: `jacobian_fd::check_jacobian_health` — central FD, flags analytic≈0 while FD informative. +- Audit catalogue confirms P0 killers; maps EXV opening clamps → **Story 0.2 `smooth_clamp` as the intended replacement API**, applied later in **0.3**. +- Explicitly noted: `smooth_*` already in core — Story 0.2 is **unify/document/ε-guidance**, not greenfield. +- Scope discipline that worked: observe/fix tooling without changing production physics. **Reuse that discipline here.** +- Epic typo note still valid for 0.3: only `valve_mass_flow_dp_up` exists (no `dp_down`). +- Numbering collision: `0-1-fix-failing-tests` is a **different**, already-done DoF story — ignore for this work. + +### Git Intelligence + +- Recent commits are FMI/docs-heavy (`f88cd7f` baseline). Smoothing + `flow_regularization` landed earlier (`3358b74` lineage) — brownfield completion, not invention. +- No commit yet closes the ε-standard doc or `smooth_clamp_derivative` gap. + +### Project Context Reference + +- `project-context.md` / `AGENTS.md`: exact Jacobians; Component trait; English technical docs; zero panic. +- Epic/FRs: `_bmad-output/planning-artifacts/epics-solver-hp.md` — Epic 0, Story 0.2, FR19, FR20, NFR11. +- Supporting: `docs/audits/jacobian-killing-pattern-audit.md`, `docs/components/flow-regularization.md`, `docs/adr/ADR-0001-multi-circuit-heat-exchanger-architecture.md` (smooth_abs / no hard zero-flow branch). + +### References + +- [Source: `_bmad-output/planning-artifacts/epics-solver-hp.md` — Epic 0, Story 0.2, FR19, NFR11] +- [Source: `crates/core/src/smoothing.rs` — existing C¹/C∞ toolkit] +- [Source: `crates/components/src/heat_exchanger/flow_regularization.rs` — HX reference pattern] +- [Source: `docs/components/flow-regularization.md` — m_eps defaults, DoF rule] +- [Source: `docs/audits/jacobian-killing-pattern-audit.md` — deferred 0.2 work + P0 map] +- [Source: `_bmad-output/implementation-artifacts/0-1-jacobian-killing-pattern-audit-fd-test-harness.md` — prior story learnings] +- [Source: `crates/components/src/jacobian_fd.rs` — verification harness (do not reimplement)] + +## Dev Agent Record + +### Agent Model Used + +Cursor Grok 4.5 (bmad-dev-story) + +### Debug Log References + +- `cargo test -p entropyk-core` — 164 lib + 41 doctests passed (incl. new smoothing derivative / C¹ / degenerate tests) +- `cargo test -p entropyk-components` — lib 910 passed; jacobian_health_sweep 8 passed; doctests 70 passed +- Story-owned production touch limited to `flow_regularization.rs` module docs (+4 lines); no clamp→smooth physics changes + +### Completion Notes List + +- Added public `smooth_max_derivative`, `smooth_min_derivative`, `smooth_clamp_derivative` with chain-rule / analytic formulas; FD-matched in unit tests. +- Documented ε/width guidance in module rustdoc and `docs/components/phantom-gradient-regularization.md`. +- Declared `flow_regularization` as HX **reference** pattern (not moved to core); linked from flow-regularization.md + audit deferred table (0.2 landed). +- Degenerate parameters (`k/eps/width <= 0`, inverted bounds) fall back to hard max/min/abs/clamp without panic. +- Scope held: valve/EXV/HX clamp physics untouched for Stories 0.3/0.4. + +### File List + +- `crates/core/src/smoothing.rs` (UPDATE) +- `crates/components/src/heat_exchanger/flow_regularization.rs` (UPDATE — module docs only) +- `docs/components/phantom-gradient-regularization.md` (NEW) +- `docs/components/flow-regularization.md` (UPDATE) +- `docs/audits/jacobian-killing-pattern-audit.md` (UPDATE — deferred table) +- `_bmad-output/implementation-artifacts/sprint-status.yaml` (UPDATE — story status) +- `_bmad-output/implementation-artifacts/0-2-phantom-gradient-regularization-standard.md` (UPDATE — this file) + +### Change Log + +- 2026-07-18: Completed phantom-gradient regularization standard — derivative APIs, C¹ tests, ε docs, flow_regularization reference decision; status → review + +## Story Completion Status + +- Status: **review** +- Note: All tasks complete; ACs satisfied; ready for `code-review` diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml index de28780..b72be89 100644 --- a/_bmad-output/implementation-artifacts/sprint-status.yaml +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -1,308 +1,106 @@ -# Sprint Status - Entropyk -# Last Updated: 2026-04-28 (story 20-1 implementation) -# Project: Entropyk -# Project Key: NOKEY -# Tracking System: file-system -# Story Location: _bmad-output/implementation-artifacts -# -# ORGANIZED BY BUSINESS PILLARS (not historical epics) -# Priority order: P4 (Solver) → P5 (Interfaces) → P6 (Applications) -# See: _bmad-output/planning-artifacts/pillars.md for full pillar definitions -# See: _bmad-output/planning-artifacts/epic-restructuring.md for migration rationale -# +# Sprint Status +# Generated by bmad-create-story workflow + # STATUS DEFINITIONS: # ================== +# Epic Status: +# - backlog: Epic not yet started +# - in-progress: Epic actively being worked on +# - done: All stories in epic completed +# # Story Status: # - backlog: Story only exists in epic file -# - ready-for-dev: Story file created in stories folder +# - ready-for-dev: Story file created, ready for development # - in-progress: Developer actively working on implementation -# - review: Ready for code review (via Dev's code-review workflow) +# - review: Implementation complete, ready for review # - done: Story completed # +# Retrospective Status: +# - optional: Can be completed but not required +# - done: Retrospective has been completed + # WORKFLOW NOTES: # =============== -# - File is read top-to-bottom by BMAD: first matching status = next action -# - epic-X keys are kept for BMAD workflow compatibility (do not remove) -# - Stories are grouped by pillar; within each pillar: active → backlog → done -# - SM creates next story after previous one is 'done' to incorporate learnings -# - Dev moves story to 'review', then runs code-review (fresh context recommended) +# - Mark epic as 'in-progress' when starting work on its first story +# - SM typically creates next story ONLY after previous one is 'done' to incorporate learnings +# - Dev moves story to 'review', then Dev runs code-review (fresh context, ideally different LLM) -generated: 2026-02-22 +generated: 2026-07-18T12:51:31Z +last_updated: 2026-07-18T16:30:00Z project: Entropyk -project_key: NOKEY +project_key: ENTROPYK tracking_system: file-system -story_location: _bmad-output/implementation-artifacts +story_location: "{project-root}/_bmad-output/implementation-artifacts" development_status: - # ═══════════════════════════════════════════════════════════════ - # EPIC STATUS (required by BMAD — epic-X keys must stay here) - # ═══════════════════════════════════════════════════════════════ - epic-1: done - epic-2: done - epic-3: done - epic-4: done - epic-5: in-progress - epic-6: in-progress - epic-7: in-progress - epic-8: done - epic-9: done - epic-10: in-progress - epic-11: in-progress - epic-12: in-progress - epic-13: in-progress - epic-14: in-progress - epic-15: in-progress - epic-16: backlog - epic-17: backlog - epic-18: backlog - epic-19: in-progress - epic-20: in-progress - - # ═══════════════════════════════════════════════════════════════ - # P1: FOUNDATION / Fondations — 14/14 DONE - # Types, traits, ports, graph topology, state machine - # ═══════════════════════════════════════════════════════════════ - 1-1-component-trait-definition: done - 1-2-physical-types-newtype-pattern: done - 1-3-port-and-connection-system: done - 1-7-component-state-machine: done - 3-1-system-graph-structure: done - 3-2-port-compatibility-validation: done - 3-3-multi-circuit-machine-definition: done - 3-4-thermal-coupling-between-circuits: done - 3-5-zero-flow-branch-handling: done - 3-6-hierarchical-subsystems-macrocomponents: done - 9-1-circuitid-type-unification: done - 9-2-fluidid-type-unification: done - 9-7-solver-refactoring-split-files: done - 9-8-systemstate-dedicated-struct: done - - # ═══════════════════════════════════════════════════════════════ - # P2: REAL COMPONENTS / Composants — 30/39 (77%) - # All thermodynamic components, vendor data parsers, boundary conditions - # ═══════════════════════════════════════════════════════════════ - 1-4-compressor-component-ahri-540: done - 1-5-generic-heat-exchanger-framework: done - 1-6-expansion-valve-component: done - 1-8-auxiliary-transport-components: done - 1-9-air-coils-evaporatorcoil-condensercoil-post-mvp: done - 1-10-pipe-helpers-for-water-and-refrigerant: done - 1-11-flow-junctions-flowsplitter-flowmerger: done - 1-12-boundary-conditions-flowsource-flowsink: done - 8-1-fluid-backend-component-integration: done - 10-1-new-physical-types: done - 10-2-refrigerant-source-sink: done - 10-3-brine-source-sink: done - 10-4-air-source-sink: done - 10-5-migration-deprecation: done - 10-6-python-bindings-update: done - 11-1-node-passive-probe: done - 11-2-drum-recirculation-drum: done - 11-3-floodedevaporator: done - 11-4-floodedcondenser: done - 11-5-bphxexchanger-base: done - 11-6-bphxevaporator: done - 11-7-bphxcondenser: done - 11-8-correlationselector: done - 11-9-movingboundaryhx-zone-discretization: done - 11-10-movingboundaryhx-cache-optimization: done - 11-11-vendorbackend-trait: done - 11-12-copeland-parser: done - 11-13-swep-parser: done - 11-14-danfoss-parser: done - 11-15-bitzer-parser: done - - # --- Backlog: P2 Extended (from legacy integration) --- - 20-1-generalized-performance-curve-engine: done # P2-31 - 20-3-reversing-valve-four-way-component: backlog # P2-32 - 20-4-high-pressure-control-valve: backlog # P2-33 - 20-5-compressor-parallel-units-vfd: backlog # P2-34 - 20-6-multi-zone-heat-exchanger-component: backlog # P2-35 - 20-7-additional-plate-hx-correlations: backlog # P2-36 - 20-8-equipment-catalog-system: backlog # P2-37 - 20-9-fouling-encrustation-resistance: backlog # P2-38 - 20-10-vendor-backend-verification-modernization: backlog # P2-39 - - # ═══════════════════════════════════════════════════════════════ - # P3: FLUID PROPERTIES / Fluides — 8/9 (89%) - # CoolProp, tabular backend, cache, mixtures, CO2 damping, psychrometrics - # ═══════════════════════════════════════════════════════════════ - 2-1-fluid-backend-trait-abstraction: done - 2-2-coolprop-integration-sys-crate: done - 2-3-tabular-interpolation-backend: done - 2-4-lru-cache-for-fluid-properties: done - 2-5-mixture-and-temperature-glide-support: done - 2-6-critical-point-damping-co2-r744: done - 2-7-incompressible-fluids-support: done - 2-8-rich-thermodynamic-state-abstraction: done - - # --- Backlog --- - 20-2-complete-psychrometric-module: backlog # P3-09 - - # ═══════════════════════════════════════════════════════════════ - # P4: SIMULATION & SOLVER — 22/26 (85%) - # >>> PRIORITY PILLAR — must complete before P5/P6 expansion - # ═══════════════════════════════════════════════════════════════ - - # --- In Progress --- - 5-4-multi-variable-control: done - 7-5-json-serialization-deserialization: done - - # --- Review --- - 5-6-control-variable-step-clipping-in-solver: review - - # --- Ready for Dev --- - 7-6-component-calibration-parameters-calib: done - - # --- Backlog --- - 19-1-inverse-calibration: in-progress # P4-25: solver-level algorithm - - # --- Backlog: Multi-point calibration --- - calib-map-multi-point-calibration: backlog # P4-26 - - # --- Done --- - 4-1-solver-trait-abstraction: done - 4-2-newton-raphson-implementation: done - 4-3-sequential-substitution-picard-implementation: done - 4-4-intelligent-fallback-strategy: done - 4-5-time-budgeted-solving: done - 4-6-smart-initialization-heuristic: done - 4-7-convergence-criteria-validation: done - 4-8-jacobian-freezing-optimization: done - 5-1-constraint-definition-framework: done - 5-2-bounded-control-variables: done - 5-3-residual-embedding-for-inverse-control: done - 5-5-swappable-calibration-variables-inverse-calibration-one-shot: done - 7-1-mass-balance-validation: done - 7-2-energy-balance-validation: done - 7-3-traceability-metadata: done - 7-4-debug-verbose-mode: done - 9-3-expansionvalve-energy-methods: done - 9-4-flowsourceflowsink-energy-methods: done - 9-5-flowsplitterflowmerger-energy-methods: done - 9-6-energy-validation-logging-improvement: done - - # ═══════════════════════════════════════════════════════════════ - # P5: USER INTERFACES — 30/59 (51%) - # CLI, Python, C FFI, WASM, Rust API (SystemBuilder) - # ═══════════════════════════════════════════════════════════════ - - # --- Review --- - 6-4-webassembly-compilation: in-progress - 15-4-cli-bphx-exchangers: review - - # --- Ready for Dev --- - 13-5-simulation-result-structured: done - 13-6-json-config-serialize: done - 13-7-fluid-backend-assignment: done - - # --- Backlog: CLI Component Coverage --- - 15-5-cli-flooded-condenser: backlog - 15-6-cli-drum: backlog - 15-7-cli-economizer: backlog - 15-8-cli-flow-splitter-merger: backlog - 15-10-cli-real-pipe: backlog - 15-11-cli-real-fan: backlog - 15-12-cli-bypass-valve: backlog - 15-13-cli-node-probe: backlog - - # --- Backlog: CLI Features & Output --- - 16-1-cli-calib-config: backlog - 16-2-cli-structured-output: backlog - 16-3-cli-multi-fluid: backlog - 16-4-cli-vendor-import: backlog - 16-5-cli-json-schema: backlog - 16-6-cli-machine-templates: backlog - 16-7-cli-parameter-sweep: backlog - - # --- Backlog: Python Parity --- - 17-1-python-fix-pump: backlog - 17-2-python-fix-fan: backlog - 17-3-python-screw-compressor: backlog - 17-4-python-mchx-condenser: backlog - 17-5-python-flooded-hx: backlog - 17-6-python-bphx-exchangers: backlog - 17-7-python-drum: backlog - 17-8-python-boundary-conditions: backlog - 17-9-python-multi-circuit: backlog - 17-10-python-calib-results: backlog - 17-11-python-bypass-coils: backlog - 17-12-python-vendor-parsers: backlog - 17-13-python-macro-snapshot: backlog - - # --- Backlog: WASM --- - 18-1-wasm-finalize-compilation: backlog - 18-2-wasm-all-components: backlog - 18-3-wasm-multi-circuit: backlog - 18-4-wasm-calib-results: backlog - 18-5-wasm-vendor-data: backlog - - # --- Done --- - 6-1-rust-native-api: done - 6-2-python-bindings-pyo3: done - 6-3-c-ffi-bindings-cbindgen: done - 6-5-cli-for-batch-execution: done - 6-6-python-solver-configuration-parity: done - 12-1-cli-internal-state-variables: done - 12-2-cli-coolprop-backend: done - 12-3-cli-screw-compressor-config: done - 12-4-cli-mchx-condenser-config: done - 12-5-cli-flooded-evaporator-brine: done - 12-6-cli-control-constraints: done - 12-7-cli-output-json-metrics: done - 12-8-cli-batch-improvements: done - 13-1-systembuilder-multi-circuit: done - 13-2-systembuilder-port-validated-edges: done - 13-3-systembuilder-constraints-api: done - 13-4-systembuilder-thermal-couplings: done - 15-1-cli-real-compressor: done - 15-2-cli-real-expansion-valve: done - 15-3-cli-real-pump: done - 15-9-cli-boundary-conditions: done - - # ═══════════════════════════════════════════════════════════════ - # P6: HVAC APPLICATIONS — 0/14 (7%) - # Free cooling, calibration, standards compliance, reporting - # ═══════════════════════════════════════════════════════════════ - - # --- In Progress --- - 14-1-freecoolingexchanger-component: review - - # --- Backlog --- - 14-2-automatic-mode-switching-logic: backlog - 14-3-pumpcontroller-optimal-sequencing: backlog - 14-4-variable-frequency-drive-vfd-optimization: backlog - 14-5-bypass-valve-control: backlog - 14-6-safety-interlocks-protection: backlog - 14-7-control-configuration-serialization: backlog - 14-8-python-api-advanced-control: backlog - 7-7-ashrae-140-bestest-validation-post-mvp: backlog # P6-09 - 7-8-inverse-calibration-parameter-estimation: backlog # P6-10 - 19-2-eurovent-reporting: backlog # P6-12 - 19-3-ashrae-bestest: backlog # MERGED into P6-09 (see epic-restructuring.md) - - # --- Backlog: Calibration workflow --- - calibration-workflow-application: backlog # P6-14 - - # ═══════════════════════════════════════════════════════════════ - # RETROSPECTIVES - # ═══════════════════════════════════════════════════════════════ + epic-0: in-progress + 0-1-fix-failing-tests: done + epic-0-retrospective: optional + # --- Epic 0: Physical Regularization & Jacobian Health (epics-solver-hp.md) --- + 0-1-jacobian-killing-pattern-audit-fd-test-harness: done + 0-2-phantom-gradient-regularization-standard: review + 0-3-valve-flow-exv-regularization: backlog + 0-4-heat-exchanger-regularization: backlog + 0-5-domain-sweep-jacobian-health-gate: backlog + # --- Epic 1: Performance Foundation - Linear Algebra Modernization --- + epic-1: in-progress + 1-1-phase-0-baseline-golden-safety-net: in-progress + 1-2-core-crate-typed-convergence-taxonomy: backlog + 1-3-recoverable-domain-violation-errors: backlog + 1-4-linearsolver-trait-explicit-lifecycle: backlog + 1-5-dense-faer-backend-strangler-migration: backlog + 1-6-factorization-scaling-reuse-frozen-jacobians: backlog + 1-7-zero-allocation-residual-jacobian-assembly: backlog + 1-8-system-rs-seam-deconstruction: backlog + 1-9-performance-validation-baseline-sign-off: backlog epic-1-retrospective: optional + # --- Epic 2: Structural Intelligence & Diagnostics --- + epic-2: backlog + 2-1-equation-variable-incidence-graph: backlog + 2-2-matching-dulmage-mendelsohn-decomposition: backlog + 2-3-localized-structural-no-solution-certificates: backlog + 2-4-scc-condensation-cached-block-ordering: backlog + 2-5-numerical-diagnostics-condition-extreme-entries: backlog + 2-6-robustness-sweep-runner-convergence-histogram: backlog epic-2-retrospective: optional + # --- Epic 3: Open Strategy Architecture --- + epic-3: backlog + 3-1-strategy-registry-core: backlog + 3-2-builtin-strategies-registry-migration: backlog + 3-3-composite-fallback-as-data: backlog + 3-4-pluggable-observer-api: backlog + 3-5-third-party-strategy-registration-proof: backlog + 3-6-physical-parameter-continuation-strategy: backlog epic-3-retrospective: optional + # --- Epic 4: Multicore Parallelism & Batch Solving --- + epic-4: backlog + 4-1-send-sync-readiness-usage-site-bounds: backlog + 4-2-parallel-residual-jacobian-evaluation: backlog + 4-3-batch-parallel-solves-api: backlog + 4-4-opt-in-deterministic-mode: backlog + 4-5-parallel-performance-validation: backlog epic-4-retrospective: optional + # --- Epic 5: Physics-Based Initialization & Warm Start --- + epic-5: backlog + 5-1-component-guess-propagation-api: backlog + 5-2-loop-walking-smart-initializer: backlog + 5-3-surrogate-initializer-seam-fallback: backlog + 5-4-versioned-warm-start-snapshots: backlog + 5-5-cold-start-robustness-validation: backlog epic-5-retrospective: optional + # --- Epic 6: Embedded Lite Solver (gate: after Story 1.9 + NFR11 >= 90% trend) --- + epic-6: backlog + 6-1-feature-tiers-std-alloc-core: backlog + 6-2-static-linear-algebra-kernels: backlog + 6-3-property-table-format-offline-generator: backlog + 6-4-lite-solver-lookup-newton-polish: backlog + 6-5-embedded-validation-mcu-linux: backlog epic-6-retrospective: optional + # --- Epic 7: Plugin Ecosystem (gate: solver API stable, post Epic 3) --- + epic-7: backlog + 7-1-plugin-interface-contracts-tier1: backlog + 7-2-c-abi-plugin-tier-abi-versioning: backlog + 7-3-wasm-component-model-plugin-tier: backlog + 7-4-plugin-developer-kit-e2e-example: backlog + 7-5-plugin-sandbox-crash-isolation-validation: backlog epic-7-retrospective: optional - epic-8-retrospective: optional - epic-9-retrospective: optional - epic-10-retrospective: optional - epic-11-retrospective: optional - epic-12-retrospective: optional - epic-13-retrospective: optional - epic-14-retrospective: optional - epic-15-retrospective: optional - epic-16-retrospective: optional - epic-17-retrospective: optional - epic-18-retrospective: optional - epic-19-retrospective: optional diff --git a/crates/components/src/heat_exchanger/flow_regularization.rs b/crates/components/src/heat_exchanger/flow_regularization.rs index c71a75d..378ba3c 100644 --- a/crates/components/src/heat_exchanger/flow_regularization.rs +++ b/crates/components/src/heat_exchanger/flow_regularization.rs @@ -1,5 +1,9 @@ //! C¹/C∞ flow regularization for zero-flow-safe heat-exchanger residuals. //! +//! **Reference specialized pattern** (Story 0.2): keep these HX helpers here; +//! the canonical general toolkit is [`entropyk_core::smoothing`]. See +//! `docs/components/phantom-gradient-regularization.md`. +//! //! Zero mass flow is a **valid** operating state (circuit staging, isolation, //! Newton trial steps). Hard branches `if |m| < ε { Q = 0 }` create Jacobian //! discontinuities and can leave energy residuals under-ranked. diff --git a/crates/core/src/smoothing.rs b/crates/core/src/smoothing.rs index 1f68083..692458c 100644 --- a/crates/core/src/smoothing.rs +++ b/crates/core/src/smoothing.rs @@ -24,6 +24,28 @@ //! - [`smooth_abs`] — C^∞ approximation of `|x|`. //! - [`smooth_clamp`] — C¹ clamp into `[lo, hi]`. //! +//! Analytic first derivatives are exported for every Newton-facing primitive +//! ([`smooth_max_derivative`], [`smooth_min_derivative`], [`smooth_abs_derivative`], +//! [`smooth_clamp_derivative`]) so component Jacobians stay exact. +//! +//! # ε / width selection (physical scale of the blend) +//! +//! | Primitive | Parameter | Meaning | Rule of thumb | +//! |---|---|---|---| +//! | [`smooth_abs`] | `eps` | value at `x = 0`; blend half-width | ~1% of characteristic `\|x\|`, or smaller if the kink is far from the operating region | +//! | [`smooth_max`] / [`smooth_min`] | `k` | overshoot at equality = `k / 2` | `k` much smaller than `\|a - b\|` in the region you care about; never so large it shifts equilibrium | +//! | [`smooth_clamp`] | `width` | transition length at each bound | `width <= (hi - lo) / 2`; keep the band physically small (e.g. opening `1e-3`..`1e-2` on `[0, 1]`) | +//! +//! Heat-exchanger mass-flow activity (`flow_activity`, `m_eps`) lives in +//! `entropyk-components` as a specialized reference pattern — see +//! `docs/components/phantom-gradient-regularization.md`. +//! +//! # Degenerate parameters (never panic) +//! +//! - `k <= 0` → hard [`f64::max`] / [`f64::min`] +//! - `eps <= 0` → hard `|x|` / `sign(x)` +//! - `width <= 0` or inverted `[lo, hi]` → hard [`f64::clamp`] (bounds swapped if needed) +//! //! # Example: smoothing a friction-factor transition //! //! ```rust @@ -250,6 +272,8 @@ pub fn quintic_blend_derivative(a: f64, b: f64, x: f64, edge0: f64, edge1: f64) /// arguments. A small `k` (relative to the scale of `a - b`) gives a sharp but /// still infinitely differentiable maximum. /// +/// When `k <= 0`, returns the hard `a.max(b)` (no panic). +/// /// # Example /// /// ```rust @@ -259,17 +283,51 @@ pub fn quintic_blend_derivative(a: f64, b: f64, x: f64, edge0: f64, edge1: f64) /// assert!((smooth_max(5.0, 1.0, 1e-3) - 5.0).abs() < 1e-2); /// // Always an upper bound on the true max. /// assert!(smooth_max(2.0, 2.0, 0.1) >= 2.0); +/// assert_eq!(smooth_max(5.0, 1.0, 0.0), 5.0); /// ``` #[inline] pub fn smooth_max(a: f64, b: f64, k: f64) -> f64 { + if k <= 0.0 { + return a.max(b); + } 0.5 * (a + b + ((a - b) * (a - b) + k * k).sqrt()) } +/// Analytic derivative of [`smooth_max`] with respect to `a` (`b`, `k` fixed). +/// +/// Equals `0.5 * (1 + (a - b) / sqrt((a - b)² + k²))` for `k > 0`. When +/// `k <= 0`, returns the hard-max subgradient pick: `1` if `a > b`, `0` if +/// `a < b`, and `0.5` if `a == b`. +/// +/// # Example +/// +/// ```rust +/// use entropyk_core::smoothing::smooth_max_derivative; +/// +/// assert!((smooth_max_derivative(5.0, 1.0, 1e-3) - 1.0).abs() < 1e-2); +/// assert!((smooth_max_derivative(1.0, 5.0, 1e-3)).abs() < 1e-2); +/// ``` +#[inline] +pub fn smooth_max_derivative(a: f64, b: f64, k: f64) -> f64 { + if k <= 0.0 { + return if a > b { + 1.0 + } else if a < b { + 0.0 + } else { + 0.5 + }; + } + 0.5 * (1.0 + (a - b) / ((a - b) * (a - b) + k * k).sqrt()) +} + /// C^∞ smooth minimum of `a` and `b` with sharpness controlled by `k > 0`. /// /// Uses `0.5 * (a + b - sqrt((a - b)² + k²))`. Always `<= min(a, b)`, converging /// to `min(a, b)` as `k -> 0`. /// +/// When `k <= 0`, returns the hard `a.min(b)` (no panic). +/// /// # Example /// /// ```rust @@ -277,12 +335,44 @@ pub fn smooth_max(a: f64, b: f64, k: f64) -> f64 { /// /// assert!((smooth_min(5.0, 1.0, 1e-3) - 1.0).abs() < 1e-2); /// assert!(smooth_min(2.0, 2.0, 0.1) <= 2.0); +/// assert_eq!(smooth_min(5.0, 1.0, -1.0), 1.0); /// ``` #[inline] pub fn smooth_min(a: f64, b: f64, k: f64) -> f64 { + if k <= 0.0 { + return a.min(b); + } 0.5 * (a + b - ((a - b) * (a - b) + k * k).sqrt()) } +/// Analytic derivative of [`smooth_min`] with respect to `a` (`b`, `k` fixed). +/// +/// Equals `0.5 * (1 - (a - b) / sqrt((a - b)² + k²))` for `k > 0`. When +/// `k <= 0`, returns the hard-min subgradient pick: `1` if `a < b`, `0` if +/// `a > b`, and `0.5` if `a == b`. +/// +/// # Example +/// +/// ```rust +/// use entropyk_core::smoothing::smooth_min_derivative; +/// +/// assert!((smooth_min_derivative(1.0, 5.0, 1e-3) - 1.0).abs() < 1e-2); +/// assert!((smooth_min_derivative(5.0, 1.0, 1e-3)).abs() < 1e-2); +/// ``` +#[inline] +pub fn smooth_min_derivative(a: f64, b: f64, k: f64) -> f64 { + if k <= 0.0 { + return if a < b { + 1.0 + } else if a > b { + 0.0 + } else { + 0.5 + }; + } + 0.5 * (1.0 - (a - b) / ((a - b) * (a - b) + k * k).sqrt()) +} + /// C^∞ approximation of the absolute value `|x|`. /// /// Returns `sqrt(x² + eps²)`. Always `>= |x|`, converging to `|x|` as @@ -290,6 +380,8 @@ pub fn smooth_min(a: f64, b: f64, k: f64) -> f64 { /// `x / sqrt(x² + eps²)` is smooth through the origin, replacing the /// non-differentiable corner of `|x|` that would otherwise break Newton. /// +/// When `eps <= 0`, returns the hard `|x|` (no panic). +/// /// # Example /// /// ```rust @@ -298,16 +390,21 @@ pub fn smooth_min(a: f64, b: f64, k: f64) -> f64 { /// assert!((smooth_abs(0.0, 1e-3) - 1e-3).abs() < 1e-12); /// assert!((smooth_abs(5.0, 1e-3) - 5.0).abs() < 1e-3); /// assert!(smooth_abs(-3.0, 0.1) > 3.0); +/// assert_eq!(smooth_abs(-3.0, 0.0), 3.0); /// ``` #[inline] pub fn smooth_abs(x: f64, eps: f64) -> f64 { + if eps <= 0.0 { + return x.abs(); + } (x * x + eps * eps).sqrt() } /// Analytic derivative of [`smooth_abs`] with respect to `x`. /// /// Equals `x / sqrt(x² + eps²)`, a smooth approximation of `sign(x)` that -/// passes continuously through `0` at the origin. +/// passes continuously through `0` at the origin. When `eps <= 0`, returns +/// hard `sign(x)` with `0.0` at the origin. /// /// # Example /// @@ -320,6 +417,15 @@ pub fn smooth_abs(x: f64, eps: f64) -> f64 { /// ``` #[inline] pub fn smooth_abs_derivative(x: f64, eps: f64) -> f64 { + if eps <= 0.0 { + return if x > 0.0 { + 1.0 + } else if x < 0.0 { + -1.0 + } else { + 0.0 + }; + } x / (x * x + eps * eps).sqrt() } @@ -333,6 +439,10 @@ pub fn smooth_abs_derivative(x: f64, eps: f64) -> f64 { /// always within `[lo, hi]`. For a flat identity region to exist, pass /// `width <= (hi - lo) / 2`; larger widths still produce a bounded, C¹ result. /// +/// Degenerate cases (never panic): +/// - `hi < lo` → bounds are swapped +/// - `width <= 0` → hard `x.clamp(lo, hi)` +/// /// # Example /// /// ```rust @@ -343,9 +453,14 @@ pub fn smooth_abs_derivative(x: f64, eps: f64) -> f64 { /// // Values well outside saturate to the bounds. /// assert!((smooth_clamp(5.0, -1.0, 1.0, 0.1) - 1.0).abs() < 1e-9); /// assert!((smooth_clamp(-5.0, -1.0, 1.0, 0.1) + 1.0).abs() < 1e-9); +/// assert_eq!(smooth_clamp(5.0, -1.0, 1.0, 0.0), 1.0); /// ``` #[inline] pub fn smooth_clamp(x: f64, lo: f64, hi: f64, width: f64) -> f64 { + let (lo, hi) = if hi < lo { (hi, lo) } else { (lo, hi) }; + if width <= 0.0 { + return x.clamp(lo, hi); + } // Lower ramp: equals `lo` for x <= lo, eases to identity `x` by x = lo + width. // Stays within [lo, x] and is C¹ at both joins (zero slope at lo, unit slope // at lo + width). @@ -354,6 +469,36 @@ pub fn smooth_clamp(x: f64, lo: f64, hi: f64, width: f64) -> f64 { hi + (lower - hi) * smoothstep(hi, hi - width, lower) } +/// Analytic derivative of [`smooth_clamp`] with respect to `x`. +/// +/// Chain-rules the lower then upper [`smoothstep`] ramps. Outside the +/// transition bands the slope is `0` (saturated) or `1` (identity interior). +/// Degenerate parameters match [`smooth_clamp`]: hard-clamp derivative is `1` +/// on the open interior and `0` elsewhere (including the kink points). +/// +/// # Example +/// +/// ```rust +/// use entropyk_core::smoothing::smooth_clamp_derivative; +/// +/// // Flat exterior, unit slope deep interior. +/// assert!((smooth_clamp_derivative(5.0, -1.0, 1.0, 0.1)).abs() < 1e-9); +/// assert!((smooth_clamp_derivative(0.0, -1.0, 1.0, 0.1) - 1.0).abs() < 1e-9); +/// ``` +#[inline] +pub fn smooth_clamp_derivative(x: f64, lo: f64, hi: f64, width: f64) -> f64 { + let (lo, hi) = if hi < lo { (hi, lo) } else { (lo, hi) }; + if width <= 0.0 { + return if x > lo && x < hi { 1.0 } else { 0.0 }; + } + let s_lo = smoothstep(lo, lo + width, x); + let d_lower = s_lo + (x - lo) * smoothstep_derivative(lo, lo + width, x); + let lower = lo + (x - lo) * s_lo; + let s_hi = smoothstep(hi, hi - width, lower); + let ds_hi = smoothstep_derivative(hi, hi - width, lower); + d_lower * (s_hi + (lower - hi) * ds_hi) +} + #[cfg(test)] mod tests { use super::*; @@ -528,15 +673,84 @@ mod tests { #[test] fn test_smooth_max_derivative_matches_fd() { - // d/da smooth_max = 0.5 * (1 + (a-b)/sqrt((a-b)^2 + k^2)) let (b, k): (f64, f64) = (1.0, 0.3); for a in [-1.0, 0.5, 1.0, 1.5, 3.0] { - let analytic = 0.5 * (1.0 + (a - b) / ((a - b) * (a - b) + k * k).sqrt()); + let analytic = smooth_max_derivative(a, b, k); let numeric = fd(|a| smooth_max(a, b, k), a, 1e-6); assert!((analytic - numeric).abs() < 1e-5, "at a={}", a); } } + #[test] + fn test_smooth_min_derivative_matches_fd() { + let (b, k): (f64, f64) = (1.0, 0.3); + for a in [-1.0, 0.5, 1.0, 1.5, 3.0] { + let analytic = smooth_min_derivative(a, b, k); + let numeric = fd(|a| smooth_min(a, b, k), a, 1e-6); + assert!((analytic - numeric).abs() < 1e-5, "at a={}", a); + } + } + + #[test] + fn test_smooth_clamp_derivative_matches_fd() { + let (lo, hi, width) = (-1.0, 1.0, 0.1); + // Exterior, both ramps, deep interior. + for x in [-5.0, -1.05, -0.95, -0.5, 0.0, 0.5, 0.95, 1.05, 5.0] { + let analytic = smooth_clamp_derivative(x, lo, hi, width); + let numeric = fd(|x| smooth_clamp(x, lo, hi, width), x, 1e-6); + assert!( + (analytic - numeric).abs() < 1e-5, + "smooth_clamp deriv mismatch at {}: {} vs {}", + x, + analytic, + numeric + ); + } + } + + #[test] + fn test_smooth_clamp_c1_at_junctions() { + // Slope continuity across the four joins: lo, lo+width, hi-width, hi. + let (lo, hi, width) = (-1.0, 1.0, 0.2); + let junctions = [lo, lo + width, hi - width, hi]; + let h = 1e-5; + for j in junctions { + let slope_left = fd(|x| smooth_clamp(x, lo, hi, width), j - h, h); + let slope_right = fd(|x| smooth_clamp(x, lo, hi, width), j + h, h); + let analytic = smooth_clamp_derivative(j, lo, hi, width); + assert!( + (slope_left - slope_right).abs() < 5e-3, + "slope jump at {}: left={} right={}", + j, + slope_left, + slope_right + ); + assert!( + (analytic - 0.5 * (slope_left + slope_right)).abs() < 5e-3, + "analytic slope mismatch at {}: analytic={} fd_avg={}", + j, + analytic, + 0.5 * (slope_left + slope_right) + ); + } + } + + #[test] + fn test_degenerate_parameters_do_not_panic() { + assert_eq!(smooth_max(3.0, 1.0, 0.0), 3.0); + assert_eq!(smooth_max(3.0, 1.0, -0.5), 3.0); + assert_eq!(smooth_min(3.0, 1.0, -1.0), 1.0); + assert_eq!(smooth_abs(-2.0, 0.0), 2.0); + assert_eq!(smooth_abs_derivative(-2.0, 0.0), -1.0); + assert_eq!(smooth_clamp(5.0, -1.0, 1.0, 0.0), 1.0); + assert_eq!(smooth_clamp_derivative(0.0, -1.0, 1.0, 0.0), 1.0); + assert_eq!(smooth_clamp_derivative(5.0, -1.0, 1.0, 0.0), 0.0); + // Inverted bounds swap. + assert!((smooth_clamp(0.0, 1.0, -1.0, 0.1) - 0.0).abs() < 1e-9); + assert_eq!(smooth_max_derivative(3.0, 1.0, 0.0), 1.0); + assert_eq!(smooth_min_derivative(3.0, 1.0, 0.0), 0.0); + } + #[test] fn test_smooth_abs_properties_and_derivative() { assert!((smooth_abs(0.0, 1e-3) - 1e-3).abs() < 1e-12); diff --git a/docs/audits/jacobian-killing-pattern-audit.md b/docs/audits/jacobian-killing-pattern-audit.md new file mode 100644 index 0000000..d92a0a4 --- /dev/null +++ b/docs/audits/jacobian-killing-pattern-audit.md @@ -0,0 +1,148 @@ +# Jacobian-Killing Pattern Audit + +**Date:** 2026-07-18 +**Story:** `0-1-jacobian-killing-pattern-audit-fd-test-harness` (Epic 0) +**Scope:** Inventory of `clamp` / `max` / `min` (and related hard gates) on Newton residual/Jacobian paths in `entropyk-components`. Physics fixes are owned by Stories 0.2–0.4. + +## Failure-mode legend + +| Mode | Meaning | +|------|---------| +| `zero-gradient` | Analytic ∂r/∂x (or helper derivative) hard-zeros in a region where physics should still inform Newton | +| `discontinuity` | Hard clamp/step on a Newton unknown → C⁰ or worse residual; FD and analytic disagree near the edge | +| `benign` | Floor/guard for division-by-zero, config validation, or non-Newton parameter — not a solver killer | +| `already-regularized` | C¹ / phantom pattern already present (may still need unification) | + +| Severity | Meaning | +|----------|---------| +| `P0` | Confirmed FR19 killer; blocks convergence at domain edges | +| `P1` | Likely Newton-relevant clamp on live unknowns / quality | +| `P2` | Secondary / correlation-local; watch when that path is active | +| `info` | Documented debt or missing API; not a clamp site | + +--- + +## FR19 known-bad sites (confirmed 2026-07-18) + +| Site | Failure mode | Severity | Fix owner | +|------|--------------|----------|-----------| +| `crates/components/src/valve_flow.rs:105-107` — `valve_mass_flow_dp_up` returns `Ok(0.0)` when `dp <= 0 \|\| m <= 0` | `zero-gradient` | **P0** | Story 0.3 | +| `crates/components/src/isenthalpic_expansion_valve.rs:233` — `state[i].clamp(0.0, 1.0)` on orifice opening (residual path) | `discontinuity` | **P0** | Story 0.3 | +| `crates/components/src/isenthalpic_expansion_valve.rs:449` — `_state[i].clamp(0.0, 1.0)` on orifice opening (Jacobian path) | `discontinuity` | **P0** | Story 0.3 | +| `crates/components/src/heat_exchanger/two_phase_dp.rs:107` — `quality.clamp(0.0, 1.0)` in `homogeneous_density` | `discontinuity` | **P0** | Story 0.4 | +| `crates/components/src/heat_exchanger/two_phase_dp.rs:150` — `quality.clamp(0.0, 1.0)` in `friedel_multiplier` | `discontinuity` | **P0** | Story 0.4 | +| `crates/components/src/heat_exchanger/condenser.rs:492` — `state[idx].clamp(0.0, 1.5)` fan actuator φ | `discontinuity` | **P0** | Story 0.4 | +| `crates/components/src/heat_exchanger/condenser.rs:752` — `state[idx].clamp(0.0, 0.98)` flooded level λ | `discontinuity` | **P0** | Story 0.4 | + +### Evidence notes + +- **Valve ΔP≤0:** `valve_mass_flow` already forces `dp = ΔP.max(0)`, so ṁ=0 and `valve_mass_flow_dp_up` returns a hard zero — Newton gets no direction to restore positive ΔP. +- **EXV opening clamps:** same hard clamp appears in both residual helper and Jacobian branch; Story 0.2 `smooth_clamp` is the intended replacement (`entropyk_core::smoothing` already exists). +- **EXV phantom ΔP:** `DP_FLOOR` (~L396–445) is a **partial** `already-regularized` mitigation for ΔP≤0 on the Jacobian only; residual still early-outs. Opening clamps remain P0. +- **Missing API:** Epic Story 0.3 text mentions `valve_mass_flow_dp_down` — **only `valve_mass_flow_dp_up` exists** today (`lib.rs` re-export). Catalogue gap severity `info`; inventing `dp_down` is out of scope for Story 0.1. + +--- + +## Broader catalogue (residual / Jacobian call graph) + +Classified from a ripgrep sweep of `.clamp(` / `.max(` / `.min(` under `crates/components/src`. Not every hit is listed — config setters, test helpers, and pure numerical floors are collapsed into summary rows. + +### P0 / P1 — Newton-relevant + +| Location | Pattern | Mode | Sev | Notes / owner | +|----------|---------|------|-----|---------------| +| `valve_flow.rs:71,75,78,99` | `dp.max(0)`, `opening.clamp`, `m.max(0)` | `zero-gradient` / `discontinuity` | P0 | Feeds EXV/orifice; Story 0.3 | +| `valve_flow.rs:105-107` | hard `Ok(0.0)` on ∂ṁ/∂P_up | `zero-gradient` | P0 | FR19 | +| `isenthalpic_expansion_valve.rs:233,449` | opening clamp on state | `discontinuity` | P0 | FR19 → 0.3 | +| `isenthalpic_expansion_valve.rs:396-445` | `DP_FLOOR` phantom | `already-regularized` | info | Keep; unify with toolkit in 0.2/0.3 | +| `two_phase_dp.rs:107,150` | quality clamp | `discontinuity` | P0 | FR19 → 0.4 | +| `two_phase_dp.rs:118-131` | hard `x<=0` / `x>=1` branches in `zivi_void_fraction` | `discontinuity` | P1 | Story 0.4 (MSH/dome) | +| `two_phase_dp.rs:255-294` | Hermite blend near x→1 (`msh_gradient`) | `already-regularized` | info | Partial MSH fix; 0.4 completes | +| `condenser.rs:492,752` | fan / flood actuator clamps | `discontinuity` | P0 | FR19 → 0.4 | +| `evaporator.rs` | analogous secondary / property floors | `benign` / P2 | P2 | Re-check when evaporator actuators mirror condenser | +| `heat_exchanger/flow_regularization.rs` | smooth mass / activity | `already-regularized` | info | Reference for Story 0.2 unification | +| `heat_exchanger/sat_domain.rs:81` | `p_pa.clamp(p_min, p_max)` | `discontinuity` | P1 | Intermediate iterate domain; Story 0.4 / sat path | +| `screw_economizer_compressor.rs:486` | eco fraction `.clamp(0.0, 0.4)` | `discontinuity` | P1 | If eco fraction is a Newton unknown | +| `screw_economizer_compressor.rs:291,343` | slide valve clamp | `discontinuity` | P1 | Config vs state — verify call site | +| `bphx_correlation.rs:588+` | quality / p_reduced clamps | `discontinuity` | P2 | Correlation internals on quality | + +### Benign / info (not Epic-0 P0) + +| Pattern class | Examples | Mode | Sev | +|---------------|----------|------|-----| +| Divisor floors | `.max(1e-9)`, `.max(1e-12)` on ρ, μ, D | `benign` | info | +| Builder / config clamps | `with_orifice_fixed` opening clamp, UA scale `.max(0)` | `benign` | info | +| Registry JSON parsing | `registry.rs` quality/RH clamps | `benign` | info | +| Air humidity RH clamp | `air_boundary.rs` | `benign` | info | +| FD step sizing in tests | `(x.abs()*1e-6).max(1e-3)` | `benign` | info | + +### Related NFR9 debt (not clamp killers — do not fix in 0.1–0.4 regularization stories) + +| Item | Issue | Sev | +|------|-------|-----| +| `heat_exchanger/exchanger.rs:854-897` | 4-port Jacobian is **100% central FD** in production | info (NFR9) | +| `screw_economizer_compressor.rs` | Incomplete analytic Jacobian rows | info | +| `python_components.rs` | Empty `jacobian_entries` stubs | info | +| Category-B solver tests (`jacobian_freezing`, inverse calibration) | Deferred per `plans/audit-2026-07-remediation-plan.md` | info | + +--- + +## Harness usage (`entropyk_components::jacobian_fd`) + +Central FD vs analytic at one state point: + +```rust +use entropyk_components::jacobian_fd::{ + check_jacobian_health, JacobianFdConfig, +}; + +let report = check_jacobian_health(&component, &state, JacobianFdConfig::default())?; +assert!( + report.has_zero_gradient_killers(), + "expected clamp/zero-gradient signature, got clean report" +); +// After Stories 0.2–0.4 fixes, Story 0.5 flips the gate to: +// assert!(report.is_clean()); +``` + +### Defaults (aligned with condenser FD tests) + +| Constant | Default | +|----------|---------| +| `rel_epsilon` | `1e-6` | +| `h_floor` | `1e-3` | +| `rel_tol` | `1e-4` | +| `analytic_atol` | `1e-12` | +| `fd_informative` | `1e-8` | + +**Do not** call `JacobianMatrix::numerical` (solver) from this harness — that API is **forward** difference for Newton fallback. + +Integration smoke tests: `crates/components/tests/jacobian_health_sweep.rs`. + +--- + +## Story 0.5 CI gate (future) + +1. Reuse `check_jacobian_health` over per-component physical-domain grids (ΔP≤0, quality 0/1, opening 0/1, dome edges, actuator clamps). +2. Fail CI on new `zero_gradient_killers` or mismatches outside an allow-list that shrinks to empty after 0.2–0.4. +3. Commit a per-component / per-region health report artifact (extend this file or generate JSON beside it). + +--- + +## Deferred work (explicit) + +| Work | Owner | +|------|-------| +| Standardize / document `smooth_*` ε guidance; declare `flow_regularization` as HX reference | Story **0.2** — **landed** (`docs/components/phantom-gradient-regularization.md`; derivatives in `crates/core/src/smoothing.rs`) | +| Valve ΔP≤0 phantom + EXV opening `smooth_clamp` | Story **0.3** | +| Quality / condenser–evaporator state clamps + MSH singularity | Story **0.4** | +| Full-envelope CI Jacobian health gate | Story **0.5** | +| Replace production FD Jacobian in generic 4-port HX | Separate NFR9 / remediation track | + +--- + +## Severity ranking summary + +1. **P0 (fix in 0.3–0.4):** valve ΔP zero-gradient; EXV opening clamps; two-phase quality clamps; condenser fan/flood clamps. +2. **P1:** sat-domain pressure clamp; screw eco/slide clamps when unknowns; zivi hard edges. +3. **P2 / info:** correlation floors, config clamps, NFR9 FD-in-production debt. diff --git a/docs/components/flow-regularization.md b/docs/components/flow-regularization.md new file mode 100644 index 0000000..8540193 --- /dev/null +++ b/docs/components/flow-regularization.md @@ -0,0 +1,62 @@ +# Flow regularization (zero-flow helpers) + +Source: `crates/components/src/heat_exchanger/flow_regularization.rs` +Used by: **FloodedEvaporator** (full residual path); **Condenser** / **Evaporator** (smooth `|ṁ|` for live `C_sec`). + +> **Standard:** This module is the **reference specialized pattern** for HX +> zero-flow residuals. The canonical general C¹ toolkit is +> `entropyk_core::smoothing` — see +> [phantom-gradient-regularization.md](./phantom-gradient-regularization.md). +> Do not move these helpers into core; do not duplicate `smooth_*` here. + +--- + +## EN + +### Why + +Zero mass flow is a **valid** state (staging, circuit off, Newton trial steps). Hard branches like `if |m| < ε { Q = 0 }` create **Jacobian discontinuities**. + +### API + +| Function | Meaning | +|----------|---------| +| `flow_activity(m, ε)` | α = m²/(m²+ε²) ∈ [0,1), α(0)=0 | +| `flow_activity_derivative` | dα/dm | +| `effective_duty(Q, α_a, α_b)` | Q_eff = α_a · α_b · Q | +| `blend_transport_residual` | blend active transport residual with Δh hold | +| `blend_transport_partials` | analytic partials of the blend | +| `smooth_mass_magnitude` | C¹-ish smooth \|m\| for `C = \|ṁ\| · cp` | +| `smooth_mass_magnitude_derivative` | d\|m\|_smooth / dm | + +Defaults: `DEFAULT_M_EPS_KG_S = 1e-4`, `DEFAULT_M_SCALE_KG_S = 0.05`. + +### Interaction with rating mode (Flooded) + +On **FloodedEvaporator system path** (live secondary): duty uses `effective_duty` with α_ref and α_sec. +On **Flooded rating path** (scalar C_sec only): residual energy uses **full Q** (no α_ref gate) so `ṁ_ref = 0` is not a trivial root when `C_sec > 0`. + +### DoF rule + +Regularization **must not** change `n_equations()`. It only reshapes residual values and derivatives. + +--- + +## FR + +### Pourquoi + +Le débit nul est un état **valide**. Les `if |m| < ε` durs cassent le Newton. + +### API + +Voir le tableau EN. + +### Rating vs système (Flooded) + +- **Système (ports live)** : duty régularisée α_ref · α_sec · Q. +- **Rating (scalaires)** : Q **plein** dans le résidu énergie (pas de racine triviale ṁ=0). + +### Règle DoF + +La régularisation **ne change pas** `n_equations()`. diff --git a/docs/components/phantom-gradient-regularization.md b/docs/components/phantom-gradient-regularization.md new file mode 100644 index 0000000..a5f81ba --- /dev/null +++ b/docs/components/phantom-gradient-regularization.md @@ -0,0 +1,88 @@ +# Phantom-Gradient Regularization Standard + +Canonical toolkit for C¹ (or smoother) replacements of hard clamps, `max`/`min`, +and absolute-value kinks on Newton unknowns. Owned by Epic 0 / Story 0.2 +(`epics-solver-hp.md`, FR19). + +Source code: `crates/core/src/smoothing.rs` (`entropyk_core::smoothing`). + +--- + +## Decisions + +| Layer | Location | Role | +|-------|----------|------| +| **Canonical general toolkit** | `entropyk_core::smoothing` | `smooth_max` / `smooth_min` / `smooth_abs` / `smooth_clamp` (+ analytic derivatives) | +| **Reference specialized pattern** | `heat_exchanger::flow_regularization` | Mass-flow activity / duty / transport blend for zero-flow HX residuals — **not** duplicated into core | +| **Verification harness** | `entropyk_components::jacobian_fd` | Central FD vs analytic (Story 0.1); CI gate is Story 0.5 | + +**Banned** in residual / Jacobian paths on Newton unknowns: + +- bare `.clamp(...)` whose derivative jumps `0 ↔ 1` +- hard `.max(0)` / early `Ok(0.0)` that zeros informative physics derivatives + +Physical clamp → smooth migrations are Stories **0.3** (valve / EXV) and **0.4** (HX). +This standard ships toolkit + docs only; it does not change component physics. + +DoF rule: regularization **must not** change `n_equations()`. + +--- + +## API (core) + +| Function | Continuity | Parameter | +|----------|------------|-----------| +| `smooth_max(a, b, k)` / `smooth_max_derivative` | C∞ | sharpness `k` (overshoot at equality = `k/2`) | +| `smooth_min(a, b, k)` / `smooth_min_derivative` | C∞ | same | +| `smooth_abs(x, eps)` / `smooth_abs_derivative` | C∞ | `eps` = value at `x=0` | +| `smooth_clamp(x, lo, hi, width)` / `smooth_clamp_derivative` | C¹ | transition `width` at each bound | +| `smoothstep` / `cubic_blend` / `quintic_blend` (+ derivatives) | C¹ / C² | correlation transition windows | + +Degenerate parameters never panic: `k <= 0` → hard max/min; `eps <= 0` → hard `|x|`; +`width <= 0` or inverted bounds → hard clamp (bounds swapped if needed). + +--- + +## ε / width selection guidance + +| Primitive | Parameter | Physical meaning | Rule of thumb | +|-----------|-----------|------------------|---------------| +| `smooth_abs` | `eps` | Blend half-width; value at origin | ~1% of characteristic `\|x\|`, or smaller if the kink is far from the operating region | +| `smooth_max` / `smooth_min` | `k` | Softness; overshoot at equality = `k/2` | `k ≪ \|a − b\|` where you need the hard extremum; never large enough to shift equilibrium | +| `smooth_clamp` | `width` | Transition length at each bound | Prefer `width ≤ (hi − lo) / 2`. Keep Newton-visible band physically small | +| `flow_activity` | `m_eps` | Mass-flow activity scale | Keep HX default `DEFAULT_M_EPS_KG_S = 1e-4` (see [flow-regularization.md](./flow-regularization.md)) | + +### Recommended starting widths (examples only) + +| Quantity | Interval | Suggested `width` | +|----------|----------|-------------------| +| Valve / EXV opening | `[0, 1]` | `1e-3` … `1e-2` | +| Thermodynamic quality | `[0, 1]` | `1e-3` … `1e-2` | +| Fan actuator | `[0, 1.5]` | `~1e-2` | +| Flood level | `[0, 0.98]` | `~1e-2` (respect `width ≤ (hi−lo)/2`) | + +Always pair value and derivative in both `compute_residuals` and `jacobian_entries` +(`smooth_clamp` + `smooth_clamp_derivative`, etc.). + +--- + +## Relationship to flow regularization + +`crates/components/src/heat_exchanger/flow_regularization.rs` is the **reference +implementation** for zero-flow-safe HX residuals. It builds on +`entropyk_core::smoothing::smooth_abs` and stays in `entropyk-components` because +it encodes residual units (watts) and characteristic mass-flow scales +(`m_scale`) that are HX-specific. + +Do **not** move `flow_activity` / `blend_transport_residual` into core. +Do **not** invent a second smoothing module under `components/`. + +Details: [flow-regularization.md](./flow-regularization.md). + +--- + +## Verification + +- Unit / FD continuity tests: `crates/core/src/smoothing.rs` (`#[cfg(test)]`). +- Component Jacobian health: `check_jacobian_health` (Story 0.1 audit harness). +- Envelope CI gate: Story 0.5.