# AP interference / airtime-reduction model — design + data

Goal (per Mike): a fleet/site-level model that decides **which AP radios to disable, and where to
reduce power**, to cut total airtime contention while preserving client coverage. Per AP **per
radio, all bands** (not 2.4-only, not per-client). Inputs: each AP's neighbor/overlap relationships
+ historical client connections + airtime.

## Data — it's ALL in the controller (corrected 2026-06-15)
First pass only checked the `ace` DB (config/current state) and wrongly concluded the history
"isn't there." It is — in **`ace_stat`** (1.5 GB of accumulated time-series) and the
`wifi_connectivity_event` collection. **No external collector is needed**; the controller already
retains it. Three databases on the UOS Mongo (port 27117):

| DB | Holds | Use |
|---|---|---|
| `ace` | config + current state | radio_table (channel/band/width/power/min_rssi), `rogue` (FOREIGN interference), `channelplan`, device/floorplan |
| **`ace_stat`** | **time-series history** | the model's airtime + client history (below) |
| `ace_audit` | audit log | — |

### `ace_stat.stat_hourly` / `stat_daily` (o:'ap') — per-AP per-band airtime history
Flat per-band fields (`ng`=2.4, `na`=5, `6e`=6): **`<band>-cu_total`** (channel utilization %),
**`<band>-cu_interf`** (airtime lost to interference %), **`<band>-cu_self_rx/tx`**, **`<band>-num_sta`**,
`<band>-satisfaction`, `<band>-tx_retries`. Plus `ap` (AP mac), `site_id`, `time` (ms epoch). This
is the historical airtime/interference/load profile per AP per band — the core "who's contending"
signal. (Also o:'user' per-client retries/anomalies; o:'site' rollups.)

### `ace_stat.wifi_connectivity_event` — the roam graph (empirical AP adjacency)
Each doc: `from_endpoint{mac(AP), channel, channel_width, band, rssi}`, `to_endpoint{mac(AP), ...}`,
`client_mac`, `successful`, `time`, `_class:WIFI_ROAMING`. Every roam is an **edge between two APs a
real client handed off between** → empirical coverage overlap/adjacency, weighted by volume, with
the handoff RSSI (coverage quality). This is the "historical connections → which APs cover the same
space" signal, and it's better than a raw RF scan because it reflects where clients actually move.

### Still useful from `ace` / setup
- `rogue` aggregate = FOREIGN co-channel interference per channel (Plane-1 audit).
- **Floorplan coords**: not set today (0/77 APs placed). Mike is willing to use the floorplan
  feature → once APs are placed, `device` x/y gives true physical distance for adjacency edges
  (complements the roam graph, esp. for APs that rarely roam-share).
- Live Network API (`stat/device`/`stat/sta`) still adds *current* utilization + the AP's live
  RF-neighbor table; nice-to-have for before/after validation, not required to build the model.

## Model
Per band `b`, over a history window (default 7d):
- **Per-radio airtime**: avg `cu_total`, avg `cu_interf`, avg `num_sta` from `stat_hourly`.
- **Overlap/redundancy**: roam volume per AP and per AP-pair from `wifi_connectivity_event` (high
  roam-share to neighbors ⇒ a client leaving this radio has somewhere to land ⇒ redundant).
- **Score (v1, `model-rank.sh`)**: `(cu_total + cu_interf) * log(1+roams) / (clients+1)` — high =
  this radio burns airtime in a contended band AND its clients are mobile/redundant ⇒ shrink or
  disable. Hint: **DISABLE** when high roam + near-zero clients; **POWER-DOWN** when busy but holds
  some unique load.
- **v2 (greedy, coverage-safe)**: iteratively disable the top candidate, recompute overlap so we
  never open a hole (stop when a radio's clients would lose their only good-RSSI neighbor), and add
  floorplan-distance edges once APs are placed. Output = ranked per-AP-per-radio actions with the
  metric that justified each, applied per zone with before/after validation.

## v1 result (Cascades 2.4GHz, 7d) — the smoking gun
`model-rank.sh cascades 7 ng`: 2.4 radios run **cu_total 74–94%, cu_interf 61–81%, ~0.3–2.6 clients
each** across 75 APs. Translation: 2.4 is saturated and mostly interference, serving almost no one —
a textbook case to disable 2.4 on most APs and power down the rest. Run `na`/`6e` for the 5/6GHz
picture (expected: keep, with 6GHz the clean capacity band).

## Materials matter — and why the roam graph already handles them
Cascades has **reinforced-concrete + steel-sheet walls across the hallways** that kill signal on all
bands (rooms either side of a hall, and floor-to-floor, behave normally; **across a hall is nearly
opaque**). This is why **geometric distance must never be the redundancy signal on its own**: two APs
on opposite sides of a hall are *close* but RF-*isolated*. The **roam graph is materials-aware by
construction** — clients can't roam across the steel wall, so those APs simply never appear as
neighbors, and the model won't call them redundant. Corollaries baked into the design:
- Distance/floorplan (once APs are placed) is only a **prior**; an AP pair counts as redundant **only
  with RF/roam evidence** (bidirectional roams at adequate p25 RSSI). Never disable on proximity alone.
- AP **room parity (odd/even)** ≈ hallway side at Cascades — a useful prior to avoid pairing across-hall APs.
- The **live AP-to-AP RF neighbor table (API wireup)** *directly measures* this attenuation — the gold
  standard for the coverage graph, and the reason it's worth wiring.

## Hardening from the multi-model review (Grok + Gemini)
Applied: bidirectional roam requirement (not one-way "escape"); band-specific p25 RSSI bars
(2.4 −68 / 5 −72 / 6 −75); **load-shift simulation** (only disable if a strong neighbor stays < 85%
after absorbing the cu_self it inherits — avoids "capacity cascades"); benefit = `cu_interf` (+ a
normalized `tx_retries` thrash term), with `cu_self` treated as transfer cost; 40%/zone disable cap;
**stepwise** output (power-down → observe → disable). `tx_retries` is a raw count → normalized by
`wifi_tx_attempts`.

## Key empirical finding (Cascades): power-down now, disable later
The **airtime data is rich and unambiguous** → `optimize-radios.sh cascades ng` recommends
**power-down on 74/75 2.4 radios** (all 74–94% utilized, 61–81% interference, ~1 client — the safe big
win; power-down keeps the BSSID). But the **roam data is too sparse** to prove coverage redundancy for
**disables** (almost no AP has a strong bidirectional roam-neighbor), so the optimizer correctly
recommends **0 disables until coverage evidence exists**. That evidence is exactly the **live AP-to-AP
RF neighbor table** → **the API wireup is the enabler for confident radio *disables*** (and for
before/after validation). So the rollout is: Phase A power-down (now) → Phase B re-measure (live API)
→ Phase C disable the radios the RF table proves redundant.

## Status
- `scripts/audit-site.sh` — config + foreign-interference audit (Plane 1).
- `scripts/model-rank.sh` — **v1 airtime-reduction ranker from real history** (this doc). Works now.
- Next: v2 greedy coverage-safe optimizer + floorplan-distance edges (after APs are placed on the
  floorplan) + optional live-API before/after validation.
