Repository: starsimhub/starsim (language: python)

Issue:
States states merge merge



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index fc00a329..45ef95cb 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,19 @@ All notable changes to the codebase are documented in this file. Changes that ma
    :depth: 1
 
 
+Version 0.0.6 (2023-08-30)
+--------------------------
+- Changes agent IDs from index-based to UID-based
+- Allows states to store their own data and live within modules
+- *GitHub info*: PR `88 <https://github.com/amath-idm/stisim/pull/88>`_
+
+
+Version 0.0.5 (2023-08-29)
+--------------------------
+- Refactor file structure 
+- *GitHub info*: PRs `77 <https://github.com/amath-idm/stisim/pull/77>`_ and `86 <https://github.com/amath-idm/stisim/pull/86>`_
+
+
 Version 0.0.2 (2023-06-29)
 --------------------------
 - Adds in basic *sim functionality
diff --git a/stisim/__init__.py b/stisim/__init__.py
index 45187cd8..82e24983 100644
--- a/stisim/__init__.py
+++ b/stisim/__init__.py
@@ -6,13 +6,14 @@
 from .people        import *
 from .networks      import *
 from .modules       import *
+from .results       import *
+from .interventions import *
+from .analyzers     import *
 from .demographics  import *
 from .hiv           import *
 from .gonorrhea     import *
-from .connectors    import *
 from .sim           import *
 
-
 # Import the version and print the license
 if options.verbose:
     print(__license__)
\ No newline at end of file
diff --git a/stisim/analyzers.py b/stisim/analyzers.py
index 81667b0e..bbbd5148 100644
--- a/stisim/analyzers.py
+++ b/stisim/analyzers.py
@@ -2,14 +2,16 @@
 Define analyzers
 """
 
-from . import utils as ssu
-from . import modules as ssm
+import stisim as ss
 
 
-class Analyzer(ssm.Module):
+__all__ = ['Analyzer', 'Analyzers']
+
+
+class Analyzer(ss.Module):
     pass
 
 
-class Analyzers(ssu.ndict):
+class Analyzers(ss.ndict):
     def __init__(self, *args, type=Analyzer, **kwargs):
         return super().__init__(self, *args, type=type, **kwargs)
\ No newline at end of file
diff --git a/stisim/connectors.py b/stisim/connectors.py
deleted file mode 100644
index 2356825b..00000000
--- a/stisim/connectors.py
+++ /dev/null
@@ -1,19 +0,0 @@
-'''
-Define connections between disease modules
-'''
-
-from . import utils as ssu
-from . import modules as ssm
-
-
-class Connector(ssm.Module):
-    def __init__(self, pars=None, *args, **kwargs):
-        self.pars = ssu.omerge(pars)
-        self.states = ssu.ndict()
-        self.results = ssu.ndict()
-        return
-
-
-class Connectors(ssu.ndict):
-    def __init__(self, *args, type=Connector, **kwargs):
-        return super().__init__(self, *args, type=type, **kwargs)
\ No newline at end of file
diff --git a/stisim/demographics.py b/stisim/demographics.py
index e45ea65b..327fe224 100644
--- a/stisim/demographics.py
+++ b/stisim/demographics.py
@@ -3,31 +3,31 @@
 """
 
 import numpy as np
-from . import utils as ssu
-from . import people as ssp
-from . import modules as ssm
-from . import results as ssr
+import stisim as ss
 
 
-class Pregnancy(ssm.Module):
+__all__ = ['Pregnancy']
+
+
+class Pregnancy(ss.Module):
 
     def __init__(self, pars=None):
         super().__init__(pars)
 
         # Other, e.g. postpartum, on contraception...
