3. Pass-fail decision logic design

In occupant protection assessment, a single crash test produces dozens of injury metrics across multiple body regions, dummies, and seating positions. The pass-fail decision is therefore never a single comparison — it is a structured evaluation of many criteria whose results must be combined into one defensible verdict. This section explains how to design that decision layer: how individual thresholds are compared, how boolean results are aggregated, and when a rule engine is warranted over hand-coded logic.

From Raw Signals to Criterion Results

Before any comparison happens, each injury metric must be reduced to a scalar that the threshold logic can consume. Typical inputs include Head Injury Criterion (HIC15), 3 ms chest acceleration clip, chest deflection, neck Nij, femur axial force, and tibia index. Each has a defined evaluation window, filter class (e.g., CFC 600 or CFC 1000 per SAE J211), and a specified side (left/right, driver/passenger). The decision layer should treat these as typed inputs rather than loose numbers, because a threshold applied to the wrong channel or the wrong filter is a silent, catastrophic error.

A robust pattern is to separate measurement from judgement. The measurement stage outputs a normalized record such as {metric: "HIC15", value: 812.4, unit: "g", location: "driver_head"}. The judgement stage then applies the threshold. This separation lets you re-run the decision logic against stored results without re-processing raw signals, which is essential for regression testing when limits change.

Threshold Comparison: Equality, Margins, and Direction

Every criterion has a limit and a direction. Most injury criteria are "lower is better," so the pass condition is value <= limit. Some, like certain restraint performance indicators, may be "higher is better." Encode the direction explicitly rather than assuming it. A comparison function should return not just a boolean but a margin, because the margin drives reporting, risk classification, and engineering triage:

def compare(value, limit, direction="max"):
    # direction="max": pass when value <= limit
    # direction="min": pass when value >= limit
    if direction == "max":
        margin = limit - value          # positive = pass headroom
        passed = value <= limit
    else:
        margin = value - limit
        passed = value >= limit
    return {
        "passed": passed,
        "margin": round(margin, 3),
        "ratio": round(value / limit, 4)  # utilization, 1.0 = at limit
    }

The ratio field (utilization) is more useful than raw margin when comparing criteria with different units. A HIC of 812 against a limit of 1000 is 0.812 utilization; a chest deflection of 58 mm against 63 mm is 0.921. The second criterion is closer to failure even though its absolute margin looks smaller in its own units. Reporting utilization lets reviewers rank risk consistently.

Floating-point equality is a trap. A value of exactly 1000.0000001 against a limit of 1000 will fail, while 999.9999999 passes. Define a tolerance (e.g., 1e-6 relative) and document whether the boundary is inclusive. Regulatory text often specifies "shall not exceed," which means the limit itself passes; encode that as <=, not <.

Boolean Aggregation: AND, OR, and the Structure of a Verdict

Individual criterion results combine into a verdict through a tree of boolean operators. The most common structure is a conjunction of per-region requirements, where each region itself may be a disjunction of alternative criteria. For example, a head protection requirement might pass if either HIC15 or the 3 ms clip is within limits, while the overall occupant verdict requires all of head, neck, chest, and lower extremity to pass.

Represent this as an explicit expression tree rather than nested if statements. The tree is inspectable, serializable, and can be rendered in a report showing exactly which branch failed:

verdict = AND(
    region("head",   OR(crit("HIC15"), crit("a3ms_head"))),
    region("neck",   AND(crit("Nij"), crit("neck_tension"))),
    region("chest",  AND(crit("a3ms_chest"), crit("deflection"))),
    region("femur",  OR(crit("femur_left"), crit("femur_right")))
)

Two aggregation pitfalls deserve attention. First, short-circuit evaluation hides information: if the head fails, an AND that stops early never evaluates the chest, so the report cannot show chest status. For assessment reports, evaluate all leaves and aggregate afterward — the cost is negligible and the diagnostic value is high. Second, mixing AND/OR without grouping produces ambiguous precedence. Always parenthesize explicitly in the data model, even if the language's operator precedence would happen to agree.

