Repository: starsimhub/starsim (language: python)

Issue:
Three options for rng
Currently, we have a boolean `multirng` option which defaults to True.

When True, distributions use the centralized numpy random number generator.

When False, a separate MultiRNG is used for each generator.

We need a third option wherein each distribution gets a _separate_ generator, but they're all just default numpy streams.
Three options for rng
Currently, we have a boolean `multirng` option which defaults to True.

When True, distributions use the centralized numpy random number generator.

When False, a separate MultiRNG is used for each generator.

We need a third option wherein each distribution gets a _separate_ generator, but they're all just default numpy streams.
Merge 348 + main
After merging with `main`, deepcopy tests fail with:
```py
FAILED test_dcp.py::test_dcp_until[single] - AttributeError: 'numpy.random._generator.Generator' object has no attribute 'step'
```
Investigating, will fix if it's something simple. Weird that all the other tests pass!


Discussion / hints:
Adding the third option should have little to no overhead and will be slightly better. I don't want to completely eliminate the centralized generator option because it's useful for comparison.
My inclination is towards the opposite actually ... I think we should eliminate the `multirng` option and just have a single (multi-RNG) path that's easy to use and performant :) 
Adding the third option should have little to no overhead and will be slightly better. I don't want to completely eliminate the centralized generator option because it's useful for comparison.
My inclination is towards the opposite actually ... I think we should eliminate the `multirng` option and just have a single (multi-RNG) path that's easy to use and performant :) 


Fix patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 2589f2c4..3012e52f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -7,6 +7,16 @@ What's new
 All notable changes to the codebase are documented in this file. Changes that may result in differences in model output, or are required in order to run an old parameter set with the current version, are flagged with the term "Regression information".
 
 
+Version 0.2.9 (2024-03-18)
+--------------------------
+- Renames and extends the multirng option in settings, now called 'rng', which set how random numbers are handled in Starsim with three options:
+
+    - "centralized" uses the centralized numpy random number generator for all distributions.
+    - "single" uses a separate (SingleRNG) random number generator for each distribution.
+    - "multi" uses a separate (MultiRNG) random number generator for each distribution.
+- *GitHub info*: PR `349 <https://github.com/amath-idm/stisim/pull/349>`_
+
+
 Version 0.2.8 (2024-03-13)
 --------------------------
 - Add ``ss.demo()`` to quickly create a default simulation.
diff --git a/starsim/disease.py b/starsim/disease.py
index dec45f85..791d3e93 100644
--- a/starsim/disease.py
+++ b/starsim/disease.py
@@ -388,12 +388,12 @@ def make_new_cases(self, sim):
             if sim.ti == 0: ss.warn(warnmsg, die=False)
             return
 
-        if not ss.options.multirng:
-            # Determine new cases for singlerng
-            new_cases, sources = self._make_new_cases_singlerng(sim)
-        else:
+        if ss.options.multirng:
             # Determine new cases for multirng
             new_cases, sources = self._make_new_cases_multirng(sim)
+        else:
+            # Determine new cases for singlerng
+            new_cases, sources = self._make_new_cases_singlerng(sim)
 
         if len(new_cases):
             self._set_cases(sim, new_cases, sources)
diff --git a/starsim/distributions.py b/starsim/distributions.py
index ee1d6fc3..ddda6c7c 100644
--- a/starsim/distributions.py
+++ b/starsim/distributions.py
@@ -177,15 +177,25 @@ def rvs(self, *args, **kwargs):
     def set_rng(rng, gen):
         # Handle random generators
         ret = gen.random_state # Default
