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

Software simulation

Source snapshot · 5 November 2026

simulator/engine/templates.js

// Technique templates -> engine plan (2026-11 additions). Pure; no DOM.
// EDUCATIONAL SIMULATION — fictional patients — not a medical device.
//
// buildTemplatePlan() expands a template from data/templates.js and fills EVERY dose and rate with
// recommend() through buildPlan() (data/scenarios.js); this module adds no numbers of its own. It
// only (1) picks the template's optional steps, (2) swaps a locked primary drug for the template's
// fallback, (3) drops lidocaine pretreatment when an IV lidocaine analgesic regimen is planned, and
// (4) with interlocks on, pre-fills the Smart NOA simulation ceiling (config.yaml) when a label
// default is above it — the same rule as the plan editor.

import { resolveLibrary, getDrug } from './library.js';
import { recommend, regimenKeyForPhase, convertRateSafe } from './dosing.js';
import { scalarWeight } from './patient.js';
import { NOA_CONFIG, SIM_TO_PY_ID, CONTROLLED, PY_RATE_UNITS, aboveCeiling } from './interlocks.js';
import { FLUID_MODEL } from './sim.js';
import { TECHNIQUE_TEMPLATES } from '../data/templates.js';
import { buildPlan } from '../data/scenarios.js';
import { roundSig } from './units.js';

export { TECHNIQUE_TEMPLATES };
export function getTemplate(id) { return TECHNIQUE_TEMPLATES.find((t) => t.id === id) || null; }

/** Optional steps of a template: [{ id, label, default }]. */
export function templateOptions(template) {
  const seen = new Map();
  for (const s of template.steps) if (s.optional && !seen.has(s.optional.id)) seen.set(s.optional.id, { ...s.optional });
  return [...seen.values()];
}

const isLidoAnalgesic = (s) => s.drugId === 'lidocaine' && s.kind !== 'stop' && s.regimenKey !== 'pretreatment';

function rateWeight(drug, p) {
  const regs = drug.regimens || {};
  for (const k of ['maintenance', 'sedation_moderate', 'sedation_deep', 'sedation_minimal', ...Object.keys(regs)]) if (regs[k] && regs[k].rate && regs[k].scalar) return scalarWeight(p, regs[k].scalar).weightKg;
  return p.weightKg;
}

/**
 * buildTemplatePlan(templateId, patientDerived, { library, interlocks = true, sedationTarget, include, prefillCeilings = interlocks })
 * (the plan editor passes prefillCeilings:false and applies the same ceiling itself, with its note)
 *  -> { plan, skipped, notes:[{ key, text, sources }], substitutions:[…], omitted:[…], prefilled:[…], template }
 * include: { [optionalId]: boolean } (absent → the option's default).
 */
