Independent education · Sources and limitations documented
Portfolio / Smart NOA simulation

Software simulation

Source snapshot · 5 November 2026

simulator/engine/interlocks.js

// Safety layer (SPEC §5.2). Behaviour ported from the Python reference
// implementation smart_noa_controller.py with the thresholds of config.yaml.
// Educational simulation only; not a medical device. Pure; no DOM.
//
// Ported behaviour (strict '<' comparisons throughout):
//  * hard lockouts from free-text profile entries, case-insensitive substring match;
//  * every step: re-enforce lockouts and latched holds (direct writes cannot bypass);
//  * unusable sensor reading (missing / non-number / NaN / ±Inf / boolean / string)
//    → zero ALL controlled infusions, latch hold, status RED, skip the rest of the step;
//  * rate fault (negative, non-finite, or above simulation_rate_ceilings) → zero that
//    agent, latch hold, status RED;
//  * HR < 48 → zero dexmedetomidine, latch, RED; MAP < 60 → zero dexmedetomidine and
//    lidocaine, latch, RED; a latched hold never auto-restarts (status HOLD);
//  * ketamine is NOT part of the HR/MAP holds (documented Python behaviour).
// The simulator adds an explicit, logged `resume_hold` user action (not in Python).

/** Mirror of /config.yaml (version 2026.10-rc1). A parity test checks these against the YAML. */
export const NOA_CONFIG = Object.freeze({
  version: '2026.10-rc1',
  hemodynamic_thresholds: { hr_critical_low: 48, map_critical_low: 60 },
  pharmacokinetics: { dexmedetomidine: { central_volume_L_per_kg: 0.8, elimination_rate_constant_k10: 0.04, effect_site_transfer_k1e: 0.1 } },
  drug_dosing: { dexmedetomidine: { standard_dose: 0.5, geriatric_dose: 0.25, geriatric_age_threshold: 65 }, ketamine: { standard_dose: 0.2 }, lidocaine: { standard_dose: 1.5 } },
  contraindications: {
    dexmedetomidine: { cardiac_conditions: ['Heart Block', 'AV Block', 'Mobitz', 'Sick Sinus Syndrome', 'Severe Bradycardia'] },
    ketorolac: { egfr_minimum: 30, allergy_triggers: ['NSAID', 'Ketorolac', 'Aspirin'] },
  },
  simulation_rate_ceilings: { Dexmedetomidine: 0.5, Ketamine: 0.2, Lidocaine: 1.5 },
  age_adjustments: { geriatric: { age_threshold: 65 } },
});

export const CONTROLLED = ['Dexmedetomidine', 'Ketamine', 'Lidocaine'];
/**
 * Absolute tolerance for "rate above the simulation ceiling", in the ceiling's own (Python) unit.
 * Mirrors RATE_CEILING_TOLERANCE in smart_noa_controller.py (a parity test keeps them equal).
 * A rate exactly at the ceiling (or within float noise of it) is allowed; admission in sim.js and
 * the per-step check here use the same unit and the same tolerance (2026-11 audit, finding 1).
 */
export const RATE_CEILING_EPS = 1e-9;
/** True if `rate` (already in the Python unit) is above `ceiling` beyond the shared tolerance. */
export function aboveCeiling(rate, ceiling) { return rate > ceiling + RATE_CEILING_EPS; }
export const PY_TO_SIM_ID = Object.freeze({ Dexmedetomidine: 'dexmedetomidine', Ketamine: 'ketamine', Lidocaine: 'lidocaine', Ketorolac: 'ketorolac' });
export const SIM_TO_PY_ID = Object.freeze(Object.fromEntries(Object.entries(PY_TO_SIM_ID).map(([k, v]) => [v, k])));
/** Units of drug_dosing / simulation_rate_ceilings (EXAMPLE_RESULTS.json rate_units). */
export const PY_RATE_UNITS = Object.freeze({ Dexmedetomidine: 'mcg/kg/h', Ketamine: 'mg/kg/h', Lidocaine: 'mg/kg/h' });

