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

Software simulation

Source snapshot · 5 November 2026

testsmartnoacontroller.py

"""
Unit tests for Smart NOA Controller (Educational Simulation)
Tests core safety interlock logic, constraint enforcement, and simulated dose calculations.
This is an educational project — not for clinical use.
"""

import os
import tempfile
import unittest

from smart_noa_controller import ConfigLoader, Patient, Pharmacokinetics, SmartNOAController


class TestConfigLoader(unittest.TestCase):
    """Test configuration file loading and validation"""

    def test_config_loads_successfully(self):
        """Configuration file should load without errors"""
        config = ConfigLoader("config.yaml")

        # Assert config object exists
        self.assertIsNotNone(config.config)

        # Assert critical sections exist
        self.assertIn('hemodynamic_thresholds', config.config)
        self.assertIn('pharmacokinetics', config.config)
        self.assertIn('drug_dosing', config.config)
        self.assertIn('contraindications', config.config)

    def test_config_get_nested_values(self):
        """Should retrieve nested configuration values correctly"""
        config = ConfigLoader("config.yaml")

        # Test nested key access
        hr_threshold = config.get('hemodynamic_thresholds', 'hr_critical_low')
        self.assertEqual(hr_threshold, 48)

        # Test deeper nesting
        dex_dose = config.get('drug_dosing', 'dexmedetomidine', 'standard_dose')
        self.assertEqual(dex_dose, 0.5)

    def test_config_missing_file_raises_error(self):
        """Should raise FileNotFoundError for missing config"""
        with self.assertRaises(FileNotFoundError):
            ConfigLoader("nonexistent_config.yaml")

    def test_config_validation_detects_missing_sections(self):
        """Should raise ValueError if critical sections missing"""
        # Create temporary incomplete config
        incomplete_config = """
version: "1.0.0"
hemodynamic_thresholds:
  hr_critical_low: 48
"""
        with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            f.write(incomplete_config)
            temp_path = f.name

        try:
            with self.assertRaises(ValueError):
                ConfigLoader(temp_path)
        finally:
            os.unlink(temp_path)


class TestContraindications(unittest.TestCase):
    """Test hard contraindication lockouts"""

    def test_heart_block_locks_dexmedetomidine(self):
        """Simulated profile with heart block should have dexmedetomidine hard-locked"""
        patient = Patient(
            age=72,
            weight_kg=70,
            asa_class=3,
            egfr=80,
            allergies=[],
            comorbidities=["Heart Block"]
        )

        controller = SmartNOAController(patient)

        # Assert dexmedetomidine is in hard lockouts
        self.assertIn("Dexmedetomidine", controller.hard_lockouts,
                     "Dexmedetomidine should be locked out for heart block")

        # Assert contraindication check returns False
        allowed, msg = controller.check_contraindication("Dexmedetomidine")
        self.assertFalse(allowed,
                        "Dexmedetomidine should not be allowed for heart block")
        self.assertIn("HARD LOCK", msg)

    def test_low_egfr_locks_ketorolac(self):
        """Simulated profile with eGFR < 30 should have ketorolac hard-locked"""
        patient = Patient(
            age=65,
            weight_kg=75,
            asa_class=3,
            egfr=24,  # Below threshold of 30
            allergies=[],
            comorbidities=[]
        )

        controller = SmartNOAController(patient)

        # Assert ketorolac is in hard lockouts
        self.assertIn("Ketorolac", controller.hard_lockouts,
                     "Ketorolac should be locked out for eGFR < 30")

        # Assert contraindication check returns False
        allowed, msg = controller.check_contraindication("Ketorolac")
        self.assertFalse(allowed,
                        "Ketorolac should not be allowed for eGFR < 30")
        self.assertIn("HARD LOCK", msg)

    def test_nsaid_allergy_locks_ketorolac(self):
        """Simulated profile with NSAID allergy should have ketorolac hard-locked"""
        patient = Patient(
            age=45,
            weight_kg=80,
            asa_class=2,
            egfr=95,
            allergies=["NSAID"],
            comorbidities=[]
        )

        controller = SmartNOAController(patient)

        # Assert ketorolac is in hard lockouts
        self.assertIn("Ketorolac", controller.hard_lockouts,
                     "Ketorolac should be locked out for NSAID allergy")

    def test_healthy_patient_no_lockouts(self):
        """Healthy simulated profile should have no hard lockouts"""
        patient = Patient(
            age=35,
            weight_kg=70,
            asa_class=1,
            egfr=100,
            allergies=[],
            comorbidities=[]
        )

        controller = SmartNOAController(patient)

        # Assert no lockouts
        self.assertEqual(len(controller.hard_lockouts), 0,
                        "Healthy simulated profile should have no hard lockouts")


