Source code for fpsim.parameters

'''
Handle sim parameters
'''

import sciris as sc
import starsim as ss
import fpsim as fp

__all__ = ['SimPars', 'FPPars', 'make_sim_pars', 'make_fp_pars', 'par_keys', 'sim_par_keys', 'all_pars', 'mergepars']


# %% Parameter creation functions

[docs] class SimPars(ss.SimPars): """ Dictionary with all parameters used within an FPsim. All parameters that don't vary across geographies are defined explicitly here. Keys for all location-specific parameters are also defined here with None values. """ def __init__(self, **kwargs): # Initialize the parent class super().__init__() # Basic parameters self.n_agents = 1_000 # Number of agents self.start = 2000 # Start year of simulation self.stop = 2020 # End year of simulation self.dt = 1/12 # The simulation timestep in 'unit's self.unit = 'year' # The unit of time for the simulation self.rand_seed = 1 # Random seed self.verbose = 1/12 # Verbosity level self.use_aging = True # Whether to age the population self.location = None # Default location self.test = False # Update with any supplied parameter values and generate things that need to be generated self.update(kwargs) return
[docs] def make_sim_pars(**kwargs): """ Shortcut for making a new instance of SimPars """ return SimPars(**kwargs)
[docs] class FPPars(ss.Pars): def __init__(self, location=None, **kwargs): super().__init__() # Settings - what aspects are being modeled - TODO, remove self.use_partnership = 0 # Age limits (in years) self.method_age = 15 self.age_limit_fecundity = 50 self.max_age = 99 # Durations (in months) self.end_first_tri = 3 # Months self.dur_pregnancy = ss.uniform(low=ss.months(9), high=ss.months(9)) self.dur_breastfeeding = ss.normal(loc=ss.months(24), scale=ss.months(6)) self.dur_postpartum = None # Updated by data, do not modify self.max_lam_dur = 5 # Duration of lactational amenorrhea (months) self.short_int = ss.months(24) # Duration of a short birth interval between live births (months) # Parameters related to the likelihood of conception self.LAM_efficacy = 0.98 # From Cochrane review: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6823189/ self.primary_infertility = 0.05 # Parameters typically tuned during calibration self.maternal_mortality_factor = 1 self.fecundity_low = 0.7 self.fecundity_high = 1.1 # Personal fecundity distribution self.exposure_factor = 1 # Overall exposure factor, to be calibrated self.exposure_age = dict(age =[0, 5, 10, 12.5, 15, 18, 20, 25, 30, 35, 40, 45, 50], rel_exp=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) self.exposure_parity = dict(parity =[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 20], rel_exp=[1, 1, 1, 1, 1, 1, 1, 0.8, 0.5, 0.3, 0.15, 0.10, 0.05, 0.01]) ################################### # Context-specific data-derived parameters, all need to be loaded from data ################################### self.abortion_prob = None self.twins_prob = None self.maternal_mortality = None self.infant_mortality = None self.miscarriage_rates = None self.stillbirth_rate = None self.age_fecundity = None self.fecundity_ratio_nullip = None self.lactational_amenorrhea = None self.sexual_activity = None self.sexual_activity_pp = None self.debut_age = None self.spacing_pref = None self.age_partnership = None self.region = None self.regional = None self.update(kwargs) if location is not None: self.update_location(location=location) else: # Process parameters even when no location is specified self.process_parameters() return
[docs] def update_location(self, location=None): """ Update the location-specific FP parameters """ location_module = fp.get_dataloader(location) location_pars = location_module.make_fp_pars() self.update(**location_pars) self.process_parameters() return
[docs] def process_parameters(self): """ Process parameters after all updates are complete. Convert fecundity_low/fecundity_high to ss.uniform distribution. """ # Convert fecundity bounds to distribution if hasattr(self, 'fecundity_low') and hasattr(self, 'fecundity_high'): self.fecundity = ss.uniform(low=self.fecundity_low, high=self.fecundity_high) elif not hasattr(self, 'fecundity'): # Fallback to default if no fecundity parameters are set self.fecundity = ss.uniform(low=0.7, high=1.1) return
[docs] def make_fp_pars(location=None): """ Shortcut for making a new instance of FPPars """ return FPPars(location=location)
[docs] def mergepars(*args, _copy=False, **kwargs): """ Merge all parameter dictionaries into a single dictionary with nested merging. This is used to initialize the SimPars class with all relevant parameters. Unlike sc.mergedicts, this function recursively merges nested dictionaries instead of replacing them entirely. This allows partial dictionary specifications in calibration parameters (e.g., only specifying preference values in spacing_pref while preserving interval, n_bins, months from defaults). Args: _copy (bool): whether to deep copy the input dictionaries (default False, same as sc.mergedicts) *args: dictionaries to merge **kwargs: additional parameters (for compatibility with sc.mergedicts) """ # Convert any Pars objects to dicts while preserving types like sc.objdict if _copy: dicts = [sc.dcp(arg) if arg is not None else {} for arg in args if arg is not None] else: dicts = [dict(arg) if arg is not None else {} for arg in args if arg is not None] if len(dicts) < 2: # Single dict or empty - use standard merging return sc.mergedicts(*dicts, _copy=_copy, **kwargs) if dicts else {} # Start with the first dictionary result = sc.dcp(dicts[0]) if _copy else dicts[0] # Recursively merge each subsequent dictionary for next_dict in dicts[1:]: for key, value in next_dict.items(): if (key in result and isinstance(value, dict) and isinstance(result[key], dict)): # Both are dicts - recursively merge result[key] = mergepars(result[key], value, _copy=_copy, **kwargs) else: # Not both dicts - use the new value result[key] = sc.dcp(value) if _copy else value return result
# Shortcut for accessing default keys par_keys = make_fp_pars().keys() sim_par_keys = make_sim_pars().keys() # ALL PARS
[docs] def all_pars(location=None): """ Return a dictionary with all parameters used within an FPsim. This includes both simulation parameters and family planning parameters. """ sim_pars = make_sim_pars() fp_pars = make_fp_pars() contra_pars = fp.ContraPars() edu_pars = fp.EduPars() death_pars = fp.DeathPars() people_pars = fp.PeoplePars() mergedpars = mergepars(sim_pars, fp_pars, contra_pars, edu_pars, death_pars, people_pars) mergedpars['location'] = location # Load location-specific parameters if a location is provided if location is not None: data_pars = fp.DataLoader(location=location).load(return_data=True) for modpars in data_pars.values(): mergedpars.update(modpars) return mergedpars