diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml
index 7ef06084..3e28bca4 100644
--- a/.github/workflows/run-integration-tests.yml
+++ b/.github/workflows/run-integration-tests.yml
@@ -27,4 +27,4 @@ jobs:
python3 -m pip install coverage
- name: Run tests
- run: coverage run test/run_integration_tests.py
+ run: coverage run --source=spatialpy test/run_integration_tests.py
diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml
index cabdc656..cebbd7b8 100644
--- a/.github/workflows/run-unit-tests.yml
+++ b/.github/workflows/run-unit-tests.yml
@@ -25,4 +25,4 @@ jobs:
python3 -m pip install coverage
- name: Run tests
- run: coverage run test/run_unit_tests.py
+ run: coverage run --source=spatialpy test/run_unit_tests.py
diff --git a/run_coverage.sh b/run_coverage.sh
new file mode 100755
index 00000000..b566b0e3
--- /dev/null
+++ b/run_coverage.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+#pip3 install coverage
+#pip3 install python-libsbml
+
+coverage run --source=spatialpy test/run_unit_tests.py -m develop
+coverage run --source=spatialpy test/run_integration_tests.py -m develop
+coverage html
diff --git a/spatialpy/core/__init__.py b/spatialpy/core/__init__.py
index 1cd9ac9d..29087774 100644
--- a/spatialpy/core/__init__.py
+++ b/spatialpy/core/__init__.py
@@ -28,6 +28,7 @@
from .result import *
from .spatialpyerror import *
from .species import *
+from .timespan import TimeSpan
from .visualization import Visualization
from .vtkreader import *
diff --git a/spatialpy/core/model.py b/spatialpy/core/model.py
index 62b38ede..b35b6345 100644
--- a/spatialpy/core/model.py
+++ b/spatialpy/core/model.py
@@ -22,6 +22,7 @@
import numpy
import scipy
+from spatialpy.core.timespan import TimeSpan
from spatialpy.solvers.build_expression import BuildExpression
from spatialpy.core.spatialpyerror import ModelError
@@ -90,10 +91,6 @@ def __init__(self, name="spatialpy"):
######################
self.tspan = None
- self.timestep_size = None
- self.num_timesteps = None
- self.output_freq = None
- self.output_steps = None
######################
# Expression utility used by the solver
@@ -150,12 +147,6 @@ def __eq__(self, other):
self.listOfReactions == other.listOfReactions and \
self.name == other.name
- def __check_if_complete(self):
- if self.timestep_size is None or self.num_timesteps is None:
- raise ModelError("The model's timespan is not set. Use 'timespan()' or 'set_timesteps()'.")
- if self.domain is None:
- raise ModelError("The model's domain is not set. Use 'add_domain()'.")
-
def __problem_with_name(self, name):
if name in Model.reserved_names:
errmsg = f'Name "{name}" is unavailable. It is reserved for internal SpatialPy use. '
@@ -304,13 +295,13 @@ def compile_prep(self):
:raises ModelError: Timestep size exceeds output frequency or Model is missing a domain
"""
- if self.timestep_size is None:
- self.timestep_size = 1e-5
- if self.output_freq < self.timestep_size:
- raise ModelError("Timestep size exceeds output frequency.")
-
- self.__check_if_complete()
+ try:
+ self.tspan.validate(coverage="all")
+ except TimespanError as err:
+ raise ModelError(f"Failed to validate timespan. Reason given: {err}") from err
+ if self.domain is None:
+ raise ModelError("The model's domain is not set. Use 'add_domain()'.")
self.domain.compile_prep()
self.__update_diffusion_restrictions()
@@ -627,26 +618,10 @@ def set_timesteps(self, output_interval, num_steps, timestep_size=None):
:param timestep_size: Size of each timestep in seconds
:type timestep_size: float
-
- :raises ModelError: Incompatible combination of timestep_size and output_interval
"""
- if timestep_size is not None:
- self.timestep_size = timestep_size
- if self.timestep_size is None:
- self.timestep_size = output_interval
-
- self.output_freq = output_interval/self.timestep_size
- if self.output_freq < self.timestep_size:
- raise ModelError("Timestep size exceeds output frequency.")
-
- self.num_timesteps = math.ceil(num_steps * self.output_freq)
-
- # array of step numbers corresponding to the simulation times in the timespan
- output_steps = numpy.arange(0, self.num_timesteps + self.timestep_size, self.output_freq)
- self.output_steps = numpy.unique(numpy.round(output_steps).astype(int))
- self.tspan = numpy.zeros((self.output_steps.size), dtype=float)
- for i, step in enumerate(self.output_steps):
- self.tspan[i] = step*self.timestep_size
+ self.tspan = TimeSpan.arange(
+ output_interval, t=num_steps * output_interval, timestep_size=timestep_size
+ )
def timespan(self, time_span, timestep_size=None):
"""
@@ -658,17 +633,14 @@ def timespan(self, time_span, timestep_size=None):
:param timestep_size: Size of each timestep in seconds
:type timestep_size: float
-
- :raises ModelError: non-uniform timespan not supported
"""
- items_diff = numpy.diff(time_span)
- items = map(lambda x: round(x, 10), items_diff)
- isuniform = (len(set(items)) == 1)
-
- if isuniform:
- self.set_timesteps(items_diff[0], len(items_diff), timestep_size=timestep_size)
+ if isinstance(time_span, TimeSpan) or type(time_span).__name__ == "TimeSpan":
+ self.tspan = time_span
+ if timestep_size is not None:
+ self.tspan.timestep_size = timestep_size
+ self.tspan.validate(coverage="all")
else:
- raise ModelError("Only uniform timespans are supported")
+ self.tspan = TimeSpan(time_span, timestep_size=timestep_size)
def add_domain(self, domain):
"""
diff --git a/spatialpy/core/spatialpyerror.py b/spatialpy/core/spatialpyerror.py
index 6ad7d0f1..ecee0353 100644
--- a/spatialpy/core/spatialpyerror.py
+++ b/spatialpy/core/spatialpyerror.py
@@ -81,6 +81,11 @@ class SpeciesError(ModelError):
Class for exceptions in the species module.
"""
+class TimespanError(ModelError):
+ """
+ Class for exceptions in the timespan module.
+ """
+
# Result Exceptions
diff --git a/spatialpy/core/timespan.py b/spatialpy/core/timespan.py
new file mode 100644
index 00000000..714600b0
--- /dev/null
+++ b/spatialpy/core/timespan.py
@@ -0,0 +1,212 @@
+# SpatialPy is a Python 3 package for simulation of
+# spatial deterministic/stochastic reaction-diffusion-advection problems
+# Copyright (C) 2019 - 2022 SpatialPy developers.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU GENERAL PUBLIC LICENSE Version 3 as
+# published by the Free Software Foundation.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU GENERAL PUBLIC LICENSE Version 3 for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+import math
+import numpy as np
+from collections.abc import Iterator
+
+from .spatialpyerror import TimespanError
+
+class TimeSpan(Iterator):
+ """
+ Model timespan that describes the duration to run the simulation and at which timepoint to sample
+ the species populations during the simulation.
+
+ :param items: Evenly-spaced list of times at which to sample the species populations during the simulation.
+ Best to use the form np.linspace(, , )
+ :type items: list, tuple, range, or numpy.ndarray
+
+ :param timestep_size: Size of each timestep in seconds
+ :type timestep_size: int | float
+
+ :raises TimespanError: items is an invalid type.
+ """
+ def __init__(self, items, timestep_size=None):
+ if isinstance(items, (list, tuple, range)):
+ items = np.array(items)
+
+ self.validate(items=items, timestep_size=timestep_size)
+
+ if timestep_size is None:
+ timestep_size = items[1] - items[0]
+ self.timestep_size = timestep_size
+
+ items_diff = np.diff(items)
+ self._set_timesteps(items_diff[0], len(items_diff))
+
+ self.validate(coverage="initialized")
+
+ def __eq__(self, o):
+ return self.items.__eq__(o).all()
+
+ def __getitem__(self, key):
+ return self.items.__getitem__(key)
+
+ def __iter__(self):
+ return self.items.__iter__()
+
+ def __len__(self):
+ return self.items.__len__()
+
+ def __next__(self):
+ return self.items.__next__()
+
+ def __str__(self):
+ return self.items.__str__()
+
+ def _ipython_display_(self):
+ print(self)
+
+ def _set_timesteps(self, output_interval, num_steps):
+ if self.timestep_size is None:
+ self.timestep_size = output_interval
+
+ self.output_freq = output_interval / self.timestep_size
+ self.num_timesteps = math.ceil(num_steps * self.output_freq)
+
+ output_steps = np.arange(
+ 0, self.num_timesteps + self.timestep_size, self.output_freq
+ )
+ self.output_steps = np.unique(np.round(output_steps).astype(int))
+ self.items = np.zeros((self.output_steps.size), dtype=float)
+ for i, step in enumerate(self.output_steps):
+ self.items[i] = step * self.timestep_size
+
+ @classmethod
+ def linspace(cls, t=20, num_points=None, timestep_size=None):
+ """
+ Creates a timespan using the form np.linspace(0, , ).
+
+ :param t: End time for the simulation.
+ :type t: float | int
+
+ :param num_points: Number of sample points for the species populations during the simulation.
+ :type num_points: int
+
+ :param timestep_size: Size of each timestep in seconds
+ :type timestep_size: int | float
+
+ :returns: Timespan for the model.
+ :rtype: spatialpy.TimeSpan
+
+ :raises TimespanError: t or num_points are None, <= 0, or invalid type.
+ """
+ if t is None or not isinstance(t, (int, float)) or t <= 0:
+ raise TimespanError("t must be a positive float or int.")
+ if num_points is not None and (not isinstance(num_points, int) or num_points <= 0):
+ raise TimespanError("num_points must be a positive int.")
+
+ if num_points is None:
+ num_points = int(t / 0.05) + 1
+ items = np.linspace(0, t, num_points)
+ return cls(items, timestep_size=timestep_size)
+
+ @classmethod
+ def arange(cls, increment, t=20, timestep_size=None):
+ """
+ Creates a timespan using the form np.arange(0, , ).
+
+ :param increment: Distance between sample points for the species populations during the simulation.
+ :type increment: float | int
+
+ :param t: End time for the simulation.
+ :type t: float | int
+
+ :param timestep_size: Size of each timestep in seconds
+ :type timestep_size: int | float
+
+ :returns: Timespan for the model.
+ :rtype: spatialpy.TimeSpan
+
+ :raises TimespanError: t or increment are None, <= 0, or invalid type.
+ """
+ if t is None or not isinstance(t, (int, float)) or t <= 0:
+ raise TimespanError("t must be a positive floar or int.")
+ if not isinstance(increment, (float, int)) or increment <= 0:
+ raise TimespanError("increment must be a positive float or int.")
+
+ items = np.arange(0, t + increment, increment)
+ return cls(items, timestep_size=timestep_size)
+
+ def validate(self, items=None, timestep_size=None, coverage="build"):
+ """
+ Validate the models time span
+
+ :param timestep_size: Size of each timestep in seconds
+ :type timestep_size: int | float
+
+ :param coverage: The scope of attributes to validate. Set to an attribute name to restrict validation \
+ to a specific attribute.
+ :type coverage: str
+
+ :raises TimespanError: Timespan is an invalid type, empty, not uniform, contains a single \
+ repeated value, or contains a negative initial time.
+ """
+ if coverage in ("all", "build"):
+ if hasattr(self, "items") and items is None:
+ items = self.items
+
+ if not isinstance(items, np.ndarray):
+ if not isinstance(items, (list, tuple, range)):
+ raise TimespanError("Timespan must be of type: list, tuple, range, or numpy.ndarray.")
+ items = np.array(items)
+ if items is not None:
+ self.items = items
+
+ if len(items) == 0:
+ raise TimespanError("Timespans must contain values.")
+ if items[0] < 0:
+ raise TimespanError("Simulation must run from t=0 to end time (t must always be positive).")
+
+ first_diff = items[1] - items[0]
+ other_diff = items[2:] - items[1:-1]
+ isuniform = np.isclose(other_diff, first_diff).all()
+
+ if coverage == "build" and not isuniform:
+ raise TimespanError("StochKit only supports uniform timespans.")
+ if first_diff == 0 or np.count_nonzero(other_diff) != len(other_diff):
+ raise TimespanError("Timespan can't contain a single repeating value.")
+
+ if coverage in ("all", "build", "timestep_size"):
+ if hasattr(self, "timestep_size") and timestep_size is None:
+ timestep_size = self.timestep_size
+
+ if timestep_size is not None:
+ if not isinstance(timestep_size, (int, float)):
+ raise TimespanError("timestep_size must be of type int or float.")
+ if timestep_size <= 0:
+ raise TimespanError("timestep_size must be a positive value.")
+
+ if coverage in ("all", "initialized"):
+ if self.timestep_size is None:
+ raise TimespanError("timestep_size can't be None type.")
+ if self.output_freq is None:
+ raise TimespanError("output_freq can't be None type.")
+ if not isinstance(self.output_freq, (int, float)):
+ raise TimespanError("output_freq must be of type int or float.")
+ if self.output_freq < self.timestep_size:
+ raise TimespanError("timestep_size exceeds output_frequency.")
+ if self.num_timesteps is None:
+ raise TimespanError("num_timesteps can't be None type.")
+ if not isinstance(self.num_timesteps, int):
+ raise TimespanError("num_timesteps must be of type int.")
+ if self.num_timesteps <= 0:
+ raise TimespanError("num_timesteps must be a positive int.")
+ if self.output_steps is None:
+ raise TimespanError("output_steps can't be None type.")
+ if not isinstance(self.output_steps, (np.ndarray)):
+ raise TimespanError("output_steps must be of type numpy.ndarray.")
+ if self.items.size != self.output_steps.size:
+ raise TimespanError("output_steps must be the same size as items.")
diff --git a/spatialpy/solvers/solver.py b/spatialpy/solvers/solver.py
index a9f50240..87629349 100644
--- a/spatialpy/solvers/solver.py
+++ b/spatialpy/solvers/solver.py
@@ -291,7 +291,7 @@ def __get_next_output(self):
output_step = "unsigned int get_next_output(ParticleSystem* system)\n{\n"
output_step += "static int index = 0;\n"
output_step += "const std::vector output_steps = {"
- output_step += f"{', '.join(self.model.output_steps.astype(str).tolist())}"
+ output_step += f"{', '.join(self.model.tspan.output_steps.astype(str).tolist())}"
output_step += "};\nunsigned int next_step = output_steps[index];\n"
output_step += "index++;\n"
output_step += "return next_step;\n}\n"
@@ -386,8 +386,8 @@ def __get_system_config(self, num_types, num_chem_species, num_chem_rxns,
system_config += "system->stoch_rxn_propensity_functions = ALLOC_propensities();\n"
system_config += "system->species_names = input_species_names;\n"
- system_config += f"system->dt = {self.model.timestep_size};\n"
- system_config += f"system->nt = {self.model.num_timesteps};\n"
+ system_config += f"system->dt = {self.model.tspan.timestep_size};\n"
+ system_config += f"system->nt = {self.model.tspan.num_timesteps};\n"
if self.h is None:
self.h = self.model.domain.find_h()
if self.h == 0.0:
diff --git a/spatialpy/stochss/stochss_export.py b/spatialpy/stochss/stochss_export.py
index 45fbf571..7fe2c545 100644
--- a/spatialpy/stochss/stochss_export.py
+++ b/spatialpy/stochss/stochss_export.py
@@ -214,8 +214,8 @@ def export(model, path=None, return_stochss_model=False):
if path is None:
path = f"{model.name}.smdl"
- end_sim = model.num_timesteps * model.timestep_size
- time_step = model.output_freq * model.timestep_size
+ end_sim = model.tspan.num_timesteps * model.tspan.timestep_size
+ time_step = model.tspan.output_freq * model.tspan.timestep_size
s_model = {"is_spatial": True,
"defaultID": 1,
@@ -225,7 +225,7 @@ def export(model, path=None, return_stochss_model=False):
"modelSettings": {
"endSim": end_sim,
"timeStep": time_step,
- "timestepSize": model.timestep_size
+ "timestepSize": model.tspan.timestep_size
},
"species": [],
"initialConditions": [],
diff --git a/test/run_unit_tests.py b/test/run_unit_tests.py
index 48e360cc..d54c1fff 100755
--- a/test/run_unit_tests.py
+++ b/test/run_unit_tests.py
@@ -28,14 +28,16 @@
print('Running unit tests in develop mode. Appending repository directory to system path.')
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
- import test_species
- import test_parameter
- import test_reaction
+ from unit_tests import test_species
+ from unit_tests import test_parameter
+ from unit_tests import test_reaction
+ from unit_tests import test_timespan
modules = [
test_species,
test_parameter,
- test_reaction
+ test_reaction,
+ test_timespan
]
for module in modules:
diff --git a/test/unit_tests/__init__.py b/test/unit_tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/test/test_parameter.py b/test/unit_tests/test_parameter.py
similarity index 100%
rename from test/test_parameter.py
rename to test/unit_tests/test_parameter.py
diff --git a/test/test_reaction.py b/test/unit_tests/test_reaction.py
similarity index 100%
rename from test/test_reaction.py
rename to test/unit_tests/test_reaction.py
diff --git a/test/test_species.py b/test/unit_tests/test_species.py
similarity index 100%
rename from test/test_species.py
rename to test/unit_tests/test_species.py
diff --git a/test/unit_tests/test_timespan.py b/test/unit_tests/test_timespan.py
new file mode 100644
index 00000000..87e3bc5d
--- /dev/null
+++ b/test/unit_tests/test_timespan.py
@@ -0,0 +1,373 @@
+# SpatialPy is a Python 3 package for simulation of
+# spatial deterministic/stochastic reaction-diffusion-advection problems
+# Copyright (C) 2019 - 2022 SpatialPy developers.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU GENERAL PUBLIC LICENSE Version 3 as
+# published by the Free Software Foundation.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU GENERAL PUBLIC LICENSE Version 3 for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+import math
+import numpy
+import unittest
+
+from spatialpy.core.timespan import TimeSpan
+from spatialpy.core.spatialpyerror import TimespanError
+
+class TestTimeSpan(unittest.TestCase):
+ '''
+ ################################################################################################
+ Unit tests for spatialpy.TimeSpan.
+ ################################################################################################
+ '''
+ def setUp(self):
+ """ Setup a clean valid timespan for testing. """
+ self.tspan = TimeSpan.linspace(t=10, num_points=11, timestep_size=0.001)
+
+ def set_timesteps(self, tspan, timestep_size):
+ items_diff = numpy.diff(tspan)
+ output_interval = items_diff[0]
+ num_steps = len(items_diff)
+
+ output_freq = output_interval / timestep_size
+ num_timesteps = math.ceil(num_steps * output_freq)
+
+ output_steps = numpy.arange(
+ 0, num_timesteps + timestep_size, output_freq
+ )
+ output_steps = numpy.unique(numpy.round(output_steps).astype(int))
+ items = numpy.zeros((output_steps.size), dtype=float)
+ for i, step in enumerate(output_steps):
+ items[i] = step * timestep_size
+ return items
+
+ def test_constructor(self):
+ """ Test the TimeSpan constructor. """
+ test_tspan = self.set_timesteps(numpy.linspace(0, 20, 401), 0.001)
+ tspan = TimeSpan(numpy.linspace(0, 20, 401), timestep_size=0.001)
+ self.assertEqual(tspan, test_tspan)
+ self.assertEqual(tspan.timestep_size, 0.001)
+
+ def test_constructor__valid_data_structures(self):
+ """ Test the TimeSpan constructor with valid data structures. """
+ test_tspans = [
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
+ range(11)
+ ]
+ for raw_tspan in test_tspans:
+ with self.subTest(tspan=raw_tspan, tspan_type=type(raw_tspan)):
+ test_tspan = self.set_timesteps(numpy.array(raw_tspan), 0.001)
+ tspan = TimeSpan(raw_tspan, timestep_size=0.001)
+ self.assertEqual(tspan, test_tspan)
+
+ def test_constructor__invalid_items(self):
+ """ Test the TimeSpan constructor with an invalid data structure type. """
+ test_tspans = [
+ None, "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", 20, 50.5,
+ set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+ ]
+ for test_tspan in test_tspans:
+ with self.subTest(items=test_tspan):
+ with self.assertRaises(TimespanError):
+ TimeSpan(test_tspan, timestep_size=0.001)
+
+ def test_constructor__timestep_size_none(self):
+ """ Test the TimeSpan constructor when timestep_size is omitted or set to None. """
+ test_tspan = TimeSpan(numpy.linspace(0, 30, 301))
+ self.assertEqual(test_tspan.timestep_size, 0.1)
+
+ def test_constructor__invaild_timestep_size(self):
+ """ Test the TimeSpan constructor when timestep_size is of an invalid type. """
+ test_tsss = ["1", [0.001]]
+ for test_tss in test_tsss:
+ with self.subTest(timestep_size=test_tss):
+ with self.assertRaises(TimespanError):
+ TimeSpan(numpy.linspace(0, 30, 301), timestep_size=test_tss)
+
+ def test_constructor__invaild_timestep_size_value(self):
+ """ Test the TimeSpan constructor when timestep_size is an invalid value. """
+ test_tsss = [0, -0.5, -1, -5]
+ for test_tss in test_tsss:
+ with self.subTest(timestep_size=test_tss):
+ with self.assertRaises(TimespanError):
+ TimeSpan(numpy.linspace(0, 30, 301), timestep_size=test_tss)
+
+ def test_constructor__timestep_size_too_large(self):
+ """ Test the TimeSpan constructor when the timestep_size > the diff of points. """
+ with self.assertRaises(TimespanError):
+ TimeSpan(numpy.arange(0, 20.5, 0.5), timestep_size=1)
+
+ def test_linspace(self):
+ """ Test TimeSpan.linspace. """
+ tspan = TimeSpan.linspace(t=30, num_points=301, timestep_size=0.001)
+ test_tspan = self.set_timesteps(numpy.linspace(0, 30, 301), 0.001)
+ self.assertEqual(tspan, test_tspan)
+ self.assertEqual(tspan.timestep_size, 0.001)
+
+ def test_linspace__no_t(self):
+ """ Test TimeSpan.linspace without passing t. """
+ tspan = TimeSpan.linspace(num_points=201, timestep_size=0.001)
+ test_tspan = self.set_timesteps(numpy.linspace(0, 20, 201), 0.001)
+ self.assertEqual(tspan, test_tspan)
+
+ def test_linspace__invalid_t(self):
+ """ Test TimeSpan.linspace with invalid t. """
+ test_values = [None, "5", 0, -0.5, -1, -2, -5, -10, [20.5]]
+ for test_val in test_values:
+ with self.subTest(t=test_val):
+ with self.assertRaises(TimespanError):
+ TimeSpan.linspace(t=test_val, num_points=301, timestep_size=0.001)
+
+ def test_linspace__no_num_points(self):
+ """ Test TimeSpan.linspace without passing num_points. """
+ tspan = TimeSpan.linspace(t=30, timestep_size=0.001)
+ test_tspan = self.set_timesteps(numpy.linspace(0, 30, int(30 / 0.05) + 1), 0.001)
+ self.assertEqual(tspan, test_tspan)
+
+ def test_linspace__invalid_num_points(self):
+ """ Test TimeSpan.linspace with invalid num_points. """
+ test_values = ["5", 0, -1, -2, -5, -10, 4.5, [40]]
+ for test_val in test_values:
+ with self.subTest(num_points=test_val):
+ with self.assertRaises(TimespanError):
+ TimeSpan.linspace(t=30, num_points=test_val, timestep_size=0.001)
+
+ def test_linspace__timestep_size_none(self):
+ """ Test TimeSpan.linspace when timestep_size is omitted or set to None. """
+ test_tspan = TimeSpan.linspace(t=30, num_points=301)
+ self.assertEqual(test_tspan.timestep_size, 0.1)
+
+ def test_linspace__invaild_timestep_size(self):
+ """ Test TimeSpan.linspace when timestep_size is of an invalid type. """
+ test_tsss = ["1", [0.001]]
+ for test_tss in test_tsss:
+ with self.subTest(timestep_size=test_tss):
+ with self.assertRaises(TimespanError):
+ TimeSpan.linspace(t=30, num_points=301, timestep_size=test_tss)
+
+ def test_linspace__invaild_timestep_size_value(self):
+ """ Test TimeSpan.linspace when timestep_size is an invalid value. """
+ test_tsss = [0, -0.5, -1, -5]
+ for test_tss in test_tsss:
+ with self.subTest(timestep_size=test_tss):
+ with self.assertRaises(TimespanError):
+ TimeSpan.linspace(t=30, num_points=301, timestep_size=test_tss)
+
+ def test_linspace__timestep_size_too_large(self):
+ """ Test TimeSpan.linspace when the timestep_size > the diff of points. """
+ with self.assertRaises(TimespanError):
+ TimeSpan.linspace(t=20, num_points=41, timestep_size=1)
+
+ def test_linspace__no_args(self):
+ """ Test TimeSpan.linspace without passing any args. """
+ tspan = TimeSpan.linspace()
+ test_tspan = self.set_timesteps(numpy.linspace(0, 20, 401), 0.05)
+ self.assertEqual(tspan, test_tspan)
+ self.assertEqual(tspan.timestep_size, 0.05)
+
+ def test_arange(self):
+ """ Test TimeSpan.arange. """
+ tspan = TimeSpan.arange(0.1, t=30, timestep_size=0.001)
+ test_tspan = self.set_timesteps(numpy.arange(0, 30.1, 0.1), 0.001)
+ self.assertEqual(tspan, test_tspan)
+ self.assertEqual(tspan.timestep_size, 0.001)
+
+ def test_arange__no_t(self):
+ """ Test TimeSpan.arange. """
+ tspan = TimeSpan.arange(0.1, timestep_size=0.001)
+ test_tspan = self.set_timesteps(numpy.arange(0, 20.1, 0.1), 0.001)
+ self.assertEqual(tspan, test_tspan)
+
+ def test_arange__invalid_t(self):
+ """ Test TimeSpan.arange with invalid t. """
+ test_values = [None, "5", 0, -0.5, -1, -2, -5, -10, [20.5]]
+ for test_val in test_values:
+ with self.subTest(t=test_val):
+ with self.assertRaises(TimespanError):
+ TimeSpan.arange(0.1, t=test_val, timestep_size=0.001)
+
+ def test_arange__invalid_increment(self):
+ """ Test TimeSpan.arange with invalid increment type. """
+ test_values = [None, "0.05", 0, -1, -2, -5, -10, [0.05]]
+ for test_val in test_values:
+ with self.subTest(imcrement=test_val):
+ with self.assertRaises(TimespanError):
+ TimeSpan.arange(test_val, t=30, timestep_size=0.001)
+
+ def test_arange__timestep_size_none(self):
+ """ Test TimeSpan.arange when timestep_size is omitted or set to None. """
+ test_tspan = TimeSpan.arange(0.1, t=30)
+ self.assertEqual(test_tspan.timestep_size, 0.1)
+
+ def test_arange__invaild_timestep_size(self):
+ """ Test TimeSpan.arange when timestep_size is of an invalid type. """
+ test_tsss = ["1", [0.001]]
+ for test_tss in test_tsss:
+ with self.subTest(timestep_size=test_tss):
+ with self.assertRaises(TimespanError):
+ TimeSpan.arange(0.1, t=30, timestep_size=test_tss)
+
+ def test_arange__invaild_timestep_size_values(self):
+ """ Test TimeSpan.arange when timestep_size is an invalid values. """
+ test_tsss = [0, -0.5, -1, -5]
+ for test_tss in test_tsss:
+ with self.subTest(timestep_size=test_tss):
+ with self.assertRaises(TimespanError):
+ TimeSpan.arange(0.1, t=30, timestep_size=test_tss)
+
+ def test_arange__timestep_size_too_large(self):
+ """ Test TimeSpan.arange when the timestep_size > the diff of points. """
+ with self.assertRaises(TimespanError):
+ TimeSpan.arange(0.5, t=20, timestep_size=1)
+
+ def test_validate__list(self):
+ """ Test TimeSpan.validate with list data structure. """
+ raw_tspan = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ self.tspan.items = raw_tspan
+ self.tspan.validate(coverage="all")
+ test_tspan = self.set_timesteps(numpy.array(raw_tspan), 0.001)
+ self.assertIsInstance(self.tspan.items, numpy.ndarray)
+ self.assertEqual(self.tspan, test_tspan)
+
+ def test_validate__tuple(self):
+ """ Test TimeSpan.validate with tuple data structure. """
+ raw_tspan = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+ self.tspan.items = raw_tspan
+ self.tspan.validate(coverage="all")
+ test_tspan = self.set_timesteps(numpy.array(raw_tspan), 0.001)
+ self.assertIsInstance(self.tspan.items, numpy.ndarray)
+ self.assertEqual(self.tspan, test_tspan)
+
+ def test_validate__range(self):
+ """ Test TimeSpan.validate with range data structure. """
+ raw_tspan = range(11)
+ self.tspan.items = raw_tspan
+ self.tspan.validate(coverage="all")
+ test_tspan = self.set_timesteps(numpy.array(raw_tspan), 0.001)
+ self.assertIsInstance(self.tspan.items, numpy.ndarray)
+ self.assertEqual(self.tspan, test_tspan)
+
+ def test_validate__invalid_type(self):
+ """ Test TimeSpan.validate with an invalid data structure type. """
+ test_tspans = [
+ None, "50", 20, 40.5, set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+ ]
+ for test_tspan in test_tspans:
+ if test_tspan is not None:
+ self.setUp()
+ with self.subTest(items=test_tspan):
+ with self.assertRaises(TimespanError):
+ self.tspan.items = test_tspan
+ self.tspan.validate(coverage="all")
+
+ def test_validate__empty_timespan(self):
+ """ Test TimeSpan.validate with an empty data structure. """
+ test_tspans = [[], ()]
+ for test_tspan in test_tspans:
+ if test_tspan != []:
+ self.setUp()
+ with self.subTest(items=test_tspan):
+ with self.assertRaises(TimespanError):
+ self.tspan.items = test_tspan
+ self.tspan.validate(coverage="all")
+
+ def test_validate__all_same_values(self):
+ """ Test TimeSpan.validate with an empty data structure. """
+ with self.assertRaises(TimespanError):
+ self.tspan.items = [2, 2, 2, 2, 2, 2, 2, 2, 2]
+ self.tspan.validate(coverage="all")
+
+ def test_validate__negative_start(self):
+ """ Test TimeSpan.validate with an initial time < 0. """
+ with self.assertRaises(TimespanError):
+ self.tspan.items = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ self.tspan.validate(coverage="all")
+
+ def test_validate__non_uniform_timespan(self):
+ """ Test TimeSpan.validate with a non-uniform timespan. """
+ with self.assertRaises(TimespanError):
+ self.tspan.items = [2, 1, 3, 4, 5, 6, 7, 8, 9, 10]
+ self.tspan.validate(coverage="all")
+
+ def test_validate__invalid_timestep_size(self):
+ """ Test TimeSpan.validate when timestep_size is of an invalid type. """
+ test_tsss = [None, "1", [0.001]]
+ for test_tss in test_tsss:
+ with self.subTest(timestep_size=test_tss):
+ with self.assertRaises(TimespanError):
+ self.tspan.timestep_size = test_tss
+ self.tspan.validate(coverage="all")
+
+ def test_validate__invalid_timestep_size_values(self):
+ """ Test TimeSpan.validate when timestep_size is an invalid values. """
+ test_tsss = [0, -0.5, -1, -5]
+ for test_tss in test_tsss:
+ with self.subTest(timestep_size=test_tss):
+ with self.assertRaises(TimespanError):
+ self.tspan.timestep_size = test_tss
+ self.tspan.validate(coverage="all")
+
+ def test_validate__invalid_output_freq(self):
+ """ Test TimeSpan.validate when output_freq is of an invalid type. """
+ test_opfs = [None, "5", [0.5]]
+ for test_opf in test_opfs:
+ with self.subTest(output_freq=test_opf):
+ with self.assertRaises(TimespanError):
+ self.tspan.output_freq = test_opf
+ self.tspan.validate(coverage="all")
+
+ def test_validate__invalid_output_freq_values(self):
+ """ Test TimeSpan.validate when output_freq is an invalid value. """
+ test_opfs = [0, -0.5, -1, -5]
+ for test_opf in test_opfs:
+ with self.subTest(output_freq=test_opf):
+ with self.assertRaises(TimespanError):
+ self.tspan.output_freq = test_opf
+ self.tspan.validate(coverage="all")
+
+ def test_validate__output_freq_lessthan_timestep_size(self):
+ """ Test TimeSpan.validate when output_freq < timestep_size. """
+ with self.assertRaises(TimespanError):
+ self.tspan.timestep_size = 0.5
+ self.tspan.output_freq = 0.1
+ self.tspan.validate(coverage="all")
+
+ def test_validate__invalid_num_timesteps(self):
+ """ Test TimeSpan.validate when num_timesteps is of an invalid type. """
+ test_nsts = [None, "5", 0.5, [5]]
+ for test_nst in test_nsts:
+ with self.subTest(num_timestep=test_nst):
+ with self.assertRaises(TimespanError):
+ self.tspan.num_timesteps = test_nst
+ self.tspan.validate(coverage="all")
+
+ def test_validate__invalid_num_timesteps_value(self):
+ """ Test TimeSpan.validate when num_timesteps is an invalid value. """
+ test_nsts = [0, -1, -5]
+ for test_nst in test_nsts:
+ with self.subTest(num_timesteps=test_nst):
+ with self.assertRaises(TimespanError):
+ self.tspan.num_timesteps = test_nst
+ self.tspan.validate(coverage="all")
+
+ def test_validate__invalid_output_steps(self):
+ """ Test TimeSpan.validate when output_steps is of an invalid type. """
+ test_opss = [None, "5", 5, 0.5, [5], {"x":5}, (6,2)]
+ for test_ops in test_opss:
+ with self.subTest(output_steps=test_ops):
+ with self.assertRaises(TimespanError):
+ self.tspan.output_steps = test_ops
+ self.tspan.validate(coverage="all")
+
+ def test_validate__invalid_output_steps_value(self):
+ """ Test TimeSpan.validate when output_steps is an invalid value. """
+ with self.assertRaises(TimespanError):
+ self.tspan.output_steps = numpy.array([6,2])
+ self.tspan.validate(coverage="all")