use serde::{Deserialize, Serialize};
use crate::codebook::{Codebook, CodebookConfig};
use crate::error::TopologyError;
use crate::record::RecordSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapacityPoint {
pub capacity: usize,
pub used_codes: usize,
pub distinct_survivors: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CollapseReport {
pub points: Vec<CapacityPoint>,
pub peak_used_codes: usize,
pub collapsed: bool,
}
pub fn design_space_collapse(
records: &RecordSet,
capacities: &[usize],
base: &CodebookConfig,
) -> Result<CollapseReport, TopologyError> {
if capacities.is_empty() {
return Err(TopologyError::BadConfig {
field: "capacities",
expected: "at least one capacity to sweep",
found: "empty".into(),
});
}
let mut points = Vec::with_capacity(capacities.len());
for &capacity in capacities {
let book = Codebook::fit(
records,
&CodebookConfig {
capacity,
..base.clone()
},
)?;
points.push(CapacityPoint {
capacity,
used_codes: book.used_codes(),
distinct_survivors: book.distinct_survivors(),
});
}
let peak_used_codes = points.iter().map(|p| p.used_codes).max().unwrap_or(0);
let largest = points
.iter()
.max_by_key(|p| p.capacity)
.expect("non-empty capacities");
let collapsed = largest.used_codes * 2 <= largest.capacity;
Ok(CollapseReport {
points,
peak_used_codes,
collapsed,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(tag = "verdict", rename_all = "snake_case")]
pub enum CostSurrogateVerdict {
Inverted { r: f32 },
Aligned { r: f32 },
Uninformative,
}
pub fn edge_count_token_correlation(records: &RecordSet) -> CostSurrogateVerdict {
let xs: Vec<f64> = records
.records()
.iter()
.map(|r| r.topology.edge_count() as f64)
.collect();
let ys: Vec<f64> = records
.normalized_costs()
.iter()
.map(|&c| c as f64)
.collect();
match pearson(&xs, &ys) {
None => CostSurrogateVerdict::Uninformative,
Some(r) if r < 0.0 => CostSurrogateVerdict::Inverted { r: r as f32 },
Some(r) => CostSurrogateVerdict::Aligned { r: r as f32 },
}
}
fn pearson(xs: &[f64], ys: &[f64]) -> Option<f64> {
if xs.len() != ys.len() || xs.len() < 2 {
return None;
}
let n = xs.len() as f64;
let mx = xs.iter().sum::<f64>() / n;
let my = ys.iter().sum::<f64>() / n;
let mut cov = 0.0;
let mut vx = 0.0;
let mut vy = 0.0;
for (x, y) in xs.iter().zip(ys.iter()) {
let dx = x - mx;
let dy = y - my;
cov += dx * dy;
vx += dx * dx;
vy += dy * dy;
}
if vx <= f64::EPSILON || vy <= f64::EPSILON {
return None;
}
let r = cov / (vx.sqrt() * vy.sqrt());
r.is_finite().then_some(r)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Homogeneity {
Homogeneous,
Heterogeneous,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScorerAdvice {
pub homogeneity: Homogeneity,
pub message_passing_is_adjacency_blind: bool,
pub reason: String,
}
pub fn team_homogeneity(profiles: &[Vec<f32>], tolerance: f32) -> Homogeneity {
let Some(first) = profiles.first() else {
return Homogeneity::Homogeneous;
};
for profile in profiles.iter().skip(1) {
if profile.len() != first.len() {
return Homogeneity::Heterogeneous;
}
if profile
.iter()
.zip(first.iter())
.any(|(a, b)| (a - b).abs() > tolerance)
{
return Homogeneity::Heterogeneous;
}
}
Homogeneity::Homogeneous
}
pub fn scorer_advice(profiles: &[Vec<f32>], tolerance: f32) -> ScorerAdvice {
let homogeneity = team_homogeneity(profiles, tolerance);
match homogeneity {
Homogeneity::Homogeneous => ScorerAdvice {
homogeneity,
message_passing_is_adjacency_blind: true,
reason: format!(
"all {} agent profiles are identical, so message passing over profile nodes \
pools the same features for every candidate and cannot rank adjacencies; \
score on the flattened adjacency instead",
profiles.len()
),
},
Homogeneity::Heterogeneous => ScorerAdvice {
homogeneity,
message_passing_is_adjacency_blind: false,
reason: "agent profiles differ, so message passing over profile nodes can in \
principle distinguish adjacencies"
.into(),
},
}
}
pub const DEFAULT_CAPACITY_SWEEP: [usize; 5] = [4, 8, 16, 32, 64];
pub const PROTOCOL_TASKS: usize = 50;
pub const PROTOCOL_RECORDS: usize = PROTOCOL_TASKS * 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "sufficiency", rename_all = "snake_case")]
pub enum Sufficiency {
AtProtocol,
Thin { records: usize, tasks: usize },
Insufficient { records: usize, tasks: usize },
}
impl Sufficiency {
pub fn at_protocol(self) -> bool {
matches!(self, Sufficiency::AtProtocol)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DiagnosticsReport {
pub records: usize,
pub tasks: usize,
pub team_size: usize,
pub embedder: Option<String>,
pub sufficiency: Sufficiency,
pub collapse: CollapseReport,
pub cost_surrogate: CostSurrogateVerdict,
pub scorer: Option<ScorerAdvice>,
}
impl DiagnosticsReport {
pub fn headline(&self) -> String {
let collapse = if self.collapse.collapsed {
format!(
"the reward-surviving space stayed at {} codes as capacity grew to {}",
self.collapse.peak_used_codes,
self.collapse
.points
.iter()
.map(|p| p.capacity)
.max()
.unwrap_or(0)
)
} else {
format!(
"the reward-surviving space kept using capacity (peak {} codes)",
self.collapse.peak_used_codes
)
};
let surrogate = match self.cost_surrogate {
CostSurrogateVerdict::Inverted { r } => {
format!("edge count is an INVERTED cost surrogate (r = {r:.2})")
}
CostSurrogateVerdict::Aligned { r } => {
format!("edge count tracks measured tokens (r = {r:.2})")
}
CostSurrogateVerdict::Uninformative => {
"edge count vs tokens is uninformative here".to_string()
}
};
format!("{collapse}; {surrogate}")
}
pub fn render(&self) -> String {
let mut out = String::new();
out.push_str("topology diagnostics\n");
out.push_str(&format!(
" records: {} over {} tasks, team size {}{}\n",
self.records,
self.tasks,
self.team_size,
match &self.embedder {
Some(e) => format!(", embedder {e}"),
None => ", embedder unrecorded".to_string(),
}
));
out.push_str(&match self.sufficiency {
Sufficiency::AtProtocol => format!(
" sample: at or above the paper's protocol ({PROTOCOL_RECORDS} records over \
{PROTOCOL_TASKS} tasks)\n"
),
Sufficiency::Thin { records, tasks } => format!(
" sample: THIN — {records} records over {tasks} tasks, below the paper's \
protocol ({PROTOCOL_RECORDS} over {PROTOCOL_TASKS}); read the verdicts below \
as provisional\n"
),
Sufficiency::Insufficient { records, tasks } => format!(
" sample: INSUFFICIENT — {records} records over {tasks} tasks; a verdict below \
may rest on no variation at all\n"
),
});
out.push_str("\n [1] does the useful design space stay a short list?\n");
for point in &self.collapse.points {
out.push_str(&format!(
" capacity {:>3} -> {:>3} codes used\n",
point.capacity, point.used_codes
));
}
out.push_str(&format!(
" {} distinct reward-surviving topologies; {}\n",
self.collapse
.points
.first()
.map(|p| p.distinct_survivors)
.unwrap_or(0),
if self.collapse.collapsed {
"COLLAPSED — extra capacity went idle, so an index fits"
} else {
"NOT collapsed — capacity kept buying codes, so a short list may be the wrong \
model for this workload"
}
));
out.push_str("\n [2] is edge count an inverted cost surrogate?\n");
out.push_str(&match self.cost_surrogate {
CostSurrogateVerdict::Inverted { r } => format!(
" INVERTED (Pearson r = {r:.3}) — minimizing |E| raises measured tokens; \
rank on measured cost, never on structure\n"
),
CostSurrogateVerdict::Aligned { r } => format!(
" aligned (Pearson r = {r:.3}) — |E| tracks tokens here, though it is \
still a proxy for something you can measure directly\n"
),
CostSurrogateVerdict::Uninformative => " uninformative — edge count or token \
cost does not vary across these records\n"
.to_string(),
});
out.push_str("\n [3] can a profile-node message-passing scorer rank anything?\n");
out.push_str(&match &self.scorer {
Some(advice) => format!(" {}\n", advice.reason),
None => " NOT MEASURED — needs the team's agent profiles, which execution \
records do not carry. Pass them to diagnose_with_profiles, or use \
car_multi::topology::scorer_advice over the AgentSpecs.\n"
.to_string(),
});
out
}
}
pub fn diagnose(
records: &RecordSet,
capacities: &[usize],
base: &CodebookConfig,
) -> Result<DiagnosticsReport, TopologyError> {
diagnose_inner(records, capacities, base, None)
}
pub fn diagnose_with_profiles(
records: &RecordSet,
capacities: &[usize],
base: &CodebookConfig,
profiles: &[Vec<f32>],
tolerance: f32,
) -> Result<DiagnosticsReport, TopologyError> {
diagnose_inner(
records,
capacities,
base,
Some(scorer_advice(profiles, tolerance)),
)
}
fn diagnose_inner(
records: &RecordSet,
capacities: &[usize],
base: &CodebookConfig,
scorer: Option<ScorerAdvice>,
) -> Result<DiagnosticsReport, TopologyError> {
let sweep: Vec<usize> = if capacities.is_empty() {
DEFAULT_CAPACITY_SWEEP.to_vec()
} else {
capacities.to_vec()
};
let collapse = design_space_collapse(records, &sweep, base)?;
let cost_surrogate = edge_count_token_correlation(records);
let tasks = records.task_ids().len();
let count = records.len();
let sufficiency = if matches!(cost_surrogate, CostSurrogateVerdict::Uninformative)
|| collapse.peak_used_codes < 2
{
Sufficiency::Insufficient {
records: count,
tasks,
}
} else if count >= PROTOCOL_RECORDS && tasks >= PROTOCOL_TASKS {
Sufficiency::AtProtocol
} else {
Sufficiency::Thin {
records: count,
tasks,
}
};
Ok(DiagnosticsReport {
records: count,
tasks,
team_size: records.team_size(),
embedder: records.embedder().map(str::to_owned),
sufficiency,
collapse,
cost_surrogate,
scorer,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::ExecutionRecord;
use crate::topology::Topology;
fn records_with(topologies: Vec<(Topology, u64)>) -> RecordSet {
let mut out = Vec::new();
for i in 0..4 {
for (t, tokens) in &topologies {
out.push(ExecutionRecord::new(
format!("t{i}"),
vec![0.5, 0.5],
t.clone(),
1.0,
*tokens,
));
}
}
RecordSet::new(out).unwrap()
}
#[test]
fn collapse_report_shows_idle_capacity() {
let n = 4;
let records = records_with(
Topology::collection_protocol(n)
.unwrap()
.into_iter()
.map(|t| (t, 500))
.collect(),
);
let report =
design_space_collapse(&records, &[4, 8, 16, 32, 64], &CodebookConfig::default())
.unwrap();
assert_eq!(report.peak_used_codes, 6);
assert!(report.collapsed);
assert_eq!(report.points[0].used_codes, 4);
for point in &report.points[1..] {
assert_eq!(point.used_codes, 6, "capacity {} used more", point.capacity);
}
}
#[test]
fn a_wide_design_space_does_not_report_collapse() {
let n = 6;
let mut out = Vec::new();
for seed in 0..40u64 {
out.push(ExecutionRecord::new(
format!("t{seed}"),
vec![0.5, 0.5],
Topology::erdos_renyi(n, 0.5, seed + 1).unwrap(),
1.0,
500,
));
}
let records = RecordSet::new(out).unwrap();
let report =
design_space_collapse(&records, &[4, 8, 16, 32], &CodebookConfig::default()).unwrap();
assert!(!report.collapsed);
assert_eq!(report.peak_used_codes, 32);
}
#[test]
fn an_empty_capacity_sweep_is_rejected() {
let records = records_with(vec![(Topology::chain(4).unwrap(), 100)]);
assert!(matches!(
design_space_collapse(&records, &[], &CodebookConfig::default()),
Err(TopologyError::BadConfig { .. })
));
}
#[test]
fn the_inverted_surrogate_is_detected() {
let n = 4;
let records = records_with(vec![
(Topology::chain(n).unwrap(), 3000),
(Topology::star(n, 0).unwrap(), 1500),
(Topology::complete(n).unwrap(), 900),
]);
match edge_count_token_correlation(&records) {
CostSurrogateVerdict::Inverted { r } => assert!(r < -0.5, "r = {r}"),
other => panic!("expected Inverted, got {other:?}"),
}
}
#[test]
fn an_aligned_surrogate_is_reported_as_such() {
let n = 4;
let records = records_with(vec![
(Topology::chain(n).unwrap(), 500),
(Topology::star(n, 0).unwrap(), 1500),
(Topology::complete(n).unwrap(), 3000),
]);
assert!(matches!(
edge_count_token_correlation(&records),
CostSurrogateVerdict::Aligned { .. }
));
}
#[test]
fn a_constant_series_is_uninformative_not_a_nan() {
let n = 4;
let records = records_with(vec![(Topology::chain(n).unwrap(), 500)]);
assert_eq!(
edge_count_token_correlation(&records),
CostSurrogateVerdict::Uninformative
);
}
#[test]
fn identical_profiles_make_message_passing_adjacency_blind() {
let profile = vec![0.1, 0.2, 0.3];
let team = vec![profile.clone(), profile.clone(), profile.clone(), profile];
let advice = scorer_advice(&team, 1e-6);
assert_eq!(advice.homogeneity, Homogeneity::Homogeneous);
assert!(advice.message_passing_is_adjacency_blind);
assert!(advice.reason.contains("cannot rank"));
}
#[test]
fn differing_profiles_are_heterogeneous() {
let team = vec![vec![0.1, 0.2], vec![0.9, 0.8]];
let advice = scorer_advice(&team, 1e-6);
assert_eq!(advice.homogeneity, Homogeneity::Heterogeneous);
assert!(!advice.message_passing_is_adjacency_blind);
}
#[test]
fn tolerance_absorbs_encoder_noise_but_not_real_differences() {
let team = vec![vec![0.1, 0.2], vec![0.1 + 1e-8, 0.2 - 1e-8]];
assert_eq!(team_homogeneity(&team, 1e-6), Homogeneity::Homogeneous);
assert_eq!(team_homogeneity(&team, 1e-12), Homogeneity::Heterogeneous);
}
#[test]
fn mismatched_profile_lengths_are_heterogeneous() {
let team = vec![vec![0.1, 0.2], vec![0.1]];
assert_eq!(team_homogeneity(&team, 1e-6), Homogeneity::Heterogeneous);
}
#[test]
fn an_empty_team_is_homogeneous() {
assert_eq!(team_homogeneity(&[], 1e-6), Homogeneity::Homogeneous);
}
fn papers_regime() -> RecordSet {
let mut out = Vec::new();
for task in 0..10 {
for topology in Topology::collection_protocol(4).unwrap() {
let edges = topology.edge_count() as u64;
out.push(ExecutionRecord::new(
format!("t{task}"),
vec![task as f32 / 10.0, 1.0 - task as f32 / 10.0],
topology,
1.0,
3200u64.saturating_sub(180 * edges),
));
}
}
RecordSet::new(out).unwrap()
}
#[test]
fn the_report_folds_both_record_derived_claims() {
let report = diagnose(&papers_regime(), &[], &CodebookConfig::default()).unwrap();
assert!(report.collapse.collapsed);
assert!(matches!(
report.cost_surrogate,
CostSurrogateVerdict::Inverted { .. }
));
assert_eq!(report.records, 60);
assert_eq!(report.tasks, 10);
assert_eq!(report.team_size, 4);
}
#[test]
fn the_homogeneity_claim_is_absent_until_profiles_are_supplied() {
let records = papers_regime();
let bare = diagnose(&records, &[], &CodebookConfig::default()).unwrap();
assert!(
bare.scorer.is_none(),
"records do not carry profiles, so the claim must read as unmeasured"
);
assert!(bare.render().contains("NOT MEASURED"));
let profile = vec![0.2, 0.4];
let team = vec![profile.clone(); 4];
let full =
diagnose_with_profiles(&records, &[], &CodebookConfig::default(), &team, 1e-6).unwrap();
assert!(!full.render().contains("NOT MEASURED"));
let advice = full.scorer.expect("profiles were supplied");
assert!(advice.message_passing_is_adjacency_blind);
}
#[test]
fn the_report_carries_the_embedder_label_when_the_set_has_one() {
let records =
RecordSet::with_embedder(papers_regime().records().to_vec(), "mini-lm").unwrap();
let report = diagnose(&records, &[], &CodebookConfig::default()).unwrap();
assert_eq!(report.embedder.as_deref(), Some("mini-lm"));
assert!(report.render().contains("embedder mini-lm"));
let unlabeled = diagnose(&papers_regime(), &[], &CodebookConfig::default()).unwrap();
assert!(unlabeled.render().contains("embedder unrecorded"));
}
#[test]
fn a_thin_sample_is_flagged_rather_than_read_as_a_finding() {
let report = diagnose(&papers_regime(), &[], &CodebookConfig::default()).unwrap();
assert!(matches!(
report.sufficiency,
Sufficiency::Thin {
records: 60,
tasks: 10
}
));
assert!(!report.sufficiency.at_protocol());
assert!(report.render().contains("THIN"));
}
#[test]
fn a_protocol_sized_sample_is_not_flagged() {
let mut out = Vec::new();
for task in 0..PROTOCOL_TASKS {
for topology in Topology::collection_protocol(4).unwrap() {
let edges = topology.edge_count() as u64;
out.push(ExecutionRecord::new(
format!("t{task}"),
vec![task as f32 / PROTOCOL_TASKS as f32, 0.5],
topology,
1.0,
3200u64.saturating_sub(180 * edges),
));
}
}
let records = RecordSet::new(out).unwrap();
assert_eq!(records.len(), PROTOCOL_RECORDS);
let report = diagnose(&records, &[], &CodebookConfig::default()).unwrap();
assert!(report.sufficiency.at_protocol());
assert!(report.render().contains("at or above the paper's protocol"));
}
#[test]
fn a_sample_with_nothing_to_vary_over_is_insufficient_not_a_verdict() {
let records = records_with(vec![(Topology::chain(4).unwrap(), 500)]);
let report = diagnose(&records, &[], &CodebookConfig::default()).unwrap();
assert!(matches!(
report.sufficiency,
Sufficiency::Insufficient { .. }
));
assert_eq!(report.cost_surrogate, CostSurrogateVerdict::Uninformative);
assert!(report.render().contains("INSUFFICIENT"));
}
#[test]
fn the_headline_names_what_each_claim_found() {
let inverted = diagnose(&papers_regime(), &[], &CodebookConfig::default()).unwrap();
let headline = inverted.headline();
assert!(headline.contains("INVERTED"), "{headline}");
assert!(headline.contains("stayed at"), "{headline}");
let aligned = records_with(vec![
(Topology::chain(4).unwrap(), 500),
(Topology::star(4, 0).unwrap(), 1500),
(Topology::complete(4).unwrap(), 3000),
]);
let headline = diagnose(&aligned, &[], &CodebookConfig::default())
.unwrap()
.headline();
assert!(headline.contains("tracks measured tokens"), "{headline}");
}
#[test]
fn an_empty_capacity_sweep_falls_back_to_the_papers_own() {
let report = diagnose(&papers_regime(), &[], &CodebookConfig::default()).unwrap();
let swept: Vec<usize> = report.collapse.points.iter().map(|p| p.capacity).collect();
assert_eq!(swept, DEFAULT_CAPACITY_SWEEP.to_vec());
}
#[test]
fn the_rendered_report_has_no_mangled_line_continuations() {
let profile = vec![0.2, 0.4];
let team = vec![profile; 4];
let report = diagnose_with_profiles(
&papers_regime(),
&[],
&CodebookConfig::default(),
&team,
1e-6,
)
.unwrap();
let rendered = report.render();
for phrase in [
"below the paper's protocol (300 over 50); read the verdicts below as provisional",
"raises measured tokens; rank on measured cost, never on structure",
"pools the same features for every candidate and cannot rank adjacencies",
] {
assert!(rendered.contains(phrase), "missing or split: {phrase:?}");
}
for line in rendered.lines() {
if line.trim_start().starts_with("capacity ") {
continue;
}
assert!(
!line.trim_start().contains(" "),
"stray whitespace run in: {line:?}"
);
}
}
#[test]
fn every_verdict_variant_renders_without_a_stray_gap() {
let aligned = records_with(vec![
(Topology::chain(4).unwrap(), 500),
(Topology::star(4, 0).unwrap(), 1500),
(Topology::complete(4).unwrap(), 3000),
]);
let flat = records_with(vec![(Topology::chain(4).unwrap(), 500)]);
for records in [aligned, flat] {
let rendered = diagnose(&records, &[], &CodebookConfig::default())
.unwrap()
.render();
for line in rendered.lines() {
if line.trim_start().starts_with("capacity ") {
continue;
}
assert!(
!line.trim_start().contains(" "),
"stray whitespace run in: {line:?}"
);
}
}
}
#[test]
fn the_report_round_trips_through_json() {
let report = diagnose(&papers_regime(), &[], &CodebookConfig::default()).unwrap();
let json = serde_json::to_string(&report).unwrap();
let back: DiagnosticsReport = serde_json::from_str(&json).unwrap();
assert_eq!(report, back);
}
}