diff --git a/atomica/framework.py b/atomica/framework.py
index 7c0338b8..7e4c5ddc 100644
--- a/atomica/framework.py
+++ b/atomica/framework.py
@@ -1119,7 +1119,7 @@ class ProjectFramework(object):
                     message = 'No compartments or characteristics appear in the databook, which means it is not possible to initialize the simulation. Please assign at least some of the compartments and/or characteristics to a databook page.'
                 else:
                     message = 'No compartments or characteristics have a setup weight (either because they do not appear in the databook, or the setup weight has been explicitly set to zero) - cannot initialize simulation. Please change some of the setup weights to be nonzero'
-                raise Exception(message)
+                logger.debug(message)
 
             A = np.zeros((len(characs), len(comps)))
             for i, charac in enumerate(characs):
diff --git a/atomica/programs.py b/atomica/programs.py
index f809645a..7d329a31 100644
--- a/atomica/programs.py
+++ b/atomica/programs.py
@@ -129,7 +129,7 @@ class ProgramInstructions(object):
         for ts in new.alloc.values():
             ts.vals = [x * scale_factor for x in ts.vals]
             ts.assumption = ts.assumption * scale_factor if ts.assumption is not None else None
-            ts.uncertainty = ts.sigma * scale_factor if ts.sigma is not None else None
+            ts.sigma = ts.sigma * scale_factor if ts.sigma is not None else None
         return new
 
 
diff --git a/atomica/utils.py b/atomica/utils.py
index e84314c4..8c453363 100644
--- a/atomica/utils.py
+++ b/atomica/utils.py
@@ -11,7 +11,7 @@ import os
 import re
 import time
 import zlib
-from bisect import bisect
+from bisect import bisect_right, bisect_left
 from datetime import datetime
 from functools import partial
 from pathlib import Path
@@ -131,11 +131,11 @@ class TimeSeries(object):
 
     """
 
-    def __init__(self, t=None, vals=None, units: str = None, assumption: float = None, sigma: float = None):
-        t = sc.promotetolist(t) if t is not None else list()
-        vals = sc.promotetolist(vals) if vals is not None else list()
+    # Use slots here to guarantee that __deepcopy__() and __eq__() only have to check these
+    # specific fields - otherwise, would need to do a more complex recursive dict comparison
+    __slots__ = ['t', 'vals', 'units', 'assumption', 'sigma', '_sampled']
 
-        assert len(t) == len(vals)
+    def __init__(self, t=None, vals=None, units: str = None, assumption: float = None, sigma: float = None):
 
         self.t = []  #: Sorted array of time points. Normally interacted with via methods like :meth:`insert()`
         self.vals = []  #: Time-specific values - indices correspond to ``self.t``
@@ -144,13 +144,28 @@ class TimeSeries(object):
         self.sigma = sigma  #: Uncertainty value, assumed to be a standard deviation
         self._sampled = False  #: Flag to indicate whether sampling has been performed. Once sampling has been performed, cannot sample again
 
-        for tx, vx in zip(t, vals):
-            self.insert(tx, vx)
+        # Using insert() means that array/list inputs containing None or duplicate entries will
+        # be sanitized via insert()
+        self.insert(t, vals)
 
     def __repr__(self):
         output = sc.prepr(self)
         return output
 
+    def __eq__(self, other):
+        """
+        Check TimeSeries equality
+
+        Two TimeSeries instances are equal if all of their attributes are equal. This is easy to
+        implement because `==` is directly defined for all of the attribute types (lists and scalars)
+        and due to `__slots__` there are guaranteed not to be any other attributes
+
+        :param other:
+        :return:
+        """
+
+        return all(getattr(self, x) == getattr(other, x) for x in self.__slots__)
+
     def __deepcopy__(self, memodict={}):
         new = TimeSeries.__new__(TimeSeries)
         new.t = self.t.copy()
@@ -161,6 +176,24 @@ class TimeSeries(object):
         new._sampled = self._sampled
         return new
 
+    def __getstate__(self):
+        return dict([(k, getattr(self,k,None)) for k in self.__slots__])
+
+    def __setstate__(self, data):
+
+        if 'format' in data:
+            # 'format' was changed to 'units' but the attribute was not dropped, however now this is a
+            # hard error because of the switch to __slots__ so we need to make sure it gets removed.
+            # This can't be done as a Migration because a Migration expects an instance of the
+            data = sc.dcp(data)
+            if 'units' not in data:
+                data['units'] = data['format']
+            del data['format']
+
+        for k, v in data.items():
+            setattr(self,k,v)
+
+
     # The operators for + and * below are prototypes but haven't been added
     # The reason is because they add a lot of complexity to the usage of the class
     # but it's not clear whether that complexity is worth it for the benefits it brings.
@@ -329,25 +362,36 @@ class TimeSeries(object):
         :param v: Value to insert. If ``None``, this function will return immediately without doing anything
 
         """