-        if options.multirng and rng and (gen.random_state == np.random.mtrand._rand):
-            # MultiRNG, rng not none, and the current "random_state" is the
-            # numpy global singleton... so let's override
-            if isinstance(rng, str):
-                ret = MultiRNG(rng) # Crate a new generator with the user-provided string
-            elif isinstance(rng, np.random.Generator):
-                ret = rng
-            else:
-                raise Exception(f'The rng must be a string or a np.random.Generator instead of {type(rng)}')
+        if gen.random_state == np.random.mtrand._rand and rng:
+            if options.rng=='multi':
+                # MultiRNG, rng not none, and the current "random_state" is the
+                # numpy global singleton... so let's override
+                if isinstance(rng, str):
+                    ret = MultiRNG(rng) # Crate a new generator with the user-provided string
+                elif isinstance(rng, np.random.Generator):
+                    ret = rng
+                else:
+                    raise Exception(f'The rng must be a string or a np.random.Generator instead of {type(rng)}')
+            elif options.rng=='single':
+                # SingleRNG, rng not none, and the current "random_state" is the
+                # numpy global singleton... so let's override
+                if isinstance(rng, str):
+                    ret = SingleRNG(rng) # Crate a new generator with the user-provided string
+                elif isinstance(rng, np.random.Generator):
+                    ret = rng
+                else:
+                    raise Exception(f'The rng must be a string or a np.random.Generator instead of {type(rng)}')
         return ret
 
     def initialize(self, sim, context):
@@ -264,17 +274,28 @@ def initialize(self, sim, context):
     def set_rng(self, rng):
         # Handle random generators
         ret = self.random_state # Default
-        if options.multirng and rng and (self.random_state == np.random.mtrand._rand):
-            # MultiRNG, rng not none, and the current "random_state" is the
-            # numpy global singleton... so let's override
-            if isinstance(rng, str):
-                ret = MultiRNG(rng) # Crate a new generator with the user-provided string
-            elif isinstance(rng, np.random.Generator):
-                ret = rng
-            else:
-                raise Exception(f'The rng must be a string or a np.random.Generator instead of {type(rng)}')
+        if self.random_state == np.random.mtrand._rand and rng:
+            if options.rng=='multi':
+                # MultiRNG, rng not none, and the current "random_state" is the
+                # numpy global singleton... so let's override
+                if isinstance(rng, str):
+                    ret = MultiRNG(rng) # Crate a new generator with the user-provided string
+                elif isinstance(rng, np.random.Generator):
+                    ret = rng
+                else:
+                    raise Exception(f'The rng must be a string or a np.random.Generator instead of {type(rng)}')
+            elif options.rng=='single':
+                # SingleRNG, rng not none, and the current "random_state" is the
+                # numpy global singleton... so let's override
+                if isinstance(rng, str):
+                    ret = SingleRNG(rng) # Crate a new generator with the user-provided string
+                elif isinstance(rng, np.random.Generator):
+                    ret = rng
+                else:
+                    raise Exception(f'The rng must be a string or a np.random.Generator instead of {type(rng)}')
         return ret
 
+
     def rvs(self, *args, **kwargs):
         """
         Return a specified number of samples from the distribution
diff --git a/starsim/random.py b/starsim/random.py
index 61204105..12c48770 100644
--- a/starsim/random.py
+++ b/starsim/random.py
@@ -98,14 +98,85 @@ def RNG(*args, **kwargs):
     Returns:
     Numpy RandomState singleton or a MultiRNG: Instance of a random number generator
     """
-    if ss.options.multirng:
-        rng = MultiRNG(*args, **kwargs)
-    else:
-        rng = SingleRNG(*args, **kwargs)
-    return rng
+    if ss.options.rng == 'multi':
+        return MultiRNG(*args, **kwargs)
+    elif ss.options.rng == 'single':
+        return SingleRNG(*args, **kwargs)
+    
+    # Centralized
+    return np.random.mtrand._rand
+
+class SingleRNG(np.random.Generator):
+    """
+    Enable a single random number generator stream per distribution without the complexity of MultiRNG.
+    Used with options.rng = 'single'
+    """
+    
+    def __init__(self, name, seed_offset=None, **kwargs):
+        """
+        Create a random number generator
+
+        seed_offset will be automatically assigned (based on hashing the name) if None
+        
+        name: a name for this random number generator, like "coin_flip"
+        """
+
+        self.name = name
+        self.kwargs = kwargs
 
+        if seed_offset is None:
+            # Obtain the seed offset by hashing the class name. Don't use python's hash because it is randomized.
+            self.seed_offset = int(hashlib.sha256(self.name.encode('utf-8')).hexdigest(), 16) % 10**8
+        else:
+            # Use user-provided seed_offset (unlikely)
+            self.seed_offset = seed_offset
+
+        self.initialized = False
+        return
 
