/** * Injectable fault profiles. * * Each profile turns "time since injection" into a set of physics modifiers that * stations.js and line.js consume. Faults are progressive where a real fault * would be progressive - the bearing does not fail the instant you press the * button, it degrades, which is the whole point of showing trend extrapolation. */ export const FAULT_PROFILES = [ { id: 'bearing-degradation', station: 'CNC-02', label: 'CNC-02 bearing degradation', short: 'Bearing wear', severity: 'major', headline: true, description: 'Spindle bearing begins to spall. Vibration rises exponentially, spindle load creeps up, and out-of-tolerance parts start reaching inspection.', }, { id: 'oven-burner', station: 'OVN-03', label: 'OVN-03 zone 2 burner fault', short: 'Burner fault', severity: 'major', description: 'Zone 2 loses roughly 40% of its heating capacity. The controller saturates trying to hold setpoint, the cure runs cold, and quality falls downstream.', }, { id: 'packer-jam', station: 'PKG-05', label: 'PKG-05 film jam', short: 'Film jam', severity: 'critical', description: 'Film binds then tears, stopping the packer. Work in progress backs up through the line and upstream stations block.', }, { id: 'sensor-dropout', station: 'INS-04', label: 'INS-04 sensor dropout', short: 'Sensor dropout', severity: 'minor', description: 'The inspection station stops reporting. Values go stale rather than to zero, which is what a real dropout looks like and what a twin has to handle honestly.', }, ]; export function faultProfile(id) { return FAULT_PROFILES.find((f) => f.id === id); } /** Time constant for the bearing ramp, in simulated seconds. */ const BEARING_TAU = 420; const BEARING_GAIN = 0.35; const BEARING_CAP = 3.2; /** * Collapse the set of active faults into physics modifiers. * * active: Map of faultId -> { injectedAt } in simulated seconds. */ export function computeModifiers(active, simTime) { const mods = { bearingVibration: 0, ovenZone2Capacity: 1, packerJamPhase: null, faultedStations: new Set(), offlineStations: new Set(), }; for (const [id, info] of active) { const t = Math.max(0, simTime - info.injectedAt); switch (id) { case 'bearing-degradation': // Exponential ramp: slow to start, unmistakable once it moves. Crosses // the 3.5 mm/s warn band around 13 simulated minutes. mods.bearingVibration = Math.min( BEARING_CAP, BEARING_GAIN * (Math.exp(t / BEARING_TAU) - 1), ); break; case 'oven-burner': { // Capacity degrades over the first minute rather than cliff-edging. const frac = Math.min(1, t / 60); mods.ovenZone2Capacity = 1 - 0.38 * frac; break; } case 'packer-jam': mods.faultedStations.add('PKG-05'); mods.packerJamPhase = t < 8 ? 'bind' : 'tear'; break; case 'sensor-dropout': mods.offlineStations.add('INS-04'); break; } } return mods; }