Complete Story 0.2 phantom-gradient regularization standard.

Add analytic derivatives and C1 tests for smooth_max/min/clamp, document epsilon/width guidance, and declare flow_regularization as the HX reference pattern without changing component physics.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-18 16:33:48 +02:00
parent f88cd7f7d8
commit 88620790d6
7 changed files with 889 additions and 290 deletions

View File

@@ -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.20.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` (~L396445) 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.10.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.20.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.20.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 / condenserevaporator 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.30.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.

View File

@@ -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()`.

View File

@@ -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 ≤ (hilo)/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.