1. Define Multi-Output Targets for Restraint Responses

Why the Output Schema Must Be Fixed First

In a multi-output restraint-response model, the target tensor is not a single column but a structured contract: every output channel has a fixed position, a physical unit, a valid range, and a defined meaning. If this contract is left implicit, three failure modes appear almost immediately. First, column order drifts between the feature builder and the loss function, so gradients are applied to the wrong head. Second, unit mismatches (for example, force in N versus kN) silently rescale the loss and bias the model toward whichever channel has the largest numeric magnitude. Third, partially labeled samples either crash the loss or, worse, inject zeros that the model learns as real restraint values. This case defines the schema before any model code exists, so that later data pipelines, loss masking, and evaluation all reference one authoritative specification.

Anatomy of a Restraint Response Target

A restraint response is the measured reaction of a restraint system to an imposed load or displacement. In a typical vehicle-occupant or structural restraint dataset, each sample may carry several response channels recorded under different conditions. The schema must capture, per channel: a stable identifier, the physical quantity, the unit, the sign convention, the expected range, and whether the channel is always present or conditionally available.

Sign conventions deserve explicit attention because restraint responses are directional. Define a coordinate frame once — for example, +X forward, +Y left, +Z up — and state that tensile restraint force is positive while compressive is negative. Every downstream consumer (loss, metrics, plots) must inherit this convention. If a channel is later re-expressed in another frame, that transformation is documented as a separate, explicit step.

Defining the Output Dimensions

The output dimension is the number of channels the model predicts simultaneously. For this case we fix a concrete, reproducible schema of six channels covering the most common restraint response families. The table below is the authoritative contract; it is the artifact that model code, data loaders, and evaluation scripts will import or mirror.

Index Channel ID Physical quantity Unit Sign convention Expected range Availability
0belt_force_peakPeak seatbelt restraint forceNTension positive0 – 12000Always
1belt_force_avgMean seatbelt force over eventNTension positive0 – 8000Always
2pretensioner_strokePretensioner retraction strokemmRetraction positive0 – 120Conditional
3load_limiter_forceLoad-limiter plateau forceNTension positive0 – 6000Conditional
4chest_accel_peakPeak chest accelerationgForward positive0 – 80Always
5head_excursionHead forward excursionmmForward positive0 – 400Conditional

Two design decisions are embedded here. First, units are heterogeneous (N, mm, g), which means the loss cannot treat raw values as comparable. Second, three channels are conditional: they exist only for configurations that include the corresponding hardware (for example, a pretensioner is not fitted on every restraint variant). This is precisely why missing-label handling must be part of the schema, not an afterthought.

Missing-Label Handling as Part of the Contract

The schema distinguishes three states for every channel on every sample: present (a real measured value), absent (the hardware or condition does not apply), and unknown (applicable but not recorded). Conflating these is the most common source of corrupted training. The contract specifies:

  • Absent and unknown labels are stored as NaN in the target tensor, never as 0.
  • A parallel boolean validity mask of shape [num_samples, num_outputs] marks which entries are present.
  • The loss is computed only over valid entries; masked entries contribute zero loss and zero gradient.
  • Evaluation reports per-channel coverage so that a channel with 5% availability is not silently averaged into an aggregate score.

Encoding absence as NaN rather than zero is deliberate: zero is a physically meaningful restraint force and would be indistinguishable from a genuine measurement. The mask carries the truth; the value carries only real data.

Do not use -1 or 0 as a missing sentinel. Both collide with valid physical values (a compressive force can be negative; a released belt reads zero). Use NaN plus an explicit mask.

Normalization and the Modeling Contract

Because channels span different units and magnitudes, the schema also fixes a normalization policy: each channel is standardized using statistics computed only on its valid entries, and the mean/scale are stored alongside the schema. This keeps the loss balanced across heads and makes the mask semantics consistent — normalization statistics are never computed over NaN entries. The schema therefore has two parts: the channel definition table above, and a companion normalization record (per-channel mean and standard deviation) that is versioned with it.

Restraint Response Output Schema (6 channels) Channel definitions [0] belt_force_peak N · tension + · always [1] belt_force_avg N · tension + · always [2] pretensioner_stroke mm · retraction + · conditional [3] load_limiter_force N · tension + · conditional [4] chest_accel_peak g · forward + · always [5] head_excursion mm · forward + · conditional Target tensor y shape [N, 6] · float32 row: [ 4820, 3110, NaN, NaN, 42.5, NaN ] absent/unknown → NaN never 0, never -1 Validity mask m shape [N, 6] · bool row: [ 1, 1, 0, 0, 1, 0 ] 1 = present, 0 = masked drives loss & coverage Masked loss L = Σ m·(ŷ−y)² ÷ Σ m masked entries: zero loss, zero grad Normalization record (versioned with schema): per-channel mean/scale computed on valid entries only — never over NaN

