Skip to content

Commit d04bf0b

Browse files
avrabeclaude
andcommitted
feat(network): window-splitting 802.1Qbv GCL synthesis baseline (REQ-TSN-SYNTH-QBV-SPLIT-BASE-001)
Pulls the worst-case-latency lever the single-window baseline (REQ-TSN-SYNTH-QBV-BASE-001) cannot: splitting a class's allocation into K evenly-spaced windows cuts its worst-case gate latency to (cycle−open)/K at the SAME bandwidth, so a port that single windows leave Oversubscribed (each latency-bound class needing a wide window to reach a low latency) becomes feasible. `synthesize_gcl_split` exploits the network-calculus identity that a K-way even split on cycle c is identical to a single window on cycle c/K (same rate, same latency = c/K − w): it synthesizes one round on c/K with the existing single-window search and replicates it K times, returning the SMALLEST feasible K (fewest windows — closest to a deployable GCL). K=1 reproduces synthesize_gcl byte-for-byte. The result self-certifies on the full multi-window schedule against the SOUND latency (REQ-TSN-SVC-MULTIWIN-001 / min_service_latency_ps) — the regime that makes splitting non-trivial. NC-level baseline: the model is guard-band-free, so max_windows_per_class caps the search off the rate-loss cliff a guard-band-aware model imposes (deployable-claim follow-up = new REQ-TSN-SYNTH-QBV-GUARDBAND-001). Precursor to the full dependency-aware REQ-TSN-SYNTH-QBV-001 (re-scoped to v0.22.0). Incremental synthesis (table-stakes vs RTaW), NOT the wedge. Oracle (4 tests): the exact oversubscribed set synth_rejects_oversubscribed_port rejects is made feasible + self-certifies + genuinely splits; a feasible demand stays K=1; max_windows=1 disables splitting; non-whole-ns / zero cycle rejected up front. 148 spar-network tests pass; mutants on the new code 12 caught / 1 unviable / 1 defensive-equivalent (the self-cert guard, matching synthesize_gcl's pattern); fmt(nightly)+clippy clean. Adds REQ-TSN-SYNTH-QBV-SPLIT-BASE-001 (implemented) + REQ-TSN-SYNTH-QBV-GUARDBAND-001 (proposed, v0.22) + TEST-TSN-SYNTH-QBV-SPLIT; REQ-TSN-SYNTH-QBV-001 re-scoped to v0.22.0 (the full dependency-aware paper). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8c039d2 commit d04bf0b

3 files changed

Lines changed: 328 additions & 10 deletions

File tree

artifacts/requirements.yaml

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3169,6 +3169,62 @@ artifacts:
31693169
fields:
31703170
release: v0.20.0
31713171

