Merge branch 'feature/ACH_to_L_s_person' into 'master'
Fitting results: added flow rate data (l/s/p) Closes #416 See merge request caimira/caimira!504
This commit is contained in:
commit
dbdc6b5760
7 changed files with 152 additions and 45 deletions
|
|
@ -22,6 +22,7 @@ class CO2FormData(FormData):
|
||||||
CO2_data: dict
|
CO2_data: dict
|
||||||
fitting_ventilation_states: list
|
fitting_ventilation_states: list
|
||||||
fitting_ventilation_type: str
|
fitting_ventilation_type: str
|
||||||
|
room_capacity: typing.Optional[int]
|
||||||
|
|
||||||
#: The default values for undefined fields. Note that the defaults here
|
#: The default values for undefined fields. Note that the defaults here
|
||||||
#: and the defaults in the html form must not be contradictory.
|
#: and the defaults in the html form must not be contradictory.
|
||||||
|
|
@ -45,6 +46,7 @@ class CO2FormData(FormData):
|
||||||
'infected_lunch_start': '12:30',
|
'infected_lunch_start': '12:30',
|
||||||
'infected_people': 1,
|
'infected_people': 1,
|
||||||
'infected_start': '08:30',
|
'infected_start': '08:30',
|
||||||
|
'room_capacity': None,
|
||||||
'room_volume': NO_DEFAULT,
|
'room_volume': NO_DEFAULT,
|
||||||
'specific_breaks': '{}',
|
'specific_breaks': '{}',
|
||||||
'total_people': NO_DEFAULT,
|
'total_people': NO_DEFAULT,
|
||||||
|
|
@ -62,6 +64,13 @@ class CO2FormData(FormData):
|
||||||
# Validate population parameters
|
# Validate population parameters
|
||||||
self.validate_population_parameters()
|
self.validate_population_parameters()
|
||||||
|
|
||||||
|
# Validate 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)
|
# Validate specific inputs - breaks (exposed and infected)
|
||||||
if self.specific_breaks != {}:
|
if self.specific_breaks != {}:
|
||||||
if type(self.specific_breaks) is not dict:
|
if type(self.specific_breaks) is not dict:
|
||||||
|
|
@ -181,9 +190,8 @@ class CO2FormData(FormData):
|
||||||
|
|
||||||
return models.CO2DataModel(
|
return models.CO2DataModel(
|
||||||
data_registry=self.data_registry,
|
data_registry=self.data_registry,
|
||||||
room_volume=self.room_volume,
|
room=models.Room(volume=self.room_volume, capacity=self.room_capacity),
|
||||||
number=models.IntPiecewiseConstant(transition_times=tuple(all_state_changes), values=tuple(total_people)),
|
occupancy=models.IntPiecewiseConstant(transition_times=tuple(all_state_changes), values=tuple(total_people)),
|
||||||
presence=None,
|
|
||||||
ventilation_transition_times=self.ventilation_transition_times(),
|
ventilation_transition_times=self.ventilation_transition_times(),
|
||||||
times=self.CO2_data['times'],
|
times=self.CO2_data['times'],
|
||||||
CO2_concentrations=self.CO2_data['CO2'],
|
CO2_concentrations=self.CO2_data['CO2'],
|
||||||
|
|
|
||||||
|
|
@ -410,6 +410,12 @@ def _safe_int_cast(value) -> int:
|
||||||
return int(value)
|
return int(value)
|
||||||
else:
|
else:
|
||||||
raise TypeError(f"Unable to safely cast {value} ({type(value)} type) to int")
|
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
|
#: 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
|
_CAST_RULES_NATIVE_TO_FORM_ARG[_field.name] = time_minutes_to_string
|
||||||
elif _field.type is int:
|
elif _field.type is int:
|
||||||
_CAST_RULES_FORM_ARG_TO_NATIVE[_field.name] = _safe_int_cast
|
_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:
|
elif _field.type is float:
|
||||||
_CAST_RULES_FORM_ARG_TO_NATIVE[_field.name] = float
|
_CAST_RULES_FORM_ARG_TO_NATIVE[_field.name] = float
|
||||||
elif _field.type is bool:
|
elif _field.type is bool:
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const CO2_data_form = [
|
||||||
"infected_lunch_start",
|
"infected_lunch_start",
|
||||||
"infected_people",
|
"infected_people",
|
||||||
"infected_start",
|
"infected_start",
|
||||||
|
"room_capacity",
|
||||||
"room_volume",
|
"room_volume",
|
||||||
"specific_breaks",
|
"specific_breaks",
|
||||||
"total_people",
|
"total_people",
|
||||||
|
|
@ -137,6 +138,7 @@ function generateJSONStructure(endpoint, jsonData) {
|
||||||
$("#generate_fitting_data").prop("disabled", false);
|
$("#generate_fitting_data").prop("disabled", false);
|
||||||
$("#fitting_ventilation_states").prop("disabled", false);
|
$("#fitting_ventilation_states").prop("disabled", false);
|
||||||
$("[name=fitting_ventilation_type]").prop("disabled", false);
|
$("[name=fitting_ventilation_type]").prop("disabled", false);
|
||||||
|
$("#room_capacity").prop("disabled", false);
|
||||||
plotCO2Data(endpoint);
|
plotCO2Data(endpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -152,7 +154,9 @@ function validateFormInputs(obj) {
|
||||||
const $referenceNode = $("#DIVCO2_data_dialog");
|
const $referenceNode = $("#DIVCO2_data_dialog");
|
||||||
for (let i = 0; i < CO2_data_form.length; i++) {
|
for (let i = 0; i < CO2_data_form.length; i++) {
|
||||||
const $requiredElement = $(`[name=${CO2_data_form[i]}]`).first();
|
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(
|
insertErrorFor(
|
||||||
$referenceNode,
|
$referenceNode,
|
||||||
`'${$requiredElement.attr('name')}' must be defined.<br />`
|
`'${$requiredElement.attr('name')}' must be defined.<br />`
|
||||||
|
|
@ -236,6 +240,19 @@ function validateCO2Form() {
|
||||||
);
|
);
|
||||||
submit = false;
|
submit = false;
|
||||||
}
|
}
|
||||||
|
// Validate room capacity
|
||||||
|
const roomCapacity = $fittingToSubmit.find("input[name=room_capacity]");
|
||||||
|
const roomCapacityVal = roomCapacity.val();
|
||||||
|
if (roomCapacityVal !== "") {
|
||||||
|
const roomCapacityNumber = Number(roomCapacityVal);
|
||||||
|
if (!Number.isInteger(roomCapacityNumber) || roomCapacityNumber <= 0) {
|
||||||
|
insertErrorFor(
|
||||||
|
$referenceNode,
|
||||||
|
`'${roomCapacity.attr('name')}' must be a valid integer (> 0).</br>`
|
||||||
|
);
|
||||||
|
submit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return submit;
|
return submit;
|
||||||
|
|
@ -261,23 +278,43 @@ function displayFittingData(json_response) {
|
||||||
// Not needed for the form submission
|
// Not needed for the form submission
|
||||||
delete json_response["CO2_plot"];
|
delete json_response["CO2_plot"];
|
||||||
delete json_response["predictive_CO2"];
|
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));
|
$("#CO2_fitting_result").val(JSON.stringify(json_response));
|
||||||
$("#exhalation_rate_fit").html(
|
$("#exhalation_rate_fit").html(
|
||||||
"Exhalation rate: " +
|
"Exhalation rate: " +
|
||||||
String(json_response["exhalation_rate"].toFixed(2)) +
|
String(json_response["exhalation_rate"].toFixed(2)) +
|
||||||
" m³/h"
|
" m³/h"
|
||||||
);
|
);
|
||||||
let ventilation_table =
|
let ventilation_table = `<tr>
|
||||||
"<tr><th>Time (HH:MM)</th><th>ACH value (h⁻¹)</th></tr>";
|
<th>Time (HH:MM)</th>
|
||||||
json_response["ventilation_values"].forEach((val, index) => {
|
<th>ACH value (h⁻¹)</th>
|
||||||
|
<th>Flow rate (L/s)</th>`;
|
||||||
|
// Check if ventilation_lsp_values is not empty
|
||||||
|
let hasLspValues = json_response['ventilation_lsp_values'] !== '';
|
||||||
|
if (hasLspValues) {
|
||||||
|
ventilation_table += `<th>Flow rate (L/s/person)</th>`;
|
||||||
|
}
|
||||||
|
ventilation_table += `</tr>`;
|
||||||
|
json_response["ventilation_values"].forEach((CO2_val, index) => {
|
||||||
let transition_times = displayTransitionTimesHourFormat(
|
let transition_times = displayTransitionTimesHourFormat(
|
||||||
json_response["transition_times"][index],
|
json_response["transition_times"][index],
|
||||||
json_response["transition_times"][index + 1]
|
json_response["transition_times"][index + 1]
|
||||||
);
|
);
|
||||||
ventilation_table += `<tr><td>${transition_times}</td><td>${val.toPrecision(
|
|
||||||
2
|
ventilation_table += `<tr>
|
||||||
)}</td></tr>`;
|
<td>${transition_times}</td>
|
||||||
|
<td>${CO2_val.toPrecision(2)}</td>
|
||||||
|
<td>${json_response['ventilation_ls_values'][index].toPrecision(2)}</td>`;
|
||||||
|
// Add the L/s/person value if available
|
||||||
|
if (hasLspValues) {
|
||||||
|
ventilation_table += `<td>${json_response['ventilation_lsp_values'][index].toPrecision(2)}</td>`;
|
||||||
|
}
|
||||||
|
ventilation_table += `</tr>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#disable_fitting_algorithm").prop("disabled", false);
|
$("#disable_fitting_algorithm").prop("disabled", false);
|
||||||
$("#ventilation_rate_fit").html(ventilation_table);
|
$("#ventilation_rate_fit").html(ventilation_table);
|
||||||
$("#generate_fitting_data").html("Fit data");
|
$("#generate_fitting_data").html("Fit data");
|
||||||
|
|
@ -337,6 +374,11 @@ function submitFittingAlgorithm(url) {
|
||||||
"disabled",
|
"disabled",
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
// Disable room capacity input
|
||||||
|
$("#room_capacity").prop(
|
||||||
|
"disabled",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Prepare data for submission
|
// Prepare data for submission
|
||||||
const CO2_mapping = formatCO2DataForm(CO2_data_form);
|
const CO2_mapping = formatCO2DataForm(CO2_data_form);
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,14 @@
|
||||||
</div>
|
</div>
|
||||||
<input type="text" class="form-control" id="fitting_ventilation_states" name="fitting_ventilation_states" placeholder="e.g. [8.5, 10, 11.5, 17]" form="not-submitted"><br>
|
<input type="text" class="form-control" id="fitting_ventilation_states" name="fitting_ventilation_states" placeholder="e.g. [8.5, 10, 11.5, 17]" form="not-submitted"><br>
|
||||||
</div>
|
</div>
|
||||||
|
<strong>Room data:</strong>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="room_capacity">Maximum occupation – design limit:</label>
|
||||||
|
<div data-tooltip="The maximum number of occupants foreseen by the conceptual (architectural) design of the room, also known as the room capacity. It is only used by the CO2 fitting algorithm to convert the ventilation rate obtained in L/s/person. If not specified, this conversion will not be performed.">
|
||||||
|
<span class="tooltip_text">?</span>
|
||||||
|
</div>
|
||||||
|
<input type="number" id="room_capacity" class="form-control col-sm-7" name="room_capacity" placeholder="Number" min=1 form="not-submitted">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="DIVCO2_fitting_result" style="display: none">
|
<div id="DIVCO2_fitting_result" style="display: none">
|
||||||
|
|
@ -380,7 +388,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div></br>
|
</div></br>
|
||||||
<div class='sub_title'>HEPA filtration:</div>
|
<div class='sub_title'>HEPA filtration:</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id="hepa_yes" name="hepa_option" value=1 onclick="require_fields(this)" data-enables="#DIVhepa_amount">
|
<input type="radio" id="hepa_yes" name="hepa_option" value=1 onclick="require_fields(this)" data-enables="#DIVhepa_amount">
|
||||||
|
|
|
||||||
|
|
@ -539,16 +539,27 @@
|
||||||
<li><p class="data_text">HEPA amount: {{ form.hepa_amount }} m³ / hour</p></li>
|
<li><p class="data_text">HEPA amount: {{ form.hepa_amount }} m³ / hour</p></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><p class="data_text">From Fitting:
|
<li><p class="data_text">From fitting:
|
||||||
{% if form.ventilation_type == "from_fitting" %}
|
{% if form.ventilation_type == "from_fitting" %}
|
||||||
Yes
|
Yes</p>
|
||||||
<table class="w-25 mt-3 ml-4" border="1">
|
{% if form.CO2_fitting_result['room_capacity'] %}
|
||||||
<tr><th> Time (HH:MM)</th><th>ACH value (h⁻¹)</th></tr>
|
<ul><li><p class="data_text">Room capacity: {{ form.CO2_fitting_result['room_capacity'] | int_format }}</p></li></ul>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
<table class="w-50 mt-3 ml-4" border="1">
|
||||||
|
<tr>
|
||||||
|
<th> Time (HH:MM)</th>
|
||||||
|
<th>ACH value (h⁻¹)</th>
|
||||||
|
<th>Flow rate (L/s)</th>
|
||||||
|
{% if form.CO2_fitting_result['room_capacity'] %}<th>Flow rate (L/s/person)</th>{% endif %}
|
||||||
|
</tr>
|
||||||
{% for ventilation in form.CO2_fitting_result['ventilation_values'] %}
|
{% for ventilation in form.CO2_fitting_result['ventilation_values'] %}
|
||||||
{% set transition_time = form.CO2_fitting_result['transition_times'] %}
|
{% set transition_time = form.CO2_fitting_result['transition_times'] %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ transition_time[loop.index - 1] | hour_format }} - {{ transition_time[loop.index] | hour_format }}</td>
|
<td>{{ transition_time[loop.index - 1] | hour_format }} - {{ transition_time[loop.index] | hour_format }}</td>
|
||||||
<td>{{ ventilation | float_format }} </td>
|
<td>{{ ventilation | float_format }} </td>
|
||||||
|
<td>{{ form.CO2_fitting_result['ventilation_ls_values'][loop.index - 1] | float_format }} </td>
|
||||||
|
{% if form.CO2_fitting_result['room_capacity'] %}<td>{{ form.CO2_fitting_result['ventilation_lsp_values'][loop.index - 1] | float_format }} </td>{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,9 @@ class Room:
|
||||||
#: The humidity in the room (from 0 to 1 - e.g. 0.5 is 50% humidity)
|
#: The humidity in the room (from 0 to 1 - e.g. 0.5 is 50% humidity)
|
||||||
humidity: _VectorisedFloat = 0.5
|
humidity: _VectorisedFloat = 0.5
|
||||||
|
|
||||||
|
#: The maximum occupation of the room - design limit
|
||||||
|
capacity: typing.Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class _VentilationBase:
|
class _VentilationBase:
|
||||||
|
|
@ -1526,34 +1529,37 @@ class ShortRangeModel:
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CO2DataModel:
|
class CO2DataModel:
|
||||||
'''
|
'''
|
||||||
The CO2DataModel class models CO2 data based on room volume, ventilation transition times, and people presence.
|
The CO2DataModel class models CO2 data based on room volume and capacity,
|
||||||
It uses optimization techniques to fit the model's parameters and estimate the exhalation rate and ventilation
|
ventilation transition times, and people presence.
|
||||||
values that best match the measured CO2 concentrations.
|
It uses optimization techniques to fit the model's parameters and estimate the
|
||||||
|
exhalation rate and ventilation values that best match the measured CO2 concentrations.
|
||||||
'''
|
'''
|
||||||
data_registry: DataRegistry
|
data_registry: DataRegistry
|
||||||
room_volume: float
|
room: Room
|
||||||
number: typing.Union[int, IntPiecewiseConstant]
|
occupancy: IntPiecewiseConstant
|
||||||
presence: typing.Optional[Interval]
|
|
||||||
ventilation_transition_times: typing.Tuple[float, ...]
|
ventilation_transition_times: typing.Tuple[float, ...]
|
||||||
times: typing.Sequence[float]
|
times: typing.Sequence[float]
|
||||||
CO2_concentrations: typing.Sequence[float]
|
CO2_concentrations: typing.Sequence[float]
|
||||||
|
|
||||||
def CO2_concentrations_from_params(self,
|
def CO2_concentration_model(self,
|
||||||
exhalation_rate: float,
|
exhalation_rate: float,
|
||||||
ventilation_values: typing.Tuple[float, ...]) -> typing.List[_VectorisedFloat]:
|
ventilation_values: typing.Tuple[float, ...]) -> CO2ConcentrationModel:
|
||||||
CO2_concentrations = CO2ConcentrationModel(
|
return CO2ConcentrationModel(
|
||||||
data_registry=self.data_registry,
|
data_registry=self.data_registry,
|
||||||
room=Room(volume=self.room_volume),
|
room=Room(volume=self.room.volume),
|
||||||
ventilation=CustomVentilation(PiecewiseConstant(
|
ventilation=CustomVentilation(PiecewiseConstant(
|
||||||
self.ventilation_transition_times, ventilation_values)),
|
self.ventilation_transition_times, ventilation_values)),
|
||||||
CO2_emitters=SimplePopulation(
|
CO2_emitters=SimplePopulation(
|
||||||
number=self.number,
|
number=self.occupancy,
|
||||||
presence=self.presence,
|
presence=None,
|
||||||
activity=Activity(
|
activity=Activity(
|
||||||
exhalation_rate=exhalation_rate, inhalation_rate=exhalation_rate),
|
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):
|
def CO2_fit_params(self):
|
||||||
if len(self.times) != len(self.CO2_concentrations):
|
if len(self.times) != len(self.CO2_concentrations):
|
||||||
|
|
@ -1566,10 +1572,11 @@ class CO2DataModel:
|
||||||
def fun(x):
|
def fun(x):
|
||||||
exhalation_rate = x[0]
|
exhalation_rate = x[0]
|
||||||
ventilation_values = tuple(x[1:])
|
ventilation_values = tuple(x[1:])
|
||||||
the_concentrations = self.CO2_concentrations_from_params(
|
CO2_concentration_model = self.CO2_concentration_model(
|
||||||
exhalation_rate=exhalation_rate,
|
exhalation_rate=exhalation_rate,
|
||||||
ventilation_values=ventilation_values
|
ventilation_values=ventilation_values
|
||||||
)
|
)
|
||||||
|
the_concentrations = self.CO2_concentrations_from_params(CO2_concentration_model)
|
||||||
return np.sqrt(np.sum((np.array(self.CO2_concentrations) -
|
return np.sqrt(np.sum((np.array(self.CO2_concentrations) -
|
||||||
np.array(the_concentrations))**2))
|
np.array(the_concentrations))**2))
|
||||||
# The goal is to minimize the difference between the two different curves (known concentrations vs. predicted concentrations)
|
# The goal is to minimize the difference between the two different curves (known concentrations vs. predicted concentrations)
|
||||||
|
|
@ -1577,10 +1584,31 @@ class CO2DataModel:
|
||||||
bounds=[(0, None) for _ in range(len(self.ventilation_transition_times))],
|
bounds=[(0, None) for _ in range(len(self.ventilation_transition_times))],
|
||||||
options={'xtol': 1e-3})
|
options={'xtol': 1e-3})
|
||||||
|
|
||||||
|
# Final prediction
|
||||||
exhalation_rate = res_dict['x'][0]
|
exhalation_rate = res_dict['x'][0]
|
||||||
ventilation_values = res_dict['x'][1:]
|
ventilation_values = res_dict['x'][1:] # In ACH
|
||||||
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)}
|
# 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
|
||||||
|
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] if self.room.capacity else None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"exhalation_rate": exhalation_rate,
|
||||||
|
"ventilation_values": list(ventilation_values),
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,17 @@ from caimira import models
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"activity_type, ventilation_active, air_exch", [
|
"activity_type, ventilation_active, air_exch, flow_rate_lsp", [
|
||||||
['Seated', [8, 12, 13, 17], [0.25, 2.45, 0.25]],
|
['Seated', [8, 12, 13, 17], [0.25, 2.45, 0.25], [2.604166667, 25.520833335, 2.604166667]],
|
||||||
['Standing', [8, 10, 11, 12, 17], [1.25, 3.25, 1.45, 0.25]],
|
['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]],
|
['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]],
|
['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]],
|
['Heavy exercise', [8, 17], [0.25], [2.6041666667]],
|
||||||
['Seated', [8, 17], [0.25]],
|
['Seated', [8, 17], [0.25], [2.6041666667]],
|
||||||
['Standing', [8, 17], [2.45]],
|
['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(
|
conc_model = models.CO2ConcentrationModel(
|
||||||
data_registry = data_registry,
|
data_registry = data_registry,
|
||||||
room=models.Room(
|
room=models.Room(
|
||||||
|
|
@ -40,10 +40,9 @@ def test_fitting_algorithm(data_registry, activity_type, ventilation_active, air
|
||||||
# Generate CO2DataModel
|
# Generate CO2DataModel
|
||||||
data_model = models.CO2DataModel(
|
data_model = models.CO2DataModel(
|
||||||
data_registry=data_registry,
|
data_registry=data_registry,
|
||||||
room_volume=75,
|
room=models.Room(volume=75, capacity=2),
|
||||||
number=models.IntPiecewiseConstant(transition_times=tuple(
|
occupancy=models.IntPiecewiseConstant(transition_times=tuple(
|
||||||
[8, 12, 13, 17]), values=tuple([2, 1, 2])),
|
[8, 12, 13, 17]), values=tuple([2, 1, 2])),
|
||||||
presence=None,
|
|
||||||
ventilation_transition_times=tuple(ventilation_active),
|
ventilation_transition_times=tuple(ventilation_active),
|
||||||
times=times,
|
times=times,
|
||||||
CO2_concentrations=CO2_concentrations
|
CO2_concentrations=CO2_concentrations
|
||||||
|
|
@ -56,4 +55,7 @@ def test_fitting_algorithm(data_registry, activity_type, ventilation_active, air
|
||||||
|
|
||||||
ventilation_values = fit_parameters['ventilation_values']
|
ventilation_values = fit_parameters['ventilation_values']
|
||||||
npt.assert_allclose(ventilation_values, air_exch, rtol=1e-2)
|
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)
|
||||||
|
|
||||||
Loading…
Reference in a new issue