Top-level package

Everything most scripts need is importable from handoverlab directly.

handoverlab

HandoverLab: system-level simulation of handover and mobility procedures.

Quick start::

import handoverlab as hl

sc = hl.Scenario()                                  # 3 x 3-block street grid, 10 GHz
res = hl.simulate(sc, [{"rule": "a3"}, {"rule": "cho"}], seeds=[1])
for r in res:
    print(r.rule, r.metrics["handovers_pm"], r.metrics["failures_pm"])

ConfigError

Bases: ValueError

Invalid configuration; the message names the offending key.

LayoutConfig dataclass

Deployment geometry.

street_grid is the urban grid of 3GPP TR 37.885 Annex A: blocks of pitch_m (433 m x 250 m between street centre lines), streets street_width_m wide. The default of 3 x 3 blocks is the minimum area of TR 37.885 Table A-1. Sites stand on the pavement every site_spacing_m metres, site_offset_m from the street centre line.

open_area is the same rectangle without buildings, with sites on a hexagonal grid of spacing site_spacing_m.

highway (since 1.0.0rc3) is the straight multi-lane road of the TR 37.885 highway case: lanes lanes of lane_width_m per direction over road_length_m, without buildings; roadside sites every site_spacing_m metres, site_offset_m beyond the road edge, alternately on the two sides.

indoor (since 1.7.0) is the indoor office of TR 38.901 Table 7.2-2: a hall of 120 m x 50 m without walls and ceiling sites on a rectangular grid of spacing site_spacing_m (20 m in the standard).

area_m (since 1.7.0) sets the width and height of the rectangle of open_area and indoor [m]; unset, open_area takes the size of the street grid and indoor 120 m x 50 m. min_site_distance_m (since 1.7.0) is the minimum 2D distance of a user's start and waypoints from every site in open_area and indoor (TR 38.901 Table 7.2-3, RMa: 35 m); the straight legs between them may pass closer.

wrap (since 1.11.0, highway only) applies the wrap-around of TR 37.885 Table A-1: a vehicle keeps its lane and direction and re-enters the road at the other end, and every distance and angle between a user and a site is taken to the nearest image of the site along the road (period road_length_m); the place-tied shadowing field is periodic along the road. Without it (the default) a vehicle turns at the end of the road.

sites_file (since 1.0.0rc3) names a CSV file with the columns x and y [m]; it is read when the scenario is loaded and fills sites.

size_m: Tuple[float, float] property

Width and height of the simulated rectangle [m].

MobilityConfig dataclass

Number of users and their speeds; groups overrides the named profile.

ProtocolConfig dataclass

Measurement chain, handover timing and the failure model.

Defaults follow 3GPP TR 36.839 / TR 36.842 (Qout -8 dB, Qin -6 dB, T310 1 s, N310 = N311 = 1, preparation 50 ms, execution 40 ms) and the L3 filter of TS 38.331 clause 5.5.3.2.

RadioConfig dataclass

Carrier, link budget, propagation and the antenna abstraction (TR 38.901).

beams (since 1.10.0) replaces the antenna abstraction by sectors with SSB beams (BeamConfig); layers (since 1.10.0) adds carriers with their own cells (LayerConfig). With either of them the measurements are SS-RSRP per resource element (the power over the bandwidth minus 10 log10(bandwidth / scs_khz)); scs_khz unset is 30 kHz below 7.125 GHz and 120 kHz above.

