Verification of HandoverLab¶
This page describes how HandoverLab is checked, how to run the checks yourself and what they
showed for version 0.2.0a1. All 1.x versions since 0.3.0a1 give bit-identical results on the same
configuration (the golden fingerprints and the bh6 regression are unchanged). 2.0.0 changed defaults; with
handoverlab.stability.LEGACY_1X it gives every 1.x golden run bit for bit, so the numbers below hold for
those settings. Run everything with
handoverlab verify # about two minutes; prints PASS / FAIL / NOTE per check
handoverlab verify --full # three seeds for the radio statistics, 60 s engine runs
handoverlab verify --ns3 # only the comparison with ns-3 / 5G-LENA (a few minutes)
handoverlab verify --results results.json --sample 5 # repeat stored runs and compare
The command exits with status 0 if no check failed and 1 otherwise; --json report.json also
writes the report with the versions of Python, NumPy and SciPy and the platform.
Levels of checking¶
| Level | What is checked | Where |
|---|---|---|
| Unit tests | path loss, LOS probability, L3 filter, A3 and CHO triggers, failure model on synthetic radio samples | tests/ |
| Invariants, end of run | attempts = sum of outcomes; failures = sum of causes; every attempt started got exactly one outcome; preparations = lost + cancelled + used + unused + open | every run |
| Invariants, debug mode | per step: valid states and serving cells, T310 only while connected, no attempt to the serving cell, an execution always belongs to an attempt, finite SINR; event-log times in order; rule outputs of valid shape without NaN; the observation given to a rule is read-only | debug=True, --debug, HANDOVERLAB_DEBUG=1 |
| Path loss by hand | TR 38.901 UMi, UMa, RMa and InH, LOS and NLOS, both breakpoint branches, LOS probabilities, soft LOS weight, noise | verify group pathloss |
| 3GPP calibration | large-scale calibration of TR 38.901 clause 7.8.1: CDFs of coupling loss, SINR and SIR of UMa, UMi and the indoor office at 6 and 30 GHz inside the range of the 21 companies of R1-165974 (details) | verify group calibration, tests/test_calibration.py |
| Radio statistics | shadowing standard deviation (LOS, NLOS), spatial correlation, correlation between sites, LOS share against the LOS probability, time-varying part, L1 error, L3 filter | verify group radio |
| Determinism | same world twice gives identical event logs; rule order changes nothing; engines batched in one step loop = engines run one by one; another seed differs; global random state untouched; a study gives the same fingerprints with one or two worker processes | verify group invariants, tests/test_batch.py, tests/test_study.py |
| Equivalences | CHO with immediate execution = A3 (three settings); a plug-in rule that reduces to A3 = A3 | verify group equivalences |
| Golden fingerprints | SHA-256 of the event logs of short reference runs (several scenarios, up to three rules each) | verify group fingerprints, tests/test_verify.py |
| Regression against bh6 | ten cases, 36 runs: every metric and (for the wide sample) every event log identical to the prototype from which HandoverLab was ported | tests/test_regression_bh6*.py |
| TR 37.885 highway (1.11.0) | geometry and link budget of the two highway cards against TR 37.885, TR 36.885 and TR 38.901; continuity of the received power at the wrap-around; coupling gain to the strongest site against an independent computation (percentiles 5-95 within 0.5 dB); LOS on every link | verify group highway, tests/test_highway_tr37885.py |
| Legacy settings (2.0.0) | every golden run of 1.12.0 repeated with LEGACY_1X: identical event logs (33 of 33) |
verify group fingerprints (section fingerprints-1x) |
| Satellite (2.0.0) | N1 link budget of the 8 S-band LEO cases of TR 38.821 Table 6.1.3.3-1 within 0.1 dB (passed, 0.028 dB); N2 3 dB beamwidth within 0.01 degree of Tables 6.1.1.1-1/-2 (failed for 1 m: 0.014 degree; see the NTN guide); N3 orbital speed and nadir footprint (passed) | verify --only ntn |
| Fast fading (2.2.0) | the TDL taps of TR 38.901 clause 7.7.2: Rayleigh with unit power (KS test), the J0 time correlation of the Jakes spectrum, the K-factor of the first TDL-D tap, the delay spread after Eq. 7.7-1 and after a K change (Eq. 7.7.6-1/-2); the effective-SINR mappings exact on a flat channel and never above the mean; the option off changes no fingerprint | verify group fading, tests/test_fading.py |
| Learning rule (1.11.0) | a3_bandit against an exhaustive search over its arms, three criteria fixed before the run (one failed: see the learning-rule guide) |
verify --only learning (about 20 minutes) |
| Imported radio maps (1.12.0) | an exported street-grid map imported again gives the identical event log (every golden case on the street grid); interpolation of measured points with its hold-out error; errors for a wrong format, other sites, a map that does not cover the area, a changed file | tests/test_mapfile.py |
| Compiled kernel (1.11.0) | the golden fingerprints with run.backend: numba |
tests/test_v111.py (with Numba installed) |
| Reference case | ns-3 / 5G-LENA, 24 A3 settings on four conditions | verify --ns3 |
| Rule contract | any rule plug-in: construction, parameters, debug run, determinism, global random state, independence, optional equivalence | handoverlab.testing.check_rule |
Event-log fingerprints¶
The fingerprint of a run is the SHA-256 of its event log written in a canonical CSV form (see
handoverlab.eventlog): times rounded to 0.1 ms, gains to 0.001 dB and positions to 0.1 m when the
row is written, numbers in Python's shortest round-trip form. Every run computes it, whether or not
the log is kept; it is stored in every CSV and JSON result row. It does not depend on how a log is
stored, so a log read back from CSV or Parquet has the same fingerprint as the one in memory.
NumPy promises identical random streams only for the same build on the same machine, and
floating-point sums can differ in the last bit between CPUs and BLAS builds; a handover is a
threshold decision, so such a difference can move a single event. HandoverLab therefore measures
agreement instead of assuming it: on the reference build the golden fingerprints must be identical;
on another build the check reports whether they are and requires the counts to agree within 10 % or
three events. verify --results reports the share of identical event logs of a stored study and the
largest relative metric difference (metrics count as equal within 1e-12).
A change that alters the golden fingerprints on purpose regenerates them with
python -m handoverlab.verify.golden --write and records the reason in CHANGELOG.md.
Radio statistics (version 0.2.0a1, seed 3)¶
| Quantity | Simulated | Model |
|---|---|---|
| Shadowing standard deviation, LOS / NLOS links | 4.01 / 7.86 dB | 4 / 7.82 dB |
| Shadowing correlation at 10 m / 20 m along a street | 0.32 / -0.05 | 1/e = 0.37 at the decorrelation distance |
| Shadowing correlation between sites | 0.51 | 0.5 |
| LOS share of links clear of buildings, 20-40 / 40-80 / 80-160 / 160-320 m | 0.74 / 0.45 / 0.24 / 0.10 | 0.79 / 0.45 / 0.19 / 0.08 (TR 38.901 UMi) |
| Time-varying part: standard deviation, correlation after 1 s | 1.50 dB, 0.37 | 1.5 dB, 1/e |
| L1 sample error: standard deviation, mean | 1.000 dB, -0.0007 dB | 1 dB, 0 |
Since 2.1.0 (radio.channel_detail: 3gpp) the place-tied fields have the exponential correlation
exp(-d/d_corr) of TR 38.901 Eq. (7.4-5); the check compares the correlation at 10 m and 20 m with
exp(-1) and exp(-2), and the correlation between sites with 0 (TR 38.901 clause 7.5 step 4). The table
above was measured with the model of 2.0.0, whose Gaussian random fields have the correlation
exp(-(d/d_corr)^2) (the 1/e of the exponential model at the decorrelation distance, but falling faster
beyond it) and 0.5 between sites; radio.channel_detail: simple gives that model back.
Regression against the bh6 prototype¶
HandoverLab was ported from the research prototype bh6 and keeps its random-stream layout, the order of all random draws and the order of the floating-point operations. Two samples are stored:
tests/data/bh6_reference.json: two cases (default and stressed) with A3 and CHO;tests/data/bh6_wide.json: eight cases (site distances 50-150 m, pedestrian to 30 m/s, mixed speeds, open area, half the shadowing moving with the user, UMa at 3.5 GHz, no clutter, N310 = N311 = 2 with T310 = 0.5 s, label load 0.5 with 100 ms preparation and 20 ms L1 period) with four rules each (two A3 settings, CHO, CHO with immediate execution), about 2,500 events in all.
On the reference build every metric of all 36 runs is equal to the last bit and every event log of the wide sample is identical (its fingerprint is stored). On other builds the tests require close agreement only.
Reference comparison with ns-3 / 5G-LENA¶
The scenario card ns3-urban-3.5ghz describes the set-up, which was
also simulated with the packet-level simulator ns-3 and its 5G-LENA module. The stored ns-3 results
(handoverlab/verify/data/ns3_urban_a3.json) cover 24 Event-A3 settings (offset 1-6 dB,
time-to-trigger 100-1000 ms) on four conditions (site distance 200 / 400 m, 16 / 30 m/s). Seeds are
different realisations, so the comparison uses the mean over the four conditions per setting.
Result of 0.2.0a1 with one seed per condition:
| Quantity | Limit | Result |
|---|---|---|
| Handover rate, median ratio HandoverLab / ns-3 | 0.8-1.25 | 0.95 |
| Unnecessary-handover rate, median ratio | 0.8-1.25 | 1.04 |
| Handover rate, rank correlation over the settings | >= 0.9 | 0.99 |
| Unnecessary-handover rate, rank correlation | >= 0.9 | 0.99 |
| Failure rate, mean (no limit) | 0.13 against 0.43 per UE-minute, rank correlation 0.54 | |
| Mean serving SINR (no limit) | 26.2 against 11.5 dB |
The same comparison with version 2.1.0 (handoverlab verify --ns3, one seed per condition): median ratios
0.95 and 1.06, rank correlations 0.99 and 0.99, failures 0.03 against 0.43 per UE-minute (rank correlation
0.57), mean serving SINR 26.1 against 11.5 dB. The ns-3 card draws its shadowing per link (place_share
0), so the channel changes of 2.1.0 do not touch it, and with N310 = 1 and few failures the corrected link
monitoring leaves these runs unchanged.
Handover and unnecessary-handover rates agree; failure rates do not (next section). Only baseline handover is compared, because 5G-LENA has no conditional handover; for CHO the evidence is the equivalence "CHO with immediate execution = A3".
Validation: the known difference in failure rates¶
HandoverLab gives about a third of the failure rate of ns-3 / 5G-LENA in the reference case: 0.13 against 0.43 failures per UE-minute (mean over the 24 A3 settings and four conditions), with a rank correlation over the settings of only 0.54. This is a real difference between the two models, not a sampling effect, and it is not resolved in this version. What is known about it (version 0.3.0a1, one seed per condition; the same numbers as in 0.2.0a1):
| Condition | Failures, HandoverLab / ns-3 [1/UE-min] | Mean serving SINR, HandoverLab / ns-3 [dB] |
|---|---|---|
| 200 m, 16 m/s | 0.16 / 0.70 | 22.5 / 8.0 |
| 200 m, 30 m/s | 0.20 / 0.52 | 22.3 / 7.5 |
| 400 m, 16 m/s | 0.09 / 0.20 | 30.3 / 15.4 |
| 400 m, 30 m/s | 0.06 / 0.32 | 29.6 / 15.1 |
| A3 time-to-trigger | 100 ms | 300 ms | 500 ms | 1000 ms |
|---|---|---|---|---|
| Failures, HandoverLab / ns-3 [1/UE-min] | 0.065 / 0.031 | 0.078 / 0.140 | 0.090 / 0.436 | 0.271 / 1.119 |
- Both simulators agree on the direction (more failures at the smaller site distance and with a longer time-to-trigger), but in ns-3 the failure rate grows about 36-fold from 100 ms to 1000 ms and in HandoverLab only about 4-fold. At the shortest time-to-trigger HandoverLab has more failures than ns-3. The difference is therefore mainly in late handovers: ns-3 declares a link failure much sooner once the serving cell fades while the time-to-trigger runs.
- The interference is defined differently. In this scenario the interfering cells of
HandoverLab carry about 3 % load (light traffic), so the mean serving SINR is 14-15 dB higher than
the SINR reported by the ns-3 study, which is computed from the RSRP of all cells with every
other cell interfering (a definition, not what the ns-3 failure detection sees). With fully loaded interfering cells (
traffic.normal_loadandtraffic.hotspot_loadset to 1) the mean SINR of HandoverLab matches ns-3 within 0.5 dB in every condition (8.4, 8.0, 15.7, 15.0 dB against 8.0, 7.5, 15.4, 15.1 dB). - Matching the SINR does not match the failures. With fully loaded interferers HandoverLab gives 3.55 failures per UE-minute, eight times the ns-3 value, almost independent of the time-to-trigger (rank correlation -0.30), and its handover rates fall below ns-3 (median ratio 0.85, unnecessary handovers 0.28). No single load setting reproduces both the SINR and the failure rate of ns-3.
- The failure models differ. HandoverLab declares a radio-link failure from the wideband SINR of the serving cell, filtered over the Qout / Qin windows, with Qout = -8 dB, N310 = 1 and T310 = 1 s (the TR 36.839 abstraction). The failure detection of the ns-3 study works on the physical-layer quantities of ns-3 rather than on this abstraction, so the same SINR trace need not give the same failure times. HandoverLab's failure model is therefore not calibrated in absolute terms.
What this means for results. Compare failure rates between rules within HandoverLab (the tuning budgets of a study are shares of a reference rule's failure rate for this reason), and report them as relative quantities. Do not compare an absolute failure rate from HandoverLab with another simulator or with field data. Handover and unnecessary-handover rates, the main outputs, agree with ns-3 within the limits above.
Calibration attempt (version 0.4.0a1). 96 % of the ns-3 failures are handover commands that
arrive while T310 runs, and T310 itself has no effect on their rate. One existing parameter, a Qout
filter window protocol.qout_window_s of about 0.12 s instead of 0.2 s, reproduces the mean
failure rate of part A (0.35-0.41 against 0.43 per UE-minute) and its ranking over the settings
(0.97), but fails every held-back set: part B is three times too high, and the response to the
preparation delay and to the number of users differs from ns-3. No calibrated preset is therefore
offered and the default is unchanged. The evidence, and what a calibration would need, are on the
calibration page.
Reproduce the comparison with handoverlab verify --ns3; the fully loaded variant is the same
scenario with traffic.normal_load: [1.0, 1.0] and traffic.hotspot_load: [1.0, 1.0].