Benchmark metrics¶
What each column of every benchmark table means, and what it is not safe to conclude from it. Part of the benchmark index; the harness, build profiles and validation anchors are in methodology.md.
AntHocNet measured against the standard NS-3 MANET routing protocols
(AODV, OLSR, DSDV) on an identical scenario — same node layout,
mobility and traffic, driven from the same RNG runs so every protocol sees the
same realisations. Metrics come from an NS-3 FlowMonitor:
A fifth arm, the oracle control (#415), appears in the grid and satellite suites. It is a control, not a protocol — global-knowledge Dijkstra over the ground-truth topology — and it changes how two columns on this page must be read:
nrl/nrl_bytesare0.00/0.0000exactly, as an asserted invariant, not as an observation. The arm emits no control traffic at all, and that is enforced byNS_ABORTin both harnesses plus ascenario_checkrule — a non-zero value aborts the run rather than being reported. Do not read the zero as "measured very low overhead"; nothing was measured, a property was asserted and held (all 120 per-seed grid rows, min = max = 0.00).- Its
delay99is frequently not comparable with any other arm's, for the general reason indelay99is not comparable across arms with materially different PDR below — the oracle simply happens to be the arm that made the rule visible.
Its exactness limits (approx=0 vs approx=1) are a harness property, not a
metric one, and live in
methodology.md.
- PDR — packet-delivery ratio (received / sent), %, over the CBR data flows.
- mean delay — average end-to-end delay of delivered packets, ms.
- 99th-percentile delay — tail of the delivered-packet delay distribution.
Caveat (#57/#54): across protocols with very different PDR this is
survivorship-confounded — the extra packets a protocol delivers are precisely
the hard/late ones — so prefer the offered-load percentiles for cross-protocol
tail claims. The same confound appears within one protocol whenever an arm
changes what gets delivered, and it is not always labelled as a delivery
change — see
Survivorship within one protocol: capped arms.
And when the PDR difference is large enough, the two arms' 99th percentiles
stop measuring comparable quantiles altogether — the stronger,
no-interpretation-available case in
delay99is not comparable across arms with materially different PDR. - jitter (
jitter_ms) — mean delay jitter over delivered packets (FlowMonitorjitterSum/ (rx−1)), ms. Accumulates|delay_i − delay_{i−1}|, wheredelay = arrival − send. - jitterEq51 (
jitter_eq51_ms) — the thesis's delay jitter, eq 5.1, ms. This is a different quantity fromjitter_ms, not a variant of it (#89):
eq 5.1: Σ |(t_i − t_{i−1}) − (t_{i−1} − t_{i−2})| normalised by (n−2)
jitter_ms: Σ |delay_i − delay_{i−1}| normalised by (n−1)
Eq 5.1 contains no send times at all — it measures how far each
inter-arrival gap falls from the previous gap, where jitter_ms measures
how far each packet's delay falls from the previous packet's. Under a
perfectly periodic source the two differ by one further differencing step,
which for iid arrival noise inflates eq 5.1 by roughly √2 in scale. There is
no constant with which to convert one into the other.
Three properties of the source formula that the implementation had to decide and therefore records here:
- The thesis writes eq 5.1 as a bare sum, though every figure caption calls it "average delay jitter". A sum scales with packets delivered, so the worse-delivering protocol would score better arithmetically — the same survivorship trap the offered-load percentiles exist to dodge. We normalise.
- The normaliser is (n−2) per flow: the sum's first well-defined term is
at
i = 3. (The thesis writesi = 2but referencest_{i−2}— an off-by-one in the source.) Flows with fewer than 3 arrivals contribute nothing. - It is computed in arrival order, not sequence order. The thesis says "the time of arrival of the ith packet", so a reordered packet legitimately registers as jitter. Do not sort by sequence number to "fix" this; that would silently change the metric.
Which to cite: paper-parity claims ("reproduces the thesis's jitter
result") must cite jitter_eq51_ms. Cross-protocol comparisons may cite
either, as long as one is used consistently — both are measured identically
across all arms on identical realisations. A large divergence between the two
columns is itself informative: it says the arrival process is bursty in a way
the delay distribution alone does not show.
- dOff90 (delay_off50_ms/delay_off90_ms in the CSV) — delay at the
50th/90th percentile of offered (sent) packets, counting an undelivered
packet as infinite delay (inf / -1 in the CSV when less than that fraction
arrived). Monotone-honest: dropping hard packets cannot improve it.
- throughput — application bytes delivered per second, kbps.
- NRL — normalized routing load: routing-control packets transmitted (each
hop) per data packet delivered, counted uniformly at the IP layer.
What it counts is control traffic only — re-injected data frames are not in it. A packet the #46 MAC-failure detector re-injects is a data packet; it consumes airtime, hops and retries exactly like any other data transmission, and NRL's numerator never sees it (its denominator, deliveries, is where it shows up — and only if it arrives). Ants, hellos, repair and link-failure traffic are the numerator; nothing else is.
This is a reader trap with a measured size. On the
re-injection arm, capping re-injection cut postTx
— IP-layer hop transmissions of re-injected packets — by 48.20 % (rwp) /
49.61 % (gaussmarkov), and NRL moved by only −2.4 % / −4.8 %, with
channel occupancy down −1.4 / −2.0 pp (20.6 → 19.2 rwp, 21.0 → 19.0 gm).
Nothing is wrong with either number: roughly half the protocol's re-transmitted
data traffic disappeared and the routing-load column barely moved, because
that traffic was never in the column. Read a large postTx change against
##AIR## (airtime, which does see it) rather than against NRL, and do not
infer "the overhead saving did not happen" from a flat NRL.
- nrl_bytes — byte-normalized routing load: routing-control bytes
transmitted (each hop, same IP-layer counting point as NRL) per data byte
delivered. Complements packet-count NRL, which flatters protocols that send
fewer, larger messages — an AntHocNet ant carries its visited path (up to
maxPathLength addresses) while an AODV RREQ is small and fixed. The measured
packet-vs-byte comparison run is tracked in #132.
The metric families at a glance¶
Five families, each measuring a different thing, each with its own validity rules. What follows the diagram is the per-family detail; this is the map.
flowchart TB
RUN["one run<br/>(seed, protocol, scenario)"]
RUN --> HEAD["<b>headline</b><br/>pdr · delay · delay99 · thrput<br/>jitter + jitter_eq51 · nrl + nrl_bytes<br/>delay_off50/off90 (#57)"]
RUN --> DROP["<b>drop causes</b> (#215)<br/>route · queue · mac · chan · ttl<br/><i>identity: pdr + Σ ≈ 100</i>"]
RUN --> ENER["<b>energy</b> (#209)<br/>energy_j · energy_per_pkt_j<br/>energy_res_min/mean/sd_j · first_death_s"]
RUN --> REOR["<b>reordering</b> (#212, RFC 4737)<br/>ratio · extent_mean/max · <b>buf_max</b>"]
RUN --> ROUTEQ["<b>route quality</b> (#217)<br/>path_hops_mean/max · jain_pkts<br/>path_div_used/max · path_entropy_bits"]
RUN --> DISP["<b>dispersion</b> (#28)<br/>*_sd over runs · ##RUN## per-seed rows (#128)"]
DISP -.->|"feeds"| CI["95% CI · paired tests<br/>(#293, planned)"]
style HEAD fill:#e2f0ed,stroke:#0f7f70,stroke-width:2px
style DROP fill:#fff3d4,stroke:#c48f00
style ENER fill:#eef,stroke:#5b4fc4
style CI fill:#eee,stroke:#888,stroke-dasharray:4 3
The trap each family carries, stated once here and in detail below:
delay99 is survivorship-confounded across protocols (a protocol that drops
hard packets looks better) and, once the PDR gap is large, is
not comparable across arms at all;
drop_queue_pct misses DSDV's silent shedding
(#229); energy absolutes are idle-dominated so only deltas and spread are
readable; reorder ratio/extent measure route flapping rather than
multipath, leaving buf_max as the honest multipath signal; and
path_div_* is unquotable outside the dedicated cell (#230).
Matched-delivery delay99 (##MATCH##, #308, NS-3 only)¶
delay99 takes each protocol's tail over its own delivered set, and those
sets are not the same size. AntHocNet delivers ~10 pp more than AODV at the
paper base scenario, and some of that surplus is precisely the packets that
waited through a reconvergence. So an unknown part of AntHocNet's worse tail is
packets AODV never delivered at all, not slower service of the packets both
carry. That is the survivorship confound named above, and until #308 nothing
measured it.
Every anthocnet-compare run now also emits, per run and per protocol:
##MATCH## <run> <proto> <rxSelf> <rxMin> <delay99Ms> <delay99MatchedMs>
rxMin is the smallest delivered count across the protocols in that run;
delay99Matched re-reads each protocol's 99th percentile at that absolute
count — the delay below which 0.99 × rxMin of its packets arrived. For the
protocol that delivered fewest it equals its own delay99; for the others it
is the tail of their fastest rxMin packets. na means that protocol did not
deliver enough packets to reach the target (it cannot happen for the protocol
that set rxMin).
Read it in one direction only. If the gap persists after truncation, the delivery surplus cannot account for it — conclusive. If the gap closes, that is consistent with the surplus explaining the tail but does not establish it, because truncating the slowest packets assumes the surplus is the slow ones rather than showing it. A closing gap is grounds for per-packet attribution (#308 phase 1), not its answer.
It is emitted on its own ##MATCH## marker rather than as extra ##RUN##
columns, because the ##RUN## field order is consumed positionally by
bench_parse.py and the campaign scripts, and appending to it would silently
shift their mapping — the failure mode the # stddev cross-check exists to
catch (#293).
Common-set delay99 (##COMMON##, #308 phase 1)¶
##MATCH## bounds the confound; it cannot remove it, because truncating by
rank still assumes the surplus deliveries are the slowest ones. Keying by
packet identity removes the assumption:
##COMMON## <run> <proto> <nSelf> <nCommon> <p99Common> <meanCommon> <p99Surplus> <hopsCommon> <hopsSurplus>
nCommon is the number of (flow, seq) pairs every protocol in the run
delivered. p99Common / meanCommon are that protocol's tail and mean over
exactly those packets — same packets, same seeds, all delivered — so no
population difference remains to confound the comparison. This is the
assumption-free version of the matched measurement.
The precondition, which is not free (#431/#459/#460). "Same packets" holds
only if (flow, seq) names the same transmission in every arm — i.e. if the
application layer is constructed and seeded identically across arms, so
that the key's packet is sent at the same instant everywhere and crosses the
same topology. That is a property of the harness, not of the key, and it was
false until #459: anthocnet-compare drew the flow start times from a
cumulative RNG stream counter that the routing helpers had already advanced by
a protocol-dependent amount (#352), so each arm started every flow at a
different time. The matched set was therefore matched by index, not by
transmission, and under mobility the same key's packets crossed unrelated
topologies — measured at ~1.5 s mean separation with the 5 s default start
window, and expected near startWindow / 3 ≈ 60 s on the paper scenario that
every published grid cell uses. Numbers measured before #459 are inventoried in
460. When adding an arm or a helper, the rule this implies is: **anything the¶
application layer draws must come from a stream index that does not depend on
which routing protocol is under test** — in the harness today, the fixed
kAppStreamOffset sub-block.
p99Surplus is the tail over the packets this protocol delivered that some
other protocol did not. It tests the hypothesis ##MATCH## could only assume:
if a protocol's extra deliveries really are its slow ones, its surplus tail
sits far above its common tail. Reading the two together is the point — the
common tail says who is actually faster, the surplus tail says why the naive
comparison disagreed.
Per-packet delay comes from SeqTsSizeHeader's send timestamp, which the
reordering trace (#212) already peeks for the sequence number, so this costs
one subtraction per delivered packet.
hopsCommon / hopsSurplus (#308 phase 2) are the same two splits measured
in hops instead of milliseconds, from the IP TTL each delivered packet carried.
They exist because the obvious decomposition of the delay deficit — is it more
hops, or slower hops? — was not expressible without them. hopsMean (#217)
averages over each protocol's own deliveries while meanCommon averages
over the intersection, so meanCommon / hopsMean silently divides one
population by another: the same survivorship confound phase 1 was about,
reappearing in the denominator. meanCommon / hopsCommon is a genuine per-hop
cost over one population.
Both are appended at the end of the line, never inserted: the earlier fields are read positionally.
Worked example, from the phase-2 re-measure (run 31042812548, paper base, disk, 20 seeds) — using the all-delivered basis, which was the only one available then:
| factor | AntHocNet ÷ AODV |
|---|---|
| hop count | 1.247× |
| ms per hop | 1.35× |
| product | 1.68× (observed mean-delay ratio: 1.68×) |
The decomposition is internally consistent there because both terms use the same
all-delivered basis. What it cannot say is how the split looks on the packets
both protocols carried, where the delay ratio is 1.44× rather than 1.68×. That
is the question hopsCommon answers.
Survivorship within one protocol: capped arms (#386)¶
##MATCH##/##COMMON## exist for the cross-protocol form of the confound.
The re-injection cap sweep produced the within-protocol
form, and it is easier to miss because both arms are the same protocol at the
same commit, differing in one integer.
Capping MaxReinjectPerPacket improves every delay column — on rwp, mean delay
59.64 → 52.70 ms (−11.6 %) and delay99 920.0 → 853.5 ms at cap=1. It also
raises pktsNever, the share of re-injected keys never delivered at all, from
2.66 % to 13.41 %. Those two facts are the same fact: the packets the cap
removes from the delay distribution are exactly the slow ones that unlimited
re-injection eventually delivers, because what a cap discards is the
pktsDelivAfterOnly class (see the ##REINJ## packet view below).
A −11.6 % mean delay bought that way is not a latency improvement. Nothing got faster; a slow tenth of the population stopped being measured. The rule that follows is narrow and mechanical:
- Never quote a capped arm's
delayordelay99without itspktsNeveralongside — the same way a cross-protocol tail claim is not quoted without the PDRs. - Prefer the offered-load percentiles (
dOff50/dOff90), which count an undelivered packet as infinite and therefore cannot be improved by dropping it. They are monotone-honest across arms for the same reason they are across protocols. - The rule generalises past this sweep: any knob whose effect includes "and it delivers less" moves the delay columns for free, in the flattering direction.
delay99 is not comparable across arms with materially different PDR (#415)¶
The two sections above are about which packets each arm delivered: the
surplus is the slow tenth, so the arm that drops it looks faster. Both admit a
bounded reading — ##MATCH## truncates by rank, ##COMMON## keys by identity,
dOff50/dOff90 count a lost packet as infinite. Campaign phase 3 produced
the stronger case, where no correction to the naive number is available
because the two arms' 99th percentiles are not measuring the same thing at all.
The rule: delay99 must not be compared across arms whose PDR differs
materially. Not "adjust for the confound" — do not compare the column.
Why, in one line of arithmetic. delay99 is the 99th percentile of an
arm's own delivered set, so it is the 0.99 × PDR quantile of the
offered load. Two arms in the same cell therefore read the tail at two
different depths:
| arm (rwp-nakagami, grid) | PDR | delay99 measures the … |
reported delay99 |
|---|---|---|---|
| oracle | 99.54 % | 98.5th percentile of offered packets | 2010.5 ms |
| anthocnet | 89.78 % | 88.9th percentile of offered packets | 920.0 ms |
Nearly ten points of offered load separate the two measurement depths. The naive difference — "the oracle's tail is 1090 ms worse" — is a comparison of the 98.5th percentile of one distribution against the 88.9th of another.
The evidence that this is a cliff and not a distribution. The oracle's
delay99 on the three Nakagami grid cells is pinned at 2010.5 / 2010.8 /
2019.5 ms, with a per-seed standard deviation of only 2.3–3.4 ms across
20 seeds. A tail statistic that reproduces to a few milliseconds over twenty
independent mobility and fading realisations is not sampling a distribution; it
is landing on a step. At 99.4–99.6 % delivery the 99th percentile of the
delivered set sits exactly on the boundary between the arm's fast bulk and
the retry-driven tail that carries its last fraction of a percent — so the
statistic reports the location of that boundary, which barely moves between
seeds, rather than the shape of the tail. An arm at 89.8 % reads a point deep
inside its own bulk instead. The two numbers are computed by the same formula
and describe different phenomena.
Two things this is not:
- Not a 2 s cap or clamp. OLSR reaches 2685–2961 ms in the same three cells, so nothing in the harness truncates delays at ~2 s. The pinning is a property of where the quantile falls, not a ceiling.
- Not the #57/#54 survivorship confound, though it is its neighbour.
Survivorship says the compared populations differ; this says the compared
quantiles differ. A survivorship correction that equalises populations
(
##MATCH##,##COMMON##) fixes it; a caveat sentence does not.
The correct instrument is ##COMMON##'s p99Common (#308,
the p99C columns) — the 99th percentile over the (flow, seq) packets every
arm in the run delivered. One population, one quantile, one depth. On the same
rwp-nakagami cell it reads 586.8 ms (anthocnet) against 1469.1 ms (oracle),
and that pair is interpretable. Note that the conclusion here happens to
survive the correction — the oracle's fading tail really is worse, for the
approx=1 graph reason documented on
grid.md —
but it survives because the identity-matched instrument said so, not
because the naive column did. Reaching the right answer from an uninterpretable
statistic is not evidence that the statistic was interpretable.
Where ##COMMON## is unavailable, dOff50/dOff90 remain the monotone-honest
fallback: they are quantiles of the offered load by construction, so they
are read at the same depth for every arm regardless of what each delivered.
Operationally: publish delay99 per arm (it is the arm's own tail, and
that is a real thing to report), and never difference two arms' delay99
unless their PDRs are close — where "close" means small next to the
0.99 × PDR depth shift the difference implies. When they are not close,
difference p99Common instead and say which instrument produced the number.
Application goodput (##GOODPUT##, #63)¶
##GOODPUT## <run> <proto> <kbps>
Application bytes delivered per second, summed over the PacketSinks —
measured at the application, not derived from FlowMonitor.
Emitted on every run, not only TCP ones, and that is deliberate: on the UDP
arm it should track the thrput column closely, and the agreement is a free
cross-check that the metric is wired correctly. On TCP the two diverge, and the
gap between them is the retransmission overhead.
On a TCP cell this is the headline, and pdr is not. TCP retransmits until
it succeeds, so FlowMonitor's txPackets inflates while rxPackets counts each
delivery once: the ratio stops being a delivery fraction. Three columns change
meaning on a TCP cell and must not be compared with a UDP one:
| column | why it breaks under TCP |
|---|---|
pdr |
retransmissions inflate the denominator; it is no longer "fraction of offered data delivered" |
thrput |
FlowMonitor counts delivered IP bytes, so retransmitted segments count as throughput |
nrl |
control packets per delivered data packet — retransmissions inflate the denominator, which flatters whichever protocol reorders most, inverting the metric exactly where it matters |
The reorder columns are absent on a TCP cell rather than zero: RecordRxSeq
reads a SeqTsSizeHeader off the sink's Rx trace, which carries whole
datagrams on UDP but byte-stream chunks on TCP, so the hook is not connected at
all. Worth stating plainly — TCP is the arm where reordering matters most, and
it is the arm this instrumentation cannot measure.
Run provenance (##CONFIG##, #369)¶
##CONFIG## scenario=<name> nNodes=.. time=.. runs=.. firstRun=.. areaX=.. \
areaY=.. speed=.. pause=.. range=.. propagation=.. flows=.. \
cbrBps=.. rateManager=.. protocols=..
##CONFIG## attr <AttributeName>=<value> # one line per AntHocNet attribute
Not a metric — the configuration the numbers were produced under, emitted into the compact block ahead of every result row.
It exists because the alternative was tried and failed. The effective knobs
used to live only in the workflow's command echo, which sits roughly 900 lines
above the reachable log tail and disappears with the run: three hold-cap
ablation arms (#308)
were cancelled before producing output, their logs expired, and the
ReconvHoldCap/RepairHoldCap values they were dispatched with are
unrecoverable. A run that cannot state its own configuration is not
reproducible, however many seeds stand behind it.
The attr lines report each AntHocNet attribute at its effective value.
ns-3 routes --ns3::anthocnet::RoutingProtocol::X=Y through
Config::SetDefault, which rewrites the TypeId's stored initial value, so
reading it back reports overrides and compiled defaults alike — and a lever
added by a future sweep is carried automatically, with no change here.
Two limits worth stating:
- Fields are
key=value, not positional — unlike##RUN##. A misread position in a provenance row would misattribute an entire campaign, and there is no# stddevcross-check to catch it the way there is for the metric columns. - Baseline protocols' attributes are not dumped. Nothing in this repo
sweeps AODV/OLSR/DSDV attributes, so a
--ns3::aodv::…override would not be recorded. If that ever becomes a swept lever, this is the place to extend. - It does not record the commit. That is
##PROV##below, and the split is not an oversight — see there.
Drop-identity counters (##DROPID##, #377, NS-3 only, UDP only)¶
##DROPID## <seed> <proto> hopTx=.. hopRx=.. ackedHops=.. macDrops=.. \
reinjected=.. macTerminal=.. macLost=.. queue=.. hopLoss=.. \
overlap=.. unackedRx=.. [pqSilent=.. pqEcb=..]
Not a metric — the raw counters behind the drop-cause residual, one row per seed per protocol, so the identity in Where the drop-cause identity comes from can be audited in counts rather than in percentages.
The two bracketed tail fields appear only on the dsdv and aodv rows, where
the #229 pending-queue
conservation hook is connected (pqSilent = keys the protocol queue swallowed
with no observable signal; pqEcb = aodv queue drops re-attributed from the
route to the queue column). On the other arms the quantity is unmeasured, and
per the #382 rule absence — not zero — is the encoding.
drop_chan_pct is inferred, not measured — it is what is left of hopLoss
after the counted causes are removed — so it is only non-negative while those
causes are genuinely a subset of hopLoss. They were not, and the failure was
large: on every Nakagami cell the residual ran negative, to −13.10 % with 19.85
pp unaccounted (#377).
Why a retry-limit drop is not a lost packet¶
802.11 unicast is data → ACK: two transmissions, two independent chances to fail. The joint outcome that breaks the subset property is data delivered, ACK lost:
| event | hopTx |
hopRx |
macDrops |
|---|---|---|---|
| data arrives, surfaces at the peer's IP layer | +1 | +1 | — |
| ACK lost; sender retries; peer discards the duplicate | — | — | — |
| retries exhausted, sender gives up | — | — | +1 |
Contribution to hopLoss: zero. Contribution to macDrops: one. Each
occurrence drives the residual negative by one packet — and the packet was
delivered, so it is not a drop at all.
That is why the fields below exist, and why macLost rather than macDrops is
what the residual subtracts:
| field | definition | reads as |
|---|---|---|
unackedRx |
hopRx − ackedHops |
frames that reached the next hop's IP layer with no ACK recorded at the sender — the delivered-but-ACK-lost count |
macLost |
macDrops − unackedRx |
retry-limit drops where the frame genuinely did not arrive. A subset of hopLoss by construction, so the residual cannot go negative for this reason |
overlap |
macLost + queue − hopLoss |
Identically −drop_chan_pct · tx / 100. Now ≤ 0 by construction; a positive value means the books overlap for some other reason, which is why the gate on it is kept |
Which columns the correction touches. drop_chan_pct always subtracts
macLost. drop_mac_pct subtracts it only where that column is
macDrops-based — i.e. where nothing was re-injected. AntHocNet's
drop_mac_pct already reports macTerminal, with
#46 re-injection removed,
and correcting it a second time would double-count.
Measured at 900 s, 5 seeds, runs 31288485211 (Nakagami) and 31288489422 (two-ray):
| cell | unackedRx per run |
drop_chan_pct before → after |
identity sum after |
|---|---|---|---|
| nakagami / aodv | 1028 | −6.04 → +6.72 | 99.87 |
| nakagami / olsr | 1155 | −7.95 → +6.69 | 100.00 |
| nakagami / dsdv | 1112 | +3.62 → +17.50 | 100.22 |
| two-ray (all) | 0.2 – 8.8 | unchanged to ~0.1 pp | 99.91 – 100.02 |
The three orders of magnitude between the channels is the point: unackedRx is
a property of independent per-frame fading, so on two-ray — where reception is
a deterministic function of distance — the correction is a no-op, exactly as it
should be.
Closed for AntHocNet — the residue was re-injection (#377 → #386). Its
fading arm lands at sum ~101–103 (1.41 pp over on the #377 probe): the
per-hop books count a re-injected packet's extra copies — a packet whose hop
failed, was re-injected by #46 and then travelled on contributes additional
hopTx/macDrops entries while the identity is end-to-end and counts it once.
That straddle is expected and documented, not a books error: the #386
detector A/B measured the identity closing at exactly 100.00 on both
900 s Nakagami cells with EnableMacFailureDetector=false on identical seeds
(#386 comment 5234323092),
so the whole overshoot is the detector's re-injection traffic. ##REINJ##
below is the per-packet accounting of the same mechanism; the hop-level bound
(unackedRx vs reinjected, ≥ 58.8 % per run under fading) was its floor.
Absent under TCP, not zero. Every counter is gated on IsDataIp, which
tests for UDP on the data port, so a TCP cell would print all zeros — reading
as "no hops, no overlap, identity clean" when nothing was counted at all. Same
rule as ##HOLD##/##AIR##, and the same mistake #382 fixed for the reorder
columns. (Note that drop_mac_pct and drop_chan_pct are structurally zero on
a TCP cell for this same reason; that is a separate pre-existing gap, tracked
on #63, not something this marker introduces.)
scenario_check.py results WARNs when a cell carries ##DROPID## rows with
overlap > 0, naming both figures, so the condition is caught by the gate
rather than by a human noticing a negative percentage.
Re-injection identity & fate (##REINJ##, #386, NS-3 only, AntHocNet + UDP only)¶
##REINJ## <seed> anthocnet events=.. parsed=.. unparsedIcmp=.. \
unparsedOther=.. ofDelivered=.. pkts=.. \
pktsDelivBefore=.. pktsDelivAfterOnly=.. pktsNever=.. \
pktsDupDeliv=.. dupRx=.. postTx=.. postRx=.. \
l3DropRoute=.. l3DropTtl=.. l3DropOther=.. \
skips=.. skipsPkts=.. skipsDelivBefore=.. \
skipsDelivAfterOnly=.. skipsNever=..
The direct counter behind the ##DROPID## inclusion–exclusion floor: under
fading, ≥ 58.8 % of the #46
detector-D re-injections are of packets that had already arrived at the
destination (unackedRx vs reinjected, a lower bound). This row measures
that overlap per packet instead of bounding it, and follows what re-injected
packets then do. One row per seed, AntHocNet arm only.
The bound has since been superseded by this counter. At 20 seeds the direct
rate is ofDelivered/events = 0.6519 [0.6405, 0.6633] (rwp) and 0.6683
[0.6536, 0.6830] (gaussmarkov) — both CIs entirely above the 0.59 floor, so
the floor's standing caveat (a downstream (src,seq) suppression could have
made "re-sent into the network" too strong) is retired. Tables and provenance:
reinjection.md.
Packet identity is the (flow, seq) key the sink-side maps already use:
flow = (source IP, source UDP port) — byte-identical to the PacketSink Rx
trace's sender address — and seq from the SeqTsSizeHeader. The adapter's
MacReinject trace source fires at the same statement that increments
MacReinjectedPackets(), so events and ##DROPID##'s reinjected are two
readings of one increment; the harness parses the key off the re-injected
packet (UDP header + SeqTs, the queued form) and off each IP hop transmission.
Event view (one count per re-injection):
| field | definition | reads as |
|---|---|---|
events |
MacReinject trace fires |
must equal ##DROPID## reinjected — a mismatch is an instrumentation bug (FAIL) |
parsed |
fires where the (flow, seq) parse succeeded |
parsed + unparsedIcmp + unparsedOther must equal events — every re-injection is either named or classified (FAIL when the books do not close) |
unparsedIcmp / unparsedOther |
parse failures, split by IP protocol 1 vs anything else | a finding, not an error (WARN when non-zero): NotifyTxError re-injects any non-ant IP payload, so ICMP (e.g. TTL-exceeded) rides the detector too. Measured on the invariance probe: 2 events at one seed the SeqTs identity could not name (#386 comment 5233656930). The fate buckets below cover only the parsed events |
ofDelivered |
fires where the key was already in the sink's delivered set at trace-fire time | re-injections of already-delivered packets. No timestamps needed: traces fire in simulation-event order, so presence in the delivered map means "delivered strictly earlier" |
ofDelivered is deliberately "already delivered AT RE-INJECTION TIME", not
"eventually delivered anyway" — a copy that passed the failing hop but had not
yet reached the sink counts as not-yet-delivered here, so it is conservative
against the floor. The eventual-fate buckets below carry the other quantity.
Packet view (distinct re-injected (flow, seq) keys, classified at end of
run):
| field | definition |
|---|---|
pkts |
distinct keys ever re-injected |
pktsDelivBefore |
delivered before their first re-injection (the waste-certain class) |
pktsDelivAfterOnly |
never delivered before first re-injection, delivered by end of run (delivered late — the re-injection plausibly saved them) |
pktsNever |
never delivered at all (dropped) |
pktsDupDeliv / dupRx |
keys the sink delivered ≥ 2 times / total surplus deliveries — duplicate arrivals at the application layer, counted by the sink-side (flow, seq) books. The #386 A/B arithmetic shows FlowMonitor's rxPackets does not count them materially (a counted dupRx would have moved the ON−OFF PDR contrast by ~27 pp; measured +6.4), so FlowMonitor PDR reads unique delivery and dupRx measures real duplicate traffic, not metric inflation |
postTx / postRx |
IP-layer hop transmissions / arrivals of re-injected keys after their first re-injection — the hops re-injected packets go on to consume (the wasted-work number); postTx − postRx is their post-re-injection in-medium loss |
pktsDelivBefore + pktsDelivAfterOnly + pktsNever = pkts exactly;
scenario_check.py results FAILs a row where the partition does not sum, and
WARNs when ofDelivered falls below the same-seed inclusion–exclusion floor
reinjected + unackedRx − macDrops (WARN, not FAIL: the floor is built from
next-hop arrivals while ofDelivered counts sink deliveries, so a small
shortfall can be timing rather than broken books).
Measured reference (reading 2,
#386 comment 5234323092).
On the 900 s Nakagami cells, both mobility models, 5 seeds each:
ofDelivered/events ≈ 0.62–0.63 — the direct rate, above the hop-level
inclusion–exclusion floor on 10/10 seeds; dupRx ≈ 0.5 per event
(duplicate deliveries are not suppressed); pktsNever ≈ 2–5 % of
re-injected keys. n=5 per cell is below the
#293 publishable floor —
treat these as the diagnostic reference the checks calibrate against, not as
publishable points. The same reading measured the detector as an operating
point: switching it off cost −6.4 pp PDR on 5/5 seeds in both cells, so
the re-injection waste these fields expose is currently paid for delivery, not
a free defect.
The publishable version of both readings is reinjection.md
(20 seeds per arm, post-flip defaults, 7471447): the duplicate rate is
0.6519 / 0.6683, pktsNever is 2.66 % on the shipped unlimited arm, and the
detector A/B is ΔPDR +5.55 [+4.78, +6.32] pp (rwp) / +6.54 [+5.79, +7.29] pp
(gaussmarkov), 20/20 sign-consistent in both. Quote that page, not the n=5
figures above.
The trailing l3DropRoute/l3DropTtl/l3DropOther fields attribute the
L3-visible drops of re-injected keys (the pending-queue ageout's error callback
lands in DROP_ROUTE_ERROR; TTL exhaustion in DROP_TTL_EXPIRED). A key that
ends in pktsNever with no L3 drop died in the medium/MAC — the residue the
377/#388 drop identity¶
is sensitive to. This stage is deliberately isolated (own callback, own trace
connect, fields grouped last) so it can be cut cleanly if an ns-3 version in
the CI matrix rejects the Ipv4L3Protocol Drop trace signature.
Capped skips (skips* tail, #402).
These count the MaxReinjectPerPacket cap's early-return — a MAC
retry-limit drop the detector matched but the cap refused to re-inject, so the
drop stays terminal:
| field | definition |
|---|---|
skips |
capped-skip events (adapter MacReinjectSkip trace fires; includes the unparsed non-data ones) |
skipsPkts |
distinct skipped (flow, seq) keys. skips > skipsPkts is possible under a cap and is not an error: the ReinjCountTag budget is per packet copy, so duplicated copies at different nodes each carry their own budget and one key can be skipped repeatedly (measured 387 events vs 370 keys on the cap=1 probe cell) |
skipsDelivBefore / skipsDelivAfterOnly / skipsNever |
fate partition of skipsPkts, same construction as the pkts partition (delivered before the first skip / only by end of run / never). Must sum exactly (scenario_check FAILs otherwise) |
The tail exists because the uncapped #388 attribution breaks under a cap
(the #402 finding, +8.50 pp residue on the cap=1 probe cell vs +3.32 pp
uncapped): a packet can accumulate re-injections — inflating the per-hop books
— and then hit the cap, so its final MAC drop lands in
macTerminal = macDrops − reinjected while the same packet's earlier hop
inflation is still in hopLoss; when that packet was in fact delivered (the
377 delivered-but-ACK-lost class, ~3/4 of skip events on the probe cell) the¶
identity double-counts it. Cap-aware rule: the harness's mac column
subtracts the delivered-key skip events from the terminal numerator —
mac = 100·(macDrops − reinjected − (skips − skipsNever))/tx, with
skips − skipsNever evaluated in event units (skip events on keys delivered
at any point) — so only never-delivered capped drops count as terminal MAC
loss and delivered-packet skips join the documented #377-class straddle. A
small residual remains and is documented, not hidden: skipsNever frames that
arrived but were never ACKed (unACKed-but-arrived, bounded by the same #377
mechanism at roughly 0.16 of the never class on the probe cell) are still
counted terminal. Uncapped arms are structurally unchanged: the adapter's
MacReinjectSkip trace lives inside the MaxReinjectPerPacket != 0 branch
and can never fire at the default 0, so every skips* field reads 0 and the
subtraction vanishes arithmetically — scenario_check FAILs skips > 0 on
any arm whose ##CONFIG## lacks a non-zero cap. ##DROPID##'s macTerminal
field stays the raw macDrops − reinjected so the books remain auditable;
only the # drops attribution applies the correction.
Absence encoding (the #382 rule, with the #229/#230 controls stated as numbers):
- Baselines (AODV/OLSR/DSDV): no row at all. They have no failure detector
and no re-injection; a
0would read as "measured, nothing happened".scenario_check.py resultsFAILs any##REINJ##row naming a non-AntHocNet protocol. - TCP cells: no row. The SeqTs identity does not exist on a byte stream (same reason the reorder columns are absent there).
EnableMacFailureDetector=false: row present, every field= 0. The hooks stay connected and the trace simply never fires, so the zeros are measured. The knob's effective value is in the##CONFIG## attrdump, andscenario_check.py resultsFAILsevents > 0alongsideEnableMacFailureDetector=false— preflight cannot enforce this (it never seesextraArgs), so the coherence check is results-side by design.
Measuring commit (##PROV##, #365)¶
##PROV## commit=<sha> ref=<branch> run_id=<id> attempt=<n> image=<ghcr tag> \
profile=<default|release> harness=<compare|baselines|isl-grid|scenario-matrix>
The other half of provenance: ##CONFIG## says how the run was configured,
##PROV## says which version of the code was configured that way. A number
carrying only one of the two is not reproducible — the same knobs against a
different protocol build are a different experiment, which is the whole content
of #365.
Emitted by the workflow, not by the harness. This is the one marker in the
compact block that does not come from anthocnet-compare's stdout, and it
cannot: a simulation binary has no way to know the commit it was built from
short of baking one in at configure time, which would then be wrong for anyone
running it from a dirty tree. The workflow knows it for certain, so the workflow
says it. Three campaign workflows emit the same line:
paper-benchmark.yml and
satellite-benchmark.yml
append it to their compact blocks; scenario-matrix.yml has no compact block —
its numbers travel as a CSV artifact — so it emits the line from a step of its
own, under if: always() so a sweep that dies mid-way still stamps the points
that completed.
Position matters. It is the last line of the block, after ##PERF##. A
cheap get_job_logs tail loses lines from the top, and losing the provenance of
a campaign is worse than losing its wall-clock.
Two limits:
key=value, not positional, for the same reason as##CONFIG##: a misread field here would misattribute a whole campaign, and no# stddevcross-check exists to catch it.- It records the commit the workflow checked out, which is the commit the binary was built from in every workflow here because each builds from its own checkout. A future workflow that ran a pre-built binary would make the line a lie; if one is ever added, it has to stamp the binary's provenance instead.
Runs from before this marker existed (everything up to v1.3.0) have no
##PROV## line. Their provenance is the release pin documented in
methodology.md, not a per-run stamp — the
mapping was never recorded, and inventing one now would be fabrication rather
than recovery.
Pending-queue hold time (##HOLD##, #308 phase 2 step 4)¶
##HOLD## <run> <proto> <setupN> <setupMeanMs> <setupMaxMs> \
<reconvN> <reconvMeanMs> <reconvMaxMs> \
<repairN> <repairMeanMs> <repairMaxMs>
How long delivered data packets waited in AntHocNet's pending queue, split by
why they were waiting: setup (first discovery for a destination), reconv
(re-discovery after a known route was lost) and repair (re-injected after a
MAC transmit failure while a local repair runs). Summed over nodes, per run.
The measurement is not new — reaching it is. HoldStats has recorded these
since #21/#104, but they were printed only inside the --diag line, which the
workflow's compact re-emit never carried. Reading them for the phase-2
decomposition therefore cost a 900-line log tail and still lost three of twenty
seeds. Emitting a number and being able to reach it are different things; this
marker closes the second half.
Measured at the paper base scenario (disk, 900 s, 20 seeds): reconv holds
1703 events per run at 93.0 ms mean, repair 1959 at 24.0 ms, setup 18 at
406.5 ms — together 27.61 ± 1.72 ms per delivered packet, against an
all-delivered mean-delay gap to AODV of 21.9 ms.
Two limits, both load-bearing.
Counts are hold events, not packets. The pending queue is per node, so a
packet crossing several hops can be held more than once. setupN + reconvN +
repairN divided by delivered packets gives hold events per delivered
packet (0.476 at the paper base), which is either ~48 % of packets held
once or fewer packets held repeatedly. These counters cannot tell the two
apart, and the distinction matters for the protocol story.
Rows appear for AntHocNet only. AODV also queues during route discovery
(aodv::RequestQueue) and OLSR/DSDV do not queue at all, but none of them is
instrumented, so no row is printed for them. A row of zeros would assert
"this protocol never held a packet" when the truth is "nobody looked" — the
same reason ##AIR## prints nothing under --energyJ=0. Consequently
##HOLD## supports statements about AntHocNet's own delay composition, and
not cross-protocol hold comparisons, until the AODV side is measured.
Channel occupancy (##AIR##, #308 phase 2 step 3)¶
##AIR## <run> <proto> <txS> <rxS> <ccaBusyS> <busyPct>
The step-2 decomposition split the delay deficit into 36 % extra path length
and 64 % extra cost per hop, and left one hypothesis standing for the per-hop
half: AntHocNet sends fewer control packets than AODV (NRL 36.08 vs 55.39) but
substantially more control bytes (nrl_bytes 42.607 vs 29.133), so it may
simply be putting more airtime on a shared 2 Mbit/s medium that data then queues
behind. That would raise per-hop delay while causing neither MAC drops (0.44 %
vs AODV's 10.43 %) nor queue overflow (drop_queue 0.00 for both) — which is
exactly the observed signature, and exactly why it has to be measured rather
than believed.
txS / rxS / ccaBusyS are node-seconds summed over every node: what this
network put on the air, what it decoded, and what it saw as busy-but-not-for-me.
busyPct is their sum as a percentage of node-time (nNodes × time), i.e. the
fraction of the run an average node saw the medium occupied — normalised so the
number does not silently depend on --nNodes or --time.
The PHY "State" trace is already connected for the energy accounting (#209)
and already carries each state's duration, so this costs no new hook and no
extra simulated work.
Coupled to the energy model, deliberately visible. That trace is only
connected when the energy model is on, so --energyJ=0 (#270) leaves the
occupancy unmeasured. In that case no ##AIR## row is printed at all,
rather than an all-zero row that would read as "the medium was never busy" — the
absence is the signal. scenario_check.py results therefore treats a present
row with all-zero occupancy and non-zero PDR as a FAIL: packets flew, so the
medium cannot have been idle throughout.
Distinguishing what this can and cannot show. It measures whether the airtime premise is true — is AntHocNet occupying more of the medium? It does not establish that the extra airtime causes the per-hop delay. A negative result kills the hypothesis; a positive one licenses the next measurement, not a conclusion.
The one assumption left, and it is checkable. Cross-protocol keying needs a
flow's (source IP, source port) to be identical across protocols in the same
run. It is — topology, addressing and application construction are identical
and identically seeded, only the routing protocol differs — and the output
makes a violation visible rather than silent: nCommon collapsing toward zero
means the keys did not line up, which no real routing difference could produce.
Adding a marker is two changes, not one. The harness emitting a
##MARKER##line is only half of shipping it:paper-benchmark.yml's "Compact result block" step re-emits a explicit allow-list of markers at the end of the log so one cheapget_job_logstail carries everything. A marker missing from that grep still reachespaper-results.txt(and the artifact, which is proxy-blocked) but sits far above the compact block. That is exactly what happened to##MATCH##on its first run:tail_lines=215instead of ~50, and the top row scrolled off anyway, costing one seed out of twenty in a paired analysis.
Where the drop-cause identity comes from¶
Every offered packet lands in exactly one bucket. That is the whole of #215, and it is why the identity can be checked rather than trusted — measured values below are the 20-seed × 900 s disk arm (run 30772704321), percentages of offered packets:
sankey-beta
offered,delivered,95.04
offered,no route,1.88
offered,channel loss,2.46
offered,MAC retry exhaustion,0.52
offered,TTL expiry,0.08
The five causes plus PDR sum to 99.98 % here; the residual is data still queued when the run stopped, which is why the gate WARNs past 1 pp and FAILs past 5 pp rather than demanding exactly 100. Reading the same picture for AODV (84.73 delivered, 10.09 MAC) makes the contrast concrete: AntHocNet's losses are dominated by route state, AODV's by MAC retry exhaustion.
A cause that accounts for nothing is the failure mode this instrumentation
exists to expose — DSDV's routing-layer queue drops used to land in no bucket
at all and open an 11.46 pp hole (−21.8 pp once #388's honest MAC accounting
stopped masking part of it,
#229). Since the #229
fix the harness measures those sheds itself (the pending-queue conservation
hook below) and folds them into drop_queue_pct, which is what re-admits DSDV
into cross-protocol drop comparisons.
How energy is accounted¶
Not a battery model — an integration of the radio's own state trace, which is why its absolutes must be read carefully.
flowchart LR
PHY["WifiPhy <b>State</b> trace<br/>TX · RX · IDLE · CCA_BUSY · SWITCHING"]
PHY -->|"time in state × current × voltage"| INT["integrate per device"]
INT --> TOT["<b>energy_j</b><br/>total over all nodes"]
INT --> RES["<b>energy_res_*_j</b><br/>residual min / mean / <b>sd</b><br/>= forwarding-load spread"]
TOT -->|"÷ delivered data packets"| PER["<b>energy_per_pkt_j</b><br/>the cross-protocol comparable"]
RES --> DEATH["<b>first_death_s</b><br/>accounting only — the radio<br/>never actually switches off"]
style PER fill:#e2f0ed,stroke:#0f7f70,stroke-width:2px
style RES fill:#eef,stroke:#5b4fc4
style DEATH fill:#eee,stroke:#888,stroke-dasharray:4 3
Three caveats that decide what may be claimed:
- Idle dominates. At the shipped currents the idle draw is ≈0.82 W, so total energy is mostly "the radio was switched on", not "the protocol worked hard". Absolute joules are near-identical across protocols by construction — compare deltas and the residual spread, never the totals.
- The modelled NIC is 802.11n while the harness runs 802.11b at 2 Mbit/s. The current values are ns-3's own provenance (Halperin, HotPower'10); they are internally consistent, not a claim about the simulated radio.
first_death_sis bookkeeping. Energy is integrated but never enforced — no node stops transmitting when its budget hits zero, so a "death" marks a threshold crossing, not a topology change.
energy_per_pkt_j is the one to quote across protocols, because it divides by
what each protocol actually delivered; energy_res_sd_j is the one to quote
about fairness of forwarding load, since a protocol that funnels traffic
through a few relays drains them faster and widens the spread.
Drop causes (#215, NS-3 only)¶
PDR says how many packets went missing. These columns say why, as a percentage of offered (sent) packets, so that
pdr_pct + drop_route_pct + drop_queue_pct + drop_mac_pct
+ drop_chan_pct + drop_ttl_pct ≈ 100
Every offered packet is accounted for exactly once: it was delivered, or it
died of one of five causes. That identity is what makes the breakdown readable
— a cause is a share of the loss, not a free-floating counter — and
scenario_check.py results enforces it (see Why the identity must close).
The five causes are measured identically for all four protocols, so a column means the same thing in the AntHocNet arm and in the AODV arm:
- drop_route_pct — the routing layer gave up: no route was ever found, or
the route it was waiting on did not come back in time. Measured from
FlowMonitor's per-flow drop reasons (
DROP_NO_ROUTE+DROP_ROUTE_ERROR), i.e. from the protocol's own error callback, whichever protocol raised it. - drop_queue_pct — the packet was overtaken by congestion at the
interface: the device transmit queue or the traffic-control queue disc was
full (
DROP_QUEUE+DROP_QUEUE_DISC). This is the "network is drowning in its own traffic" signal, not a routing failure. - drop_mac_pct — the link broke under the packet: 802.11 exhausted its
retry limit on the unicast (
WIFI_MAC_DROP_REACHED_RETRY_LIMITon theWifiMacDroppedMpdutrace — the same signal AntHocNet's ADR-0008 detector D uses). Terminal failures only: AntHocNet re-injects a MAC-dropped data packet into its pending queue (#46), and a re-injected packet is not lost — its real fate is counted wherever it finally ends up — so re-injections are subtracted here rather than counted twice. - drop_chan_pct — sent and never received. Counted as the difference
between data IP packets handed to a real interface and data IP packets that
arrived at the next hop's IP layer (the
Ipv4L3ProtocolTx/Rxtraces, the same counting point as NRL), minus the MAC and interface-queue drops already attributed above. What remains is genuine on-air loss: collisions, capture, a next hop that moved out of range mid-frame, ARP failures. This one can only come from the simulator — it is the gap between "we sent it" and "it arrived", whichcore/by construction cannot see (golden rule 1). - drop_ttl_pct — the IP TTL reached zero (
DROP_TTL_EXPIRE): the packet was forwarded in a loop or along a path longer than the TTL allows. Note this is the IP TTL on data packets;Config::maxPathLengthis the analogous ceiling on ants and does not appear here (ants are control traffic, counted by NRL).
Three further columns split drop_route_pct for AntHocNet. They are a
sub-breakdown of that column, not extra causes — adding them into the
identity would double-count — and they are blank, never 0, for the other
protocols, because the mechanisms simply do not exist there. Blank means "not
applicable"; 0 would mean "applicable and never fired", and the two must stay
distinguishable:
- drop_setup_pct — held in the pending queue for a destination this node
had never routed to, and aged out at
QueueTimeout(or was evicted when the queue filled). This is the true no route case: discovery never succeeded. - drop_reconv_pct — same exit, but for a destination this node had
routed to: a route existed, was lost, and did not return within
QueueTimeout. Reconvergence loss, not discovery failure. Packets re-injected after a MAC failure and then aged out land here too — they were also waiting for a route that had existed. - drop_repair_pct — released by
DiscardPendingafter a local repair timed out (the paper's §3.5, decision D6). The core counts the repair events (AntRouterLogic::repairDiscards()); the NS-3 adapter owns the queue and so counts the packets.
Which causes are protocol behaviour, not faults¶
This is the part that matters when reading a table. Not every drop is a bug.
| Cause | Reading |
|---|---|
| drop_repair_pct | Expected, deliberate behaviour. The AntHocNet paper (§3.4 link failures / §3.5 local repair) chooses to discard the packets buffered behind a failed local repair rather than hold them indefinitely: it trades a slice of PDR for a bounded delay tail. A non-zero repair-discard share is the protocol working as specified, and #21's RepairHoldCap/ReconvHoldCap levers move packets deliberately between this column and the delay tail. Read it against delay99_ms/jitter_ms, never on its own. |
| drop_reconv_pct | Expected under mobility, in proportion to how fast links break. It is the direct cost of reactive re-discovery; a rising share as pause falls is the protocol responding to mobility, not degrading. It becomes a finding only when it dominates on a static field. |
| drop_setup_pct | Expected during the start-up transient and for genuinely unreachable destinations (a partitioned field — scenario_check.py preflight warns about those before the run). A large steady-state share means discovery is failing: that is #169's signature. |
| drop_chan_pct | PHY/scenario property, not a protocol property, up to the control traffic the protocol puts on the air. All four arms share a channel, so compare the column across arms: the protocol with the higher share is the one filling the medium. A collapse dominated by this column is #173's exact signature — and having it as a column is why #215 exists at all, since #173 cost a benchmark campaign, a testbench reproduction and a code audit to reach that same conclusion. |
| drop_queue_pct | Congestion, same reading as drop_chan_pct: offered load (or control overhead) exceeding what the interface can drain. Cross-check against --qdiag and preflight's offered-load fraction. |
| drop_mac_pct | Link breakage. Expected under mobility; a high share at low speed points at the PHY setup (--rateManager, #51) rather than at routing. |
| drop_ttl_pct | Always a fault signal. Data should not loop. Anything materially above zero means forwarding loops — read it with EnableMultipath and the A1 loop-suppression caveat in the NS-3 adapter. |
Why the identity must close¶
The five protocol-agnostic causes come from three independent books:
FlowMonitor's per-flow drop reasons, the Ipv4L3Protocol Tx/Rx hop
tallies, and the WifiMac retry-limit verdicts. Nothing forces them to agree,
which is what makes their sum a real check rather than an accounting tautology.
scenario_check.py results therefore treats a mismatch as a harness
regression, in the same class as the #51 anchor floors:
- WARN past 1 percentage point, FAIL past 5.
- The tolerance exists for one legitimate residual: data still sitting in a
routing protocol's pending queue when the run stops. A packet waits there at
most
QueueTimeout(3 s), so the residual is bounded by 3 s of offered traffic — ≈0.3 % of a 900 s paper run, ≈2.5 % of the 120 s--quickpreset. 5 pp leaves plenty of room for that while still catching a #173-scale misattribution (tens of pp). - A negative share in any cause also FAILs: it means two causes are counting the same packet.
- One documented overshoot: the AntHocNet re-injection straddle on fading
cells. An AntHocNet fading-cell sum of ~101–103 is expected, not a
books error (#377's
closure): the per-hop books count a #46-re-injected packet's extra copies
while the identity is end-to-end. The #386 detector A/B pinned it — on
identical seeds the same cells read exactly 100.00 with
EnableMacFailureDetector=false(#386 comment 5234323092). Read the overshoot against##REINJ##'spostTx/dupRx; it scales with re-injection volume, not with any misattribution.
If the check fails, do not read the breakdown — one of the three books is wrong (a drop path that fires no error callback, a trace hook that did not connect on this ns-3 release, a cause counted twice) and every per-cause conclusion is unsafe until it is fixed.
The three AntHocNet-only columns are a sub-breakdown of drop_route_pct, not
causes on top of it. drop_setup_pct + drop_reconv_pct + drop_repair_pct
partitions drop_route_pct: all three exit paths end in the same L3 error
callback, so the parent already contains them. Adding them to the identity
double-counts route failures and manufactures a ~156 % total out of a correct
breakdown — a mistake made once while reading these very columns
(#229). The identity has
five terms plus PDR. scenario_check.py cross-checks the partition
separately (WARN if the three do not reconstruct their parent within 1 pp).
What is measurable, per protocol¶
drop_queue_pct starts from Ipv4FlowProbe's DROP_QUEUE / DROP_QUEUE_DISC
reasons, which observe the interface and qdisc queues only. A routing
protocol's own pending queue — the buffer holding packets awaiting a route — is
invisible to them unless the protocol reports its discards through an L3 error
callback. ns-3's stock dsdv::PacketQueue reports nothing: its Drop()
has the error callback commented out (identical ns-3.36 through ns-3.48), its
Enqueue() overflow returns false into a caller that ignores it, and the
protocol registers no TraceSource — every one of its shed paths is silent.
The #229 fix closes
this from the harness alone (the baselines run stock ns-3 code, so nothing in
src/dsdv is patched): a pending-queue conservation hook in
anthocnet-compare.cc (PqTrackTx/PqTrackDrop, dsdv + aodv arms, UDP
only). Both protocols defer a route-less packet by looping it through the
loopback interface, so the hook records each data (flow, seq) key at the L3
Tx trace on interface 0 and clears it when the key crosses Tx on a real
interface. Keys never seen again are the queue's silent losses (pqSilent on
the ##DROPID## line — DSDV's sheds, plus packets still queued at run end);
pending keys that surface at the L3 Drop trace are AODV queue drops that
fired the entry's error callback (pqEcb) and were sitting misattributed in
drop_route_pct. Both are folded into drop_queue_pct (and pqEcb removed
from drop_route_pct — a queue timeout is congestion, not a route failure).
| Protocol | Routing-layer pending-queue drops |
|---|---|
| anthocnet | Counted by the protocol. They run through the error callback, so they land in DROP_NO_ROUTE and hence in drop_route_pct (further split by the three sub-columns above). This is why AntHocNet's identity closes to within 0.06 pp on every scenario. |
| aodv | Counted. Timeout/overflow/RERR sheds fire the error callback (aodv::RequestQueue::Drop is stock-instrumented); since #229 the conservation hook re-attributes them from drop_route_pct to drop_queue_pct, and packets still queued at run end (the former −2.4 pp dense-small residue) are added to drop_queue_pct. |
| olsr | Not applicable — OLSR is proactive and does not buffer awaiting a route. |
| dsdv | Counted by the harness (#229 conservation hook). Stock dsdv::PacketQueue sheds on MaxQueuedPacketsPerDst / MaxQueueLen overflow and MaxQueueTime purge with no observable signal; the hook measures those sheds by conservation and they appear in drop_queue_pct — 21.8 pp at dense-small, ≤ 0.73 pp elsewhere. |
| oracle (#415) | Not applicable — there is no pending queue. The control always holds a route to every reachable destination, so it never buffers awaiting one; drop_route_pct is 0.00 in every measured cell, by construction rather than by luck. Its nrl / nrl_bytes are likewise 0.00 / 0.0000 as an asserted invariant (NS_ABORT + scenario_check), not as a measurement. What the arm does not escape is the MAC and the channel: it runs the same PHY as every other arm, so its residual losses are real and its sub-100 % PDR under fading is expected. |
Before the hook, the silence cost 11.46 pp of the DSDV identity at
dense-small as filed (−21.8 pp measured post-#388, when honest MAC
accounting stopped masking part of the hole) and ≤ 0.73 pp elsewhere, and
DSDV had to be excluded from cross-protocol drop-cause comparisons. One
residual imprecision is accepted and documented in the hook's block comment: a
packet that exits the queue and dies to TTL expiry before reaching a real
interface is double-counted (ttl + queue); TTL starts at 64, so the path is
structurally negligible. DSDV's aggregate metrics — PDR, delay, NRL — never
depended on the breakdown and are unaffected either way.
Caveats¶
- NS-3 only. The NS-2 adapter has no equivalent instrumentation; see cross-validation.md.
- The human table prints these on a
# dropsline (like# energy) rather than widening the fixed-width table; the CSV carries all eight columns. # dropsalso printsother=, the L3 drops in none of the five buckets (bad checksum, interface down, fragment timeout — structurally zero in this harness). If it is ever non-zero it is the reason the identity misses.
Route quality: path length, path diversity, fairness (#217, NS-3 only)¶
AntHocNet's defining property is that it lays and maintains multiple paths, and until #217 the harness measured nothing about paths. These columns are produced protocol-agnostically from NS-3's own traces for all four protocols — the single-path baselines are the instrumentation's self-check, not a blank column.
- path_hops_mean / path_hops_max — mean and maximum hop count actually
traversed by delivered data packets, where one hop is one transmission (a
packet delivered to a neighbour is 1 hop). This is what settles whether
AntHocNet's delay advantage comes from shorter paths or from better path
selection: compare
path_hops_meanalongsidedelay_ms. 0 when nothing was delivered (thenrlconvention).
Definition and source. Taken from the IP TTL observed at the destination's
Ipv4L3Protocol LocalDeliver trace — one call per packet handed to the
local transport, i.e. exactly the packets PDR counts — as
hops = (initial TTL − TTL at delivery) + 1. The initial TTL is NS-3's
Ipv4L3Protocol::DefaultTtl default of 64 (unchanged across the 3.36–3.48 CI
matrix; nothing in the harness sets a SocketIpTtlTag). Every real forwarding
hop passes through Ipv4L3Protocol::IpForward, which decrements the TTL
exactly once. This is numerically the same quantity as FlowMonitor's
FlowStats::timesForwarded + 1 — which accumulates only over delivered
packets — but observed per packet rather than per flow, which is what makes
the maximum available at all: FlowMonitor exposes only the sum.
- path_div_used — used path diversity: the mean number of distinct next
hops that actually carried a data packet for a destination, per (node,
destination) pair, within one
path_div_window_swindow, averaged over the cells that carried data. path_div_max is the largest such count seen in any one cell.
Used, not available — and the distinction is the point. Pheromone entries
above minPheromone are the paths the table makes available; a table full
of pheromone that data never uses would read as diversity that does not
exist. What is reported here is usage: a next hop counts only when a data
frame it carried was acknowledged.
Why a window. Diversity is concurrency. Over a whole run a single-path protocol also touches several next hops for one destination — sequentially, as routes break and are rediscovered — so a whole-run distinct count would credit AODV with multipath it does not have. Counting per window separates concurrent spreading from sequential replacement. Expect the baselines (AODV/OLSR/DSDV) to read ≈1; the residual excess above 1 is route churn inside one window, not spreading, and it grows with mobility. A baseline reading well above 1, or AntHocNet reading exactly 1, is an instrumentation or protocol regression.
Source. The WifiMac AckedMpdu trace: it fires at the transmitter for
every unicast MPDU the next hop acknowledged, and the 802.11 header's Addr1
is the next hop. It is the only address-bearing transmit hook NS-3 offers
uniformly to all four protocols (the Ipv4L3Protocol traces do not carry the
gateway), and "acknowledged" makes "carried" literal — a frame the next hop
never received is not a path that was used. The AntHocNet adapter already
depends on this trace across the whole CI matrix (#68).
-
path_entropy_bits — mean Shannon entropy (bits) of the per-next-hop packet split within a cell, averaged over the same cells as
path_div_used. 0 = single path; 1 bit = an even two-way split; it is bounded above by log2(path_div_max). Reported because the count alone cannot distinguish a genuine 50/50 split from 999 packets down one next hop and 1 down another. -
path_div_window_s — the window (s) the diversity columns are counted over (
--pathWindowS, default 10 s), carried in the CSV because it defines what the diversity number means. Provenance: a repo choice, not a paper value. It must be long enough to hold several packets of a flow (the paper base scenario offers 1 packet/s per flow, so 10 s ≈ 10 packets) and short enough that a single-path protocol rarely replaces a route inside it. Comparisons across different window settings are meaningless — check the column matches before differencing two runs. -
jain_pkts — Jain's fairness index over the per-flow delivered-packet counts,
J = (Σxᵢ)² / (n·Σxᵢ²).J = 1when every flow is served equally,J = 1/nat maximum unfairness (one flow gets everything).heavy-loadruns 40 flows and the rest of the table reports only aggregates, so without this one starved flow is invisible. This is where stochastic spreading should beat single-path protocols.
Population. n is the number of source applications actually installed, so
a flow so starved that not one of its packets ever reached the IP layer — its
socket send failed for want of a route, which FlowMonitor never records —
still counts as a zero rather than vanishing from the index. (That is a
deliberate divergence from PDR's denominator, which counts only packets
FlowMonitor saw offered.)
One index, two readings. Computed over delivered packet counts; because every flow in this harness sends the same 64-byte payload, the per-flow throughput vector is a scalar multiple of the packet-count vector and Jain's index is scale-invariant, so the same number is also the throughput-based index. If the harness ever gains per-flow packet sizes, the two separate and a second column is needed.
Caveats (route quality)¶
path_div_*andpath_entropy_bitsare not publishable at the current default window. The single-path baselines are this metric's self-check, and they fail it. OLSR installs exactly one route per destination and so must readpath_div_used≈ 1.000; on the first full campaign it read 1.428 atheavy-load, 1.383 athigh-mobilityand 1.344 atpaper-base. AntHocNet's whole range (1.225–1.512) sits inside OLSR's.The cause is the window, not the counter:
path_div_window_sdefaults to 10 s, which is longer than a route survives under mobility, so a single route being replaced is counted as two concurrent paths.sparse-staticis the control that proves it — the only scenario with no route churn, and the only one where the baseline reads ≈ 1 (olsr 1.004) and the separation from AntHocNet (1.225) is real.The window was swept, and no value works. Five single points on the paper-base knobs, identical config,
--pathWindowSthe only variable:
window anthocnet aodv olsr dsdv 0.5 s 1.001 1.000 1.000 1.000 1.0 s 1.001 1.000 1.000 1.000 2.0 s 1.002 1.002 1.000 1.001 5.0 s 1.147 1.157 1.164 1.066 10.0 s 1.291 1.311 1.327 1.167 Below 2 s every protocol reads ≈ 1.000 — AntHocNet included, so there is no signal to have. At 5 s and above the baselines are already past 1.05 and OLSR outranks AntHocNet. There is no window in between: at 2 s AntHocNet ties AODV to three decimals.
Root cause: the metric is sample-starved at the paper's traffic rate, not merely mis-windowed. A (node, destination, window) cell can only report diversity above 1 if at least two packets land in it, so packets-per-cell is bounded by
pktPerSec × window— 1 × 2 s = 2 at paper-base, the bare floor, where a cell can only ever read 1.0 or 2.0. Shortening the window to escape route churn starves the sample; lengthening it to gather samples lets churn dominate. The two bounds cross, which is why the sweep has no solution.
kDefaultPathWindowSis therefore left at 10 s — no other value is better at the paper's 1 pkt/s, and moving it would imply the problem was solved. A 900 s re-sweep (w ∈ {2,4,6,10}, runs 30649042555/30649049794/ 30649059276/30599510462) reproduced the table above point for point.Resolution (#230, owner-approved): a dedicated diversity-measurement cell. Raising the offered rate feeds the cells instead of shrinking the window past the churn bound: at
--cbrBps=4096 --pathWindowS=2(8 pkt/s → 16 packets per cell; run 30650903707, 900 s) AntHocNet separates from every baseline —divUsed1.229 vs aodv 1.133 / olsr 1.090 / dsdv 1.046,entropyBits0.163 vs ≤ 0.093,divMax7 vs ≤ 6. Concurrent spreading is real and was sample-starved at 1 pkt/s.Rules for using the cell:
- All
path_div_*/path_entropy_bitsclaims come from this cell only (cbrBps=4096,pathWindowS=2), which is its own load regime (8× the paper rate) and must be labeled as such — never mixed into the headline paper cell, whose diversity columns remain unquotable.- Report diversity as excess over the in-run single-path floor, not as an absolute: at 8 pkt/s even a churn-free window catches AODV's route replacement landing packets on two routes inside one window (measured floor 1.133), so the honest figure is AntHocNet − AODV in the same run (today: +0.096 used-paths, +0.070 entropy bits). No (rate, window) pair drives the floor to 1.0 while keeping the cells fed.
scenario_check.pyreads rule 2 rather than an absolute bound. Per cell it measures the single-path floor in that run as the largestdivUsedover aodv/olsr/dsdv, reports AntHocNet's excess over it once, and WARNs — it does not FAIL — when the floor is itself churn-dominated (> 1.10) or when AntHocNet fails to exceed a clean floor. The severity is deliberate:divUsedis accumulated off the WifiMacAckedMpdutrace into its own counter and shares no accumulator with any published metric, so a churn-inflated diversity figure cannot move PDR, delay, delay99, NRL or energy. What still FAILs: a baseline above 1.50 at a churn-free window, and every arithmetic invariant (div < 1,div > divMax,div == 0at non-zero PDR,entropy > log2(divMax)).An absolute threshold was tried and refuted. The floor is a function of the scenario knobs, not a constant: aodv/olsr read 1.000/1.000 at
windowS=2, 1.190/1.198 at 6 and 1.311/1.322 at 10, and 1.133 atwindowS=2once the rate goes to 8 pkt/s. No constant is both satisfiable and informative — the old 1.10 rule FAILed the three control rows in 14 of 14 v1.5.0 campaign cells while passing the one row least worth trusting.What the v1.5.0 campaign's 14 cells say (all
windowS=10): no diversity claim survives at the paper window, and the sign is the wrong way round. AntHocNet's excess over the in-run floor is negative in 14/14 cells, −0.061 to −0.144 (anthocnet 1.236–1.306 against floors 1.339–1.405). A multipath protocol scoring below three protocols that install one route per destination is not a calibration problem; at this window the column is measuring route churn, and the baselines simply churn more. This strengthens the "not publishable" verdict above rather than softening it.The per-packet-pair concurrency counter remains the clean long-term instrument that would retire the floor comparison; #230 keeps it as the follow-up. Its shape is now specific: count interleaving (a next-hop sequence that returns to a previously-used hop, a→b→a) instead of distinct hops per window. Route replacement is monotone and never returns, so that counter has an a-priori control of exactly 0 for aodv/olsr/dsdv — the falsifiable control
path_div_usednever had.path_hops_*andjain_pktsare unaffected by all of this and readable from any cell.
- Reactive protocols read one hop high on route-discovery packets. A
protocol with no route yet bounces the packet through the loopback device to
reach
RouteInput(AODV and this adapter both do); the packet then leaves viaIpForward, costing one TTL decrement that no radio carried. Those packets report one hop too many, sopath_hops_meanforanthocnet/aodvis a slight over-estimate, bounded by (route discoveries)/(delivered packets) — a fraction of a percent in the paper scenario, larger where routes break often. FlowMonitor'stimesForwardedcounts the same bounce, so this is a property of the loopback idiom, not of the TTL route to the number. - Diversity is measured on acknowledged unicasts only. Broadcast frames are never acknowledged; that is correct here (routing control must not count as a used data path) but it also means a protocol that delivered data over broadcast would read 0. None of the four does.
- Diversity aggregates over every relay, not just sources. A cell is a (node, destination, window) triple, so a node forwarding for one destination in a window contributes a 1 whatever the source is doing. That is the intended reading — AntHocNet spreads at every hop — but it means the mean is diluted by relays that saw only one packet in a window.
- NS-3 only. The NS-2 adapter has no equivalent instrumentation; see cross-validation.md.
Energy (#209, NS-3 only)¶
Radio energy is integrated directly from each device's WifiPhy "State"
trace — every state interval contributes duration × I(state) × V, with the
TX/RX/idle currents and voltage taken from the same parameters for all four
protocols, so the joule columns are comparable across arms exactly the way
PDR and NRL are. (This replaced the ns-3 BasicEnergySource +
WifiRadioEnergyModel framework in PR #271: WifiRadioEnergyModel
cancel-reschedules its battery-depletion event on every PHY state change, and
cancelled ns-3 events stay in the scheduler until their timestamp — ~12 MB of
dead events per simulated second, the #256 OOM. The trace integration
reproduces the framework's numbers within 0.014 % with zero scheduled events;
the upstream report is #272.)
- energy_j — total energy consumed over the run, summed over all nodes, J.
- energy_per_pkt_j —
energy_j/ data packets delivered, J per delivered packet. The efficiency figure, and the one that stays comparable across protocols sitting at different PDRs: every node's radio is powered for the same wall-clock time whatever the protocol does, so total joules alone mostly measures the run length, while joules-per-delivered-packet prices the control airtime that NRL counts. 0 when nothing was delivered (same convention as NRL). - energy_res_min_j / energy_res_mean_j / energy_res_sd_j — residual energy
across nodes at end of run: minimum, mean, and sample stddev, J. This is
where routing fairness shows up — a protocol that funnels every flow
through the same relay drains that node first and partitions the network
early, which PDR and delay cannot see. Read the spread (
sd, andmean − min), not the absolute level. - energy_init_j — the initial per-node energy the run was configured with
(
--energyJ), carried in the CSV soresidual ≤ initialis checkable downstream (scenario_check.py results). - first_death_s — sim time at which the first node's cumulative
consumption reached 90 % of its initial energy, s. Sentinel
-1= no node died. The 10 %-remaining threshold is kept from the oldBasicEnergyLowBatteryThresholdsemantics so the column means the same thing across the PR #271 change — but note the new model records the crossing without switching the radio off: accounting continues and routing is unaffected. That is deliberate — the harness sizes--energyJso no death occurs in a normal run (below), making this a reporting marker, not a behavioural event; a run where it fires is telling you the scenario left the energy-neutral regime. Averaged over the runs that saw a death;-1when no run did.
Parameters and their provenance¶
--energyJ (initial energy per node) defaults to 5000 J, deliberately
sized so that no node dies in a normal run: a dying node makes PDR
energy-limited and silently changes the meaning of every other metric in the
taxonomy. Upper bound on one node's draw is the transmit current for the whole
run (0.380 A × 3.0 V = 1.14 W), so the longest scenario the harness runs
(900 s — --scenario=paper and =thesis) cannot consume more than 1026 J per
node; the realistic idle-dominated draw is ≈0.82 W, i.e. ≈740 J. 5000 J is
≈4.9× that hard bound while remaining a physically plausible cell
(5000 J / 3.0 V = 463 mAh). Raise it for longer runs; lower it to provoke node
deaths deliberately.
The radio currents are ns-3's WifiRadioEnergyModel defaults, restated
explicitly rather than inherited silently (the configuration.md
provenance rule — unsourced defaults caused #88, #169 and #173) and overridable
with --txCurrentA / --rxCurrentA / --idleCurrentA / --voltageV:
| Parameter | Default | Source |
|---|---|---|
--txCurrentA |
0.380 A | P_tx = 1.14 W at 0 dBm ÷ 3.0 V |
--rxCurrentA |
0.313 A | P_rx = 0.94 W ÷ 3.0 V |
--idleCurrentA |
0.273 A | P_idle = 0.82 W ÷ 3.0 V (also applied to the CCA-busy and switching states, as ns-3 does) |
--voltageV |
3.0 V | BasicEnergySource default supply voltage |
| (sleep) | 0.033 A | P_sleep = 0.10 W ÷ 3.0 V; never reached — AdhocWifiMac has no power-save state here |
The powers are measurements of a single-antenna 802.11n NIC reported in
D. Halperin, B. Greenstein, A. Sheth, D. Wetherall, "Demystifying 802.11n
power consumption", HotPower'10 — the citation ns-3 itself gives in
src/wifi/model/wifi-radio-energy-model.h. The values are unchanged across
every ns-3 release the CI matrix covers (3.36–3.48).
Caveats¶
- Energy is model-dependent and not comparable across PHY configurations.
The numbers are a function of the radio currents, the supply voltage and the
PHY/rate/propagation setup, not of the routing protocol alone. Compare energy
columns only between arms of the same harness configuration; never against
another paper's joules, and never across
--propagationor--rateManagersettings. - The modelled NIC is 802.11n; this harness runs 802.11b DCF at 2 Mbit/s (#51). Absolute joules are therefore an internally consistent yardstick across the four protocols, not a measurement of the paper's radio.
- Idle current is 72 % of transmit current, so the constant always-on draw dominates the absolute totals and the residual level. The routing-attributable component is the difference between arms — read deltas and the residual spread, not the absolute joule figure.
- NS-3 only. The NS-2 adapter has no energy instrumentation; see cross-validation.md.
Packet reordering (#212, NS-3 only)¶
Stochastic multipath forwarding delivers packets out of order by
construction: AntHocNet picks the next hop per packet, pheromone-weighted,
across several paths of differing delay, so a packet sent later on a shorter
path can overtake one sent earlier on a longer one.
configuration.md already names this as the cost of a low
betaData ("lower = more spread — and more reordering"); these columns measure
it. On a deterministic channel, read a non-zero AntHocNet figure as the
mechanism working as designed, not as a fault — under fading the dominant
cause is a different mechanism entirely, and the correction below is the
measurement that established which. It is a trade, not a defect: what it buys is the load spreading and
the route redundancy that the PDR and NRL columns report. The number that
matters is whether the reordering an application would feel is worth those
gains — which is exactly the question #179 (betaData 2 → the thesis's 20)
has to answer, and could not before this metric existed.
**Correction for fading cells (#399,
386).** The gloss above holds on the deterministic channels only. Under a¶
fading channel AntHocNet's
reorder_ratiois dominated by the #46 MAC-failure re-injection's duplicate deliveries, not by multipath spreading — and that attribution rests on a direct ablation, not on an inference from the mechanism's plausibility.The evidence. Turning the detector off on identical seeds collapses the ratio from 0.1745 (rwp) / 0.2301 (gaussmarkov) to 0.0010 / 0.0009 — 175× / 256×, from the single knob that creates duplicates, with all three baselines byte-identical across the arms. 0.001 is two-ray and AODV scale (0.0004 / 0.0003). The intermediate cap arms interpolate monotonically (rwp 0.1745 unlimited → 0.1320 at cap=2 → 0.0939 at cap=1), so the ratio tracks the amount of duplication rather than jumping with the knob's presence. Measured at 20 seeds per arm, 900 s Nakagami,
7471447— the re-injection arm page carries the full tables and provenance. (The earlier 5-seed reading of the same ablation, at the pre-flip 1 s hold cap, is #386 comment 5234323092: 0.1788 / 0.2408 → 0.0008 / 0.0007.)The multipath reading stays valid off-fading — the matched two-ray cell reads 0.0004, genuinely multipath-scale. On a fading cell, read
reorder_ratioas a re-injection-duplicate gauge (cross-check##REINJ##'sdupRx) and usereorder_buf_maxwith the same caution.
FlowMonitor reports per-flow counts and delays, never per-packet order, so
these come from an application-level sequence number instead: the ns-3
OnOffApplication sources set EnableSeqTsSizeHeader, and every PacketSink
reads the sequence number off each delivered datagram. Packet size is
unchanged — ns-3 builds the 20-byte SeqTsSizeHeader inside the configured
64-byte PacketSize, so the datagram is still 64 bytes on the wire and no
existing PDR / delay / throughput / NRL number moves.
Metrics are computed per flow (keyed by the source socket address, so the
--sink converge mode's shared sink still separates flows) and only then
aggregated. A single badly-spread flow is the interesting case and a pooled
number alone would hide it, hence the worst-flow column.
- reorder_ratio — out-of-order delivery ratio, pooled over all data flows: reordered packets / received packets, in [0,1].
- reorder_ratio_max — the same ratio for the worst single flow.
- reorder_extent_mean / reorder_extent_max — reordering extent over the reordered packets, in packets: mean, and the largest seen.
- reorder_buf_max — the largest reorder-buffer occupancy, in packets, that a receiver would need to hand the delivered packets up in sequence order.
The exact definitions used¶
"Reordering" has several non-equivalent definitions in the literature, so the choice is stated here rather than left implicit. All three follow RFC 4737 (Packet Reordering Metrics), applied to the receive stream of one flow at the sink:
- Reordered / out-of-order — RFC 4737's
Type-P-Reorderedsingleton. MaintainNextExp, the largest sequence number seen so far plus one. An arriving packet with sequence numbersis reordered iffs < NextExp; when it is not,NextExpadvances tos + 1, and when it is,NextExpis left alone.NextExpnever decreases, which is what makes the criterion order-preserving. Equivalently — and this is how issue #212 words it — a packet counts as reordered when its sequence number is below the running maximum for that flow. Loss is not reordering: a lost packet merely makesNextExpjump, and the packets that follow are still in order. This matters here, because the harness runs at PDRs well below 100 %. - Reordering extent — RFC 4737's
Type-P-Packet-Reordering-Extent, the position-distance form: for a reordered packet received at positioniwith sequence numbers, the extent isi − min{ j < i : seq[j] > s }, i.e. the number of positions back, in the received stream, to the earliest packet received that has a larger sequence number. It is ≥ 1 for every reordered packet by construction. This is what distinguishes "one straggler" (a large extent on a handful of packets) from "systematically interleaved" (a small extent on many). RFC 4737 notes that this position-distance form tends to overestimate the receiver storage actually needed to restore order — which is precisely why the next metric is carried alongside it. - Reorder-buffer occupancy — the high-water mark of a receiver buffer that releases packets in sequence order, defined over the packets that actually arrived. On each arrival the packet is buffered, then the longest complete prefix of the flow's delivered-packet sequence is released; the metric is the largest number held at once. Defining it over arrivals rather than over the sent sequence is deliberate: a buffer that waits for the literal next sequence number stalls forever on the first lost packet, so at this harness's PDRs it would measure loss rather than reordering.
Duplicates are not handled specially: OnOffApplication emits each sequence
number once and IP-layer unicast forwarding does not duplicate. On a fading
cell that premise fails: the #46 re-injection path retransmits packets whose
first copy already arrived (delivered-but-ACK-lost), so the sink can receive
the same (flow, seq) more than once — measured at dupRx ≈ 1500–2400 per
900 s Nakagami run (#386 comment 5234323092). A duplicate arrives with
s < NextExp and is therefore counted as reordered, which is the mechanism
behind the fading correction above. Off-fading the uniqueness assumption holds
and the columns read as documented.
Instrumentation self-check¶
Two arms should read ≈ 0 and are the reason to believe the AntHocNet number:
- AODV, OLSR and DSDV are single-path — one next hop per destination at a time — so the only reordering they can produce is the incidental kind around a route change. This is the control a reviewer will look for first.
- AntHocNet with
--ns3::anthocnet::RoutingProtocol::EnableMultipath=falsecollapses to a single next hop and should likewise fall to ≈ 0. That is a free validation of the instrumentation itself: if it does not, the metric is measuring something other than multipath spreading.
Neither is expected to be exactly zero — a route change can hand consecutive packets to paths of different length under any protocol — but both should be small next to the multipath figure.
What the self-check actually returned¶
The floor half passes: on the stable-field scenarios (sparse-static,
paper-base) every single-path baseline reads exactly 0.0000 at 93–100 %
PDR. Loss is correctly not counted as reordering and the sequence plumbing is
sound.
The discrimination half fails. Pooled over the six scenarios, reorder_ratio
reads anthocnet max 0.0208 against aodv max 0.0243 — AntHocNet does not
exceed the single-path control. At dense-small it is inverted outright:
| reorder_ratio | reorder_extent_mean | reorder_buf_max | |
|---|---|---|---|
| anthocnet | 0.0030 | 5.49 | 151.0 |
| aodv | 0.0243 | 45.80 | 22.6 |
Why: reorder_ratio and reorder_extent_* measure route flapping, not
concurrent paths. RFC 4737 tags every subsequent lower-sequence arrival
after one early packet. On the arrival order 1, 10, 2, 3, 4, 5, packets 2–5
are each Type-P-Reordered with extents 1, 2, 3, 4 — a mean extent of 2.5 from
a single displacement. That is RFC-correct and the implementation is right;
it simply makes the statistic a function of how often a route switches
forward. AODV re-discovers constantly under dense-small congestion, and each
re-discovery onto a shorter path is one such event. The fingerprint is a mean
extent above the buffer high-water mark (45.80 vs 22.6) — impossible unless few
displacements are tagging long runs behind them, and now a scenario_check.py
WARN.
reorder_buf_max is the honest multipath signal. It is the receiver-side
buffer depth concurrent paths actually impose, and it separates AntHocNet from
every baseline on 6 of 6 scenarios (anthocnet 12.6–973.8, baselines
0.0–22.6, largest in every scenario). Prefer it. Quote reorder_ratio or
reorder_extent_* only alongside the caveat above —
#230 tracks the
documentation and check work.
Caveats¶
- The
*_maxcolumns are averaged over the RNG runs like every other column: they are a mean of per-run maxima, not a maximum over runs. - Reordering is traffic-pattern dependent. At the paper's 1 packet/s per
flow, consecutive packets are 1 s apart and only a path-delay difference of
that order can reorder them; a denser source reorders far more readily at the
same routing behaviour. Compare these columns only between arms of the same
scenario, never across scenarios with different
cbrBpsorflows. - The reported figures are CSV columns; the human table prints them on a
# reorderline rather than as table columns, because the table's field positions are a parsing contract forbench_parse.pyand the workflows'##BENCH##re-emit. - NS-3 only. The NS-2 adapter has no equivalent instrumentation.
Aggregates are means over the RNG runs; the CSV also carries per-metric sample
stddev across runs (pdr_sd, delay_sd, delay99_sd, nrl_sd), which the
charts render as error bars, and the human table prints as # stddev lines.
Caveat — the scenario and sweep pages are two different vintages. Two protocol defaults were corrected on 2026-07-25. The scenario pages are regenerated on every merge and already include both; the sweep pages come from the manual campaign workflow and do not:
- #88 (PR #167, merged):
T_hop50 ms → 3 ms, the value stated in the 2007 thesis. It scales the hop-count term of every pheromone deposit, so it moves all delay-derived metrics. Measured effect on the pause sweep: mean delay −7…−16 %, jitter −8…−17 %, delay99 −8…−12 % under mobility, at flat PDR and NRL (#88 measurement). - #169 (PR #170, merged):
reactiveMaxBroadcasts2 → unbounded. The finite budget was a hop limit on route discovery — destinations more than ~5 hops away were never found — so it affects PDR and NRL, not just the delay columns.
So: read a sweep page as the pre-fix behaviour, pending re-measurement. Read
a scenario page as current — with one exception, because #169's fix exposed
a third defect: #173 (P1,
open) leaves reactive forward ants bounded neither by a broadcast budget nor
by (src,seq) duplicate suppression, so discovery floods combinatorially in
dense graphs. That inflates NRL and depresses PDR on large-scale and
heavy-load specifically.
See docs/fidelity.md and configuration.md for the provenance of these values.