Rule interface

Base classes, the registry and the built-in rules; see Tutorial 2 for a worked example.

handoverlab.rules

Handover rules and their registry.

Built-in rules are registered under "a3", "cho", "daps", "ltm", "cltm", "a3_bandit", "a3_mro" (since 1.7.0), "a3a5" (since 1.10.0) and "dqn" (since 1.11.0; needs PyTorch: pip install handoverlab[torch]). Your own rule can be registered in code::

from handoverlab.rules import BaselineRule, register_rule

@register_rule("a3_speed")
class SpeedA3(BaselineRule):
    ...

or, from an installed package, through the entry-point group handoverlab.rules in its pyproject.toml::

[project.entry-points."handoverlab.rules"]
a3_speed = "my_rules:SpeedA3"

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.

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

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.

FailureRecord dataclass

Radio-link failure without a handover in flight; the UE re-established in cell.

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).

Observation dataclass

What the network observes at one measurement instant (arrays over users U and cells M).

OutcomeRecord dataclass

Observable outcome of one handover attempt.

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).

RuleContext dataclass

Given to Rule.reset once per run.

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]

A3A5

Bases: BaselineRule

Event A3 within a carrier and Event A5 towards other carriers (multi-carrier model; since 1.10.0).

Trigger towards a cell on the serving carrier: Event A3 (TS 38.331 clause 5.5.4.4), neighbour better than the serving cell by off_db for ttt_s. Trigger towards a cell on another carrier: Event A5 (clause 5.5.4.6), entering conditions A5-1 Mp + Hys < Thresh1 and A5-2 Mn + Ofn + Ocn - Hys > Thresh2 (Ofn = Ocn = 0) held for a5_ttt_s; Mp and Mn are the L3-filtered SS-RSRP per resource element [dBm] of the multi-carrier model. The thresholds apply towards a carrier of higher frequency; a5_down_thresh1_dbm / a5_down_thresh2_dbm (unset: the same) towards a lower one, as a network configures A5 per measurement object. a3_inter also applies A3 towards the other carriers. Report, preparation and command as A3. On one carrier the rule is A3.

off_db A3 offset [dB] ttt_s A3 time-to-trigger [s] a5_thresh1_dbm a5-Threshold1 (serving) [dBm] a5_thresh2_dbm a5-Threshold2 (neighbour) [dBm] a5_hys_db hysteresis of the A5 event [dB] a5_ttt_s A5 time-to-trigger [s] a3_inter A3 also towards cells of other carriers a5_down_thresh1_dbm, a5_down_thresh2_dbm A5 thresholds towards a lower carrier (unset: as above)

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

CHOD2

Bases: CHO

Conditional handover with condEventD2 together with A3 (NTN with moving cells; since 2.0.0).

TS 38.331 V18 5.5.4.15a, Event D2: entering when Ml1 - Hys > Thresh1 (distance of the UE from the serving cell's moving reference location) and Ml2 + Hys < Thresh2 (distance from the candidate's moving reference location); leaving when Ml1 + Hys < Thresh1 or Ml2 - Hys > Thresh2. The reference locations are ctx.cell_center(t): the beam centres on the ground, which move with the satellite; on cells that do not move (terrestrial sites) the event is Event D1 (5.5.4.15). TS 38.331 (CondReconfigToAddMod) has the network configure a second event (A3, A4 or A5) together with D1 or D2, except for a CHO with only D1, D2 or T1; the rule uses D2 with A3. Since 2.1.0 the two events follow TS 38.331 5.3.5.13.4 (V18.11.0): each has its own time-to-trigger (ttt_d2_s is timeToTrigger-r18 of condEventD2, ttt_exec_s that of condEventA3); an event turns fulfilled when its entry condition has held for its time-to-trigger and not fulfilled when its leaving condition has held for it, and the UE executes when both are fulfilled (2.0.0: one time-to-trigger over both conditions together). Distances use the UE's reported position.

d_thresh1_m distanceThreshFromReference1 [m] (serving cell); default: half the 50 km nadir beam diameter of the LEO-600 S-band beams of TR 38.821 Table 6.1.1.1-1 (TS 38.331 gives no default) d_thresh2_m distanceThreshFromReference2 [m] (candidate); the same default hys_m hysteresisLocation [m] (a multiple of 10 m, TS 38.331 HysteresisLocation) ttt_d2_s timeToTrigger of condEventD2 [s] (since 2.1.0; a TimeToTrigger value) Other parameters as CHO. The thresholds are multiples of 50 m (distanceThreshFromReference).

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

