Merge branch 'develop/hinged_window' into 'master'
Hinged windows Closes #95 See merge request cara/cara!109
This commit is contained in:
commit
b2b8938b9f
12 changed files with 251 additions and 71 deletions
|
|
@ -52,21 +52,24 @@ Please bear in mind that any of the two inputs only consider the supply of fresh
|
|||
Natural ventilation refers to rooms which have openable windows.
|
||||
There are many possibilities to calculate natural ventilation air flows, for simplification this tool assumes a single-sided natural ventilation scheme which is a conservative approach for the purpose of this tool.
|
||||
|
||||
Please choose the type of window (see illustration below):
|
||||
* Sliding or side-hung
|
||||
* Top- or bottom-hung
|
||||

|
||||
|
||||
Please enter the number, height and width and opening distance of the windows (in m).
|
||||
If there are multiple windows of different sizes, you should take an average.
|
||||
|
||||
The window opening distance (in m) is:
|
||||
|
||||
* In the case of windows that slide, the length the window is moved open.
|
||||
|
||||
* For articulated windows, it is the distance between the fixed frame and the movable glazed part when open. Window opening distance example (image of open window and measuring tape):
|
||||
* In the case of Sliding or Side-Hung option, the length the window is moved open
|
||||
* In case of Top- or Bottom-Hung, the distance between the fixed frame and the movable glazed part when open
|
||||
Window opening distance example (image of open window and measuring tape):
|
||||