3172+
- id: REQ-TSN-SYNTH-QBV-SPLIT-BASE-001
3173+
type: requirement
3174+
title: "802.1Qbv window-splitting synthesis baseline — even K-split, smallest feasible K"
3175+
description: >
3176+
spar shall synthesize a window-SPLITTING 802.1Qbv gate-control list
3177+
that pulls the worst-case-latency lever the single-window baseline
3178+
(REQ-TSN-SYNTH-QBV-BASE-001) cannot: splitting a class's allocation
3179+
into K evenly-spaced windows cuts its worst-case gate latency to
3180+
(cycle − open)/K at the SAME bandwidth, so a port that single
3181+
windows leave Oversubscribed (each latency-bound class needing a
3182+
wide window to reach a low latency) becomes feasible.
3183+
`synthesize_gcl_split` uses the network-calculus identity that a
3184+
K-way even split on cycle c is identical to a single window on cycle
3185+
c/K (same rate, same latency = c/K − w): it synthesizes one round on
3186+
c/K with the existing single-window search and replicates it K
3187+
times, returning the SMALLEST feasible K (fewest windows). K=1
3188+
reproduces synthesize_gcl exactly; the result SELF-CERTIFIES on the
3189+
full multi-window schedule against the SOUND latency
3190+
(REQ-TSN-SVC-MULTIWIN-001 / min_service_latency_ps) — the
3191+
counterexample regime that makes this non-trivial. NC-level baseline:
3192+
the model is guard-band-free, so `max_windows_per_class` caps the
3193+
search off the rate-loss cliff a guard-band-aware model imposes
3194+
(deployable claim = REQ-TSN-SYNTH-QBV-GUARDBAND-001). Precursor to
3195+
the full dependency-aware REQ-TSN-SYNTH-QBV-001, mirroring
3196+
REQ-TSN-SYNTH-QBV-BASE-001 → REQ-TSN-SYNTH-QBV-001. Incremental
3197+
synthesis (table-stakes vs RTaW), NOT the PLP≪TFA wedge. [SOLID]
3198+
status: implemented
3199+
tags: [tsn, synthesis, qbv, tier2]
3200+
fields:
3201+
release: v0.21.0
3202+
3203+
- id: REQ-TSN-SYNTH-QBV-GUARDBAND-001
3204+
type: requirement
3205+
title: "802.1Qbv guard-band-aware window-splitting (deployable GCL claim)"
3206+
description: >
3207+
Follow-up to REQ-TSN-SYNTH-QBV-SPLIT-BASE-001, which synthesizes
3208+
split GCLs over a guard-band-FREE model (sound at the NC
3209+
service-curve level, but NOT a deployable-hardware claim — real
3210+
802.1Qbv loses ~K guard bands of transmit time to frame-straddle
3211+
protection, which the model does not see, so the baseline can
3212+
over-state available rate by (K−1)·g/cycle). spar should subtract
3213+
the per-window guard band g (≈ Max_Frame_Size / link_rate; readers
3214+
exist) from effective open time and add g to latency — using the
3215+
already-present structural hook in
3216+
`tas_residual_service_with_sync_error` (the ε-subtraction pattern) —
3217+
so the synthesizer's "meets deadline" claim becomes
3218+
deployment-sound and the breakeven K (self-starvation at
3219+
K = 1 + cycle·ρ/g) is enforced automatically by the oracle rejecting
3220+
rate-starving splits. Verified-economics: latency gain ~1/K vs rate
3221+
loss linear in K; rich to K≈10, catastrophic past ~50 for typical
3222+
automotive cycles/frames. [SOLID]
3223+
status: proposed
3224+
tags: [tsn, synthesis, qbv, guard-band, tier2]
3225+
fields:
3226+
release: v0.22.0
3227+
31723228
- id: REQ-TSN-SYNTH-QBV-001
31733229
type: requirement
31743230
title: "802.1Qbv GCL synthesis — dependency-aware, window-splitting, mixed-criticality"
@@ -3177,19 +3233,18 @@ artifacts:
31773233
(inverse of the existing TAS checking, REQ-TSN-002) via
31783234
dependency-aware priority adjustment with WINDOW SPLITTING
31793235
(arXiv:2407.00987, 2024): ~300 flows, mixed-criticality, +20.6%
3180-
success over SOTA. Builds on the single-window baseline
3181-
(REQ-TSN-SYNTH-QBV-BASE-001) by splitting a class's allocation into
3182-
multiple windows distributed around the cycle — the only lever that
3183-
lowers worst-case gate latency below cycle − duration for a fixed
3184-
bandwidth (with one window per class, window order alone is a
3185-
provable no-op; splitting/placement is the paper's actual
3186-
contribution). KILL-GATE: kill the AADL→TSN bridge if we cannot
3187-
articulate why our integrated architecture-to-timing story beats
3188-
RTaW's already-cert-accepted ARXML→TSN+NC. [SOLID]
3236+
success over SOTA. The even-split LEVER shipped as the precursor
3237+
REQ-TSN-SYNTH-QBV-SPLIT-BASE-001 (smallest feasible K, NC-level,
3238+
self-certified against the sound bound); this requirement is the
3239+
FULL contribution that remains — dependency-aware priority/placement
3240+
(not just even splits) and mixed-criticality across ~300 flows.
3241+
KILL-GATE: kill the AADL→TSN bridge if we cannot articulate why our
3242+
integrated architecture-to-timing story beats RTaW's already-cert-
3243+
accepted ARXML→TSN+NC. [SOLID]
31893244
status: proposed
31903245
tags: [tsn, synthesis, qbv, tier2]
31913246
fields:
3192-
release: v0.21.0
3247+
release: v0.22.0
31933248

31943249
- id: REQ-TSN-SYNTH-MILP-001
31953250
type: requirement

artifacts/verification.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2335,6 +2335,35 @@ artifacts:
23352335
- type: satisfies
23362336
target: REQ-TSN-SYNTH-QBV-BASE-001
23372337

