Enhancements for Dynamic and Static Occupancy Handling

- Added default 'static' keyword to single group with static occupancy
- Added default values for 'dynamic_exposed_occupancy' and 'dynamic_infected_occupancy'
- Adapted default model to use ExposureModelGroup instance when static occupancy is defined
- Updated JS methods to generate multiple plots when dynamic occupancy is defined
- Handled the alternative scenario generation when dynamic occupancy is defined
- Improved results generation for exposure groups
- Updated HTML report for visualising results across multiple groups
- Enhanced model representations
This commit is contained in:
lrdossan 2025-01-28 16:49:21 +01:00
parent d46b5846a1
commit 61c6b2c2c3
15 changed files with 597 additions and 580 deletions

View file

@ -32,5 +32,6 @@ def submit_virus_form(form_data: typing.Dict, report_generation_parallelism: typ
# Handle model representation
if report_data['model']: report_data['model'] = repr(report_data['model'])
for single_group_output in report_data['groups'].values(): del single_group_output['model'] # Model representation per group not needed
return report_data

View file

@ -666,7 +666,7 @@ class Particle:
# deposition fraction depends on aerosol particle diameter.
d = (self.diameter * evaporation_factor)
IFrac = 1 - 0.5 * (1 - (1 / (1 + (0.00076*(d**2.8)))))
fdep = IFrac * (0.0587
fdep = IFrac * (0.0587 # type: ignore
+ (0.911/(1 + np.exp(4.77 + 1.485 * np.log(d))))
+ (0.943/(1 + np.exp(0.508 - 2.58 * np.log(d))))) # type: ignore
return fdep
@ -1642,6 +1642,9 @@ class ExposureModel:
#: Total people with short-range interactions
exposed_to_short_range: int = 0
#: Unique group identifier
identifier: str = 'static'
#: The number of times the exposure event is repeated (default 1).
@property
def repeats(self) -> int:

View file

@ -5,6 +5,9 @@ import io
import typing
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Agg')
from collections import defaultdict
from caimira.calculator.models import models, dataclass_utils, profiler, monte_carlo as mc
from caimira.calculator.models.enums import ViralLoads
@ -108,208 +111,220 @@ def interesting_times(model: typing.Union[models.ExposureModelGroup, models.Expo
return nice_times
def process_short_range_interactions(model: typing.Union[models.ExposureModelGroup, models.ExposureModel],
times: typing.List[float]):
def _concentrations_with_sr_breathing(form: VirusFormData, model: models.ExposureModel,
time: float, fn_name: typing.Optional[str] = None):
"""
Process both ExposureModel and ExposureModelGroup for short-range
expirations, intervals and concentrations. Returns a tuple containing
lower concentrations, short-range expirations, and short-range intervals.
Returns the zoomed viral concentrations.
"""
if isinstance(model, models.ExposureModelGroup):
model_list = model.exposure_models
elif isinstance(model, models.ExposureModel):
model_list = (model,)
else:
raise TypeError(f"Model should be either an instance of ExposureModel or ExposureModelGroup. Got '{type(model)}.'")
# Collect short-range expirations and intervals
short_range_expirations: typing.List[str] = []
short_range_intervals: typing.List[models.BoundarySequence_t] = []
for model in model_list:
for short_range_model in model.short_range:
short_range_expirations.append(short_range_model.expiration_def) # type: ignore
short_range_intervals.extend(short_range_model.presence.boundaries())
# Collect lower concentrations (including Breathing)
lower_concentrations = []
for time in times:
breathing_found = False
for model in model_list:
for short_range_model in model.short_range:
((start, stop),) = short_range_model.presence.boundaries()
# Check if the expiration is "Breathing" and the if time is within boundaries
if short_range_model.expiration_def == 'Breathing' and (start <= time <= stop):
lower_concentrations.append(np.sum([np.array(model.concentration(float(time))).mean() for model in model_list]))
breathing_found = True
break
if breathing_found:
break
lower_concentrations.append(np.sum([np.array(model.concentration_model.concentration(float(time))).mean() for model in model_list]))
return lower_concentrations, short_range_expirations, short_range_intervals
for index, (start, stop) in enumerate([interaction.presence.boundaries()[0] for interaction in model.short_range]):
if start <= time <= stop and form.short_range_interactions[model.identifier][index]['expiration'] == 'Breathing':
return np.array(model.concentration(float(time))).mean(), fn_name
return np.array(model.concentration_model.concentration(float(time))).mean(), fn_name
def _calculate_deposited_exposure(model: typing.Union[models.ExposureModelGroup, models.ExposureModel],
def _calculate_deposited_exposure(model: models.ExposureModel,
time1: float, time2: float, fn_name: typing.Optional[str] = None):
if isinstance(model, models.ExposureModelGroup):
return np.sum([np.array(nth_model.deposited_exposure_between_bounds(float(time1), float(time2))).mean() for nth_model in model.exposure_models]), fn_name
else:
return np.array(model.deposited_exposure_between_bounds(float(time1), float(time2))).mean(), fn_name
return np.array(model.deposited_exposure_between_bounds(float(time1), float(time2))).mean(), fn_name
def _calculate_long_range_deposited_exposure(model: typing.Union[models.ExposureModelGroup, models.ExposureModel],
def _calculate_long_range_deposited_exposure(model: models.ExposureModel,
time1: float, time2: float, fn_name: typing.Optional[str] = None):
if isinstance(model, models.ExposureModelGroup):
return np.sum([np.array(nth_model.long_range_deposited_exposure_between_bounds(float(time1), float(time2))).mean() for nth_model in model.exposure_models]), fn_name
else:
return np.array(model.long_range_deposited_exposure_between_bounds(float(time1), float(time2))).mean(), fn_name
return np.array(model.long_range_deposited_exposure_between_bounds(float(time1), float(time2))).mean(), fn_name
def _calculate_concentration(model: typing.Union[models.ExposureModelGroup, models.ExposureModel],
def _calculate_concentration(model: models.ExposureModel,
time: float, fn_name: typing.Optional[str] = None):
if isinstance(model, models.ExposureModelGroup):
return np.sum([np.array(nth_model.concentration(float(time))).mean() for nth_model in model.exposure_models]), fn_name
else:
return np.array(model.concentration(float(time))).mean(), fn_name
"""
Returns the concentration of viruses emitted by
the infected population. Short- and long-range included.
"""
return np.array(model.concentration(float(time))).mean(), fn_name
def _calculate_co2_concentration(CO2_model: models.CO2ConcentrationModel, time: float, fn_name: typing.Optional[str] = None):
"""
Returns the CO2 concentration emitted by all
the present population.
"""
return np.array(CO2_model.concentration(float(time))).mean(), fn_name
def merge_intervals(intervals: typing.List[typing.List[float]]) -> typing.List[typing.List[float]]:
"""
Merges overlapping intervals from a list of intervals.
Assumes intervals are sorted based on start times.
"""
if not intervals:
return []
merged = [list(intervals[0])]
for start, end in intervals[1:]:
if merged[-1][1] < start:
merged.append([start, end])
else:
merged[-1][1] = max(merged[-1][1], end)
return merged
def merge_short_range_interactions(all_exposed_groups: typing.Dict[str, typing.Any]) -> typing.List[typing.Dict[str, typing.Any]]:
"""
Expands the short range interactions per exposed group to a single data structure.
"""
merged_interactions = defaultdict(list)
for group in all_exposed_groups.values():
for interaction in group["short_range_interactions"]:
merged_interactions[interaction["expiration"]].extend(interaction["presence_interval"])
# Merge and sort intervals
return [
{"expiration": exp, "presence_interval": merge_intervals(sorted(intervals, key=lambda x: x[0]))}
for exp, intervals in merged_interactions.items()
]
def group_results(form: VirusFormData, model_group: models.ExposureModelGroup) -> typing.Dict[str, typing.Any]:
"""
Generates the output per group of exposure models.
"""
groups: dict = defaultdict(dict)
for single_group in model_group.exposure_models:
# Probability of infection
prob = single_group.infection_probability()
prob_dist_count, prob_dist_bins = np.histogram(prob/100, bins=100, density=True)
# Expected new cases
expected_new_cases = np.array(single_group.expected_new_cases())
groups[single_group.identifier] = {
"model": single_group,
"prob_inf": prob.mean(),
"prob_inf_sd": prob.std(),
"prob_dist": list(prob),
"prob_hist_count": list(prob_dist_count),
"prob_hist_bins": list(prob_dist_bins),
"expected_new_cases": expected_new_cases.mean(),
"exposed_presence_intervals": list(single_group.exposed.presence_interval().boundaries()),
}
# In case of conditional probability plot
if (form.conditional_probability_viral_loads and
single_group.data_registry.virological_data['virus_distributions'][form.virus_type]['viral_load_in_sputum'] == ViralLoads.COVID_OVERALL.value): # type: ignore
conditional_probability_data = manufacture_conditional_probability_data(single_group, prob)
groups[single_group.identifier].update({
"conditional_probability_data": conditional_probability_data,
"uncertainties_plot_src": img2base64(_figure2bytes(uncertainties_plot(prob, conditional_probability_data)))
})
# Probabilistic exposure
if form.exposure_option == "p_probabilistic_exposure":
groups[single_group.identifier].update({
"prob_probabilistic_exposure": np.array(single_group.total_probability_rule()).mean()
})
# In case of short-range interactions
if single_group.short_range != ():
# Short range outputs
short_range_interactions: dict = defaultdict(list)
for short_range_model in single_group.short_range:
short_range_interactions[short_range_model.expiration_def].extend(
short_range_model.presence.boundaries()
)
long_range_single_group = dataclass_utils.nested_replace(
single_group, {'short_range': ()}
)
groups[single_group.identifier].update({
"long_range_prob": long_range_single_group.infection_probability().mean(),
"long_range_expected_new_cases": long_range_single_group.expected_new_cases().mean(),
"short_range_interactions": [
{"expiration": expiration, "presence_interval": intervals}
for expiration, intervals in short_range_interactions.items()
],
})
return groups
@profiler.profile
def calculate_report_data(form: VirusFormData, executor_factory: typing.Callable[[], concurrent.futures.Executor]) -> typing.Dict[str, typing.Any]:
"""
General output data of a test scenario.
Simulation output data.
"""
model: typing.Union[models.ExposureModel, models.ExposureModelGroup] = form.build_model()
times = interesting_times(model)
if isinstance(model, models.ExposureModelGroup):
exposed_presence_intervals = []
probabilities_of_infection = []
for nth_model in model.exposure_models:
exposed_presence_intervals.extend(list(nth_model.exposed.presence_interval().boundaries()))
probabilities_of_infection.append(nth_model.infection_probability())
index_of_max_mean = max(
range(len(probabilities_of_infection)),
key=lambda i: probabilities_of_infection[i].mean()
)
probability_of_infection = probabilities_of_infection[index_of_max_mean]
elif isinstance(model, models.ExposureModel):
exposed_presence_intervals = [list(interval) for interval in model.exposed.presence_interval().boundaries()]
probability_of_infection = model.infection_probability()
else:
raise TypeError(f"Model should be either an instance of ExposureModel or ExposureModelGroup. Got '{type(model)}.'")
# Handle short-range related outputs
lower_concentrations, short_range_expirations, short_range_intervals = None, None, None
# Short-range related data:
if (form.short_range_option == "short_range_yes"):
lower_concentrations, short_range_expirations, short_range_intervals = process_short_range_interactions(model, times)
# Probability of infection
prob = probability_of_infection
prob_dist_count, prob_dist_bins = np.histogram(prob/100, bins=100, density=True)
model_group: models.ExposureModelGroup = form.build_model()
results_per_group: typing.Dict[str, typing.Any] = group_results(form, model_group)
# Expected new cases
expected_new_cases = np.array(model.expected_new_cases()).mean()
# Expected number of new cases per group
# expected_new_cases_per_group = [np.array(model.expected_new_cases()).mean() for model in models_set.exposure_models]
times = interesting_times(model_group)
# CO2 concentration
CO2_model: models.CO2ConcentrationModel = form.build_CO2_model()
# Compute deposited exposures and virus/CO2 concentrations in parallel to increase performance
deposited_exposures = []
long_range_deposited_exposures = []
deposited_exposures = defaultdict(list)
long_range_deposited_exposures = defaultdict(list)
concentrations = defaultdict(list)
concentrations_zoomed = defaultdict(list)
CO2_concentrations = []
concentrations = []
tasks = []
with executor_factory() as executor: # TODO: parallelism in the models
with executor_factory() as executor:
for time1, time2 in zip(times[:-1], times[1:]):
tasks.append(executor.submit(
_calculate_deposited_exposure, model, time1, time2, fn_name="de"))
tasks.append(executor.submit(
_calculate_long_range_deposited_exposure, model, time1, time2, fn_name="de_lr"))
tasks.append(executor.submit(
_calculate_concentration, model, time1, fn_name="cn"))
# virus and co2 concentration: takes each time as param, not the interval
for single_group in model_group.exposure_models:
tasks.append(executor.submit(
_calculate_deposited_exposure, single_group, time1, time2, fn_name=f"{single_group.identifier}:de"))
# virus and co2 concentration: takes each time as param, not the interval
tasks.append(executor.submit(
_calculate_concentration, single_group, time1, fn_name=f"{single_group.identifier}:cn"))
if single_group.short_range != ():
tasks.append(executor.submit(
_calculate_long_range_deposited_exposure, single_group, time1, time2, fn_name=f"{single_group.identifier}:de_lr"))
tasks.append(executor.submit(
_concentrations_with_sr_breathing, form, single_group, time1, fn_name=f"{single_group.identifier}:cn_zoomed"))
tasks.append(executor.submit(
_calculate_co2_concentration, CO2_model, time1, fn_name="co2"))
# virus and co2 concentration: calculate the last time too
tasks.append(executor.submit( _calculate_concentration,
model, times[-1], fn_name="cn"))
for single_model in model_group.exposure_models:
tasks.append(executor.submit(_calculate_concentration,
single_model, times[-1], fn_name=f"{single_model.identifier}:cn"))
if single_group.short_range != ():
tasks.append(executor.submit(_concentrations_with_sr_breathing,
form, single_model, times[-1], fn_name=f"{single_model.identifier}:cn_zoomed"))
tasks.append(executor.submit(_calculate_co2_concentration,
CO2_model, times[-1], fn_name="co2"))
for task in tasks:
result, fn_name = task.result()
if fn_name == "de":
deposited_exposures.append(result)
elif fn_name == "de_lr":
long_range_deposited_exposures.append(result)
elif fn_name == "cn":
concentrations.append(result)
elif fn_name == "co2":
CO2_concentrations.append(result)
cumulative_doses = np.cumsum(deposited_exposures)
long_range_cumulative_doses = np.cumsum(long_range_deposited_exposures)
prob_probabilistic_exposure = None
if isinstance(model, models.ExposureModel) and form.exposure_option == "p_probabilistic_exposure":
prob_probabilistic_exposure = np.array(model.total_probability_rule()).mean()
conditional_probability_data = None
uncertainties_plot_src = None
if form.conditional_probability_viral_loads:
if isinstance(model, models.ExposureModelGroup):
all_the_same_virus = True
for nth_model in model.exposure_models:
if nth_model.data_registry.virological_data['virus_distributions'][form.virus_type]['viral_load_in_sputum'] != ViralLoads.COVID_OVERALL.value: # type: ignore
all_the_same_virus = False
if all_the_same_virus:
# Given the similarities, pick the first exposure model
the_model: models.ExposureModel = model.exposure_models[0]
# Generate all the required data for the conditional probability plot
conditional_probability_data = manufacture_conditional_probability_data(the_model, prob)
# Generate the matplotlib image based on the received data
uncertainties_plot_src = img2base64(_figure2bytes(uncertainties_plot(prob, conditional_probability_data)))
elif isinstance(model, models.ExposureModel):
if model.data_registry.virological_data['virus_distributions'][form.virus_type]['viral_load_in_sputum'] != ViralLoads.COVID_OVERALL.value: # type: ignore
# Generate all the required data for the conditional probability plot
conditional_probability_data = manufacture_conditional_probability_data(model, prob)
# Generate the matplotlib image based on the received data
uncertainties_plot_src = img2base64(_figure2bytes(uncertainties_plot(prob, conditional_probability_data)))
if ":" in fn_name:
if fn_name.split(":")[1] == "de":
deposited_exposures[fn_name.split(':')[0]].append(result)
elif fn_name.split(":")[1] == "de_lr":
long_range_deposited_exposures[fn_name.split(':')[0]].append(result)
elif fn_name.split(":")[1] == "cn":
concentrations[fn_name.split(':')[0]].append(result)
elif fn_name.split(":")[1] == "cn_zoomed":
concentrations_zoomed[fn_name.split(':')[0]].append(result)
else:
raise TypeError(f"Model should be either an instance of ExposureModel or ExposureModelGroup. Got '{type(model)}.'")
if fn_name == "co2":
CO2_concentrations.append(result)
# Update results per group
for single_group in model_group.exposure_models:
results_per_group[single_group.identifier]["concentrations"] = concentrations[single_group.identifier]
results_per_group[single_group.identifier]["cumulative_doses"] = list(np.cumsum(deposited_exposures[single_group.identifier]))
# Calculate long_range results when short-range interactions are defined
if single_group.short_range != ():
results_per_group[single_group.identifier]["concentrations_zoomed"] = concentrations_zoomed[single_group.identifier]
results_per_group[single_group.identifier]["long_range_cumulative_doses"] = list(np.cumsum(long_range_deposited_exposures[single_group.identifier]))
return {
"model": model.exposure_models[0] if isinstance(model, models.ExposureModelGroup) else model, # TODO: which model do we want to show info about?
# General results across all groups
"model": model_group.exposure_models[0],
"times": list(times),
"exposed_presence_intervals": exposed_presence_intervals,
"short_range_intervals": short_range_intervals,
"short_range_expirations": short_range_expirations,
"concentrations": list(concentrations),
"concentrations_zoomed": lower_concentrations,
"cumulative_doses": list(cumulative_doses),
"long_range_cumulative_doses": list(long_range_cumulative_doses),
"prob_inf": prob.mean(),
"prob_inf_sd": prob.std(),
"prob_dist": list(prob),
"prob_hist_count": list(prob_dist_count),
"prob_hist_bins": list(prob_dist_bins),
"prob_probabilistic_exposure": prob_probabilistic_exposure,
"expected_new_cases": expected_new_cases,
"uncertainties_plot_src": uncertainties_plot_src,
"CO2_concentrations": CO2_concentrations,
"conditional_probability_data": conditional_probability_data,
# Group specific results
"groups": results_per_group,
}
@ -435,9 +450,7 @@ def calculate_vl_scenarios_percentiles(model: mc.ExposureModel) -> typing.Dict[s
scenarios = {}
for percentil in (0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99):
vl = np.quantile(viral_load, percentil)
specific_vl_scenario = dataclass_utils.nested_replace(model,
{'concentration_model.infected.virus.viral_load_in_sputum': vl}
)
specific_vl_scenario = dataclass_utils.nested_replace(model, {'concentration_model.infected.virus.viral_load_in_sputum': vl})
scenarios[str(vl)] = np.mean(
specific_vl_scenario.infection_probability())
return {
@ -517,55 +530,43 @@ def manufacture_alternative_scenarios(form: VirusFormData) -> typing.Dict[str, t
def scenario_statistics(
mc_model: typing.Union[mc.ExposureModel, mc.ExposureModelGroup],
mc_model_group: mc.ExposureModelGroup,
sample_times: typing.List[float],
compute_prob_exposure: bool,
):
model: typing.Union[models.ExposureModelGroup, models.ExposureModel] = mc_model.build_model(
size=mc_model.data_registry.monte_carlo['sample_size'])
model_group: models.ExposureModelGroup = mc_model_group.build_model(
size=mc_model_group.data_registry.monte_carlo['sample_size'])
model = model_group.exposure_models[0]
if isinstance(model, models.ExposureModelGroup):
concentrations = [
np.sum([np.array(nth_model.concentration(float(time))).mean() for nth_model in model.exposure_models])
for time in sample_times
]
prob = np.max([np.mean(nth_model.infection_probability()) for nth_model in model.exposure_models])
elif isinstance(model, models.ExposureModel):
concentrations = [
return {
'probability_of_infection': np.mean(model.infection_probability()),
'expected_new_cases': np.mean(model.expected_new_cases()),
'concentrations': [
np.mean(model.concentration(time))
for time in sample_times
]
prob = np.mean(model.infection_probability())
else:
raise TypeError(f"Model should be either an instance of ExposureModel or ExposureModelGroup. Got '{type(model)}.'")
return {
'probability_of_infection': prob,
'expected_new_cases': np.mean(model.expected_new_cases()),
'concentrations': concentrations,
'prob_probabilistic_exposure': model.total_probability_rule() if isinstance(model, models.ExposureModel) and compute_prob_exposure else None
],
'prob_probabilistic_exposure': model.total_probability_rule() if compute_prob_exposure else None,
}
def comparison_report(
form: VirusFormData,
report_data: typing.Dict[str, typing.Any],
scenarios: typing.Dict[str, typing.Union[mc.ExposureModel, mc.ExposureModelGroup]],
scenarios: typing.Dict[str, mc.ExposureModelGroup],
executor_factory: typing.Callable[[], concurrent.futures.Executor],
):
):
if (form.short_range_option == "short_range_no"):
statistics = {
'Current scenario': {
'probability_of_infection': report_data['prob_inf'],
'expected_new_cases': report_data['expected_new_cases'],
'concentrations': report_data['concentrations'],
'probability_of_infection': report_data['groups']['static']['prob_inf'],
'expected_new_cases': report_data['groups']['static']['expected_new_cases'],
'concentrations': report_data['groups']['static']['concentrations'],
}
}
else:
statistics = {}
static_occupancy = form.occupancy_format == "static"
compute_prob_exposure = form.short_range_option == "short_range_yes" and form.exposure_option == "p_probabilistic_exposure" and static_occupancy
compute_prob_exposure = form.short_range_option == "short_range_yes" and form.exposure_option == "p_probabilistic_exposure" and form.occupancy_format == "static"
with executor_factory() as executor:
results = executor.map(
@ -576,7 +577,7 @@ def comparison_report(
timeout=60,
)
for (name, model), model_stats in zip(scenarios.items(), results):
for (name, _), model_stats in zip(scenarios.items(), results):
statistics[name] = model_stats
return {

View file

@ -6,7 +6,6 @@ import ast
import json
import re
from collections import defaultdict
import numpy as np
from .defaults import DEFAULTS, NO_DEFAULT, COFFEE_OPTIONS_INT

View file

@ -245,7 +245,7 @@ class VirusFormData(FormData):
return models.Room(volume=volume, inside_temp=models.PiecewiseConstant((0, 24), (inside_temp,)), humidity=humidity) # type: ignore
def build_mc_model(self) -> typing.Union[mc.ExposureModel, mc.ExposureModelGroup]:
def build_mc_model(self) -> mc.ExposureModelGroup:
size = self.data_registry.monte_carlo['sample_size']
room: models.Room = self.initialize_room()
@ -295,30 +295,31 @@ class VirusFormData(FormData):
exposed=exposed_population,
geographical_data=geographical_data,
exposed_to_short_range=self.short_range_occupants,
identifier=exposure_group,
)
exposure_model_set.append(exposure_model)
if len(list(self.dynamic_exposed_occupancy.keys())) == 1:
return exposure_model_set[0]
else:
return mc.ExposureModelGroup(
data_registry=self.data_registry,
exposure_models=[individual_model.build_model(size) for individual_model in exposure_model_set]
)
return mc.ExposureModelGroup(
data_registry=self.data_registry,
exposure_models=[individual_model.build_model(size) for individual_model in exposure_model_set]
)
elif self.occupancy_format == 'static':
exposed_population = self.exposed_population()
short_range_tuple = tuple(item for sublist in short_range.values() for item in sublist)
return mc.ExposureModel(
return mc.ExposureModelGroup(
data_registry=self.data_registry,
concentration_model=concentration_model,
short_range=short_range_tuple,
exposed=exposed_population,
geographical_data=geographical_data,
exposed_to_short_range=self.short_range_occupants,
exposure_models = [mc.ExposureModel(
data_registry=self.data_registry,
concentration_model=concentration_model,
short_range=short_range_tuple,
exposed=exposed_population,
geographical_data=geographical_data,
exposed_to_short_range=self.short_range_occupants,
).build_model(size)]
)
def build_model(self, sample_size=None) -> typing.Union[models.ExposureModel, models.ExposureModelGroup]:
def build_model(self, sample_size=None) -> models.ExposureModelGroup:
size = self.data_registry.monte_carlo['sample_size'] if not sample_size else sample_size
return self.build_mc_model().build_model(size=size)

View file

@ -17,7 +17,7 @@ from caimira.calculator.store.data_registry import DataRegistry
def test_model_from_dict(baseline_form_data, data_registry):
form = virus_validator.VirusFormData.from_dict(baseline_form_data, data_registry)
assert isinstance(form.build_model(), models.ExposureModel)
assert isinstance(form.build_model(), models.ExposureModelGroup)
def test_model_from_dict_invalid(baseline_form_data, data_registry):

View file

@ -123,8 +123,8 @@ class VirusReportGenerator:
data_registry_version: typing.Optional[str] = f"v{model.data_registry.version}" if model.data_registry.version else None
# Alternative scenarios data
alternative_scenarios: typing.Dict[str,typing.Any] = alternative_scenarios_data(form, report_data, executor_factory)
context.update(alternative_scenarios)
if form.occupancy_format == 'static':
context.update(alternative_scenarios_data(form, report_data, executor_factory))
# Alternative viral load data
if form.conditional_probability_viral_loads:

View file

@ -126,13 +126,13 @@ p.notes {
padding: 15px;
page-break-inside: avoid;
}
#button_full_exposure, #button_hide_high_concentration {
#button_full_exposure-group_static, #button_hide_high_concentration-group_static {
display: none!important;
}
#long_range_cumulative_checkbox, #lr_cumulative_checkbox_label {
#long_range_cumulative_checkbox-group_static, #lr_cumulative_checkbox_label-group_static {
display: none!important;
}
#button_alternative_full_exposure, #button_alternative_hide_high_concentration {
#button_alternative_full_exposure-group_static, #button_alternative_hide_high_concentration-group_static {
display: none!important;
}
#export-csv {

View file

@ -301,6 +301,7 @@ function displayFittingData(json_response) {
// Not needed for the form submission
delete json_response["CO2_plot_img"];
delete json_response["predictive_CO2"];
delete json_response["CO2_plot_data"];
// Convert nulls to empty strings in the JSON response
if (json_response["room_capacity"] === null) json_response["room_capacity"] = '';
if (json_response["ventilation_lsp_values"] === null) json_response["ventilation_lsp_values"] = '';

View file

@ -4,13 +4,18 @@ function on_report_load(conditional_probability_viral_loads) {
}
/* Generate the concentration plot using d3 library. */
function draw_plot(svg_id) {
function draw_plot(svg_id, group_id, times, concentrations_zoomed,
concentrations, cumulative_doses, long_range_cumulative_doses,
exposed_presence_intervals, short_range_interactions) {
// Used for controlling the short-range interactions
let button_full_exposure = document.getElementById("button_full_exposure");
let button_hide_high_concentration = document.getElementById("button_hide_high_concentration");
let long_range_checkbox = document.getElementById('long_range_cumulative_checkbox')
let show_sr_legend = short_range_expirations?.length > 0;
let button_full_exposure = document.getElementById(`button_full_exposure-group_${group_id}`);
let button_hide_high_concentration = document.getElementById(`button_hide_high_concentration-group_${group_id}`);
let long_range_checkbox = document.getElementById(`long_range_cumulative_checkbox-group_${group_id}`);
let show_sr_legend = short_range_interactions.length > 0;
let short_range_intervals = short_range_interactions.map((interaction) => interaction["presence_interval"]);
let short_range_expirations = short_range_interactions.map((interaction) => interaction["expiration"]);
var data_for_graphs = {
'concentrations': [],
@ -521,12 +526,12 @@ function draw_plot(svg_id) {
}
// Draw for the first time to initialize.
redraw();
redraw(svg_id);
update_concentration_plot(concentrations, cumulative_doses);
// Redraw based on the new size whenever the browser window is resized.
window.addEventListener("resize", e => {
redraw();
redraw(svg_id);
if (button_full_exposure && button_full_exposure.disabled) update_concentration_plot(concentrations, cumulative_doses);
else update_concentration_plot(concentrations_zoomed, long_range_cumulative_doses)
});
@ -536,12 +541,13 @@ function draw_plot(svg_id) {
// 'list_of_scenarios' is a dictionary with all the scenarios
// 'times' is a list of times for all the scenarios
function draw_generic_concentration_plot(
plot_svg_id,
svg_id,
times,
y_axis_label,
h_lines,
) {
if (plot_svg_id === 'CO2_concentration_graph') {
if (svg_id === 'CO2_concentration_graph') {
list_of_scenarios = {'CO₂ concentration': {'concentrations': CO2_concentrations}};
min_y_axis_domain = 400;
}
@ -575,7 +581,7 @@ function draw_generic_concentration_plot(
var first_scenario = Object.values(data_for_scenarios)[0]
// Add main SVG element
var plot_div = document.getElementById(plot_svg_id);
var plot_div = document.getElementById(svg_id);
var vis = d3.select(plot_div).append('svg');
var xRange = d3.scaleTime().domain([first_scenario[0].hour, first_scenario[first_scenario.length - 1].hour]);
@ -706,7 +712,7 @@ function draw_generic_concentration_plot(
}
function update_concentration_plot(concentration_data) {
list_of_scenarios = (plot_svg_id === 'CO2_concentration_graph') ? {'CO₂ concentration': {'concentrations': CO2_concentrations}} : alternative_scenarios
list_of_scenarios = (svg_id === 'CO2_concentration_graph') ? {'CO₂ concentration': {'concentrations': CO2_concentrations}} : alternative_scenarios
var highest_concentration = 0.
for (scenario in list_of_scenarios) {
@ -739,11 +745,11 @@ function draw_generic_concentration_plot(
var graph_width;
var graph_height;
function redraw() {
function redraw(svg_id) {
// Define width and height according to the screen size. Always use an already defined
var window_width = document.getElementById('concentration_plot').clientWidth;
var window_width = document.getElementById(svg_id).clientWidth;
var div_width = window_width;
var div_height = document.getElementById('concentration_plot').clientHeight;
var div_height = document.getElementById(svg_id).clientHeight;
graph_width = div_width;
graph_height = div_height;
var margins = { top: 30, right: 20, bottom: 50, left: 60 };
@ -882,12 +888,12 @@ function draw_generic_concentration_plot(
}
// Draw for the first time to initialize.
redraw();
redraw(svg_id);
update_concentration_plot('concentrations');
// Redraw based on the new size whenever the browser window is resized.
window.addEventListener("resize", e => {
redraw();
redraw(svg_id);
update_concentration_plot('concentrations');
});
}

View file

@ -455,8 +455,8 @@
</div><br>
<input type="text" class="form-control d-none" name="occupancy_format" value="static" required> {# "static" vs. "dynamic" #}
<input type="text" class="form-control d-none" name="dynamic_exposed_occupancy">
<input type="text" class="form-control d-none" name="dynamic_infected_occupancy">
<input type="text" class="form-control d-none" name="dynamic_exposed_occupancy" value="{}">
<input type="text" class="form-control d-none" name="dynamic_infected_occupancy" value="[]">
<div class="form-group row">
<div class="col-sm-4"><label class="col-form-label">Total number of occupants:</label></div>

View file

@ -54,356 +54,361 @@
<div class="tab-content" style="border-top: #dee2e6 1px solid; margin-top: -1px" >
<div class="tab-pane show active" id="results" role="tabpanel" aria-labelledby="results-tab" style="padding: 1%">
{% set long_range_prob_inf = prob_inf %}
{% set long_range_expected_cases = expected_new_cases %}
{# Update values if short range option is "short_range_yes" #}
{% if form.short_range_option == "short_range_yes" %}
{% set scenario = alternative_scenarios.stats.values() | first %}
{# Probability of infection values #}
{% set long_range_prob_inf = scenario.probability_of_infection %}
{# Expected new case values #}
{% set long_range_expected_cases = scenario.expected_new_cases %}
{% if form.exposure_option == 'p_probabilistic_exposure' %}
{% set long_range_prob_probabilistic_exposure = scenario.prob_probabilistic_exposure %}
{% for group_id, group_results in groups.items() %}
{% if group_results.get('long_range_prob_inf') %}{{group_results.long_range_prob_inf}}{% endif %}
{% set long_range_prob_inf = group_results.prob_inf %}
{% set long_range_expected_cases = group_results.expected_new_cases %}
{# Update values if short range option is "short_range_yes" #}
{% if group_results.get('long_range_prob') %}
{# Probability of infection values #}
{% set long_range_prob_inf = group_results.long_range_prob %}
{# Expected new case values #}
{% set long_range_expected_cases = group_results.long_range_expected_new_cases %}
{% if form.exposure_option == 'p_probabilistic_exposure' %}
{% set long_range_prob_probabilistic_exposure = scenario.prob_probabilistic_exposure %}
{% endif %}
{% endif %}
{% endif %}
{% block report_results %}
<div class="card bg-light mb-3" id="results-div">
<div class="card-header"><strong>Results </strong>
<button class="icon_button p-0 float-right" data-toggle="collapse" href="#collapseResults" role="button" aria-expanded="true" aria-controls="collapseResults">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-expand" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"/>
</svg>
</button>
</div>
<div class="collapse show" id="collapseResults">
<div class="card-body">
<p class="card-text">
<div class="align-self-center">
<div class="split">
<div class="card card-body align-self-center pi-box" style="text-align:center; max-width: 300px">
<h6 class="card-title">
<b>Probability of infection (%)</b><br>
{% if form.short_range_option == "short_range_yes" %}
Without <b>short-range interactions</b>
{% endif %}
</h6>
<br>
<img src="{{ get_url('/static/images') }}/long_range_anim.png" class="align-middle mb-3 pi-image">
<div class="d-flex" style="min-height: 160px">
{% block long_range_warning_animation %}
<div class="intro-banner-vdo-play-btn animation-color m-auto d-flex align-items-center justify-content-center">
<b>{{long_range_prob_inf | non_zero_percentage}}</b>
<i class="glyphicon glyphicon-play whiteText" aria-hidden="true"></i>
<span class="ripple animation-color"></span>
<span class="ripple animation-color"></span>
<span class="ripple animation-color"></span>
</div>
{% endblock long_range_warning_animation %}
</div>
<h6><b>Expected new cases:</b> {{ long_range_expected_cases | float_format }}</h6>
</div>
<br>
{% if form.short_range_option == "short_range_yes" %}
<div class="card card-body align-self-center pi-box" style="text-align:center; max-width: 300px">
<h6 class="card-title">
<b>Probability of infection (%)</b><br>
With <b>short-range interactions</b>
</h6>
<br>
<img src="{{ get_url('/static/images') }}/short_range_anim.png" class="align-middle mb-3 pi-image">
<div class="d-flex" style="min-height: 160px">
{% block warning_animation %}
<div class="intro-banner-vdo-play-btn animation-color m-auto d-flex align-items-center justify-content-center">
<b>{{prob_inf | non_zero_percentage}}</b>
<i class="glyphicon glyphicon-play whiteText" aria-hidden="true"></i>
<span class="ripple animation-color"></span>
<span class="ripple animation-color"></span>
<span class="ripple animation-color"></span>
</div>
{% endblock warning_animation %}
</div>
<h6><b>Expected new cases:</b> {{ expected_new_cases | float_format }}</h6>
</div>
{% endif %}
<div class="d-flex">
{% block report_summary %}
<div class="flex-row align-self-center">
<div class="align-self-center alert alert-dark mb-0" role="alert">
Taking into account the uncertainties tied to the model variables, in this scenario and assuming all occupants are exposed equally (i.e. without short-range interactions), the <b>probability of one exposed occupant getting infected is {{ long_range_prob_inf | non_zero_percentage }}</b>
and the <b>expected number of new cases is {{ long_range_expected_cases | float_format }}</b>*.
</div>
{% if form.short_range_option == "short_range_yes" %}
<br>
<div class="align-self-center alert alert-dark mb-0" role="alert">
In this scenario, the <b>probability the occupant(s) exposed to short-range interactions get infected can go as high as {{ prob_inf | non_zero_percentage }}</b>
and the <b>expected number of new cases increases to {{ expected_new_cases | float_format }}</b>.
</div>
{% endif %}
{% block probabilistic_exposure_probability %}
{% if form.exposure_option == "p_probabilistic_exposure" %}
<br>
<div class="align-self-center alert alert-dark mb-0" role="alert">
{% if form.occupancy_format == "static" %}
The above {{ "result assumes" if form.short_range_option == "short_range_no" else "results assume" }} that <b>{{ form.infected_people }}
{{ "occupant is infected" if form.infected_people == 1 else "occupants are infected" }}
</b> in the room.
By taking into account the estimate of cases currently circulating in <b>{{ form.location_name }}</b>,
the probability of on-site transmission, having at least 1 new infection in an <b>event
with {{ form.total_people }} occupants</b>, is
{% if form.short_range_option == 'short_range_yes' %}:
<ul>
<li><b>{{ long_range_prob_probabilistic_exposure | non_zero_percentage }}</b>, assuming all occupants are exposed equally (i.e. without short-range interactions).</li>
<li><b>{{ prob_probabilistic_exposure | non_zero_percentage }}</b>, assuming short-range interactions occur with the infector(s).</li>
</ul>
{% else %}
<b>{{ prob_probabilistic_exposure | non_zero_percentage }}</b>.
{% endif %}
{% else %}
<p><strong>Warning: </strong>Since dynamic occupancy was defined, the results for probabilistic exposure with incidence rates have not been computed.</p>
{% endif %}
</div>
{% endif %}
{% endblock probabilistic_exposure_probability %}
</div>
{% endblock report_summary %}
</div>
</div>
<br>
{% block report_summary_footnote %}
{% endblock report_summary_footnote %}
</div>
<br><p id="section1">* The results are based on the parameters and assumptions published in the CARA publication: <a href="https://doi.org/10.1098/rsfs.2021.0076"> doi.org/10.1098/rsfs.2021.0076</a>.</p><br>
{% if form.short_range_option == "short_range_yes" %}
{% if 'Speaking' in form.short_range_interactions|string or 'Shouting' in form.short_range_interactions|string %}
<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_hide_high_concentration">Hide high concentration</button>
{% endif %}
<input type="checkbox" id="long_range_cumulative_checkbox"><label class="form-check-label ml-1" for="long_range_cumulative_checkbox" id="lr_cumulative_checkbox_label">Show doses from long-range exposure alone</label>
{% endif %}
<div id="concentration_plot" style="height: 400px"></div>
<script type="application/javascript">
let times = {{ times | JSONify }};
let concentrations_zoomed = {{ concentrations_zoomed | JSONify }};
let concentrations = {{ concentrations | JSONify }};
let cumulative_doses = {{ cumulative_doses | JSONify }};
let long_range_cumulative_doses = {{ long_range_cumulative_doses | JSONify }};
let exposed_presence_intervals = {{ exposed_presence_intervals | JSONify }};
let short_range_intervals = {{ short_range_intervals | JSONify }};
let short_range_expirations = {{ short_range_expirations | JSONify }};
draw_plot("concentration_plot");
</script>
<i>IRP - Infectious Respiratory Particles.</i>
</p>
</div>
</div>
</div>
<div class="card bg-light mb-3" id="results-div">
<div class="card-header"><strong>Result uncertainties </strong>
<button class="icon_button p-0 float-right" data-toggle="collapse" href="#collapseUncertainties" role="button" aria-expanded="true" aria-controls="collapseUncertainties">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-expand" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"/>
</svg>
</button>
</div>
<div class="collapse show" id="collapseUncertainties">
<div class="card-body">
<div class="align-self-center">
<div id="prob_inf_hist" style="height: 400px"></div>
<script type="application/javascript">
let prob_dist = {{ prob_dist | JSONify }};
let prob_hist_count = {{ prob_hist_count | JSONify }};
let prob_hist_bins = {{ prob_hist_bins | JSONify }};
draw_histogram("prob_inf_hist", {{ prob_inf }}, {{ prob_inf_sd }});
</script>
<br>
{% if model.data_registry.virological_data['virus_distributions'][form.virus_type]['viral_load_in_sputum'] == 'Ref: Viral load - covid overal viral load data' %}
<div class="form-check">
<input type="checkbox" id="conditional_probability_viral_loads" class="tabbed form-check-input" name="conditional_probability_viral_loads" value="1" onClick="conditional_probability_viral_loads(this.checked, {{ form.conditional_probability_viral_loads | int }});">
<label id="label_conditional_probability_viral_loads" for="conditional_probability_viral_loads" class="form-check-label col-sm-12">Generate full uncertainty data (as function of the viral load)</label>
</div>
{% endif %}
{% if form.conditional_probability_viral_loads %}
<div id="conditional_probability_div">
<img src= "{{ uncertainties_plot_src }}" />
<div class="ml-5">
<p>(i) &nbsp;&nbsp;Predictive probability of infection for a given value of the viral load</p>
<p>(ii) &nbsp;Histogram of the viral load data</p>
<p>(iii) Histogram of the conditional probability of infection (result of total predictive probability in the middle)</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="card bg-light mb-3">
<div class="card-header"><strong>Predictive CO₂ Concentration Profile</strong>
<button class="icon_button p-0 float-right" data-toggle="collapse" href="#collapseCO₂Profile" role="button" aria-expanded="false" aria-controls="collapseCO₂Profile">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-expand" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"/>
</svg>
</button>
</div>
<div class="collapse" id="collapseCO₂Profile">
<div class="card-body">
<div>
<div id="CO2_concentration_graph" style="height: 400px"></div>
<script type="application/javascript">
var CO2_concentrations = {{ CO2_concentrations | JSONify }}
draw_generic_concentration_plot(
"CO2_concentration_graph",
"Mean concentration (ppm)",
h_lines = [
{'label': 'Acceptable level',
'y': 800,
'color': 'forestgreen',
'style': 'dashed'
},
{'label': 'Insufficient level',
'y': 1500,
'color': 'firebrick',
'style': 'dashed'
},
]
);
</script>
</div>
</div>
</div>
</div>
{% if form.short_range_option == "short_range_no" %}
<div class="card bg-light mb-3">
<div class="card-header"><strong>Alternative scenarios</strong>
<button class="icon_button p-0 float-right" data-toggle="collapse" href="#collapseAlternativeScenarios" role="button" aria-expanded="false" aria-controls="collapseAlternativeScenarios">
{% block report_results scoped %}
<div class="card bg-light mb-3" id="results-div">
<div class="card-header"><strong>Results{% if form.occupancy_format == 'dynamic' %} - {{ group_id }}{% endif %}</strong>
<button class="icon_button p-0 float-right" data-toggle="collapse" href="#collapseResults-group_{{ group_id }}" role="button" aria-expanded="true" aria-controls="collapseResults">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-expand" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"/>
</svg>
</button>
</div>
<div class="collapse" id="collapseAlternativeScenarios">
<div class="collapse show" id="collapseResults-group_{{ group_id }}">
<div class="card-body">
<div>
<div id="alternative_scenario_plot" style="height: 400px"></div>
<p class="card-text">
<div class="align-self-center">
<div class="split">
<div class="card card-body align-self-center pi-box" style="text-align:center; max-width: 300px">
<h6 class="card-title">
<b>Probability of infection (%)</b><br>
{% if group_results.get('long_range_prob') %}
Without <b>short-range interactions</b>
{% endif %}
</h6>
<br>
<img src="{{ get_url('/static/images') }}/long_range_anim.png" class="align-middle mb-3 pi-image">
<div class="d-flex" style="min-height: 160px">
{% block long_range_warning_animation scoped %}
<div class="intro-banner-vdo-play-btn animation-color m-auto d-flex align-items-center justify-content-center">
<b>{{ long_range_prob_inf | non_zero_percentage }}</b>
<i class="glyphicon glyphicon-play whiteText" aria-hidden="true"></i>
<span class="ripple animation-color"></span>
<span class="ripple animation-color"></span>
<span class="ripple animation-color"></span>
</div>
{% endblock long_range_warning_animation %}
</div>
<h6><b>Expected new cases:</b> {{ long_range_expected_cases | float_format }}</h6>
</div>
<br>
{% if group_results.get('long_range_prob') %}
<div class="card card-body align-self-center pi-box" style="text-align:center; max-width: 300px">
<h6 class="card-title">
<b>Probability of infection (%)</b><br>
With <b>short-range interactions</b>
</h6>
<br>
<img src="{{ get_url('/static/images') }}/short_range_anim.png" class="align-middle mb-3 pi-image">
<div class="d-flex" style="min-height: 160px">
{% block warning_animation scoped %}
<div class="intro-banner-vdo-play-btn animation-color m-auto d-flex align-items-center justify-content-center">
<b>{{ group_results.prob_inf | non_zero_percentage }}</b>
<i class="glyphicon glyphicon-play whiteText" aria-hidden="true"></i>
<span class="ripple animation-color"></span>
<span class="ripple animation-color"></span>
<span class="ripple animation-color"></span>
</div>
{% endblock warning_animation %}
</div>
<h6><b>Expected new cases:</b> {{ group_results.expected_new_cases | float_format }}</h6>
</div>
{% endif %}
<div class="d-flex">
{% block report_summary scoped %}
<div class="flex-row align-self-center">
<div class="align-self-center alert alert-dark mb-0" role="alert">
Taking into account the uncertainties tied to the model variables, in this scenario and assuming all occupants are exposed equally (i.e. without short-range interactions), the <b>probability of one exposed occupant getting infected is {{ long_range_prob_inf | non_zero_percentage }}</b>
and the <b>expected number of new cases is {{ long_range_expected_cases | float_format }}</b>*.
</div>
{% if group_results.get('long_range_prob') %}
<br>
<div class="align-self-center alert alert-dark mb-0" role="alert">
In this scenario, the <b>probability the occupant(s) exposed to short-range interactions get infected can go as high as {{ group_results.prob_inf | non_zero_percentage }}</b>
and the <b>expected number of new cases increases to {{ group_results.expected_new_cases | float_format }}</b>.
</div>
{% endif %}
{% block probabilistic_exposure_probability %}
{% if form.exposure_option == "p_probabilistic_exposure" %}
<br>
<div class="align-self-center alert alert-dark mb-0" role="alert">
{% if form.occupancy_format == "static" %}
The above {{ "result assumes" if form.short_range_option == "short_range_no" else "results assume" }} that <b>{{ form.infected_people }}
{{ "occupant is infected" if form.infected_people == 1 else "occupants are infected" }}
</b> in the room.
By taking into account the estimate of cases currently circulating in <b>{{ form.location_name }}</b>,
the probability of on-site transmission, having at least 1 new infection in an <b>event
with {{ form.total_people }} occupants</b>, is
{% if form.short_range_option == 'short_range_yes' %}:
<ul>
<li><b>{{ long_range_prob_probabilistic_exposure | non_zero_percentage }}</b>, assuming all occupants are exposed equally (i.e. without short-range interactions).</li>
<li><b>{{ prob_probabilistic_exposure | non_zero_percentage }}</b>, assuming short-range interactions occur with the infector(s).</li>
</ul>
{% else %}
<b>{{ prob_probabilistic_exposure | non_zero_percentage }}</b>.
{% endif %}
{% else %}
<p><strong>Warning: </strong>Since dynamic occupancy was defined, the results for probabilistic exposure with incidence rates have not been computed.</p>
{% endif %}
</div>
{% endif %}
{% endblock probabilistic_exposure_probability %}
</div>
{% endblock report_summary %}
</div>
</div>
<br>
{% block report_summary_footnote %}
{% endblock report_summary_footnote %}
</div>
<br><p id="section1">* The results are based on the parameters and assumptions published in the CARA publication: <a href="https://doi.org/10.1098/rsfs.2021.0076"> doi.org/10.1098/rsfs.2021.0076</a>.</p><br>
{% if group_results.get('long_range_prob') %}
{% if form.short_range_option == 'short_range_yes' and ('Speaking' in form.short_range_interactions[group_id]|string or 'Shouting' in form.short_range_interactions[group_id]|string) %}
<button class="btn btn-sm btn-primary" id="button_full_exposure-group_{{ group_id }}" disabled>Show full exposure</button>
<button class="btn btn-sm btn-primary ml-0" id="button_hide_high_concentration-group_{{ group_id }}">Hide high concentration</button>
{% endif %}
<input type="checkbox" id="long_range_cumulative_checkbox-group_{{ group_id }}">
<label class="form-check-label ml-1" for="long_range_cumulative_checkbox-group_{{ group_id }}" id="lr_cumulative_checkbox_label-group_{{ group_id }}">Show doses from long-range exposure alone</label>
{% endif %}
<div id="concentration_plot-group_{{ group_id }}" style="height: 400px"></div>
<script type="application/javascript">
var alternative_scenarios = {{ alternative_scenarios.stats | JSONify }}
draw_generic_concentration_plot(
"alternative_scenario_plot",
"Mean concentration (IRP/m³)",
);
draw_plot("concentration_plot-group_{{ group_id }}",
{{ group_id | JSONify }},
{{ times | JSONify }},
{{ group_results.get("concentrations_zoomed", []) | JSONify }},
{{ group_results.concentrations | JSONify }},
{{ group_results.cumulative_doses | JSONify }},
{{ group_results.get("long_range_cumulative_doses", []) | JSONify }},
{{ group_results.exposed_presence_intervals | JSONify }},
{{ group_results.get("short_range_interactions", []) | JSONify }}
)
</script>
<br>
{% block report_scenarios_summary_table %}
<table class="table w-auto">
<thead class="thead-light">
<tr>
<th>Scenario</th>
<th>P(I)</th>
{% if form.occupancy_format == "static" %}<th>Expected new cases</th>{% endif %}
</tr>
</thead>
<tbody>
{% for scenario_name, scenario_stats in alternative_scenarios.stats.items() %}
<tr>
<td> {{ scenario_name }}</td>
<td> {{ scenario_stats.probability_of_infection | non_zero_percentage }}</td>
{% if form.occupancy_format == "static" %}<td style="text-align:right">{{ scenario_stats.expected_new_cases | float_format }}</td>{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% endblock report_scenarios_summary_table %}
</div>
<br/>
<p class="data_text"> <strong> Notes for alternative scenarios: </strong><br>
<ol>
<li>This graph shows the concentration of infectious quanta in the air. The filtration of Type I and FFP2 masks, if worn, applies not only to the emission rate but also to the individual exposure (i.e. inhalation).
For this reason, scenarios with different types of mask will show the same concentration on the graph but have different absorbed doses and infection probabilities.</li>
<li>If you have selected more sophisticated options, such as HEPA filtration or FFP2 masks, this will be indicated in the plot as the "base scenario", representing the inputs inserted in the form.<br>
The other alternative scenarios shown for comparison will not include either HEPA filtration or FFP2 masks.</li>
</ol>
<br>
<i>IRP - Infectious Respiratory Particles.</i>
</p>
</div>
</div>
</div>
{% endif %}
{% endblock report_results %}
<!-- Export Data Concentration Modal -->
<div class="modal fade" id="modalCSV" tabindex="-1" role="dialog" aria-labelledby="modalCSV" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">Export data (.csv)</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
<div class="card bg-light mb-3" id="results-div">
<div class="card-header"><strong>Result uncertainties{% if form.occupancy_format == 'dynamic' %} - {{ group_id }}{% endif %}</strong>
<button class="icon_button p-0 float-right" data-toggle="collapse" href="#collapseUncertainties-group_{{ group_id }}" role="button" aria-expanded="true" aria-controls="collapseUncertainties">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-expand" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"/>
</svg>
</button>
</div>
<div class="modal-body">
<b>Select the data to export:</b>
<div class="form-check mt-3">
<input type="checkbox" class="form-check-input" name="checkedItems" id="Time" onclick="display_rename_column(this.checked, 'time-rename-div')" checked disabled>
<label class="form-check-label" for="Time">Time of day</label>
<div id="time-rename-div" class="d-flex align-items-center">
<label class="col-form-label" for="time-rename-div">Column name:</label>
<input type="text" class="form-control form-control-sm col-sm-4 ml-2" id="Time__rename" placeholder="Time" value="Time">
<div class="collapse show" id="collapseUncertainties-group_{{ group_id }}">
<div class="card-body">
<div class="align-self-center">
<div id="prob_inf_hist-group_{{ group_id }}" style="height: 400px"></div>
<script type="application/javascript">
prob_dist = {{ group_results.prob_dist | JSONify }};
prob_hist_count = {{ group_results.prob_hist_count | JSONify }};
prob_hist_bins = {{ group_results.prob_hist_bins | JSONify }};
draw_histogram("prob_inf_hist-group_{{ group_id }}", {{ group_results.prob_inf }}, {{ group_results.prob_inf_sd }});
</script>
<br>
{% if group_results.model.data_registry.virological_data['virus_distributions'][form.virus_type]['viral_load_in_sputum'] == 'Ref: Viral load - covid overal viral load data' %}
<div class="form-check">
<input type="checkbox" id="conditional_probability_viral_loads" class="tabbed form-check-input" name="conditional_probability_viral_loads" value="1" onClick="conditional_probability_viral_loads(this.checked, {{ form.conditional_probability_viral_loads | int }});">
<label id="label_conditional_probability_viral_loads" for="conditional_probability_viral_loads" class="form-check-label col-sm-12">Generate full uncertainty data (as function of the viral load)</label>
</div>
{% endif %}
{% if form.conditional_probability_viral_loads %}
<div id="conditional_probability_div">
<img src= "{{ group_results.uncertainties_plot_src }}" />
<div class="ml-5">
<p>(i) &nbsp;&nbsp;Predictive probability of infection for a given value of the viral load</p>
<p>(ii) &nbsp;Histogram of the viral load data</p>
<p>(iii) Histogram of the conditional probability of infection (result of total predictive probability in the middle)</p>
</div>
</div>
{% endif %}
</div>
</div>
{# If short-range interactions are set, we don't display Alternative Scenarios, and there is no need to arrange items in a list #}
{% if form.short_range_option == "short_range_no" %}
<ul style="padding-left: inherit">
<li>
Current Scenario
{% endif %}
<div class="form-check">
<input type="checkbox" class="form-check-input" name="checkedItems" id="Concentration" onclick="display_rename_column(this.checked, 'concentration-rename-div')">
<label class="form-check-label" for="Concentration">Concentration</label>
<div id="concentration-rename-div" class="align-items-center" style="display:none">
<label class="col-form-label" for="concentration-rename-div">Column name:</label>
<input type="text" class="form-control form-control-sm col-sm-4 ml-2" id="Concentration__rename" placeholder="Concentration" value="Concentration">
</div>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="checkedItems" id="Cumulative Dose" onclick="display_rename_column(this.checked, 'cumulative-dose-rename-div')">
<label class="form-check-label" for="Cumulative Dose">Cumulative dose</label>
<div id="cumulative-dose-rename-div" class="align-items-center" style="display:none">
<label class="col-form-label" for="cumulative-dose-rename-div">Column name:</label>
<input type="text" class="form-control form-control-sm col-sm-4 ml-2" id="Cumulative Dose__rename" placeholder="Cumulative Dose" value="Cumulative Dose">
</div>
</div>
</li>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="checkedItems" id="CO2_Concentration" onclick="display_rename_column(this.checked, 'CO2-rename-div')">
<label class="form-check-label" for="CO2_Concentration">CO₂ Concentration</label>
<div id="CO2-rename-div" class="align-items-center" style="display:none">
<label class="col-form-label" for="CO2-rename-div">Column name:</label>
<input type="text" class="form-control form-control-sm col-sm-4 ml-2" id="CO2_Concentration__rename" placeholder="CO₂ Concentration" value="CO₂ Concentration">
</div>
</div>
{% if form.short_range_option == "short_range_no" %}
</li>
<li>
Alternative Scenarios
<div class="form-check">
<input type="checkbox" class="form-check-input" name="checkedItems" id="Alternative Scenarios" onclick="check_download_button(); display_column_name_warning(this.checked);">
<label class="form-check-label" for="Alternative Scenarios">Concentration</label>
<p id="alternative_scenario_warning" class="text-warning" style="display: none">The column name will be the scenario name.</p>
</div>
</li>
{% endif %}
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button id="downloadCSV" type="button" class="btn btn-primary" onclick="export_csv();" disabled>Download</button>
</div>
{% if form.short_range_option == "short_range_no" and form.occupancy_format == "static" %}
<div class="card bg-light mb-3">
<div class="card-header"><strong>Alternative scenarios</strong>
<button class="icon_button p-0 float-right" data-toggle="collapse" href="#collapseAlternativeScenarios" role="button" aria-expanded="false" aria-controls="collapseAlternativeScenarios">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-expand" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"/>
</svg>
</button>
</div>
<div id="collapseAlternativeScenarios">
<div class="card-body">
<div>
<div id="alternative_scenario_plot" style="height: 400px"></div>
<script type="application/javascript">
var alternative_scenarios = {{ alternative_scenarios.stats | JSONify }}
draw_generic_concentration_plot(
"alternative_scenario_plot",
{{ times | JSONify }},
"Mean concentration (IRP/m³)",
);
</script>
<br>
{% block report_scenarios_summary_table %}
<table class="table w-auto">
<thead class="thead-light">
<tr>
<th>Scenario</th>
<th>P(I)</th>
<th>Expected new cases</th>
</tr>
</thead>
<tbody>
{% for scenario_name, scenario_stats in alternative_scenarios.stats.items() %}
<tr>
<td> {{ scenario_name }}</td>
<td> {{ scenario_stats.probability_of_infection | non_zero_percentage }}</td>
<td style="text-align:right">{{ scenario_stats.expected_new_cases | float_format }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock report_scenarios_summary_table %}
</div>
<br/>
<p class="data_text"> <strong> Notes for alternative scenarios: </strong><br>
<ol>
<li>This graph shows the concentration of infectious quanta in the air. The filtration of Type I and FFP2 masks, if worn, applies not only to the emission rate but also to the individual exposure (i.e. inhalation).
For this reason, scenarios with different types of mask will show the same concentration on the graph but have different absorbed doses and infection probabilities.</li>
<li>If you have selected more sophisticated options, such as HEPA filtration or FFP2 masks, this will be indicated in the plot as the "base scenario", representing the inputs inserted in the form.<br>
The other alternative scenarios shown for comparison will not include either HEPA filtration or FFP2 masks.</li>
</ol>
<br>
</p>
</div>
</div>
</div>
{% endif %}
{% endblock report_results %}
<!-- Export Data Concentration Modal -->
<div class="modal fade" id="modalCSV" tabindex="-1" role="dialog" aria-labelledby="modalCSV" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">Export data (.csv)</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<b>Select the data to export:</b>
<div class="form-check mt-3">
<input type="checkbox" class="form-check-input" name="checkedItems" id="Time" onclick="display_rename_column(this.checked, 'time-rename-div')" checked disabled>
<label class="form-check-label" for="Time">Time of day</label>
<div id="time-rename-div" class="d-flex align-items-center">
<label class="col-form-label" for="time-rename-div">Column name:</label>
<input type="text" class="form-control form-control-sm col-sm-4 ml-2" id="Time__rename" placeholder="Time" value="Time">
</div>
</div>
{# If short-range interactions are set, we don't display Alternative Scenarios, and there is no need to arrange items in a list #}
{% if form.short_range_option == "short_range_no" %}
<ul style="padding-left: inherit">
<li>
Current Scenario
{% endif %}
<div class="form-check">
<input type="checkbox" class="form-check-input" name="checkedItems" id="Concentration" onclick="display_rename_column(this.checked, 'concentration-rename-div')">
<label class="form-check-label" for="Concentration">Concentration</label>
<div id="concentration-rename-div" class="align-items-center" style="display:none">
<label class="col-form-label" for="concentration-rename-div">Column name:</label>
<input type="text" class="form-control form-control-sm col-sm-4 ml-2" id="Concentration__rename" placeholder="Concentration" value="Concentration">
</div>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="checkedItems" id="Cumulative Dose" onclick="display_rename_column(this.checked, 'cumulative-dose-rename-div')">
<label class="form-check-label" for="Cumulative Dose">Cumulative dose</label>
<div id="cumulative-dose-rename-div" class="align-items-center" style="display:none">
<label class="col-form-label" for="cumulative-dose-rename-div">Column name:</label>
<input type="text" class="form-control form-control-sm col-sm-4 ml-2" id="Cumulative Dose__rename" placeholder="Cumulative Dose" value="Cumulative Dose">
</div>
</div>
</li>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="checkedItems" id="CO2_Concentration" onclick="display_rename_column(this.checked, 'CO2-rename-div')">
<label class="form-check-label" for="CO2_Concentration">CO₂ Concentration</label>
<div id="CO2-rename-div" class="align-items-center" style="display:none">
<label class="col-form-label" for="CO2-rename-div">Column name:</label>
<input type="text" class="form-control form-control-sm col-sm-4 ml-2" id="CO2_Concentration__rename" placeholder="CO₂ Concentration" value="CO₂ Concentration">
</div>
</div>
{% if form.short_range_option == "short_range_no" and form.occupancy_format == "static" %}
</li>
<li>
Alternative Scenarios
<div class="form-check">
<input type="checkbox" class="form-check-input" name="checkedItems" id="Alternative Scenarios" onclick="check_download_button(); display_column_name_warning(this.checked);">
<label class="form-check-label" for="Alternative Scenarios">Concentration</label>
<p id="alternative_scenario_warning" class="text-warning" style="display: none">The column name will be the scenario name.</p>
</div>
</li>
{% endif %}
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button id="downloadCSV" type="button" class="btn btn-primary" onclick="export_csv();" disabled>Download</button>
</div>
</div>
</div>
</div>
{% endfor %}
<div class="card bg-light mb-3">
<div class="card-header"><strong>Predictive CO₂ Concentration Profile</strong>
<button class="icon_button p-0 float-right" data-toggle="collapse" href="#collapseCO₂Profile" role="button" aria-expanded="true" aria-controls="collapseCO₂Profile">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-expand" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"/>
</svg>
</button>
</div>
<div id="collapseCO₂Profile">
<div class="card-body">
<div>
<div id="CO2_concentration_graph" style="height: 400px"></div>
<script type="application/javascript">
var CO2_concentrations = {{ CO2_concentrations | JSONify }}
draw_generic_concentration_plot(
"CO2_concentration_graph",
{{ times | JSONify }},
"Mean concentration (ppm)",
h_lines = [
{'label': 'Acceptable level',
'y': 800,
'color': 'forestgreen',
'style': 'dashed'
},
{'label': 'Insufficient level',
'y': 1500,
'color': 'firebrick',
'style': 'dashed'
},
]
);
</script>
</div>
</div>
</div>
@ -437,7 +442,7 @@
</div>
<div class="tab-pane" id="data" role="tabpanel" aria-labelledby="data-tab" style="padding: 1%">
{% block simulation_overview %}
{% block simulation_overview scoped %}
<div class="card">
<div class="card-header"><strong>Simulation:</strong></div>
<div class="card-body">

View file

@ -4,26 +4,26 @@
{% set orange_prob_lim = 2 %}
{% set red_prob_lim = 10 %}
{% if form.short_range_option == "short_range_yes" %}
{% set scenario = alternative_scenarios.stats.values() | first %}
{% set long_range_prob_inf = scenario.probability_of_infection %}
{% set group_results = groups.values() | first %}
{% if group_results.get('long_range_prob') %}
{% set long_range_prob_inf = group_results.long_range_prob %}
{% else %}
{% set long_range_prob_inf = prob_inf %}
{% set long_range_prob_inf = group_results.prob_inf %}
{% endif %}
{% if ((long_range_prob_inf > red_prob_lim) or (form.occupancy_format == "static" and expected_new_cases >= 1)) %}
{% if ((long_range_prob_inf > red_prob_lim) or (form.occupancy_format == "static" and group_results.expected_new_cases >= 1)) %}
{% set long_range_scale_warning = 'red' %}
{% set long_range_warning_color= 'bg-danger' %}
{% elif (orange_prob_lim <= long_range_prob_inf <= red_prob_lim) %}
{% set long_range_scale_warning = 'orange' %}
{% set long_range_warning_color = 'bg-warning' %}
{% set long_range_scale_warning = 'orange' %}
{% set long_range_warning_color = 'bg-warning' %}
{% else %}
{% set long_range_scale_warning = 'green' %}
{% set long_range_warning_color = 'bg-success' %}
{% endif %}
{% if ((prob_inf > red_prob_lim) or (form.occupancy_format == "static" and expected_new_cases >= 1)) %} {% set scale_warning = 'red' %}
{% elif (orange_prob_lim <= prob_inf <= red_prob_lim) %} {% set scale_warning = 'orange' %}
{% if ((group_results.prob_inf > red_prob_lim) or (form.occupancy_format == "static" and group_results.expected_new_cases >= 1)) %} {% set scale_warning = 'red' %}
{% elif (orange_prob_lim <= group_results.prob_inf <= red_prob_lim) %} {% set scale_warning = 'orange' %}
{% else %} {% set scale_warning = 'green' %}
{% endif %}
@ -49,7 +49,7 @@
{% elif scale_warning == 'green' %} {% set warning_color = 'bg-success' %}
{% endif %}
<div class="intro-banner-vdo-play-btn {{warning_color}} m-auto d-flex align-items-center justify-content-center">
<b>{{prob_inf | non_zero_percentage}}</b>
<b>{{ group_results.prob_inf | non_zero_percentage}}</b>
<i class="glyphicon glyphicon-play whiteText" aria-hidden="true"></i>
<span class="ripple {{warning_color}}"></span>
<span class="ripple {{warning_color}}"></span>
@ -85,8 +85,8 @@
<div class="alert alert-success mb-0" role="alert">
<strong>Acceptable:</strong>
{% endif %}
In this scenario, the <b>probability the occupant(s) exposed to short-range interactions get infected can go as high as {{ prob_inf | non_zero_percentage }}</b>
and the <b>expected number of new cases increases to {{ expected_new_cases | float_format }}</b>
In this scenario, the <b>probability the occupant(s) exposed to short-range interactions get infected can go as high as {{ group_results.prob_inf | non_zero_percentage }}</b>
and the <b>expected number of new cases increases to {{ group_results.expected_new_cases | float_format }}</b>
</div>
{% endif %}
@ -94,7 +94,7 @@
{{ super() }}
{% endblock probabilistic_exposure_probability %}
{% if (prob_inf > 2) %}
{% if (group_results.prob_inf > 2) %}
<br>
{% if cern_level == "green-1" %}
<div class="alert alert-dark mb-0" role="alert" style="height:fit-content">

View file

@ -90,6 +90,6 @@ def exposure_model_w_outside_temp_changes(data_registry, baseline_exposure_model
def baseline_form_with_sr(baseline_form_data, data_registry):
form_data_sr = baseline_form_data
form_data_sr['short_range_option'] = 'short_range_yes'
form_data_sr['short_range_interactions'] = '{"group_1": [{"expiration": "Shouting", "start_time": "10:30", "duration": 30}]}'
form_data_sr['short_range_interactions'] = '{"static": [{"expiration": "Shouting", "start_time": "10:30", "duration": 30}]}'
form_data_sr['short_range_occupants'] = 5
return virus_validator.VirusFormData.from_dict(form_data_sr, data_registry)

View file

@ -112,8 +112,8 @@ def test_expected_new_cases(baseline_form_with_sr: VirusFormData):
# Short- and Long-range contributions
report_data = rep_gen.calculate_report_data(baseline_form_with_sr, executor_factory)
sr_lr_expected_new_cases = report_data['expected_new_cases']
sr_lr_prob_inf = report_data['prob_inf']/100
sr_lr_expected_new_cases = report_data['groups']['static']['expected_new_cases']
sr_lr_prob_inf = report_data['groups']['static']['prob_inf']/100
# Long-range contributions alone
alternative_scenarios = rep_gen.manufacture_alternative_scenarios(baseline_form_with_sr)