From 438cacad24b500585d084fa437dd99da5aaf074a Mon Sep 17 00:00:00 2001 From: lukassimon Date: Fri, 23 Oct 2026 21:14:39 +0000 Subject: [PATCH] Preserve typed native wavefunction precision failures --- Cargo.lock | 34 ++-- rust/crates/rusticol-core/Cargo.toml | 2 +- rust/crates/rusticol-core/src/error.rs | 10 ++ rust/loop-reduction/Cargo.toml | 22 +-- rust/loop-reduction/src/runtime/mod.rs | 15 +- rust/loop-reduction/src/runtime/states.rs | 180 +++++++++++++++++++++- 6 files changed, 229 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed14c0e71..f2dbbe74f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -819,7 +819,7 @@ dependencies = [ [[package]] name = "dot-parser" version = "0.5.1" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "either", "pest", @@ -945,7 +945,7 @@ dependencies = [ [[package]] name = "feynkit-amplitude" version = "0.1.0" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "feynkit-graph", "feynkit-model", @@ -959,7 +959,7 @@ dependencies = [ [[package]] name = "feynkit-generator" version = "0.1.0" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "bincode", "bincode-trait-derive", @@ -979,7 +979,7 @@ dependencies = [ [[package]] name = "feynkit-graph" version = "0.1.0" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "feynkit-kinematics", "feynkit-model", @@ -998,7 +998,7 @@ dependencies = [ [[package]] name = "feynkit-kinematics" version = "0.1.0" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "linnet", "numerica", @@ -1011,7 +1011,7 @@ dependencies = [ [[package]] name = "feynkit-model" version = "0.1.0" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "bincode", "bincode-trait-derive", @@ -1099,7 +1099,7 @@ dependencies = [ [[package]] name = "gammaloop-workspace-hack" version = "0.1.0" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "ahash", "arrayvec", @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "idenso" version = "0.3.0" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "ahash", "base64", @@ -1475,7 +1475,7 @@ dependencies = [ [[package]] name = "linnet" version = "0.17.0" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "ahash", "bincode", @@ -1645,7 +1645,7 @@ dependencies = [ [[package]] name = "ninjars" version = "0.1.0" -source = "git+https://github.com/alphal00p/ninjars.git?rev=de858c88e81058623d53a16fa4400b414f3aa260#de858c88e81058623d53a16fa4400b414f3aa260" +source = "git+https://github.com/alphal00p/ninjars.git?rev=a069504e3dc884d6e2682aadc75fb12a7f4e7c07#a069504e3dc884d6e2682aadc75fb12a7f4e7c07" dependencies = [ "feynkit-kinematics", "symbolica", @@ -1655,7 +1655,7 @@ dependencies = [ [[package]] name = "ninjars-graph" version = "0.1.0" -source = "git+https://github.com/alphal00p/ninjars.git?rev=de858c88e81058623d53a16fa4400b414f3aa260#de858c88e81058623d53a16fa4400b414f3aa260" +source = "git+https://github.com/alphal00p/ninjars.git?rev=a069504e3dc884d6e2682aadc75fb12a7f4e7c07#a069504e3dc884d6e2682aadc75fb12a7f4e7c07" dependencies = [ "feynkit-amplitude", "feynkit-generator", @@ -1674,7 +1674,7 @@ dependencies = [ [[package]] name = "ninjars-positional" version = "0.1.0" -source = "git+https://github.com/alphal00p/ninjars.git?rev=de858c88e81058623d53a16fa4400b414f3aa260#de858c88e81058623d53a16fa4400b414f3aa260" +source = "git+https://github.com/alphal00p/ninjars.git?rev=a069504e3dc884d6e2682aadc75fb12a7f4e7c07#a069504e3dc884d6e2682aadc75fb12a7f4e7c07" dependencies = [ "symbolica", "thiserror", @@ -1683,7 +1683,7 @@ dependencies = [ [[package]] name = "ninjars-symbolica" version = "0.1.0" -source = "git+https://github.com/alphal00p/ninjars.git?rev=de858c88e81058623d53a16fa4400b414f3aa260#de858c88e81058623d53a16fa4400b414f3aa260" +source = "git+https://github.com/alphal00p/ninjars.git?rev=a069504e3dc884d6e2682aadc75fb12a7f4e7c07#a069504e3dc884d6e2682aadc75fb12a7f4e7c07" dependencies = [ "ninjars", "ninjars-positional", @@ -2660,7 +2660,7 @@ checksum = "02ef091d4e8b981dbaa4cc17789d00b53d5d2c001ae689f14e6093f30c529036" [[package]] name = "spenso" version = "0.6.0" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "ahash", "append-only-vec", @@ -2703,7 +2703,7 @@ dependencies = [ [[package]] name = "spenso-hep-lib" version = "0.1.7" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "ahash", "idenso", @@ -2716,7 +2716,7 @@ dependencies = [ [[package]] name = "spenso-macros" version = "0.3.2" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "proc-macro2", "quote", @@ -2796,7 +2796,7 @@ dependencies = [ [[package]] name = "symbolica-utils" version = "0.1.0" -source = "git+https://github.com/alphal00p/gammaloop.git?rev=4b8569c03f7498b13482bc954953e66f6de5ffc1#4b8569c03f7498b13482bc954953e66f6de5ffc1" +source = "git+https://github.com/alphal00p/gammaloop.git?rev=ee18b8e6386cb36ca88b1917e00ab2ebae93e68e#ee18b8e6386cb36ca88b1917e00ab2ebae93e68e" dependencies = [ "bincode", "bincode-trait-derive", diff --git a/rust/crates/rusticol-core/Cargo.toml b/rust/crates/rusticol-core/Cargo.toml index 75149a244..09c26c288 100644 --- a/rust/crates/rusticol-core/Cargo.toml +++ b/rust/crates/rusticol-core/Cargo.toml @@ -29,7 +29,7 @@ loop-v2 = ["dep:ninjars", "symbolica-runtime"] bincode.workspace = true libloading = { version = "0.9", optional = true } libc.workspace = true -ninjars = { git = "https://github.com/alphal00p/ninjars.git", rev = "de858c88e81058623d53a16fa4400b414f3aa260", optional = true } +ninjars = { git = "https://github.com/alphal00p/ninjars.git", rev = "a069504e3dc884d6e2682aadc75fb12a7f4e7c07", optional = true } num-complex = { version = "0.4", optional = true } rayon.workspace = true serde.workspace = true diff --git a/rust/crates/rusticol-core/src/error.rs b/rust/crates/rusticol-core/src/error.rs index a472d255b..18de35063 100644 --- a/rust/crates/rusticol-core/src/error.rs +++ b/rust/crates/rusticol-core/src/error.rs @@ -17,6 +17,12 @@ pub enum RusticolErrorKind { /// exact program and physical inputs in a wider scalar may recover it. /// This category does not diagnose malformed caller-supplied values. NonFiniteResult, + /// A valid numerical operation lost information at this working precision. + /// Reconstruct the original inputs in a wider scalar before retrying; the + /// bit count describes the attempt, not a component's tracked precision. + InsufficientPrecision { + bits: u32, + }, Selector, ModelParameter, UnsupportedPrecision, @@ -77,6 +83,10 @@ impl RusticolError { Self::with_kind(RusticolErrorKind::NonFiniteResult, message) } + pub fn insufficient_precision(bits: u32, message: impl Into) -> Self { + Self::with_kind(RusticolErrorKind::InsufficientPrecision { bits }, message) + } + pub fn selector(message: impl Into) -> Self { Self::with_kind(RusticolErrorKind::Selector, message) } diff --git a/rust/loop-reduction/Cargo.toml b/rust/loop-reduction/Cargo.toml index 193149b26..1ba05ef78 100644 --- a/rust/loop-reduction/Cargo.toml +++ b/rust/loop-reduction/Cargo.toml @@ -9,18 +9,18 @@ license = "0BSD" publish = false [dependencies] -feynkit-amplitude = { git = "https://github.com/alphal00p/gammaloop.git", rev = "4b8569c03f7498b13482bc954953e66f6de5ffc1", default-features = false, features = ["native"] } -feynkit-generator = { git = "https://github.com/alphal00p/gammaloop.git", rev = "4b8569c03f7498b13482bc954953e66f6de5ffc1", default-features = false, features = ["native"] } -feynkit-graph = { git = "https://github.com/alphal00p/gammaloop.git", rev = "4b8569c03f7498b13482bc954953e66f6de5ffc1", default-features = false, features = ["native"] } -idenso = { git = "https://github.com/alphal00p/gammaloop.git", rev = "4b8569c03f7498b13482bc954953e66f6de5ffc1", default-features = false, features = ["native"] } -spenso = { git = "https://github.com/alphal00p/gammaloop.git", rev = "4b8569c03f7498b13482bc954953e66f6de5ffc1", default-features = false, features = ["native", "shadowing"] } -spenso-hep-lib = { git = "https://github.com/alphal00p/gammaloop.git", rev = "4b8569c03f7498b13482bc954953e66f6de5ffc1", default-features = false, features = ["native"] } -feynkit-model = { git = "https://github.com/alphal00p/gammaloop.git", rev = "4b8569c03f7498b13482bc954953e66f6de5ffc1", default-features = false, features = ["native"] } -feynkit-kinematics = { git = "https://github.com/alphal00p/gammaloop.git", rev = "4b8569c03f7498b13482bc954953e66f6de5ffc1", default-features = false, features = ["native"] } +feynkit-amplitude = { git = "https://github.com/alphal00p/gammaloop.git", rev = "ee18b8e6386cb36ca88b1917e00ab2ebae93e68e", default-features = false, features = ["native"] } +feynkit-generator = { git = "https://github.com/alphal00p/gammaloop.git", rev = "ee18b8e6386cb36ca88b1917e00ab2ebae93e68e", default-features = false, features = ["native"] } +feynkit-graph = { git = "https://github.com/alphal00p/gammaloop.git", rev = "ee18b8e6386cb36ca88b1917e00ab2ebae93e68e", default-features = false, features = ["native"] } +idenso = { git = "https://github.com/alphal00p/gammaloop.git", rev = "ee18b8e6386cb36ca88b1917e00ab2ebae93e68e", default-features = false, features = ["native"] } +spenso = { git = "https://github.com/alphal00p/gammaloop.git", rev = "ee18b8e6386cb36ca88b1917e00ab2ebae93e68e", default-features = false, features = ["native", "shadowing"] } +spenso-hep-lib = { git = "https://github.com/alphal00p/gammaloop.git", rev = "ee18b8e6386cb36ca88b1917e00ab2ebae93e68e", default-features = false, features = ["native"] } +feynkit-model = { git = "https://github.com/alphal00p/gammaloop.git", rev = "ee18b8e6386cb36ca88b1917e00ab2ebae93e68e", default-features = false, features = ["native"] } +feynkit-kinematics = { git = "https://github.com/alphal00p/gammaloop.git", rev = "ee18b8e6386cb36ca88b1917e00ab2ebae93e68e", default-features = false, features = ["native"] } rusticol-core = { path = "../crates/rusticol-core", features = ["symbolica-runtime", "python-generation-bridge", "loop-v2"] } -ninjars = { git = "https://github.com/alphal00p/ninjars.git", rev = "de858c88e81058623d53a16fa4400b414f3aa260" } -ninjars-graph = { git = "https://github.com/alphal00p/ninjars.git", rev = "de858c88e81058623d53a16fa4400b414f3aa260" } -ninjars-symbolica = { git = "https://github.com/alphal00p/ninjars.git", rev = "de858c88e81058623d53a16fa4400b414f3aa260", features = ["native-masters"] } +ninjars = { git = "https://github.com/alphal00p/ninjars.git", rev = "a069504e3dc884d6e2682aadc75fb12a7f4e7c07" } +ninjars-graph = { git = "https://github.com/alphal00p/ninjars.git", rev = "a069504e3dc884d6e2682aadc75fb12a7f4e7c07" } +ninjars-symbolica = { git = "https://github.com/alphal00p/ninjars.git", rev = "a069504e3dc884d6e2682aadc75fb12a7f4e7c07", features = ["native-masters"] } oneloop = { git = "https://github.com/alphal00p/oneloopmaster.git", rev = "8ec256fc26cb361b20e10f59c19beb35717ef32c", default-features = false } symbolica = { version = "3.0.1", default-features = false, features = ["integer-gmp", "float-mpfr"] } serde = { version = "1", features = ["derive"] } diff --git a/rust/loop-reduction/src/runtime/mod.rs b/rust/loop-reduction/src/runtime/mod.rs index 9fff02007..64dc50671 100644 --- a/rust/loop-reduction/src/runtime/mod.rs +++ b/rust/loop-reduction/src/runtime/mod.rs @@ -53,10 +53,21 @@ impl ExecutionError { self.0.kind() } fn nonfinite_result(value: impl Display) -> Self { - Self(rusticol_core::RusticolError::nonfinite_result(value.to_string())) + Self(rusticol_core::RusticolError::nonfinite_result( + value.to_string(), + )) + } + fn insufficient_precision(bits: u32, value: impl Display) -> Self { + Self(rusticol_core::RusticolError::insufficient_precision( + bits, + value.to_string(), + )) } fn context(self, value: impl Display) -> Self { - Self(rusticol_core::RusticolError::with_kind(self.kind(), value.to_string())) + Self(rusticol_core::RusticolError::with_kind( + self.kind(), + value.to_string(), + )) } } impl From for rusticol_core::RusticolError { diff --git a/rust/loop-reduction/src/runtime/states.rs b/rust/loop-reduction/src/runtime/states.rs index 95698722a..e3406c54a 100644 --- a/rust/loop-reduction/src/runtime/states.rs +++ b/rust/loop-reduction/src/runtime/states.rs @@ -52,6 +52,7 @@ fn state_components( p: &[Complex], helicity: Helicity, kind: WavefunctionKind, + working_bits: u32, ) -> ExecutionResult>> { if !p.iter().all(real_finite) { return Err(error("momenta must be finite and real")); @@ -61,6 +62,16 @@ fn state_components( .map(|wavefunction| wavefunction.into_components()) .map_err(|failure| match failure { WavefunctionError::NonFinite => ExecutionError::nonfinite_result(failure), + WavefunctionError::InsufficientPrecision { reason, .. } => { + // Input components may have lower tracked precision after prior + // arithmetic. Preserve the batch's original working precision. + ExecutionError::insufficient_precision( + working_bits, + format!( + "external-state chart needs more than {working_bits} working bits: {reason}" + ), + ) + } _ => error(failure), }) } @@ -80,7 +91,9 @@ fn evaluate( ); failure.context(message) }; - output.extend(state_components(p, helicity, kind).map_err(lane_error)?); + output.extend( + state_components(p, helicity, kind, input.precision_bits()).map_err(lane_error)?, + ); } Batch::typed( output, @@ -90,6 +103,11 @@ fn evaluate( ) } +/// Sparse successful rows and legacy textual status for every input row. +/// +/// Precision failures retain their lane identity and diagnostic text here, but +/// not their typed category or working-bit metadata. Call the strict batch API +/// when a caller needs machine-readable precision retry decisions. pub type PartialStates = (Batch, Vec<(u64, String)>); fn evaluate_partial( @@ -105,7 +123,7 @@ fn evaluate_partial( .chunks_exact(4) .zip(input.ids().iter().zip(specifications)) { - match state_components(p, helicity, kind) { + match state_components(p, helicity, kind, input.precision_bits()) { Ok(components) => { output.extend(components); ids.push(id); @@ -139,7 +157,8 @@ fn on_shell( .on_shell(Some(&mass.re)); if (&momentum).into_iter().any(|value| !value.is_finite()) { return Err(ExecutionError::nonfinite_result(format!( - "on-shell construction produced nonfinite momentum in row {}", input.ids()[row] + "on-shell construction produced nonfinite momentum in row {}", + input.ids()[row] ))); } momenta.extend( @@ -228,3 +247,158 @@ pub fn on_shell_wavefunctions_batch( _ => unreachable!("same_rows checked carriers"), } } + +#[cfg(test)] +mod tests { + use super::*; + use rusticol_core::{RusticolError, RusticolErrorKind}; + use symbolica::domains::float::{FloatLike, SingleFloat}; + + fn momentum(prototype: &T, mixed_scales: bool) -> Vec> { + let values = if mixed_scales { + let large = prototype.from_usize(2).pow(600); + [large.clone(), large.inv(), prototype.zero(), large] + } else { + let delta = prototype.from_usize(2).pow(30).inv(); + let square = delta.clone() * delta.clone(); + [ + prototype.one() + square.clone(), + prototype.from_usize(2) * delta, + prototype.zero(), + -prototype.one() + square, + ] + }; + values + .into_iter() + .map(|value| Complex::new(value, prototype.zero())) + .collect() + } + + #[test] + fn wavefunction_chart_errors_preserve_lane_identity_and_allow_fresh_reconstruction() { + for mixed_scales in [false, true] { + let mut low = [2., 0., 0., 2.] + .map(|value| Complex::new(value, 0.)) + .to_vec(); + low.extend(momentum(&0.0_f64, mixed_scales)); + let input = Batch::typed(low, vec![31, 907], 4, 53).unwrap(); + let error = wavefunctions_batch(&input, vec![1, 1], vec!["u".into(); 2], Some(4)) + .err() + .unwrap(); + assert_eq!( + error.kind(), + RusticolErrorKind::InsufficientPrecision { bits: 53 } + ); + assert!(error.to_string().contains("lane 907 (row 1)")); + let core: RusticolError = error.into(); + assert_eq!( + core.kind(), + RusticolErrorKind::InsufficientPrecision { bits: 53 } + ); + + // Recreate both rows from exact integer/power-of-two source inputs. + // The wider request never promotes the rounded binary64 momentum. + let bits = 137; + let prototype = Float::with_val(bits, 0); + let mut high = [2, 0, 0, 2] + .map(|value| Complex::new(prototype.from_i64(value), prototype.zero())) + .to_vec(); + high.extend(momentum(&prototype, mixed_scales)); + let input = Batch::typed(high, vec![31, 907], 4, bits).unwrap(); + let output = + wavefunctions_batch(&input, vec![1, 1], vec!["u".into(); 2], Some(4)).unwrap(); + assert_eq!(output.ids(), &[31, 907]); + assert_eq!(output.precision_bits(), bits); + assert!(output.nonfinite_rows().is_empty()); + let Data::Arbitrary(values) = output.data() else { + panic!("wrong carrier") + }; + assert!(!values[6].re.is_zero()); + assert!(!values[7].re.is_zero()); + if mixed_scales { + let vectors = + wavefunctions_batch(&input, vec![1, 1], vec!["epsilon".into(); 2], Some(4)) + .unwrap(); + let Data::Arbitrary(values) = vectors.data() else { + panic!("wrong carrier") + }; + assert!(!values[7].re.is_zero()); + } + } + } + + #[test] + fn wavefunction_precision_category_uses_working_bits_not_component_bits() { + let tracked = Float::with_val(53, 0); + let values = momentum(&tracked, false); + let physical = + PhysicalMomentum::from(std::array::from_fn::<_, 4, _>(|i| values[i].re.clone())); + assert!(matches!( + physical.wavefunction(WavefunctionKind::U, Helicity::PLUS), + Err(WavefunctionError::InsufficientPrecision { bits, .. }) if bits < 137 + )); + // The original attempt used 137 working bits; its stored intermediate + // components now have only 53 tracked bits. No precision is padded here. + let input = Batch::typed(values, vec![1021], 4, 137).unwrap(); + let failure = wavefunctions_batch(&input, vec![1], vec!["u".into()], Some(4)) + .err() + .unwrap(); + assert_eq!( + failure.kind(), + RusticolErrorKind::InsufficientPrecision { bits: 137 } + ); + assert!(failure.to_string().contains("lane 1021 (row 0)")); + } + + #[test] + fn partial_chart_failure_keeps_successful_rows_without_a_zero_placeholder() { + let mut values = [2., 0., 0., 2.] + .map(|value| Complex::new(value, 0.)) + .to_vec(); + values.extend(momentum(&0.0_f64, false)); + let input = Batch::typed(values, vec![31, 907], 4, 53).unwrap(); + let (successful, statuses) = + wavefunctions_partial_batch(&input, vec![1, 1], vec!["u".into(); 2], Some(4)).unwrap(); + assert_eq!(successful.ids(), &[31]); + assert_eq!(successful.rows(), 1); + assert_eq!(statuses[0], (31, "success".into())); + assert_eq!(statuses[1].0, 907); + assert!(statuses[1].1.contains("53 working bits")); + assert_ne!(statuses[1].1, "success"); + } + + #[test] + fn chart_precision_does_not_reclassify_nonfinite_results_or_bad_inputs() { + let make = |values: [f64; 4]| { + Batch::typed( + values.map(|value| Complex::new(value, 0.)).to_vec(), + vec![89], + 4, + 53, + ) + .unwrap() + }; + let overflow = make([1e308, 0., 0., 1e308]); + let failure = wavefunctions_batch(&overflow, vec![1], vec!["u".into()], Some(4)) + .err() + .unwrap(); + assert_eq!(failure.kind(), RusticolErrorKind::NonFiniteResult); + + let malformed = make([f64::INFINITY, 0., 0., 1.]); + let failure = wavefunctions_batch(&malformed, vec![1], vec!["u".into()], Some(4)) + .err() + .unwrap(); + assert_eq!(failure.kind(), RusticolErrorKind::Evaluation); + let ordinary = make([2., 0., 0., 2.]); + for (helicities, kinds) in [ + (vec![], vec!["u".into()]), + (vec![0], vec!["u".into()]), + (vec![1], vec!["unknown".into()]), + ] { + let failure = wavefunctions_batch(&ordinary, helicities, kinds, Some(4)) + .err() + .unwrap(); + assert_eq!(failure.kind(), RusticolErrorKind::Evaluation); + } + } +}