2338+
- id: TEST-TSN-SYNTH-QBV-SPLIT
2339+
type: feature
2340+
title: 802.1Qbv window-splitting synthesis baseline — splits an oversubscribed port
2341+
description: >
2342+
Verifies synthesize_gcl_split (REQ-TSN-SYNTH-QBV-SPLIT-BASE-001), the
2343+
smallest-feasible-K even-split synthesizer. spar-network::tsn unit
2344+
tests cover: (1) THE LEVER — the exact two-class latency-bound demand
2345+
set that synth_rejects_oversubscribed_port REJECTS with single
2346+
windows is made FEASIBLE by splitting; the result self-certifies
2347+
(each class's delay_bound over the SOUND min_service_latency curve ≤
2348+
its deadline) and genuinely splits (≥ 2 windows per class);
2349+
(2) NO-NEEDLESS-SPLIT — a comfortably feasible demand returns K=1,
2350+
byte-identical to synthesize_gcl; (3) K-CAP — max_windows_per_class=1
2351+
disables splitting, so the oversubscribed set is rejected;
2352+
(4) INPUT VALIDATION — a non-whole-nanosecond cycle (and cycle 0) are
2353+
rejected up front as CycleNotWholeNanos, not silently mis-reported.
2354+
Self-certifying: the full multi-window schedule is re-checked through
2355+
the sound checker before return, so an unsound split fails
2356+
construction, not just the test.
2357+
fields:
2358+
method: automated-test
2359+
steps:
2360+
- run: "cargo test -p spar-network --lib -- split_"
2361+
status: passing
2362+
tags: [v0.21.0, network, tsn, synthesis, qbv, oracle]
2363+
links:
2364+
- type: satisfies
2365+
target: REQ-TSN-SYNTH-QBV-SPLIT-BASE-001
2366+
23382367
- id: TEST-TSN-EXPORT-YANG
23392368
type: feature
23402369
title: 802.1Qcw YANG/NETCONF export of a synthesized gate schedule

crates/spar-network/src/tsn.rs

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,6 +1530,116 @@ pub fn synthesize_gcl(
15301530
Ok(sched)
15311531
}
15321532