-class MultiRNG(np.random.Generator):
+    def initialize(self, container, slots=None):
+        if self.initialized:
+            return
+
+        if container is not None:
+            self.seed = container.add(self) # base_seed + seed_offset
+        else:
+            # Enable use of SingleRNG without a container
+            self.seed = self.seed_offset
+
+        if 'bit_generator' not in self.kwargs:
+            self.kwargs['bit_generator'] = np.random.PCG64DXSM(seed=self.seed)
+        super().__init__(**self.kwargs)
+
+        self.initialized = True
+        return
+
+    def reset(self):
+        pass
+
+    def step(self, ti):
+        pass
+
+    def __deepcopy__(self, memo):
+        cls = self.__class__
+        result = cls.__new__(cls)
+        memo[id(self)] = result
+        for k, v in self.__dict__.items():
+            setattr(result, k, deepcopy(v, memo))
+
+        #'bit_generator' kwarg has changed
+        super(SingleRNG, result).__init__(**result.kwargs)
+
+        # and so has the _init_state
+        result._init_state = result.bit_generator.state # Store the initial state
+
+        self.seed = None # Will be determined once added to the RNG Container
+        result.initialized = True
+        result.ready = True
+
+        return result
+
+class MultiRNG(SingleRNG):
     """
     Class for tracking one random number generators associated with one decision per timestep.
 
@@ -163,8 +234,7 @@ def __init__(self, name, seed_offset=None, **kwargs):
         name: a name for this random number generator, like "coin_flip"
         """
 
-        self.name = name
-        self.kwargs = kwargs
+        super().__init__(name, seed_offset, **kwargs)
 
         if seed_offset is None:
             # Obtain the seed offset by hashing the class name. Don't use python's hash because it is randomized.
@@ -174,19 +244,11 @@ def __init__(self, name, seed_offset=None, **kwargs):
             self.seed_offset = seed_offset
 
         self.seed = None # Will be determined once added to the RNG Container
-        self.initialized = False
         self.ready = True
         return
 
     def initialize(self, container, slots):
-        if self.initialized:
-            return
-
-        if container is not None:
-            self.seed = container.add(self) # base_seed + seed_offset
-        else:
-            # Enable use of MultiRNG without a container
-            self.seed = self.seed_offset
+        super().initialize(container, slots)
 
         if isinstance(slots, int):
             # Handle edge case in which the user wants n sequential slots, as used in testing.
@@ -194,13 +256,8 @@ def initialize(self, container, slots):
         else:
             self.slots = slots # E.g. sim.people.slots (instead of using uid as the slots directly)
 
-        if 'bit_generator' not in self.kwargs:
-            self.kwargs['bit_generator'] = np.random.PCG64DXSM(seed=self.seed)
-        super().__init__(**self.kwargs)
-
         self._init_state = self.bit_generator.state # Store the initial state
 
-        self.initialized = True
         self.ready = True
         return
 
@@ -220,63 +277,3 @@ def step(self, ti):
         # jumped returns a new bit_generator, use directly instead of setting state?
         self.bit_generator.state = self.bit_generator.jumped(jumps=ti).state
         return
