From b3f7321a285794ead925f4849a3870c81253fadb Mon Sep 17 00:00:00 2001 From: lrdossan Date: Tue, 9 Jul 2024 16:22:52 +0200 Subject: [PATCH 01/14] added table for l/s/p results --- .../apps/calculator/co2_model_generator.py | 3 +- caimira/apps/calculator/static/js/co2_form.js | 14 ++-- .../templates/base/calculator.form.html.j2 | 2 +- caimira/models.py | 68 +++++++++++++++---- .../tests/models/test_fitting_algorithm.py | 3 +- 5 files changed, 67 insertions(+), 23 deletions(-) diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index a4a6a9c2..3544fe54 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -182,8 +182,7 @@ def build_model(self, size=None) -> models.CO2DataModel: # type: ignore return models.CO2DataModel( data_registry=self.data_registry, room_volume=self.room_volume, - number=models.IntPiecewiseConstant(transition_times=tuple(all_state_changes), values=tuple(total_people)), - presence=None, + occupancy=models.IntPiecewiseConstant(transition_times=tuple(all_state_changes), values=tuple(total_people)), ventilation_transition_times=self.ventilation_transition_times(), times=self.CO2_data['times'], CO2_concentrations=self.CO2_data['CO2'], diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index e846bbcf..ee31c24e 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -268,16 +268,20 @@ function displayFittingData(json_response) { " m³/h" ); let ventilation_table = - "Time (HH:MM)ACH value (h⁻¹)"; - json_response["ventilation_values"].forEach((val, index) => { + "Time (HH:MM)ACH value (h⁻¹)Flow rate (L/s/person)"; + json_response["ventilation_values"].forEach((CO2_val, index) => { let transition_times = displayTransitionTimesHourFormat( json_response["transition_times"][index], json_response["transition_times"][index + 1] ); - ventilation_table += `${transition_times}${val.toPrecision( - 2 - )}`; + + ventilation_table += ` + ${transition_times} + ${CO2_val.toPrecision(2)} + ${json_response['ventilation_lsp_values'][index].toPrecision(2)} + `; }); + $("#disable_fitting_algorithm").prop("disabled", false); $("#ventilation_rate_fit").html(ventilation_table); $("#generate_fitting_data").html("Fit data"); diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 745612e6..497bf33e 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -380,7 +380,7 @@ -
+
HEPA filtration:
diff --git a/caimira/models.py b/caimira/models.py index 88f9ffb1..b22b7a5f 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1532,28 +1532,30 @@ class CO2DataModel: ''' data_registry: DataRegistry room_volume: float - number: typing.Union[int, IntPiecewiseConstant] - presence: typing.Optional[Interval] + occupancy: IntPiecewiseConstant ventilation_transition_times: typing.Tuple[float, ...] times: typing.Sequence[float] CO2_concentrations: typing.Sequence[float] - def CO2_concentrations_from_params(self, - exhalation_rate: float, - ventilation_values: typing.Tuple[float, ...]) -> typing.List[_VectorisedFloat]: - CO2_concentrations = CO2ConcentrationModel( + def CO2_concentration_model(self, + exhalation_rate: float, + ventilation_values: typing.Tuple[float, ...]) -> CO2ConcentrationModel: + return CO2ConcentrationModel( data_registry=self.data_registry, room=Room(volume=self.room_volume), ventilation=CustomVentilation(PiecewiseConstant( self.ventilation_transition_times, ventilation_values)), CO2_emitters=SimplePopulation( - number=self.number, - presence=self.presence, + number=self.occupancy, + presence=None, activity=Activity( exhalation_rate=exhalation_rate, inhalation_rate=exhalation_rate), ) ) - return [CO2_concentrations.concentration(time) for time in self.times] + + def CO2_concentrations_from_params(self, CO2_concentration_model: CO2ConcentrationModel) -> typing.List[_VectorisedFloat]: + # Calculate the predictive CO2 concentration + return [CO2_concentration_model.concentration(time) for time in self.times] def CO2_fit_params(self): if len(self.times) != len(self.CO2_concentrations): @@ -1566,10 +1568,11 @@ def CO2_fit_params(self): def fun(x): exhalation_rate = x[0] ventilation_values = tuple(x[1:]) - the_concentrations = self.CO2_concentrations_from_params( + CO2_concentration_model = self.CO2_concentration_model( exhalation_rate=exhalation_rate, ventilation_values=ventilation_values ) + the_concentrations = self.CO2_concentrations_from_params(CO2_concentration_model) return np.sqrt(np.sum((np.array(self.CO2_concentrations) - np.array(the_concentrations))**2)) # The goal is to minimize the difference between the two different curves (known concentrations vs. predicted concentrations) @@ -1577,10 +1580,49 @@ def fun(x): bounds=[(0, None) for _ in range(len(self.ventilation_transition_times))], options={'xtol': 1e-3}) + # Final prediction exhalation_rate = res_dict['x'][0] - ventilation_values = res_dict['x'][1:] - predictive_CO2 = self.CO2_concentrations_from_params(exhalation_rate=exhalation_rate, ventilation_values=ventilation_values) - return {"exhalation_rate": exhalation_rate, "ventilation_values": list(ventilation_values), 'predictive_CO2': list(predictive_CO2)} + ventilation_values = res_dict['x'][1:] # In ACH + + # Final CO2ConcentrationModel with obtained prediction + the_CO2_concentration_model = self.CO2_concentration_model( + exhalation_rate=exhalation_rate, + ventilation_values=ventilation_values + ) + the_predictive_CO2 = self.CO2_concentrations_from_params(the_CO2_concentration_model) + + # Ventilation in L/s/person + def max_occupancy_in_interval(start: float, stop: float) -> int: + """ + Given a certain ventilation interval, get the maximum number of + people in that period od time. + """ + max_people: int = 0 + for i, (people_start, people_stop) in enumerate(zip(self.occupancy.transition_times[:-1], + self.occupancy.transition_times[1:])): + if people_stop <= start or people_start >= stop: + continue + if self.occupancy.values[i] > max_people: max_people = self.occupancy.values[i] + return max_people + + vent_volume_liter_person = [] + for i, (vent_start, vent_stop) in enumerate(zip(self.ventilation_transition_times[:-1], + self.ventilation_transition_times[1:])): + max_people = max_occupancy_in_interval(vent_start, vent_stop) + if max_people == 0: + # If in a certain interval there are no occupancy, the flow rate per second/person is 0 + vent_volume_liter_person.append(0) + else: + vent_volume_liter_person.append( + ventilation_values[i] / 3600 * self.room_volume / max_people * 1000 + ) # 1m^3 = 1000L + + return { + "exhalation_rate": exhalation_rate, + "ventilation_values": list(ventilation_values), + "ventilation_lsp_values": vent_volume_liter_person, + 'predictive_CO2': list(the_predictive_CO2) + } @dataclass(frozen=True) diff --git a/caimira/tests/models/test_fitting_algorithm.py b/caimira/tests/models/test_fitting_algorithm.py index 65b6f447..b917f9a1 100644 --- a/caimira/tests/models/test_fitting_algorithm.py +++ b/caimira/tests/models/test_fitting_algorithm.py @@ -41,9 +41,8 @@ def test_fitting_algorithm(data_registry, activity_type, ventilation_active, air data_model = models.CO2DataModel( data_registry=data_registry, room_volume=75, - number=models.IntPiecewiseConstant(transition_times=tuple( + occupancy=models.IntPiecewiseConstant(transition_times=tuple( [8, 12, 13, 17]), values=tuple([2, 1, 2])), - presence=None, ventilation_transition_times=tuple(ventilation_active), times=times, CO2_concentrations=CO2_concentrations From 3326c6234cc652840400bbed481ae44f535c71e8 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Thu, 18 Jul 2024 12:19:41 +0200 Subject: [PATCH 02/14] typo --- caimira/apps/calculator/static/js/co2_form.js | 2 +- caimira/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index ee31c24e..fb45044b 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -74,7 +74,7 @@ function uploadFile(endpoint) { } } - const data = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false }); + const data = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: true }); // Check if there is any data below the header row if (data.length <= 1) { $("#upload-error") diff --git a/caimira/models.py b/caimira/models.py index b22b7a5f..6127125a 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1595,7 +1595,7 @@ def fun(x): def max_occupancy_in_interval(start: float, stop: float) -> int: """ Given a certain ventilation interval, get the maximum number of - people in that period od time. + people in that period of time. """ max_people: int = 0 for i, (people_start, people_stop) in enumerate(zip(self.occupancy.transition_times[:-1], From ada64d6acc41f242c57c9880062a4d633f7bbd85 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Thu, 18 Jul 2024 12:32:04 +0200 Subject: [PATCH 03/14] added test for lsp ventilation --- .../tests/models/test_fitting_algorithm.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/caimira/tests/models/test_fitting_algorithm.py b/caimira/tests/models/test_fitting_algorithm.py index b917f9a1..0d1f2440 100644 --- a/caimira/tests/models/test_fitting_algorithm.py +++ b/caimira/tests/models/test_fitting_algorithm.py @@ -6,17 +6,17 @@ @pytest.mark.parametrize( - "activity_type, ventilation_active, air_exch", [ - ['Seated', [8, 12, 13, 17], [0.25, 2.45, 0.25]], - ['Standing', [8, 10, 11, 12, 17], [1.25, 3.25, 1.45, 0.25]], - ['Light activity', [8, 12, 17], [1.25, 0.25]], - ['Moderate activity', [8, 13, 15, 16, 17], [2.25, 0.25, 3.45, 0.25]], - ['Heavy exercise', [8, 17], [0.25]], - ['Seated', [8, 17], [0.25]], - ['Standing', [8, 17], [2.45]], + "activity_type, ventilation_active, air_exch, flow_rate_lsp", [ + ['Seated', [8, 12, 13, 17], [0.25, 2.45, 0.25], [2.604166667, 51.04166667, 2.604166667]], + ['Standing', [8, 10, 11, 12, 17], [1.25, 3.25, 1.45, 0.25], [13.02083333333, 33.8541666667, 15.1041666667, 2.6041666667]], + ['Light activity', [8, 12, 17], [1.25, 0.25], [13.02083333333, 2.6041666667]], + ['Moderate activity', [8, 13, 15, 16, 17], [2.25, 0.25, 3.45, 0.25], [23.4375, 2.6041666667, 35.9375, 2.6041666667]], + ['Heavy exercise', [8, 17], [0.25], [2.6041666667]], + ['Seated', [8, 17], [0.25], [2.6041666667]], + ['Standing', [8, 17], [2.45], [25.5208333333]], ] ) -def test_fitting_algorithm(data_registry, activity_type, ventilation_active, air_exch): +def test_fitting_algorithm(data_registry, activity_type, ventilation_active, air_exch, flow_rate_lsp): conc_model = models.CO2ConcentrationModel( data_registry = data_registry, room=models.Room( @@ -55,4 +55,7 @@ def test_fitting_algorithm(data_registry, activity_type, ventilation_active, air ventilation_values = fit_parameters['ventilation_values'] npt.assert_allclose(ventilation_values, air_exch, rtol=1e-2) + + ventilation_lsp_values = fit_parameters['ventilation_lsp_values'] + npt.assert_allclose(ventilation_lsp_values, flow_rate_lsp, rtol=1e-2) \ No newline at end of file From 53b3a52513297bf2c74f223e15e9c33534395d59 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Tue, 20 Aug 2024 15:53:23 +0100 Subject: [PATCH 04/14] added new input "room_capacity" and adapted the calculations of the flow rate (l/s/p) accordingly --- .../apps/calculator/co2_model_generator.py | 11 +++++ caimira/apps/calculator/static/js/co2_form.js | 41 ++++++++++++++++++- .../templates/base/calculator.form.html.j2 | 5 +++ caimira/models.py | 34 ++++----------- 4 files changed, 63 insertions(+), 28 deletions(-) diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index 3544fe54..51a950b2 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -22,6 +22,7 @@ class CO2FormData(FormData): CO2_data: dict fitting_ventilation_states: list fitting_ventilation_type: str + room_capacity: int #: The default values for undefined fields. Note that the defaults here #: and the defaults in the html form must not be contradictory. @@ -45,6 +46,7 @@ class CO2FormData(FormData): 'infected_lunch_start': '12:30', 'infected_people': 1, 'infected_start': '08:30', + 'room_capacity': 10, 'room_volume': NO_DEFAULT, 'specific_breaks': '{}', 'total_people': NO_DEFAULT, @@ -62,6 +64,14 @@ def validate(self): # Validate population parameters self.validate_population_parameters() + # Validate room capacity + if type(self.room_capacity) is not int: + raise TypeError(f'The room capacity should be a valid integer (> 0). Got {type(self.room_capacity)}.') + if self.room_capacity <= 0: + raise TypeError(f'The room capacity should be a valid integer (> 0). Got {self.room_capacity}.') + if self.room_capacity < self.total_people: + raise TypeError(f'The room capacity should be higher than the total people in the room. Got {self.room_capacity}.') + # Validate specific inputs - breaks (exposed and infected) if self.specific_breaks != {}: if type(self.specific_breaks) is not dict: @@ -181,6 +191,7 @@ def build_model(self, size=None) -> models.CO2DataModel: # type: ignore return models.CO2DataModel( data_registry=self.data_registry, + room_capacity=self.room_capacity, room_volume=self.room_volume, occupancy=models.IntPiecewiseConstant(transition_times=tuple(all_state_changes), values=tuple(total_people)), ventilation_transition_times=self.ventilation_transition_times(), diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index fb45044b..5b8d4a2c 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -19,6 +19,7 @@ const CO2_data_form = [ "infected_lunch_start", "infected_people", "infected_start", + "room_capacity", "room_volume", "specific_breaks", "total_people", @@ -137,6 +138,7 @@ function generateJSONStructure(endpoint, jsonData) { $("#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); } } @@ -152,7 +154,9 @@ function validateFormInputs(obj) { const $referenceNode = $("#DIVCO2_data_dialog"); for (let i = 0; i < CO2_data_form.length; i++) { const $requiredElement = $(`[name=${CO2_data_form[i]}]`).first(); - if ($requiredElement.attr('name') !== "fitting_ventilation_states" && $requiredElement.val() === "") { + if ($requiredElement.attr('name') !== "fitting_ventilation_states" && + $requiredElement.attr('name') !== "room_capacity" && + $requiredElement.val() === "") { insertErrorFor( $referenceNode, `'${$requiredElement.attr('name')}' must be defined.
` @@ -236,6 +240,36 @@ function validateCO2Form() { ); submit = false; } + // Validate room capacity + const roomCapacity = $fittingToSubmit.find("input[name=room_capacity]"); + const roomCapacityVal = roomCapacity.val(); + if (roomCapacityVal !== "") { + const roomCapacityNumber = Number(roomCapacityVal); + const totalPeopleNumber = Number($("#total_people").val()); + if (!Number.isInteger(roomCapacityNumber) || roomCapacityNumber <= 0) { + insertErrorFor( + $referenceNode, + `'${roomCapacity.attr('name')}' must be a valid integer (> 0).
` + ); + submit = false; + } + else if (roomCapacityNumber < totalPeopleNumber){ + insertErrorFor( + $referenceNode, + `'${roomCapacity.attr('name')}' must be higher than the total people.
` + ); + submit = false; + } + console.log(roomCapacityNumber) + console.log(totalPeopleNumber) + } + else { + insertErrorFor( + $referenceNode, + `'${roomCapacity.attr('name')}' must be defined.
` + ); + submit = false; + } } return submit; @@ -341,6 +375,11 @@ function submitFittingAlgorithm(url) { "disabled", true ); + // Disable room capacity input + $("#room_capacity").prop( + "disabled", + true + ); // Prepare data for submission const CO2_mapping = formatCO2DataForm(CO2_data_form); diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 497bf33e..377f0433 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -363,6 +363,11 @@

+ Room data: +
+
+
+
Room data: -
-
-
+
+ +
+ ? +
+
diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2 index 041cf3ef..c7f330d4 100644 --- a/caimira/apps/templates/base/calculator.report.html.j2 +++ b/caimira/apps/templates/base/calculator.report.html.j2 @@ -541,14 +541,21 @@ {% endif %}
  • From Fitting: {% if form.ventilation_type == "from_fitting" %} - Yes + Yes

  • - + + + + + + {% for ventilation in form.CO2_fitting_result['ventilation_values'] %} {% set transition_time = form.CO2_fitting_result['transition_times'] %} + + {% endfor %}
    Time (HH:MM)ACH value (h⁻¹)
    Time (HH:MM)ACH value (h⁻¹)Flow rate (L/s)Flow rate (L/s/person)
    {{ transition_time[loop.index - 1] | hour_format }} - {{ transition_time[loop.index] | hour_format }} {{ ventilation | float_format }} {{ form.CO2_fitting_result['ventilation_ls_values'][loop.index - 1] | float_format }} {{ form.CO2_fitting_result['ventilation_lsp_values'][loop.index - 1] | float_format }}
    diff --git a/caimira/models.py b/caimira/models.py index 4bcb0f22..49d918ad 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1593,13 +1593,17 @@ def fun(x): ) the_predictive_CO2 = self.CO2_concentrations_from_params(the_CO2_concentration_model) - # Ventilation in L/s/person - vent_volume_liter_person = [vent / 3600 * self.room_volume / self.room_capacity * 1000 + # Ventilation in L/s + vent_volume_liter = [vent / 3600 * self.room_volume * 1000 for vent in ventilation_values] # 1m^3 = 1000L + # Ventilation in L/s/person + vent_volume_liter_person = [vent / self.room_capacity for vent in vent_volume_liter] + return { "exhalation_rate": exhalation_rate, "ventilation_values": list(ventilation_values), + "ventilation_ls_values": vent_volume_liter, "ventilation_lsp_values": vent_volume_liter_person, 'predictive_CO2': list(the_predictive_CO2) } From 309b00a7cd1a33cca577c5829bde7093c202f6d5 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Mon, 26 Aug 2024 15:50:46 +0200 Subject: [PATCH 07/14] added room_capacity as prop of room --- caimira/apps/calculator/co2_model_generator.py | 3 +-- caimira/models.py | 17 ++++++++++------- caimira/tests/models/test_fitting_algorithm.py | 3 +-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index 51a950b2..85a41515 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -191,8 +191,7 @@ def build_model(self, size=None) -> models.CO2DataModel: # type: ignore return models.CO2DataModel( data_registry=self.data_registry, - room_capacity=self.room_capacity, - room_volume=self.room_volume, + room=models.Room(volume=self.room_volume, capacity=self.room_capacity), occupancy=models.IntPiecewiseConstant(transition_times=tuple(all_state_changes), values=tuple(total_people)), ventilation_transition_times=self.ventilation_transition_times(), times=self.CO2_data['times'], diff --git a/caimira/models.py b/caimira/models.py index 49d918ad..4c4f54a3 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -217,6 +217,9 @@ class Room: #: The humidity in the room (from 0 to 1 - e.g. 0.5 is 50% humidity) humidity: _VectorisedFloat = 0.5 + #: The maximum occupation of the room - design limit + capacity: float = 10 + @dataclass(frozen=True) class _VentilationBase: @@ -1532,8 +1535,7 @@ class CO2DataModel: exhalation rate and ventilation values that best match the measured CO2 concentrations. ''' data_registry: DataRegistry - room_capacity: int - room_volume: float + room: Room occupancy: IntPiecewiseConstant ventilation_transition_times: typing.Tuple[float, ...] times: typing.Sequence[float] @@ -1544,7 +1546,7 @@ def CO2_concentration_model(self, ventilation_values: typing.Tuple[float, ...]) -> CO2ConcentrationModel: return CO2ConcentrationModel( data_registry=self.data_registry, - room=Room(volume=self.room_volume), + room=Room(volume=self.room.volume), ventilation=CustomVentilation(PiecewiseConstant( self.ventilation_transition_times, ventilation_values)), CO2_emitters=SimplePopulation( @@ -1594,17 +1596,18 @@ def fun(x): the_predictive_CO2 = self.CO2_concentrations_from_params(the_CO2_concentration_model) # Ventilation in L/s - vent_volume_liter = [vent / 3600 * self.room_volume * 1000 + flow_rates_l_s = [vent / 3600 * self.room.volume * 1000 for vent in ventilation_values] # 1m^3 = 1000L # Ventilation in L/s/person - vent_volume_liter_person = [vent / self.room_capacity for vent in vent_volume_liter] + flow_rates_l_s_p = [flow_rate / self.room.capacity for flow_rate in flow_rates_l_s] return { "exhalation_rate": exhalation_rate, "ventilation_values": list(ventilation_values), - "ventilation_ls_values": vent_volume_liter, - "ventilation_lsp_values": vent_volume_liter_person, + "room_capacity": self.room.capacity, + "ventilation_ls_values": flow_rates_l_s, + "ventilation_lsp_values": flow_rates_l_s_p, 'predictive_CO2': list(the_predictive_CO2) } diff --git a/caimira/tests/models/test_fitting_algorithm.py b/caimira/tests/models/test_fitting_algorithm.py index 6dd275ae..d0027e18 100644 --- a/caimira/tests/models/test_fitting_algorithm.py +++ b/caimira/tests/models/test_fitting_algorithm.py @@ -40,8 +40,7 @@ def test_fitting_algorithm(data_registry, activity_type, ventilation_active, air # Generate CO2DataModel data_model = models.CO2DataModel( data_registry=data_registry, - room_capacity=2, - room_volume=75, + room=models.Room(volume=75, capacity=2), occupancy=models.IntPiecewiseConstant(transition_times=tuple( [8, 12, 13, 17]), values=tuple([2, 1, 2])), ventilation_transition_times=tuple(ventilation_active), From eb9e1dcee6647b3438c6f7e240662b905d286623 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Mon, 26 Aug 2024 15:51:18 +0200 Subject: [PATCH 08/14] removed restriction of total_people <= room_capacity --- caimira/apps/calculator/co2_model_generator.py | 2 -- caimira/apps/calculator/static/js/co2_form.js | 9 --------- 2 files changed, 11 deletions(-) diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index 85a41515..de04a666 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -69,8 +69,6 @@ def validate(self): raise TypeError(f'The room capacity should be a valid integer (> 0). Got {type(self.room_capacity)}.') if self.room_capacity <= 0: raise TypeError(f'The room capacity should be a valid integer (> 0). Got {self.room_capacity}.') - if self.room_capacity < self.total_people: - raise TypeError(f'The room capacity should be higher than the total people in the room. Got {self.room_capacity}.') # Validate specific inputs - breaks (exposed and infected) if self.specific_breaks != {}: diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index 46e98bd2..eb019c5b 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -253,15 +253,6 @@ function validateCO2Form() { ); submit = false; } - else if (roomCapacityNumber < totalPeopleNumber){ - insertErrorFor( - $referenceNode, - `'${roomCapacity.attr('name')}' must be higher than the total people.
    ` - ); - submit = false; - } - console.log(roomCapacityNumber) - console.log(totalPeopleNumber) } else { insertErrorFor( From 37c5deaedebfd603a4b6fbbafa368905ef01fa1f Mon Sep 17 00:00:00 2001 From: lrdossan Date: Mon, 26 Aug 2024 15:51:27 +0200 Subject: [PATCH 09/14] adjusted report page --- caimira/apps/templates/base/calculator.report.html.j2 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2 index c7f330d4..e53fdca8 100644 --- a/caimira/apps/templates/base/calculator.report.html.j2 +++ b/caimira/apps/templates/base/calculator.report.html.j2 @@ -539,10 +539,11 @@
  • HEPA amount: {{ form.hepa_amount }} m³ / hour

  • {% endif %} -
  • From Fitting: +

  • From fitting: {% if form.ventilation_type == "from_fitting" %} - Yes

  • - + Yes

    +
    • Room capacity: {{ form.CO2_fitting_result['room_capacity'] | int_format }}

    +
    From 4f984b201b4f6652cf5ba0214556803deb593cba Mon Sep 17 00:00:00 2001 From: lrdossan Date: Wed, 28 Aug 2024 10:47:38 +0200 Subject: [PATCH 10/14] added room_capacity as optional (and therefore results in l/s/p) --- caimira/apps/calculator/co2_model_generator.py | 13 +++++++------ caimira/apps/calculator/form_data.py | 8 ++++++++ caimira/models.py | 7 +++---- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index de04a666..e8a054ca 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -22,7 +22,7 @@ class CO2FormData(FormData): CO2_data: dict fitting_ventilation_states: list fitting_ventilation_type: str - room_capacity: int + 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. @@ -46,7 +46,7 @@ class CO2FormData(FormData): 'infected_lunch_start': '12:30', 'infected_people': 1, 'infected_start': '08:30', - 'room_capacity': 10, + 'room_capacity': None, 'room_volume': NO_DEFAULT, 'specific_breaks': '{}', 'total_people': NO_DEFAULT, @@ -65,10 +65,11 @@ def validate(self): self.validate_population_parameters() # Validate room capacity - if type(self.room_capacity) is not int: - raise TypeError(f'The room capacity should be a valid integer (> 0). Got {type(self.room_capacity)}.') - if self.room_capacity <= 0: - raise TypeError(f'The room capacity should be a valid integer (> 0). Got {self.room_capacity}.') + if self.room_capacity: + if type(self.room_capacity) is not int: + raise TypeError(f'The room capacity should be a valid integer (> 0). Got {type(self.room_capacity)}.') + if self.room_capacity <= 0: + raise TypeError(f'The room capacity should be a valid integer (> 0). Got {self.room_capacity}.') # Validate specific inputs - breaks (exposed and infected) if self.specific_breaks != {}: diff --git a/caimira/apps/calculator/form_data.py b/caimira/apps/calculator/form_data.py index a0a96b67..7f6c8914 100644 --- a/caimira/apps/calculator/form_data.py +++ b/caimira/apps/calculator/form_data.py @@ -410,6 +410,12 @@ def _safe_int_cast(value) -> int: return int(value) else: raise TypeError(f"Unable to safely cast {value} ({type(value)} type) to int") + + +def _safe_optional_int_cast(value) -> typing.Optional[int]: + if value is None or value == '': + return None + return _safe_int_cast(value) #: Mapping of field name to a callable which can convert values from form @@ -427,6 +433,8 @@ def cast_class_fields(cls): _CAST_RULES_NATIVE_TO_FORM_ARG[_field.name] = time_minutes_to_string elif _field.type is int: _CAST_RULES_FORM_ARG_TO_NATIVE[_field.name] = _safe_int_cast + elif _field.type is typing.Optional[int]: + _CAST_RULES_FORM_ARG_TO_NATIVE[_field.name] = _safe_optional_int_cast elif _field.type is float: _CAST_RULES_FORM_ARG_TO_NATIVE[_field.name] = float elif _field.type is bool: diff --git a/caimira/models.py b/caimira/models.py index 4c4f54a3..3fe58e22 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -218,7 +218,7 @@ class Room: humidity: _VectorisedFloat = 0.5 #: The maximum occupation of the room - design limit - capacity: float = 10 + capacity: typing.Optional[int] = None @dataclass(frozen=True) @@ -1596,11 +1596,10 @@ def fun(x): the_predictive_CO2 = self.CO2_concentrations_from_params(the_CO2_concentration_model) # Ventilation in L/s - flow_rates_l_s = [vent / 3600 * self.room.volume * 1000 - for vent in ventilation_values] # 1m^3 = 1000L + flow_rates_l_s = [vent / 3600 * self.room.volume * 1000 for vent in ventilation_values] # 1m^3 = 1000L # Ventilation in L/s/person - flow_rates_l_s_p = [flow_rate / self.room.capacity for flow_rate in flow_rates_l_s] + flow_rates_l_s_p = [flow_rate / self.room.capacity for flow_rate in flow_rates_l_s] if self.room.capacity else None return { "exhalation_rate": exhalation_rate, From 9743bf890107f14670ac15a9b522e23f06370587 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Wed, 28 Aug 2024 10:48:05 +0200 Subject: [PATCH 11/14] UI modifications for optional room_capacity input --- caimira/apps/calculator/static/js/co2_form.js | 37 +++++++++++-------- .../templates/base/calculator.form.html.j2 | 4 ++ .../templates/base/calculator.report.html.j2 | 9 +++-- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index eb019c5b..afcf7638 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -154,8 +154,8 @@ function validateFormInputs(obj) { const $referenceNode = $("#DIVCO2_data_dialog"); for (let i = 0; i < CO2_data_form.length; i++) { const $requiredElement = $(`[name=${CO2_data_form[i]}]`).first(); - if ($requiredElement.attr('name') !== "fitting_ventilation_states" && - $requiredElement.attr('name') !== "room_capacity" && + if ($requiredElement.attr('name') !== "fitting_ventilation_states" && + $requiredElement.attr('name') !== "room_capacity" && $requiredElement.val() === "") { insertErrorFor( $referenceNode, @@ -245,7 +245,6 @@ function validateCO2Form() { const roomCapacityVal = roomCapacity.val(); if (roomCapacityVal !== "") { const roomCapacityNumber = Number(roomCapacityVal); - const totalPeopleNumber = Number($("#total_people").val()); if (!Number.isInteger(roomCapacityNumber) || roomCapacityNumber <= 0) { insertErrorFor( $referenceNode, @@ -255,11 +254,7 @@ function validateCO2Form() { } } else { - insertErrorFor( - $referenceNode, - `'${roomCapacity.attr('name')}' must be defined.
    ` - ); - submit = false; + $fittingToSubmit.find("#warning_room_capacity_null").show(); } } @@ -286,6 +281,10 @@ function displayFittingData(json_response) { // Not needed for the form submission delete json_response["CO2_plot"]; delete json_response["predictive_CO2"]; + // Convert nulls to empty strings in the JSON response + if (json_response["room_capacity"] === null) json_response["room_capacity"] = ''; + if (json_response["ventilation_lsp_values"] === null) json_response["ventilation_lsp_values"] = ''; + // Populate the hidden input $("#CO2_fitting_result").val(JSON.stringify(json_response)); $("#exhalation_rate_fit").html( "Exhalation rate: " + @@ -295,10 +294,14 @@ function displayFittingData(json_response) { let ventilation_table = `
    - - - `; - json_response["ventilation_values"].forEach((CO2_val, index) => { + `; + // Check if ventilation_lsp_values is not empty + let hasLspValues = json_response['ventilation_lsp_values'] !== ''; + if (hasLspValues) { + ventilation_table += ``; + } + ventilation_table += ``; + json_response["ventilation_values"].forEach((CO2_val, index) => { let transition_times = displayTransitionTimesHourFormat( json_response["transition_times"][index], json_response["transition_times"][index + 1] @@ -307,9 +310,12 @@ function displayFittingData(json_response) { ventilation_table += ` - - - `; + `; + // Add the L/s/person value if available + if (hasLspValues) { + ventilation_table += ``; + } + ventilation_table += ``; }); $("#disable_fitting_algorithm").prop("disabled", false); @@ -419,6 +425,7 @@ function clearFittingResultComponent() { $referenceNode.find("#DIVCO2_fitting_result, #CO2_input_data_div").hide(); $referenceNode.find("#DIVCO2_fitting_to_submit").hide(); $referenceNode.find("#CO2_data_plot").attr("src", ""); + $referenceNode.find("#warning_room_capacity_null").hide(); // Update the ventilation scheme components $referenceNode.find("#fitting_ventilation_states, [name=fitting_ventilation_type]").prop( diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 9c74d295..90fbf6b0 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -370,6 +370,10 @@ ? + diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2 index e53fdca8..5f0edad0 100644 --- a/caimira/apps/templates/base/calculator.report.html.j2 +++ b/caimira/apps/templates/base/calculator.report.html.j2 @@ -542,13 +542,16 @@
  • From fitting: {% if form.ventilation_type == "from_fitting" %} Yes

    -
    • Room capacity: {{ form.CO2_fitting_result['room_capacity'] | int_format }}

  • + {% if form.CO2_fitting_result['room_capacity'] %} +
    • Room capacity: {{ form.CO2_fitting_result['room_capacity'] | int_format }}

    + {% endif %} +
    Time (HH:MM) ACH value (h⁻¹)
    Time (HH:MM) ACH value (h⁻¹)Flow rate (L/s)Flow rate (L/s/person)
    Flow rate (L/s)Flow rate (L/s/person)
    ${transition_times} ${CO2_val.toPrecision(2)}${json_response['ventilation_ls_values'][index].toPrecision(2)}${json_response['ventilation_lsp_values'][index].toPrecision(2)}
    ${json_response['ventilation_ls_values'][index].toPrecision(2)}${json_response['ventilation_lsp_values'][index].toPrecision(2)}
    - + {% if form.CO2_fitting_result['room_capacity'] %}{% endif %} {% for ventilation in form.CO2_fitting_result['ventilation_values'] %} {% set transition_time = form.CO2_fitting_result['transition_times'] %} @@ -556,7 +559,7 @@ - + {% if form.CO2_fitting_result['room_capacity'] %}{% endif %} {% endfor %}
    Time (HH:MM) ACH value (h⁻¹) Flow rate (L/s)Flow rate (L/s/person)Flow rate (L/s/person)
    {{ transition_time[loop.index - 1] | hour_format }} - {{ transition_time[loop.index] | hour_format }} {{ ventilation | float_format }} {{ form.CO2_fitting_result['ventilation_ls_values'][loop.index - 1] | float_format }} {{ form.CO2_fitting_result['ventilation_lsp_values'][loop.index - 1] | float_format }} {{ form.CO2_fitting_result['ventilation_lsp_values'][loop.index - 1] | float_format }}
    From adce41b52e25e7855a70abb6a0766f8d5507d644 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Wed, 28 Aug 2024 11:07:34 +0200 Subject: [PATCH 12/14] adapted tooltip message --- caimira/apps/templates/base/calculator.form.html.j2 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 90fbf6b0..59076fcc 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -366,13 +366,13 @@ Room data:
    -
    +
    ?
    From 9e3e66b4848b2c06eae953fdff60b7c762127760 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Wed, 28 Aug 2024 15:28:58 +0200 Subject: [PATCH 13/14] adapted tooltip and removed note on empty room capacity --- caimira/apps/calculator/static/js/co2_form.js | 4 ---- caimira/apps/templates/base/calculator.form.html.j2 | 6 +----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index afcf7638..77b0b756 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -253,9 +253,6 @@ function validateCO2Form() { submit = false; } } - else { - $fittingToSubmit.find("#warning_room_capacity_null").show(); - } } return submit; @@ -425,7 +422,6 @@ function clearFittingResultComponent() { $referenceNode.find("#DIVCO2_fitting_result, #CO2_input_data_div").hide(); $referenceNode.find("#DIVCO2_fitting_to_submit").hide(); $referenceNode.find("#CO2_data_plot").attr("src", ""); - $referenceNode.find("#warning_room_capacity_null").hide(); // Update the ventilation scheme components $referenceNode.find("#fitting_ventilation_states, [name=fitting_ventilation_type]").prop( diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 59076fcc..40fae7ad 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -366,14 +366,10 @@ Room data:
    -
    +
    ?
    -
    From fa0e7fab85056c09dcb4cf949ded64176c0b3640 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Fri, 30 Aug 2024 08:56:40 +0200 Subject: [PATCH 14/14] typo --- caimira/apps/templates/base/calculator.form.html.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 40fae7ad..ba423325 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -366,7 +366,7 @@ Room data:
    -
    +
    ?