diff --git a/cara/apps/calculator/README.md b/cara/apps/calculator/README.md
index 08fef2c9..8d9685af 100644
--- a/cara/apps/calculator/README.md
+++ b/cara/apps/calculator/README.md
@@ -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.
diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py
index ef0b479a..1974e6df 100644
--- a/cara/apps/calculator/model_generator.py
+++ b/cara/apps/calculator/model_generator.py
@@ -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:
diff --git a/cara/apps/calculator/static/images/window_type.PNG b/cara/apps/calculator/static/images/window_type.PNG
new file mode 100644
index 00000000..8d7d7405
Binary files /dev/null and b/cara/apps/calculator/static/images/window_type.PNG differ
diff --git a/cara/apps/calculator/static/js/form.js b/cara/apps/calculator/static/js/form.js
index 6203a677..fef4d621 100644
--- a/cara/apps/calculator/static/js/form.js
+++ b/cara/apps/calculator/static/js/form.js
@@ -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),
diff --git a/cara/apps/calculator/templates/calculator.form.html.j2 b/cara/apps/calculator/templates/calculator.form.html.j2
index bab459b2..cee8be34 100644
--- a/cara/apps/calculator/templates/calculator.form.html.j2
+++ b/cara/apps/calculator/templates/calculator.form.html.j2
@@ -79,6 +79,12 @@
-{% endblock main %}
\ No newline at end of file
+{% endblock main %}
diff --git a/cara/apps/calculator/templates/report.html.j2 b/cara/apps/calculator/templates/report.html.j2
index b95c94e0..ded74dbb 100644
--- a/cara/apps/calculator/templates/report.html.j2
+++ b/cara/apps/calculator/templates/report.html.j2
@@ -38,9 +38,9 @@
@@ -52,8 +52,15 @@
Yes
Number of windows: {{ form.windows_number }}
- Height of window: {{ form.window_height }}
- Opening distance: {{ form.opening_distance }}
+ Height of window: {{ form.window_height }} m
+ Window type:
+ {% if form.window_type == "hinged" %}
+ Top- or Bottom-Hung
+ Width of window: {{ form.window_width }} m
+ {% elif form.window_type == "sliding" %}
+ Sliding / Side-Hung
+ {% endif %}
+ Opening distance: {{ form.opening_distance }} m
Windows open: {{ form.windows_open }}
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.
@@ -63,7 +70,7 @@
HEPA Filtration: {{ 'Yes' if form.hepa_option else 'No' }}
{% if form.hepa_option %}
{% endif %}
diff --git a/cara/apps/expert.py b/cara/apps/expert.py
index 809f484a..3dbd634d 100644
--- a/cara/apps/expert.py
+++ b/cara/apps/expert.py
@@ -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,
diff --git a/cara/models.py b/cara/models.py
index 8fcf242b..4478c1ce 100644
--- a/cara/models.py
+++ b/cara/models.py
@@ -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)
diff --git a/cara/tests/apps/calculator/test_model_generator.py b/cara/tests/apps/calculator/test_model_generator.py
index 57779c6b..0b660798 100644
--- a/cara/tests/apps/calculator/test_model_generator.py
+++ b/cara/tests/apps/calculator/test_model_generator.py
@@ -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),
diff --git a/cara/tests/conftest.py b/cara/tests/conftest.py
index 59dbe826..24983cf1 100644
--- a/cara/tests/conftest.py
+++ b/cara/tests/conftest.py
@@ -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,
diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py
index ddceed02..042d9db0 100644
--- a/cara/tests/test_known_quantities.py
+++ b/cara/tests/test_known_quantities.py
@@ -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),)),
diff --git a/cara/tests/test_ventilation.py b/cara/tests/test_ventilation.py
index aaf64983..cfb5569c 100644
--- a/cara/tests/test_ventilation.py
+++ b/cara/tests/test_ventilation.py
@@ -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