Skip to content

ran-controller: observe-only A3 (CHO) handover evaluation - #528

Merged
josephnef merged 2 commits into
masterfrom
feat/m6-cu-a3
Nov 5, 2026
Merged

josephnef merged 2 commits into
masterfrom
feat/m6-cu-a3

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Part of M6 (#260), epic #254. Step 1 of the integration: the CU evaluates handovers but does not act on them.

  • What it does: each plan round, ran-controller runs the merged A3Trigger for every UE. It compares the serving DU's filtered measurement (F1 v2 meas) against every other DU's filtered heard measurement of that UE. An absent or stale neighbour counts as no evidence.
  • Logs: ran.ho_candidate when the A3 condition has held for the time-to-trigger, ran.ho_clear when it stops. Nothing is pushed to the DUs.
  • Knobs: RAN_HO, RAN_A3_METRIC (rssi|snr), RAN_A3_OFFSET_DB, RAN_A3_HYST_DB, RAN_A3_TTT_MS. The effective time-to-trigger rounds up to RAN_PLAN_MS.

Rig (testrig m5_ran_coord.sh, two cells, stationary UEs, 90 s each):

offset candidates
3 dB (default) 0
−2 dB (config check) exactly 1: cell 1's UE toward cell 2, serving −54.5 / target −54.0 dBm, held 1.0 s, no clear or repeat

At −2 dB, cell 2's UE, which is about 15 dB stronger at its own DU, produced no candidate. The M5 coordination verdict passes in both runs.

Next steps (separate PRs): grant migration over F1, then same-channel DAPS with dedup.

🤖 Generated with Claude Code

…s' filtered reports

For every UE, the CU runs src/cell/Handover.h A3Trigger each plan round:
the serving DU's own filtered measurement (F1 v2 meas) against every other
DU's heard one. An absent or stale neighbour is no evidence. It logs
ran.ho_candidate when the condition has held for the time-to-trigger, and
ran.ho_clear when it stops. Nothing is pushed to the DUs.
RAN_HO, RAN_A3_METRIC (rssi|snr), RAN_A3_OFFSET_DB, RAN_A3_HYST_DB,
RAN_A3_TTT_MS. The effective time-to-trigger rounds up to RAN_PLAN_MS.

Rig (testrig m5_ran_coord.sh, two cells, stationary UEs, 90 s):
- the default 3 dB offset gives 0 candidates;
- at -2 dB, exactly one: cell 1's UE toward cell 2 (serving -54.5, target
  -54.0 dBm, held 1.0 s), with no clear or repeat; cell 2's UE (about
  15 dB stronger at its own DU) none.
The M5 verdict passes in both runs.

Refs #260

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Evaluate A3 handover candidates without initiating handovers

✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Evaluate serving and neighbouring DU measurements for each UE during controller plan rounds.
• Log handover candidates and clears without sending handover commands to DUs.
• Add configurable A3 metric, offset, hysteresis, time-to-trigger, and enable switch.
Diagram

graph TD
  D["DU F1 reports"] --> F{"Fresh serving report?"} -->|yes| A["Per-UE A3 evaluation"] --> L["Candidate or clear logs"]
  F -->|no| X["Skip evaluation"]
  D --> P["Existing schedule planner"] --> S["DU schedules"]
Loading
High-Level Assessment

Reusing the existing A3Trigger in the controller’s plan loop is appropriate for an observe-only integration: it preserves one decision point without changing the DU protocol or schedule path. A separate evaluator service or DU-side decisions would add coordination complexity without benefiting this stage.

Files changed (1) +99 / -0

Enhancement (1) +99 / -0
main.cppAdd observe-only A3 evaluation to controller plan rounds +99/-0

Add observe-only A3 evaluation to controller plan rounds

• Reads and validates handover settings, then maintains an A3 trigger and current candidate per serving-cell/UE pair. Each plan round compares fresh serving measurements with available neighbouring reports and emits JSONL candidate or clear events on transitions. No handover instruction is sent to a DU.

tools/ran-controller/main.cpp

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Missing UEs retain handover timers ✓ Resolved
Description
The A3 loop skips a stale serving DU and only evaluates UEs present in its current rep.meas,
leaving their existing HoState untouched when serving evidence disappears. If a UE stops appearing
in reports and later reappears, its old time-to-trigger can produce an immediate candidate, while an
earlier candidate receives no clear event during the gap.

2. Forged reports can stall the controller ✓ Resolved
Description
The new A3 loop scans all retained DU states for every measured UE, including DU IDs that are not in
RAN_DUS, making round cost grow quadratically as distinct reports arrive. With the default empty
RAN_F1_KEY, a sender able to reach the UDP port can inject reports under many DU IDs; those IDs
remain in dus even after their reports become stale.


