Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

General add method for model #764

Merged
merged 24 commits into from
Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8983b6f
Added a general add function (shell) to the model.
BryanRumsey Mar 31, 2022
fbbb753
Added unit tests for Model.add.
BryanRumsey Mar 31, 2022
db49d47
Created a timespan component with errors and imports.
BryanRumsey Apr 4, 2022
6b4407c
Refactored the tspan blocks to use the new timespan component.
BryanRumsey Apr 4, 2022
52188b1
Added support for single components to Model.add.
BryanRumsey Apr 4, 2022
a56716f
Added missing imports.
BryanRumsey Apr 4, 2022
753008b
Added missing imports.
BryanRumsey Apr 4, 2022
c20e95c
Added missing imports.
BryanRumsey Apr 4, 2022
7e45e0e
Added missing imports.
BryanRumsey Apr 4, 2022
fc52e7f
Added support for multiple components. Added a check for invalid com…
BryanRumsey Apr 4, 2022
7ad828c
Fixed unit tests.
BryanRumsey Apr 4, 2022
4cbb985
Fixed type issue.
BryanRumsey Apr 4, 2022
75f791b
Fixed unit tests.
BryanRumsey Apr 4, 2022
e9083f3
Added support for len to timespan.
BryanRumsey Apr 4, 2022
d5d072f
Updated uniform timespan test.
BryanRumsey Apr 4, 2022
28f9678
Added jsonify to TimeSpan class.
BryanRumsey Apr 4, 2022
a39ed88
Added __eq__ function.
BryanRumsey Apr 5, 2022
c7ec39d
Added unit tests for timespan component.
BryanRumsey Apr 5, 2022
c046888
Refactored timespan class to pass unit testing.
BryanRumsey Apr 5, 2022
20d13f4
Addressed review comments for timespan class.
BryanRumsey Apr 6, 2022
c44dd40
Updated docs for TimeSpan and Model.add.
BryanRumsey Apr 6, 2022
dd6f73f
Fixed issue with t=None passed to TimeSpan.arrange.
BryanRumsey Apr 6, 2022
116c5af
Fixed spelling errors in docs
BryanRumsey Apr 7, 2022
c909c16
Fixed spelling error in docs
BryanRumsey Apr 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gillespy2/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from .results import *
from .sortableobject import *
from .species import *
from .timespan import TimeSpan
from gillespy2.__version__ import __version__

_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
Expand Down
4 changes: 4 additions & 0 deletions gillespy2/core/gillespyError.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ class ParameterError(ModelError):
pass


class TimespanError(ModelError):
pass


# Solver specific errors
class SolverError(Exception):
pass
Expand Down
21 changes: 10 additions & 11 deletions gillespy2/core/gillespySolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import copy
import numpy

from .timespan import TimeSpan
from .gillespyError import SimulationError, ModelError
from typing import Set, Type

Expand Down Expand Up @@ -97,18 +98,16 @@ def validate_tspan(self, increment, t):
)

if self.model.tspan is None:
end = 20 + increment if t is None else t + increment
self.model.timespan(numpy.arange(0, end, increment))
if t is None:
tspan = TimeSpan.arange(increment)
else:
tspan = TimeSpan.arange(increment, t=t)
self.model.timespan(tspan)
elif not isinstance(self.model.tspan, TimeSpan) or type(self.model.tspan).__name__ != "TimeSpan":
tspan = TimeSpan(self.model.tspan)
self.model.timespan(tspan)
else:
if self.model.tspan[0] < 0:
raise SimulationError("Simulation must run from t=0 to end time (t must always be positive).")

first_diff = self.model.tspan[1] - self.model.tspan[0]
other_diff = self.model.tspan[2:] - self.model.tspan[1:-1]
isuniform = numpy.isclose(other_diff, first_diff).all()

if not isuniform:
raise SimulationError("StochKit only supports uniform timespans")
self.model.tspan.validate()

@classmethod
def get_supported_features(cls) -> "Set[Type]":
Expand Down
88 changes: 75 additions & 13 deletions gillespy2/core/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,29 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import numpy as np
from typing import Set, Type
from collections import OrderedDict

import gillespy2
from gillespy2.core.jsonify import TranslationTable
from gillespy2.core.reaction import *
from gillespy2.core.raterule import RateRule
from gillespy2.core.assignmentrule import AssignmentRule
from gillespy2.core.events import Event
from gillespy2.core.functiondefinition import FunctionDefinition
from gillespy2.core.parameter import Parameter
from gillespy2.core.species import Species
from gillespy2.core.raterule import RateRule
from gillespy2.core.reaction import Reaction
import numpy as np
from gillespy2.core.results import Trajectory,Results
from collections import OrderedDict
from gillespy2.core.gillespyError import *
from .gillespyError import SimulationError
from typing import Set, Type
from gillespy2.core.species import Species
from gillespy2.core.timespan import TimeSpan
from gillespy2.core.sortableobject import SortableObject
from gillespy2.core.jsonify import Jsonify, TranslationTable
from gillespy2.core.results import Trajectory, Results
from gillespy2.core.gillespyError import (
ParameterError,
ModelError,
SimulationError,
StochMLImportError,
InvalidStochMLError
)

