Added colors/labels for each short range interaction. Added cumulative line for long range exposure

This commit is contained in:
Luis Aleixo 2022-03-18 11:53:01 +00:00
parent 3965123393
commit c19ccbafe7
6 changed files with 166 additions and 85 deletions

View file

@ -245,11 +245,17 @@ class FormData:
else:
humidity = 0.5
room = models.Room(volume=volume, humidity=humidity)
if self.short_range_option == "short_range_yes":
sr_presence=self.short_range_intervals()
sr_activities=self.short_range_activities()
short_range_expirations = tuple(short_range_expiration_distributions[activity] for activity in sr_activities)
dilutions=dilution_factor(activities=sr_activities, distance=np.random.uniform(0.5, 1.5, 250000))
else:
sr_presence=()
short_range_expirations=()
dilutions=()
sr_presence=self.short_range_intervals()
sr_activities=self.short_range_activities()
short_range_expirations = tuple(short_range_expiration_distributions[activity] for activity in sr_activities)
# Initializes and returns a model with the attributes defined above
return mc.ExposureModel(
concentration_model=mc.ConcentrationModel(
@ -261,7 +267,7 @@ class FormData:
short_range = mc.ShortRangeModel(
presence=sr_presence,
expirations=short_range_expirations,
dilutions=dilution_factor(activities=sr_activities, distance=np.random.uniform(0.5, 1.5, 250000)),
dilutions=dilutions,
),
exposed=self.exposed_population(),
)
@ -639,15 +645,13 @@ class FormData:
)
def short_range_intervals(self) -> typing.Tuple[typing.Tuple[str, models.SpecificInterval], ...]:
if (self.short_range_interactions):
short_range_intervals = []
for interaction in self.short_range_interactions:
start_time = time_string_to_minutes(interaction['start_time'])
duration = float(interaction['duration'])
short_range_intervals.append((interaction['activity'], models.SpecificInterval((start_time/60, (start_time + duration)/60))))
return tuple(short_range_intervals)
else:
return ()
short_range_intervals = []
for interaction in self.short_range_interactions:
start_time = time_string_to_minutes(interaction['start_time'])
duration = float(interaction['duration'])
short_range_intervals.append((interaction['activity'], models.SpecificInterval((start_time/60, (start_time + duration)/60))))
return tuple(short_range_intervals)
def exposed_present_interval(self) -> models.Interval:
return self.present_interval(

View file

@ -127,12 +127,18 @@ def calculate_report_data(model: models.ExposureModel):
np.array(model.deposited_exposure_between_bounds(float(time1), float(time2))).mean()
for time1, time2 in zip(times[:-1], times[1:])
])
long_range_cumulative_doses = np.cumsum([
np.array(model.long_range_deposited_exposure_between_bounds(float(time1), float(time2))).mean()
for time1, time2 in zip(times[:-1], times[1:])
])
return {
"times": list(times),
"short_range_intervals": short_range_intervals,
"short_range_activities": short_range_activities,
"exposed_presence_intervals": [list(interval) for interval in model.exposed.presence.boundaries()],
"cumulative_doses": list(cumulative_doses),
"long_range_cumulative_doses": list(long_range_cumulative_doses),
"short_range_concentrations": short_range_concentrations,
"concentrations": sr_breathing_concentrations,
"highest_const": highest_const,

View file

@ -405,7 +405,7 @@ function validate_form(form) {
}
// Generate the short range interactions list
let short_range_interactions = [];
var short_range_interactions = [];
$(".form_field_outer_row").each(function (index, element){
let obj = {};
obj.activity = $(element).find("[name='short_range_activity']").val();
@ -413,6 +413,11 @@ function validate_form(form) {
obj.duration = $(element).find("[name='short_range_duration']").val();
short_range_interactions.push(JSON.stringify(obj));
});
// Sort list by time
short_range_interactions.sort(function (a, b) {
return JSON.parse(a).start_time.localeCompare(JSON.parse(b).start_time);
});
$("input[type=text][name=short_range_interactions]").val('[' + short_range_interactions + ']');
if (short_range_interactions.length == 0) {
$("input[type=radio][id=short_range_no]").prop("checked", true);

View file

@ -1,17 +1,22 @@
/* Generate the concentration plot using d3 library. */
function draw_plot(svg_id, times, concentrations, short_range_concentrations, cumulative_doses, exposed_presence_intervals, short_range_intervals) {
function draw_plot(svg_id, times, concentrations, short_range_concentrations,
cumulative_doses, long_range_cumulative_doses, exposed_presence_intervals,
short_range_intervals, short_range_activities) {
// Used for controlling the short range interactions
let button_full_exposure = document.getElementById("button_full_exposure");
let button_long_exposure = document.getElementById("button_long_exposure");
let long_range_checkbox = document.getElementById('long_range_cumulative_checkbox')
let show_sr_legend = button_full_exposure || Math.round(Math.max(...concentrations)) == Math.round(Math.max(...short_range_concentrations))
var data_for_graphs = {
'concentrations': [],
'cumulative_doses': [],
'long_range_cumulative_doses': [],
}
times.map((time, index) => data_for_graphs.concentrations.push({ 'time': time, 'hour': new Date().setHours(Math.trunc(time), (time - Math.trunc(time)) * 60), 'concentration': short_range_concentrations[index]}));
times.map((time, index) => data_for_graphs.cumulative_doses.push({ 'time': time, 'hour': new Date().setHours(Math.trunc(time), (time - Math.trunc(time)) * 60), 'concentration': cumulative_doses[index]}));
times.map((time, index) => data_for_graphs.long_range_cumulative_doses.push({ 'time': time, 'hour': new Date().setHours(Math.trunc(time), (time - Math.trunc(time)) * 60), 'concentration': long_range_cumulative_doses[index]}));
// Add main SVG element
var plot_div = document.getElementById(svg_id);
@ -24,10 +29,10 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
bisecHour = d3.bisector((d) => { return d.hour; }).left,
yRange = d3.scaleLinear(),
yCumulativeRange = d3.scaleLinear().domain([0., Math.max(...cumulative_doses)*1.1]),
yCumulativeRange = d3.scaleLinear(),
yAxis = d3.axisLeft();
yCumulativeAxis = d3.axisRight(yCumulativeRange).ticks(4);
yAxis = d3.axisLeft(),
yCumulativeAxis = d3.axisRight();
// X axis declaration.
var xAxisEl = vis.append('svg:g')
@ -54,7 +59,6 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
// Y cumulative concentration axis declaration.
var yAxisCumEl = vis.append('svg:g')
.attr('class', 'y axis')
.style('font-size', 14)
.style("stroke-dasharray", "5 5");
// Y cumulated concentration axis label.
@ -81,12 +85,17 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
.attr('fill', '#1f77b4')
.attr('fill-opacity', '0.1');
sr_unique_activities = [...new Set(short_range_activities)]
if (show_sr_legend) {
var legendShortRangeAreaIcon = vis.append('rect')
var legendShortRangeAreaIcon = {};
sr_unique_activities.forEach((b, index) => {
legendShortRangeAreaIcon[index] = vis.append('rect')
.attr('width', 20)
.attr('height', 15)
.attr('fill', '#1f00b4')
.attr('fill-opacity', '0.1');
.attr('height', 15);
if (sr_unique_activities[index] == 'Breathing') legendShortRangeAreaIcon[index].attr('fill', 'red').attr('fill-opacity', '0.2');
else if (sr_unique_activities[index] == 'Speaking') legendShortRangeAreaIcon[index].attr('fill', 'green').attr('fill-opacity', '0.1');
else legendShortRangeAreaIcon[index].attr('fill', 'blue').attr('fill-opacity', '0.1');
});
}
var legendLineText = vis.append('text')
@ -105,14 +114,17 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
.attr('alignment-baseline', 'central');
if (show_sr_legend) {
var legendShortRangeText = vis.append('text')
.text('Short range interaction(s)')
var legendShortRangeText = {};
sr_unique_activities.forEach((b, index) => {
legendShortRangeText[index] = vis.append('text')
.text('Short range - ' + sr_unique_activities[index])
.style('font-size', '15px')
.attr('alignment-baseline', 'central');
});
}
// Legend bounding
if (show_sr_legend) legendBBox_height = 90;
if (show_sr_legend) legendBBox_height = 68 + 20 * sr_unique_activities.length;
else legendBBox_height = 68;
var legendBBox = vis.append('rect')
.attr('width', 255)
@ -140,12 +152,23 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
// Line representing the cumulative concentration.
var lineCumulative = d3.line();
var draw_cumulative_line = vis.append('svg:path')
var draw_cumulative_line = draw_area.append('svg:path')
.attr('stroke', '#1f77b4')
.attr('stroke-width', 2)
.style("stroke-dasharray", "5 5")
.attr('fill', 'none');
// Line representing the long range cumulative concentration.
if (show_sr_legend) {
var longRangeCumulative = d3.line();
var draw_long_range_cumulative_line = draw_area.append('svg:path')
.attr('stroke', 'purple')
.attr('stroke-width', 2)
.style("stroke-dasharray", "5 5")
.attr('fill', 'none')
.attr('opacity', 0);
}
// Area representing the presence of exposed person(s).
var exposedArea = {};
var drawArea = {};
@ -161,10 +184,11 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
var drawShortRangeArea = {};
short_range_intervals.forEach((b, index) => {
shortRangeArea[index] = d3.area();
drawShortRangeArea[index] = draw_area.append('svg:path')
.attr('class', 'draw_short_range_area')
.attr('fill', '#1f00b4')
.attr('fill-opacity', '0.1');
drawShortRangeArea[index] = draw_area.append('svg:path');
if (short_range_activities[index] == 'Breathing') drawShortRangeArea[index].attr('fill', 'red').attr('fill-opacity', '0.2');
else if (short_range_activities[index] == 'Speaking') drawShortRangeArea[index].attr('fill', 'green').attr('fill-opacity', '0.1');
else drawShortRangeArea[index].attr('fill', 'blue').attr('fill-opacity', '0.1');
});
// Tooltip.
@ -202,9 +226,12 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
.attr('pointer-events', 'all');
}
function update_concentration_plot(data) {
yRange.domain([0., Math.max(...data)]);
function update_concentration_plot(concentration_data, cumulative_data) {
yRange.domain([0., Math.max(...concentration_data)*1.1]);
yAxisEl.transition().duration(1000).call(yAxis);
yCumulativeRange.domain([0., Math.max(...cumulative_data)*1.1]);
yAxisCumEl.transition().duration(1000).call(yCumulativeAxis)
// Concentration line
lineFunc.defined(d => !isNaN(d.concentration))
@ -215,6 +242,24 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
.duration(1000)
.attr("d", lineFunc(data_for_graphs.concentrations));
// Cumulative line.
lineCumulative.defined(d => !isNaN(d.concentration))
.x(d => xTimeRange(d.time))
.y(d => yCumulativeRange(d.concentration));
draw_cumulative_line.transition()
.duration(1000)
.attr("d", lineCumulative(data_for_graphs.cumulative_doses));
// Long range cumulative line.
if (show_sr_legend) {
longRangeCumulative.defined(d => !isNaN(d.concentration))
.x(d => xTimeRange(d.time))
.y(d => yCumulativeRange(d.concentration));
draw_long_range_cumulative_line.transition()
.duration(1000)
.attr("d", lineCumulative(data_for_graphs.long_range_cumulative_doses));
}
// Area.
exposed_presence_intervals.forEach((b, index) => {
exposedArea[index].x(d => xTimeRange(d.time))
@ -327,6 +372,7 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
// Axis.
var xAxis = d3.axisBottom(xRange).tickFormat(d => time_format(d));
yAxis.scale(yRange);
yCumulativeAxis.scale(yCumulativeRange);
xAxisEl.attr('transform', 'translate(0,' + (graph_height - margins.bottom) + ')')
.call(xAxis);
@ -374,10 +420,12 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
.attr('y', margins.top + 3 * size);
if (show_sr_legend) {
legendShortRangeAreaIcon.attr('x', graph_width + size * 2.5)
.attr('y', margins.top + 3.6 * size);
legendShortRangeText.attr('x', graph_width + 4 * size)
.attr('y', margins.top + 4 * size);
sr_unique_activities.forEach((b, index) => {
legendShortRangeAreaIcon[index].attr('x', graph_width + size * 2.5)
.attr('y', margins.top + 3.6 * size + index * size);
legendShortRangeText[index].attr('x', graph_width + 4 * size)
.attr('y', margins.top + 4 * size + index * size);
});
}
legendBBox.attr('x', graph_width * 1.07)
@ -403,10 +451,12 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
.attr('y', graph_height + 2.6 * size);
if (show_sr_legend) {
legendShortRangeAreaIcon.attr('x', size * 0.50)
.attr('y', graph_height * 1.175 + size);
legendShortRangeText.attr('x', 2 * size)
.attr('y', graph_height + 3.65 * size);
sr_unique_activities.forEach((b, index) => {
legendShortRangeAreaIcon[index].attr('x', size * 0.50)
.attr('y', graph_height * 1.175 + size + index * size);
legendShortRangeText[index].attr('x', 2 * size)
.attr('y', graph_height + 3.65 * size + index * size);
});
}
legendBBox.attr('x', 1)
@ -419,23 +469,25 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
.attr('height', graph_height);
}
// Cumulative line.
lineCumulative.defined(d => !isNaN(d.concentration))
.x(d => xTimeRange(d.time))
.y(d => yCumulativeRange(d.concentration));
draw_cumulative_line.attr("d", lineCumulative(data_for_graphs.cumulative_doses));
}
if (long_range_checkbox) {
long_range_checkbox.addEventListener("click", () => {
if (long_range_checkbox.checked) draw_long_range_cumulative_line.transition().duration(1000).attr("opacity", 1);
else draw_long_range_cumulative_line.transition().duration(1000).attr("opacity", 0);
});
};
if (button_full_exposure) {
button_full_exposure.addEventListener("click", () => {
update_concentration_plot(short_range_concentrations);
update_concentration_plot(short_range_concentrations, cumulative_doses);
button_full_exposure.disabled = true;
button_long_exposure.disabled = false;
});
}
if (button_long_exposure) {
button_long_exposure.addEventListener("click", () => {
update_concentration_plot(concentrations);
update_concentration_plot(concentrations, long_range_cumulative_doses);
button_full_exposure.disabled = false;
button_long_exposure.disabled = true;
});
@ -443,13 +495,13 @@ function draw_plot(svg_id, times, concentrations, short_range_concentrations, cu
// Draw for the first time to initialize.
redraw();
update_concentration_plot(short_range_concentrations);
update_concentration_plot(short_range_concentrations, cumulative_doses);
// Redraw based on the new size whenever the browser window is resized.
window.addEventListener("resize", e => {
redraw();
if (button_full_exposure.disabled) update_concentration_plot(short_range_concentrations);
else update_concentration_plot(concentrations)
if (button_full_exposure.disabled) update_concentration_plot(short_range_concentrations, cumulative_doses);
else update_concentration_plot(concentrations, long_range_cumulative_doses)
});

View file

@ -89,19 +89,26 @@
{% endblock report_summary_footnote %}
</div>
<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>
{% if (form.short_range_option == "short_range_yes" and concentrations|max|int != short_range_concentrations|max|int ) %}
<button class="btn btn-sm btn-primary" id="button_full_exposure" disabled>Show full exposure</button>
<button class="btn btn-sm btn-primary ml-0" id="button_long_exposure">Hide high concentration</button>
{% endif %}
{% if form.short_range_option == "short_range_yes" %}
{% if concentrations|max|int != short_range_concentrations|max|int %}
<button class="btn btn-sm btn-primary" id="button_full_exposure" disabled>Show full exposure</button>
<button class="btn btn-sm btn-primary ml-0" id="button_long_exposure">Hide high concentration</button>
{% endif %}
<input type="checkbox" id="long_range_cumulative_checkbox">
<label class="form-check-label" for="long_range_cumulative_checkbox">Show long range cumulative doses</label>
{% endif %}
<div id="concentration_plot" style="height: 400px"></div>
<script type="application/javascript">
var times = {{ times | JSONify }}
var concentrations = {{ concentrations | JSONify }}
var short_range_concentrations = {{ short_range_concentrations | JSONify }}
var cumulative_doses = {{ cumulative_doses | JSONify }}
var long_range_cumulative_doses = {{ long_range_cumulative_doses | JSONify }}
var exposed_presence_intervals = {{ exposed_presence_intervals | JSONify }}
var short_range_intervals = {{ short_range_intervals | JSONify }}
draw_plot("concentration_plot", times, concentrations, short_range_concentrations, cumulative_doses, exposed_presence_intervals, short_range_intervals)
var short_range_activities = {{ short_range_activities | JSONify }}
draw_plot("concentration_plot", times, concentrations, short_range_concentrations, cumulative_doses,
long_range_cumulative_doses, exposed_presence_intervals, short_range_intervals, short_range_activities)
</script>
</p>
</div>

View file

@ -1212,6 +1212,38 @@ class ExposureModel:
return (self.concentration_model.concentration(time) +
self.short_range.short_range_concentration(self.concentration_model, time))
def long_range_deposited_exposure_between_bounds(self, time1: float, time2: float) -> _VectorisedFloat:
deposited_exposure = 0.
emission_rate_per_aerosol = self.concentration_model.infected.emission_rate_per_aerosol_when_present()
aerosols = self.concentration_model.infected.aerosols()
f_inf = self.concentration_model.infected.fraction_of_infectious_virus()
fdep = self.long_range_fraction_deposited()
diameter = self.concentration_model.infected.particle.diameter
if not np.isscalar(diameter) and diameter is not None:
# we compute first the mean of all diameter-dependent quantities
# to perform properly the Monte-Carlo integration over
# particle diameters (doing things in another order would
# lead to wrong results).
dep_exposure_integrated = np.array(self._long_range_normed_exposure_between_bounds(time1, time2) *
aerosols *
fdep).mean()
else:
# in the case of a single diameter or no diameter defined,
# one should not take any mean at this stage.
dep_exposure_integrated = self._long_range_normed_exposure_between_bounds(time1, time2)*aerosols*fdep
# then we multiply by the diameter-independent quantity emission_rate_per_aerosol,
# and parameters of the vD equation (i.e. BR_k and n_in).
deposited_exposure += (dep_exposure_integrated * emission_rate_per_aerosol *
self.exposed.activity.inhalation_rate *
(1 - self.exposed.mask.inhale_efficiency()))
# In the end we multiply the final results by the fraction of infectious virus of the vD equation.
return deposited_exposure * f_inf
def deposited_exposure_between_bounds(self, time1: float, time2: float) -> _VectorisedFloat:
"""
The number of virus per m^3 deposited on the respiratory tract
@ -1257,36 +1289,11 @@ class ExposureModel:
# then we multiply by the diameter-independent quantity virus viral load
deposited_exposure *= self.concentration_model.virus.viral_load_in_sputum
# Long range concentration
emission_rate_per_aerosol = self.concentration_model.infected.emission_rate_per_aerosol_when_present()
# long range concentration
f_inf = self.concentration_model.infected.fraction_of_infectious_virus()
aerosols = self.concentration_model.infected.aerosols()
fdep = self.long_range_fraction_deposited()
deposited_exposure += self.long_range_deposited_exposure_between_bounds(time1, time2)/f_inf
diameter = self.concentration_model.infected.particle.diameter
if not np.isscalar(diameter) and diameter is not None:
# we compute first the mean of all diameter-dependent quantities
# to perform properly the Monte-Carlo integration over
# particle diameters (doing things in another order would
# lead to wrong results).
dep_exposure_integrated = np.array(self._long_range_normed_exposure_between_bounds(time1, time2) *
aerosols *
fdep).mean()
else:
# in the case of a single diameter or no diameter defined,
# one should not take any mean at this stage.
dep_exposure_integrated = self._long_range_normed_exposure_between_bounds(time1, time2)*aerosols*fdep
# then we multiply by the diameter-independent quantity emission_rate_per_aerosol,
# and parameters of the vD equation (i.e. BR_k and n_in).
deposited_exposure += (dep_exposure_integrated * emission_rate_per_aerosol *
self.exposed.activity.inhalation_rate *
(1 - self.exposed.mask.inhale_efficiency()))
# In the end we multiply the final results by the fraction of infectious virus of the vD equation.
return f_inf * deposited_exposure
return deposited_exposure * f_inf
def deposited_exposure(self) -> _VectorisedFloat:
"""