-
-    def __deepcopy__(self, memo):
-        cls = self.__class__
-        result = cls.__new__(cls)
-        memo[id(self)] = result
-        for k, v in self.__dict__.items():
-            setattr(result, k, deepcopy(v, memo))
-
-        #'bit_generator' kwarg has changed
-        super(MultiRNG, result).__init__(**result.kwargs)
-
-        return result
-
-
-class SingleRNG(np.random.Generator):
-    """
-    Dummy class that mirrors MultiRNG but provides the numpy singleton rng instead
-    """
-    
-    def __init__(self, name, seed_offset=None, **kwargs):
-        """
-        Create a random number generator
-
-        seed_offset will be automatically assigned (based on hashing the name) if None
-        
-        name: a name for this random number generator, like "coin_flip"
-        """
-
-        self.name = name
-        self.kwargs = kwargs
-
-        if seed_offset is None:
-            # Obtain the seed offset by hashing the class name. Don't use python's hash because it is randomized.
-            self.seed_offset = int(hashlib.sha256(self.name.encode('utf-8')).hexdigest(), 16) % 10**8
-        else:
-            # Use user-provided seed_offset (unlikely)
-            self.seed_offset = seed_offset
-
-        self.initialized = False
-        return
-
-    def initialize(self, container, slots=None):
-        if self.initialized:
-            return
-
-        if container is not None:
-            container.add(self) # base_seed + seed_offset
-
-        if 'bit_generator' not in self.kwargs:
-            self.kwargs['bit_generator'] = np.random.mtrand._rand._bit_generator
-        super().__init__(**self.kwargs)
-
-        self.initialized = True
-        return
-
-    def reset(self):
-        pass
-
-    def step(self, ti):
-        pass
diff --git a/starsim/settings.py b/starsim/settings.py
index c10fe2a4..46e59c9b 100644
--- a/starsim/settings.py
+++ b/starsim/settings.py
@@ -65,11 +65,8 @@ def get_orig_options():
         optdesc = sc.objdict()  # Help for the options
         options = sc.objdict()  # The options
 
-        optdesc.multirng = 'Set True to use multiple random number generators\
-            via the MultiRNG class, which is compatible with common random\
-            numbers. The default value is False, which instead uses a single\
-            centralized random number generator.'
-        options.multirng = False
+        optdesc.rng = 'Set how random numbers are handled in Starsim with three options: 1) "centralized" uses the centralized numpy random number generator for all distributions. 2) "single" uses a separate (SingleRNG) random number generator for each distribution. 3) "multi" uses a separate (MultiRNG) random number generator for each distribution. In comparing two simulations, the "single" option may be slightly better than the "centralized" option without any real disadvantages. Only "multi" can achieve full common random number (CRN) coherence, but is more computationally expensive and only produces CRN results when using CRN-safe code. The default value is "single".'
+        options.rng = ['centralized', 'single', 'multi'][1]
 
         optdesc.verbose = 'Set default level of verbosity (i.e. logging detail): e.g., 0.1 is an update every 10 simulated timesteps.'
         options.verbose = float(os.getenv('STARSIM_VERBOSE', 0.1))
@@ -217,6 +214,9 @@ def set_precision(self):
             raise ValueError(errormsg)
         return
 
+    @property
+    def multirng(self):
+        return self['rng'] == 'multi'
 
 # Create the options on module load
 options = Options()
diff --git a/starsim/version.py b/starsim/version.py
index 297eb325..b7034a0c 100644
--- a/starsim/version.py
+++ b/starsim/version.py
@@ -4,6 +4,6 @@
 
 __all__ = ['__version__', '__versiondate__', '__license__']
 
-__version__ = '0.2.8'
-__versiondate__ = '2024-03-13'
+__version__ = '0.2.9'
+__versiondate__ = '2024-03-18'
 __license__ = f'Starsim {__version__} ({__versiondate__}) — © 2023-2024 by IDM'


Test patch:
diff --git a/tests/baseline.json b/tests/baseline.json
index 060dd6f1..77ffb7fb 100644
--- a/tests/baseline.json
+++ b/tests/baseline.json
@@ -1,17 +1,17 @@
 {
   "summary": {
-    "n_alive": 9666.47619047619,
-    "new_deaths": 6.704761904761905,
-    "cum_deaths": 696.0,
+    "n_alive": 9661.44761904762,
+    "new_deaths": 7.076190476190476,
+    "cum_deaths": 736.0,
     "pregnancy_pregnancies": 0.0,
     "pregnancy_births": 0.0,
     "pregnancy_cbr": 0.0,
     "hiv_n_on_art": 0.0,
-    "hiv_n_susceptible": 8918.485714285714,
-    "hiv_n_infected": 747.9904761904762,
-    "hiv_prevalence": 0.0774374080994773,
-    "hiv_new_infections": 14.152380952380952,
-    "hiv_cum_infections": 1484.0,
-    "hiv_new_deaths": 6.704761904761905
+    "hiv_n_susceptible": 8881.438095238096,
+    "hiv_n_infected": 780.0095238095238,
+    "hiv_prevalence": 0.08081250879044433,
+    "hiv_new_infections": 14.723809523809523,
+    "hiv_cum_infections": 1540.0,
+    "hiv_new_deaths": 7.076190476190476
   }
 }
