fix: resolve P0 safety issues in ruvector-dither, thermorust, and exo-ai

- Replace debug_assert with assert for bits bounds in quantize functions
- Guard ChannelDither against 0 channels and invalid bits
- Handle non-finite beta/rate in Langevin/Poisson noise (return 0)
- Remove unused itertools dependency from thermorust
- Fix partial_cmp().unwrap() NaN panics across 7 exo-ai files
- Fix SystemTime unwrap() in transfer_crdt (use unwrap_or_default)
- Fix domain ID mismatch (exo_retrieval → exo-retrieval) in orchestrator
- Update tests to match corrected domain IDs

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
rUv 2026-02-27 16:12:45 +00:00
parent 658c762994
commit b4d2b7343f
14 changed files with 32 additions and 21 deletions

View file

@ -32,6 +32,8 @@ impl ChannelDither {
/// If the slice is not a multiple of `n_channels`, the remainder is
/// processed using channel 0.
pub fn quantize_batch(&mut self, activations: &mut [f32]) {
assert!(!self.channels.is_empty(), "ChannelDither must have >= 1 channel");
assert!(self.bits >= 2 && self.bits <= 31, "bits must be in [2, 31]");
let nc = self.channels.len();
let qmax = ((1u32 << (self.bits - 1)) - 1) as f32;
let lsb = 1.0 / qmax;

View file

@ -21,7 +21,7 @@ use crate::DitherSource;
/// ```
#[inline]
pub fn quantize_dithered(x: f32, bits: u32, eps: f32, source: &mut impl DitherSource) -> f32 {
debug_assert!(bits >= 1 && bits <= 31, "bits must be in [1, 31]");
assert!(bits >= 2 && bits <= 31, "bits must be in [2, 31]");
let qmax = ((1u32 << (bits - 1)) - 1) as f32;
let lsb = 1.0 / qmax;
let dither = source.next(eps * lsb);
@ -50,6 +50,7 @@ pub fn quantize_slice_dithered(
eps: f32,
source: &mut impl DitherSource,
) {
assert!(bits >= 2 && bits <= 31, "bits must be in [2, 31]");
let qmax = ((1u32 << (bits - 1)) - 1) as f32;
let lsb = 1.0 / qmax;
for x in xs.iter_mut() {
@ -64,7 +65,7 @@ pub fn quantize_slice_dithered(
/// Useful when you need the integer representation rather than a re-scaled float.
#[inline]
pub fn quantize_to_code(x: f32, bits: u32, eps: f32, source: &mut impl DitherSource) -> i32 {
debug_assert!(bits >= 1 && bits <= 31);
assert!(bits >= 2 && bits <= 31, "bits must be in [2, 31]");
let qmax = ((1u32 << (bits - 1)) - 1) as f32;
let lsb = 1.0 / qmax;
let dither = source.next(eps * lsb);

View file

@ -15,7 +15,6 @@ readme = "README.md"
[dependencies]
rand = { version = "0.8", features = ["small_rng"] }
rand_distr = "0.4"
itertools = "0.12"
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }

View file

@ -9,16 +9,23 @@ use rand_distr::{Distribution, Normal, Poisson};
/// the noise amplitude must be √(2kT) = √(2/β) in dimensionless units.
#[inline]
pub fn langevin_noise(beta: f32, rng: &mut impl Rng) -> f32 {
if beta <= 0.0 || !beta.is_finite() {
return 0.0;
}
let sigma = (2.0 / beta).sqrt();
Normal::new(0.0_f32, sigma)
.expect("sigma must be finite and positive")
.unwrap_or_else(|_| Normal::new(0.0_f32, 1e-6).unwrap())
.sample(rng)
}
/// Draw `n` independent Langevin noise samples.
pub fn langevin_noise_vec(beta: f32, n: usize, rng: &mut impl Rng) -> Vec<f32> {
if beta <= 0.0 || !beta.is_finite() {
return vec![0.0; n];
}
let sigma = (2.0 / beta).sqrt();
let dist = Normal::new(0.0_f32, sigma).expect("sigma must be finite");
let dist = Normal::new(0.0_f32, sigma)
.unwrap_or_else(|_| Normal::new(0.0_f32, 1e-6).unwrap());
(0..n).map(|_| dist.sample(rng)).collect()
}
@ -27,7 +34,10 @@ pub fn langevin_noise_vec(beta: f32, n: usize, rng: &mut impl Rng) -> Vec<f32> {
/// Returns the kick to add to a single activation (0.0 if no spike this step).
#[inline]
pub fn poisson_spike(rate: f64, kick: f32, rng: &mut impl Rng) -> f32 {
let dist = Poisson::new(rate).expect("rate must be > 0");
if rate <= 0.0 || !rate.is_finite() {
return 0.0;
}
let dist = Poisson::new(rate).unwrap_or_else(|_| Poisson::new(1e-6).unwrap());
let count = dist.sample(rng) as u64;
if count > 0 {
// Random sign

View file

@ -138,7 +138,6 @@ fn cold_system_stays_near_ground_state() {
#[test]
fn langevin_lowers_energy_on_average() {
use thermorust::energy::SoftSpin;
use thermorust::motifs::SoftSpinMotif;
let n = 8;
let mut motif = SoftSpinMotif::random(n, 1.0, 0.5, 13);

View file

@ -62,8 +62,8 @@ pub struct ExoTransferOrchestrator {
impl ExoTransferOrchestrator {
/// Create a new orchestrator.
pub fn new(_node_id: impl Into<String>) -> Self {
let src_id = DomainId("exo_retrieval".to_string());
let dst_id = DomainId("exo_graph".to_string());
let src_id = DomainId("exo-retrieval".to_string());
let dst_id = DomainId("exo-graph".to_string());
let mut engine = DomainExpansionEngine::new();
engine.register_domain(Box::new(ExoRetrievalDomain::new()));

View file

@ -60,8 +60,8 @@ fn test_full_transfer_pipeline_multi_cycle() {
// - CRDT should know both domain IDs.
let prior = orch.best_prior().expect("CRDT must hold a prior");
assert_eq!(prior.src_domain, "exo_retrieval");
assert_eq!(prior.dst_domain, "exo_graph");
assert_eq!(prior.src_domain, "exo-retrieval");
assert_eq!(prior.dst_domain, "exo-graph");
assert!(prior.improvement >= 0.0 && prior.improvement <= 1.0);
assert!(prior.confidence >= 0.0 && prior.confidence <= 1.0);
assert!(prior.cycle >= 1);
@ -111,7 +111,7 @@ fn test_crdt_prior_consistency() {
}
let prior = orch.best_prior().expect("prior must exist after 3 cycles");
assert_eq!(prior.src_domain, "exo_retrieval");
assert_eq!(prior.dst_domain, "exo_graph");
assert_eq!(prior.src_domain, "exo-retrieval");
assert_eq!(prior.dst_domain, "exo-graph");
assert!(prior.cycle >= 1 && prior.cycle <= 3);
}

View file

@ -260,7 +260,7 @@ impl SubstrateBackend for NeuromorphicBackend {
SearchResult { id, score, embedding: vec![] }
})
.collect();
results.sort_unstable_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
results.sort_unstable_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
results.truncate(k);
let _elapsed = t0.elapsed();
results

View file

@ -112,7 +112,7 @@ impl InterferenceState {
amplitude_im: im,
})
.collect();
measurements.sort_unstable_by(|a, b| b.probability.partial_cmp(&a.probability).unwrap());
measurements.sort_unstable_by(|a, b| b.probability.partial_cmp(&a.probability).unwrap_or(std::cmp::Ordering::Equal));
measurements.truncate(k);
measurements
}
@ -180,7 +180,7 @@ impl SubstrateBackend for QuantumStubBackend {
SearchResult { id: *id, score: score.max(0.0), embedding: pattern.clone() }
})
.collect();
results.sort_unstable_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
results.sort_unstable_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
results.truncate(k);
let _elapsed = t0.elapsed();
results

View file

@ -219,7 +219,7 @@ impl GenomicPatternStore {
results.sort_unstable_by(|a, b| {
b.weighted_score
.partial_cmp(&a.weighted_score)
.unwrap()
.unwrap_or(std::cmp::Ordering::Equal)
});
results.truncate(k);
results

View file

@ -149,7 +149,7 @@ impl ReasoningBank {
(t, sim)
})
.collect();
scored.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
scored.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scored.truncate(k);
scored.into_iter().map(|(t, _)| t).collect()
}

View file

@ -135,7 +135,7 @@ fn current_millis() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.unwrap_or_default()
.as_millis() as u64
}

View file

@ -214,7 +214,7 @@ impl SparseRipsComplex {
// Sort edges by weight (filtration order)
let mut sorted_edges: Vec<&SimplexEdge> = edges.iter().collect();
sorted_edges
.sort_unstable_by(|a, b| a.weight.partial_cmp(&b.weight).unwrap());
.sort_unstable_by(|a, b| a.weight.partial_cmp(&b.weight).unwrap_or(std::cmp::Ordering::Equal));
for edge in sorted_edges {
let pu = find(&mut parent, edge.u as usize);

View file

@ -135,7 +135,7 @@ impl QuantumDecayPool {
fn evict_weakest(&mut self) {
if let Some(idx) = self.patterns.iter()
.enumerate()
.min_by(|a, b| a.1.decoherence_score().partial_cmp(&b.1.decoherence_score()).unwrap())
.min_by(|a, b| a.1.decoherence_score().partial_cmp(&b.1.decoherence_score()).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
{
self.patterns.remove(idx);