/** Python _is_valid_reading: a finite, non-boolean number. */
export function isValidReading(v) { return typeof v === 'number' && Number.isFinite(v); }
/** Display text of a reading in log/alarm messages: an unusable value (NaN, missing, non-finite, non-number) reads "unusable", never "NaN" (final-check fixes; display only — the decision is unchanged and the Python message is not compared by the parity tests). */
export function readingText(v) { return isValidReading(v) ? String(v) : 'unusable'; }

/** Python _matches: case-insensitive substring of any trigger in any entry. */
export function matchesFreeText(entries, triggers) {
  const norm = (entries || []).map((e) => String(e).toLowerCase().trim());
  return triggers.some((t) => norm.some((e) => e.includes(t.toLowerCase())));
}

/** Python _calculate_initial_lockouts (same order). */
export function pythonLockouts({ egfr, allergies = [], comorbidities = [] }, config = NOA_CONFIG) {
  const locks = [];
  if (egfr < config.contraindications.ketorolac.egfr_minimum) locks.push('Ketorolac');
  if (matchesFreeText(comorbidities, config.contraindications.dexmedetomidine.cardiac_conditions)) locks.push('Dexmedetomidine');
  if (matchesFreeText(allergies, config.contraindications.ketorolac.allergy_triggers) && !locks.includes('Ketorolac')) locks.push('Ketorolac');
  return locks;
}

/** Shared decision core used by both the Python port and the simulator. */
export class InterlockCore {
  constructor({ hardLockouts = [], config = NOA_CONFIG, infusions = null } = {}) {
    this.config = config;
    this.hardLockouts = [...hardLockouts];
    this.infusions = infusions || { Dexmedetomidine: 0, Ketamine: 0, Lidocaine: 0 };
    this.status = 'INITIALIZING';
    this.holdLatched = false;
    this.heldDrugs = new Set();
  }
  latchHold(drugs) {
    this.holdLatched = true;
    for (const d of drugs) { this.heldDrugs.add(d); this.infusions[d] = 0; }
  }
  enforceConstraints() {
    for (const d of Object.keys(this.infusions)) if (this.hardLockouts.includes(d) || (this.holdLatched && this.heldDrugs.has(d))) this.infusions[d] = 0;
  }
  rateFaults() {
    const out = [];
    for (const [d, r] of Object.entries(this.infusions)) {
      const ceiling = this.config.simulation_rate_ceilings[d];
      const valid = typeof r === 'number' && Number.isFinite(r) && r >= 0;
      if (!valid || ceiling === undefined || ceiling === null || aboveCeiling(r, ceiling)) out.push(d);
    }
    return out;
  }
  /**
   * One 1-s control iteration (Python monitor_and_control loop body).
   * `modelUpdate` is called where Python updates the dexmedetomidine PK model.
   * Returns { status, invalid, rateFaults, events:[{code, drugs, message}] }.
   */
  evaluate(hr, map, modelUpdate = null) {
    const thr = this.config.hemodynamic_thresholds;
    const events = [];
    this.enforceConstraints();
    if (!(isValidReading(hr) && isValidReading(map))) {
      this.latchHold(Object.keys(this.infusions));
      this.status = 'RED';
      events.push({ code: 'sensor_fault', drugs: Object.keys(this.infusions), message: `Invalid sensor reading (HR=${readingText(hr)}, MAP=${readingText(map)}): all controlled infusions set to zero; hold latched.` });
      return { status: this.status, invalid: true, rateFaults: [], events };
    }
    const faults = this.rateFaults();
    if (faults.length) {
      this.latchHold(faults);
      this.status = 'RED';
      events.push({ code: 'rate_fault', drugs: faults, message: `Rate outside simulation ceiling (${faults.join(', ')}): set to zero; hold latched.` });
    }
    if (modelUpdate) modelUpdate();
    if (hr < thr.hr_critical_low) {
      this.latchHold(['Dexmedetomidine']);
      this.status = 'RED';
      events.push({ code: 'hr_low', drugs: ['Dexmedetomidine'], message: `HR ${hr} < ${thr.hr_critical_low}: dexmedetomidine set to zero; hold latched.` });
    }
    if (map < thr.map_critical_low) {
      this.latchHold(['Dexmedetomidine', 'Lidocaine']);
      this.status = 'RED';
      events.push({ code: 'map_low', drugs: ['Dexmedetomidine', 'Lidocaine'], message: `MAP ${map} < ${thr.map_critical_low}: dexmedetomidine and lidocaine set to zero; hold latched.` });
    } else if (this.holdLatched && hr >= thr.hr_critical_low && !faults.length) {
      this.status = 'HOLD';
    } else if (!this.holdLatched && !faults.length) {
      this.status = 'GREEN';
    }
    return { status: this.status, invalid: false, rateFaults: faults, events };
  }
  /** Simulator-only explicit user action. */
  resumeHold() {
    const was = [...this.heldDrugs];
    this.holdLatched = false; this.heldDrugs.clear();
    return was;
  }
}

