Instructions to use fumd-ai/handover-forecaster with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- TF-Keras
How to use fumd-ai/handover-forecaster with TF-Keras:
# Note: 'keras<3.x' or 'tf_keras' must be installed (legacy) # See https://github.com/keras-team/tf-keras for more details. from huggingface_hub import from_pretrained_keras model = from_pretrained_keras("fumd-ai/handover-forecaster") - Keras
How to use fumd-ai/handover-forecaster with Keras:
# Available backend options are: "jax", "torch", "tensorflow". import os os.environ["KERAS_BACKEND"] = "jax" import keras model = keras.saving.load_model("hf://fumd-ai/handover-forecaster") - Notebooks
- Google Colab
- Kaggle
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 (
.kerasformat, 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):
laneencoding.laneis 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 code4109rather than raising — so the model degrades gracefully on a road segment the training traffic never visited.- Standardisation. All 23 columns are then z-scored with a single
StandardScalerfitted on the training windows flattened to(n_windows × 6, 23)— 6,513,948 rows.preprocessing.jsoncarries itsmean,scaleandvaras plain lists, so you do not need scikit-learn 1.5.2 (orjoblibat 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
lanevocabulary 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/yare 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:
- Preprocessing (raw SUMO/OMNeT++ → labelled, AI-ready CSV): https://github.com/FUMD-AI/fumd-ai-preprocessing-workflow
- Training & postprocessing (this model): https://github.com/FUMD-AI/fumd-ai-training-postprocessing-workflow
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
- -
Evaluation results
- Top-1 accuracy @ +1 s on FUMD-AI labeled SUMO/OMNeT++ dataset (combined, held-out vehicles)self-reported0.981
- Top-2 accuracy @ +1 s on FUMD-AI labeled SUMO/OMNeT++ dataset (combined, held-out vehicles)self-reported0.999
- Weighted F1 @ +1 s on FUMD-AI labeled SUMO/OMNeT++ dataset (combined, held-out vehicles)self-reported0.981
- Top-1 accuracy @ +7 s on FUMD-AI labeled SUMO/OMNeT++ dataset (combined, held-out vehicles)self-reported0.973
- Top-2 accuracy @ +7 s on FUMD-AI labeled SUMO/OMNeT++ dataset (combined, held-out vehicles)self-reported0.997
- Weighted F1 @ +7 s on FUMD-AI labeled SUMO/OMNeT++ dataset (combined, held-out vehicles)self-reported0.973