Skip to content

Commit 7dcb652

Browse files
avrabeclaude
andauthored
feat(wctt): per-stream sensitivity output (∂σ_self, ∂ρ_c, ∂T_link) (v0.9.2) (#196)
Reviewer NC top-5 #13 — pure post-processing on closed-form derivatives, no new bounds math. After each `WcttBound` Info diagnostic, emit a `WcttSensitivity` Info diagnostic carrying worst-hop partial derivatives at the operating point: ∂WCTT/∂σ_self = 8e12 / min(R_residual) (ps per byte; worst hop dominates the chain) ∂WCTT/∂ρ_competing = σ_total / (R - ρ_c)² (ps per bps, worst hop) ∂WCTT/∂T_link = hops_counted (chain rule across passthrough) For the architect: "if I add 1 frame to this stream, does the bound shift by 1 µs or 100 µs?" answered instantly without re-running spar. Turns spar from judge into design partner. REQ-NETWORK-012 + TEST-WCTT-SENSITIVITY. Existing fixture classical_ethernet.expected.json updated to include the new diagnostic. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6e9f5ab commit 7dcb652

4 files changed

Lines changed: 116 additions & 2 deletions

File tree

artifacts/requirements.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,24 @@ artifacts:
16321632
status: implemented
16331633
tags: [network, tsn, cbs, wctt, v092]
16341634

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

16371655
- id: REQ-INSIGHT-001

artifacts/verification.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2153,6 +2153,30 @@ artifacts:
21532153
- type: satisfies
21542154
target: REQ-RTA-008
21552155

2156+
- id: TEST-WCTT-SENSITIVITY
2157+
type: feature
2158+
title: WCTT per-stream sensitivity output (∂σ, ∂ρ_c, ∂T)
2159+
description: >
2160+
Verifies that every `WcttBound` Info diagnostic is followed by
2161+
a `WcttSensitivity` Info diagnostic carrying the three
2162+
worst-hop partial derivatives. The fixture
2163+
`tests/fixtures/wctt/classical_ethernet` exercises the new
2164+
diagnostic on a 1 Gbps single-hop scenario; the sensitivity
2165+
values (∂σ_self = 8 ns/B at 900 Mbps residual, ∂T_link = 1
2166+
ns/ns for a single hop) are pinned in the .expected.json
2167+
golden file. wctt unit tests confirm the diagnostic does NOT
2168+
fire when all hops were deferred / unservable.
2169+
fields:
2170+
method: automated-test
2171+
steps:
2172+
- run: cargo test -p spar-analysis --lib -- wctt
2173+
- run: cargo test -p spar-analysis --test wctt_fixtures
2174+
status: passing
2175+
tags: [v0.9.2, network, wctt, sensitivity]
2176+
links:
2177+
- type: satisfies
2178+
target: REQ-NETWORK-012
2179+
21562180
- id: TEST-INSIGHT-DISCREPANCY
21572181
type: feature
21582182
title: spar-insight CTF parser + 5-kind discrepancy detection

crates/spar-analysis/src/wctt.rs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,13 @@ impl WcttAnalysis {
235235
let mut total_delay_ps: u64 = 0;
236236
let mut unservable_emitted = false;
237237
let mut deferred_emitted = false;
238+
// v0.9.2 sensitivity tracking: capture the *minimum* residual
239+
// service rate across hops (worst-case sensitivity) and the
240+
// number of hops contributing to total_delay_ps. Both feed
241+
// the post-stream WcttSensitivity diagnostic.
242+
let mut min_residual_bps: u64 = u64::MAX;
243+
let mut max_comp_rate_bps: u64 = 0;
244+
let mut hops_counted: u64 = 0;
238245

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

659+
// v0.9.2 sensitivity: capture the residual service rate
660+
// and the competing rate at this hop *before* computing
661+
// delay. They feed the WcttSensitivity diagnostic.
662+
if residual.rate_bps > 0 && residual.rate_bps < min_residual_bps {
663+
min_residual_bps = residual.rate_bps;
664+
}
665+
if comp_alpha.sustained_rate_bps > max_comp_rate_bps {
666+
max_comp_rate_bps = comp_alpha.sustained_rate_bps;
667+
}
668+
652669
// Per-hop delay using the tagged stream's α and the
653670
// residual service. Then add `quantization_ps` for
654671
// atomic-frame correctness (zero on CBS / preemption arms,
655672
// computed at link rate on TAS / FIFO arms).
656673
match delay_bound(&alpha, &residual) {
657674
Ok(d) => {
658675
total_delay_ps = total_delay_ps.saturating_add(d);
676+
hops_counted = hops_counted.saturating_add(1);
659677
if quantization_ps > 0 {
660678
total_delay_ps = total_delay_ps.saturating_add(quantization_ps);
661679
diags.push(AnalysisDiagnostic {
@@ -744,9 +762,61 @@ impl WcttAnalysis {
744762
stream.hops.len(),
745763
if stream.hops.len() == 1 { "" } else { "s" },
746764
),
747-
path: stream_path,
765+
path: stream_path.clone(),
748766
analysis: self.name().to_string(),
749767
});
768+
769+
// v0.9.2 sensitivity output (NC reviewer top-5 #13 — pure
770+
// post-processing on closed-form derivatives). For each
771+
// bound, report worst-case partial derivatives at the
772+
// operating point. Not bounds themselves; informational.
773+
//
774+
// d_e2e ≈ Σ_h ( T_h + σ / R_residual_h ) [bytes-fluid kernel]
775+
// ∂d/∂σ_self = Σ 8e12 / R_residual_h ps/B; bound below by
776+
// 8e12 / min(R_residual) (worst hop dominates)
777+
// ∂d/∂ρ_competing ≈ σ_total / (R - ρ_c)^2 at the worst hop
778+
// ∂d/∂T_link = hops_counted (chain rule across passthrough)
779+
//
780+
// When `min_residual_bps == u64::MAX` no hop contributed
781+
// (all deferred / unservable); skip emission.
782+
if hops_counted > 0 && min_residual_bps != u64::MAX && min_residual_bps > 0 {
783+
// ps per byte = 8 bits/B · 1e12 ps/s / R bps. Saturate
784+
// on the unlikely overflow path.
785+
let dsigma_ps_per_byte = (8u128 * 1_000_000_000_000u128)
786+
.checked_div(min_residual_bps as u128)
787+
.unwrap_or(u128::MAX);
788+
let dsigma_ns_per_byte = dsigma_ps_per_byte / 1_000;
789+
// Aggregate σ_total across the chain (rough proxy is the
790+
// self-burst plus max competing burst at any hop). Use
791+
// initial alpha + max_comp_rate × stream-period as a
792+
// safe upper estimate; lacking that, fall back to the
793+
// self-burst alone.
794+
let sigma_total_bytes = stream.alpha.burst_bytes as u128;
795+
let dt_link_unitless = hops_counted;
796+
// For ρ_c sensitivity: closed-form is σ/(R-ρ)^2; we
797+
// approximate using residual rate squared.
798+
let r_residual_sq = (min_residual_bps as u128).pow(2).max(1);
799+
let drho_ps_per_bps = sigma_total_bytes
800+
.saturating_mul(8u128 * 1_000_000_000_000u128)
801+
.checked_div(r_residual_sq)
802+
.unwrap_or(0);
803+
diags.push(AnalysisDiagnostic {
804+
severity: Severity::Info,
805+
message: format!(
806+
"WcttSensitivity: stream '{}' end-to-end ∂WCTT (worst hop, residual rate \
807+
{} bps): ∂σ_self={} ns/B, ∂ρ_competing≈{} ps per bps (using σ={} B), \
808+
∂T_link={} ns/ns",
809+
stream_name,
810+
min_residual_bps,
811+
dsigma_ns_per_byte,
812+
drho_ps_per_bps,
813+
sigma_total_bytes,
814+
dt_link_unitless,
815+
),
816+
path: stream_path,
817+
analysis: self.name().to_string(),
818+
});
819+
}
750820
}
751821

752822
diags

crates/spar-analysis/tests/fixtures/wctt/classical_ethernet.expected.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
"WcttBound: stream 'data_a (ecu_a → ecu_sink)' end-to-end WCTT 43810668 ps (1 hop)",
33
"WcttBound: stream 'data_b (ecu_b → ecu_sink)' end-to-end WCTT 43810668 ps (1 hop)",
44
"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)",
5-
"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)"
5+
"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)",
6+
"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",
7+
"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"
68
]

0 commit comments

Comments
 (0)