// ---------------------------------------------------------------------------
// Faithful port of the Python classes, used by the parity tests.

const finitePos = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0;
const finiteNonNeg = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;

export class PyPharmacokinetics {
  constructor(weightKg, centralVol, k10, k1e) {
    if (!finitePos(weightKg) || !finitePos(centralVol)) throw new Error('weight_kg and central_vol must be finite and positive');
    if (!finiteNonNeg(k10) || !finiteNonNeg(k1e)) throw new Error('k10 and k1e must be finite and nonnegative');
    this.Vc = centralVol * weightKg;
    if (!Number.isFinite(this.Vc) || this.Vc <= 0) throw new Error('Derived central volume must be finite and positive');
    this.k10 = k10; this.k1e = k1e; this.Cp = 0; this.Ce = 0;
  }
  updateConcentration(rateMcgPerMin, dtMin = 1) {
    if (!finiteNonNeg(rateMcgPerMin)) throw new Error('Infusion input must be finite and nonnegative');
    if (!finitePos(dtMin)) throw new Error('Time step must be finite and positive');
    if (Math.max(this.k10, this.k1e) * dtMin > 1) throw new Error('Time step exceeds this explicit Euler model stability bound');
    const infusionMass = rateMcgPerMin * dtMin;
    const eliminationMass = this.Cp * this.k10 * this.Vc * dtMin;
    const newMass = this.Cp * this.Vc + infusionMass - eliminationMass;
    const cp = Math.max(0, newMass / this.Vc);
    const ce = this.Ce + this.k1e * (cp - this.Ce) * dtMin;
    if (![infusionMass, eliminationMass, newMass, cp, ce].every(Number.isFinite)) throw new Error('Model arithmetic exceeded the finite input domain');
    this.Cp = cp; this.Ce = Math.max(0, ce);
    return [this.Cp, this.Ce];
  }
}

export function validatePyPatient(p) {
  const isInt = (v) => typeof v === 'number' && Number.isInteger(v);
  if (!isInt(p.age) || p.age < 0 || p.age > 120) throw new Error('Simulated age must be an integer from 0 to 120');
  if (!isInt(p.asa_class) || p.asa_class < 1 || p.asa_class > 6) throw new Error('Simulated ASA class must be an integer from 1 to 6');
  if (!finitePos(p.weight_kg)) throw new Error('weight_kg must be finite and within the simulation input domain');
  if (!finiteNonNeg(p.egfr)) throw new Error('egfr must be finite and within the simulation input domain');
  for (const v of [p.allergies, p.comorbidities]) if (!Array.isArray(v) || v.some((x) => typeof x !== 'string')) throw new Error('Simulated allergies and comorbidities must be lists of strings');
  return p;
}