try:
import lxml.etree as eTree
Expand Down Expand Up @@ -229,6 +239,55 @@ def decorate(header):

return print_string

def add(self, components):
"""
Adds a component, or list of components to the model. If a list is provided, Species
and Parameters are added before other components. Lists may contain any combination
of accepte types other that lists and do not need to be in any particular order.
BryanRumsey marked this conversation as resolved.
Show resolved Hide resolved

:param components: The component or list of components to be added the the model.
:type components: Species, Parameters, Reactions, Events, Rate Rules, Assignment Rules, \
FunctionDefinitions, and TimeSpan or list

:returns: The components that were added to the model.
:rtype: Species, Parameters, Reactions, Events, Rate Rules, Assignment Rules, \
FunctionDefinitions, and TimeSpan or list

:raises ModelError: Component is invalid.
"""
if isinstance(components, list):
p_types = (Species, Parameter, FunctionDefinition, TimeSpan)
p_names = (p_type.__name__ for p_type in p_types)

others = []
for component in components:
if isinstance(component, p_types) or type(component).__name__ in p_names:
self.add(component)
else:
others.append(component)

for component in others:
self.add(component)
elif isinstance(components, AssignmentRule) or type(components).__name__ == AssignmentRule.__name__:
self.add_assignment_rule(components)
elif isinstance(components, Event) or type(components).__name__ == Event.__name__:
self.add_event(components)
elif isinstance(components, FunctionDefinition) or type(components).__name__ == FunctionDefinition.__name__:
self.add_function_definition(components)
elif isinstance(components, Parameter) or type(components).__name__ == Parameter.__name__:
self.add_parameter(components)
elif isinstance(components, RateRule) or type(components).__name__ == RateRule.__name__:
self.add_rate_rule(components)
elif isinstance(components, Reaction) or type(components).__name__ == Reaction.__name__:
self.add_reaction(components)
elif isinstance(components, Species) or type(components).__name__ == Species.__name__:
self.add_species(components)
elif isinstance(components, TimeSpan) or type(components).__name__ == TimeSpan.__name__:
self.timespan(components)
else:
raise ModelError(f"Unsupported component: {type(components)} is not a valid component.")
return components

def make_translation_table(self):
from collections import ChainMap

Expand Down Expand Up @@ -653,10 +712,13 @@ def timespan(self, time_span):
timespans.

:param time_span: Evenly-spaced list of times at which to sample the species populations during the simulation.
Best to use the form np.linspace(<start time>, <end time>, <number of time-points, inclusive>)
:type time_span: numpy ndarray
Best to use the form gillespy2.TimeSpan(np.linspace(<start time>, <end time>, <number of time-points, inclusive>))
:type time_span: gillespy2.TimeSpan | iterator
"""
self.tspan = time_span
if isinstance(time_span, TimeSpan) or type(time_span).__name__ == "TimeSpan":
self.tspan = time_span
else:
self.tspan = TimeSpan(time_span)

def get_reaction(self, rname):
"""
Expand Down
134 changes: 134 additions & 0 deletions gillespy2/core/timespan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""
GillesPy2 is a modeling toolkit for biochemical simulation.
Copyright (C) 2019-2021 GillesPy2 developers.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

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 for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import numpy as np
from collections.abc import Iterator

from gillespy2.core.jsonify import Jsonify
from .gillespyError import TimespanError

class TimeSpan(Iterator, Jsonify):
"""
Model timespan that describes the duration to rub the simulation and at which timepoint to sample
BryanRumsey marked this conversation as resolved.
Show resolved Hide resolved
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(<start time>, <end time>, <number of time-points, inclusive>)
: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.")
BryanRumsey marked this conversation as resolved.
Show resolved Hide resolved

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, <t>, <num_points, inclusive>).

:param t: End time for the simulation.
:type t: 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: gillespy2.TimeSpan

:raises TimespanError: t or num_points are None, <= 0, or invalid type.
"""
if t is None or not isinstance(t, int) or t <= 0:
raise TimespanError("t must be a positive 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, <t, inclusive>, <increment>).

: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: int

:returns: Timespan for the model.
:rtype: gillespy2.TimeSpan

:raises TimespanError: t or increment are None, <= 0, or invalid type.
"""
if t is None or not isinstance(t, int) or t <= 0:
raise TimespanError("t must be a positive 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.")
2 changes: 2 additions & 0 deletions test/run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@
import test_jsonify
import test_notebooks
from unit_tests import test_model as unittest_model
from unit_tests import test_timespan as unittest_timespan

modules = [
unittest_timespan,
unittest_model,
test_empty_model,
test_build_engine,
Expand Down
2 changes: 1 addition & 1 deletion test/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def test_model_reordered_equality(self):
def test_uniform_timespan(self):
model = Model()
model.timespan(np.linspace(0, 1, 100))
with self.assertRaises(SimulationError):
with self.assertRaises(TimespanError):
model.timespan(np.array([0, 0.1, 0.5]))
model.run()

Expand Down
Loading