-        if isinstance(t, list):
-            assert isinstance(v, list) and len(t) == len(v), 'Cannot insert non-matching lengths or types  of time and values %s and %s' % (t, v)
+
+        # Check if inputs are iterable
+        iterable_input = True
+        try:
+            assert len(t) == len(v),  'Cannot insert non-matching lengths or types of time and values %s and %s' % (t, v)
+        except TypeError:
+            iterable_input = False
+
+        # If inputs are iterable, call insert() for each zipped item
+        if iterable_input:
             for ti, vi in zip(t, v):
                 self.insert(ti, vi)
-        else:
-            if v is None:  # Can't cast a None to a float, just skip it
-                return
+            return
 
-            v = float(v)  # Convert input to float
+        if v is None:  # Can't cast a None to a float, so just skip it
+            return
 
-            if t is None:
-                self.assumption = v
-            elif t in self.t:
-                idx = self.t.index(t)
-                self.vals[idx] = v
-            else:
-                idx = bisect(self.t, t)
-                self.t.insert(idx, t)
-                self.vals.insert(idx, v)
+        v = float(v)  # Convert input to float
+
+        if t is None: # Store the value in the assumption
+            self.assumption = v
+            return
+
+        idx = bisect_left(self.t, t)
+        if idx < len(self.t) and self.t[idx] == t:
+            # Overwrite an existing entry
+            self.vals[idx] = v
+        else:
+            self.t.insert(idx, t)
+            self.vals.insert(idx, v)
 
     def get(self, t) -> float:
         """
diff --git a/atomica/version.py b/atomica/version.py
index 892d4b4c..d5a338b9 100644
--- a/atomica/version.py
+++ b/atomica/version.py
@@ -5,6 +5,6 @@ Standard location for module version number and date.
 """
 
 from .utils import fast_gitinfo
-version = "1.21.0"
+version = "1.21.1"
 versiondate = "2020-07-06"
 gitinfo = fast_gitinfo(__file__)
diff --git a/requirements.txt b/requirements.txt
index 869c3c37..7e0fbe0c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 # Dependencies for testing/documentation
-pytest
+pytest<6  # Pytest 6 breaks nbsmoke and nbval runs each cell as a separate test
 pytest-xdist
 pytest-env
 pytest-cov

