FUMD-AI Handover Forecaster

A multi-step serving-cell forecaster for vehicular 5G networks. Given 6 seconds of a vehicle's joint mobility + radio history, it predicts which base station will serve that vehicle at each of the next 1 to 7 seconds — one softmax per horizon step, over the 9 cells of the simulated deployment (a 10-way head; class 0 is a reserved "not connected" code).

The intended use is proactive handover and resource management: knowing a vehicle's future serving cell a few seconds ahead allows a network to pre-position context, bearers or edge workloads before the handover actually happens, instead of reacting to it.

The architecture is a 2×bidirectional-LSTM encoder feeding a per-step Bahdanau (additive) attention decoder. Each decoder step attends over the encoder sequence, emits a softmax over cells, and conditions the next step on the embedding of its own argmax prediction (autoregressive over the forecast horizon).

  • Developed by: Cristina Bernad (0000-0001-9537-415X), Sonja Filiposka (0000-0003-0034-2855), Katja Gilly (0000-0002-8985-0639)
  • Funded by: the FUMD-AI project, an EOSC GRAVITY Inter Project, Grant Number 25-EOSC-GRV-INTER-013
  • Model type: encoder–decoder sequence classifier (BiLSTM + Bahdanau attention), 7 classification heads
  • Framework: TensorFlow 2.14 / Keras 2.14 (.keras format, two custom layers shipped in this repo)
  • Parameters: ≈1.14 M trainable
  • License: MIT

Quick start

import json
import numpy as np
from tensorflow.keras.models import load_model
from fumd_model import BahdanauAttention, ZeroInitialState

model = load_model(
    "model.keras",
    custom_objects={
        "BahdanauAttention": BahdanauAttention,
        "ZeroInitialState": ZeroInitialState,
    },
)

# X: (n_windows, 6, 23) float32, already standardised — see "Input format" below.
X = np.zeros((1, 6, 23), dtype="float32")

probs = model.predict(X)              # list of 7 arrays, each (n_windows, 10)
pred_cells = np.stack([p.argmax(-1) for p in probs], axis=1)   # (n_windows, 7)

predict.py in this repo does the whole thing end to end from a raw dataset_labeled_w3.csv: encoding, windowing, scaling, prediction and a metrics report. Run python predict.py --dataset path/to/dataset_labeled_w3.csv.

Input format

Shape: (batch, 6, 23) — 6 consecutive 1-second observations of a single vehicle, 23 features each, standardised with the bundled scaler.

The 23 features, in this exact order (order matters — the scaler and the first LSTM layer are positional):

