Skip to content

Commit 12c8c78

Browse files
authored
Merge pull request #49 from CovertLab/fix-48
Avoid selection of 0 propensity reactions and negative counts
2 parents 66c783d + 975e2c5 commit 12c8c78

File tree

7 files changed

+134
-39
lines changed

7 files changed

+134
-39
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,25 @@ There are more command line features in test_arrow:
104104

105105
> python -m arrow.test.test_arrow --time
106106

107+
More examples:
108+
109+
> python -m arrow.test.test_hang
110+
111+
> pytest -m arrow/test/test_arrow.py
112+
113+
> pytest -k flagella
114+
107115
## Changelog
108116

117+
### Version 0.5.0
118+
119+
* Add the arrow_hang unit test which catches a nasty edge-case (Issue #48),
120+
fix the bug, and make the code more robust to some other potential bugs.
121+
122+
### Version 0.4.4
123+
124+
* Can pickle StochasticSystem instances.
125+
109126
### Version 0.3.0
110127

111128
* Introduced backwards-incompatible API change for supplying rates at `evolve()` time rather than `__init__()` for `StochasticSystem`.

arrow/obsidian.c

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,16 @@ evolve_result evolve(Info *info, double duration, int64_t *state, double *rates)
175175
// Find the total for all propensities
176176
total = 0.0;
177177
for (reaction = 0; reaction < reactions_count; reaction++) {
178+
if (propensities[reaction] < 0) {
179+
status = 3; // a negative propensity
180+
}
178181
total += propensities[reaction];
179182
}
180183

184+
if (status > 0) {
185+
break;
186+
}
187+
181188
if (isnan(total)) {
182189
printf("failed simulation: total propensity is NaN\n");
183190
int max_reaction = 0;
@@ -191,6 +198,7 @@ evolve_result evolve(Info *info, double duration, int64_t *state, double *rates)
191198
interval = 0.0;
192199
choice = -1;
193200
status = 1; // overflow
201+
break;
194202
}
195203

196204
// If the total is zero, then we have no more reactions to perform and can exit
@@ -207,22 +215,29 @@ evolve_result evolve(Info *info, double duration, int64_t *state, double *rates)
207215
// `interval` from an exponential distribution.
208216
interval = sample_exponential(random_state, total);
209217
point = sample_uniform(random_state) * total;
218+
if (point > total) {
219+
// If roundoff made point > total, the `progress` loop would go past the
220+
// end of the array.
221+
point = total;
222+
}
223+
224+
// If we have surpassed the provided duration we can exit now
225+
if (now + interval > duration) {
226+
break;
227+
}
210228

211229
// Based on the random sample, find the event that it corresponds to by
212230
// iterating through the propensities until we surpass our sampled value
213231
choice = 0;
214232
progress = 0.0;
215233

216-
while (progress + propensities[choice] < point) {
234+
// Note: Even if `point` happens to be 0, this needs to skip 0 propensity
235+
// choices to avoid computing negative counts.
236+
while (progress + propensities[choice] < point || propensities[choice] == 0) {
217237
progress += propensities[choice];
218238
choice += 1;
219239
}
220240

221-
// If we have surpassed the provided duration we can exit now
222-
if (choice == -1 || (now + interval) > duration) {
223-
break;
224-
}
225-
226241
// Increase time by the interval sampled above
227242
now += interval;
228243

@@ -237,6 +252,14 @@ evolve_result evolve(Info *info, double duration, int64_t *state, double *rates)
237252
index = substrates_indexes[choice] + involve;
238253
adjustment = stoichiometry[choice * substrates_count + substrates[index]];
239254
outcome[substrates[index]] += adjustment;
255+
256+
if (outcome[substrates[index]] < 0) {
257+
status = 2; // negative counts
258+
}
259+
}
260+
261+
if (status > 0) {
262+
break;
240263
}
241264

242265
// Find which propensities depend on this reaction and therefore need to be

arrow/test/complex-counts.npy

20.4 KB
Binary file not shown.

arrow/test/rates.npy

8.58 KB
Binary file not shown.

arrow/test/stoich.npy

21.5 MB
Binary file not shown.

arrow/test/test_hang.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# A test case for Issue #48.
2+
#
3+
# This code should reproduce the error: the program hangs after it prints "7100"
4+
# and eventually runs out of memory. You'll have to Ctrl+Z to stop it.
5+
# Ctrl+C won't work.
6+
#
7+
# It hangs only with certain seeds and numbers of molecules. The system can
8+
# evolve with the same number of molecule counts for 7179 iterations before it
9+
# hangs. Adding 1 to all of the molecules causes it to hang at an earlier
10+
# iteration.
11+
#
12+
# TODO: Debug this. Is it caused by a Gillespie algorithm blowup (see below),
13+
# integer overflow Undefined Behavior in C, or something else?
14+
#
15+
# The Gillespie algorithm is prone to explode [symptom?] under certain
16+
# conditions if the exponent term in the choice calculation is too large.
17+
#
18+
# The workaround is to find the problematic reaction and decompose the
19+
# stoichiometry into an equivalent problem with more steps.
20+
#
21+
# The flagella example had something like 170 identical subunits which caused
22+
# the problem. Breaking it into 2+ equivalent reactions fixed it.
23+
#
24+
# It'd be good for the Arrow code to catch this problem when/before it happens
25+
# and at least identify which reactions are problematic.
26+
27+
import os
28+
29+
from arrow import StochasticSystem
30+
import numpy as np
31+
32+
33+
def np_load(filename):
34+
filepath = os.path.join(os.path.dirname(__file__), filename)
35+
return np.load(filepath)
36+
37+
38+
def test_hang():
39+
# TODO: Use a pytest plug-in to timeout after some threshold.
40+
41+
seed = 807952948
42+
43+
stoich = np_load('stoich.npy')
44+
mol = np_load('complex-counts.npy')
45+
rates = np_load('rates.npy')
46+
47+
system = StochasticSystem(stoich, random_seed=seed)
48+
for i in range(7300):
49+
if i % 100 == 0:
50+
print(i)
51+
52+
result = system.evolve(1, mol, rates)
53+
54+
55+
if __name__ == '__main__':
56+
test_hang()

setup.py

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import os
2-
# from glob import glob
32
import setuptools # used indirectly for bdist_wheel cmd and long_description_content_type
43
from distutils.core import setup
54
from distutils.extension import Extension
65
import numpy.distutils.misc_util
76

87
with open("README.md", 'r') as readme:
9-
long_description = readme.read()
8+
long_description = readme.read()
109

1110
current_dir = os.getcwd()
1211
arrow_dir = os.path.join(current_dir, 'arrow')
@@ -23,38 +22,38 @@
2322
ext = '.pyx' if USE_CYTHON else '.c'
2423

2524
cython_extensions = [
26-
Extension('arrow.arrowhead',
27-
sources=['arrow/mersenne.c', 'arrow/obsidian.c', 'arrow/arrowhead'+ext,],
28-
include_dirs=['arrow'] + numpy.distutils.misc_util.get_numpy_include_dirs(),
29-
define_macros=[('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION')],
30-
)]
25+
Extension('arrow.arrowhead',
26+
sources=['arrow/mersenne.c', 'arrow/obsidian.c', 'arrow/arrowhead'+ext,],
27+
include_dirs=['arrow'] + numpy.distutils.misc_util.get_numpy_include_dirs(),
28+
define_macros=[('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION')],
29+
)]
3130

