# Network metrics
The IO-HMM is the **behavioral** layer. Once we have a state trajectory
(latent or fitted), we can ask how that behavior translates into
**operational** outcomes: how much time vehicles lose to congestion, how
peaked the road network gets, whether shelters overflow, and how many
households are caught en-route at landfall.
These metrics are computed *post-hoc* from saved simulation arrays. They do
not feed back into the DGP and are not used in inference — they are a
read-only summary of `SimulationBundle` outputs.
The reference implementation lives in
[`src/iohmm_evac/network/metrics.py`](../src/iohmm_evac/network/metrics.py)
(per-array math) and
[`src/iohmm_evac/network/apply.py`](../src/iohmm_evac/network/apply.py)
(`SimulationBundle` → `NetworkMetrics`).
---
## Definitions
Let:
* $S_{i,t}$ — the latent state of household $i$ at hour $t$.
* $X_{i,t}$ — the cumulative displacement of household $i$ at hour $t$.
* $\mathrm{evac\_path}_i \in \{\text{NONE}, \text{AWAY}, \text{HOME}\}$ —
set when the household first leaves PR.
* $T$ — the simulation horizon (i.e. landfall).
* $N_{\text{cap}}$ — network capacity (per-hour ER population that
saturates congestion). From `feedback.n_cap` in the sidecar config.
* $K$ — shelter capacity. From `feedback.shelter_capacity`.
* $v_{\text{free}} = 40$ km/h, $\alpha = 0.6$ — free-flow speed and
congestion penalty. From `emissions.v_free` and
`emissions.congestion_penalty`.
### Total delay (hours)
$$
\Delta = \sum_{i,t : S_{i,t}=\text{ER}}
\delta_{i,t} \cdot \left(\frac{1}{v^{\text{eff}}_t} - \frac{1}{v_{\text{free}}}\right),
$$
where
$$
\delta_{i,t} = \max(X_{i,t} - X_{i,t-1}, 0),
\qquad
v^{\text{eff}}_t = v_{\text{free}}\,(1 - \alpha\,c_t),
\qquad
c_t = \min\!\left(\frac{\#\{i : S_{i,t-1}=\text{ER}\}}{N_{\text{cap}}}, 1\right).
$$
Only ER hours contribute (other states are stationary). $c_t$ is recomputed
from the lagged state vector exactly as in the DGP's feedback step, so the
metric is a faithful post-hoc accounting — never a re-simulation.
### Peak EnRoute share
$$
\text{peak\_enroute\_share} = \max_t \frac{\#\{i : S_{i,t}=\text{ER}\}}{N},
\qquad
\text{peak\_enroute\_hour} = \arg\max_t \frac{\#\{i : S_{i,t}=\text{ER}\}}{N}.
$$
The maximum population share simultaneously en-route, and the hour at which
that maximum is attained. Captures the worst-case load on the road network.
Build 3.5 replaced the original *clearance time* metric with this one.
Clearance time was defined as the first hour at which the en-route
population dropped to zero, defaulting to the horizon $T$ when the
population never fully cleared. In every scenario at default $N=10000$,
$T=120$, the population does not clear by landfall — clearance time pegged
at $T$ for all four scenarios, making the metric uninformative for
cross-scenario comparison. Peak EnRoute share is well-defined regardless of
whether the population clears, captures the same operational concern ("how
stressed is the road network at the worst moment?"), and the accompanying
peak hour tells the chapter reader *when* the stress hits.
The shared helper :func:`peak_enroute_share_and_hour` lives in
`network/metrics.py` and is reused by `SimulationResult.summary()` so the
two code paths cannot drift.
### Shelter overflow (count)
$$
\mathcal{O} = \max\!\bigl(0, N_{\text{arrivals\_away}} - K\bigr),
$$
where $N_{\text{arrivals\_away}}$ is the cumulative number of households
that ever entered SH while their `evac_path` was `away`. This is a coarse
operational proxy: it assumes capacity $K$ is shared across all evacuating
households and is consumed monotonically. Build 4 may refine the
accounting, but the chapter's framing only needs the headline number.
### Failed evacuations (count)
$$
\mathcal{F} = \#\{i : S_{i,T} = \text{ER}\}.
$$
Households still on the road at landfall. Same number that already appears
in `report summary`'s `share_failed_evacuation`, surfaced here as a
network-side count for Fig. 5.
---
## Per-hour diagnostics
`NetworkMetrics` also exposes three `(T+1,)`-shaped arrays:
* `delay_per_hour` — $\Delta_t$ summed over households for hour $t$.
* `enroute_count_per_hour` — $\#\{i : S_{i,t}=\text{ER}\}$.
* `arrivals_away_per_hour` — new SH-away arrivals (state transitioned into
SH at $t$ AND `evac_path == AWAY`).
These power scenario-internal time-resolved plots (e.g. delay-profile
charts during a chapter walk-through). Build 3 itself does not render them —
Fig. 4 and Fig. 5 use only the four scalar metrics — but the diagnostics are
serialized so chapter-figure code can pick them up later.
---
## Where the metric is *not* the right tool
* **Inference.** None of these metrics is fed to the EM loop or used for
posterior decoding. The IO-HMM does not see them.
* **Closed-loop simulation.** The metrics are post-hoc; they do not change
the next hour's transitions. `c_t` already feeds into the DGP's
transitions — that is the operational coupling. The metrics here are the
read-out, not the cause.
* **Cross-scenario uncertainty.** No bootstrap intervals are produced.
Build 4 will add bootstrap-based bands.