class TestAgeDosing(unittest.TestCase):
    """Test age-adjusted dose calculations"""

    def test_geriatric_dexmedetomidine_reduction(self):
        """Simulated geriatric profile (> 65) should get 50% dexmedetomidine dose reduction"""
        patient = Patient(
            age=78,
            weight_kg=65,
            asa_class=3,
            egfr=70,
            allergies=[],
            comorbidities=[]  # No heart block
        )

        controller = SmartNOAController(patient)
        rates = controller.generate_starting_rates()

        # Assert dexmedetomidine dose is reduced to 0.25 mcg/kg/hr
        self.assertEqual(rates["Dexmedetomidine"], 0.25,
                        "Geriatric profiles should get 0.25 mcg/kg/hr dex dose")

    def test_young_adult_standard_dosing(self):
        """Simulated adult profile (< 65) should get standard dexmedetomidine dose"""
        patient = Patient(
            age=42,
            weight_kg=75,
            asa_class=2,
            egfr=95,
            allergies=[],
            comorbidities=[]
        )

        controller = SmartNOAController(patient)
        rates = controller.generate_starting_rates()

        # Assert standard dexmedetomidine dose is 0.5 mcg/kg/hr
        self.assertEqual(rates["Dexmedetomidine"], 0.5,
                        "Young adults should get 0.5 mcg/kg/hr dex dose")

    def test_boundary_age_65(self):
        """Profile at age boundary (65) should not trigger geriatric dose reduction"""
        patient = Patient(
            age=65,
            weight_kg=70,
            asa_class=2,
            egfr=80,
            allergies=[],
            comorbidities=[]
        )

        controller = SmartNOAController(patient)
        rates = controller.generate_starting_rates()

        # Assert reduced dose at boundary
        self.assertEqual(rates["Dexmedetomidine"], 0.5,
                        "Age 65 should still get standard dose (threshold is >65)")


class TestPharmacokinetics(unittest.TestCase):
    """Test pharmacokinetic model calculations"""

    def test_pk_model_initialization(self):
        """PK model should initialize with correct parameters"""
        pk = Pharmacokinetics(
            weight_kg=70,
            central_vol=0.8,
            k10=0.04,
            k1e=0.1
        )

        # Assert initial concentrations are zero
        self.assertEqual(pk.Cp, 0.0, "Initial plasma concentration should be 0")
        self.assertEqual(pk.Ce, 0.0, "Initial effect-site concentration should be 0")

        # Assert volume calculation
        expected_vc = 0.8 * 70  # 56 L
        self.assertEqual(pk.Vc, expected_vc, "Central volume should be 56 L")

    def test_pk_concentration_increases_with_infusion(self):
        """Concentrations should increase during infusion"""
        pk = Pharmacokinetics(
            weight_kg=70,
            central_vol=0.8,
            k10=0.04,
            k1e=0.1
        )

        # Numerical model exercise at 35 mcg/min; not a clinical dose recommendation
        for _ in range(10):
            pk.update_concentration(infusion_rate_mcg_per_min=35, time_delta_min=1.0)

        # Assert concentrations have increased
        self.assertGreater(pk.Cp, 0, "Plasma concentration should increase")
        self.assertGreater(pk.Ce, 0, "Effect-site concentration should increase")

    def test_pk_concentration_decreases_after_stop(self):
        """Concentrations should decrease after stopping infusion"""
        pk = Pharmacokinetics(
            weight_kg=70,
            central_vol=0.8,
            k10=0.04,
            k1e=0.1
        )

        # Build up concentration
        for _ in range(10):
            pk.update_concentration(infusion_rate_mcg_per_min=35, time_delta_min=1.0)

        peak_cp = pk.Cp

        # Stop infusion and run for 5 minutes
        for _ in range(5):
            pk.update_concentration(infusion_rate_mcg_per_min=0, time_delta_min=1.0)

        # Assert plasma concentration decreased
        self.assertLess(pk.Cp, peak_cp,
                       "Plasma concentration should decrease after stopping infusion")

    def test_pk_no_negative_concentrations(self):
        """PK model should never produce negative concentrations"""
        pk = Pharmacokinetics(
            weight_kg=70,
            central_vol=0.8,
            k10=0.04,
            k1e=0.1
        )

        # Run extreme scenario: very high elimination with no infusion
        for _ in range(100):
            pk.update_concentration(infusion_rate_mcg_per_min=0, time_delta_min=1.0)

        # Assert no negative values
        self.assertGreaterEqual(pk.Cp, 0, "Plasma concentration cannot be negative")
        self.assertGreaterEqual(pk.Ce, 0, "Effect-site concentration cannot be negative")


