Code and results of the paper "When Is Handover History Worth Remembering? Repeatability of Handover Outcomes in Dense 5G Networks" by Mostafa Jalili Sanjarani, Yasser Elmi Sola, Hesam Hasanpour and Ali Akbar Neghabi (Islamic Azad University, Sabzevar).
Many handover schemes keep a memory of what earlier handovers led to and adjust the next decision with it. They all assume that the outcome of a handover at a place says something about the next handover at that place. The study measures when this holds:
- in which environments does a per-place memory of handover outcomes help an Event-A3 rule, and in which does it hurt;
- can this be told in advance, from the logs of ordinary A3 operation, before any memory is deployed.
The repository contains
- a load-coupled system-level simulator (
hosim/) whose shadowing has a controllable share that is common to all users; - a minimal memory rule (
memstudy/rule.py): Event A3 whose hysteresis is shifted per place and per target cell by past outcomes, and whose control is the identical A3 rule; - a repeatability index computed from an event log (
memstudy/repeatability.py); - the experiment, analysis and figure scripts, the results of all 710 runs and the figures;
- a validation in ns-3 with the 5G-LENA NR module (
ns3/): plan, scenario source and run script, analysis script and the results of 360 protocol-level runs in two environments.
The slow shadowing between a user and a cell is the sum of three parts:
| Part | Standard deviation | Behaviour |
|---|---|---|
| shared | sigma_s |
a static, spatially correlated field per cell: every user at the same place sees the same value |
| private | sigma_p |
a first-order process along each user's own path (correlation exp(-distance / d_c)), independent between users |
| fast | 1.5 dB | temporal component with a correlation time of 1 s |
The total slow variance is fixed, sigma_s^2 + sigma_p^2 = (4 dB)^2, and split by the
shared share
phi = sigma_s^2 / (sigma_s^2 + sigma_p^2)
With phi = 1 everything is shared: what happened to one user at a place will happen to the
next. With phi = 0 nothing is shared. Both parts use the same decorrelation distance d_c
and the same inter-site correlation (0.5); blockage-prone cells scale all three parts by 1.5.
The propagation sweep covers phi = 1, 0.75, 0.5, 0.25, 0 and d_c = 12.5, 25, 50, 100,
200 m; phi = 1, d_c = 50 m is the reference environment. In the code the split is
shadow(phi, d_corr) in experiments/common.py; the two standard deviations are
SimParams.shadow_sigma and SimParams.sigma_p in hosim/simulator.py.
MemA3 is 3GPP Event A3 with one extra term in the hysteresis:
margin(place, target) = h + eta * (risk(place, target) - mean risk), clipped to [0, 9] dB
risk = P(unnecessary) + P(early failure) - P(too late)
- A place is a 40 m location bin (other bin sizes and "one place per serving cell" are part of the experiments); the memory is a table of outcome counts per (place, target cell).
- The four outcomes are the ones a network can observe: useful, unnecessary, failed shortly after the handover (too early), link lost before a handover completed (too late).
- The probabilities are estimated from the counts, shrunk towards the network-wide shares
with 3 pseudo-observations, so a place without history gets the plain hysteresis
h. Counts are forgotten with a time constant of 600 s. mean riskis the same expression for the network-wide shares; subtracting it makes the shift zero on average, so the rule does not simply raise or lower the hysteresis everywhere.
Control. With eta = 0 the rule is exactly A3 with hysteresis h and the same
time-to-trigger. Every comparison is between MemA3 and A3 with h = 4 dB and
time-to-trigger 0.1 s (an A3 setting tuned for this layout in preliminary runs) on the same
trajectories, services and shadowing. The memory rule is warmed up for 240 s on independent
traces of the same environment before each test run. The strength eta is chosen once, on
validation seeds: among the candidates 2, 4, 8, 12, 16, 24 dB whose mobility-failure rate
does not exceed that of A3 with 3 dB and 0.3 s, the one with the fewest unnecessary
handovers (results/tuned_eta.json: 24 dB).
A long control run (A3, 720 s) writes an event log: one record per classified handover and
per too-late failure, with the position where it was decided. Each event gets a risk score
z (+1 unnecessary or too early, 0 useful, -1 too late) and is assigned to its place.
From the log alone, memstudy/repeatability.py computes
| Name | Meaning |
|---|---|
rho |
intraclass correlation of z over places: the share of the outcome variance that belongs to the place |
n |
events per visited place: how much evidence a place collects |
skill |
prequential skill of an outcome table: every event is predicted from the earlier events of its place; 1 - MSE(table) / MSE(running overall mean) |
half |
split-half correlation: mean z per place seen by the even-numbered users against the odd-numbered users |
law |
rho times the mean of m / (m + (1 - rho) / rho), with m the earlier events in the place: the skill expected from rho and n alone |
No memory rule is involved in these numbers.
hosim/ the system-level simulator
simulator.py SimParams, RadioEnv, gen_traj, assign_services, simulate
policies.py Base (interface of a decision rule) and A3
radiomap.py RadioMap: cell positions and path-loss fits from a drive-test table
mobility.py the five mobility profiles
services.py the five 5QI service classes
memstudy/ the memory rule and the index
rule.py MemA3: Event A3 with a per-place outcome memory
repeatability.py repeatability indices from an event log
experiments/
common.py paths, radio environment, shadowing split, scenario()
run_study.py job list and resumable runner -> results/runs.jsonl, tuned_eta.json, logs/
analyze.py statistics and indices -> results/summary.json
threshold_eval.py skill threshold on held-out environments and on the ns-3 runs
-> results/threshold_eval.json, .txt
make_figures.py the figures of the paper -> figures/fig_*.pdf (also .png)
plotstyle.py matplotlib style
examples/quickstart.py small A3-versus-memory demo with the parameters at the top of the file
data/synthetic/ the 40-cell synthetic layout (UDN-40)
results/ result files (see results/README.md)
figures/ figures
ns3/ validation in ns-3: plan, scenario source and run script, analysis, results (see ns3/README.md)
docs/ additional documentation
run_all.sh the full pipeline
A Persian guide for readers with little programming experience is in docs/ (docs/CODE_GUIDE.md,
and the same content as a single page in docs/code_guide.html).
Python 3.10 or newer.
git clone https://github.com/mjs19971997/handover-memory-study.git
cd handover-memory-study
pip install -r requirements.txt
requirements.txt pins the versions the experiments were run with (Python 3.12). The scripts
are run from a checkout; nothing needs to be installed as a package.
python experiments/run_study.py smoke
runs four jobs of 30 s through the whole chain (simulator, both rules, private shadowing,
event log, index) and ends with smoke test passed. It takes about a minute and writes only
to results/smoke/. One of its checks is that the memory rule with eta = 0 gives the same
numbers as the control.
python examples/quickstart.py
compares A3 and the memory rule on one small scenario and prints the index of the control
run. The scenario (mobility profile, users, phi, d_c, eta, ...) is set by constants at
the top of the file.
python experiments/run_study.py # results/runs.jsonl, tuned_eta.json, logs/*.npz
python experiments/analyze.py # results/summary.json
python experiments/threshold_eval.py # results/threshold_eval.json, threshold_eval.txt (reads summary.json and ns3/report_mem.json)
python experiments/make_figures.py # figures/fig_*.pdf (also .png)
bash run_all.sh runs these steps in a row (and re-creates missing event logs before the analysis).
-
Resumable. Every finished run is appended to
results/runs.jsonlat once; a repeated call runs only what is missing. With the result files of this repository in place,run_study.pytherefore has nothing to do. To repeat the study from scratch, setMEMSTUDY_RESULTSto an empty directory (or removeresults/runs.jsonl).python experiments/run_study.py statusshows what is done. -
Run time. The study has 710 runs (32 validation, 678 test) of 300 users each; a 240 s run takes one to two minutes of CPU time. All runs together took 15.4 CPU-hours, about 2.6 hours with 6 worker processes on an 8-core machine. The analysis takes about 3 minutes, the figures a few seconds.
-
Event logs. The analysis reads the event logs of the 174 long control runs (
results/logs/, about 90 MB; 120 MB with the logs of the short control runs). They are too large for the repository and are not included.results/summary.jsonis included, so the figures can be drawn without them. To run the analysis, re-create the logs withpython experiments/run_study.py logsThis repeats the 174 long control runs (5.6 CPU-hours, about an hour with 6 worker processes), writes their logs and checks every repeated run against its row in
runs.jsonl. Without the logsanalyze.pystops with a message that says so. -
Workers: all cores but one by default; set
MEMSTUDY_PROCSto change it. -
Output directory:
results/, or the directory named byMEMSTUDY_RESULTS. -
Determinism: all random streams are seeded, and both rules of a (scenario, seed) pair see the same trajectories, services and shadowing. For a given set of library versions the results are reproducible exactly; the only non-deterministic columns are
secs(run time) anddec_us(decision time). The shadowing field, the hotspot cells and the blockage-prone cells are drawn anew for every seed.
Reference environment (phi = 1, d_c = 50 m), 40-cell layout, 300 users, test seeds 1–6.
Rates are per user-minute; n, rho and skill come from the control logs. Produced by
experiments/analyze.py (results/summary.json, key profiles).
| Mobility | Unnecessary HO, A3 | Unnecessary HO, memory | Change | Failures, A3 | Failures, memory | Change | n |
rho |
skill |
|---|---|---|---|---|---|---|---|---|---|
| Pedestrian | 0.39 | 0.51 | +29% | 0.035 | 0.038 | +8% | 1.7 | 0.25 | 0.01 |
| Vehicular, open area | 1.44 | 1.44 | 0% | 0.32 | 0.34 | +6% | 3.4 | 0.38 | 0.06 |
| Vehicular, street grid | 1.54 | 1.21 | -21% | 0.37 | 0.37 | +1% | 5.0 | 0.39 | 0.13 |
| Highway | 3.92 | 1.39 | -65% | 0.67 | 0.65 | -3% | 33.0 | 0.43 | 0.37 |
| High-speed train | 8.01 | 5.93 | -26% | 1.05 | 1.43 | +37% | 145.6 | 0.41 | 0.38 |
Over the propagation sweep (key sweep) the change of the unnecessary handovers goes from
-38% to +49% on the street grid and from -66% to -6% on the highway. Across the 53
environments (25 propagation settings on the street grid, 25 on the highway, and the three
other profiles) the Spearman rank correlation between an index of the control log and the
change of the unnecessary handovers is -0.10 for rho, -0.95 for n, -0.92 for skill and
-0.93 for half; a threshold of 0.03 on skill separates gain from loss in 91% of the
environments (key predict). That figure is in-sample. experiments/threshold_eval.py repeats
the choice of the threshold with environments held out (results/threshold_eval.txt): 87%
with each environment held out in turn (46 of 53; the threshold stays between 0.031 and
0.060), 91% with a whole mobility profile held out, 87% with a whole value of the shared
share or of the decorrelation distance held out, against 68% for always expecting a gain.
With the threshold fixed from these 53 environments, the sign is as predicted in all 12 ns-3
configurations; in 9 of them the change differs from zero. The comparison with the threshold
was not part of the plan of the ns-3 runs (ns3/PLAN_MEM.md states its expectation in terms
of rho), and any threshold between 0.021 and 0.063 gives the same result there. The paper
discusses these results and their limits.
The figures of the results:
| File | Content |
|---|---|
figures/fig_profiles.pdf |
the five mobility profiles: unnecessary handovers, failures, and the indices of the control runs |
figures/fig_sweep.pdf |
change of unnecessary handovers and of failures over the propagation sweep |
figures/fig_index.pdf |
rho, events per place and skill over the propagation sweep |
figures/fig_predict.pdf |
change of unnecessary handovers against rho, n and skill, one point per environment |
figures/fig_curves.pdf |
length of the warm-up, length of the log, bin size and strength eta |
figures/fig_ns3.pdf |
ns-3: change of unnecessary handovers against eta and against the skill of the control runs (ns3/make_figure.py) |
figures/fig_ns3_logs.pdf |
ns-3: place-wise view of the per-attempt files of 24 runs, with the layouts of the two environments (ns3/make_figure_logs.py, files in ns3/attempt_logs/) |
The same rule with the same parameters was run in ns-3 (release 3.48) with the 5G-LENA NR
module (version 5.1): 7 gNBs, 20 users, runs of 900 s, six seeds, eta = 0, 6, 12, 24, 36 dB,
in an open area with per-link shadowing and on a street grid in which buildings block the
line of sight. Set-up, procedure, all numbers and the limits are in ns3/README.md.
| Environment | rho |
n |
skill |
Unnecessary HO, A3 | memory (24 dB) | Change | Failures, A3 | memory |
|---|---|---|---|---|---|---|---|---|
| Street grid with buildings | 0.23 to 0.43 | 7.6 to 15.5 | 0.11 to 0.33 | 3.91 | 2.68 | -31.5% | 0.009 | 0.016 |
| Open area | 0.19 to 0.28 | 1.5 to 6.6 | -0.01 to 0.06 | 1.52 | 1.64 | +7.6% | 0.001 | 0.001 |
The sign of the effect follows the skill of the control runs, as in the system-level study;
the failure rate on the street grid rises with the memory, and the relation between rho, n
and skill overestimates the skill in the open area. The check is small (six seeds per
configuration, light traffic) and uses an NR module with one local change. The ns-3 scenario
program with its build and run scripts is in ns3/scenario/.
All runs use the 40-cell layout with 300 users. A triple is a control run (240 s, logged), a memory-rule run (240 s after a 240 s warm-up) and a long logged control run (720 s).
| Block | Runs | Content |
|---|---|---|
tune |
32 | Validation (seeds 101, 102; street grid and highway): the control, A3 with 3 dB / 0.3 s (the failure budget) and the six candidate strengths. |
ref |
36 | Reference environment: triples on the street grid and the highway, six seeds. |
prof |
54 | Triples for the pedestrian, open-area vehicular and train profiles, six seeds. |
warm |
36 | Memory rule with a warm-up of 0, 60 and 960 s (street grid, highway; six seeds). |
res |
60 | Memory rule with bins of 10, 20, 80 and 160 m and with one place per serving cell. |
eta |
60 | Memory rule with the other five strengths. |
sweep |
432 | Triples for the 24 other propagation settings (phi x d_c), street grid and highway, three seeds. |
Each line of results/runs.jsonl is one run: the metrics returned by simulate and the job
description (id, block, profile, seed, phi, d_corr, kind, eta, ...).
results/README.md describes the result files field by field.
All values are the defaults of SimParams in hosim/simulator.py.
-
Time: all users advance together in 100 ms steps; default run length 240 s.
-
Radio map: cell positions and a per-cell log-distance path-loss fit estimated from the samples of a drive-test table (
hosim/radiomap.py); each cell has its 6 nearest cells as neighbours. -
Shadowing: as described above. The shared field has a 5 m grid; a quarter of the cells are blockage-prone (1.5 times the standard deviation).
-
Measurements: L3-filtered RSRP with filter weight 0.30 per 100 ms sample.
-
SINR and load: thermal noise -122 dBm per resource element; inter-cell interference is proportional to the load of the interfering cells (coupling 0.20). A quarter of the cells are hotspots with background load 0.55 to 0.85, the others 0.05 to 0.25. Carrier: 106 PRB (38.16 MHz), spectral efficiency capped at 7.4 bit/s/Hz. GBR demands are served first, the remainder is shared by the non-GBR users; cell load feeds back into interference.
-
Failures: radio-link failure when the serving SINR stays below Qout = -8 dB for T310 = 1 s; a handover commanded while the serving SINR is below Qout fails (too late). Either event causes a 1 s re-establishment outage. A successful handover interrupts the user for 50 ms.
-
Mobility profiles (
hosim/mobility.py): pedestrian (0.8 to 2 m/s, random waypoint), urban vehicular (8 to 16 m/s, random waypoint in the open area), urban street grid (8 to 16 m/s, 150 m blocks), highway (22 to 33 m/s, three lanes, both directions), high-speed train (40 to 60 m/s, all users on one track at a common speed). -
Service classes (
hosim/services.py), with rate demand, T_min and theta_Q:Service 5QI Resource type Rate [Mbit/s] T_min [s] theta_Q [dB] Share Conversational voice 1 GBR 0.1 4 0.7 0.20 Conversational video 2 GBR 2 6 1.0 0.20 Real-time gaming 3 Delay-critical GBR 1 7 1.2 0.15 Buffered streaming 6 Non-GBR 5 3 1.5 0.30 Web / background 9 Non-GBR 1 2 0.4 0.15 -
Outcome of a handover (evaluation): observed for T_min of the user's service or until the user leaves the target. The handover is failed if the link is lost in the target within the window; otherwise it is unnecessary if the stay was shorter than T_min or the mean SINR gain over the source cell was below theta_Q, unless the source cell itself would have gone into radio-link failure. Handovers whose window is cut by the end of the run are not classified. The label fed back to a rule, and written to the event log as the observable outcome, uses only what the network can observe (dwell time, L3 measurements, failure events).
-
Main metrics returned by
simulate:HO_pm,UHO_pm(handovers and unnecessary handovers per user-minute),UHR(unnecessary share of the classified handovers),fail_pm(radio-link failures plus too-late handovers per user-minute),outage_pct,satisf(delivered over demanded rate),thr_mbps,sinr_db, and per-service variants.
A decision rule is a subclass of hosim.policies.Base that implements decide(ctx) and
returns, per user, the target cell or -1 to stay; the hooks on_outcome, on_late and
after_step report the observable outcomes back to it. memstudy/rule.py is a complete
example of about a hundred lines.
- Code and result files: MIT licence, see
LICENSE. data/synthetic/realistic_5G_2x.csv: synthetic drive-test table created by the authors, distributed under the same licence.
M. Jalili Sanjarani, Y. Elmi Sola, H. Hasanpour, A. A. Neghabi, "When Is Handover History Worth Remembering? Repeatability of Handover Outcomes in Dense 5G Networks," 2026.
CITATION.cff holds the same entry in machine-readable form.
Yasser Elmi Sola (corresponding author), Yasser.Elmi@iau.ac.ir