Merge branch 'master' of https://gitlab.cern.ch/cara/cara into develop/expert_app_ventilation_temp

This commit is contained in:
Nicolas Mounet 2020-11-10 09:19:11 +01:00
commit f348a0c7c5
12 changed files with 218 additions and 90 deletions

View file

@ -2,6 +2,8 @@
This is a guide to help you use the calculator tool.
If you are using the expert version of the tool, you should look at the expert notes.
Please bear in mind that this beta version is for an extensive testing of the functionality of the tool and analyse the results.
At this stage, do not use the results as final output of the workplace risk assessment.
## Disclaimer
@ -29,48 +31,56 @@ We do not assume responsibility for any injury or damage to persons or property
In order to be able to trace the risk assessments that you perform with the calculator, you can give each one a unique name - for example "Office use on Tuesday mornings".
The simulation name has no bearing on the calculation.
A room number is included, if you do not wish to use a formal room number any reference will do.
A room number is included, if you do not wish to use a formal room number any reference will do - for example "57/2-004"
### Room Data
Please enter either the room volume (in m3) or both the floor area (m2) and the room height.
This information is available via GIS.
Please enter either the room volume (in m3) or both the floor area (m2) and the room height (m).
This information is available via GIS Portal (https://gis.cern.ch/gisportal/).
### Ventilation type
There are two main options:
There are three main options:
#### Mechanical
#### Mechanical ventilation
If the room has mechanical ventilation (either a local or centralised system), you should select this option.
In order to make an accurate calculation you will need to know either the flow rate or the number of air changes per hour.
If the room has mechanical ventilation, suppling fresh air from outside (either a local or centralised system), you should select this option.
In order to make an accurate calculation you will need to know either the flow rate of fresh air supplied in the room or th total number of air changes per hour with fresh air.
#### Natural
Please bear in mind that any of the two inputs only consider the supply of fresh air. If a portion of air is recirculated, it shall not be accounted for in the inputs.
Natural ventilation refers to rooms which have openable windows.
Please enter the number, height and width of the windows.
#### Natural ventilation
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 appraoch for the purpose of this tool.
Please enter the number, height and width of the windows (in m).
If there are multiple windows of different sizes, you should take an average.
The window opening distance is:
The window opening distance (in m) is:
* In the case of windows that slide, the length the window is moved open.
* For hinged windows, it is the distance between the fixed frame and the movable glazed part when open.
![Window Opening Distance](window_opening.png "How to measure window opening distance")
**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), but is included for completeness.
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 temperature for each hour of the day has been computed for every month of the year based on historical data for Geneva, Switzerland.
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), but is included for completeness of the report.
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 event time and date in the event data section.
Finally, you must specify when the windows are open - all the time (always), following HSE recommendations for 10 minutes every 2 hours, or during breaks (lunch and coffee breaks).
If you are unsure, we recommend choosing the 10 minutes per 2 hours option.
Finally, you must specify when the windows are open - all the time (always), or for 10 minutes every 2 hours.
#### No ventilation
This option assumes the is neither Mechanical nor Natural ventilation in the simulation.
#### HEPA filtration
A HEPA filter is a high efficiency particulate matter filter, which removes small molecules from the air.
They can be very useful for removing virus particles from the air in an enclosed space.
The calculator allows you to simulate the installation of a HEPA air filter within the space.
A HEPA filter is a high efficiency particulate matter filter, which removes small airborne particles from the air.
They can be very useful for removing viruses from the air in an enclosed space.
The calculator allows you to simulate the installation of a HEPA air filter within the room.
The default air flow rate for the HEPA filter in the model is 250m3/hour.
### Event Data
@ -85,16 +95,18 @@ As an example, for a shared office with 4 people, where one person is infected,
There are three predefined activities in the tool at present.
**Office** = All persons seated, talking. Everyone (occupants and infected occupants) is treated the same in this model.
**Office / Meeting ** = All persons seated, talking. Everyone (occupants and infected occupants) is treated the same in this model.
**Workshop** = Based on a mechanical assembly workshop, all persons are doing light exercise (standing, moving around, using tools) and talking. Everyone (occupants and infected occupants) is treated the same in this model.
**Training** = Based on a typical training course scenario. One individual (the trainer) is doing light exercise (standing) and talking, with all other individuals seated and talking quietly (whispering). In this case it is assumed that the infected person is the trainer, because this is the worst case in terms of transmission risk.
**Training** = Based on a typical training course scenario.
One individual (the trainer) is doing light exercise (standing) and talking, with all other individuals seated and talking quietly (whispering).
In this case it is assumed that the infected person is the trainer, because this is the worst case in terms of viral sheeding.
### Timings
You should enter the time (hours:minutes) for the start and end of the simulation period (i.e. 8:30am to 5:30pm for a typical office day).
You should enter the time (hours:minutes) for the start and end of the simulation period (i.e. 8:30 to 17:30 for a typical office day).
It is important to enter the correct times for the simulation, in particular when using natural ventilation.
It is possible to specify a different time for the entry and exit of the infected person, however for most cases (where we do not know apriori which of the occupants is infected), it is recommended to set these to the same values as the activity start and end.
@ -116,17 +128,16 @@ If you plan to eat lunch in the same area where you have been working, you shoul
### Coffee Breaks
Regular breaks are an important part of maintaining productivity during the day.
You have the option to choose no coffee breaks, 2 or 4 during the simulated period.
It is assumed that all occupants vacate the space during the break period.
If coffee breaks are taken in-situ, this option should be set to no breaks.
If coffee breaks are taken in-situ, this option should be set to 'No breaks'.
When enabled, the breaks are spread evenly throughout the day - for example if we simulate the period from 9am to 6pm, with a lunch break from 1pm to 2pm, with 2 coffee breaks, one will be scheduled at 11am and the second at 4pm.
The exact timing of the breaks within the day is not particularly critical to an accurate simulation, so you do not need to be concerned about major differences if you take a coffee break at 10am instead of 11am.
When enabled, the breaks are spread evenly throughout the day - for example if we simulate the period from 9:00 to 18:00, with a lunch break from 13:00 to 14:00, with 2 coffee breaks, one will be scheduled at 11:00 and the second at 16:00.
The exact timing of the breaks within the day is not particularly critical to an accurate simulation, so you do not need to be concerned about major differences if you take a coffee break at 10:00 instead of 11:00.
The variation of coffee breaks can be altered in 5 minute increments up to 30 minutes in length.
Note that this doesn't necessarily have to be a coffee break, it can represent any period where the simulated space is vacated.
It should also be noted that the risk presented in the report does not take into account any potential exposures during break times.
It should also be noted that the infection probabilities presented in the report does not take into account any potential exposures during the break times.
#### Face Masks
@ -135,6 +146,7 @@ Alternatively, the continuous wearing of masks can be simulated, i.e. all occupa
If you have selected the Training activity type, this equates to the trainer and all participants either wearing masks throughout the training (Yes), or removing them when seated/standing at their socially distanced positions within the training room (No).
For the time being only the Type 1 surgical mask is simulated.
## Generate Report
@ -149,23 +161,24 @@ It contains a summary of all the input data, which will allow the simulation to
## Results
This part of the report shows the ``P(i)`` or probability of infection.
It is estimated based on the emission of virus into the simulated volume, and the amount which is inhaled by exposed individuals. This probability is valid for the simulation duration - i.e. if you have simulated one day and plan to work 5 days in these conditions, the cumulative probability of infection is ``(1-(1-P(i))^5)```.
It is estimated based on the emission rate of virus into the simulated volume, and the amount which is inhaled by exposed individuals.
This probability is valid for the simulation duration - i.e. if you have simulated one day and plan to work 5 days in these conditions and the infected person emits the same amoung of viruses each day, the cumulative probability of infection is ``(1-(1-P(i))^5)```.
If you are using the natural ventilation option, the simulation is only valid for the selected month, because the following or preceding month will have a different average temperature profile.
The ``R0`` for the simulation is calculated based on the probability of infection, multiplied by the number of exposed people.
### Exposure graph
The graph shows the variation in the concentration of infectious quanta (one quanta is the amount of material which can cause infection if inhaled) within the simulated volume.
The graph shows the variation in the concentration of infectious quanta (one quanta is the amount of inhaled viruses which can cause infection in 63) within the simulated volume.
It is determined by:
* The presence of the infected person, who produces viral load in the space.
* The rate of production is related to the type of activity of the infected person (sitting, light exercise), their level of vocalisation (whispering or talking).
* The accumulation of infectious quanta in the space is driven by ventilation (either natural or mechanical, and or HEPA filtration).
* The presence of the infected person, who emits airborne viruses in the volume.
* The emission rate is related to the type of activity of the infected person (sitting, light exercise), their level of vocalisation (whispering or talking).
* The accumulation of infectious quanta in the space is driven by ventilation, if applicable (either natural or mechanical, and or HEPA filtration).
* In a mechanical ventilation scenario, the removal rate is constant, based on air flow in and out of the simulated space.
* Under natural ventilation conditions, the effectiveness of ventilation relies upon the temperature difference between the inside and outside air temperature.
* Under natural ventilation conditions, the effectiveness of ventilation relies upon the hourly temperature difference between the inside and outside air temperature.
* A HEPA filter removes infectious quanta from the air at a constant rate and is modelled in the same way as mechanical ventilation, however air passed through a HEPA filter is recycled not renewed (i.e. it is not fresh air).
# Conclusion
This tool provides illustrations for COVID-19 Airborne risk only.
This tool provides illustrations for COVID-19 (long range) airborne risk only - see Disclaimer
If you have any comments on your experience with the app, or feedback for potential improvements, please share them with the development team at cara-dev@cern.ch.

