From b39131c14c7f7da2f4762579ad0bd829958a2b04 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Fri, 1 Dec 2023 07:30:29 +0100 Subject: [PATCH 1/7] Putting f_inf in emission rate, in models.py, and removing it from dose computation --- caimira/models.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 476a71b3..fd5121cb 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -979,6 +979,7 @@ class InfectedPopulation(_PopulationWithVirus): ER = (self.virus.viral_load_in_sputum * self.activity.exhalation_rate * + self.fraction_of_infectious_virus() * 10 ** 6) return ER @@ -1641,7 +1642,6 @@ class ExposureModel: emission_rate_per_aerosol_per_person = \ self.concentration_model.infected.emission_rate_per_aerosol_per_person_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 @@ -1667,7 +1667,7 @@ class ExposureModel: (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 + return deposited_exposure def deposited_exposure_between_bounds(self, time1: float, time2: float) -> _VectorisedFloat: """ @@ -1716,9 +1716,8 @@ class ExposureModel: # Then we multiply by diameter-independent quantities: viral load # and fraction of infected virions - f_inf = self.concentration_model.infected.fraction_of_infectious_virus() - deposited_exposure *= (f_inf - * self.concentration_model.virus.viral_load_in_sputum + deposited_exposure *= ( + self.concentration_model.virus.viral_load_in_sputum * (1 - self.exposed.mask.inhale_efficiency())) # Long-range concentration deposited_exposure += self.long_range_deposited_exposure_between_bounds(time1, time2) From 5b4345e40a0b5a3babc6864c32efb44400d7a992 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Fri, 1 Dec 2023 07:32:03 +0100 Subject: [PATCH 2/7] Multiplying all known concentrations in test_exposure_model by f_inf=0.5, as concentration is now with f_inf included --- caimira/tests/models/test_exposure_model.py | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/caimira/tests/models/test_exposure_model.py b/caimira/tests/models/test_exposure_model.py index bfe59c1c..07722dd6 100644 --- a/caimira/tests/models/test_exposure_model.py +++ b/caimira/tests/models/test_exposure_model.py @@ -75,19 +75,19 @@ def known_concentrations(func): @pytest.mark.parametrize( "population, cm, expected_exposure, expected_probability", [ - [populations[1], known_concentrations(lambda t: 36.), + [populations[1], known_concentrations(lambda t: 18.), np.array([64.02320633, 59.45012016]), np.array([67.9503762594, 65.2366759251])], - [populations[2], known_concentrations(lambda t: 36.), + [populations[2], known_concentrations(lambda t: 18.), np.array([40.91708675, 45.73086166]), np.array([51.6749232285, 55.6374622042])], - [populations[0], known_concentrations(lambda t: np.array([36., 72.])), + [populations[0], known_concentrations(lambda t: np.array([18., 36.])), np.array([45.73086166, 91.46172332]), np.array([55.6374622042, 80.3196524031])], - [populations[1], known_concentrations(lambda t: np.array([36., 72.])), + [populations[1], known_concentrations(lambda t: np.array([18., 36.])), np.array([64.02320633, 118.90024032]), np.array([67.9503762594, 87.9151129926])], - [populations[2], known_concentrations(lambda t: np.array([36., 72.])), + [populations[2], known_concentrations(lambda t: np.array([18., 36.])), np.array([40.91708675, 91.46172332]), np.array([51.6749232285, 80.3196524031])], ]) def test_exposure_model_ndarray(population, cm, @@ -113,7 +113,7 @@ def test_exposure_model_ndarray(population, cm, ]) def test_exposure_model_ndarray_and_float_mix(population, expected_deposited_exposure, sr_model, cases_model): cm = known_concentrations( - lambda t: 0. if np.floor(t) % 2 else np.array([1.2, 1.2])) + lambda t: 0. if np.floor(t) % 2 else np.array([0.6, 0.6])) model = ExposureModel(cm, sr_model, population, cases_model) np.testing.assert_almost_equal( @@ -130,7 +130,7 @@ def test_exposure_model_ndarray_and_float_mix(population, expected_deposited_exp [populations[2], np.array([1.36390289, 1.52436206])], ]) def test_exposure_model_vector(population, expected_deposited_exposure, sr_model, cases_model): - cm_array = known_concentrations(lambda t: np.array([1.2, 1.2])) + cm_array = known_concentrations(lambda t: np.array([0.6, 0.6])) model_array = ExposureModel(cm_array, sr_model, population, cases_model) np.testing.assert_almost_equal( model_array.deposited_exposure(), np.array(expected_deposited_exposure) @@ -138,7 +138,7 @@ def test_exposure_model_vector(population, expected_deposited_exposure, sr_model def test_exposure_model_scalar(sr_model, cases_model): - cm_scalar = known_concentrations(lambda t: 1.2) + cm_scalar = known_concentrations(lambda t: 0.6) model_scalar = ExposureModel(cm_scalar, sr_model, populations[0], cases_model) expected_deposited_exposure = 1.52436206 np.testing.assert_almost_equal( @@ -234,7 +234,7 @@ def test_infectious_dose_vectorisation(sr_model, cases_model): expiration=models.Expiration.types['Speaking'], host_immunity=0., ) - cm = known_concentrations(lambda t: 1.2) + cm = known_concentrations(lambda t: 0.6) cm = replace(cm, infected=infected_population) presence_interval = models.SpecificInterval(((0., 1.),)) @@ -289,13 +289,13 @@ def test_prob_meet_infected_person(pop, cases, AB, exposed, infected, prob_meet_ @pytest.mark.parametrize( "exposed_population, cm, pop, cases, AB, probabilistic_exposure_probability",[ - [10, known_concentrations(lambda t: 36.), + [10, known_concentrations(lambda t: 18.), 100000, 68, 5, 41.50971131], - [10, known_concentrations(lambda t: 0.2), + [10, known_concentrations(lambda t: 0.1), 100000, 68, 5, 2.185785075], - [20, known_concentrations(lambda t: 72.), + [20, known_concentrations(lambda t: 36.), 100000, 68, 5, 64.09068488], - [30, known_concentrations(lambda t: 1.2), + [30, known_concentrations(lambda t: 0.6), 100000, 68, 5, 55.93154502], ]) def test_probabilistic_exposure_probability(sr_model, exposed_population, cm, @@ -396,8 +396,8 @@ def test_diameter_vectorisation_room(diameter_dependent_model, sr_model, cases_m @pytest.mark.parametrize( ["cm", "host_immunity", "expected_probability"], [ - [known_concentrations(lambda t: 36.), np.array([0.25, 0.5]), np.array([57.40415859, 41.03956914])], - [known_concentrations(lambda t: 36.), np.array([0., 1.]), np.array([67.95037626, 0.])], + [known_concentrations(lambda t: 18.), np.array([0.25, 0.5]), np.array([57.40415859, 41.03956914])], + [known_concentrations(lambda t: 18.), np.array([0., 1.]), np.array([67.95037626, 0.])], ] ) def test_host_immunity_vectorisation(sr_model, cases_model, cm, host_immunity, expected_probability): From 605a2d3fb7edeec847e3c6d8f9fd3cf737d7b9a8 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Fri, 1 Dec 2023 07:33:26 +0100 Subject: [PATCH 3/7] Adapting test_full_algorithm to new concentration with f_inf included (f_inf and HI displaced in SimpleConcentrationModel) --- caimira/tests/test_full_algorithm.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/caimira/tests/test_full_algorithm.py b/caimira/tests/test_full_algorithm.py index 9f2ee70d..a26f3555 100644 --- a/caimira/tests/test_full_algorithm.py +++ b/caimira/tests/test_full_algorithm.py @@ -56,6 +56,12 @@ class SimpleConcentrationModel: #: Number of infected people num_infected: int = 1 + #: Fraction of infected viruses + finf: _VectorisedFloat = 0.5 + + #: Host immunity factor (0. for not immune) + HI: _VectorisedFloat = 0. + #: Relative humidity RH humidity: float = 0.3 @@ -176,7 +182,7 @@ class SimpleConcentrationModel: return ( ( (0 if not self.infected_presence.triggered(t) else self.f(lambda_rate,0)) + result * np.exp(-lambda_rate*(t-ti)) ) - * self.num_infected/self.room_volume) + * self.num_infected * self.finf * (1. - self.HI) / self.room_volume) @dataclass(frozen=True) @@ -295,12 +301,6 @@ class SimpleExposureModel(SimpleConcentrationModel): interaction intervals are within presence intervals of the infected. """ - #: Fraction of infected viruses - finf: _VectorisedFloat = 0.5 - - #: Host immunity factor (0. for not immune) - HI: _VectorisedFloat = 0. - #: Infectious dose (ID50) ID50: _VectorisedFloat = 50. @@ -410,7 +410,7 @@ class SimpleExposureModel(SimpleConcentrationModel): else self.f_with_fdep(lambda_rate,0,evaporation)*(t2-t1)) + (primitive(t2) * np.exp(-lambda_rate*(t2-ti)) - primitive(t1) * np.exp(-lambda_rate*(t1-ti)) ) ) - * self.num_infected/self.room_volume) + * self.num_infected * self.finf * (1. - self.HI) / self.room_volume) @method_cache def integrated_shortrange_concentration(self) -> _VectorisedFloat: @@ -448,7 +448,7 @@ class SimpleExposureModel(SimpleConcentrationModel): result += self.integrated_shortrange_concentration() - return result * self.finf * (1. - self.HI) + return result def probability_infection(self): """ @@ -528,6 +528,8 @@ def simple_c_model() -> SimpleConcentrationModel: room_volume = 50., lambda_ventilation= 1., BLO_factors = expiration_BLO_factors['Breathing'], + finf = models.Virus.types['SARS_CoV_2_DELTA'].viable_to_RNA_ratio, + HI = 0., ) @@ -724,6 +726,9 @@ def test_longrange_concentration_with_distributions(c_model_distr,time): room_volume = 50., lambda_ventilation= 1., BLO_factors = expiration_BLO_factors['Breathing'], + finf = virus_distributions['SARS_CoV_2_DELTA' + ].build_model(SAMPLE_SIZE).viable_to_RNA_ratio, + HI = 0., ) npt.assert_allclose( c_model_distr.build_model(SAMPLE_SIZE).concentration(time).mean(), From 1f90d08163f9da1a2ba4872ca0615799a8b28fca Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Fri, 1 Dec 2023 07:35:07 +0100 Subject: [PATCH 4/7] Multiplying all emission rate values by 0.305 (average of uniform distribution of f_inf - 0.01 to 0.6) in test_monte_carlo_full_models, to adapt it to f_inf now included in emission rate --- caimira/tests/test_monte_carlo_full_models.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/caimira/tests/test_monte_carlo_full_models.py b/caimira/tests/test_monte_carlo_full_models.py index b6e3948c..60cdf477 100644 --- a/caimira/tests/test_monte_carlo_full_models.py +++ b/caimira/tests/test_monte_carlo_full_models.py @@ -311,17 +311,22 @@ def waiting_room_mc(): ) +# In the following tests all the initial expected values for emission +# rate were multiplied by the average of the distribution of fraction +# of infected viruses (0.305, for a uniform distribution from 0.01 to 0.6) +# following the change of convention that this ratio should be +# applied at the emission level. @retry(tries=10) @pytest.mark.parametrize( "mc_model, expected_pi, expected_new_cases, expected_dose, expected_ER_per_person", [ - ["shared_office_mc", 5.38, 0.16, 3.350, 1056], - ["classroom_mc", 8.21, 1.56, 11.356, 7416], - ["ski_cabin_mc", 12.92, 0.39, 21.796, 10231], - ["skagit_chorale_mc",61.01, 36.53, 84.730, 190422], - ["bus_ride_mc", 10.59, 7.06, 6.650, 5419], - ["gym_mc", 0.52, 0.14, 0.249, 1450/2.], # there are two infected in this case - ["waiting_room_mc", 1.53, 0.21, 0.844, 929], + ["shared_office_mc", 5.38, 0.16, 3.350, 1056*0.305], + ["classroom_mc", 8.21, 1.56, 11.356, 7416*0.305], + ["ski_cabin_mc", 12.92, 0.39, 21.796, 10231*0.305], + ["skagit_chorale_mc",61.01, 36.53, 84.730, 190422*0.305], + ["bus_ride_mc", 10.59, 7.06, 6.650, 5419*0.305], + ["gym_mc", 0.52, 0.14, 0.249, 1450*0.305/2.], # there are two infected in this case + ["waiting_room_mc", 1.53, 0.21, 0.844, 929*0.305], ] ) def test_report_models(mc_model, expected_pi, expected_new_cases, @@ -343,12 +348,12 @@ def test_report_models(mc_model, expected_pi, expected_new_cases, @pytest.mark.parametrize( "mask_type, month, expected_pi, expected_dose, expected_ER", [ - ["No mask", "Jul", 7.689, 10.050, 1034.435], - ["Type I", "Jul", 1.663, 0.938, 193.52], - ["FFP2", "Jul", 0.523, 0.253, 193.52], - ["Type I", "Feb", 0.659, 0.325, 193.52], - ["Cloth", "Feb", 2.653, 1.741, 673.10], - ["Cloth", "Jul", 5.322, 5.064, 673.10], + ["No mask", "Jul", 7.689, 10.050, 1034.435*0.305], + ["Type I", "Jul", 1.663, 0.938, 193.52*0.305], + ["FFP2", "Jul", 0.523, 0.253, 193.52*0.305], + ["Type I", "Feb", 0.659, 0.325, 193.52*0.305], + ["Cloth", "Feb", 2.653, 1.741, 673.10*0.305], + ["Cloth", "Jul", 5.322, 5.064, 673.10*0.305], ], ) def test_small_shared_office_Geneva(mask_type, month, expected_pi, From d53d62f3f3b4ccbef0dbae66be561ddbbebd1b7b Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Fri, 1 Dec 2023 17:37:30 +0100 Subject: [PATCH 5/7] Renaming finf in test_full_algorithm --- caimira/apps/calculator/static/js/report.js | 41 +++++++++---------- caimira/apps/expert.py | 16 ++++---- .../templates/base/calculator.report.html.j2 | 2 +- caimira/tests/test_full_algorithm.py | 22 +++++----- 4 files changed, 40 insertions(+), 41 deletions(-) diff --git a/caimira/apps/calculator/static/js/report.js b/caimira/apps/calculator/static/js/report.js index 6cfd96a1..956c0dd6 100644 --- a/caimira/apps/calculator/static/js/report.js +++ b/caimira/apps/calculator/static/js/report.js @@ -59,7 +59,7 @@ function draw_plot(svg_id) { .attr('class', 'y label') .attr('fill', 'black') .attr('text-anchor', 'middle') - .text('Mean concentration (virions/m³)'); + .text('Mean concentration (IRP/m³)'); // Y cumulative concentration axis declaration. var yAxisCumEl = vis.append('svg:g') @@ -71,7 +71,7 @@ function draw_plot(svg_id) { .attr('class', 'y label') .attr('fill', 'black') .attr('text-anchor', 'middle') - .text('Mean cumulative dose (infectious virus)'); + .text('Mean cumulative dose (IRP)'); // Legend for the plot elements - line and area. @@ -82,8 +82,7 @@ function draw_plot(svg_id) { .style('fill', '#1f77b4'); // Concentration line text var legendLineText = vis.append('text') - .text('Mean concentration') - .style('font-size', '15px'); + .text('Mean concentration (Infectious Respiratory Particles)') // Cumulative dose line icon var legendCumulativeIcon = vis.append('line') @@ -92,8 +91,7 @@ function draw_plot(svg_id) { .style("stroke", '#1f77b4'); // Cumulative dose line text var legendCumutiveText = vis.append('text') - .text('Cumulative dose') - .style('font-size', '15px'); + .text('Cumulative dose (Infectious Respiratory Particles)') // Area line icon var legendAreaIcon = vis.append('rect') @@ -104,7 +102,6 @@ function draw_plot(svg_id) { // Area line text var legendAreaText = vis.append('text') .text('Presence of exposed person(s)') - .style('font-size', '15px'); sr_unique_activities = [...new Set(short_range_expirations)] if (show_sr_legend) { @@ -142,7 +139,7 @@ function draw_plot(svg_id) { if (show_sr_legend) legendBBox_height = 68 + 20 * sr_unique_activities.length; else legendBBox_height = 68; var legendBBox = vis.append('rect') - .attr('width', 255) + .attr('width', 420) .attr('height', legendBBox_height) .attr('stroke', 'lightgrey') .attr('stroke-width', '2') @@ -310,9 +307,9 @@ function draw_plot(svg_id) { graph_width = div_width; graph_height = div_height var margins = { top: 30, right: 20, bottom: 50, left: 60 }; - if (div_width >= 900) { // For screens with width > 900px legend can be on the graph's right side. - div_width = 900; - graph_width = div_width * (2/3); + if (div_width >= 1100) { // For screens with width > 1100px legend can be on the graph's right side. + div_width = 1100; + graph_width = 600; const svg_margins = {'margin-left': '0rem'}; Object.entries(svg_margins).forEach(([prop,val]) => vis.style(prop,val)); } @@ -361,7 +358,7 @@ function draw_plot(svg_id) { yAxisCumLabelEl.attr('transform', 'rotate(-90, 0,' + graph_height + ')') .attr('x', (graph_height + margins.bottom) / 2.1); - if (plot_div.clientWidth >= 900) { + if (plot_div.clientWidth >= 1100) { yAxisCumLabelEl.attr('y', graph_width * 1.7); } else { @@ -373,7 +370,7 @@ function draw_plot(svg_id) { const space_between_text_icon = 30; const text_height = 6; // Legend on right side. - if (plot_div.clientWidth >= 900) { + if (plot_div.clientWidth >= 1100) { legendLineIcon.attr('x', graph_width + legend_x_start) .attr('y', margins.top + size); legendLineText.attr('x', graph_width + legend_x_start + space_between_text_icon) @@ -746,9 +743,9 @@ function draw_generic_concentration_plot( graph_width = div_width; graph_height = div_height; var margins = { top: 30, right: 20, bottom: 50, left: 60 }; - if (window_width >= 900) { // For screens with width > 900px legend can be on the graph's right side. - div_width = 900; - graph_width = div_width * (2/3); + if (window_width >= 1100) { // For screens with width > 1100px legend can be on the graph's right side. + div_width = 1100; + graph_width = 600; const svg_margins = {'margin-left': '0rem'}; Object.entries(svg_margins).forEach(([prop,val]) => vis.style(prop,val)); } @@ -799,7 +796,7 @@ function draw_generic_concentration_plot( var scenario_index = Object.keys(data_for_scenarios).indexOf(scenario_name) // Legend on right side. var size = 20 * (scenario_index + 1); - if (window_width >= 900) { + if (window_width >= 1100) { label_icons[scenario_name].attr('x', graph_width + legend_x_start) .attr('y', margins.top + size); label_text[scenario_name].attr('x', graph_width + legend_x_start + space_between_text_icon) @@ -817,7 +814,7 @@ function draw_generic_concentration_plot( if (h_lines) { h_lines.map((line, index) => { size = 21 * (scenario_index + index + 2); // account for previous legend elements - if (window_width >= 900) { + if (window_width >= 1100) { h_line_label_icon[line.label].attr("x1", graph_width + legend_x_start) .attr("x2", graph_width + legend_x_start + 20) .attr("y1", margins.top + size) @@ -839,7 +836,7 @@ function draw_generic_concentration_plot( } // Legend on right side. - if (window_width >= 900) { + if (window_width >= 1100) { legendBBox.attr('x', graph_width * 1.02) .attr('y', margins.top * 1.15); @@ -899,10 +896,10 @@ function draw_histogram(svg_id, prob, prob_sd) { var vis = d3.select(plot_div).append('svg'); // set the dimensions and margins of the graph - if (div_width > 900) { - div_width = 900; + if (div_width > 1100) { + div_width = 1100; var margins = { top: 30, right: 20, bottom: 50, left: 60 }; - var graph_width = div_width * (2/3); + var graph_width = 600; const svg_margins = {'margin-left': '0rem'}; Object.entries(svg_margins).forEach(([prop,val]) => vis.style(prop,val)); } diff --git a/caimira/apps/expert.py b/caimira/apps/expert.py index 5162d8f0..8a14e76a 100644 --- a/caimira/apps/expert.py +++ b/caimira/apps/expert.py @@ -131,13 +131,13 @@ class ExposureModelResult(View): ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) ax.set_xlabel('Time (hours)') - ax.set_ylabel('Mean concentration ($virions/m^{3}$)') - ax.set_title('Concentration of virions \nand Cumulative dose') + ax.set_ylabel('Mean concentration ($IRP/m^{3}$)') + ax.set_title('Concentration and Cumulative\ndose of Infectious Respiratory Particles') ax2 = ax.twinx() ax2.spines['left'].set_visible(False) ax2.spines['top'].set_visible(False) - ax2.set_ylabel('Mean cumulative dose (infectious virus)') + ax2.set_ylabel('Mean cumulative dose (IRP)') ax2.spines['right'].set_linestyle((0,(1,4))) return ax, ax2 @@ -193,8 +193,8 @@ class ExposureModelResult(View): self.ax.set_xlim(left = min(min(infected_presence.boundaries()[0]), min(exposed_presence.boundaries()[0])), right = max(max(infected_presence.boundaries()[1]), max(exposed_presence.boundaries()[1]))) - figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='Mean concentration'), - mlines.Line2D([], [], color='#0000c8', markersize=15, ls="dotted", label='Cumulative dose'), + figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='Mean concentration (Infectious Respiratory Particles)'), + mlines.Line2D([], [], color='#0000c8', markersize=15, ls="dotted", label='Cumulative dose (Infectious Respiratory Particles)'), patches.Patch(edgecolor="#96cbff", facecolor='#96cbff', label='Presence of exposed person(s)')] self.ax.legend(handles=figure_legends) @@ -236,15 +236,15 @@ class ExposureComparisonResult(View): ax.spines['top'].set_visible(False) ax.set_xlabel('Time (hours)') - ax.set_ylabel('Mean concentration ($virions/m^{3}$)') - ax.set_title('Concentration of virions \nand Cumulative dose') + ax.set_ylabel('Mean concentration ($IRP/m^{3}$)') + ax.set_title('Concentration and Cumulative\ndose of Infectious Respiratory Particles') ax2 = ax.twinx() ax2.spines['left'].set_visible(False) ax2.spines['top'].set_visible(False) ax2.spines['right'].set_linestyle((0,(1,4))) - ax2.set_ylabel('Mean cumulative dose (infectious virus)') + ax2.set_ylabel('Mean cumulative dose (IRP)') return ax, ax2 diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2 index 58bf8f68..94343afe 100644 --- a/caimira/apps/templates/base/calculator.report.html.j2 +++ b/caimira/apps/templates/base/calculator.report.html.j2 @@ -277,7 +277,7 @@ var alternative_scenarios = {{ alternative_scenarios.stats | JSONify }} draw_generic_concentration_plot( "alternative_scenario_plot", - "Mean concentration (virions/m³)", + "Mean concentration (IRP/m³)", );
diff --git a/caimira/tests/test_full_algorithm.py b/caimira/tests/test_full_algorithm.py index a26f3555..ff5f3843 100644 --- a/caimira/tests/test_full_algorithm.py +++ b/caimira/tests/test_full_algorithm.py @@ -56,8 +56,8 @@ class SimpleConcentrationModel: #: Number of infected people num_infected: int = 1 - #: Fraction of infected viruses - finf: _VectorisedFloat = 0.5 + #: Fraction of infected viruses (viable to RNA ratio) + viable_to_RNA: _VectorisedFloat = 0.5 #: Host immunity factor (0. for not immune) HI: _VectorisedFloat = 0. @@ -182,7 +182,8 @@ class SimpleConcentrationModel: return ( ( (0 if not self.infected_presence.triggered(t) else self.f(lambda_rate,0)) + result * np.exp(-lambda_rate*(t-ti)) ) - * self.num_infected * self.finf * (1. - self.HI) / self.room_volume) + * self.num_infected * self.viable_to_RNA + * (1. - self.HI) / self.room_volume) @dataclass(frozen=True) @@ -410,7 +411,8 @@ class SimpleExposureModel(SimpleConcentrationModel): else self.f_with_fdep(lambda_rate,0,evaporation)*(t2-t1)) + (primitive(t2) * np.exp(-lambda_rate*(t2-ti)) - primitive(t1) * np.exp(-lambda_rate*(t1-ti)) ) ) - * self.num_infected * self.finf * (1. - self.HI) / self.room_volume) + * self.num_infected * self.viable_to_RNA + * (1. - self.HI) / self.room_volume) @method_cache def integrated_shortrange_concentration(self) -> _VectorisedFloat: @@ -528,7 +530,7 @@ def simple_c_model() -> SimpleConcentrationModel: room_volume = 50., lambda_ventilation= 1., BLO_factors = expiration_BLO_factors['Breathing'], - finf = models.Virus.types['SARS_CoV_2_DELTA'].viable_to_RNA_ratio, + viable_to_RNA = models.Virus.types['SARS_CoV_2_DELTA'].viable_to_RNA_ratio, HI = 0., ) @@ -576,7 +578,7 @@ def simple_expo_sr_model(simple_sr_models) -> SimpleExposureModel: room_volume = 50., lambda_ventilation= 1., BLO_factors = expiration_BLO_factors['Breathing'], - finf = models.Virus.types['SARS_CoV_2_DELTA'].viable_to_RNA_ratio, + viable_to_RNA = models.Virus.types['SARS_CoV_2_DELTA'].viable_to_RNA_ratio, HI = 0., ID50 = models.Virus.types['SARS_CoV_2_DELTA'].infectious_dose, transmissibility = models.Virus.types['SARS_CoV_2_DELTA'].transmissibility_factor, @@ -624,7 +626,7 @@ def simple_expo_sr_model_distr() -> SimpleExposureModel: room_volume = 50., lambda_ventilation= 1., BLO_factors = expiration_BLO_factors['Breathing'], - finf = virus_distributions['SARS_CoV_2_DELTA' + viable_to_RNA = virus_distributions['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).viable_to_RNA_ratio, HI = 0., ID50 = virus_distributions['SARS_CoV_2_DELTA' @@ -685,7 +687,7 @@ def test_longrange_exposure(c_model): room_volume = 50., lambda_ventilation= 1., BLO_factors = expiration_BLO_factors['Breathing'], - finf = models.Virus.types['SARS_CoV_2_DELTA'].viable_to_RNA_ratio, + viable_to_RNA = models.Virus.types['SARS_CoV_2_DELTA'].viable_to_RNA_ratio, HI = 0., ID50 = models.Virus.types['SARS_CoV_2_DELTA'].infectious_dose, transmissibility = models.Virus.types['SARS_CoV_2_DELTA'].transmissibility_factor, @@ -726,7 +728,7 @@ def test_longrange_concentration_with_distributions(c_model_distr,time): room_volume = 50., lambda_ventilation= 1., BLO_factors = expiration_BLO_factors['Breathing'], - finf = virus_distributions['SARS_CoV_2_DELTA' + viable_to_RNA = virus_distributions['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).viable_to_RNA_ratio, HI = 0., ) @@ -746,7 +748,7 @@ def test_longrange_exposure_with_distributions(c_model_distr): room_volume = 50., lambda_ventilation= 1., BLO_factors = expiration_BLO_factors['Breathing'], - finf = virus_distributions['SARS_CoV_2_DELTA' + viable_to_RNA = virus_distributions['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).viable_to_RNA_ratio, HI = 0., ID50 = virus_distributions['SARS_CoV_2_DELTA' From 600cee9c9ea28b6283556eb97f8c43e3cd99438b Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 11 Dec 2023 15:12:13 +0100 Subject: [PATCH 6/7] minor version update --- caimira/apps/calculator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index b5a11900..51cc1961 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -37,7 +37,7 @@ from .user import AuthenticatedUser, AnonymousUser # calculator version. If the calculator needs to make breaking changes (e.g. change # form attributes) then it can also increase its MAJOR version without needing to # increase the overall CAiMIRA version (found at ``caimira.__version__``). -__version__ = "4.14.0" +__version__ = "4.14.1" LOG = logging.getLogger(__name__) From 2244a1dd192770d8eebf1b206a9c11f5ce065c58 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Tue, 12 Dec 2023 16:22:18 +0100 Subject: [PATCH 7/7] updated concentration and dose labels to "Infectious Respiratory Particles" --- caimira/apps/calculator/static/js/report.js | 43 ++++++++----------- caimira/apps/expert.py | 4 +- .../templates/base/calculator.report.html.j2 | 1 + 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/caimira/apps/calculator/static/js/report.js b/caimira/apps/calculator/static/js/report.js index 956c0dd6..57b1511e 100644 --- a/caimira/apps/calculator/static/js/report.js +++ b/caimira/apps/calculator/static/js/report.js @@ -82,7 +82,7 @@ function draw_plot(svg_id) { .style('fill', '#1f77b4'); // Concentration line text var legendLineText = vis.append('text') - .text('Mean concentration (Infectious Respiratory Particles)') + .text('Mean concentration') // Cumulative dose line icon var legendCumulativeIcon = vis.append('line') @@ -91,7 +91,7 @@ function draw_plot(svg_id) { .style("stroke", '#1f77b4'); // Cumulative dose line text var legendCumutiveText = vis.append('text') - .text('Cumulative dose (Infectious Respiratory Particles)') + .text('Cumulative dose') // Area line icon var legendAreaIcon = vis.append('rect') @@ -113,7 +113,6 @@ function draw_plot(svg_id) { .attr('opacity', 0); var legendLongCumutiveText = vis.append('text') .text('Long-range cumulative dose') - .style('font-size', '15px') .attr('opacity', 0); // Short-range area icon var legendShortRangeAreaIcon = {}; @@ -130,8 +129,7 @@ function draw_plot(svg_id) { var legendShortRangeText = {}; sr_unique_activities.forEach((b, index) => { legendShortRangeText[index] = vis.append('text') - .text('Short-range - ' + sr_unique_activities[index]) - .style('font-size', '15px'); + .text('Short-range - ' + sr_unique_activities[index]); }); } @@ -139,7 +137,7 @@ function draw_plot(svg_id) { if (show_sr_legend) legendBBox_height = 68 + 20 * sr_unique_activities.length; else legendBBox_height = 68; var legendBBox = vis.append('rect') - .attr('width', 420) + .attr('width', 270) .attr('height', legendBBox_height) .attr('stroke', 'lightgrey') .attr('stroke-width', '2') @@ -307,8 +305,8 @@ function draw_plot(svg_id) { graph_width = div_width; graph_height = div_height var margins = { top: 30, right: 20, bottom: 50, left: 60 }; - if (div_width >= 1100) { // For screens with width > 1100px legend can be on the graph's right side. - div_width = 1100; + if (div_width >= 1000) { // For screens with width > 1000px legend can be on the graph's right side. + div_width = 1000; graph_width = 600; const svg_margins = {'margin-left': '0rem'}; Object.entries(svg_margins).forEach(([prop,val]) => vis.style(prop,val)); @@ -358,7 +356,7 @@ function draw_plot(svg_id) { yAxisCumLabelEl.attr('transform', 'rotate(-90, 0,' + graph_height + ')') .attr('x', (graph_height + margins.bottom) / 2.1); - if (plot_div.clientWidth >= 1100) { + if (plot_div.clientWidth >= 1000) { yAxisCumLabelEl.attr('y', graph_width * 1.7); } else { @@ -370,7 +368,7 @@ function draw_plot(svg_id) { const space_between_text_icon = 30; const text_height = 6; // Legend on right side. - if (plot_div.clientWidth >= 1100) { + if (plot_div.clientWidth >= 1000) { legendLineIcon.attr('x', graph_width + legend_x_start) .attr('y', margins.top + size); legendLineText.attr('x', graph_width + legend_x_start + space_between_text_icon) @@ -607,7 +605,7 @@ function draw_generic_concentration_plot( max_key_length = Math.max(Math.max(...(Object.keys(data_for_scenarios).map(el => el.length))), h_line_max_key); var legendBBox = vis.append('rect') - .attr('width', 9 * max_key_length ) + .attr('width', 10 * max_key_length ) .attr('height', 25 * ((Object.keys(data_for_scenarios).length) + h_lines_lenght)) .attr('stroke', 'lightgrey') .attr('stroke-width', '2') @@ -643,8 +641,7 @@ function draw_generic_concentration_plot( .style('fill', colors[scenario_index]); label_text[scenario_name] = vis.append('text') - .text(scenario_name) - .style('font-size', '15px'); + .text(scenario_name); } if (h_lines) { var h_lines_draw = {}, h_line_label_icon = {}, h_line_label_text = {}; @@ -661,8 +658,7 @@ function draw_generic_concentration_plot( .attr('stroke-width', '2') .style("stroke", line.color); h_line_label_text[line.label] = vis.append('text') - .text(line.label) - .style('font-size', '15px'); + .text(line.label); }) } @@ -743,8 +739,8 @@ function draw_generic_concentration_plot( graph_width = div_width; graph_height = div_height; var margins = { top: 30, right: 20, bottom: 50, left: 60 }; - if (window_width >= 1100) { // For screens with width > 1100px legend can be on the graph's right side. - div_width = 1100; + if (window_width >= 1000) { // For screens with width > 1000px legend can be on the graph's right side. + div_width = 1000; graph_width = 600; const svg_margins = {'margin-left': '0rem'}; Object.entries(svg_margins).forEach(([prop,val]) => vis.style(prop,val)); @@ -796,7 +792,7 @@ function draw_generic_concentration_plot( var scenario_index = Object.keys(data_for_scenarios).indexOf(scenario_name) // Legend on right side. var size = 20 * (scenario_index + 1); - if (window_width >= 1100) { + if (window_width >= 1000) { label_icons[scenario_name].attr('x', graph_width + legend_x_start) .attr('y', margins.top + size); label_text[scenario_name].attr('x', graph_width + legend_x_start + space_between_text_icon) @@ -814,7 +810,7 @@ function draw_generic_concentration_plot( if (h_lines) { h_lines.map((line, index) => { size = 21 * (scenario_index + index + 2); // account for previous legend elements - if (window_width >= 1100) { + if (window_width >= 1000) { h_line_label_icon[line.label].attr("x1", graph_width + legend_x_start) .attr("x2", graph_width + legend_x_start + 20) .attr("y1", margins.top + size) @@ -836,7 +832,7 @@ function draw_generic_concentration_plot( } // Legend on right side. - if (window_width >= 1100) { + if (window_width >= 1000) { legendBBox.attr('x', graph_width * 1.02) .attr('y', margins.top * 1.15); @@ -896,8 +892,8 @@ function draw_histogram(svg_id, prob, prob_sd) { var vis = d3.select(plot_div).append('svg'); // set the dimensions and margins of the graph - if (div_width > 1100) { - div_width = 1100; + if (div_width > 1000) { + div_width = 1000; var margins = { top: 30, right: 20, bottom: 50, left: 60 }; var graph_width = 600; const svg_margins = {'margin-left': '0rem'}; @@ -1032,7 +1028,6 @@ function draw_histogram(svg_id, prob, prob_sd) { // CDF line text vis.append('text') .text('CDF') - .style('font-size', '15px') .attr('x', graph_width + legend_x_start + space_between_text_icon) .attr('y', margins.top + size + text_height); // Hist icon @@ -1045,7 +1040,6 @@ function draw_histogram(svg_id, prob, prob_sd) { // Hist text vis.append('text') .text('Histogram') - .style('font-size', '15px') .attr('x', graph_width + legend_x_start + space_between_text_icon) .attr('y', margins.top + 2 * size + text_height*2); // Mean text @@ -1060,7 +1054,6 @@ function draw_histogram(svg_id, prob, prob_sd) { // Mean line text vis.append('text') .text('Mean') - .style('font-size', '15px') .attr('x', graph_width + legend_x_start + space_between_text_icon) .attr('y', margins.top + 3 * size + text_height*3); diff --git a/caimira/apps/expert.py b/caimira/apps/expert.py index 8a14e76a..240e90cb 100644 --- a/caimira/apps/expert.py +++ b/caimira/apps/expert.py @@ -193,8 +193,8 @@ class ExposureModelResult(View): self.ax.set_xlim(left = min(min(infected_presence.boundaries()[0]), min(exposed_presence.boundaries()[0])), right = max(max(infected_presence.boundaries()[1]), max(exposed_presence.boundaries()[1]))) - figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='Mean concentration (Infectious Respiratory Particles)'), - mlines.Line2D([], [], color='#0000c8', markersize=15, ls="dotted", label='Cumulative dose (Infectious Respiratory Particles)'), + figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='Mean concentration'), + mlines.Line2D([], [], color='#0000c8', markersize=15, ls="dotted", label='Cumulative dose'), patches.Patch(edgecolor="#96cbff", facecolor='#96cbff', label='Presence of exposed person(s)')] self.ax.legend(handles=figure_legends) diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2 index 94343afe..6f37e7e2 100644 --- a/caimira/apps/templates/base/calculator.report.html.j2 +++ b/caimira/apps/templates/base/calculator.report.html.j2 @@ -181,6 +181,7 @@ let short_range_expirations = {{ short_range_expirations | JSONify }}; draw_plot("concentration_plot"); + IRP - Infectious Respiratory Particles.