Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
Some checks failed
CI / check (push) Has been cancelled
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ====================
|
||||
|
||||
Reference in New Issue
Block a user