diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index da43e39c..b05da082 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -13,8 +13,8 @@ from cara import data import cara.data.weather import cara.monte_carlo as mc from .. import calculator -from cara.monte_carlo.data import activity_distributions, dilution_factor, virus_distributions, mask_distributions, initial_concentrations_mouth -from cara.monte_carlo.data import expiration_distribution, expiration_BLO_factors, expiration_distributions +from cara.monte_carlo.data import activity_distributions, virus_distributions, mask_distributions, dilution_factor +from cara.monte_carlo.data import expiration_distribution, expiration_BLO_factors, expiration_distributions, short_range_expiration_distributions LOG = logging.getLogger(__name__) @@ -234,7 +234,7 @@ class FormData: raise ValueError("mechanical_ventilation_type cannot be 'not-applicable' if " "ventilation_type is 'mechanical_ventilation'") - def build_mc_model(self) -> mc.ExposureModel: + def build_mc_model(self) -> mc.SimulationModel: # Initializes room with volume either given directly or as product of area and height if self.volume_type == 'room_volume_explicit': volume = self.room_volume @@ -253,28 +253,29 @@ class FormData: sr_presence=self.short_range_intervals() sr_activities=self.short_range_activities() - jet_origin_concentration = [initial_concentrations_mouth[activity] for activity in sr_activities] - - short_range = models.ShortRangeModel( - presence=sr_presence, - activities=sr_activities, - dilutions=dilution_factor(activities=sr_activities, distance=np.random.uniform(0.5, 1.5, 250000)), - jet_origin_concentrations=jet_origin_concentration, - ) + short_range_expirations = [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( - room=room, - ventilation=self.ventilation(), - infected=self.infected_population(), - short_range=short_range, - evaporation_factor=0.3, + return mc.SimulationModel( + mc.ExposureModel( + concentration_model=mc.ConcentrationModel( + room=room, + ventilation=self.ventilation(), + infected=self.infected_population(), + evaporation_factor=0.3, + ), + exposed=self.exposed_population(), + ), + mc.ShortRangeModel( + presence=sr_presence, + long_range_presence=self.long_range_intervals(), + activities=sr_activities, + expirations=short_range_expirations, + dilutions=dilution_factor(activities=sr_activities, distance=np.random.uniform(0.5, 1.5, 250000)), ), - exposed=self.exposed_population(), ) - def build_model(self, sample_size=_DEFAULT_MC_SAMPLE_SIZE) -> models.ExposureModel: + def build_model(self, sample_size=_DEFAULT_MC_SAMPLE_SIZE) -> models.SimulationModel: return self.build_mc_model().build_model(size=sample_size) def tz_name_and_utc_offset(self) -> typing.Tuple[str, float]: @@ -643,7 +644,7 @@ class FormData: def infected_present_interval(self) -> models.Interval: return self.present_interval( self.infected_start, self.infected_finish, - breaks=self.infected_lunch_break_times() + self.infected_coffee_break_times(), + breaks=self.infected_lunch_break_times() + self.infected_coffee_break_times() ) def short_range_intervals(self) -> typing.List[models.SpecificInterval]: @@ -657,10 +658,22 @@ class FormData: else: return [] + def long_range_intervals(self) -> models.Interval: + short_range_intervals = [] + for interval in self.short_range_interactions: + short_range_intervals.append( + (time_string_to_minutes(interval['start_time']), + time_string_to_minutes(interval['start_time']) + float(interval['duration'])), + ) + return self.present_interval( + self.exposed_start, self.exposed_finish, + breaks=self.infected_lunch_break_times() + self.infected_coffee_break_times() + tuple(short_range_intervals), + ) + def exposed_present_interval(self) -> models.Interval: return self.present_interval( self.exposed_start, self.exposed_finish, - breaks=self.exposed_lunch_break_times() + self.exposed_coffee_break_times(), + breaks=self.exposed_lunch_break_times() + self.exposed_coffee_break_times() ) def short_range_activities(self) -> typing.List[str]: @@ -678,12 +691,6 @@ def build_expiration(expiration_definition) -> models._ExpirationBase: for exp_type, weight in expiration_definition.items() ], axis=0) return expiration_distribution(tuple(BLO_factors)) - - -def build_concentration_mouth(short_range_def) -> models._ExpirationBase: - expiration_definition = short_range_def[1]['activity'] - if isinstance(expiration_definition, str): - return initial_concentrations_mouth[expiration_definition] def baseline_raw_form_data(): diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index 258ed6bf..ef8bafc4 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -96,18 +96,29 @@ def interesting_times(model: models.ExposureModel, approx_n_pts=100) -> typing.L return nice_times -def calculate_report_data(model: models.ExposureModel): - times = interesting_times(model) +def short_range_interesting_times(model: models.ExposureModel, times: typing.List[float]) -> typing.List[float]: + short_range_times : typing.List[float] = [] + for period in model.concentration_model.infected.short_range_presence: + start, finish = tuple(period.boundaries()) + short_range_times = short_range_times + [time for time in times if time >= start and time <= finish] + return short_range_times + + +def calculate_report_data(model: models.SimulationModel): + times = interesting_times(model.exposure_model) + short_range_intervals = [] + for interval in model.short_range.presence: + short_range_intervals.append(list(interval.boundaries())) concentrations = [ - np.array(model.concentration_model.concentration(float(time))).mean() + np.array(model.concentration(float(time))).mean() for time in times ] highest_const = max(concentrations) prob = np.array(model.infection_probability()).mean() - er = np.array(model.concentration_model.infected.emission_rate_when_present()).mean() - exposed_occupants = model.exposed.number - expected_new_cases = np.array(model.expected_new_cases()).mean() + er = np.array(model.exposure_model.concentration_model.infected.emission_rate_when_present()).mean() + exposed_occupants = model.exposure_model.exposed.number + expected_new_cases = np.array(model.exposure_model.expected_new_cases()).mean() cumulative_doses = np.cumsum([ np.array(model.deposited_exposure_between_bounds(float(time1), float(time2))).mean() for time1, time2 in zip(times[:-1], times[1:]) @@ -115,7 +126,8 @@ def calculate_report_data(model: models.ExposureModel): return { "times": list(times), - "exposed_presence_intervals": [list(interval) for interval in model.exposed.presence.boundaries()], + "short_range_intervals": short_range_intervals, + "exposed_presence_intervals": [list(interval) for interval in model.exposure_model.exposed.presence.boundaries()], "cumulative_doses": list(cumulative_doses), "concentrations": concentrations, "highest_const": highest_const, @@ -234,20 +246,20 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, mc.Exp return scenarios -def scenario_statistics(mc_model: mc.ExposureModel, sample_times: np.ndarray): +def scenario_statistics(mc_model: mc.SimulationModel, sample_times: np.ndarray): model = mc_model.build_model(size=_DEFAULT_MC_SAMPLE_SIZE) return { - 'probability_of_infection': np.mean(model.infection_probability()), - 'expected_new_cases': np.mean(model.expected_new_cases()), + 'probability_of_infection': np.mean(model.exposure_model.infection_probability()), + 'expected_new_cases': np.mean(model.exposure_model.expected_new_cases()), 'concentrations': [ - np.mean(model.concentration_model.concentration(time)) + np.mean(model.exposure_model.concentration_model.concentration(time)) for time in sample_times ], } def comparison_report( - scenarios: typing.Dict[str, mc.ExposureModel], + scenarios: typing.Dict[str, mc.SimulationModel], sample_times: typing.List[float], executor_factory: typing.Callable[[], concurrent.futures.Executor], ): @@ -285,7 +297,7 @@ class ReportGenerator: def prepare_context( self, base_url: str, - model: models.ExposureModel, + model: models.SimulationModel, form: FormData, executor_factory: typing.Callable[[], concurrent.futures.Executor], ) -> dict: @@ -293,12 +305,12 @@ class ReportGenerator: time = now.strftime("%Y-%m-%d %H:%M:%S UTC") context = { - 'model': model, + 'model': model.exposure_model, 'form': form, 'creation_date': time, } - scenario_sample_times = interesting_times(model) + scenario_sample_times = interesting_times(model.exposure_model) context.update(calculate_report_data(model)) alternative_scenarios = manufacture_alternative_scenarios(form) diff --git a/cara/apps/calculator/static/js/form.js b/cara/apps/calculator/static/js/form.js index 575179bd..b041ffa3 100644 --- a/cara/apps/calculator/static/js/form.js +++ b/cara/apps/calculator/static/js/form.js @@ -404,6 +404,10 @@ function validate_form(form) { short_range_interactions.push(JSON.stringify(obj)); }); $("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); + on_short_range_option_change(); + } if (submit) { $("#generate_report").prop("disabled", true); @@ -859,7 +863,11 @@ $(document).ready(function () { //Short range modal - save button $("body").on("click", ".save_btn_frm_field", function() { var last_element = $(".form_field_outer").find(".form_field_outer_row").last().find(".short_range_option").prop("id"); - if (!last_element) $('#short_range_dialog').modal('hide'); + if (!last_element) { + $('#short_range_dialog').modal('hide'); + $("input[type=radio][id=short_range_no]").prop("checked", true); + on_short_range_option_change(); + } else { let index = last_element.split("_").slice(-1)[0]; let activity = validate_sr_parameter('#sr_activity_no_' + String(index)[0], "You must specify the activity type."); @@ -875,13 +883,14 @@ $(document).ready(function () { } }); - //Short range modal - close button + //Short range modal - reset button $("body").on("click", ".dismiss_btn_frm_field", function() { $(".form_field_outer_row").remove(); $("#sr_interactions").text(0); + $('input[type=radio][id=short_range_no]').prop("checked", true); + on_short_range_option_change(); }); - }); diff --git a/cara/apps/calculator/static/js/report.js b/cara/apps/calculator/static/js/report.js index c9c79841..a185b094 100644 --- a/cara/apps/calculator/static/js/report.js +++ b/cara/apps/calculator/static/js/report.js @@ -1,5 +1,5 @@ /* Generate the concentration plot using d3 library. */ -function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses, exposed_presence_intervals) { +function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses, exposed_presence_intervals, short_range_intervals) { var time_format = d3.timeFormat('%H:%M'); @@ -50,6 +50,16 @@ function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses .attr('fill-opacity', '0.1'); }); + // Area representing the short range interaction(s). + var shortRangeArea = {}; + var drawShortRangeArea = {}; + short_range_intervals.forEach((b, index) => { + shortRangeArea[index] = d3.area(); + drawShortRangeArea[index] = vis.append('svg:path') + .attr('fill', '#1f00b4') + .attr('fill-opacity', '0.1'); + }); + // Plot tittle. var plotTitleEl = vis.append('svg:foreignObject') .attr("background-color", "transparent") @@ -105,10 +115,16 @@ function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses var legendAreaIcon = vis.append('rect') .attr('width', 20) - .attr('height', 20) + .attr('height', 15) .attr('fill', '#1f77b4') .attr('fill-opacity', '0.1'); + var legendShortRangeAreaIcon = vis.append('rect') + .attr('width', 20) + .attr('height', 15) + .attr('fill', '#1f00b4') + .attr('fill-opacity', '0.1'); + var legendLineText = vis.append('text') .text('Mean concentration') .style('font-size', '15px') @@ -124,10 +140,15 @@ function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses .style('font-size', '15px') .attr('alignment-baseline', 'central'); + var legendShortRangeText = vis.append('text') + .text('Short range interaction(s)') + .style('font-size', '15px') + .attr('alignment-baseline', 'central'); + // Legend bounding var legendBBox = vis.append('rect') .attr('width', 255) - .attr('height', 70) + .attr('height', 90) .attr('stroke', 'lightgrey') .attr('stroke-width', '2') .attr('rx', '5px') @@ -217,7 +238,7 @@ function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses .y(d => yRange(d.concentration)); draw_line.attr("d", lineFunc(data_for_graphs.concentrations)); - // Cumulative line + // Cumulative line. lineCumulative.defined(d => !isNaN(d.concentration)) .x(d => xTimeRange(d.time)) .y(d => yCumulativeRange(d.concentration)); @@ -234,6 +255,18 @@ function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses }))); }); + // Short Range Area. + short_range_intervals.forEach((b, index) => { + shortRangeArea[index].x(d => xTimeRange(d.time)) + .y0(graph_height - margins.bottom) + .y1(d => yRange(d.concentration)); + + drawShortRangeArea[index].attr('d', shortRangeArea[index](data_for_graphs.concentrations.filter(d => { + return d.time >= b[0] && d.time <= b[1] + }))) + }) + + // Title. plotTitleEl.attr('width', graph_width); @@ -282,10 +315,15 @@ function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses .attr('y', margins.top + 2 * size); legendAreaIcon.attr('x', graph_width + size * 2.5) - .attr('y', margins.top + 2.5 * size); + .attr('y', margins.top + 2.6 * size); legendAreaText.attr('x', graph_width + 4 * size) .attr('y', margins.top + 3 * size); + 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); + legendBBox.attr('x', graph_width * 1.07) .attr('y', margins.top * 1.2); } @@ -306,7 +344,12 @@ function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses legendAreaIcon.attr('x', size * 0.50) .attr('y', graph_height * 1.09 + size); legendAreaText.attr('x', 2 * size) - .attr('y', graph_height + 2.7 * size); + .attr('y', graph_height + 2.6 * size); + + 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); legendBBox.attr('x', 1) .attr('y', graph_height); diff --git a/cara/apps/expert.py b/cara/apps/expert.py index a7f82243..bba12b6b 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -501,7 +501,6 @@ baseline_model = models.ExposureModel( expiration=models.Expiration.types['Speaking'], host_immunity=0., ), - short_range=[], evaporation_factor=0.3, ), exposed=models.Population( diff --git a/cara/apps/templates/base/calculator.report.html.j2 b/cara/apps/templates/base/calculator.report.html.j2 index b113710b..7278aab5 100644 --- a/cara/apps/templates/base/calculator.report.html.j2 +++ b/cara/apps/templates/base/calculator.report.html.j2 @@ -96,7 +96,8 @@ var concentrations = {{ concentrations | JSONify }} var cumulative_doses = {{ cumulative_doses | JSONify }} var exposed_presence_intervals = {{ exposed_presence_intervals | JSONify }} - draw_concentration_plot("concentration_plot", times, concentrations, cumulative_doses, exposed_presence_intervals); + var short_range_intervals = {{ short_range_intervals | JSONify }} + draw_concentration_plot("concentration_plot", times, concentrations, cumulative_doses, exposed_presence_intervals, short_range_intervals);

@@ -309,6 +310,18 @@ Gym = For comparison only, all persons doing heavy physical exercise, breathing and not speaking. {% endif %}

+ {% if form.short_range_option == "short_range_yes" %} +
  • + Short range interactions: {{ form.short_range_interactions|length }} +

  • + + {% endif %}
  • Exposed occupant(s) activity time: