From 0ca24335dbe254ff5d6250d5f7ab514de96c4071 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 20 May 2022 10:27:27 -0400 Subject: [PATCH 01/15] Added a run coverage script. --- run_coverage.sh | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100755 run_coverage.sh 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 From 13530f1fd7edfe6f58f8f50a19ef7de00fcf835d Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 20 May 2022 10:28:36 -0400 Subject: [PATCH 02/15] Added a module to contain the unittest files. --- test/unit_tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/unit_tests/__init__.py diff --git a/test/unit_tests/__init__.py b/test/unit_tests/__init__.py new file mode 100644 index 00000000..e69de29b From d2f9a6a8e064df472bd1c902ac9844a18319cfb7 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 20 May 2022 10:36:24 -0400 Subject: [PATCH 03/15] Moved the unittest files into the unittests module. --- test/run_unit_tests.py | 6 +++--- test/{ => unit_tests}/test_parameter.py | 0 test/{ => unit_tests}/test_reaction.py | 0 test/{ => unit_tests}/test_species.py | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename test/{ => unit_tests}/test_parameter.py (100%) rename test/{ => unit_tests}/test_reaction.py (100%) rename test/{ => unit_tests}/test_species.py (100%) diff --git a/test/run_unit_tests.py b/test/run_unit_tests.py index 48e360cc..8c994dca 100755 --- a/test/run_unit_tests.py +++ b/test/run_unit_tests.py @@ -28,9 +28,9 @@ 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 modules = [ test_species, 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 From 976725a07ebc2e7fbc599933ecfd0994eedeab46 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 20 May 2022 10:39:03 -0400 Subject: [PATCH 04/15] Added source args to coverage calls in testing github actions. --- .github/workflows/run-integration-tests.yml | 2 +- .github/workflows/run-unit-tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 From 5f9a469a39930fa242c68cc3cd05224b676e29c3 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 20 May 2022 10:45:31 -0400 Subject: [PATCH 05/15] Added unittests for the new timespan module. --- test/run_unit_tests.py | 4 +- test/unit_tests/test_timespan.py | 193 +++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 test/unit_tests/test_timespan.py diff --git a/test/run_unit_tests.py b/test/run_unit_tests.py index 8c994dca..d54c1fff 100755 --- a/test/run_unit_tests.py +++ b/test/run_unit_tests.py @@ -31,11 +31,13 @@ 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/test_timespan.py b/test/unit_tests/test_timespan.py new file mode 100644 index 00000000..2ca310ce --- /dev/null +++ b/test/unit_tests/test_timespan.py @@ -0,0 +1,193 @@ +# 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 numpy +import unittest + +from spatialpy.core.timespan import TimeSpan +from spatialpy.core.gillespyError 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=100, num_points=101) + + def test_constructor(self): + """ Test the TimeSpan constructor. """ + test_tspan = numpy.linspace(0, 20, 401) + tspan = TimeSpan(test_tspan) + self.assertEqual(tspan, test_tspan) + + def test_constructor__valid_data_structures(self): + """ Test the TimeSpan constructor with valid data structures. """ + test_tspans = [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + (1, 2, 3, 4, 5, 6, 7, 8, 9, 10), + range(11) + ] + for test_tspan in test_tspans: + with self.subTest(tspan=test_tspan, tspan_type=type(test_tspan)): + tspan = TimeSpan(test_tspan) + self.assertEqual(tspan, numpy.array(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) + + def test_linspace(self): + """ Test TimeSpan.linspace. """ + tspan = TimeSpan.linspace(t=30, num_points=301) + self.assertEqual(tspan, numpy.linspace(0, 30, 301)) + + def test_linspace__no_t(self): + """ Test TimeSpan.linspace without passing t. """ + tspan = TimeSpan.linspace(num_points=201) + self.assertEqual(tspan, numpy.linspace(0, 20, 201)) + + 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): + tspan = TimeSpan.linspace(t=test_val, num_points=301) + + def test_linspace__no_num_points(self): + """ Test TimeSpan.linspace without passing num_points. """ + tspan = TimeSpan.linspace(t=30) + self.assertEqual(tspan, numpy.linspace(0, 30, int(30 / 0.05) + 1)) + + 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): + tspan = TimeSpan.linspace(t=30, num_points=test_val) + + def test_linspace__no_args(self): + """ Test TimeSpan.linspace without passing any args. """ + tspan = TimeSpan.linspace() + self.assertEqual(tspan, numpy.linspace(0, 20, 401)) + + def test_arange(self): + """ Test TimeSpan.arange. """ + tspan = TimeSpan.arange(0.1, t=30) + self.assertEqual(tspan, numpy.arange(0, 30.1, 0.1)) + + def test_arange__no_t(self): + """ Test TimeSpan.arange. """ + tspan = TimeSpan.arange(0.1) + self.assertEqual(tspan, numpy.arange(0, 20.1, 0.1)) + + 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): + tspan = TimeSpan.arange(0.1, t=test_val) + + 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): + tspan = TimeSpan.arange(test_val, t=30) + + def test_validate__list(self): + """ Test TimeSpan.validate with list data structure. """ + test_tspan = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + self.tspan.items = test_tspan + self.tspan.validate() + self.assertIsInstance(self.tspan.items, numpy.ndarray) + self.assertEqual(self.tspan, numpy.array(test_tspan)) + + def test_validate__tuple(self): + """ Test TimeSpan.validate with tuple data structure. """ + test_tspan = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + self.tspan.items = test_tspan + self.tspan.validate() + self.assertIsInstance(self.tspan.items, numpy.ndarray) + self.assertEqual(self.tspan, numpy.array(test_tspan)) + + def test_validate__range(self): + """ Test TimeSpan.validate with range data structure. """ + test_tspan = range(11) + self.tspan.items = test_tspan + self.tspan.validate() + self.assertIsInstance(self.tspan.items, numpy.ndarray) + self.assertEqual(self.tspan, numpy.array(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() + + 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() + + def test_validate__all_same_values(self): + """ Test TimeSpan.validate with an empty data structure. """ + test_tspan = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + tspan = TimeSpan(test_tspan) + with self.assertRaises(TimespanError): + tspan.items = [2, 2, 2, 2, 2, 2, 2, 2, 2] + tspan.validate() + + def test_validate__negative_start(self): + """ Test TimeSpan.validate with an initial time < 0. """ + test_tspan = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + tspan = TimeSpan(test_tspan) + with self.assertRaises(TimespanError): + tspan.items = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + tspan.validate() + + def test_validate__non_uniform_timespan(self): + """ Test TimeSpan.validate with a non-uniform timespan. """ + test_tspan = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + tspan = TimeSpan(test_tspan) + with self.assertRaises(TimespanError): + tspan.items = [2, 1, 3, 4, 5, 6, 7, 8, 9, 10] + tspan.validate() From 549292317f5b328fbd9c4ff1ff9631266ff4bdd6 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 20 May 2022 10:49:27 -0400 Subject: [PATCH 06/15] Added the error class for the timespan module. --- spatialpy/core/spatialpyerror.py | 5 +++++ 1 file changed, 5 insertions(+) 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 From 02c39ca2d88133f6630f123d445cd416d95f55c8 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 20 May 2022 10:53:34 -0400 Subject: [PATCH 07/15] Added the timespan module to the core directory. --- spatialpy/core/timespan.py | 131 +++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 spatialpy/core/timespan.py diff --git a/spatialpy/core/timespan.py b/spatialpy/core/timespan.py new file mode 100644 index 00000000..ca84ef21 --- /dev/null +++ b/spatialpy/core/timespan.py @@ -0,0 +1,131 @@ +# 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 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 + + :raises TimespanError: items is an invalid type. + """ + def __init__(self, items): + if isinstance(items, np.ndarray): + self.items = items + elif isinstance(items, (list, tuple, range)): + self.items = np.array(items) + else: + raise TimespanError("Timespan must be of type: list, tuple, range, or numpy.ndarray.") + + self.validate() + + 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__() + + @classmethod + def linspace(cls, t=20, num_points=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 + + :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) + + @classmethod + def arange(cls, increment, t=20): + """ + 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 + + :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) + + def validate(self): + """ + Validate the models time span + + :raises TimespanError: Timespan is an invalid type, empty, not uniform, contains a single \ + repeated value, or contains a negative initial time. + """ + if not isinstance(self.items, np.ndarray): + if not isinstance(self.items, (list, tuple, range)): + raise TimespanError("Timespan must be of type: list, tuple, range, or numpy.ndarray.") + self.items = np.array(self.items) + + if len(self.items) == 0: + raise TimespanError("Timespans must contain values.") + if self.items[0] < 0: + raise TimespanError("Simulation must run from t=0 to end time (t must always be positive).") + + first_diff = self.items[1] - self.items[0] + other_diff = self.items[2:] - self.items[1:-1] + isuniform = np.isclose(other_diff, first_diff).all() + + if 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.") From c736547db4c5c7555948a58c1b5db2fb89bea7a6 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 20 May 2022 10:55:42 -0400 Subject: [PATCH 08/15] Fixed import error with timespan error class. --- test/unit_tests/test_timespan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit_tests/test_timespan.py b/test/unit_tests/test_timespan.py index 2ca310ce..b88c5721 100644 --- a/test/unit_tests/test_timespan.py +++ b/test/unit_tests/test_timespan.py @@ -17,7 +17,7 @@ import unittest from spatialpy.core.timespan import TimeSpan -from spatialpy.core.gillespyError import TimespanError +from spatialpy.core.spatialpyerror import TimespanError class TestTimeSpan(unittest.TestCase): ''' From 7fb2abc48373d7283b91beff4f4599e78d81a204 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 20 May 2022 10:58:16 -0400 Subject: [PATCH 09/15] Added the timespan to the core init. --- spatialpy/core/__init__.py | 1 + 1 file changed, 1 insertion(+) 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 * From 2a76aa0b4d3c87eb3e36e76cbbbf79e354774de7 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Sun, 22 May 2022 11:38:34 -0400 Subject: [PATCH 10/15] Added a timespan module. --- spatialpy/core/timespan.py | 137 ++++++++++++++++++++++++++++--------- 1 file changed, 106 insertions(+), 31 deletions(-) diff --git a/spatialpy/core/timespan.py b/spatialpy/core/timespan.py index ca84ef21..7b30c7e7 100644 --- a/spatialpy/core/timespan.py +++ b/spatialpy/core/timespan.py @@ -13,6 +13,7 @@ # 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 @@ -27,17 +28,25 @@ class TimeSpan(Iterator): 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): - if isinstance(items, np.ndarray): - self.items = items - elif isinstance(items, (list, tuple, range)): - self.items = np.array(items) - else: - raise TimespanError("Timespan must be of type: list, tuple, range, or numpy.ndarray.") + 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 - self.validate() + 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() @@ -54,8 +63,23 @@ def __len__(self): def __next__(self): return self.items.__next__() + 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): + def linspace(cls, t=20, num_points=None, timestep_size=None): """ Creates a timespan using the form np.linspace(0, , ). @@ -65,6 +89,9 @@ def linspace(cls, t=20, num_points=None): :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 @@ -78,10 +105,10 @@ def linspace(cls, t=20, num_points=None): if num_points is None: num_points = int(t / 0.05) + 1 items = np.linspace(0, t, num_points) - return cls(items) + return cls(items, timestep_size=timestep_size) @classmethod - def arange(cls, increment, t=20): + def arange(cls, increment, t=20, timestep_size=None): """ Creates a timespan using the form np.arange(0, , ). @@ -91,6 +118,9 @@ def arange(cls, increment, t=20): :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 @@ -102,30 +132,75 @@ def arange(cls, increment, t=20): raise TimespanError("increment must be a positive float or int.") items = np.arange(0, t + increment, increment) - return cls(items) + return cls(items, timestep_size=timestep_size) - def validate(self): + 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 not isinstance(self.items, np.ndarray): - if not isinstance(self.items, (list, tuple, range)): - raise TimespanError("Timespan must be of type: list, tuple, range, or numpy.ndarray.") - self.items = np.array(self.items) - - if len(self.items) == 0: - raise TimespanError("Timespans must contain values.") - if self.items[0] < 0: - raise TimespanError("Simulation must run from t=0 to end time (t must always be positive).") - - first_diff = self.items[1] - self.items[0] - other_diff = self.items[2:] - self.items[1:-1] - isuniform = np.isclose(other_diff, first_diff).all() - - if 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"): + 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 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.") From 54f376bd9db9e5a5330a642c3f99e2b9a9a425c4 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Sun, 22 May 2022 11:39:24 -0400 Subject: [PATCH 11/15] Added unittest for the new timespan module. --- test/unit_tests/test_timespan.py | 282 +++++++++++++++++++++++++------ 1 file changed, 231 insertions(+), 51 deletions(-) diff --git a/test/unit_tests/test_timespan.py b/test/unit_tests/test_timespan.py index b88c5721..87e3bc5d 100644 --- a/test/unit_tests/test_timespan.py +++ b/test/unit_tests/test_timespan.py @@ -13,6 +13,7 @@ # 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 @@ -27,25 +28,44 @@ class TestTimeSpan(unittest.TestCase): ''' def setUp(self): """ Setup a clean valid timespan for testing. """ - self.tspan = TimeSpan.linspace(t=100, num_points=101) + 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 = numpy.linspace(0, 20, 401) - tspan = TimeSpan(test_tspan) + 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 = [ - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - (1, 2, 3, 4, 5, 6, 7, 8, 9, 10), + [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 test_tspan in test_tspans: - with self.subTest(tspan=test_tspan, tspan_type=type(test_tspan)): - tspan = TimeSpan(test_tspan) - self.assertEqual(tspan, numpy.array(test_tspan)) + 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. """ @@ -56,17 +76,46 @@ def test_constructor__invalid_items(self): for test_tspan in test_tspans: with self.subTest(items=test_tspan): with self.assertRaises(TimespanError): - TimeSpan(test_tspan) + 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) - self.assertEqual(tspan, numpy.linspace(0, 30, 301)) + 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) - self.assertEqual(tspan, numpy.linspace(0, 20, 201)) + 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. """ @@ -74,12 +123,13 @@ def test_linspace__invalid_t(self): for test_val in test_values: with self.subTest(t=test_val): with self.assertRaises(TimespanError): - tspan = TimeSpan.linspace(t=test_val, num_points=301) + 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) - self.assertEqual(tspan, numpy.linspace(0, 30, int(30 / 0.05) + 1)) + 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. """ @@ -87,22 +137,53 @@ def test_linspace__invalid_num_points(self): for test_val in test_values: with self.subTest(num_points=test_val): with self.assertRaises(TimespanError): - tspan = TimeSpan.linspace(t=30, num_points=test_val) + 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() - self.assertEqual(tspan, numpy.linspace(0, 20, 401)) + 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) - self.assertEqual(tspan, numpy.arange(0, 30.1, 0.1)) + 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) - self.assertEqual(tspan, numpy.arange(0, 20.1, 0.1)) + 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. """ @@ -110,7 +191,7 @@ def test_arange__invalid_t(self): for test_val in test_values: with self.subTest(t=test_val): with self.assertRaises(TimespanError): - tspan = TimeSpan.arange(0.1, t=test_val) + TimeSpan.arange(0.1, t=test_val, timestep_size=0.001) def test_arange__invalid_increment(self): """ Test TimeSpan.arange with invalid increment type. """ @@ -118,31 +199,60 @@ def test_arange__invalid_increment(self): for test_val in test_values: with self.subTest(imcrement=test_val): with self.assertRaises(TimespanError): - tspan = TimeSpan.arange(test_val, t=30) + 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. """ - test_tspan = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - self.tspan.items = test_tspan - self.tspan.validate() + 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, numpy.array(test_tspan)) + self.assertEqual(self.tspan, test_tspan) def test_validate__tuple(self): """ Test TimeSpan.validate with tuple data structure. """ - test_tspan = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) - self.tspan.items = test_tspan - self.tspan.validate() + 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, numpy.array(test_tspan)) + self.assertEqual(self.tspan, test_tspan) def test_validate__range(self): """ Test TimeSpan.validate with range data structure. """ - test_tspan = range(11) - self.tspan.items = test_tspan - self.tspan.validate() + 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, numpy.array(test_tspan)) + self.assertEqual(self.tspan, test_tspan) def test_validate__invalid_type(self): """ Test TimeSpan.validate with an invalid data structure type. """ @@ -155,7 +265,7 @@ def test_validate__invalid_type(self): with self.subTest(items=test_tspan): with self.assertRaises(TimespanError): self.tspan.items = test_tspan - self.tspan.validate() + self.tspan.validate(coverage="all") def test_validate__empty_timespan(self): """ Test TimeSpan.validate with an empty data structure. """ @@ -166,28 +276,98 @@ def test_validate__empty_timespan(self): with self.subTest(items=test_tspan): with self.assertRaises(TimespanError): self.tspan.items = test_tspan - self.tspan.validate() + self.tspan.validate(coverage="all") def test_validate__all_same_values(self): """ Test TimeSpan.validate with an empty data structure. """ - test_tspan = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - tspan = TimeSpan(test_tspan) with self.assertRaises(TimespanError): - tspan.items = [2, 2, 2, 2, 2, 2, 2, 2, 2] - tspan.validate() + 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. """ - test_tspan = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - tspan = TimeSpan(test_tspan) with self.assertRaises(TimespanError): - tspan.items = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - tspan.validate() + 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. """ - test_tspan = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - tspan = TimeSpan(test_tspan) with self.assertRaises(TimespanError): - tspan.items = [2, 1, 3, 4, 5, 6, 7, 8, 9, 10] - tspan.validate() + 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") From cb3a4d56e9eb4ab3807ab9b08aa2fc30fbbda0a8 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Sun, 22 May 2022 12:37:40 -0400 Subject: [PATCH 12/15] Integrated the new timespan module into the package. --- spatialpy/core/model.py | 60 ++++++++--------------------- spatialpy/solvers/solver.py | 6 +-- spatialpy/stochss/stochss_export.py | 6 +-- 3 files changed, 22 insertions(+), 50 deletions(-) diff --git a/spatialpy/core/model.py b/spatialpy/core/model.py index 62b38ede..7904aeac 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 TimeSpan 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/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": [], From 853b12b94460d428fc54d93bfc2e79b631d30095 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 6 Jun 2022 10:47:53 -0400 Subject: [PATCH 13/15] Added the __str__ and _ipython_display_ function to the TimeSapn Module. --- spatialpy/core/timespan.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spatialpy/core/timespan.py b/spatialpy/core/timespan.py index 7b30c7e7..a8b8e7f1 100644 --- a/spatialpy/core/timespan.py +++ b/spatialpy/core/timespan.py @@ -63,6 +63,12 @@ def __len__(self): 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 From 4f95fd07234dcd67666f93b1b7117528392e9768 Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Mon, 6 Jun 2022 10:48:47 -0400 Subject: [PATCH 14/15] Fixed issue in compile prep function. --- spatialpy/core/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spatialpy/core/model.py b/spatialpy/core/model.py index 7904aeac..b35b6345 100644 --- a/spatialpy/core/model.py +++ b/spatialpy/core/model.py @@ -297,7 +297,7 @@ def compile_prep(self): """ try: self.tspan.validate(coverage="all") - except TimeSpan as err: + except TimespanError as err: raise ModelError(f"Failed to validate timespan. Reason given: {err}") from err if self.domain is None: From 2568e8f2fb10a445359526101018f20dc4885720 Mon Sep 17 00:00:00 2001 From: BryanRumsey <44621966+BryanRumsey@users.noreply.github.com> Date: Mon, 6 Jun 2022 12:35:28 -0400 Subject: [PATCH 15/15] Silenced isuniform check for coverage=="all" --- spatialpy/core/timespan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spatialpy/core/timespan.py b/spatialpy/core/timespan.py index a8b8e7f1..714600b0 100644 --- a/spatialpy/core/timespan.py +++ b/spatialpy/core/timespan.py @@ -174,7 +174,7 @@ def validate(self, items=None, timestep_size=None, coverage="build"): other_diff = items[2:] - items[1:-1] isuniform = np.isclose(other_diff, first_diff).all() - if not isuniform: + 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.")