1533+
/// Synthesize a window-SPLITTING 802.1Qbv GCL (REQ-TSN-SYNTH-QBV-001) —
1534+
/// the worst-case-latency lever that [`synthesize_gcl`]'s single window
1535+
/// per class cannot pull. Splitting a class's allocation into K evenly
1536+
/// spaced windows cuts its worst-case gate latency to `(cycle − open)/K`
1537+
/// at the SAME bandwidth, so a port that single windows leave
1538+
/// `Oversubscribed` (each latency-bound class needing a wide window to
1539+
/// reach a low latency) can become feasible.
1540+
///
1541+
/// Key identity: a K-way even split on cycle `c` with per-round window `w`
1542+
/// is network-calculus-identical to a SINGLE window `w` on a cycle `c/K`
1543+
/// (same rate `R·w/(c/K)`, same latency `c/K − w`). So this tries
1544+
/// `K = 1, 2, …, max_windows_per_class`, synthesizes ONE round on `c/K`
1545+
/// with the existing single-window search, and replicates it K times. It
1546+
/// returns the SMALLEST feasible K (fewest windows — closest to a
1547+
/// deployable GCL); `K = 1` reproduces [`synthesize_gcl`] exactly. The
1548+
/// result self-certifies on the full multi-window schedule against the
1549+
/// SOUND latency [`GateSchedule::min_service_latency_ps`].
1550+
///
1551+
/// SCOPE: this is an NC-level service-curve baseline. The model does not
1552+
/// account for 802.1Qbv guard bands, so each extra window is "free" here;
1553+
/// `max_windows_per_class` is the cap that keeps the search off the
1554+
/// rate-loss cliff a guard-band-aware model would impose (the deployable
1555+
/// claim is the follow-up REQ-TSN-SYNTH-QBV-GUARDBAND-001). Not the
1556+
/// PLP≪TFA wedge — incremental synthesis, table-stakes against RTaW.
1557+
pub fn synthesize_gcl_split(
1558+
demands: &[ClassDemand],
1559+
cycle_ps: u64,
1560+
link_rate_bps: u64,
1561+
max_windows_per_class: u64,
1562+
) -> Result<GateSchedule, GclSynthError> {
1563+
if demands.is_empty() {
1564+
return Err(GclSynthError::NoDemands);
1565+
}
1566+
if cycle_ps == 0 || !cycle_ps.is_multiple_of(1_000) {
1567+
return Err(GclSynthError::CycleNotWholeNanos { cycle_ps });
1568+
}
1569+
let kcap = max_windows_per_class.max(1);
1570+
let mut last_oversub = GclSynthError::Oversubscribed {
1571+
required_ps: 0,
1572+
cycle_ps,
1573+
};
1574+
for k in 1..=kcap {
1575+
// The round cycle `cycle_ps / k` must be a whole number of
1576+
// nanoseconds, i.e. K must divide the cycle in ns — skip K otherwise.
1577+
if !cycle_ps.is_multiple_of(k.saturating_mul(1_000)) {
1578+
continue;
1579+
}
1580+
let round_cycle_ps = cycle_ps / k;
1581+
match synthesize_gcl(demands, round_cycle_ps, link_rate_bps) {
1582+
Ok(round) => {
1583+
// K == 1 is exactly the single-window baseline.
1584+
if k == 1 {
1585+
return Ok(round);
1586+
}
1587+
// Replicate the round K times across the full cycle.
1588+
let mut blob = String::new();
1589+
for r in 0..k {
1590+
let base = r * round_cycle_ps;
1591+
for w in &round.windows {
1592+
if !blob.is_empty() {
1593+
blob.push(';');
1594+
}
1595+
blob.push_str(&format!(
1596+
"{}:{}:0x{:02X}",
1597+
(base + w.offset_ps) / 1_000,
1598+
w.duration_ps / 1_000,
1599+
w.allowed_cos_mask
1600+
));
1601+
}
1602+
}
1603+
let sched = GateSchedule::parse(&blob).map_err(|e| {
1604+
GclSynthError::SelfCheck(format!("split re-parse failed for {:?}: {}", blob, e))
1605+
})?;
1606+
// Self-certify the FULL multi-window schedule against the
1607+
// SOUND latency — a failure here is a synthesizer bug, not
1608+
// an unsound schedule reaching the caller.
1609+
for d in demands {
1610+
let beta = tas_residual_service(&sched, d.cos, link_rate_bps);
1611+
match delay_bound(&d.arrival, &beta) {
1612+
Ok(delay) if delay <= d.deadline_ps => {}
1613+
Ok(delay) => {
1614+
return Err(GclSynthError::SelfCheck(format!(
1615+
"class {} delay {} ps exceeds deadline {} ps after {}-split",
1616+
d.cos.0, delay, d.deadline_ps, k
1617+
)));
1618+
}
1619+
Err(e) => {
1620+
return Err(GclSynthError::SelfCheck(format!(
1621+
"class {} unservable after {}-split: {:?}",
1622+
d.cos.0, k, e
1623+
)));
1624+
}
1625+
}
1626+
}
1627+
return Ok(sched);
1628+
}
1629+
// Oversubscribed shrinks as K grows for latency-bound classes —
1630+
// a finer split may fit. Keep the error and try the next K.
1631+
Err(e @ GclSynthError::Oversubscribed { .. }) => {
1632+
last_oversub = e;
1633+
continue;
1634+
}
1635+
// ClassInfeasible (can't be served even at rate=link, latency=0)
1636+
// and structural errors are K-independent — fail fast.
1637+
Err(other) => return Err(other),
1638+
}
1639+
}
1640+
Err(last_oversub)
1641+
}
1642+
15331643
/// Worst-case delay (ps) for `demand` if its class receives a single open
15341644
/// window of `dur_ns` in a `cycle_ns` cycle on a `link_rate_bps` link,
15351645
/// computed via the real TAS checker ([`tas_residual_service`] +
@@ -2618,6 +2728,130 @@ mod tests {
26182728
);
26192729
}
26202730