When a Rule Engine Earns Its Place

Hand-coded boolean trees work well when criteria are stable and few. They become fragile when limits vary by market, dummy type, seating row, or test configuration, and when non-engineers need to review or amend rules. A rule engine — typically a decision table or a declarative ruleset — externalizes the logic so it can be versioned, audited, and changed without redeploying code.

A decision table is often the most readable form for multi-criteria assessment. Each row is a rule; columns are conditions and the resulting action. The table below is an illustrative example with assumed limits, not regulatory values:

RuleRegionMetricConditionOutcome
R1HeadHIC15≤ 1000PASS
R2HeadHIC15> 1000 and ≤ 1100REVIEW
R3HeadHIC15> 1100FAIL
R4ChestDeflection≤ 63 mmPASS
R5ChestDeflection> 63 mmFAIL

Note the three-valued outcome. Real assessment rarely collapses to binary at the criterion level; a "review" band captures near-limit results that need engineering judgement before a formal verdict. The aggregation layer then maps criterion outcomes to a final verdict: any FAIL yields overall FAIL; otherwise any REVIEW yields CONDITIONAL; otherwise PASS. This is a small state lattice, and encoding it explicitly prevents the common bug where a near-limit value is silently rounded into a pass.

Putting the Layers Together

The figure below shows the full decision pipeline for one occupant: typed metric inputs enter a comparison stage that emits pass/fail plus utilization, results flow into a grouped boolean tree, and the tree resolves to a three-valued verdict with a traceable failure path.

1. Typed metric inputs HIC15 = 812.4 a3ms_chest = 47.2 deflection = 58 mm Nij = 0.71 2. Threshold comparison HIC15 ≤ 1000 PASS · util 0.812 a3ms ≤ 60 PASS · util 0.787 deflection ≤ 63 PASS · util 0.921 Nij ≤ 1.0 PASS · util 0.710 3. Boolean aggregation AND (overall) OR (head) AND (chest) AND (neck) 4. Verdict PASS max util 0.921 CONDITIONAL if any REVIEW Failure trace (example: deflection = 66 mm) chest.deflection → FAIL (util 1.048) AND(chest) → FAIL → AND(overall) → FAIL report path pinpoints the failing leaf criterion pass criterion fail / trace aggregation node

The trace on the right of the figure is the practical payoff of evaluating all leaves: when a verdict fails, the report can name the exact criterion, its value, and its utilization, rather than merely stating "occupant failed." That trace is what engineers use to decide whether to adjust a restraint, re-examine a sensor channel, or accept the result.

Implementation Notes and Failure Symptoms

Three implementation details separate a trustworthy decision layer from a brittle one. First, version the ruleset alongside the results; a verdict is only meaningful relative to the limits and logic that produced it. Store a hash of the ruleset with each result record. Second, validate the input schema before comparison — a missing chest deflection should raise an explicit "incomplete data" outcome, not default to zero and pass. Third, test the boundaries: unit tests should cover values exactly at the limit, one epsilon above, and one epsilon below, for every criterion and both directions.

Common failure symptoms map to specific causes. A verdict that flips between runs on identical data usually indicates unordered aggregation or floating-point boundary handling. A report that shows PASS while a reviewer's manual check shows a near-limit value often means short-circuit evaluation skipped a leaf, or a REVIEW band was collapsed to PASS. A criterion that never fails across many tests may be wired to the wrong channel or the wrong filter class — verify the metric's evaluation window and CFC against the test specification. Treating these symptoms as diagnostic signals, rather than rerunning until the result looks right, keeps the decision layer auditable and defensible.

Keep the comparison, aggregation, and reporting layers as separate modules. The comparison layer is pure and unit-testable; the aggregation layer is a data structure you can serialize and render; the reporting layer consumes the trace. This separation is what makes a multi-criteria pass-fail system maintainable as limits and configurations evolve.