'''
Set the defaults across each of the different files.
'''
import numpy as np
import sciris as sc
import pylab as pl
from .settings import options as hpo # To set options
# Specify all externally visible functions this file defines -- other things are available as e.g. hpv.defaults.default_int
__all__ = ['datadir', 'default_float', 'default_int', 'get_default_plots']
# Define paths
datadir = sc.path(sc.thisdir(__file__)) / 'data'
#%% Specify what data types to use
result_float = np.float64 # Always use float64 for results, for simplicity
if hpo.precision == 32:
default_float = np.float32
default_int = np.int32
elif hpo.precision == 64: # pragma: no cover
default_float = np.float64
default_int = np.int64
else:
raise NotImplementedError(f'Precision must be either 32 bit or 64 bit, not {hpo.precision}')
#%% Define all properties of people
class State(sc.prettyobj):
def __init__(self, name, dtype, fill_value=0, shape=None, label=None, color=None,
totalprefix=None):
'''
Args:
name: name of the result as used in the model (e.g. cin1)
dtype: datatype
fill_value: default value for this state upon model initialization
shape: If not none, set to match a string in `pars` containing the dimensionality e.g., `n_genotypes`)
label: text used to construct labels for the result for displaying on plots and other outputs
color: color (used for plotting stocks)
totalprefix: the prefix used for differentiating by-genotype results from total results. Set to None if the result only appears as a total
'''
self.name = name
self.dtype = dtype
self.fill_value = fill_value
self.shape = shape
self.label = label or name
self.color = color
self.totalprefix = totalprefix or ('total_' if shape else '')
return
@property
def ndim(self):
return len(sc.tolist(self.shape))+1 # None -> 1, 'n_genotypes' -> 2, etc.
def new(self, pars, n):
shape = sc.tolist(self.shape) # e.g. convert 'n_genotypes' to ['n_genotypes']
shape = [pars[s] for s in shape] # e.g. convert ['n_genotypes'] to [2]
shape.append(n) # We always want to have shape n
return np.full(shape, dtype=self.dtype, fill_value=self.fill_value)
class PeopleMeta(sc.prettyobj):
''' For storing all the keys relating to a person and people '''
# (attribute, nrows, dtype, default value)
# If the default value is None, then the array will not be initialized - this is faster and can
# be used for variables where the People object explicitly supplies the values e.g. age
def __init__(self):
# Set the properties of a person
self.person = [
State('uid', default_int), # Int
State('cluster', default_int, 0), # Int
State('scale', default_float, 1.0), # Float
State('level0', bool, True), # "Normal" people
State('level1', bool, False), # "High-resolution" people: e.g. cancer agents
State('age', default_float, np.nan), # Float
State('sex', default_float, np.nan), # Float
State('debut', default_float, np.nan), # Float
State('ever_partnered', bool, False), # Whether this person has ever been partnered
State('rel_sev', default_float, 1.0), # Individual relative risk for rate severe disease growth
State('rel_sus', default_float, 1.0), # Individual relative risk for acquiring infection (does not vary by genotype)
State('rel_imm', default_float, 1.0), # Individual relative level of immunity acquired from infection clearance/vaccination
State('doses', default_int, 0), # Number of doses of the prophylactic vaccine given per person
State('txvx_doses', default_int, 0), # Number of doses of the therapeutic vaccine given per person
State('vaccine_source', default_int, -1), # Index of the prophylactic vaccine that individual received
State('screens', default_int, 0), # Number of screens given per person
State('cin_treatments', default_int, 0), # Number of CIN treatments given per person
State('cancer_treatments', default_int, 0), # Number of cancer treatments given per person
State('art_adherence', default_float, 0, label='adherence on ART', color='#aaa8ff'),
State('n_infections', default_int, 0, 'n_genotypes')
]
###### The following section consists of all the boolean states
# The following three groupings are all mutually exclusive and collectively exhaustive.
self.alive_states = [
# States related to whether or not the person is alive or dead
State('alive', bool, True, label='Population'), # Save this as a state so we can record population sizes
State('dead_cancer', bool, False, label='Cumulative cancer deaths'), # Dead from cancer
State('dead_other', bool, False, label='Cumulative deaths from other causes'), # Dead from all other causes
State('emigrated', bool, False, label='Emigrated'), # Emigrated
]
self.viral_states = [
# States related to whether virus is present
State('susceptible', bool, True, 'n_genotypes', label='Number susceptible', color='#4d771e'), # Allowable cell states: normal
State('infectious', bool, False, 'n_genotypes', label='Number infectious', color='#c78f65'), # Allowable cell states: normal, cin
State('inactive', bool, False, 'n_genotypes', label='Number with inactive infection', color='#9e1149'), # Allowable cell states: normal, cancer in at least one genotype
]
self.cell_states = [
# States related to the cellular changes present in the cervix.
State('normal', bool, True, 'n_genotypes', label='Number with no cellular changes', color='#9e1149'), # Allowable viral states: susceptible, infectious, and inactive
State('cin', bool, False, 'n_genotypes', label='Number with detectable dysplasia', color='#9e1149'), # Allowable viral states: infectious
State('cancerous', bool, False, 'n_genotypes', label='Number with cancer', color='#5f5cd2'), # Allowable viral states: inactive
]
self.derived_states = [
# From the viral states, cell states, and severity markers, we derive the following additional states:
State('infected', bool, False, 'n_genotypes', label='Number infected', color='#c78f65'), # Union of infectious and inactive. Includes people with cancer, people with latent infections, and people with active infections
State('abnormal', bool, False, 'n_genotypes', label='Number with abnormal cells', color='#9e1149'), # Union of CIN and cancerous. Allowable viral states: infectious, inactive
State('latent', bool, False, 'n_genotypes', label='Number with latent infection', color='#5f5cd2'), # Intersection of normal and inactive.
State('precin', bool, False, 'n_genotypes', label='Number with precin', color='#9e1149'), # Intersection of normal and infectious
]
# Additional intervention states
self.intv_states = [
State('detected_cancer', bool, False, label='Number with detected cancer'), # Whether the person's cancer has been detected
State('screened', bool, False, label='Number screened'), # Whether the person has been screened (how does this change over time?)
State('cin_treated', bool, False, label='Number treated for precancerous lesions'), # Whether the person has been treated for CINs
State('cancer_treated', bool, False, label='Number treated for cancer'), # Whether the person has been treated for cancer
State('vaccinated', bool, False, label='Number vaccinated'), # Whether the person has received the prophylactic vaccine
State('tx_vaccinated', bool, False, label='Number given therapeutic vaccine'), # Whether the person has received the therapeutic vaccine
]
# Any other stock states - add a placeholder here to be populated later
self.other_stock_states = []
# Immune states, by genotype/vaccine
self.imm_states = [
State('sus_imm', default_float, 0,'n_imm_sources'), # Float, by genotype
State('sev_imm', default_float, 0, 'n_imm_sources'), # Float, by genotype
State('peak_imm', default_float, 0,'n_imm_sources'), # Float, peak level of immunity
State('nab_imm', default_float, 0,'n_imm_sources'), # Float, current immunity level
State('t_imm_event', default_int, 0,'n_imm_sources'), # Int, time since immunity event
State('cell_imm', default_float, 0,'n_imm_sources'),
]
# Relationship states
self.rship_states = [
State('rship_start_dates', default_float, np.nan, shape='n_partner_types'),
State('rship_end_dates', default_float, np.nan, shape='n_partner_types'),
State('n_rships', default_int, 0, shape='n_partner_types'),
State('partners', default_float, np.nan, shape='n_partner_types'), # Int by relationship type
State('current_partners', default_float, 0, 'n_partner_types'), # Int by relationship type
]
# Duration of different states: these are floats per person -- used in people.py
self.durs = [
State('dur_infection', default_float, np.nan, shape='n_genotypes'), # Length of time that a person has any HPV present. Defined for males and females. For females, dur_infection = dur_episomal + dur_transformed. For males, it's taken from a separate distribution
State('dur_precin', default_float, np.nan, shape='n_genotypes'), # Length of time that a person has HPV prior to precancerous changes
State('dur_cin', default_float, np.nan, shape='n_genotypes'), # Length of time that a person has precancerous changes
State('dur_cancer', default_float, np.nan, shape='n_genotypes'), # Duration of cancer
]
# Collection of mutually exclusive + collectively exhaustive states
@property
def mece_states(self):
return self.alive_states + self.viral_states + self.cell_states
# Collection of states that we store as stock results
@property
def stock_states(self):
return self.viral_states + self.cell_states + self.derived_states + self.intv_states + self.other_stock_states
# Collection of states for which we store associated dates
@property
def date_states(self):
return [state for state in self.alive_states + self.stock_states if not state.fill_value]
# Set dates
@property
def dates(self):
''' Dates are stored for all states except susceptible, and alive, and normal (which are True by default) '''
dates = [State(f'date_{state.name}', default_float, np.nan, shape=state.shape) for state in self.date_states]
dates += [
State('date_clearance', default_float, np.nan, shape='n_genotypes'),
State('date_exposed', default_float, np.nan, shape='n_genotypes'),
State('date_reactivated', default_float, np.nan, shape='n_genotypes'),
State('date_latent', default_float, np.nan, shape='n_genotypes'),
]
return dates
# All states
@property
def all_states(self):
return self.person + self.alive_states + self.viral_states + self.cell_states + self.derived_states + self.intv_states + self.other_stock_states + self.imm_states + self.rship_states + self.durs
# States to set - same as above but does not include derived states
@property
def states_to_set(self):
return self.person + self.alive_states + self.viral_states + self.cell_states + self.intv_states + self.other_stock_states + self.imm_states + self.rship_states + self.durs + self.dates
@property
def stock_keys(self):
return [state.name for state in self.stock_states]
@property
def stock_names(self):
return [state.label for state in self.stock_states]
@property
def stock_colors(self):
return [state.color for state in self.stock_states]
@property
def genotype_stock_keys(self):
return [state.name for state in self.stock_states if state.shape=='n_genotypes']
@property
def other_stock_keys(self):
return [state.name for state in self.other_stock_states]
@property
def intv_stock_keys(self):
return [state.name for state in self.intv_states]
def validate(self):
"""
Check that states are valid
This check should be performed when PeopleMeta is consumed (i.e., typically in the People() constructor)
so that any run-time modifications to the states by the end user get accounted for in validation
Presently, the only validation check is that the state names are unique, but in principle other
aspects of the states could be checked too
:return: None if states are valid
:raises: ValueError if states are not valid
"""
# Validate
state_types = ['person', 'mece_states', 'imm_states', 'intv_states', 'dates', 'durs', 'all_states']
for state_type in state_types:
states = getattr(self, state_type)
n_states = len(states)
n_unique_states = len(set(states))
if n_states != n_unique_states: # pragma: no cover
errormsg = f'In {state_type}, only {n_unique_states} of {n_states} state names are unique'
raise ValueError(errormsg)
return
#%% Default result settings
# Flows
# All are stored (1) by genotype and (2) as the total across genotypes
class Flow():
def __init__(self, name, label=None, color=None, by_genotype=True):
self.name = name
self.label = label or name
self.color = color
self.by_genotype = by_genotype
flows = [
Flow('infections', color='#c78f65', label='Infections'),
Flow('dysplasias', color='#c1ad71', label='Dysplasias'),
Flow('precins', color='#c1ad71', label='Pre-CINs'),
Flow('cins', color='#b86113', label='CINs'),
Flow('cancers', color='#5f5cd2', label='Cancers'),
Flow('detected_cancers', color='#5f5cd2', label='Cancer detections', by_genotype=False),
Flow('cancer_deaths', color='#000000', label='Cancer deaths', by_genotype=False),
Flow('detected_cancer_deaths', color='#000000', label='Detected cancer deaths', by_genotype=False),
Flow('reinfections', color='#732e26', label='Reinfections'),
Flow('reactivations', color='#732e26', label='Reactivations'),
]
flow_keys = [flow.name for flow in flows]
genotype_flow_keys = [flow.name for flow in flows if flow.by_genotype]
# Incidence. Strong overlap with stocks, but with slightly different naming conventions
# All are stored (1) by genotype and (2) as the total across genotypes
inci_keys = ['hpv', 'dysplasia', 'cancer']
inci_names = ['HPV', 'Dysplasia', 'Cancer']
inci_colors = ['#c78f65', '#c1ad71', '#5f5cd2']
# Demographics
dem_keys = ['births', 'other_deaths', 'migration']
dem_names = ['births', 'other deaths', 'migration']
dem_colors = ['#fcba03', '#000000', '#000000']
# Results by sex
by_sex_keys = ['infections_by_sex', 'other_deaths_by_sex']
by_sex_names = ['infections by sex', 'deaths from other causes by sex']
by_sex_colors = ['#000000', '#000000']
# Results for storing type distribution by dysplasia
type_dist_keys = ['precin', 'cin', 'cancerous']
type_dist_names = ['Pre-CIN', 'CIN2+', 'Cancer']
#%% Default initial prevalence
default_init_prev = {
'age_brackets' : np.array([ 12, 17, 24, 34, 44, 64, 80, 150]),
'm' : np.array([ 0.0, 0.25, 0.6, 0.25, 0.05, 0.01, 0.0005, 0]),
'f' : np.array([ 0.0, 0.35, 0.7, 0.25, 0.05, 0.01, 0.0005, 0]),
}
#%% Default plotting settings
# Define the 'overview plots', i.e. the most useful set of plots to explore different aspects of a simulation
overview_plots = [
'infections',
'cins',
'cancers',
]
class plot_args():
''' Mini class for defining default plot specifications '''
def __init__(self, keys, name=None, plot_type=None, year=None):
self.keys = sc.tolist(keys)
self.name = name
self.plot_type = plot_type
self.year = year
return
[docs]
def get_default_plots(which='default', kind='sim', sim=None):
'''
Specify which quantities to plot; used in sim.py.
Args:
which (str): either 'default' or 'overview'
'''
which = str(which).lower() # To make comparisons easier
# Check that kind makes sense
sim_kind = 'sim'
scens_kind = 'scens'
kindmap = {
None: sim_kind,
'sim': sim_kind,
'default': sim_kind,
'msim': scens_kind,
'scen': scens_kind,
'scens': scens_kind,
}
if kind not in kindmap.keys():
errormsg = f'Expecting "sim" or "scens", not "{kind}"'
raise ValueError(errormsg)
else:
is_sim = kindmap[kind] == sim_kind
# Default plots -- different for sims and scenarios
if which in ['none', 'default', 'epi']:
if is_sim:
plots = sc.objdict({
'HPV prevalence by age': ['precin_prevalence_by_age'],
# 'HPV prevalence': ['hpv_prevalence_by_genotype'],
'Cancers by age': 'cancers_by_age',
'Cancer incidence (per 100,000 women)': ['cancer_incidence', 'asr_cancer_incidence'],
'HPV type distribution': 'type_dist',
})
else: # pragma: no cover
plots = sc.odict({
'HPV incidence': [
'hpv_prevalence',
],
'Cancers per 100,000 women': [
'cancer_incidence',
],
})
# Demographic plots
elif which in ['demographic', 'demographics', 'dem', 'demography']:
if is_sim:
plots = sc.odict({
'Birth and death rates': [
'cdr',
'cbr',
],
'Population size': [
'n_alive',
'n_alive_by_sex',
],
})
# Show an overview
elif which == 'overview': # pragma: no cover
plots = sc.dcp(overview_plots)
# Plot absolutely everything
elif which == 'all': # pragma: no cover
plots = sim.result_keys('all')
# Show an overview
elif 'overview' in which: # pragma: no cover
plots = sc.dcp(overview_plots)
else: # pragma: no cover
errormsg = f'The choice which="{which}" is not supported'
raise ValueError(errormsg)
return plots