BanditA3

Bases: BaselineRule

Event A3 whose offset each cell learns with an epsilon-greedy bandit (a learning-based example).

Trigger: Event A3 (TS 38.331 clause 5.5.4.4) with the offset of the arm that the serving cell plays and a fixed time-to-trigger. The rewards follow the outcome classes of mobility robustness optimisation (TS 38.300 clause 15.5.2: too-late, too-early and wrong-cell handovers) plus unnecessary handovers: useful +1, unnecessary -w_unnecessary, early or late -w_failure; a link failure without a handover counts as late for the source cell.

Every cell plays one arm (an A3 offset of arms_db) for epoch outcomes of handovers from it, then updates the mean reward of that arm and picks the next arm: with probability epsilon at random (from ctx.rng), otherwise an arm not yet played or the best one. An outcome reaches the arm that its source cell plays when the outcome is known, so an arm change can credit a few outcomes to the next arm. No dependency beyond NumPy; the learned values are kept per run.

arms_db candidate A3 offsets [dB] ttt_s time-to-trigger [s] epsilon exploration probability epoch outcomes per evaluation of an arm w_unnecessary penalty of an unnecessary handover w_failure penalty of an early or late handover or a link failure

With a single arm the rule is A3 with that offset.

Since 1.11.0 the rule keeps a record of what it learned (it does not change any decision): outcomes lists (t, cell, arm, reward) of every credited outcome and epochs lists (t, cell, arm) of every finished epoch.

reward(label: int) -> float

Reward of an outcome label.

choose(cell: int) -> int

Next arm of cell: random with probability epsilon, else an unplayed or the best arm.

DQNA3

Bases: BaselineRule

Event A3 whose offset a deep Q-network chooses per user (see the module text).

offsets_db the A3 offsets the network chooses from [dB] ttt_s time-to-trigger [s] mode "train" (learn, epsilon-greedy) or "test" (greedy, no learning) weights file of trained weights to start from (or None: random initial weights) hidden units of each of the two hidden layers lr learning rate of the Adam optimiser gamma discount of the next decision step epsilon exploration probability while training batch transitions per gradient step buffer size of the replay buffer train_every new transitions per gradient step target_every gradient steps between copies to the target network w_unnecessary penalty of an unnecessary handover w_failure penalty of an early or late handover or a link failure

save(path: str) -> None

Write the weights of the Q-network to path (to be used as weights).

q_values(states: np.ndarray) -> np.ndarray

Q-values (N, K) of states (N, 5).

reward(label: int) -> float

Reward of an outcome label.

MROA3

Bases: BaselineRule

Event A3 whose offset per cell pair is tuned by mobility robustness optimisation.

off_db A3 offset before any tuning [dB] ttt_s time-to-trigger [s] step_db change of a pair's offset per window [dB]; 1 dB by default (since 2.1.0; 0.5 before), so that every offset is a value of Q-OffsetRange (TS 38.331 V18.11.0: whole dB from -5 to 5, steps of 2 dB beyond 6 dB); handoverlab.standard.check_rule checks a setting window_s length of a counting window [s] min_db, max_db bounds of a pair's offset [dB]; -4 and 4 by default, within Q-OffsetRange (TS 38.331: -24 to 24 dB) and the handover-trigger deviation of MRO (TS 28.313 Table 7.1.2.2.3-1: -20 to 20 in 0.5 dB, i.e. +-10 dB) min_events counted outcomes a pair needs in a window before its offset moves count_pingpong count a return to the previous cell within pingpong_s as too early pingpong_s ping-pong time [s]; None: protocol.pingpong_s of the scenario count_unnecessary also count every unnecessary handover as too early

update() -> None

Close a window: move the offsets of the pairs with enough outcomes, restart the counts.

register_rule(key: str)

Class decorator that makes a rule available under key.

get_rule_class(key: str) -> Type[Rule]

Rule class registered under key (built-in, decorated or entry point).

rule_key(rule) -> Optional[str]

Registry key of a rule object or class, or None if it is not registered.

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

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

handoverlab.rules.base

The handover-rule interface.

A rule decides; the engine simulates. The engine calls the hooks of a rule at every measurement instant and after outcomes; the rule never calls the engine. Hooks receive an Observation with what a network can observe: filtered measurements, the serving cell, the reported position, speed and heading, and the nominal site plan from RuleContext. A rule never sees the true SINR, the buildings or the cell load.

Two families exist:

