ieeeColors = ({
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
red: "#c0392b",
green: "#27ae60",
lightGray: "#ecf0f1"
})
// Calibration method definitions with characteristics
calibrationMethods = [
{
id: "single-point",
name: "1-Point Calibration (Offset Only)",
shortName: "1-Point",
description: "Quick field calibration using single reference point. Corrects zero offset but assumes gain is correct. Fast and cheap, but limited accuracy.",
characteristics: {
accuracy: 2,
time: 5,
cost: 5,
complexity: 5
},
timeMinutes: 5,
costDollars: 10,
typicalAccuracy: "+/-2-5% of span",
bestFor: ["Quick field checks", "Known linear sensors", "Offset drift only", "Cost-sensitive applications"],
limitations: ["Cannot correct gain errors", "Accuracy decreases far from calibration point", "Not suitable for non-linear sensors"],
referencePoints: 1
},
{
id: "two-point",
name: "2-Point Calibration (Offset + Gain)",
shortName: "2-Point",
description: "Standard industrial calibration using low and high reference points. Corrects both offset and gain (slope). Best balance of accuracy, time, and cost for linear sensors.",
characteristics: {
accuracy: 4,
time: 4,
cost: 4,
complexity: 4
},
timeMinutes: 20,
costDollars: 50,
typicalAccuracy: "+/-0.5-1% of span",
bestFor: ["Linear sensors", "Most industrial applications", "Good cost/accuracy balance", "Standard practice"],
limitations: ["Assumes linear response", "Won't catch non-linearity", "Sensitive to reference accuracy"],
referencePoints: 2
},
{
id: "multi-point",
name: "Multi-Point Calibration (Curve Fit)",
shortName: "Multi-Point",
description: "Laboratory-grade calibration using 5-10 reference points. Fits polynomial curve to correct non-linearity. Highest accuracy but time-consuming and expensive.",
characteristics: {
accuracy: 5,
time: 2,
cost: 2,
complexity: 2
},
timeMinutes: 60,
costDollars: 200,
typicalAccuracy: "+/-0.1-0.25% of span",
bestFor: ["Non-linear sensors", "High-precision applications", "Regulatory compliance", "Lab standards"],
limitations: ["Time-consuming", "Expensive reference equipment", "Requires skilled technician", "Overfitting risk"],
referencePoints: 5
},
{
id: "temp-compensated",
name: "Temperature-Compensated Calibration",
shortName: "Temp Comp",
description: "Advanced calibration measuring sensor response at multiple temperatures. Creates lookup table or equation to correct temperature drift. Essential for outdoor/industrial sensors.",
characteristics: {
accuracy: 5,
time: 1,
cost: 1,
complexity: 2
},
timeMinutes: 120,
costDollars: 400,
typicalAccuracy: "+/-0.1% across temp range",
bestFor: ["Wide temperature range", "Outdoor deployment", "Industrial environments", "High-stability requirements"],
limitations: ["Very time-consuming", "Requires temperature chamber", "Complex data processing", "May need periodic updates"],
referencePoints: 10
}
]
// ----------------------------------------------------------------------------
// SECTION 2: CALIBRATION SCENARIOS BY DIFFICULTY
// ----------------------------------------------------------------------------
scenarios = ({
beginner: [
{
id: 1,
title: "Warehouse Temperature Monitoring",
sensorType: "NTC Thermistor",
description: "You need to calibrate 50 temperature sensors for a warehouse monitoring system (15-25C range). The warehouse owner wants quick deployment and low cost. Occasional +/-1C error is acceptable.",
environment: {
tempRange: "15-25C (controlled)",
operatingConditions: "Stable indoor environment",
criticalityLevel: "Low (inventory protection)",
budget: "Tight ($500 total for 50 sensors)"
},
sensorSpecs: {
sensorModel: "Generic NTC 10k Ohm",
uncalError: "+/-3C",
linearity: "Non-linear (thermistor)",
driftRate: "0.1C/year",
datasheetAccuracy: "+/-2C after 2-point cal"
},
datasheetNote: "Manufacturer specifies: 2-point calibration (0C, 25C) achieves +/-1C. Multi-point reduces to +/-0.5C but adds $3/sensor cost.",
optimalMethod: "two-point",
explanation: "2-point calibration is optimal because: (1) Warehouse operates in narrow 15-25C range, so calibrate at 15C and 25C boundaries, (2) Corrects both offset and gain for thermistor non-linearity in this range, (3) Cost-effective: $1/sensor vs $4/sensor for multi-point, (4) Fast: 20 min/sensor vs 60+ min, (5) Achieves acceptable +/-1C target. **Why not others?** 1-point can't correct thermistor's non-linear gain (would drift at 25C), multi-point is overkill (10x cost for 0.5C improvement), temp compensation unnecessary (controlled indoor environment).",
scores: {
"single-point": { accuracy: 60, time: 95, cost: 98, overall: 70 },
"two-point": { accuracy: 90, time: 85, cost: 90, overall: 95 },
"multi-point": { accuracy: 95, time: 50, cost: 40, overall: 70 },
"temp-compensated": { accuracy: 98, time: 20, cost: 20, overall: 50 }
}
},
{
id: 2,
title: "Home Weather Station (pH Sensor)",
sensorType: "pH Electrode",
description: "Calibrating a pH sensor for a home aquarium monitor (pH 6.5-8.5 range). Hobbyist application, monthly recalibration acceptable. Need reliable readings for fish health.",
environment: {
tempRange: "22-26C (aquarium heater)",
operatingConditions: "Clean freshwater, stable temp",
criticalityLevel: "Medium (fish health)",
budget: "Moderate ($50 calibration budget)"
},
sensorSpecs: {
sensorModel: "Atlas Scientific pH probe",
uncalError: "+/-0.5 pH",
linearity: "Linear in 6-9 pH range",
driftRate: "0.02 pH/week (electrode aging)",
datasheetAccuracy: "+/-0.1 pH with 2-point"
},
datasheetNote: "Manufacturer recommends: 2-point calibration (pH 7.0, 10.0) monthly. Use pH 4, 7, 10 buffers for 3-point if +/-0.05 needed.",
optimalMethod: "two-point",
explanation: "2-point calibration is optimal because: (1) pH electrodes are linear in the 6-9 range (aquarium use), (2) Standard pH 7.0 (neutral) and 10.0 (basic) buffers are cheap ($15) and shelf-stable, (3) +/-0.1 pH is sufficient for aquarium (fish tolerate 6.5-8.5 range), (4) Monthly recalibration is realistic for hobbyist (20 min), (5) Catches electrode drift from aging/fouling. **Why not others?** 1-point cannot correct slope drift (common in aging pH probes), 3-point unnecessary (linear response, adds cost/time), temp compensation overkill (stable aquarium heater maintains 24C).",
scores: {
"single-point": { accuracy: 55, time: 92, cost: 95, overall: 65 },
"two-point": { accuracy: 92, time: 88, cost: 92, overall: 95 },
"multi-point": { accuracy: 97, time: 60, cost: 70, overall: 80 },
"temp-compensated": { accuracy: 98, time: 30, cost: 40, overall: 60 }
}
},
{
id: 3,
title: "HVAC Humidity Control",
sensorType: "Capacitive Humidity Sensor",
description: "Calibrating 10 humidity sensors for office HVAC system (30-70% RH target). Comfort application, +/-5% RH acceptable. Sensors will run continuously in temperature-controlled building.",
environment: {
tempRange: "20-24C (HVAC controlled)",
operatingConditions: "Indoor, filtered air",
criticalityLevel: "Low (comfort only)",
budget: "Moderate ($300 total)"
},
sensorSpecs: {
sensorModel: "Sensirion SHT31",
uncalError: "+/-3% RH",
linearity: "Very linear 0-100% RH",
driftRate: "<0.5% RH/year",
datasheetAccuracy: "+/-2% RH typical"
},
datasheetNote: "Sensirion ships factory-calibrated. Field recalibration using salt solutions: 33% RH (MgCl2) and 75% RH (NaCl) for 2-point check.",
optimalMethod: "single-point",
explanation: "1-point calibration is optimal (surprising choice!) because: (1) Sensirion SHT31 ships factory-calibrated with +/-2% accuracy - already meets +/-5% spec, (2) Single-point verification at 75% RH (humid, critical for comfort) confirms sensor is still functional, (3) Capacitive humidity sensors are very stable (<0.5%/year drift), (4) HVAC maintains stable 20-24C (no temp compensation needed), (5) Salt solution calibration is cheap ($5) and doesn't require precision equipment. **Why not others?** 2-point adds cost/time with minimal benefit (factory cal sufficient), multi-point massive overkill for comfort application, temp compensation unnecessary (narrow 4C range).",
scores: {
"single-point": { accuracy: 85, time: 95, cost: 98, overall: 95 },
"two-point": { accuracy: 92, time: 80, cost: 85, overall: 85 },
"multi-point": { accuracy: 96, time: 50, cost: 50, overall: 65 },
"temp-compensated": { accuracy: 97, time: 25, cost: 30, overall: 50 }
}
},
{
id: 4,
title: "Cold Chain Logistics (Basic)",
sensorType: "Thermocouple (Type K)",
description: "Calibrating 20 thermocouples for refrigerated truck monitoring (2-8C cold chain). Pharmaceutical shipments require +/-0.5C accuracy with documentation for FDA compliance.",
environment: {
tempRange: "2-8C (critical range)",
operatingConditions: "Refrigerated trucks, vibration",
criticalityLevel: "High (FDA compliance)",
budget: "Adequate ($1000 total)"
},
sensorSpecs: {
sensorModel: "Type K Thermocouple + MAX31855",
uncalError: "+/-2C (cold junction error)",
linearity: "Linear 0-10C",
driftRate: "Stable (thermocouples don't drift)",
datasheetAccuracy: "+/-0.5C with 2-point cal"
},
datasheetNote: "NIST traceable ice bath (0C) and reference thermometer required for FDA compliance. Calibrate at 0C and 8C.",
optimalMethod: "two-point",
explanation: "2-point calibration is optimal because: (1) FDA 21 CFR Part 11 requires traceable calibration with documented procedure - 2-point is industry standard, (2) Calibrate at 0C (ice bath) and 8C (upper limit) to bracket operating range, (3) Thermocouples are inherently linear in 0-10C range, (4) +/-0.5C achievable with NIST-traceable references ($50/cal), (5) Calibration certificate documents traceability for audits. **Why not others?** 1-point insufficient for regulatory compliance and can't correct MAX31855 gain error, multi-point adds cost without accuracy improvement (linear sensor), temp compensation unnecessary (sensors only see 2-8C).",
scores: {
"single-point": { accuracy: 50, time: 90, cost: 92, overall: 60 },
"two-point": { accuracy: 95, time: 80, cost: 85, overall: 95 },
"multi-point": { accuracy: 98, time: 50, cost: 60, overall: 75 },
"temp-compensated": { accuracy: 98, time: 25, cost: 40, overall: 60 }
}
}
],
intermediate: [
{
id: 5,
title: "Industrial Pressure Transmitter",
sensorType: "Piezo-resistive Pressure Sensor",
description: "Calibrating pressure transmitter for pneumatic control system (0-100 PSI). Controls valve actuators, +/-0.5 PSI accuracy needed. Sensor experiences 10-40C temperature swings in factory.",
environment: {
tempRange: "10-40C (factory floor)",
operatingConditions: "Dusty, vibration, temp swings",
criticalityLevel: "High (process control)",
budget: "Good ($500/sensor)"
},
sensorSpecs: {
sensorModel: "Honeywell TruStability HSC",
uncalError: "+/-2 PSI",
linearity: "+/-0.5% FSO non-linearity",
driftRate: "+/-0.25% FSO/year",
tempCoefficient: "+/-0.02%/C zero, +/-0.015%/C span",
datasheetAccuracy: "+/-0.25 PSI with multi-point + temp comp"
},
datasheetNote: "Datasheet: Temp coefficient causes 1-2 PSI error over 30C swing. Multi-point cal at 3 temps (10C, 25C, 40C) with 5 pressures each recommended.",
optimalMethod: "temp-compensated",
explanation: "Temperature-compensated calibration is optimal because: (1) 30C temperature swing causes +/-0.6-1.2 PSI error (exceeds +/-0.5 spec), (2) Piezo-resistive sensors have significant temp coefficient (both zero and span), (3) Multi-point at single temp can't correct temp drift, (4) Creates compensation lookup table: offset(T) and gain(T), (5) Process control application justifies cost ($500 cal vs thousands in product waste). **Why not others?** 1-point/2-point fail in real operating conditions (only accurate at cal temp), multi-point at 25C gives +/-0.25 PSI at 25C but +/-1.5 PSI at 10C/40C.",
scores: {
"single-point": { accuracy: 35, time: 92, cost: 95, overall: 50 },
"two-point": { accuracy: 55, time: 85, cost: 90, overall: 65 },
"multi-point": { accuracy: 75, time: 55, cost: 70, overall: 70 },
"temp-compensated": { accuracy: 95, time: 40, cost: 50, overall: 95 }
}
},
{
id: 6,
title: "Medical Lab Blood Analyzer (pH)",
sensorType: "Glass pH Electrode",
description: "Calibrating pH sensor for blood gas analyzer (pH 7.0-7.6 physiological range). Clinical lab requires +/-0.02 pH accuracy for diagnosis. CLIA regulations mandate monthly verification.",
environment: {
tempRange: "37C +/-0.1C (body temp, precise)",
operatingConditions: "Temperature-controlled analyzer",
criticalityLevel: "Critical (patient diagnosis)",
budget: "No budget limit (patient safety)"
},
sensorSpecs: {
sensorModel: "Radiometer pHC10101",
uncalError: "+/-0.05 pH",
linearity: "Linear 6.8-7.8 pH",
driftRate: "0.005 pH/day (fast drift in use)",
datasheetAccuracy: "+/-0.01 pH with 3-point at 37C",
tempSensitivity: "Critical: pH changes 0.014 units/C"
},
datasheetNote: "NIST-traceable pH 7.00, 7.38, 7.60 buffers at exactly 37C required. Daily 2-point check, weekly 3-point recalibration per CLIA.",
optimalMethod: "multi-point",
explanation: "Multi-point calibration is optimal (not temp-compensated!) because: (1) Blood pH range is tiny (7.0-7.6) and non-linear at this scale - 3 points capture subtle curvature, (2) Medical analyzer precisely controls temp at 37C +/-0.1C, so temp compensation unnecessary, (3) +/-0.02 pH accuracy impossible with 2-point (need curve fit), (4) CLIA regulations require NIST-traceable buffers at physiological pH (7.00, 7.38, 7.60), (5) Fast electrode drift (0.005 pH/day) requires frequent recal - multi-point catches it. **Why not temp-compensated?** Analyzer maintains 37C with Peltier cooler - no temp variation to compensate.",
scores: {
"single-point": { accuracy: 25, time: 90, cost: 95, overall: 40 },
"two-point": { accuracy: 60, time: 85, cost: 92, overall: 70 },
"multi-point": { accuracy: 97, time: 60, cost: 80, overall: 95 },
"temp-compensated": { accuracy: 95, time: 30, cost: 50, overall: 70 }
}
},
{
id: 7,
title: "Dissolved Oxygen (Aquaculture)",
sensorType: "Galvanic DO Sensor",
description: "Calibrating dissolved oxygen sensors for fish farm ponds (0-12 mg/L range). Fish die below 5 mg/L. Water temperature varies 15-28C seasonally. Need +/-0.5 mg/L across temp range.",
environment: {
tempRange: "15-28C (seasonal)",
operatingConditions: "Outdoor ponds, algae, sediment",
criticalityLevel: "Critical (fish survival)",
budget: "Moderate ($200/sensor, 10 sensors)"
},
sensorSpecs: {
sensorModel: "YSI 5100 DO probe",
uncalError: "+/-1.5 mg/L",
linearity: "Non-linear (oxygen solubility curve)",
driftRate: "Membrane degradation, 1-2 weeks",
tempSensitivity: "Huge: O2 solubility 14.6 mg/L at 0C to 8.2 mg/L at 30C",
datasheetAccuracy: "+/-0.3 mg/L with temp comp"
},
datasheetNote: "DO sensors MUST be temp-compensated: oxygen solubility halves from 0C to 30C. Calibrate at air saturation (100%) across temp range 10-30C.",
optimalMethod: "temp-compensated",
explanation: "Temperature-compensated calibration is absolutely essential because: (1) Dissolved oxygen solubility is highly temperature-dependent (Henry's Law) - reading will be wrong by 3-5 mg/L without correction, (2) 13C seasonal swing causes massive DO concentration change, (3) Calibrate at 100% air saturation (shake probe in air) at 10C, 20C, 30C, (4) Create lookup table: DO_actual(sensor_reading, temperature), (5) Without temp comp, fish could die (false 'safe' reading at 28C). **This is non-negotiable.** Even 3-point at single temp fails across seasons.",
scores: {
"single-point": { accuracy: 20, time: 90, cost: 95, overall: 35 },
"two-point": { accuracy: 40, time: 85, cost: 90, overall: 50 },
"multi-point": { accuracy: 60, time: 60, cost: 75, overall: 65 },
"temp-compensated": { accuracy: 95, time: 45, cost: 60, overall: 95 }
}
},
{
id: 8,
title: "Strain Gauge Load Cell (Non-linear)",
sensorType: "Strain Gauge Wheatstone Bridge",
description: "Calibrating load cell for weighing scale (0-500kg capacity). Food processing application needs +/-0.1% accuracy (+/-0.5kg). Load cell shows slight non-linearity at extremes (0-50kg and 450-500kg).",
environment: {
tempRange: "5-35C (processing plant)",
operatingConditions: "Washdown, humidity, temp cycles",
criticalityLevel: "High (trade measurement)",
budget: "Good ($400/cell, 5 cells)"
},
sensorSpecs: {
sensorModel: "Mettler-Toledo SLC411",
uncalError: "+/-2kg",
linearity: "+/-0.15% FSO non-linearity",
driftRate: "+/-0.02%/year",
tempCoefficient: "+/-0.0015%/C zero and span",
datasheetAccuracy: "+/-0.05% with 5-point cal"
},
datasheetNote: "Datasheet recommends 5-point calibration: 0, 100, 250, 400, 500kg using NIST-traceable weights. Non-linearity at low (0-50kg) and high (450-500kg) ends.",
optimalMethod: "multi-point",
explanation: "Multi-point calibration is optimal (not temp-compensated) because: (1) Load cell has +/-0.15% non-linearity (0.75kg error) - exceeds +/-0.5kg spec, (2) 5-point cal captures non-linear S-curve: heavy weighting at 0, 50, 250, 450, 500kg, (3) Polynomial fit (2nd order) reduces error to +/-0.05% (+/-0.25kg), (4) Trade measurement regulations often require 5-point NIST-traceable cal, (5) 30C temp swing only causes +/-0.15kg error (within spec), so temp comp not needed. **Why not temp-compensated?** Temp coefficient is low (+/-0.0015%/C), and processing plant varies only 5-35C.",
scores: {
"single-point": { accuracy: 30, time: 92, cost: 95, overall: 45 },
"two-point": { accuracy: 65, time: 85, cost: 88, overall: 70 },
"multi-point": { accuracy: 95, time: 60, cost: 75, overall: 95 },
"temp-compensated": { accuracy: 96, time: 35, cost: 50, overall: 75 }
}
}
],
expert: [
{
id: 9,
title: "Meteorological Station (Barometric Pressure)",
sensorType: "MEMS Barometric Pressure Sensor",
description: "Calibrating pressure sensors for weather station network (950-1050 hPa). Meteorological-grade accuracy +/-0.1 hPa required. Sensors deployed outdoors across -30C to +50C range. Altitude compensation needed (stations at 0-2000m elevation).",
environment: {
tempRange: "-30 to +50C (extreme outdoor)",
operatingConditions: "Outdoor, all weather, altitude variation",
criticalityLevel: "High (weather forecasting)",
budget: "High ($800/sensor, government contract)"
},
sensorSpecs: {
sensorModel: "Bosch BMP388",
uncalError: "+/-0.5 hPa",
linearity: "+/-0.12 hPa non-linearity",
driftRate: "+/-0.06 hPa/year",
tempCoefficient: "+/-0.08 hPa/C (huge for MEMS)",
altitudeDependence: "-0.12 hPa/meter elevation",
datasheetAccuracy: "+/-0.08 hPa with full temp compensation"
},
datasheetNote: "Professional meteo calibration: 5 pressures x 5 temps (25 points). Reference: Paroscientific digiquartz (+/-0.01 hPa). Temp chamber -30/0/20/40/50C.",
optimalMethod: "temp-compensated",
explanation: "Temperature-compensated calibration is mandatory because: (1) 80C operating range causes +/-6.4 hPa error without correction (64x over spec!), (2) MEMS pressure sensors have huge temp coefficient (silicon piezoresistors), (3) Calibrate at 5 temps (-30, 0, 20, 40, 50C) x 5 pressures (950, 975, 1000, 1025, 1050 hPa) = 25-point matrix, (4) Generate 2D lookup table or polynomial: P_corrected = f(P_raw, T), (5) Must use Paroscientific digiquartz reference (+/-0.01 hPa, $12k) for traceability. **Why so complex?** Weather forecasting needs +/-0.1 hPa - pressure changes of 1 hPa/hour indicate storms. Even multi-point at 20C fails at -30C or +50C field deployment.",
scores: {
"single-point": { accuracy: 10, time: 90, cost: 95, overall: 25 },
"two-point": { accuracy: 30, time: 85, cost: 90, overall: 40 },
"multi-point": { accuracy: 60, time: 50, cost: 70, overall: 60 },
"temp-compensated": { accuracy: 97, time: 35, cost: 45, overall: 95 }
}
},
{
id: 10,
title: "Jet Engine Thermocouple (Aerospace)",
sensorType: "Type R Platinum Thermocouple",
description: "Calibrating thermocouples for jet engine exhaust gas temp (EGT) monitoring (400-900C). Aerospace application requires +/-2C accuracy. Thermocouples experience rapid temp cycles and oxidation. FAA traceability required.",
environment: {
tempRange: "400-900C (jet exhaust)",
operatingConditions: "High temp, oxidation, vibration, thermal shock",
criticalityLevel: "Critical (flight safety)",
budget: "Unlimited (FAA compliance)"
},
sensorSpecs: {
sensorModel: "Type R Pt-Rh thermocouple",
uncalError: "+/-5C (wire inhomogeneity)",
linearity: "Slightly non-linear (Seebeck curve)",
driftRate: "Pt oxidation causes drift at high temp",
thermalShock: "Grain boundary changes from rapid cycling",
datasheetAccuracy: "+/-1.5C with 4-point cal per ASTM E220"
},
datasheetNote: "FAA requires: NIST-traceable calibration at 450, 600, 750, 900C using precision tube furnace. Checked against secondary standard (+/-0.5C). Recalibrate every 500 flight hours.",
optimalMethod: "multi-point",
explanation: "Multi-point calibration is optimal (not temp-compensated!) because: (1) Thermocouples are self-compensating - they generate voltage from temp differential, no external compensation needed, (2) 4-point cal (450, 600, 750, 900C) captures Seebeck curve non-linearity and wire inhomogeneity, (3) FAA AC 43.13-1B mandates multi-point NIST traceability for flight-critical sensors, (4) Polynomial fit corrects manufacturing variations and drift from oxidation, (5) Tube furnace ($15k) provides stable reference temps with +/-1C uniformity. **Why not temp-compensated?** Thermocouples measure temperature directly - they don't need ambient temp correction like RTDs/thermistors. Multi-point captures high-temp drift.",
scores: {
"single-point": { accuracy: 20, time: 90, cost: 95, overall: 35 },
"two-point": { accuracy: 55, time: 80, cost: 85, overall: 65 },
"multi-point": { accuracy: 96, time: 55, cost: 70, overall: 95 },
"temp-compensated": { accuracy: 85, time: 30, cost: 50, overall: 60 }
}
},
{
id: 11,
title: "Cleanroom Particle Counter (Non-linear Response)",
sensorType: "Laser Particle Counter",
description: "Calibrating optical particle counter for ISO Class 5 cleanroom (semiconductor fab). Must count 0.1-5.0 um particles with +/-10% accuracy per size bin. Response is highly non-linear (Mie scattering). Critical for product yield.",
environment: {
tempRange: "20-22C (cleanroom spec)",
operatingConditions: "Ultra-clean, temp/humidity controlled",
criticalityLevel: "Critical (million-dollar batches)",
budget: "Very high ($2000/cal, 20 units)"
},
sensorSpecs: {
sensorModel: "Lighthouse Solair 3100",
uncalError: "+/-50% (uncalibrated)",
linearity: "Highly non-linear (Mie scattering curve)",
driftRate: "Laser aging, mirror contamination",
sizeBins: "0.1, 0.3, 0.5, 1.0, 3.0, 5.0 um",
datasheetAccuracy: "+/-10% per bin with 6-point calibration"
},
datasheetNote: "ISO 21501-4 calibration: PSL (polystyrene latex) spheres at 6 sizes. NIST-traceable size standards. Each size bin calibrated independently due to Mie scattering non-linearity.",
optimalMethod: "multi-point",
explanation: "Multi-point calibration is absolutely required because: (1) Optical particle counters are fundamentally non-linear - Mie scattering intensity varies chaotically with particle size (not monotonic!), (2) 6-point calibration uses NIST-traceable PSL spheres (0.1, 0.3, 0.5, 1.0, 3.0, 5.0 um), (3) Each size bin gets independent calibration curve - can't interpolate between points, (4) ISO 21501-4 standard mandates this procedure, (5) Semiconductor cleanrooms need +/-10% accuracy (0.1 um particles cause defects -> $1M batch loss). **Why not others?** 2-point completely fails (Mie curve has peaks/valleys), temp compensation irrelevant (cleanroom is 20C +/-2C), 1-point meaningless for multi-bin counter.",
scores: {
"single-point": { accuracy: 15, time: 88, cost: 92, overall: 30 },
"two-point": { accuracy: 35, time: 80, cost: 85, overall: 45 },
"multi-point": { accuracy: 95, time: 50, cost: 65, overall: 95 },
"temp-compensated": { accuracy: 95, time: 30, cost: 50, overall: 70 }
}
},
{
id: 12,
title: "Research-Grade CO2 Sensor (Climate Science)",
sensorType: "NDIR CO2 Sensor",
description: "Calibrating NDIR CO2 sensor for atmospheric research station (400-450 ppm range). Climate science requires +/-1 ppm accuracy to detect trends. Sensor deployed in Arctic (-40C winter) and measured against WMO reference.",
environment: {
tempRange: "-40 to +20C (Arctic station)",
operatingConditions: "Outdoor, extreme cold, high humidity",
criticalityLevel: "Critical (climate monitoring)",
budget: "Research-grade ($3000/cal, NSF funding)"
},
sensorSpecs: {
sensorModel: "LI-COR LI-7000",
uncalError: "+/-15 ppm",
linearity: "Non-linear (Beer-Lambert curve)",
driftRate: "+/-2 ppm/year (IR source aging)",
tempSensitivity: "+/-0.3 ppm/C zero offset",
pressureDependence: "+/-0.2 ppm/hPa (high altitude)",
datasheetAccuracy: "+/-0.5 ppm with full calibration"
},
datasheetNote: "WMO GAW requirements: 5-point calibration (350, 400, 450, 500, 550 ppm) using NOAA-certified reference gases. Temp compensation -40 to +20C. Pressure correction for altitude.",
optimalMethod: "temp-compensated",
explanation: "Temperature-compensated calibration is required because: (1) 60C operating range causes +/-18 ppm zero drift (18x over +/-1 ppm spec!) from thermal expansion of optical cavity, (2) Climate science needs sub-ppm accuracy to detect 2 ppm/year atmospheric CO2 rise, (3) Multi-point at single temp would fail: sensor accurate at +20C calibration but drifts +/-15 ppm at -40C Arctic winter, (4) Calibrate using NOAA-certified reference gases (+/-0.05 ppm) at 5 temps x 5 CO2 levels, (5) Generate correction equation: CO2_corrected = f(CO2_raw, T, P), (6) WMO GAW standards mandate this for official climate monitoring stations. **This is the most demanding calibration scenario.**",
scores: {
"single-point": { accuracy: 5, time: 88, cost: 92, overall: 20 },
"two-point": { accuracy: 25, time: 82, cost: 88, overall: 40 },
"multi-point": { accuracy: 70, time: 45, cost: 65, overall: 70 },
"temp-compensated": { accuracy: 98, time: 30, cost: 50, overall: 95 }
}
}
]
})
// ----------------------------------------------------------------------------
// SECTION 3: GAME STATE MANAGEMENT
// ----------------------------------------------------------------------------
mutable gameState = ({
difficulty: "beginner",
currentScenarioIndex: 0,
score: 0,
answered: false,
selectedMethod: null,
correctAnswers: 0,
totalQuestions: 0,
gameComplete: false,
showFeedback: false,
performanceMetrics: {
accuracyTotal: 0,
timeTotal: 0,
costTotal: 0,
questionsAnswered: 0
}
})
// Get current scenario
currentScenario = scenarios[gameState.difficulty][gameState.currentScenarioIndex]
// Calculate total scenarios for current difficulty
totalScenarios = scenarios[gameState.difficulty].length
// ----------------------------------------------------------------------------
// SECTION 4: GAME LOGIC FUNCTIONS
// ----------------------------------------------------------------------------
function selectMethod(methodId) {
if (gameState.answered) return;
mutable gameState = { ...gameState, selectedMethod: methodId };
}
function submitAnswer() {
if (!gameState.selectedMethod || gameState.answered) return;
const isCorrect = gameState.selectedMethod === currentScenario.optimalMethod;
const scores = currentScenario.scores[gameState.selectedMethod];
// Calculate points based on overall score (accuracy + time + cost balance)
const points = Math.round(scores.overall);
mutable gameState = {
...gameState,
answered: true,
showFeedback: true,
score: gameState.score + points,
correctAnswers: isCorrect ? gameState.correctAnswers + 1 : gameState.correctAnswers,
totalQuestions: gameState.totalQuestions + 1,
performanceMetrics: {
accuracyTotal: gameState.performanceMetrics.accuracyTotal + scores.accuracy,
timeTotal: gameState.performanceMetrics.timeTotal + scores.time,
costTotal: gameState.performanceMetrics.costTotal + scores.cost,
questionsAnswered: gameState.performanceMetrics.questionsAnswered + 1
}
};
}
function nextScenario() {
const nextIndex = gameState.currentScenarioIndex + 1;
if (nextIndex >= scenarios[gameState.difficulty].length) {
mutable gameState = { ...gameState, gameComplete: true };
} else {
mutable gameState = {
...gameState,
currentScenarioIndex: nextIndex,
answered: false,
selectedMethod: null,
showFeedback: false
};
}
}
function changeDifficulty(difficulty) {
mutable gameState = {
difficulty: difficulty,
currentScenarioIndex: 0,
score: 0,
answered: false,
selectedMethod: null,
correctAnswers: 0,
totalQuestions: 0,
gameComplete: false,
showFeedback: false,
performanceMetrics: {
accuracyTotal: 0,
timeTotal: 0,
costTotal: 0,
questionsAnswered: 0
}
};
}
function restartGame() {
mutable gameState = {
...gameState,
currentScenarioIndex: 0,
score: 0,
answered: false,
selectedMethod: null,
correctAnswers: 0,
totalQuestions: 0,
gameComplete: false,
showFeedback: false,
performanceMetrics: {
accuracyTotal: 0,
timeTotal: 0,
costTotal: 0,
questionsAnswered: 0
}
};
}
// ----------------------------------------------------------------------------
// SECTION 5: UI RENDERING
// ----------------------------------------------------------------------------
viewof calibrationGame = {
const container = html`<div class="calibration-game-container">
<div class="game-header">
<h2>Sensor Calibration Challenge</h2>
<p>Choose the best calibration method for each scenario</p>
</div>
<div class="game-controls">
<button class="difficulty-btn beginner ${gameState.difficulty === 'beginner' ? 'active' : ''}"
onclick=${() => changeDifficulty('beginner')}>
Beginner
</button>
<button class="difficulty-btn intermediate ${gameState.difficulty === 'intermediate' ? 'active' : ''}"
onclick=${() => changeDifficulty('intermediate')}>
Intermediate
</button>
<button class="difficulty-btn expert ${gameState.difficulty === 'expert' ? 'active' : ''}"
onclick=${() => changeDifficulty('expert')}>
Expert
</button>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${(gameState.currentScenarioIndex / totalScenarios) * 100}%"></div>
</div>
<div class="game-stats">
<div class="stat-card">
<div class="stat-value">${gameState.score}</div>
<div class="stat-label">Score</div>
</div>
<div class="stat-card">
<div class="stat-value">${gameState.currentScenarioIndex + 1}/${totalScenarios}</div>
<div class="stat-label">Scenario</div>
</div>
<div class="stat-card">
<div class="stat-value">${gameState.correctAnswers}/${gameState.totalQuestions}</div>
<div class="stat-label">Optimal</div>
</div>
<div class="stat-card">
<div class="stat-value">${gameState.totalQuestions > 0 ? Math.round((gameState.correctAnswers / gameState.totalQuestions) * 100) : 0}%</div>
<div class="stat-label">Accuracy</div>
</div>
</div>
<div id="game-content"></div>
</div>`;
const gameContent = container.querySelector("#game-content");
if (gameState.gameComplete) {
renderGameComplete(gameContent);
} else {
renderScenario(gameContent);
}
return container;
}
function renderScenario(container) {
const scenario = currentScenario;
container.innerHTML = `
<div class="scenario-card">
<div class="scenario-header">
<div class="scenario-title">${scenario.title}</div>
<div class="scenario-badges">
<div class="scenario-badge badge-sensor">${scenario.sensorType}</div>
<div class="scenario-badge badge-number">Scenario ${scenario.id}</div>
</div>
</div>
<div class="scenario-description">
${scenario.description}
</div>
<div class="sensor-specs">
<div class="specs-title">Sensor Specifications and Operating Conditions</div>
<div class="specs-grid">
<div class="spec-item">
<span class="spec-label">Temperature Range:</span>
<span class="spec-value">${scenario.environment.tempRange}</span>
</div>
<div class="spec-item">
<span class="spec-label">Criticality:</span>
<span class="spec-value">${scenario.environment.criticalityLevel}</span>
</div>
<div class="spec-item">
<span class="spec-label">Uncalibrated Error:</span>
<span class="spec-value">${scenario.sensorSpecs.uncalError}</span>
</div>
<div class="spec-item">
<span class="spec-label">Linearity:</span>
<span class="spec-value">${scenario.sensorSpecs.linearity}</span>
</div>
<div class="spec-item">
<span class="spec-label">Drift Rate:</span>
<span class="spec-value">${scenario.sensorSpecs.driftRate}</span>
</div>
<div class="spec-item">
<span class="spec-label">Budget:</span>
<span class="spec-value">${scenario.environment.budget}</span>
</div>
</div>
<div class="datasheet-info">
<strong>Datasheet Note:</strong> ${scenario.datasheetNote}
</div>
</div>
<div style="margin-bottom: 12px;">
<strong>Select the best calibration method for this scenario:</strong>
</div>
<div class="calibration-options">
${calibrationMethods.map(method => {
let btnClass = 'calibration-btn';
if (gameState.selectedMethod === method.id && !gameState.answered) {
btnClass += ' selected';
} else if (gameState.answered && method.id === scenario.optimalMethod) {
btnClass += ' correct';
} else if (gameState.answered && gameState.selectedMethod === method.id && method.id !== scenario.optimalMethod) {
btnClass += ' incorrect';
}
const scores = scenario.scores[method.id];
return `
<button class="${btnClass}"
onclick="selectMethod('${method.id}')"
${gameState.answered ? 'disabled' : ''}>
<div class="calibration-info">
<div class="calibration-details">
<div class="calibration-name">${method.name}</div>
<div class="calibration-description">${method.description}</div>
</div>
<div class="calibration-stats">
<div class="stat-mini">
<div class="stat-mini-label">Accuracy</div>
<div class="stat-mini-value">${scores.accuracy}%</div>
</div>
<div class="stat-mini">
<div class="stat-mini-label">Time</div>
<div class="stat-mini-value">${scores.time}%</div>
</div>
<div class="stat-mini">
<div class="stat-mini-label">Cost</div>
<div class="stat-mini-value">${scores.cost}%</div>
</div>
</div>
</div>
</button>
`;
}).join('')}
</div>
<div class="action-buttons">
<button class="action-btn submit"
onclick="submitAnswer()"
${!gameState.selectedMethod || gameState.answered ? 'disabled' : ''}>
Submit Answer
</button>
<button class="action-btn next"
onclick="nextScenario()"
${!gameState.answered ? 'disabled' : ''}>
Next Scenario
</button>
</div>
<div class="feedback-panel ${gameState.showFeedback ? 'show' : ''} ${gameState.selectedMethod === scenario.optimalMethod ? 'correct' : 'incorrect'}">
${gameState.showFeedback ? renderFeedback() : ''}
</div>
</div>
`;
}
function renderFeedback() {
const scenario = currentScenario;
const selectedMethod = calibrationMethods.find(m => m.id === gameState.selectedMethod);
const optimalMethod = calibrationMethods.find(m => m.id === scenario.optimalMethod);
const isCorrect = gameState.selectedMethod === scenario.optimalMethod;
const scores = scenario.scores[gameState.selectedMethod];
return `
<div class="feedback-title">
${isCorrect ? 'Optimal Choice!' : 'Suboptimal Choice'}
${` +${Math.round(scores.overall)} points (Accuracy: ${scores.accuracy}%, Time: ${scores.time}%, Cost: ${scores.cost}%)`}
</div>
<div class="feedback-content">
<p><strong>Optimal Method: ${optimalMethod.name}</strong></p>
<p>${scenario.explanation}</p>
${!isCorrect ? `
<p style="margin-top: 16px;">
<strong>Your Selection (${selectedMethod.name}):</strong><br/>
While this method could work, it's not optimal for this scenario. You scored ${Math.round(scores.overall)}/100 (the optimal method would score ~95).
</p>
` : ''}
<table class="comparison-table">
<thead>
<tr>
<th>Method Comparison</th>
<th>${optimalMethod.shortName}</th>
${!isCorrect ? `<th>${selectedMethod.shortName}</th>` : ''}
</tr>
</thead>
<tbody>
<tr>
<td><strong>Time Required</strong></td>
<td>${optimalMethod.timeMinutes} min</td>
${!isCorrect ? `<td>${selectedMethod.timeMinutes} min</td>` : ''}
</tr>
<tr>
<td><strong>Cost per Sensor</strong></td>
<td>$${optimalMethod.costDollars}</td>
${!isCorrect ? `<td>$${selectedMethod.costDollars}</td>` : ''}
</tr>
<tr>
<td><strong>Typical Accuracy</strong></td>
<td>${optimalMethod.typicalAccuracy}</td>
${!isCorrect ? `<td>${selectedMethod.typicalAccuracy}</td>` : ''}
</tr>
<tr>
<td><strong>Reference Points</strong></td>
<td>${optimalMethod.referencePoints} point(s)</td>
${!isCorrect ? `<td>${selectedMethod.referencePoints} point(s)</td>` : ''}
</tr>
</tbody>
</table>
<p style="margin-top: 16px;">
<strong>Key Lesson:</strong> Calibration method selection requires balancing accuracy requirements, time constraints, budget, sensor characteristics (linearity, temp sensitivity, drift), and operating conditions. The "best" method depends entirely on the application context.
</p>
</div>
`;
}
function renderGameComplete(container) {
const accuracy = gameState.totalQuestions > 0 ? Math.round((gameState.correctAnswers / gameState.totalQuestions) * 100) : 0;
const maxScore = scenarios[gameState.difficulty].length * 95; // Max ~95 points per scenario
const scorePercentage = Math.round((gameState.score / maxScore) * 100);
// Calculate average performance metrics
const avgAccuracy = Math.round(gameState.performanceMetrics.accuracyTotal / gameState.performanceMetrics.questionsAnswered);
const avgTime = Math.round(gameState.performanceMetrics.timeTotal / gameState.performanceMetrics.questionsAnswered);
const avgCost = Math.round(gameState.performanceMetrics.costTotal / gameState.performanceMetrics.questionsAnswered);
let feedback, emoji;
if (accuracy >= 90 && scorePercentage >= 90) {
feedback = "Outstanding! You're a calibration expert! You understand how to balance accuracy, time, and cost based on application requirements.";
emoji = "Trophy";
} else if (accuracy >= 75 && scorePercentage >= 75) {
feedback = "Great job! You have strong calibration knowledge. Review the feedback to refine your decision-making.";
emoji = "Celebration";
} else if (accuracy >= 50 && scorePercentage >= 60) {
feedback = "Good effort! You're learning the trade-offs. Study the explanations to understand why certain methods are optimal.";
emoji = "Thumbs up";
} else {
feedback = "Keep learning! Calibration method selection is complex. Review the scenarios and try again to master the concepts.";
emoji = "Books";
}
container.innerHTML = `
<div class="game-complete">
<div class="complete-icon">${emoji}</div>
<div class="complete-title">Challenge Complete!</div>
<div class="complete-score">
Final Score: ${gameState.score} / ${maxScore} (${scorePercentage}%)
</div>
<div class="complete-score">
Optimal Choices: ${gameState.correctAnswers} / ${gameState.totalQuestions} (${accuracy}%)
</div>
<div class="complete-feedback">${feedback}</div>
<div class="performance-breakdown">
<h3>Your Performance Breakdown</h3>
<div class="performance-bar">
<div class="performance-label">
<span>Accuracy Balance</span>
<span>${avgAccuracy}%</span>
</div>
<div class="performance-track">
<div class="performance-fill" style="width: ${avgAccuracy}%">${avgAccuracy}%</div>
</div>
</div>
<div class="performance-bar">
<div class="performance-label">
<span>Time Efficiency</span>
<span>${avgTime}%</span>
</div>
<div class="performance-track">
<div class="performance-fill" style="width: ${avgTime}%">${avgTime}%</div>
</div>
</div>
<div class="performance-bar">
<div class="performance-label">
<span>Cost Efficiency</span>
<span>${avgCost}%</span>
</div>
<div class="performance-track">
<div class="performance-fill" style="width: ${avgCost}%">${avgCost}%</div>
</div>
</div>
</div>
<div style="margin: 24px 0; text-align: left; max-width: 700px; margin-left: auto; margin-right: auto; background: #f8f9fa; padding: 20px; border-radius: 10px;">
<h3 style="color: #2C3E50; margin-bottom: 16px;">What You've Learned</h3>
<ul style="line-height: 1.8; color: #333;">
<li><strong>1-Point Calibration:</strong> Fast and cheap, corrects offset only. Best for field checks and known-linear sensors. Cannot fix gain errors.</li>
<li><strong>2-Point Calibration:</strong> Industry standard. Corrects offset + gain. Excellent cost/accuracy balance for linear sensors. Most common choice.</li>
<li><strong>Multi-Point Calibration:</strong> Lab-grade accuracy. Corrects non-linearity with curve fitting. Required for precision instruments and regulatory compliance.</li>
<li><strong>Temperature-Compensated Calibration:</strong> Essential when sensor operates across wide temp range. Creates correction curves for temperature-dependent errors.</li>
</ul>
<h3 style="color: #2C3E50; margin: 20px 0 16px 0;">Key Decision Factors</h3>
<ul style="line-height: 1.8; color: #333;">
<li><strong>Sensor Linearity:</strong> Linear sensors -> 2-point. Non-linear -> multi-point.</li>
<li><strong>Temperature Range:</strong> Narrow/stable -> simple cal. Wide swing -> temp compensation mandatory.</li>
<li><strong>Accuracy Requirements:</strong> Comfort (+/-5%) -> 1-point. Industrial (+/-1%) -> 2-point. Lab (+/-0.1%) -> multi-point.</li>
<li><strong>Application Criticality:</strong> Low-risk -> simple. Safety/regulatory -> comprehensive.</li>
<li><strong>Budget vs Accuracy:</strong> Always balance cost, time, and required accuracy. "Perfect" calibration isn't always optimal.</li>
</ul>
<h3 style="color: #2C3E50; margin: 20px 0 16px 0;">Real-World Calibration Tips</h3>
<ul style="line-height: 1.8; color: #333;">
<li><strong>Always read the datasheet</strong> - manufacturers specify recommended cal methods and reference standards</li>
<li><strong>Drift matters</strong> - pH electrodes drift weekly, thermocouples are stable for years</li>
<li><strong>Temperature coefficients</strong> - most sensors have temp coefficients, check if compensation is needed</li>
<li><strong>NIST traceability</strong> - regulatory/critical apps require traceable reference standards</li>
<li><strong>Calibration point selection</strong> - always calibrate at or near the operating range boundaries</li>
</ul>
</div>
<button class="restart-btn" onclick="restartGame()">
Play Again
</button>
<div style="margin-top: 24px;">
<p style="font-size: 0.9em; color: #666;">
Try different difficulty levels to master calibration decision-making across various applications!
</p>
</div>
</div>
`;
}
// Make functions globally accessible
window.selectMethod = selectMethod;