3231
if USE_CYTHON:
33-
from Cython.Build import cythonize
34-
cython_extensions = cythonize(
35-
cython_extensions,
36-
include_path=['arrow'],
37-
annotate=True, # to get an HTML code listing
38-
)
32+
from Cython.Build import cythonize
33+
cython_extensions = cythonize(
34+
cython_extensions,
35+
include_path=['arrow'],
36+
annotate=True, # to get an HTML code listing
37+
)
3938

4039
setup(
41-
name='stochastic-arrow',
42-
version='0.4.4',
43-
packages=['arrow'],
44-
author='Ryan Spangler, John Mason, Jerry Morrison',
45-
author_email='[email protected]',
46-
url='https://github.com/CovertLab/arrow',
47-
license='MIT',
48-
include_dirs=include,
49-
ext_modules=cython_extensions,
50-
long_description=long_description,
51-
long_description_content_type='text/markdown',
52-
requires=['numpy (>=1.14)', 'six'],
53-
classifiers=[
54-
'Development Status :: 3 - Alpha',
55-
'License :: OSI Approved :: MIT License',
56-
'Programming Language :: Python',
57-
'Programming Language :: Python :: 2.7',
58-
'Programming Language :: Python :: 3',
59-
'Topic :: Scientific/Engineering',
60-
])
40+
name='stochastic-arrow',
41+
version='0.5.0',
42+
packages=['arrow'],
43+
author='Ryan Spangler, John Mason, Jerry Morrison, Chris Skalnik, Travis Ahn-Horst',
44+
author_email='[email protected]',
45+
url='https://github.com/CovertLab/arrow',
46+
license='MIT',
47+
include_dirs=include,
48+
ext_modules=cython_extensions,
49+
long_description=long_description,
50+
long_description_content_type='text/markdown',
51+
requires=['numpy (>=1.14)', 'six'],
52+
classifiers=[
53+
'Development Status :: 3 - Alpha',
54+
'License :: OSI Approved :: MIT License',
55+
'Programming Language :: Python',
56+
'Programming Language :: Python :: 2.7',
57+
'Programming Language :: Python :: 3',
58+
'Topic :: Scientific/Engineering',
59+
])

0 commit comments

Comments
 (0)