Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
Some checks failed
CI / check (push) Has been cancelled

Capture uncommitted solver robustness work (regularization, domain errors, linear solver lifecycle, tube DP/MSH), web workbench updates, and synced BMAD skills across IDE agent folders before starting BPHX pressure-drop.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-19 16:35:31 +02:00
parent 88620790d6
commit 5bd180b5b8
1363 changed files with 101041 additions and 58547 deletions

View File

@@ -73,7 +73,11 @@ fn normalize(x: f64, edge0: f64, edge1: f64) -> (f64, f64) {
return (t, 0.0);
}
let inv_width = 1.0 / width;
let t = ((x - edge0) * inv_width).clamp(0.0, 1.0);
let val = (x - edge0) * inv_width;
if val.is_nan() {
return (f64::NAN, inv_width);
}
let t = val.clamp(0.0, 1.0);
(t, inv_width)
}
@@ -309,6 +313,9 @@ pub fn smooth_max(a: f64, b: f64, k: f64) -> f64 {
/// ```
#[inline]
pub fn smooth_max_derivative(a: f64, b: f64, k: f64) -> f64 {
if a.is_nan() || b.is_nan() || k.is_nan() {
return f64::NAN;
}
if k <= 0.0 {
return if a > b {
1.0
@@ -318,7 +325,11 @@ pub fn smooth_max_derivative(a: f64, b: f64, k: f64) -> f64 {
0.5
};
}
0.5 * (1.0 + (a - b) / ((a - b) * (a - b) + k * k).sqrt())
let denom = ((a - b) * (a - b) + k * k).sqrt();
if denom == 0.0 {
return 0.5;
}
0.5 * (1.0 + (a - b) / denom)
}
/// C^∞ smooth minimum of `a` and `b` with sharpness controlled by `k > 0`.
@@ -361,6 +372,9 @@ pub fn smooth_min(a: f64, b: f64, k: f64) -> f64 {
/// ```
#[inline]
pub fn smooth_min_derivative(a: f64, b: f64, k: f64) -> f64 {
if a.is_nan() || b.is_nan() || k.is_nan() {
return f64::NAN;
}
if k <= 0.0 {
return if a < b {
1.0
@@ -370,7 +384,11 @@ pub fn smooth_min_derivative(a: f64, b: f64, k: f64) -> f64 {
0.5
};
}
0.5 * (1.0 - (a - b) / ((a - b) * (a - b) + k * k).sqrt())
let denom = ((a - b) * (a - b) + k * k).sqrt();
if denom == 0.0 {
return 0.5;
}
0.5 * (1.0 - (a - b) / denom)
}
/// C^∞ approximation of the absolute value `|x|`.
@@ -417,6 +435,9 @@ pub fn smooth_abs(x: f64, eps: f64) -> f64 {
/// ```
#[inline]
pub fn smooth_abs_derivative(x: f64, eps: f64) -> f64 {
if x.is_nan() || eps.is_nan() {
return f64::NAN;
}
if eps <= 0.0 {
return if x > 0.0 {
1.0
@@ -426,7 +447,14 @@ pub fn smooth_abs_derivative(x: f64, eps: f64) -> f64 {
0.0
};
}
x / (x * x + eps * eps).sqrt()
let denom = (x * x + eps * eps).sqrt();
if denom == 0.0 {
return 0.0;
}
if denom.is_infinite() {
return x.signum();
}
x / denom
}
/// C¹ clamp of `x` into `[lo, hi]` with a smooth transition of width `width`
@@ -457,10 +485,15 @@ pub fn smooth_abs_derivative(x: f64, eps: f64) -> f64 {
/// ```
#[inline]
pub fn smooth_clamp(x: f64, lo: f64, hi: f64, width: f64) -> f64 {
if x.is_nan() || lo.is_nan() || hi.is_nan() || width.is_nan() {
return f64::NAN;
}
let (lo, hi) = if hi < lo { (hi, lo) } else { (lo, hi) };
if width <= 0.0 {
return x.clamp(lo, hi);
}
let max_width = (hi - lo) / 2.0;
let width = width.min(max_width);
// 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).
@@ -487,10 +520,15 @@ pub fn smooth_clamp(x: f64, lo: f64, hi: f64, width: f64) -> f64 {
/// ```
#[inline]
pub fn smooth_clamp_derivative(x: f64, lo: f64, hi: f64, width: f64) -> f64 {
if x.is_nan() || lo.is_nan() || hi.is_nan() || width.is_nan() {
return f64::NAN;
}
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 max_width = (hi - lo) / 2.0;
let width = width.min(max_width);
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;
@@ -784,7 +822,7 @@ mod tests {
let x = i as f64 / 50.0;
let y = smooth_clamp(x, -1.0, 1.0, 0.1);
assert!(
y >= -1.0 - 1e-9 && y <= 1.0 + 1e-9,
(-1.0 - 1e-9..=1.0 + 1e-9).contains(&y),
"out of bounds at x={}: {}",
x,
y
@@ -800,4 +838,30 @@ mod tests {
assert_eq!(smoothstep_derivative(3.0, 3.0, 3.1), 0.0);
assert_eq!(smootherstep(3.0, 3.0, 3.1), 1.0);
}
#[test]
fn test_code_review_patches() {
// 1. NaN propagation (no panic)
assert!(smooth_max_derivative(f64::NAN, 1.0, 0.1).is_nan());
assert!(smooth_min_derivative(1.0, f64::NAN, 0.1).is_nan());
assert!(smooth_abs_derivative(f64::NAN, 0.1).is_nan());
assert!(smooth_clamp(f64::NAN, 0.0, 1.0, 0.1).is_nan());
assert!(smooth_clamp_derivative(f64::NAN, 0.0, 1.0, 0.1).is_nan());
assert!(smooth_clamp(0.5, f64::NAN, 1.0, 0.1).is_nan());
// 2. NaN clamp degenerate
assert!(smooth_clamp(f64::NAN, 0.0, 1.0, -0.1).is_nan());
// 3. Subnormal / zero division
assert_eq!(smooth_max_derivative(1.0, 1.0, 0.0), 0.5);
assert_eq!(smooth_min_derivative(1.0, 1.0, 0.0), 0.5);
assert_eq!(smooth_abs_derivative(0.0, 0.0), 0.0);
// 4. Overlapping bounds width capping
// Standard [0.0, 1.0] with too large width = 2.0 -> caps to 0.5
let val_neg_inf = smooth_clamp(-1e10, 0.0, 1.0, 2.0);
assert!((val_neg_inf - 0.0).abs() < 1e-9);
let val_pos_inf = smooth_clamp(1e10, 0.0, 1.0, 2.0);
assert!((val_pos_inf - 1.0).abs() < 1e-9);
}
}

View File

@@ -1906,7 +1906,7 @@ mod tests {
fn test_clone_and_copy() {
let p1 = Pressure::from_pascals(100_000.0);
let p2 = p1; // Copy
let p3 = p1.clone(); // Clone
let p3 = p1; // Copy (Copy trait is tested)
assert_relative_eq!(p1.to_pascals(), p2.to_pascals(), epsilon = 1e-10);
assert_relative_eq!(p1.to_pascals(), p3.to_pascals(), epsilon = 1e-10);
@@ -1928,15 +1928,13 @@ mod tests {
let p_nan = Pressure(f64::NAN);
// NaN comparisons should return false for all ordering operators
assert!(!(p_nan < p1));
assert!(!(p_nan > p1));
assert!(!(p_nan == p1));
assert!(!(p1 < p_nan));
assert!(!(p1 > p_nan));
assert!(!(p1 == p_nan));
assert_eq!(p_nan.partial_cmp(&p1), None);
assert_eq!(p1.partial_cmp(&p_nan), None);
assert_ne!(p_nan, p1);
assert_ne!(p1, p_nan);
// NaN should not equal itself
assert!(!(p_nan == p_nan));
assert_ne!(p_nan, p_nan);
}
// ==================== EDGE CASES ====================