\ No newline at end of file
diff --git a/tests/benchmark.json b/tests/benchmark.json
index 6e055a13..dcb0209c 100644
--- a/tests/benchmark.json
+++ b/tests/benchmark.json
@@ -1,13 +1,13 @@
 {
   "time": {
-    "people": 0.002,
-    "initialize": 0.016,
-    "run": 0.244
+    "people": 0.003,
+    "initialize": 0.014,
+    "run": 0.168
   },
   "parameters": {
     "n_agents": 10000,
     "n_years": 20,
     "dt": 0.2
   },
-  "cpu_performance": 0.2697182573649721
+  "cpu_performance": 0.7758325385274683
 }
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
index 19ee91d8..161b190e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -4,12 +4,9 @@
 import pytest
 
 # Run all tests with both single and multi RNG streams
-@pytest.fixture(params=['single','multi'], autouse=True)
+@pytest.fixture(params=['centralized', 'single', 'multi'], autouse=True)
 def set_rng_type(request):
-    original_rng_type = ss.options.multirng
-    if request.param == 'single':
-        ss.options.multirng = False
-    else:
-        ss.options.multirng = True
+    original_rng_type = ss.options.rng
+    ss.options.rng = request.param
     yield
-    ss.options.multirng = original_rng_type
\ No newline at end of file
+    ss.options.rng = original_rng_type
\ No newline at end of file
diff --git a/tests/devtests/devtest_rng.py b/tests/devtests/devtest_rng.py
index c132d7f1..ee30f10f 100644
--- a/tests/devtests/devtest_rng.py
+++ b/tests/devtests/devtest_rng.py
@@ -1,5 +1,5 @@
 """
-Exploring singlerng vs multirng for a model HIV system sweeping coverage of ART.
+Exploring singlerng vs rng for a model HIV system sweeping coverage of ART.
 """
 
 # %% Imports and settings
@@ -16,6 +16,8 @@
 warnings.filterwarnings("ignore", "is_categorical_dtype")
 warnings.filterwarnings("ignore", "use_inf_as_na")
 
+rngs = ['centralized', 'single', 'multi']
+
 n = 100 # Agents
 n_rand_seeds = 25
 intv_cov_levels = [0.01, 0.10, 0.25, 0.73] + [0] # Must include 0 as that's the baseline
@@ -23,9 +25,9 @@
 figdir = os.path.join(os.getcwd(), 'figs', 'ART')
 sc.path(figdir).mkdir(parents=True, exist_ok=True)
 
-def run_sim(n, idx, intv_cov, rand_seed, multirng):
+def run_sim(n, idx, intv_cov, rand_seed, rng):
 
-    print(f'Starting sim {idx} with rand_seed={rand_seed} and intv_cov={intv_cov}, multirng={multirng}')
+    print(f'Starting sim {idx} with rand_seed={rand_seed} and intv_cov={intv_cov}, rng={rng}')
 
     ppl = ss.People(n)
 
@@ -56,7 +58,8 @@ def run_sim(n, idx, intv_cov, rand_seed, multirng):
     if intv_cov > 0:
         pars['interventions'] = [ ss.hiv.ART(year=[2004, 2020], coverage=[0, intv_cov]) ]
 
-    sim = ss.Sim(people=ppl, networks=networks, diseases=[hiv], demographics=[pregnancy, deaths], pars=pars, label=f'Sim with {n} agents and intv_cov={intv_cov}')
+    sim = ss.Sim(people=ppl, networks=networks, diseases=[hiv], demographics=[pregnancy, deaths], pars=pars,
+            label=f'Sim with {n} agents and intv_cov={intv_cov}')
     sim.initialize()
     sim.run()
 
@@ -69,24 +72,24 @@ def run_sim(n, idx, intv_cov, rand_seed, multirng):
     })
     df['intv_cov'] = intv_cov
     df['rand_seed'] = rand_seed
-    df['multirng'] = multirng
+    df['rng'] = rng
 
