Exemplo n.º 1
0
    def action_default(self):
        """Present currently unlocked set of results"""
        patientId = int(self.requestData["sim_patient_id"]);
        simTime = int(self.requestData["sim_time"]);
        
        manager = SimManager();

        # Load and format the results available so far
        results = manager.loadResults(patientId, simTime);
        lastGroupStrings = ['New']; # Sentinel value that will be different than first real data row
        lenLastGroupStrings = len(lastGroupStrings);
        htmlLines = list();
        for dataModel in results:
            self.formatDataModel(dataModel);
            for i, groupString in enumerate(dataModel["groupStrings"]):
                if i >= lenLastGroupStrings or groupString != lastGroupStrings[i]:
                    htmlLines.append( GROUP_HEADER_TEMPLATE % {"spacer": "   "*i, "groupString": groupString });
            htmlLines.append( LINE_TEMPLATE % dataModel );
            lastGroupStrings = dataModel["groupStrings"];
            lenLastGroupStrings = len(lastGroupStrings);
        self.requestData["detailTable"] = str.join("\n", htmlLines );

        # Load and format information on any pending result orders
        pendingResultOrders = manager.loadPendingResultOrders(patientId, simTime);
        htmlLines = list();
        for dataModel in pendingResultOrders:
            self.formatPendingResultOrderModel(dataModel);
            htmlLines.append( PENDING_ORDER_LINE_TEMPLATE % dataModel );
        self.requestData["pendingResultOrdersTable"] = str.join("\n", htmlLines );
        if len(pendingResultOrders) < 1: # Leave default "no results pending" message if don't find any
            self.requestData["pendingResultOrdersTable"] = EMPTY_RESULTS_LINE;
Exemplo n.º 2
0
class TestSimManager(DBTestCase):
    def setUp(self):
        """Prepare state for test cases"""
        DBTestCase.setUp(self)

        self.manager = SimManager()
        # Instance to test on

        from stride.clinical_item.ClinicalItemDataLoader import ClinicalItemDataLoader
        ClinicalItemDataLoader.build_clinical_item_psql_schemata()
        self.manager.buildCPOESimSchema()

        self.testPatientId = None

        self.purgeTestRecords()

        log.info("Populate the database with test data")

        self.clinicalItemCategoryIdStrList = list()
        headers = ["clinical_item_category_id", "source_table"]
        dataModels = \
            [
                RowItemModel( [-1, "Labs"], headers ),
                RowItemModel( [-2, "Imaging"], headers ),
                RowItemModel( [-3, "Meds"], headers ),
                RowItemModel( [-4, "Nursing"], headers ),
                RowItemModel( [-5, "Problems"], headers ),
                RowItemModel( [-6, "Lab Results"], headers ),
            ]
        for dataModel in dataModels:
            (dataItemId,
             isNew) = DBUtil.findOrInsertItem("clinical_item_category",
                                              dataModel)
            self.clinicalItemCategoryIdStrList.append(str(dataItemId))

        headers = [
            "clinical_item_id", "clinical_item_category_id", "name",
            "analysis_status"
        ]
        dataModels = \
            [
                RowItemModel( [-1, -1, "CBC",1], headers ),
                RowItemModel( [-2, -1, "BMP",1], headers ),
                RowItemModel( [-3, -1, "Hepatic Panel",1], headers ),
                RowItemModel( [-4, -1, "Cardiac Enzymes",1], headers ),
                RowItemModel( [-5, -2, "CXR",1], headers ),
                RowItemModel( [-6, -2, "RUQ Ultrasound",1], headers ),
                RowItemModel( [-7, -2, "CT Abdomen/Pelvis",1], headers ),
                RowItemModel( [-8, -2, "CT PE Thorax",1], headers ),
                RowItemModel( [-9, -3, "Acetaminophen",1], headers ),
                RowItemModel( [-10, -3, "Carvedilol",1], headers ),
                RowItemModel( [-11, -3, "Enoxaparin",1], headers ),
                RowItemModel( [-12, -3, "Warfarin",1], headers ),
                RowItemModel( [-13, -3, "Ceftriaxone",1], headers ),
                RowItemModel( [-14, -4, "Foley Catheter",1], headers ),
                RowItemModel( [-15, -4, "Vital Signs",1], headers ),
                RowItemModel( [-16, -4, "Fall Precautions",1], headers ),
            ]
        for dataModel in dataModels:
            (dataItemId,
             isNew) = DBUtil.findOrInsertItem("clinical_item", dataModel)


        dataTextStr = \
"""sim_user_id;name
-1;Test User
"""     # Parse into DB insertion object
        DBUtil.insertFile(StringIO(dataTextStr), "sim_user", delim=";")

        dataTextStr = \
"""sim_patient_id;age_years;gender;name
-1;60;Female;Test Female Patient
-2;55;Male;Test Male Patient
"""     # Parse into DB insertion object
        DBUtil.insertFile(StringIO(dataTextStr), "sim_patient", delim=";")

        dataTextStr = \