View file

@ -3,6 +3,7 @@ import html
import typing
from cara import models
from cara import data
@dataclass
@ -131,7 +132,7 @@ class FormData:
month = self.recurrent_event_month[:3]
inside_temp = models.PiecewiseConstant((0, 24), (293,))
outside_temp = models.GenevaTemperatures[month]
outside_temp = data.GenevaTemperatures[month]
ventilation = models.WindowOpening(
active=window_interval,

View file

@ -6,6 +6,10 @@
color: black;
}
.red {
.red_border {
box-shadow: 0px 0px 2px 1px red;
}
.red_text {
color: red;
}

View file

@ -160,12 +160,28 @@ function validate_form(form) {
//Validate all dates
$("input[required].datepicker").each(function () {
$(this).removeClass("red");
$(this).removeClass("red_border");
$(this).next().hide();
var fromDate = $(this).val();
if (!isValidDate(fromDate)) {
$(this).addClass("red");
$(this).addClass("red_border");
submit = false;
$(this).next().show();
}
});
//Validate all times
$("input[required].finish_time").each(function () {
$(this).removeClass("red_border");
$(this).next().hide();
var startTime = parseValToNumber($(this).prev());
var finishTime = parseValToNumber($(this));
if (startTime > finishTime) {
$(this).addClass("red_border");
submit = false;
$(this).next().show();
}
});
@ -183,6 +199,10 @@ function isValidDate(date) {
return composedDate.getDate() == d && composedDate.getMonth() + 1 == m && composedDate.getFullYear() == y;
}
function parseValToNumber(obj) {
return parseInt(obj.val().replace(':',''), 10);
}
/* ------ On Load ---------- */
$(document).ready(function () {

View file

@ -80,22 +80,25 @@ Beta v1.0.0 <span style="float:right; font-weight:bold">Please send feedback to
Number of infected people: <input type="number" id="infected_people" name="infected_people" min=1 required><br>
<hr width="80%">
Activity type: <select id="activity_type" name="activity_type">
<option value="office">Office</option>
<option value="office">Office/Meeting</option>
<option value="workshop">Workshop</option>
<option value="training">Training</option>
</select><br>
Start: <input type="time" id="activity_start" name="activity_start" value="09:00" required> &nbsp;&nbsp;
Finish: <input type="time" id="activity_finish" name="activity_finish" value="18:00" required><br>
Finish: <input type="time" id="activity_finish" class="finish_time" name="activity_finish" value="18:00" required>
<span id="activity_time_error" class="red_text" hidden>Finish time must be after start</span><br>
Infected person(s) presence: <br>
Start: <input type="time" id="infected_start" name="infected_start" value="09:00" required> &nbsp;&nbsp;
Finish: <input type="time" id="infected_finish" name="infected_finish" value="18:00" required><br>
Finish: <input type="time" id="infected_finish" class="finish_time" name="infected_finish" value="18:00" required>
<span id="infected_time_error" class="red_text" hidden>Finish time must be after start</span><br>
<hr width="80%">
When is the event?<br>
<input type="radio" id="event_type_single" name="event_type" value="single_event" onclick="require_fields(this)" required></input>
<label for="event_type_single">Single event</label> &nbsp;&nbsp;
<label for="single_event_date">Date: </label>
<input type="text" id="single_event_date" class="datepicker" name="single_event_date" placeholder="dd/mm/yyyy"><br>
<input type="text" id="single_event_date" class="datepicker" name="single_event_date" placeholder="dd/mm/yyyy">
<span id="event_time_error" class="red_text" hidden> Incorrect date format</span><br>
<input type="radio" id="event_type_recurrent" name="event_type" value="recurrent_event" onclick="require_fields(this)" required></input>
<label for="event_type_recurrent">Recurrent usage</label>
<select id="recurrent_event_month" name="recurrent_event_month">
@ -122,7 +125,8 @@ Beta v1.0.0 <span style="float:right; font-weight:bold">Please send feedback to
<div id="DIVlunch_break">
Start: <input type="time" id="lunch_start" name="lunch_start" unrequired> &nbsp;&nbsp;
Finish: <input type="time" id="lunch_finish" name="lunch_finish" unrequired><br>
Finish: <input type="time" id="lunch_finish" class="finish_time" name="lunch_finish" unrequired>
<span id="lunch_time_error" class="red_text" hidden>Finish time must be after start</span><br>
</div>
<!-- Coffee Options -->
@ -162,7 +166,7 @@ Beta v1.0.0 <span style="float:right; font-weight:bold">Please send feedback to
Enter the data on the available means for venting of indoor spaces. For mechanical ventilation, you should check with a specialist for the air flow or air change rate.<br>
<b>Event data</b><br>
Enter the total number of occupants in the room and how many of them you assume are infected. We have provided common activity types:<br>
Office = typical scenario all persons seated, talking.<br>
Office/Meeting = typical scenario all persons seated, talking.<br>
Workshop = assembly workshop environment, all persons doing light exercise, talking.<br>
Training = one person (the trainer) standing, talking, all others seated, talking quietly (whispering). It is assumed the trainer is the infected person, for the worst case scenario.<br>
You should specify if the event is a one off (give date) or recurrent use of the same space for the same activity, in which case select the month when the activity takes place.<br>

View file

@ -4,6 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Report | CARA (COVID Airborne Risk Assessment)</title>
<link rel="stylesheet" type="text/css" href="/calculator/static/css/report.css">
</head>
@ -63,7 +65,7 @@
infected.</p></li>
<li><p class="data_text">Activity type:
{% if form.activity_type == "office" %}
Office work typical scenario with all persons seated, talking.
Office/Meeting typical scenario with all persons seated, talking.
{% elif form.activity_type == "workshop" %}
Workshop = assembly workshop environment, all persons doing light exercise, talking.
{% elif form.activity_type == "training" %}

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

44
cara/data.py Normal file
View file

@ -0,0 +1,44 @@
import numpy as np
from cara import models
# average temperature of each month, hour per hour (from midnight to 11 pm)
Geneva_hourly_temperatures_celsius_per_hour = {
'Jan': [0.2, -0.3, -0.5, -0.9, -1.1, -1.4, -1.5, -1.5, -1.1, 0.1, 1.5,
2.8, 3.8, 4.4, 4.5, 4.4, 4.4, 3.9, 3.1, 2.7, 2.2, 1.7, 1.5, 1.1],
'Feb': [0.9, 0.3, 0.0, -0.5, -0.7, -1.1, -1.2, -1.1, -0.7, 0.8, 2.5,
4.2, 5.4, 6.2, 6.3, 6.2, 6.1, 5.5, 4.5, 4.1, 3.5, 2.8, 2.5, 2.0],
'Mar': [4.2, 3.5, 3.1, 2.5, 2.1, 1.6, 1.5, 1.6, 2.2, 4.0, 6.3, 8.4,
10.0, 11.1, 11.2, 11.1, 11.0, 10.2, 8.9, 8.3, 7.5, 6.7, 6.3, 5.6],
'Apr': [7.4, 6.7, 6.2, 5.5, 5.2, 4.7, 4.5, 4.6, 5.3, 7.2, 9.6, 11.9,
13.7, 14.8, 14.9, 14.8, 14.7, 13.8, 12.4, 11.8, 10.9, 10.1, 9.6, 8.9],
'May': [11.8, 11.1, 10.6, 9.9, 9.5, 8.9, 8.8, 8.9, 9.6, 11.6, 14.2, 16.6,
18.4, 19.6, 19.7, 19.6, 19.4, 18.6, 17.1, 16.5, 15.6, 14.6, 14.2, 13.4],
'Jun': [15.2, 14.4, 13.9, 13.2, 12.7, 12.2, 12.0, 12.1, 12.8, 15.0, 17.7,
20.2, 22.1, 23.3, 23.5, 23.4, 23.2, 22.3, 20.8, 20.1, 19.1, 18.2, 17.7, 16.9],
'Jul': [17.6, 16.7, 16.1, 15.3, 14.9, 14.3, 14.1, 14.2, 15.0, 17.3, 20.2,
23.0, 25.0, 26.3, 26.5, 26.4, 26.2, 25.2, 23.6, 22.8, 21.8, 20.8, 20.2, 19.4],
'Aug': [17.1, 16.2, 15.7, 14.9, 14.5, 13.9, 13.7, 13.8, 14.6, 16.9, 19.7,
22.4, 24.4, 25.6, 25.8, 25.7, 25.5, 24.5, 22.9, 22.2, 21.2, 20.2, 19.7, 18.9],
'Sep': [13.4, 12.7, 12.2, 11.5, 11.2, 10.7, 10.5, 10.6, 11.3, 13.2, 15.6,
17.9, 19.6, 20.8, 20.9, 20.8, 20.7, 19.8, 18.4, 17.8, 16.9, 16.1, 15.6, 14.9],
'Oct': [9.4, 8.8, 8.5, 7.9, 7.6, 7.2, 7.1, 7.2, 7.7, 9.3, 11.2, 13.0,
14.4, 15.3, 15.4, 15.3, 15.2, 14.5, 13.4, 12.9, 12.2, 11.6, 11.2, 10.6],
'Nov': [4.0, 3.6, 3.3, 2.9, 2.6, 2.3, 2.2, 2.2, 2.7, 3.9, 5.5, 6.9, 8.0,
8.7, 8.8, 8.7, 8.7, 8.1, 7.2, 6.8, 6.3, 5.7, 5.5, 5.0],
'Dec': [1.4, 1.0, 0.8, 0.4, 0.2, -0.0, -0.1, -0.1, 0.3, 1.3, 2.6, 3.8,
4.7, 5.2, 5.3, 5.2, 5.2, 4.7, 4.0, 3.7, 3.2, 2.8, 2.6, 2.2]
}
# Geneva hourly temperatures as piecewise constant function (in Kelvin)
GenevaTemperatures_hourly = {
month: models.PiecewiseConstant(tuple(np.arange(25.)),
tuple(273.15+np.array(temperatures)))
for month,temperatures in Geneva_hourly_temperatures_celsius_per_hour.items()
}
# same temperatures on a finer temperature mesh
GenevaTemperatures = {
month: GenevaTemperatures_hourly[month].refine(refine_factor=10)
for month,temperatures in Geneva_hourly_temperatures_celsius_per_hour.items()
}

View file

@ -6,34 +6,6 @@ from abc import abstractmethod
from dataclasses import dataclass
# average temperature of each month, hour per hour (from midnight to 11 pm)
Geneva_hourly_temperatures_celsius_per_hour = {
'Jan': [0.2, -0.3, -0.5, -0.9, -1.1, -1.4, -1.5, -1.5, -1.1, 0.1, 1.5,
2.8, 3.8, 4.4, 4.5, 4.4, 4.4, 3.9, 3.1, 2.7, 2.2, 1.7, 1.5, 1.1],
'Feb': [0.9, 0.3, 0.0, -0.5, -0.7, -1.1, -1.2, -1.1, -0.7, 0.8, 2.5,
4.2, 5.4, 6.2, 6.3, 6.2, 6.1, 5.5, 4.5, 4.1, 3.5, 2.8, 2.5, 2.0],
'Mar': [4.2, 3.5, 3.1, 2.5, 2.1, 1.6, 1.5, 1.6, 2.2, 4.0, 6.3, 8.4,
10.0, 11.1, 11.2, 11.1, 11.0, 10.2, 8.9, 8.3, 7.5, 6.7, 6.3, 5.6],
'Apr': [7.4, 6.7, 6.2, 5.5, 5.2, 4.7, 4.5, 4.6, 5.3, 7.2, 9.6, 11.9,
13.7, 14.8, 14.9, 14.8, 14.7, 13.8, 12.4, 11.8, 10.9, 10.1, 9.6, 8.9],
'May': [11.8, 11.1, 10.6, 9.9, 9.5, 8.9, 8.8, 8.9, 9.6, 11.6, 14.2, 16.6,
18.4, 19.6, 19.7, 19.6, 19.4, 18.6, 17.1, 16.5, 15.6, 14.6, 14.2, 13.4],
'Jun': [15.2, 14.4, 13.9, 13.2, 12.7, 12.2, 12.0, 12.1, 12.8, 15.0, 17.7,
20.2, 22.1, 23.3, 23.5, 23.4, 23.2, 22.3, 20.8, 20.1, 19.1, 18.2, 17.7, 16.9],
'Jul': [17.6, 16.7, 16.1, 15.3, 14.9, 14.3, 14.1, 14.2, 15.0, 17.3, 20.2,
23.0, 25.0, 26.3, 26.5, 26.4, 26.2, 25.2, 23.6, 22.8, 21.8, 20.8, 20.2, 19.4],
'Aug': [17.1, 16.2, 15.7, 14.9, 14.5, 13.9, 13.7, 13.8, 14.6, 16.9, 19.7,
22.4, 24.4, 25.6, 25.8, 25.7, 25.5, 24.5, 22.9, 22.2, 21.2, 20.2, 19.7, 18.9],
'Sep': [13.4, 12.7, 12.2, 11.5, 11.2, 10.7, 10.5, 10.6, 11.3, 13.2, 15.6,
17.9, 19.6, 20.8, 20.9, 20.8, 20.7, 19.8, 18.4, 17.8, 16.9, 16.1, 15.6, 14.9],
'Oct': [9.4, 8.8, 8.5, 7.9, 7.6, 7.2, 7.1, 7.2, 7.7, 9.3, 11.2, 13.0,
14.4, 15.3, 15.4, 15.3, 15.2, 14.5, 13.4, 12.9, 12.2, 11.6, 11.2, 10.6],
'Nov': [4.0, 3.6, 3.3, 2.9, 2.6, 2.3, 2.2, 2.2, 2.7, 3.9, 5.5, 6.9, 8.0,
8.7, 8.8, 8.7, 8.7, 8.1, 7.2, 6.8, 6.3, 5.7, 5.5, 5.0],
'Dec': [1.4, 1.0, 0.8, 0.4, 0.2, -0.0, -0.1, -0.1, 0.3, 1.3, 2.6, 3.8,
4.7, 5.2, 5.3, 5.2, 5.2, 4.7, 4.0, 3.7, 3.2, 2.8, 2.6, 2.2]
}
@dataclass(frozen=True)
class Room:
@ -103,6 +75,9 @@ class PeriodicInterval(Interval):
@dataclass(frozen=True)
class PiecewiseConstant:
# TODO: implement rather a periodic version (24-hour period), where
# transition_times and values have the same length.
#: transition times at which the function changes value (hours).
transition_times: typing.Tuple[float, ...]
@ -135,13 +110,14 @@ class PiecewiseConstant:
present_times.append((t1,t2))
return SpecificInterval(present_times=present_times)
# Geneva hourly temperatures as piecewise constant function (in Kelvin)
GenevaTemperatures = {
month: PiecewiseConstant(tuple(np.arange(25.)),
tuple(273.15+np.array(temperatures)))
for month,temperatures in Geneva_hourly_temperatures_celsius_per_hour.items()
}
def refine(self,refine_factor=10):
# build a new PiecewiseConstant object with a refined mesh,
# using a linear interpolation in-between the initial mesh points
refined_times = np.linspace(self.transition_times[0],self.transition_times[-1],
(len(self.transition_times)-1)*refine_factor+1)
return PiecewiseConstant(tuple(refined_times),
tuple(np.interp(refined_times[:-1],self.transition_times,
self.values+(self.values[-1],) ) ) )
@dataclass(frozen=True)

View file

@ -2,6 +2,7 @@ import pytest
from cara.apps.calculator import model_generator
from cara import models
from cara import data
import numpy as np
@pytest.fixture
@ -25,7 +26,7 @@ def test_ventilation_window(baseline_form):
window = models.WindowOpening(
active=models.PeriodicInterval(period=120, duration=10),
inside_temp=models.PiecewiseConstant((0, 24), (293,)),
outside_temp=models.GenevaTemperatures['Dec'],
outside_temp=data.GenevaTemperatures['Dec'],
cd_b=0.6, window_height=1.6, opening_length=0.6,
)
baseline_form.ventilation_type = 'natural'
@ -75,7 +76,7 @@ def test_ventilation_window_hepa(baseline_form):
window = models.WindowOpening(
active=models.PeriodicInterval(period=120, duration=10),
inside_temp=models.PiecewiseConstant((0, 24), (293,)),
outside_temp=models.GenevaTemperatures['Dec'],
outside_temp=data.GenevaTemperatures['Dec'],
cd_b=0.6, window_height=1.6, opening_length=0.6,
)
hepa = models.HEPAFilter(
@ -113,4 +114,4 @@ def test_present_intervals(baseline_form):
def test_key_validation(baseline_form_data):
baseline_form_data['activity_type'] = 'invalid key'
with pytest.raises(ValueError):
model_generator.FormData.from_dict(baseline_form_data)
model_generator.FormData.from_dict(baseline_form_data)

View file

@ -3,6 +3,7 @@ import numpy.testing as npt
import pytest
import cara.models as models
import cara.data as data
def test_no_mask_aerosols(baseline_model):
@ -245,6 +246,15 @@ def test_piecewiseconstant(time, expected_value):
assert fun.value(time) == expected_value
def test_piecewiseconstant_interp():
transition_times = (0, 8, 16, 24)
values = (2, 5, 8)
fun = models.PiecewiseConstant(transition_times,values)
refined_fun = models.PiecewiseConstant(transition_times,values).refine(refine_factor=2)
assert refined_fun.transition_times == (0, 4, 8, 12, 16, 20 ,24)
assert refined_fun.values == (2, 3.5, 5, 6.5, 8, 8)
def test_constantfunction():
transition_times = (0,24)
values = (20,)
@ -267,7 +277,7 @@ def test_piecewiseconstant_vs_interval(time):
def test_piecewiseconstant_transition_times():
outside_temp=models.GenevaTemperatures['Jan']
outside_temp=data.GenevaTemperatures['Jan']
assert set(outside_temp.transition_times) == outside_temp.interval().transition_times()
@ -289,13 +299,27 @@ def test_windowopening(time, expected_value):
def build_hourly_dependent_model(month, intervals_open=((7.5, 8.5),),
intervals_presence_infected=((0, 4), (5, 7.5))):
intervals_presence_infected=((0, 4), (5, 7.5)),
artificial_refinement=False,
temperatures=data.GenevaTemperatures_hourly):
if artificial_refinement:
# 5-fold increase of number of times, WITHOUT interpolation
# (hence transparent for the results)
refine_factor = 2
times_refined = tuple(np.linspace(0.,24,
refine_factor*len(temperatures[month].values)+1))
temperatures_refined = tuple(np.hstack([[v]*refine_factor
for v in temperatures[month].values]))
outside_temp = models.PiecewiseConstant(times_refined,temperatures_refined)
else:
outside_temp = temperatures[month]
model = models.Model(
room=models.Room(volume=75),
ventilation=models.WindowOpening(
active=models.SpecificInterval(intervals_open),
inside_temp=models.PiecewiseConstant((0,24),(293,)),
outside_temp=models.GenevaTemperatures[month],
outside_temp=outside_temp,
cd_b=0.6, window_height=1.6, opening_length=0.6,
),
infected=models.InfectedPerson(
@ -340,7 +364,7 @@ def build_hourly_dependent_model_multipleventilation(month, intervals_open=((7.5
models.WindowOpening(
active=models.SpecificInterval(intervals_open),
inside_temp=models.PiecewiseConstant((0,24),(293,)),
outside_temp=models.GenevaTemperatures[month],
outside_temp=data.GenevaTemperatures[month],
cd_b=0.6, window_height=1.6, opening_length=0.6,
),
models.HEPAFilter(
@ -366,7 +390,7 @@ def build_hourly_dependent_model_multipleventilation(month, intervals_open=((7.5
@pytest.mark.parametrize(
"month, temperatures",
models.Geneva_hourly_temperatures_celsius_per_hour.items(),
data.Geneva_hourly_temperatures_celsius_per_hour.items(),
)
@pytest.mark.parametrize(
"time",
@ -381,7 +405,7 @@ def test_concentrations_hourly_dep_temp_vs_constant(month, temperatures, time):
@pytest.mark.parametrize(
"month, temperatures",
models.Geneva_hourly_temperatures_celsius_per_hour.items(),
data.Geneva_hourly_temperatures_celsius_per_hour.items(),
)
@pytest.mark.parametrize(
"time",
@ -402,7 +426,7 @@ def test_concentrations_hourly_dep_multipleventilation():
@pytest.mark.parametrize(
"month_temp_item",
models.Geneva_hourly_temperatures_celsius_per_hour.items(),
data.Geneva_hourly_temperatures_celsius_per_hour.items(),
)
@pytest.mark.parametrize(
"time",
@ -414,3 +438,42 @@ def test_concentrations_hourly_dep_adding_artificial_transitions(month_temp_item
m1 = build_hourly_dependent_model(month,intervals_open=((7.5, 8.5),))
m2 = build_hourly_dependent_model(month,intervals_open=((7.5, 8.5),(8.,8.1)))
npt.assert_allclose(m1.concentration(time), m2.concentration(time), rtol=1e-5)
@pytest.mark.parametrize(
"time",
list(np.random.random_sample(10)*24.)+list(np.arange(0,24.5,0.5)),
)
def test_concentrations_refine_times(time):
month = 'Jan'
m1 = build_hourly_dependent_model(month,intervals_open=((0, 24),))
m2 = build_hourly_dependent_model(month,intervals_open=((0, 24),),
artificial_refinement=True)
npt.assert_allclose(m1.concentration(time), m2.concentration(time), rtol=1e-8)
@pytest.mark.parametrize(
"month, expected_r0",
[
['Jan', 91.06953],
['Jun', 99.46692],
],
)
def test_r0_hourly_dep(month,expected_r0):
m = build_hourly_dependent_model(month,intervals_open=((0,24),),
intervals_presence_infected=((8,12),(13,17)))
p = m.infection_probability()
npt.assert_allclose(p, expected_r0)
@pytest.mark.parametrize(
"month, expected_r0",
[
['Jan', 91.19912],
['Jun', 99.59226],
],
)
def test_r0_hourly_dep_refined(month,expected_r0):
m = build_hourly_dependent_model(month,intervals_open=((0,24),),
intervals_presence_infected=((8,12),(13,17)),
temperatures=data.GenevaTemperatures)
p = m.infection_probability()
npt.assert_allclose(p, expected_r0)