BaselineRule Baseline handover: Event A3 held for the time-to-trigger, report, preparation, handover command. Hooks BaselineRule.a3_offsets and BaselineRule.time_to_trigger.

ConditionalRule Conditional handover (3GPP Rel-16): the network prepares up to n_max candidate cells; each conditional configuration carries an execution offset; the UE executes on its own when the condition holds for ttt_exec_s. Hooks ConditionalRule.prep_offsets and ConditionalRule.exec_offsets.

Two further families (since 1.0.0rc2) reuse these hooks:

DAPSRule Dual active protocol stack handover (Rel-16): triggered like a baseline handover, but the source link keeps carrying data during the execution and the UE falls back to the source if the target cannot be joined. Hooks of BaselineRule.

LTMRule L1/L2-triggered mobility (Rel-18): candidate cells are configured in advance from L3 reports; the network switches the UE to a candidate on L1 measurement reports with a MAC command. Hooks LTMRule.prep_offsets and LTMRule.switch_offsets.

Rules that learn from outcomes set learns = True and implement Rule.on_outcome and Rule.on_link_failure. A rule that needs randomness must use ctx.rng, never NumPy's global state.

Stability: this interface has the version handoverlab.stability.RULE_API_VERSION (1). Within a major release, the hooks keep their names and arguments, and the fields of RuleContext, Observation, OutcomeRecord and FailureRecord are only added to, never removed or renamed; a rule written for 1.0 runs unchanged on every 1.x release (see docs/stability.md).

RuleContext dataclass

Given to Rule.reset once per run.

Observation dataclass

What the network observes at one measurement instant (arrays over users U and cells M).

OutcomeRecord dataclass

Observable outcome of one handover attempt.

FailureRecord dataclass

Radio-link failure without a handover in flight; the UE re-established in cell.

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).

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.

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).

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.

handoverlab.rules.builtin

Built-in rules.

A3 Event A3 baseline handover CHO Rel-16 conditional handover with fixed offsets DAPS Rel-16 DAPS handover triggered by Event A3 (since 1.0.0rc2) LTM Rel-18 L1/L2-triggered mobility with fixed offsets (since 1.0.0rc2) BanditA3 Event A3 whose offset is learned per cell by an epsilon-greedy bandit (since 1.0.0rc2) A3A5 Event A3 within a carrier, Event A5 between carriers (since 1.10.0)

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

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

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

CHOD2

Bases: CHO

Conditional handover with condEventD2 together with A3 (NTN with moving cells; since 2.0.0).

TS 38.331 V18 5.5.4.15a, Event D2: entering when Ml1 - Hys > Thresh1 (distance of the UE from the serving cell's moving reference location) and Ml2 + Hys < Thresh2 (distance from the candidate's moving reference location); leaving when Ml1 + Hys < Thresh1 or Ml2 - Hys > Thresh2. The reference locations are ctx.cell_center(t): the beam centres on the ground, which move with the satellite; on cells that do not move (terrestrial sites) the event is Event D1 (5.5.4.15). TS 38.331 (CondReconfigToAddMod) has the network configure a second event (A3, A4 or A5) together with D1 or D2, except for a CHO with only D1, D2 or T1; the rule uses D2 with A3. Since 2.1.0 the two events follow TS 38.331 5.3.5.13.4 (V18.11.0): each has its own time-to-trigger (ttt_d2_s is timeToTrigger-r18 of condEventD2, ttt_exec_s that of condEventA3); an event turns fulfilled when its entry condition has held for its time-to-trigger and not fulfilled when its leaving condition has held for it, and the UE executes when both are fulfilled (2.0.0: one time-to-trigger over both conditions together). Distances use the UE's reported position.

d_thresh1_m distanceThreshFromReference1 [m] (serving cell); default: half the 50 km nadir beam diameter of the LEO-600 S-band beams of TR 38.821 Table 6.1.1.1-1 (TS 38.331 gives no default) d_thresh2_m distanceThreshFromReference2 [m] (candidate); the same default hys_m hysteresisLocation [m] (a multiple of 10 m, TS 38.331 HysteresisLocation) ttt_d2_s timeToTrigger of condEventD2 [s] (since 2.1.0; a TimeToTrigger value) Other parameters as CHO. The thresholds are multiples of 50 m (distanceThreshFromReference).

BanditA3

Bases: BaselineRule

Event A3 whose offset each cell learns with an epsilon-greedy bandit (a learning-based example).