"""sim_result_id;name;description;group_string;priority
-10;Temp;Temperature (F);Flowsheet>Vitals;10
-20;Pulse;Pulse / Heart Rate (HR);Flowsheet>Vitals;20
-30;SBP;Blood Pressure, Systolic (SBP);Flowsheet>Vitals;30
-40;DBP;Blood Pressure, Diastolic (DBP);Flowsheet>Vitals;40
-50;Resp;Respirations (RR);Flowsheet>Vitals;50
-60;FiO2;Fraction Inspired Oxygen;Flowsheet>Vitals;60
-70;Urine;Urine Output (UOP);Flowsheet>Vitals;70
-11000;WBC;WBC;LAB BLOOD ORDERABLES>Hematology>Automated Blood Count;11000
-11010;HGB;HEMOGLOBIN;LAB BLOOD ORDERABLES>Hematology>Automated Blood Count;11010
-11020;HCT;HEMATOCRIT;LAB BLOOD ORDERABLES>Hematology>Automated Blood Count;11020
-11030;PLT;PLATELET COUNT;LAB BLOOD ORDERABLES>Hematology>Automated Blood Count;11030
-13010;NA;SODIUM, SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13010
-13020;K;POTASSIUM, SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13020
-13030;CL;CHLORIDE, SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13030
-13040;CO2;CO2, SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13040
-13050;BUN;UREA NITROGEN,SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13050
-13060;CR;CREATININE, SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13060
-13070;GLU;GLUCOSE, SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13070
-13090;CA;CALCIUM, SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13090
-13110;MG;MAGNESIUM, SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13110
-13120;PHOS;PHOSPHORUS, SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13120
-13210;TBIL;TOTAL BILIRUBIN;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13210
-13220;DBIL;CONJUGATED BILI;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13220
-13230;IBIL;UNCONJUGATED BILIRUBIN;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13230
-13240;AST;AST (SGOT), SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13240
-13250;ALT;ALT (SGPT), SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13250
-13260;ALKP;ALK P'TASE, TOTAL, SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13260
-13270;ALB;ALBUMIN, SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13270
-13280;TP;PROTEIN, TOTAL, SER/PLAS;LAB BLOOD ORDERABLES>Chemistry>General Chemistry;13280
"""     # Parse into DB insertion object
        DBUtil.insertFile(StringIO(dataTextStr), "sim_result", delim=";")

        # Map orders to expected results.
        # Simplify expect vital signs to result in 5 minutes. Basic chemistry labs in 10 minutes, CBC in 15 minutes
        dataTextStr = \
"""sim_order_result_map_id;clinical_item_id;sim_result_id;turnaround_time
-1;-15;-10;300
-2;-15;-20;300
-3;-15;-30;300
-4;-15;-40;300
-5;-15;-50;300
-6;-1;-11000;900
-7;-1;-11010;900
-8;-1;-11020;900
-9;-1;-11030;900
-10;-2;-13010;600
-11;-2;-13020;600
-12;-2;-13030;600
-13;-2;-13040;600
-14;-2;-13050;600
-15;-2;-13060;600
-16;-2;-13070;600
-17;-2;-13090;600
-18;-3;-13210;600
-19;-3;-13240;600
-20;-3;-13250;600
-21;-3;-13260;600
-22;-3;-13270;600
-23;-3;-13280;600
"""     # Parse into DB insertion object
        DBUtil.insertFile(StringIO(dataTextStr),
                          "sim_order_result_map",
                          delim=";")


        dataTextStr = \
"""sim_state_id;name;description
0;Test 0; Test State 0
-1;Test 1;Test State 1
-2;Test 2;Test State 2
-3;Test 3;Test State 3
-4;Test 4;Test State 4
"""     # Parse into DB insertion object
        DBUtil.insertFile(StringIO(dataTextStr), "sim_state", delim=";")

        dataTextStr = \
"""sim_state_transition_id;pre_state_id;post_state_id;clinical_item_id;time_trigger;description
-1;-1;-2;None;9000;Passive time from 1 to 2
-2;-2;-3;-11;None;Transition 2 to 3 if order for 11 (Enoxaparin)
-3;-2;-3;-12;None;Transition 2 to 3 if order for 12 (Warfarin) (don't need both anti-coagulants. One adequate to trigger transition)
-4;-2;-4;-13;None;Transition 2 to 4 if order for 13 (Ceftriaxone)
-5;-3;-1;-10;9000;Transition 3 back to 1 if order for 10 (Carvedilol) OR 9000 seconds pass
"""     # Parse into DB insertion object
        DBUtil.insertFile(StringIO(dataTextStr),
                          "sim_state_transition",
                          delim=";")

        dataTextStr = \
"""sim_patient_state_id;sim_patient_id;sim_state_id;relative_time_start;relative_time_end
-1;-1;-1;-7200;0
-3;-1;-1;0;1800
-2;-1;-2;1800;None
"""     # Parse into DB insertion object
        DBUtil.insertFile(StringIO(dataTextStr),
                          "sim_patient_state",
                          delim=";")

        # Order Vital Signs at time 0, then basic labs (CBC, BMP, LFTs) at 10 minutes (600 seconds)
        dataTextStr = \
