from dataclasses import dataclass import typing import numpy as np from scipy import special as sp from scipy.stats import weibull_min import caimira.monte_carlo as mc from caimira.monte_carlo.sampleable import LogCustom, LogNormal,LogCustomKernel,CustomKernel,Uniform, Custom sqrt2pi = np.sqrt(2.*np.pi) sqrt2 = np.sqrt(2.) @dataclass(frozen=True) class BLOmodel: """ Represents the probability distribution from the BLO model. It is a sum of three lognormal distributions, each of the form A * cn * (1 / d) * (1 / (np.sqrt(2 * np.pi) * sigma)) * np.exp(-(np.log(d)-mu) ** 2 / (2 * sigma ** 2)) with A the factor in front of the B, L or O mode. From G. Johnson et al., Modality of human expired aerosol size distributions, Journal of Aerosol Science, vol. 42, no. 12, pp. 839 – 851, 2011, https://doi.org/10.1016/j.jaerosci.2011.07.009). """ #: Factors assigned to resp. the B, L and O modes. They are # charateristics of the kind of expiratory activity (e.g. breathing, # speaking, singing, or shouting). These are applied on top of the # cn concentrations (see below), and depend on the kind of activity # (breathing, speaking, singing/shouting) BLO_factors: typing.Tuple[float, float, float] #: cn (cm^-3) for resp. the B, L and O modes. Corresponds to the # total concentration of aerosols for each mode. cn: typing.Tuple[float, float, float] = (0.06, 0.2, 0.0010008) # Mean of the underlying normal distributions (represents the log of a # diameter in microns), for resp. the B, L and O modes. mu: typing.Tuple[float, float, float] = (0.989541, 1.38629, 4.97673) # Std deviation of the underlying normal distribution, for resp. # the B, L and O modes. sigma: typing.Tuple[float, float, float] = (0.262364, 0.506818, 0.585005) def distribution(self, d): """ Returns the raw value of the probability distribution for a given diameter d (microns). """ return sum( (1 / d) * (A * cn / (sqrt2pi * sigma)) * np.exp(-(np.log(d) - mu) ** 2 / (2 * sigma ** 2)) for A,cn,mu,sigma in zip(self.BLO_factors, self.cn, self.mu, self.sigma) ) def integrate(self, dmin, dmax): """ Returns the integral between dmin and dmax (in microns) of the probability distribution. """ result = 0. for A,cn,mu,sigma in zip(self.BLO_factors, self.cn, self.mu, self.sigma): ymin = (np.log(dmin)-mu)/(sqrt2*sigma) ymax = (np.log(dmax)-mu)/(sqrt2*sigma) result += A * cn * (sp.erf(ymax)-sp.erf(ymin)) / 2. return result # From https://doi.org/10.1101/2021.10.14.21264988 and references therein activity_distributions = { 'Seated': mc.Activity(LogNormal(-0.6872121723362303, 0.10498338229297108), LogNormal(-0.6872121723362303, 0.10498338229297108)), 'Standing': mc.Activity(LogNormal(-0.5742377578494785, 0.09373162411398223), LogNormal(-0.5742377578494785, 0.09373162411398223)), 'Light activity': mc.Activity(LogNormal(0.21380242785625422,0.09435378091059601), LogNormal(0.21380242785625422,0.09435378091059601)), 'Moderate activity': mc.Activity(LogNormal(0.551771330362601, 0.1894616357138137), LogNormal(0.551771330362601, 0.1894616357138137)), 'Heavy exercise': mc.Activity(LogNormal(1.1644665696723049, 0.21744554768657565), LogNormal(1.1644665696723049, 0.21744554768657565)), } # From https://doi.org/10.1101/2021.10.14.21264988 and references therein symptomatic_vl_frequencies = LogCustomKernel( np.array((2.46032, 2.67431, 2.85434, 3.06155, 3.25856, 3.47256, 3.66957, 3.85979, 4.09927, 4.27081, 4.47631, 4.66653, 4.87204, 5.10302, 5.27456, 5.46478, 5.6533, 5.88428, 6.07281, 6.30549, 6.48552, 6.64856, 6.85407, 7.10373, 7.30075, 7.47229, 7.66081, 7.85782, 8.05653, 8.27053, 8.48453, 8.65607, 8.90573, 9.06878, 9.27429, 9.473, 9.66152, 9.87552)), np.array((0.001206885, 0.007851618, 0.008078144, 0.01502491, 0.013258014, 0.018528495, 0.020053765, 0.021896167, 0.022047184, 0.018604005, 0.01547796, 0.018075445, 0.021503523, 0.022349217, 0.025097721, 0.032875078, 0.030594727, 0.032573045, 0.034717482, 0.034792991, 0.033267721, 0.042887485, 0.036846816, 0.03876473, 0.045016819, 0.040063473, 0.04883754, 0.043944602, 0.048142864, 0.041588741, 0.048762031, 0.027921732, 0.033871788, 0.022122693, 0.016927718, 0.008833228, 0.00478598, 0.002807662)), kernel_bandwidth=0.1 ) # Weibull distribution with a shape factor of 3.47 and a scale factor of 7.01. # From https://elifesciences.org/articles/65774 and first line of the figure in # https://iiif.elifesciences.org/lax:65774%2Felife-65774-fig4-figsupp3-v2.tif/full/1500,/0/default.jpg viral_load = np.linspace(weibull_min.ppf(0.01, c=3.47, scale=7.01), weibull_min.ppf(0.99, c=3.47, scale=7.01), 30) frequencies_pdf = weibull_min.pdf(viral_load, c=3.47, scale=7.01) covid_overal_vl_data = LogCustom(bounds=(2, 10), function=lambda d: np.interp(d, viral_load, frequencies_pdf, left=0., right=0.), max_function=0.2) # Derived from data in doi.org/10.1016/j.ijid.2020.09.025 and # https://iosh.com/media/8432/aerosol-infection-risk-hospital-patient-care-full-report.pdf (page 60) viable_to_RNA_ratio_distribution = Uniform(0.01, 0.6) # From discussion with virologists infectious_dose_distribution = Uniform(10., 100.) # From https://doi.org/10.1101/2021.10.14.21264988 and refererences therein virus_distributions = { 'SARS_CoV_2': mc.SARSCoV2( viral_load_in_sputum=covid_overal_vl_data, infectious_dose=infectious_dose_distribution, viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, transmissibility_factor=1., ), 'SARS_CoV_2_ALPHA': mc.SARSCoV2( viral_load_in_sputum=covid_overal_vl_data, infectious_dose=infectious_dose_distribution, viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, transmissibility_factor=0.78, ), 'SARS_CoV_2_BETA': mc.SARSCoV2( viral_load_in_sputum=covid_overal_vl_data, infectious_dose=infectious_dose_distribution, viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, transmissibility_factor=0.8, ), 'SARS_CoV_2_GAMMA': mc.SARSCoV2( viral_load_in_sputum=covid_overal_vl_data, infectious_dose=infectious_dose_distribution, viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, transmissibility_factor=0.72, ), 'SARS_CoV_2_DELTA': mc.SARSCoV2( viral_load_in_sputum=covid_overal_vl_data, infectious_dose=infectious_dose_distribution, viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, transmissibility_factor=0.51, ), 'SARS_CoV_2_OMICRON': mc.SARSCoV2( viral_load_in_sputum=covid_overal_vl_data, infectious_dose=infectious_dose_distribution, viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, transmissibility_factor=0.2, ), } # From: # https://doi.org/10.1080/02786826.2021.1890687 # https://doi.org/10.1016/j.jhin.2013.02.007 # https://doi.org/10.4209/aaqr.2020.08.0531 mask_distributions = { 'Type I': mc.Mask(Uniform(0.25, 0.80)), 'FFP2': mc.Mask(Uniform(0.83, 0.91)), } def expiration_distribution( BLO_factors, d_max=30., ) -> mc.Expiration: """ Returns an Expiration with an aerosol diameter distribution, defined by the BLO factors (a length-3 tuple). The total concentration of aerosols, cn, is computed by integrating the distribution between 0.1 and 30 microns - these boundaries are an historical choice based on previous implementations of the model (it limits the influence of the O-mode). """ dscan = np.linspace(0.1, d_max, 3000) return mc.Expiration( CustomKernel( dscan, BLOmodel(BLO_factors).distribution(dscan), kernel_bandwidth=0.1, ), cn=BLOmodel(BLO_factors).integrate(0.1, d_max), ) expiration_BLO_factors = { 'Breathing': (1., 0., 0.), 'Speaking': (1., 1., 1.), 'Singing': (1., 5., 5.), 'Shouting': (1., 5., 5.), } expiration_distributions = { exp_type: expiration_distribution(BLO_factors) for exp_type, BLO_factors in expiration_BLO_factors.items() } short_range_expiration_distributions = { exp_type: expiration_distribution(BLO_factors, d_max=100) for exp_type, BLO_factors in expiration_BLO_factors.items() } # Derived from Fig 8 a) "stand-stand" in https://www.mdpi.com/1660-4601/17/4/1445/htm distances = np.array((0.5,0.6,0.7,0.8,0.9,1,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,2)) frequencies = np.array((0.0598036,0.0946154,0.1299152,0.1064905,0.1099066,0.0998209, 0.0845298,0.0479286,0.0406084,0.039795,0.0205997,0.0152316,0.0118155,0.0118155,0.018485,0.0205997)) short_range_distances = Custom(bounds=(0.5,2.), function=lambda x: np.interp(x,distances,frequencies,left=0.,right=0.), max_function=0.13)