|
||||
|
||||
|
||||
**Notes**: If you are unsure about the opening distance for the window, it is recommended to choose a conservative value (5 cms, 0.05m or 10cms, 0.10m).
|
||||
If you open the window at different distances throughout the day, choose an average value.
|
||||
|
||||
The width of the window is not currently used as an input to the model (height and opening distance is sufficient to calculate the free area).
|
||||
When using natural ventilation, the circulation of air is simulated as a function of the difference between the temperature inside the room and the outside air temperature. The average outdoor temperature for each hour of the day has been computed for every month of the year based on historical data for Geneva, Switzerland.
|
||||
It is therefore very important to enter the correct time and date in the event data section.
|
||||
Finally, you must specify when the windows are open - all the time (always), or for 10 minutes every 2 hours.
|
||||
|
|
|
|||
|
|
@ -44,13 +44,15 @@ class FormData:
|
|||
ventilation_type: str
|
||||
volume_type: str
|
||||
window_height: float
|
||||
window_type: str
|
||||
window_width: float
|
||||
windows_number: int
|
||||
windows_open: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, form_data: typing.Dict) -> "FormData":
|
||||
|
||||
valid_na_values = ['windows_open', 'mechanical_ventilation_type']
|
||||
valid_na_values = ['windows_open', 'window_type', 'mechanical_ventilation_type']
|
||||
for name in valid_na_values:
|
||||
if not form_data.get(name, ''):
|
||||
form_data[name] = 'not-applicable'
|
||||
|
|
@ -66,14 +68,20 @@ class FormData:
|
|||
('mask_wearing', MASK_WEARING),
|
||||
('ventilation_type', VENTILATION_TYPES),
|
||||
('volume_type', VOLUME_TYPES),
|
||||
('windows_open', WINDOWS_OPEN)]
|
||||
('windows_open', WINDOWS_OPEN),
|
||||
('window_type', WINDOWS_TYPES)]
|
||||
for key, valid_set in validation_tuples:
|
||||
if key not in form_data:
|
||||
raise ValueError(f"Missing key {key}")
|
||||
if form_data[key] not in valid_set:
|
||||
raise ValueError(f"{form_data[key]} is not a valid value for {key}")
|
||||
|
||||
# Don't let arbirtrary unescaped HTML through the net.
|
||||
if (form_data['ventilation_type'] == 'natural' and
|
||||
form_data['window_type'] == 'not-applicable'):
|
||||
raise ValueError("window_type cannot be ''not-applicable'' if "
|
||||
"ventilation_type is ''natural''")
|
||||
|
||||
# Don't let arbitrary unescaped HTML through the net.
|
||||
for key, value in form_data.items():
|
||||
if isinstance(value, str):
|
||||
form_data[key] = html.escape(value)
|
||||
|
|
@ -114,6 +122,8 @@ class FormData:
|
|||
ventilation_type=form_data['ventilation_type'],
|
||||
volume_type=form_data['volume_type'],
|
||||
window_height=float(form_data['window_height']),
|
||||
window_type=form_data['window_type'],
|
||||
window_width=float(form_data['window_width']),
|
||||
windows_number=int(form_data['windows_number']),
|
||||
windows_open=form_data['windows_open'],
|
||||
infected_start=time_string_to_minutes(form_data['infected_start']),
|
||||
|
|
@ -141,13 +151,24 @@ class FormData:
|
|||
inside_temp = models.PiecewiseConstant((0, 24), (293,))
|
||||
outside_temp = data.GenevaTemperatures[month]
|
||||
|
||||
ventilation = models.WindowOpening(
|
||||
active=window_interval,
|
||||
inside_temp=inside_temp, outside_temp=outside_temp, cd_b=0.6,
|
||||
window_height=self.window_height,
|
||||
opening_length=self.opening_distance,
|
||||
number_of_windows=self.windows_number,
|
||||
)
|
||||
if self.window_type == 'sliding':
|
||||
ventilation = models.SlidingWindow(
|
||||
active=window_interval,
|
||||
inside_temp=inside_temp, outside_temp=outside_temp,
|
||||
window_height=self.window_height,
|
||||
opening_length=self.opening_distance,
|
||||
number_of_windows=self.windows_number,
|
||||
)
|
||||
elif self.window_type == 'hinged':
|
||||
ventilation = models.HingedWindow(
|
||||
active=window_interval,
|
||||
inside_temp=inside_temp, outside_temp=outside_temp,
|
||||
window_height=self.window_height,
|
||||
window_width=self.window_width,
|
||||
opening_length=self.opening_distance,
|
||||
number_of_windows=self.windows_number,
|
||||
)
|
||||
|
||||
elif self.ventilation_type == "no-ventilation":
|
||||
ventilation = models.AirChange(active=always_on, air_exch=0.)
|
||||
else:
|
||||
|
|
@ -414,6 +435,8 @@ def baseline_raw_form_data():
|
|||
'ventilation_type': 'natural',
|
||||
'volume_type': 'room_volume',
|
||||
'window_height': '2',
|
||||
'window_type': 'sliding',
|
||||
'window_width': '2',
|
||||
'windows_number': '1',
|
||||
'windows_open': 'interval'
|
||||
}
|
||||
|
|
@ -427,6 +450,7 @@ MASK_WEARING = {'continuous', 'removed'}
|
|||
VENTILATION_TYPES = {'natural', 'mechanical', 'no-ventilation'}
|
||||
VOLUME_TYPES = {'room_volume', 'room_dimensions'}
|
||||
WINDOWS_OPEN = {'always', 'interval', 'breaks', 'not-applicable'}
|
||||
WINDOWS_TYPES = {'sliding', 'hinged', 'not-applicable'}
|
||||
|
||||
|
||||
def time_string_to_minutes(time: str) -> int:
|
||||
|
|
|
|||
BIN
cara/apps/calculator/static/images/window_type.PNG
Normal file
BIN
cara/apps/calculator/static/images/window_type.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
|
|
@ -35,6 +35,12 @@ function require_fields(obj) {
|
|||
require_mechanical_ventilation(false);
|
||||
require_natural_ventilation(true);
|
||||
break;
|
||||
case "window_sliding":
|
||||
require_window_width(false);
|
||||
break;
|
||||
case "window_hinged":
|
||||
require_window_width(true);
|
||||
break;
|
||||
case "air_type_changes":
|
||||
require_air_changes(true);
|
||||
require_air_supply(false);
|
||||
|
|
@ -89,14 +95,14 @@ function unrequire_fields(obj) {
|
|||
|
||||
function require_room_volume(option) {
|
||||
require_input_field("#room_volume", option);
|
||||
disable_input_field("#room_volume", option);
|
||||
disable_input_field("#room_volume", !option);
|
||||
}
|
||||
|
||||
function require_room_dimensions(option) {
|
||||
require_input_field("#floor_area", option);
|
||||
require_input_field("#ceiling_height", option);
|
||||
disable_input_field("#floor_area", option);
|
||||
disable_input_field("#ceiling_height", option);
|
||||
disable_input_field("#floor_area", !option);
|
||||
disable_input_field("#ceiling_height", !option);
|
||||
}
|
||||
|
||||
function require_mechanical_ventilation(option) {
|
||||
|
|
@ -112,28 +118,38 @@ function require_natural_ventilation(option) {
|
|||
require_input_field("#windows_number", option);
|
||||
require_input_field("#window_height", option);
|
||||
require_input_field("#opening_distance", option);
|
||||
$("#window_sliding").prop('required', option);
|
||||
$("#window_hinged").prop('required', option);
|
||||
$("#always").prop('required', option);
|
||||
$("#interval").prop('required', option);
|
||||
|
||||
$("#window_sliding").prop('checked', option);
|
||||
require_window_width(false);
|
||||
}
|
||||
|
||||
function require_window_width(option) {
|
||||
require_input_field("#window_width", option);
|
||||
disable_input_field("#window_width", !option);
|
||||
}
|
||||
|
||||
function require_air_changes(option) {
|
||||
require_input_field("#air_changes", option);
|
||||
disable_input_field("#air_changes", option);
|
||||
disable_input_field("#air_changes", !option);
|
||||
}
|
||||
|
||||
function require_air_supply(option) {
|
||||
require_input_field("#air_supply", option);
|
||||
disable_input_field("#air_supply", option);
|
||||
disable_input_field("#air_supply", !option);
|
||||
}
|
||||
|
||||
function require_single_event(option) {
|
||||
require_input_field("#single_event_date", option);
|
||||
disable_input_field("#single_event_date", option);
|
||||
disable_input_field("#single_event_date", !option);
|
||||
}
|
||||
|
||||
function require_recurrent_event(option) {
|
||||
$("#recurrent_event_month").prop('required', option);
|
||||
disable_input_field("#recurrent_event_month", option);
|
||||
disable_input_field("#recurrent_event_month", !option);
|
||||
}
|
||||
|
||||
function require_lunch(option) {
|
||||
|
|
@ -166,7 +182,7 @@ function require_mask(option) {
|
|||
|
||||
function require_hepa(option) {
|
||||
require_input_field("#hepa_amount", option);
|
||||
disable_input_field("#hepa_amount", option);
|
||||
disable_input_field("#hepa_amount", !option);
|
||||
}
|
||||
|
||||
function require_input_field(id, option) {
|
||||
|
|
@ -178,9 +194,9 @@ function require_input_field(id, option) {
|
|||
|
||||
function disable_input_field(id, option) {
|
||||
if (option)
|
||||
$(id).removeClass("disabled");
|
||||
else
|
||||
$(id).addClass("disabled");
|
||||
else
|
||||
$(id).removeClass("disabled");
|
||||
}
|
||||
|
||||
function setMaxInfectedPeople() {
|
||||
|
|
@ -266,6 +282,9 @@ function click_radio(id) {
|
|||
case "air_changes":
|
||||
$("#air_type_changes").click();
|
||||
break;
|
||||
case "window_width":
|
||||
$("#window_hinged").click();
|
||||
break;
|
||||
case "hepa_amount":
|
||||
$("#hepa_yes").click();
|
||||
break;
|
||||
|
|
@ -351,7 +370,7 @@ function validateValue(obj) {
|
|||
$(obj).removeClass("red_border");
|
||||
removeErrorFor(obj);
|
||||
|
||||
if (!isNonZeroOrEmpty($(obj).val())) {
|
||||
if (!isLessThanZeroOrEmpty($(obj).val())) {
|
||||
$(obj).addClass("red_border");
|
||||
insertErrorFor(obj, "Value must be > 0");
|
||||
return false;
|
||||
|
|
@ -359,9 +378,9 @@ function validateValue(obj) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function isNonZeroOrEmpty(value) {
|
||||
function isLessThanZeroOrEmpty(value) {
|
||||
if (value === "") return true;
|
||||
if (value == 0)
|
||||
if (value <= 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -472,6 +491,7 @@ $(document).ready(function () {
|
|||
require_fields($("input[name='lunch_option']:checked"));
|
||||
require_fields($("input[name='volume_type']:checked"));
|
||||
require_fields($("input[name='mechanical_ventilation_type']:checked"));
|
||||
require_fields($("input[name='window_type']:checked"));
|
||||
require_fields($("input[name='hepa_option']:checked"));
|
||||
|
||||
// Setup the maximum number of people at page load (to handle back/forward),
|
||||
|
|
|
|||
|
|
@ -79,6 +79,12 @@
|
|||
<div id="DIVnatural_ventilation" style="display:none">
|
||||
Number of windows: <input type="number" id="windows_number" class="non_zero" name="windows_number" min="1"><br>
|
||||
Height of window: <input type="number" step="any" id="window_height" class="non_zero" name="window_height" placeholder="meters" min="0"><br>
|
||||
Window type:
|
||||
<input type="radio" id="window_sliding" name="window_type" value="sliding" onclick="require_fields(this)" checked="checked">
|
||||
<label for="window_sliding">Sliding / Side-Hung</label>
|
||||
<input type="radio" id="window_hinged" name="window_type" value="hinged" onclick="require_fields(this)">
|
||||
<label for="window_hinged">Top- or Bottom-Hung</label> <br>
|
||||
Width of window: <input type="number" step="any" id="window_width" class="non_zero has_radio disabled" name="window_width" placeholder="meters" min="0"><br>
|
||||
Opening distance: <input type="number" step="any" id="opening_distance" class="non_zero" name="opening_distance" placeholder="meters" min="0"><br>
|
||||
Windows open: <input type="radio" id="always" name="windows_open" value="always">
|
||||
<label for="always">Always</label>
|
||||
|
|
@ -209,8 +215,8 @@
|
|||
This tool simulates the long range airborne spread SARS-CoV-2 virus in a finite volume and estimates the risk of COVID-19 infection. It is based on current scientific data and can be used to compare the effectiveness of different mitigation measures.<br>
|
||||
<b>Ventilation data:</b> <br>
|
||||
<ul>
|
||||
<li>Mechanical ventilation = check the rates with a specialist.</li>
|
||||
<li>Natural ventilation = the window opening distance is between the fixed frame and movable part when open (commonly used values are window height = 1.6m and window opening = 0.15m).</li>
|
||||
<li>Mechanical ventilation = check the flow rates with the concerned technical department.</li>
|
||||
<li>Natural ventilation = the type of window. The opening distance is between the fixed frame and movable part when open (commonly used values are window height = 1.6m and window opening = 0.15m).</li>
|
||||
<li>HEPA filtration = the air flow of the device. The following values are based on the different fan velocities of a specific commercial device proposed by the HSE Unit:</li>
|
||||
<ul>
|
||||
<li>Level 6 (max) = 430 m<sup>3</sup>/h (noisy)</li>
|
||||
|
|
@ -257,4 +263,4 @@
|
|||
<div class="text-component text-component-page clearfix"></div>
|
||||
<br>
|
||||
</div>
|
||||
{% endblock main %}
|
||||
{% endblock main %}
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@
|
|||
<ul>
|
||||
<li><p class="data_subtext">
|
||||
{% if form.mechanical_ventilation_type == "air_supply"%}
|
||||
Air supply flow rate: {{ form.air_supply }}
|
||||
Air supply flow rate: {{ form.air_supply }} m³ / hour
|
||||
{% elif form.mechanical_ventilation_type == "air_changes"%}
|
||||
Air changes per hour: {{ form.air_changes }}
|
||||
Air changes per hour: {{ form.air_changes }} h⁻¹
|
||||
{% endif %}
|
||||
</p></li>
|
||||
</ul>
|
||||
|
|
@ -52,8 +52,15 @@
|
|||
Yes </p></li>
|
||||
<ul>
|
||||
<li><p class="data_subtext">Number of windows: {{ form.windows_number }}</p></li>
|
||||
<li><p class="data_subtext">Height of window: {{ form.window_height }}</p></li>
|
||||
<li><p class="data_subtext">Opening distance: {{ form.opening_distance }}</p></li>
|
||||
<li><p class="data_subtext">Height of window: {{ form.window_height }} m</p></li>
|
||||
<li><p class="data_subtext">Window type:
|
||||
{% if form.window_type == "hinged" %}
|
||||
Top- or Bottom-Hung</p></li>
|
||||
<li><p class="data_subtext">Width of window: {{ form.window_width }} m</p></li>
|
||||
{% elif form.window_type == "sliding" %}
|
||||
Sliding / Side-Hung</p></li>
|
||||
{% endif %}
|
||||
<li><p class="data_subtext">Opening distance: {{ form.opening_distance }} m</p></li>
|
||||
<li><p class="data_subtext">Windows open: {{ form.windows_open }}</p></li>
|
||||
</ul>
|
||||
<p class="data_subtext data_italic">When using the natural ventilation option, air flows are calculated using averaged hourly temperatures for the Geneva region, based on historical data for the month selected.</p>
|
||||
|
|
@ -63,7 +70,7 @@
|
|||
<li><p class="data_text">HEPA Filtration: {{ 'Yes' if form.hepa_option else 'No' }}</li>
|
||||
{% if form.hepa_option %}
|
||||
<ul>
|
||||
<li><p class="data_text">HEPA amount: {{ form.hepa_amount }}</p></li>
|
||||
<li><p class="data_text">HEPA amount: {{ form.hepa_amount }} m³ / hour</p></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -454,11 +454,11 @@ class ModelWidgets(View):
|
|||
baseline_model = models.ExposureModel(
|
||||
concentration_model=models.ConcentrationModel(
|
||||
room=models.Room(volume=75),
|
||||
ventilation=models.WindowOpening(
|
||||
ventilation=models.SlidingWindow(
|
||||
active=models.PeriodicInterval(period=120, duration=15),
|
||||
inside_temp=models.PiecewiseConstant((0,24),(293.15,)),
|
||||
outside_temp=models.PiecewiseConstant((0,24),(283.15,)),
|
||||
cd_b=0.6, window_height=1.6, opening_length=0.6,
|
||||
window_height=1.6, opening_length=0.6,
|
||||
),
|
||||
infected=models.InfectedPopulation(
|
||||
number=1,
|
||||
|
|
|
|||
|
|
@ -189,22 +189,27 @@ class WindowOpening(Ventilation):
|
|||
#: The temperature outside of the window (Kelvin).
|
||||
outside_temp: PiecewiseConstant
|
||||
|
||||
#: The height of the window.
|
||||
#: The height of the window (m).
|
||||
window_height: float
|
||||
|
||||
#: The length of the opening-gap when the window is open
|
||||
#: The length of the opening-gap when the window is open (m).
|
||||
opening_length: float
|
||||
|
||||
#: The number of windows of the given dimensions.
|
||||
number_of_windows: int = 1
|
||||
|
||||
#: Discharge coefficient: what portion effective area is
|
||||
#: used to exchange air (0 <= cd_b <= 1)
|
||||
cd_b: float = 0.6
|
||||
|
||||
#: Minimum difference between inside and outside temperature
|
||||
#: Minimum difference between inside and outside temperature (K).
|
||||
min_deltaT: float = 0.1
|
||||
|
||||
@property
|
||||
def discharge_coefficient(self) -> float:
|
||||
"""
|
||||
Discharge coefficient (or cd_b): what portion effective area is
|
||||
used to exchange air (0 <= discharge_coefficient <= 1).
|
||||
To be implemented in subclasses.
|
||||
"""
|
||||
raise NotImplementedError("Unknown discharge coefficient")
|
||||
|
||||
def transition_times(self) -> typing.Set[float]:
|
||||
transitions = super().transition_times()
|
||||
transitions.update(self.inside_temp.transition_times)
|
||||
|
|
@ -228,7 +233,63 @@ class WindowOpening(Ventilation):
|
|||
temp_gradient = (inside_temp - outside_temp) / outside_temp
|
||||
root = np.sqrt(9.81 * self.window_height * temp_gradient)
|
||||
window_area = self.window_height * self.opening_length * self.number_of_windows
|
||||
return (3600 / (3 * room.volume)) * self.cd_b * window_area * root
|
||||
return (3600 / (3 * room.volume)) * self.discharge_coefficient * window_area * root
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SlidingWindow(WindowOpening):
|
||||
"""
|
||||
Sliding window, or side-hung window (with the hinge perpendicular to
|
||||
the horizontal plane).
|
||||
"""
|
||||
@property
|
||||
def discharge_coefficient(self) -> float:
|
||||
"""
|
||||
Average measured value of discharge coefficient for sliding or
|
||||
side-hung windows.
|
||||
"""
|
||||
return 0.6
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HingedWindow(WindowOpening):
|
||||
"""
|
||||
Top-hung or bottom-hung hinged window (with the hinge parallel to
|
||||
horizontal plane).
|
||||
"""
|
||||
#: Window width (m).
|
||||
window_width: float = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.window_width is None:
|
||||
raise ValueError('window_width must be set')
|
||||
|
||||
@property
|
||||
def discharge_coefficient(self) -> float:
|
||||
"""
|
||||
Simple model to compute discharge coefficient for top or bottom
|
||||
hung hinged windows, in the absence of empirical test results
|
||||
from manufacturers.
|
||||
From an excel spreadsheet calculator (Richard Daniels, Crawford
|
||||
Wright, Benjamin Jones - 2018) from the UK government -
|
||||
see Section 8.3 of BB101 and Section 11.3 of
|
||||
ESFA Output Specification Annex 2F on Ventilation opening areas.
|
||||
"""
|
||||
window_ratio = self.window_width / self.window_height
|
||||
if window_ratio < 0.5:
|
||||
M = 0.06
|
||||
cd_max = 0.612
|
||||
elif window_ratio < 1:
|
||||
M = 0.048
|
||||
cd_max = 0.589
|
||||
elif window_ratio < 2:
|
||||
M = 0.04
|
||||
cd_max = 0.563
|
||||
else:
|
||||
M = 0.038
|
||||
cd_max = 0.548
|
||||
window_angle = 2.*np.rad2deg(np.arcsin(self.opening_length/(2.*self.window_height)))
|
||||
return cd_max*(1-np.exp(-M*window_angle))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
|
|||
|
|
@ -30,16 +30,17 @@ def test_blend_expiration():
|
|||
assert r == expected
|
||||
|
||||
|
||||
def test_ventilation_window(baseline_form):
|
||||
def test_ventilation_slidingwindow(baseline_form):
|
||||
room = models.Room(75)
|
||||
window = models.WindowOpening(
|
||||
window = models.SlidingWindow(
|
||||
active=models.PeriodicInterval(period=120, duration=10),
|
||||
inside_temp=models.PiecewiseConstant((0, 24), (293,)),
|
||||
outside_temp=data.GenevaTemperatures['Dec'],
|
||||
cd_b=0.6, window_height=1.6, opening_length=0.6,
|
||||
window_height=1.6, opening_length=0.6,
|
||||
)
|
||||
baseline_form.ventilation_type = 'natural'
|
||||
baseline_form.windows_open = 'interval'
|
||||
baseline_form.window_type = 'sliding'
|
||||
baseline_form.event_type = 'recurrent_event'
|
||||
baseline_form.recurrent_event_month = 'December'
|
||||
baseline_form.window_height = 1.6
|
||||
|
|
@ -50,6 +51,28 @@ def test_ventilation_window(baseline_form):
|
|||
[baseline_form.ventilation().air_exchange(room, t) for t in ts])
|
||||
|
||||
|
||||
def test_ventilation_hingedwindow(baseline_form):
|
||||
room = models.Room(75)
|
||||
window = models.HingedWindow(
|
||||
active=models.PeriodicInterval(period=120, duration=10),
|
||||
inside_temp=models.PiecewiseConstant((0, 24), (293,)),
|
||||
outside_temp=data.GenevaTemperatures['Dec'],
|
||||
window_height=1.6, window_width=1., opening_length=0.6,
|
||||
)
|
||||
baseline_form.ventilation_type = 'natural'
|
||||
baseline_form.windows_open = 'interval'
|
||||
baseline_form.window_type = 'hinged'
|
||||
baseline_form.event_type = 'recurrent_event'
|
||||
baseline_form.recurrent_event_month = 'December'
|
||||
baseline_form.window_height = 1.6
|
||||
baseline_form.window_width = 1.
|
||||
baseline_form.opening_distance = 0.6
|
||||
|
||||
ts = np.linspace(8, 16, 100)
|
||||
np.testing.assert_allclose([window.air_exchange(room, t) for t in ts],
|
||||
[baseline_form.ventilation().air_exchange(room, t) for t in ts])
|
||||
|
||||
|
||||
def test_ventilation_mechanical(baseline_form):
|
||||
room = models.Room(75)
|
||||
mech = models.HVACMechanical(
|
||||
|
|
@ -82,11 +105,11 @@ def test_ventilation_airchanges(baseline_form):
|
|||
|
||||
def test_ventilation_window_hepa(baseline_form):
|
||||
room = models.Room(75)
|
||||
window = models.WindowOpening(
|
||||
window = models.SlidingWindow(
|
||||
active=models.PeriodicInterval(period=120, duration=10),
|
||||
inside_temp=models.PiecewiseConstant((0, 24), (293,)),
|
||||
outside_temp=data.GenevaTemperatures['Dec'],
|
||||
cd_b=0.6, window_height=1.6, opening_length=0.6,
|
||||
window_height=1.6, opening_length=0.6,
|
||||
)
|
||||
hepa = models.HEPAFilter(
|
||||
active=models.PeriodicInterval(period=120, duration=120),
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import pytest
|
|||
def baseline_model():
|
||||
model = models.ConcentrationModel(
|
||||
room=models.Room(volume=75),
|
||||
ventilation=models.WindowOpening(
|
||||
ventilation=models.SlidingWindow(
|
||||
active=models.PeriodicInterval(period=120, duration=120),
|
||||
inside_temp=models.PiecewiseConstant((0,24),(293,)),
|
||||
outside_temp=models.PiecewiseConstant((0,24),(283,)),
|
||||
cd_b=0.6, window_height=1.6, opening_length=0.6,
|
||||
window_height=1.6, opening_length=0.6,
|
||||
),
|
||||
infected=models.InfectedPopulation(
|
||||
number=1,
|
||||
|
|
|
|||
|
|
@ -26,11 +26,11 @@ def test_no_mask_emission_rate(baseline_model):
|
|||
|
||||
@pytest.fixture
|
||||
def baseline_periodic_window():
|
||||
return models.WindowOpening(
|
||||
return models.SlidingWindow(
|
||||
active=models.PeriodicInterval(period=120, duration=15),
|
||||
inside_temp=models.PiecewiseConstant((0,24),(293,)),
|
||||
outside_temp=models.PiecewiseConstant((0,24),(283,)),
|
||||
cd_b=0.6, window_height=1.6, opening_length=0.6,
|
||||
window_height=1.6, opening_length=0.6,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ def test_multiple_ventilation_HEPA_window(baseline_periodic_hepa, time, expected
|
|||
room = models.Room(volume=68.)
|
||||
tempOutside = models.PiecewiseConstant((0., 1., 2.5),(273.15, 283.15))
|
||||
tempInside = models.PiecewiseConstant((0., 24.),(293.15,))
|
||||
window = models.WindowOpening(active=models.SpecificInterval([(1 / 60, 24.)]),
|
||||
window = models.SlidingWindow(active=models.SpecificInterval([(1 / 60, 24.)]),
|
||||
inside_temp=tempInside,outside_temp=tempOutside,
|
||||
window_height=1.,opening_length=0.6)
|
||||
vent = models.MultipleVentilation([window, baseline_periodic_hepa])
|
||||
|
|
@ -146,7 +146,7 @@ def test_multiple_ventilation_HEPA_window(baseline_periodic_hepa, time, expected
|
|||
def test_multiple_ventilation_HEPA_window_transitions(baseline_periodic_hepa):
|
||||
tempOutside = models.PiecewiseConstant((0., 1., 2.5),(273.15, 283.15))
|
||||
tempInside = models.PiecewiseConstant((0., 24.),(293.15,))
|
||||
window = models.WindowOpening(active=models.SpecificInterval([(1 / 60, 24.)]),
|
||||
window = models.SlidingWindow(active=models.SpecificInterval([(1 / 60, 24.)]),
|
||||
inside_temp=tempInside,outside_temp=tempOutside,
|
||||
window_height=1.,opening_length=0.6)
|
||||
vent = models.MultipleVentilation([window, baseline_periodic_hepa])
|
||||
|
|
@ -264,7 +264,7 @@ def test_piecewiseconstant_transition_times():
|
|||
def test_windowopening(time, expected_value):
|
||||
tempOutside = models.PiecewiseConstant((0,10,24),(273.15,283.15))
|
||||
tempInside = models.PiecewiseConstant((0,24),(293.15,))
|
||||
w = models.WindowOpening(active=models.SpecificInterval([(0,24)]),
|
||||
w = models.SlidingWindow(active=models.SpecificInterval([(0,24)]),
|
||||
inside_temp=tempInside,outside_temp=tempOutside,
|
||||
window_height=1.,opening_length=0.6)
|
||||
npt.assert_allclose(w.air_exchange(models.Room(volume=68),time),
|
||||
|
|
@ -289,11 +289,11 @@ def build_hourly_dependent_model(month, intervals_open=((7.5, 8.5),),
|
|||
|
||||
model = models.ConcentrationModel(
|
||||
room=models.Room(volume=75),
|
||||
ventilation=models.WindowOpening(
|
||||
ventilation=models.SlidingWindow(
|
||||
active=models.SpecificInterval(intervals_open),
|
||||
inside_temp=models.PiecewiseConstant((0,24),(293,)),
|
||||
outside_temp=outside_temp,
|
||||
cd_b=0.6, window_height=1.6, opening_length=0.6,
|
||||
window_height=1.6, opening_length=0.6,
|
||||
),
|
||||
infected=models.InfectedPopulation(
|
||||
number=1,
|
||||
|
|
@ -310,11 +310,11 @@ def build_hourly_dependent_model(month, intervals_open=((7.5, 8.5),),
|
|||
def build_constant_temp_model(outside_temp, intervals_open=((7.5, 8.5),)):
|
||||
model = models.ConcentrationModel(
|
||||
room=models.Room(volume=75),
|
||||
ventilation=models.WindowOpening(
|
||||
ventilation=models.SlidingWindow(
|
||||
active=models.SpecificInterval(intervals_open),
|
||||
inside_temp=models.PiecewiseConstant((0,24),(293,)),
|
||||
outside_temp=models.PiecewiseConstant((0,24),(outside_temp,)),
|
||||
cd_b=0.6, window_height=1.6, opening_length=0.6,
|
||||
window_height=1.6, opening_length=0.6,
|
||||
),
|
||||
infected=models.InfectedPopulation(
|
||||
number=1,
|
||||
|
|
@ -330,11 +330,11 @@ def build_constant_temp_model(outside_temp, intervals_open=((7.5, 8.5),)):
|
|||
|
||||
def build_hourly_dependent_model_multipleventilation(month, intervals_open=((7.5, 8.5),)):
|
||||
vent = models.MultipleVentilation((
|
||||
models.WindowOpening(
|
||||
models.SlidingWindow(
|
||||
active=models.SpecificInterval(intervals_open),
|
||||
inside_temp=models.PiecewiseConstant((0,24),(293,)),
|
||||
outside_temp=data.GenevaTemperatures[month],
|
||||
cd_b=0.6, window_height=1.6, opening_length=0.6,
|
||||
window_height=1.6, opening_length=0.6,
|
||||
),
|
||||
models.HEPAFilter(
|
||||
active=models.SpecificInterval(((0,24),)),
|
||||
|
|
|
|||
|
|
@ -1,25 +1,61 @@
|
|||
import dataclasses
|
||||
|
||||
import pytest
|
||||
import numpy.testing as npt
|
||||
|
||||
from cara import models
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def baseline_window():
|
||||
return models.WindowOpening(
|
||||
def baseline_slidingwindow():
|
||||
return models.SlidingWindow(
|
||||
active=models.SpecificInterval(((0, 4), (5, 9))),
|
||||
inside_temp=models.PiecewiseConstant((0, 24), (293,)),
|
||||
outside_temp=models.PiecewiseConstant((0, 24), (283,)),
|
||||
cd_b=0.6, window_height=1.6, opening_length=0.6,
|
||||
window_height=1.6, opening_length=0.6,
|
||||
)
|
||||
|
||||
|
||||
def test_number_of_windows(baseline_window):
|
||||
room = models.Room(75)
|
||||
two_windows = dataclasses.replace(baseline_window, number_of_windows=2)
|
||||
@pytest.fixture
|
||||
def baseline_hingedwindow():
|
||||
return models.HingedWindow(
|
||||
active=models.SpecificInterval(((0, 4), (5, 9))),
|
||||
inside_temp=models.PiecewiseConstant((0, 24), (293,)),
|
||||
outside_temp=models.PiecewiseConstant((0, 24), (283,)),
|
||||
window_height=1.6, opening_length=0.6, window_width=1.,
|
||||
)
|
||||
|
||||
one_window_exchange = baseline_window.air_exchange(room, 1)
|
||||
|
||||
def test_number_of_windows(baseline_slidingwindow):
|
||||
room = models.Room(75)
|
||||
two_windows = dataclasses.replace(baseline_slidingwindow, number_of_windows=2)
|
||||
|
||||
one_window_exchange = baseline_slidingwindow.air_exchange(room, 1)
|
||||
two_window_exchange = two_windows.air_exchange(room, 1)
|
||||
assert one_window_exchange != 0
|
||||
assert one_window_exchange * 2 == two_window_exchange
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"window_width, expected_discharge_coefficient",
|
||||
[
|
||||
[0.5, 0.447],
|
||||
[1., 0.379],
|
||||
[2., 0.328],
|
||||
[4., 0.308],
|
||||
],
|
||||
)
|
||||
def test_hinged_window(baseline_hingedwindow,window_width,
|
||||
expected_discharge_coefficient):
|
||||
room = models.Room(75)
|
||||
hinged_window = dataclasses.replace(baseline_hingedwindow,
|
||||
window_width=window_width)
|
||||
|
||||
npt.assert_allclose(hinged_window.discharge_coefficient,
|
||||
expected_discharge_coefficient, rtol=1e-2)
|
||||
|
||||
|
||||
def test_sliding_window(baseline_slidingwindow):
|
||||
room = models.Room(75)
|
||||
|
||||
assert baseline_slidingwindow.discharge_coefficient == 0.6
|
||||
|
|
|
|||
Loading…
Reference in a new issue