Trigger: Event A3 (TS 38.331 clause 5.5.4.4) with the offset of the arm that the serving cell plays and a fixed time-to-trigger. The rewards follow the outcome classes of mobility robustness optimisation (TS 38.300 clause 15.5.2: too-late, too-early and wrong-cell handovers) plus unnecessary handovers: useful +1, unnecessary -w_unnecessary, early or late -w_failure; a link failure without a handover counts as late for the source cell.

Every cell plays one arm (an A3 offset of arms_db) for epoch outcomes of handovers from it, then updates the mean reward of that arm and picks the next arm: with probability epsilon at random (from ctx.rng), otherwise an arm not yet played or the best one. An outcome reaches the arm that its source cell plays when the outcome is known, so an arm change can credit a few outcomes to the next arm. No dependency beyond NumPy; the learned values are kept per run.

arms_db candidate A3 offsets [dB] ttt_s time-to-trigger [s] epsilon exploration probability epoch outcomes per evaluation of an arm w_unnecessary penalty of an unnecessary handover w_failure penalty of an early or late handover or a link failure

With a single arm the rule is A3 with that offset.

Since 1.11.0 the rule keeps a record of what it learned (it does not change any decision): outcomes lists (t, cell, arm, reward) of every credited outcome and epochs lists (t, cell, arm) of every finished epoch.

reward(label: int) -> float

Reward of an outcome label.

choose(cell: int) -> int

Next arm of cell: random with probability epsilon, else an unplayed or the best arm.

A3A5

Bases: BaselineRule

Event A3 within a carrier and Event A5 towards other carriers (multi-carrier model; since 1.10.0).

Trigger towards a cell on the serving carrier: Event A3 (TS 38.331 clause 5.5.4.4), neighbour better than the serving cell by off_db for ttt_s. Trigger towards a cell on another carrier: Event A5 (clause 5.5.4.6), entering conditions A5-1 Mp + Hys < Thresh1 and A5-2 Mn + Ofn + Ocn - Hys > Thresh2 (Ofn = Ocn = 0) held for a5_ttt_s; Mp and Mn are the L3-filtered SS-RSRP per resource element [dBm] of the multi-carrier model. The thresholds apply towards a carrier of higher frequency; a5_down_thresh1_dbm / a5_down_thresh2_dbm (unset: the same) towards a lower one, as a network configures A5 per measurement object. a3_inter also applies A3 towards the other carriers. Report, preparation and command as A3. On one carrier the rule is A3.

off_db A3 offset [dB] ttt_s A3 time-to-trigger [s] a5_thresh1_dbm a5-Threshold1 (serving) [dBm] a5_thresh2_dbm a5-Threshold2 (neighbour) [dBm] a5_hys_db hysteresis of the A5 event [dB] a5_ttt_s A5 time-to-trigger [s] a3_inter A3 also towards cells of other carriers a5_down_thresh1_dbm, a5_down_thresh2_dbm A5 thresholds towards a lower carrier (unset: as above)

handoverlab.rules.mro

Event A3 with mobility robustness optimisation (MRO), available since version 1.7.0.

MRO is the self-organising-network function that tunes handover parameters from the observed outcomes of handovers (TS 38.300 clause 15.5.2; SON for 5G in TS 28.313 clause 7.1.2; the SON concepts of TS 32.500). The rule a3_mro keeps one cell individual offset (CIO) for every pair of serving and neighbour cell and moves it in fixed steps once per time window, as counter-based MRO schemes do: towards an earlier handover when too-late handovers dominate the window, towards a later one when too-early, wrong-cell and ping-pong handovers dominate.

Assumptions, made explicit:

  • The three failure classes are those of TS 38.300 clause 15.5.2, taken from the outcome labels that HandoverLab feeds to learning rules: a late handover and a link failure without a handover after which the UE re-establishes in a neighbour count as too late for (source, cell); a too-early and a wrong-cell handover count as too early for (source, target).
  • A ping-pong is a return to the previous cell within pingpong_s (default: protocol.pingpong_s of the scenario); the rule sees it in the serving cell of the user, as a network sees it in the cell history of the UE, and counts it as too early for the pair of the first handover. The wider HandoverLab label unnecessary (short stay or low gain) is not an MRO counter; count_unnecessary adds it as too early (a ping-pong then counts twice).
  • The update is deterministic: at the first measurement instant at or after the end of a window, every pair with at least min_events counted outcomes moves by step_db in the direction of the larger count (no move on a tie), the offset stays in [min_db, max_db], and all counts restart.
  • The offset is added to the A3 offset of the pair (positive: later handover), as a CIO enters the A3 condition of TS 38.331 clause 5.5.4.4. Offsets are kept per run; the time-to-trigger is not tuned.