class TestProtocolGeneration(unittest.TestCase):
    """Test complete protocol generation"""

    def test_multi_drug_protocol_generation(self):
        """Controller should generate complete multi-drug protocol"""
        patient = Patient(
            age=50,
            weight_kg=80,
            asa_class=2,
            egfr=90,
            allergies=[],
            comorbidities=[]
        )

        controller = SmartNOAController(patient)
        rates = controller.generate_starting_rates()

        # Assert all three drugs are present
        self.assertIn("Dexmedetomidine", rates)
        self.assertIn("Ketamine", rates)
        self.assertIn("Lidocaine", rates)

        # Assert doses are reasonable
        self.assertGreater(rates["Dexmedetomidine"], 0)
        self.assertGreater(rates["Ketamine"], 0)
        self.assertGreater(rates["Lidocaine"], 0)

    def test_contraindication_sets_dose_to_zero(self):
        """Hard lockout should result in zero dose for that drug"""
        patient = Patient(
            age=70,
            weight_kg=75,
            asa_class=3,
            egfr=25,  # Will lock out ketorolac
            allergies=[],
            comorbidities=["Heart Block"]  # Will lock out dexmedetomidine
        )

        controller = SmartNOAController(patient)
        rates = controller.generate_starting_rates()

        # Assert dexmedetomidine is zero
        self.assertEqual(rates["Dexmedetomidine"], 0.0,
                        "Contraindicated dexmedetomidine should have zero dose")

        # Assert ketorolac is marked as locked out
        self.assertIn("LOCKED OUT", rates.get("Ketorolac Adjunct", ""))


class TestRateConversion(unittest.TestCase):
    """Test infusion rate unit conversions"""

    def test_mcg_kg_hr_to_mcg_min_conversion(self):
        """Test conversion from mcg/kg/hr to mcg/min"""
        patient = Patient(
            age=40,
            weight_kg=70,
            asa_class=2,
            egfr=95,
            allergies=[],
            comorbidities=[]
        )

        controller = SmartNOAController(patient)

        # Set dexmedetomidine to 0.6 mcg/kg/hr
        controller.infusions["Dexmedetomidine"] = 0.6

        # Convert to mcg/min
        rate_mcg_min = controller._rate_to_mcg_per_min("Dexmedetomidine")

        # Expected: 0.6 mcg/kg/hr × 70 kg / 60 min = 0.7 mcg/min
        expected = (0.6 * 70) / 60
        self.assertAlmostEqual(rate_mcg_min, expected, places=2,
                              msg="Rate conversion should be accurate")


if __name__ == '__main__':
    # Run tests with verbose output
    unittest.main(verbosity=2)
Source and scope

Published from Smart-NOA-Controller/test_smart_noa_controller.py 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.