channel_detail (since 2.1.0) selects how closely the large-scale channel follows TR 38.901 V19.5.0:

  • "3gpp" (the default): the place-tied shadowing and LOS fields have the exponential autocorrelation exp(-d / d_cor) of Eq. (7.4-5) (clause 7.4.4; clause 7.6.3.1 for the LOS state), the shadowing of different sites is uncorrelated unless shadow_site_corr is set (clause 7.5, step 4), the UMa LOS probability has the factor C'(h_UT) of Table 7.4.2-1 and the UMa path loss the random effective environment height h_E of Table 7.4.1-1 note 1 (both matter only for h_UT >= 13 m), a co-sited layer must keep the LOS-state and shadowing settings of the first layer (clause 7.6.5), the antenna heights must lie in the applicability ranges of Table 7.4.1-1, and an NTN satellite takes the tabled 3 dB beamwidth of TR 38.821 Tables 6.1.1.1-1/-2 (Table 6.1.1.1-4);
  • "simple": the model of 2.0.0 and 1.x (Gaussian-shaped field correlation exp(-(d / d_cor)^2), shadowing 0.5-correlated between sites, no C'(h_UT), h_E = 1 m, no checks of the heights or of co-sited layers, the beamwidth computed from the aperture).

shadow_site_corr unset (None, since 2.1.0) is 0 with "3gpp" and 0.5 with "simple".

extended: bool property

True if beams or further layers are configured (the extended radio model of 1.10.0).

noise_dbm: float property

Thermal noise over the carrier plus the noise figure [dBm].

RunConfig dataclass

Length of a run and (since 1.11.0) the backend of the step loop.

backend "numpy" (the default and the reference) or "numba": a compiled kernel for the radio-link monitoring of every step (handoverlab.engine._kernels; needs pip install handoverlab[fast]). Both give bit-identical results, so the backend is not part of the job IDs.

Scenario dataclass

One simulated condition. Together with a seed it fixes a world.

to_dict(full: bool = False) -> Dict[str, Any]

The scenario as a plain dictionary.

Keys added after 1.0.0rc2 (LATE_KEYS) are left out while they have their default value, so that the job IDs and study locks of scenarios that do not use them stay as they were; full=True keeps every key.

from_dict(data: Union[Dict[str, Any], str]) -> 'Scenario' classmethod

Scenario from a dictionary.

data may also be the name of a built-in scenario, or contain the key base with such a name; the other values then change the built-in scenario (see handoverlab.scenarios).

with_overrides(overrides: Dict[str, Any]) -> 'Scenario'

Copy with changed values; keys are dotted paths such as "layout.site_spacing_m".

validate() -> None

Range checks that name the offending key.

Service dataclass

A service class (5QI of TS 23.501 Table 5.7.4-1) and its label thresholds.

SpeedGroup dataclass

A share of users with a uniform speed range and lateral displacement range.

TrafficConfig dataclass

Services, background load and the scheduler abstraction.

InvariantError

Bases: RuntimeError

An internal consistency check of the engine failed.

A3

Bases: BaselineRule

Event A3 with one offset and one time-to-trigger for all users.

off_db A3 offset [dB] (neighbour better than serving by more than this) ttt_s time-to-trigger [s]

CHO

Bases: ConditionalRule

Conditional handover with fixed offsets.

o_prep_db preparation offset [dB]: a neighbour is reported and prepared when it is better than the serving cell by this much for ttt_prep_s (negative: prepared before it is stronger) o_exec_db execution offset [dB] carried in the conditional configuration ttt_exec_s time-to-trigger of the execution condition [s] ttt_prep_s time-to-trigger of the reporting condition [s] n_max largest prepared set (1-8, maxNrofCondCells-r16) rel_hys_db release hysteresis [dB] immediate execute as soon as the configuration arrives

CLTM

Bases: CLTMRule

Conditional L1/L2-triggered mobility with fixed offsets (3GPP Rel-19 work on LTM).

Specification: TS 38.300 clause 9.2.3.5 (LTM, Rel-18) for the candidate configuration, L1 measurements and RACH-less execution; conditional LTM is TS 38.300 V19.4.0 clause 9.2.3.7: the UE executes a configured LTM cell switch when L1-based or L3-based execution conditions are met, without the cell switch command. The rule was written before that text was frozen; it models an L1-based condition like the LTM switch condition: a configured candidate whose unfiltered L1 measurement is better than the serving cell by o_switch_db at every L1 sample for ttt_switch_s; the UE then executes at once (exec_s).

o_prep_db candidate preparation offset [dB] (L3) o_switch_db cell switch offset [dB] (L1), evaluated by the UE ttt_switch_s time the switch condition must hold [s] (0: one L1 sample) ttt_prep_s time-to-trigger of the candidate report [s] n_max largest candidate set (1-8) rel_hys_db release hysteresis [dB] exec_s execution time of the cell switch [s] recovery a failed UE that selects a candidate recovers into it in cho_recovery_s

DAPS

Bases: DAPSRule

Dual active protocol stack (DAPS) handover triggered by Event A3 (3GPP Rel-16).

Specification: TS 38.300 clause 9.2.3.2 (handover; DAPS keeps the source link during the handover), clause 9.2.7 (radio-link failure detection on the source continues until the target is joined), TS 38.331 clause 5.3.5.8.3 (failure of the reconfiguration with sync: fallback to the source when its link has not failed).

Trigger: Event A3 (TS 38.331 clause 5.5.4.4), neighbour better than the serving cell by off_db for ttt_s; report, preparation and command as a baseline handover.

off_db A3 offset [dB] ttt_s time-to-trigger [s] interruption_s user-plane interruption of an execution [s] (Rel-16 aims at 0) fallback fall back to the source if the target cannot be joined and the source SINR is at least Qout source_data the source keeps serving data during the execution

With fallback=False and source_data=False the rule gives the same event log as A3 with the same offset and time-to-trigger.

LTM

Bases: LTMRule

L1/L2-triggered mobility (LTM) with fixed offsets (3GPP Rel-18).

Specification: TS 38.300 clause 9.2.3.5 (LTM: candidate cell configuration, L1 measurement reporting, cell switch command, early timing advance acquisition and RACH-less execution); TS 38.321 (LTM cell switch command MAC CE); TS 38.331 clause 5.5.3.2 (L3 filtering of the measurements that select the candidates).

Candidates: a neighbour whose L3-filtered measurement is better than the serving cell by o_prep_db for ttt_prep_s is reported and configured as a candidate (at most n_max, best first; released rel_hys_db below the offset). Cell switch: the network switches to a configured candidate whose unfiltered L1 measurement is better than the serving cell by o_switch_db at every L1 report for ttt_switch_s; the command reaches the UE after cmd_delay_s, the execution takes exec_s.

o_prep_db candidate preparation offset [dB] (L3) o_switch_db cell switch offset [dB] (L1) ttt_switch_s time the switch condition must hold [s] (0: one L1 report) ttt_prep_s time-to-trigger of the candidate report [s] n_max largest candidate set (1-8) rel_hys_db release hysteresis [dB] cmd_delay_s delay of the cell switch command [s] exec_s execution time of the cell switch [s] recovery a failed UE that selects a candidate recovers into it in cho_recovery_s

BaselineRule

Bases: Rule

Baseline handover triggered by Event A3.

a3_offsets(obs: Observation) -> Union[float, np.ndarray]

A3 offset [dB]: a scalar or an array (U, M).

time_to_trigger(obs: Observation) -> Union[float, np.ndarray]

Time-to-trigger [s]: a scalar, an array (U,) or (since 1.10.0) an array (U, M).

entering(obs: Observation, offsets) -> Optional[tuple]

Own entering condition (since 1.10.0): (condition, score), both (U, M), or None for Event A3.

condition says where the event's entering condition holds (the serving cell and users that are not connected are excluded by the engine); among the cells whose time-to-trigger expires, the one with the highest score becomes the target. The default (None) is Event A3 with offsets.

CLTMRule

Bases: LTMRule

Conditional L1/L2-triggered mobility (Rel-19 work on LTM; available since version 1.6.0).

As LTMRule (candidates configured on L3 reports, cell switch on L1 measurements, RACH-less execution), but the UE evaluates the switch condition itself and executes as soon as it holds, without a cell switch command (as conditional handover does on L3 measurements). cmd_delay_s is not used. The same hooks as LTMRule.

ConditionalRule

Bases: Rule

Conditional handover (Rel-16).

Attributes read by the engine (set them in __init__):

report_offset_db the UE reports a neighbour that is this much better than the serving cell for ttt_prep_s ttt_prep_s time-to-trigger of the reporting condition ttt_exec_s time-to-trigger of the execution condition n_max largest prepared set (at most 8) release_hys_db a prepared cell is released when it falls this far below its preparation offset stick_db ranking bonus of an already prepared cell immediate execute as soon as the configuration arrives (the conditional handover then acts as a baseline one)

prep_offsets(obs: Observation) -> Union[float, np.ndarray]

Preparation offset [dB]: a scalar or an array (U, M).

exec_offsets(obs: Observation) -> Union[float, np.ndarray]

Execution offset [dB] written into a new conditional configuration: scalar or (U, M).

exec_condition(obs: Observation) -> Optional[np.ndarray]

A further execution condition (U, M) that must hold together with the execution offset (since 2.0.0), e.g. condEventD1 / D2 / T1 of TS 38.331 5.5.4.15-16; None (the default): the offset alone. Called at every measurement instant (since 2.1.0), so that a condition with a state stays current.

release_user(u: int) -> None

The conditional configurations of user u were removed (after an execution or a failure; since 2.1.0): reset any state kept for them. The default does nothing.

DAPSRule

Bases: BaselineRule

Dual active protocol stack (DAPS) handover (Rel-16, TS 38.300 clause 9.2.3.2).

Triggered like a baseline handover (hooks BaselineRule.a3_offsets and BaselineRule.time_to_trigger). Attributes read by the engine:

interruption_s user-plane interruption of an execution while the source link carries data (Rel-16 aims at 0 ms) fallback an execution that cannot join the target falls back to the source if the source SINR is at least Qout (TS 38.331 clause 5.3.5.8.3) instead of a re-establishment source_data the source keeps serving data during the execution

LTMRule

Bases: Rule

L1/L2-triggered mobility (Rel-18, TS 38.300 clause 9.2.3.5).

Attributes read by the engine (set them in __init__):

report_offset_db L3 report of a neighbour this much better than serving for ttt_prep_s (candidate configuration) ttt_prep_s time-to-trigger of the L3 reporting condition n_max largest set of candidate cells (at most 8) release_hys_db a candidate is released when it falls this far below its preparation offset stick_db ranking bonus of an already configured candidate ttt_switch_s the L1 switch condition must hold this long cmd_delay_s delay of the cell switch command (MAC CE) exec_s execution time of the cell switch (RACH-less, early TA) recovery a failed UE that selects a candidate recovers into it in cho_recovery_s (otherwise a full re-establishment)

prep_offsets(obs: Observation) -> Union[float, np.ndarray]

Candidate preparation offset [dB] on L3 measurements: a scalar or an array (U, M).

switch_offsets(obs: Observation) -> Union[float, np.ndarray]

Cell switch offset [dB] on L1 measurements (obs.l1_dbm): a scalar or an array (U, M).

Rule

Common part of all rules.

params() -> Dict[str, Any]

Parameters of the rule (written into the results).

reset(ctx: RuleContext) -> None

Called once before a run.

memory_key(obs: Observation) -> Optional[np.ndarray]

Key (U,) under which an attempt prepared now is remembered, or None.

on_outcome(rec: OutcomeRecord) -> None

Outcome of a handover attempt (only called if learns).

Radio-link failure without a handover (only called if learns).

RunResult dataclass

Metrics of one rule on one seed.

fingerprint is the SHA-256 of the canonical event log (see handoverlab.eventlog) and is always set; events holds the log itself only if it was requested. key is the registry key of the rule ("a3", "cho", ...), so that the run can be repeated. trace holds the procedure trace at event-log level "full" (see handoverlab.eventlog).

spec() -> Dict[str, Any]

Rule specification that recreates this rule ({"rule": key, "name": ..., **params}).

row() -> Dict[str, Any]

Flat dictionary for a CSV row: seed, rule, parameters (prefixed param.) and metrics.

World dataclass

n_cells: int property

Number of cells: one per site, or the cells of the extended radio model.

cell_sites: np.ndarray property

(M, 2) positions of the cells' sites [m].

nominal_cell property

Nominal best server at points (the operator's plan, see RadioMap).

cell_center(t: float) -> np.ndarray

(M, 2) reference locations of the cells at time t [m]: the sites, or the moving beam centres (NTN).

load_config(source: Union[str, os.PathLike, Dict[str, Any]]) -> Experiment

Experiment from a file path or a dictionary.

fingerprint(events: Iterable) -> str

SHA-256 (hex) of the canonical CSV of an event log.

read_events(path: str) -> List[dict]

Rows of an event table written by write_events (typed values, in file order).

write_events(results: Sequence, path: str) -> str

Write the event logs of results to path and return the path written.

.parquet writes Parquet if pyarrow is installed; without it the table is written as CSV next to it (.csv instead of .parquet) and a warning is issued. Any other name writes CSV.

make_rule(spec: Dict[str, Any]) -> Rule

Rule from {"rule": <key>, <parameters>...}.

register_rule(key: str)

Class decorator that makes a rule available under key.

builtin_scenario(name: str) -> Scenario

The built-in scenario name.

scenario_card(name: str) -> Dict[str, Any]

The scenario card of name as a dictionary (scenario holds the values that differ from the defaults).

scenario_names(deprecated: bool = False) -> List[str]

Names of the built-in scenarios; deprecated ones (DEPRECATED) only with deprecated=True.

run_experiment(exp: Experiment, log_events: Union[bool, str] = False, progress: Optional[Callable[[str], None]] = None, debug: Optional[bool] = None) -> List[RunResult]

Run an Experiment (scenario, seeds and rules).

run_world(world: World, rules: Sequence[RuleSpec], log_events: Union[bool, str] = False, chunk_steps: int = 1000, debug: Optional[bool] = None, batch: bool = True, progress: Optional[Callable[[float], None]] = None) -> List[RunResult]

Run every rule of rules on world with identical radio samples.

log_events is the event-log level: "off" (or False), "summary" (or True: RunResult.events) or "full" (also RunResult.trace). progress is called after every radio chunk with the share of the run simulated so far (0 to 1).

debug turns on the invariant checks of the engine (see handoverlab.engine.invariants); results are the same either way. With batch (the default) the engines of all rules share one step loop (EngineBatch); results are bit-identical to running them one by one (batch=False), only faster. Engines in debug mode always run one by one.

build_world(scenario: Scenario, seed: int) -> World

Layout, radio map, trajectories, services and background load of one seed.