With step_db = 0 the rule is A3 with off_db and ttt_s.

MROA3

Bases: BaselineRule

Event A3 whose offset per cell pair is tuned by mobility robustness optimisation.

off_db A3 offset before any tuning [dB] ttt_s time-to-trigger [s] step_db change of a pair's offset per window [dB]; 1 dB by default (since 2.1.0; 0.5 before), so that every offset is a value of Q-OffsetRange (TS 38.331 V18.11.0: whole dB from -5 to 5, steps of 2 dB beyond 6 dB); handoverlab.standard.check_rule checks a setting window_s length of a counting window [s] min_db, max_db bounds of a pair's offset [dB]; -4 and 4 by default, within Q-OffsetRange (TS 38.331: -24 to 24 dB) and the handover-trigger deviation of MRO (TS 28.313 Table 7.1.2.2.3-1: -20 to 20 in 0.5 dB, i.e. +-10 dB) min_events counted outcomes a pair needs in a window before its offset moves count_pingpong count a return to the previous cell within pingpong_s as too early pingpong_s ping-pong time [s]; None: protocol.pingpong_s of the scenario count_unnecessary also count every unnecessary handover as too early

update() -> None

Close a window: move the offsets of the pairs with enough outcomes, restart the counts.

handoverlab.rules.dqn

A deep Q-network that chooses the A3 offset of every user (optional PyTorch; since 1.11.0).

pip install handoverlab[torch] installs PyTorch; import handoverlab does not need it, and only building the rule imports it.

The rule is Event A3 (TS 38.331 clause 5.5.4.4) with a fixed time-to-trigger whose offset a Q-network picks per user from offsets_db. A decision step of a user lasts from one handover (or link failure) to the next:

  • state, from what the network observes: the serving cell's L3 RSRP, the three best neighbour-minus-serving differences and the reported speed (scaled);
  • action: the A3 offset used until the user's next handover;
  • reward, when the outcome of that handover is known: useful +1, unnecessary -w_unnecessary, early or late -w_failure; a link failure without a handover counts as -w_failure (the labels of mobility robustness optimisation, TS 38.300 clause 15.5.2, as for a3_bandit);
  • next state: the state at the instant after the handover.

Learning (mode="train") is Q-learning with a replay buffer and a target network (Mnih et al., Nature 518, 2015): epsilon-greedy actions, one gradient step on a random batch every train_every new transitions, the target network copied every target_every steps. mode="test" acts greedily and does not learn. weights names a file of trained weights (written by train); training starts from it if given.

Reproducibility: CPU only, torch.use_deterministic_algorithms(True), one thread during every forward and backward pass, the network initialised under a seed drawn from ctx.rng and batches drawn from ctx.rng; PyTorch's and NumPy's global random states are left unchanged. Two runs on one machine and PyTorch version give identical event logs; other hardware or versions may give other results, so there is no golden fingerprint for this rule.

DQNA3

Bases: BaselineRule

Event A3 whose offset a deep Q-network chooses per user (see the module text).

offsets_db the A3 offsets the network chooses from [dB] ttt_s time-to-trigger [s] mode "train" (learn, epsilon-greedy) or "test" (greedy, no learning) weights file of trained weights to start from (or None: random initial weights) hidden units of each of the two hidden layers lr learning rate of the Adam optimiser gamma discount of the next decision step epsilon exploration probability while training batch transitions per gradient step buffer size of the replay buffer train_every new transitions per gradient step target_every gradient steps between copies to the target network w_unnecessary penalty of an unnecessary handover w_failure penalty of an early or late handover or a link failure

save(path: str) -> None

Write the weights of the Q-network to path (to be used as weights).

q_values(states: np.ndarray) -> np.ndarray

Q-values (N, K) of states (N, 5).

reward(label: int) -> float

Reward of an outcome label.

available() -> bool

True if PyTorch can be imported.

features(obs) -> np.ndarray

State of every user (U, 5): serving RSRP, three best neighbour differences and speed, scaled to about +-1.

train(scenario, seeds: Sequence[int], path: Optional[str] = None, **params) -> DQNA3

Train one DQNA3 over the worlds of seeds in turn (keeping its network) and save it to path.

Use seeds that never appear among the test seeds. Returns the trained rule; test it with {"rule": "dqn", "mode": "test", "weights": path}.