diff --git a/.gitignore b/.gitignore index 1873478..de19497 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyc build/ dist/ +validation_data/ stocal.egg-info/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b5b9cb2..ba5724b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [1.2] - 2018-08-10 + +### Added +- Added Gibson & Bruck's NextReactionMethod +- Added Cao et al's tau leaping method in stocal.experimental.tauleap +- Added statistical validation suite in stocal.examples.validation and stocal.samples.dsmts +- Added modular trajectory sampling interface stocal.experimental.samplers +- Added flattening of rule-based processes into static processes +- StochasticSimulationAlgorithm instances accept an optional random seed + +### Deprecated +- Deprecated AndersonNRM in favour of AndersonMethod +- Deprecated TrajectorySampler in favour of StochasticSimulationAlgorithm +- Deprecated ReactionRule in favour of TransitionRule +- Deprecated Process.trajectory in favour of Process.sample + +### Changed +- TrajectorySampler instances now use an internal random number generator (sampler.rng) rather than python's global one. +- Improved performance of DirectMethod and AndersonMethod +- Improved performance of transition inference in TransitionRule + ## [1.1.2] - 2018-05-23 ### Fixed @@ -18,6 +39,7 @@ ### Changed - MassAction.__repr__ now also prints rate constant + ## [1.1] - 2018-03-08 ### Fixed diff --git a/MANIFEST.in b/MANIFEST.in index 72927fa..e2c771a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include README.md include doc/* +include stocal/examples/dsmts/*.csv diff --git a/README.md b/README.md index 91e6e52..a1db7f7 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ process = stocal.Process([ # Sample a stochastic trajectory of the process initial_state = {} -trajectory = process.trajectory(initial_state, tmax=100) : +trajectory = process.sample(initial_state, tmax=100) : +for dt, transitions in trajectory: print trajectory.time, trajectory.state['A2'] ``` diff --git a/TODO b/TODO index 0d85b09..dba971a 100644 --- a/TODO +++ b/TODO @@ -1,13 +1,24 @@ +* stocal.algorithms.AndersonMethod.update_state is broken +* stocal.experimental.samplers do not chain as claimed ++ support for proper delay events (-> feature/events) += release/1.3 +- remove dict support in MassAction.reactions and TransitionRule.infer_transitions +- remove invalid-name arguments +- remove AndersonNRM +- remove TrajectorySampler +- remove ReactionRule +- remove Process.trajectory +- allow species equals 0 in initial state? +- regroup tests into specifications, unittests and bugs +- move stocal.experimental.CaoMethod into stocal.algorithms += release/2.0 +- utilities to simplify scipy/matplotlib interaction +- persistency support (save and resume simulations) +- arithmetic operations for Process - string rewrite rule support -- support for proper delay events +- other kinetic laws, e.g. Hill function - libSBML integration -- statistical verification tests -- every/until filters and other stop criteria - curried reactions (determine products only upon application) -- other kinetic laws, e.g. Hill function -- SSA profiling and algorithm variations - C/C++/D implementation -= version 1.2+ -- deprecate dict support in MassAction.reactions and ReactionRule.infer_transitions -- deprecate invalid-name arguments -= version 2 += version 2.1+ += version 3 diff --git a/doc/developer.md b/doc/developer.md index 2fd20a2..8638713 100644 --- a/doc/developer.md +++ b/doc/developer.md @@ -28,8 +28,8 @@ If in doubt, pylint decides. The test suite for stocal is contained in `stocal.tests` and can be run using ```bash -python setup.py test -python3 setup.py test +python -m unittest discover stocal.tests +python3 -m unittest discover stocal.tests ``` Passing of all tests is an enforced requirement for all code merged @@ -50,6 +50,23 @@ to derive implementation test cases from interface test cases. See `pydoc stocal.tests` for more information. +## Validation +Stocal ships with a validation suite for stochastic simulation +algorithms. The validation suite is based on the discrete stochastic +simulation model test suite DSMTS. To run validations, call +$ python stocal/examples/validation.py run N +from the command line. To generate a validation report, run +$ python stocal/examples/validation.py report +This generates a file validation.tex that can be compiled with pdflatex. +See +$ python stocal/examples/validation.py -h +for more information. The DSMTS user guide recommends N=1,000 as an +absolute minimum to be run regularly in conjunction with unit test, +and n=100,000 or n=1,000,000 for a thorough statistical analysis. +A rudimentary LaTeX template that collates report results into a +single document can be found in doc/validation.tex + + ## Releases When preparing a new release, these steps should be followed @@ -57,6 +74,7 @@ When preparing a new release, these steps should be followed * git flow release start * ensure an optimal code coverage of the test suite * ensure that all tests pass + * ensure that any novel algorithm passes validation * ensure that documentation (README, tutorial, etc.) is up to date * update CHANGELOG.md * bump the version number diff --git a/doc/tutorial.md b/doc/tutorial.md index aa765ba..a176632 100644 --- a/doc/tutorial.md +++ b/doc/tutorial.md @@ -43,15 +43,15 @@ process = Process([r1, r2]) ``` We can use this process, to sample stochastic trajectories. The method -`Process.trajectory` instantiates a trajectory sampler for a given +`Process.sample` instantiates a trajectory sampler for a given initial condition and stop criterion. The trajectory sampler implements the iterator protocol, so we can simply iterate through the trajectory, invoking one stochastic transition at a time. With each transition, time and state of the trajectory are properly updated: ```python -trajectory = process.trajectory({'A':100}, steps=1000) -for transition in trajectory : +trajectory = process.sample({'A':100}, steps=1000) +for dt, transitions in trajectory : print trajectory.time, trajectory.state['A'], trajectory.state['A2'] ``` @@ -124,7 +124,7 @@ reactions. As such, rules generate a whole set of reactions. Defining a rule requires to create a python [class](https://docs.python.org/2/tutorial/classes.html) with some required attributes and methods. The class needs to be derived from -`ReactionRule`, which requires our subclass to have the following +`TransitionRule`, which requires our subclass to have the following attributes: | attribute | description | @@ -135,7 +135,7 @@ attributes: Taking this all together, we define the following Dilution rule: ```python -class Dilution(ReactionRule) : +class Dilution(TransitionRule) : Transition = MassAction def novel_reactions(self, species) : @@ -160,7 +160,7 @@ alternatively be provided as return type annotation of the ```python from typing import Iterator -class Dilution(ReactionRule) : +class Dilution(TransitionRule) : def novel_reactions(self, species) -> Iterator[MassAction]: yield MassAction([species], [], 0.001) ``` @@ -191,7 +191,7 @@ To model this, we define a rule class for the polymerization that generates a Polymerization reaction for any two reactants: ```python -class Polymerization(ReactionRule) : +class Polymerization(TransitionRule) : Transition = MassAction def novel_reactions(self, k, l) : @@ -210,7 +210,7 @@ constants of these reactions depends on the lengths of the hydrolysis products, so that polymers are more likely to break in the middle. ```python -class Hydrolysis(ReactionRule) : +class Hydrolysis(TransitionRule) : Transition = MassAction def novel_reactions(self, k) : @@ -234,6 +234,19 @@ process = Process(transitions=[feed], Note that no change is necessary for the dilution rule, since it already generates a reaction for every chemical in the system. +*New in version 1.2:* Rule-based processes that expand into a finite +set of transitions can be flattened into equivalent static processes +that employ specific transitions rather than general rules: + +```python +process = Process(rules=[Dilution()]) +flat_process = process.flatten(['a', 'b', 'c']) +``` +This will generate a new process objects where the original rule is +expanded into three transitions, each one modelling the specific +dilution of one of the provided molecular species. + + ## Complex States So far, all our molecular species have been character sequences, either @@ -337,7 +350,7 @@ potentially form two different polymerization products: _k+l_ and _l+k_. Therefore, the polymerization rule has to generate both reactions: ```python -class Polymerization(ReactionRule) : +class Polymerization(TransitionRule) : Transition = MassAction def novel_reactions(self, k, l) : @@ -383,7 +396,7 @@ with the above overloads for `__eq__`, `__ne__` and `__hash__`. The nondirectional Polymerization rule now becomes: ```python -class Polymerization(ReactionRule) : +class Polymerization(TransitionRule) : Transition = MassAction def novel_reactions(self, k, l) : @@ -412,14 +425,14 @@ reaction rules. For this example, we look into modelling the association of proteins with mRNA's. We want to define a rule for the association of an arbitrary protein with an arbitrary mRNA. -With the above ReactionRule's we would need to constantly check whether -the species supplied to `ReactionRule.novel_reactions` are indeed +With the above TransitionRule's we would need to constantly check whether +the species supplied to `TransitionRule.novel_reactions` are indeed proteins and RNA's and only yield a transition in case they are. Not knowing which argument of the reactant combination is the protein and which the RNA further complicates the code. ```python -class Association(ReactionRule): +class Association(TransitionRule): Transition = MassAction def novel_reactions(self, k, l): @@ -443,16 +456,16 @@ class Rna(str): pass ``` -We can now write a typed `ReactionRule` for their association, simply by -setting the optional ReactionRule attribute `signature` to the list of -types that the rule should accept. When defining a signature, it must +We can now write a typed `TransitionRule` for their association, simply +by setting the optional TransitionRule attribute `signature` to the list +of types that the rule should accept. When defining a signature, it must have the same number of elements as the `novel_reactions` method. `novel_reactions` will now only be called with arguments that adhere to the type given in the signature. In our case, writing the rule becomes as simple as: ```python -class Association(ReactionRule): +class Association(TransitionRule): Transition = MassAction signature = [Protein, Rna] @@ -466,7 +479,7 @@ rule signature: ```python from typing import Iterator -class Association(ReactionRule): +class Association(TransitionRule): def novel_reactions(self, protein: Protein, rna: Rna) -> Iterator[MassAction]: yield MassAction([protein, rna], [(protein,rna)], 1.) ``` @@ -514,6 +527,41 @@ have used default `MassAction` reactions before. stocal/examples/temperature_cycle.py gives an example of how reactions can be modified to take changing temperature instead of volumes instead. +## Stochastic simulation algorithms + +stocal ships with several variants of the stochastic simulation algorithm, +refered to as sampler. A call to `Process.sample` inspects the +underlying process and will instantiate an appropriate sampler. +Currently, this creates an instance of Gibson and Bruck's next reaction +method, unless at least one transition of the process is time-dependent +(in which case the method creates an instance of Anderon's method). + +If you want to control which simulation algorithm is instantiated, you +can instantiate the desired sampler directly, as in, e.g., + +```python +sampler = algorithms.DirectMethod(process, state, tmax=100.) +for dt, transitions in sampler: + print(dt, transitions) +``` + +Currently, stocal provides the following samplers: + +| algorithm | description | +|------------------- | -------------------------------------------------------------------------------------------------------------- | +|DirectMethod | Original Gillespie algorithm | +|FirstReactionMethod | Stochastic simulation algorithm that can operate account for scheduled events | +|NextReactionMethod | Variant of FirstReactionMethod with improved performance *(new in version 1.2)* | +|AndersonMethod | Variant of NextReactionMethod that allows for propensity functions to be time-dependent *(new in version 1.1)* | +|CaoMethod | An (inexact) tau-leaping variant of SSA -- available in stocal.experimental.tauleap *(new in version 1.2)* | + +Please refer to the class documentation for information about the exact +implementation and reference publication. + +If you want to implement your own stochastic simulation algorithm, it +should be programmed against the interface defined by +`stocal.algorithms.StochasticSimulationAlgorithm`. + ## Further Documentation The full API of stocal is available via pydoc: diff --git a/doc/validation.tex b/doc/validation.tex new file mode 100644 index 0000000..c66a1f8 --- /dev/null +++ b/doc/validation.tex @@ -0,0 +1,44 @@ +\documentclass[notitlepage]{revtex4-1} +\usepackage{graphicx} +\graphicspath{{validation_data/1.1.2-64-g0d0fc6f/}} + +\begin{document} +\title{DSMTS Validation results for \textit{stocal} samplers} +\author{version: \VAR{version}} +\maketitle{} + +For each algorithm and model, figures show: +\begin{itemize} +\item on the left averaged trajectories of all system species over time +obtained by simulation (green) versus reported values from the DSMTS +repository (blue); + +\item in the center the absolute standard error of sample averages +($\bar X_{t}$) from the reported mean ($\mu_{t}$) as a function of +sample size $N$: +\[ + \left|\frac{\bar X_{t}-\mu_{t}}{\sigma_{t}^{2}}\right| \sim \mathcal{N}\left(0, \frac{1}{\sqrt{N}}\right), +\] +where $\sigma_{t}$ is the reported standard deviation. +The red line ($3/\sqrt{N}$) is an upper bound for the error scaling +at three times the reported standard deviation and should be only +occasionally exceeded; + +\item on the right the relative standard error between sampled +($\bar S_{t}^{2}$) and reported standard deviation ($\sigma_{t}^{2}$) +as a function of sample size: +\[ + \left|\frac{\bar S_{t}^{2}}{\sigma_{t}^{2}} - 1\right| \sim \mathcal{N}\left(0, \frac{2}{N}\right) . +\] +The red line ($5/\sqrt{N/2}$) is an upper bound for the error scaling +at five times the reported standard deviation and should be only +occasionally exceeded. +\end{itemize} + +\BLOCK{ for method_name, figures in methods.items() } +\section{\VAR{method_name}} +\BLOCK{ for figure in figures } +\includegraphics[width=\textwidth]{\VAR{figure}} +\BLOCK{ endfor } +\BLOCK{ endfor } +\end{document} diff --git a/setup.py b/setup.py index 162af24..3504c8e 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ def readme(): setup(name = "stocal", - version = "1.1.2", + version = "1.2", description = "simple rule-based stochastic simulation", long_description = readme(), classifiers=[ @@ -24,7 +24,8 @@ def readme(): author = "Harold Fellermann", author_email = "harold.fellermann@newcastle.ac.uk", license='MIT', - packages = ["stocal", "stocal.examples", "stocal.tests"], + packages = ["stocal", "stocal.examples", "stocal.experimental", "stocal.tests"], include_package_data=True, zip_safe = True, - test_suite = 'stocal.tests') + test_suite = 'stocal.tests', + install_requires=['pqdict']) diff --git a/stocal/__init__.py b/stocal/__init__.py index f5da79f..4e274dd 100644 --- a/stocal/__init__.py +++ b/stocal/__init__.py @@ -32,7 +32,8 @@ from .types import molecular_type from .transitions import Transition, Reaction, MassAction, Event -from .transitions import Rule, ReactionRule, Process +from .transitions import Rule, ReactionRule, TransitionRule, Process +from .structures import multiset from . import types from . import algorithms diff --git a/stocal/utils.py b/stocal/_utils.py similarity index 100% rename from stocal/utils.py rename to stocal/_utils.py diff --git a/stocal/algorithms.py b/stocal/algorithms.py index a8346ce..d8d51a2 100644 --- a/stocal/algorithms.py +++ b/stocal/algorithms.py @@ -21,18 +21,232 @@ """Stochastic simulation algorithms This module provides implementations of different stochastic simulation -algorithms. All implementations have to implement the TrajectorySampler -interface by subclassing this abstract base class. +algorithms. All implementations have to implement the +StochasticSimulationAlgorithm interface by subclassing this abstract +base class. + +The module also provides several helper classes for general stochastic +simulation algorithms. DependencyGraph is a generic species-dependency +graph that gives quick access to the set of affected transitions from +a set of affected species. MultiDict and PriorityQueue are data +structures that can be used for managing transitions. MultiDict is +a mapping from transitions to propensities used in DirectMethod and +related algorithms. Priority Queue is an indexed priority queue as it +appears in Gibson-Bruck like NextReactionMethod's. Both data structures +allow for transitions to be added multiple times, and keep track of +their multiplicity. """ import abc +import warnings +from pqdict import pqdict -from .utils import with_metaclass +from ._utils import with_metaclass from .structures import multiset from .transitions import Event, Reaction -class TrajectorySampler(with_metaclass(abc.ABCMeta, object)): +class DependencyGraph(dict): + """Species-transition dependency graph + + A mapping from species to transitions that are affected by a + change in the respecive species' count. A transition counts as + affected if it appears among the reactants of the transition. + """ + def add_reaction(self, reaction): + """Add reaction to dependencies""" + for reactant in reaction.reactants: + self.setdefault(reactant, set()).add(reaction) + + def remove_reaction(self, reaction): + """Remove reaction from dependencies""" + for reactant in reaction.affected_species: + if reactant in self: + self[reactant].discard(reaction) + if not self[reactant]: + del self[reactant] + + def affected_transitions(self, species): + """Get all transitions affected by a change in any of the given species + + Species is an iterable and the method returns a set of Transition's. + """ + return set( + trans for reactant in species + for trans in self.get(reactant, []) + ) + + +class MultiDict(object): + """Dictionary with multiplicity count + + MultiDict supports DirectMethod like samplers with a dictionary + that maps transitions to propensities. Unlike normal dictionaries + MultiDict keep count of the number of times a key is added, i.e. + its multipliciry. Therefore, the signature of MultiDict is + + transition -> (propensity, multiplicity) + + This class and its interface are not module level implementation + details and are not specified through tests. They might change in + future versions of stocal. + """ + def __init__(self): + self._dict = dict() + + def __contains__(self, item): + return item not in self._dict + + def add_item(self, key, value): + """Add item with associated value. + + If the key has been inserted ealrier, its multiplicity is + increased by one. Otherwise, it is stored with multiplicity + one.""" + if key not in self._dict: + # insert transition with propensity and multiplicity one + self._dict[key] = [value, 1] + else: + # increase multiplicity count + self._dict[key][1] += 1 + + def __delitem__(self, key): + """Delete all instances of the given key.""" + del self._dict[key] + + def keys(self): + """Return a list of all present transition instances.""" + warnings.warn("Method will return an iterator in future versions.", DeprecationWarning) + return [ + key for key, (prop, mult) in self._dict.items() + for i in range(mult) + ] + + def items(self): + """Iterate over key, value, multiplicity triples.""" + for key, item in self._dict.items(): + value, multiplicity = item + yield key, value, multiplicity + + def update_item(self, key, value): + """Change value associated with key""" + self._dict[key][0] = value + + +class PriorityQueue(object): + """Indexed priority queue + + Data structure used for Gibson-Bruck-like transition selection. + See the documentation of pqdict for the general properties of the + data structure. Unlike the standard indexed priority queue, this + implementation allows keys (transitions) to have multiple associated + values. In addition, each instance of a key can have optional + associated data. The exact mapping is therefore + + key -> [(value_1, data_1), (value_2, data_2), ...] + + + Instead of assigning values directly, The PriorityQueue constructor + takes a function to calculate values for a key. Its signature is + + value_callback(key, data) -> float. + + Custom data is passed as keyword arguments to the add_item method. + add_item creates a mutable instance of type PriorityQueue.Item for + each transition and binds keyword arguments to it. This data object + is retrieved by queue access methods and is also passed to the + value_callback. + """ + class Item(object): + """Namespace to hold custom associated data""" + def __init__(self, **params): + for key, value in params.items(): + setattr(self, key, value) + + def __repr__(self): + return '' % ', '.join('%s=%r' % (k, v) + for k, v + in self.__dict__.items()) + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + def __lt__(self, other): + return False + + + def __init__(self, value_callback): + self._queue = pqdict() + self.value_callback = value_callback + self.depletion_value = float('inf') + + def __bool__(self): + return bool(self._queue) + + __nonzero__ = __bool__ + + def __getitem__(self, key): + return self._queue[key] + + def keys(self): + """Return a list of all present transition instances.""" + warnings.warn("Method will return an iterator in future versions.", DeprecationWarning) + return [ + key for key, instances in self._queue.items() + for i in instances + ] + + def topitem(self): + """Yield (value, key, (data,)) triple with lowest value.""" + key, instances = self._queue.topitem() + value, data = instances[0] + return value, key, (data,) + + def add_item(self, key, **params): + """Add instance of key to the queue. + + Any additional keyword arguments are used to populate a + PriorityQueue.Item object that is subsequently passed to + the queue's value_callback and stored together with the value + of the instance. + """ + if key not in self._queue: + self._queue[key] = [] + + data = self.Item(**params) + value = self.value_callback(key, data) + self._queue[key].append((value, data)) + self._queue[key].sort() + self._queue.heapify() + + def remove_item(self, key): + """Remove all instances of key with a value of float('inf')""" + remaining = [t for t in self._queue[key] + if t != self.depletion_value] + if remaining: + self._queue[key] = remaining + else: + del self._queue[key] + + def update_one_instance(self, key): + """Recalculates the value for the 'first' instance of key.""" + instances = self._queue[key] + _, data = instances[0] + instances[0] = self.value_callback(key, data), data + instances.sort() + self._queue.heapify() + + def update_items(self, keys): + """Recalculate values for all provided keys.""" + for key in keys: + instances = sorted( + (self.value_callback(key, data), data) + for value, data in self._queue[key] + ) + self._queue[key] = instances + + +class StochasticSimulationAlgorithm(with_metaclass(abc.ABCMeta, object)): """Abstract base class for stochastic trajectory samplers. This is the interface for stochastic trajectory samplers, i.e. @@ -45,14 +259,21 @@ class TrajectorySampler(with_metaclass(abc.ABCMeta, object)): >>> trajectory = DirectMethod(process, state, steps=10000) >>> for transition in trajectory: ... print trajectory.time, trajectory.state, transition + + When implementing a novel StochasticSimulationAlgorithm, make sure + to only generate random numbers using the + StochasticSimulationAlgorithm.rng random number generator. """ - def __init__(self, process, state, t=0., tmax=float('inf'), steps=None): + def __init__(self, process, state, t=0., tmax=float('inf'), steps=None, seed=None): """Initialize the sampler for the given Process and state. State is a dictionary that maps chemical species to positive integers denoting their copy number. An optional start time - t, end time tmax, and maximal number of steps can be provided. + t, end time tmax, maximal number of steps, and random seed + can be provided. """ + from random import Random + if t < 0: raise ValueError("t must not be negative.") if tmax < 0: @@ -67,22 +288,35 @@ def __init__(self, process, state, t=0., tmax=float('inf'), steps=None): self.steps = steps self.time = t self.tmax = tmax + self.rng = Random(seed) - for transition in self.process.transitions: - self.add_transition(transition) self.state = multiset() self.update_state(state) - def update_state(self, dct): + for transition in self.process.transitions: + self.add_transition(transition) + + @abc.abstractproperty + def transitions(self): + """Return list of all transitions + + Implementations need to return every instance of an added + transition, i.e. two copies of a transition that has + multiplicity two. + """ + return [] + + def update_state(self, mapping): """Modify sampler state. Update system state and infer new applicable transitions. """ + mapping = mapping if isinstance(mapping, multiset) else multiset(mapping) for rule in self.process.rules: - for trans in rule.infer_transitions(dct, self.state): + for trans in rule.infer_transitions(mapping, self.state): trans.rule = rule self.add_transition(trans) - self.state.update(dct) + self.state.update(mapping) @abc.abstractmethod def add_transition(self, transition): @@ -108,7 +342,8 @@ def propose_potential_transition(self): Must return a triple where the first item is the time of the proposed transition, the second item the transition itself, and the last item a tuple of additional arguments that are - passed through to calls to TrajectorySampler.perform_transition. + passed through to calls to + StochasticSimulationAlgorithm.perform_transition. Time and transition have to be picked from the correct probability distributions. """ @@ -116,7 +351,7 @@ def propose_potential_transition(self): def is_applicable(self, time, transition, *args): """True if the transition is applicable - + The standard implementation always returns True, i.e. it assumes that any transition returned by propose_potential_transition is applicable. Overwrite this method if you want to implement @@ -124,7 +359,7 @@ def is_applicable(self, time, transition, *args): """ return True - def perform_transition(self, time, transition): + def perform_transition(self, time, transition, *args): """Perform the given transition. Sets sampler.time to time, increases the number of steps, @@ -133,7 +368,7 @@ def perform_transition(self, time, transition): for every rule of the process. If overwritten by a subclass, the signature can have additional arguments which are populated with the argument tuple returned - by TrajectorySampler.propose_potential_transition. + by StochasticSimulationAlgorithm.propose_potential_transition. """ self.step += 1 self.time = time @@ -147,7 +382,7 @@ def perform_transition(self, time, transition): def reject_transition(self, time, transition, *args): """Do not execute the given transition. - + The default implementation does nothing. Overwrite this method if you, for example, want to prevent the same transition from being proposed again. @@ -156,7 +391,7 @@ def reject_transition(self, time, transition, *args): def has_reached_end(self): """True if given max steps or tmax are reached.""" - return self.step == self.steps or self.time >= self.tmax + return (self.steps is not None and self.step >= self.steps) or self.time > self.tmax def __iter__(self): """Standard interface to sample a stochastic trajectory. @@ -170,7 +405,8 @@ def __iter__(self): Consider to overwrite self.propose_potential_transition, self.is_applicable, self.perform_transition, self.reject_transition or self.has_reached_end in favour of - overloading __iter__ when implementing a TrajectorySampler. + overloading __iter__ when implementing a + StochasticSimulationAlgorithm. """ while not self.has_reached_end(): time, transition, args = self.propose_potential_transition() @@ -188,7 +424,15 @@ def __iter__(self): self.time = self.tmax -class DirectMethod(TrajectorySampler): +class TrajectorySampler(StochasticSimulationAlgorithm): + """Deprecated. Identical to StochasticSimulationAlgorithm""" + def __init__(self, *args, **opts): + warnings.warn("Use StochasticSimulationAlgorithm instead", + DeprecationWarning) + super(TrajectorySampler, self).__init__(*args, **opts) + + +class DirectMethod(StochasticSimulationAlgorithm): """Implementation of Gillespie's direct method. The standard stochastic simulation algorithm, published in @@ -203,51 +447,77 @@ class DirectMethod(TrajectorySampler): calculated for all transitions and time and occuring transition are drawn from the appropriate probability distributions. - See help(TrajectorySampler) for usage information. + See help(StochasticSimulationAlgorithm) for usage information. """ - def __init__(self, process, state, t=0., tmax=float('inf'), steps=None): + def __init__(self, process, state, t=0., tmax=float('inf'), steps=None, seed=None): if any(not isinstance(r, Reaction) for r in process.transitions): raise ValueError("DirectMethod only works with Reactions.") if any(not issubclass(r.Transition, Reaction) for r in process.rules): raise ValueError("DirectMethod only works with Reactions.") - self.transitions = [] - self.propensities = [] - super(DirectMethod, self).__init__(process, state, t, tmax, steps) + self.dependency_graph = DependencyGraph() + self.propensities = MultiDict() + self.depleted = [] + super(DirectMethod, self).__init__(process, state, t, tmax, steps, seed) + + @property + def transitions(self): + return self.propensities.keys() + + def update_state(self, dct): + super(DirectMethod, self).update_state(dct) + affected_transitions = self.dependency_graph.affected_transitions(dct) + self.update_propensities(affected_transitions) def add_transition(self, transition): - self.transitions.append(transition) + self.dependency_graph.add_reaction(transition) + propensity = transition.propensity(self.state) + self.propensities.add_item(transition, propensity) def prune_transitions(self): - depleted = [ - i for i, (p, t) in enumerate(zip(self.propensities, self.transitions)) - if p == 0. and t.rule - ] - for i in reversed(depleted): - del self.transitions[i] - del self.propensities[i] + for trans in self.depleted: + self.dependency_graph.remove_reaction(trans) + del self.propensities[trans] + self.depleted = [] def propose_potential_transition(self): from math import log - from random import random - self.propensities = [r.propensity(self.state) for r in self.transitions] - total_propensity = sum(self.propensities) + total_propensity = sum(mult*prop + for transition, prop, mult + in self.propensities.items()) if not total_propensity: return float('inf'), None, tuple() - delta_t = -log(random())/total_propensity + delta_t = -log(self.rng.random())/total_propensity transition = None - pick = random()*total_propensity - for propensity, transition in zip(self.propensities, self.transitions): - pick -= propensity + pick = self.rng.random()*total_propensity + for transition, prop, mult in self.propensities.items(): + pick -= mult*prop if pick < 0.: break return self.time + delta_t, transition, tuple() + def perform_transition(self, time, transition): + super(DirectMethod, self).perform_transition(time, transition) + affected = self.dependency_graph.affected_transitions(transition.affected_species) + self.update_propensities(affected) + + def update_propensities(self, affected_transitions): + """Update propensities of all given transitions. + + If the new propensity of a transition is 0 and the transition + has been derived by a rule or is an Event, the transition gets + added to self.depleted for later pruning.""" + for trans in affected_transitions: + propensity = trans.propensity(self.state) + if propensity == 0 and (trans.rule or isinstance(trans, Event)): + self.depleted.append(trans) + self.propensities.update_item(trans, propensity) -class FirstReactionMethod(TrajectorySampler): + +class FirstReactionMethod(StochasticSimulationAlgorithm): """Implementation of Gillespie's first reaction method. A stochastic simulation algorithm, published in @@ -256,15 +526,19 @@ class FirstReactionMethod(TrajectorySampler): FirstReactionMethod works with processes that feature deterministic transitions, i.e. Event's. - See help(TrajectorySampler) for usage information. + See help(StochasticSimulationAlgorithm) for usage information. """ - def __init__(self, process, state, t=0., tmax=float('inf'), steps=None): - self.transitions = [] + def __init__(self, process, state, t=0., tmax=float('inf'), steps=None, seed=None): + self._transitions = [] self.firings = [] - super(FirstReactionMethod, self).__init__(process, state, t, tmax, steps) + super(FirstReactionMethod, self).__init__(process, state, t, tmax, steps, seed) + + @property + def transitions(self): + return self._transitions def add_transition(self, transition): - self.transitions.append(transition) + self._transitions.append(transition) def prune_transitions(self): depleted = [ @@ -272,13 +546,13 @@ def prune_transitions(self): if t == float('inf') and (r.rule or isinstance(r, Event)) ] for i in reversed(depleted): - del self.transitions[i] + del self._transitions[i] del self.firings[i] def propose_potential_transition(self): self.firings = [ - (trans.next_occurrence(self.time, self.state), trans, tuple()) - for trans in self.transitions + (trans.next_occurrence(self.time, self.state, self.rng), trans, tuple()) + for trans in self._transitions ] if self.firings: @@ -290,12 +564,12 @@ def is_applicable(self, time, transition, *args): """Returns False for Event's that lack their reactants.""" if isinstance(transition, Event): return transition.reactants <= self.state - else : + else: return super(FirstReactionMethod, self).is_applicable(time, transition, *args) def reject_transition(self, time, transition, *args): """Reject inapplicable Event - + Advance system time to transition time and pretend transition had happened there, but do not change the state. """ @@ -303,7 +577,78 @@ def reject_transition(self, time, transition, *args): transition.last_occurrence = time -class AndersonNRM(FirstReactionMethod): +class NextReactionMethod(FirstReactionMethod): + """Implementation of Gibson & Bruck's next reaction method. + + A stochastic simulation algorithm, published in + M. A. Gibson & J. Bruck, J. Phys. Chem. A 2000, 104, 1876-1889 (1999). + + NextReactionMethod works with processes that feature deterministic + transitions, i.e. Event's. It is an optimized version of Gillespie's + FirstReactionMethod whose runtime scales logarithmically with the + number of reactions. + + See help(StochasticSimulationAlgorithm) for usage information. + """ + def __init__(self, process, state, t=0., tmax=float('inf'), steps=None, seed=None): + self.dependency_graph = DependencyGraph() + self.firings = PriorityQueue(self.calculate_next_occurrence) + self.depleted = [] + super(FirstReactionMethod, self).__init__(process, state, t, tmax, steps, seed) + + @property + def transitions(self): + return self.firings.keys() + + def update_state(self, dct): + super(NextReactionMethod, self).update_state(dct) + affected = self.dependency_graph.affected_transitions(dct) + self.firings.update_items(affected) + + def add_transition(self, transition, **params): + self.dependency_graph.add_reaction(transition) + self.firings.add_item(transition, **params) + + def prune_transitions(self): + for trans in self.depleted: + self.dependency_graph.remove_reaction(trans) + self.firings.remove_item(trans) + self.depleted = [] + + def propose_potential_transition(self): + if self.firings: + return self.firings.topitem() + else: + return float('inf'), None, () + + def perform_transition(self, time, transition, *args): + super(NextReactionMethod, self).perform_transition(time, transition, *args) + self.update_firing_times(time, transition, *args) + + def reject_transition(self, time, transition, *args): + self.time = time + transition.last_occurrence = time + self.firings.update_one_instance(transition) + + def update_firing_times(self, time, transition, *args): + """Update next occurrences of firing and all affected transitions""" + # update affected firing times + affected = self.dependency_graph.affected_transitions(transition.affected_species) + self.firings.update_one_instance(transition) + self.firings.update_items(affected) + # mark depleted reactions + for trans in affected: + if (any(occurrence[0] == float('inf') + for occurrence in self.firings[trans]) + and (trans.rule or isinstance(trans, Event))): + self.depleted.append(trans) + + def calculate_next_occurrence(self, transition, data): + """Calculate next occurrence of given reaction.""" + return transition.next_occurrence(self.time, self.state, self.rng) + + +class AndersonMethod(NextReactionMethod): """Next reaction method modified for time-dependent processes A stochastic simulation algorithm, published in @@ -313,20 +658,92 @@ class AndersonNRM(FirstReactionMethod): This sampler correctly treats non-autonomous transitions, i.e. transitions with time dependent stochastic rates. - See help(TrajectorySampler) for usage information. + See help(StochasticSimulationAlgorithm) for usage information. """ - def __init__(self, process, state, t=0., tmax=float('inf'), steps=None): + def add_transition(self, transition): + """Add transition with own internal clock (T, P)""" + from math import log + super(AndersonMethod, self).add_transition( + transition, + T=0, P=-log(self.rng.random()), t=self.time + ) + + def perform_transition(self, time, transition, data): + from math import log + + def int_a_dt(trans, delta_t): + """Integrate propensity for given delta_t""" + if isinstance(trans, Event): + return 0 + else: + return trans.propensity_integral(self.state, self.time, delta_t) + + data.T = data.P + data.P -= log(self.rng.random()) + data.t = time + + affected = self.dependency_graph.affected_transitions( + transition.affected_species + ) + affected.discard(transition) + + for trans in affected: + for t, data in self.firings[trans]: + data.T += int_a_dt(trans, time-data.t) + data.t = time + + super(AndersonMethod, self).perform_transition(time, transition) + + def calculate_next_occurrence(self, transition, data): + """Determine next firing time of a transition in global time scale""" + target = data.P-data.T + if isinstance(transition, Event): + return transition.next_occurrence(self.time) + else: + return self.time + transition.propensity_meets_target( + self.state, self.time, target) + + def update_state(self, dct): + # XXX AndersonMethod.update_state does not validate against DSMTS. + warnings.warn("""AndersonMethod.update_state behavior is currently invalid. + + If this functionality is vital, use AndersonFRM instead, + which provides a correct but significantly slower implementation. + """, Warning) + super(AndersonMethod, self).update_state(dct) + + +class AndersonNRM(AndersonMethod): + """Deprecated. Identical to AndersonMethod""" + def __init__(self, *args, **opts): + warnings.warn("Use AndersonMethod instead", DeprecationWarning) + super(AndersonMethod, self).__init__(*args, **opts) + + +class AndersonFRM(FirstReactionMethod): + """First reaction method modified for time-dependent processes + + This sampler is an inefficient implementation of Anderson's method, + and will be removed in future versions. + + It should only be used when simulating processes with time-dependent + transitions in situations where + StochasticSimulationAlgorithm.update_state is used, because the + more efficient implementation in AndersonMethod currently fails + validation for this scenario. + """ + def __init__(self, process, state, t=0., tmax=float('inf'), steps=None, seed=None): + warnings.warn("AndersonFRM will be removed in future versions.", DeprecationWarning) self.T = [] self.P = [] - super(AndersonNRM, self).__init__(process, state, t, tmax, steps) + super(AndersonNRM, self).__init__(process, state, t, tmax, steps, seed) def add_transition(self, transition): from math import log - from random import random super(AndersonNRM, self).add_transition(transition) self.T.append(0) - self.P.append(-log(random())) + self.P.append(-log(self.rng.random())) def prune_transitions(self): depleted = [ @@ -358,7 +775,6 @@ def eq_13(trans, target): def perform_transition(self, time, transition, mu): from math import log - from random import random def int_a_dt(trans, delta_t): """Integrate propensity for given delta_t""" @@ -367,7 +783,7 @@ def int_a_dt(trans, delta_t): else: return trans.propensity_integral(self.state, self.time, delta_t) - super(AndersonNRM, self).perform_transition(time, transition) self.T = [Tk+int_a_dt(trans, time-self.time) for Tk, trans in zip(self.T, self.transitions)] - self.P[mu] -= log(random()) + self.P[mu] -= log(self.rng.random()) + super(AndersonNRM, self).perform_transition(time, transition) diff --git a/stocal/examples/brusselator.py b/stocal/examples/brusselator.py index f74629b..056c1f6 100644 --- a/stocal/examples/brusselator.py +++ b/stocal/examples/brusselator.py @@ -13,13 +13,15 @@ a = 2. b = 10. -traj = stocal.Process([ +process = stocal.Process([ stocal.MassAction({}, {"x": 1}, a), stocal.MassAction({"x": 2, "y": 1}, {"x": 3}, 1.), stocal.MassAction({"x": 1}, {"y": 1, "c": 1}, b), stocal.MassAction({"x": 1}, {"d": 1}, 1.), -]).trajectory({}, tmax=50) +]) -print("# time\tx\ty\tc\td") -for transition in traj: - print(traj.time, '\t'.join(str(traj.state[s]) for s in "xycd")) +if __name__ == '__main__': + traj = process.sample({}, tmax=50) + print("# time\tx\ty\tc\td") + for dt, transitions in traj: + print(traj.time, '\t'.join(str(traj.state[s]) for s in "xycd")) diff --git a/stocal/examples/dsmts/DSMTS_001_01-mean.csv b/stocal/examples/dsmts/DSMTS_001_01-mean.csv new file mode 100644 index 0000000..8c44540 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_001_01-mean.csv @@ -0,0 +1,52 @@ +time,X +0,100.00000 +1,99.00498 +2,98.01987 +3,97.04455 +4,96.07894 +5,95.12294 +6,94.17645 +7,93.23938 +8,92.31163 +9,91.39312 +10,90.48374 +11,89.58341 +12,88.69204 +13,87.80954 +14,86.93582 +15,86.07080 +16,85.21438 +17,84.36648 +18,83.52702 +19,82.69591 +20,81.87308 +21,81.05842 +22,80.25188 +23,79.45336 +24,78.66279 +25,77.88008 +26,77.10516 +27,76.33795 +28,75.57837 +29,74.82636 +30,74.08182 +31,73.34470 +32,72.61490 +33,71.89237 +34,71.17703 +35,70.46881 +36,69.76763 +37,69.07343 +38,68.38614 +39,67.70569 +40,67.03200 +41,66.36503 +42,65.70468 +43,65.05091 +44,64.40364 +45,63.76282 +46,63.12836 +47,62.50023 +48,61.87834 +49,61.26264 +50,60.65307 diff --git a/stocal/examples/dsmts/DSMTS_001_01-sd.csv b/stocal/examples/dsmts/DSMTS_001_01-sd.csv new file mode 100644 index 0000000..82d6a56 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_001_01-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0.00000 +1,4.54834 +2,6.38431 +3,7.76081 +4,8.89458 +5,9.87032 +6,10.73185 +7,11.50542 +8,12.20829 +9,12.85255 +10,13.44708 +11,13.99865 +12,14.51256 +13,14.99307 +14,15.44365 +15,15.86721 +16,16.26619 +17,16.64267 +18,16.99845 +19,17.33509 +20,17.65397 +21,17.95630 +22,18.24316 +23,18.51553 +24,18.77427 +25,19.02018 +26,19.25397 +27,19.47628 +28,19.68773 +29,19.88886 +30,20.08018 +31,20.26216 +32,20.43524 +33,20.59981 +34,20.75625 +35,20.90493 +36,21.04615 +37,21.18025 +38,21.30750 +39,21.42818 +40,21.54255 +41,21.65085 +42,21.75330 +43,21.85014 +44,21.94157 +45,22.02777 +46,22.10895 +47,22.18527 +48,22.25691 +49,22.32403 +50,22.38677 diff --git a/stocal/examples/dsmts/DSMTS_001_03-mean.csv b/stocal/examples/dsmts/DSMTS_001_03-mean.csv new file mode 100644 index 0000000..a3beba7 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_001_03-mean.csv @@ -0,0 +1,52 @@ +time,X +0,100.00000 +1,90.48374 +2,81.87308 +3,74.08182 +4,67.03200 +5,60.65307 +6,54.88116 +7,49.65853 +8,44.93290 +9,40.65697 +10,36.78794 +11,33.28711 +12,30.11942 +13,27.25318 +14,24.65970 +15,22.31302 +16,20.18965 +17,18.26835 +18,16.52989 +19,14.95686 +20,13.53353 +21,12.24564 +22,11.08032 +23,10.02588 +24,9.07180 +25,8.20850 +26,7.42736 +27,6.72055 +28,6.08101 +29,5.50232 +30,4.97871 +31,4.50492 +32,4.07622 +33,3.68832 +34,3.33733 +35,3.01974 +36,2.73237 +37,2.47235 +38,2.23708 +39,2.02419 +40,1.83156 +41,1.65727 +42,1.49956 +43,1.35686 +44,1.22773 +45,1.11090 +46,1.00518 +47,0.90953 +48,0.82297 +49,0.74466 +50,0.67379 diff --git a/stocal/examples/dsmts/DSMTS_001_03-sd.csv b/stocal/examples/dsmts/DSMTS_001_03-sd.csv new file mode 100644 index 0000000..b891e7b --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_001_03-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0.00000 +1,13.44708 +2,17.65397 +3,20.08018 +4,21.54255 +5,22.38677 +6,22.80343 +7,22.91234 +8,22.79491 +9,22.50930 +10,22.09848 +11,21.59497 +12,21.02380 +13,20.40447 +14,19.75228 +15,19.07932 +16,18.39517 +17,17.70739 +18,17.02198 +19,16.34367 +20,15.67614 +21,15.02224 +22,14.38416 +23,13.76353 +24,13.16150 +25,12.57890 +26,12.01623 +27,11.47374 +28,10.95151 +29,10.44944 +30,9.96732 +31,9.50482 +32,9.06153 +33,8.63701 +34,8.23073 +35,7.84216 +36,7.47074 +37,7.11588 +38,6.77700 +39,6.45349 +40,6.14478 +41,5.85029 +42,5.56942 +43,5.30164 +44,5.04637 +45,4.80310 +46,4.57129 +47,4.35044 +48,4.14008 +49,3.93972 +50,3.74891 diff --git a/stocal/examples/dsmts/DSMTS_001_04-mean.csv b/stocal/examples/dsmts/DSMTS_001_04-mean.csv new file mode 100644 index 0000000..62c13bc --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_001_04-mean.csv @@ -0,0 +1,52 @@ +time,X +0,10.00000 +1,9.90050 +2,9.80199 +3,9.70446 +4,9.60789 +5,9.51229 +6,9.41765 +7,9.32394 +8,9.23116 +9,9.13931 +10,9.04837 +11,8.95834 +12,8.86920 +13,8.78095 +14,8.69358 +15,8.60708 +16,8.52144 +17,8.43665 +18,8.35270 +19,8.26959 +20,8.18731 +21,8.10584 +22,8.02519 +23,7.94534 +24,7.86628 +25,7.78801 +26,7.71052 +27,7.63380 +28,7.55784 +29,7.48264 +30,7.40818 +31,7.33447 +32,7.26149 +33,7.18924 +34,7.11770 +35,7.04688 +36,6.97676 +37,6.90734 +38,6.83861 +39,6.77057 +40,6.70320 +41,6.63650 +42,6.57047 +43,6.50509 +44,6.44036 +45,6.37628 +46,6.31284 +47,6.25002 +48,6.18783 +49,6.12626 +50,6.06531 diff --git a/stocal/examples/dsmts/DSMTS_001_04-sd.csv b/stocal/examples/dsmts/DSMTS_001_04-sd.csv new file mode 100644 index 0000000..7f801b6 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_001_04-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,1.43831151 +2,2.018895738 +3,2.454182145 +4,2.812712214 +5,3.121268973 +6,3.393708886 +7,3.63833341 +8,3.860600989 +9,4.064332664 +10,4.252340532 +11,4.426761796 +12,4.589274452 +13,4.741224525 +14,4.883711703 +15,5.017653834 +16,5.143821537 +17,5.26287374 +18,5.375380917 +19,5.481835459 +20,5.58267409 +21,5.678279669 +22,5.768994713 +23,5.855125105 +24,5.936947027 +25,6.014709469 +26,6.0886386 +27,6.158941467 +28,6.225807578 +29,6.289410942 +30,6.34991181 +31,6.407458935 +32,6.462189258 +33,6.514230576 +34,6.563703223 +35,6.610717813 +36,6.655377525 +37,6.697782469 +38,6.738022707 +39,6.776184767 +40,6.812351283 +41,6.846598425 +42,6.878998474 +43,6.909621553 +44,6.938532986 +45,6.965793566 +46,6.991464081 +47,7.015599048 +48,7.038253334 +49,7.059476609 +50,7.079319176 diff --git a/stocal/examples/dsmts/DSMTS_001_05-mean.csv b/stocal/examples/dsmts/DSMTS_001_05-mean.csv new file mode 100644 index 0000000..f07ab92 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_001_05-mean.csv @@ -0,0 +1,52 @@ +time,X +0,10000.00000 +1,9900.49800 +2,9801.98700 +3,9704.45500 +4,9607.89400 +5,9512.29400 +6,9417.64500 +7,9323.93800 +8,9231.16300 +9,9139.31200 +10,9048.37400 +11,8958.34100 +12,8869.20400 +13,8780.95400 +14,8693.58200 +15,8607.08000 +16,8521.43800 +17,8436.64800 +18,8352.70200 +19,8269.59100 +20,8187.30800 +21,8105.84200 +22,8025.18800 +23,7945.33600 +24,7866.27900 +25,7788.00800 +26,7710.51600 +27,7633.79500 +28,7557.83700 +29,7482.63600 +30,7408.18200 +31,7334.47000 +32,7261.49000 +33,7189.23700 +34,7117.70300 +35,7046.88100 +36,6976.76300 +37,6907.34300 +38,6838.61400 +39,6770.56900 +40,6703.20000 +41,6636.50300 +42,6570.46800 +43,6505.09100 +44,6440.36400 +45,6376.28200 +46,6312.83600 +47,6250.02300 +48,6187.83400 +49,6126.26400 +50,6065.30700 diff --git a/stocal/examples/dsmts/DSMTS_001_05-sd.csv b/stocal/examples/dsmts/DSMTS_001_05-sd.csv new file mode 100644 index 0000000..4b1d324 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_001_05-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0.00000 +1,45.48345 +2,63.84308 +3,77.60805 +4,88.94577 +5,98.70320 +6,107.31850 +7,115.05420 +8,122.08292 +9,128.52548 +10,134.47081 +11,139.98650 +12,145.12560 +13,149.93068 +14,154.43652 +15,158.67215 +16,162.66192 +17,166.42668 +18,169.98447 +19,173.35086 +20,176.53966 +21,179.56297 +22,182.43163 +23,185.15531 +24,187.74275 +25,190.20181 +26,192.53966 +27,194.76283 +28,196.87732 +29,198.88864 +30,200.80184 +31,202.62164 +32,204.35237 +33,205.99806 +34,207.56252 +35,209.04925 +36,210.46152 +37,211.80248 +38,213.07499 +39,214.28178 +40,215.42546 +41,216.50845 +42,217.53303 +43,218.50142 +44,219.41568 +45,220.27773 +46,221.08951 +47,221.85272 +48,222.56911 +49,223.24025 +50,223.86773 diff --git a/stocal/examples/dsmts/DSMTS_001_07-mean.csv b/stocal/examples/dsmts/DSMTS_001_07-mean.csv new file mode 100644 index 0000000..5f7dbc0 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_001_07-mean.csv @@ -0,0 +1,52 @@ +time,X,Sink +0,100,0 +1,99.00498,10.94518 +2,98.01987,21.78146 +3,97.04455,32.50991 +4,96.07894,43.13162 +5,95.12294,53.64763 +6,94.17645,64.05901 +7,93.23938,74.3668 +8,92.31163,84.57202 +9,91.39312,94.6757 +10,90.48374,104.6788 +11,89.58341,114.5825 +12,88.69204,124.3875 +13,87.80954,134.095 +14,86.93582,143.7059 +15,86.0708,153.2212 +16,85.21438,162.6418 +17,84.36648,171.9687 +18,83.52702,181.2028 +19,82.69591,190.345 +20,81.87308,199.3962 +21,81.05842,208.3573 +22,80.25188,217.2293 +23,79.45336,226.013 +24,78.66279,234.7094 +25,77.88008,243.3191 +26,77.10516,251.8433 +27,76.33795,260.2826 +28,75.57837,268.6379 +29,74.82636,276.9101 +30,74.08182,285.1 +31,73.3447,293.2083 +32,72.6149,301.2361 +33,71.89237,309.1839 +34,71.17703,317.0526 +35,70.46881,324.8431 +36,69.76763,332.556 +37,69.07343,340.1922 +38,68.38614,347.7524 +39,67.70569,355.2374 +40,67.032,362.6479 +41,66.36503,369.9847 +42,65.70468,377.2485 +43,65.05091,384.44 +44,64.40364,391.5599 +45,63.76282,398.609 +46,63.12836,405.588 +47,62.50023,412.4975 +48,61.87834,419.3383 +49,61.26264,426.111 +50,60.65307,432.8163 diff --git a/stocal/examples/dsmts/DSMTS_001_07-sd.csv b/stocal/examples/dsmts/DSMTS_001_07-sd.csv new file mode 100644 index 0000000..7164d45 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_001_07-sd.csv @@ -0,0 +1,52 @@ +time,X,Sink +0,0,0 +1,4.548344,3.134817 +2,6.384308,4.203535 +3,7.760805,4.904559 +4,8.894577,5.430367 +5,9.87032,5.870239 +6,10.73185,6.279785 +7,11.50542,6.698284 +8,12.20829,7.154432 +9,12.85255,7.668587 +10,13.44708,8.254027 +11,13.99865,8.918105 +12,14.51256,9.66352 +13,14.99307,10.48961 +14,15.44365,11.39353 +15,15.86721,12.37115 +16,16.26619,13.41776 +17,16.64267,14.52849 +18,16.99845,15.69862 +19,17.33509,16.92365 +20,17.65397,18.19945 +21,17.9563,19.5222 +22,18.24316,20.88847 +23,18.51553,22.2951 +24,18.77427,23.73924 +25,19.02018,25.2183 +26,19.25397,26.72992 +27,19.47628,28.27195 +28,19.68773,29.8424 +29,19.88886,31.43945 +30,20.08018,33.06144 +31,20.26216,34.7068 +32,20.43524,36.3741 +33,20.59981,38.06199 +34,20.75625,39.76924 +35,20.90492,41.49466 +36,21.04615,43.23716 +37,21.18025,44.9957 +38,21.3075,46.76932 +39,21.42818,48.55709 +40,21.54255,50.35814 +41,21.65084,52.17166 +42,21.7533,53.99685 +43,21.85014,55.83298 +44,21.94157,57.67935 +45,22.02777,59.53527 +46,22.10895,61.40011 +47,22.18527,63.27324 +48,22.25691,65.1541 +49,22.32403,67.0421 +50,22.38677,68.93673 diff --git a/stocal/examples/dsmts/DSMTS_002_01-mean.csv b/stocal/examples/dsmts/DSMTS_002_01-mean.csv new file mode 100644 index 0000000..dd34404 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_01-mean.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,0.9516258 +2,1.812692 +3,2.591818 +4,3.2968 +5,3.934693 +6,4.511884 +7,5.034147 +8,5.50671 +9,5.934303 +10,6.321206 +11,6.671289 +12,6.988058 +13,7.274682 +14,7.53403 +15,7.768698 +16,7.981035 +17,8.173165 +18,8.347011 +19,8.504314 +20,8.646647 +21,8.775436 +22,8.891968 +23,8.997412 +24,9.09282 +25,9.17915 +26,9.257264 +27,9.327945 +28,9.3919 +29,9.449768 +30,9.50213 +31,9.549508 +32,9.592378 +33,9.631168 +34,9.666267 +35,9.698026 +36,9.726763 +37,9.752765 +38,9.776292 +39,9.79758 +40,9.816844 +41,9.834273 +42,9.850044 +43,9.864314 +44,9.877227 +45,9.88891 +46,9.899482 +47,9.909047 +48,9.917703 +49,9.925534 +50,9.93262 diff --git a/stocal/examples/dsmts/DSMTS_002_01-sd.csv b/stocal/examples/dsmts/DSMTS_002_01-sd.csv new file mode 100644 index 0000000..2d02995 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_01-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,0.975513096 +2,1.346362507 +3,1.60991242 +4,1.815709228 +5,1.98360606 +6,2.124119582 +7,2.243690487 +8,2.346638021 +9,2.436042487 +10,2.514200867 +11,2.582883853 +12,2.643493522 +13,2.697161842 +14,2.744818755 +15,2.787238418 +16,2.825072565 +17,2.858874779 +18,2.889119416 +19,2.916215698 +20,2.940518152 +21,2.96233624 +22,2.981940308 +23,2.999568636 +24,3.015430318 +25,3.029711207 +26,3.042575225 +27,3.054168463 +28,3.064620694 +29,3.074047495 +30,3.082552514 +31,3.090227823 +32,3.097156438 +33,3.103412316 +34,3.109062077 +35,3.114165378 +36,3.118775882 +37,3.122941722 +38,3.126706254 +39,3.130108624 +40,3.133184323 +41,3.135964445 +42,3.138477975 +43,3.140750547 +44,3.142805594 +45,3.144663734 +46,3.146344228 +47,3.147863879 +48,3.149238479 +49,3.15048155 +50,3.15160594 diff --git a/stocal/examples/dsmts/DSMTS_002_02-mean.csv b/stocal/examples/dsmts/DSMTS_002_02-mean.csv new file mode 100644 index 0000000..eaa23b9 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_02-mean.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,9.51626 +2,18.12692 +3,25.91818 +4,32.968 +5,39.34693 +6,45.11884 +7,50.34147 +8,55.0671 +9,59.34303 +10,63.21206 +11,66.71289 +12,69.88058 +13,72.74682 +14,75.3403 +15,77.68698 +16,79.81035 +17,81.73165 +18,83.47011 +19,85.04314 +20,86.46647 +21,87.75436 +22,88.91968 +23,89.97412 +24,90.9282 +25,91.7915 +26,92.57264 +27,93.27945 +28,93.919 +29,94.49768 +30,95.0213 +31,95.49508 +32,95.92378 +33,96.31168 +34,96.66267 +35,96.98026 +36,97.26763 +37,97.52765 +38,97.76292 +39,97.9758 +40,98.16844 +41,98.34273 +42,98.50044 +43,98.64314 +44,98.77227 +45,98.8891 +46,98.99482 +47,99.09047 +48,99.17703 +49,99.25534 +50,99.3262 diff --git a/stocal/examples/dsmts/DSMTS_002_02-sd.csv b/stocal/examples/dsmts/DSMTS_002_02-sd.csv new file mode 100644 index 0000000..26ed6d4 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_02-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,3.084843594 +2,4.257572078 +3,5.090990081 +4,5.741776729 +5,6.272713129 +6,6.717055903 +7,7.095172302 +8,7.420720989 +9,7.703442737 +10,7.950601235 +11,8.167795908 +12,8.359460509 +13,8.529174638 +14,8.679879031 +15,8.814021783 +16,8.933663862 +17,9.040555846 +18,9.136197787 +19,9.221883756 +20,9.29873486 +21,9.367729714 +22,9.42972322 +23,9.485468887 +24,9.535627929 +25,9.580788068 +26,9.621467664 +27,9.658128701 +28,9.691181559 +29,9.720991719 +30,9.747886951 +31,9.772158411 +32,9.794068613 +33,9.813851436 +34,9.831717551 +35,9.847855604 +36,9.862435298 +37,9.875608842 +38,9.887513338 +39,9.898272577 +40,9.907998789 +41,9.916790307 +42,9.924738787 +43,9.931925292 +44,9.938423919 +45,9.944299875 +46,9.949614063 +47,9.954419621 +48,9.95876649 +49,9.962697426 +50,9.966253057 diff --git a/stocal/examples/dsmts/DSMTS_002_03-mean.csv b/stocal/examples/dsmts/DSMTS_002_03-mean.csv new file mode 100644 index 0000000..72a0a55 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_03-mean.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,4.75813 +2,9.06346 +3,12.95909 +4,16.484 +5,19.67347 +6,22.55942 +7,25.17073 +8,27.53355 +9,29.67152 +10,31.60603 +11,33.35645 +12,34.94029 +13,36.37341 +14,37.67015 +15,38.84349 +16,39.90517 +17,40.86582 +18,41.73506 +19,42.52157 +20,43.23324 +21,43.87718 +22,44.45984 +23,44.98706 +24,45.4641 +25,45.89575 +26,46.28632 +27,46.63972 +28,46.9595 +29,47.24884 +30,47.51065 +31,47.74754 +32,47.96189 +33,48.15584 +34,48.33134 +35,48.49013 +36,48.63381 +37,48.76382 +38,48.88146 +39,48.9879 +40,49.08422 +41,49.17137 +42,49.25022 +43,49.32157 +44,49.38613 +45,49.44455 +46,49.49741 +47,49.54524 +48,49.58851 +49,49.62767 +50,49.6631 diff --git a/stocal/examples/dsmts/DSMTS_002_03-sd.csv b/stocal/examples/dsmts/DSMTS_002_03-sd.csv new file mode 100644 index 0000000..63e1194 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_03-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,2.18131 +2,3.01056 +3,3.59987 +4,4.06005 +5,4.43548 +6,4.74968 +7,5.01704 +8,5.24724 +9,5.44716 +10,5.62192 +11,5.7755 +12,5.91103 +13,6.03104 +14,6.1376 +15,6.23245 +16,6.31705 +17,6.39264 +18,6.46027 +19,6.52086 +20,6.5752 +21,6.62399 +22,6.66782 +23,6.70724 +24,6.74271 +25,6.77464 +26,6.80341 +27,6.82933 +28,6.8527 +29,6.87378 +30,6.8928 +31,6.90996 +32,6.92545 +33,6.93944 +34,6.95207 +35,6.96349 +36,6.97379 +37,6.98311 +38,6.99153 +39,6.99914 +40,7.00601 +41,7.01223 +42,7.01785 +43,7.02293 +44,7.02753 +45,7.03168 +46,7.03544 +47,7.03884 +48,7.04191 +49,7.04469 +50,7.04721 diff --git a/stocal/examples/dsmts/DSMTS_002_04-mean.csv b/stocal/examples/dsmts/DSMTS_002_04-mean.csv new file mode 100644 index 0000000..a5b71d5 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_04-mean.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,951.6258 +2,1812.692 +3,2591.818 +4,3296.8 +5,3934.693 +6,4511.884 +7,5034.147 +8,5506.71 +9,5934.303 +10,6321.206 +11,6671.289 +12,6988.058 +13,7274.682 +14,7534.03 +15,7768.698 +16,7981.035 +17,8173.165 +18,8347.011 +19,8504.314 +20,8646.647 +21,8775.436 +22,8891.968 +23,8997.412 +24,9092.82 +25,9179.15 +26,9257.264 +27,9327.945 +28,9391.9 +29,9449.768 +30,9502.13 +31,9549.508 +32,9592.378 +33,9631.168 +34,9666.267 +35,9698.026 +36,9726.763 +37,9752.765 +38,9776.292 +39,9797.58 +40,9816.844 +41,9834.273 +42,9850.044 +43,9864.314 +44,9877.227 +45,9888.91 +46,9899.482 +47,9909.047 +48,9917.703 +49,9925.534 +50,9932.62 diff --git a/stocal/examples/dsmts/DSMTS_002_04-sd.csv b/stocal/examples/dsmts/DSMTS_002_04-sd.csv new file mode 100644 index 0000000..f73e6d0 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_04-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,30.84843 +2,42.57572 +3,50.9099 +4,57.41777 +5,62.72713 +6,67.17056 +7,70.95172 +8,74.20721 +9,77.03443 +10,79.50601 +11,81.67796 +12,83.59461 +13,85.29175 +14,86.79879 +15,88.14022 +16,89.33664 +17,90.40556 +18,91.36198 +19,92.21884 +20,92.98735 +21,93.6773 +22,94.29723 +23,94.85469 +24,95.35628 +25,95.80788 +26,96.21468 +27,96.58129 +28,96.91182 +29,97.20992 +30,97.47887 +31,97.72158 +32,97.94069 +33,98.13851 +34,98.31718 +35,98.47856 +36,98.62435 +37,98.75609 +38,98.87513 +39,98.98273 +40,99.07999 +41,99.1679 +42,99.24739 +43,99.31925 +44,99.38424 +45,99.443 +46,99.49614 +47,99.5442 +48,99.58766 +49,99.62697 +50,99.66253 diff --git a/stocal/examples/dsmts/DSMTS_002_06-mean.csv b/stocal/examples/dsmts/DSMTS_002_06-mean.csv new file mode 100644 index 0000000..5bf4415 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_06-mean.csv @@ -0,0 +1,52 @@ +time,X,Source,Sink +0,0,0,0 +1,9.516258,0,0.4837418 +2,18.12692,0,1.873075 +3,25.91818,0,4.081822 +4,32.968,0,7.032005 +5,39.34693,0,10.65307 +6,45.11884,0,14.88116 +7,50.34147,0,19.65853 +8,55.0671,0,24.9329 +9,59.34303,0,30.65697 +10,63.21206,0,36.78794 +11,66.71289,0,43.28711 +12,69.88058,0,50.11942 +13,72.74682,0,57.25318 +14,75.3403,0,64.6597 +15,77.68698,0,72.31302 +16,79.81035,0,80.18965 +17,81.73165,0,88.26835 +18,83.47011,0,96.52989 +19,85.04314,0,104.9569 +20,86.46647,0,113.5335 +21,87.75436,0,122.2456 +22,88.91968,0,131.0803 +23,89.97412,0,140.0259 +24,90.9282,0,149.0718 +25,91.7915,0,158.2085 +26,92.57264,0,167.4274 +27,93.27945,0,176.7206 +28,93.919,0,186.081 +29,94.49768,0,195.5023 +30,95.0213,0,204.9787 +31,95.49508,0,214.5049 +32,95.92378,0,224.0762 +33,96.31168,0,233.6883 +34,96.66267,0,243.3373 +35,96.98026,0,253.0197 +36,97.26763,0,262.7324 +37,97.52765,0,272.4724 +38,97.76292,0,282.2371 +39,97.9758,0,292.0242 +40,98.16844,0,301.8316 +41,98.34273,0,311.6573 +42,98.50044,0,321.4996 +43,98.64314,0,331.3569 +44,98.77227,0,341.2277 +45,98.8891,0,351.1109 +46,98.99482,0,361.0052 +47,99.09047,0,370.9095 +48,99.17703,0,380.823 +49,99.25534,0,390.7447 +50,99.3262,0,400.6738 diff --git a/stocal/examples/dsmts/DSMTS_002_06-sd.csv b/stocal/examples/dsmts/DSMTS_002_06-sd.csv new file mode 100644 index 0000000..3f4278c --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_06-sd.csv @@ -0,0 +1,52 @@ +time,X,Source,Sink +0,0,0,0 +1,3.084843,0,0.6955155 +2,4.257573,0,1.368603 +3,5.09099,0,2.020352 +4,5.741776,0,2.651793 +5,6.272713,0,3.263903 +6,6.717056,0,3.857611 +7,7.095172,0,4.433794 +8,7.420721,0,4.993285 +9,7.703443,0,5.536873 +10,7.950601,0,6.065307 +11,8.167796,0,6.579294 +12,8.35946,0,7.079507 +13,8.529175,0,7.566583 +14,8.67988,0,8.041125 +15,8.814022,0,8.503706 +16,8.933664,0,8.954867 +17,9.040556,0,9.395124 +18,9.136198,0,9.824963 +19,9.221884,0,10.24485 +20,9.298735,0,10.65521 +21,9.36773,0,11.05648 +22,9.429723,0,11.44903 +23,9.485469,0,11.83325 +24,9.535628,0,12.2095 +25,9.580788,0,12.5781 +26,9.621468,0,12.93937 +27,9.658129,0,13.29363 +28,9.691181,0,13.64115 +29,9.720992,0,13.98221 +30,9.747887,0,14.31708 +31,9.772158,0,14.64599 +32,9.794069,0,14.96918 +33,9.813852,0,15.28687 +34,9.831718,0,15.59927 +35,9.847856,0,15.90659 +36,9.862435,0,16.20902 +37,9.875609,0,16.50674 +38,9.887513,0,16.79991 +39,9.898273,0,17.08872 +40,9.907999,0,17.3733 +41,9.91679,0,17.65382 +42,9.924739,0,17.93041 +43,9.931925,0,18.20321 +44,9.938424,0,18.47235 +45,9.9443,0,18.73795 +46,9.949614,0,19.00014 +47,9.95442,0,19.25901 +48,9.958766,0,19.51469 +49,9.962698,0,19.76726 +50,9.966253,0,20.01684 diff --git a/stocal/examples/dsmts/DSMTS_002_09-mean.csv b/stocal/examples/dsmts/DSMTS_002_09-mean.csv new file mode 100644 index 0000000..2b12b12 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_09-mean.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,0.9516258 +2,1.812692 +3,2.591818 +4,3.2968 +5,3.934693 +6,4.511884 +7,5.034147 +8,5.50671 +9,5.934303 +10,6.321206 +11,6.671289 +12,6.988058 +13,7.274682 +14,7.53403 +15,7.768698 +16,7.981035 +17,8.173165 +18,8.347011 +19,8.504314 +20,8.646647 +21,8.775436 +22,8.891968 +23,8.997412 +24,9.09282 +25,50.0 +26,46.1934967214 +27,42.7492301231 +28,39.6327288273 +29,36.8128018414 +30,34.2612263885 +31,31.9524654438 +32,29.8634121517 +33,27.9731585647 +34,26.2627863896 +35,24.7151776469 +36,23.3148433479 +37,22.0477684765 +38,20.9012717214 +39,19.8638785577 +40,18.9252064059 +41,18.0758607198 +42,17.3073409621 +43,16.6119555289 +44,15.9827447689 +45,15.4134113295 +46,14.8982571301 +47,14.4321263345 +48,14.0103537489 +49,13.6287181316 +50,13.283399945 diff --git a/stocal/examples/dsmts/DSMTS_002_09-sd.csv b/stocal/examples/dsmts/DSMTS_002_09-sd.csv new file mode 100644 index 0000000..7985af1 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_09-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,0.975513096 +2,1.346362507 +3,1.60991242 +4,1.815709228 +5,1.98360606 +6,2.124119582 +7,2.243690487 +8,2.346638021 +9,2.436042487 +10,2.514200867 +11,2.582883853 +12,2.643493522 +13,2.697161842 +14,2.744818755 +15,2.787238418 +16,2.825072565 +17,2.858874779 +18,2.889119416 +19,2.916215698 +20,2.940518152 +21,2.96233624 +22,2.981940308 +23,2.999568636 +24,3.015430318 +25,0.0 +26,2.29280593761 +27,3.03862268492 +28,3.49172550791 +29,3.7876580674 +30,3.9833722309 +31,4.11007966445 +32,4.18730987086 +33,4.22827774217 +34,4.24238635423 +35,4.23655679592 +36,4.21600349025 +37,4.18471872556 +38,4.14579218131 +39,4.10163082522 +40,4.05411556169 +41,4.00471603499 +42,3.95457677433 +43,3.90458312839 +44,3.85541258183 +45,3.80757526321 +46,3.76144630283 +47,3.71729193625 +48,3.67529073159 +49,3.6355509595 +50,3.59812487207 \ No newline at end of file diff --git a/stocal/examples/dsmts/DSMTS_002_10-mean.csv b/stocal/examples/dsmts/DSMTS_002_10-mean.csv new file mode 100644 index 0000000..60179f8 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_10-mean.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,0.9516258 +2,1.812692 +3,2.591818 +4,3.2968 +5,3.934693 +6,4.511884 +7,5.034147 +8,5.50671 +9,5.934303 +10,6.321206 +11,6.671289 +12,6.988058 +13,7.274682 +14,7.53403 +15,7.768698 +16,7.981035 +17,8.173165 +18,8.347011 +19,8.504314 +20,8.646647 +21,8.775436 +22,8.891968 +23,19.512294245 +24,18.6070797643 +25,17.7880078307 +26,17.0468808972 +27,16.3762815162 +28,15.7694981038 +29,15.2204577676 +30,14.7236655274 +31,14.2741493195 +32,13.8674102345 +33,13.4993774911 +34,13.1663676938 +35,12.8650479686 +36,12.5924026065 +37,12.3457028809 +38,12.1224797383 +39,11.9204990862 +40,11.7377394345 +41,11.5723716631 +42,11.4227407159 +43,11.2873490359 +44,11.1648415777 +45,11.0539922456 +46,10.9536916222 +47,10.862935865 +48,10.78081666 +49,10.7065121306 +50,10.6392786121 diff --git a/stocal/examples/dsmts/DSMTS_002_10-sd.csv b/stocal/examples/dsmts/DSMTS_002_10-sd.csv new file mode 100644 index 0000000..14dff4c --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_002_10-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0 +1,0.975513096 +2,1.346362507 +3,1.60991242 +4,1.815709228 +5,1.98360606 +6,2.124119582 +7,2.243690487 +8,2.346638021 +9,2.436042487 +10,2.514200867 +11,2.582883853 +12,2.643493522 +13,2.697161842 +14,2.744818755 +15,2.787238418 +16,2.825072565 +17,2.858874779 +18,2.889119416 +19,2.916215698 +20,2.940518152 +21,2.96233624 +22,2.981940308 +23,1.18976715549 +24,1.94697595019 +25,2.37852782966 +26,2.66742850351 +27,2.87139135636 +28,3.01862161091 +29,3.12567143298 +30,3.20328929765 +31,3.25890761428 +32,3.29788384424 +33,3.32419146952 +34,3.34083684417 +35,3.35012656419 +36,3.3538473954 +37,3.35339208561 +38,3.34985009969 +39,3.34407472078 +40,3.3367336972 +41,3.32834810886 +42,3.31932259459 +43,3.30996911282 +44,3.30052577592 +45,3.29117187562 +46,3.28203992665 +47,3.27322535179 +48,3.26479428582 +49,3.25678986925 +50,3.24923732333 diff --git a/stocal/examples/dsmts/DSMTS_003_01-mean.csv b/stocal/examples/dsmts/DSMTS_003_01-mean.csv new file mode 100644 index 0000000..fe015f4 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_003_01-mean.csv @@ -0,0 +1,52 @@ +time,P,P2 +0,100,0 +1,91.031766,4.484117 +2,83.61694,8.19153 +3,77.396987,11.301506 +4,72.115319,13.94234 +5,67.583559,16.208221 +6,63.660332,18.169834 +7,60.237496,19.881252 +8,57.230932,21.384534 +9,54.574231,22.712885 +10,52.214271,23.892864 +11,50.108057,24.945972 +12,48.220414,25.889793 +13,46.522291,26.738854 +14,44.989483,27.505258 +15,43.60166,28.19917 +16,42.341623,28.829189 +17,41.194721,29.402639 +18,40.148402,29.925799 +19,39.191842,30.404079 +20,38.315665,30.842168 +21,37.511705,31.244147 +22,36.77282,31.61359 +23,36.092734,31.953633 +24,35.465912,32.267044 +25,34.887453,32.556273 +26,34.353005,32.823498 +27,33.858686,33.070657 +28,33.401027,33.299486 +29,32.976919,33.511541 +30,32.583564,33.708218 +31,32.218442,33.890779 +32,31.879275,34.060362 +33,31.564003,34.217998 +34,31.270754,34.364623 +35,30.997828,34.501086 +36,30.743674,34.628163 +37,30.506879,34.746561 +38,30.286151,34.856925 +39,30.080307,34.959847 +40,29.888262,35.055869 +41,29.709021,35.145489 +42,29.54167,35.229165 +43,29.385364,35.307318 +44,29.23933,35.380335 +45,29.10285,35.448575 +46,28.975263,35.512368 +47,28.855959,35.57202 +48,28.744373,35.627814 +49,28.639981,35.68001 +50,28.542298,35.728851 diff --git a/stocal/examples/dsmts/DSMTS_003_01-sd.csv b/stocal/examples/dsmts/DSMTS_003_01-sd.csv new file mode 100644 index 0000000..3d1d6f6 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_003_01-sd.csv @@ -0,0 +1,52 @@ +time,P,P2 +0,0,0 +1,3.862504,1.931252 +2,4.815194,2.407597 +3,5.26609,2.633045 +4,5.489485,2.744742 +5,5.592782,2.796391 +6,5.628348,2.814174 +7,5.624594,2.812297 +8,5.597947,2.798974 +9,5.558298,2.779149 +10,5.51176,2.75588 +11,5.462172,2.731086 +12,5.411959,2.705979 +13,5.362641,2.68132 +14,5.315155,2.657577 +15,5.270054,2.635027 +16,5.227638,2.613819 +17,5.188036,2.594018 +18,5.151268,2.575634 +19,5.117279,2.558639 +20,5.085969,2.542984 +21,5.057209,2.528605 +22,5.030854,2.515427 +23,5.006752,2.503376 +24,4.984748,2.492374 +25,4.964691,2.482345 +26,4.946431,2.473216 +27,4.92983,2.464915 +28,4.914752,2.457376 +29,4.901072,2.450536 +30,4.888673,2.444337 +31,4.877445,2.438723 +32,4.867287,2.433644 +33,4.858105,2.429052 +34,4.849811,2.424906 +35,4.842327,2.421163 +36,4.835578,2.417789 +37,4.829498,2.414749 +38,4.824024,2.412012 +39,4.819101,2.409551 +40,4.814677,2.407339 +41,4.810705,2.405353 +42,4.807142,2.403571 +43,4.803949,2.401974 +44,4.80109,2.400545 +45,4.798532,2.399266 +46,4.796247,2.398124 +47,4.794208,2.397104 +48,4.79239,2.396195 +49,4.790771,2.395385 +50,4.789331,2.394665 diff --git a/stocal/examples/dsmts/DSMTS_003_02-mean.csv b/stocal/examples/dsmts/DSMTS_003_02-mean.csv new file mode 100644 index 0000000..6df8801 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_003_02-mean.csv @@ -0,0 +1,52 @@ +time,P,P2 +0,1000,0 +1,833.775625,83.112188 +2,715.495313,142.252343 +3,627.159302,186.420349 +4,558.773544,220.613228 +5,504.344816,247.827592 +6,460.061025,269.969488 +7,423.381776,288.309112 +8,392.548909,303.725545 +9,366.306929,316.846535 +10,343.735327,328.132336 +11,324.143827,337.928087 +12,307.004622,346.497689 +13,291.90722,354.04639 +14,278.527545,360.736227 +15,266.606332,366.696834 +16,255.933678,372.033161 +17,246.337825,376.831088 +18,237.676865,381.161568 +19,229.832516,385.083742 +20,222.705402,388.647299 +21,216.211413,391.894294 +22,210.278879,394.86056 +23,204.846354,397.576823 +24,199.860848,400.069576 +25,195.276425,402.361788 +26,191.053061,404.47347 +27,187.155726,406.422137 +28,183.553628,408.223186 +29,180.21959,409.890205 +30,177.129533,411.435233 +31,174.262054,412.868973 +32,171.598056,414.200972 +33,169.120456,415.439772 +34,166.813919,416.593041 +35,164.664646,417.667677 +36,162.660188,418.669906 +37,160.789281,419.605359 +38,159.041718,420.479141 +39,157.408219,421.295891 +40,155.880339,422.059831 +41,154.450371,422.774815 +42,153.111272,423.444364 +43,151.856593,424.071704 +44,150.680419,424.659791 +45,149.577317,425.211342 +46,148.542288,425.728856 +47,147.570727,426.214636 +48,146.658387,426.670807 +49,145.801342,427.099329 +50,144.995965,427.502018 diff --git a/stocal/examples/dsmts/DSMTS_003_02-sd.csv b/stocal/examples/dsmts/DSMTS_003_02-sd.csv new file mode 100644 index 0000000..9480047 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_003_02-sd.csv @@ -0,0 +1,52 @@ +time,P,P2 +0,0,0 +1,15.287342,7.643671 +2,17.390293,8.695147 +3,17.757485,8.878742 +4,17.556405,8.778202 +5,17.15378,8.57689 +6,16.689629,8.344815 +7,16.222018,8.111009 +8,15.775414,7.887707 +9,15.359448,7.679724 +10,14.976939,7.488469 +11,14.627542,7.313771 +12,14.309493,7.154747 +13,14.020461,7.01023 +14,13.757964,6.878982 +15,13.519576,6.759788 +16,13.303021,6.65151 +17,13.106206,6.553103 +18,12.927231,6.463616 +19,12.764385,6.382193 +20,12.616127,6.308064 +21,12.481076,6.240538 +22,12.357992,6.178996 +23,12.24576,6.12288 +24,12.143381,6.07169 +25,12.049954,6.024977 +26,11.964669,5.982334 +27,11.886793,5.943396 +28,11.815665,5.907833 +29,11.750688,5.875344 +30,11.69132,5.84566 +31,11.63707,5.818535 +32,11.587491,5.793745 +33,11.542179,5.771089 +34,11.500764,5.750382 +35,11.462911,5.731455 +36,11.428313,5.714157 +37,11.396693,5.698347 +38,11.367795,5.683898 +39,11.341387,5.670694 +40,11.317257,5.658628 +41,11.29521,5.647605 +42,11.275069,5.637534 +43,11.256672,5.628336 +44,11.239871,5.619935 +45,11.224529,5.612264 +46,11.210522,5.605261 +47,11.197737,5.598868 +48,11.186069,5.593034 +49,11.175423,5.587711 +50,11.165711,5.582855 diff --git a/stocal/examples/dsmts/DSMTS_003_03-mean.csv b/stocal/examples/dsmts/DSMTS_003_03-mean.csv new file mode 100644 index 0000000..5b463bb --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_003_03-mean.csv @@ -0,0 +1,52 @@ +Time,P,P2 +0,100,0 +1,91.031766,4.484117 +2,83.61694,8.19153 +3,77.396987,11.301506 +4,72.115319,13.94234 +5,67.583559,16.208221 +6,63.660332,18.169834 +7,60.237496,19.881252 +8,57.230932,21.384534 +9,54.574231,22.712885 +10,52.214271,23.892864 +11,50.108057,24.945972 +12,48.220414,25.889793 +13,46.522291,26.738854 +14,44.989483,27.505258 +15,43.60166,28.19917 +16,42.341623,28.829189 +17,41.194721,29.402639 +18,40.148402,29.925799 +19,39.191842,30.404079 +20,38.315665,30.842168 +21,37.511705,31.244147 +22,36.77282,31.61359 +23,36.092734,31.953633 +24,35.465912,32.267044 +25,100,0 +26,91.031766,4.484117 +27,83.61694,8.19153 +28,77.396987,11.301506 +29,72.115319,13.94234 +30,67.583559,16.208221 +31,63.660332,18.169834 +32,60.237496,19.881252 +33,57.230932,21.384534 +34,54.574231,22.712885 +35,52.214271,23.892864 +36,50.108057,24.945972 +37,48.220414,25.889793 +38,46.522291,26.738854 +39,44.989483,27.505258 +40,43.60166,28.19917 +41,42.341623,28.829189 +42,41.194721,29.402639 +43,40.148402,29.925799 +44,39.191842,30.404079 +45,38.315665,30.842168 +46,37.511705,31.244147 +47,36.77282,31.61359 +48,36.092734,31.953633 +49,35.465912,32.267044 +50,34.887453,32.556273 diff --git a/stocal/examples/dsmts/DSMTS_003_03-sd.csv b/stocal/examples/dsmts/DSMTS_003_03-sd.csv new file mode 100644 index 0000000..ed9286e --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_003_03-sd.csv @@ -0,0 +1,52 @@ +Time,P,P2 +0,0,0 +1,3.862504,1.931252 +2,4.815194,2.407597 +3,5.26609,2.633045 +4,5.489485,2.744742 +5,5.592782,2.796391 +6,5.628348,2.814174 +7,5.624594,2.812297 +8,5.597947,2.798974 +9,5.558298,2.779149 +10,5.51176,2.75588 +11,5.462172,2.731086 +12,5.411959,2.705979 +13,5.362641,2.68132 +14,5.315155,2.657577 +15,5.270054,2.635027 +16,5.227638,2.613819 +17,5.188036,2.594018 +18,5.151268,2.575634 +19,5.117279,2.558639 +20,5.085969,2.542984 +21,5.057209,2.528605 +22,5.030854,2.515427 +23,5.006752,2.503376 +24,4.984748,2.492374 +25,0,0 +26,3.862504,1.931252 +27,4.815194,2.407597 +28,5.26609,2.633045 +29,5.489485,2.744742 +30,5.592782,2.796391 +31,5.628348,2.814174 +32,5.624594,2.812297 +33,5.597947,2.798974 +34,5.558298,2.779149 +35,5.51176,2.75588 +36,5.462172,2.731086 +37,5.411959,2.705979 +38,5.362641,2.68132 +39,5.315155,2.657577 +40,5.270054,2.635027 +41,5.227638,2.613819 +42,5.188036,2.594018 +43,5.151268,2.575634 +44,5.117279,2.558639 +45,5.085969,2.542984 +46,5.057209,2.528605 +47,5.030854,2.515427 +48,5.006752,2.503376 +49,4.984748,2.492374 +50,4.964691,2.482345 diff --git a/stocal/examples/dsmts/DSMTS_003_04-mean.csv b/stocal/examples/dsmts/DSMTS_003_04-mean.csv new file mode 100644 index 0000000..5d64c36 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_003_04-mean.csv @@ -0,0 +1,52 @@ +Time,P,P2 +0,100,0 +1,91.0317661320719,4.48411693396405 +2,83.6169402601695,8.19152986991526 +3,77.3969871497467,11.3015064251266 +4,72.1153195361182,13.9423402319409 +5,67.5835703894463,16.2082148052768 +6,63.660661648771,18.1696691756145 +7,60.2416710693044,19.8791644653478 +8,57.2605828335013,21.3697085832494 +9,54.711515143544,22.644242428228 +10,52.673369243377,23.6633153783115 +11,51.3007398825122,24.3496300587439 +12,50.7597354500895,24.6201322749552 +13,51.1355050731977,24.4322474634012 +14,52.3663222469767,23.8168388765116 +15,54.2400408826632,22.8799795586684 +16,56.4473335935565,21.7763332032217 +17,58.6588264799524,20.6705867600238 +18,60.5925140580473,19.7037429709764 +19,62.0533779644583,18.9733110177709 +20,62.9439236173895,18.5280381913053 +21,63.2541358541918,18.3729320729041 +22,63.0414539892798,18.4792730053601 +23,62.4088064474496,18.7955967762752 +24,61.4848600394223,19.2575699802888 +25,60.407558308643,19.7962208456785 +26,59.3105074233145,20.3447462883427 +27,58.3116102093236,20.8441948953382 +28,57.5038922071672,21.2480538964164 +29,56.949037494722,21.525481252639 +30,56.674341386481,21.6628293067595 +31,56.6735079211356,21.6632460394322 +32,56.9111385026634,21.5444307486683 +33,57.3301375833438,21.3349312083281 +34,57.8608234219285,21.0695882890358 +35,58.430382690631,20.7848086546845 +36,58.9714347307008,20.5142826346496 +37,59.4287865538584,20.2856067230708 +38,59.7638534047136,20.1180732976432 +39,59.9566005063138,20.0216997468431 +40,60.005174256496,19.997412871752 +41,59.9236146475454,20.0381926762273 +42,59.7381765307487,20.1309117346257 +43,59.48284675562,20.25857662219 +44,59.1946397883244,20.4026801058378 +45,58.9091972984423,20.5454013507788 +46,58.6571179476566,20.6714410261717 +47,58.4613141355596,20.7693429322202 +48,58.3355470715965,20.8322264642017 +49,58.2841462933224,20.8579268533388 +50,58.3027904004864,20.8486047997568 diff --git a/stocal/examples/dsmts/DSMTS_003_04-sd.csv b/stocal/examples/dsmts/DSMTS_003_04-sd.csv new file mode 100644 index 0000000..4476edd --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_003_04-sd.csv @@ -0,0 +1,52 @@ +Time,P,P2 +0,0,0 +1,3.86250446736244,1.93125223368122 +2,4.8151940036245,2.40759700181225 +3,5.26608969152289,2.63304484576145 +4,5.48948466701417,2.74474233350708 +5,5.59278295368853,2.79639147684426 +6,5.6285725517126,2.8142862758563 +7,5.62969264869447,2.81484632434724 +8,5.64769527211089,2.82384763605544 +9,5.83667015625652,2.91833507812826 +10,6.52003184026817,3.26001592013409 +11,8.00451753919577,4.00225876959788 +12,10.2468682609657,5.12343413048283 +13,12.873652802697,6.43682640134851 +14,15.4242669578986,7.7121334789493 +15,17.518316664537,8.75915833226851 +16,18.9308626576201,9.46543132881007 +17,19.6053788001332,9.80268940006658 +18,19.6228806481661,9.81144032408306 +19,19.1483627961145,9.57418139805725 +20,18.374785192258,9.18739259612902 +21,17.4783840647972,8.7391920323986 +22,16.5921106236088,8.29605531180439 +23,15.7988018671357,7.89940093356785 +24,15.1411041015065,7.57055205075326 +25,14.6403118047299,7.32015590236494 +26,14.3132361707696,7.15661808538481 +27,14.1779879296609,7.08899396483046 +28,14.2465000231212,7.12325001156062 +29,14.5105087104243,7.25525435521214 +30,14.9325273419496,7.46626367097478 +31,15.4493094650835,7.72465473254173 +32,15.9857915499708,7.99289577498539 +33,16.4716210385176,8.2358105192588 +34,16.8535904058155,8.42679520290773 +35,17.1015390637191,8.55076953185957 +36,17.2083959107467,8.60419795537334 +37,17.186227641746,8.59311382087301 +38,17.0602545175946,8.53012725879732 +39,16.8624951633584,8.43124758167921 +40,16.6262655684742,8.31313278423712 +41,16.3822330740512,8.19111653702561 +42,16.1561564292457,8.07807821462285 +43,15.9679484966195,7.98397424830976 +44,15.8314431283757,7.91572156418784 +45,15.7543278181298,7.87716390906489 +46,15.738047905457,7.86902395272848 +47,15.7778705735323,7.88893528676615 +48,15.8634734162274,7.9317367081137 +49,15.9802960680636,7.99014803403179 +50,16.1115740592116,8.05578702960581 diff --git a/stocal/examples/dsmts/DSMTS_003_05-mean.csv b/stocal/examples/dsmts/DSMTS_003_05-mean.csv new file mode 100644 index 0000000..87286c7 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_003_05-mean.csv @@ -0,0 +1,52 @@ +Time,P2 +0,0 +1,4.484117 +2,8.19153 +3,11.301506 +4,13.94234 +5,16.208221 +6,18.169834 +7,19.881252 +8,21.384534 +9,22.712885 +10,23.892864 +11,24.945972 +12,25.889793 +13,26.738854 +14,27.505258 +15,28.19917 +16,28.829189 +17,29.402639 +18,29.925799 +19,30.404079 +20,30.842168 +21,31.244147 +22,31.61359 +23,31.953633 +24,32.267044 +25,32.556273 +26,32.823498 +27,33.070657 +28,33.299486 +29,33.511541 +30,33.708218 +31,33.890779 +32,34.060362 +33,34.217998 +34,34.364623 +35,34.501086 +36,34.628163 +37,34.746561 +38,34.856925 +39,34.959847 +40,35.055869 +41,35.145489 +42,35.229165 +43,35.307318 +44,35.380335 +45,35.448575 +46,35.512368 +47,35.57202 +48,35.627814 +49,35.68001 +50,35.728851 diff --git a/stocal/examples/dsmts/DSMTS_003_05-sd.csv b/stocal/examples/dsmts/DSMTS_003_05-sd.csv new file mode 100644 index 0000000..b26a98a --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_003_05-sd.csv @@ -0,0 +1,52 @@ +Time,P2 +0,0 +1,1.931252 +2,2.407597 +3,2.633045 +4,2.744742 +5,2.796391 +6,2.814174 +7,2.812297 +8,2.798974 +9,2.779149 +10,2.75588 +11,2.731086 +12,2.705979 +13,2.68132 +14,2.657577 +15,2.635027 +16,2.613819 +17,2.594018 +18,2.575634 +19,2.558639 +20,2.542984 +21,2.528605 +22,2.515427 +23,2.503376 +24,2.492374 +25,2.482345 +26,2.473216 +27,2.464915 +28,2.457376 +29,2.450536 +30,2.444337 +31,2.438723 +32,2.433644 +33,2.429052 +34,2.424906 +35,2.421163 +36,2.417789 +37,2.414749 +38,2.412012 +39,2.409551 +40,2.407339 +41,2.405353 +42,2.403571 +43,2.401974 +44,2.400545 +45,2.399266 +46,2.398124 +47,2.397104 +48,2.396195 +49,2.395385 +50,2.394665 diff --git a/stocal/examples/dsmts/DSMTS_004_01-mean.csv b/stocal/examples/dsmts/DSMTS_004_01-mean.csv new file mode 100644 index 0000000..223f45f --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_004_01-mean.csv @@ -0,0 +1,52 @@ +time,X +0, 0.000000 +1, 4.531731 +2, 8.241999 +3,11.279709 +4,13.766776 +5,15.803014 +6,17.470145 +7,18.835076 +8,19.952587 +9,20.867528 +10,21.616618 +11,22.229921 +12,22.732051 +13,23.143161 +14,23.479748 +15,23.755323 +16,23.980945 +17,24.165668 +18,24.316907 +19,24.440731 +20,24.542109 +21,24.625111 +22,24.693067 +23,24.748704 +24,24.794256 +25,24.831551 +26,24.862086 +27,24.887085 +28,24.907553 +29,24.924311 +30,24.938031 +31,24.949264 +32,24.958461 +33,24.965991 +34,24.972156 +35,24.977203 +36,24.981335 +37,24.984719 +38,24.987489 +39,24.989757 +40,24.991613 +41,24.993134 +42,24.994378 +43,24.995397 +44,24.996232 +45,24.996915 +46,24.997474 +47,24.997932 +48,24.998307 +49,24.998614 +50,24.998865 diff --git a/stocal/examples/dsmts/DSMTS_004_01-sd.csv b/stocal/examples/dsmts/DSMTS_004_01-sd.csv new file mode 100644 index 0000000..4bf78ec --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_004_01-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0.000000 +1,4.584292 +2,5.981267 +3,6.798529 +4,7.326114 +5,7.683505 +6,7.933111 +7,8.111385 +8,8.241024 +9,8.336746 +10,8.408379 +11,8.462627 +12,8.504150 +13,8.536236 +14,8.561241 +15,8.580873 +16,8.596387 +17,8.608715 +18,8.618560 +19,8.626454 +20,8.632806 +21,8.637932 +22,8.642079 +23,8.645441 +24,8.648171 +25,8.650392 +26,8.652200 +27,8.653674 +28,8.654875 +29,8.655857 +30,8.656658 +31,8.657312 +32,8.657847 +33,8.658285 +34,8.658643 +35,8.658935 +36,8.659175 +37,8.659371 +38,8.659531 +39,8.659662 +40,8.659770 +41,8.659857 +42,8.659929 +43,8.659988 +44,8.660036 +45,8.660076 +46,8.660108 +47,8.660135 +48,8.660156 +49,8.660174 +50,8.660189 diff --git a/stocal/examples/dsmts/DSMTS_004_02-mean.csv b/stocal/examples/dsmts/DSMTS_004_02-mean.csv new file mode 100644 index 0000000..d15d3ff --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_004_02-mean.csv @@ -0,0 +1,52 @@ +time,X +0,0.000000 +1,8.241999 +2,13.766776 +3,17.470145 +4,19.952587 +5,21.616618 +6,22.732051 +7,23.479748 +8,23.980945 +9,24.316907 +10,24.542109 +11,24.693067 +12,24.794256 +13,24.862086 +14,24.907553 +15,24.938031 +16,24.958461 +17,24.972156 +18,24.981335 +19,24.987489 +20,24.991613 +21,24.994378 +22,24.996232 +23,24.997474 +24,24.998307 +25,24.998865 +26,24.999239 +27,24.999490 +28,24.999658 +29,24.999771 +30,24.999846 +31,24.999897 +32,24.999931 +33,24.999954 +34,24.999969 +35,24.999979 +36,24.999986 +37,24.999991 +38,24.999994 +39,24.999996 +40,24.999997 +41,24.999998 +42,24.999999 +43,24.999999 +44,24.999999 +45,25.000000 +46,25.000000 +47,25.000000 +48,25.000000 +49,25.000000 +50,25.000000 diff --git a/stocal/examples/dsmts/DSMTS_004_02-sd.csv b/stocal/examples/dsmts/DSMTS_004_02-sd.csv new file mode 100644 index 0000000..cc81979 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_004_02-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0.000000 +1,8.378096 +2,10.176120 +3,10.943691 +4,11.307822 +5,11.491567 +6,11.589055 +7,11.643184 +8,11.674503 +9,11.693285 +10,11.704887 +11,11.712220 +12,11.716938 +13,11.720011 +14,11.722031 +15,11.723367 +16,11.724255 +17,11.724846 +18,11.725241 +19,11.725505 +20,11.725681 +21,11.725799 +22,11.725879 +23,11.725932 +24,11.725967 +25,11.725991 +26,11.726007 +27,11.726018 +28,11.726025 +29,11.726030 +30,11.726033 +31,11.726035 +32,11.726036 +33,11.726037 +34,11.726038 +35,11.726039 +36,11.726039 +37,11.726039 +38,11.726039 +39,11.726039 +40,11.726039 +41,11.726039 +42,11.726039 +43,11.726039 +44,11.726039 +45,11.726039 +46,11.726039 +47,11.726039 +48,11.726039 +49,11.726039 +50,11.726039 diff --git a/stocal/examples/dsmts/DSMTS_004_03-mean.csv b/stocal/examples/dsmts/DSMTS_004_03-mean.csv new file mode 100644 index 0000000..130a018 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_004_03-mean.csv @@ -0,0 +1,52 @@ +time,X +0,0.00000 +1,24.54211 +2,24.99161 +3,24.99985 +4,25.00000 +5,25.00000 +6,25.00000 +7,25.00000 +8,25.00000 +9,25.00000 +10,25.00000 +11,25.00000 +12,25.00000 +13,25.00000 +14,25.00000 +15,25.00000 +16,25.00000 +17,25.00000 +18,25.00000 +19,25.00000 +20,25.00000 +21,25.00000 +22,25.00000 +23,25.00000 +24,25.00000 +25,25.00000 +26,25.00000 +27,25.00000 +28,25.00000 +29,25.00000 +30,25.00000 +31,25.00000 +32,25.00000 +33,25.00000 +34,25.00000 +35,25.00000 +36,25.00000 +37,25.00000 +38,25.00000 +39,25.00000 +40,25.00000 +41,25.00000 +42,25.00000 +43,25.00000 +44,25.00000 +45,25.00000 +46,25.00000 +47,25.00000 +48,25.00000 +49,25.00000 +50,25.00000 diff --git a/stocal/examples/dsmts/DSMTS_004_03-sd.csv b/stocal/examples/dsmts/DSMTS_004_03-sd.csv new file mode 100644 index 0000000..3432a36 --- /dev/null +++ b/stocal/examples/dsmts/DSMTS_004_03-sd.csv @@ -0,0 +1,52 @@ +time,X +0,0.00000 +1,35.51939 +2,35.53156 +3,35.53167 +4,35.53168 +5,35.53168 +6,35.53168 +7,35.53168 +8,35.53168 +9,35.53168 +10,35.53168 +11,35.53168 +12,35.53168 +13,35.53168 +14,35.53168 +15,35.53168 +16,35.53168 +17,35.53168 +18,35.53168 +19,35.53168 +20,35.53168 +21,35.53168 +22,35.53168 +23,35.53168 +24,35.53168 +25,35.53168 +26,35.53168 +27,35.53168 +28,35.53168 +29,35.53168 +30,35.53168 +31,35.53168 +32,35.53168 +33,35.53168 +34,35.53168 +35,35.53168 +36,35.53168 +37,35.53168 +38,35.53168 +39,35.53168 +40,35.53168 +41,35.53168 +42,35.53168 +43,35.53168 +44,35.53168 +45,35.53168 +46,35.53168 +47,35.53168 +48,35.53168 +49,35.53168 +50,35.53168 diff --git a/stocal/examples/dsmts/__init__.py b/stocal/examples/dsmts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stocal/examples/dsmts/models.py b/stocal/examples/dsmts/models.py new file mode 100644 index 0000000..fd83270 --- /dev/null +++ b/stocal/examples/dsmts/models.py @@ -0,0 +1,270 @@ +#! /usr/bin/env python3 +"""Perform statistical validation tests of the stocal library +""" +import abc +import stocal +try: + import numpy as np +except ImportError: + logging.error("dsmts.models suite requires numpy.") + sys.exit(1) + + +from stocal._utils import with_metaclass + + +class DSMTS_Test(with_metaclass(abc.ABCMeta, object)): + tmax = 50. + dt = 1. + + def __call__(self, sampler, delta_t=0, tmax=None): + """Sample species along sampler every delta_t time units + + species is a list of species labels that should be sampled. + + Returns a tuple of two elements, the first is a list of all firing + times, the second a dictionary that holds for each species the + list of copy numbers at each corresponding time point. If delta_t + is given, it specifies the interval at which the sampler is sampled. + """ + def every(sampler, delta_t, tmax): + sampler.tmax = sampler.time + while sampler.time < tmax: + transitions = {} + if sampler.steps and sampler.step >= sampler.steps: + break + sampler.tmax += delta_t + for trans in sampler: + transitions[trans] = transitions.get(trans, 0) + 1 + yield transitions + + delta_t = delta_t or self.dt + tmax = tmax if tmax is not None else self.tmax + + times = [sampler.time] + counts = {s: np.array([sampler.state[s]]) for s in self.species} + it = every(sampler, delta_t, tmax) if delta_t else iter(sampler) + for _ in it: + times.append(sampler.time) + for s in self.species: + counts[s] = np.append(counts[s], [sampler.state[s]]) + return np.array(times), counts + + @abc.abstractproperty + def process(self): pass + + @abc.abstractproperty + def initial_state(self): pass + + @abc.abstractproperty + def species(self): pass + + @classmethod + def reported_means(cls): + import os + from csv import DictReader + + dirname = os.path.dirname(__file__) + fname = cls.__name__ + '-mean.csv' + path = os.path.join(dirname, fname) + with open(path) as stats: + reader = DictReader(stats) + species = reader.fieldnames[1:] + times = [] + mean = {s: [] for s in species} + for record in reader: + times.append(record['time']) + for s in species: + mean[s] = np.append(mean[s], [float(record[s])]) + return np.array(times), mean + + @classmethod + def reported_stdevs(cls): + import os + from csv import DictReader + try: + import numpy as np + except ImportError: + logging.error("DSMTS_Test.reported_stdevs requires numpy.") + sys.exit(1) + + dirname = os.path.dirname(__file__) + fname = cls.__name__ + '-sd.csv' + path = os.path.join(dirname, fname) + with open(path) as stats: + reader = DictReader(stats) + species = reader.fieldnames[1:] + times = [] + mean = {s: [] for s in species} + for record in reader: + times.append(record['time']) + for s in species: + mean[s] = np.append(mean[s], [float(record[s])**.5]) + return np.array(times), mean + + +class DSMTS_001_01(DSMTS_Test): + species = ['X'] + process = stocal.Process([ + stocal.MassAction(['X'], ['X', 'X'], 0.1), + stocal.MassAction(['X'], [], 0.11)]) + initial_state = {'X': 100} + + +class DSMTS_001_03(DSMTS_001_01): + process = stocal.Process([ + stocal.MassAction(['X'], ['X', 'X'], 1.), + stocal.MassAction(['X'], [], 1.1)]) + + +class DSMTS_001_04(DSMTS_001_01): + initial_state = {'X': 10} + + +class DSMTS_001_05(DSMTS_001_01): + initial_state = {'X': 10000} + + +class DSMTS_001_07(DSMTS_001_01): + species = ['X', 'Sink'] + process = stocal.Process([ + stocal.MassAction(['X'], ['X', 'X'], 0.1), + stocal.MassAction(['X'], ['Sink'], 0.11)]) + + +class DSMTS_002_01(DSMTS_Test): + species = ['X'] + process = stocal.Process([ + stocal.MassAction([], ['X'], 1.), + stocal.MassAction(['X'], [], 0.1)]) + initial_state = {} + + +class DSMTS_002_02(DSMTS_002_01): + process = stocal.Process([ + stocal.MassAction([], ['X'], 10.), + stocal.MassAction(['X'], [], 0.1)]) + + +class DSMTS_002_03(DSMTS_002_01): + # The original test tests for the overloading of global parameters + # by local parameters. stocal does not have these concepts. + # We merely check whether the process produces reported results + # for an immigration rate of 5. + process = stocal.Process([ + stocal.MassAction([], ['X'], 5.), + stocal.MassAction(['X'], [], 0.1)]) + + +class DSMTS_002_04(DSMTS_002_01): + process = stocal.Process([ + stocal.MassAction([], ['X'], 1000.), + stocal.MassAction(['X'], [], 0.1)]) + + +class DSMTS_002_06(DSMTS_002_01): + species = ['X', 'Sink'] + process = stocal.Process([ + stocal.MassAction([], ['X'], 10.), + stocal.MassAction(['X'], ['Sink'], 0.1)]) + + +class DSMTS_002_09(DSMTS_002_01): + """DSMTS-002-09 + + stocal has no equivalent to SBML Events (stocal.transitions.Event + is something different). Instead, the model's run method samples + times in two blocks, resetting the 'X' between the two blocks. + """ + def __call__(self, sampler, delta_t=0, tmax=None): + # sample until t=24. + times0, counts0 = super(DSMTS_002_09, self).__call__(sampler, tmax=24.) + + # advance sampler to t=25. + sampler.tmax = 25. + for _ in sampler: + pass + + # update state + sampler.update_state({'X': 50}) + + # sample until t=26. + times1, counts1 = super(DSMTS_002_09, self).__call__(sampler, tmax=50.) + + # merge state count dictionaries + counts = { + s: np.append(counts0[s], counts1[s]) + for s in set(counts0).union(counts1) + } + return np.append(times0, times1), counts + + + +class DSMTS_002_10(DSMTS_002_01): + """DSMTS-002-10 + + stocal has no equivalent to SBML Events (stocal.transitions.Event + is something different). Instead, the model's run method samples + times in two blocks, resetting the 'X' between the two blocks. + """ + def __call__(self, sampler, delta_t=0, tmax=None): + # sample until t=22. + times0, counts0 = super(DSMTS_002_10, self).__call__(sampler, tmax=22.) + + # advance sampler to t=22.5 + sampler.tmax = 22.5 + for _ in sampler: + pass + + # update state + sampler.update_state({'X': 20}) + + # advance sampler to t=23. + sampler.tmax = 23. + for _ in sampler: + pass + + # sample until t=26. + times1, counts1 = super(DSMTS_002_10, self).__call__(sampler, tmax=50.) + + # merge state count dictionaries + counts = { + s: np.append(counts0[s], counts1[s]) + for s in set(counts0).union(counts1) + } + return np.append(times0, times1), counts + + +class DSMTS_003_01(DSMTS_Test): + species = ['P', 'P2'] + process = stocal.Process([ + stocal.MassAction(['P', 'P'], ['P2'], 0.001), + stocal.MassAction(['P2'], ['P', 'P'], 0.01)]) + initial_state = {'P': 100} + + +class DSMTS_003_02(DSMTS_003_01): + process = stocal.Process([ + stocal.MassAction(['P', 'P'], ['P2'], 0.0002), + stocal.MassAction(['P2'], ['P', 'P'], 0.004)]) + initial_state = {'P': 1000} + + +class DSMTS_004_01(DSMTS_Test): + species = ['X'] + process = stocal.Process([ + stocal.MassAction([], {'X': 5}, 1.), + stocal.MassAction(['X'], [], 0.2)]) + initial_state = {} + + +class DSMTS_004_02(DSMTS_004_01): + process = stocal.Process([ + stocal.MassAction([], {'X': 10}, 1.), + stocal.MassAction(['X'], [], 0.4)]) + + +class DSMTS_004_03(DSMTS_004_01): + process = stocal.Process([ + stocal.MassAction([], {'X': 100}, 1.), + stocal.MassAction(['X'], [], 4.)]) diff --git a/stocal/examples/events.py b/stocal/examples/events.py index 794285a..9bd4d43 100644 --- a/stocal/examples/events.py +++ b/stocal/examples/events.py @@ -1,8 +1,8 @@ """Event example stocal.Event's can be added to a processes definition just like -Reactions. Process.trajectory returns an TrajectorySampler that -can cope with deterministic transitions (e.g. FirstReactionMethod). +Reactions. Process.trajectory returns an StochasticSimulationAlgorithm +that can cope with deterministic transitions (e.g. FirstReactionMethod). Sampler selection and usage is entirely transparent to the user. """ import stocal @@ -16,6 +16,6 @@ if __name__ == '__main__': - traj = process.trajectory({}, tmax=100) + traj = process.sample({}, tmax=100) for _ in traj: print(traj.time, traj.state['A'], traj.state['A2']) diff --git a/stocal/examples/pre2017.py b/stocal/examples/pre2017.py index cbb6338..2bca34d 100644 --- a/stocal/examples/pre2017.py +++ b/stocal/examples/pre2017.py @@ -35,7 +35,7 @@ initial_state = {c: 200000 for c in 'ab'} -class DegradationRule(stocal.ReactionRule): +class DegradationRule(stocal.TransitionRule): """Break a string into any two nonempty substrings""" Transition = stocal.MassAction @@ -49,7 +49,7 @@ def novel_reactions(self, kl): yield self.Transition([kl], [k, l], 1.) -class LigationRule(stocal.ReactionRule): +class LigationRule(stocal.TransitionRule): """Join any two strings into their concatenations""" Transition = stocal.MassAction @@ -61,7 +61,7 @@ def novel_reactions(self, k, l): yield self.Transition([k, l], [l+k], alpha) -class AutoCatalysisRule(stocal.ReactionRule): +class AutoCatalysisRule(stocal.TransitionRule): """Replicate any string from two matching substrings""" Transition = stocal.MassAction @@ -82,6 +82,6 @@ def novel_reactions(self, k, l, m): if __name__ == '__main__': - traj = process.trajectory(initial_state, tmax=100.) + traj = process.sample(initial_state, tmax=100.) for _ in traj: print(traj.time, traj.state) diff --git a/stocal/examples/temperature_cycle.py b/stocal/examples/temperature_cycle.py index f342eab..bb9b080 100644 --- a/stocal/examples/temperature_cycle.py +++ b/stocal/examples/temperature_cycle.py @@ -19,9 +19,9 @@ dH = -10 # enthalpy change of association dS = dH - dG # entropy change of association -def temp(t, low=0.5, high=1.5, period=50): +def temp(time, low=0.5, high=1.5, period=50): """Temperature cycle""" - return low+(high-low)*(sin(2*pi*t/period)+1)/2. + return low+(high-low)*(sin(2*pi*time/period)+1)/2. class Dissociation(stocal.MassAction): @@ -44,9 +44,9 @@ def propensity(self, state, time): Dissociation(['x2'], ['x', 'x'], k_forward), ]) -state = {'x2': x_tot/3, 'x': x_tot-2*x_tot/3} +state = {'x2': x_tot//3, 'x': x_tot-2*x_tot//3} if __name__ == '__main__': - traj = process.trajectory(state, tmax=125.) + traj = process.sample(state, tmax=125.) for trans in traj: print(traj.time, traj.state['x'], traj.state['x2'], temp(traj.time)) diff --git a/stocal/examples/typed_rules.py b/stocal/examples/typed_rules.py index a522d77..9dc2306 100644 --- a/stocal/examples/typed_rules.py +++ b/stocal/examples/typed_rules.py @@ -22,7 +22,7 @@ # We now define a base class for generic polymerzation. We will derive # typed polymerization rules from this genereic base class. -class Polymerization(stocal.ReactionRule): +class Polymerization(stocal.TransitionRule): """Generic polymerization rule. Polymerization.Transition's are MassAction reactions. @@ -99,6 +99,6 @@ class BA_BB(Polymerization): # And we go in to sample the trajectory of the process... if __name__ == '__main__': - traj = process.trajectory(state, steps=1000) + traj = process.sample(state, steps=1000) for trans in traj: print(traj.time, traj.state) diff --git a/stocal/examples/validation.py b/stocal/examples/validation.py new file mode 100644 index 0000000..7dca3c6 --- /dev/null +++ b/stocal/examples/validation.py @@ -0,0 +1,511 @@ +"""This python script performs validation of the stocal suite + +The module is meant to be run from command line as + +> python stocal/examples/validation.py {run|report} + +see python stocal/examples/validation.py -h + +for more information. + +The script can be used to run validation tests from the DSMTS suite +(by default stocal.examples.dsmts.models) on stochastic simulation +algorithm implementations (by default all algorithms in +stocal.algorithms). Samples trajectories are fed into a DataStore +that performs aggregation to estimate mean and standard deviations +for all points along the trajectory. By default, the path of the +data store is determined from the current git version (via git describe) +but can be changed via command line option. Optional multiprocessing +allows to perform simulations in parallel. + +Simulation results can be visualized (as png or pdf images) using the +report command. The report command also generates a validation.tex file +that can be compiled into a report. +""" +import sys +import os +import logging + +from collections import namedtuple +from math import sqrt + +try: + import numpy as np + import jinja2 +except ImportError: + logging.error("Example validation.py requires numpy and jinja2.") + sys.exit(1) + + +class Stats(namedtuple('_Stats', + ('runs', 'times', 'mean', 'M2', 'conv_mean', 'conv_stdev', 'config'))): + """Simulation result statistics + + Stats collects simulation statistics for use in the DataStore, + where each algorithm/model configuration has one associated + Stats instance. + + Stats groups the number of runs, a sequence of trajectory time + points, together with mean and M2 values for each species in the + model. (M2 is a temporary value used to keep track of standard + deviations. See DataStore docmentation for details) mean and M2 are + dictionaries that map each species identifier to a sequence of + floating point numbers. + + Stats.stdev returns a dictionary of standard deviation sequences + for each species. + """ + @property + def stdev(self): + """Dictionary of standard deviation sequences for each species""" + return { + s: (values/(self.runs-1))**.5 + for s, values in self.M2.items() + } + + +class DataStore(object): + """Persistent store for aggregated data + + This class provides a data store that maintains statistics of + simulation results throughout multiple incarnations of the + validation script. + + A DataStore accepts individual simulation results for given + configurations, and allows retrieval of the aggregated statistics + for a given configuration. + """ + # times at which current aggregate data is memorized + checkpoints = [int(sqrt(10)**n) for n in range(100)][1:] + + def __init__(self, path): + import errno + self.path = path + try: + os.makedirs(self.path) + except OSError as exc: + if exc.errno != errno.EEXIST: + raise + + def __iter__(self): + """Access all data files and statistics in the data store""" + import pickle + for dirpath, _, filenames in os.walk(self.path): + for name in filenames: + fname = os.path.join(dirpath, name) + if fname.endswith('.dat'): + try: + with open(fname, 'rb') as fstats: + config = pickle.load(fstats).config + yield fname, self.get_stats(config) + except Exception as exc: + logging.warn("Could not access data in %s", fname) + logging.info(exc, exc_info=True) + yield fname, None + + def get_path_for_config(self, config): + """Retrieve path of datafile for a given configuration""" + model, algo = config + prefix = '-'.join((algo.__name__, model.__name__)) + return os.path.join(self.path, prefix+'.dat') + + def feed_result(self, result, config): + """Add a single simulation result for a given configuration + + feed_result uses an online algorithm to update mean and + standard deviation with every new result fed into the store. + (The online aggregation is adapted from + https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm) + + At times defined in self.checkpoints, a dump of the current + statistics is memorized in Stats.conv_mean and Stats.conv_stdev. + """ + + import pickle + from shutil import copyfile + from math import log, floor + + fname = self.get_path_for_config(config) + if os.path.exists(fname): + with open(fname, 'rb') as fstats: + stats = pickle.load(fstats) + N = stats.runs + 1 + times = stats.times + delta = { + s: values - stats.mean[s] + for s, values in result[1].items() + } + mean = { + s: values + delta[s]/float(N) + for s, values in stats.mean.items() + } + delta2 = { + s: values - mean[s] + for s, values in result[1].items() + } + M2 = { + s: values + delta[s]*delta2[s] + for s, values in stats.M2.items() + } + conv_mean = stats.conv_mean + conv_stdev = stats.conv_stdev + copyfile(fname, fname+'~') + + else: + N = 1 + times, mean = result + M2 = {s: np.array([0. for _ in mean[s]]) for s in mean} + conv_mean = {} + conv_stdev = {} + + if N in self.checkpoints: + for s, means in mean.items(): + for t, mu, m2 in zip(times, means, M2[s]): + if (s, t) not in conv_mean: + conv_mean[s, t] = np.array([]) + conv_stdev[s, t] = np.array([]) + conv_mean[s, t] = np.append(conv_mean[s, t], [mu]) + conv_stdev[s, t] = np.append(conv_stdev[s, t], [sqrt(m2/(N-1))]) + + with open(fname, 'wb') as outfile: + stats = Stats(N, times, mean, M2, conv_mean, conv_stdev, config) + outfile.write(pickle.dumps(stats)) + + if os.path.exists(fname+'~'): + os.remove(fname+'~') + + def get_stats(self, config): + """Read stats for a given configuration""" + import pickle + fname = self.get_path_for_config(config) + with open(fname, 'rb') as fstats: + return pickle.load(fstats) + + +def run_simulation(Model, Algorithm, max_steps=100000): + """Perform single simulation of Model using Algorithm. + + Returns the result of a single simulation run. + """ + # setup model and algorithm + model = Model() + trajectory = Algorithm(model.process, model.initial_state, + tmax=model.tmax, steps=max_steps) + + # perform simulation + logging.debug("Start simulation of %s with %s.", + Model.__name__, Algorithm.__name__) + result = model(trajectory) + logging.debug("Simulation of %s with %s finished.", + Model.__name__, Algorithm.__name__) + return result + + +def run_in_process(queue, locks, store): + """Worker process for parallel execution of simulations. + + The worker continuously fetches a simulation configuration from + the queue, runs the simulation and feeds the simulation result + into the data store. The worker stops if it fetches a single None + from the queue. + """ + while True: + config = queue.get() + if not config: + break + + try: + result = run_simulation(*config) + except Exception as exc: + logging.warning("Could not run simulation for %s", str(config)) + logging.info(exc, exc_info=True) + + with locks[config]: + try: + store.feed_result(result, config) + except Exception as exc: + logging.warning("Could not store result for %s", str(config)) + logging.info(exc, exc_info=True) + + logging.debug("Worker finished") + + +def run_validation(args): + """Perform validation simulations. + + Run simulations required for the store to hold aggregregated + statistics from args.N samples for each given algorithm and model + combination. If args.model is not given, models classes are + loaded from stocal.examples.dsmts.models. If args.algo is not given, + algorithms are loaded from stocal.algorithms. + + If args.cpu is given and greater than 1, simulations are performed + in parallel. + """ + from multiprocessing import Process, Queue, Lock + from inspect import isclass, isabstract + from itertools import product + + def get_implementations(module, cls): + return [ + member for member in module.__dict__.values() + if isclass(member) + and issubclass(member, cls) + and not isabstract(member) + ] + + # collect algorithms to validate + if not args.algo: + from stocal import algorithms + args.algo = get_implementations(algorithms, algorithms.StochasticSimulationAlgorithm) + + # collect models for validation + if not args.models: + from stocal.examples.dsmts import models as dsmts + args.models = get_implementations(dsmts, dsmts.DSMTS_Test) + + # generate required simulation configurations + def configurations(N): + import random + required = { + config: (max(0, N-args.store.get_stats(config).runs) + if os.path.exists(args.store.get_path_for_config(config)) + else N) + for config in product(args.models, args.algo) + } + configs = list(required) + while configs: + next_config = random.choice(configs) + required[next_config] -= 1 + if not required[next_config]: + configs.remove(next_config) + yield next_config + + if args.cpu > 1: + queue = Queue(maxsize=args.cpu) + locks = { + config: Lock() + for config in product(args.models, args.algo) + } + processes = [Process(target=run_in_process, + args=(queue, locks, args.store)) + for _ in range(args.cpu)] + for proc in processes: + proc.start() + logging.debug("%d processes started." % args.cpu) + for config in configurations(args.N): + queue.put(config) + logging.debug("All jobs requested.") + for _ in processes: + queue.put(None) + logging.debug("Shutdown signal sent.") + queue.close() + for proc in processes: + proc.join() + else: + for config in configurations(args.N): + result = run_simulation(*config) + args.store.feed_result(result, config) + logging.debug("Done.") + + +def report_validation(args, frmt='png', template='doc/validation.tex'): + """Generate figures for all results in args.store""" + def camel_case_split(identifier): + import re + matches = re.finditer('.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', identifier) + return ' '.join(m.group(0) for m in matches) + + figures = {} + + # generate figures for the entire data store + for fname, stats in args.store: + if stats: + figname = fname[:-len('.dat')]+'.'+ frmt + if not os.path.exists(figname) or os.path.getmtime(fname) > os.path.getmtime(figname): + # only if .dat newer than + logging.debug("Generate figure for %s", fname) + try: + generate_figure(stats, figname) + except Exception as exc: + logging.warning("Could not generate figure for %s", fname) + logging.info(exc, exc_info=True) + + model, algo = stats.config + algo_name = camel_case_split(algo.__name__).replace('_', ' ') + figures[algo_name] = sorted(figures.get(algo_name, [])+[figname]) + + # populate latex template + latex_jinja_env = jinja2.Environment( + block_start_string = '\BLOCK{', + block_end_string = '}', + variable_start_string = '\VAR{', + variable_end_string = '}', + comment_start_string = '\#{', + comment_end_string = '}', + line_statement_prefix = '%%', + line_comment_prefix = '%#', + trim_blocks = True, + autoescape = False, + loader = jinja2.FileSystemLoader(os.path.abspath('.')) + ) + template = latex_jinja_env.get_template(template) + context = { + 'version': os.path.basename(args.store.path).replace('_', '\_'), + 'methods': figures, + } + reportfile = args.reportfile or 'validation-%s.tex' % context['version'] + with open(reportfile, 'w') as report: + report.write(template.render(**context)) + +def generate_figure(stats, fname): + """Generate figure for given stats and save it to fname.""" + try: + from matplotlib import pyplot as plt + except ImportError: + logging.error("Example validation.py requires matplotlib.") + sys.exit(1) + + model, algo = stats.config + rep_times, rep_means = model.reported_means() + rep_times, rep_stdevs = model.reported_stdevs() + Ns = DataStore.checkpoints[:len(list(stats.conv_mean.values())[0])] + + fig = plt.figure(figsize=plt.figaspect(.3)) + title = '%s %s (%d samples)' % (model.__name__, algo.__name__, stats.runs) + fig.suptitle(title) + colormaps = [plt.cm.winter, plt.cm.copper] + + ax = fig.add_subplot(131) + plt.title("simulation results") + for (species, mu), cm in zip(stats.mean.items(), colormaps): + low = mu - stats.stdev[species]**.5 + high = mu + stats.stdev[species]**.5 + + rep_low = rep_means[species] - rep_stdevs[species] + rep_high = rep_means[species] + rep_stdevs[species] + + ax.fill_between(stats.times, rep_low, rep_high, facecolor=cm(0), alpha=0.3) + ax.fill_between(stats.times, low, high, facecolor=cm(0.99), alpha=0.3) + ax.plot(rep_times, rep_means[species], + color=cm(0), label=r'$\mathregular{%s_{exp}}$' % species) + ax.plot(stats.times, mu, + color=cm(0.99), label=r'$\mathregular{%s_{sim}}$' % species, alpha=.67) + plt.xlabel('time') + plt.ylabel('# molecules') + plt.legend(loc=0) + + ax = fig.add_subplot(132) + plt.title("convergence toward mean") + ax.set_xscale('log') + ax.set_yscale('log') + for s, cm in zip(stats.mean, colormaps): + ax.set_prop_cycle(plt.cycler('color', + (cm(x/stats.times[-1]) for x in stats.times))) + for t in stats.times: + exp = rep_means[s][int(t)] + ys = [abs((sim-exp)/rep_stdevs[s][int(t)]**2) + if rep_stdevs[s][int(t)] + else 0 + for sim in stats.conv_mean[s, t]] + if not all(ys): + continue + if t in stats.times[:2] or t == max(stats.times): + ax.plot(Ns, ys, alpha=0.67, label="time = %.1f" % t) + else: + ax.plot(Ns, ys, alpha=0.67) + ymin, ymax = plt.gca().get_ylim() + ax.plot(Ns, [3/sqrt(n) for n in Ns], color='r', label="bound") + plt.xlabel('samples N') + plt.ylabel('normalized error') + plt.legend(loc=3) + + ax = fig.add_subplot(133) + plt.title("convergence toward std. dev.") + ax.set_xscale('log') + ax.set_yscale('log') + for s, cm in zip(stats.mean, colormaps): + ax.set_prop_cycle(plt.cycler('color', (cm(x/stats.times[-1]) for x in stats.times))) + for t in stats.times: + exp = rep_stdevs[s][int(t)] + ys = [abs(sim/exp**2-1) if exp else 0 for sim in stats.conv_stdev[s, t]] + if not all(ys): + continue + if t in stats.times[:2] or t == max(stats.times): + ax.plot(Ns, ys, alpha=0.67, label="time = %.1f" % t) + else: + ax.plot(Ns, ys, alpha=0.67) + ymin, ymax = plt.gca().get_ylim() + ax.plot(Ns, [5/sqrt(n/2.) for n in Ns], color='r', label="bound") + plt.xlabel('samples N') + plt.ylabel('normalized error') + plt.legend(loc=3) + + fig.savefig(fname) + plt.close() + + +if __name__ == '__main__': + import argparse + import subprocess + + def import_by_name(name): + """import and return a module member given by name + + e.g. 'stocal.algorithms.DirectMethod' will return the class + + """ + from importlib import import_module + module, member = name.rsplit('.', 1) + mod = import_module(module) + return getattr(mod, member) + + git_label = subprocess.check_output(["git", "describe"]).strip() + default_store = os.path.join('validation_data', git_label) + + parser = argparse.ArgumentParser( + prog=sys.argv[0], + description="Perform statistical validation tests.", + epilog="""If --dir is provided, it specifies a directory used to + hold validation data.""") + # global options + parser.add_argument('--dir', dest='store', + type=DataStore, + default=DataStore(default_store), + help='directory for/with simulation results') + + subparsers = parser.add_subparsers(help='validation sub-command') + + # parser for the "run" command + parser_run = subparsers.add_parser('run', help='run simulations to generate validation data') + parser_run.add_argument('N', + type=int, + help='number of simulations to be performed in total') + parser_run.add_argument('--algo', + type=import_by_name, + action='append', + help='specify algorithm to be validated') + parser_run.add_argument('--model', + type=import_by_name, + action='append', + dest='models', + help='specify model to be validated') + parser_run.add_argument('--cpu', metavar='N', + type=int, + default=1, + help='number of parallel processes') + parser_run.set_defaults(func=run_validation) + + # parser for the "report" command + parser_report = subparsers.add_parser('report', help='generate figures from generated data') + parser_report.add_argument('--file', + action='store', + dest='reportfile', + default='', + help='file name of generated report') + parser_report.set_defaults(func=report_validation) + + # parse and act + logging.basicConfig(level=logging.INFO) + args = parser.parse_args() + args.func(args) diff --git a/stocal/experimental/__init__.py b/stocal/experimental/__init__.py new file mode 100644 index 0000000..21861cc --- /dev/null +++ b/stocal/experimental/__init__.py @@ -0,0 +1,27 @@ +# Copyright 2018 Harold Fellermann +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Experimental stocal modules + +Experimental modules provide functionality that is not yet part of the +stable stocal framework. Experimental module API's are allowed to change +in minor release updates without any prior announcement. +""" diff --git a/stocal/experimental/samplers.py b/stocal/experimental/samplers.py new file mode 100644 index 0000000..93eca4c --- /dev/null +++ b/stocal/experimental/samplers.py @@ -0,0 +1,417 @@ +# Copyright 2018 Harold Fellermann +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Chainable process samplers + +This module is currently experimental. Its API might change without +notification, and there is no guarantee that it will be adopted by +the stable stocal core. + +The samplers module provides a convenient framework to sample +stocal.Process instances. The framework is based on Sampler instances +that can be chained to customize the way a trajectory is sampled. The +simplest way to obtain a Sampler for a given process is to call the +processes sample method: + +>>> sampler = process.sample(initial_state, tstart=0) + +Process.sample is a factory method that selects and initializes an +appropriate stochastic simulation algorithm depending on the properties +of the given process. + +The sampler obtained this way simply gives access to each iteration of +the algorithm: + +>>> for time, state, transitions in sampler: +>>> print(time, state) + +The yielded triple gives information of system time and state, plus +a dictionary that counts all transitions that occurred since the last +iteration (having only one key with value one in the above scenario). + + +All samplers expose methods that fine-tune the way a trajectory is +sampled. For example, to sample a trajectory until a given stop time +is reached, use: + +>>> process.sample(initial_state).until(time=stop_time) + +To sample only the first n steps, use: + +>>> process.sample(initial_state).until(steps=n) + +This will return samplers augmented with the given stop criteria. + + +By default, samplers yield one datapoint for each Transition generated +by the simulation algorithm. To only yield datapoints after m steps, +use: + +>>> process.sample(initial_state).every(steps=m).until(steps=n) + +or, to return a data point every dt time units, use: + +>>> process.sample(initial_state).every(time=dt).until(steps=n) + + +Iterating over Sampler.every will yield the system state at the given +intervals. If you instead want to obtain the average system state over +the sampled interval, iterate over: + +>>> process.sample(initial_state).average(steps=m) + +or + +>>> process.sample(initial_state).average(time=dt) + +Now, the yielded state is the average state over the entire sampled +interval. In the first case, each state has the same weight in the +average. In the second case, states are weighed by the time they +occupied in the sampled interval. + + +To obtain data points only after certain Transition's have taken place, +use: + +>>> process.sample(initial_state).filter([tran_a, trans_b, ...]) + + +Note that the order in which Sampler's are chained is significant: + +>>> process.sample(initial_state).average(steps=10).filter([trans]) + +will average the system states in blocks of ten and yield only those +blocks that feature trans. + +>>> process.sample(initial_state).filter([trans]).average(steps=10) + +in contrast, will take only system states ater each trans transition +and calculate averages of these states. + +See the Sampler documentation for a full explanation of Sampler factory +methods. + +# XXX Chaining of Samplers as described in the doc is currently broken +""" +import abc +try: + from itertools import izip as zip + range = xrange +except ImportError: + pass + +import stocal.transitions +from stocal._utils import with_metaclass +from stocal.structures import multiset + +StandardProcess = stocal.transitions.Process + +class Process(StandardProcess): + """Process class that supports the new sampler protocol + + Importing the module performs a monkey patch that replaces + stocal.transitions.Process with the samplers.Process class. + """ + def sample(self, state, tstart=0., tmax=float('inf'), steps=None, every=None, seed=None): + """Create trajectory sampler for given state + + The method selects a suitable stochastic simulation algorithm + given the Process'es Transition's and Rule's, and wraps this + algorithm into a Sampler, specified via optional arguments. + If tmax is supplied, the sampler will terminate when the sampler + reaches the given time. If steps is specified, the sampler + terminates after the given number of steps instead. Both + stop criteria can be supplied to stop at whichever event occurs + first. If tmax or steps (bot not both) are given, every can be + used to specify a sampling interval. In conjunction with tmax, + the sampler will yield trajectory states in given time intervals. + In conjunction with steps, the sampler will yield results after + a given number of steps. + """ + algorithm = super(Process, self).trajectory(state, tstart=tstart, seed=seed) + sampler = _Wrapper(algorithm) + + # instantiate requested sampling method + if every is not None: + if tmax != float('inf') and steps is not None: + raise ValueError("every can only be provided when either steps or tmax is given, not both.") + elif tmax != float('inf'): + return sampler.every(time=every).until(time=tmax) + elif steps is not None: + if not isinstance(every, int): + raise ValueError("every must be an integer in combination with steps.") + return sampler.every(steps=every).until(steps=steps) + else: + raise ValueError("every can only be used in conjunction with a stop criterion.") + else: + if tmax != float('inf') and steps is not None: + return sampler.until(time=tmax, steps=steps) + elif tmax != float('inf'): + return sampler.until(time=tmax) + elif steps is not None: + return sampler.until(steps=steps) + else: + return sampler + + +# monkey patch stocal.Process +stocal.Process = Process +stocal.transitions.Process = Process + + +class Sampler(with_metaclass(abc.ABCMeta, object)): + """Abstract base class for Samplers + + Concretizations have to provide an __iter__ method. + """ + def __init__(self, sampler): + self.sampler = sampler + self.algorithm = sampler.algorithm + + @abc.abstractmethod + def __iter__(self): + """Iterator support. + + All Sampler classes need to provide iterators that yield + information about the sampled trajectory, namely a triple + (time, state, transitions), where time and state are the + system time and state and transitions is a dictionary counting + all transitions having occurred within one iteration. + """ + raise StopIteration + + def __getattr__(self, attr): + """Delegate attribute lookup through sampler chain.""" + return getattr(self.sampler, attr) + + def until(self, time=float('inf'), steps=None): + """Return sampler with given stop criterion.""" + if time != float('inf') and steps != None: + return UntilTimeSampler(self, time).until(steps=steps) + elif time != float('inf'): + return UntilTimeSampler(self, time) + elif steps: + return UntilStepSampler(self, steps) + else: + raise ValueError("Either time or steps must be given.") + + def every(self, time=float('inf'), steps=None): + """Return sampler that yields every given time or steps.""" + if time != float('inf') and steps != None: + raise ValueError("time and steps cannot both be given.") + elif time != float('inf'): + return EveryTimeSampler(self, time) + elif steps: + return EveryStepSampler(self, steps) + else: + raise ValueError("Either time or steps must be given.") + + def average(self, time=float('inf'), steps=None): + """Return sampler that averages over given time or steps.""" + if time != float('inf') and steps != None: + raise ValueError("time and steps cannot both be given.") + elif time != float('inf'): + return AverageTimeSampler(self, time) + elif steps: + return AverageStepSampler(self, steps) + else: + raise ValueError("Either time or steps must be given.") + + def filter(self, transitions): + """Return sampler that yields only if any given transition occurred.""" + return FilteredSampler(self, transitions) + + +class _Wrapper(Sampler): + """Wrapper to use StochasticSimulationAlgorithm with Sampler interface + + This class only exists for transition to the new interface and will + be removed when no longer necessary. Please do not use explicitly + in productive code. + """ + def __init__(self, sampler): + self.sampler = sampler + self.algorithm = sampler + + def __iter__(self): + traj = self.sampler + for transition in traj: + yield traj.time, traj.state, { transition: 1} + + +class UntilTimeSampler(Sampler): + """Sample until time equals given final time + + Iterate over the underlying sampler until sampler.time + equals the given final time. + """ + def __init__(self, sampler, time): + super(UntilTimeSampler, self).__init__(sampler) + self.tmax = time + + def __iter__(self): + while True: + time, transition, args = self.propose_potential_transition() # TODO: needs to iterate over self.sampler! + + if time > self.tmax: + break + else: + self.perform_transition(time, transition, *args) + yield time, self.state, transition + self.algorithm.time = self.tmax + + +class UntilStepSampler(Sampler): + """Sample for given number of steps + + Iterate over the underlying sampler for a given number of steps. + """ + def __init__(self, sampler, steps): + super(UntilStepSampler, self).__init__(sampler) + self.steps = steps + + def __iter__(self): + for n, data in zip(range(self.steps), self.sampler): + yield data + + +class EveryTimeSampler(Sampler): + """Yield samples every dt time units + + Iterates over the underlying sampler and returns time, state + and transitions every dt time units. If initialized with skip=True, + the sampler skips time points where no system change has occurred. + """ + def __init__(self, sampler, time, skip=False): + super(EveryTimeSampler, self).__init__(sampler) + self.dt = time + self.skip = skip + + def __iter__(self): + algorithm = self.algorithm + time = self.sampler.time + transitions = multiset() + while True: + ptime, trans, args = algorithm.propose_potential_transition() # TODO: needs to iterate over self.sampler! + if ptime > time+self.dt: + time += self.dt + yield time, self.state, transitions + transitions = multiset() + + if ptime == float('inf'): + break + while self.skip and time+self.dt < ptime: + time += self.dt + + transitions[trans] += 1 + self.algorithm.perform_transition(ptime, trans, *args) + + +class EveryStepSampler(Sampler): + """Yield samples every n steps + + Iterates over the underlying sampler and returns time, state + and transitions every n steps. + """ + def __init__(self, sampler, steps): + super(EveryStepSampler, self).__init__(sampler) + self.steps = steps + + def __iter__(self): + while True: + transitions = multiset() + for n, data in zip(range(self.steps), self.sampler): + transitions += data[2] + if not transitions: + break + yield self.time, self.state, transitions + + +class AverageTimeSampler(EveryTimeSampler): + """Average samples over dt time units + + Iterates over the underlying sampler and averages the system state + over the given period of time. When averaging, each state is weighed + by the time that it persisted. + """ + def __iter__(self): + algorithm = self.algorithm + time = self.sampler.time + transitions = multiset() + averages = multiset() + while True: + ptime, trans, args = algorithm.propose_potential_transition() # TODO: needs to iterate over self.sampler! + + if ptime > time+self.dt: + time += self.dt + yield time, 1./self.dt*averages, transitions + if ptime == float('inf'): + break + transitions = multiset() + averages = multiset() + while ptime > time+self.dt: + time += self.dt + if not self.skip: + yield time, self.state, {} + + transitions[trans] += 1 + averages += (ptime-max(time, algorithm.time))*algorithm.state + algorithm.perform_transition(ptime, trans, *args) + + +class AverageStepSampler(EveryStepSampler): + """Average samples over dt time units + + Iterates over the underlying sampler and averages system state + over the given number of steps. Each state has equal qeight in the + average. + """ + def __iter__(self): + while True: + transitions = multiset() + averages = multiset() + for n, data in zip(range(self.steps), self.sampler): + transitions += data[2] + averages += data[1] + if not transitions: + break + yield self.time, 1./self.steps*averages, transitions + + +class FilteredSampler(Sampler): + """Yield samples only when one of the given transitions occurred. + + Iterates over the underlying sampler and returns time, state + and transitions if one of the given transitions occurred. + + TODO: In addition to Transition instances, this sampler could + accept Transition classes, Rule instances and Rule classes and + yield after any transition that equals any given Transition class, + isinstance of any given Transition class, or has been infered from + any given Rule instance or class. + """ + def __init__(self, sampler, transitions): + super(FilteredSampler, self).__init__(sampler) + self.transitions = transitions + + def __iter__(self): + for time, state, transitions in self.sampler: + if any(trans in transitions for trans in self.transitions): + yield time, state, transitions diff --git a/stocal/experimental/tauleap.py b/stocal/experimental/tauleap.py new file mode 100644 index 0000000..e4fef30 --- /dev/null +++ b/stocal/experimental/tauleap.py @@ -0,0 +1,245 @@ +"""tau leaping + +This module provides an approximate stochastic simulation algorithm +based on tau-leaping, as derived in + +Efficient step size selection for the tau-leaping simulation method +Y. Cao, D. T. Gillespie, L. R. Petzold, J. Chem. Phys. 124, 044109 (2006) + +""" +import sys +import logging +from math import log +try: + from numpy.random import RandomState +except ImportError: + logging.error("stocal.experimental.tauleap requires numpy.") + sys.exit(1) + +from stocal.algorithms import DirectMethod +from stocal.structures import multiset + + +class CaoMethod(DirectMethod): + n_crit = 10 + tauleap_threshold = 10 + micro_steps = 100 + + def __init__(self, process, state, epsilon=0.03, t=0., tmax=float('inf'), steps=None, seed=None): + super(CaoMethod, self).__init__(process, state, t=t, tmax=tmax, steps=steps, seed=seed) + self.num_reactions = 0 + self.epsilon = epsilon + self.rng2 = RandomState(seed) + self.abandon_tauleap = -1 + + def __iter__(self): + while not self.has_reached_end(): + # step 1: partition reactions -- Eq. (10) + Jcrit, Jncr = self.identify_critical_reactions() + + Irs = set(s for trans in self.transitions for s in trans.reactants) + Incr = set(s for trans in Jncr for s in trans.reactants) + + if Incr: + # step 2: determine noncritical tau -- Eqs. (32) and (33) + mu = {s: sum(trans.stoichiometry.get(s, 0)*a + for trans, a in Jncr.items()) for s in Incr} + var = {s: sum(trans.stoichiometry.get(s, 0)**2*a + for trans, a in Jncr.items()) for s in Incr} + eps = {s: max(self.epsilon*self.state[s]*self.gi(s), 1.) for s in Incr} + + tau_ncr1 = min((eps[s]/abs(mu[s])) if mu[s] else float('inf') for s in Incr) + tau_ncr2 = min((eps[s]**2/abs(var[s])) if var[s] else float('inf') for s in Incr) + tau_ncr = min((tau_ncr1, tau_ncr2, self.tmax-self.time)) + else: + tau_ncr = float('inf') + + a0 = sum(mult*prop for _, prop, mult in self.propensities.items()) + + if not a0: + break + + while True: + # step 3: abandon tau leaping if not enough expected gain + if tau_ncr <= self.tauleap_threshold / a0: + if self.abandon_tauleap == -1: + self.abandon_tauleap = self.step + it = DirectMethod.__iter__(self) + for _ in range(self.micro_steps): + trans = next(it) + self.num_reactions += 1 + yield self.time, self.state, {trans: 1} + break + elif self.abandon_tauleap != -1: + logging.debug("Abandoned tau-leaping for %d steps" % (self.step-self.abandon_tauleap)) + self.abandon_tauleap = -1 + + # step 4: determine critical tau + ac = sum(propensity for trans, propensity in Jcrit.items()) + tau_crit = -log(self.rng.random())/ac if ac else float('inf') + + # step 5: determine actual tau + tau = min((tau_ncr, tau_crit, self.tmax-self.time)) + + # step 5a + firings = {trans: self.rng2.poisson(propensity*tau) + for trans, propensity in Jncr.items()} + firings = {trans: n for trans, n in firings.items() if n} + + # step 5b + if tau == tau_crit: + # fire exactly one ciritical reaction + transition = None + pick = self.rng.random()*ac + for transition, propensity in Jcrit.items(): + pick -= propensity + if pick < 0.: + break + firings[transition] = 1 + + new_reactions = sum(firings.values()) + + # avoid overshooting self.steps + if self.steps and self.step+new_reactions > self.steps: + tau_ncr /= 2 + continue + + all_reactants = sum((n*trans.true_reactants + for trans, n in firings.items()), multiset()) + all_products = sum((n*trans.true_products + for trans, n in firings.items()), multiset()) + net_reactants = all_reactants - all_products + net_products = all_products - all_reactants + + # step 6a: avoid negative copy numbers + if any(self.state[s]= 2 else 3 + elif order == 3 and trans.reactants[species] == 1: + g = max(g, 3) + elif order == 3 and trans.reactants[species] == 2: + g = max(g, 1.5*(2+(self.state[species]-1)**-1)) if self.state[species] >= 2 else 4.5 + elif order == 3 and trans.reactants[species] == 3: + g = max(g, 3+(self.state[species]-1)**-1+(self.state[species]-2)**-1) if self.state[species] >= 3 else 5.5 + else: + raise RuntimeError("Tau-leaping not implemented for reactions of order %d" % order) + return g + + +if sys.argv[0].endswith('stocal/examples/validation.py'): + # Provide some specialized methods with fixed epsilon values + + class CaoMethod_003(CaoMethod): + def __init__(self, process, state, t=0., tmax=float('inf'), steps=None, seed=None): + super(CaoMethod_003, self).__init__(process, state, 0.03, t=t, tmax=tmax, steps=steps, seed=seed) + + class CaoMethod_001(CaoMethod): + def __init__(self, process, state, t=0., tmax=float('inf'), steps=None, seed=None): + super(CaoMethod_001, self).__init__(process, state, 0.01, t=t, tmax=tmax, steps=steps, seed=seed) + + class CaoMethod_0003(CaoMethod): + def __init__(self, process, state, t=0., tmax=float('inf'), steps=None, seed=None): + super(CaoMethod_0003, self).__init__(process, state, 0.003, t=t, tmax=tmax, steps=steps, seed=seed) + + class CaoMethod_0001(CaoMethod): + def __init__(self, process, state, t=0., tmax=float('inf'), steps=None, seed=None): + super(CaoMethod_0001, self).__init__(process, state, 0.001, t=t, tmax=tmax, steps=steps, seed=seed) + + class CaoMethod_00003(CaoMethod): + def __init__(self, process, state, t=0., tmax=float('inf'), steps=None, seed=None): + super(CaoMethod_00003, self).__init__(process, state, 0.0003, t=t, tmax=tmax, steps=steps, seed=seed) + + class CaoMethod_00001(CaoMethod): + def __init__(self, process, state, t=0., tmax=float('inf'), steps=None, seed=None): + super(CaoMethod_00001, self).__init__(process, state, 0.0001, t=t, tmax=tmax, steps=steps, seed=seed) + + +# testing +import unittest +from stocal.tests.test_algorithms import TestTrajectorySampler + +class TestCaoMethod(TestTrajectorySampler): + """Test CaoMethod + + This tests the regular StochasticSimulationAlgorithm interface.""" + Sampler = CaoMethod + + @unittest.skip("Sampler does not adhere to specification") + def test_add_transition_enables_transition(self): + self.fail("CaoMethod violates current StochasticSimulationAlgorithm specification.") + + @unittest.skip("Sampler does not adhere to specification") + def test_update_state_enables_infered(self): + self.fail("CaoMethod violates current StochasticSimulationAlgorithm specification.") + + @unittest.skip("Sampler does not adhere to specification") + def test_update_state_enables_static(self): + self.fail("CaoMethod violates current StochasticSimulationAlgorithm specification.") + + +if __name__ == '__main__': + unittest.main() diff --git a/stocal/experimental/test_samplers.py b/stocal/experimental/test_samplers.py new file mode 100644 index 0000000..8f8b503 --- /dev/null +++ b/stocal/experimental/test_samplers.py @@ -0,0 +1,300 @@ +"""Tests for the stocal.experimental.samplers module""" +import unittest +import stocal.experimental.samplers +from stocal.experimental.samplers import Process + +class TestProcess(unittest.TestCase): + Process = Process + + def test_sample_arguments(self): + """Process.sample can be called with optional arguments""" + proc = self.Process([]) + proc.sample({}) + proc.sample({}, tstart=1.) + proc.sample({}, tmax=1.) + proc.sample({}, steps=100) + proc.sample({}, tmax=1., steps=100) + proc.sample({}, tmax=1., every=0.1) + proc.sample({}, steps=100, every=10) + with self.assertRaises(ValueError): + proc.sample({}, every=0.5) + with self.assertRaises(ValueError): + proc.sample({}, steps=100, every=0.5) + with self.assertRaises(ValueError): + proc.sample({}, steps=100, tmax=10., every=1) + proc.sample({}, seed=10) + + +class TestSampler(unittest.TestCase): + Sampler = stocal.experimental.samplers.Sampler + + def test_iter_returns_triple(self): + """Sampler.__iter__ returns time, state, dict triple""" + process = Process([stocal.Event([], ['a'], 1.)]) + sampler = process.sample({}) + result = next(iter(sampler)) + self.assertEqual(len(result), 3) + self.assertIsInstance(result[0], float) + self.assertIsInstance(result[1], stocal.multiset) + self.assertIsInstance(result[2], dict) + + def test_iter_yields_stop_when_empty(self): + """Sampler.__iter__ raises StopIteration for empty process""" + process = Process([]) + with self.assertRaises(StopIteration): + next(iter(process.sample({}))) + + def test_until_time_returns_correct_sampler(self): + """Sampler.until(time) returns UntilTimeSampler""" + process = Process([stocal.Event([], ['a'], 1., 10.)]) + sampler = process.sample({}).until(time=1.) + self.assertIsInstance(sampler, stocal.experimental.samplers.UntilTimeSampler) + + def test_until_steps_returns_correct_sampler(self): + """Sampler.until(steps) returns UntilStepSampler""" + process = Process([stocal.Event([], ['a'], 1., 10.)]) + sampler = process.sample({}).until(steps=10) + self.assertIsInstance(sampler, stocal.experimental.samplers.UntilStepSampler) + + def test_every_time_returns_correct_sampler(self): + """Sampler.every(time) returns EveryTimeSampler""" + process = Process([stocal.Event([], ['a'], 1., 10.)]) + sampler = process.sample({}).every(time=1.) + self.assertIsInstance(sampler, stocal.experimental.samplers.EveryTimeSampler) + + def test_every_steps_returns_correct_sampler(self): + """Sampler.every(steps) returns EveryStepSampler""" + process = Process([stocal.Event([], ['a'], 1., 10.)]) + sampler = process.sample({}).every(steps=10) + self.assertIsInstance(sampler, stocal.experimental.samplers.EveryStepSampler) + + def test_average_time_returns_correct_sampler(self): + """Sampler.average(time) returns AverageTimeSampler""" + process = Process([stocal.Event([], ['a'], 1., 10.)]) + sampler = process.sample({}).average(time=1.) + self.assertIsInstance(sampler, stocal.experimental.samplers.AverageTimeSampler) + + def test_average_steps_returns_correct_sampler(self): + """Sampler.average(steps) returns AverageStepSampler""" + process = Process([stocal.Event([], ['a'], 1., 10.)]) + sampler = process.sample({}).average(steps=10) + self.assertIsInstance(sampler, stocal.experimental.samplers.AverageStepSampler) + + def test_filter_returns_correct_sampler(self): + """Sampler.filter() returns FilteredSampler""" + process = Process([stocal.Event([], ['a'], 1., 10.)]) + sampler = process.sample({}).filter([stocal.MassAction]) + self.assertIsInstance(sampler, stocal.experimental.samplers.FilteredSampler) + + def test_state_returns_trajectory_state(self): + """Sampler.state returns trajectory state""" + process = Process([stocal.Event([], ['a'], 1., 10.)]) + state = {} + sampler = process.sample(state) + self.assertEqual(sampler.state, state) + + def test_time_returns_trajectory_time(self): + """Sampler.time returns trajectory time""" + process = Process([stocal.Event([], ['a'], 1., 10.)]) + sampler = process.sample({}) + self.assertIsInstance(sampler.time, float) + + +class TestUntilTimeSampler(TestSampler): + Sampler = stocal.experimental.samplers.UntilTimeSampler + + def test_iter_advances_empty(self, tmax=10.): + """Sampler.__iter__ advances to end time for empty processes""" + process = Process() + traj = process.sample({}) + sampler = self.Sampler(traj, tmax) + for _ in sampler: + pass + self.assertEqual(sampler.time, tmax) + + def test_iter_advances_long(self, tmax=10.): + """Sampler.__iter__ advances to end time for long lasting processes""" + process = Process([stocal.Event([], ['a'], 1., 100.)]) + traj = process.sample({}) + sampler = self.Sampler(traj, tmax) + for _ in sampler: + pass + self.assertEqual(sampler.time, tmax) + + def test_iter_advances_short(self, tmax=10.): + """Sampler.__iter__ advances to end time for short lasting processes""" + process = Process([stocal.Event(['a'], [], 1., 1.)]) + traj = process.sample({'a': 3}) + sampler = self.Sampler(traj, tmax) + for _ in sampler: + pass + self.assertEqual(sampler.time, tmax) + + def test_iter_includes_all_transitions_at_tmax(self, tmax=1.): + """Sampler.__iter__ includes all events that happen at tmax""" + process = Process([ + stocal.Event([], ['a'], tmax), + stocal.Event([], ['b'], 0., tmax), + stocal.Event([], ['c'], tmax/2, tmax/2)]) + traj = process.sample({}) + sampler = self.Sampler(traj, tmax) + for _ in sampler: + pass + self.assertEqual(sampler.step, 5) + + +class TestUntilStepSampler(TestSampler): + Sampler = stocal.experimental.samplers.UntilStepSampler + + def test_iter_number_of_steps(self, steps=10): + """Sampler.__iter__ yields exact number of steps""" + process = Process([stocal.MassAction([], ['a'], 1.)]) + traj = process.sample({}) + sampler = self.Sampler(traj, steps) + for _ in sampler: + pass + self.assertEqual(sampler.step, steps) + + def test_iter_empty_does_not_proceed(self, steps=10): + """Sampler.__iter__ does not increase steps for empty process""" + process = Process([]) + traj = process.sample({}) + sampler = self.Sampler(traj, steps) + for _ in sampler: + pass + self.assertEqual(sampler.step, 0) + + +class TestEveryTimeSampler(TestSampler): + class Sampler(stocal.experimental.samplers.EveryTimeSampler): + def __init__(self, state, skip=False): + super(TestEveryTimeSampler.Sampler, self).__init__(state, time=1., skip=skip) + + def test_init_optional_skip(self): + """Sampler.__init__ accepts optional skip argument""" + sampler = Process().sample({}) + self.Sampler(sampler, skip=True) + + def test_iter_yield_times(self): + """Sampler.__iter__ yields for each interval""" + process = Process([stocal.Event([], ['a'], 0.5, 2.4)]) + target = [1., 2., 3., 4., 5.] + sampler = self.Sampler(process.sample({})) + for a,b in zip((result[0] for result in sampler), target): + self.assertEqual(a, b) + + def test_iter_skipping_behavior(self): + """Sampler.__iter__ skips empty iterations if initialized with skip=True""" + process = Process([stocal.Event([], ['a'], 0.5, 2.4)]) + target = [1., 3., 6., 8., 11.] + sampler = self.Sampler(process.sample({}), skip=True) + for a,b in zip((result[0] for result in sampler), target): + self.assertEqual(a, b) + + def test_iter_performs_all_transitions(self, target=20): + """Sampler.__iter__ performs all transitions""" + process = Process([stocal.MassAction(['a'], [], .1)]) + sampler = self.Sampler(process.sample({'a': target}), skip=True) + total = 0 + for time, state, trans in sampler: + total += sum(trans.values()) + self.assertEqual(total, target) + + def test_iter_works_when_chained(self): + """test that every and until can be chained""" + process = Process([stocal.MassAction(['a'], [], .1)]) + sampler = self.Sampler(process.sample({'a': 20})) + sampler = sampler.until(time=5) + time, trans, state = next(iter(sampler)) + self.assertEqual(time, 1.) + + +class TestEveryStepSampler(TestSampler): + class Sampler(stocal.experimental.samplers.EveryStepSampler): + def __init__(self, state): + super(TestEveryStepSampler.Sampler, self).__init__(state, steps=10) + + def test_iter_yield_times(self): + """Sampler.__iter__ yields for each interval""" + process = Process([stocal.Event([], ['a'], 3., 3.)]) + target = [30., 60., 90.] + sampler = self.Sampler(process.sample({})) + for a,b in zip((result[0] for result in sampler), target): + self.assertEqual(a, b) + + def test_iter_performs_all_transitions(self, target=100): + """Sampler.__iter__ performs all transitions""" + process = Process([stocal.MassAction(['a'], [], 1.)]) + sampler = self.Sampler(process.sample({'a': target})) + total = 0 + for time, state, trans in sampler: + total += sum(trans.values()) + self.assertEqual(total, target) + + +class TestAverageTimeSampler(TestEveryTimeSampler): + class Sampler(stocal.experimental.samplers.AverageTimeSampler): + def __init__(self, state, skip=False): + super(TestAverageTimeSampler.Sampler, self).__init__(state, time=1., skip=skip) + + def test_iter_correct_averages(self): + """Sampler.__iter__ calculates correct averages""" + process = Process([ + stocal.Event([], ['a'], 0., 3.), + stocal.Event([], ['a'], 1., 2.)]) + target = [1., 2., 2., 4., 4., 5. ,6., 7., 7., 9., 9., 10., 11.] + sampler = self.Sampler(process.sample({})) + for a,b in zip((result[1]['a'] for result in sampler), target): + self.assertAlmostEqual(a, b) + + def test_iter_correct_skipped_averages(self): + """Sampler.__iter__ calculates correct averages""" + process = Process([ + stocal.Event([], ['a'], 0., 3.), + stocal.Event([], ['a'], 1., 2.)]) + target = [1., 2., 4., 5. ,6., 7., 9., 10., 11.] + sampler = self.Sampler(process.sample({}), skip=True) + for a,b in zip((result[1]['a'] for result in sampler), target): + self.assertAlmostEqual(a, b) + + +class TestAverageStepSampler(TestEveryStepSampler): + class Sampler(stocal.experimental.samplers.AverageStepSampler): + def __init__(self, state, skip=False): + super(TestAverageStepSampler.Sampler, self).__init__(state, steps=10) + + def test_iter_correct_averages(self): + """Sampler.__iter__ calculates correct averages""" + process = Process([stocal.Event([], ['a'], 0., 3.)]) + target = [5.5, 15.5, 25.5] + sampler = self.Sampler(process.sample({})) + for a,b in zip((result[1]['a'] for result in sampler), target): + self.assertAlmostEqual(a, b) + + +class TestFilteredSampler(TestSampler): + class Sampler(stocal.experimental.samplers.FilteredSampler): + def __init__(self, state, transitions=None): + super(TestFilteredSampler.Sampler, self).__init__( + state, transitions or []) + + def test_iter_correct_transitions(self): + """Sampler.__iter__ only yields after declared transitions""" + r1 = stocal.MassAction(['a'], ['b'], 1.) + r2 = stocal.MassAction(['b'], ['c'], 1.) + process = Process([r1, r2]) + sampler = self.Sampler(process.sample({'a':50, 'b':50}), [r1]) + for result in sampler: + self.assertIn(r1, result[2]) + + def test_iter_empty_filter_list(self): + """Sampler.__iter__ advances to simulation end when transitions are empty""" + process = Process([stocal.MassAction(['a'], [''], 1.)]) + sampler = self.Sampler(process.sample({'a':10}), []) + with self.assertRaises(StopIteration): + next(iter(sampler)) + self.assertEqual(sampler.state['a'], 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/stocal/tests/bugs.py b/stocal/tests/bugs.py index dd0b718..037bac9 100644 --- a/stocal/tests/bugs.py +++ b/stocal/tests/bugs.py @@ -54,14 +54,14 @@ def test_expanding_anderson_nrm(self): it = iter(traj) try: - trans = it.next() + trans = next(it) except StopIteration: self.fail("Static event not fired.") self.assertEqual(traj.time, 1) self.assertEqual(traj.state, {'a': 1}) try: - trans = it.next() + trans = next(it) except StopIteration: self.fail("Infered event not fired.") self.assertEqual(traj.time, 10) diff --git a/stocal/tests/test_algorithms.py b/stocal/tests/test_algorithms.py index aba221a..5b296c8 100644 --- a/stocal/tests/test_algorithms.py +++ b/stocal/tests/test_algorithms.py @@ -96,6 +96,13 @@ def test_propose_potential_transition_empty(self): self.assertEqual(time, float('inf')) self.assertEqual(transition, None) + def test_propose_potential_transition_seed(self): + """Samplers initialized with the same random seed propose equal transitions""" + process = stocal.Process([stocal.MassAction([], ['a'], 1.)]) + sampler_a = self.Sampler(process, {'a':100}, seed=10) + sampler_b = self.Sampler(process, sampler_a.state, seed=10) + self.assertEqual(sampler_a.propose_potential_transition(), sampler_b.propose_potential_transition()) + def test_propose_potential_transition_in_finite_time(self): """Proposed (time,transition) for empty process is (inf,None)""" process = stocal.Process([stocal.MassAction([], ['a'], 1.)]) @@ -108,21 +115,24 @@ def test_perform_transition_advances_steps(self): transition = stocal.MassAction([], ['a'], 1.) process = stocal.Process([transition]) sampler = self.Sampler(process, {}, tmax=100.) - sampler.perform_transition(1., transition) + time, trans, args = sampler.propose_potential_transition() + sampler.perform_transition(time, trans, *args) self.assertEqual(sampler.step, 1) def test_perform_transition_advances_time(self): transition = stocal.MassAction([], ['a'], 1.) process = stocal.Process([transition]) sampler = self.Sampler(process, {}, tmax=100.) - sampler.perform_transition(1., transition) - self.assertEqual(sampler.time, 1.) + time, trans, args = sampler.propose_potential_transition() + sampler.perform_transition(time, trans, *args) + self.assertGreater(sampler.time, 0.) def test_perform_transition_changes_state(self): transition = stocal.MassAction([], ['a'], 1.) process = stocal.Process([transition]) sampler = self.Sampler(process, {}, tmax=100.) - sampler.perform_transition(1., transition) + time, trans, args = sampler.propose_potential_transition() + sampler.perform_transition(time, trans, *args) self.assertEqual(sampler.state, {'a':1}) def test_iter_empty(self): @@ -162,6 +172,14 @@ def test_iter_steps_and_tmax(self): self.assertEqual(sampler.step, 0) self.assertEqual(sampler.time, 0.) + def test_transitions_counts_mutliplicities(self): + """Sampler.transitions should give access to all transitions.""" + proc = stocal.Process() + sampler = self.Sampler(proc, {}) + sampler.add_transition(stocal.MassAction({}, {'a':1}, 1.)) + sampler.add_transition(stocal.MassAction({}, {'a':1}, 1.)) + sampler.add_transition(stocal.MassAction({}, {'b':1}, 1.)) + self.assertEqual(len(sampler.transitions), 3) class TestDirectMethod(TestTrajectorySampler): @@ -191,6 +209,18 @@ def test_iter_simultaneous_events(self): self.assertEqual(sampler.step, 2) self.assertEqual(sampler.time, 10) + def test_iter_includes_all_events_at_tmax(self): + proc = stocal.Process([ + stocal.Event({}, {'a':1}, 10, 10), + stocal.Event({}, {'b':1}, 0, 10), + ]) + sampler = self.Sampler(proc, {}, tmax=10) + for _ in sampler: + pass + self.assertEqual(sampler.step, 3) + self.assertEqual(sampler.time, 10) + self.assertEqual(sampler.state, {'a':1, 'b':2}) + def test_exact_number_of_events(self): """sampler performs specified number of events""" proc = stocal.Process([ @@ -240,30 +270,16 @@ def test_do_not_apply_inapplicable_events(self): self.assertEqual(traj.state, stocal.structures.multiset({})) -class TestAndersonNRM(TestFirstReactionMethod): - """Test stocal.algorithms.AndersonNRM""" - Sampler = stocal.algorithms.AndersonNRM +class TestNextReactionMethod(TestFirstReactionMethod): + """Test stocal.algorithms.DirectMethod - def test_perform_transition_advances_steps(self): - transition = stocal.MassAction([], ['a'], 1.) - process = stocal.Process([transition]) - sampler = self.Sampler(process, {}, tmax=100.) - sampler.perform_transition(1., transition, 0) - self.assertEqual(sampler.step, 1) + This tests the regular TrajectorySampler interface.""" + Sampler = stocal.algorithms.NextReactionMethod - def test_perform_transition_advances_time(self): - transition = stocal.MassAction([], ['a'], 1.) - process = stocal.Process([transition]) - sampler = self.Sampler(process, {}, tmax=100.) - sampler.perform_transition(1., transition, 0) - self.assertEqual(sampler.time, 1.) - def test_perform_transition_changes_state(self): - transition = stocal.MassAction([], ['a'], 1.) - process = stocal.Process([transition]) - sampler = self.Sampler(process, {}, tmax=100.) - sampler.perform_transition(1., transition, 0) - self.assertEqual(sampler.state, {'a':1}) +class TestAndersonNRM(TestFirstReactionMethod): + """Test stocal.algorithms.AndersonNRM""" + Sampler = stocal.algorithms.AndersonNRM if __name__ == '__main__': diff --git a/stocal/tests/test_examples.py b/stocal/tests/test_examples.py index 10abf92..8064ac2 100644 --- a/stocal/tests/test_examples.py +++ b/stocal/tests/test_examples.py @@ -4,7 +4,9 @@ """ import unittest import sys -from stocal.tests.test_transitions import TestReactionRule, TestMassAction +import os + +from stocal.tests.test_transitions import TestReactionRule as TestTransitionRule, TestMassAction from stocal.examples.pre2017 import DegradationRule from stocal.examples.pre2017 import LigationRule @@ -13,16 +15,11 @@ class TestBrusselator(unittest.TestCase): """Test examples.brusselator""" - def setUp(self): - self.stdout = sys.stdout - sys.stdout = open('/dev/null', 'w') - - def tearDown(self): - sys.stdout = self.stdout - def test_example(self): - """test running the module""" - import stocal.examples.brusselator + """test process instantiation""" + from stocal.examples.brusselator import process + for _ in process.sample({}, steps=100): + pass class TestEvents(unittest.TestCase): @@ -30,7 +27,7 @@ class TestEvents(unittest.TestCase): def test_example(self): """test process instantiation""" from stocal.examples.events import process - for _ in process.trajectory({}, steps=100): + for _ in process.sample({}, steps=100): pass @@ -39,11 +36,11 @@ class TestPre2017(unittest.TestCase): def test_example(self): """test process instantiation""" from stocal.examples.pre2017 import process - for _ in process.trajectory({}, steps=100): + for _ in process.sample({}, steps=100): pass -class TestPre2017Rule(TestReactionRule): +class TestPre2017Rule(TestTransitionRule): """Base class for rules used in pre2017""" def setUp(self): self.rule = self.Rule() @@ -122,7 +119,7 @@ def __init__(self, reactants, products, c=1.): reactants, products, c) -class TestTypedRules(TestReactionRule): +class TestTypedRules(TestTransitionRule): from stocal.examples.typed_rules import AA_BB as Rule def test_infer_transitions_signature(self): @@ -143,9 +140,45 @@ class TestTemperatureCycle(unittest.TestCase): def test_example(self): """test process instantiation""" from stocal.examples.temperature_cycle import process - for _ in process.trajectory({}, steps=100): + for _ in process.sample({}, steps=100): pass +class TestValidation(unittest.TestCase): + """Test validation example""" + from stocal.examples.validation import DataStore + + def setUp(self): + from tempfile import mkdtemp + self.tmpdir = mkdtemp(prefix='stocal-tmp') + self.store = self.DataStore(self.tmpdir) + + def tearDown(self): + from shutil import rmtree + rmtree(self.tmpdir) + + def test_run(self): + """Assert that validation run is executable""" + from argparse import Namespace + from stocal.algorithms import DirectMethod as TestMethod + from stocal.examples.dsmts.models import DSMTS_001_01 as TestModel + from stocal.examples.validation import run_validation + + args = Namespace(models=[TestModel], algo=[TestMethod], N=3, + cpu=1, store=self.store) + run_validation(args) + + def test_report(self): + """Assert that validation report is executable""" + # populate the store first... + from argparse import Namespace + from stocal.examples.validation import report_validation + + report_name = os.path.join(self.tmpdir, 'validation.tex') + args = Namespace(reportfile=report_name, cpu=1, store=self.store) + self.test_run() + report_validation(args) + + if __name__ == '__main__': unittest.main() diff --git a/stocal/tests/test_transitions.py b/stocal/tests/test_transitions.py index f11e73b..f91961a 100644 --- a/stocal/tests/test_transitions.py +++ b/stocal/tests/test_transitions.py @@ -37,6 +37,72 @@ def test_trajectory_with_events(self): proc = self.Process([stocal.Event({}, {'a':1}, 1.)]) proc.trajectory({}) + def test_sample_arguments(self): + """Process.trajectory can be called with optional arguments""" + proc = self.Process([]) + proc.sample({}) + proc.sample({}, 1.) + proc.sample({}, 1., 2.) + proc.sample({}, tstart=1.) + proc.sample({}, tmax=2.) + proc.sample({}, steps=10) + + def test_sample_with_events(self): + """Partly deterministic processes return an appropriate sampler""" + proc = self.Process([stocal.Event({}, {'a':1}, 1.)]) + proc.sample({}) + + def test_sample_return_type_behavior(self): + """Partly deterministic processes return an appropriate sampler""" + proc = self.Process([stocal.Event({}, {'a':1}, 1.)]) + traj = iter(proc.sample({})) + self.assertEqual(len(next(traj)), 2) + + def test_flatten_returns_new_process(self): + """Process.flatten creates a new Process instance""" + class Dimerize(stocal.ReactionRule): + Transition = stocal.MassAction + def novel_reactions(self, k, l): + if len(k) == len(l) == 1: + yield self.Transition([k, l], [k+l], 1.) + process = self.Process(rules=[Dimerize()]) + self.assertIsNot(process.flatten(['a', 'b']), process) + + def test_flatten_static_process_is_invariant(self): + """Processes without rules return a copy of themselves""" + process = self.Process(stocal.MassAction(['a'], ['b'], 1.)) + self.assertEqual(process, process.flatten(['a', 'b'])) + + def test_flatten_flat_process_has_no_rules(self): + """All rules of a flat process are resolved""" + class Dimerize(stocal.ReactionRule): + Transition = stocal.MassAction + def novel_reactions(self, k, l): + if len(k) == len(l) == 1: + yield self.Transition([k, l], [k+l], 1.) + + self.assertFalse(self.Process([]).flatten([]).rules) + self.assertFalse(self.Process(rules=[Dimerize()]).flatten(['a', 'b']).rules) + + def test_flatten_rules_generate_flat_transitions(self): + """Flattening converts applicable rules into transitions""" + class Dimerize(stocal.ReactionRule): + Transition = stocal.MassAction + def novel_reactions(self, k, l): + if len(k) == len(l) == 1: + yield self.Transition([k, l], [k+l], 1.) + + class Split(stocal.ReactionRule): + Transition = stocal.MassAction + def novel_reactions(self, kl): + if len(kl) == 2: + yield self.Transition([kl], [kl[:1], kl[1:]], 1.) + + initial_species = ['a', 'b'] + proc = self.Process(rules=[Dimerize(), Split()]) + flat_proc = proc.flatten(initial_species) + self.assertEquals(len(flat_proc.transitions), 6) + class TestRule(AbstractTestCase('Rule', stocal.Rule)): """Rule specification @@ -135,6 +201,11 @@ def test_true_reactants(self): self.assertEqual(trans1.true_reactants, trans2.true_reactants) self.assertEqual(trans1.true_products, trans2.true_products) + def test_stoichiometry(self): + """stoichiometry is the net change of species""" + self.assertEqual(self.Transition({'a':1}, {'a':2}).stoichiometry, {'a':1}) + self.assertEqual(self.Transition({'a':1}, {'b':2}).stoichiometry, {'a':-1, 'b':2}) + def test_hash(self): """Equal transitions must have equal hash values""" trans_1 = self.Transition({'a':1}, {'z':1}) diff --git a/stocal/tests/test_tutorial.py b/stocal/tests/test_tutorial.py index 6aa5f5f..c0c6dbe 100644 --- a/stocal/tests/test_tutorial.py +++ b/stocal/tests/test_tutorial.py @@ -1,33 +1,35 @@ -"""Test that the exampe is working +"""Test that all tutorial examples are working """ import unittest -from stocal import MassAction, Event, ReactionRule, Process +from stocal import MassAction, Event, TransitionRule, Process, multiset +from stocal import algorithms +from stocal.experimental import tauleap -from stocal.tests.test_transitions import TestReactionRule +from stocal.tests.test_transitions import TestReactionRule as TestTransitionRule -class Dilution(ReactionRule): +class Dilution(TransitionRule): """Dilution rule""" Transition = MassAction def novel_reactions(self, species): yield self.Transition([species], [], 0.001) -class TestDilution(TestReactionRule): +class TestDilution(TestTransitionRule): Rule = Dilution -class Polymerization(ReactionRule): +class Polymerization(TransitionRule): """Polymerization rule""" Transition = MassAction def novel_reactions(self, k, l): yield self.Transition([k, l], [k+l], 10.) -class TestPolymerization(TestReactionRule): +class TestPolymerization(TestTransitionRule): Rule = Polymerization -class Hydrolysis(ReactionRule): +class Hydrolysis(TransitionRule): """Hydrolysis rule""" Transition = MassAction @@ -36,7 +38,7 @@ def novel_reactions(self, k): constant = 10.*i*(len(k)-i) yield self.Transition([k], [k[:i], k[i:]], constant) -class TestHydrolysis(TestReactionRule): +class TestHydrolysis(TestTransitionRule): Rule = Hydrolysis @@ -46,14 +48,14 @@ class Protein(str): class Rna(str): pass -class Association(ReactionRule): +class Association(TransitionRule): Transition = MassAction signature = [Protein, Rna] def novel_reactions(self, protein, rna): yield self.Transition([protein, rna], [(protein, rna)], 1.) -class TestAssociation(TestReactionRule): +class TestAssociation(TestTransitionRule): Rule = Association @@ -75,7 +77,7 @@ def test_simple_example(self): r1 = MassAction({'A': 2}, {'A2': 1}, 1.) r2 = MassAction({'A2': 1}, {'A': 2}, 10.) process = Process([r1, r2]) - trajectory = process.trajectory({'A':100}, steps=1000) + trajectory = process.sample({'A':100}, steps=1000) for _ in trajectory: result = trajectory.time, trajectory.state.get('A', 0), trajectory.state.get('A2', 0) @@ -85,7 +87,7 @@ def test_events(self): r2 = MassAction({'A2': 1}, {'A': 2}, 10.) feed = Event([], ['A'], 0.0, 1.0) process = Process([r1, r2, feed]) - trajectory = process.trajectory({}, steps=100) + trajectory = process.sample({}, steps=100) for _ in trajectory: pass @@ -95,7 +97,7 @@ def test_rule_based_dilution(self): r2 = MassAction({'A2': 1}, {'A': 2}, 10.) feed = Event([], ['A'], 0.0, 1.0) process = Process([r1, r2, feed], [Dilution()]) - trajectory = process.trajectory({}, steps=100) + trajectory = process.sample({}, steps=100) for _ in trajectory: pass @@ -103,28 +105,49 @@ def test_rule_based_polymers(self): """Adding general polymerization and hydrolysis""" feed = Event([], ['A'], 0.0, 1.0) process = Process(transitions=[feed], rules=[Dilution(), Polymerization(), Hydrolysis()]) - trajectory = process.trajectory({}, steps=100) + trajectory = process.sample({}, steps=100) for _ in trajectory: pass + def test_flattening(self): + """Flattening a rule-based process""" + process = Process(rules=[Dilution()]) + flat_process = process.flatten(['a', 'b', 'c']) + self.assertEqual(len(flat_process.transitions), 3) + self.assertEqual(len(flat_process.rules), 0) + def test_types(self): - """Specifying types via ReactionRule.signature""" + """Specifying types via TransitionRule.signature""" process = Process(rules=[Association()]) - trajectory = process.trajectory({Protein('TF'):40, - Rna('mRNA_a'):10, - Rna('mRNA_b'):10}, - steps=100) - self.assertEqual(len(trajectory.transitions), 2) + trajectory = process.sample({Protein('TF'):40, + Rna('mRNA_a'):10, + Rna('mRNA_b'):10}, + steps=100) + flat_process = process.flatten(trajectory.state.domain) + self.assertEqual(len(list(flat_process.transitions)), 2) + for _ in trajectory: pass def test_time_dependence(self): """Specifying volume-dependent reactions""" process = Process([VolumeDependentMassAction(['x', 'x'], ['x2'], 1.)]) - trajectory = process.trajectory({'x':100}, steps=100) + trajectory = process.sample({'x':100}, steps=100) for _ in trajectory: pass + def test_samplers_available(self): + self.assertTrue(issubclass(algorithms.DirectMethod, + algorithms.StochasticSimulationAlgorithm)) + self.assertTrue(issubclass(algorithms.FirstReactionMethod, + algorithms.StochasticSimulationAlgorithm)) + self.assertTrue(issubclass(algorithms.NextReactionMethod, + algorithms.StochasticSimulationAlgorithm)) + self.assertTrue(issubclass(algorithms.AndersonMethod, + algorithms.StochasticSimulationAlgorithm)) + self.assertTrue(issubclass(tauleap.CaoMethod, + algorithms.StochasticSimulationAlgorithm)) + if __name__ == '__main__': unittest.main() diff --git a/stocal/transitions.py b/stocal/transitions.py index f3b76b2..6d06198 100644 --- a/stocal/transitions.py +++ b/stocal/transitions.py @@ -28,7 +28,7 @@ import abc import warnings -from .utils import with_metaclass +from ._utils import with_metaclass from .structures import multiset @@ -47,8 +47,8 @@ class Transition(with_metaclass(abc.ABCMeta, object)): catalysts). Instances also have an attribute self.rule which defaults to None - and which is set by the TrajectorySampler if a Transition has been - inferred by a Rule. + and which is set by the StochasticSimulationAlgorithm if a + Transition has been inferred by a Rule. Modiying any of these attributes after initialization is an error and leads to undefined behavior. @@ -81,6 +81,12 @@ def __init__(self, reactants, products): self.true_reactants = reactants - products self.true_products = products - reactants + self.affected_species = self.true_reactants.union(self.true_products).domain + + self.stoichiometry = { + species: self.true_products[species]-self.true_reactants[species] + for species in self.affected_species + } self.last_occurrence = -float('inf') self._hash = 0 @@ -109,8 +115,8 @@ def __hash__(self): def __repr__(self): try: - return '%s(%s, %s)' % ( - type(self).__name__, self.reactants, self.products + return '<%s %s>' % ( + type(self).__name__, self ) except AttributeError: return super(Transition, self).__repr__() @@ -202,21 +208,22 @@ def propensity(self, state): """ return 0. - def next_occurrence(self, time, state): + def next_occurrence(self, time, state, rng=None): """Determine next reaction firing time. This is a helper function to use Reactions in next-firing-time - based TrajectorySampler's. The method randomly draws a delay - from a Poisson distribution with mean propensity and returns - the given current time plus the delay. + based StochasticSimulationAlgorithm's. The method randomly draws + a delay from a Poisson distribution with mean propensity and + returns the given current time plus the delay. """ - from random import random from math import log + if not rng: + import random as rng propensity = self.propensity(state) if not propensity: return float('inf') else: - return time - log(random())/propensity + return time - log(rng.random())/propensity def propensity_integral(self, state, time, delta_t): """Integrate propensity function from time to time+delta_t @@ -278,8 +285,8 @@ def __init__(self, reactants, products, c): def __repr__(self): try: - return '%s(%s, %s, %g)' % ( - type(self).__name__, self.reactants, self.products, self.constant + return '<%s %s, %g>' % ( + type(self).__name__, self, self.constant ) except AttributeError: return super(MassAction, self).__repr__() @@ -291,15 +298,14 @@ def propensity(self, state): """ from functools import reduce # for python3 compatibility - if not isinstance(state, multiset): - warnings.warn("state must be a multiset.", DeprecationWarning) + state = state if isinstance(state, multiset) else multiset(state) def choose(n, k): """binomial coefficient""" return reduce(lambda x, i: x*(n+1-i)/i, range(1, k+1), 1) return reduce( lambda a, b: a*b, - (choose(state.get(s, 0), n) for s, n in self.reactants.items()), + (choose(state[s], n) for s, n in self.reactants.items()), self.constant ) @@ -341,6 +347,14 @@ def __init__(self, reactants, products, time, frequency=0): self.time = time self.frequency = frequency + def __repr__(self): + try: + return '<%s %s, %g, frequency=%g>' % ( + type(self).__name__, self, self.time, self.frequency + ) + except AttributeError: + return super(Event, self).__repr__() + def __eq__(self, other): """Structural congruence @@ -362,7 +376,7 @@ def __hash__(self): )) return self._hash - def next_occurrence(self, time, state=None): + def next_occurrence(self, time, state=None, rng=None): """Next occurrence of the Event at or after time. If the event does not re-occur, returns float('inf'). @@ -404,44 +418,62 @@ def infer_transitions(self, new_species, state): from the state but before adding new_species as products. Implementations must return an iterable of Transition objects. """ - # XXX Should this take last_transition? raise StopIteration -class ReactionRule(Rule): - """Abstract base class that facilitates inference of Reactions +class _TransitionRuleMetaclass(abc.ABCMeta): + def __call__(self): + cls = super(_TransitionRuleMetaclass, self).__call__() + cls.order = self.get_order(cls) + if not hasattr(cls, 'signature'): + cls.signature = self.get_signature(cls) + if not hasattr(cls, 'Transition'): + cls.Transition = self.get_Transition(cls) + return cls - This class provides a standard implementation of infer_transitions - that generates all possible reactant combinations from species in - state and new_species, that only became possible because of species - in new_species, and could not have been formed by reactants in state - alone. - If ReactionRule.signature is given, it must evaluate to a sequence - of type objects. Combinations are then only formed among reactants - that are instances of the given type. - For each combination, the inference algorithm calls - ReactionRule.novel_reactions. This method, to be implemented by a - subclass, should return an iterable over every reaction that takes - the novel species as reactants. - """ + def get_order(self, cls): + """Reaction order of infered reactions. - @abc.abstractmethod - def novel_reactions(self, *reactants): - """Infer reactions for the given unordered list of reactants. + The order of a reaction is the number of reactant molecules. + To be defined by a subclass.""" + import inspect + try: + # python 3 + parameters = inspect.signature(cls.novel_reactions).parameters + return sum(1 for par in parameters.values() + if par.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD) + except AttributeError: + # python 2.7 + return len(inspect.getargspec(cls.novel_reactions).args) - 1 - To be implemented by a subclass. + def get_signature(self, cls): + """Type signature of TransitionRule.novel_reactions + + In python2, this defaults to self.order*[object]. Override the + attribute in a subclass to constrain reactant types of + novel_reactions. + + In python3, the signature is inferred from type annotations + of the novel_reactions parameters (defaulting to object for + every non-annotated parameter). """ - raise StopIteration + import inspect + try: + # python 3 + signature = inspect.signature(cls.novel_reactions) + return [p.annotation if p.annotation != inspect.Parameter.empty else object + for p in signature.parameters.values()] + except AttributeError: + return cls.order*[object] - @property - def Transition(self): + def get_Transition(self, cls): """Return transition type from novel_reactions return annotation - In python3, ReactionRule.Transition is optional and can alternatively - be provided as novel_reactions return type annotation: + In python3, TransitionRule.Transition is optional and can + alternatively be provided as novel_reactions return type annotation: from typing import Iterator - class MyRule(stocal.ReactionRule): + class MyRule(stocal.TransitionRule): def novel_reactions(self, *reactants) -> Iterator[TransitionClass]: In python2, the property raises an AttributeError. @@ -449,7 +481,7 @@ def novel_reactions(self, *reactants) -> Iterator[TransitionClass]: import inspect try: # python 3 - signature = inspect.signature(self.novel_reactions) + signature = inspect.signature(cls.novel_reactions) ret_ann = signature.return_annotation cls = ret_ann.__args__[0] if not issubclass(cls, Transition): @@ -458,117 +490,108 @@ def novel_reactions(self, *reactants) -> Iterator[TransitionClass]: else: return cls except AttributeError: - raise TypeError("%s.Transition not defined and not inferable" - +" from novel_reactions signature" - % type(self).__name__) + raise TypeError(("%s.Transition not defined and not inferable" + +" from novel_reactions signature") + % cls.__name__) - @property - def order(self): - """Reaction order of infered reactions. - The order of a reaction is the number of reactant molecules. - To be defined by a subclass.""" - import inspect - try: - # python 3 - parameters = inspect.signature(self.novel_reactions).parameters - return sum(1 for par in parameters.values() - if par.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD) - except AttributeError: - # python 2.7 - return len(inspect.getargspec(self.novel_reactions).args) - 1 +class TransitionRule(Rule, with_metaclass(_TransitionRuleMetaclass, Rule)): + """Abstract base class that facilitates inference of Reactions - @property - def signature(self): - """Type signature of ReactionRule.novel_reactions + This class provides a standard implementation of infer_transitions + that generates all possible reactant combinations from species in + state and new_species, that only became possible because of species + in new_species, and could not have been formed by reactants in state + alone. For each combination, the inference algorithm calls + TransitionRule.novel_reactions. This method, to be implemented by a + subclass, should return an iterable over every reaction that takes + the novel species as reactants. - In python2, this defaults to self.order*[object]. Override the - attribute in a subclass to constrain reactant types of - novel_reactions. + If TransitionRule.signature is given, it must evaluate to a sequence + of type objects. Combinations are then only formed among reactants + that are instances of the given type. In python3, the signature can + automatically be inferred from type annotations of the + novel_reactions parameters (defaulting to object for every + non-annotated parameter). + + In python3, TransitionRule.Transition can alternatively be provided + as novel_reactions return type annotation: - In python3, the signature is inferred from type annotations - of the novel_reactions parameters (defaulting to object for - every non-annotated parameter). + from typing import Iterator + class MyRule(stocal.TransitionRule): + def novel_reactions(self, *reactants) -> Iterator[TransitionClass]: + """ + + @abc.abstractmethod + def novel_reactions(self, *reactants): + """Infer reactions for the given unordered list of reactants. + + To be implemented by a subclass. """ - import inspect - try: - # python 3 - signature = inspect.signature(self.novel_reactions) - return [p.annotation if p.annotation != inspect.Parameter.empty else object - for p in signature.parameters.values()] - except AttributeError: - return self.order*[object] + raise StopIteration def infer_transitions(self, new_species, state): """Standard inference algorithm for Reactions. see help(type(self)) for an explanation of the algorithm. """ - if not isinstance(new_species, multiset): - warnings.warn("last_products must be a multiset.", DeprecationWarning) - if not isinstance(state, multiset): - warnings.warn("state must be a multiset.", DeprecationWarning) - - def combinations(reactants, signature, annotated_species, novel): - """Yield all novel combinations comaptible with signature - - See class doc for details.""" - if not signature: - if novel: - yield reactants - return - - skipped = [] - while annotated_species: - species, start, end = annotated_species.pop(0) - if isinstance(species, signature[0]): - break - else: - skipped.append((species, start, end)) - else: - if not annotated_species: - return - - for combination in combinations(reactants, - signature, - skipped+annotated_species, - novel): - yield combination - if end > 1: - annotated_species.insert(0, (species, start-1, end-1)) - for combination in combinations(reactants+[species], - signature[1:], - skipped+annotated_species, - novel or start == 1): - yield combination - - # could be simplified if specification would enforce multiset state: - # next_state = state + last_products - # novel_species = sorted(( - # (species, state[species]+1, - # min(next_state[species], - # len([typ for typ in self.signature if isinstance(species, typ)]))) - # for species in set(new_species).union(state) - #), key=lambda item: item[1]-item[2]) + new_species = new_species if isinstance(new_species, multiset) else multiset(new_species) + state = state if isinstance(state, multiset) else multiset(state) novel_species = sorted(( - (species, state.get(species, 0)+1, - min(new_species.get(species, 0)+state.get(species, 0), + (species, state[species]+1, + min(new_species[species]+state[species], len([typ for typ in self.signature if isinstance(species, typ)]))) for species in set(new_species).union(state) - ), key=lambda item: item[1]-item[2]) - for reactants in combinations([], self.signature, novel_species, False): + if any(isinstance(species, typ) for typ in self.signature) + ), key=lambda item: item[2]-item[1]) + for reactants in self._combinations([], self.signature, novel_species, False): for trans in self.novel_reactions(*reactants): yield trans + def _combinations(self, reactants, signature, annotated_species, novel): + """Yield all novel combinations comaptible with signature + + See class doc for details.""" + if not signature: + if novel: + yield reactants + return + elif not novel and all(end < start + for _, start, end in annotated_species): + return + + skipped = [] + while annotated_species: + species, start, end = annotated_species.pop(0) + if isinstance(species, signature[0]): + break + else: + skipped.append((species, start, end)) + else: + if not annotated_species: + return + + for combination in self._combinations(reactants, + signature, + skipped+annotated_species, + novel): + yield combination + if end > 1: + annotated_species.insert(0, (species, start-1, end-1)) + for combination in self._combinations(reactants+[species], + signature[1:], + skipped+annotated_species, + novel or start == 1): + yield combination class Process(object): """Stochastic process class A collection of all transitions and rules that define a - stochastic process. When initializing a TrajectorySampler with - a Process instance, transitions get copied over into the sampler. + stochastic process. When initializing a StochasticSimulationAlgorithm + with a Process instance, transitions get copied over into the sampler. This makes it possible to use a single process instance with multiple samplers. """ @@ -576,17 +599,34 @@ def __init__(self, transitions=None, rules=None): self.transitions = transitions or [] self.rules = rules or [] - def trajectory(self, state, t=0., tstart=0., tmax=float('inf'), steps=None): + def __eq__(self, other): + return self.transitions == other.transitions and self.rules == other.rules + + def __ne__(self, other): + return not self == other + + def trajectory(self, state, t=0., tstart=0., tmax=float('inf'), steps=None, seed=None): """Create trajectory sampler for given state - The method automatically chooses a suitable sampler for the - given stochastic process, initialized with the given state - and time. + Depreated: please use Process.sample instead. + + The method does the same as Process.trajectory, but returns + a sampler that only yields individual Transition objects + in each iteration: + + >>> process = Process() + >>> state = {} + >>> trajectory = process.trajectory(state) + >>> for transition in trajectory(): + ... print(trajectory.time, trajectory.state, transition) + + C.f. stocal.algorithms.StochasticSimulationAlgorithm for details + on the sampler class. """ - if t: - warnings.warn("pass start time as tstart", DeprecationWarning) - tstart = tstart or t + warnings.warn("Use Process.sample instead", DeprecationWarning) + return self._trajectory(state, tstart=tstart or t, tmax=tmax, steps=steps, seed=seed) + def _trajectory(self, state, t=0., tstart=0., tmax=float('inf'), steps=None, seed=None): def transition_types(): """Yield all generated transtion types of the process""" for trans in self.transitions: @@ -594,15 +634,79 @@ def transition_types(): for rule in self.rules: yield rule.Transition - # DirectMethod for process with normal reactions - if all(issubclass(r, Reaction) and r.is_autonomous - for r in transition_types()): - from .algorithms import DirectMethod as Sampler - # FirstReactionMethod if all reactions are autonomous - elif all(r.is_autonomous for r in transition_types()): - from .algorithms import FirstReactionMethod as Sampler - # AndersonNRM if reactions are non-autonomous + # select suitable simulation algorithm + if any(not r.is_autonomous for r in transition_types()): + # AndersonMethod for processes with non-autonomous reactions + from .algorithms import AndersonMethod as Sampler else: - from .algorithms import AndersonNRM as Sampler + # NextReactionMethod for anything else + from .algorithms import NextReactionMethod as Sampler return Sampler(self, state, tstart, tmax, steps) + + def sample(self, state, tstart=0., tmax=float('inf'), steps=None, seed=None): + """Create trajectory sampler for given state + + The method returns an automatically chosen sampling algorithm + suitable for the given stochastic process. The returned sampler + can be iterated over to generate transitions along a trajectory: + + >>> process = Process() + >>> state = {} + >>> trajectory = process.trajectory(state) + >>> for dt, transitions in trajectory(): + ... print(trajectory.time, trajectory.state, transitions) + + C.f. stocal.algorithms.StochasticSimulationAlgorithm for details + on the sampler class. + """ + # This method yields transitions according to the future + # StochasticSimulationAlgorithm specification in the form + # (dt, transition_dct). It is planned to replace the current + # trajectory method. + sampler = self._trajectory(state, tstart=tstart, tmax=tmax, steps=steps, seed=seed) + + class _Wrapper(object): + def __getattr__(self, attr): + return getattr(sampler, attr) + + def __setattr__(self, attr, val): + return setattr(sampler, attr, val) + + def __iter__(self): + time = sampler.time + for transition in sampler: + yield sampler.time-time, {transition: 1} + time = sampler.time + return _Wrapper() + + def flatten(self, initial_species, max_steps=1000): + Proc = type(self) + flat_process = Proc(self.transitions) + + novel_species = multiset({s:float('inf') for s in initial_species }) + species = multiset() + + for step in range(max_steps): + next_species = multiset() + for rule in self.rules: + for trans in rule.infer_transitions(novel_species, species): + flat_process.transitions.append(trans) + next_species += {s: float('inf') for s in trans.true_products if s not in species} + species += novel_species + if not next_species.domain: + break + else: + novel_species = next_species + else: + raise ValueError("Flattening did not converge within %d steps" % max_steps) + + return flat_process + + +class ReactionRule(TransitionRule): + """Deprecated. Identical to TransitionRule""" + def __init__(self, *args, **opts): + warnings.warn("Use TransitionRule instead", DeprecationWarning) + super(ReactionRule, self).__init__(*args, **opts) +