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")