From 3285643266becfc7814bd0766961811a96b62f3c Mon Sep 17 00:00:00 2001 From: lrdossan Date: Thu, 27 Jun 2024 15:55:01 +0100 Subject: [PATCH 01/17] adapted pelt algorithm suggested times --- caimira/apps/calculator/__init__.py | 29 ++++++++------ .../apps/calculator/co2_model_generator.py | 39 +++++++++++++------ caimira/models.py | 7 +++- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index 5a746648..46de85e9 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -404,7 +404,10 @@ class CO2ModelResponse(BaseRequestHandler): requested_model_config = tornado.escape.json_decode(self.request.body) try: - form = co2_model_generator.CO2FormData.from_dict(requested_model_config, data_registry) + form: co2_model_generator.CO2FormData = co2_model_generator.CO2FormData.from_dict( + requested_model_config, + data_registry + ) except Exception as err: if self.settings.get("debug", False): import traceback @@ -415,26 +418,28 @@ class CO2ModelResponse(BaseRequestHandler): return if endpoint.rstrip('/') == 'plot': - transition_times = co2_model_generator.CO2FormData.find_change_points_with_pelt(form.CO2_data) - self.finish({'CO2_plot': co2_model_generator.CO2FormData.generate_ventilation_plot(form.CO2_data, transition_times), - 'transition_times': [round(el, 2) for el in transition_times]}) + transition_times: list = form.find_change_points_with_pelt() + self.finish({ + 'CO2_plot': form.generate_ventilation_plot(transition_times), + 'transition_times': [round(el, 2) for el in transition_times] + }) else: executor = loky.get_reusable_executor( max_workers=self.settings['handler_worker_pool_size'], timeout=300, ) - report_task = executor.submit( - co2_model_generator.CO2FormData.build_model, form, - ) + report_task = executor.submit(form.build_model) report = await asyncio.wrap_future(report_task) - - result = dict(report.CO2_fit_params()) + # Ventilation times after user manipulation from the suggested Pelt algorithm times. ventilation_transition_times = report.ventilation_transition_times - result['fitting_ventilation_type'] = form.fitting_ventilation_type + # The result of the following method is a dict with the results of the fitting + # algorithm, namely the breathing rate and ACH values. It also returns the + # predictive CO2 result based on the fitting results. + result: typing.Dict = dict(report.CO2_fit_params()) + # Add the transition times and CO2 plot to the results. result['transition_times'] = ventilation_transition_times - result['CO2_plot'] = co2_model_generator.CO2FormData.generate_ventilation_plot(CO2_data=form.CO2_data, - transition_times=ventilation_transition_times[:-1], + result['CO2_plot'] = form.generate_ventilation_plot(transition_times=ventilation_transition_times[:-1], predictive_CO2=result['predictive_CO2']) self.finish(result) diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index e8a054ca..440a2b1c 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -99,15 +99,14 @@ class CO2FormData(FormData): if not re.compile("^(2[0-3]|[01]?[0-9]):([0-5]?[0-9])$").match(time): raise TypeError(f'Wrong time format - "HH:MM". Got "{time}".') - @classmethod - def find_change_points_with_pelt(self, CO2_data: dict): + def find_change_points_with_pelt(self) -> list: """ Perform change point detection using Pelt algorithm from ruptures library with pen=15. + Incorporate existing state change candidates and adjust the result accordingly. Returns a list of tuples containing (index, X-axis value) for the detected significant changes. """ - - times: list = CO2_data['times'] - CO2_values: list = CO2_data['CO2'] + times: list = self.CO2_data['times'] + CO2_values: list = self.CO2_data['CO2'] if len(times) != len(CO2_values): raise ValueError("times and CO2 values must have the same length.") @@ -131,14 +130,30 @@ class CO2FormData(FormData): for segment in merged_segments[:-2]: result_set.add(times[CO2_values.index(min(CO2_np[segment]))]) result_set.add(times[CO2_values.index(max(CO2_np[segment]))]) - return list(result_set) - @classmethod - def generate_ventilation_plot(self, CO2_data: dict, + # Calculate presence intervals and respective merge + infected_presence = self.infected_present_interval() + exposed_presence = self.exposed_present_interval() + all_state_change_times = self.population_present_changes(infected_presence, exposed_presence) + # Check proximity to existing state changes and update result set if necessary. + # If suggested point is close enough to a simulation time, replace the result with the + # simulation time. Otherwise, add the exact suggested point. + for change_point in all_state_change_times: + closest_point = min(result_set, key=lambda x: abs(x - change_point)) + if abs(closest_point - change_point) <= 1: # Threshold for close points + result_set.remove(closest_point) + result_set.add(change_point) + else: + result_set.add(change_point) + return sorted(list(result_set)) + + def generate_ventilation_plot(self, transition_times: typing.Optional[list] = None, - predictive_CO2: typing.Optional[list] = None): - times_values = CO2_data['times'] - CO2_values = CO2_data['CO2'] + predictive_CO2: typing.Optional[list] = None) -> str: + + # Plot data (x-axis: times; y-axis: CO2 concentrations) + times_values: list = self.CO2_data['times'] + CO2_values: list = self.CO2_data['CO2'] fig = plt.figure(figsize=(7, 4), dpi=110) plt.plot(times_values, CO2_values, label='Input CO₂') @@ -184,7 +199,7 @@ class CO2FormData(FormData): activity=None, # type: ignore ) - all_state_changes=self.population_present_changes(infected_presence, exposed_presence) + all_state_changes = self.population_present_changes(infected_presence, exposed_presence) total_people = [infected_population.people_present(stop) + exposed_population.people_present(stop) for _, stop in zip(all_state_changes[:-1], all_state_changes[1:])] diff --git a/caimira/models.py b/caimira/models.py index 3fe58e22..5170cc20 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1561,7 +1561,7 @@ class CO2DataModel: # Calculate the predictive CO2 concentration return [CO2_concentration_model.concentration(time) for time in self.times] - def CO2_fit_params(self): + def CO2_fit_params(self) -> typing.Dict: if len(self.times) != len(self.CO2_concentrations): raise ValueError('times and CO2_concentrations must have same length.') @@ -1570,6 +1570,11 @@ class CO2DataModel: 'times and CO2_concentrations must contain at last two elements') def fun(x): + ''' + The objective function to be minimized, where x is an argument + containing the initial guess for the breathing rate (exhalation_rate) + and ventilation values (ventilation_values). + ''' exhalation_rate = x[0] ventilation_values = tuple(x[1:]) CO2_concentration_model = self.CO2_concentration_model( From f83a886928dda3dc96ec7c71338630f144e6b9e7 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Thu, 27 Jun 2024 15:55:30 +0100 Subject: [PATCH 02/17] added tests with RMSEP --- .../models/test_co2_concentration_model.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/caimira/tests/models/test_co2_concentration_model.py b/caimira/tests/models/test_co2_concentration_model.py index ce57110b..001ee64b 100644 --- a/caimira/tests/models/test_co2_concentration_model.py +++ b/caimira/tests/models/test_co2_concentration_model.py @@ -1,9 +1,21 @@ import numpy.testing as npt +import numpy as np +import typing import pytest from caimira import models +@pytest.fixture +def real_sensor_data(): + day_times = [8.0, 8.033333333333333, 8.066666666666666, 8.1, 8.133333333333333, 8.166666666666666, 8.2, 8.233333333333333, 8.266666666666667, 8.3, 8.333333333333334, 8.366666666666667, 8.4, 8.433333333333334, 8.466666666666667, 8.5, 8.533333333333333, 8.566666666666666, 8.6, 8.633333333333333, 8.666666666666666, 8.7, 8.733333333333333, 8.766666666666667, 8.8, 8.833333333333334, 8.866666666666667, 8.9, 8.933333333333334, 8.966666666666667, 9.0, 9.033333333333333, 9.066666666666666, 9.1, 9.133333333333333, 9.166666666666666, 9.2, 9.233333333333333, 9.266666666666667, 9.3, 9.333333333333334, 9.366666666666667, 9.4, 9.433333333333334, 9.466666666666667, 9.5, 9.533333333333333, 9.566666666666666, 9.6, 9.633333333333333, 9.666666666666666, 9.7, 9.733333333333333, 9.766666666666667, 9.8, 9.833333333333334, 9.866666666666667, 9.9, 9.933333333333334, 9.966666666666667, 10.0, 10.033333333333333, 10.066666666666666, 10.1, 10.133333333333333, 10.166666666666666, 10.2, 10.233333333333333, 10.266666666666667, 10.3, 10.333333333333334, 10.366666666666667, 10.4, 10.433333333333334, 10.466666666666667, 10.5, 10.533333333333333, 10.566666666666666, 10.6, 10.633333333333333, 10.666666666666666, 10.7, 10.733333333333333, 10.766666666666667, 10.8, 10.833333333333334, 10.866666666666667, 10.9, 10.933333333333334, 10.966666666666667, 11.0, 11.033333333333333, 11.066666666666666, 11.1, 11.133333333333333, 11.166666666666666, 11.2, 11.233333333333333, 11.266666666666667, 11.3, 11.333333333333334, 11.366666666666667, 11.4, 11.433333333333334, 11.466666666666667, 11.5, 11.533333333333333, 11.566666666666666, 11.6, 11.633333333333333, 11.666666666666666, 11.7, 11.733333333333333, 11.766666666666667, 11.8, 11.833333333333334, 11.866666666666667, 11.9, 11.933333333333334, 11.966666666666667, 12.0, 12.033333333333333, 12.066666666666666, 12.1, 12.133333333333333, 12.166666666666666, 12.2, 12.233333333333333, 12.26666666666667, 12.3, 12.333333333333336, 12.366666666666667, 12.400000000000002, 12.433333333333334, 12.466666666666669, 12.5, 12.533333333333335, 12.566666666666666, 12.600000000000001, 12.633333333333333, 12.666666666666668, 12.7, 12.733333333333334, 12.766666666666666, 12.8, 12.833333333333332, 12.866666666666667, 12.899999999999999, 12.933333333333334, 12.966666666666665, 13.0, 13.033333333333331, 13.066666666666666, 13.099999999999998, 13.133333333333333, 13.166666666666664, 13.2, 13.23333333333333, 13.266666666666667, 13.3, 13.333333333333334, 13.366666666666667, 13.4, 13.433333333333334, 13.466666666666667, 13.5, 13.533333333333333, 13.566666666666666, 13.6, 13.633333333333333, 13.666666666666666, 13.7, 13.733333333333333, 13.76666666666667, 13.8, 13.833333333333336, 13.866666666666667, 13.900000000000002, 13.933333333333334, 13.966666666666669, 14.0, 14.033333333333335, 14.066666666666666, 14.100000000000001, 14.133333333333333, 14.166666666666668, 14.2, 14.233333333333334, 14.266666666666666, 14.3, 14.333333333333332, 14.366666666666667, 14.399999999999999, 14.433333333333334, 14.466666666666665, 14.5, 14.533333333333331, 14.566666666666666, 14.599999999999998, 14.633333333333333, 14.666666666666664, 14.7, 14.73333333333333, 14.766666666666667, 14.8, 14.833333333333334, 14.866666666666667, 14.9, 14.933333333333334, 14.966666666666667, 15.0, 15.033333333333333, 15.066666666666666, 15.1, 15.133333333333333, 15.166666666666666, 15.2, 15.233333333333333, 15.26666666666667, 15.3, 15.333333333333336, 15.366666666666667, 15.400000000000002, 15.433333333333334, 15.466666666666669, 15.5, 15.533333333333335, 15.566666666666666, 15.600000000000001, 15.633333333333333, 15.666666666666668, 15.7, 15.733333333333334, 15.766666666666666, 15.8, 15.833333333333332, 15.866666666666667, 15.899999999999999, 15.933333333333334, 15.966666666666665, 16.0, 16.033333333333335, 16.066666666666666, 16.1, 16.133333333333333, 16.166666666666668, 16.2, 16.233333333333334, 16.266666666666666, 16.3, 16.333333333333332, 16.366666666666667, 16.4, 16.433333333333334, 16.466666666666665, 16.5, 16.533333333333335, 16.566666666666666, 16.6, 16.633333333333333, 16.666666666666668, 16.7, 16.733333333333334, 16.766666666666666, 16.8, 16.833333333333332, 16.866666666666667, 16.9, 16.933333333333334, 16.966666666666665, 17.0, 17.033333333333335, 17.066666666666666, 17.1, 17.133333333333333, 17.166666666666668, 17.2, 17.233333333333334, 17.266666666666666, 17.3, 17.333333333333332, 17.366666666666667, 17.4, 17.433333333333334, 17.466666666666665, 17.5, 17.533333333333335, 17.566666666666666, 17.6, 17.633333333333333, 17.666666666666668, 17.7, 17.733333333333334, 17.766666666666666, 17.8, 17.833333333333332, 17.866666666666667, 17.9, 17.933333333333334, 17.966666666666665, 18.0, 18.033333333333335, 18.066666666666666, 18.1, 18.133333333333333, 18.166666666666668, 18.2, 18.233333333333334, 18.266666666666666, 18.3, 18.333333333333332, 18.366666666666667, 18.4, 18.433333333333334, 18.466666666666665, 18.5, 18.533333333333335, 18.566666666666666, 18.6, 18.633333333333333, 18.666666666666668, 18.7, 18.733333333333334, 18.766666666666666, 18.8, 18.833333333333332, 18.866666666666667, 18.9, 18.933333333333334, 18.966666666666665, 19.0, 19.033333333333335, 19.066666666666666, 19.1, 19.133333333333333, 19.166666666666668, 19.2, 19.233333333333334, 19.266666666666666, 19.3, 19.333333333333332, 19.366666666666667, 19.4, 19.433333333333334, 19.466666666666665, 19.5, 19.533333333333335, 19.566666666666666, 19.6, 19.633333333333333, 19.666666666666668, 19.7, 19.733333333333334, 19.766666666666666, 19.8, 19.833333333333332, 19.866666666666667, 19.9, 19.933333333333334, 19.966666666666665] + day_co2 = [445.189166666667, 443.284166666667, 440.908333333333, 443.430833333333, 442.365833333333, 444.094166666667, 445.151666666667, 445.655833333333, 447.9675, 447.998333333333, 443.95, 442.546666666667, 439.313333333333, 438.225, 441.4325, 441.19, 443.804166666667, 445.173333333333, 446.494166666667, 445.2775, 452.073333333333, 458.844166666667, 470.828333333333, 478.146666666667, 488.3375, 502.125833333333, 522.056666666667, 545.519166666667, 579.880833333333, 616.245, 641.154166666667, 676.288333333333, 701.9375, 720.464166666667, 746.933333333333, 765.83, 779.098333333333, 794.173333333333, 810.624166666667, 825.966666666667, 838.34, 854.355, 876.381666666667, 886.208333333334, 898.408333333333, 921.7175, 942.848333333333, 953.811666666667, 978.955833333333, 990.320833333333, 1002.93083333333, 1017.36083333333, 1029.37916666667, 1041.02833333333, 1051.8825, 1067.22, 1073.53, 1079.73833333333, 1093.73333333333, 1104.81416666667, 1125.7975, 1141.115, 1151.04583333333, 1160.0525, 1176.36666666667, 1193.665, 1180.10416666667, 1015.33416666667, 864.745833333333, 802.680833333333, 774.455, 728.268333333333, 697.325833333333, 676.063333333333, 657.555, 640.564166666667, 606.534166666667, 595.925, 577.7525, 553.605, 530.2125, 524.968333333333, 523.1525, 521.534166666667, 512.944166666667, 505.296666666667, 502.055833333333, 502.463333333333, 505.2475, 507.476666666667, 509.170833333333, 511.3125, 513.78, 520.3925, 529.136666666667, 532.798333333333, 530.110833333333, 523.964166666667, 521.574166666667, 519.051666666667, 510.294166666667, 509.981666666667, 514.349166666667, 518.395833333333, 524.6025, 521.003333333333, 519.448333333333, 523.3125, 527.46, 528.325833333333, 526.355, 527.008333333333, 529.9675, 534.019166666667, 535.615833333333, 533.514166666667, 530.551666666667, 522.348333333333, 524.2425, 532.020833333333, 539.126666666667, 538.835833333333, 526.185833333333, 517.509166666667, 507.993333333333, 493.7025, 485.631666666667, 479.526666666667, 471.584166666667, 472.225833333333, 468.205833333333, 463.099166666667, 461.0375, 458.98, 456.354166666667, 458.615, 459.161666666667, 462.9625, 465.558333333333, 468.448333333333, 475.206666666667, 480.3225, 488.961666666667, 527.991818181818, 579.613333333333, 606.594166666667, 611.2175, 617.0225, 635.926666666667, 651.079166666667, 676.646666666667, 696.63, 714.603333333333, 729.926666666667, 744.6525, 765.995833333333, 788.4925, 812.105833333333, 832.75, 854.715, 883.851666666667, 895.591666666667, 910.026666666667, 924.373333333333, 944.516666666667, 956.769166666667, 971.446666666667, 981.2725, 993.645833333333, 1004.37833333333, 1021.56833333333, 1035.155, 1043.84916666667, 1063.7225, 1070.96083333333, 1065.62416666667, 1065.89333333333, 1073.72333333333, 1086.39333333333, 1093.525, 1120.085, 1189.26, 1202.875, 1218.55583333333, 1238.46416666667, 1250.06, 1263.46, 1265.04333333333, 1270.10166666667, 1281.61, 1294.92416666667, 1304.21833333333, 1315.50583333333, 1338.43416666667, 1351.53083333333, 1353.35916666667, 1364.0425, 1361.66583333333, 1343.3225, 1329.69833333333, 1320.43583333333, 1310.45, 1313.62166666667, 1305.505, 1313.36, 1307.45916666667, 1289.97666666667, 1286.87666666667, 1289.315, 1276.8075, 1268.87083333333, 1266.07083333333, 1264.0475, 1271.76416666667, 1268.46833333333, 1244.53166666667, 1206.35416666667, 1173.62666666667, 1144.95833333333, 1157.15166666667, 1194.38333333333, 1198.275, 1196.0825, 1182.46583333333, 1167.85666666667, 1150.36083333333, 1132.83833333333, 1108.08, 1097.41583333333, 1099.82333333333, 1093.3775, 1086.7575, 1086.875, 1083.80166666667, 1075.48166666667, 1059.92083333333, 1048.40416666667, 1047.35, 1042.55166666667, 1036.11333333333, 1026.88333333333, 1022.6775, 1017.64666666667, 1023.52083333333, 1021.01666666667, 1017.255, 1004.57166666667, 908.28, 906.460833333333, 979.216666666667, 955.8475, 928.884166666667, 915.265833333333, 914.135833333333, 930.121666666666, 923.345833333333, 920.959166666667, 865.924166666667, 860.181666666667, 867.003333333333, 869.708333333333, 871.378333333333, 861.466666666667, 862.920833333334, 850.374166666667, 843.933333333333, 839.650833333334, 838.1425, 839.793333333333, 849.7425, 841.635833333333, 820.793333333333, 824.950833333333, 838.52, 853.844166666667, 855.645, 838.704166666667, 818.070833333333, 811.7225, 804.270833333333, 794.473333333333, 790.601666666667, 781.965, 788.555833333333, 779.635833333333, 804.179166666667, 836.930833333333, 852.391666666667, 856.87, 858.073333333333, 857.895, 856.51, 856.744166666667, 851.720833333333, 849.561666666667, 849.17, 846.0125, 846.755833333333, 844.778333333333, 841.989166666667, 838.9925, 836.0375, 833.210833333333, 832.631666666667, 830.8925, 825.924166666667, 823.550833333333, 823.720833333333, 818.218333333333, 812.421666666667, 810.169166666667, 808.388333333333, 806.663333333333, 803.0125, 800.164166666667, 794.33, 790.45, 790.359166666667, 787.696666666667, 783.316666666667, 780.483333333334, 783.9625, 780.8675, 780.761666666667, 777.228333333333, 774.976666666667, 768.6975, 763.635, 761.453333333334, 757.805, 760.021666666667, 762.135833333333, 761.649166666667, 761.2175, 761.484166666667, 757.53, 754.458333333333, 752.550833333333, 752.123333333333, 751.679166666666, 748.565, 744.345833333333, 742.103333333333, 737.648333333333, 731.049166666667, 732.635833333333, 726.8275, 726.21, 726.948333333333, 727.14, 726.7575, 728.934166666667, 729.886666666667, 726.511666666667, 724.84, 723.9075, 722.995833333333, 721.1125, 720.204166666667, 721.064166666667] + return { + 'times': day_times, + 'CO2': day_co2, + } + + @pytest.fixture def simple_co2_conc_model(data_registry): return models.CO2ConcentrationModel( @@ -41,3 +53,32 @@ def test_integrated_concentration(simple_co2_conc_model): c3 = simple_co2_conc_model.integrated_concentration(1, 2) assert c1 != 0 npt.assert_almost_equal(c1, c2 + c3) + + +def test_predictive_model_accuracy(data_registry, real_sensor_data): + ''' + 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. + ''' + 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=real_sensor_data['times'], + CO2_concentrations=real_sensor_data['CO2'], + ) + # Get fitting results + fitting_results: typing.Dict = fitting_model.CO2_fit_params() + predictive_CO2: typing.List[float] = fitting_results['predictive_CO2'] + + def root_mean_square_error_percentage(actual, predicted) -> float: + return np.sqrt(np.mean(((actual - predicted) / actual) ** 2)) * 100 + + # Calculate RMSEP metric + rmsep = root_mean_square_error_percentage(np.array(real_sensor_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 From 84ffcb5e2374107ad761b1af21d516465b388561 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Thu, 27 Jun 2024 16:00:20 +0100 Subject: [PATCH 03/17] removed fitting ventilation type input (not needed) --- .../apps/calculator/co2_model_generator.py | 16 +-- caimira/apps/calculator/model_generator.py | 11 +- caimira/apps/calculator/static/js/co2_form.js | 105 +++++++----------- caimira/apps/calculator/static/js/form.js | 20 ---- .../templates/base/calculator.form.html.j2 | 13 +-- 5 files changed, 54 insertions(+), 111 deletions(-) diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index 440a2b1c..4e238ce4 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -21,13 +21,13 @@ LOG = logging.getLogger(__name__) class CO2FormData(FormData): CO2_data: dict fitting_ventilation_states: list - fitting_ventilation_type: str room_capacity: typing.Optional[int] #: The default values for undefined fields. Note that the defaults here #: and the defaults in the html form must not be contradictory. _DEFAULTS: typing.ClassVar[typing.Dict[str, typing.Any]] = { 'CO2_data': '{}', + 'fitting_ventilation_states': '[]', 'exposed_coffee_break_option': 'coffee_break_0', 'exposed_coffee_duration': 5, 'exposed_finish': '17:30', @@ -35,8 +35,6 @@ class CO2FormData(FormData): 'exposed_lunch_option': True, 'exposed_lunch_start': '12:30', 'exposed_start': '08:30', - 'fitting_ventilation_states': '[]', - 'fitting_ventilation_type': 'fitting_natural_ventilation', 'infected_coffee_break_option': 'coffee_break_0', 'infected_coffee_duration': 5, 'infected_dont_have_breaks_with_exposed': False, @@ -173,14 +171,10 @@ class CO2FormData(FormData): state_change_times.update(exposed_presence.transition_times()) return sorted(state_change_times) - def ventilation_transition_times(self) -> typing.Tuple[float, ...]: - # Check what type of ventilation is considered for the fitting - if self.fitting_ventilation_type == 'fitting_natural_ventilation': - vent_states = self.fitting_ventilation_states - vent_states.append(self.CO2_data['times'][-1]) - return tuple(vent_states) - else: - return tuple((self.CO2_data['times'][0], self.CO2_data['times'][-1])) + def ventilation_transition_times(self) -> typing.List[float]: + vent_states = self.fitting_ventilation_states + vent_states.append(self.CO2_data['times'][-1]) # The last time value is always needed for the last ACH interval. + return vent_states def build_model(self, size=None) -> models.CO2DataModel: # type: ignore size = size or self.data_registry.monte_carlo['sample_size'] diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index d1895848..b0656851 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -330,13 +330,10 @@ class VirusFormData(FormData): min(self.infected_start, self.exposed_start)/60) if self.ventilation_type == 'from_fitting': ventilations = [] - if self.CO2_fitting_result['fitting_ventilation_type'] == 'fitting_natural_ventilation': - transition_times = self.CO2_fitting_result['transition_times'] - for index, (start, stop) in enumerate(zip(transition_times[:-1], transition_times[1:])): - ventilations.append(models.AirChange(active=models.SpecificInterval(present_times=((start, stop), )), - air_exch=self.CO2_fitting_result['ventilation_values'][index])) - else: - ventilations.append(models.AirChange(active=always_on, air_exch=self.CO2_fitting_result['ventilation_values'][0])) + transition_times = self.CO2_fitting_result['transition_times'] + for index, (start, stop) in enumerate(zip(transition_times[:-1], transition_times[1:])): + ventilations.append(models.AirChange(active=models.SpecificInterval(present_times=((start, stop), )), + air_exch=self.CO2_fitting_result['ventilation_values'][index])) return models.MultipleVentilation(tuple(ventilations)) # Initializes a ventilation instance as a window if 'natural_ventilation' is selected, or as a HEPA-filter otherwise diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index 77b0b756..5f6ff406 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -9,7 +9,6 @@ const CO2_data_form = [ "exposed_lunch_start", "exposed_start", "fitting_ventilation_states", - "fitting_ventilation_type", "infected_coffee_break_option", "infected_coffee_duration", "infected_dont_have_breaks_with_exposed", @@ -137,7 +136,6 @@ function generateJSONStructure(endpoint, jsonData) { inputToPopulate.val(JSON.stringify(finalStructure)); $("#generate_fitting_data").prop("disabled", false); $("#fitting_ventilation_states").prop("disabled", false); - $("[name=fitting_ventilation_type]").prop("disabled", false); $("#room_capacity").prop("disabled", false); plotCO2Data(endpoint); } @@ -177,66 +175,54 @@ function validateCO2Form() { if (validateFormInputs($("#button_fit_data"))) submit = true; const $fittingToSubmit = $('#DIVCO2_fitting_to_submit'); - // Check if natural ventilation is selected - if ( - $fittingToSubmit.find('input[name="fitting_ventilation_type"]:checked').val() == - "fitting_natural_ventilation" - ) { - // Validate ventilation scheme - const $ventilationStates = $fittingToSubmit.find("input[name=fitting_ventilation_states]"); - const $referenceNode = $("#DIVCO2_fitting_result"); - if ($ventilationStates.val() !== "") { - // validate input format - try { - const parsedValue = JSON.parse($ventilationStates.val()); - if (Array.isArray(parsedValue)) { - if (parsedValue.length <= 1) { - insertErrorFor( - $referenceNode, - `'${$ventilationStates.attr('name')}' must have more than one $ventilationStates.
` - ); - submit = false; - } - else { - const infected_finish = $(`[name=infected_finish]`).first().val(); - const exposed_finish = $(`[name=exposed_finish]`).first().val(); - - const [hours_infected, minutes_infected] = infected_finish.split(":").map(Number); - const elapsed_time_infected = hours_infected * 60 + minutes_infected; - - const [hours_exposed, minutes_exposed] = exposed_finish.split(":").map(Number); - const elapsed_time_exposed = hours_exposed * 60 + minutes_exposed; - - const max_presence_time = Math.max(elapsed_time_infected, elapsed_time_exposed); - const max_transition_time = parsedValue[parsedValue.length - 1] * 60; - - if (max_transition_time > max_presence_time) { - insertErrorFor( - $referenceNode, - `The last transition time (${parsedValue[parsedValue.length - 1]}) should be before the last presence time (${max_presence_time / 60}).
` - ); - submit = false; - } - } - } - else { + // Validate ventilation scheme + const $ventilationStates = $fittingToSubmit.find("input[name=fitting_ventilation_states]"); + const $referenceNode = $("#DIVCO2_fitting_result"); + if ($ventilationStates.val() !== "") { + // validate input format + try { + const parsedValue = JSON.parse($ventilationStates.val()); + if (Array.isArray(parsedValue)) { + if (parsedValue.length <= 1) { insertErrorFor( $referenceNode, - `'${$ventilationStates.attr('name')}' must be a list.
` + `'${$ventilationStates.attr('name')}' must have more than one ventilation state change (at least the beggining and end of simulation time).
` ); submit = false; } - } catch { + else { + const infected_finish = $(`[name=infected_finish]`).first().val(); + const exposed_finish = $(`[name=exposed_finish]`).first().val(); + + const [hours_infected, minutes_infected] = infected_finish.split(":").map(Number); + const elapsed_time_infected = hours_infected * 60 + minutes_infected; + + const [hours_exposed, minutes_exposed] = exposed_finish.split(":").map(Number); + const elapsed_time_exposed = hours_exposed * 60 + minutes_exposed; + + const max_presence_time = Math.max(elapsed_time_infected, elapsed_time_exposed); + const max_transition_time = parsedValue[parsedValue.length - 1] * 60; + + if (max_transition_time > max_presence_time) { + insertErrorFor( + $referenceNode, + `The last transition time (${parsedValue[parsedValue.length - 1]}) should be before the last presence time (${max_presence_time / 60}).
` + ); + submit = false; + } + } + } + else { insertErrorFor( $referenceNode, - `'${$ventilationStates.attr('name')}' must be a list of numbers.
` + `'${$ventilationStates.attr('name')}' must be a list.
` ); submit = false; } - } else { + } catch { insertErrorFor( $referenceNode, - `'${$ventilationStates.attr('name')}' must be defined.
` + `'${$ventilationStates.attr('name')}' must be a list of numbers.
` ); submit = false; } @@ -253,8 +239,13 @@ function validateCO2Form() { submit = false; } } + } else { + insertErrorFor( + $referenceNode, + `'${$ventilationStates.attr('name')}' must be defined.
` + ); + submit = false; } - return submit; } @@ -369,17 +360,11 @@ function plotCO2Data(url) { function submitFittingAlgorithm(url) { if (validateCO2Form()) { - // Disable all the ventilation inputs - $("#fitting_ventilation_states, [name=fitting_ventilation_type]").prop( - "disabled", - true - ); // Disable room capacity input $("#room_capacity").prop( "disabled", true ); - // Prepare data for submission const CO2_mapping = formatCO2DataForm(CO2_data_form); $("#CO2_input_data_div").show(); @@ -423,12 +408,6 @@ function clearFittingResultComponent() { $referenceNode.find("#DIVCO2_fitting_to_submit").hide(); $referenceNode.find("#CO2_data_plot").attr("src", ""); - // Update the ventilation scheme components - $referenceNode.find("#fitting_ventilation_states, [name=fitting_ventilation_type]").prop( - "disabled", - false - ); - // Update the bottom right buttons $referenceNode.find("#generate_fitting_data").show(); $referenceNode.find("#save_and_dismiss_dialog").hide(); diff --git a/caimira/apps/calculator/static/js/form.js b/caimira/apps/calculator/static/js/form.js index e63bbb45..631be3cc 100644 --- a/caimira/apps/calculator/static/js/form.js +++ b/caimira/apps/calculator/static/js/form.js @@ -479,20 +479,6 @@ function on_coffee_break_option_change() { } } -function on_CO2_fitting_ventilation_change() { - ventilation_options = $('input[type=radio][name=fitting_ventilation_type]'); - ventilation_options.each(function (index) { - if (this.checked) { - getChildElement($(this)).show(); - require_fields(this); - } - else { - getChildElement($(this)).hide(); - require_fields(this); - } - }) -} - /* -------UI------- */ function show_disclaimer() { @@ -1070,12 +1056,6 @@ $(document).ready(function () { // Call the function now to handle forward/back button presses in the browser. on_coffee_break_option_change(); - // When the ventilation on the fitting changes we want to make its respective - // children show/hide. - $("input[type=radio][name=fitting_ventilation_type]").change(on_CO2_fitting_ventilation_change); - // Call the function now to handle forward/back button presses in the browser. - on_CO2_fitting_ventilation_change(); - // Setup the maximum number of people at page load (to handle back/forward), // and update it when total people is changed. validateMaxInfectedPeople(); diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index fa97661e..40fc6d90 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -347,16 +347,9 @@

The dashed lines are suggestions for the ventilation transition times
- (generated from the input data using the Pelt algorithm).

- Ventilation scheme: -
- - - - -
- -