"""sim_patient_order_id;sim_user_id;sim_patient_id;sim_state_id;clinical_item_id;relative_time_start;relative_time_end
-1;-1;-1;-1;-15;0;None
-2;-1;-1;-1;-1;600;None
-3;-1;-1;-1;-2;600;None
-4;-1;-1;-1;-3;600;None
-5;-1;-1;-2;-15;1800;None
-6;-1;-1;-2;-1;1800;None
-7;-1;-1;-2;-2;1800;None
"""     # Parse into DB insertion object
        DBUtil.insertFile(StringIO(dataTextStr),
                          "sim_patient_order",
                          delim=";")

        dataTextStr = \
"""sim_state_result_id;sim_result_id;sim_state_id;num_value;num_value_noise;text_value;result_flag
-1;-10;0;98.7;0.2;;
-2;-20;0;75;3;;
-3;-30;0;130;4;;
-4;-40;0;85;2;;
-5;-50;0;12;1;;
-6;-60;0;0.21;0;;
-7;-70;0;500;100;;
-8;-11000;0;7;1;;
-9;-11010;0;13;0.5;;
-10;-11020;0;40;1;;
-11;-11030;0;300;25;;
-12;-13010;0;140;4;;
-13;-13020;0;4.5;0.4;;
-14;-13030;0;95;3;;
-15;-13040;0;24;1;;
-16;-13050;0;12;3;;
-17;-13060;0;0.7;0.2;;
-18;-13070;0;140;12;;
-19;-13090;0;9;0.4;;
-20;-13110;0;2;0.3;;
-21;-13120;0;3;0.5;;
-22;-13210;0;0.2;0.1;;
-23;-13240;0;29;5;;
-24;-13250;0;20;4;;
-25;-13260;0;85;8;;
-26;-13270;0;4;0.4;;
-27;-13280;0;6;0.5;;
-28;-10;-1;101.4;0.4;Fever;H
-29;-20;-1;115;4;Tachycardia;H
-30;-30;-1;92;5;Hypotension;L
-31;-40;-1;55;3;Hypotension;L
-32;-70;-1;50;10;Low UOP;L
-33;-11000;-1;12;1;Leukocytosis;H
-34;-13060;-1;2.4;0.3;AKI;H
-35;-20;-2;105;4;Tachycardia;H
-36;-11000;-2;10;1;;
-37;-13060;-2;1.9;0.3;AKI;H
-38;-13070;-2;250;13;;H
"""     # Parse into DB insertion object
        DBUtil.insertFile(StringIO(dataTextStr), "sim_state_result", delim=";")

        dataTextStr = \
