diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..4e08803 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,83 @@ +# Modified from GitHub Actions template + +name: Pytest + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test-on-ubuntu: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11'] + steps: + - uses: actions/checkout@v3 + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + pip install numpy + pip install -r requirements.txt + - name: Compile Cython + run: | + make clean compile + - name: Test with pytest + run: | + pytest + test-on-windows: + runs-on: windows-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11'] + steps: + - uses: actions/checkout@v3 + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + pip install numpy + pip install -r requirements.txt + - name: Compile Cython + run: | + make clean compile + - name: Test with pytest + run: | + pytest + test-on-mac: + runs-on: macos-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11'] + steps: + - uses: actions/checkout@v3 + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + pip install numpy + pip install -r requirements.txt + - name: Compile Cython + run: | + make clean compile + - name: Test with pytest + run: | + pytest diff --git a/.gitignore b/.gitignore index 3ae3c95..5352095 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,9 @@ profile /.idea/ /tmp/ -arrow/arrowhead.c -arrow/*.html +stochastic_arrow/arrowhead.c +stochastic_arrow/arrowhead.*.pyd +stochastic_arrow/*.html arrow.egg-info/ stochastic_arrow.egg-info/ diff --git a/MANIFEST.in b/MANIFEST.in index b96f6c2..1f4aaf8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include arrow/obsidian.h -include arrow/mersenne.h \ No newline at end of file +include stochastic_arrow/obsidian.h +include stochastic_arrow/mersenne.h \ No newline at end of file diff --git a/Makefile b/Makefile index fccad4f..41f5961 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,9 @@ .DEFAULT_GOAL := compile clean: - rm -rf arrow/arrowhead*.so arrow/arrowhead.c arrow/arrowhead.html build/ dist/ MANIFEST .pytest_cache/ stochastic_arrow.egg-info/ + ### Files for older versions of stochastic_arrow are in arrow folder + rm -rf arrow/arrowhead*.so arrow/arrowhead.c arrow/arrowhead.html + rm -rf stochastic_arrow/arrowhead*.so stochastic_arrow/arrowhead.c stochastic_arrow/arrowhead.html build/ dist/ MANIFEST .pytest_cache/ stochastic_arrow.egg-info/ find . -name "*.pyc" -delete find . -name "__pycache__" -delete diff --git a/README.md b/README.md index b624e47..7885bb9 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,21 @@ Add the following to your `requirements.txt`, or run stochastic-arrow +**NOTE:** If upgrading from a version older than 1.0.0, check if the [`arrow`](https://github.com/arrow-py/arrow) datetime package is installed. If so, uninstall `arrow` before upgrading `stochastic-arrow`, then reinstall `arrow`. + + > pip show arrow + > pip uninstall arrow + > pip install stochastic-arrow + > pip install arrow + ## Usage -The `arrow` library presents a single class as an interface, +The `stochastic_arrow` library presents a single class as an interface, `StochasticSystem`, which operates on a set of reactions (encoded as a `numpy` matrix of stoichiometrix coefficients) and associated reaction rates: ```python -from arrow import StochasticSystem +from stochastic_arrow import StochasticSystem import numpy as np # Each row is a reaction and each column is a molecular species (or other @@ -79,14 +86,14 @@ derived from the list of events and the stoichiometric matrix, along with the in state. `reenact_events` will do this for you: ```python -from arrow import reenact_events +from stochastic_arrow import reenact_events -history = reenact_events(stoichiometry, result['events'], state) +history = reenact_events(stoichiometric_matrix, result['events'], state) ``` ## Testing -`arrow` uses [pytest](https://docs.pytest.org/en/latest/). To test it: +`stochastic_arrow` uses [pytest](https://docs.pytest.org/en/latest/). To test it: > make clean compile > pytest @@ -95,26 +102,30 @@ history = reenact_events(stoichiometry, result['events'], state) There are more command line features in test_arrow: - > python -m arrow.test.test_arrow --complexation + > python -m stochastic_arrow.test.test_arrow --complexation - > python -m arrow.test.test_arrow --plot + > python -m stochastic_arrow.test.test_arrow --plot - > python -m arrow.test.test_arrow --obsidian + > python -m stochastic_arrow.test.test_arrow --obsidian - > python -m arrow.test.test_arrow --memory + > python -m stochastic_arrow.test.test_arrow --memory - > python -m arrow.test.test_arrow --time + > python -m stochastic_arrow.test.test_arrow --time More examples: - > python -m arrow.test.test_hang + > python -m stochastic_arrow.test.test_hang - > pytest -m arrow/test/test_arrow.py + > pytest -m stochastic_arrow/test/test_arrow.py > pytest -k flagella ## Changelog +### Version 1.0.0 + +* Rename module to `stochastic_arrow` to avoid name conflict (Issue #51). **All users must update their import statements to use `stochastic_arrow` instead of `arrow`.** + ### Version 0.5.2 * Update to Cython 0.29.34. (Cython 3.0.0 is now in beta.) diff --git a/setup.py b/setup.py index f7f548e..3f06677 100644 --- a/setup.py +++ b/setup.py @@ -13,11 +13,11 @@ _ = setuptools -with open("README.md", 'r') as readme: +with open("README.md", 'r', encoding="utf-8") as readme: long_description = readme.read() current_dir = os.getcwd() -arrow_dir = os.path.join(current_dir, 'arrow') +arrow_dir = os.path.join(current_dir, 'stochastic_arrow') # Compile the Cython code to C for development builds: # USE_CYTHON=1 python setup.py build_ext --inplace @@ -30,9 +30,12 @@ ext = '.pyx' if USE_CYTHON else '.c' cython_extensions = [ - Extension('arrow.arrowhead', - sources=['arrow/mersenne.c', 'arrow/obsidian.c', 'arrow/arrowhead'+ext,], - include_dirs=['arrow', np.get_include()], + Extension('stochastic_arrow.arrowhead', + sources=[ + 'stochastic_arrow/mersenne.c', + 'stochastic_arrow/obsidian.c', + 'stochastic_arrow/arrowhead'+ext,], + include_dirs=['stochastic_arrow', np.get_include()], define_macros=[('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION')], )] @@ -40,15 +43,15 @@ from Cython.Build import cythonize cython_extensions = cythonize( cython_extensions, - include_path=['arrow'], + include_path=['stochastic_arrow'], annotate=True, # to get an HTML code listing ) setup( name='stochastic-arrow', - version='0.5.2', - packages=['arrow'], - author='Ryan Spangler, John Mason, Jerry Morrison, Chris Skalnik, Travis Ahn-Horst', + version='1.0.0', + packages=['stochastic_arrow'], + author='Ryan Spangler, John Mason, Jerry Morrison, Chris Skalnik, Travis Ahn-Horst, Sean Cheah', author_email='ryan.spangler@gmail.com', url='https://github.com/CovertLab/arrow', license='MIT', diff --git a/arrow/__init__.py b/stochastic_arrow/__init__.py similarity index 100% rename from arrow/__init__.py rename to stochastic_arrow/__init__.py diff --git a/arrow/analysis/__init__.py b/stochastic_arrow/analysis/__init__.py similarity index 100% rename from arrow/analysis/__init__.py rename to stochastic_arrow/analysis/__init__.py diff --git a/arrow/analysis/distributions.py b/stochastic_arrow/analysis/distributions.py similarity index 97% rename from arrow/analysis/distributions.py rename to stochastic_arrow/analysis/distributions.py index cf20759..c7eaf02 100644 --- a/arrow/analysis/distributions.py +++ b/stochastic_arrow/analysis/distributions.py @@ -96,7 +96,7 @@ def _last_where(bool_array): if __name__ == '__main__': import matplotlib.pyplot as plt - from arrow.analysis.plotting import plot_full_history + from stochastic_arrow.analysis.plotting import plot_full_history (fig, axes) = plt.subplots(constrained_layout = True) diff --git a/arrow/analysis/plotting.py b/stochastic_arrow/analysis/plotting.py similarity index 100% rename from arrow/analysis/plotting.py rename to stochastic_arrow/analysis/plotting.py diff --git a/arrow/arrow.pxd b/stochastic_arrow/arrow.pxd similarity index 60% rename from arrow/arrow.pxd rename to stochastic_arrow/arrow.pxd index 0642537..a5b4559 100644 --- a/arrow/arrow.pxd +++ b/stochastic_arrow/arrow.pxd @@ -1,4 +1,4 @@ # cython: language_level=3str # This file works around a Cython 3.0.0a1+ error on arrowhead.pyx: -# arrow/arrowhead.pyx:14:0: 'arrow.pxd' not found +# stochastic_arrow/arrowhead.pyx:14:0: 'arrow.pxd' not found diff --git a/arrow/arrow.py b/stochastic_arrow/arrow.py similarity index 97% rename from arrow/arrow.py rename to stochastic_arrow/arrow.py index e85702b..b363b57 100644 --- a/arrow/arrow.py +++ b/stochastic_arrow/arrow.py @@ -33,9 +33,9 @@ def flat_indexes(assorted_lists): lengths = np.array([ len(l) - for l in assorted_lists]) - indexes = np.insert(lengths, 0, 0).cumsum()[:-1] - flat = np.array(flatten(assorted_lists)) + for l in assorted_lists], dtype=np.int64) + indexes = np.insert(lengths, 0, 0).cumsum()[:-1].astype(np.int64) + flat = np.array(flatten(assorted_lists), dtype=np.int64) return flat, lengths, indexes def reenact_events(stoichiometry, events, state): diff --git a/arrow/arrowhead.pyx b/stochastic_arrow/arrowhead.pyx similarity index 97% rename from arrow/arrowhead.pyx rename to stochastic_arrow/arrowhead.pyx index 04a6bda..ddd0e94 100644 --- a/arrow/arrowhead.pyx +++ b/stochastic_arrow/arrowhead.pyx @@ -65,8 +65,8 @@ cdef class Arrowhead: self.random_seed = random_seed mersenne.seed(self.info.random_state, random_seed) - self.info.reactions_count = stoichiometry.shape[0] - self.info.substrates_count = stoichiometry.shape[1] + self.info.reactions_count = stoichiometry.shape[0] + self.info.substrates_count = stoichiometry.shape[1] self.info.stoichiometry = &stoichiometry[0, 0] self.info.reactants_lengths = &reactants_lengths[0] diff --git a/arrow/math.py b/stochastic_arrow/math.py similarity index 100% rename from arrow/math.py rename to stochastic_arrow/math.py diff --git a/arrow/mersenne.c b/stochastic_arrow/mersenne.c similarity index 100% rename from arrow/mersenne.c rename to stochastic_arrow/mersenne.c diff --git a/arrow/mersenne.h b/stochastic_arrow/mersenne.h similarity index 100% rename from arrow/mersenne.h rename to stochastic_arrow/mersenne.h diff --git a/arrow/mersenne.pxd b/stochastic_arrow/mersenne.pxd similarity index 100% rename from arrow/mersenne.pxd rename to stochastic_arrow/mersenne.pxd diff --git a/arrow/obsidian.c b/stochastic_arrow/obsidian.c similarity index 96% rename from arrow/obsidian.c rename to stochastic_arrow/obsidian.c index 266d829..ebb48aa 100644 --- a/arrow/obsidian.c +++ b/stochastic_arrow/obsidian.c @@ -115,14 +115,14 @@ evolve_result evolve(Info *info, double duration, int64_t *state, double *rates) // if something goes wrong (like an overflow in propensities), the status will be // set to some meaningful number - int64_t status = 0; + int status = 0; if (time == NULL || events == NULL || outcome == NULL || propensities == NULL || update == NULL) { - printf("arrow.obsidian.evolve - failed to allocate memory: %d", errno); + printf("stochastic_arrow.obsidian.evolve - failed to allocate memory: %d", errno); free(time); free(events); @@ -187,14 +187,14 @@ evolve_result evolve(Info *info, double duration, int64_t *state, double *rates) if (isnan(total)) { printf("failed simulation: total propensity is NaN\n"); - int max_reaction = 0; + int64_t max_reaction = 0; for (reaction = 0; reaction < reactions_count; reaction++) { - printf("reaction %lld is %f\n", reaction, propensities[reaction]); + printf("reaction %lld is %f\n", (long long)reaction, propensities[reaction]); if (isnan(propensities[reaction]) || propensities[reaction] > propensities[max_reaction]) { max_reaction = reaction; } } - printf("largest reaction is %d at %f\n", max_reaction, propensities[max_reaction]); + printf("largest reaction is %lld at %f\n", (long long)max_reaction, propensities[max_reaction]); interval = 0.0; choice = -1; status = 1; // overflow @@ -278,7 +278,7 @@ evolve_result evolve(Info *info, double duration, int64_t *state, double *rates) if (step >= event_bounds) { double *new_time = malloc((sizeof (double)) * event_bounds * 2); if (new_time == NULL) { - printf("arrow.obsidian.evolve - failed to allocate memory: %d", errno); + printf("stochastic_arrow.obsidian.evolve - failed to allocate memory: %d", errno); free(time); free(events); @@ -295,7 +295,7 @@ evolve_result evolve(Info *info, double duration, int64_t *state, double *rates) int64_t *new_events = malloc((sizeof (int64_t)) * event_bounds * 2); if (new_events == NULL) { - printf("arrow.obsidian.evolve - failed to allocate memory: %d", errno); + printf("stochastic_arrow.obsidian.evolve - failed to allocate memory: %d", errno); free(time); free(events); diff --git a/arrow/obsidian.h b/stochastic_arrow/obsidian.h similarity index 100% rename from arrow/obsidian.h rename to stochastic_arrow/obsidian.h diff --git a/arrow/obsidian.pxd b/stochastic_arrow/obsidian.pxd similarity index 100% rename from arrow/obsidian.pxd rename to stochastic_arrow/obsidian.pxd diff --git a/arrow/reference.py b/stochastic_arrow/reference.py similarity index 99% rename from arrow/reference.py rename to stochastic_arrow/reference.py index 6ce1fa4..83eb139 100644 --- a/arrow/reference.py +++ b/stochastic_arrow/reference.py @@ -3,7 +3,7 @@ import numpy as np from six import moves -from arrow.math import multichoose +from stochastic_arrow.math import multichoose def derive_reactants(stoichiometric_matrix): diff --git a/arrow/test/__init__.py b/stochastic_arrow/test/__init__.py similarity index 100% rename from arrow/test/__init__.py rename to stochastic_arrow/test/__init__.py diff --git a/arrow/test/complex-counts.npy b/stochastic_arrow/test/complex-counts.npy similarity index 100% rename from arrow/test/complex-counts.npy rename to stochastic_arrow/test/complex-counts.npy diff --git a/arrow/test/negative_counts.py b/stochastic_arrow/test/negative_counts.py similarity index 92% rename from arrow/test/negative_counts.py rename to stochastic_arrow/test/negative_counts.py index bdde931..6f2209f 100644 --- a/arrow/test/negative_counts.py +++ b/stochastic_arrow/test/negative_counts.py @@ -1,6 +1,6 @@ import json -from arrow import StochasticSystem +from stochastic_arrow import StochasticSystem import numpy as np diff --git a/arrow/test/rates.npy b/stochastic_arrow/test/rates.npy similarity index 100% rename from arrow/test/rates.npy rename to stochastic_arrow/test/rates.npy diff --git a/arrow/test/stoich.npy b/stochastic_arrow/test/stoich.npy similarity index 100% rename from arrow/test/stoich.npy rename to stochastic_arrow/test/stoich.npy diff --git a/arrow/test/test_arrow.py b/stochastic_arrow/test/test_arrow.py similarity index 86% rename from arrow/test/test_arrow.py rename to stochastic_arrow/test/test_arrow.py index 28f6bfc..d97284b 100644 --- a/arrow/test/test_arrow.py +++ b/stochastic_arrow/test/test_arrow.py @@ -15,12 +15,19 @@ from time import time as seconds_since_epoch import json import numpy as np +from pathlib import Path +import platform import psutil +import pytest import argparse import pickle +import re +import subprocess +import sys -from arrow import reenact_events, StochasticSystem -from arrow import GillespieReference +from stochastic_arrow import reenact_events, StochasticSystem +from stochastic_arrow import GillespieReference +from stochastic_arrow.arrow import SimulationFailure def check_equilibration(): stoichiometric_matrix = np.array([ @@ -82,7 +89,7 @@ def load_complexation(prefix='simple'): def load_state(filename): with open(os.path.join(fixtures_root, filename)) as f: - state = np.array(json.load(f)) + state = np.array(json.load(f), dtype=np.int64) return state @@ -218,7 +225,10 @@ def test_memory(): print('obsidian C implementation elapsed seconds for {} runs: {}'.format( amplify, obsidian_end - obsidian_start)) - assert memory_increases <= 1 + if platform.system() == 'Windows': + assert memory_increases <= 10 + else: + assert memory_increases <= 1 def test_pickle(): stoichiometric_matrix = np.array([ @@ -245,7 +255,7 @@ def test_pickle(): print('arrow object pickled is {} bytes'.format(len(pickled_arrow))) -def test_flagella(): +def test_fail_flagella(): stoichiometry = np.array( [[ 0, 0, 0, 0, 0, -4, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -270,7 +280,7 @@ def test_flagella(): [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -5, -120, 0, 0, 1, - 0, -1, -1]]) + 0, -1, -1]], dtype=np.int64) substrate = np.array([ 21, 1369, 69, 4, 1, 1674, 0, 48, 53, 49, 61, 7, @@ -280,15 +290,36 @@ def test_flagella(): rates = np.array([1.e-05, 1.e-05, 1.e-05, 1.e-05, 1.e-05, 1.e-05]) arrow = StochasticSystem(stoichiometry) - result = arrow.evolve(1.0, substrate, rates) - - print('flagella result: {}'.format(result)) + # This should raise SimulationFailure + with pytest.raises(SimulationFailure): + result = arrow.evolve(1.0, substrate, rates) + +# All reaction propensities should be printed if simulation fails +def test_fail_stdout(): + curr_file = Path(os.path.realpath(__file__)) + main_dir = curr_file.parents[2] + # sys.executable more reliable than 'python' in Windows virtualenv + result = subprocess.run( + [sys.executable, curr_file, '--test-fail-flagella'], + capture_output=True, env={**os.environ, 'PYTHONPATH': str(main_dir)}) + assert re.search(( + 'failed simulation: total propensity is NaN.*' + 'reaction 0 is -?0.000000.*' + 'reaction 1 is -?0.000000.*' + 'reaction 2 is -?0.000000.*' + 'reaction 3 is -?0.000000.*' + 'reaction 4 is -?0.000000.*' + 'reaction 5 is -?nan.*' + 'largest reaction is 5 at -?nan.*'), + result.stdout.decode('utf-8'), flags=re.DOTALL) + assert result.stderr == b'' def test_get_set_random_state(): - stoich = np.array([[1, 1, -1, 0], [-2, 0, 0, 1], [-1, -1, 1, 0]]) + stoich = np.array([[1, 1, -1, 0], [-2, 0, 0, 1], [-1, -1, 1, 0]], + dtype=np.int64) system = StochasticSystem(stoich) - state = np.array([1000, 1000, 0, 0]) + state = np.array([1000, 1000, 0, 0], dtype=np.int64) rates = np.array([3.0, 1.0, 1.0]) system.evolve(1, state, rates) @@ -336,14 +367,16 @@ def check_gillespie_reference(): test_compare_runtime() elif args.pickle: test_pickle() - elif args.flagella: - test_flagella() + elif args.test_fail_flagella: + test_fail_flagella() + elif args.test_fail_stdout: + test_fail_stdout() else: for system in systems: system() else: import matplotlib.pyplot as plt - from arrow.analysis.plotting import plot_full_history + from stochastic_arrow.analysis.plotting import plot_full_history n_systems = len(systems) @@ -385,6 +418,7 @@ def check_gillespie_reference(): parser.add_argument('--memory', action='store_true') parser.add_argument('--time', action='store_true') parser.add_argument('--pickle', action='store_true') - parser.add_argument('--flagella', action='store_true') + parser.add_argument('--test-fail-flagella', action='store_true') + parser.add_argument('--test-fail-stdout', action='store_true') main(parser.parse_args()) diff --git a/arrow/test/test_hang.py b/stochastic_arrow/test/test_hang.py similarity index 97% rename from arrow/test/test_hang.py rename to stochastic_arrow/test/test_hang.py index 6924b08..b47bd8a 100644 --- a/arrow/test/test_hang.py +++ b/stochastic_arrow/test/test_hang.py @@ -26,7 +26,7 @@ import os -from arrow import StochasticSystem +from stochastic_arrow import StochasticSystem import numpy as np