@@ -1327,6 +1327,59 @@ def normalization_factor(self) -> _VectorisedFloat:
13271327 # CO2 concentration given in ppm, hence the 1e6 factor.
13281328 return (1e6 * self .population .activity .exhalation_rate
13291329 * self .CO2_fraction_exhaled )
1330+
1331+ class CO2ConcentrationModelGroup :
1332+ """
1333+ CO2 model with dynamic occupancy, one CO2ConcentrationModel per group.
1334+ """
1335+
1336+ concentration_models = list [CO2ConcentrationModel ]
1337+
1338+ def __post_init__ (self ):
1339+ self .data_registry = set ([model .data_registry for model in self .concentration_models ])
1340+ self .room = set ([model .data_registry for model in self .concentration_models ])
1341+ self .ventilation = set ([model .data_registry for model in self .concentration_models ])
1342+ self .background_concentration = set ([model .min_background_concentration () for model in self .concentration_models ])
1343+
1344+ if len (self .data_registry ) != 1 :
1345+ raise ValueError ("All concentration models must have the same data_registry." )
1346+ if len (self .room ) != 1 :
1347+ raise ValueError ("All concentration models must have the same room." )
1348+ if len (self .ventilation ) != 1 :
1349+ raise ValueError ("All concentration models must have the same ventilation." )
1350+ if len (self .background_concentration ) != 1 :
1351+ raise ValueError ("All concentration models must have the same background_concentration." )
1352+
1353+ @property
1354+ def population (self ) -> MultiplePopulations :
1355+ return MultiplePopulations (
1356+ populations = [model .population for model in self .concentration_models ]
1357+ )
1358+
1359+ @method_cache
1360+ def state_change_times (self ) -> typing .List [float ]:
1361+ """
1362+ All time dependent entities on this model must provide information about
1363+ the times at which their state changes.
1364+ """
1365+ # state_change_times = {0.}
1366+ # for model in self.concentration_models:
1367+ # state_change_times.update(model.state_change_times())
1368+ # return sorted(state_change_times)
1369+ state_change_times = {0. }
1370+ state_change_times .update (self .population .transition_times ())
1371+ state_change_times .update (self .ventilation .transition_times (self .room ))
1372+ return sorted (state_change_times )
1373+
1374+ def _align_state_change_times (self ):
1375+ for i , model in enumerate (self .concentration_models ):
1376+ """if state change not in model add it to the model""" #PROBLEM: calculations stop when group leaves
1377+ pass
1378+
1379+ def concentration (self ):
1380+ self ._align_state_change_times ()
1381+ return np .sum ([model .concentration for model in self .concentration_models ]) - self .background_concentration * (len (self .concentration_models ) - 1 )
1382+
13301383
13311384
13321385@dataclass (frozen = True )
@@ -1706,7 +1759,7 @@ def population_state_change_times(self) -> typing.List[float]:
17061759 All time dependent population entities on this model must provide information
17071760 about the times at which their state changes.
17081761 """
1709- state_change_times = set (self .concentration_model .infected .presence_interval ().transition_times ())
1762+ state_change_times = set (self .concentration_model .infected .presence_interval ().transition_times ()) #update with all concentration_models
17101763 state_change_times .update (self .exposed .presence_interval ().transition_times ())
17111764
17121765 return sorted (state_change_times )
@@ -1718,9 +1771,9 @@ def long_range_fraction_deposited(self) -> _VectorisedFloat:
17181771 particle diameter.
17191772 """
17201773 return self .concentration_model .infected .particle .fraction_deposited (
1721- self .concentration_model .evaporation_factor )
1774+ self .concentration_model .evaporation_factor ) #sum over all concentration_models
17221775
1723- def _long_range_normed_exposure_between_bounds (self , time1 : float , time2 : float ) -> _VectorisedFloat :
1776+ def _long_range_normed_exposure_between_bounds (self , time1 : float , time2 : float ) -> _VectorisedFloat :#sum over all concentration_models
17241777 """
17251778 The number of virions per meter^3 between any two times, normalized
17261779 by the emission rate of the infected population
@@ -1748,7 +1801,7 @@ def concentration(self, time: float) -> _VectorisedFloat:
17481801 It considers the long-range concentration with the
17491802 contribution of the short-range concentration.
17501803 """
1751- concentration = self .concentration_model .concentration (time )
1804+ concentration = self .concentration_model .concentration (time )#sum over all concentration_models
17521805 for interaction in self .short_range :
17531806 concentration += interaction .short_range_concentration (self .concentration_model , time )
17541807 return concentration
@@ -1758,7 +1811,7 @@ def long_range_deposited_exposure_between_bounds(self, time1: float, time2: floa
17581811
17591812 emission_rate_per_aerosol_per_person = \
17601813 self .concentration_model .infected .emission_rate_per_aerosol_per_person_when_present ()
1761- aerosols = self .concentration_model .infected .aerosols ()
1814+ aerosols = self .concentration_model .infected .aerosols ()#sum over all concentration_models
17621815 fdep = self .long_range_fraction_deposited ()
17631816
17641817 diameter = self .concentration_model .infected .particle .diameter
@@ -1861,18 +1914,18 @@ def _infection_probability_list(self):
18611914 vD_list = self ._deposited_exposure_list ()
18621915
18631916 # oneoverln2 multiplied by ID_50 corresponds to ID_63.
1864- infectious_dose = oneoverln2 * self .concentration_model .virus .infectious_dose
1917+ infectious_dose = oneoverln2 * self .concentration_model .virus .infectious_dose #all concentration_models with same virus
18651918
18661919 # Probability of infection.
18671920 return [(1 - np .exp (- ((vD * (1 - self .exposed .host_immunity ))/ (infectious_dose *
1868- self .concentration_model .virus .transmissibility_factor )))) for vD in vD_list ]
1921+ self .concentration_model .virus .transmissibility_factor )))) for vD in vD_list ] #all concentration_models with same virus (if several viruses, calculate infection risk for each of them)
18691922
18701923 @method_cache
18711924 def infection_probability (self ) -> _VectorisedFloat :
18721925 return (1 - np .prod ([1 - prob for prob in self ._infection_probability_list ()], axis = 0 )) * 100
18731926
18741927 def total_probability_rule (self ) -> _VectorisedFloat :
1875- if (isinstance (self .concentration_model .infected .number , IntPiecewiseConstant )):
1928+ if (isinstance (self .concentration_model .infected .number , IntPiecewiseConstant )):#change test
18761929 raise NotImplementedError ("Cannot compute total probability "
18771930 "(including incidence rate) with dynamic occupancy" )
18781931
@@ -1918,7 +1971,7 @@ def reproduction_number(self) -> _VectorisedFloat:
19181971 The reproduction number can be thought of as the expected number of
19191972 cases directly generated by one infected case in a population.
19201973 """
1921- infected_population : InfectedPopulation = self .concentration_model .infected
1974+ infected_population : InfectedPopulation = self .concentration_model .infected #sum over all concentration_models
19221975 if isinstance (infected_population .number , int ) and infected_population .number == 1 :
19231976 return self .expected_new_cases ()
19241977
0 commit comments