-        self.states = ssu.ndict(
-            ssp.State('infertile', bool, False),  # Applies to girls and women outside the fertility window
-            ssp.State('susceptible', bool, True),  # Applies to girls and women inside the fertility window - needs renaming
-            ssp.State('pregnant', bool, False),  # Currently pregnant
-            ssp.State('postpartum', bool, False),  # Currently post-partum
-            ssp.State('ti_pregnant', float, np.nan),  # Time pregnancy begins
-            ssp.State('ti_delivery', float, np.nan),  # Time of delivery
-            ssp.State('ti_postpartum', float, np.nan),  # Time postpartum ends
-            ssp.State('ti_dead', float, np.nan),  # Maternal mortality
+        self.states = ss.ndict(
+            ss.State('infertile', bool, False),  # Applies to girls and women outside the fertility window
+            ss.State('susceptible', bool, True),  # Applies to girls and women inside the fertility window - needs renaming
+            ss.State('pregnant', bool, False),  # Currently pregnant
+            ss.State('postpartum', bool, False),  # Currently post-partum
+            ss.State('ti_pregnant', float, np.nan),  # Time pregnancy begins
+            ss.State('ti_delivery', float, np.nan),  # Time of delivery
+            ss.State('ti_postpartum', float, np.nan),  # Time postpartum ends
+            ss.State('ti_dead', float, np.nan),  # Maternal mortality
             self.states,
         )
 
-        self.pars = ssu.omerge({
+        self.pars = ss.omerge({
             'dur_pregnancy': 0.75,  # Make this a distribution?
             'dur_postpartum': 0.5,  # Make this a distribution?
             'inci': 0.03,  # Replace this with age-specific rates
@@ -37,13 +37,11 @@ def __init__(self, pars=None):
 
         return
 
-
     def initialize(self, sim):
         super().initialize(sim)
         self.init_results(sim)
         sim.pars['birth_rates'] = None  # This turns off birth rate pars so births only come from this module
         return
-    
 
     def init_results(self, sim):
         """
@@ -51,11 +49,10 @@ def init_results(self, sim):
         Still unclear whether this logic should live in the pregnancy module, the
         individual disease modules, the connectors, or the sim.
         """
-        self.results['pregnancies'] = ssr.Result('pregnancies', self.name, sim.npts, dtype=int)
-        self.results['births']      = ssr.Result('births', self.name, sim.npts, dtype=int)
+        self.results += ss.Result(self.name, 'pregnancies', sim.npts, dtype=int)
+        self.results += ss.Result(self.name, 'births', sim.npts, dtype=int)
         return
-    
-    
+
     def update(self, sim):
         """
         Perform all updates
@@ -65,34 +62,32 @@ def update(self, sim):
         self.update_results(sim)
         return
 
-
     def update_states(self, sim):
         """
         Update states
         """
 
         # Check for new deliveries
-        deliveries = sim.people[self.name].pregnant & (sim.people[self.name].ti_delivery <= sim.ti)
-        sim.people[self.name].pregnant[deliveries] = False
-        sim.people[self.name].postpartum[deliveries] = True
-        sim.people[self.name].susceptible[deliveries] = False
-        sim.people[self.name].ti_delivery[deliveries] = sim.ti
+        deliveries = self.pregnant & (self.ti_delivery <= sim.ti)
+        self.pregnant[deliveries] = False
+        self.postpartum[deliveries] = True
+        self.susceptible[deliveries] = False
+        self.ti_delivery[deliveries] = sim.ti
 
         # Check for new women emerging from post-partum
-        postpartum = ~sim.people[self.name].pregnant & (sim.people[self.name].ti_postpartum <= sim.ti)
-        sim.people[self.name].postpartum[postpartum] = False
-        sim.people[self.name].susceptible[postpartum] = True
-        sim.people[self.name].ti_postpartum[postpartum] = sim.ti
+        postpartum = ~self.pregnant & (self.ti_postpartum <= sim.ti)
+        self.postpartum[postpartum] = False
+        self.susceptible[postpartum] = True
+        self.ti_postpartum[postpartum] = sim.ti
 
         # Maternal deaths
-        maternal_deaths = ssu.true(sim.people[self.name].ti_dead <= sim.ti)
+        maternal_deaths = ss.true(self.ti_dead <= sim.ti)
         if len(maternal_deaths):
             sim.people.alive[maternal_deaths] = False
             sim.people.ti_dead[maternal_deaths] = sim.ti
 
         return
 
-
     def make_pregnancies(self, sim):
         """
         Select people to make pregnancy using incidence data
@@ -104,9 +99,9 @@ def make_pregnancies(self, sim):
         # If incidence of pregnancy is non-zero, make some cases
         # Think about how to deal with age/time-varying fertility
         if self.pars.inci > 0:
-            demon_conds = ppl.female & ppl.active & ppl[self.name].susceptible
-            inds_to_choose_from = ssu.true(demon_conds)
-            uids = ssu.binomial_filter(self.pars.inci, inds_to_choose_from)
+            denom_conds = ppl.female & ppl.active & self.susceptible
+            inds_to_choose_from = ss.true(denom_conds)
+            uids = ss.binomial_filter(self.pars.inci, inds_to_choose_from)
 
             # Add UIDs for the as-yet-unborn agents so that we can track prognoses and transmission patterns
             n_unborn_agents = len(uids)
@@ -120,7 +115,7 @@ def make_pregnancies(self, sim):
                 # Placeholder code to be moved / refactored. The maternal network may need to be
                 # handled separately to the sexual networks, TBC how to handle this most elegantly
                 for lkey, layer in sim.people.networks.items():
-                    if layer.transmission == 'vertical':  # What happens if there's more than one vertical layer?
+                    if layer.vertical:  # What happens if there's more than one vertical layer?
                         durs = np.full(n_unborn_agents, fill_value=self.pars.dur_pregnancy+self.pars.dur_postpartum)
                         layer.add_pairs(uids, new_uids, dur=durs)
 
@@ -129,7 +124,6 @@ def make_pregnancies(self, sim):
 
         return
 
-
     def set_prognoses(self, sim, uids):
         """
         Make pregnancies
@@ -137,27 +131,24 @@ def set_prognoses(self, sim, uids):
         Also reconciliation with birth rates
         Q, is this also a good place to check for other conditions and set prognoses for the fetus?
         """
-        pars = sim.pars[self.name]
 
         # Change states for the newly pregnant woman
-        sim.people[self.name].susceptible[uids] = False
-        sim.people[self.name].pregnant[uids] = True
-        sim.people[self.name].ti_pregnant[uids] = sim.ti
+        self.susceptible[uids] = False
+        self.pregnant[uids] = True
+        self.ti_pregnant[uids] = sim.ti
 
         # Outcomes for pregnancies
-        dur = np.full(len(uids), sim.ti + pars['dur_pregnancy'] / sim.dt)
-        dead = np.random.random(len(uids)) < sim.pars[self.name].p_death
-        sim.people[self.name].ti_delivery[uids] = dur  # Currently assumes maternal deaths still result in a live baby
-        dur_post_partum = np.full(len(uids), dur + pars['dur_postpartum'] / sim.dt)
-        sim.people[self.name].ti_postpartum[uids] = dur_post_partum
+        dur = np.full(len(uids), sim.ti + self.pars.dur_pregnancy / sim.dt)
+        dead = np.random.random(len(uids)) < self.pars.p_death
+        self.ti_delivery[uids] = dur  # Currently assumes maternal deaths still result in a live baby
+        dur_post_partum = np.full(len(uids), dur + self.pars.dur_postpartum / sim.dt)
+        self.ti_postpartum[uids] = dur_post_partum
 
         if np.count_nonzero(dead):
-            sim.people[self.name].ti_dead[uids[dead]] = dur[dead]
+            self.ti_dead[uids[dead]] = dur[dead]
         return
 
-
     def update_results(self, sim):
-        mppl = sim.people[self.name] # TODO: refactor with states owning their own data
-        self.results['pregnancies'][sim.ti] = np.count_nonzero(mppl.ti_pregnant == sim.ti)
-        self.results['births'][sim.ti] = np.count_nonzero(mppl.ti_delivery == sim.ti)
-        return
\ No newline at end of file
+        self.results['pregnancies'][sim.ti] = np.count_nonzero(self.ti_pregnant == sim.ti)
+        self.results['births'][sim.ti] = np.count_nonzero(self.ti_delivery == sim.ti)
+        return
diff --git a/stisim/gonorrhea.py b/stisim/gonorrhea.py
index cc83ce06..de83b286 100644
--- a/stisim/gonorrhea.py
+++ b/stisim/gonorrhea.py
@@ -3,26 +3,28 @@
 """
 
 import numpy as np
-from . import utils as ssu
-from . import people as ssp
-from . import modules as ssm
+import stisim as ss
 
 
-class Gonorrhea(ssm.Disease):
+__all__ = ['Gonorrhea']
+
+
+class Gonorrhea(ss.Disease):
 
     def __init__(self, pars=None):
         super().__init__(pars)
-        self.states = ssu.ndict(
-            ssp.State('susceptible', bool, True),
-            ssp.State('infected', bool, False),
-            ssp.State('ti_infected', float, 0),
-            ssp.State('ti_recovered', float, 0),
-            ssp.State('ti_dead', float, np.nan), # Death due to gonorrhea
+
+        self.states = ss.ndict(
+            ss.State('susceptible', bool, True),
+            ss.State('infected', bool, False),
+            ss.State('ti_infected', float, 0),
+            ss.State('ti_recovered', float, 0),
+            ss.State('ti_dead', float, np.nan),  # Death due to gonorrhea
             self.states,
         )
 
-        self.pars = ssu.omerge({
-            'dur_inf': 3, # not modelling diagnosis or treatment explicitly here
+        self.pars = ss.omerge({
+            'dur_inf': 3,  # not modelling diagnosis or treatment explicitly here
             'p_death': 0.2,
             'initial': 3,
             'eff_condoms': 0.7,
@@ -33,29 +35,26 @@ def update_states(self, sim):
         # What if something in here should depend on another module?
         # I guess we could just check for it e.g., 'if HIV in sim.modules' or
         # 'if 'hiv' in sim.people' or something
-        gonorrhea_deaths = sim.people.gonorrhea.ti_dead <= sim.ti
+        gonorrhea_deaths = self.ti_dead <= sim.ti
         sim.people.alive[gonorrhea_deaths] = False
         sim.people.ti_dead[gonorrhea_deaths] = sim.ti
         return
     
-
     def update_results(self, sim):
         super(Gonorrhea, self).update_results(sim)
         return
     
-    
     def make_new_cases(self, sim):
         super(Gonorrhea, self).make_new_cases(sim)
         return
 
-
     def set_prognoses(self, sim, uids):
-        sim.people[self.name].susceptible[uids] = False
-        sim.people[self.name].infected[uids] = True
-        sim.people[self.name].ti_infected[uids] = sim.ti
-
-        dur = sim.ti + np.random.poisson(sim.pars[self.name]['dur_inf']/sim.pars.dt, len(uids))
-        dead = np.random.random(len(uids)) < sim.pars[self.name].p_death
-        sim.people[self.name].ti_recovered[uids[~dead]] = dur[~dead]
-        sim.people[self.name].ti_dead[uids[dead]] = dur[dead]
+        self.susceptible[uids] = False
+        self.infected[uids] = True
+        self.ti_infected[uids] = sim.ti
+
+        dur = sim.ti + np.random.poisson(self.pars['dur_inf']/sim.pars.dt, len(uids))
+        dead = np.random.random(len(uids)) < self.pars.p_death
+        self.ti_recovered[uids[~dead]] = dur[~dead]
+        self.ti_dead[uids[dead]] = dur[dead]
         return
diff --git a/stisim/hiv.py b/stisim/hiv.py
index 361c5bdd..87fad133 100644
--- a/stisim/hiv.py
+++ b/stisim/hiv.py
@@ -4,80 +4,72 @@
 
 import numpy as np
 import sciris as sc
-from . import utils as ssu
-from . import people as ssp
-from . import modules as ssm
-from . import results as ssr
-from . import interventions as ssi
-from . import analyzers as ssa
+import stisim as ss
 
+__all__ = ['HIV', 'ART', 'CD4_analyzer']
+
+
+class HIV(ss.Disease):
 
-class HIV(ssm.Disease):
-    
     def __init__(self, pars=None):
         super().__init__(pars)
 
-        self.susceptible = ssp.State('susceptible', bool, True)
-        self.infected = ssp.State('infected', bool, False)
-        self.ti_infected = ssp.State('ti_infected', float, 0)
-        self.on_art = ssp.State('on_art', bool, False)
-        self.cd4 = ssp.State('cd4', float, 500)
+        self.states = ss.ndict(
+            ss.State('susceptible', bool, True),
+            ss.State('infected', bool, False),
+            ss.State('ti_infected', float, 0),
+            ss.State('on_art', bool, False),
+            ss.State('cd4', float, 500),
+            self.states
+        )
 
-        self.states.append(self.susceptible)
-        self.states.append(self.infected)
-        self.states.append(self.ti_infected)
-        self.states.append(self.on_art)
-        self.states.append(self.cd4)
-
-        self.pars = ssu.omerge({
+        self.pars = ss.omerge({
             'cd4_min': 100,
             'cd4_max': 500,
             'cd4_rate': 5,
             'initial': 30,
             'eff_condoms': 0.7,
         }, self.pars)
-        return
 
+        return
 
     def update_states(self, sim):
         """ Update CD4 """
         self.cd4[sim.people.alive & self.infected & self.on_art] += (self.pars.cd4_max - self.cd4[sim.people.alive & self.infected & self.on_art])/self.pars.cd4_rate
         self.cd4[sim.people.alive & self.infected & ~self.on_art] += (self.pars.cd4_min - self.cd4[sim.people.alive & self.infected & ~self.on_art])/self.pars.cd4_rate
         return
-    
 
     def init_results(self, sim):
         super().init_results(sim)
-        self.results['n_art'] = ssr.Result('n_art', self.name, sim.npts, dtype=int)
         return
-    
 
     def update_results(self, sim):
         super(HIV, self).update_results(sim)
-        sim.results[self.name]['n_art'] = np.count_nonzero(sim.people.alive & sim.people[self.name].on_art)
         return
-    
 
     def make_new_cases(self, sim):
         # eff_condoms = sim.pars[self.name]['eff_condoms'] # TODO figure out how to add this
         super().make_new_cases(sim)
         return
-    
+
     def set_prognoses(self, sim, uids):
         self.susceptible[uids] = False
         self.infected[uids] = True
         self.ti_infected[uids] = sim.ti
 
 
-#%% Interventions
+# %% Interventions
 
-class ART(ssi.Intervention):
+class ART(ss.Intervention):
 
-    def __init__(self,t:np.array,capacity: np.array):
+    def __init__(self, t: np.array, capacity: np.array):
         self.requires = HIV
         self.t = sc.promotetoarray(t)
         self.capacity = sc.promotetoarray(capacity)
 
+    def initialize(self, sim):
+        sim.hiv.results += ss.Result(self.name, 'n_art', sim.npts, dtype=int)
+
     def apply(self, sim):
         if sim.t < self.t[0]:
             return
@@ -91,20 +83,23 @@ def apply(self, sim):
             eligible = sim.people.alive & sim.people.hiv.infected & ~sim.people.hiv.on_art
             n_eligible = np.count_nonzero(eligible)
             if n_eligible:
-                inds = np.random.choice(ssu.true(eligible), min(n_eligible, n_change), replace=False)
+                inds = np.random.choice(ss.true(eligible), min(n_eligible, n_change), replace=False)
                 sim.people.hiv.on_art[inds] = True
         elif n_change < 0:
             # Take some people off ART
             eligible = sim.people.alive & sim.people.hiv.infected & sim.people.hiv.on_art
-            inds = np.random.choice(ssu.true(eligible), min(n_change), replace=False)
+            inds = np.random.choice(ss.true(eligible), min(n_change), replace=False)
             sim.people.hiv.on_art[inds] = False
-        
+
+        # Add result
+        sim.results.hiv.n_art = np.count_nonzero(sim.people.alive & sim.people.hiv.on_art)
+
         return
-    
-    
+
+
 #%% Analyzers
-    
-class CD4_analyzer(ssa.Analyzer):
+
+class CD4_analyzer(ss.Analyzer):
 
     def __init__(self):
         self.requires = HIV
diff --git a/stisim/interventions.py b/stisim/interventions.py
index 07046fba..a71ea94f 100644
--- a/stisim/interventions.py
+++ b/stisim/interventions.py
@@ -1,11 +1,17 @@
-from . import utils as ssu
-from . import modules as ssm
+"""
+Define interventions
+"""
 
+import stisim as ss
 
-class Intervention(ssm.Module):
+
+__all__ = ['Intervention', 'Interventions']
+
+
+class Intervention(ss.Module):
     pass
 
 
-class Interventions(ssu.ndict):
+class Interventions(ss.ndict):
     def __init__(self, *args, type=Intervention, **kwargs):
         return super().__init__(self, *args, type=type, **kwargs)
diff --git a/stisim/modules.py b/stisim/modules.py
index 3cb9c175..40de87a4 100644
--- a/stisim/modules.py
+++ b/stisim/modules.py
@@ -4,24 +4,24 @@
 
 import numpy as np
 import sciris as sc
-from . import people as ssp
-from . import results as ssr
-from . import utils as ssu
+import stisim as ss
+
+
+__all__ = ['Module', 'Modules', 'Disease']
 
 
 class Module(sc.prettyobj):
     
     def __init__(self, pars=None, label=None, requires=None, *args, **kwargs):
-        self.pars = ssu.omerge(pars)
+        self.pars = ss.omerge(pars)
         self.label = label if label else ''
         self.requires = sc.mergelists(requires)
-        self.states = ssu.ndict()
-        self.results = ssr.Results()
+        self.states = ss.ndict()
+        self.results = ss.Results()
         self.initialized = False
         self.finalized = False
         return
-    
-    
+
     def __call__(self, *args, **kwargs):
         """ Makes module(sim) equivalent to module.apply(sim) """
         if not self.initialized:  # pragma: no cover
@@ -29,14 +29,12 @@ def __call__(self, *args, **kwargs):
             raise RuntimeError(errormsg)
         return self.apply(*args, **kwargs)
     
-    
     def check_requires(self, sim):
         for req in self.requires:
             if req not in sim.modules:
                 raise Exception(f'{self.__name__} requires module {req} but the Sim did not contain this module')
         return
     
-    
     def initialize(self, sim):
         self.check_requires(sim)
         
@@ -44,6 +42,10 @@ def initialize(self, sim):
         sim.pars[self.name] = self.pars
         sim.results[self.name] = self.results
 
+        # Add the module states to the module
+        for state in self.states.values():
+            self.__setattr__(state.name, state)
+
         # Add this module to a People instance. This would always involve calling People.add_module
         # but subsequently modules could have their own logic for initializing the default values
         # and initializing any outputs that are required
@@ -51,23 +53,20 @@ def initialize(self, sim):
         
         self.initialized = True
         return
-    
-    
+
     def apply(self, sim):
         pass
-    
-    
+
     def finalize(self, sim):
         self.finalized = True
-    
-    
+
     @property
     def name(self):
         """ The module name is a lower-case version of its class name """
         return self.__class__.__name__.lower()
 
 
-class Modules(ssu.ndict):
+class Modules(ss.ndict):
     def __init__(self, *args, type=Module, **kwargs):
         return super().__init__(self, *args, type=type, **kwargs)
 
@@ -77,13 +76,12 @@ class Disease(Module):
     
     def __init__(self, pars=None, *args, **kwargs):
         super().__init__(pars, *args, **kwargs)
-        self.states = ssu.ndict(
-            ssp.State('rel_sus', float, 1),
-            ssp.State('rel_sev', float, 1),
-            ssp.State('rel_trans', float, 1),
+        self.states = ss.ndict(
+            ss.State('rel_sus', float, 1),
+            ss.State('rel_sev', float, 1),
+            ss.State('rel_trans', float, 1),
         )
         return
-
     
     def initialize(self, sim):
         super().initialize(sim)
@@ -93,8 +91,6 @@ def initialize(self, sim):
         self.init_states(sim)
         self.init_results(sim)
         return
-        
-
 
     def validate_pars(self, sim):
         """
@@ -113,17 +109,15 @@ def init_states(self, sim):
         self.set_prognoses(sim, initial_cases)
         return
 
-
     def init_results(self, sim):
         """
-        Initialize results. TODO, should these be stored in the module or just added directly to the sim?
+        Initialize results
         """
-        self.results['n_susceptible']  = ssr.Result(self.name, 'n_susceptible', sim.npts, dtype=int)
-        self.results['n_infected']     = ssr.Result(self.name, 'n_infected', sim.npts, dtype=int)
-        self.results['prevalence']     = ssr.Result(self.name, 'prevalence', sim.npts, dtype=float)
-        self.results['new_infections'] = ssr.Result(self.name, 'n_infected', sim.npts, dtype=int)
+        self.results += ss.Result(self.name, 'n_susceptible', sim.npts, dtype=int)
+        self.results += ss.Result(self.name, 'n_infected', sim.npts, dtype=int)
+        self.results += ss.Result(self.name, 'prevalence', sim.npts, dtype=float)
+        self.results += ss.Result(self.name, 'new_infections', sim.npts, dtype=int)
         return
-    
 
     def update(self, sim):
         """
@@ -133,20 +127,18 @@ def update(self, sim):
         self.make_new_cases(sim)
         self.update_results(sim)
         return
-    
 
     def update_states(self, sim):
         # Carry out any autonomous state changes at the start of the timestep
         pass
 
-
     def make_new_cases(self, sim):
         """ Add new cases of module, through transmission, incidence, etc. """
         pars = sim.pars[self.name]
         for k, layer in sim.people.networks.items():
             if k in pars['beta']:
-                rel_trans = (sim.people[self.name].infected & sim.people.alive).astype(float)
-                rel_sus = (sim.people[self.name].susceptible & sim.people.alive).astype(float)
+                rel_trans = (self.infected & sim.people.alive).astype(float)
+                rel_sus = (self.susceptible & sim.people.alive).astype(float)
                 for a, b, beta in [[layer['p1'], layer['p2'], pars['beta'][k][0]], [layer['p2'], layer['p1'], pars['beta'][k][1]]]:
                     # probability of a->b transmission
                     p_transmit = rel_trans[a] * rel_sus[b] * layer['beta'] * beta
@@ -154,17 +146,14 @@ def make_new_cases(self, sim):
                     if new_cases.any():
                         self.set_prognoses(sim, b[new_cases])
 
-
     def set_prognoses(self, sim, uids):
         pass
 
-
     def update_results(self, sim):
-        self.results['n_susceptible'][sim.ti]  = np.count_nonzero(sim.people[self.name].susceptible)
-        self.results['n_infected'][sim.ti]     = np.count_nonzero(sim.people[self.name].infected)
-        self.results['prevalence'][sim.ti]     = sim.results[self.name].n_infected[sim.ti] / len(sim.people)
-        self.results['new_infections'][sim.ti] = np.count_nonzero(sim.people[self.name].ti_infected == sim.ti)
-
+        self.results['n_susceptible'][sim.ti]  = np.count_nonzero(self.susceptible)
+        self.results['n_infected'][sim.ti]     = np.count_nonzero(self.infected)
+        self.results['prevalence'][sim.ti]     = self.results.n_infected[sim.ti] / len(sim.people)
+        self.results['new_infections'][sim.ti] = np.count_nonzero(self.ti_infected == sim.ti)
 
     def finalize_results(self, sim):
         pass
diff --git a/stisim/networks.py b/stisim/networks.py
index 4e0215d6..de5c843c 100644
--- a/stisim/networks.py
+++ b/stisim/networks.py
@@ -5,8 +5,7 @@
 # %% Imports
 import numpy as np
 import sciris as sc
-from . import utils as ssu
-from . import settings as sss
+import stisim as ss
 
 
 # Specify all externally visible functions this file defines
@@ -14,11 +13,12 @@
 
 class Network(sc.objdict):
     """
-    A small class holding a single network of contact edges (connections) between people.
+    A class holding a single network of contact edges (connections) between people
+    as well as methods for updating these.
 
     The input is typically arrays including: person 1 of the connection, person 2 of
     the connection, the weight of the connection, the duration and start/end times of
-    the connection. Connections are undirected; each person is both a source and sink.
+    the connection.
 
     Args:
         p1 (array): an array of length N, the number of connections in the network, with the indices of people
@@ -51,14 +51,14 @@ class Network(sc.objdict):
         network2 = ss.Network(**network, index=index, self_conn=self_conn, label=network.label)
     """
 
-    def __init__(self, *args, key_dict=None, transmission='horizontal', label=None, **kwargs):
+    def __init__(self, *args, key_dict=None, vertical=False, label=None, **kwargs):
         default_keys = {
-            'p1': sss.default_int,
-            'p2': sss.default_int,
-            'beta': sss.default_float,
+            'p1': ss.int_,
+            'p2': ss.int_,
+            'beta': ss.float_,
         }
         self.meta = sc.mergedicts(default_keys, key_dict)
-        self.transmission = transmission  # "vertical" or "horizontal", determines whether transmission is bidirectional
+        self.vertical = vertical  # Whether transmission is bidirectional
         self.basekey = 'p1'  # Assign a base key for calculating lengths and performing other operations
         self.label = label
         self.initialized = False
@@ -230,9 +230,9 @@ def find_contacts(self, inds, as_array=True):
             inds = np.array(inds, dtype=np.int64)
 
         # Find the contacts
-        contact_inds = ssu.find_contacts(self['p1'], self['p2'], inds)
+        contact_inds = ss.find_contacts(self['p1'], self['p2'], inds)
         if as_array:
-            contact_inds = np.fromiter(contact_inds, dtype=sss.default_int)
+            contact_inds = np.fromiter(contact_inds, dtype=ss.int_)
             contact_inds.sort()  # Sorting ensures that the results are reproducible for a given seed as well as being identical to previous versions of HPVsim
 
         return contact_inds
@@ -246,8 +246,7 @@ def update(self):
         pass
 
 
-
-class Networks(ssu.ndict):
+class Networks(ss.ndict):
     def __init__(self, *args, type=Network, **kwargs):
         return super().__init__(self, *args, type=type, **kwargs)
     
@@ -259,10 +258,10 @@ class simple_sexual(Network):
     """
     def __init__(self, mean_dur=5):
         key_dict = {
-            'p1': sss.default_int,
-            'p2': sss.default_int,
-            'dur': sss.default_float,
-            'beta': sss.default_float,
+            'p1': ss.int_,
+            'p2': ss.int_,
+            'dur': ss.float_,
+            'beta': ss.float_,
         }
 
         # Call init for the base class, which sets all the keys
@@ -313,12 +312,12 @@ class hpv_network(Network):
     def __init__(self, pars=None):
 
         key_dict = {
-            'p1': sss.default_int,
-            'p2': sss.default_int,
-            'dur': sss.default_float,
-            'acts': sss.default_float,
-            'start': sss.default_float,
-            'beta': sss.default_float,
+            'p1': ss.int_,
+            'p2': ss.int_,
+            'dur': ss.float_,
+            'acts': ss.float_,
+            'start': ss.float_,
+            'beta': ss.float_,
         }
 
         # Call init for the base class, which sets all the keys
@@ -398,7 +397,7 @@ def add_pairs(self, people, ti=0):
         current_partners = np.zeros((len(people)))
         current_partners[f_partnered_inds] = f_partnered_counts
         current_partners[m_partnered_inds] = m_partnered_counts
-        partners = ssu.sample(**self.pars['partners'], size=len(people)) + 1
+        partners = ss.sample(**self.pars['partners'], size=len(people)) + 1
         underpartnered = current_partners < partners  # Indices of underpartnered people
         f_eligible = f_active & underpartnered
         m_eligible = m_active & underpartnered
@@ -407,22 +406,22 @@ def add_pairs(self, people, ti=0):
         bins = self.pars['participation'][0, :]  # Extract age bins
 
         # Try randomly select females for pairing
-        f_eligible_inds = ssu.true(f_eligible)  # Inds of all eligible females
+        f_eligible_inds = ss.true(f_eligible)  # Inds of all eligible females
         age_bins_f = np.digitize(people.age[f_eligible_inds], bins=bins) - 1  # Age bins of selected females
         bin_range_f = np.unique(age_bins_f)  # Range of bins
         f = []  # Initialize the female partners
         for ab in bin_range_f:  # Loop over age bins
-            these_f_contacts = ssu.binomial_filter(self.pars['participation'][1][ab], f_eligible_inds[
+            these_f_contacts = ss.binomial_filter(self.pars['participation'][1][ab], f_eligible_inds[
                 age_bins_f == ab])  # Select females according to their participation rate in this layer
             f += these_f_contacts.tolist()
 
         # Select males according to their participation rate in this layer
-        m_eligible_inds = ssu.true(m_eligible)
+        m_eligible_inds = ss.true(m_eligible)
         age_bins_m = np.digitize(people.age[m_eligible_inds], bins=bins) - 1
         bin_range_m = np.unique(age_bins_m)  # Range of bins
         m = []  # Initialize the male partners
         for ab in bin_range_m:
-            these_m_contacts = ssu.binomial_filter(self.pars['participation'][2][ab], m_eligible_inds[
+            these_m_contacts = ss.binomial_filter(self.pars['participation'][2][ab], m_eligible_inds[
                 age_bins_m == ab])  # Select males according to their participation rate in this layer
             m += these_m_contacts.tolist()
 
@@ -456,8 +455,8 @@ def add_pairs(self, people, ti=0):
         p1 = np.array(f)
         p2 = selected_males
         n_partnerships = len(p1)
-        dur = ssu.sample(**self.pars['dur_pship'], size=n_partnerships)
-        acts = ssu.sample(**self.pars['acts'], size=n_partnerships)
+        dur = ss.sample(**self.pars['dur_pship'], size=n_partnerships)
+        acts = ss.sample(**self.pars['acts'], size=n_partnerships)
         age_p1 = people.age[p1]
         age_p2 = people.age[p2]
 
@@ -488,11 +487,11 @@ def add_pairs(self, people, ti=0):
         retired_vals = 0
 
         # Set values and return
-        scaled_acts = np.full(len(acts), np.nan, dtype=sss.default_float)
+        scaled_acts = np.full(len(acts), np.nan, dtype=ss.float_)
         scaled_acts[below_peak_inds] = below_peak_vals
         scaled_acts[above_peak_inds] = above_peak_vals
         scaled_acts[retired_inds] = retired_vals
-        start = np.array([ti] * n_partnerships, dtype=sss.default_float)
+        start = np.array([ti] * n_partnerships, dtype=ss.float_)
         beta = np.ones(n_partnerships)
 
         new_contacts = dict(
@@ -521,12 +520,12 @@ def update(self, people, ti=None, dt=None):
 
 
 class maternal(Network):
-    def __init__(self, key_dict=None, transmission='vertical'):
+    def __init__(self, key_dict=None, vertical=True):
         """
         Initialized empty and filled with pregnancies throughout the simulation
         """
-        key_dict = sc.mergedicts({'dur': sss.default_float}, key_dict)
-        super().__init__(key_dict=key_dict, transmission=transmission)
+        key_dict = sc.mergedicts({'dur': ss.float_}, key_dict)
+        super().__init__(key_dict=key_dict, vertical=vertical)
         return
 
     def update(self, people, dt=None):
diff --git a/stisim/parameters.py b/stisim/parameters.py
index 07f54907..b2267a33 100644
--- a/stisim/parameters.py
+++ b/stisim/parameters.py
@@ -3,11 +3,10 @@
 """
 
 import sciris as sc
-from .settings import options as sso  # For setting global options
-
-__all__ = ['make_pars']
+import stisim as ss
 
 
+__all__ = ['Parameters', 'make_pars']
 
 
 class Parameters(sc.objdict):
@@ -42,8 +41,7 @@ def __init__(self, **kwargs):
         self.dt              = 1.0           # Timestep (in years)
         self.dt_demog        = 1.0           # Timestep for demographic updates (in years)
         self.rand_seed       = 1             # Random seed, if None, don't reset
-        self.verbose         = sso.verbose   # Whether or not to display information during the run -- options are 0 (silent), 0.1 (some; default), 1 (default), 2 (everything)
-        self.use_migration   = True          # Whether to estimate migration rates to correct the total population size
+        self.verbose         = ss.options.verbose # Whether or not to display information during the run -- options are 0 (silent), 0.1 (some; default), 1 (default), 2 (everything)
     
         # Events and interventions
         self.connectors      = sc.autolist()
diff --git a/stisim/people.py b/stisim/people.py
index aa7ae88c..0a5d2fdd 100644
--- a/stisim/people.py
+++ b/stisim/people.py
@@ -3,15 +3,13 @@
 """
 
 # %% Imports
-import sciris as sc
 import numpy as np
-import functools
-from . import utils as ssu
-from .version import __version__
-from .states import DynamicView, State, INT_NAN
+import sciris as sc
+import stisim as ss
 
 __all__ = ['BasePeople', 'People']
 
+
 # %% Main people class
 class BasePeople(sc.prettyobj):
     """
@@ -23,8 +21,8 @@ class BasePeople(sc.prettyobj):
     def __init__(self, n):
 
         self.initialized = False
-        self._uid_map = DynamicView(int, fill_value=INT_NAN) # This variable tracks all UIDs ever created
-        self.uid = DynamicView(int, fill_value=INT_NAN) # This variable tracks all UIDs currently in use
+        self._uid_map = ss.DynamicView(int, fill_value=ss.INT_NAN) # This variable tracks all UIDs ever created
+        self.uid = ss.DynamicView(int, fill_value=ss.INT_NAN) # This variable tracks all UIDs currently in use
 
         self._uid_map.initialize(n)
         self._uid_map[:] = np.arange(0, n)
@@ -45,9 +43,7 @@ def __len__(self):
     def add_state(self, state):
         if id(state) not in self._states:
             self._states[id(state)] = state
-
-    def __len__(self):
-        return len(self.uid)
+        return
 
     def grow(self, n):
         """
@@ -88,8 +84,9 @@ def remove(self, uids_to_remove):
             state._trim(keep_inds)
 
         # Update the UID map
-        self._uid_map[:] = INT_NAN # Clear out all previously used UIDs
+        self._uid_map[:] = ss.INT_NAN # Clear out all previously used UIDs
         self._uid_map[keep_uids] = np.arange(0, len(keep_uids)) # Assign the array indices for all of the current UIDs
+        return
 
 
     def __getitem__(self, key):
@@ -142,32 +139,31 @@ class People(BasePeople):
     # %% Basic methods
 
     def __init__(self, n, extra_states=None, networks=None):
-        """
-        Initialize
-        """
+        """ Initialize """
 
         super().__init__(n)
         
-        self.version = __version__  # Store version info
+        self.initialized = False
+        self.version = ss.__version__  # Store version info
 
         states = [
-            State('age', float),
-            State('female', bool, False),
-            State('debut', float),
-            State('alive', bool, True),
-            State('ti_dead', float, np.nan),  # Time index for death
-            State('scale', float, 1.0),
+            ss.State('age', float),
+            ss.State('female', bool, False),
+            ss.State('debut', float),
+            ss.State('alive', bool, True),
+            ss.State('ti_dead', float, np.nan),  # Time index for death
+            ss.State('scale', float, 1.0),
         ]
         states.extend(sc.promotetolist(extra_states))
 
-        self.states = ssu.ndict()
+        self.states = ss.ndict()
 
         for state in states:
             self.add_state(state)  # Register the state internally for dynamic growth
             self.states.append(state)  # Expose these states with their original names
             state.initialize(self) # Connect the state to this people instance
             setattr(self, state.name, state)
-        self.networks = ssu.ndict(networks)
+        self.networks = ss.ndict(networks)
 
         return
 
@@ -244,3 +240,8 @@ def active(self):
     def dead(self):
         """ Dead boolean """
         return ~self.alive
+
+    @property
+    def male(self):
+        """ Male boolean """
+        return ~self.female
diff --git a/stisim/results.py b/stisim/results.py
index bdd01991..655acc2d 100644
--- a/stisim/results.py
+++ b/stisim/results.py
@@ -1,16 +1,13 @@
 """
-Results and associated structures. Currently only holds results, could move to base.py
-unless other things get added (e.g. Resultsets, MultiResults, other...)
+Result structures.
 """
 
 import numpy as np
 import sciris as sc
-from . import utils as ssu
+import stisim as ss
 
 
-class Results(ssu.ndict):
-    def __init__(self, *args, **kwargs):
-        super().__init__(self, type=Result, *args, **kwargs)
+__all__ = ['Result', 'Results']
 
 
 class Result(np.ndarray):
@@ -29,4 +26,9 @@ def __repr__(self):
         return out
     
     def to_df(self):
-        return sc.dataframe({self.name:self})
\ No newline at end of file
+        return sc.dataframe({self.name:self})
+    
+    
+class Results(ss.ndict):
+    def __init__(self, *args, **kwargs):
+        super().__init__(self, type=Result, *args, **kwargs)
\ No newline at end of file
diff --git a/stisim/settings.py b/stisim/settings.py
index d4403666..9152e034 100644
--- a/stisim/settings.py
+++ b/stisim/settings.py
@@ -2,26 +2,18 @@
 Define options for STIsim.
 All options should be set using set() or directly, e.g.::
 
-    ss.options(font_size=18)
-
-To reset default options, use::
-
-    ss.options('default')
-
-Note: "options" is used to refer to the choices available (e.g., DPI), while "settings"
-is used to refer to the choices made (e.g., DPI=150).
+    ss.options(verbose=False)
 """
 
 import os
-import sciris as sc
 import numpy as np
+import sciris as sc
 
-# Specify all externally visible classes this file defines
-__all__ = ['options']
 
+__all__ = ['options', 'float_', 'int_']
 
-# %% Define the options class
 
+# Not public to avoid confusion with ss.options
 class Options(sc.objdict):
     """
     Set options for STIsim.
@@ -216,14 +208,14 @@ def changed(self, key):
 # Default for precision
 # Used in various places throughout the code, generally as:
 #   import stisim.settings as sss
-#   arr = np.full(100, 0, sss.default_float)
+#   arr = np.full(100, 0, sss.float_)
 result_float = np.float64  # Always use float64 for results, for simplicity
 if options.precision == 32:
-    default_float = np.float32
-    default_int = np.int32
+    float_ = np.float32
+    int_ = np.int32
 elif options.precision == 64:  # pragma: no cover
-    default_float = np.float64
-    default_int = np.int64
+    float_ = np.float64
+    int_ = np.int64
 else:
     raise NotImplementedError(f'Precision must be either 32 bit or 64 bit, not {options.precision}')
 
diff --git a/stisim/sim.py b/stisim/sim.py
index a37cd787..5d99ef85 100644
--- a/stisim/sim.py
+++ b/stisim/sim.py
@@ -5,29 +5,23 @@
 # Imports
 import numpy as np
 import sciris as sc
-from . import settings as sss
-from . import utils as ssu
-from . import people as ssppl
-from . import modules as ssm
-from . import connectors as ssc
-from . import parameters as sspar
-from . import interventions as ssi
-from . import analyzers as ssa
-from . import results as ssr
+import stisim as ss
 
 
-# Define the model
-class Sim:
+__all__ = ['Sim', 'AlreadyRunError']
 
-    def __init__(self, pars=None, label=None, people=None, modules=None, connectors=None, **kwargs):
+
+class Sim(sc.prettyobj):
+
+    def __init__(self, pars=None, label=None, people=None, modules=None, **kwargs):
 
         # Set attributes
         self.label = label  # The label/name of the simulation
         self.created = None  # The datetime the sim was created
         self.people = people  # People object
-        self.modules = ssm.Modules(modules)  # List of modules to simulate
-        self.connectors = ssc.Connectors(connectors)
-        self.results = ssr.Results()  # For storing results
+        self.modules = ss.Modules(modules)  # List of modules to simulate
+        self.connectors = None  # Placeholder storage while we determine what these are
+        self.results = ss.Results()  # For storing results
         self.summary = None  # For storing a summary of the results
         self.initialized = False  # Whether initialization is complete
         self.complete = False  # Whether a simulation has completed running # TODO: replace with finalized?
@@ -41,12 +35,12 @@ def __init__(self, pars=None, label=None, people=None, modules=None, connectors=
         self.npts = None
 
         # Make default parameters (using values from parameters.py)
-        self.pars = sspar.make_pars()  # Start with default pars
+        self.pars = ss.make_pars()  # Start with default pars
         self.pars.update_pars(sc.mergedicts(pars, kwargs))  # Update the parameters
 
         # Initialize other quantities
-        self.interventions = ssi.Interventions()
-        self.analyzers = ssa.Analyzers()
+        self.interventions = ss.Interventions()
+        self.analyzers = ss.Analyzers()
 
         return
 
@@ -58,6 +52,14 @@ def dt(self):
     def year(self):
         return self.yearvec[self.ti]
 
+    @property
+    def disease_list(self):
+        return [m for m in self.modules if isinstance(m, ss.Disease)]
+
+    @property
+    def diseases_present(self):
+        return len(self.disease_list) > 0
+
     def initialize(self, popdict=None, reset=False, **kwargs):
         """
         Perform all initializations on the sim.
@@ -67,7 +69,7 @@ def initialize(self, popdict=None, reset=False, **kwargs):
         self.validate_pars()  # Ensure parameters have valid values
         self.validate_dt()
         self.init_time_vecs()  # Initialize time vecs
-        ssu.set_seed(self.pars['rand_seed'])  # Reset the random seed before the population is created
+        ss.set_seed(self.pars['rand_seed'])  # Reset the random seed before the population is created
 
         # Initialize the core sim components
         self.init_people(popdict=popdict, reset=reset, **kwargs)  # Create all the people (the heaviest step)
@@ -76,11 +78,13 @@ def initialize(self, popdict=None, reset=False, **kwargs):
         self.init_modules()
         self.init_interventions()
         self.init_analyzers()
-        self.validate_layer_pars()
+
+        # Perform post-initialization validation
+        self.validate_post_init()
 
         # Reset the random seed to the default run seed, so that if the simulation is run with
         # reset_seed=False right after initialization, it will still produce the same output
-        ssu.set_seed(self.pars['rand_seed'] + 1)
+        ss.set_seed(self.pars['rand_seed'] + 1)
 
         # Final steps
         self.initialized = True
@@ -89,30 +93,6 @@ def initialize(self, popdict=None, reset=False, **kwargs):
 
         return self
 
-    def layer_keys(self):
-        """
-        Attempt to retrieve the current network names
-        """
-        try:
-            keys = list(self.people['networks'].keys())
-        except:  # pragma: no cover
-            keys = []
-        return keys
-
-    def validate_layer_pars(self):
-        """
-        Check if there is a contact network
-        """
-
-        if self.people is not None:
-            modules = len(self.modules) > 0
-            pop_keys = set(self.people.networks.keys())
-            if modules and not len(pop_keys):
-                warnmsg = f'Warning: your simulation has {len(self.modules)} modules but no contact layers.'
-                ssu.warn(warnmsg, die=False)
-
-        return
-
     def validate_dt(self):
         """
         Check that 1/dt is an integer value, otherwise results and time vectors will have mismatching shapes.
@@ -201,19 +181,21 @@ def init_people(self, popdict=None, reset=False, verbose=None, **kwargs):
 
         # If people have not been supplied, make them
         if self.people is None:
-            self.people = ssppl.People(n=self.pars['n_agents'], **kwargs)  # This just assigns UIDs and length
+            self.people = ss.People(n=self.pars['n_agents'], **kwargs)  # This just assigns UIDs and length
 
         # If a popdict has not been supplied, we can make one from location data
         if popdict is None:
             if self.pars['location'] is not None:
                 # Check where to get total_pop from
-                if self.pars['total_pop'] is not None:  # If no pop_scale has been provided, try to get it from the location
+                if self.pars[
+                    'total_pop'] is not None:  # If no pop_scale has been provided, try to get it from the location
                     errormsg = 'You can either define total_pop explicitly or via the location, but not both'
                     raise ValueError(errormsg)
-                total_pop, popdict = ssppl.make_popdict(n=self.pars['n_agents'], location=self.pars['location'], verbose=self.pars['verbose'])
+                total_pop, popdict = ss.make_popdict(n=self.pars['n_agents'], location=self.pars['location'], verbose=self.pars['verbose'])
 
             else:
-                if self.pars['total_pop'] is not None:  # If no pop_scale has been provided, try to get it from the location
+                if self.pars[
+                    'total_pop'] is not None:  # If no pop_scale has been provided, try to get it from the location
                     total_pop = self.pars['total_pop']
                 else:
                     if self.pars['pop_scale'] is not None:
@@ -235,16 +217,12 @@ def init_people(self, popdict=None, reset=False, verbose=None, **kwargs):
 
         return self
 
-
     def init_modules(self):
         """ Initialize modules and connectors to be simulated """
         for module in self.modules.values():
             module.initialize(self)
-        for connector in self.connectors.values():
-            connector.initialize(self)
         return
 
-
     def init_networks(self):
         """ Initialize networks if these have been provided separately from the people """
 
@@ -252,7 +230,7 @@ def init_networks(self):
         # This means networks will be stored in self.pars['networks'] and we'll need to copy them to the people.
         if self.people.networks is None or len(self.people.networks) == 0:
             if self.pars['networks'] is not None:
-                self.people.networks = ssu.ndict(self.pars['networks'])
+                self.people.networks = ss.ndict(self.pars['networks'])
 
         for key, network in self.people.networks.items():
             if network.label is not None:
@@ -265,16 +243,13 @@ def init_networks(self):
 
         return
 
-
     def init_results(self):
         """
         Create the main results structure.
         """
         # Make results
-        results = ssr.Results(
-            ssr.Result('births', None, self.npts, sss.default_float),
-            ssr.Result('deaths', None, self.npts, sss.default_float),
-            ssr.Result('n_alive', None, self.npts, sss.default_int),
+        results = ss.Results(
+            ss.Result('n_alive', None, self.npts, ss.int_),
         )
 
         # Final items
@@ -283,15 +258,14 @@ def init_results(self):
 
         return
 
-
     def init_interventions(self):
         """ Initialize and validate the interventions """
 
         # Translate the intervention specs into actual interventions
         for i, intervention in enumerate(self.pars['interventions']):
-            if isinstance(intervention, type) and issubclass(intervention, ssi.Intervention):
+            if isinstance(intervention, type) and issubclass(intervention, ss.Intervention):
                 intervention = intervention()  # Convert from a class to an instance of a class
-            if isinstance(intervention, ssi.Intervention):
+            if isinstance(intervention, ss.Intervention):
                 intervention.initialize(self)
                 self.interventions += intervention
             elif callable(intervention):
@@ -302,25 +276,35 @@ def init_interventions(self):
 
         return
 
-
     def init_analyzers(self):
         """ Initialize the analyzers """
 
         # Interpret analyzers
         for ai, analyzer in enumerate(self.pars['analyzers']):
-            if isinstance(analyzer, type) and issubclass(analyzer, ssa.Analyzer):
+            if isinstance(analyzer, type) and issubclass(analyzer, ss.Analyzer):
                 analyzer = analyzer()  # Convert from a class to an instance of a class
-            if not (isinstance(analyzer, ssa.Analyzer) or callable(analyzer)):
+            if not (isinstance(analyzer, ss.Analyzer) or callable(analyzer)):
                 errormsg = f'Analyzer {analyzer} does not seem to be a valid analyzer: must be a function or Analyzer subclass'
                 raise TypeError(errormsg)
             self.analyzers += analyzer  # Add it in
 
         for analyzer in self.analyzers:
-            if isinstance(analyzer, ssa.Analyzer):
+            if isinstance(analyzer, ss.Analyzer):
                 analyzer.initialize(self)
 
         return
 
+    def validate_post_init(self):
+        """
+        Validate inputs again once everything has been initialized.
+        TBC whether we keep this or incorporate the checks into the init methods
+        """
+        # Make sure that there's a contact network if any diseases are present
+        networks_present = len(self.people.networks.keys())
+        if self.diseases_present and not networks_present:
+            warnmsg = f'Warning: your simulation has {len(self.disease_list)} diseases but no contact network(s).'
+            ss.warn(warnmsg, die=False)
+        return
 
     def step(self):
         """ Step through time and update values """
@@ -333,7 +317,6 @@ def step(self):
         self.people.update(self)
         self.apply_interventions()
         self.update_modules()
-        self.apply_connectors()
         self.apply_analyzers()
 
         # Tidy up
@@ -343,7 +326,6 @@ def step(self):
 
         return
 
-
     def apply_interventions(self):
         """
         Apply the interventions
@@ -352,7 +334,6 @@ def apply_interventions(self):
             intervention(self)
         return
 
-
     def update_modules(self):
         """
         Update modules
@@ -361,17 +342,6 @@ def update_modules(self):
             module.update(self)
         return
 
-
-    def apply_connectors(self):
-        """ Update connectors """
-        for connector in self.connectors:
-            if callable(connector): # TODO: make logic consistent with interventions, etc.
-                connector(self)
-            else:
-                connector.apply(self)
-        return
-
-
     def apply_analyzers(self):
         """
         Apply the analyzers
@@ -380,7 +350,6 @@ def apply_analyzers(self):
             analyzer(self)
         return
 
-
     def run(self, until=None, reset_seed=True, verbose=None):
         """ Run the model once """
 
@@ -394,7 +363,7 @@ def run(self, until=None, reset_seed=True, verbose=None):
             verbose = self.pars['verbose']
 
         if reset_seed:
-            ssu.set_seed(self.pars['rand_seed'] + 1)
+            ss.set_seed(self.pars['rand_seed'] + 1)
 
         # Check for AlreadyRun errors
         errormsg = None
@@ -440,7 +409,6 @@ def run(self, until=None, reset_seed=True, verbose=None):
 
         return self
 
-
     def finalize(self, verbose=None):
         """ Compute final results """
 
@@ -454,7 +422,6 @@ def finalize(self, verbose=None):
         self.ti -= 1  # During the run, this keeps track of the next step; restore this be the final day of the sim
         return
 
-
     def shrink(self, skip_attrs=None, in_place=True):
         """
         "Shrinks" the simulation by removing the people and other memory-intensive
@@ -488,7 +455,6 @@ def shrink(self, skip_attrs=None, in_place=True):
         else:
             return shrunken
 
-
     def save(self, filename=None, keep_people=None, skip_attrs=None, **kwargs):
         """
         Save to disk as a gzipped pickle.
diff --git a/stisim/states.py b/stisim/states.py
index fd609295..f76dcd8e 100644
--- a/stisim/states.py
+++ b/stisim/states.py
@@ -2,12 +2,13 @@
 import numpy as np
 import sciris as sc
 import numba as nb
-from numpy.lib.mixins import NDArrayOperatorsMixin # Inherit from this to automatically gain operators like +, -, ==, <, etc.
+from numpy.lib.mixins import NDArrayOperatorsMixin  # Inherit from this to automatically gain operators like +, -, ==, <, etc.
 
 __all__ = ['State', 'DynamicView', 'INT_NAN']
 
 INT_NAN = np.iinfo(int).max  # Value to use to flag removed UIDs (i.e., an integer value we are treating like NaN, since NaN can't be stored in an integer array)
 
+
 class FusedArray(NDArrayOperatorsMixin):
     # This is a class that allows indexing by UID but does not support dynamic growth
     # It's kind of like a Pandas series but one that only supports a monotonically increasing
diff --git a/stisim/utils.py b/stisim/utils.py
index b6154832..e38fbb17 100644
--- a/stisim/utils.py
+++ b/stisim/utils.py
@@ -7,7 +7,7 @@
 import warnings
 import numpy as np
 import sciris as sc
-from . import settings as sss
+import stisim as ss
 
 
 # What functions are externally visible -- note, this gets populated in each section below
@@ -42,24 +42,22 @@ def __init__(self, *args, name='name', type=None, strict=True, **kwargs):
         self._initialize(*args, **kwargs)
         return
     
-    def _process_arg(self, arg, key=None):
-        _name   = self.getattribute('_name')
-        _strict = self.getattribute('_strict')
+    def append(self, arg, key=None):
         valid = False
         if arg is None:
             return # Nothing to do
-        elif hasattr(arg, _name):
-            key = key or getattr(arg, _name)
+        elif hasattr(arg, self._name):
+            key = key or getattr(arg, self._name)
             valid = True
         elif isinstance(arg, dict):
-            if _name in arg:
-                key = key or arg[_name]
+            if self._name in arg:
+                key = key or arg[self._name]
                 valid = True
             else:
                 for k,v in arg.items():
-                    self._process_arg(v, key=k)
+                    self.append(v, key=k)
                 valid = None # Skip final processing
-        elif not _strict:
+        elif not self._strict:
             key = key or f'item{len(self)+1}'
             valid = True
         else:
@@ -71,16 +69,15 @@ def _process_arg(self, arg, key=None):
         elif valid is None:
             pass # Nothing to do
         else:
-            errormsg = f'Could not interpret argument {arg}: does not have expected attribute "{_name}"'
+            errormsg = f'Could not interpret argument {arg}: does not have expected attribute "{self._name}"'
             raise ValueError(errormsg)
             
         return
         
     def _check_type(self, arg):
         """ Check types """
-        _type   = self.getattribute('_type')
-        if _type is not None:
-            if not isinstance(arg, _type):
+        if self._type is not None:
+            if not isinstance(arg, self._type):
                 errormsg = f'The following item does not have the expected type {self._type}:\n{arg}'
                 raise TypeError(errormsg)
         return
@@ -88,19 +85,26 @@ def _check_type(self, arg):
     def _initialize(self, *args, **kwargs):
         args = sc.mergelists(*args)
         for arg in args:
-            self._process_arg(arg)
+            self.append(arg)
         for key,arg in kwargs.items():
-            self._process_arg(arg, key=key)
+            self.append(arg, key=key)
         return
     
-    def append(self, arg):
-        ''' Allow being used like a list '''
-        self._process_arg(arg)
-        return
+    def copy(self):
+        new = self.__class__.__new__(name=self._name, type=self._type, strict=self._strict)
+        new.update(self)
+        return new
     
     def __add__(self, dict2):
-        ''' Allow two dictionaries to be added (merged) '''
-        return sc.mergedicts(self, self._validate(dict2))
+        """ Allow c = a + b """
+        new = self.copy()
+        new.append(dict2)
+        return new
+
+    def __iadd__(self, dict2):
+        """ Allow a += b """
+        self.append(dict2)
+        return self
 
 
 def omerge(*args, **kwargs):
@@ -112,11 +116,11 @@ def warn(msg, category=None, verbose=None, die=None):
     """ Helper function to handle warnings -- not for the user """
 
     # Handle inputs
-    warnopt = sss.options.warnings if not die else 'error'
+    warnopt = ss.options.warnings if not die else 'error'
     if category is None:
         category = RuntimeWarning
     if verbose is None:
-        verbose = sss.options.verbose
+        verbose = ss.options.verbose
 
     # Handle the different options
     if warnopt in ['error', 'errors']:  # Include alias since hard to remember
@@ -418,4 +422,4 @@ def undefined(arr):
 
         inds = ss.defined(np.array([1,np.nan,0,np.nan,1,0,1]))
     """
-    return np.isnan(arr).nonzero()[-1]
\ No newline at end of file
+    return np.isnan(arr).nonzero()[-1]
diff --git a/stisim/version.py b/stisim/version.py
index eeb95e7b..bd126163 100644
--- a/stisim/version.py
+++ b/stisim/version.py
@@ -4,6 +4,6 @@
 
 __all__ = ['__version__', '__versiondate__', '__license__']
 
-__version__ = '0.0.3'
-__versiondate__ = '2023-08-14'
+__version__ = '0.0.6'
+__versiondate__ = '2023-08-30'
 __license__ = f'STIsim {__version__} ({__versiondate__}) — © 2023 by IDM'


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 576c683d..00c668fc 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -1,4 +1,4 @@
-name: Sciris CI tests
+name: STIsim CI tests
 on: [pull_request]
 
 jobs:
diff --git a/tests/simple.py b/tests/simple.py
index 5989de10..e348d2e3 100644
--- a/tests/simple.py
+++ b/tests/simple.py
@@ -1,5 +1,5 @@
 """
-Test objects from base.py
+Run simplest tests
 """
 
 # %% Imports and settings
@@ -13,7 +13,7 @@
 hiv = ss.HIV()
 hiv.pars['beta'] = {'simple_sexual': [0.0008, 0.0004], 'maternal': [0.2, 0]}
 
-sim = ss.Sim(people=ppl, modules=[hiv, ss.Pregnancy()])
+sim = ss.Sim(people=ppl, modules=[hiv, ss.Gonorrhea(), ss.Pregnancy()])
 sim.initialize()
 sim.run()