Encoding the Schema in Code

The schema should exist as a single source of truth that other modules import. The following Python module is self-contained and runnable; it defines the channels, builds a target tensor and mask from raw records, and demonstrates the masked loss on a small example. It uses only the standard library plus NumPy.

import numpy as np

# Authoritative channel order — index position IS the output dimension.
CHANNELS = [
    {"id": "belt_force_peak",     "unit": "N",  "sign": "tension+",   "always": True},
    {"id": "belt_force_avg",      "unit": "N",  "sign": "tension+",   "always": True},
    {"id": "pretensioner_stroke", "unit": "mm", "sign": "retraction+","always": False},
    {"id": "load_limiter_force",  "unit": "N",  "sign": "tension+",   "always": False},
    {"id": "chest_accel_peak",    "unit": "g",  "sign": "forward+",   "always": True},
    {"id": "head_excursion",      "unit": "mm", "sign": "forward+",   "always": False},
]
NUM_OUTPUTS = len(CHANNELS)
INDEX = {c["id"]: i for i, c in enumerate(CHANNELS)}

def build_targets(records):
    """records: list of dicts mapping channel_id -> float or None.
    Returns y [N, NUM_OUTPUTS] float32 with NaN for missing, and mask [N, NUM_OUTPUTS] bool."""
    n = len(records)
    y = np.full((n, NUM_OUTPUTS), np.nan, dtype=np.float32)
    mask = np.zeros((n, NUM_OUTPUTS), dtype=bool)
    for r, rec in enumerate(records):
        for cid, val in rec.items():
            j = INDEX[cid]
            if val is not None:
                y[r, j] = val
                mask[r, j] = True
    return y, mask

def masked_mse(y_true, y_pred, mask):
    """Mean squared error over valid entries only."""
    diff = (y_pred - y_true) ** 2
    diff = np.where(mask, diff, 0.0)
    denom = mask.sum()
    return float(diff.sum() / denom) if denom else 0.0

# --- Example (illustrative values, not measured data) ---
records = [
    {"belt_force_peak": 4820.0, "belt_force_avg": 3110.0,
     "chest_accel_peak": 42.5},                       # no pretensioner/limiter/head
    {"belt_force_peak": 5310.0, "belt_force_avg": 3405.0,
     "pretensioner_stroke": 78.0, "load_limiter_force": 4200.0,
     "chest_accel_peak": 47.1, "head_excursion": 210.0},
]
y, mask = build_targets(records)
print("y shape:", y.shape, "| mask shape:", mask.shape)
print("row0:", y[0], "| mask0:", mask[0].astype(int))

y_pred = np.where(mask, y + 100.0, 0.0)  # pretend predictions offset by +100
print("masked MSE:", masked_mse(y, y_pred, mask))
print("coverage per channel:", mask.mean(axis=0).round(2))

Running this prints a target tensor of shape (2, 6) with NaN in the three conditional slots of the first sample, a mask of matching shape, and a masked MSE computed only over the nine valid entries. The coverage vector [1.0, 1.0, 0.5, 0.5, 1.0, 0.5] immediately reveals that half the samples lack the conditional channels — the exact signal that later informs loss masking and split design.

Verification Checklist

Before any model code is written, confirm the schema passes these checks:

  • Dimension lock: NUM_OUTPUTS equals the number of rows in the channel table, and INDEX order matches the table order.
  • No sentinel leakage: missing entries are NaN; a scan of y finds no -1 or spurious 0 in conditional slots.
  • Mask consistency: mask == ~np.isnan(y) holds elementwise.
  • Unit audit: each channel's unit and sign convention are recorded and match the source data dictionary.
  • Normalization record: per-channel mean/scale exist and were computed on valid entries only.

If a downstream loss produces a gradient on a masked entry, or a coverage report shows a channel at 0%, the schema contract has been violated and should be corrected at the source rather than patched in the training loop.

Treat the channel table and normalization record as versioned artifacts. When a new restraint response channel is added, increment the schema version, append it at the next index, and re-run the coverage check — never reorder existing indices, since that silently remaps every stored target.