diff --git a/.github/workflows/core_tests.yml b/.github/workflows/core_tests.yml index ef4b4acaf4..16867e4066 100644 --- a/.github/workflows/core_tests.yml +++ b/.github/workflows/core_tests.yml @@ -251,6 +251,59 @@ jobs: run: | python -m pytest activitysim/examples/${{ matrix.region }}/test --durations=0 + random_seed_generation: + needs: foundation + env: + mamba-env-prefix: /usr/share/miniconda3/envs/asim-test + python-version: 3.9 + label: linux-64 + defaults: + run: + shell: bash -l {0} + name: random_seed_generation_test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Mambaforge + uses: conda-incubator/setup-miniconda@v2 + with: + miniforge-variant: Mambaforge + miniforge-version: latest + activate-environment: asim-test + use-mamba: true + python-version: ${{ env.python-version }} + + - name: Set cache date for year and month + run: echo "DATE=$(date +'%Y%m')" >> $GITHUB_ENV + + - uses: actions/cache@v2 + with: + path: ${{ env.mamba-env-prefix }} + key: ${{ env.label }}-conda-${{ hashFiles('conda-environments/github-actions-tests.yml') }}-${{ env.DATE }}-${{ env.CACHE_NUMBER }} + id: cache + + - name: Update environment + run: mamba env update -n asim-test -f conda-environments/github-actions-tests.yml + if: steps.cache.outputs.cache-hit != 'true' + + - name: Install activitysim + # installing without dependencies is faster, we trust that all needed dependencies + # are in the conda environment defined above. Also, this avoids pip getting + # confused and reinstalling tables (pytables). + run: | + python -m pip install -e . --no-deps + + - name: Conda checkup + run: | + mamba info -a + mamba list + + - name: Test Random Seed Generation + run: | + python -m pytest test/random_seed/test_random_seed.py --durations=0 + estimation_mode: needs: foundation env: diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 3145ab0665..7b41abe603 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -75,6 +75,58 @@ jobs: repository_url: https://test.pypi.org/legacy/ verbose: true + docbuild: + needs: test-built-dist + if: github.event_name == 'release' + name: ubuntu-latest py3.9 + runs-on: ubuntu-latest + defaults: + run: + shell: bash -l {0} + steps: + - uses: actions/checkout@v3 + - uses: actions/download-artifact@v3 + with: + name: releases + path: dist + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install dependencies + uses: conda-incubator/setup-miniconda@v2 + with: + miniforge-variant: Mambaforge + miniforge-version: latest + use-mamba: true + environment-file: conda-environments/docbuild.yml + python-version: 3.9 + activate-environment: docbuild + auto-activate-base: false + auto-update-conda: false + - name: Install activitysim + run: | + python -m pip install dist/activitysim-*.whl + - name: Conda checkup + run: | + conda info -a + conda list + echo REPOSITORY ${{ github.repository }} + echo REF ${{ github.ref }} + echo REF_NAME ${{ github.ref_name }} + - name: Build the docs + run: | + cd docs + make clean + make html + - name: Push to GitHub Pages + uses: peaceiris/actions-gh-pages@v3.8.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + # Token is created automatically by Github Actions, no other config needed + publish_dir: ./docs/_build/html + destination_dir: ${{ github.ref_name }} + upload-to-pypi: needs: test-built-dist if: github.event_name == 'release' @@ -89,4 +141,4 @@ jobs: with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} - verbose: true \ No newline at end of file + verbose: true diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 02d6fadea2..bc9935add1 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -65,7 +65,7 @@ def pipeline_file_name(settings): @inject.injectable() def rng_base_seed(): - return 0 + return setting("rng_base_seed", 0) @inject.injectable(cache=True) diff --git a/activitysim/core/random.py b/activitysim/core/random.py index 12527840f4..229ba022ac 100644 --- a/activitysim/core/random.py +++ b/activitysim/core/random.py @@ -531,7 +531,7 @@ def set_base_seed(self, seed=None): assert len(list(self.channels.keys())) == 0 if seed is None: - self.base_seed = np.random.RandomState().randint(_MAX_SEED) + self.base_seed = np.random.RandomState().randint(_MAX_SEED, dtype=np.uint32) logger.debug("Set random seed randomly to %s" % self.base_seed) else: logger.debug("Set random seed base to %s" % seed) diff --git a/conda-environments/docbuild.yml b/conda-environments/docbuild.yml index a4617fcf60..c598d8606c 100644 --- a/conda-environments/docbuild.yml +++ b/conda-environments/docbuild.yml @@ -1,4 +1,11 @@ # Environment for building docs +# +# This conda environment is used to build the documentation. It includes a +# number of dependencies needed for the documentation only, and not to run or +# test ActivitySim itself. Note that ActivitySim itself is *not* installed +# in this environment, you must install it separately after using this recipe, +# which allows you to install the specific version you want. + name: docbuild channels: - conda-forge @@ -43,4 +50,3 @@ dependencies: - pip: - autodoc_pydantic - - -e .. diff --git a/conda-environments/github-actions-tests.yml b/conda-environments/github-actions-tests.yml index 01108bdc48..77ab0af99d 100644 --- a/conda-environments/github-actions-tests.yml +++ b/conda-environments/github-actions-tests.yml @@ -1,4 +1,7 @@ # Environment for testing in GitHub Actions +# This environment contains a minimal set of dependencies needed to run most tests. +# It does not install ActivitySim itself (which is done by the test scripts) and +# is not meant for use outside the CI tools. name: asim-test channels: - conda-forge @@ -9,11 +12,11 @@ dependencies: - cytoolz >= 0.8.1 - isort - nbmake -- numba >= 0.55.2 +- numba = 0.55.2 # see https://github.com/conda-forge/numba-feedstock/pull/104 - numpy >= 1.16.1,<=1.21 - openmatrix >= 0.3.4.1 - orca >= 1.6 -- pandas >= 1.1.0 +- pandas >= 1.1.0,<1.5 - psutil >= 4.1 - pyarrow >= 2.0 - pypyr >= 5.3 @@ -23,7 +26,7 @@ dependencies: - pytest-regressions - pyyaml >= 5.1 - requests >= 2.7 -- sharrow >= 2.2.4 +- sharrow >= 2.3.2 - simwrapper > 1.7 - xarray >= 0.21 - zarr diff --git a/test/random_seed/.gitignore b/test/random_seed/.gitignore new file mode 100644 index 0000000000..4d692c93ae --- /dev/null +++ b/test/random_seed/.gitignore @@ -0,0 +1,2 @@ +# Configuration files created by test +configs_random_seed_*/ \ No newline at end of file diff --git a/test/random_seed/simulation.py b/test/random_seed/simulation.py new file mode 100644 index 0000000000..8313dd45e7 --- /dev/null +++ b/test/random_seed/simulation.py @@ -0,0 +1,15 @@ +# ActivitySim +# See full license in LICENSE.txt. + +import argparse +import sys + +from activitysim.cli.run import add_run_args, run + +if __name__ == "__main__": + + parser = argparse.ArgumentParser() + add_run_args(parser) + args = parser.parse_args() + + sys.exit(run(args)) diff --git a/test/random_seed/test_random_seed.py b/test/random_seed/test_random_seed.py new file mode 100644 index 0000000000..d0d39a43f4 --- /dev/null +++ b/test/random_seed/test_random_seed.py @@ -0,0 +1,129 @@ +# ActivitySim +# See full license in LICENSE.txt. +import os +import subprocess +import yaml +from shutil import copy + +import pandas as pd +import pandas.testing as pdt +import pkg_resources + +from activitysim.core import inject + + +def update_settings(settings_file, key, value): + with open(settings_file, "r") as f: + settings = yaml.safe_load(f) + f.close() + + settings[key] = value + + with open(settings_file, "w") as f: + yaml.safe_dump(settings, f) + f.close() + + +def run_test_random_seed(): + + steps_to_run = [ + "initialize_landuse", + "initialize_households", + "compute_accessibility", + "workplace_location", + "write_tables", + ] + + def example_path(dirname): + resource = os.path.join("examples", "prototype_mtc", dirname) + return pkg_resources.resource_filename("activitysim", resource) + + def test_path(dirname): + return os.path.join(os.path.dirname(__file__), dirname) + + def create_rng_configs(rng_base_seed=None): + new_configs_dir = test_path("configs_random_seed_=_{}".format(rng_base_seed)) + new_settings_file = os.path.join(new_configs_dir, "settings.yaml") + os.mkdir(new_configs_dir) + for f in os.listdir(example_path("configs")): + copy(os.path.join(example_path("configs"), f), new_configs_dir) + + update_settings(new_settings_file, "models", steps_to_run) + if rng_base_seed != "": # Undefined + update_settings(new_settings_file, "rng_base_seed", rng_base_seed) + + # (run name, rng_base_seed value) + runs = [ + ("0-a", 0), + ("0-b", 0), + ("1-a", 1), + ("1-b", 1), + ("None-a", None), + ("None-b", None), + ("Undefined", ""), + ] + + seeds = list(set([run[1] for run in runs])) + for seed in seeds: + create_rng_configs(seed) + + outputs = {} + + def check_outputs(df1, df2, should_be_equal=True): + """ + Compares df1 and df2 and raises an AssertionError if they are unequal when `should_be_equal` is True and equal when `should_be_equal` is False + """ + if should_be_equal: + pdt.assert_frame_equal(outputs[df1], outputs[df2]) + else: + try: + pdt.assert_frame_equal(outputs[df1], outputs[df2]) + except AssertionError: + pass + else: + raise AssertionError + + file_path = os.path.join(os.path.dirname(__file__), "simulation.py") + + # running prototype mtc model through workplace_location with 3 random seed settings, 0, 1, None, and undefined. + # final_persons.csv is compared to ensure the same setting returns the same variables and a different setting returns something different. + for name, seed in runs: + + run_args = [ + "-c", + test_path("configs_random_seed_=_{}".format(seed)), + "-d", + example_path("data"), + "-o", + test_path("output"), + ] + + try: + os.mkdir(test_path("output")) + except FileExistsError: + pass + + subprocess.run(["coverage", "run", "-a", file_path] + run_args, check=True) + + # Read in output to memory to compare later + outputs[name] = pd.read_csv( + os.path.join(test_path("output"), "final_persons.csv") + ) + + check_outputs("0-a", "0-b", True) + check_outputs("0-a", "Undefined", True) + check_outputs("1-a", "1-b", True) + check_outputs("None-a", "None-b", False) + check_outputs("0-a", "1-a", False) + check_outputs("None-a", "0-a", False) + check_outputs("None-a", "1-a", False) + check_outputs("None-b", "0-a", False) + check_outputs("None-b", "1-a", False) + + +def test_random_seed(): + run_test_random_seed() + + +if __name__ == "__main__": + run_test_random_seed()