Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions artifacts/requirements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1632,6 +1632,24 @@ artifacts:
status: implemented
tags: [network, tsn, cbs, wctt, v092]

- id: REQ-NETWORK-012
type: requirement
title: WCTT sensitivity output (∂σ_self, ∂ρ_competing, ∂T_link)
description: >
For each `WcttBound` Info diagnostic the `WcttAnalysis` pass also
emits a `WcttSensitivity` Info diagnostic carrying worst-hop
partial derivatives at the operating point: ∂WCTT/∂σ_self
(ps per byte of self-burst, dominated by the worst hop's
residual service rate), ∂WCTT/∂ρ_competing (ps per bps of
competing rate, σ/(R-ρ)² at the worst hop), and ∂WCTT/∂T_link
(chain-rule passthrough = number of hops). Pure post-processing
on the existing closed-form delay/output bounds — no new bounds
math, no impact on `WcttBound` numeric output. Per the
post-v0.9.0 reviewer's NC top-5 #13: cheapest workflow win,
turns spar from judge into design partner.
status: implemented
tags: [network, wctt, sensitivity, v092]

# ── Track G: spar-insight discrepancy assistant (v0.9.0) ──────────

- id: REQ-INSIGHT-001
Expand Down
24 changes: 24 additions & 0 deletions artifacts/verification.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2153,6 +2153,30 @@ artifacts:
- type: satisfies
target: REQ-RTA-008

- id: TEST-WCTT-SENSITIVITY
type: feature
title: WCTT per-stream sensitivity output (∂σ, ∂ρ_c, ∂T)
description: >
Verifies that every `WcttBound` Info diagnostic is followed by
a `WcttSensitivity` Info diagnostic carrying the three
worst-hop partial derivatives. The fixture
`tests/fixtures/wctt/classical_ethernet` exercises the new
diagnostic on a 1 Gbps single-hop scenario; the sensitivity
values (∂σ_self = 8 ns/B at 900 Mbps residual, ∂T_link = 1
ns/ns for a single hop) are pinned in the .expected.json
golden file. wctt unit tests confirm the diagnostic does NOT
fire when all hops were deferred / unservable.
fields:
method: automated-test
steps:
- run: cargo test -p spar-analysis --lib -- wctt
- run: cargo test -p spar-analysis --test wctt_fixtures
status: passing
tags: [v0.9.2, network, wctt, sensitivity]
links:
- type: satisfies
target: REQ-NETWORK-012