TimeSeries should deal better with arrays
Under some conditions, arrays need to be converted to lists in order to be added - better to remove this limitation
diff --git a/tests/test_tox_timeseries.py b/tests/test_tox_timeseries.py
new file mode 100644
index 00000000..10ee002e
--- /dev/null
+++ b/tests/test_tox_timeseries.py
@@ -0,0 +1,97 @@
+# Test timeseries utility object
+
+import atomica as at
+import numpy as np
+
+def test_printing():
+    a = at.TimeSeries([1], [5])
+    print(a)
+
+
+def test_equality():
+    # Two time series should be equal if their values are equal
+    a = at.TimeSeries()
+    b = at.TimeSeries()
+    assert a == b
+
+    a.insert(None, 1)
+    assert a != b
+
+    b.insert(None, 1)
+    assert a == b
+
+    a = at.TimeSeries([1, 2, 3], [1, 2, 3])
+    b = at.TimeSeries([1, 2, 3], [1, 2, 3])
+    c = at.TimeSeries([1, 2, 3, 4], [1, 2, 3, 4])
+    assert a == b
+    assert a != c
+    a.insert(None, 1)
+    assert a != b
+
+
+def test_constructor():
+    a = at.TimeSeries()
+    a.insert(1,1)
+    a.insert(2,2)
+    a.insert(None,0)
+    a.insert(3,3)
+
+    b = at.TimeSeries([None,1,2,3],[0,1,2,3])
+    assert a == b
+
+    c = at.TimeSeries(np.array([None,1,2,3]),np.array([0,1,2,3]))
+    assert a == c
+
+    a.insert(np.array([2,3]),[4,5])
+    c.insert(2,4)
+    c.insert(3,5)
+    assert a == c
+
+    d = at.TimeSeries(range(3),[0,1,4])
+    assert a != d
+    d.remove(0)
+    d.insert(3,5)
+    d.insert(None,0)
+    assert a == d
+
+
+def test_insert_sorting():
+    a = at.TimeSeries([1,2],[1,2])
+
+    # Insert at end
+    a.insert(3,3)
+    assert a.t == [1,2,3]
+    assert a.vals == [1,2,3]
+
+    # Insert in middle
+    a.insert(2.5,2.5)
+    assert a.t == [1,2,2.5,3]
+    assert a.vals == [1,2,2.5,3]
+
+    # Insert at start
+    a.insert(0,0)
+    assert a.t == [0,1,2,2.5,3]
+    assert a.vals == [0,1,2,2.5,3]
+
+    # Overwrite existing at end
+    a.insert(3,4)
+    assert a.t == [0,1,2,2.5,3]
+    assert a.vals == [0,1,2,2.5,4]
+
+    # Overwrite existing at start
+    a.insert(0, 4)
+    assert a.t == [0, 1, 2, 2.5, 3]
+    assert a.vals == [4, 1, 2, 2.5, 4]
+
+    # Overwrite existing in middle
+    a.insert(1, 4)
+    assert a.t == [0, 1, 2, 2.5, 3]
+    assert a.vals == [4, 4, 2, 2.5, 4]
+
+
+if __name__ == '__main__':
+
+    test_constructor()
+    test_printing()
+    test_equality()
+    test_insert_sorting()
\ No newline at end of file
diff --git a/tests/validate_integration.py b/tests/validate_integration.py
index 4381516a..01cb2fa5 100644
--- a/tests/validate_integration.py
+++ b/tests/validate_integration.py
@@ -66,7 +66,7 @@ def test_validate_model(model):
     P1.run_sim(P1.parsets[0], result_name='parset', store_results=True)
     P1.run_sim(P1.parsets[0], P1.progsets[0], at.ProgramInstructions(start_year=2018), result_name='progset', store_results=True)
 
-    fname = tmpdir / 'validation_' + model + '.prj'
+    fname = tmpdir / ('validation_' + model + '.prj')
     if os.path.isfile(fname):
         P2 = at.Project.load(fname)
         print('Validating %s parset' % (model))