"""sim_note_id;sim_state_id;relative_state_time;content
-1;-1;7200;Initial Note
-2;-2;0;Later Note
"""     # Parse into DB insertion object
        DBUtil.insertFile(StringIO(dataTextStr), "sim_note", delim=";")

    def purgeTestRecords(self):
        log.info("Purge test records from the database")
        if self.testPatientId is not None:
            # Delete test generated data
            DBUtil.execute(
                "delete from sim_patient_order where sim_patient_id = %s",
                (self.testPatientId, ))
            DBUtil.execute(
                "delete from sim_patient_state where sim_patient_id = %s",
                (self.testPatientId, ))
            DBUtil.execute("delete from sim_patient where sim_patient_id = %s",
                           (self.testPatientId, ))

        DBUtil.execute("delete from sim_note where sim_note_id < 0")
        DBUtil.execute(
            "delete from sim_state_result where sim_state_result_id < 0")
        DBUtil.execute(
            "delete from sim_patient_order where sim_state_id < 0 or sim_patient_order_id < 0"
        )
        DBUtil.execute(
            "delete from sim_order_result_map where sim_order_result_map_id < 0"
        )
        DBUtil.execute("delete from sim_result where sim_result_id < 0")
        DBUtil.execute(
            "delete from sim_patient_state where sim_state_id < 0 or sim_patient_state_id < 0"
        )
        DBUtil.execute(
            "delete from sim_state_transition where pre_state_id < 0")
        DBUtil.execute("delete from sim_state where sim_state_id <= 0")
        DBUtil.execute("delete from sim_user where sim_user_id < 0")
        DBUtil.execute("delete from sim_patient where sim_patient_id < 0")
        DBUtil.execute("delete from clinical_item where clinical_item_id < 0")

    def tearDown(self):
        """Restore state from any setUp or test steps"""
        self.purgeTestRecords()
        DBTestCase.tearDown(self)

    def test_copyPatientTemplate(self):
        # Copy a patient template, including deep copy of notes, orders, states, but only up to relative time zero
        newPatientData = {
            "name": "Template Copy"
        }
        templatePatientId = -1
        self.testPatientId = self.manager.copyPatientTemplate(
            newPatientData, templatePatientId)
        futureTime = 1000000
        # Far future time to test that we still only copied the results up to time zero

        # Verify basic patient information
        patientCols = ["name", "age_years", "gender", "sim_state_id"]
        patientModel = self.manager.loadPatientInfo([self.testPatientId])[0]
        expectedPatientModel = RowItemModel(
            ["Template Copy", 60, "Female", -1], patientCols)
        self.assertEqualDict(expectedPatientModel, patientModel, patientCols)

        # Verify notes
        dataCols = ["sim_patient_id", "content"]
        sampleData = self.manager.loadNotes(self.testPatientId, futureTime)
        verifyData = \
            [   RowItemModel([self.testPatientId,"Initial Note"], dataCols),
                RowItemModel([self.testPatientId,"Initial Note"], dataCols),    # Second copy because another state initiation at time zero and negative onset time
            ]
        self.assertEqualDictList(verifyData, sampleData, dataCols)

        # Verify orders
        dataCols = [
            "sim_user_id", "sim_patient_id", "sim_state_id",
            "clinical_item_id", "relative_time_start", "relative_time_end"
        ]
        sampleData = self.manager.loadPatientOrders(self.testPatientId,
                                                    futureTime,
                                                    loadActive=None)
        verifyData = \
            [   RowItemModel([-1,self.testPatientId,-1,-15,0,None], dataCols),
            ]
        self.assertEqualDictList(verifyData, sampleData, dataCols)

        # Verify states
        dataCols = [
            "sim_patient_id", "sim_state_id", "relative_time_start",
            "relative_time_end"
        ]
        query = SQLQuery()
        for dataCol in dataCols:
            query.addSelect(dataCol)
        query.addFrom("sim_patient_state")
        query.addWhereEqual("sim_patient_id", self.testPatientId)
        query.addOrderBy("relative_time_start")
        sampleDataTable = DBUtil.execute(query, includeColumnNames=True)
        sampleData = modelListFromTable(sampleDataTable)
        verifyData = \
            [   RowItemModel([self.testPatientId,-1,-7200,0], dataCols),
                RowItemModel([self.testPatientId,-1,0,None], dataCols),
            ]
        self.assertEqualDictList(verifyData, sampleData, dataCols)

    def test_loadResults(self):
        # Query for results based on simulated turnaround times, including fallback to default normal values
        #   if no explicit (abnormal) values specified for simulated state

        colNames = ["name", "num_value", "result_relative_time"]

        # Time zero, no orders for diagnostics, so no results should exist
        patientId = -1
        relativeTime = 0
        sampleResults = self.manager.loadResults(patientId, relativeTime)
        verifyResults = \
            [
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Time 2 minutes, orders done, but not long-enough to get results back, so no results should exist
        relativeTime = 120
        sampleResults = self.manager.loadResults(patientId, relativeTime)
        verifyResults = \
            [
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Time 5 minutes, vital signs should result now
        relativeTime = 300
        sampleResults = self.manager.loadResults(patientId, relativeTime)
        verifyResults = \
            [
                RowItemModel(["Temp", 101.4, 300], colNames),
                RowItemModel(["Pulse", 115, 300], colNames),
                RowItemModel(["SBP", 92, 300], colNames),
                RowItemModel(["DBP", 55, 300], colNames),
                RowItemModel(["Resp", 12, 300], colNames),  # Normal result retrieve from default state 0
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Time 10 minutes, labs ordered, but not results expected yet. Prior vital signs should still be available.
        relativeTime = 600
        sampleResults = self.manager.loadResults(patientId, relativeTime)
        verifyResults = \
            [
                RowItemModel(["Temp", 101.4, 300], colNames),
                RowItemModel(["Pulse", 115, 300], colNames),
                RowItemModel(["SBP", 92, 300], colNames),
                RowItemModel(["DBP", 55, 300], colNames),
                RowItemModel(["Resp", 12, 300], colNames),  # Normal result retrieve from default state 0
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Time 20 minutes, labs ordered 10 minutes ago, should have chemistry results
        relativeTime = 1200
        sampleResults = self.manager.loadResults(patientId, relativeTime)
        verifyResults = \
            [
                RowItemModel(["Temp", 101.4, 300], colNames),
                RowItemModel(["Pulse", 115, 300], colNames),
                RowItemModel(["SBP", 92, 300], colNames),
                RowItemModel(["DBP", 55, 300], colNames),
                RowItemModel(["Resp", 12, 300], colNames),  # Normal result retrieve from default state 0

                RowItemModel(["NA", 140, 1200], colNames),
                RowItemModel(["K", 4.5, 1200], colNames),
                RowItemModel(["CL", 95, 1200], colNames),
                RowItemModel(["CO2", 24, 1200], colNames),
                RowItemModel(["BUN", 12, 1200], colNames),
                RowItemModel(["CR", 2.4, 1200], colNames),  # Abnormal value in state 1, all others default state 0
                RowItemModel(["GLU", 140, 1200], colNames),
                RowItemModel(["CA", 9, 1200], colNames),
                RowItemModel(["TBIL", 0.2, 1200], colNames),
                RowItemModel(["AST", 29, 1200], colNames),
                RowItemModel(["ALT", 20, 1200], colNames),
                RowItemModel(["ALKP", 85, 1200], colNames),
                RowItemModel(["ALB", 4, 1200], colNames),
                RowItemModel(["TP", 6, 1200], colNames),
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Time 25 minutes, CBC results should now be available as well
        relativeTime = 1500
        sampleResults = self.manager.loadResults(patientId, relativeTime)
        verifyResults = \
            [
                RowItemModel(["Temp", 101.4, 300], colNames),
                RowItemModel(["Pulse", 115, 300], colNames),
                RowItemModel(["SBP", 92, 300], colNames),
                RowItemModel(["DBP", 55, 300], colNames),
                RowItemModel(["Resp", 12, 300], colNames),  # Normal result retrieve from default state 0

                RowItemModel(["NA", 140, 1200], colNames),
                RowItemModel(["K", 4.5, 1200], colNames),
                RowItemModel(["CL", 95, 1200], colNames),
                RowItemModel(["CO2", 24, 1200], colNames),
                RowItemModel(["BUN", 12, 1200], colNames),
                RowItemModel(["CR", 2.4, 1200], colNames),  # Abnormal value in state 1, all others default state 0
                RowItemModel(["GLU", 140, 1200], colNames),
                RowItemModel(["CA", 9, 1200], colNames),
                RowItemModel(["TBIL", 0.2, 1200], colNames),
                RowItemModel(["AST", 29, 1200], colNames),
                RowItemModel(["ALT", 20, 1200], colNames),
                RowItemModel(["ALKP", 85, 1200], colNames),
                RowItemModel(["ALB", 4, 1200], colNames),
                RowItemModel(["TP", 6, 1200], colNames),

                RowItemModel(["WBC", 12, 1500], colNames),  # Abnormal state value
                RowItemModel(["HGB", 13, 1500], colNames),
                RowItemModel(["HCT", 40, 1500], colNames),
                RowItemModel(["PLT", 300, 1500], colNames),

            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Time 30 minutes, patient entered new state (-2), and repeat vitals + labs ordered, but no new results yet due to turnaround time
        relativeTime = 1800
        sampleResults = self.manager.loadResults(patientId, relativeTime)
        verifyResults = \
            [
                RowItemModel(["Temp", 101.4, 300], colNames),
                RowItemModel(["Pulse", 115, 300], colNames),
                RowItemModel(["SBP", 92, 300], colNames),
                RowItemModel(["DBP", 55, 300], colNames),
                RowItemModel(["Resp", 12, 300], colNames),  # Normal result retrieve from default state 0

                RowItemModel(["NA", 140, 1200], colNames),
                RowItemModel(["K", 4.5, 1200], colNames),
                RowItemModel(["CL", 95, 1200], colNames),
                RowItemModel(["CO2", 24, 1200], colNames),
                RowItemModel(["BUN", 12, 1200], colNames),
                RowItemModel(["CR", 2.4, 1200], colNames),  # Abnormal value in state 1, all others default state 0
                RowItemModel(["GLU", 140, 1200], colNames),
                RowItemModel(["CA", 9, 1200], colNames),
                RowItemModel(["TBIL", 0.2, 1200], colNames),
                RowItemModel(["AST", 29, 1200], colNames),
                RowItemModel(["ALT", 20, 1200], colNames),
                RowItemModel(["ALKP", 85, 1200], colNames),
                RowItemModel(["ALB", 4, 1200], colNames),
                RowItemModel(["TP", 6, 1200], colNames),

                RowItemModel(["WBC", 12, 1500], colNames),  # Abnormal state value
                RowItemModel(["HGB", 13, 1500], colNames),
                RowItemModel(["HCT", 40, 1500], colNames),
                RowItemModel(["PLT", 300, 1500], colNames),

            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Time 45 minutes, patient second state and repeat labs, should retrieve both new and old results (with relative timestamps)
        relativeTime = 2700
        sampleResults = self.manager.loadResults(patientId, relativeTime)
        verifyResults = \
            [
                RowItemModel(["Temp", 101.4, 300], colNames),
                RowItemModel(["Pulse", 115, 300], colNames),
                RowItemModel(["SBP", 92, 300], colNames),
                RowItemModel(["DBP", 55, 300], colNames),
                RowItemModel(["Resp", 12, 300], colNames),  # Normal result retrieve from default state 0

                RowItemModel(["NA", 140, 1200], colNames),
                RowItemModel(["K", 4.5, 1200], colNames),
                RowItemModel(["CL", 95, 1200], colNames),
                RowItemModel(["CO2", 24, 1200], colNames),
                RowItemModel(["BUN", 12, 1200], colNames),
                RowItemModel(["CR", 2.4, 1200], colNames),  # Abnormal value in state 1, all others default state 0
                RowItemModel(["GLU", 140, 1200], colNames),
                RowItemModel(["CA", 9, 1200], colNames),
                RowItemModel(["TBIL", 0.2, 1200], colNames),
                RowItemModel(["AST", 29, 1200], colNames),
                RowItemModel(["ALT", 20, 1200], colNames),
                RowItemModel(["ALKP", 85, 1200], colNames),
                RowItemModel(["ALB", 4, 1200], colNames),
                RowItemModel(["TP", 6, 1200], colNames),

                RowItemModel(["WBC", 12, 1500], colNames),  # Abnormal state value
                RowItemModel(["HGB", 13, 1500], colNames),
                RowItemModel(["HCT", 40, 1500], colNames),
                RowItemModel(["PLT", 300, 1500], colNames),

                # State 2 Results
                RowItemModel(["Temp", 98.7, 2100], colNames),
                RowItemModel(["Pulse", 105, 2100], colNames),   # Still abnormal, other vitals revert to default
                RowItemModel(["SBP", 130, 2100], colNames),
                RowItemModel(["DBP", 85, 2100], colNames),
                RowItemModel(["Resp", 12, 2100], colNames),

                RowItemModel(["NA", 140, 2400], colNames),
                RowItemModel(["K", 4.5, 2400], colNames),
                RowItemModel(["CL", 95, 2400], colNames),
                RowItemModel(["CO2", 24, 2400], colNames),
                RowItemModel(["BUN", 12, 2400], colNames),
                RowItemModel(["CR", 1.9, 2400], colNames),  # Abnormal value in state 1, all others default state 0
                RowItemModel(["GLU", 250, 2400], colNames), # Newly abnormal value in state 2
                RowItemModel(["CA", 9, 2400], colNames),

                RowItemModel(["WBC", 10, 2700], colNames),  # Abnormal state value, but not as bad as prior state
                RowItemModel(["HGB", 13, 2700], colNames),
                RowItemModel(["HCT", 40, 2700], colNames),
                RowItemModel(["PLT", 300, 2700], colNames),
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

    def test_discontinueOrders(self):
        # Verify behavior of discontinuing/cancelling orders
        #   Should not generate subsequent results or count as "recent items" that should
        #   be considered to trigger recommender. If cancelled at same time as order time,
        #   then delete the record altogether.

        colNames = ["name", "num_value", "result_relative_time"]

        # See setUp for test data construction
        userId = -1
        patientId = -1

        # Time zero, vital sign orders check entered (clinical_item_id = -15, sim_patient_order_id = -1)
        # Time 2 minutes, orders done, but not long-enough to get results back, so no results should exist
        relativeTime = 120
        sampleResults = self.manager.loadResults(patientId, relativeTime)
        verifyResults = \
            [
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Record of vital sign order (but not yet resulted)
        sampleRecentItemIds = self.manager.recentItemIds(
            patientId, relativeTime)
        verifyRecentItemIds = set([-15])
        self.assertEqual(verifyRecentItemIds, sampleRecentItemIds)

        # Time 5 minutes, vital signs should result now
        relativeTime = 300
        sampleResults = self.manager.loadResults(patientId, relativeTime)
        verifyResults = \
            [
                RowItemModel(["Temp", 101.4, 300], colNames),
                RowItemModel(["Pulse", 115, 300], colNames),
                RowItemModel(["SBP", 92, 300], colNames),
                RowItemModel(["DBP", 55, 300], colNames),
                RowItemModel(["Resp", 12, 300], colNames),  # Normal result retrieve from default state 0
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Restrict recent item search to limited time that should miss the vital sign order
        sampleRecentItemIds = self.manager.recentItemIds(
            patientId, relativeTime, 100)
        verifyRecentItemIds = set([])
        self.assertEqual(verifyRecentItemIds, sampleRecentItemIds)

        # Include longer time difference that should catch the prior order
        sampleRecentItemIds = self.manager.recentItemIds(
            patientId, relativeTime, 400)
        verifyRecentItemIds = set([-15])
        self.assertEqual(verifyRecentItemIds, sampleRecentItemIds)

        # Go back and simulate the vitals check order being discontinued before results came back
        discontinueTime = 120
        newOrderItemIds = []
        # No new orders
        discontinuePatientOrderIds = [-1]
        # See setUp data for the ID of the order to simulate canceling
        self.manager.signOrders(userId, patientId, discontinueTime,
                                newOrderItemIds, discontinuePatientOrderIds)

        # Redo simulation of Time 5 minutes, vital signs should not appear now, since order was cancelled
        relativeTime = 300
        sampleResults = self.manager.loadResults(patientId, relativeTime)
        verifyResults = \
            [
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Check that there is still a record of orders, including the cancelled one (but not in "recent items" list)
        orderCols = ["name", "relative_time_start", "relative_time_end"]
        sampleOrders = self.manager.loadPatientOrders(patientId,
                                                      300,
                                                      loadActive=None)
        verifyOrders = \
            [
                RowItemModel(["Vital Signs", 0, 120], orderCols),
            ]
        self.assertEqualDictList(verifyOrders, sampleOrders, orderCols)

        sampleRecentItemIds = self.manager.recentItemIds(
            patientId, relativeTime)
        verifyRecentItemIds = set([])
        # Don't count the cancelled order
        self.assertEqual(verifyRecentItemIds, sampleRecentItemIds)

        # Go back and simulate the vitals check order being discontinued immediately (same time as order),
        #   then don't even keep a record of it to clean up data entry error
        discontinueTime = 0
        newOrderItemIds = []
        # No new orders
        discontinuePatientOrderIds = [-1]
        # See setUp data for the ID of the order to simulate canceling
        self.manager.signOrders(userId, patientId, discontinueTime,
                                newOrderItemIds, discontinuePatientOrderIds)

        # Check that there is no record of the order anymore, even including the cancelled one
        orderCols = ["name", "relative_time_start", "relative_time_end"]
        sampleOrders = self.manager.loadPatientOrders(patientId,
                                                      300,
                                                      loadActive=None)
        verifyOrders = \
            [
            ]
        self.assertEqualDictList(verifyOrders, sampleOrders, orderCols)

        sampleRecentItemIds = self.manager.recentItemIds(
            patientId, relativeTime)
        verifyRecentItemIds = set([])
        self.assertEqual(verifyRecentItemIds, sampleRecentItemIds)

    def test_loadPendingResultOrders(self):
        # Verify behavior of finding patient orders that will, but have not yet, yielded test results
        colNames = ["clinical_item_id", "name", "time_until_result"]

        # See setUp for test data construction
        userId = -1
        patientId = -1

        # Time zero, vital sign orders check entered (clinical_item_id = -15, sim_patient_order_id = -1)
        # Time 2 minutes, orders done, but not long-enough to get results back, so no results should exist, but can see pending result
        relativeTime = 120
        sampleResults = self.manager.loadPendingResultOrders(
            patientId, relativeTime)
        verifyResults = \
            [   RowItemModel([-15, "Vital Signs", 300-(120-0)], colNames),
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Time 5 minutes, vital signs should result now. So no pending results
        relativeTime = 300
        sampleResults = self.manager.loadPendingResultOrders(
            patientId, relativeTime)
        verifyResults = \
            [
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Go back and simulate the vitals check order being discontinued before results came back
        discontinueTime = 100
        newOrderItemIds = []
        # No new orders
        discontinuePatientOrderIds = [-1]
        # See setUp data for the ID of the order to simulate canceling
        self.manager.signOrders(userId, patientId, discontinueTime,
                                newOrderItemIds, discontinuePatientOrderIds)

        # Redo simulation shortly after. Should not show pending results since order was cancelled
        relativeTime = 120
        sampleResults = self.manager.loadPendingResultOrders(
            patientId, relativeTime)
        verifyResults = \
            [
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Time 10 minutes, should have multiple pending orders just entered
        relativeTime = 600
        sampleResults = self.manager.loadPendingResultOrders(
            patientId, relativeTime)
        verifyResults = \
            [   # Show results sorted by time until result, then alphabetical order
                RowItemModel([-2, "BMP", 600-(600-600)], colNames),
                RowItemModel([-3, "Hepatic Panel", 600-(600-600)], colNames),
                RowItemModel([-1, "CBC", 900-(600-600)], colNames),
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Time 20 minutes, some of the results should be back, while others still pending
        relativeTime = 1200
        sampleResults = self.manager.loadPendingResultOrders(
            patientId, relativeTime)
        verifyResults = \
            [
                RowItemModel([-1, "CBC", 900-(1200-600)], colNames),
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Go back and sign new orders of same type
        relativeTime = 900
        orderItemIds = [-1, -2]
        self.manager.signOrders(userId, patientId, relativeTime, orderItemIds)

        # Should now see some overlapping pending order times
        relativeTime = 1200
        sampleResults = self.manager.loadPendingResultOrders(
            patientId, relativeTime)
        verifyResults = \
            [   # If multiple results with same time until result, then show in order of first one ordered
                RowItemModel([-1, "CBC", 900-(1200-600)], colNames),
                RowItemModel([-2, "BMP", 600-(1200-900)], colNames),
                RowItemModel([-1, "CBC", 900-(1200-900)], colNames),
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

        # Later batch of more orders at 30 minutes
        relativeTime = 1800
        sampleResults = self.manager.loadPendingResultOrders(
            patientId, relativeTime)
        verifyResults = \
            [
                RowItemModel([-15, "Vital Signs", 300-(1800-1800)], colNames),
                RowItemModel([-2, "BMP", 600-(1800-1800)], colNames),
                RowItemModel([-1, "CBC", 900-(1800-1800)], colNames),
            ]
        self.assertEqualDictList(verifyResults, sampleResults, colNames)

    def test_stateTransition(self):
        # Query for results based on simulated turnaround times, including fallback to default normal values
        #   if no explicit (abnormal) values specified for simulated state

        colNames = ["sim_state_id"]

        userId = -1
        patientId = -1

        # Time zero, initial state expected
        relativeTime = 0
        samplePatient = self.manager.loadPatientInfo([patientId],
                                                     relativeTime)[0]
        verifyPatient = RowItemModel([-1], colNames)
        self.assertEqualDict(samplePatient, verifyPatient, colNames)

        # After previously recorded second state
        relativeTime = 2000
        samplePatient = self.manager.loadPatientInfo([patientId],
                                                     relativeTime)[0]
        verifyPatient = RowItemModel([-2], colNames)
        self.assertEqualDict(samplePatient, verifyPatient, colNames)

        # Sign orders that do not affect this state
        orderItemIds = [-4, -5]
        self.manager.signOrders(userId, patientId, relativeTime, orderItemIds)

        # Expect to stay in same state
        relativeTime = 2100
        samplePatient = self.manager.loadPatientInfo([patientId],
                                                     relativeTime)[0]
        verifyPatient = RowItemModel([-2], colNames)
        self.assertEqualDict(samplePatient, verifyPatient, colNames)

        # Sign orders that should trigger state transition
        relativeTime = 2100
        orderItemIds = [-11, -6]
        self.manager.signOrders(userId, patientId, relativeTime, orderItemIds)

        # Expect transition to new state
        relativeTime = 2100
        samplePatient = self.manager.loadPatientInfo([patientId],
                                                     relativeTime)[0]
        verifyPatient = RowItemModel([-3], colNames)
        self.assertEqualDict(samplePatient, verifyPatient, colNames)

        # Expect transition to original state 1 then further to state 2 with enough time passage
        relativeTime = 22000
        samplePatient = self.manager.loadPatientInfo([patientId],
                                                     relativeTime)[0]
        verifyPatient = RowItemModel([-2], colNames)
        self.assertEqualDict(samplePatient, verifyPatient, colNames)

        # Retroactive query to verify state 1 intermediate transition state
        relativeTime = 12000
        samplePatient = self.manager.loadPatientInfo([patientId],
                                                     relativeTime)[0]
        verifyPatient = RowItemModel([-1], colNames)
        self.assertEqualDict(samplePatient, verifyPatient, colNames)

        # Sign alternative order to trigger state 2 to 3 transition as well as order to immediately trigger state 3 to 1
        relativeTime = 22000
        orderItemIds = [-12, -10]
        self.manager.signOrders(userId, patientId, relativeTime, orderItemIds)

        # Expect transition to new state 3 them 1 immediately
        relativeTime = 22100
        samplePatient = self.manager.loadPatientInfo([patientId],
                                                     relativeTime)[0]
        verifyPatient = RowItemModel([-1], colNames)
        self.assertEqualDict(samplePatient, verifyPatient, colNames)

        # Expect transition back to state 2 with enough time
        relativeTime = 32200
        samplePatient = self.manager.loadPatientInfo([patientId],
                                                     relativeTime)[0]
        verifyPatient = RowItemModel([-2], colNames)
        self.assertEqualDict(samplePatient, verifyPatient, colNames)

        # Sign orders simultaneously that could trigger two different branching state transition paths
        #   Arbitrarily go based on the first action provided / encountered
        relativeTime = 32200
        orderItemIds = [-13, -11, -12]
        self.manager.signOrders(userId, patientId, relativeTime, orderItemIds)

        # Expect transition back to branched state 4
        relativeTime = 32300
        samplePatient = self.manager.loadPatientInfo([patientId],
                                                     relativeTime)[0]
        verifyPatient = RowItemModel([-4], colNames)
        self.assertEqualDict(samplePatient, verifyPatient, colNames)

    def test_loadPatientLastEventTime(self):
        # Query for last time have a record of a patient order start or cancellation
        #   as natural point to resume a simulated case

        # See setUp for test data construction
        userId = -1
        patientId = -1

        # Initial test data already loaded with example orders
        sampleValue = self.manager.loadPatientLastEventTime(patientId)
        verifyValue = 1800
        self.assertEqual(verifyValue, sampleValue)

        # Sign an additional order
        relativeTime = 2000
        newOrderItemIds = [-1]
        # Any additional order
        self.manager.signOrders(userId, patientId, relativeTime,
                                newOrderItemIds)

        # Verify update of last order time
        sampleValue = self.manager.loadPatientLastEventTime(patientId)
        verifyValue = 2000
        self.assertEqual(verifyValue, sampleValue)

        # Find and cancel the last order at a later time
        relativeTime = 2200
        patientOrders = self.manager.loadPatientOrders(patientId, relativeTime)
        lastOrder = patientOrders[0]
        for patientOrder in patientOrders:
            if lastOrder["relative_time_start"] < patientOrder[
                    "relative_time_start"]:
                lastOrder = patientOrder

        newOrderItemIds = []
        # No new orders
        discontinuePatientOrderIds = [lastOrder["sim_patient_order_id"]]
        self.manager.signOrders(userId, patientId, relativeTime,
                                newOrderItemIds, discontinuePatientOrderIds)

        # Verify update of last order time includes discontinue times
        sampleValue = self.manager.loadPatientLastEventTime(patientId)
        verifyValue = 2200
        self.assertEqual(verifyValue, sampleValue)

        # Lookup patient with no prior orders recorded, then should default to time 0
        patientId = -2
        sampleValue = self.manager.loadPatientLastEventTime(patientId)
        verifyValue = 0
        self.assertEqual(verifyValue, sampleValue)