/** * ASCEND illustrative service-flow model, not a fitted clinical or causal model. * Units: expected operations and unresolved cases; months; incremental BRL. * Fractional counts are deliberate expected flows, not individual patient records. * * Each month: available = opening unresolved cases + new arrivals; * completed = min(available, referral throughput, staffed capacity, funded slots). * Referral throughput = arrivals * min(1, baseline referral + intervention gain). * Therefore completions cannot exceed arrivals: this model can slow backlog growth * but cannot clear an existing backlog or estimate waiting times. It has no exits, * mortality, contraindications, repeat operations, or empirical effect estimates. * * An additional slot costs unitCost, bundling staff, theatre service, and funding. * Theatre/combined purchase capacity and fund it; referral only funds use of * existing capacity. All intervention budgets are committed expenditure, even * when other constraints make some purchased slots unused. Programme fixed costs * are paid in full. Current-service costs are excluded from incremental spending. */ export const DEFAULTS = Object.freeze({ budget: 2400000, capacity: 90, referralGain: 0.15, arrivals: 150, initialQueue: 240, baseReferral: 0.6, unitCost: 12000, referralFixedCost: 600000, baselineFundedSlots: 100, months: 12, }); export const STRATEGIES = Object.freeze([ Object.freeze({ id: 'current', name: 'Current service', color: '#8493a6' }), Object.freeze({ id: 'theatre', name: 'Expand surgical capacity', color: '#61d8bf' }), Object.freeze({ id: 'referral', name: 'Strengthen referral', color: '#a99bff' }), Object.freeze({ id: 'combined', name: 'Combine both', color: '#f4c56a' }), ]); const BOUNDS = Object.freeze({ budget: [0, 6000000], capacity: [40, 180], referralGain: [0, 0.4], arrivals: [0, 10000], initialQueue: [0, 1000000], baseReferral: [0, 1], unitCost: [1, 10000000], referralFixedCost: [0, 6000000], baselineFundedSlots: [0, 10000], months: [1, 120], }); const TOLERANCE = 1e-7; /** Only actual finite numbers are accepted; missing fields use defaults. */ export function normalizeState(input = {}) { if (input === null || typeof input !== 'object' || Array.isArray(input)) { throw new TypeError('Model state must be an object of finite numbers.'); } const state = {}; for (const [key, fallback] of Object.entries(DEFAULTS)) { const value = Object.hasOwn(input, key) ? input[key] : fallback; if (typeof value !== 'number' || !Number.isFinite(value)) { throw new TypeError(`${key} must be an actual finite number.`); } const [lower, upper] = BOUNDS[key]; state[key] = Math.max(lower, Math.min(upper, value)); } state.months = Math.floor(state.months); return state; } function simulate(state, { additionalSlots = 0, referralGain = 0, expand = false } = {}) { const capacity = state.capacity + (expand ? additionalSlots : 0); const fundedSlots = state.baselineFundedSlots + additionalSlots; const referralThroughput = state.arrivals * Math.min(1, state.baseReferral + referralGain); const monthly = []; let queue = state.initialQueue; let total = 0; for (let index = 0; index < state.months; index += 1) { const openingQueue = queue; const available = openingQueue + state.arrivals; const completed = Math.min(available, referralThroughput, capacity, fundedSlots); queue = Math.max(0, available - completed); total += completed; monthly.push({ month: index + 1, openingQueue, arrivals: state.arrivals, available, referralThroughput: Math.min(available, referralThroughput), capacity, fundedSlots, completed, closingQueue: queue, queue, }); } const final = monthly.at(-1); const limits = [ ['Demand', final.available], ['Referral flow', final.referralThroughput], ['Staffed capacity', final.capacity], ['Operating funds', final.fundedSlots], ]; const bindingConstraints = limits.filter(([, limit]) => Math.abs(limit - final.completed) <= TOLERANCE).map(([label]) => label); const bottleneck = bindingConstraints.includes('Demand') ? 'Demand' : bindingConstraints.join(' + '); return { completed: total, queue, monthly, bottleneck, bindingConstraints }; } /** Returns current, theatre, referral, combined; infeasible options are explicit. */ export function evaluate(input = {}) { const state = normalizeState(input); const baseline = simulate(state); return STRATEGIES.map(strategy => { const isCurrent = strategy.id === 'current'; const usesReferral = strategy.id === 'referral' || strategy.id === 'combined'; const expand = strategy.id === 'theatre' || strategy.id === 'combined'; const feasible = isCurrent || (state.budget > 0 && (!usesReferral || state.budget >= state.referralFixedCost)); const reason = feasible ? '' : state.budget <= 0 ? 'No incremental budget is available.' : 'Budget is below the fixed referral programme cost.'; const programmeCost = !isCurrent && feasible && usesReferral ? state.referralFixedCost : 0; const cost = !isCurrent && feasible ? state.budget : 0; const slotExpenditure = Math.max(0, cost - programmeCost); const additionalSlots = slotExpenditure / state.unitCost / state.months; const result = isCurrent || !feasible ? baseline : simulate(state, { additionalSlots, referralGain: usesReferral ? state.referralGain : 0, expand, }); return { ...strategy, ...result, feasible, reason, incremental: Math.max(0, result.completed - baseline.completed), cost, programmeCost, slotExpenditure, additionalSlots, }; }); } /** * Maximizes extra completed operations, then minimizes committed cost. * Exact outcome/cost ties retain input order; tiedIds makes that convention visible. * This is a scenario ranking, not a clinical recommendation or cost-effectiveness test. */ export function recommendation(results) { if (!Array.isArray(results)) throw new TypeError('Results must be an array.'); const feasible = results.filter(result => result.feasible !== false); if (!feasible.length) throw new RangeError('At least one feasible result is required.'); for (const result of feasible) { if (!Number.isFinite(result.incremental) || !Number.isFinite(result.cost)) { throw new TypeError('Feasible results must have finite incremental outcomes and cost.'); } } const bestEffect = Math.max(...feasible.map(result => result.incremental)); const effectTies = feasible.filter(result => bestEffect - result.incremental <= TOLERANCE); const lowestCost = Math.min(...effectTies.map(result => result.cost)); const ties = effectTies.filter(result => result.cost - lowestCost <= TOLERANCE); return { ...ties[0], tiedIds: ties.map(result => result.id), effectTiedIds: effectTies.map(result => result.id) }; } /** Row-major scenario grid. y=0 is no assumed referral improvement. */ export function landscape(input = {}, cols = 35, rows = 25) { if (![cols, rows].every(value => Number.isInteger(value) && value >= 2 && value <= 200)) { throw new RangeError('Landscape dimensions must be integers between 2 and 200.'); } const state = normalizeState(input); // Referral throughput never exceeds arrivals, so available demand cannot bind // below it, even with an empty opening queue. All remaining limits are fixed: // monthly completions are constant, and no monthly history is needed here. // Keep evaluate()'s detailed simulation for monthly inspection and conservation. const options = STRATEGIES.map(strategy => { const isCurrent = strategy.id === 'current'; const usesReferral = strategy.id === 'referral' || strategy.id === 'combined'; const feasible = isCurrent || (state.budget > 0 && (!usesReferral || state.budget >= state.referralFixedCost)); const cost = !isCurrent && feasible ? state.budget : 0; const programmeCost = !isCurrent && feasible && usesReferral ? state.referralFixedCost : 0; const additionalSlots = (cost - programmeCost) / state.unitCost / state.months; return { ...strategy, feasible, cost, additionalSlots, usesReferral: feasible && usesReferral, expand: feasible && (strategy.id === 'theatre' || strategy.id === 'combined'), }; }); const baseThroughput = state.arrivals * state.baseReferral; const cells = []; for (let row = 0; row < rows; row += 1) { const y = 0.4 * row / (rows - 1); const improvedThroughput = state.arrivals * Math.min(1, state.baseReferral + y); for (let col = 0; col < cols; col += 1) { const x = 40 + 140 * col / (cols - 1); const baselineCompleted = Math.min(baseThroughput, x, state.baselineFundedSlots) * state.months; const results = options.map(option => { const monthlyCompleted = Math.min( option.usesReferral ? improvedThroughput : baseThroughput, x + (option.expand ? option.additionalSlots : 0), state.baselineFundedSlots + option.additionalSlots, ); const completed = monthlyCompleted * state.months; return { ...option, completed, incremental: Math.max(0, completed - baselineCompleted), queue: Math.max(0, state.initialQueue + state.months * (state.arrivals - monthlyCompleted)), }; }); const winner = recommendation(results); cells.push({ index: cells.length, col, row, xIndex: col, yIndex: row, x, y, winnerId: winner.id, winnerName: winner.name, color: winner.color, incremental: winner.incremental, completed: winner.completed, cost: winner.cost, queue: winner.queue, tiedIds: winner.tiedIds, effectTiedIds: winner.effectTiedIds, }); } } return cells; }