diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index d4cff4c4..2c990523 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -102,7 +102,8 @@ class CO2FormData(FormData): """ Perform change point detection using scipy library (find_peaks method) with rolling average of data. Incorporate existing state change candidates and adjust the result accordingly. - Returns a list of the detected ventilation state changes. + Returns a list of the detected ventilation state changes, discarding any contribution + of occupancy state changes. """ times: list = self.CO2_data['times'] CO2_values: list = self.CO2_data['CO2'] @@ -178,6 +179,12 @@ class CO2FormData(FormData): return sorted(state_change_times) def ventilation_transition_times(self) -> typing.List[float]: + ''' + Check if the last time from the input data is + included in the ventilation ventilations state. + Given that the last time is a required state change, + if not included, this method adds it. + ''' vent_states = self.fitting_ventilation_states last_time_from_input = self.CO2_data['times'][-1] if (vent_states and last_time_from_input != vent_states[-1]): # The last time value is always needed for the last ACH interval. diff --git a/caimira/tests/models/test_co2_concentration_model.py b/caimira/tests/models/test_co2_concentration_model.py index 09392b14..60aefeee 100644 --- a/caimira/tests/models/test_co2_concentration_model.py +++ b/caimira/tests/models/test_co2_concentration_model.py @@ -47,40 +47,62 @@ def test_integrated_concentration(simple_co2_conc_model): @pytest.mark.parametrize( - "scenario_data, room_volume, total_people, start, finish, state_changes", [ - ["office_scenario_1_sensor_data", 102, 4, "14:00", "17:30", (14.78, 15.10, 15.53, 15.87, 16.52, 16.83)], - ["office_scenario_2_sensor_data", 60, 2, "08:38", "17:30", (10.17, 12.45, 14.50)], # Second should be 12.87 + "scenario_data, room_volume, max_total_people, start, finish, state_changes", [ + ["office_scenario_1_sensor_data", 102, 4, "14:00", "17:30", (14.78, 15.1, 15.53, 15.87, 16.52, 16.83)], + ["office_scenario_2_sensor_data", 60, 2, "08:38", "17:30", (10.17, 12.45, 14.5)], # Second should be 12.87 ["meeting_scenario_1_sensor_data", 83, 3, "09:04", "11:45", (10.37, 11.07)], - ["meeting_scenario_2_sensor_data", 83, 4, "13:40", "16:40", (14.37, 14.70, 14.98, 15.33, 15.68, 16.02)] + ["meeting_scenario_2_sensor_data", 83, 4, "13:40", "16:40", (14.37, 14.72, 15, 15.33, 15.68, 16.03)] ] ) -def test_find_change_points(scenario_data, room_volume, total_people, start, finish, state_changes, request): +def test_find_change_points(scenario_data, room_volume, max_total_people, start, finish, state_changes, request): + ''' + Specific test of the find_change_points method using the + scipy find_peaks and specific smoothing techniques. Only + the ventilation state changes are target for detection. + ''' CO2_form_model: CO2FormData = CO2FormData( CO2_data=request.getfixturevalue(scenario_data), fitting_ventilation_states=[], exposed_start=start, exposed_finish=finish, - total_people=total_people, + total_people=max_total_people, room_volume=room_volume, ) find_points = CO2_form_model.find_change_points() assert np.allclose(find_points, state_changes, rtol=1e-2) -def test_predictive_model_accuracy(data_registry, office_scenario_2_sensor_data): +@pytest.mark.parametrize( + "scenario_data, room_volume, occupancy, presence_interval, all_state_changes", [ + ["office_scenario_1_sensor_data", 102, (4,), (14, 17.5), (14, 14.25, 14.78, 15.1, 15.53, 15.87, 16.52, 16.83, 17.5)], + ["office_scenario_2_sensor_data", 60, (2, 0, 2), (8.62, 11.93, 12.42, 17.5), (8.62, 10.17, 12.45, 14.5, 17.5, 20.)], + ["meeting_scenario_1_sensor_data", 83, (2, 3, 2, 3), (9.07, 9.32, 9.75, 10.75, 11.75), (9.07, 10.37, 11.07, 11.75)], + ["meeting_scenario_2_sensor_data", 83, (2, 3, 4), (13.67, 13.75, 15.87, 16.67), (13.67, 14.37, 14.72, 15.00, 15.33, 15.68, 16.03, 16.67)] + ] +) +def test_predictive_model_accuracy(data_registry, scenario_data, room_volume, occupancy, presence_interval, all_state_changes, request): ''' - Specific test corresponding to the template data from a simulation day - in one office in Geneva. The room volume, number of people and ventilation - transition times correspond to the real occurencies in the simulation day. + Specific test corresponding to specific data files of four + different occurencies (2 office and 2 meeting room scenarios). + The room volume, number of people and ventilation transition times + correspond to the real occurencies in the simulation days. + + Note that the last time from the input file is considered as a ventilation + state change. ''' + input_fitting_data = request.getfixturevalue(scenario_data) + fitting_model: models.CO2DataModel = models.CO2DataModel( data_registry=data_registry, - room_volume=59.787, - number=2, - presence=models.SpecificInterval(((8.63, 11.95), (12.42, 17.5))), - ventilation_transition_times=(8.63, 10.17, 12.89, 14.5, 17.5, 22.), - times=office_scenario_2_sensor_data['times'], - CO2_concentrations=office_scenario_2_sensor_data['CO2'], + room_volume=room_volume, + number=models.IntPiecewiseConstant( + transition_times=presence_interval, + values=occupancy + ), + presence=None, + ventilation_transition_times=all_state_changes, + times=input_fitting_data['times'], + CO2_concentrations=input_fitting_data['CO2'], ) # Get fitting results fitting_results: typing.Dict = fitting_model.CO2_fit_params() @@ -90,7 +112,8 @@ def test_predictive_model_accuracy(data_registry, office_scenario_2_sensor_data) return np.sqrt(np.mean(((actual - predicted) / actual) ** 2)) * 100 # Calculate RMSEP metric - rmsep = root_mean_square_error_percentage(np.array(office_scenario_2_sensor_data['CO2']), np.array(predictive_CO2)) + rmsep = root_mean_square_error_percentage(np.array(input_fitting_data['CO2']), np.array(predictive_CO2)) + acceptable_rmsep = 10 # Threshold of 10% for the accepted error margin assert rmsep <= acceptable_rmsep, f"RMSEP {rmsep} exceeds acceptable threshold {acceptable_rmsep}" \ No newline at end of file