pip install -e .
parse_log_pytest
requirements.txt
apt-get update apt-get install -y gcc
3.8
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
asttokens==3.0.0
-e git+https://github.com/atomicateam/atomica.git@213ecfc4210dd8111877bd51201596ea0207e51e#egg=atomica
attrs==25.3.0
babel==2.17.0
backcall==0.2.0
beautifulsoup4==4.13.3
bleach==6.1.0
certifi==2025.1.31
charset-normalizer==3.4.1
cloudpickle==3.1.1
comm==0.2.2
commonmark==0.9.1
contourpy==1.1.1
coverage==7.6.1
cycler==0.12.1
debugpy==1.8.13
decorator==5.2.1
defusedxml==0.7.1
dill==0.3.9
docutils==0.20.1
et_xmlfile==2.0.0
exceptiongroup==1.2.2
execnet==2.1.1
executing==2.2.0
fastjsonschema==2.21.1
fonttools==4.56.0
future==1.0.0
gitdb==4.0.12
GitPython==3.1.44
hyperopt==0.2.7
idna==3.10
imagesize==1.4.1
importlib_metadata==8.5.0
importlib_resources==6.4.5
iniconfig==2.1.0
ipykernel==6.29.5
ipython==8.12.3
jdcal==1.4.1
jedi==0.19.2
jellyfish==1.1.3
Jinja2==3.1.6
jsonpickle==4.0.5
jsonschema==4.23.0
jsonschema-specifications==2023.12.1
jupyter_client==8.6.3
jupyter_core==5.7.2
jupyterlab_pygments==0.3.0
kiwisolver==1.4.7
line_profiler==4.2.0
Markdown==3.7
MarkupSafe==2.1.5
matplotlib==3.7.5
matplotlib-inline==0.1.7
memory-profiler==0.61.0
mistune==3.1.3
mpld3 @ git+https://github.com/sciris/mpld3@602753a85660009afe5ba8b1b0c29c0227456a1d
multiprocess==0.70.17
nbclient==0.10.1
nbconvert==7.16.6
nbformat==5.10.4
nbsmoke==0.6.0
nbsphinx==0.9.7
nest-asyncio==1.6.0
networkx==3.1
numpy==1.24.4
openpyxl==2.5.14
packaging==24.2
pandas==2.0.3
pandoc==2.4
pandocfilters==1.5.1
param==2.1.1
parso==0.8.4
pexpect==4.9.0
pickleshare==0.7.5
pillow==10.4.0
pkgutil_resolve_name==1.3.10
platformdirs==4.3.6
pluggy==1.5.0
plumbum==1.9.0
ply==3.11
prompt_toolkit==3.0.50
psutil==7.0.0
ptyprocess==0.7.0
pure_eval==0.2.3
py4j==0.10.9.9
pyflakes==3.2.0
Pygments==2.19.1
pypandoc==1.15
pyparsing==3.1.4
pyswarm==0.6
pytest==8.3.5
pytest-cov==5.0.0
pytest-env==1.1.5
pytest-xdist==3.6.1
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.2
pyzmq==26.3.0
recommonmark==0.7.1
referencing==0.35.1
requests==2.32.3
rpds-py==0.20.1
scipy==1.10.1
sciris==3.2.0
six==1.17.0
smmap==5.0.2
snowballstemmer==2.2.0
soupsieve==2.6
Sphinx==7.1.2
sphinx-autodoc-typehints==2.0.1
sphinx-markdown-tables==0.0.17
sphinx-rtd-theme==3.0.2
sphinxcontrib-applehelp==1.0.4
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.1
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
stack-data==0.6.3
tinycss2==1.2.1
tomli==2.2.1
tornado==6.4.2
tqdm==4.67.1
traitlets==5.14.3
typing_extensions==4.13.0
tzdata==2025.2
urllib3==2.2.3
wcwidth==0.2.13
webencodings==0.5.1
XlsxWriter==3.2.2
zipp==3.20.2
zstandard==0.23.0