-    print(f'Finishing sim {idx} with rand_seed={rand_seed} and intv_cov={intv_cov}, multirng={multirng}')
+    print(f'Finishing sim {idx} with rand_seed={rand_seed} and intv_cov={intv_cov}, rng={rng}')
 
     return df
 
 def run_scenarios():
     results = []
     times = {}
-    for multirng in [True, False]:
-        ss.options(multirng=multirng)
+    for rng in rngs:
+        ss.options(rng=rng)
         cfgs = []
         for rs in range(n_rand_seeds):
             for intv_cov in intv_cov_levels:
-                cfgs.append({'intv_cov':intv_cov, 'rand_seed':rs, 'multirng':multirng, 'idx':len(cfgs)})
+                cfgs.append({'intv_cov':intv_cov, 'rand_seed':rs, 'rng':rng, 'idx':len(cfgs)})
         T = sc.tic()
         results += sc.parallelize(run_sim, kwargs={'n': n}, iterkwargs=cfgs, die=True, serial=False)
-        times[f'multirng={multirng}'] = sc.toc(T, output=True)
+        times[f'rng={rng}'] = sc.toc(T, output=True)
 
     print('Timings:', times)
 
@@ -95,41 +98,41 @@ def run_scenarios():
     return df
 
 def plot_scenarios(df):
-    d = pd.melt(df, id_vars=['year', 'rand_seed', 'intv_cov', 'multirng'], var_name='channel', value_name='Value')
+    d = pd.melt(df, id_vars=['year', 'rand_seed', 'intv_cov', 'rng'], var_name='channel', value_name='Value')
     d['baseline'] = d['intv_cov']==0
     bl = d.loc[d['baseline']]
     scn = d.loc[~d['baseline']]
-    bl = bl.set_index(['year', 'channel', 'rand_seed', 'intv_cov', 'multirng'])[['Value']].reset_index('intv_cov')
-    scn = scn.set_index(['year', 'channel', 'rand_seed', 'intv_cov', 'multirng'])[['Value']].reset_index('intv_cov')
-    mrg = scn.merge(bl, on=['year', 'channel', 'rand_seed', 'multirng'], suffixes=('', '_ref'))
+    bl = bl.set_index(['year', 'channel', 'rand_seed', 'intv_cov', 'rng'])[['Value']].reset_index('intv_cov')
+    scn = scn.set_index(['year', 'channel', 'rand_seed', 'intv_cov', 'rng'])[['Value']].reset_index('intv_cov')
+    mrg = scn.merge(bl, on=['year', 'channel', 'rand_seed', 'rng'], suffixes=('', '_ref'))
     mrg['Value - Reference'] = mrg['Value'] - mrg['Value_ref']
     mrg = mrg.sort_index()
 
     fkw = {'sharey': False, 'sharex': 'col', 'margin_titles': True}
 
     ## TIMESERIES