# Column Source Meaning, units, observed range
1 angle SUMO vehicle heading, degrees clockwise from north, 0–360
2 speed SUMO m/s; 0–19.2 observed
3 pos SUMO metres travelled along the current lane; 0–345 observed
4 lane SUMO categorical — lane id, an OSM way id plus lane index (-94092847#1_0). Integer-encoded, 4,109 classes + 1 unknown code
5 signals SUMO turn-signal bitmask. Numerically encoded state, not a magnitude, but fed as a plain scalar rather than one-hot; 6 distinct values observed
6 averageCqiDl Simu5G nrPhy mean downlink CQI index, 1–15; fractional because it is an average
7 distance Simu5G nrChannelModel metres to the serving gNB; 4.7–514 observed
8 measuredSinrDl Simu5G nrPhy downlink SINR measured by the UE, dB; −20.9 to 86.3 observed
9 measuredSinrUl Simu5G nrPhy uplink SINR, dB; −14.8 to 97.7 observed
10 rcvdSinrDl Simu5G nrPhy downlink SINR of received transmissions, dB; −14.3 to 91.9 observed
11 rlcDelayDl Simu5G nrRlc.um downlink RLC delay, seconds; median 4.3 ms, max 1.95 s. 0 for vehicles with no downlink traffic
12 rlcPduDelayDl Simu5G nrRlc.um downlink RLC PDU delay, seconds; coarse (31 distinct values), max 58 ms. Same zero rule
13 rlcThroughputDl Simu5G nrRlc.um downlink RLC throughput in Simu5G's own units, not rescaled by the workflow; 0–3,144 observed. Same zero rule
14 servingCell Simu5G nrPhy current serving cell id, 1–9; 0 is reserved for "not connected"
15–21 servingCell-1 … servingCell-7 Simu5G nrPhy serving cell 1–7 s in the past. -1 where the vehicle has no history that far back, 0 for "not connected"
22 x SUMO WGS84 longitude, degrees (~19.92–19.95) — not metres
23 y SUMO WGS84 latitude, degrees (~50.05–50.07) — not metres

Ranges are from run 900_1 (259,918 rows) and are indicative of this scenario, not validated bounds. The three rlc*Dl columns are exactly 0 for any vehicle that never carried downlink application traffic — about a third of rows in that run — rather than missing; the preprocessing workflow fills them deliberately.

Two preprocessing steps must be applied, both parameterised by preprocessing.json (a pickle-free export of the fitted scaler.joblib and label_encoders.joblib also included here):

  1. lane encoding. lane is a per-map SUMO edge id string. It is mapped to an integer via the fitted encoder's class list (4109 classes, sorted). Any lane id not seen in training maps to the reserved unknown code 4109 rather than raising — so the model degrades gracefully on a road segment the training traffic never visited.
  2. Standardisation. All 23 columns are then z-scored with a single StandardScaler fitted on the training windows flattened to (n_windows × 6, 23) — 6,513,948 rows. preprocessing.json carries its mean, scale and var as plain lists, so you do not need scikit-learn 1.5.2 (or joblib at all) to reproduce it.

Windowing. Windows are built per vehicle, sorted by time, stride 1: window i is rows i … i+5, and its targets are the serving cell at rows i+6+k for k = 1…7. A vehicle with fewer than 6 + 7 = 13 rows produces no windows. Windows never cross a vehicle boundary.

Columns deliberately excluded from the inputs — servingCell1…servingCell7 (no dash; these are lead/future values), and migration / destination (labels derived from each vehicle's future trajectory). Feeding any of them in would leak the answer.

Output format

A list of 7 arrays, one per horizon step, each (batch, 10) — a softmax over cell ids. outputs[k] is the distribution for +(k+1) seconds after the end of the input window. argmax gives the predicted cell id; the model is trained with sparse_categorical_crossentropy, so class index == serving-cell id.

Note the head is 10-way over a 9-cell deployment: ids 1–9 are the nine base stations, and class 0 is the reserved "not connected" code, which never occurs as a target in this training data. Class 0 probability mass is therefore not meaningful — treat it as unused rather than as a real prediction.

Training data

The model was trained on the combined output of four runs of the FUMD-AI preprocessing workflow: 900_1, 1000_1, 1200_1 and 1400_1 — the same simulated urban scenario at four vehicle densities. Each run is a joint SUMO (mobility) + OMNeT++/Simu5G (radio) simulation, labelled per row with the vehicle's handover/migration state.

The scenario is central Kraków, Poland (roughly 19.923–19.951°E, 50.053–50.071°N — the Old Town and the Planty ring, extending west to Zwierzyniec and south-east to Stradom), served by a synthetic 3×3 grid of 9 base stations at about 667 m spacing. The grid is a construct of the project, not a real operator deployment, and the cell sites carry Kraków landmark labels purely for human readability.

Vehicle ids are offset by 1,000,000 per source run before concatenation, so no two runs' vehicles are ever merged into one false trajectory.

Training windows 1,085,658
Validation windows 271,759
Split by vehicle, 80/20, seed 42
Scaler rows 6,513,948 (train only)
Base stations 9 (output head is 10-way; see above)
Lane vocabulary 4,109 (+1 unknown code)

The split is by vehicle, not by window. Consecutive sliding windows from one vehicle overlap in 5 of their 6 rows, so a window-level split would put near-copies of training samples into validation. No validation window shares a single source row with a training window.

Simulation stack: OMNeT++ 6.3.0, Simu5G 1.4.4, INET 4.5.4, SUMO (fcd-output).

Training procedure

Hyperparameter Value
LSTM units (per direction) 128
Attention units 128
Embedding dim 128
Dense units 128
Dropout 0.2
Optimizer Adam, lr 1e-3, clipvalue=1.0
Loss sparse_categorical_crossentropy (summed over the 7 heads)
Batch size 64
Epochs 9 run of 50 requested
Early stopping patience=3, restore_best_weights=True

Validation loss bottomed out at epoch 6 (0.4245) and rose for the next three epochs while training loss kept falling; early stopping fired at epoch 9 and restored the epoch-6 weights. metrics/training_curves.png and metrics/training_history.json contain the full curves.

Evaluation

All numbers below are on the held-out validation vehicles (271,759 windows), from metrics/metrics_train_val.csv.

Per forecast step

Horizon Top-1 accuracy Top-2 accuracy Weighted F1
+1 s 0.9812 0.9989 0.9812
+2 s 0.9801 0.9989 0.9801
+3 s 0.9792 0.9985 0.9792
+4 s 0.9781 0.9983 0.9781
+5 s 0.9761 0.9980 0.9761
+6 s 0.9744 0.9977 0.9744
+7 s 0.9727 0.9974 0.9727
mean 0.9774 0.9982 0.9774

Accuracy degrades smoothly with horizon, as expected, and top-2 accuracy stays above 99.7% throughout — when the model is wrong about the exact cell, the true cell is almost always its second guess.

Accuracy where it actually matters

Aggregate accuracy on this task is flattering, because most rows are steady state, where "the serving cell will not change" is already almost always right. The breakdown below splits the +1 s forecast by whether the forecast point falls inside a pre-handover warning window (migration ≠ 0) or in steady state:

Regime Windows Top-1 accuracy @ +1 s
Steady state 266,192 0.9886
Pre-handover warning window 5,567 0.6283

Read this before quoting the headline number. Right before a real handover — the case the model exists to serve — it gets the next cell right about 63% of the time, not 98%. Warning-window rows are ~2% of the data, so they barely move the aggregate. Any downstream evaluation of this model for proactive handover should report the warning-window figure.

Limitations and out-of-scope use

  • One city, one radio deployment. All training data comes from a single simulated urban area (central Kraków) with a fixed 9-cell synthetic grid. The output layer is hard-wired to 10 classes and cell ids are positional, so the model cannot be applied to a different deployment — or even a different cell numbering over the same streets — without retraining.
  • The base-station layout is synthetic. A uniform 3×3 grid at ~667 m spacing is not how real networks are laid out. Handover geometry under a regular grid is more predictable than under a real, irregular deployment, which likely flatters the accuracies below.
  • The lane vocabulary is map-specific. Its 4,109 classes are SUMO lane ids (OSM way ids) for this one road network. On another map every lane maps to the unknown code and the feature degenerates to a constant.
  • x/y are geographic coordinates, not local metres. They are WGS84 degrees, standardised against Kraków's own narrow extent, so the fitted scale for those two features is meaningless anywhere else on earth.
  • Simulated, not measured. This is SUMO + OMNeT++/Simu5G output, not real network traces. Simulated radio conditions are cleaner and more regular than measured ones; treat these accuracies as an upper bound.
  • Four runs of one scenario. The four source runs differ in traffic density (900/1000/1200/1400 vehicles), not in map or radio layout, and the validation split is drawn from the same four runs. This model has not been evaluated on an unseen preprocessing run. (Per-density sibling models were cross-evaluated against each other's runs and held 0.94–0.98 F1; that evidence is about the architecture, not about these specific weights.)
  • Not a safety component. It has no calibration guarantees and no uncertainty estimate beyond the raw softmax.

Files in this repository

File What it is
model.keras The trained model (Keras 2.14 .keras archive, includes optimizer state)
fumd_model.py The BahdanauAttention and ZeroInitialState custom layers — required to load the model — plus build_model() for retraining
preprocessing.py Self-contained lane encoding, windowing and scaling, driven by preprocessing.json
preprocessing.json Pickle-free export of the fitted scaler and lane encoder
predict.py End-to-end example: CSV → windows → predictions → metrics
config.json Architecture and preprocessing parameters in one machine-readable place
scaler.joblib, label_encoders.joblib The original fitted sklearn/SafeLabelEncoder objects (needs scikit-learn 1.5.2 and the training package to unpickle; prefer preprocessing.json)
metrics/ metrics_train_val.{json,csv}, training_history.json, training_curves.png, run_manifest.json
requirements.txt Pinned inference environment

Reproducing

Both halves of the pipeline are public workflows:

These weights are the pipeline_run_combined output of the training workflow at v0.1.4. metrics/run_manifest.json records the exact parameters of that run.

Citation

@software{bernad_fumd_ai_training_workflow,
  author  = {Bernad, Cristina and Filiposka, Sonja and Gilly, Katja},
  title   = {{FUMD-AI} Training \& Postprocessing Workflow},
  version = {0.1.4},
  year    = {2026},
  license = {MIT},
  url     = {https://github.com/FUMD-AI/fumd-ai-training-postprocessing-workflow}
}

Acknowledgements

This work has been funded by the FUMD-AI project, an EOSC GRAVITY — Inter Project with Grant Number 25-EOSC-GRV-INTER-013.

We gratefully acknowledge Polish high-performance computing infrastructure PLGrid (HPC Centers: ACK Cyfronet AGH) for providing computer facilities and support within computational grant no. PLGINT/2026/019844.

The research work was supported by the Open Science Cloud research laboratory (OSC-LAB) at the Faculty of Computer Science and Engineering (FINKI), Ss. Cyril and Methodius University in Skopje, North Macedonia.

Downloads last month
-
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Evaluation results

  • Top-1 accuracy @ +1 s on FUMD-AI labeled SUMO/OMNeT++ dataset (combined, held-out vehicles)
    self-reported
    0.981
  • Top-2 accuracy @ +1 s on FUMD-AI labeled SUMO/OMNeT++ dataset (combined, held-out vehicles)
    self-reported
    0.999
  • Weighted F1 @ +1 s on FUMD-AI labeled SUMO/OMNeT++ dataset (combined, held-out vehicles)
    self-reported
    0.981
  • Top-1 accuracy @ +7 s on FUMD-AI labeled SUMO/OMNeT++ dataset (combined, held-out vehicles)
    self-reported
    0.973
  • Top-2 accuracy @ +7 s on FUMD-AI labeled SUMO/OMNeT++ dataset (combined, held-out vehicles)
    self-reported
    0.997
  • Weighted F1 @ +7 s on FUMD-AI labeled SUMO/OMNeT++ dataset (combined, held-out vehicles)
    self-reported
    0.973