- id: TEST-INSIGHT-DISCREPANCY
type: feature
title: spar-insight CTF parser + 5-kind discrepancy detection
Expand Down
72 changes: 71 additions & 1 deletion crates/spar-analysis/src/wctt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,13 @@ impl WcttAnalysis {
let mut total_delay_ps: u64 = 0;
let mut unservable_emitted = false;
let mut deferred_emitted = false;
// v0.9.2 sensitivity tracking: capture the *minimum* residual
// service rate across hops (worst-case sensitivity) and the
// number of hops contributing to total_delay_ps. Both feed
// the post-stream WcttSensitivity diagnostic.
let mut min_residual_bps: u64 = u64::MAX;
let mut max_comp_rate_bps: u64 = 0;
let mut hops_counted: u64 = 0;

for (hop_idx, sw_idx) in stream.hops.iter().enumerate() {
let st = switch_type.get(sw_idx).copied().unwrap_or(SwitchType::Fifo);
Expand Down Expand Up @@ -649,13 +656,24 @@ impl WcttAnalysis {
}
};

// v0.9.2 sensitivity: capture the residual service rate
// and the competing rate at this hop *before* computing
// delay. They feed the WcttSensitivity diagnostic.
if residual.rate_bps > 0 && residual.rate_bps < min_residual_bps {
min_residual_bps = residual.rate_bps;
}
if comp_alpha.sustained_rate_bps > max_comp_rate_bps {
max_comp_rate_bps = comp_alpha.sustained_rate_bps;
}

// Per-hop delay using the tagged stream's α and the
// residual service. Then add `quantization_ps` for
// atomic-frame correctness (zero on CBS / preemption arms,
// computed at link rate on TAS / FIFO arms).
match delay_bound(&alpha, &residual) {
Ok(d) => {
total_delay_ps = total_delay_ps.saturating_add(d);
hops_counted = hops_counted.saturating_add(1);
if quantization_ps > 0 {
total_delay_ps = total_delay_ps.saturating_add(quantization_ps);
diags.push(AnalysisDiagnostic {
Expand Down Expand Up @@ -744,9 +762,61 @@ impl WcttAnalysis {
stream.hops.len(),
if stream.hops.len() == 1 { "" } else { "s" },
),
path: stream_path,
path: stream_path.clone(),
analysis: self.name().to_string(),
});

// v0.9.2 sensitivity output (NC reviewer top-5 #13 — pure
// post-processing on closed-form derivatives). For each
// bound, report worst-case partial derivatives at the
// operating point. Not bounds themselves; informational.
//
// d_e2e ≈ Σ_h ( T_h + σ / R_residual_h ) [bytes-fluid kernel]
// ∂d/∂σ_self = Σ 8e12 / R_residual_h ps/B; bound below by
// 8e12 / min(R_residual) (worst hop dominates)
// ∂d/∂ρ_competing ≈ σ_total / (R - ρ_c)^2 at the worst hop
// ∂d/∂T_link = hops_counted (chain rule across passthrough)
//
// When `min_residual_bps == u64::MAX` no hop contributed
// (all deferred / unservable); skip emission.
if hops_counted > 0 && min_residual_bps != u64::MAX && min_residual_bps > 0 {
// ps per byte = 8 bits/B · 1e12 ps/s / R bps. Saturate
// on the unlikely overflow path.
let dsigma_ps_per_byte = (8u128 * 1_000_000_000_000u128)
.checked_div(min_residual_bps as u128)
.unwrap_or(u128::MAX);
let dsigma_ns_per_byte = dsigma_ps_per_byte / 1_000;
// Aggregate σ_total across the chain (rough proxy is the
// self-burst plus max competing burst at any hop). Use
// initial alpha + max_comp_rate × stream-period as a
// safe upper estimate; lacking that, fall back to the
// self-burst alone.
let sigma_total_bytes = stream.alpha.burst_bytes as u128;
let dt_link_unitless = hops_counted;
// For ρ_c sensitivity: closed-form is σ/(R-ρ)^2; we
// approximate using residual rate squared.
let r_residual_sq = (min_residual_bps as u128).pow(2).max(1);
let drho_ps_per_bps = sigma_total_bytes
.saturating_mul(8u128 * 1_000_000_000_000u128)
.checked_div(r_residual_sq)
.unwrap_or(0);
diags.push(AnalysisDiagnostic {
severity: Severity::Info,
message: format!(
"WcttSensitivity: stream '{}' end-to-end ∂WCTT (worst hop, residual rate \
{} bps): ∂σ_self={} ns/B, ∂ρ_competing≈{} ps per bps (using σ={} B), \
∂T_link={} ns/ns",
stream_name,
min_residual_bps,
dsigma_ns_per_byte,
drho_ps_per_bps,
sigma_total_bytes,
dt_link_unitless,
),
path: stream_path,
analysis: self.name().to_string(),
});
}
}

diags
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
"WcttBound: stream 'data_a (ecu_a → ecu_sink)' end-to-end WCTT 43810668 ps (1 hop)",
"WcttBound: stream 'data_b (ecu_b → ecu_sink)' end-to-end WCTT 43810668 ps (1 hop)",
"WcttFrameQuantization: stream 'data_a (ecu_a → ecu_sink)' at hop 0 on switch 'sw': atomic-frame correction +12144 ns (max-frame serialization at link rate)",
"WcttFrameQuantization: stream 'data_b (ecu_b → ecu_sink)' at hop 0 on switch 'sw': atomic-frame correction +12144 ns (max-frame serialization at link rate)"
"WcttFrameQuantization: stream 'data_b (ecu_b → ecu_sink)' at hop 0 on switch 'sw': atomic-frame correction +12144 ns (max-frame serialization at link rate)",
"WcttSensitivity: stream 'data_a (ecu_a → ecu_sink)' end-to-end ∂WCTT (worst hop, residual rate 900000000 bps): ∂σ_self=8 ns/B, ∂ρ_competing≈0 ps per bps (using σ=1500 B), ∂T_link=1 ns/ns",
"WcttSensitivity: stream 'data_b (ecu_b → ecu_sink)' end-to-end ∂WCTT (worst hop, residual rate 900000000 bps): ∂σ_self=8 ns/B, ∂ρ_competing≈0 ps per bps (using σ=1500 B), ∂T_link=1 ns/ns"
]
Loading