mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 11:11:59 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 532a7c6886 | |||
| bafb0b5e22 | |||
| 7c09575ab1 | |||
| 2a4a55287c |
@@ -905,6 +905,7 @@ pub fn gn_enclose_over(
|
||||
) -> Result<Option<Enclosure>> {
|
||||
crate::profile::profile_scope!(k, "blend::certificate::gn_enclose_over");
|
||||
let s = at.s;
|
||||
crate::profile::profile_note!(k, "enclose s=[{},{}] order={} n={}", s.lo, s.hi, order_s, n);
|
||||
let tr = Trunc::new(order_s, n)?;
|
||||
// a ball whose radius varies: its law over the span (0d43b4c)
|
||||
let law_r = match law {
|
||||
@@ -1018,6 +1019,7 @@ pub fn gn_enclose_over(
|
||||
} else {
|
||||
let contracts = q < 1.0;
|
||||
if !contracts {
|
||||
crate::profile::profile_note!(k, "enclose reject contraction q={q} limit=1");
|
||||
return Ok(None); // the span is too long for its Jacobian to be one preconditioner's
|
||||
}
|
||||
// the next box from the contraction: K's radii are at most d + |C| w for a box
|
||||
@@ -1053,8 +1055,10 @@ pub fn gn_enclose_over(
|
||||
}
|
||||
}
|
||||
if !proved {
|
||||
crate::profile::profile_note!(k, "enclose reject inclusion iterations={it} q={q}");
|
||||
return Ok(None);
|
||||
}
|
||||
crate::profile::profile_note!(k, "enclose root q={q} iterations={it}");
|
||||
|
||||
// the feet's Taylor coefficients over the span, order by order: x = -Y R + C x,
|
||||
// |x| <= rho
|
||||
@@ -1674,6 +1678,7 @@ pub fn arc_rows<S: TayScalar + GnScalar>(
|
||||
#[must_use]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn arc_row_terms_over(
|
||||
k: &Kernel,
|
||||
f: &[VSer<Ivl>; 2],
|
||||
centre: &VSer<Ivl>,
|
||||
fm: &[VSer<Ivl>; 2],
|
||||
@@ -1683,6 +1688,8 @@ pub fn arc_row_terms_over(
|
||||
rt: &mut GnRowTerms,
|
||||
touch: bool,
|
||||
) -> bool {
|
||||
#[cfg(not(feature = "profile"))]
|
||||
let _ = k;
|
||||
const K: usize = 4 + TAYLOR;
|
||||
let mut rho = [[Tay::<Ivl>::zero(); 3]; 2];
|
||||
let mut mc = [Tay::<Ivl>::zero(); 3];
|
||||
@@ -1725,6 +1732,7 @@ pub fn arc_row_terms_over(
|
||||
bd = bd + (r0q - mq) * dq;
|
||||
}
|
||||
if !dd.at(0).is_positive() {
|
||||
crate::profile::profile_note!(k, "arc reject speed2={:?} required_lo>0", dd.at(0));
|
||||
return false;
|
||||
}
|
||||
let kk = bd / dd;
|
||||
@@ -1734,6 +1742,26 @@ pub fn arc_row_terms_over(
|
||||
}
|
||||
let [rho0, rho1] = ρ
|
||||
let Some((h, w)) = arc_rows(rho0, rho1, &mc, o) else {
|
||||
#[cfg(feature = "profile")]
|
||||
{
|
||||
// Repeat only the failed positivity test for the diagnostic; never use
|
||||
// the observation to decide certification or alter its arithmetic.
|
||||
let mut b11 = Tay::<Ivl>::zero();
|
||||
let mut b22 = Tay::<Ivl>::zero();
|
||||
let mut b12 = Tay::<Ivl>::zero();
|
||||
for ((&r1, &r2), &mq) in rho0.iter().zip(rho1).zip(&mc) {
|
||||
let b1 = r1 - mq;
|
||||
let b2 = r2 - mq;
|
||||
b11 = b11 + b1 * b1;
|
||||
b22 = b22 + b2 * b2;
|
||||
b12 = b12 + b1 * b2;
|
||||
}
|
||||
crate::profile::profile_note!(k, "arc reject radii2={:?},{:?} required_lo>0", b11.at(0), b22.at(0));
|
||||
if b11.at(0).is_positive() && b22.at(0).is_positive() {
|
||||
let half = (Tay::<Ivl>::lift(1.0) + b12 / tay_sqrt(&(b11 * b22))) * Tay::<Ivl>::lift(0.5);
|
||||
crate::profile::profile_note!(k, "arc reject half={:?} required_lo>0", half.at(0));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// |S - O| <= |m - O| + the ball's radius over the piece
|
||||
@@ -1754,6 +1782,7 @@ pub fn arc_row_terms_over(
|
||||
let dx = 24.0 * crate::geom::vector3d::sqrt(sq);
|
||||
let dy = 24.0 * wj.at(TAY).mag();
|
||||
let dwlo = wj.at(0).lo;
|
||||
crate::profile::profile_note!(k, "arc row={j} x4={dx} w4={dy} w_lo={dwlo} radius={d_r}");
|
||||
if !(dx.is_finite() && dy.is_finite() && dwlo > 0.0) {
|
||||
return false;
|
||||
}
|
||||
@@ -1814,6 +1843,15 @@ pub fn gn_certify_span(
|
||||
mut defs: Option<&mut GnDefs<'_>>,
|
||||
) -> Result<f64> {
|
||||
crate::profile::profile_scope!(k, "blend::certificate::gn_certify_span");
|
||||
crate::profile::profile_note!(
|
||||
k,
|
||||
"span begin s=[{},{}] target={} n={} law={}",
|
||||
a.s,
|
||||
b.s,
|
||||
target,
|
||||
n,
|
||||
law.is_some()
|
||||
);
|
||||
// the series' orders are checked once (the fork leaves them to its callers)
|
||||
Trunc::new(4 + TAYLOR, n)?;
|
||||
let mut bound_out = f64::INFINITY;
|
||||
@@ -1848,6 +1886,7 @@ pub fn gn_certify_span(
|
||||
for level in 0..=6_u32 {
|
||||
k.check_cancelled(OP_CERTIFY_SPAN)?;
|
||||
let pieces = 1_usize << level;
|
||||
crate::profile::profile_note!(k, "level begin level={level} pieces={pieces}");
|
||||
// the pieces' end stations
|
||||
let mut ends = Vec::new();
|
||||
ends.push(*a);
|
||||
@@ -1889,6 +1928,7 @@ pub fn gn_certify_span(
|
||||
i += 1;
|
||||
}
|
||||
if !solved {
|
||||
crate::profile::profile_note!(k, "level reject station");
|
||||
continue;
|
||||
}
|
||||
ends.push(*b);
|
||||
@@ -1981,6 +2021,7 @@ pub fn gn_certify_span(
|
||||
Some(ps) => {
|
||||
let d = Ivl::new(ea.s, eb.s) - Ivl::from(ps.mid);
|
||||
arc_row_terms_over(
|
||||
k,
|
||||
&ps.f,
|
||||
&ps.v,
|
||||
&ps.fm,
|
||||
@@ -1996,6 +2037,10 @@ pub fn gn_certify_span(
|
||||
}
|
||||
}
|
||||
if !proved {
|
||||
crate::profile::profile_note!(
|
||||
k,
|
||||
"level reject enclosure_or_rows m4={m4:?} uv4={uv4:?}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// the level's bound with the uv curves' term `uv` (#2690: taken with each
|
||||
@@ -2034,6 +2079,11 @@ pub fn gn_certify_span(
|
||||
};
|
||||
let den = lj - kh * yj;
|
||||
let num = wj.hi * da + kh * xj + kh * yj * rows.r;
|
||||
crate::profile::profile_note!(
|
||||
k,
|
||||
"row bound row={j} den={den} w_lo={lj} kh_w4={} num={num}",
|
||||
kh * yj
|
||||
);
|
||||
let positive = den > 0.0;
|
||||
bound = if positive {
|
||||
cmax(bound, num / den)
|
||||
@@ -2049,6 +2099,7 @@ pub fn gn_certify_span(
|
||||
};
|
||||
let bound = level_bound(&uv4_fork);
|
||||
if !bound.is_finite() {
|
||||
crate::profile::profile_note!(k, "level reject nonfinite_bound");
|
||||
continue;
|
||||
}
|
||||
// certified by each parameter's own term, never larger than the fork's
|
||||
@@ -2060,6 +2111,10 @@ pub fn gn_certify_span(
|
||||
v.w = [cmin(lo, rlo), cmax(hi, rhi)];
|
||||
}
|
||||
}
|
||||
crate::profile::profile_note!(
|
||||
k,
|
||||
"level bound={bound} target={target} uv4={uv4:?} m4={m4:?}"
|
||||
);
|
||||
bound_out = cmin(bound_out, bound);
|
||||
if bound_out <= target {
|
||||
break;
|
||||
@@ -2076,6 +2131,7 @@ pub fn gn_certify_span(
|
||||
}
|
||||
lower = cmax(lower, cmax(if rows_vary { 0.0 } else { sum }, k4 * uvl));
|
||||
}
|
||||
crate::profile::profile_note!(k, "level lower={lower} target={target} last={last}");
|
||||
if round_down(lower * (1.0 - 1.0e-12)) > target {
|
||||
break;
|
||||
}
|
||||
@@ -2085,6 +2141,7 @@ pub fn gn_certify_span(
|
||||
}
|
||||
last = bound;
|
||||
}
|
||||
crate::profile::profile_note!(k, "span end bound={bound_out} target={target}");
|
||||
Ok(bound_out)
|
||||
}
|
||||
|
||||
|
||||
+67
@@ -74,7 +74,21 @@ macro_rules! profile_count {
|
||||
($k:expr, $counter:ident, $n:expr) => {};
|
||||
}
|
||||
|
||||
/// Records a diagnostic note only when explicitly captured by the profiler.
|
||||
#[cfg(feature = "profile")]
|
||||
macro_rules! profile_note {
|
||||
($k:expr, $($arg:tt)*) => {
|
||||
$k.profiler.note(|| format!($($arg)*));
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "profile"))]
|
||||
macro_rules! profile_note {
|
||||
($k:expr, $($arg:tt)*) => {};
|
||||
}
|
||||
|
||||
pub(crate) use profile_count;
|
||||
pub(crate) use profile_note;
|
||||
pub(crate) use profile_scope;
|
||||
|
||||
#[cfg(feature = "profile")]
|
||||
@@ -176,6 +190,7 @@ mod enabled {
|
||||
nodes: Vec<Node>,
|
||||
roots: Vec<u32>,
|
||||
stack: Vec<Frame>,
|
||||
notes: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -321,6 +336,30 @@ mod enabled {
|
||||
}
|
||||
|
||||
impl Profiler {
|
||||
/// Starts a fresh diagnostic capture. Normal profiles keep no per-call notes.
|
||||
/// At most 1,000,000 notes are retained, independently of the call-path profile.
|
||||
/// A final `diagnostic capture overflow` note makes truncation explicit.
|
||||
pub fn capture_notes(&self) {
|
||||
self.shared.lock().notes = Some(Vec::new());
|
||||
}
|
||||
|
||||
/// Ends diagnostic capture and returns its notes in execution order.
|
||||
#[must_use]
|
||||
pub fn take_notes(&self) -> Vec<String> {
|
||||
self.shared.lock().notes.take().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Adds a note, formatting it only during an explicit diagnostic capture.
|
||||
pub(crate) fn note(&self, make: impl FnOnce() -> String) {
|
||||
if let Some(notes) = self.shared.lock().notes.as_mut() {
|
||||
if notes.len() < 1_000_000 {
|
||||
notes.push(make());
|
||||
} else if notes.len() == 1_000_000 {
|
||||
notes.push("diagnostic capture overflow".to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a scope named `name` under the innermost open one; it closes when the
|
||||
/// returned guard drops. Prefer [`profile_scope!`](crate::profile), which keeps the
|
||||
/// guard to the end of the block.
|
||||
@@ -590,6 +629,34 @@ mod enabled {
|
||||
profile_count!(k, Evaluations, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostic_notes_are_opt_in_ordered_and_reset() {
|
||||
let k = Kernel::default();
|
||||
k.profiler.note(|| panic!("uncaptured notes must not be formatted"));
|
||||
assert!(k.profiler.take_notes().is_empty());
|
||||
k.profiler.capture_notes();
|
||||
k.profiler.note(|| "first".to_owned());
|
||||
k.profiler.clone().note(|| "second".to_owned());
|
||||
assert_eq!(k.profiler.take_notes(), ["first", "second"]);
|
||||
assert!(k.profiler.take_notes().is_empty());
|
||||
k.profiler.capture_notes();
|
||||
k.profiler.note(|| "discarded".to_owned());
|
||||
k.profiler.capture_notes();
|
||||
assert!(k.profiler.take_notes().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostic_overflow_is_explicit_and_bounded() {
|
||||
let p = super::Profiler::default();
|
||||
p.capture_notes();
|
||||
p.shared.lock().notes = Some(vec![String::new(); 1_000_000]);
|
||||
p.note(|| panic!("overflow must not format a note"));
|
||||
p.note(|| panic!("overflow must stay bounded"));
|
||||
let notes = p.take_notes();
|
||||
assert_eq!(notes.len(), 1_000_001);
|
||||
assert_eq!(notes.last().unwrap(), "diagnostic capture overflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scopes_aggregate_by_path_and_close_on_early_returns() {
|
||||
let mut k = Kernel::default();
|
||||
|
||||
+88
-6
@@ -518,11 +518,23 @@ impl LawRound {
|
||||
degree: usize,
|
||||
knots: &[f64],
|
||||
radii: &[f64],
|
||||
) -> Self {
|
||||
Self::build_on(&mut law_kernel(), size, from, to, degree, knots, radii)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_on(
|
||||
k: &mut Kernel,
|
||||
size: [f64; 3],
|
||||
from: Point3,
|
||||
to: Point3,
|
||||
degree: usize,
|
||||
knots: &[f64],
|
||||
radii: &[f64],
|
||||
) -> Self {
|
||||
use solidcore::blend::fillet_geom::p1::FilletSolverId;
|
||||
let mut k = law_kernel();
|
||||
let mut b = create_box(&mut k, Vec3::ZERO, size[0], size[1], size[2]).unwrap();
|
||||
let (e, reversed) = edge_between(&k, &b, from, to);
|
||||
let mut b = create_box(k, Vec3::ZERO, size[0], size[1], size[2]).unwrap();
|
||||
let (e, reversed) = edge_between(k, &b, from, to);
|
||||
let law = spline_law(degree, knots, radii, reversed);
|
||||
let base = radii[0].max(radii[radii.len() - 1]);
|
||||
let mut tols = SolverTols {
|
||||
@@ -531,11 +543,11 @@ impl LawRound {
|
||||
};
|
||||
let mut pieces = Vec::new();
|
||||
{
|
||||
let mut session = FilletSession::new(&mut k, &mut b).unwrap();
|
||||
let mut session = FilletSession::new(k, &mut b).unwrap();
|
||||
session
|
||||
.prepare_variable_radius_edges(&mut k, &[e], base, law, 1, 1)
|
||||
.prepare_variable_radius_edges(k, &[e], base, law, 1, 1)
|
||||
.unwrap();
|
||||
session.do_fillet(&mut k).unwrap();
|
||||
session.do_fillet(k).unwrap();
|
||||
for id in 0.. {
|
||||
let Ok(s) = session.solver(FilletSolverId(id)) else {
|
||||
break;
|
||||
@@ -1204,3 +1216,73 @@ fn law_peak() {
|
||||
assert_eq!(faces, 8);
|
||||
}
|
||||
}
|
||||
|
||||
/// Count the unmodified certificate on the ops law-corner test's four inputs, plus
|
||||
/// the existing two-piece (2, 4, 2) helper. No wall-clock value is an oracle.
|
||||
#[cfg(feature = "profile")]
|
||||
#[test]
|
||||
#[ignore = "opt-in per-span certificate diagnostic (#3078)"]
|
||||
fn variable_round_certificate_work() {
|
||||
let cases = [
|
||||
(
|
||||
"ops peak",
|
||||
[10.0, 20.0, 30.0],
|
||||
Point3::new(10.0, 0.0, 30.0),
|
||||
Point3::new(10.0, 20.0, 30.0),
|
||||
LINEAR,
|
||||
&[2.0, 4.0, 2.0][..],
|
||||
),
|
||||
(
|
||||
"ops dip",
|
||||
[20.0; 3],
|
||||
CUBE_FROM,
|
||||
CUBE_TO,
|
||||
LINEAR,
|
||||
&[4.0, 2.0, 4.0][..],
|
||||
),
|
||||
(
|
||||
"ops dip peak",
|
||||
[20.0; 3],
|
||||
CUBE_FROM,
|
||||
CUBE_TO,
|
||||
&[0.0, 0.0, 0.25, 0.75, 1.0, 1.0][..],
|
||||
&[4.0, 2.0, 4.0, 2.0][..],
|
||||
),
|
||||
(
|
||||
"ops dip peak reversed",
|
||||
[20.0; 3],
|
||||
CUBE_TO,
|
||||
CUBE_FROM,
|
||||
&[0.0, 0.0, 0.25, 0.75, 1.0, 1.0][..],
|
||||
&[4.0, 2.0, 4.0, 2.0][..],
|
||||
),
|
||||
];
|
||||
for (name, size, from, to, knots, radii) in cases {
|
||||
let mut k = law_kernel();
|
||||
k.profiler.capture_notes();
|
||||
let round = LawRound::build_on(&mut k, size, from, to, 1, knots, radii);
|
||||
assert!(!round.pieces.is_empty());
|
||||
eprintln!("CASE {name}");
|
||||
let notes = k.profiler.take_notes();
|
||||
assert!(notes.iter().any(|s| s.starts_with("span begin")));
|
||||
assert!(!notes.iter().any(|s| s == "diagnostic capture overflow"));
|
||||
let enclosure_calls = notes.iter().filter(|s| s.starts_with("enclose s=")).count() as u64;
|
||||
for note in notes {
|
||||
eprintln!("{note}");
|
||||
}
|
||||
let profile = k.take_profile();
|
||||
assert_eq!(enclosure_calls, profile.summary.iter().find(|s| s.name == "blend::certificate::gn_enclose_over").unwrap().count);
|
||||
profile.write_jsonl(&mut std::io::stderr()).unwrap();
|
||||
}
|
||||
let k = gn_kernel();
|
||||
k.profiler.capture_notes();
|
||||
let (pieces, _) = law_corner_pieces(&k, [2.0, 4.0, 2.0]);
|
||||
assert_eq!(pieces.pieces.len(), 2);
|
||||
eprintln!("CASE helper peak");
|
||||
// This existing curve-only helper has no support faces/definition certificate.
|
||||
// Keep that zero-work control distinct from the certified executive inputs.
|
||||
assert!(k.profiler.take_notes().is_empty());
|
||||
let profile = k.take_profile();
|
||||
assert!(!profile.summary.iter().any(|s| s.name == "blend::certificate::gn_certify_span"));
|
||||
profile.write_jsonl(&mut std::io::stderr()).unwrap();
|
||||
}
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
# #3078: variable-round certificate work
|
||||
|
||||
Unit: blend.fillet_section. Diagnostic only; no certificate arithmetic, target,
|
||||
acceptance test, station solve, or fitted net is changed.
|
||||
|
||||
## Reproduction and evidence
|
||||
|
||||
`variable_round_certificate_work` in `wr685_law_corners.rs` is an opt-in,
|
||||
profile-feature test. It constructs the four box/edge/law inputs from
|
||||
`solidcore-ops/tests/blend.rs::a_variable_round_with_a_corner_in_its_law_is_its_balls_union`
|
||||
through the kernel executive, with all fixes enabled. Each construction uses a
|
||||
fresh kernel. It then runs the existing two-piece `(2, 4, 2)` curve-only helper.
|
||||
That helper invokes `CircularCrossSectionFsg::create_surface`, not a support-face
|
||||
procedural definition: it has **zero certificate spans and zero enclosures**.
|
||||
It is a zero-work control, not an example of level-zero certification.
|
||||
|
||||
Run the following on a build host (the operator's `myy-cargo` wrapper is required
|
||||
on the laptop):
|
||||
|
||||
```sh
|
||||
cargo test --profile goldens -p solidcore --features profile \
|
||||
--test wr685_law_corners variable_round_certificate_work -- --ignored --nocapture
|
||||
```
|
||||
|
||||
The profiler records notes only during an explicit capture. Without `profile`
|
||||
the instrumentation expands to nothing. A capture is bounded, with an explicit
|
||||
overflow marker; the measuring test rejects overflow and compares its enclosure
|
||||
notes to the independent call-path profiler total. The original 100,000-note
|
||||
capture truncated the two longer cases; those partial counts are not evidence
|
||||
for the completed table. The final capture uses a 1,000,000-note limit.
|
||||
|
||||
## Counts
|
||||
|
||||
The accompanying `blend.fillet_section--issue-3078-shorter-cases.csv` records
|
||||
every attempted span and every reached level for the **two complete shorter
|
||||
captures only**. The longer two captures were truncated and are deliberately
|
||||
excluded. The table includes rejected parent spans later bisected by the caller. `pieces` is the
|
||||
level's planned partition size, not the number necessarily visited: a failed
|
||||
first piece can end a level after just two enclosure calls. `enclosures` counts
|
||||
actual calls. Repeated `[s0,s1]` attempts are retained in execution order.
|
||||
`span_bound` is the best returned bound; an accepted attempt is one at or below
|
||||
its recorded target. The CSV retains the failed arc construction's interval inputs, minimum rational
|
||||
denominator and its numerator/weight terms, finite upper/lower bounds and excess
|
||||
over target, and maximum station/UV fourth-derivative bounds. The new diagnostic
|
||||
prints the circle's failed positivity interval directly, but that version has
|
||||
not returned a remote verdict. No sampling manufactures bounds.
|
||||
|
||||
| Input | Span attempts | Accepted / rejected parents | Enclosures | Last-level counts L3 / L4 / L5 / L6 |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| peak | 124 | 64 / 60 | 20,808 | 16 / 24 / 17 / 67 |
|
||||
| dip | 104 | 54 / 50 | 18,774 | 16 / 12 / 12 / 64 |
|
||||
| dip-peak | incomplete capture | unknown | profiler total 29,368 | unknown |
|
||||
| reversed dip-peak | incomplete capture | unknown | profiler total 29,198 | unknown |
|
||||
| curve-only helper | 0 | 0 / 0 | 0 | none |
|
||||
|
||||
The independent profiler totals for the longer inputs are complete, but the
|
||||
per-span notes are not. Do not use their truncated per-span counts. Peak level
|
||||
starts are `124,124,124,124,108,84,67`; dip's are
|
||||
`104,104,104,104,88,76,64` (L0 through L6). Peak/dip have 136/104 failed
|
||||
arc-row levels and 28/22 non-finite-bound levels. The extractor verifies every
|
||||
span has an end and enclosure counts equal the independent profiler total;
|
||||
1,399 level records cover all 228 shorter-case span attempts.
|
||||
|
||||
## What forces the work
|
||||
|
||||
The expensive refinement is observable without timings. In the first peak-case
|
||||
span `[-4.999899999999967, 0]`, target `4.000000000000001e-6`, levels 0 through 4
|
||||
fail arc-circle positivity after the first piece. Both station fourth-derivative
|
||||
bounds remain around `2.2e-29` to `2.5e-29`, not large geometric curvature.
|
||||
At level 5 all 32 pieces are enclosed (64 calls), but the middle homogeneous row
|
||||
has
|
||||
|
||||
```text
|
||||
w_lo = 0.5795065387500216
|
||||
(h^4/384) * w4 = 17.410143434327928
|
||||
w_lo - (h^4/384) * w4 = -16.830636895577907
|
||||
```
|
||||
|
||||
Level 6 encloses all 64 pieces (128 calls), yet its denominator is still
|
||||
`0.6519574090667212 - 0.7782375059267068 = -0.12628009685998565`.
|
||||
The returned bound is infinity; this one rejected parent already cost 202 calls.
|
||||
Halving that span gives `[-4.999899999999967, -2.4999499999999837]`:
|
||||
level 5's finite bound is `1.1381794509220917`, level 6's is
|
||||
`0.1959090577823233`, still about 48,977 times the original `4e-6` target.
|
||||
Its station and UV fourth bounds remain about `2.4e-29`.
|
||||
|
||||
This localizes the excessive refinement to the interval **touch-circle / rational
|
||||
arc-row composition**, not failure to prove the station root or a large rail
|
||||
fourth derivative. On the complete peak/dip traces every one of the 20,808 / 18,774
|
||||
root proofs succeeds in one Krawczyk iteration. Their contraction factors range
|
||||
from `1.24e-13` to `2.47e-13` / `9.00e-14` to `3.04e-13`, far below the rejection
|
||||
threshold of one. In `arc_row_terms_over`, each rail and the centre is first
|
||||
centred independently over the whole piece. The touch-circle projection, relative
|
||||
radius vectors, squared lengths, normalized dot product, square root, and
|
||||
homogeneous middle row then compose these dependent intervals. Their common
|
||||
motion and scale correlations are no longer available. Subdividing the enclosure
|
||||
domain reduces this overestimation, but the remainder multiplier is the original
|
||||
span's `h^4/384`, correctly not the piece's. The resulting excessive row fourth
|
||||
bounds force deeper enclosure levels and eventually smaller interpolation spans.
|
||||
|
||||
For the ideal plane-plane geometry with an affine centre and linear radius on
|
||||
one smooth law segment, the touch-circle radius vectors are fixed directions
|
||||
times the radius: their normalized dot product and weight are constant, and the
|
||||
homogeneous rows are affine. This explains the cancellation that the interval
|
||||
composition misses. The tiny *numerically enclosed* station fourth derivatives
|
||||
are not an algebraic proof that the stored definition's derivatives are zero;
|
||||
none is discarded. Law kinks already delimit smooth pieces, and the quoted
|
||||
failure occurs within a single piece, so removing or smoothing a kink is not a
|
||||
sound remedy. The target is unchanged and not intrinsically incompatible with
|
||||
the geometry; it exposes the arc-row overestimation.
|
||||
|
||||
## Scope and next proof obligation
|
||||
|
||||
No performance fix is included. The measured cause is repeated high-level
|
||||
certification work; this does not measure machine copies or establish their
|
||||
share of elapsed time. Series temporaries may make each enclosure expensive,
|
||||
but the add-of-scale/in-place work of #2728 is deliberately untouched.
|
||||
|
||||
A general fix needs a dependency-preserving enclosure of the relative circle
|
||||
vectors and rational rows, with a proof that all required derivative intervals
|
||||
and denominator lower bounds remain enclosing, including varying-speed centres
|
||||
and non-planar supports. An affine-only numerical-zero shortcut, sampled bound,
|
||||
smaller multiplier, relaxed target, or skipped positivity test would not supply
|
||||
that proof. The diagnostic is retained to compare every span before/after such
|
||||
a change. There are no claimed after-count reductions or golden movements.
|
||||
|
||||
## Validation
|
||||
|
||||
- Earlier remote job 31148 returned wrapper exit 0 with no log/copy-back; the
|
||||
operator confirmed it had been deliberately cancelled and nothing ran.
|
||||
- Remote job 31215, through `myy-cargo`: `cargo fmt --all` and the opt-in trace
|
||||
test passed (1 passed, 15 filtered out). That run used the original capture
|
||||
instrumentation at `44e13e89a17` plus formatting, not the subsequent larger
|
||||
capture / numeric-positivity diagnostics / profiler unit tests.
|
||||
- The longer captures in 31215 hit the old note limit. Shorter-case extraction
|
||||
and its completeness/count assertions pass (228 spans, 1,399 levels).
|
||||
- Remote job **31233**, name `sc-gpt-1107-141314-2-edit`, was submitted through
|
||||
`myy-cargo -t 120` for the complete trace and formatting. The local background
|
||||
waiter reached its maximum 7,200-second tool limit and was killed, with **no
|
||||
remote exit verdict and no copied-back files**. The Slurm job's state is
|
||||
unknown; it has not been cancelled or restarted. Do not resubmit into that
|
||||
build tree until the operator resolves its state. The wrapper reported its
|
||||
remote log as
|
||||
`/home/aapo/scratch/solidcore/logs/slurm-31233-gpt-1107-141314-2-edit.out`.
|
||||
- The latest instrumentation changes and added profile tests are therefore
|
||||
uncompiled/unverified. Its formatting has not returned either.
|
||||
- `git diff --check` passed locally. No cargo/rustc/build/test ran on the laptop.
|
||||
- `reports/issue-3078-validate.sh` records the remaining validation commands:
|
||||
section/profile lib tests, full ops blend binary, all selected variable/fillet
|
||||
integration binaries, fmt, workspace and profile clippy, wasm, panic-site
|
||||
check, and blend-unit goldens. **None of those gates has run on the latest
|
||||
source.** No golden movement/no-change comparison is claimed.
|
||||
|
||||
This is a partial investigation, not a ready performance-fix branch. Remaining
|
||||
work is the complete four-case per-span capture, direct numerical circle
|
||||
positivity diagnostics, compilation of the final instrumentation/tests, and all
|
||||
requested gates/goldens. No certificate-preserving optimization is claimed.
|
||||
File diff suppressed because it is too large
Load Diff
+83
@@ -0,0 +1,83 @@
|
||||
"""Extract every attempted span/level; verify counts against independent profiles."""
|
||||
import collections
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
rows = []
|
||||
case = None
|
||||
span = None
|
||||
level = None
|
||||
counts = collections.defaultdict(collections.Counter)
|
||||
profile_counts = {}
|
||||
number = r'[-+]?(?:\d*\.?\d+(?:[eE][-+]?\d+)?|inf|NaN)'
|
||||
|
||||
for text in Path(sys.argv[1]).read_text().splitlines():
|
||||
if text.startswith('CASE '):
|
||||
case = text[5:]
|
||||
elif text.startswith('span begin'):
|
||||
counts[case]['spans'] += 1
|
||||
s = re.match(r'span begin s=\[(.*?),(.*?)\] target=(\S+) n=(\d+) law=(\S+)', text)
|
||||
span = dict(case=case, span=counts[case]['spans'], s0=s[1], s1=s[2], target=s[3])
|
||||
elif text.startswith('level begin'):
|
||||
m = re.match(r'level begin level=(\d+) pieces=(\d+)', text)
|
||||
level = dict(span, level=m[1], pieces=m[2], enclosures=0, failure='', bound='', lower='', min_den='', span_bound='')
|
||||
rows.append(level)
|
||||
counts[case]['L'+m[1]] += 1
|
||||
elif text.startswith('enclose s='):
|
||||
level['enclosures'] += 1
|
||||
counts[case]['enclosures'] += 1
|
||||
elif text.startswith(('enclose reject', 'arc reject')):
|
||||
level['failure'] += text + '; '
|
||||
counts[case][text.split('=')[0].split(' radii2')[0].split(' half')[0]] += 1
|
||||
elif text.startswith('level reject'):
|
||||
level['failure'] += text.split(' m4=')[0] + '; '
|
||||
if ' m4=' in text:
|
||||
m4, uv4 = text.split(' m4=')[1].split(' uv4=')
|
||||
level['m4_max'] = max(map(float, re.findall(number, m4)))
|
||||
level['uv4_max'] = max(map(float, re.findall(number, uv4)))
|
||||
counts[case][text.split(' m4=')[0]] += 1
|
||||
elif text.startswith('row bound'):
|
||||
den = float(re.search(r'den=(\S+)', text)[1])
|
||||
if level['min_den'] == '' or den < level['min_den']:
|
||||
level['min_den'] = den
|
||||
level['worst_row'] = text
|
||||
elif text.startswith('level bound='):
|
||||
m = re.match(r'level bound=(\S+) target=\S+ uv4=(.*?) m4=(.*)', text)
|
||||
level['bound'] = m[1]
|
||||
level['bound_excess'] = float(m[1]) - float(span['target'])
|
||||
if level['bound_excess'] > 0:
|
||||
level['failure'] += 'upper bound > target; '
|
||||
level['uv4_max'] = max(map(float, re.findall(number, m[2])))
|
||||
level['m4_max'] = max(map(float, re.findall(number, m[3])))
|
||||
elif text.startswith('level lower='):
|
||||
level['lower'] = text.split()[1].split('=')[1]
|
||||
level['last'] = text.split()[-1].split('=')[1]
|
||||
elif text.startswith('span end'):
|
||||
bound = re.search(r'bound=(\S+)', text)[1]
|
||||
for row in reversed(rows):
|
||||
if row['case'] != case or row['span'] != span['span']:
|
||||
break
|
||||
row['span_bound'] = bound
|
||||
counts[case]['span_ends'] += 1
|
||||
counts[case]['accepted' if float(bound) <= float(span['target']) else 'rejected'] += 1
|
||||
counts[case]['last_L'+level['level']] += 1
|
||||
elif text.startswith('{"type":"summary"'):
|
||||
record = json.loads(text)
|
||||
if record['name'] == 'blend::certificate::gn_enclose_over':
|
||||
profile_counts[case] = record['count']
|
||||
elif text == 'diagnostic capture overflow':
|
||||
raise AssertionError('incomplete capture')
|
||||
|
||||
for case, c in counts.items():
|
||||
assert c['spans'] == c['span_ends'], (case, c)
|
||||
assert c['enclosures'] == profile_counts[case], (case, c, profile_counts)
|
||||
print(case, json.dumps(dict(c), sort_keys=True))
|
||||
fields = ['case', 'span', 's0', 's1', 'target', 'level', 'pieces', 'enclosures', 'failure', 'bound', 'bound_excess', 'lower', 'last', 'min_den', 'worst_row', 'm4_max', 'uv4_max', 'span_bound']
|
||||
with Path(sys.argv[2]).open('w') as out:
|
||||
writer = csv.DictWriter(out, fieldnames=fields)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
print('rows', len(rows))
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run one focused phase per myy-cargo job; never run builds on the laptop.
|
||||
set -uo pipefail
|
||||
export CARGO_BUILD_JOBS=2
|
||||
phase=${1:?use tests, integrations, gates, or goldens}
|
||||
mkdir -p reports/issue-3078-validation
|
||||
failed=0
|
||||
run() {
|
||||
local name=$1; shift
|
||||
printf 'BEGIN %s\n' "$name"
|
||||
nice -n 10 "$@" >"reports/issue-3078-validation/$name.log" 2>&1
|
||||
local rc=$?
|
||||
printf '%s exit=%s\n' "$name" "$rc" | tee -a "reports/issue-3078-validation/$phase-status.txt"
|
||||
tail -12 "reports/issue-3078-validation/$name.log"
|
||||
if [ "$rc" != 0 ]; then failed=1; fi
|
||||
}
|
||||
: >"reports/issue-3078-validation/$phase-status.txt"
|
||||
case "$phase" in
|
||||
tests)
|
||||
run section-lib cargo test --profile goldens -p solidcore --features profile --lib -- blend::fillet_section
|
||||
run profile-lib cargo test --profile goldens -p solidcore --features profile --lib -- profile::
|
||||
run ops-blend cargo test --profile goldens -p solidcore-ops --test blend
|
||||
;;
|
||||
integrations)
|
||||
args=()
|
||||
while IFS= read -r file; do
|
||||
args+=(--test "$(basename "$file" .rs)")
|
||||
done < <(grep -ilE 'variable|fillet' crates/solidcore/tests/*.rs | sort)
|
||||
printf '%s\n' "${args[@]}" > reports/issue-3078-validation/integration-targets.txt
|
||||
run integrations cargo test --profile goldens -p solidcore --no-fail-fast "${args[@]}" -- --test-threads=2
|
||||
;;
|
||||
gates)
|
||||
run fmt cargo fmt --all --check
|
||||
run clippy cargo clippy --workspace --all-targets -- -D warnings
|
||||
run profile-clippy cargo clippy -p solidcore --features profile --lib --test wr685_law_corners -- -D warnings
|
||||
run wasm cargo check -p solidcore --target wasm32-unknown-unknown
|
||||
;;
|
||||
goldens)
|
||||
run panic-sites python3 -I scripts/ci/check_panic_sites.py --base f4f70127afe
|
||||
run blend-goldens cargo run --profile goldens -p goldens -- --unit 'blend.*' --goldens
|
||||
;;
|
||||
*) printf 'Unknown phase: %s\n' "$phase" >&2; exit 2 ;;
|
||||
esac
|
||||
exit "$failed"
|
||||
Reference in New Issue
Block a user