export function buildTemplatePlan(templateId, patient, { library = null, interlocks = true, sedationTarget = null, include = {}, prefillCeilings = interlocks } = {}) {
  const lib = resolveLibrary(library);
  const tpl = getTemplate(templateId);
  if (!tpl) throw new Error(`Unknown template '${templateId}'`);
  const target = tpl.mode === 'MAC' ? (sedationTarget || tpl.sedationTarget || 'moderate') : null;
  const want = (s) => !s.optional || (Object.prototype.hasOwnProperty.call(include, s.optional.id) ? !!include[s.optional.id] : !!s.optional.default);
  const substitutions = []; const omitted = []; const swap = new Map();
  let steps = tpl.steps.filter(want).map((s) => ({ ...s, regimenKey: s.regimenKey === 'sedation' ? regimenKeyForPhase('sedation', target) : s.regimenKey }));
  // fallback when the primary drug is locked (e.g. sevoflurane for an MH-susceptible patient)
  steps = steps.map((s) => {
    if (!s.fallback || !getDrug(lib, s.drugId)) return s;
    const rec = recommend(patient, s.regimenKey, s.drugId, lib);
    if (!rec.locked) return s;
    swap.set(s.drugId, s.fallback.drugId);
    substitutions.push({ from: s.drugId, to: s.fallback.drugId, reason: rec.lockReasons.join('; ') });
    return { phase: s.phase, atSec: s.atSec, ...s.fallback, ...(s.timingRationale ? { timingRationale: s.timingRationale } : {}) };
  });
  steps = steps.map((s) => (s.kind === 'stop' && swap.has(s.drugId) ? { ...s, drugId: swap.get(s.drugId) } : s));
  // lidocaine pretreatment omitted when an IV lidocaine analgesic regimen is planned (Foo 2021 note)
  const lidoAnalgesic = steps.some(isLidoAnalgesic) && !!getDrug(lib, 'lidocaine') && !recommend(patient, 'induction', 'lidocaine', lib).locked;
  steps = steps.filter((s) => { if (s.omitIf === 'lidocaineAnalgesic' && lidoAnalgesic) { omitted.push({ drugId: s.drugId, regimenKey: s.regimenKey, reason: 'IV lidocaine analgesic regimen planned (Foo 2021: every lidocaine dose counts toward the cumulative total)' }); return false; } return true; });
  // de-duplicate stops (e.g. two stops for propofol after a fallback swap)
  const seenStop = new Set();
  steps = steps.filter((s) => { if (s.kind !== 'stop') return true; const k = `${s.phase}|${s.drugId}`; if (seenStop.has(k)) return false; seenStop.add(k); return true; });
  // a stop for a drug that is not started in the plan does nothing: drop it
  steps = steps.filter((s) => s.kind !== 'stop' || steps.some((x) => x.drugId === s.drugId && (x.kind === 'infusion' || x.kind === 'inhaled')));

  const built = buildPlan({ mode: tpl.mode, sedationTarget: target, steps }, patient, lib, { fluidDefaultMlH: FLUID_MODEL.MAINTENANCE_DEFAULT_ML_H });
  // remove stops whose drug was skipped (locked) by buildPlan
  const started = new Set(built.plan.steps.filter((s) => s.kind === 'infusion' || s.kind === 'inhaled').map((s) => s.drugId));
  built.plan.steps = built.plan.steps.filter((s) => s.kind !== 'stop' || started.has(s.drugId));
  // interlock ceiling pre-fill (same rule as the plan editor)
  const prefilled = [];
  if (interlocks && prefillCeilings) {
    for (const s of built.plan.steps) {
      if (s.kind !== 'infusion') continue;
      const py = SIM_TO_PY_ID[s.drugId];
      if (!py || !CONTROLLED.includes(py)) continue;
      const drug = getDrug(lib, s.drugId); const w = rateWeight(drug, patient);
      const pyRate = convertRateSafe(s.rate, s.rateUnit, PY_RATE_UNITS[py], w);
      const ceil = NOA_CONFIG.simulation_rate_ceilings[py];
      if (pyRate === null || aboveCeiling(pyRate, ceil)) {
        const v = s.rateUnit === PY_RATE_UNITS[py] ? ceil : convertRateSafe(ceil, PY_RATE_UNITS[py], s.rateUnit, w);
        prefilled.push({ drugId: s.drugId, from: s.rate, to: v === null ? 0 : roundSig(v, 12), unit: s.rateUnit });
        s.rate = v === null ? 0 : (s.rateUnit === PY_RATE_UNITS[py] ? ceil : Math.floor(v * 1e6) / 1e6);
      }
    }
  }
  const N = lib.TEMPLATE_NOTES || {};
  const notes = (tpl.notes || []).filter((k) => N[k] && (k !== 'lidocaineOverlap' || omitted.length) && (k !== 'dexInterlock' || interlocks)).map((k) => ({ key: k, text: N[k].text, sources: [...N[k].sources] }));
  built.plan.template = { id: tpl.id, label: tpl.label };
  return { ...built, notes, substitutions, omitted, prefilled, template: tpl };
}
Source and scope

Published from Smart-NOA-Controller/simulator/engine/templates.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.