# 📦 WP-123 — `feat(losses): RLE loss`

> Engineering log · [Phase 12 — Keypoint detection, v0.5.0](https://borda.github.io/lucid-YOLO/engineering/phase-12-keypoints/)
>
> 🔗 Depends on [WP-122](https://borda.github.io/lucid-YOLO/work_packages/wp-122/) · Needed by [WP-124](https://borda.github.io/lucid-YOLO/work_packages/wp-124/), [WP-132](https://borda.github.io/lucid-YOLO/work_packages/wp-132/), [WP-135](https://borda.github.io/lucid-YOLO/work_packages/wp-135/)
>
> ⬅️ [WP-122](https://borda.github.io/lucid-YOLO/work_packages/wp-122/) · [Roadmap](https://borda.github.io/lucid-YOLO/ROADMAP/) · [WP-124](https://borda.github.io/lucid-YOLO/work_packages/wp-124/) ➡️


## WP-123 — a loss written by hand, from equations fetched three times

<a id="wp-123"></a>

Not delegated. AGENTS.md sec. 6 reserves loss/assignment/optimizer/rotated-geometry modules for hand-written implementation from the equations -- the highest AGPL-regurgitation-risk surface -- and R14's RLE loss is squarely a loss module, the first one this project's Phase 12 work has touched. Every other WP in this phase (120/121/122) went to `codex:codex-rescue`; this one did not, and no part of it was delegated except (implicitly) nothing -- the module, its RealNVP flow, and its test suite are all written directly against R14's own equations, fetched from arXiv's HTML rendering (the allowlisted primary-source endpoint) across three separate targeted `WebFetch` passes rather than one broad read, specifically to cross-corroborate the one detail secondary summarization is most likely to get subtly wrong: which direction the flow runs and what sits at its latent end. All three fetches agreed on Eq. 8 verbatim, R14 sec. 4's `K=6`/`Lfc=3`/`Nn=64`/Leaky-ReLU conditioner config, and Eq. 12's affine coupling formula; the third fetch additionally recovered R14 sec. 3.2's `z_bar ~ N(0, I)` latent statement and sec. 3.3's `sigma_hat` sigmoid sentence -- resolving A65, which [WP-122](https://borda.github.io/lucid-YOLO/work_packages/wp-122/) had deliberately left open rather than guessed at. No detection-repository code, including R14's own official implementation, was opened at any point (AGENTS.md sec. 7).

**The loss (Eq. 8, `log s` dropped per R14's own stated implementation choice):** `L_rle = -log Q(x_bar) - log G_phi(x_bar) + log sigma_hat`, where `x_bar = (mu_g - mu_hat) / sigma_hat` is the standardized residual, `Q` a fixed unit Laplace (no parameters, R14's "preset density"), and `G_phi` the density the RealNVP flow induces over `x_bar` via the standard change-of-variables identity. `Q`'s gradient reaching `mu_hat`/`sigma_hat` independently of the flow is R14's own stated "gradient shortcut" motivation for the additive split (sec. 3.2) rather than an incidental property -- `test_gradient_shortcut_reaches_mu_and_sigma_with_flow_frozen` freezes every flow parameter and confirms the regression head still receives nonzero gradient, a direct test of that specific paper claim rather than a generic backprop smoke test.

**The flow (Eq. 12, Appendix A):** six stacked affine coupling layers over the 2-D residual, each holding one coordinate fixed and transforming the other by a scale-and-shift pair from a 3-layer/64-unit/Leaky-ReLU conditioner. The paper states the coupling formula but not that the fixed/transformed partition alternates between layers -- that is RealNVP's own standard construction (Dinh et al., which R14 cites rather than restates), and without it one of the two coordinates could never move at all under the whole stack, making it degenerate rather than a valid bijection over R^2. Implemented as `synthesize` (the generative `z -> x` direction) and `invert` (`x -> z`, RealNVP's defining closed-form property, needed for density evaluation) as two directions of the same module rather than one direction plus a solved inverse.

**What a unit gate can and cannot prove here:** the mechanism-claim validation this row's roadmap text (and R14 itself) cares about -- RLE beating an OKS-only ablation -- needs the [GPU][PRINCIPAL]-gated [WP-125](https://borda.github.io/lucid-YOLO/work_packages/wp-125/) tier run this session cannot execute. Every property a unit test *can* pin was therefore treated as load-bearing rather than incidental: `test_flow_density_integrates_to_one` numerically integrates the flow's induced density over a 161x161 grid and checks it lands within 0.05 of 1.0 -- the single most discriminating check of change-of-variables bookkeeping, since a base-density term without its accompanying log-determinant (or vice versa) passes every other test in the file but does not integrate to a proper probability density. `test_log_determinant_matches_autograd_jacobian` compares the analytic per-layer log-determinant against `torch.autograd.functional.jacobian`'s own determinant, independently of `invert` entirely -- self-consistency between `synthesize` and `invert` alone could not catch a sign error that happened to cancel between the two, and this check cannot make that mistake since it never calls `invert`.

**Design decisions this row records rather than silently makes (A65, A66):** sigma's positivity mapping (deferred by [WP-122](https://borda.github.io/lucid-YOLO/work_packages/wp-122/)/A65) resolves here as `torch.sigmoid`, applied in the loss rather than the head, per R14 sec. 3.3's own stated sentence -- not chosen freehand. Visibility masking (A66, new): COCO `v=0` is excluded unconditionally (no ground truth exists to train against), `v=1`/`v=2` both contribute equally, since COCO's own OKS protocol already scores against `v>=1` regardless of occlusion and training against a narrower set than the metric evaluates against would be the inconsistent choice, not the safe one.

**A structural fix along the way:** every other loss in this repo (`instance_mask_loss`, `ciou_loss`, `probabilistic_iou`, ...) is a parameter-free pure function over already-produced network outputs. RLE's flow is the first loss component in this project with trainable weights of its own, and a first draft reached for a lazily-built module-level global singleton to hold it -- convenient for a bare function's signature, but a violation of this project's own "no global mutable state" rule, undiscoverable by an optimizer's `.parameters()`, and unable to move devices or checkpoint the way a registered submodule does. Corrected before landing: `RLELoss` is an `nn.Module` owning the flow as `self.flow`, the shape every other stateful component in this codebase (`DualDetectionHead`, `ProtoNet`) already takes.