2731+
// ── REQ-TSN-SYNTH-QBV-001: window-splitting synthesis ────────────────
2732+
2733+
#[test]
2734+
fn split_fits_an_oversubscribed_latency_bound_port() {
2735+
// The exact demand set synth_rejects_oversubscribed_port rejects:
2736+
// two latency-bound classes that one window per class oversubscribes.
2737+
// Splitting cuts each class's worst-case latency at the SAME
2738+
// bandwidth, so a finer split fits where single windows did not.
2739+
let link = 1_000_000_000u64;
2740+
let cycle_ps = 10_000_000u64;
2741+
let demands = vec![
2742+
ClassDemand {
2743+
cos: cos(7),
2744+
arrival: ArrivalCurve::affine(125, 100_000_000),
2745+
deadline_ps: 5_500_000,
2746+
},
2747+
ClassDemand {
2748+
cos: cos(3),
2749+
arrival: ArrivalCurve::affine(125, 100_000_000),
2750+
deadline_ps: 5_500_000,
2751+
},
2752+
];
2753+
// One window per class oversubscribes (the QBV-BASE limit).
2754+
assert!(matches!(
2755+
synthesize_gcl(&demands, cycle_ps, link),
2756+
Err(GclSynthError::Oversubscribed { .. })
2757+
));
2758+
// Window-splitting finds a feasible schedule.
2759+
let sched = synthesize_gcl_split(&demands, cycle_ps, link, 8).expect("split feasible");
2760+
// SOUND self-check: each class meets its deadline under the exact
2761+
// min-service latency on the resulting multi-window schedule.
2762+
for d in &demands {
2763+
let beta = tas_residual_service(&sched, d.cos, link);
2764+
let delay = delay_bound(&d.arrival, &beta).expect("servable");
2765+
assert!(
2766+
delay <= d.deadline_ps,
2767+
"class {} delay {} > deadline {}",
2768+
d.cos.0,
2769+
delay,
2770+
d.deadline_ps
2771+
);
2772+
}
2773+
// It genuinely SPLIT — at least two windows for each class.
2774+
for d in &demands {
2775+
let n = sched
2776+
.windows
2777+
.iter()
2778+
.filter(|w| w.allowed_cos_mask & (1 << d.cos.0) != 0)
2779+
.count();
2780+
assert!(n >= 2, "class {} not split: {} window(s)", d.cos.0, n);
2781+
}
2782+
}
2783+
2784+
#[test]
2785+
fn split_uses_single_window_when_feasible() {
2786+
// A comfortably feasible demand needs no splitting: K=1, and the
2787+
// result is byte-identical to synthesize_gcl (no needless windows).
2788+
let link = 1_000_000_000u64;
2789+
let cycle_ps = 10_000_000u64;
2790+
let demands = vec![
2791+
ClassDemand {
2792+
cos: cos(7),
2793+
arrival: ArrivalCurve::affine(125, 100_000_000),
2794+
deadline_ps: 8_000_000,
2795+
},
2796+
ClassDemand {
2797+
cos: cos(3),
2798+
arrival: ArrivalCurve::affine(125, 100_000_000),
2799+
deadline_ps: 9_000_000,
2800+
},
2801+
];
2802+
let base = synthesize_gcl(&demands, cycle_ps, link).expect("feasible");
2803+
let split = synthesize_gcl_split(&demands, cycle_ps, link, 8).expect("feasible");
2804+
assert_eq!(
2805+
base.to_gcl_blob(),
2806+
split.to_gcl_blob(),
2807+
"K=1 must reproduce synthesize_gcl byte-for-byte"
2808+
);
2809+
}
2810+
2811+
#[test]
2812+
fn split_caps_at_max_windows() {
2813+
// With max_windows_per_class = 1, splitting is disabled, so the
2814+
// oversubscribed set is rejected (same as the single-window path).
2815+
let link = 1_000_000_000u64;
2816+
let cycle_ps = 10_000_000u64;
2817+
let demands = vec![
2818+
ClassDemand {
2819+
cos: cos(7),
2820+
arrival: ArrivalCurve::affine(125, 100_000_000),
2821+
deadline_ps: 5_500_000,
2822+
},
2823+
ClassDemand {
2824+
cos: cos(3),
2825+
arrival: ArrivalCurve::affine(125, 100_000_000),
2826+
deadline_ps: 5_500_000,
2827+
},
2828+
];
2829+
assert!(matches!(
2830+
synthesize_gcl_split(&demands, cycle_ps, link, 1),
2831+
Err(GclSynthError::Oversubscribed { .. })
2832+
));
2833+
}
2834+
2835+
#[test]
2836+
fn split_rejects_non_whole_nanosecond_cycle() {
2837+
// A non-whole-ns cycle is rejected up front (not silently skipped
2838+
// into a misleading Oversubscribed) — and cycle 0 likewise.
2839+
let link = 1_000_000_000u64;
2840+
let demands = vec![ClassDemand {
2841+
cos: cos(7),
2842+
arrival: ArrivalCurve::affine(125, 100_000_000),
2843+
deadline_ps: 5_000_000,
2844+
}];
2845+
assert!(matches!(
2846+
synthesize_gcl_split(&demands, 1_500, link, 8),
2847+
Err(GclSynthError::CycleNotWholeNanos { cycle_ps: 1_500 })
2848+
));
2849+
assert!(matches!(
2850+
synthesize_gcl_split(&demands, 0, link, 8),
2851+
Err(GclSynthError::CycleNotWholeNanos { .. })
2852+
));
2853+
}
2854+
26212855
#[test]
26222856
fn synth_input_validation() {
26232857
let link = 1_000_000_000u64;

0 commit comments

Comments
 (0)