export class SmartNoaControllerPort {
  constructor(patient, config = NOA_CONFIG) {
    this.patient = validatePyPatient(patient);
    this.config = config;
    this.core = new InterlockCore({ hardLockouts: pythonLockouts(patient, config), config });
    const pk = config.pharmacokinetics.dexmedetomidine;
    this.dexPk = new PyPharmacokinetics(patient.weight_kg, pk.central_volume_L_per_kg, pk.elimination_rate_constant_k10, pk.effect_site_transfer_k1e);
  }
  get infusions() { return this.core.infusions; }
  get hardLockouts() { return this.core.hardLockouts; }
  get status() { return this.core.status; }
  get holdLatched() { return this.core.holdLatched; }
  checkContraindication(drug) {
    if (!['Dexmedetomidine', 'Ketamine', 'Lidocaine', 'Ketorolac'].includes(drug)) return [false, 'UNASSESSED: no simulation rule for this drug'];
    if (this.core.hardLockouts.includes(drug)) {
      const reason = { Dexmedetomidine: 'Configured cardiac-condition example; incomplete clinical assessment', Ketorolac: 'Configured renal-threshold or allergy example; incomplete clinical assessment' }[drug] || 'Profile-specific contraindication';
      return [false, `HARD LOCK: ${reason}`];
    }
    const age = this.config.age_adjustments.geriatric.age_threshold;
    if (drug === 'Dexmedetomidine' && this.patient.age > age) return [true, `SOFT WARNING: Geriatric patient (>${age}yo). Illustrative age-adjusted configuration applies; no clinical dose recommendation.`];
    return [true, 'No configured lockout triggered; clinical safety is unassessed'];
  }
  generateStartingRates() {
    const d = this.config.drug_dosing;
    const rates = { Lidocaine: d.lidocaine.standard_dose, Ketamine: d.ketamine.standard_dose, Dexmedetomidine: d.dexmedetomidine.standard_dose };
    if (this.core.hardLockouts.includes('Dexmedetomidine')) rates.Dexmedetomidine = 0;
    else if (this.patient.age > d.dexmedetomidine.geriatric_age_threshold) rates.Dexmedetomidine = d.dexmedetomidine.geriatric_dose;
    rates['Ketorolac Adjunct'] = this.core.hardLockouts.includes('Ketorolac') ? 'LOCKED OUT (CI)' : 'No configured lockout; administration is not modeled';
    if (this.core.holdLatched) for (const k of Object.keys(this.core.infusions)) rates[k] = 0;
    this.core.infusions = Object.fromEntries(Object.entries(rates).filter(([, v]) => typeof v === 'number'));
    return rates;
  }
  /** Python monitor_and_control with scripted [hr, map] readings instead of random.randint. */
  monitorAndControl(readings) {
    if (!Array.isArray(readings)) throw new Error('readings must be an array of [hr, map]');
    const trace = [];
    for (let i = 0; i < readings.length; i++) {
      // A malformed reading (missing, not a pair) fails safe like an unusable value (Python parity).
      const pair = readings[i];
      const [hr, map] = Array.isArray(pair) && pair.length === 2 ? pair : [null, null];
      const before = { ...this.core.infusions };
      const r = this.core.evaluate(hr, map, () => {
        const rate = (this.core.infusions.Dexmedetomidine || 0) * this.patient.weight_kg / 60;
        this.dexPk.updateConcentration(rate, 1 / 60);
      });
      trace.push({ step: i + 1, hr, map, rates_before: before, rates_after: { ...this.core.infusions }, status: r.status, hold_latched: this.core.holdLatched, Cp: this.dexPk.Cp, Ce: this.dexPk.Ce, events: r.events });
    }
    return trace;
  }
}
Source and scope

Published from Smart-NOA-Controller/simulator/engine/interlocks.js at revision 6e5b205e3843. Full source text is retained; HTML presentation does not establish independent clinical review. In-memory simulation with no patient, monitor or pump interface; not clinically validated.