-    g = sns.relplot(kind='line', data=d, x='year', y='Value', hue='intv_cov', col='channel', row='multirng',
+    g = sns.relplot(kind='line', data=d, x='year', y='Value', hue='intv_cov', col='channel', row='rng', row_order=rngs,
         height=5, aspect=1.2, palette='Set1', errorbar='sd', lw=2, facet_kws=fkw)
-    g.set_titles(col_template='{col_name}', row_template='multirng: {row_name}')
+    g.set_titles(col_template='{col_name}', row_template='rng: {row_name}')
     g.set_xlabels(r'$t_i$')
-    g.fig.savefig(os.path.join(figdir, 'timeseries.png'), bbox_inches='tight', dpi=300)
+    g.figure.savefig(os.path.join(figdir, 'timeseries.png'), bbox_inches='tight', dpi=300)
 
     ## DIFF TIMESERIES
-    for ms, mrg_by_ms in mrg.groupby('multirng'):
-        g = sns.relplot(kind='line', data=mrg_by_ms, x='year', y='Value - Reference', hue='intv_cov', col='channel', row='intv_cov',
-            height=3, aspect=1.0, palette='Set1', estimator=None, units='rand_seed', lw=0.5, facet_kws=fkw) #errorbar='sd', lw=2, 
+    for ms, mrg_by_ms in mrg.groupby('rng'):
+        g = sns.relplot(kind='line', data=mrg_by_ms, x='year', y='Value - Reference', hue='intv_cov', col='channel',
+                row='intv_cov', height=3, aspect=1.0, palette='Set1', estimator=None, units='rand_seed', lw=0.5, facet_kws=fkw) #errorbar='sd', lw=2, 
         g.set_titles(col_template='{col_name}', row_template='Coverage: {row_name}')
-        g.fig.suptitle('MultiRNG' if ms else 'SingleRNG')
-        g.fig.subplots_adjust(top=0.88)
+        #g.figure.suptitle('MultiRNG' if ms else 'SingleRNG')
+        g.figure.subplots_adjust(top=0.88)
         g.set_xlabels('Year')
-        g.fig.savefig(os.path.join(figdir, 'diff_multi.png' if ms else 'diff_single.png'), bbox_inches='tight', dpi=300)
+        g.figure.savefig(os.path.join(figdir, f'diff_{ms}.png'), bbox_inches='tight', dpi=300)
 
     ## FINAL TIME
     tf = df['year'].max()
     mtf = mrg.loc[tf]
-    g = sns.displot(data=mtf.reset_index(), kind='kde', fill=True, rug=True, cut=0, hue='intv_cov', x='Value - Reference', col='channel', row='multirng',
-        height=5, aspect=1.2, facet_kws=fkw, palette='Set1')
-    g.set_titles(col_template='{col_name}', row_template='multirng: {row_name}')
+    g = sns.displot(data=mtf.reset_index(), kind='kde', fill=True, rug=True, cut=0, hue='intv_cov', x='Value - Reference',
+            col='channel', row='rng', row_order=rngs, height=5, aspect=1.2, facet_kws=fkw, palette='Set1')
+    g.set_titles(col_template='{col_name}', row_template='rng: {row_name}')
     g.set_xlabels(f'Value - Reference at year {tf}')
     g.fig.savefig(os.path.join(figdir, 'final.png'), bbox_inches='tight', dpi=300)
 
@@ -153,5 +156,4 @@ def plot_scenarios(df):
         df = run_scenarios()
 
     plot_scenarios(df)
-
-    print('Done')
\ No newline at end of file
+    print('Done')
diff --git a/tests/test_baselines.py b/tests/test_baselines.py
index f9db1a49..95a9c088 100644
--- a/tests/test_baselines.py
+++ b/tests/test_baselines.py
@@ -6,6 +6,7 @@
 import numpy as np
 import sciris as sc
 import starsim as ss
+import pytest
 
 do_plot = True
 do_save = False
@@ -80,8 +81,8 @@ def test_baseline():
     ''' Compare the current default sim against the saved baseline '''
     
     # Do not run with multi-RNG
-    if ss.options.multirng:
-        return
+    if ss.options.rng != 'single':
+        pytest.skip('This test is only for the single rng.')
 
     # Load existing baseline
     baseline = sc.loadjson(baseline_filename)
diff --git a/tests/test_distributions.py b/tests/test_distributions.py
index 64a8ff0e..0bb55377 100644
--- a/tests/test_distributions.py
+++ b/tests/test_distributions.py
@@ -68,7 +68,7 @@ def test_uniform_scalar_str(n):
 
     dist = sps.uniform(loc=1, scale=4)
     d = ScipyDistribution(dist, 'Uniform') # String here!
-    if ss.options.multirng:
+    if ss.options.rng in ['single', 'multi']:
         d.rng.initialize(container=None, slots=n) # Only really needed for testing as initializing the distribution will do something similar.
 
     uids = np.array([1,3])
diff --git a/tests/test_random.py b/tests/test_random.py
index fb54842d..85e2b988 100644
--- a/tests/test_random.py
+++ b/tests/test_random.py
@@ -19,7 +19,8 @@ def make_rng(slots=5, base_seed=1, name='Test', **kwargs):
     rng_container = ss.RNGContainer()
     rng_container.initialize(base_seed=base_seed)
     rng = ss.RNG(name, **kwargs)
-    rng.initialize(rng_container, slots=slots)
+    if ss.options.rng in ['single', 'multi']:
+        rng.initialize(rng_container, slots=slots)
     return rng
 
 
@@ -35,8 +36,8 @@ def test_random(rng, n=5):
 
 def test_SingleRNG_using_np_singleton(rng, n=5):
     """ Make sure the SingleRNG is using the numpy.random singleton """
-    if ss.options.multirng:
-        pytest.skip('Skipping multirng mode')
+    if ss.options.rng != 'centralized':
+        pytest.skip('This test is only for the centralized rng.')
     sc.heading('test_singleton_rng: Testing RNG object')
 
     np.random.seed(0)
@@ -49,6 +50,8 @@ def test_SingleRNG_using_np_singleton(rng, n=5):
 
 def test_reset(rng, n=5):
     """ Sample, reset, sample """
+    if ss.options.rng not in ['single', 'multi']:
+        pytest.skip('Reset is only for SingleRNG and MultiRNG.')
     sc.heading('test_reset: Testing sample, reset, sample')
 
     draws1 = rng.random(n)
@@ -69,6 +72,8 @@ def test_reset(rng, n=5):
 
 def test_step(rng, n=5):
     """ Sample, step, sample """
+    if ss.options.rng not in ['single', 'multi']:
+        pytest.skip('Step only for SingleRNG and MultiRNG.')
     sc.heading('test_step: Testing sample, step, sample')
 
     draws1 = rng.random(n)
diff --git a/tests/test_rngcontainer.py b/tests/test_rngcontainer.py
index e4a4763f..20ba712c 100644
--- a/tests/test_rngcontainer.py
+++ b/tests/test_rngcontainer.py
@@ -39,7 +39,8 @@ def test_rng(rng_container, rngs, n=5):
     sc.heading('test_rng: Testing RNGContainer object')
 
     rng0 = rngs[0]
-    rng0.initialize(rng_container, slots=n)
+    if ss.options.rng in ['single', 'multi']:
+        rng0.initialize(rng_container, slots=n)
 
     draws = rng0.random(n)
     print(f'Created seed and sampled: {draws}')
@@ -86,7 +87,7 @@ def test_reset(rng_container, dists, n=5):
     sc.heading('test_reset: Testing sample, reset, sample')
 
     d0 = dists[0]
-    if ss.options.multirng:
+    if ss.options.rng in ['single', 'multi']:
         d0.rng.initialize(rng_container, slots=n)
 
     uids = np.arange(0,n,2) # every other to make it interesting
@@ -110,7 +111,7 @@ def test_step(rng_container, dists, n=5):
     sc.heading('test_step: Testing sample, step, sample')
 
     d0 = dists[0]
-    if ss.options.multirng:
+    if ss.options.rng in ['single', 'multi']:
         d0.rng.initialize(rng_container, slots=n)
 
     uids = np.arange(0,n,2) # every other to make it interesting
@@ -128,6 +129,8 @@ def test_step(rng_container, dists, n=5):
 
 def test_initialize(rngs, n=5):
     """ Sample without initializing, should raise exception """
+    if ss.options.rng not in ['single', 'multi']:
+        pytest.skip('Initialize only for SingleRNG and MultiRNG.')
     sc.heading('test_initialize: Testing without initializing, should raise exception.')
     rng_container = ss.RNGContainer()
     #rng_container.initialize(base_seed=3) # Do not initialize
@@ -157,7 +160,7 @@ def test_samplingorder(rng_container, dists, n=5):
     uids = np.arange(0,n,2) # every other to make it interesting
 
     d0, d1 = dists
-    if ss.options.multirng:
+    if ss.options.rng in ['single', 'multi']:
         d0.rng.initialize(rng_container, slots=n)
         d1.rng.initialize(rng_container, slots=n)
 
@@ -183,6 +186,8 @@ def test_samplingorder(rng_container, dists, n=5):
 
 def test_repeatname(rng_container, n=5):
     """ Test two random number generators with the same name """
+    if ss.options.rng not in ['single', 'multi']:
+        pytest.skip('Repeate name is only for SingleRNG and MultiRNG.')
     sc.heading('test_repeatname: Testing if two random number generators with the same name are allowed')
 
     rng0 = ss.RNG('test')