3. Fallback Trigger Logic Based on OOD Score Thresholds
Why a Threshold Alone Is Not a Trigger
An OOD score is a continuous quantity; a fallback trigger is a discrete decision. The gap between them is where most production failures hide. In the LS-DYNA context, the detector from the baseline case produces a scalar anomaly score s(x) for each simulation run — typically a Mahalanobis distance over features such as peak kinetic energy, contact force integral, and nodal displacement statistics. The trigger must convert that score into one of two solver configurations: the nominal configuration (the tuned, high-fidelity setup) or the conservative configuration (a robust, lower-risk setup).
A naive comparison s(x) > τ is insufficient for three reasons. First, a single threshold produces chatter: a run whose score oscillates around τ across successive restart segments can flip configurations repeatedly, which is both expensive and non-deterministic. Second, the score distribution is asymmetric — nominal runs cluster tightly, while off-nominal runs form a long tail — so a symmetric threshold treats the two error types (false fallback and missed detection) as equally costly when they are not. Third, the trigger must be auditable: when a fallback fires, an engineer needs to know which score, which threshold, and which rule caused it.
Trigger Architecture: Score → Decision → Configuration
The design below separates scoring from decision-making so that the threshold logic can be recalibrated (case 4) without touching the detector (case 1) or the feature extractor (case 2). The trigger consumes a score stream, applies hysteresis and a confirmation window, and emits a configuration selection plus a reason code.
Implementing the Trigger
The reference implementation below is a self-contained Python module. It defines the trigger state machine, the hysteresis band, and the confirmation counter, and it emits a structured decision record. The thresholds TAU_HIGH = 3.20 and TAU_LOW = 2.60 are illustrative example values chosen to sit above a nominal cluster centered near 1.8; they are not calibrated results and must be replaced by the calibration procedure in case 4.
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional
import json
class Config(Enum):
NOMINAL = "nominal"
CONSERVATIVE = "conservative_v2"
@dataclass
class TriggerDecision:
run_id: str
score: float
tau_high: float
tau_low: float
consecutive: int
decision: str
config: str
reason: str
class FallbackTrigger:
"""Hysteresis + confirmation-window trigger for LS-DYNA fallback."""
def __init__(self, tau_high: float = 3.20, tau_low: float = 2.60,
confirm_n: int = 3):
if tau_low >= tau_high:
raise ValueError("tau_low must be strictly below tau_high")
self.tau_high = tau_high
self.tau_low = tau_low
self.confirm_n = confirm_n
self._active = False # current fallback state
self._above_count = 0 # consecutive segments above tau_high
self._below_count = 0 # consecutive segments below tau_low
def evaluate(self, run_id: str, score: float) -> TriggerDecision:
reason = "within_band"
if not self._active:
if score > self.tau_high:
self._above_count += 1
reason = f"above_tau_high({self._above_count}/{self.confirm_n})"
if self._above_count >= self.confirm_n:
self._active = True
self._above_count = 0
reason = "confirmed_fallback"
else:
self._above_count = 0
else:
if score < self.tau_low:
self._below_count += 1
reason = f"below_tau_low({self._below_count}/{self.confirm_n})"
if self._below_count >= self.confirm_n:
self._active = False
self._below_count = 0
reason = "confirmed_recovery"
else:
self._below_count = 0
cfg = Config.CONSERVATIVE if self._active else Config.NOMINAL
return TriggerDecision(
run_id=run_id, score=round(score, 3),
tau_high=self.tau_high, tau_low=self.tau_low,
consecutive=self._above_count if not self._active else self._below_count,
decision="FALLBACK" if self._active else "NOMINAL",
config=cfg.value, reason=reason,
)
Two design choices deserve emphasis. The dead band between tau_low and tau_high means a score that drifts back and forth across a single threshold cannot toggle the configuration; it must cross the opposite boundary and stay there for confirm_n segments. The confirmation window means a single spurious spike — for example, a transient contact-force artifact in one restart segment — cannot fire a fallback on its own.
Exercising the Trigger with a Score Sequence
The following driver feeds a synthetic score sequence through the trigger. The sequence is constructed to demonstrate the three behaviors that matter: a spike that is rejected, a sustained excursion that fires, and a recovery that requires confirmation.
trigger = FallbackTrigger(tau_high=3.20, tau_low=2.60, confirm_n=3)
# (run_id, score) — synthetic sequence for demonstration only
sequence = [
("R-0401", 1.72), ("R-0402", 1.81), ("R-0403", 3.55), # isolated spike
("R-0404", 1.90), ("R-0405", 1.77), # spike rejected
("R-0406", 3.41), ("R-0407", 3.62), ("R-0408", 3.28), # sustained excursion
("R-0409", 3.05), ("R-0410", 2.71), # inside dead band
("R-0411", 2.44), ("R-0412", 2.31), ("R-0413", 2.18), # recovery confirmed
]
for run_id, score in sequence:
d = trigger.evaluate(run_id, score)
print(json.dumps(d.__dict__))
Expected observable behavior (derived directly from the logic above, not from a live run):
R-0403reportsabove_tau_high(1/3)and staysNOMINAL;R-0404resets the counter to zero. The isolated spike is correctly rejected.R-0406throughR-0408increment the counter to 3;R-0408emitsconfirmed_fallbackand selectsconservative_v2.R-0409andR-0410fall inside the dead band and do not start the recovery counter — the trigger remains in fallback.R-0411throughR-0413accumulate three consecutive scores belowtau_low;R-0413emitsconfirmed_recoveryand returns tonominal.
Mapping the Decision to an LS-DYNA Configuration
The trigger's output is a configuration selector, not a solver command. The mapping below shows the concrete keyword-level differences between the two configurations. The conservative configuration trades runtime for robustness: a smaller explicit time step, tighter contact penalty scaling, and a switch to implicit for the final stage.
| Keyword / Parameter | Nominal | Conservative (fallback) | Rationale |
|---|---|---|---|
*CONTROL_TERMINATION DTMIN | 1.0e-6 | 1.0e-7 | Prevents premature termination on distorted elements |
*CONTROL_TIMESTEP TSSFAC | 0.90 | 0.60 | Smaller scale factor improves stability margin |
*CONTACT PENALTY scale | 1.0 | 1.5 | Reduces penetration in off-nominal contact |
*CONTROL_IMPLICIT_GENERAL | off | on (final stage) | Implicit integration for stiff final response |
*DATABASE_BINARY_D3PLOT DT | 1.0e-3 | 5.0e-4 | Finer output for post-fallback diagnosis |
config. This lets you add a third tier (e.g., conservative_v3) or roll back a configuration without re-validating the trigger logic.Verification and Failure Symptoms
Verify the trigger with three targeted checks before wiring it into a production pipeline. First, a chatter test: feed an alternating sequence [3.3, 2.5, 3.3, 2.5, ...] and confirm the configuration never changes, because neither boundary is crossed confirm_n times consecutively. Second, a boundary test: feed exactly tau_high and confirm the comparison is strict (>), so a score equal to the threshold does not fire. Third, a recovery test: after a confirmed fallback, feed two scores below tau_low followed by one above, and confirm the recovery counter resets.
Common failure symptoms and their causes: if the trigger fires on every run, tau_high is below the nominal cluster mean — recalibrate. If it never fires on known off-nominal runs, confirm_n is too large relative to the number of restart segments available. If the configuration oscillates despite the dead band, the score stream is being evaluated per-timestep rather than per-segment; the trigger assumes one score per run or restart segment, and feeding it finer-grained scores will defeat the confirmation window.