name: atomica
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=24.2=py38h06a4308_0
  - python=3.8.20=he870216_0
  - readline=8.2=h5eee18b_0
  - setuptools=75.1.0=py38h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.44.0=py38h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - asttokens==3.0.0
      - attrs==25.3.0
      - babel==2.17.0
      - backcall==0.2.0
      - beautifulsoup4==4.13.3
      - bleach==6.1.0
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - cloudpickle==3.1.1
      - comm==0.2.2
      - commonmark==0.9.1
      - contourpy==1.1.1
      - coverage==7.6.1
      - cycler==0.12.1
      - debugpy==1.8.13
      - decorator==5.2.1
      - defusedxml==0.7.1
      - dill==0.3.9
      - docutils==0.20.1
      - et-xmlfile==2.0.0
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - executing==2.2.0
      - fastjsonschema==2.21.1
      - fonttools==4.56.0
      - future==1.0.0
      - gitdb==4.0.12
      - gitpython==3.1.44
      - hyperopt==0.2.7
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.5.0
      - importlib-resources==6.4.5
      - iniconfig==2.1.0
      - ipykernel==6.29.5
      - ipython==8.12.3
      - jdcal==1.4.1
      - jedi==0.19.2
      - jellyfish==1.1.3
      - jinja2==3.1.6
      - jsonpickle==4.0.5
      - jsonschema==4.23.0
      - jsonschema-specifications==2023.12.1
      - jupyter-client==8.6.3
      - jupyter-core==5.7.2
      - jupyterlab-pygments==0.3.0
      - kiwisolver==1.4.7
      - line-profiler==4.2.0
      - markdown==3.7
      - markupsafe==2.1.5
      - matplotlib==3.7.5
      - matplotlib-inline==0.1.7
      - memory-profiler==0.61.0
      - mistune==3.1.3
      - mpld3==0.4.12
      - multiprocess==0.70.17
      - nbclient==0.10.1
      - nbconvert==7.16.6
      - nbformat==5.10.4
      - nbsmoke==0.6.0
      - nbsphinx==0.9.7
      - nest-asyncio==1.6.0
      - networkx==3.1
      - numpy==1.24.4
      - openpyxl==2.5.14
      - packaging==24.2
      - pandas==2.0.3
      - pandoc==2.4
      - pandocfilters==1.5.1
      - param==2.1.1
      - parso==0.8.4
      - pexpect==4.9.0
      - pickleshare==0.7.5
      - pillow==10.4.0
      - pkgutil-resolve-name==1.3.10
      - platformdirs==4.3.6
      - pluggy==1.5.0
      - plumbum==1.9.0
      - ply==3.11
      - prompt-toolkit==3.0.50
      - psutil==7.0.0
      - ptyprocess==0.7.0
      - pure-eval==0.2.3
      - py4j==0.10.9.9
      - pyflakes==3.2.0
      - pygments==2.19.1
      - pypandoc==1.15
      - pyparsing==3.1.4
      - pyswarm==0.6
      - pytest==8.3.5
      - pytest-cov==5.0.0
      - pytest-env==1.1.5
      - pytest-xdist==3.6.1
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==6.0.2
      - pyzmq==26.3.0
      - recommonmark==0.7.1
      - referencing==0.35.1
      - requests==2.32.3
      - rpds-py==0.20.1
      - scipy==1.10.1
      - sciris==3.2.0
      - six==1.17.0
      - smmap==5.0.2
      - snowballstemmer==2.2.0
      - soupsieve==2.6
      - sphinx==7.1.2
      - sphinx-autodoc-typehints==2.0.1
      - sphinx-markdown-tables==0.0.17
      - sphinx-rtd-theme==3.0.2
      - sphinxcontrib-applehelp==1.0.4
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.1
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - stack-data==0.6.3
      - tinycss2==1.2.1
      - tomli==2.2.1
      - tornado==6.4.2
      - tqdm==4.67.1
      - traitlets==5.14.3
      - typing-extensions==4.13.0
      - tzdata==2025.2
      - urllib3==2.2.3
      - wcwidth==0.2.13
      - webencodings==0.5.1
      - xlsxwriter==3.2.2
      - zipp==3.20.2
      - zstandard==0.23.0
prefix: /opt/conda/envs/atomica

tests/test_tox_timeseries.py::test_constructor tests/test_tox_timeseries.py::test_equality
tests/validate_integration.py::test_validate_model[usdt] tests/validate_integration.py::test_validate_model[tb_simple] tests/validate_integration.py::test_validate_model[tb_simple_dyn] tests/validate_integration.py::test_validate_model[udt] tests/validate_integration.py::test_validate_model[tb] tests/validate_integration.py::test_validate_model[hypertension] tests/validate_integration.py::test_validate_model[udt_dyn] tests/validate_integration.py::test_validate_model[hiv] tests/validate_integration.py::test_validate_model[hypertension_dyn] tests/validate_integration.py::test_validate_model[hiv_dyn] tests/validate_integration.py::test_validate_model[combined] tests/validate_integration.py::test_validate_model[sir] tests/validate_integration.py::test_validate_model[diabetes] tests/validate_integration.py::test_validate_model[cervicalcancer]
tests/test_tox_timeseries.py::test_insert_sorting tests/test_tox_timeseries.py::test_printing tests/validate_integration.py::test_validate_model[dt] tests/validate_integration.py::test_validate_model[sir_vaccine] tests/validate_integration.py::test_validate_model[service]
atomica/programs.py atomica/version.py atomica/framework.py atomica/utils.py requirements.txt
atomica/programs.py atomica/version.py atomica/framework.py atomica/utils.py requirements.txt