Remediation recommended

3. Candidates can use expired measurements ✓ Resolved
Description
The fresh predicate treats every measurement in a received DU report as valid for three seconds
after reception, without knowing when its underlying radio sample was taken. A measurement already
near the DU filter's two-second expiry at report creation can continue a time-to-trigger timer and
appear in a candidate after that expiry.

4. Replaced handover candidates never clear ✓ Resolved
Description
The logging branch emits a new ran.ho_candidate when tgt changes, but emits ran.ho_clear only
when tgt is empty. If the old neighbour stops qualifying while another already meets
time-to-trigger, consumers receive a candidate for the new target without a clear for the old one.

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display > Comment parts

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

- Only the configured RAN_DUS: a report from any other DU id creates no
  state, and the A3 loop walks only configured DUs.
- F1 v3: every measurement carries its sample age at report time
  (MeasTable -> fill_report_measurements). A value counts only until its
  age plus the report's age passes RAN_MEAS_STALE_MS.
- A UE with no valid serving measurement in a round has its A3 timers
  dropped and any logged candidate cleared (why: no_serving), so a gap
  cannot carry an old time-to-trigger.
- A direct switch between targets logs the clear (why: retarget) before
  the new candidate.
- selftests: ages through the producer and the wire, v3 framing.

Rig (m5_ran_coord.sh, 90 s each):
- default: 0 candidates;
- offset -2 dB: one candidate (cell 1's UE toward cell 2), then a
  no_serving clear when the UEs stopped.
The M5 verdict passes in both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@josephnef

Copy link
Copy Markdown
Collaborator Author

Qodo review addressed in 66bbbf0:

  1. Missing UEs: a UE with no valid serving measurement in a round has its A3 timers dropped and any logged candidate cleared (why: no_serving). On the rig, the candidate cleared when the UEs stopped.
  2. Unconfigured DUs: reports from DU ids outside RAN_DUS create no state, and the A3 loop walks only configured DUs.
  3. Expired measurements: F1 v3 carries every measurement's sample age at report time, and the CU counts a value only until age plus report age passes RAN_MEAS_STALE_MS. Tested through the producer and the wire.
  4. Retarget: a direct target switch logs a clear (why: retarget) before the new candidate.

Rig: default thresholds gave 0 candidates. At -2 dB there was one candidate, then a no_serving clear at the end. The M5 verdict passes in both runs.

@josephnef

Copy link
Copy Markdown
Collaborator Author

Qodo findings addressed (see the reply above)

@josephnef josephnef added the skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision) label Nov 5, 2026
@josephnef
josephnef merged commit 24cded3 into master Nov 5, 2026
27 of 28 checks passed
@josephnef
josephnef deleted the feat/m6-cu-a3 branch November 5, 2026 12:54
josephnef added a commit that referenced this pull request Nov 5, 2026
…itional fallback (#530)

Part of M6 (#260), epic #254. Step 2b: handover execution on top of #528
(observe-only A3) and #529 (command and IE). Full write-up in
`docs/cell-m6.md`.

- **CU (`RAN_HO_EXECUTE=1`):**
- an A3 candidate becomes a `HoCommand`: the target's free id and
position, the switch instant and the TSF delta;
- **two-phase:** sent to the target first, to the source on the target's
ack, and a cancel goes to the target if the source refuses;
  - `ran.ho_done` when the target reports the UE.
- **Target DU:** pre-admits the UE under the commanded id and publishes
its grant for the switch instant.
- **Source DU:** airs the tagged IE until the switch, then releases the
UE only if it stopped uplinking there; otherwise it keeps it.
- **UE:** at the instant it shifts its clock (`TsfSync::ShiftRemote`,
new, selftested), takes its target identity and runs the target's
buffered map. **Conditional:** with no target grant within 10
superframes it returns to the source.
- **Fix:** a DU with no UE now paces its main loop. Its superframe
count, and so its reported network superframe, used to run away.

Rig (two PTP cells; the crossing is forced with `RAN_A3_OFFSET_DB=-2`,
as the UEs are stationary):

| | normal | target DU killed right after staging |
|---|---|---|
| switch | source sf 143 → target sf 137 (= the target's effective sf) |
source sf 172 |
| target hears the UE | in slot from the first superframe after:
1115/1117 | — |
| outcome | committed; source released at +21; `ran.ho_done` +5 | UE
fell back at +10; source kept it, 1092/1092 in slot |

ctest 92/92. Not yet: DAPS (duplicated uplink plus `SeqDedup`), an
emulated moving crossing, and different-channel handover.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@josephnef josephnef mentioned this pull request Nov 5, 2026
3 of 5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant