Merge branch 'master' into feature/virions_plot

This commit is contained in:
Luis Aleixo 2021-08-06 11:41:55 +02:00
commit b2d794316a
8 changed files with 245 additions and 35 deletions

View file

@ -26,7 +26,7 @@ Each event modelled is unique, and the results generated therein are only as acc
## Authors ## Authors
CARA was developed by following members of CERN - European Council for Nuclear Research (visit https://home.cern/): CARA was developed by following members of CERN - European Council for Nuclear Research (visit https://home.cern/):
Andre Henriques<sup>1</sup>, Marco Andreini<sup>1</sup>, Gabriella Azzopardi<sup>2</sup>, James Devine<sup>3</sup>, Philip Elson<sup>4</sup>, Nicolas Mounet<sup>2</sup>, Markus Kongstein Rognlien<sup>2,6</sup>, Nicola Tarocco<sup>5</sup> Andre Henriques<sup>1</sup>, Luis Aleixo<sup>1</sup>, Marco Andreini<sup>1</sup>, Gabriella Azzopardi<sup>2</sup>, James Devine<sup>3</sup>, Philip Elson<sup>4</sup>, Nicolas Mounet<sup>2</sup>, Markus Kongstein Rognlien<sup>2,6</sup>, Nicola Tarocco<sup>5</sup>
<sup>1</sup>HSE Unit, Occupational Health & Safety Group, CERN<br> <sup>1</sup>HSE Unit, Occupational Health & Safety Group, CERN<br>
<sup>2</sup>Beams Department, Accelerators and Beam Physics Group, CERN<br> <sup>2</sup>Beams Department, Accelerators and Beam Physics Group, CERN<br>

View file

@ -44,13 +44,13 @@ def calculate_report_data(model: models.ExposureModel):
return { return {
"times": list(times), "times": list(times),
"exposed_presence_intervals": [list(interval) for interval in model.exposed.presence.boundaries()],
"concentrations": concentrations, "concentrations": concentrations,
"highest_const": highest_const, "highest_const": highest_const,
"prob_inf": prob, "prob_inf": prob,
"emission_rate": er, "emission_rate": er,
"exposed_occupants": exposed_occupants, "exposed_occupants": exposed_occupants,
"expected_new_cases": expected_new_cases, "expected_new_cases": expected_new_cases,
"scenario_plot_src": img2base64(_figure2bytes(plot(times, concentrations, model))),
} }
@ -133,7 +133,7 @@ def plot(times, concentrations, model: models.ExposureModel):
ax.set_ylim(0) ax.set_ylim(0)
return fig return fig
def minutes_to_time(minutes: int) -> str: def minutes_to_time(minutes: int) -> str:
minute_string = str(minutes % 60) minute_string = str(minutes % 60)

View file

@ -0,0 +1,190 @@
/* Generate the concentration plot using d3 library. */
function draw_concentration_plot(svg_id, times, concentrations, exposed_presence_intervals) {
var visBoundingBox = d3.select(svg_id)
.node()
.getBoundingClientRect();
var time_format = d3.timeFormat('%H:%M');
var data = []
times.map((time, index) => data.push({ 'time': time, 'hour': new Date().setHours(Math.trunc(time), (time - Math.trunc(time)) * 60), 'concentration': concentrations[index] }))
var vis = d3.select(svg_id),
width = visBoundingBox.width - 300,
height = visBoundingBox.height,
margins = { top: 30, right: 20, bottom: 50, left: 50 },
// H:M time format for x axis.
xRange = d3.scaleTime().range([margins.left, width - margins.right]).domain([data[0].hour, data[data.length - 1].hour]),
bisecHour = d3.bisector((d) => { return d.hour; }).left,
yRange = d3.scaleLinear().range([height - margins.bottom, margins.top]).domain([data[0].concentration, data[data.length - 1].concentration]),
xTimeRange = d3.scaleLinear().range([margins.left, width - margins.right]).domain([data[0].time, data[data.length - 1].time]),
xAxis = d3.axisBottom(xRange).tickFormat(d => time_format(d)),
yAxis = d3.axisLeft(yRange);
// Plot tittle.
vis.append('svg:foreignObject')
.attr('width', width)
.attr('height', margins.top)
.append('xhtml:body')
.style('text-align', 'center')
.html('Mean concentration of infectious quanta');
// Line representing the mean concentration.
var lineFunc = d3.line()
.defined(d => !isNaN(d.concentration))
.x(d => xTimeRange(d.time))
.y(d => yRange(d.concentration))
.curve(d3.curveBasis);
vis.append('svg:path')
.attr('d', lineFunc(data))
.attr('stroke', '#1f77b4')
.attr('stroke-width', 2)
.attr('fill', 'none');
// X axis declaration.
vis.append('svg:g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + (height - margins.bottom) + ')')
.call(xAxis);
// X axis label.
vis.append('text')
.attr('class', 'x label')
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.attr('x', (width + margins.right) / 2)
.attr('y', height * 0.97)
.text('Time of day')
// Y axis declaration.
vis.append('svg:g')
.attr('class', 'y axis')
.attr('transform', 'translate(' + margins.left + ',0)')
.call(yAxis);
// Y axis label.
vis.append('svg:text')
.attr('class', 'y label')
.attr('fill', 'black')
.attr('transform', 'rotate(-90, 0,' + height + ')')
.attr('text-anchor', 'middle')
.attr('x', (height + margins.bottom) / 2)
.attr('y', (height + margins.left) * 0.92)
.text('Mean concentration (q/m^3)');
// Area representing the presence of exposed person(s).
exposed_presence_intervals.forEach(b => {
vis.append('svg:path')
.attr('d', lineFunc(data.filter(d => {
return d.time >= b[0] && d.time <= b[1]
})))
.attr('fill', 'none');
var curveFunc = d3.area()
.x(d => xTimeRange(d.time))
.y0(height - margins.bottom)
.y1(d => yRange(d.concentration));
vis.append('svg:path')
.attr('d', curveFunc(data.filter(d => {
return d.time >= b[0] && d.time <= b[1]
})))
.attr('fill', '#1f77b4')
.attr('fill-opacity', '0.1');
})
// Legend for the plot elements - line and area.
var size = 20
vis.append('rect')
.attr('x', width + size)
.attr('y', margins.top + size)
.attr('width', 20)
.attr('height', 3)
.style('fill', '#1f77b4');
vis.append('rect')
.attr('x', width + size)
.attr('y', 3 * size)
.attr('width', 20)
.attr('height', 20)
.attr('fill', '#1f77b4')
.attr('fill-opacity', '0.1');
vis.append('text')
.attr('x', width + 3 * size)
.attr('y', margins.top + size)
.text('Mean concentration')
.style('font-size', '15px')
.attr('alignment-baseline', 'central');
vis.append('text')
.attr('x', width + 3 * size)
.attr('y', margins.top + 2 * size)
.text('Presence of exposed person(s)')
.style('font-size', '15px')
.attr('alignment-baseline', 'central');
// Legend bounding box.
vis.append('rect')
.attr('width', 275)
.attr('height', 50)
.attr('x', width * 1.005)
.attr('y', margins.top + 5)
.attr('stroke', 'lightgrey')
.attr('stroke-width', '2')
.attr('rx', '5px')
.attr('ry', '5px')
.attr('stroke-linejoin', 'round')
.attr('fill', 'none');
// Tooltip.
var focus = vis.append('svg:g')
.style('display', 'none');
focus.append('circle')
.attr('r', 3);
focus.append('rect')
.attr('fill', 'white')
.attr('stroke', '#000')
.attr('width', 80)
.attr('height', 50)
.attr('x', 10)
.attr('y', -22)
.attr('rx', 4)
.attr('ry', 4);
focus.append('text')
.attr('id', 'tooltip-time')
.attr('x', 18)
.attr('y', -2);
focus.append('text')
.attr('id', 'tooltip-concentration')
.attr('x', 18)
.attr('y', 18);
vis.append('rect')
.attr('fill', 'none')
.attr('pointer-events', 'all')
.attr('width', width - margins.right)
.attr('height', height)
.on('mouseover', () => { focus.style('display', null); })
.on('mouseout', () => { focus.style('display', 'none'); })
.on('mousemove', mousemove);
function mousemove() {
var x0 = xRange.invert(d3.pointer(event, this)[0]),
i = bisecHour(data, x0, 1),
d0 = data[i - 1],
d1 = data[i],
d = x0 - d0.hour > d1.hour - x0 ? d1 : d0;
focus.attr('transform', 'translate(' + xRange(d.hour) + ',' + yRange(d.concentration) + ')');
focus.select('#tooltip-time').text('x = ' + time_format(d.hour));
focus.select('#tooltip-concentration').text('y = ' + d.concentration.toFixed(2));
}
}

View file

@ -8,6 +8,10 @@
<link rel="stylesheet" type="text/css" href="{{ calculator_prefix }}/static/css/report.css"> <link rel="stylesheet" type="text/css" href="{{ calculator_prefix }}/static/css/report.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="{{ calculator_prefix }}/static/js/report.js" type="application/javascript"></script>
</head> </head>
<body id="body"> <body id="body">
@ -216,7 +220,13 @@
<p id="section1">[*] The results are based on the parameters and assumptions published in the CERN Open Report <a href="https://cds.cern.ch/record/2756083"> CERN-OPEN-2021-004</a></p> <p id="section1">[*] The results are based on the parameters and assumptions published in the CERN Open Report <a href="https://cds.cern.ch/record/2756083"> CERN-OPEN-2021-004</a></p>
</p> </p>
<img id="scenario_concentration_plot" src="{{ scenario_plot_src }}"> <svg id="result_plot" width="900" height="400"></svg>
<script type="application/javascript">
var times = {{times}}
var concentrations = {{concentrations}}
var exposed_presence_intervals = {{exposed_presence_intervals}}
draw_concentration_plot("#result_plot", times, concentrations, exposed_presence_intervals);
</script>
<p class="data_title">Alternative scenarios:</p> <p class="data_title">Alternative scenarios:</p>
<p class="data_text"> <p class="data_text">

View file

@ -43,7 +43,7 @@
<h2>Authors</h2> <h2>Authors</h2>
<div class="text-component-text cern_full_html" > <div class="text-component-text cern_full_html" >
<p> <p>
<h4>Andre Henriques<sup>1</sup>, Marco Andreini<sup>1</sup>, Gabriella Azzopardi<sup>2</sup>, James Devine<sup>3</sup>, Philip Elson<sup>4</sup>, Nicolas Mounet<sup>2</sup>, Markus Kongstein Rognlien<sup>2,6</sup>, Nicola Tarocco<sup>5</sup></h4><br> <h4>Andre Henriques<sup>1</sup>, Luis Aleixo<sup>1</sup>, Marco Andreini<sup>1</sup>, Gabriella Azzopardi<sup>2</sup>, James Devine<sup>3</sup>, Philip Elson<sup>4</sup>, Nicolas Mounet<sup>2</sup>, Markus Kongstein Rognlien<sup>2,6</sup>, Nicola Tarocco<sup>5</sup></h4><br>
<sup>1</sup>HSE Unit, Occupational Health & Safety Group, CERN<br> <sup>1</sup>HSE Unit, Occupational Health & Safety Group, CERN<br>
<sup>2</sup>Beams Department, Accelerators and Beam Physics Group, CERN<br> <sup>2</sup>Beams Department, Accelerators and Beam Physics Group, CERN<br>

View file

@ -420,7 +420,7 @@ class Virus:
#: RNA copies / mL #: RNA copies / mL
viral_load_in_sputum: _VectorisedFloat viral_load_in_sputum: _VectorisedFloat
#: RNA-copies #: Dose to initiate infection, in RNA copies
infectious_dose: _VectorisedFloat infectious_dose: _VectorisedFloat
#: Pre-populated examples of Viruses. #: Pre-populated examples of Viruses.

View file

@ -50,12 +50,12 @@ populations = [
# A population with some array component for inhalation_rate. # A population with some array component for inhalation_rate.
models.Population( models.Population(
10, halftime, models.Mask.types['Type I'], 10, halftime, models.Mask.types['Type I'],
models.Activity(np.array([0.51,0.57]), 0.57), models.Activity(np.array([0.51, 0.57]), 0.57),
), ),
] ]
dummyRoom = models.Room(50, 0.5) dummy_room = models.Room(50, 0.5)
dummyVentilation = models._VentilationBase() dummy_ventilation = models._VentilationBase()
dummyInfPopulation = models.InfectedPopulation( dummy_infected_population = models.InfectedPopulation(
number=1, number=1,
presence=halftime, presence=halftime,
mask=models.Mask.types['Type I'], mask=models.Mask.types['Type I'],
@ -64,30 +64,31 @@ dummyInfPopulation = models.InfectedPopulation(
expiration=models.Expiration.types['Talking'] expiration=models.Expiration.types['Talking']
) )
def known_concentrations(func): def known_concentrations(func):
return KnownConcentrations(dummyRoom, dummyVentilation, dummyInfPopulation, func) return KnownConcentrations(dummy_room, dummy_ventilation, dummy_infected_population, func)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"population, cm, f_dep, expected_exposure, expected_probability",[ "population, cm, f_dep, expected_exposure, expected_probability", [
[populations[1], known_concentrations(lambda t: 36), 1., [populations[1], known_concentrations(lambda t: 36), 1.,
np.array([432, 432]), np.array([99.6803184113, 99.5181053773])], np.array([432, 432]), np.array([99.6803184113, 99.5181053773])],
[populations[2], known_concentrations(lambda t: 36), 1., [populations[2], known_concentrations(lambda t: 36), 1.,
np.array([432, 432]), np.array([97.4574432074, 98.3493482895])], np.array([432, 432]), np.array([97.4574432074, 98.3493482895])],
[populations[0], known_concentrations(lambda t: np.array([36, 72])), 1., [populations[0], known_concentrations(lambda t: np.array([36, 72])), 1.,
np.array([432, 864]), np.array([98.3493482895, 99.9727534893])], np.array([432, 864]), np.array([98.3493482895, 99.9727534893])],
[populations[1], known_concentrations(lambda t: np.array([36, 72])), 1., [populations[1], known_concentrations(lambda t: np.array([36, 72])), 1.,
np.array([432, 864]), np.array([99.6803184113, 99.9976777757])], np.array([432, 864]), np.array([99.6803184113, 99.9976777757])],
[populations[0], known_concentrations(lambda t: 72), np.array([0.5, 1.]), [populations[0], known_concentrations(lambda t: 72), np.array([0.5, 1.]),
864, np.array([98.3493482895, 99.9727534893])], 864, np.array([98.3493482895, 99.9727534893])],
]) ])
def test_exposure_model_ndarray(population, cm, f_dep, def test_exposure_model_ndarray(population, cm, f_dep,
expected_exposure, expected_probability): expected_exposure, expected_probability):
model = ExposureModel(cm, population, fraction_deposited = f_dep) model = ExposureModel(cm, population, fraction_deposited=f_dep)
np.testing.assert_almost_equal( np.testing.assert_almost_equal(
model.exposure(), expected_exposure model.exposure(), expected_exposure
) )
@ -103,7 +104,8 @@ def test_exposure_model_ndarray(population, cm, f_dep,
@pytest.mark.parametrize("population", populations) @pytest.mark.parametrize("population", populations)
def test_exposure_model_ndarray_and_float_mix(population): def test_exposure_model_ndarray_and_float_mix(population):
cm = known_concentrations(lambda t: 0 if np.floor(t) % 2 else np.array([1.2, 1.2])) cm = known_concentrations(lambda t: 0 if np.floor(t) %
2 else np.array([1.2, 1.2]))
model = ExposureModel(cm, population) model = ExposureModel(cm, population)
expected_exposure = np.array([14.4, 14.4]) expected_exposure = np.array([14.4, 14.4])
@ -132,7 +134,8 @@ def test_exposure_model_compare_scalar_vector(population):
@pytest.fixture @pytest.fixture
def conc_model(): def conc_model():
interesting_times = models.SpecificInterval(([0, 1], [1.01, 1.02], [12, 24])) interesting_times = models.SpecificInterval(
([0, 1], [1.01, 1.02], [12, 24]))
always = models.SpecificInterval(((0, 24),)) always = models.SpecificInterval(((0, 24),))
return models.ConcentrationModel( return models.ConcentrationModel(
models.Room(25), models.Room(25),
@ -149,14 +152,16 @@ def conc_model():
# expected exposure were computed with a trapezoidal integration, using # expected exposure were computed with a trapezoidal integration, using
# a mesh of 10'000 pts per exposed presence interval. # a mesh of 10'000 pts per exposed presence interval.
@pytest.mark.parametrize("exposed_time_interval, expected_exposure", [ @pytest.mark.parametrize("exposed_time_interval, expected_exposure", [
[(0, 1), 266.67176], [(0, 1), 266.67176],
[(1, 1.01), 3.0879539], [(1, 1.01), 3.0879539],
[(1.01, 1.02), 3.00082435], [(1.01, 1.02), 3.00082435],
[(12, 12.01), 0.095063235], [(12, 12.01), 0.095063235],
[(12, 24), 3775.65025], [(12, 24), 3775.65025],
[(0, 24), 4097.8494], [(0, 24), 4097.8494],
] ]
) )
def test_exposure_model_integral_accuracy(exposed_time_interval, def test_exposure_model_integral_accuracy(exposed_time_interval,
expected_exposure, conc_model): expected_exposure, conc_model):
@ -168,19 +173,21 @@ def test_exposure_model_integral_accuracy(exposed_time_interval,
model = ExposureModel(conc_model, population, fraction_deposited=1.) model = ExposureModel(conc_model, population, fraction_deposited=1.)
np.testing.assert_allclose(model.exposure(), expected_exposure) np.testing.assert_allclose(model.exposure(), expected_exposure)
def test_infectious_dose_vectorisation(): def test_infectious_dose_vectorisation():
infPopulation = models.InfectedPopulation( infected_population = models.InfectedPopulation(
number=1, number=1,
presence=halftime, presence=halftime,
mask=models.Mask.types['Type I'], mask=models.Mask.types['Type I'],
activity=models.Activity.types['Standing'], activity=models.Activity.types['Standing'],
virus = models.SARSCoV2( virus=models.SARSCoV2(
viral_load_in_sputum=1e9, viral_load_in_sputum=1e9,
infectious_dose=np.array([50, 20, 30]), infectious_dose=np.array([50, 20, 30]),
), ),
expiration=models.Expiration.types['Talking'] expiration=models.Expiration.types['Talking']
) )
cm = KnownConcentrations(dummyRoom, dummyVentilation, infPopulation, lambda t: 1.2) cm = KnownConcentrations(
dummy_room, dummy_ventilation, infected_population, lambda t: 1.2)
presence_interval = models.SpecificInterval(((0, 1),)) presence_interval = models.SpecificInterval(((0, 1),))
population = models.Population( population = models.Population(

View file

@ -25,3 +25,6 @@ ignore_missing_imports = True
[mypy-scipy.*] [mypy-scipy.*]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-tqdm.*]
ignore_missing_imports = True