From ed86fc6619feadbc535836fae0a70ff793ecf362 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 24 Nov 2025 13:41:38 -0500 Subject: [PATCH 001/143] remove junk --- dev/README.md | 6 - dev/docs.requirements.txt | 7 - dev/interactive.requirements.txt | 2 - dev/qa.requirements.txt | 5 - dev/testing.requirements.txt | 3 - dev/typechecking.requirements.txt | 1 - env.sh | 1 - pytest.ini | 2 - requirements-distributed.txt | 11 - requirements-graphics.txt | 11 - requirements-md.txt | 6 - requirements-prometheus.txt | 3 - requirements.in | 36 - requirements.txt | 18 - tasks/__init__.py | 58 -- tasks/config.py | 14 - tasks/modules/__init__.py | 23 - tasks/modules/clean.py | 26 - tasks/modules/containers.py | 115 --- tasks/modules/core.py | 33 - tasks/modules/docs.py | 625 ------------ tasks/modules/env.py | 611 ------------ tasks/modules/git.py | 48 - tasks/modules/lxd.py | 48 - tasks/modules/py.py | 905 ------------------ tasks/plugins/__init__.py | 13 - tasks/plugins/custom.py | 6 - tasks/plugins/tests.py | 62 -- tasks/sysconfig.py | 100 -- tasks/toplevel.py | 6 - .../envs/conda_blank/dev.requirements.list | 1 - templates/envs/conda_blank/env.yaml | 6 - templates/envs/conda_blank/pyversion.txt | 1 - templates/envs/conda_blank/requirements.in | 1 - .../envs/conda_blank/self.requirements.txt | 1 - templates/examples/org/README.org | 1 - templates/examples/org/input/.keep | 0 templates/examples/org/source/.keep | 0 templates/examples/org/tasks.py | 44 - templates/jigs/org/README.org | 1 - templates/jigs/org/input/.keep | 0 templates/jigs/org/source/.keep | 0 templates/jigs/org/tasks.py | 44 - templates/tutorials/jupyter/README.ipynb | 32 - templates/tutorials/jupyter/input/.keep | 0 templates/tutorials/jupyter/tasks.py | 36 - templates/tutorials/org/README.org | 2 - templates/tutorials/org/input/.keep | 0 templates/tutorials/org/source/.keep | 0 templates/tutorials/org/tasks.py | 44 - 50 files changed, 3019 deletions(-) delete mode 100644 dev/README.md delete mode 100644 dev/docs.requirements.txt delete mode 100644 dev/interactive.requirements.txt delete mode 100644 dev/qa.requirements.txt delete mode 100644 dev/testing.requirements.txt delete mode 100644 dev/typechecking.requirements.txt delete mode 100644 env.sh delete mode 100644 pytest.ini delete mode 100644 requirements-distributed.txt delete mode 100644 requirements-graphics.txt delete mode 100644 requirements-md.txt delete mode 100644 requirements-prometheus.txt delete mode 100644 requirements.in delete mode 100644 requirements.txt delete mode 100644 tasks/__init__.py delete mode 100644 tasks/config.py delete mode 100644 tasks/modules/__init__.py delete mode 100644 tasks/modules/clean.py delete mode 100644 tasks/modules/containers.py delete mode 100644 tasks/modules/core.py delete mode 100644 tasks/modules/docs.py delete mode 100644 tasks/modules/env.py delete mode 100644 tasks/modules/git.py delete mode 100644 tasks/modules/lxd.py delete mode 100644 tasks/modules/py.py delete mode 100644 tasks/plugins/__init__.py delete mode 100644 tasks/plugins/custom.py delete mode 100644 tasks/plugins/tests.py delete mode 100644 tasks/sysconfig.py delete mode 100644 tasks/toplevel.py delete mode 100644 templates/envs/conda_blank/dev.requirements.list delete mode 100644 templates/envs/conda_blank/env.yaml delete mode 100644 templates/envs/conda_blank/pyversion.txt delete mode 100644 templates/envs/conda_blank/requirements.in delete mode 100644 templates/envs/conda_blank/self.requirements.txt delete mode 100644 templates/examples/org/README.org delete mode 100644 templates/examples/org/input/.keep delete mode 100644 templates/examples/org/source/.keep delete mode 100644 templates/examples/org/tasks.py delete mode 100644 templates/jigs/org/README.org delete mode 100644 templates/jigs/org/input/.keep delete mode 100644 templates/jigs/org/source/.keep delete mode 100644 templates/jigs/org/tasks.py delete mode 100644 templates/tutorials/jupyter/README.ipynb delete mode 100644 templates/tutorials/jupyter/input/.keep delete mode 100644 templates/tutorials/jupyter/tasks.py delete mode 100644 templates/tutorials/org/README.org delete mode 100644 templates/tutorials/org/input/.keep delete mode 100644 templates/tutorials/org/source/.keep delete mode 100644 templates/tutorials/org/tasks.py diff --git a/dev/README.md b/dev/README.md deleted file mode 100644 index d44d09cc..00000000 --- a/dev/README.md +++ /dev/null @@ -1,6 +0,0 @@ -This contains external specifications of the different profiles of dependencies -needed for different tasks. - -They are split up this way to allow for the absolute minimum dependencies needed -in environments like CI where we want performance to be optimal and reducing the -number of dependencies can help that a lot. diff --git a/dev/docs.requirements.txt b/dev/docs.requirements.txt deleted file mode 100644 index aa224a4a..00000000 --- a/dev/docs.requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -sphinx -sphinxcontrib-napoleon -sphinxcontrib-newsfeed -sphinxcontrib-bibtex -sphinxcontrib-newsfeed -nbsphinx -notebook diff --git a/dev/interactive.requirements.txt b/dev/interactive.requirements.txt deleted file mode 100644 index cc33b699..00000000 --- a/dev/interactive.requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -ipython -pdbpp diff --git a/dev/qa.requirements.txt b/dev/qa.requirements.txt deleted file mode 100644 index e0588d55..00000000 --- a/dev/qa.requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -black -isort -flake8 -flake8-bugbear -interrogate diff --git a/dev/testing.requirements.txt b/dev/testing.requirements.txt deleted file mode 100644 index 0c69b775..00000000 --- a/dev/testing.requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest -coverage -pytest-cov diff --git a/dev/typechecking.requirements.txt b/dev/typechecking.requirements.txt deleted file mode 100644 index f0aa93ac..00000000 --- a/dev/typechecking.requirements.txt +++ /dev/null @@ -1 +0,0 @@ -mypy diff --git a/env.sh b/env.sh deleted file mode 100644 index a0a6e3d6..00000000 --- a/env.sh +++ /dev/null @@ -1 +0,0 @@ -. ./.venv/bin/activate diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 2e24f7bc..00000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -addopts = --verbose diff --git a/requirements-distributed.txt b/requirements-distributed.txt deleted file mode 100644 index 2ee17232..00000000 --- a/requirements-distributed.txt +++ /dev/null @@ -1,11 +0,0 @@ -# frozen requirements generated by pip-deepfreeze -cloudpickle==2.2.1 -dask==2023.3.2 -fsspec==2023.3.0 -importlib-metadata==6.2.0 -locket==1.0.0 -packaging==23.0 -partd==1.3.0 -PyYAML==6.0 -toolz==0.12.0 -zipp==3.19.1 diff --git a/requirements-graphics.txt b/requirements-graphics.txt deleted file mode 100644 index 8538641a..00000000 --- a/requirements-graphics.txt +++ /dev/null @@ -1,11 +0,0 @@ -# frozen requirements generated by pip-deepfreeze -contourpy==1.1.1 -cycler==0.12.1 -fonttools==4.43.1 -importlib-resources==6.1.0 -kiwisolver==1.4.5 -matplotlib==3.8.0 -packaging==23.0 -Pillow>=10.3.0 -pyparsing==3.0.9 -zipp>=3.19.1 diff --git a/requirements-md.txt b/requirements-md.txt deleted file mode 100644 index 7e417c84..00000000 --- a/requirements-md.txt +++ /dev/null @@ -1,6 +0,0 @@ -# frozen requirements generated by pip-deepfreeze -astunparse==1.6.3 -mdtraj==1.9.7 -openmm-systems==0.0.0 -pyparsing==3.0.9 -wheel==0.41.2 diff --git a/requirements-prometheus.txt b/requirements-prometheus.txt deleted file mode 100644 index 733fc2b3..00000000 --- a/requirements-prometheus.txt +++ /dev/null @@ -1,3 +0,0 @@ -# frozen requirements generated by pip-deepfreeze -prometheus-client==0.16.0 -Pympler==1.0.1 diff --git a/requirements.in b/requirements.in deleted file mode 100644 index 839de833..00000000 --- a/requirements.in +++ /dev/null @@ -1,36 +0,0 @@ -# this is an abstract listing of the requirements for other projects -# which are co-developing with this repo can use to compile dependencies -# for - ---index-url https://pypi.python.org/simple/ - -numpy -h5py > 3 -networkx == 2.3 -pandas -dill - -click -scipy -matplotlib -tabulate -jinja2 -pint - -eliot -multiprocessing_logging - -# mdtraj -mdtraj - -# distributed -dask[bag] - -# prometheus -prometheus_client -pympler - -# causes simultaneous dev to fail -# git+https://github.com/ADicksonLab/geomm -# git+https://github.com/ADicksonLab/openmm_systems - diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d2aa5d80..00000000 --- a/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -# frozen requirements generated by pip-deepfreeze -click==8.1.3 -dill==0.3.6 -geomm==0.3.0 -h5py==3.8.0 -Jinja2>=3.1.6 -MarkupSafe==2.1.2 -multiprocessing-logging==0.3.4 -networkx==3.1 -numpy==1.24.2 -pandas==2.0.0 -Pint==0.20.1 -python-dateutil==2.8.2 -pytz==2023.3 -scipy==1.10.1 -six==1.16.0 -tabulate==0.9.0 -tzdata==2023.3 diff --git a/tasks/__init__.py b/tasks/__init__.py deleted file mode 100644 index 97fd9f49..00000000 --- a/tasks/__init__.py +++ /dev/null @@ -1,58 +0,0 @@ -from invoke import Collection, Task, task - -import inspect - - -## Utilities - -# these helper functions are for automatically listing all of the -# functions defined in the tasks module - -def _is_mod_task(mod, func): - return issubclass(type(func), Task) and inspect.getmodule(func) == mod - -def _get_functions(mod): - """get only the functions that aren't module functions and that - aren't private (i.e. start with a '_')""" - - return {func.__name__ : func for func in mod.__dict__.values() - if _is_mod_task(mod, func) } - - -## Namespace - -# add all of the modules to the CLI -ns = Collection() - -## Top-level - -from . import toplevel -for func in _get_functions(toplevel).values(): - ns.add_task(func) - - -## STUB: User-added modules -# from user_modules import MODULES as user_modules - -# for module in user_modules: -# ns.add_collection(module) - -## Upstream - -from .modules import MODULES as modules - -for module in modules: - ns.add_collection(module) - -## Plugins - -try: - # import all the user defined stuff and override - from .plugins import PLUGIN_MODULES as plugins - - for module in plugins: - ns.add_collection(module) - -except Exception as e: - print("Loading plugins failed with error ignoring:") - print(e) diff --git a/tasks/config.py b/tasks/config.py deleted file mode 100644 index 99404313..00000000 --- a/tasks/config.py +++ /dev/null @@ -1,14 +0,0 @@ -"""User settings for a project.""" - -# load the system configuration. You can override them in this module, -# but beware it might break stuff -from .sysconfig import * - -## Customize these for all features - -PROJECT_SLUG = "wepy" - -VERSION = '1.1.0' - - -ENV_METHOD = 'conda' diff --git a/tasks/modules/__init__.py b/tasks/modules/__init__.py deleted file mode 100644 index 85f07906..00000000 --- a/tasks/modules/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ - -# SNIPPET: add this to import modules - -# should be copied in by the installation process -from . import core -from . import clean -from . import env -from . import git -from . import py -from . import docs -from . import lxd -from . import containers - -MODULES = [ - core, - clean, - env, - git, - py, - docs, - lxd, - containers, -] diff --git a/tasks/modules/clean.py b/tasks/modules/clean.py deleted file mode 100644 index f239991d..00000000 --- a/tasks/modules/clean.py +++ /dev/null @@ -1,26 +0,0 @@ -from invoke import task - -from ..config import ( - CLEAN_EXPRESSIONS, -) - -### User config examples - -# SNIPPET: expecting something like this -# CLEAN_EXPRESSIONS = [ -# "\"*~\"", -# ] - -@task -def ls(cx): - - for clean_expr in CLEAN_EXPRESSIONS: - cx.run('find . -type f -name {} -print'.format(clean_expr)) - -@task(pre=[ls], default=True) -def clean(cx): - - print("Deleting Targets") - for clean_expr in CLEAN_EXPRESSIONS: - cx.run('find . -type f -name {} -delete'.format(clean_expr)) - diff --git a/tasks/modules/containers.py b/tasks/modules/containers.py deleted file mode 100644 index dedac2fc..00000000 --- a/tasks/modules/containers.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Tasks for managing containers and clusters. - -While a lot of this might be able to be done with special purpose -tools we try to cover as many things that we have tried. - -Warning -------- - -This does not cover best practices at this time. - -""" -import os -from pathlib import Path - -from invoke import task - - -from ..config import ( - PROJECT_SLUG, - CONTAINER_TOOL, -) - -## Container definitions - -@task -def build(cx, root=None): - """Build all containers in dir `containers` using Dockerfiles.""" - - assert root is not None, \ - "Must provide a root directory with expected structure." - - jig_name = Path(root).stem - - containers_dir = Path(root) / "input/containers" - - cx.run(f"mkdir -p {root}/_output/containers") - - print(containers_dir) - for container in os.listdir(containers_dir): - - container_dir = containers_dir / container - - image_name = f"{PROJECT_SLUG}-{jig_name}-{container}" - - print(f"making: {image_name}") - - # remove if already in there - cx.run(f"{CONTAINER_TOOL} rmi {image_name}", warn=True) - - # rebuild - cx.run(f"{CONTAINER_TOOL} build -t {image_name} {container_dir}") - - cx.run(f"{CONTAINER_TOOL} image save {image_name} > {root}/_output/containers/{image_name}.tar") - - # remove from the index - cx.run(f"{CONTAINER_TOOL} rmi {image_name}") - -@task -def list_built(cx, root=None): - """List the built containers in dirs (not container tool memory).""" - - assert root is not None, \ - "Must provide a root directory with expected structure." - - images_dir = Path(root) / "_output/containers" - - image_names = [] - for image_fname in os.listdir(images_dir): - print(image_fname) - - image_name = Path(image_fname).stem - - image_names.append(image_name) - - return image_names - - -@task -def load(cx): - """Load the containers into container tool local memory.""" - - assert root is not None, \ - "Must provide a root directory with expected structure." - - containers_list_built(cx) - - jig_name = Path(root).stem - - images_dir = Path(root) / "_output/containers" - - image_names = list_built(cx) - - for image_name in image_names: - cx.run(f"{CONTAINER_TOOL} load < {images_dir}/{image_name}.tar {image_name}") - -@task -def unload(cx): - - raise NotImplementedError - - assert root is not None, \ - "Must provide a root directory with expected structure." - - list_built(cx) - - jig_name = Path(root).stem - - images_dir = Path(root) / "_output/containers" - - image_names = list_built(cx) - - for image_name in image_names: - cx.run(f"{CONTAINER_TOOL} rm {image_name}", warn=True) - - diff --git a/tasks/modules/core.py b/tasks/modules/core.py deleted file mode 100644 index 7b4d9240..00000000 --- a/tasks/modules/core.py +++ /dev/null @@ -1,33 +0,0 @@ -from invoke import task - -import os.path as osp -import os -from pathlib import Path - -@task -def sanity(cx): - """Perform sanity check for jubeo""" - - print("All systems go!") - - -@task -def pin_tool_deps(cx): - """Pins or upgrades the requirements.txt for the jubeo tooling from - the requirements.in (from the upstream repo) and the - local.requirements.in (for project specific tooling dependencies) - files.""" - - req_in = Path('.jubeo') / "requirements.in" - local_req_in = Path('.jubeo') / "local.requirements.in" - req_txt = Path('.jubeo') / "requirements.txt" - - assert osp.exists(req_in), "No 'requirements.in' file" - - # add the local reqs if given - if osp.exists(local_req_in): - req_str = f"{req_in} {local_req_in}" - else: - req_str = req_in - - cx.run(f"pip-compile --upgrade --output-file={req_txt} {req_str}") diff --git a/tasks/modules/docs.py b/tasks/modules/docs.py deleted file mode 100644 index bb2721f9..00000000 --- a/tasks/modules/docs.py +++ /dev/null @@ -1,625 +0,0 @@ -from invoke import task - -# from ..config import () - -import os -import os.path as osp -from pathlib import Path -import shutil as sh -from warnings import warn - -## Paths for the different things - -DOCS_TEST_DIR = "tests/test_docs/_tangled_docs" -DOCS_EXAMPLES_DIR = "tests/test_docs/_examples" -DOCS_TUTORIALS_DIR = "tests/test_docs/_tutorials" - -DOCS_SPEC = { - 'LANDING_PAGE' : "README.org", - - 'INFO_INDEX' : "info/README.org", - 'QUICK_START' : "info/quick_start.org", - 'INTRODUCTION' : "info/introduction.org", - 'INSTALLATION' : "info/installation.org", - 'USERS_GUIDE' : "info/users_guide.org", - 'HOWTOS' : "info/howtos.org", - 'REFERENCE' : "info/reference.org", - 'TROUBLESHOOTING' : "info/troubleshooting.org", - - 'GLOSSARY' : "info/glossary.rst", - 'BIBLIOGRAPHY' : "info/docs.bib", - - 'DEV_GUIDE' : "info/dev_guide.org", - 'GENERAL' : "info/general_info.org", - 'NEWS' : "info/news.org", - 'CHANGELOG' : "info/changelog.org", - - 'EXAMPLES_DIR' : "info/examples", - 'EXAMPLES_LISTING_INDEX' : "info/examples/README.org", - - # Other examples must be in a directory in the EXAMPLES_DIR and have - # their own structure: - - # potentially literate document with source code. If not literate then - # code should be in the EXAMPLE_SOURCE directory. This index should - # still exist and give instructions on how to use and run etc. tangled - # source will go in the EXAMPLE_TANGLE_SOURCE folder. - 'EXAMPLE_INDEX' : "README.org", - - 'EXAMPLE_TASKS' : "tasks.py", - 'EXAMPLE_BUILD' : "dodo.py", - - # Source code for example that is not literately included in the - # README.org - 'EXAMPLE_SOURCE' : "source", - - # included in the source tree - 'EXAMPLE_INPUT' : "input", - - # values are automatically excluded from the source tree via - # .gitignore - 'EXAMPLE_OUTPUT' : "_output", - - # the directory that tangled source files will go, separate from the - # source dir, this folder will be ignored by VCS - 'EXAMPLE_TANGLE_SOURCE' : "_tangle_source", - - # the actual dir the env will be built into - 'EXAMPLE_ENV' : "_env", - - 'TUTORIALS_DIR' : "info/tutorials", - 'TUTORIALS_LISTING_INDEX' : "info/tutorials/README.org", - - # Other tutorials must be in a directory in the TUTORIALS_DIR and have - # their own structure: - - # the main document for the tutorial can be *one* of any of the - # values supporting: org, Jupyter Notebooks. In order of - # precedence. - 'TUTORIAL_INDEX' : ( - "README.org", - "README.ipynb", - ), - - 'TUTORIAL_TASKS' : "tasks.py", - 'TUTORIAl_BUILD' : "dodo.py", - - # Source code for tutorial that is not literately included in the - # README.org - 'TUTORIAL_SOURCE' : "source", - - # included in the source tree - 'TUTORIAL_INPUT' : "input", - - # values are automatically excluded from the source tree via - # .gitignore - 'TUTORIAL_OUTPUT' : "_output", - - # the directory that tangled source files will go, separate from the - # source dir, this folder will be ignored by VCS - 'TUTORIAL_TANGLE_SOURCE' : "_tangle_source", - - # the actual dir the env will be built into - 'TUTORIAL_ENV' : "_env", - -} - -# here for reference potentially could be applied with an init function -GITIGNORE_LINES = [ - "info/examples/*/_output", - "info/examples/*/_tangle_source", - "info/examples/*/_env", - "info/tutorials/*/_output", - "info/tutorials/*/_tangle_source", - "info/tutorials/*/_env", -] - -# TODO: add a docs init task that generates all the files and adds to -# the gitignore. - -def visit_docs(): - """Returns a list of all the doc pages with their relative paths to - the root of the project. Not including examples and tutorials - which are tested differently. - - """ - - # get the pages which are always there - page_keys = [ - 'LANDING_PAGE', - 'INFO_INDEX', - 'QUICK_START', - 'INTRODUCTION', - 'INSTALLATION', - 'USERS_GUIDE', - 'HOWTOS', - 'REFERENCE', - 'TROUBLESHOOTING', - 'GLOSSARY', - 'DEV_GUIDE', - 'GENERAL', - 'NEWS', - 'CHANGELOG', - 'EXAMPLES_LISTING_INDEX', - 'TUTORIALS_LISTING_INDEX', - ] - - # dereference their paths - page_paths = [DOCS_SPEC[key] for key in page_keys] - - return page_paths - -def visit_examples(): - """Get the relative paths to all of the example dirs.""" - - # get the pages for the tutorials and examples - examples = [ex for ex in os.listdir(DOCS_SPEC['EXAMPLES_DIR']) - if ( - ex != Path(DOCS_SPEC['EXAMPLES_LISTING_INDEX']).parts[-1] and - ex != '.keep' and - not ex.endswith("~") - ) - ] - - example_dirs = [Path(DOCS_SPEC['EXAMPLES_DIR']) / example for example in examples] - - return example_dirs - -def visit_example_contents(example): - - example_pages = [] - if osp.exists(DOCS_SPEC['EXAMPLE_INDEX']): - example_index = example_dir / DOCS_SPEC['EXAMPLE_INDEX'] - example_pages.append(example_index) - else: - warn(f"No example index page for {example}") - - page_paths.extend(example_pages) - -def visit_tutorials(): - """Get the relative paths to all of the tutorial dirs.""" - - # get the pages for the tutorials and tutorials - tutorials = [tut for tut in os.listdir(DOCS_SPEC['TUTORIALS_DIR']) - if ( - tut != Path(DOCS_SPEC['TUTORIALS_LISTING_INDEX']).parts[-1] and - tut != 'index.rst' and - tut != '.keep' and - not tut.endswith("~") - ) - ] - - tutorial_dirs = [Path(DOCS_SPEC['TUTORIALS_DIR']) / tutorial for tutorial in tutorials] - - return tutorial_dirs - -def tangle_orgfile(cx, file_path): - """Tangle the target file using emacs in batch mode. Implicitly dumps - things relative to the file.""" - - cx.run(f"emacs -Q --batch -l org {file_path} -f org-babel-tangle") - -def tangle_jupyter(cx, file_path): - """Tangle the target file using jupyter-nbconvert to a python - script. Implicitly dumps things relative to the file. Only can - make a single script from the notebook with the same name. - - """ - - cx.run(f"jupyter-nbconvert --to 'python' {file_path}") - - -@task -def list_docs(cx): - """List paths relative to this context""" - - print('\n'.join([str(Path(cx.cwd) / p) for p in visit_docs()])) - -@task -def list_examples(cx): - """List paths relative to this context""" - - print('\n'.join([str(Path(cx.cwd) / ex) for ex in visit_examples()])) - -@task -def list_tutorials(cx): - """List paths relative to this context""" - - print('\n'.join([str(Path(cx.cwd) / tut) for tut in visit_tutorials()])) - -@task() -def clean_tangle(cx): - """remove the tangle dirs""" - - sh.rmtree(Path(cx.cwd) / DOCS_TEST_DIR, - ignore_errors=True) - - sh.rmtree(Path(cx.cwd) / DOCS_EXAMPLES_DIR, - ignore_errors=True) - - sh.rmtree(Path(cx.cwd) / DOCS_TUTORIALS_DIR, - ignore_errors=True) - - -@task(pre=[clean_tangle]) -def tangle_pages(cx): - """Tangle the docs into the docs testing directory.""" - - docs_test_dir = Path(cx.cwd) / DOCS_TEST_DIR - - os.makedirs( - docs_test_dir, - exist_ok=True, - ) - - doc_pages = visit_docs() - for page_path in doc_pages: - - page_path = Path(page_path) - - page_name_parts = page_path.parts[0:-1] + (page_path.stem,) - page_name = Path(*page_name_parts) - page_type = page_path.suffix.strip('.') - - page_tangle_dir = docs_test_dir / page_name - # make a directory for this file to have it's own tangle environment - os.makedirs(page_tangle_dir, - exist_ok=False) - - # copy the page to its directory - target_orgfile = docs_test_dir / page_name / f"{page_name.stem}.{page_type}" - sh.copyfile(page_path, - target_orgfile) - - # then tangle them - tangle_orgfile(cx, target_orgfile) - -@task(pre=[clean_tangle]) -def tangle_examples(cx): - - examples_test_dir = Path(cx.cwd) / DOCS_EXAMPLES_DIR - - os.makedirs( - examples_test_dir, - exist_ok=True, - ) - - for example_dir in visit_examples(): - - example = example_dir.stem - - # ignore if there are any built files at the start location, - # need to build fresh for tests - sh.copytree( - example_dir, - examples_test_dir / example, - ignore=sh.ignore_patterns("_*"), - ) - - with cx.cd(str(examples_test_dir / example)): - cx.run("inv clean") - cx.run("inv tangle") - - -@task(pre=[clean_tangle]) -def tangle_tutorials(cx): - - tutorials_test_dir = Path(cx.cwd) / DOCS_TUTORIALS_DIR - - os.makedirs( - tutorials_test_dir, - exist_ok=True, - ) - - for tutorial_dir in visit_tutorials(): - - tutorial = tutorial_dir.stem - - # ignore if there are any built files at the start location, - # need to build fresh for tests - sh.copytree( - tutorial_dir, - tutorials_test_dir / tutorial, - ignore=sh.ignore_patterns("_*"), - ) - - - with cx.cd(str(tutorials_test_dir / tutorial)): - cx.run("inv clean") - cx.run("inv tangle") - - -@task(pre=[clean_tangle, tangle_pages, tangle_examples, tangle_tutorials]) -def tangle(cx): - """Tangle the doc pages, examples, and tutorials into the docs testing - directories.""" - - pass - - -@task -def new_example(cx, name=None, template="org", env='venv_blank'): - """Create a new example in the info/examples directory. - - Can choose between the following templates: - - - 'org' :: org mode notebook - - Choose from the following env templates: - - - None - - venv_blank - - venv_dev - - conda_blank - - conda_dev - - - """ - - assert name is not None, "Must provide a name" - - template_path = Path(f"templates/examples/{template}") - - # check if the template exists - if not template_path.is_dir(): - - raise ValueError( - f"Unkown template {template}. Check the 'templates/examples' folder") - - # check if the env exists - if env is not None: - env_tmpl_path = Path(f"templates/envs/{env}") - - if not env_tmpl_path.is_dir(): - - raise ValueError( - f"Unkown env template {env}. Check the 'templates/envs' folder") - - target_path = Path(f"info/examples/{name}") - - if target_path.exists(): - raise FileExistsError(f"Example with name {name} already exists. Not overwriting.") - - # copy the template - cx.run(f"cp -r {template_path} {target_path}") - - # copy the env - cx.run(f"cp -r {env_tmpl_path} {target_path / 'env'}") - - print(f"New example created at: {target_path}") - -@task -def new_tutorial(cx, name=None, template="org", env='venv_blank'): - """Create a new tutorial in the info/tutorials directory. - - Can choose between the following templates: - - 'org' :: org mode notebook - - 'jupyter' :: Jupyter notebook - - - Choose from the following env templates: - - - None - - venv_blank - - venv_dev - - conda_blank - - conda_dev - - """ - - assert name is not None, "Must provide a name" - - template_path = Path(f"templates/tutorials/{template}") - - # check if the template exists - if not template_path.is_dir(): - - raise ValueError( - f"Unkown template {template}. Check the 'templates/tutorials' folder") - - # check if the env exists - if env is not None: - env_tmpl_path = Path(f"templates/envs/{env}") - - if not env_tmpl_path.is_dir(): - - raise ValueError( - f"Unkown env template {env}. Check the 'templates/envs' folder") - - - target_path = Path(f"info/tutorials/{name}") - - if target_path.exists(): - raise FileExistsError(f"Tutorial with name {name} already exists. Not overwriting.") - - # copy the template - cx.run(f"cp -r {template_path} {target_path}") - - # copy the env - cx.run(f"cp -r {env_tmpl_path} {target_path / 'env'}") - - print(f"New tutorial created at: {target_path}") - - -@task -def test_example(cx, - name=None, - tag=None, -): - """Test a specific doc example in the current virtual environment.""" - - if name is None: - examples = visit_examples() - else: - examples = [Path("info/examples") / name] - - for example in examples: - - path = example - - assert path.exists() and path.is_dir(), \ - f"Example {example.stem} doesn't exist at {path}" - - # TODO: add support for reports and such - print("tag is ignored") - - cx.run(f"pytest tests/test_docs/test_examples/test_{example.stem}.py", - warn=True) - -@task -def test_examples_nox(cx, - name=None): - """Test either a specific example when 'name' is given or all of them, - using the nox test matrix specified in the noxfile.py file for - 'test_example' session. - - """ - - if name is None: - examples = [example.stem for example in visit_examples()] - else: - examples = [name] - - for example in examples: - cx.run(f"nox -s test_example -- {example}", - warn=True) - -@task -def test_tutorial(cx, - name=None, - tag=None, -): - """Test a specific doc tutorial in the current virtual environment.""" - - - if name is None: - tutorials = visit_tutorials() - else: - tutorials = [Path("info/tutorials") / name] - - for tutorial in tutorials: - - path = tutorial - - assert path.exists() and path.is_dir(), \ - f"Tutorial {tutorial} doesn't exist at {path}" - - # TODO: add support for reports and such - print("tag is ignored") - - cx.run(f"pytest tests/test_docs/test_tutorials/test_{tutorial.stem}.py", - warn=True) - -@task -def test_tutorials_nox(cx, - name=None): - """Test either a specific tutorial when 'name' is given or all of them, - using the nox test matrix specified in the noxfile.py file for - 'test_tutorial' session. - - """ - - if name is None: - tutorials = [tutorial.stem for tutorial in visit_tutorials()] - else: - tutorials = [name] - - for tutorial in tutorials: - cx.run(f"nox -s test_tutorial -- {tutorial}") - - -@task -def test_pages(cx, tag=None): - """Test the doc pages in the current virtual environment.""" - - if tag is None: - cx.run("pytest tests/test_docs/test_pages", - warn=True) - - else: - cx.run(f"pytest --html=reports/pytest/{tag}/docs/report.html tests/test_docs/test_pages", - warn=True) - - -@task -def test_pages_nox(cx, tag=None): - """Test the doc pages in the nox test matrix session.""" - - cx.run(f"nox -s test_doc_pages") - -@task -def pin_example(cx, name=None): - """Pin the deps for an example or all of them if 'name' is None.""" - - if name is None: - examples = visit_examples() - else: - examples = [Path(DOCS_SPEC['EXAMPLES_DIR']) / name] - - print(examples) - for example in examples: - - path = example / 'env' - - assert path.exists() and path.is_dir(), \ - f"Env for Example {example} doesn't exist" - - cx.run(f"inv env.deps-pin-path -p {path}") - -@task -def pin_tutorial(cx, name=None): - - if name is None: - tutorials = visit_tutorials() - else: - tutorials = [name] - - for tutorial in tutorials: - - path = tutorial / 'env' - - assert path.exists() and path.is_dir(), \ - f"Env for Tutorial {tutorial} doesn't exist" - - cx.run(f"inv env.deps-pin-path -p {path}") - -@task -def env_example(cx, name=None): - """Make a the example env in its local dir.""" - - if name is None: - examples = visit_examples() - else: - examples = [name] - - for example in examples: - - - spec_path = Path(DOCS_SPEC['EXAMPLES_DIR']) / example / 'env' - env_path = Path(DOCS_SPEC['EXAMPLES_DIR']) / example / DOCS_SPEC['EXAMPLE_ENV'] - - assert spec_path.exists() and spec_path.is_dir(), \ - f"Tutorial {example} doesn't exist" - - cx.run(f"inv env.make-env -s {spec_path} -p {env_path}") - -@task -def env_tutorial(cx, name=None): - """Make a the tutorial env in its local dir.""" - - if name is None: - tutorials = visit_tutorials() - else: - tutorials = [name] - - for tutorial in tutorials: - - - spec_path = Path(DOCS_SPEC['TUTORIALS_DIR']) / tutorial / 'env' - env_path = Path(DOCS_SPEC['TUTORIALS_DIR']) / tutorial / DOCS_SPEC['TUTORIAL_ENV'] - - assert spec_path.exists() and spec_path.is_dir(), \ - f"Tutorial {tutorial} doesn't exist" - - cx.run(f"inv env.make-env -s {spec_path} -p {env_path}") - diff --git a/tasks/modules/env.py b/tasks/modules/env.py deleted file mode 100644 index 9dfacbcb..00000000 --- a/tasks/modules/env.py +++ /dev/null @@ -1,611 +0,0 @@ -from invoke import task - -import sys -import os -import os.path as osp -from pathlib import Path -from warnings import warn -import shutil - -from ..config import ( - ENV_METHOD, - DEFAULT_ENV, - ENVS_DIR, - PYTHON_VERSION_SOURCE, - PYTHON_VERSIONS, -) - -## user config examples - -# SNIPPET: - -# # which virtual environment tool to use: pyenv-virtualenv, venv, or -# conda. pyenv-virtualenv and conda works for all versions, venv only -# works with python3.3 and above ENV_METHOD = 'venv' - -# # which env spec to use by default -# DEFAULT_ENV = 'dev' - -# # directory where env specs are read from -# ENVS_DIR = 'envs' - -# Python version source, this is how we get the different python -# versions - -# PYTHON_VERSION_SOURCE = "pyenv" - -# which versions to install - -# PYTHON_VERSIONS = ( -# '3.8.1', -# '3.7.6', -# ) - - -## Constants - -# directories the actual environments are stored -VENV_DIR = "_venv" -CONDA_ENVS_DIR = "_conda_envs" -# this will be set to the PYENV_PREFIX with the path to the project -# dir -PYENV_DIR = "_pyenv" - - - -# specified names of env specs files -SELF_REQUIREMENTS = 'self.requirements.txt' -"""How to install the work piece""" - -# requirements for specific packaging tooling, like pip -ENV_TOOLS_REQUIREMENTS = 'tools.requirements.txt' - -PYTHON_VERSION_FILE = 'pyversion.txt' -"""Specify which version of python to use for the env""" - -DEV_REQUIREMENTS_LIST = 'dev.requirements.list' -"""Multi-development mode repos to read dependencies from.""" - -# pip specific -PIP_ABSTRACT_REQUIREMENTS = 'requirements.in' -PIP_COMPILED_REQUIREMENTS = 'requirements.txt' - -# conda specific -CONDA_ABSTRACT_REQUIREMENTS = 'env.yaml' -CONDA_COMPILED_REQUIREMENTS = 'env.pinned.yaml' - -### Util - -def parse_list_format(list_str): - - return [line for line in list_str.split('\n') - if not line.startswith("#") and line.strip()] - - -def read_pyversion_file(py_version_path: Path): - - with open(py_version_path, 'r') as rf: - py_version = rf.read().strip() - - return py_version - - -def get_current_pyversion(): - return f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - -### Dependencies -# managing dependencies for the project at runtime - -## pip: things that can be controlled by pip - -def deps_pip_pin(cx, - path=None, - upgrade=False): - - assert path is not None - - path = Path(path) - - # gather any development repos that are colocated on this machine - # and solve the dependencies together - - specs = [ - str(path / PIP_ABSTRACT_REQUIREMENTS), - str(path / ENV_TOOLS_REQUIREMENTS) - ] - - # to get the development repos read the DEV list - if osp.exists(path / DEV_REQUIREMENTS_LIST): - with open(path / DEV_REQUIREMENTS_LIST) as rf: - dev_repo_specs = parse_list_format(rf.read()) - - # for each repo spec add this to the list of specs to evaluate for - for dev_repo_spec in dev_repo_specs: - # expand env vars - dev_repo_spec = osp.expandvars(osp.expanduser(dev_repo_spec)) - - assert osp.exists(dev_repo_spec), f"Repo spec {dev_repo_spec} doesn't exist" - - specs.append(dev_repo_spec) - - - spec_str = " ".join(specs) - - print("Using simultaneous dev specs:") - print(spec_str) - - upgrade_str = '' - if upgrade: - upgrade_str = "--upgrade" - - - cx.run("pip-compile " - f"{upgrade_str} " - f"--output-file={path}/{PIP_COMPILED_REQUIREMENTS} " - f"{spec_str}") - - # SNIPPET: generate hashes is not working right, or just confusing me - # cx.run("python -m piptools compile " - # "--generate-hashes " - # "--output-file={PIP_COMPILED_REQUIREMENTS} " - # f"{PIP_ABSTRACT_REQUIREMENTS}") - - -## conda: managing conda dependencies -def deps_conda_pin(cx, - path=None, - upgrade=False, - optional=False, -): - - # STUB: currently upgrade does nothing - - assert path is not None - - env_spec_path = Path(path) - - if not optional: - assert osp.exists(env_spec_path / CONDA_ABSTRACT_REQUIREMENTS), \ - "There must be an 'env.yaml' file to compile from" - - else: - if not osp.exists(env_spec_path / CONDA_ABSTRACT_REQUIREMENTS): - return None - - # delete the pinned file - if osp.exists(env_spec_path / CONDA_COMPILED_REQUIREMENTS): - os.remove(env_spec_path / CONDA_COMPILED_REQUIREMENTS) - - # make the environment under a mangled name so we don't screw with - # the other one - mangled_name = f"__mangled_tmp_env" - - mangled_env_spec_path = Path(ENVS_DIR) / mangled_name - - # remove if there is one already there - if osp.exists(mangled_env_spec_path): - shutil.rmtree(mangled_env_spec_path) - - # make sure the new dir is initialized - os.makedirs(mangled_env_spec_path, - exist_ok=True - ) - - # TODO: simultaneous dev requirements from other projects that - # require a conda install - - # copy the 'env.yaml' and 'pyversion.txt' files to the new env. If - # we include the requirements.txt file it will screw with the - # pinning process - shutil.copyfile( - env_spec_path / CONDA_ABSTRACT_REQUIREMENTS, - mangled_env_spec_path / CONDA_ABSTRACT_REQUIREMENTS, - ) - - if osp.exists(env_spec_path / PYTHON_VERSION_FILE): - shutil.copyfile( - env_spec_path / PYTHON_VERSION_FILE, - mangled_env_spec_path / PYTHON_VERSION_FILE, - ) - - - # then create the mangled env - env_dir = conda_env(cx, - spec=mangled_env_spec_path, - path=Path(CONDA_ENVS_DIR) / mangled_name, - ) - - # then install the packages so we can export them - with cx.prefix(f'eval "$(conda shell.bash hook)" && conda activate {env_dir}'): - - # only install the declared dependencies - cx.run(f"conda env update " - f"--prefix {env_dir} " - f"--file {env_spec_path}/{CONDA_ABSTRACT_REQUIREMENTS}") - - # pin to a 'env.pinned.yaml' file - cx.run(f"conda env export " - f"-p {env_dir} " - f"-f {env_spec_path}/{CONDA_COMPILED_REQUIREMENTS}") - - # then destroy the temporary mangled env and spec - shutil.rmtree(env_dir) - shutil.rmtree(mangled_env_spec_path) - - print("--------------------------------------------------------------------------------") - print(f"This is an automated process do not attempt to activate the '__mangled' environment") - -@task -def deps_pin_path(cx, - path=None, - upgrade=False): - """Pin an environment given by the path.""" - - deps_pip_pin(cx, - path=path, - upgrade=False) - - if ENV_METHOD == 'conda': - - # WKRD, FIXME: disabled for now since the end result isn't useful anymore - pass - # deps_conda_pin(cx, - # path=path, - # upgrade=False, - # optional=True,) - - -# altogether -@task -def deps_pin(cx, name=DEFAULT_ENV): - """Pin an environment in the 'envs' directory.""" - - path = Path(ENVS_DIR) / name - - deps_pin_path(cx, path=path) - - -@task -def deps_pin_update(cx, name=DEFAULT_ENV): - """Update the pinned environment in the 'envs' directory.""" - - path = Path(ENVS_DIR) / name - - deps_pin_path(cx, - path=path, - upgrade=True, - ) - - -### Environments - -def conda_env(cx, - spec=None, - path=None, -): - - # where the specs of the environment are - env_spec_path = Path(spec) - - # using the local envs dir - env_dir = Path(path) - - # ensure the directory - cx.run(f"mkdir -p {env_dir}") - - - # clean up old envs if they weren't already - if osp.exists(env_dir): - shutil.rmtree(env_dir) - - # figure out which python version to use, if the 'pyversion.txt' - # file exists read it - py_version_path = env_spec_path / PYTHON_VERSION_FILE - if osp.exists(py_version_path): - - print("Using specified python version") - - py_version = read_pyversion_file(py_version_path) - - # otherwise use the one you are currently using - else: - print("Using current envs python version") - py_version = get_current_pyversion() - - print(f"Using python version: {py_version}") - - # create the environment - cx.run(f"conda create -y " - f"--prefix {env_dir} " - f"python={py_version}", - pty=True) - - with cx.prefix(f'eval "$(conda shell.bash hook)" && conda activate {env_dir}'): - - # install the conda dependencies. choose a specification file - # based on these priorities of most pinned to least frozen. - - # WKRD, FIXME: this is disabled because this is super-platform - # dependent and not reliable - # - # if osp.exists(env_spec_path / CONDA_COMPILED_REQUIREMENTS): - # cx.run(f"conda env update " - # f"--prefix {env_dir} " - # f"--file {env_spec_path}/{CONDA_COMPILED_REQUIREMENTS}") - - - if osp.exists(env_spec_path / CONDA_ABSTRACT_REQUIREMENTS): - - cx.run(f"conda env update " - f"--prefix {env_dir} " - f"--file {env_spec_path}/{CONDA_ABSTRACT_REQUIREMENTS}") - - else: - print("No conda dependencies specified") - # don't do a conda env pin - pass - - - # install the tooling, like pip version etc. - if osp.exists(env_spec_path / ENV_TOOLS_REQUIREMENTS): - cx.run(f"{env_dir}/bin/pip install -r {env_spec_path}/{ENV_TOOLS_REQUIREMENTS}") - - # install the extra pip dependencies. - - # this ignores anything already installed by the conda env - # which can cause problem if this tries to install it again. - if osp.exists(env_spec_path / PIP_COMPILED_REQUIREMENTS): - cx.run(f"{env_dir}/bin/pip install " - "--ignore-installed " - f"-r {env_spec_path}/{PIP_COMPILED_REQUIREMENTS}") - - # install the package itself - if osp.exists(env_spec_path / SELF_REQUIREMENTS): - cx.run(f"{env_dir}/bin/pip install -r {env_spec_path}/{SELF_REQUIREMENTS}") - - print("--------------------------------------------------------------------------------") - print(f"run: conda activate {env_dir}") - - return env_dir - - -def venv_env(cx, - spec=None, - path=None, -): - - assert spec is not None - assert path is not None - - venv_path = Path(path) - env_spec_path = Path(spec) - - # ensure the directory - cx.run(f"mkdir -p {venv_path}") - - py_version_path = env_spec_path / PYTHON_VERSION_FILE - - py_version = get_current_pyversion() - if osp.exists(py_version_path): - - spec_py_version = read_pyversion_file(py_version_path) - if spec_py_version != py_version: - raise ValueError( - f"Python version {spec_py_version} was specified in {PYTHON_VERSION_FILE} " - f"but Python {py_version} is activated. For the venv method you must have " - f"the desired python version already activated" - ) - - - print(f"Using python version: {py_version}") - - # create the env requested - cx.run(f"python -m venv {venv_path}") - - # then install the things we need - with cx.prefix(f"source {venv_path}/bin/activate"): - - # install the tooling, like pip version etc. - if osp.exists(env_spec_path / ENV_TOOLS_REQUIREMENTS): - cx.run(f"{env_dir}/bin/pip install -r {env_spec_path}/{ENV_TOOLS_REQUIREMENTS}") - - # install the pip pinned requirements - if osp.exists(env_spec_path / PIP_COMPILED_REQUIREMENTS): - cx.run(f"pip install -r {env_spec_path}/{PIP_COMPILED_REQUIREMENTS}") - - else: - print("No requirements.txt found") - - # if there is a 'self.requirements.txt' file specifying how to - # install the package that is being worked on install it - if osp.exists(env_spec_path / SELF_REQUIREMENTS): - cx.run(f"pip install -r {env_spec_path}/{SELF_REQUIREMENTS}") - - else: - print("No self.requirements.txt found") - - print("----------------------------------------") - print("to activate run:") - print(f"source {venv_path}/bin/activate") - - return venv_path - -def pyenv_env(cx, - spec=None, - path=None, -): - - assert spec is not None - assert path is not None - - # project has its own pyenv root - pyenv_local_dir = Path(path) - - env_spec_path = Path(spec) - - # ensure the directory - cx.run(f"mkdir -p {pyenv_local_dir}") - - - env_path = f"{pyenv_local_dir}/{name}" - cx.run("rm -rf {env_path}") - - py_version_path = env_spec_path / PYTHON_VERSION_FILE - - if osp.exists(py_version_path): - - py_version = read_pyversion_file(py_version_path) - - # otherwise use the one you are currently using - else: - py_version = get_current_pyversion() - - print(f"Using python version: {py_version}") - - # TODO: use pyenv to make the virtualenv with the right version - - # if you already have pyenv installed and there are versions of - # python installed there we will preferentially use them while - # ignoring the envs there since that is a lot of unnecessary files - # to have all the pythons installed separately. - - pyenv_root = Path(osp.expandvars("$PYENV_ROOT")) - - # go ahead and use the pyenv-virtual-local command - if pyenv_root.exists(): - cx.run(f"pyenv virtualenv-local \\" - f"--alt-dir {pyenv_local_dir} \\" - f"{py_version} \\" - f"{name}" - ) - - - # currently we dont' support installing it - else: - raise FileNotFoundError( - f"pyenv not installed" - ) - - # then install the things we need - with cx.prefix(f"source {env_path}/bin/activate"): - - # install the tooling, like pip version etc. - if osp.exists(env_spec_path / ENV_TOOLS_REQUIREMENTS): - cx.run(f"{env_dir}/bin/pip install -r {env_spec_path}/{ENV_TOOLS_REQUIREMENTS}") - - # install the pinned packages - if osp.exists(env_spec_path / SELF_REQUIREMENTS): - cx.run(f"pip install -r {env_spec_path}/{PIP_COMPILED_REQUIREMENTS}") - - else: - print("No requirements.txt found") - - # if there is a 'self.requirements.txt' file specifying how to - # install the package that is being worked on install it - if osp.exists(env_spec_path / SELF_REQUIREMENTS): - cx.run(f"pip install -r {env_spec_path}/{SELF_REQUIREMENTS}") - - else: - print("No self.requirements.txt found") - - print("----------------------------------------") - print("to activate run:") - print(f"source {env_path}/bin/activate") - - return env_path - -@task -def make_env(cx, - spec=None, - path=None, - venv=ENV_METHOD, -): - - assert spec is not None - assert path is not None - - # choose your method: - if venv == 'conda': - conda_env(cx, - spec=spec, - path=path, - ) - - elif venv == 'venv': - venv_env(cx, - spec=spec, - path=path, - ) - - - elif venv == 'pyenv': - pyenv_env(cx, - spec=spec, - path=path, - ) - - -@task(default=True) -def make(cx, name=DEFAULT_ENV): - - spec_path = Path(ENVS_DIR) / name - - # get the path to your envs based on the method - if ENV_METHOD == 'conda': - env_path = Path(CONDA_ENVS_DIR) / name - - elif ENV_METHOD == 'venv': - env_path = Path(VENV_DIR) / name - - elif ENV_METHOD == 'pyenv': - env_path = Path(PYENV_DIR) / name - - else: - raise ValueError(f"Unrecognized venv type: {ENV_METHOD}") - - make_env(cx, - spec=spec_path, - path=env_path, - venv = ENV_METHOD, - ) - -@task -def ls_conda(cx): - print('\n'.join(os.listdir(CONDA_ENVS_DIR))) - -@task -def ls_venv(cx): - - print('\n'.join(os.listdir(VENV_DIR))) - -@task -def ls_specs(cx): - - print('\n'.join(os.listdir(ENV_SPEC_DIR))) - -@task -def ls(cx): - - # choose your method: - if ENV_METHOD == 'conda': - ls_conda(cx) - - elif ENV_METHOD == 'venv': - ls_venv(cx) - -@task -def clean(cx): - cx.run(f"rm -rf {VENV_DIR}") - - -@task -def install_pythons(cx): - """Install different python versions.""" - - assert PYTHON_VERSION_SOURCE == 'pyenv', \ - "Only pyenv is supported for different python versions" - - with cx.prefix("unset PYENV_VERSION"): - for version in PYTHON_VERSIONS: - cx.run(f"pyenv install --skip-existing {version}", - warn=True) diff --git a/tasks/modules/git.py b/tasks/modules/git.py deleted file mode 100644 index e161884d..00000000 --- a/tasks/modules/git.py +++ /dev/null @@ -1,48 +0,0 @@ -from invoke import task - -from ..config import ( - INITIAL_VERSION, - GIT_LFS_TARGETS, - VERSION, -) - -## Constants - -VCS_RELEASE_TAG_TEMPLATE = "v{}" - -@task -def lfs_track(cx): - """Update all the files that need tracking via git-lfs.""" - - for lfs_target in GIT_LFS_TARGETS: - cx.run("git lfs track {}".format(lfs_target)) - - -@task -def init(cx): - - tag_string = VCS_RELEASE_TAG_TEMPLATE.format(INITIAL_VERSION) - - cx.run("git init && " - "git add -A && " - "git commit -m 'initial commit' && " - f"git tag -a {tag_string} -m 'initialization release'") - - - -@task -def publish(cx): - - tag_string = VCS_RELEASE_TAG_TEMPLATE.format(VERSION) - - cx.run(f"git push origin {tag_string}") - - -@task -def release(cx): - - tag_string = VCS_RELEASE_TAG_TEMPLATE.format(VERSION) - - print("Releasing: ", VERSION, "with tag: ", tag_string) - - cx.run(f"git tag -a {tag_string} -m 'See the changelog for details'") diff --git a/tasks/modules/lxd.py b/tasks/modules/lxd.py deleted file mode 100644 index d481c7e8..00000000 --- a/tasks/modules/lxd.py +++ /dev/null @@ -1,48 +0,0 @@ -from invoke import task - -from ..config import ( - PROJECT_SLUG, -) - -@task -def copy_ssh(cx, name='dev'): - """Copy SSH keys to a container.""" - - cx.run(f'ssh-keygen -f "$HOME/.ssh/known_hosts" -R "{PROJECT_SLUG}.dev.lxd"') - cx.run(f"ssh-copy-id {PROJECT_SLUG}.{name}.lxd") - -@task -def push_profile(cx, name='dev'): - """Update your dotfiles in a container.""" - - cx.run(f"fab -H {PROJECT_SLUG}.{name} push-profile") - -@task -def bootstrap(cx, name='dev'): - """Bootstrap the container from a bare image. - - Not necessary if you started from a premade dev env container. - - """ - - cx.run(f"fab -H {PROJECT_SLUG}.{name} bootstrap") - -@task -def push(cx, name='dev'): - """Push the files for this project - - Ignores according to the gitignore file - - """ - - cx.run(f"fab -H {PROJECT_SLUG}.{name} push-project") - -@task -def pull(cx, name='dev'): - """Pull the files for this project - - Ignores according to the gitignore file - - """ - - cx.run(f"fab -H {PROJECT_SLUG}.{name} pull-project") diff --git a/tasks/modules/py.py b/tasks/modules/py.py deleted file mode 100644 index 9c0eb87d..00000000 --- a/tasks/modules/py.py +++ /dev/null @@ -1,905 +0,0 @@ -from invoke import task - -from ..config import ( - VERSION, - REPORTS_DIR, - ORG_DOCS_SOURCES, - RST_DOCS_SOURCES, - BIB_DOCS_SOURCES, - LOGO_DIR, - TESTING_PYPIRC, - PYPIRC, - PYENV_CONDA_NAME, - ENV_METHOD, - TESTS_DIR, - BENCHMARKS_DIR, -) - -import sys -import os -import os.path as osp -from pathlib import Path -import shutil as sh - -## User config examples - -# SNIPPET: -# REPORTS_DIR = "reports" -# ORG_DOCS_SOURCES = [ -# 'changelog', -# 'dev_guide', -# 'general_info', -# 'installation', -# 'introduction', -# 'news', -# 'quick_start', -# 'troubleshooting', -# 'users_guide', -# 'reference', -# ] -# RST_DOCS_SOURCES = [ -# 'glossary', -# 'tutorials/index', -# ] -# PYPIRC = "$HOME/.pypirc" -# TESTING_PYPIRC = "$HOME/.test-pypirc" - -# PYENV_CONDA_NAME = 'miniconda3-latest' - -## CONSTANTS - - -BENCHMARK_STORAGE_URI = f"\"file://{REPORTS_DIR}/benchmarks\"" - - -def project_slug(): - - try: - from ..config import PROJECT_SLUG - except ImportError: - print("You must set the 'PROJECT_SLUG' in conifig.py to use this") - else: - return PROJECT_SLUG - -@task -def init(cx): - - # install the versioneer files - - # this always exits in an annoying way so we just warn here. - cx.run("versioneer install", - warn=True) - -@task -def clean_dist(cx): - """Remove all build products.""" - - cx.run("python setup.py clean") - cx.run("rm -rf dist build */*.egg-info *.egg-info") - -@task -def clean_cache(cx): - """Remove all of the __pycache__ files in the packages.""" - cx.run('find . -name "__pycache__" -exec rm -r {} +') - -@task -def clean_docs(cx): - """Remove all documentation build products""" - - docs_clean(cx) - -@task -def clean_website(cx): - """Remove all local website build products""" - cx.run("rm -rf docs/*") - - # if the website accidentally got onto the main branch we remove - # that crap too - for thing in [ - '_images', - '_modules', - '_sources', - '_static', - 'api', - 'genindex.html', - 'index.html', - 'invoke.html', - 'objects.inv', - 'py-modindex.html', - 'search.html', - 'searchindex.js', - 'source', - 'tutorials', - ]: - - cx.run(f"rm -rf {thing}") - -@task(pre=[clean_cache, clean_dist, clean_docs, clean_website]) -def clean(cx): - pass - - -### Docs - -@task -def docs_clean(cx): - - cx.run("cd sphinx && make clean") - cx.run("rm -rf sphinx/_build") - cx.run("rm -rf sphinx/_source") - cx.run("rm -rf sphinx/_api") - cx.run("rm -rf sphinx/_static") - - # reports - cx.run("rm -rf reports/benchmarks/asv/_html") - -@task -def docs_regressions(cx): - - with cx.cd("benchmarks"): - cx.run("asv publish", warn=True) - -@task -def docs_coverage(cx): - cx.run("coverage html -d reports/coverage/_html/index.html", - warn=True) - -@task -def docs_complexity(cx): - - os.makedirs(f"{REPORTS_DIR}/code_quality/_html", - exist_ok=True) - cx.run(f"lizard -o {REPORTS_DIR}/code_quality/_html/index.html src/{project_slug()}", - warn=True) - -@task(pre=[ - docs_regressions, - docs_coverage, - docs_complexity, -]) -def docs_reports(cx): - """Build all of the reports from source.""" - pass - -@task(pre=[docs_clean, docs_reports]) -def docs_build(cx): - """Buld the documentation""" - - # make sure the 'source' folder exists - cx.run("mkdir -p sphinx/_source") - cx.run("mkdir -p sphinx/_source/tutorials") - cx.run("mkdir -p sphinx/_source/tutorials/data_analysis") - cx.run("mkdir -p sphinx/_source/tutorials/multiple_runs") - cx.run("mkdir -p sphinx/_source/examples") - cx.run("mkdir -p sphinx/_static") - - # copy the logo over - cx.run(f"cp {LOGO_DIR}/* sphinx/_static/") - - # and the other theming things - cx.run(f"cp sphinx/static/* sphinx/_static/") - - # copy the plain RST files over to the sources - for source in RST_DOCS_SOURCES: - - source_path = Path('info') / source - - # glob expand if it is a directory - if source_path.is_dir(): - sources = [path.stem for path in source_path.glob("*.rst")] - else: - sources = [source] - targets = [source] - - for source, target in zip(sources, targets): - - cx.run(f"cp info/{source}.rst sphinx/_source/{target}.rst") - - # copy the Bibtex files over - for source in BIB_DOCS_SOURCES: - cx.run(f"cp info/{source}.bib sphinx/_source/{source}.bib") - - # convert the org mode to rst in the source folder - for source in ORG_DOCS_SOURCES: - - source_dir = Path("info") - target_dir = Path("sphinx/_source") - - source_path = source_dir / source - - # glob expand if it is a directory - if source_path.is_dir(): - sources = [f"{source}/{path.stem}" for path in source_path.glob("*.org")] - targets = sources - - # also make sure the directory exists at the target - os.makedirs(target_dir / source, - exist_ok=True) - else: - sources = [source] - targets = sources - - for source, target in zip(sources, targets): - - cx.run("pandoc " - "-f org " - "-t rst " - f"-o {target_dir}/{target}.rst " - f"{source_dir}/{source}.org") - - ## Examples - - # examples don't get put into the documentation and rendered like - # the tutorials do, but we do copy the README as the index - - # copy the tutorials_index.rst file to the tutorials in _source - # TODO: remove, don't think I will use this - # sh.copyfile( - # "sphinx/examples_index.rst", - # "sphinx/_source/examples/index.rst", - # ) - - - ## Tutorials - - # convert the README - sh.copyfile( - "sphinx/tutorials_index.rst", - "sphinx/_source/tutorials/index.rst", - ) - - sh.copyfile( - "sphinx/data_analysis_index.rst", - "sphinx/_source/tutorials/data_analysis/index.rst", - ) - - sh.copyfile( - "sphinx/multiple_runs_index.rst", - "sphinx/_source/tutorials/multiple_runs/index.rst", - ) - - sh.copyfile( - "sphinx/quick_start_index.rst", - "sphinx/_source/quick_start/index.rst", - ) - - # convert any of the tutorials that exist with an org mode extension as well - for item in os.listdir('info/tutorials'): - item = Path('info/tutorials') / item - - # tutorials are in their own dirs - if item.is_dir(): - docs = list(item.glob("README.org")) + \ - list(item.glob("README.ipynb")) + \ - list(item.glob("README.rst")) - - if len(docs) > 1: - raise ValueError(f"Multiple tutorial files for {item}") - else: - readme_path = docs[0] - - tutorial = item.stem - - os.makedirs(f"sphinx/_source/tutorials/{tutorial}", - exist_ok=True) - - # we must convert org mode files to rst - if readme_path.suffix == '.org': - - cx.run("pandoc " - "-f org " - "-t rst " - f"-o sphinx/_source/tutorials/{tutorial}/README.rst " - f"info/tutorials/{tutorial}/README.org") - - # just copy notebooks since teh sphinx extension handles - # them - elif readme_path.suffix in ('.ipynb', '.rst',): - - sh.copyfile( - readme_path, - f"sphinx/_source/tutorials/{tutorial}/{readme_path.stem}{readme_path.suffix}", - ) - - # otherwise just move it - else: - raise ValueError(f"Unkown tutorial type for file: {readme_path.stem}{readme_path.suffix}") - - - - # convert any of the data analysis files that exist with an org mode extension as well - for item in os.listdir('info/tutorials/data_analysis'): - item = Path('info/tutorials/data_analysis') / item - - # quick starts are in their own dirs - if item.is_dir(): - - docs = list(item.glob("README.org")) + \ - list(item.glob("README.ipynb")) + \ - list(item.glob("README.rst")) - - if len(docs) > 1: - raise ValueError(f"Multiple quick start files for {item}") - else: - readme_path = docs[0] - - tutorial = item.stem - - os.makedirs(f"sphinx/_source/tutorials/data_analysis/{tutorial}", - exist_ok=True) - - # we must convert org mode files to rst - if readme_path.suffix == '.org': - - cx.run("pandoc " - "-f org " - "-t rst " - f"-o sphinx/_source/tutorials/data_analysis/{tutorial}/README.rst " - f"info/tutorials/data_analysis/{tutorial}/README.org") - - # just copy notebooks since teh sphinx extension handles - # them - elif readme_path.suffix in ('.ipynb', '.rst',): - - sh.copyfile( - readme_path, - f"sphinx/_source/tutorials/data_analysis/{tutorial}/{readme_path.stem}{readme_path.suffix}", - ) - - # otherwise just move it - else: - raise ValueError(f"Unkown tutorial type for file: {readme_path.stem}{readme_path.suffix}") - - - # convert any of the multiple runs files that exist with an org mode extension as well - for item in os.listdir('info/tutorials/multiple_runs'): - item = Path('info/tutorials/multiple_runs') / item - - # quick starts are in their own dirs - if item.is_dir(): - - docs = list(item.glob("README.org")) + \ - list(item.glob("README.ipynb")) + \ - list(item.glob("README.rst")) - - if len(docs) > 1: - raise ValueError(f"Multiple quick start files for {item}") - else: - readme_path = docs[0] - - tutorial = item.stem - - os.makedirs(f"sphinx/_source/tutorials/multiple_runs/{tutorial}", - exist_ok=True) - - # we must convert org mode files to rst - if readme_path.suffix == '.org': - - cx.run("pandoc " - "-f org " - "-t rst " - f"-o sphinx/_source/tutorials/multiple_runs/{tutorial}/README.rst " - f"info/tutorials/multiple_runs/{tutorial}/README.org") - - # just copy notebooks since teh sphinx extension handles - # them - elif readme_path.suffix in ('.ipynb', '.rst',): - - sh.copyfile( - readme_path, - f"sphinx/_source/tutorials/multiple_runs/{tutorial}/{readme_path.stem}{readme_path.suffix}", - ) - - # otherwise just move it - else: - raise ValueError(f"Unkown tutorial type for file: {readme_path.stem}{readme_path.suffix}") - - - - # convert any of the quick start files that exist with an org mode extension as well - for item in os.listdir('info/quick_start'): - item = Path('info/quick_start') / item - - # quick starts are in their own dirs - if item.is_dir(): - - docs = list(item.glob("README.org")) + \ - list(item.glob("README.ipynb")) + \ - list(item.glob("README.rst")) - - if len(docs) > 1: - raise ValueError(f"Multiple quick start files for {item}") - else: - readme_path = docs[0] - - tutorial = item.stem - - os.makedirs(f"sphinx/_source/quick_start/{tutorial}", - exist_ok=True) - - # we must convert org mode files to rst - if readme_path.suffix == '.org': - - cx.run("pandoc " - "-f org " - "-t rst " - f"-o sphinx/_source/quick_start/{tutorial}/README.rst " - f"info/quick_start/{tutorial}/README.org") - - # just copy notebooks since teh sphinx extension handles - # them - elif readme_path.suffix in ('.ipynb', '.rst',): - - sh.copyfile( - readme_path, - f"sphinx/_source/quick_start/{tutorial}/{readme_path.stem}{readme_path.suffix}", - ) - - # otherwise just move it - else: - raise ValueError(f"Unkown tutorial type for file: {readme_path.stem}{readme_path.suffix}") - - # run the build steps for sphinx - with cx.cd('sphinx'): - - # build the API Documentation - cx.run(f"sphinx-apidoc -f --separate --private --ext-autodoc --module-first --maxdepth 1 -o _api ../src/{project_slug()}") - - # then do the sphinx build process - cx.run("sphinx-build -b html -E -a -j 6 -c . . ./_build/html/") - - - - ## Post Sphinx - - # add things like adding in metrics etc. here - # copy the benchmark regressions over if available - - # asv regressions - regression_pages = Path("reports/benchmarks/asv/_html") - - if regression_pages.exists() and regression_pages.is_dir(): - sh.copytree( - regression_pages, - "sphinx/_build/html/regressions" - ) - - quality_pages = Path("reports/code_quality/_html") - - if quality_pages.exists() and quality_pages.is_dir(): - sh.copytree( - quality_pages, - "sphinx/_build/html/quality" - ) - - # coverage - coverage_pages = Path("reports/coverage/_html") - - if coverage_pages.exists() and coverage_pages.is_dir(): - sh.copytree( - coverage_pages, - "sphinx/_build/html/coverage" - ) - -@task(pre=[docs_build]) -def docs_serve(cx): - """Local server for documenation""" - cx.run("python -m http.server -d sphinx/_build/html 8022") - -### Website - -@task(pre=[clean_docs, clean_website, docs_build]) -def website_serve(cx): - """Serve the main web page locally for development.""" - - # TODO: implement using Nikola - - # STUB: just use the docs for this right now - docs_serve(cx) - - -# STUB: @task(pre=[clean_docs, docs_build]) -@task -def website_deploy(cx): - """Deploy the documentation onto the internet.""" - - # use the ghp-import tool which handles the branch switching to - # `gh-pages` for you - cx.run("ghp-import --no-jekyll --push --force sphinx/_build/html") - - -### Jigs - -# jigs are for that kind of in between work of not in module, not -# documentation etc. Could be prototypes, troubleshooting, or anything -# that needs non-trivial setup but isn't part of a "framework". Uses -# the same schema as examples to give some order to it. - -@task -def new_jig(cx, name=None, template="org", env='venv_blank'): - """Create a new jig. - - Can choose between the following templates: - - - 'org' :: org mode notebook - - Choose from the following env templates: - - - None - - venv_blank - - venv_dev - - conda_blank - - conda_dev - - """ - - assert name is not None, "Must provide a name" - - template_path = Path(f"templates/jigs/{template}") - - # check if the template exists - if not template_path.is_dir(): - - raise ValueError( - f"Unkown template {template}. Check the 'templates/jigs' folder") - - # check if the env exists - if env is not None: - env_tmpl_path = Path(f"templates/envs/{env}") - - if not env_tmpl_path.is_dir(): - - raise ValueError( - f"Unkown env template {env}. Check the 'templates/envs' folder") - - - target_path = Path(f"jigs/{name}") - - if target_path.exists(): - raise FileExistsError(f"Jig with name {name} already exists. Not overwriting.") - - # copy the template - cx.run(f"cp -r {template_path} {target_path}") - - # copy the env - cx.run(f"cp -r {env_tmpl_path} {target_path / 'env'}") - - print(f"New example created at: {target_path}") - -@task -def pin_jig(cx, name=None): - """Pin the deps for an example or all of them if 'name' is None.""" - - path = Path('jigs') / name / 'env' - - assert path.exists() and path.is_dir(), \ - f"Env for Jig {name} doesn't exist" - - cx.run(f"inv env.deps-pin-path -p {path}") - -@task -def env_jig(cx, name=None): - """Make a the example env in its local dir.""" - - if name is None: - raise ValueError("Must specify which jig to use") - - spec_path = Path('jigs') / name / 'env' - env_path = Path('jigs') / name / '_env' - - assert spec_path.exists() and spec_path.is_dir(), \ - f"Jig {name} doesn't exist" - - cx.run(f"inv env.make-env -s {spec_path} -p {env_path}") - - -### Tests - - -@task -def tests_benchmarks(cx): - cx.run("pytest -m 'not interactive' tests/test_benchmarks", - warn=True) - -@task -def tests_integration(cx, tag=None): - - if tag is None: - cx.run(f"coverage run -m pytest -m 'not interactive' tests/test_integration", - warn=True) - else: - cx.run(f"coverage run -m pytest --html=reports/pytest/{tag}/integration/report.html -m 'not interactive' tests/test_integration", - warn=True) - - -@task -def tests_unit(cx, tag=None): - - if tag is None: - cx.run(f"coverage run -m pytest -m 'not interactive' tests/test_unit", - warn=True) - else: - cx.run(f"coverage run -m pytest --html=reports/pytest/{tag}/unit/report.html -m 'not interactive' tests/test_unit", - warn=True) - - -@task -def tests_interactive(cx): - """Run the interactive tests so we can play with things.""" - cx.run("pytest -m 'interactive'", - warn=True) - -@task() -def tests_all(cx, tag=None): - """Run all the automated tests. No benchmarks. - - There are different kinds of nodes that we can run on that - different kinds of tests are available for. - - - minor : does not have a GPU, can still test most other code paths - - - dev : has at least 1 GPU, enough for small tests of all code paths - - - production : has multiple GPUs, good for running benchmarks - and full stress tests - - """ - - tests_unit(cx, tag=tag) - tests_integration(cx, tag=tag) - -@task -def tests_nox(cx): - - if ENV_METHOD == 'pyenv': - - # run with base venv maker - with cx.prefix("unset PYENV_VERSION"): - cx.run("nox -s test") - - elif ENV_METHOD == 'conda': - - # test running with conda - with cx.prefix(f"pyenv shell {PYENV_CONDA_NAME}"): - cx.run("nox -s test") - - else: - - raise ValueError(f"Unsupported ENV_METHOD: {ENV_METHOD}") - - -### Code & Test Quality - -@task -def docstrings_report(cx): - - cx.run("mkdir -p reports/docstring_coverage") - cx.run("interrogate -o reports/docstring_coverage/src.interrogate.txt -vv src") - - # TODO add it for tests and docs etc. - -@task -def coverage_report(cx): - # cx.run("coverage report") - cx.run("coverage xml -o reports/coverage/coverage.xml", - warn=True) - cx.run("coverage json -o reports/coverage/coverage.json", - warn=True - ) - -@task -def coverage_serve(cx): - cx.run("python -m http.server -d reports/coverage/html 8020", - asynchronous=True) - - -@task -def lint(cx): - - cx.run(f"mkdir -p {REPORTS_DIR}/lint") - - cx.run(f"rm -f {REPORTS_DIR}/lint/flake8.txt") - cx.run(f"flake8 --output-file={REPORTS_DIR}/lint/flake8.txt src/{project_slug()}", - warn=True) - -@task -def complexity(cx): - """Analyze the complexity of the project.""" - - cx.run(f"mkdir -p {REPORTS_DIR}/code_quality") - - cx.run(f"lizard -o {REPORTS_DIR}/code_quality/lizard.csv src/{project_slug()}", - warn=True) - - # SNIPPET: annoyingly opens the browser - - # make a cute word cloud of the things used - # cx.run(f"(cd {REPORTS_DIR}/code_quality; lizard -EWordCount src/project_slug() > /dev/null)") - -@task -def complexity_serve(cx): - cx.run("python -m http.server -d reports/conde_quality/lizard.html 8021", - asynchronous=True) - -@task(pre=[coverage_report, complexity, lint]) -def quality(cx): - pass - -@task(pre=[coverage_serve, complexity_serve]) -def quality_serve(cx): - pass - - -### Profiling - -@task -def profile(cx): - NotImplemented - -### Performance Benchmarks - -## regressions - -@task -def regressions_all(cx): - """Run regression benchmarks for all of the hashes/tags in the - benchmarks/benchmark_selection.list file""" - - with cx.cd("benchmarks"): - cx.run("asv run HASHFILE:benchmark_selection.list") - -@task -def regression_current(cx): - - with cx.cd("benchmarks"): - cx.run("asv run") - -# @task -# def asv_update - -@task -def benchmark_adhoc(cx): - """An ad hoc benchmark that will not be saved.""" - - cx.run("pytest benchmarks/pytest_benchmark/test_benchmarks") - -@task -def benchmark_save(cx): - """Run a proper benchmark that will be saved into the metrics for regression testing etc.""" - - run_command = \ -f"""pytest --benchmark-autosave --benchmark-save-data \ - --benchmark-storage={BENCHMARK_STORAGE_URI} \ - tests/test_benchmarks -""" - - cx.run(run_command) - -@task -def benchmark_compare(cx): - - # TODO logic for comparing across the last two - - run_command = \ -"""pytest-benchmark \ - --storage {storage} \ - compare 'Linux-CPython-3.6-64bit/*' \ - --csv=\"{csv}\" \ - > {output} -""".format(storage=BENCHMARK_STORAGE_URI, - csv="{}/Linux-CPython-3.6-64bit/comparison.csv".format(BENCHMARK_STORAGE_URL), - output="{}/Linux-CPython-3.6-64bit/report.pytest.txt".format(BENCHMARK_STORAGE_URL), -) - - cx.run(run_command) - -@task -def version_which(cx): - """Tell me what version the project is at.""" - - # get the current version - cx.run(f"python -m {project_slug()}._print_version") - -### Packaging - -## Building - -# IDEA here are some ideas I want to do - -# Source Distribution - -# Wheel: Binary Distribution - -# Beeware cross-patform - -# Debian Package (with `dh_virtualenv`) - -@task -def update_tools(cx): - - # WKRD, FIXME: because new versions of pip are incompatible with - # pip-tools we can't blindly update in envs. Want to make a - # mechanism for installing the tools in a separate - # requirements.txt file since we can't put these in the pip tools - # input files. In short should be taken care of in the 'env' module - print("Disable 'update_tools' use the envs 'tools.requirements.txt' instead") - pass - # cx.run("pip install --upgrade pip setuptools wheel twine") - -@task(pre=[update_tools]) -def build_sdist(cx): - """Make a source distribution""" - cx.run("python setup.py sdist") - -@task(pre=[update_tools]) -def build_bdist(cx): - """Make a binary wheel distribution.""" - - cx.run("python setup.py bdist_wheel") - -# STUB -@task -def conda_build(cx): - - cx.run("conda-build conda-recipe") - -@task(pre=[build_sdist, build_bdist,]) -def build(cx): - """Build all the python distributions supported.""" - pass - - -# IDEA: add a 'test_builds' target, that opens a clean environment and -# installs each build - - -## Publishing - -# testing publishing - - - -@task(pre=[clean_dist, build_sdist]) -def publish_test_pypi(cx, - version=None, -): - - assert version is not None - - cx.run("twine upload " - "--non-interactive " - f"--repository pypi_test " - f"--config-file {TESTING_PYPIRC} " - "dist/*") - -@task(pre=[clean_dist, update_tools, build_sdist]) -def publish_test(cx): - - publish_test_pypi(cx, - version=VERSION) - -# PyPI - - -@task(pre=[clean_dist, build]) -def publish_pypi(cx, version=None): - - assert version is not None - - cx.run(f"twine upload " - "--non-interactive " - "--repository pypi " - f"--config-file {PYPIRC} " - f"dist/*") - - -# TODO, SNIPPET, STUB: this is a desired target for uploading dists to github -# @task -# def publish_github_dists(cx, release=None): -# assert release is not None, "Release tag string must be given" -# pass - -@task(pre=[clean_dist, update_tools, build]) -def publish(cx): - - publish_pypi(cx, version=VERSION) diff --git a/tasks/plugins/__init__.py b/tasks/plugins/__init__.py deleted file mode 100644 index f9a6eb80..00000000 --- a/tasks/plugins/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Specify which plugins to load""" - -# import plugins: - -from . import custom -from . import tests - -# specify which plugins to install, the custom one is included by -# default to get users going -PLUGIN_MODULES = [ - custom, - tests, -] diff --git a/tasks/plugins/custom.py b/tasks/plugins/custom.py deleted file mode 100644 index 8bbc62ff..00000000 --- a/tasks/plugins/custom.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Put user defined tasks in the plugins folder. You can start with -some customizations in this file which is included by default.""" - -from invoke import task - - diff --git a/tasks/plugins/tests.py b/tasks/plugins/tests.py deleted file mode 100644 index 26505124..00000000 --- a/tasks/plugins/tests.py +++ /dev/null @@ -1,62 +0,0 @@ -from invoke import task - -from ..config import ( - REPORTS_DIR, -) - -import sys -import os -import os.path as osp -from pathlib import Path - -import pytest - - -# TODO: this should be done better -@task -def integration(cx, tag=None, node='node_minor'): - """Run the integration tests. - - This is a large test suite and needs specific hardware resources - in order to run all of them. For this reason there are different - test objects which are tagged as different grades of nodes. The - idea is that depending on the machine you are able to test on you - will still be able to run some of the tests to test pure-python - code paths or code paths that involve GPUs etc. - - The node types are: - - - minor :: no GPUs - - dev :: at least 1 GPU - - production :: more than 1 GPU - - You can use these as a 'mark' selection when running pytest or use - it as the option for this command. - - """ - - lines = [ - f"coverage run -m pytest ", - f"-m 'not interactive' ", - f"tests/test_integration", - ] - - if node == 'minor': - node = '' - - options = { - "html" : ( - "--html=reports/pytest/{tag}/integration/report.html" - if tag is not None - else "" - ), - - "node" : node, - } - - if tag is None: - cx.run('heerr', - warn=True) - else: - cx.run(f"coverage run -m pytest -m 'not interactive' tests/test_integration", - warn=True) diff --git a/tasks/sysconfig.py b/tasks/sysconfig.py deleted file mode 100644 index 9c26abd6..00000000 --- a/tasks/sysconfig.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Configuration managed by the system. All changes here will be -overwrote upon update. - -Typically gives a collection of good defaults. Override in config.py - -""" - -### Cleaning - -CLEAN_EXPRESSIONS = [ - "\"*~\"", -] - - -### Envs - -# which virtual environment tool to use: venv or conda -ENV_METHOD = 'venv' - -# which env spec to use by default -DEFAULT_ENV = 'dev' - -# directory where env specs are read from -ENVS_DIR = 'envs' - -# Python version source, this is how we get the different python -# versions. This is a keyword not a path -PYTHON_VERSION_SOURCE = "pyenv" - -# which versions will be requested to be installed, in the order of -# precendence for interactive work -PYTHON_VERSIONS = ( - '3.8.1', - '3.7.6', - '3.6.10', -) - - -### Git - -INITIAL_VERSION = '0.0.0a0.dev0' -GIT_LFS_TARGETS = [] -VERSION = '0.0.0a0.dev0' - - -### Python Code base - -REPORTS_DIR = "reports" - -## docs - -LOGO_DIR = "info/logo" - -ORG_DOCS_SOURCES = [ - 'changelog', - 'dev_guide', - 'general_info', - 'installation', - 'introduction', - 'news', - 'quick_start', - 'troubleshooting', - 'users_guide', - 'reference', - 'news-articles', -] - -RST_DOCS_SOURCES = [ - 'glossary', - 'api', -] - -BIB_DOCS_SOURCES = [ - 'docs', -] - -PYPIRC="$HOME/.pypirc" -TESTING_PYPIRC="$HOME/.pypirc" - -# this is the name of the pyenv "version" to use for creating and -# activating conda -PYENV_CONDA_NAME = 'miniconda3-latest' - -## tests -TESTS_DIR = "tests" - -## benchmarks -BENCHMARKS_DIR = "benchmarks" - -# the range of commits to use for running all of the asv regression -# benchmarks. See documentation in `asv run --help` for details. -# Defaults to using the HASHFILE. -ASV_RANGE = "HASHFILE:benchmark_selection.list" - - -### Containers - -# choose the container tool, options really are just: docker or podman -# since these two are compatible -CONTAINER_TOOL = "podman" diff --git a/tasks/toplevel.py b/tasks/toplevel.py deleted file mode 100644 index e3710f3b..00000000 --- a/tasks/toplevel.py +++ /dev/null @@ -1,6 +0,0 @@ -"""User editable top-level commands""" - -from invoke import task - -from .config import * - diff --git a/templates/envs/conda_blank/dev.requirements.list b/templates/envs/conda_blank/dev.requirements.list deleted file mode 100644 index 8b137891..00000000 --- a/templates/envs/conda_blank/dev.requirements.list +++ /dev/null @@ -1 +0,0 @@ - diff --git a/templates/envs/conda_blank/env.yaml b/templates/envs/conda_blank/env.yaml deleted file mode 100644 index f1438cbc..00000000 --- a/templates/envs/conda_blank/env.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: wepy-dev -channels: - - conda-forge - - defaults -dependencies: - diff --git a/templates/envs/conda_blank/pyversion.txt b/templates/envs/conda_blank/pyversion.txt deleted file mode 100644 index 8b137891..00000000 --- a/templates/envs/conda_blank/pyversion.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/templates/envs/conda_blank/requirements.in b/templates/envs/conda_blank/requirements.in deleted file mode 100644 index 11deb9f2..00000000 --- a/templates/envs/conda_blank/requirements.in +++ /dev/null @@ -1 +0,0 @@ ---index-url https://pypi.python.org/simple/ diff --git a/templates/envs/conda_blank/self.requirements.txt b/templates/envs/conda_blank/self.requirements.txt deleted file mode 100644 index 8b137891..00000000 --- a/templates/envs/conda_blank/self.requirements.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/templates/examples/org/README.org b/templates/examples/org/README.org deleted file mode 100644 index f309c256..00000000 --- a/templates/examples/org/README.org +++ /dev/null @@ -1 +0,0 @@ -* Example of an example diff --git a/templates/examples/org/input/.keep b/templates/examples/org/input/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/templates/examples/org/source/.keep b/templates/examples/org/source/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/templates/examples/org/tasks.py b/templates/examples/org/tasks.py deleted file mode 100644 index 149239c6..00000000 --- a/templates/examples/org/tasks.py +++ /dev/null @@ -1,44 +0,0 @@ -from invoke import task - -import os -import os.path as osp -from pathlib import Path - -def tangle_orgfile(cx, file_path): - """Tangle the target file using emacs in batch mode. Implicitly dumps - things relative to the file.""" - - cx.run(f"emacs -Q --batch -l org {file_path} -f org-babel-tangle") - -@task -def init(cx): - cx.run("mkdir -p _tangle_source") - cx.run("mkdir -p _output") - -@task -def clean(cx): - cx.run("rm -rf _tangle_source") - cx.run("rm -rf _output") - -@task(pre=[init]) -def tangle(cx): - tangle_orgfile(cx, "README.org") - cx.run(f"chmod ug+x ./_tangle_source/*.bash", warn=True) - cx.run(f"chmod ug+x ./_tangle_source/*.sh", warn=True) - cx.run(f"chmod ug+x ./_tangle_source/*.py", warn=True) - -@task -def clean_env(cx): - cx.run("rm -rf _env") - -@task(pre=[init]) -def env(cx): - """Create the environment from the specs in 'env'. Must have the - entire repository available as it uses the tooling from it. - - """ - - example_name = Path(os.getcwd()).stem - - with cx.cd("../../../"): - cx.run(f"inv docs.env-example -n {example_name}") diff --git a/templates/jigs/org/README.org b/templates/jigs/org/README.org deleted file mode 100644 index f309c256..00000000 --- a/templates/jigs/org/README.org +++ /dev/null @@ -1 +0,0 @@ -* Example of an example diff --git a/templates/jigs/org/input/.keep b/templates/jigs/org/input/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/templates/jigs/org/source/.keep b/templates/jigs/org/source/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/templates/jigs/org/tasks.py b/templates/jigs/org/tasks.py deleted file mode 100644 index 07dfda44..00000000 --- a/templates/jigs/org/tasks.py +++ /dev/null @@ -1,44 +0,0 @@ -from invoke import task - -import os -import os.path as osp -from pathlib import Path - -def tangle_orgfile(cx, file_path): - """Tangle the target file using emacs in batch mode. Implicitly dumps - things relative to the file.""" - - cx.run(f"emacs -Q --batch -l org {file_path} -f org-babel-tangle") - -@task -def init(cx): - cx.run("mkdir -p _tangle_source") - cx.run("mkdir -p _output") - -@task -def clean(cx): - cx.run("rm -rf _tangle_source") - cx.run("rm -rf _output") - -@task(pre=[init]) -def tangle(cx): - tangle_orgfile(cx, "README.org") - cx.run(f"chmod ug+x ./_tangle_source/*.bash", warn=True) - cx.run(f"chmod ug+x ./_tangle_source/*.sh", warn=True) - cx.run(f"chmod ug+x ./_tangle_source/*.py", warn=True) - -@task -def clean_env(cx): - cx.run("rm -rf _env") - -@task(pre=[init]) -def env(cx): - """Create the environment from the specs in 'env'. Must have the - entire repository available as it uses the tooling from it. - - """ - - jig_name = Path(os.getcwd()).stem - - with cx.cd("../../../"): - cx.run(f"inv py.env-jig -n {jig_name}") diff --git a/templates/tutorials/jupyter/README.ipynb b/templates/tutorials/jupyter/README.ipynb deleted file mode 100644 index cccdcaeb..00000000 --- a/templates/tutorials/jupyter/README.ipynb +++ /dev/null @@ -1,32 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.1" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/templates/tutorials/jupyter/input/.keep b/templates/tutorials/jupyter/input/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/templates/tutorials/jupyter/tasks.py b/templates/tutorials/jupyter/tasks.py deleted file mode 100644 index b271a2d1..00000000 --- a/templates/tutorials/jupyter/tasks.py +++ /dev/null @@ -1,36 +0,0 @@ -from invoke import task - -import os -import os.path as osp -from pathlib import Path - -@task -def init(cx): - cx.run("mkdir -p _output") - cx.run("mkdir -p _tangle_source") - -@task -def clean(cx): - cx.run("rm -rf _output/*") - cx.run("rm -rf _tangle_source/*") - -@task(pre=[init]) -def tangle(cx): - cx.run("jupyter-nbconvert --to 'python' --output-dir=_tangle_source README.ipynb") - - -@task -def clean_env(cx): - cx.run("rm -rf _env") - -@task(pre=[init]) -def env(cx): - """Create the environment from the specs in 'env'. Must have the - entire repository available as it uses the tooling from it. - - """ - - example_name = Path(os.getcwd()).stem - - with cx.cd("../../../"): - cx.run(f"inv docs.env-tutorial -n {example_name}") diff --git a/templates/tutorials/org/README.org b/templates/tutorials/org/README.org deleted file mode 100644 index 7bd900c8..00000000 --- a/templates/tutorials/org/README.org +++ /dev/null @@ -1,2 +0,0 @@ - -* Your Tutorial Here diff --git a/templates/tutorials/org/input/.keep b/templates/tutorials/org/input/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/templates/tutorials/org/source/.keep b/templates/tutorials/org/source/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/templates/tutorials/org/tasks.py b/templates/tutorials/org/tasks.py deleted file mode 100644 index d4b9361f..00000000 --- a/templates/tutorials/org/tasks.py +++ /dev/null @@ -1,44 +0,0 @@ -from invoke import task - -import os -import os.path as osp -from pathlib import Path - -def tangle_orgfile(cx, file_path): - """Tangle the target file using emacs in batch mode. Implicitly dumps - things relative to the file.""" - - cx.run(f"emacs -Q --batch -l org {file_path} -f org-babel-tangle") - -@task -def init(cx): - cx.run("mkdir -p _tangle_source") - cx.run("mkdir -p _output") - -@task -def clean(cx): - cx.run("rm -rf _tangle_source") - cx.run("rm -rf _output") - -@task(pre=[init]) -def tangle(cx): - tangle_orgfile(cx, "README.org") - cx.run(f"chmod ug+x ./_tangle_source/*.bash", warn=True) - cx.run(f"chmod ug+x ./_tangle_source/*.sh", warn=True) - cx.run(f"chmod ug+x ./_tangle_source/*.py", warn=True) - -@task -def clean_env(cx): - cx.run("rm -rf _env") - -@task(pre=[init]) -def env(cx): - """Create the environment from the specs in 'env'. Must have the - entire repository available as it uses the tooling from it. - - """ - - example_name = Path(os.getcwd()).stem - - with cx.cd("../../../"): - cx.run(f"inv docs.env-tutorial -n {example_name}") From 8624a04889fd6c25d4d67e2ddf628306f4ad9f99 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 24 Nov 2025 13:41:54 -0500 Subject: [PATCH 002/143] update AUTHORS files --- AUTHORS.org => AUTHORS.md | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename AUTHORS.org => AUTHORS.md (71%) diff --git a/AUTHORS.org b/AUTHORS.md similarity index 71% rename from AUTHORS.org rename to AUTHORS.md index 839c73c3..512eb07a 100644 --- a/AUTHORS.org +++ b/AUTHORS.md @@ -1,10 +1,10 @@ -* Credits +# Credits -** Development Lead +## Development Lead Samuel D. Lotz -** Contributors +## Contributors Alex Dickson Nazanin Donyapour diff --git a/pyproject.toml b/pyproject.toml index a6d14d89..a1638bf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ license = "MIT" requires-python = ">=3.11" authors = [ - { name = "Samuel Lotz", email = "salotz@salotz.info" }, + { name = "Samuel Lotz", email = "samuel.lotz@salotz.info" }, { name = "Alex Dickson", email = "alexrd@msu.edu" }, { name = "Tom Dixon" }, { name = "Robert Hall" }, From 04329d704eeaaae082a27abe7811e6522d09ec8e Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 24 Nov 2025 14:55:41 -0500 Subject: [PATCH 003/143] enable using uv for proj mgmt --- .envrc | 3 + .gitignore | 2 +- pyproject.toml | 41 +- uv.lock | 3230 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 3266 insertions(+), 10 deletions(-) create mode 100644 .envrc create mode 100644 uv.lock diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..bb852784 --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +source_env_if_exists .envrc.local +. ./.venv/bin/activate + diff --git a/.gitignore b/.gitignore index 21fbb97d..fbcf6960 100644 --- a/.gitignore +++ b/.gitignore @@ -287,5 +287,5 @@ tags .vim # End of https://www.gitignore.io/api/vim -uv.lock _output +.envrc.local \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a1638bf8..05a9bd27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,3 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" [project] name = "wepy" @@ -16,7 +13,7 @@ authors = [ { name = "Robert Hall" }, { name = "Nicole Roussey" }, ] -dynamic = ["version"] +version = "1.1.0" classifiers = [ "Topic :: Utilities", @@ -66,6 +63,7 @@ graphics = [ "pillow>=10.0.1", ] + [project.urls] Documentation = "https://adicksonlab.github.io/wepy/index.html" @@ -76,11 +74,36 @@ Issues = "https://github.com/ADicksonLab/wepy/issues" wepy = "wepy.__main__:cli" -# NOTE: currently disabled since it requires OpenMM currently to work which is -# not ideal for unit testing - -# [project.entry-points.pytest11] -# pytest-wepy = "pytest_wepy" +[build-system] +requires = ["uv_build>=0.9.11,<0.10.0"] +build-backend = "uv_build" + +[dependency-groups] + +dev = [ + # testing + "pytest", + "coverage", + "pytest-cov", + # qa + "black", + "isort", + "ruff", + "interrogate", + # typechecking + "mypy", + # interactive + "ipython", + "pdbpp", + # docs + "sphinx", + "sphinxcontrib-napoleon", + "sphinxcontrib-newsfeed", + "sphinxcontrib-bibtex", + "sphinxcontrib-newsfeed", + "nbsphinx", + "notebook", +] [tool.interrogate] diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..b94fd9b3 --- /dev/null +++ b/uv.lock @@ -0,0 +1,3230 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", + "python_full_version < '3.12'", +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "async-lru" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/4d/71ec4d3939dc755264f680f6c2b4906423a304c3d18e96853f0a595dfe97/async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb", size = 10380, upload-time = "2025-03-16T17:25:36.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069, upload-time = "2025-03-16T17:25:35.422Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" }, +] + +[[package]] +name = "black" +version = "25.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, + { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, + { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" }, + { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "coverage" +version = "7.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, + { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, + { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, + { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, + { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, + { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, + { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, + { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, + { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, + { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" }, + { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" }, + { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" }, + { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" }, + { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" }, + { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" }, + { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" }, + { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" }, + { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" }, + { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" }, + { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" }, + { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" }, + { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" }, + { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" }, + { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" }, + { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" }, + { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" }, + { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" }, + { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" }, + { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" }, + { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" }, + { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "dask" +version = "2025.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "cloudpickle" }, + { name = "fsspec" }, + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "partd" }, + { name = "pyyaml" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/33/eacaa72731f7fc64868caaf2d35060d50049eff889bd217263e68f76472f/dask-2025.11.0.tar.gz", hash = "sha256:23d59e624b80ee05b7cc8df858682cca58262c4c3b197ccf61da0f6543c8f7c3", size = 10984781, upload-time = "2025-11-06T16:56:51.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/54/a46920229d12c3a6e9f0081d1bdaeffad23c1826353ace95714faee926e5/dask-2025.11.0-py3-none-any.whl", hash = "sha256:08c35a8146c05c93b34f83cf651009129c42ee71762da7ca452fb7308641c2b8", size = 1477108, upload-time = "2025-11-06T16:56:44.892Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129, upload-time = "2025-09-17T16:33:20.633Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/53/3af72b5c159278c4a0cf4cffa518675a0e73bdb7d1cac0239b815502d2ce/debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840", size = 2207154, upload-time = "2025-09-17T16:33:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6d/204f407df45600e2245b4a39860ed4ba32552330a0b3f5f160ae4cc30072/debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f", size = 3170322, upload-time = "2025-09-17T16:33:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/f2/13/1b8f87d39cf83c6b713de2620c31205299e6065622e7dd37aff4808dd410/debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da", size = 5155078, upload-time = "2025-09-17T16:33:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c5/c012c60a2922cc91caa9675d0ddfbb14ba59e1e36228355f41cab6483469/debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4", size = 5179011, upload-time = "2025-09-17T16:33:35.711Z" }, + { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" }, + { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" }, + { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" }, + { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053, upload-time = "2025-09-17T16:33:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/597e5cb97d026274ba297af8d89138dfd9e695767ba0e0895edb20963f40/debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464", size = 2538386, upload-time = "2025-09-17T16:33:54.594Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/ce5c34fcdfec493701f9d1532dba95b21b2f6394147234dce21160bd923f/debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088", size = 4292100, upload-time = "2025-09-17T16:33:56.353Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/7873cf2146577ef71d2a20bf553f12df865922a6f87b9e8ee1df04f01785/debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83", size = 5277002, upload-time = "2025-09-17T16:33:58.231Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/18c79a1cee5ff539a94ec4aa290c1c069a5580fd5cfd2fb2e282f8e905da/debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420", size = 5319047, upload-time = "2025-09-17T16:34:00.586Z" }, + { url = "https://files.pythonhosted.org/packages/de/45/115d55b2a9da6de812696064ceb505c31e952c5d89c4ed1d9bb983deec34/debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1", size = 2536899, upload-time = "2025-09-17T16:34:02.657Z" }, + { url = "https://files.pythonhosted.org/packages/5a/73/2aa00c7f1f06e997ef57dc9b23d61a92120bec1437a012afb6d176585197/debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f", size = 4268254, upload-time = "2025-09-17T16:34:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/86/b5/ed3e65c63c68a6634e3ba04bd10255c8e46ec16ebed7d1c79e4816d8a760/debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670", size = 5277203, upload-time = "2025-09-17T16:34:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/b0/26/394276b71c7538445f29e792f589ab7379ae70fd26ff5577dfde71158e96/debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c", size = 5318493, upload-time = "2025-09-17T16:34:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fancycompleter" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "python_full_version < '3.13' and sys_platform == 'win32'" }, + { name = "pyrepl", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/4c/d11187dee93eff89d082afda79b63c79320ae1347e49485a38f05ad359d0/fancycompleter-0.11.1.tar.gz", hash = "sha256:5b4ad65d76b32b1259251516d0f1cb2d82832b1ff8506697a707284780757f69", size = 341776, upload-time = "2025-05-26T12:59:11.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/c3/6f0e3896f193528bbd2b4d2122d4be8108a37efab0b8475855556a8c4afa/fancycompleter-0.11.1-py3-none-any.whl", hash = "sha256:44243d7fab37087208ca5acacf8f74c0aa4d733d04d593857873af7513cdf8a6", size = 11207, upload-time = "2025-05-26T12:59:09.857Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "flexcache" +version = "0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b0/8a21e330561c65653d010ef112bf38f60890051d244ede197ddaa08e50c1/flexcache-0.3.tar.gz", hash = "sha256:18743bd5a0621bfe2cf8d519e4c3bfdf57a269c15d1ced3fb4b64e0ff4600656", size = 15816, upload-time = "2024-03-09T03:21:07.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl", hash = "sha256:d43c9fea82336af6e0115e308d9d33a185390b8346a017564611f1466dcd2e32", size = 13263, upload-time = "2024-03-09T03:21:05.635Z" }, +] + +[[package]] +name = "flexparser" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/99/b4de7e39e8eaf8207ba1a8fa2241dd98b2ba72ae6e16960d8351736d8702/flexparser-0.4.tar.gz", hash = "sha256:266d98905595be2ccc5da964fe0a2c3526fbbffdc45b65b3146d75db992ef6b2", size = 31799, upload-time = "2024-11-07T02:00:56.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl", hash = "sha256:3738b456192dcb3e15620f324c447721023c0293f6af9955b481e91d00179846", size = 27625, upload-time = "2024-11-07T02:00:54.523Z" }, +] + +[[package]] +name = "fonttools" +version = "4.60.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" }, + { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" }, + { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" }, + { url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" }, + { url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" }, + { url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" }, + { url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" }, + { url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" }, + { url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" }, + { url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" }, + { url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" }, +] + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, +] + +[[package]] +name = "geomm" +version = "0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pint" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/b3/1a961038e36961522062fbb1799f731392b3cc6c34e22c724e5c68a97d36/geomm-0.3.tar.gz", hash = "sha256:c074e29e4e0d967807792f72a5e764e136aad65e9d5322ecca5fcdffdfb90336", size = 201199, upload-time = "2025-07-07T19:56:18.842Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/5f/a38ea01eb6f6e1d62509667c0d3919c0e3d573d582c62fc96a1e949fc7cb/geomm-0.3-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c4087441b8d74c45005d6b63bc1c40b63a79240b6854d4454faa73d978305e88", size = 155587, upload-time = "2025-07-07T19:56:17.819Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h5py" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/6a/0d79de0b025aa85dc8864de8e97659c94cf3d23148394a954dc5ca52f8c8/h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69", size = 426236, upload-time = "2025-10-16T10:35:27.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243", size = 3434135, upload-time = "2025-10-16T10:33:47.954Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509", size = 2870958, upload-time = "2025-10-16T10:33:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e3/c255cafc9b85e6ea04e2ad1bba1416baa1d7f57fc98a214be1144087690c/h5py-3.15.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80e5bb5b9508d5d9da09f81fd00abbb3f85da8143e56b1585d59bc8ceb1dba8b", size = 4504770, upload-time = "2025-10-16T10:33:54.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf", size = 4700329, upload-time = "2025-10-16T10:33:57.616Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e4/932a3a8516e4e475b90969bf250b1924dbe3612a02b897e426613aed68f4/h5py-3.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f6c841efd4e6e5b7e82222eaf90819927b6d256ab0f3aca29675601f654f3c", size = 4152456, upload-time = "2025-10-16T10:34:00.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/f74d589883b13737021b2049ac796328f188dbb60c2ed35b101f5b95a3fc/h5py-3.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8a3a22458956ee7b40d8e39c9a9dc01f82933e4c030c964f8b875592f4d831", size = 4617295, upload-time = "2025-10-16T10:34:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878", size = 2882129, upload-time = "2025-10-16T10:34:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/cfcc70b8a42222ba3ad4478bcef1791181ea908e2adbd7d53c66395edad5/h5py-3.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:b39239947cb36a819147fc19e86b618dcb0953d1cd969f5ed71fc0de60392427", size = 2477121, upload-time = "2025-10-16T10:34:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/62/b8/c0d9aa013ecfa8b7057946c080c0c07f6fa41e231d2e9bd306a2f8110bdc/h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c", size = 3399089, upload-time = "2025-10-16T10:34:12.135Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5e/3c6f6e0430813c7aefe784d00c6711166f46225f5d229546eb53032c3707/h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76", size = 2847803, upload-time = "2025-10-16T10:34:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/ba36273b888a4a48d78f9268d2aee05787e4438557450a8442946ab8f3ec/h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e", size = 4914884, upload-time = "2025-10-16T10:34:18.452Z" }, + { url = "https://files.pythonhosted.org/packages/3a/30/d1c94066343a98bb2cea40120873193a4fed68c4ad7f8935c11caf74c681/h5py-3.15.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25c8843fec43b2cc368aa15afa1cdf83fc5e17b1c4e10cd3771ef6c39b72e5ce", size = 5109965, upload-time = "2025-10-16T10:34:21.853Z" }, + { url = "https://files.pythonhosted.org/packages/81/3d/d28172116eafc3bc9f5991b3cb3fd2c8a95f5984f50880adfdf991de9087/h5py-3.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a308fd8681a864c04423c0324527237a0484e2611e3441f8089fd00ed56a8171", size = 4561870, upload-time = "2025-10-16T10:34:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/393a7226024238b0f51965a7156004eaae1fcf84aa4bfecf7e582676271b/h5py-3.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f4a016df3f4a8a14d573b496e4d1964deb380e26031fc85fb40e417e9131888a", size = 5037161, upload-time = "2025-10-16T10:34:30.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/51/329e7436bf87ca6b0fe06dd0a3795c34bebe4ed8d6c44450a20565d57832/h5py-3.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:59b25cf02411bf12e14f803fef0b80886444c7fe21a5ad17c6a28d3f08098a1e", size = 2874165, upload-time = "2025-10-16T10:34:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/2d02b10a66747c54446e932171dd89b8b4126c0111b440e6bc05a7c852ec/h5py-3.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:61d5a58a9851e01ee61c932bbbb1c98fe20aba0a5674776600fb9a361c0aa652", size = 2458214, upload-time = "2025-10-16T10:34:35.733Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/40207e0192415cbff7ea1d37b9f24b33f6d38a5a2f5d18a678de78f967ae/h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8440fd8bee9500c235ecb7aa1917a0389a2adb80c209fa1cc485bd70e0d94a5", size = 3376511, upload-time = "2025-10-16T10:34:38.596Z" }, + { url = "https://files.pythonhosted.org/packages/31/96/ba99a003c763998035b0de4c299598125df5fc6c9ccf834f152ddd60e0fb/h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab2219dbc6fcdb6932f76b548e2b16f34a1f52b7666e998157a4dfc02e2c4123", size = 2826143, upload-time = "2025-10-16T10:34:41.342Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/fc6375d07ea3962df7afad7d863fe4bde18bb88530678c20d4c90c18de1d/h5py-3.15.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8cb02c3a96255149ed3ac811eeea25b655d959c6dd5ce702c9a95ff11859eb5", size = 4908316, upload-time = "2025-10-16T10:34:44.619Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:121b2b7a4c1915d63737483b7bff14ef253020f617c2fb2811f67a4bed9ac5e8", size = 5103710, upload-time = "2025-10-16T10:34:48.639Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f6/11f1e2432d57d71322c02a97a5567829a75f223a8c821764a0e71a65cde8/h5py-3.15.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59b0d63b318bf3cc06687def2b45afd75926bbc006f7b8cd2b1a231299fc8599", size = 4556042, upload-time = "2025-10-16T10:34:51.841Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/3eda3ef16bfe7a7dbc3d8d6836bbaa7986feb5ff091395e140dc13927bcc/h5py-3.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e02fe77a03f652500d8bff288cbf3675f742fc0411f5a628fa37116507dc7cc0", size = 5030639, upload-time = "2025-10-16T10:34:55.257Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ea/fbb258a98863f99befb10ed727152b4ae659f322e1d9c0576f8a62754e81/h5py-3.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:dea78b092fd80a083563ed79a3171258d4a4d307492e7cf8b2313d464c82ba52", size = 2864363, upload-time = "2025-10-16T10:34:58.099Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c9/35021cc9cd2b2915a7da3026e3d77a05bed1144a414ff840953b33937fb9/h5py-3.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:c256254a8a81e2bddc0d376e23e2a6d2dc8a1e8a2261835ed8c1281a0744cd97", size = 2449570, upload-time = "2025-10-16T10:35:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/926eba1514e4d2e47d0e9eb16c784e717d8b066398ccfca9b283917b1bfb/h5py-3.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f4fb0567eb8517c3ecd6b3c02c4f4e9da220c8932604960fd04e24ee1254763", size = 3380368, upload-time = "2025-10-16T10:35:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/65/4b/d715ed454d3baa5f6ae1d30b7eca4c7a1c1084f6a2edead9e801a1541d62/h5py-3.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:954e480433e82d3872503104f9b285d369048c3a788b2b1a00e53d1c47c98dd2", size = 2833793, upload-time = "2025-10-16T10:35:05.623Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d4/ef386c28e4579314610a8bffebbee3b69295b0237bc967340b7c653c6c10/h5py-3.15.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd125c131889ebbef0849f4a0e29cf363b48aba42f228d08b4079913b576bb3a", size = 4903199, upload-time = "2025-10-16T10:35:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/33/5d/65c619e195e0b5e54ea5a95c1bb600c8ff8715e0d09676e4cce56d89f492/h5py-3.15.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28a20e1a4082a479b3d7db2169f3a5034af010b90842e75ebbf2e9e49eb4183e", size = 5097224, upload-time = "2025-10-16T10:35:12.808Z" }, + { url = "https://files.pythonhosted.org/packages/30/30/5273218400bf2da01609e1292f562c94b461fcb73c7a9e27fdadd43abc0a/h5py-3.15.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8df5267f545b4946df8ca0d93d23382191018e4cda2deda4c2cedf9a010e13", size = 4551207, upload-time = "2025-10-16T10:35:16.24Z" }, + { url = "https://files.pythonhosted.org/packages/d3/39/a7ef948ddf4d1c556b0b2b9559534777bccc318543b3f5a1efdf6b556c9c/h5py-3.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99d374a21f7321a4c6ab327c4ab23bd925ad69821aeb53a1e75dd809d19f67fa", size = 5025426, upload-time = "2025-10-16T10:35:19.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d8/7368679b8df6925b8415f9dcc9ab1dab01ddc384d2b2c24aac9191bd9ceb/h5py-3.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:9c73d1d7cdb97d5b17ae385153472ce118bed607e43be11e9a9deefaa54e0734", size = 2865704, upload-time = "2025-10-16T10:35:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/4a806f85d62c20157e62e58e03b27513dc9c55499768530acc4f4c5ce4be/h5py-3.15.1-cp314-cp314-win_arm64.whl", hash = "sha256:a6d8c5a05a76aca9a494b4c53ce8a9c29023b7f64f625c6ce1841e92a362ccdf", size = 2465544, upload-time = "2025-10-16T10:35:25.695Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "interrogate" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "colorama" }, + { name = "py" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/22/74f7fcc96280eea46cf2bcbfa1354ac31de0e60a4be6f7966f12cef20893/interrogate-1.7.0.tar.gz", hash = "sha256:a320d6ec644dfd887cc58247a345054fc4d9f981100c45184470068f4b3719b0", size = 159636, upload-time = "2024-04-07T22:30:46.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl", hash = "sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12", size = 46982, upload-time = "2024-04-07T22:30:44.277Z" }, +] + +[[package]] +name = "ipykernel" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c", size = 117968, upload-time = "2025-10-27T09:46:37.805Z" }, +] + +[[package]] +name = "ipython" +version = "9.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/e6/48c74d54039241a456add616464ea28c6ebf782e4110d419411b83dae06f/ipython-9.7.0.tar.gz", hash = "sha256:5f6de88c905a566c6a9d6c400a8fed54a638e1f7543d17aae2551133216b1e4e", size = 4422115, upload-time = "2025-11-05T12:18:54.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl", hash = "sha256:bce8ac85eb9521adc94e1845b4c03d88365fd6ac2f4908ec4ed1eb1b0a065f9f", size = 618911, upload-time = "2025-11-05T12:18:52.484Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "isort" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "json5" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/ae/929aee9619e9eba9015207a9d2c1c54db18311da7eb4dcf6d41ad6f0eb67/json5-0.12.1.tar.gz", hash = "sha256:b2743e77b3242f8d03c143dd975a6ec7c52e2f2afe76ed934e53503dd4ad4990", size = 52191, upload-time = "2025-08-12T19:47:42.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/e2/05328bd2621be49a6fed9e3030b1e51a2d04537d3f816d211b9cc53c5262/json5-0.12.1-py3-none-any.whl", hash = "sha256:d9c9b3bc34a5f54d43c35e11ef7cb87d8bdd098c6ace87117a7b7e83e705c1d5", size = 36119, upload-time = "2025-08-12T19:47:41.131Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyter-events" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" }, +] + +[[package]] +name = "jupyter-server" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "overrides", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430, upload-time = "2024-03-12T14:37:03.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa", size = 13656, upload-time = "2024-03-12T14:37:00.708Z" }, +] + +[[package]] +name = "jupyterlab" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "setuptools" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/e5/4fa382a796a6d8e2cd867816b64f1ff27f906e43a7a83ad9eb389e448cd8/jupyterlab-4.5.0.tar.gz", hash = "sha256:aec33d6d8f1225b495ee2cf20f0514f45e6df8e360bdd7ac9bace0b7ac5177ea", size = 23989880, upload-time = "2025-11-18T13:19:00.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/1e/5a4d5498eba382fee667ed797cf64ae5d1b13b04356df62f067f48bb0f61/jupyterlab-4.5.0-py3-none-any.whl", hash = "sha256:88e157c75c1afff64c7dc4b801ec471450b922a4eae4305211ddd40da8201c8a", size = 12380641, upload-time = "2025-11-18T13:18:56.252Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "latexcodec" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/dd/4270b2c5e2ee49316c3859e62293bd2ea8e382339d63ab7bbe9f39c0ec3b/latexcodec-3.0.1.tar.gz", hash = "sha256:e78a6911cd72f9dec35031c6ec23584de6842bfbc4610a9678868d14cdfb0357", size = 31222, upload-time = "2025-06-17T18:47:34.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/40/23569737873cc9637fd488606347e9dd92b9fa37ba4fcda1f98ee5219a97/latexcodec-3.0.1-py3-none-any.whl", hash = "sha256:a9eb8200bff693f0437a69581f7579eb6bca25c4193515c09900ce76451e452e", size = 18532, upload-time = "2025-06-17T18:47:30.726Z" }, +] + +[[package]] +name = "locket" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" }, + { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" }, + { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" }, + { url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" }, + { url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" }, + { url = "https://files.pythonhosted.org/packages/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212, upload-time = "2025-10-09T00:26:56.752Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713, upload-time = "2025-10-09T00:26:59.001Z" }, + { url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527, upload-time = "2025-10-09T00:27:00.69Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690, upload-time = "2025-10-09T00:27:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732, upload-time = "2025-10-09T00:27:04.653Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727, upload-time = "2025-10-09T00:27:06.814Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958, upload-time = "2025-10-09T00:27:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849, upload-time = "2025-10-09T00:27:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225, upload-time = "2025-10-09T00:27:12.241Z" }, + { url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708, upload-time = "2025-10-09T00:27:13.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409, upload-time = "2025-10-09T00:27:15.684Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054, upload-time = "2025-10-09T00:27:17.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100, upload-time = "2025-10-09T00:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131, upload-time = "2025-10-09T00:27:21.608Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/e5bc2c321b6a7e3a75638d937d19ea267c34bd5a90e12bee76c4d7c7a0d9/matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100", size = 8273787, upload-time = "2025-10-09T00:27:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/6efae459c56c2fbc404da154e13e3a6039129f3c942b0152624f1c621f05/matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f", size = 8131348, upload-time = "2025-10-09T00:27:24.926Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5a/a4284d2958dee4116359cc05d7e19c057e64ece1b4ac986ab0f2f4d52d5a/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715", size = 9533949, upload-time = "2025-10-09T00:27:26.704Z" }, + { url = "https://files.pythonhosted.org/packages/de/ff/f3781b5057fa3786623ad8976fc9f7b0d02b2f28534751fd5a44240de4cf/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1", size = 9804247, upload-time = "2025-10-09T00:27:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/5a/993a59facb8444efb0e197bf55f545ee449902dcee86a4dfc580c3b61314/matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722", size = 9595497, upload-time = "2025-10-09T00:27:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a5/77c95aaa9bb32c345cbb49626ad8eb15550cba2e6d4c88081a6c2ac7b08d/matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866", size = 8252732, upload-time = "2025-10-09T00:27:32.332Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/45d269b4268d222390d7817dae77b159651909669a34ee9fdee336db5883/matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb", size = 8124240, upload-time = "2025-10-09T00:27:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c7/ca01c607bb827158b439208c153d6f14ddb9fb640768f06f7ca3488ae67b/matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1", size = 8316938, upload-time = "2025-10-09T00:27:35.534Z" }, + { url = "https://files.pythonhosted.org/packages/84/d2/5539e66e9f56d2fdec94bb8436f5e449683b4e199bcc897c44fbe3c99e28/matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4", size = 8178245, upload-time = "2025-10-09T00:27:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/77/b5/e6ca22901fd3e4fe433a82e583436dd872f6c966fca7e63cf806b40356f8/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318", size = 9541411, upload-time = "2025-10-09T00:27:39.387Z" }, + { url = "https://files.pythonhosted.org/packages/9e/99/a4524db57cad8fee54b7237239a8f8360bfcfa3170d37c9e71c090c0f409/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca", size = 9803664, upload-time = "2025-10-09T00:27:41.492Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" }, + { url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" }, + { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "mdtraj" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pyparsing" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/87/04f21b321e7bb6f3e09b5339df51699b8caae4846796175845bbc44f298b/mdtraj-1.11.0.tar.gz", hash = "sha256:2cf0ed2ee9a603dc4599743b1806e1387c655e2a1e9d013841b99fb137ad852e", size = 2860049, upload-time = "2025-06-25T02:06:19.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/bc/1a6d273467a24dad5048bf527288a7bbe997ecee8342f6e8fdd7465c9132/mdtraj-1.11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a6bbf2bc03a2ed23fd1fe696f6f6f970f495e843f3baaa71a0e99da086420de1", size = 2463123, upload-time = "2025-06-25T02:03:06.602Z" }, + { url = "https://files.pythonhosted.org/packages/09/dc/37bd8a22c66d05a293979d8d8cf1de5e6c03d21ebd6950b6a839c8d8a16b/mdtraj-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c34002c49282d208fdd34cd1b2578859bc7377a22ebd66a194efa1fc17395e5", size = 8023237, upload-time = "2025-06-25T02:03:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/4c/48/6fc195907c056df9b6ff6442bb39c5dd433b971b9deaecf6d18a0761efa8/mdtraj-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:4c794aac3db13937ed7e4f8c2b773564a89b2b539c15f43bae9dab6e1ffa5de1", size = 1355146, upload-time = "2025-06-25T02:03:31.524Z" }, + { url = "https://files.pythonhosted.org/packages/21/71/56adb24577052f2cd6f9b678da908e8e2d7963db3a88faff39792be43903/mdtraj-1.11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:878fb8e62962b881ba75d4200637280b518e891e4aa5f213052ddb1905704558", size = 2448664, upload-time = "2025-06-25T02:03:43.052Z" }, + { url = "https://files.pythonhosted.org/packages/50/af/7d65be36619befb29b51b6118a97121de6685720af7f4cfd6ff1902ca7eb/mdtraj-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01eb00d9b62f22056303fb8e4be0f85257a2ce1003f1e6274393bbcc977badaf", size = 8028112, upload-time = "2025-06-25T02:04:12.949Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8f/fd05aa3613fad29b3a779e3384797d2b64aa329bcfd18d25f8fb9c0d725e/mdtraj-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:f87117f7e6742b11a3a39e55fe5c9b4fc013d27691ac671bcc6dfb8193beae0f", size = 1341985, upload-time = "2025-06-25T02:04:19.133Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8f/adbeabf14f27a2befd021548c5d8e696ca129924e0473863fb0e9e546f27/mdtraj-1.11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:125db8d7b8f795ed2a771d5d6f419461e2f9dd21ef74d43311570b9dadbf0d46", size = 2432588, upload-time = "2025-06-25T02:04:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c4/4a2e8db0f24c1deb85f4719d54a0a52c56403e17db61ddd72a95d39e07d6/mdtraj-1.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46def167a429af5834f4f26a80bca372518952519847c1ceffd0163814b1164e", size = 7983336, upload-time = "2025-06-25T02:04:57.022Z" }, + { url = "https://files.pythonhosted.org/packages/ac/17/239ae1869fa0f6fe11e42388f5d1e094ac774f4d122767722f0451523989/mdtraj-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ad81b0aabf090b1b0e60ee7974bd676046e48d35a7efdc9a37a8eacaf5b70e4", size = 1340489, upload-time = "2025-06-25T02:05:04.776Z" }, + { url = "https://files.pythonhosted.org/packages/ca/56/20981603c4c06f598ec655a373ef95598dbcbf769ead833f74ed2c6b3d64/mdtraj-1.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:18d237661deeb49e4b56c43426ef7e4824051ae5ee21df976cb70e2baeafb8fa", size = 1341102, upload-time = "2025-10-22T20:20:52.799Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cc/8ddab5179e62c52f98d942dc51b181c8ffdddc874eab1885c351bca3850a/mdtraj-1.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9d9e2b44e6139d40525cb78b3a1d79dbe0504256a133621bfdeec11a22f44519", size = 1313566, upload-time = "2025-10-22T20:20:54.912Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a6/658751c60c0484e2ec7e31ae8bb811885a2d7ca847cd004cc67da5535a01/mdtraj-1.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18433b12a7b08848589b459961e1ccfcf3152afbbbad8ca847783075ba79e9e1", size = 7754412, upload-time = "2025-10-22T20:20:58.404Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a5/82af73a397434488042fa52ad8922028e1221d06ec31535ed13fff63cb57/mdtraj-1.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4d682faf2dd2eaf1d9cbba53f8e04504e80f0710580d6cd95f05286bdbeb755", size = 7914822, upload-time = "2025-10-22T20:21:00.39Z" }, +] + +[[package]] +name = "mistune" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/02/a7fb8b21d4d55ac93cdcde9d3638da5dd0ebdd3a4fed76c7725e10b81cbe/mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164", size = 94588, upload-time = "2025-08-29T07:20:43.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d", size = 53481, upload-time = "2025-08-29T07:20:42.218Z" }, +] + +[[package]] +name = "multiprocessing-logging" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/fe/32bd864bcb604b0607924a4cf618ed267a0ef21ac9c3e255109256046e1f/multiprocessing_logging-0.3.4-py2.py3-none-any.whl", hash = "sha256:8a5be02b02edbd6fa6e3e89499af7680db69db9e2d8707fcd28d445fa248f23e", size = 8770, upload-time = "2023-02-05T17:06:20.632Z" }, +] + +[[package]] +name = "mypy" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424, upload-time = "2024-12-19T10:32:27.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434, upload-time = "2024-12-19T10:32:24.139Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.16.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715, upload-time = "2025-01-28T09:29:14.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525, upload-time = "2025-01-28T09:29:12.551Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nbsphinx" +version = "0.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "sphinx" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/84/b1856b7651ac34e965aa567a158714c7f3bd42a1b1ce76bf423ffb99872c/nbsphinx-0.9.7.tar.gz", hash = "sha256:abd298a686d55fa894ef697c51d44f24e53aa312dadae38e82920f250a5456fe", size = 180479, upload-time = "2025-03-03T19:46:08.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/2d/8c8e635bcc6757573d311bb3c5445426382f280da32b8cd6d82d501ef4a4/nbsphinx-0.9.7-py3-none-any.whl", hash = "sha256:7292c3767fea29e405c60743eee5393682a83982ab202ff98f5eb2db02629da8", size = 31660, upload-time = "2025-03-03T19:46:06.581Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/fc/7b6fd4d22c8c4dc5704430140d8b3f520531d4fe7328b8f8d03f5a7950e8/networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad", size = 2511464, upload-time = "2025-11-24T03:03:47.158Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c7/d64168da60332c17d24c0d2f08bdf3987e8d1ae9d84b5bbd0eec2eb26a55/networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f", size = 2063713, upload-time = "2025-11-24T03:03:45.21Z" }, +] + +[[package]] +name = "notebook" +version = "7.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, + { name = "jupyterlab" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/ac/a97041621250a4fc5af379fb377942841eea2ca146aab166b8fcdfba96c2/notebook-7.5.0.tar.gz", hash = "sha256:3b27eaf9913033c28dde92d02139414c608992e1df4b969c843219acf2ff95e4", size = 14052074, upload-time = "2025-11-19T08:36:20.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/96/00df2a4760f10f5af0f45c4955573cae6189931f9a30265a35865f8c1031/notebook-7.5.0-py3-none-any.whl", hash = "sha256:3300262d52905ca271bd50b22617681d95f08a8360d099e097726e6d2efb5811", size = 14460968, upload-time = "2025-11-19T08:36:15.869Z" }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, +] + +[[package]] +name = "openmm" +version = "8.4.0.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/35/9a51d07fc60fc267a241a2199deb31502baa2e438801d4223a256b898c8a/openmm-8.4.0.post1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:eb5d3da43b7ac4b3e41e3a29005982b1f87de1879d2d7a97b982d20022321383", size = 13228166, upload-time = "2025-11-07T18:03:58.349Z" }, + { url = "https://files.pythonhosted.org/packages/95/50/772848b1b81c7609cbe119c5456996488ce2efd4863e782c257e42829be4/openmm-8.4.0.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef172c5ab504f99a7346c3e4b35e29703be803a610ab72e6b2d16e39f754ebea", size = 12741362, upload-time = "2025-11-07T18:04:04.236Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b2/8acf418560b6fe29635f4f992f3a1a71d9f017cca19ebe4ccff9f86fae90/openmm-8.4.0.post1-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:2b5dcbc789d9d16fbe4fcc7326532499f45143a2bec05c869fa98af0c23c3d80", size = 14248283, upload-time = "2025-11-07T18:04:10.516Z" }, + { url = "https://files.pythonhosted.org/packages/38/1f/976e1fae88a6ff4ba74310dc663faf80e58e9f740ebfc12c890309706032/openmm-8.4.0.post1-cp311-cp311-win_amd64.whl", hash = "sha256:a49c498564bdaeb3bad936440ed402dca4b0718a6578147142f2c3ffe4580559", size = 13096103, upload-time = "2025-11-07T18:04:16.787Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c1/63de7b93c6b57323cb3524f1038fbb75749d44f087a9c443de4a5d35be33/openmm-8.4.0.post1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:117165ce53a465389f05249cae9feb90276874dc7ac5bf0cc8b9151031b06fa6", size = 13224758, upload-time = "2025-11-07T18:04:22.948Z" }, + { url = "https://files.pythonhosted.org/packages/dc/17/ddae4b246950b68a45a5521f925a90db332e0761c28194a3b7533e2e4495/openmm-8.4.0.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:226d462ea3e1b36daa5276b931979d36db72c9daf0e3502f7cfd5528ade8861f", size = 12739396, upload-time = "2025-11-07T18:04:29.885Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ca/2063be344cefbeaec480d9d44662551f41f27097c0df825cc2bcbea221cc/openmm-8.4.0.post1-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:a503c2b860e21364f1c49bf23c5bb8b6fa04e1a4227bc577d543d4137e99eaf5", size = 14253643, upload-time = "2025-11-07T18:04:37.888Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2d/6cafa30d7085bb76a61b3de0c492d1518939068d52924eb8c93a9571e2de/openmm-8.4.0.post1-cp312-cp312-win_amd64.whl", hash = "sha256:04599a8371f340a86bfbd494797b2bcb63327450ffabe44ad4ea375999e821c6", size = 13098400, upload-time = "2025-11-07T18:04:43.861Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d0/c79e94e3708a0756af59cb3f0583dea8795df53493503dd9ec1627c4b94c/openmm-8.4.0.post1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3dc56968b2f4eca5e981d006a52daf4cc96a92b65db750784f0f2cbd66e318eb", size = 13224726, upload-time = "2025-11-07T18:04:48.439Z" }, + { url = "https://files.pythonhosted.org/packages/65/e6/bbc688fe968c656eeb31a8e9d3e7e05ced8adc18f8eb78895a308485fe8f/openmm-8.4.0.post1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5053880aa0c9938f10dff889b110c6a743cfb4772daae10da090ab21c54d2175", size = 12738281, upload-time = "2025-11-07T18:04:53.239Z" }, + { url = "https://files.pythonhosted.org/packages/a0/7d/575d0b0e37f84282135841800d23d8f6c24a632f38a989b634835b9b4348/openmm-8.4.0.post1-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ae114defa3c5cdca8f5cf73a5d1a2786de96c7881e50b81baf31ed77e1bdaf8d", size = 14251632, upload-time = "2025-11-07T18:05:00.878Z" }, + { url = "https://files.pythonhosted.org/packages/81/15/ddf0d68a87ce6c4779d32e8e83e6a6ab13d1f8708ac10532e63a605181c5/openmm-8.4.0.post1-cp313-cp313-win_amd64.whl", hash = "sha256:519518a7fd55f10fa2d24aaeacc261cbbbf59e5cfb4a3c7ea013da31c1477f56", size = 13096639, upload-time = "2025-11-07T18:05:07.365Z" }, + { url = "https://files.pythonhosted.org/packages/76/9a/5d83ce626594add0762199dd487a161d854bdb81a5ad34b019a70c0c2b4f/openmm-8.4.0.post1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ea8c0745cf88d43538774a5d814927b490ce5898fc390cbfcb32061b4224e8ae", size = 13228335, upload-time = "2025-11-07T18:05:14.17Z" }, + { url = "https://files.pythonhosted.org/packages/4e/19/452be1362c8ce806b63171e20fa7d5722bfeab62a854ec8144a30ae7ff55/openmm-8.4.0.post1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c657ac46316f19e4356e3ee1c64acab42a7eec1d36ce87373781a550cdc1eab", size = 12741135, upload-time = "2025-11-07T18:05:21.301Z" }, + { url = "https://files.pythonhosted.org/packages/b8/80/e132b6548566671633afd3a429315208d3f3af1a12d8bc1f6705d65a3501/openmm-8.4.0.post1-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:62285d6349d4214fdde8b921b9b191b11f60611f110cca1dd71f4a20a5b3a592", size = 14253053, upload-time = "2025-11-07T18:05:28.353Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/723644512a2e36fdc5b850bbec33cadc53e41d733c829f73b2a6b9f92cff/openmm-8.4.0.post1-cp314-cp314-win_amd64.whl", hash = "sha256:5e62008a142e6c9a862791e7ab2ee509ebb76bb6c15a9a50157065b504dd4467", size = 13097585, upload-time = "2025-11-07T18:05:33.08Z" }, +] + +[[package]] +name = "openmm-systems" +version = "0.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdtraj" }, + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/53/b321144a077a07168717291ee61c801f49a0be5d82ffefa8781f4fdc3a64/openmm_systems-0.0.0.tar.gz", hash = "sha256:b65717bcb59230313bf3670190603a227087ea2c1e0bdf8eaa62191dbce2ae52", size = 11082338, upload-time = "2020-04-30T19:51:34.773Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/e1/02debe3a6f49490d2fc40c3fc5a96ab902ad96a66e5b8173254d3ef900b5/openmm_systems-0.0.0-py2.py3-none-any.whl", hash = "sha256:9e9ce3157a1034baf9fae1f652dc5187f6b36e6d66b2871521a4ef98f3c4b64b", size = 11327692, upload-time = "2020-04-30T19:51:18.139Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, +] + +[[package]] +name = "partd" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "locket" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pdbpp" +version = "0.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fancycompleter" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/4c/118ef9534ac0632859b48c305d8c5dc9d6f963564fdfa66bc785c560247c/pdbpp-0.11.7.tar.gz", hash = "sha256:cb6604ac31a35ed0f2a29650a8c022b26284620be3e01cfd41b683b91da1ff14", size = 76026, upload-time = "2025-07-18T09:36:02.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/e9/704bbc08aace64fee536e4c2c20f63f64f6fdbad72938c5ed46c9723a9f1/pdbpp-0.11.7-py3-none-any.whl", hash = "sha256:51916b63693898cf4881b36b4501c83947758d73f582f1f84893662b163bdb75", size = 30545, upload-time = "2025-07-18T09:36:01.478Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, + { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +] + +[[package]] +name = "pint" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flexcache" }, + { name = "flexparser" }, + { name = "platformdirs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/74/bc3f671997158aef171194c3c4041e549946f4784b8690baa0626a0a164b/pint-0.25.2.tar.gz", hash = "sha256:85a45d1da8fe9c9f7477fed8aef59ad2b939af3d6611507e1a9cbdacdcd3450a", size = 254467, upload-time = "2025-11-06T22:08:09.184Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/88/550d41e81e6d43335603a960cd9c75c1d88f9cf01bc9d4ee8e86290aba7d/pint-0.25.2-py3-none-any.whl", hash = "sha256:ca35ab1d8eeeb6f7d9942b3cb5f34ca42b61cdd5fb3eae79531553dcca04dda7", size = 306762, upload-time = "2025-11-06T22:08:07.745Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pockets" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/8e/0601097cfcce2e8c2297db5080e9719f549c2bd4b94420ddc8d3f848bbca/pockets-0.9.1.tar.gz", hash = "sha256:9320f1a3c6f7a9133fe3b571f283bcf3353cd70249025ae8d618e40e9f7e92b3", size = 24993, upload-time = "2019-11-02T14:46:19.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/2f/a4583c70fbd8cd04910e2884bcc2bdd670e884061f7b4d70bc13e632a993/pockets-0.9.1-py2.py3-none-any.whl", hash = "sha256:68597934193c08a08eb2bf6a1d85593f627c22f9b065cc727a4f03f669d96d86", size = 26263, upload-time = "2019-11-02T14:46:17.814Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "psutil" +version = "7.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751, upload-time = "2025-11-02T12:25:58.161Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368, upload-time = "2025-11-02T12:26:00.491Z" }, + { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134, upload-time = "2025-11-02T12:26:02.613Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904, upload-time = "2025-11-02T12:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642, upload-time = "2025-11-02T12:26:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518, upload-time = "2025-11-02T12:26:09.719Z" }, + { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843, upload-time = "2025-11-02T12:26:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369, upload-time = "2025-11-02T12:26:14.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210, upload-time = "2025-11-02T12:26:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182, upload-time = "2025-11-02T12:26:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466, upload-time = "2025-11-02T12:26:21.183Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756, upload-time = "2025-11-02T12:26:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, + { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, + { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "py" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796, upload-time = "2021-11-04T17:17:01.377Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload-time = "2021-11-04T17:17:00.152Z" }, +] + +[[package]] +name = "pybtex" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "latexcodec" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/bc/c2be05ca72f8c103670e983df8be26d1e288bc6556f487fa8cccaa27779f/pybtex-0.25.1.tar.gz", hash = "sha256:9eaf90267c7e83e225af89fea65c370afbf65f458220d3946a9e3049e1eca491", size = 406157, upload-time = "2025-06-26T13:27:41.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/68/ceb5d6679baa326261f5d3e5113d9cfed6efef2810afd9f18bffb8ed312b/pybtex-0.25.1-py2.py3-none-any.whl", hash = "sha256:9053b0d619409a0a83f38abad5d9921de5f7b3ede00742beafcd9f10ad0d8c5c", size = 127437, upload-time = "2025-06-26T13:27:43.585Z" }, +] + +[[package]] +name = "pybtex-docutils" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pybtex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/84/796ea94d26188a853660f81bded39f8de4cfe595130aef0dea1088705a11/pybtex-docutils-1.0.3.tar.gz", hash = "sha256:3a7ebdf92b593e00e8c1c538aa9a20bca5d92d84231124715acc964d51d93c6b", size = 18348, upload-time = "2023-08-22T18:47:54.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/b1/ce1f4596211efb5410e178a803f08e59b20bedb66837dcf41e21c54f9ec1/pybtex_docutils-1.0.3-py3-none-any.whl", hash = "sha256:8fd290d2ae48e32fcb54d86b0efb8d573198653c7e2447d5bec5847095f430b9", size = 6385, upload-time = "2023-08-22T06:43:20.513Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pympler" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/37/c384631908029676d8e7213dd956bb686af303a80db7afbc9be36bc49495/pympler-1.1.tar.gz", hash = "sha256:1eaa867cb8992c218430f1708fdaccda53df064144d1c5656b1e6f1ee6000424", size = 179954, upload-time = "2024-06-28T19:56:06.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/4f/a6a2e2b202d7fd97eadfe90979845b8706676b41cbd3b42ba75adf329d1f/Pympler-1.1-py3-none-any.whl", hash = "sha256:5b223d6027d0619584116a0cbc28e8d2e378f7a79c1e5e024f9ff3b673c58506", size = 165766, upload-time = "2024-06-28T19:56:05.087Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, +] + +[[package]] +name = "pyrepl" +version = "0.11.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/4f/7088417e5465c53a30b918d30542aad89352ea0d635a5d077717c69a7d2b/pyrepl-0.11.4.tar.gz", hash = "sha256:efe988b4a6e5eed587e9769dc2269aeec2b6feec2f5d77995ee85b9ad7cf7063", size = 51089, upload-time = "2025-07-17T22:56:25.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/a5/ce97a778f096aaa27cfcb7ad09f1198cf73277dcab6c68a4b8f332d91e48/pyrepl-0.11.4-py3-none-any.whl", hash = "sha256:ac30d6340267a21c39e1b1934f92bca6b8735017d14b17e40f903b2d1563541d", size = 55596, upload-time = "2025-07-17T22:56:24.537Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + +[[package]] +name = "pytokens" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pywinpty" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/bb/a7cc2967c5c4eceb6cc49cfe39447d4bfc56e6c865e7c2249b6eb978935f/pywinpty-3.0.2.tar.gz", hash = "sha256:1505cc4cb248af42cb6285a65c9c2086ee9e7e574078ee60933d5d7fa86fb004", size = 30669, upload-time = "2025-10-03T21:16:29.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a1/409c1651c9f874d598c10f51ff586c416625601df4bca315d08baec4c3e3/pywinpty-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:327790d70e4c841ebd9d0f295a780177149aeb405bca44c7115a3de5c2054b23", size = 2050304, upload-time = "2025-10-03T21:19:29.466Z" }, + { url = "https://files.pythonhosted.org/packages/02/4e/1098484e042c9485f56f16eb2b69b43b874bd526044ee401512234cf9e04/pywinpty-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:99fdd9b455f0ad6419aba6731a7a0d2f88ced83c3c94a80ff9533d95fa8d8a9e", size = 2050391, upload-time = "2025-10-03T21:19:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/fc/19/b757fe28008236a4a713e813283721b8a40aa60cd7d3f83549f2e25a3155/pywinpty-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:18f78b81e4cfee6aabe7ea8688441d30247b73e52cd9657138015c5f4ee13a51", size = 2050057, upload-time = "2025-10-03T21:19:26.732Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/cbae12ecf6f4fa4129c36871fd09c6bef4f98d5f625ecefb5e2449765508/pywinpty-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:663383ecfab7fc382cc97ea5c4f7f0bb32c2f889259855df6ea34e5df42d305b", size = 2049874, upload-time = "2025-10-03T21:18:53.923Z" }, + { url = "https://files.pythonhosted.org/packages/ca/15/f12c6055e2d7a617d4d5820e8ac4ceaff849da4cb124640ef5116a230771/pywinpty-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:28297cecc37bee9f24d8889e47231972d6e9e84f7b668909de54f36ca785029a", size = 2050386, upload-time = "2025-10-03T21:18:50.477Z" }, + { url = "https://files.pythonhosted.org/packages/de/24/c6907c5bb06043df98ad6a0a0ff5db2e0affcecbc3b15c42404393a3f72a/pywinpty-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:34b55ae9a1b671fe3eae071d86618110538e8eaad18fcb1531c0830b91a82767", size = 2049834, upload-time = "2025-10-03T21:19:25.688Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/ab/7fb95163a53ab122c74a7c42d2d2f012819af2cf3deb43fb0d5acf45cc1a/rpds_py-0.29.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b9c764a11fd637e0322a488560533112837f5334ffeb48b1be20f6d98a7b437", size = 372344, upload-time = "2025-11-16T14:47:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/b3/45/f3c30084c03b0d0f918cb4c5ae2c20b0a148b51ba2b3f6456765b629bedd/rpds_py-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fd2164d73812026ce970d44c3ebd51e019d2a26a4425a5dcbdfa93a34abc383", size = 363041, upload-time = "2025-11-16T14:47:58.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e9/4d044a1662608c47a87cbb37b999d4d5af54c6d6ebdda93a4d8bbf8b2a10/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a097b7f7f7274164566ae90a221fd725363c0e9d243e2e9ed43d195ccc5495c", size = 391775, upload-time = "2025-11-16T14:48:00.197Z" }, + { url = "https://files.pythonhosted.org/packages/50/c9/7616d3ace4e6731aeb6e3cd85123e03aec58e439044e214b9c5c60fd8eb1/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cdc0490374e31cedefefaa1520d5fe38e82fde8748cbc926e7284574c714d6b", size = 405624, upload-time = "2025-11-16T14:48:01.496Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/6d7d6941ca0843609fd2d72c966a438d6f22617baf22d46c3d2156c31350/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89ca2e673ddd5bde9b386da9a0aac0cab0e76f40c8f0aaf0d6311b6bbf2aa311", size = 527894, upload-time = "2025-11-16T14:48:03.167Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f7/aee14dc2db61bb2ae1e3068f134ca9da5f28c586120889a70ff504bb026f/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5d9da3ff5af1ca1249b1adb8ef0573b94c76e6ae880ba1852f033bf429d4588", size = 412720, upload-time = "2025-11-16T14:48:04.413Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e2/2293f236e887c0360c2723d90c00d48dee296406994d6271faf1712e94ec/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8238d1d310283e87376c12f658b61e1ee23a14c0e54c7c0ce953efdbdc72deed", size = 392945, upload-time = "2025-11-16T14:48:06.252Z" }, + { url = "https://files.pythonhosted.org/packages/14/cd/ceea6147acd3bd1fd028d1975228f08ff19d62098078d5ec3eed49703797/rpds_py-0.29.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2d6fb2ad1c36f91c4646989811e84b1ea5e0c3cf9690b826b6e32b7965853a63", size = 406385, upload-time = "2025-11-16T14:48:07.575Z" }, + { url = "https://files.pythonhosted.org/packages/52/36/fe4dead19e45eb77a0524acfdbf51e6cda597b26fc5b6dddbff55fbbb1a5/rpds_py-0.29.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:534dc9df211387547267ccdb42253aa30527482acb38dd9b21c5c115d66a96d2", size = 423943, upload-time = "2025-11-16T14:48:10.175Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7b/4551510803b582fa4abbc8645441a2d15aa0c962c3b21ebb380b7e74f6a1/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d456e64724a075441e4ed648d7f154dc62e9aabff29bcdf723d0c00e9e1d352f", size = 574204, upload-time = "2025-11-16T14:48:11.499Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/071ccdd7b171e727a6ae079f02c26f75790b41555f12ca8f1151336d2124/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a738f2da2f565989401bd6fd0b15990a4d1523c6d7fe83f300b7e7d17212feca", size = 600587, upload-time = "2025-11-16T14:48:12.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/09/96983d48c8cf5a1e03c7d9cc1f4b48266adfb858ae48c7c2ce978dbba349/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a110e14508fd26fd2e472bb541f37c209409876ba601cf57e739e87d8a53cf95", size = 562287, upload-time = "2025-11-16T14:48:14.108Z" }, + { url = "https://files.pythonhosted.org/packages/40/f0/8c01aaedc0fa92156f0391f39ea93b5952bc0ec56b897763858f95da8168/rpds_py-0.29.0-cp311-cp311-win32.whl", hash = "sha256:923248a56dd8d158389a28934f6f69ebf89f218ef96a6b216a9be6861804d3f4", size = 221394, upload-time = "2025-11-16T14:48:15.374Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/a8b21c54c7d234efdc83dc034a4d7cd9668e3613b6316876a29b49dece71/rpds_py-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:539eb77eb043afcc45314d1be09ea6d6cafb3addc73e0547c171c6d636957f60", size = 235713, upload-time = "2025-11-16T14:48:16.636Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1f/df3c56219523947b1be402fa12e6323fe6d61d883cf35d6cb5d5bb6db9d9/rpds_py-0.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:bdb67151ea81fcf02d8f494703fb728d4d34d24556cbff5f417d74f6f5792e7c", size = 229157, upload-time = "2025-11-16T14:48:17.891Z" }, + { url = "https://files.pythonhosted.org/packages/3c/50/bc0e6e736d94e420df79be4deb5c9476b63165c87bb8f19ef75d100d21b3/rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954", size = 376000, upload-time = "2025-11-16T14:48:19.141Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3a/46676277160f014ae95f24de53bed0e3b7ea66c235e7de0b9df7bd5d68ba/rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c", size = 360575, upload-time = "2025-11-16T14:48:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ba/411d414ed99ea1afdd185bbabeeaac00624bd1e4b22840b5e9967ade6337/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d", size = 392159, upload-time = "2025-11-16T14:48:22.12Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b1/e18aa3a331f705467a48d0296778dc1fea9d7f6cf675bd261f9a846c7e90/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9efe71687d6427737a0a2de9ca1c0a216510e6cd08925c44162be23ed7bed2d5", size = 410602, upload-time = "2025-11-16T14:48:23.563Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/04f27f0c9f2299274c76612ac9d2c36c5048bb2c6c2e52c38c60bf3868d9/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40f65470919dc189c833e86b2c4bd21bd355f98436a2cef9e0a9a92aebc8e57e", size = 515808, upload-time = "2025-11-16T14:48:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/83/56/a8412aa464fb151f8bc0d91fb0bb888adc9039bd41c1c6ba8d94990d8cf8/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:def48ff59f181130f1a2cb7c517d16328efac3ec03951cca40c1dc2049747e83", size = 416015, upload-time = "2025-11-16T14:48:26.782Z" }, + { url = "https://files.pythonhosted.org/packages/04/4c/f9b8a05faca3d9e0a6397c90d13acb9307c9792b2bff621430c58b1d6e76/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7bd570be92695d89285a4b373006930715b78d96449f686af422debb4d3949", size = 395325, upload-time = "2025-11-16T14:48:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/34/60/869f3bfbf8ed7b54f1ad9a5543e0fdffdd40b5a8f587fe300ee7b4f19340/rpds_py-0.29.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:5a572911cd053137bbff8e3a52d31c5d2dba51d3a67ad902629c70185f3f2181", size = 410160, upload-time = "2025-11-16T14:48:29.338Z" }, + { url = "https://files.pythonhosted.org/packages/91/aa/e5b496334e3aba4fe4c8a80187b89f3c1294c5c36f2a926da74338fa5a73/rpds_py-0.29.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d583d4403bcbf10cffc3ab5cee23d7643fcc960dff85973fd3c2d6c86e8dbb0c", size = 425309, upload-time = "2025-11-16T14:48:30.691Z" }, + { url = "https://files.pythonhosted.org/packages/85/68/4e24a34189751ceb6d66b28f18159922828dd84155876551f7ca5b25f14f/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:070befbb868f257d24c3bb350dbd6e2f645e83731f31264b19d7231dd5c396c7", size = 574644, upload-time = "2025-11-16T14:48:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/474a005ea4ea9c3b4f17b6108b6b13cebfc98ebaff11d6e1b193204b3a93/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fc935f6b20b0c9f919a8ff024739174522abd331978f750a74bb68abd117bd19", size = 601605, upload-time = "2025-11-16T14:48:33.252Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b1/c56f6a9ab8c5f6bb5c65c4b5f8229167a3a525245b0773f2c0896686b64e/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c5a8ecaa44ce2d8d9d20a68a2483a74c07f05d72e94a4dff88906c8807e77b0", size = 564593, upload-time = "2025-11-16T14:48:34.643Z" }, + { url = "https://files.pythonhosted.org/packages/b3/13/0494cecce4848f68501e0a229432620b4b57022388b071eeff95f3e1e75b/rpds_py-0.29.0-cp312-cp312-win32.whl", hash = "sha256:ba5e1aeaf8dd6d8f6caba1f5539cddda87d511331714b7b5fc908b6cfc3636b7", size = 223853, upload-time = "2025-11-16T14:48:36.419Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6a/51e9aeb444a00cdc520b032a28b07e5f8dc7bc328b57760c53e7f96997b4/rpds_py-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f6134faf54b3cb83375db0f113506f8b7770785be1f95a631e7e2892101977", size = 239895, upload-time = "2025-11-16T14:48:37.956Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d4/8bce56cdad1ab873e3f27cb31c6a51d8f384d66b022b820525b879f8bed1/rpds_py-0.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:b016eddf00dca7944721bf0cd85b6af7f6c4efaf83ee0b37c4133bd39757a8c7", size = 230321, upload-time = "2025-11-16T14:48:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/c5de60d9d371bbb186c3e9bf75f4fc5665e11117a25a06a6b2e0afb7380e/rpds_py-0.29.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1585648d0760b88292eecab5181f5651111a69d90eff35d6b78aa32998886a61", size = 375710, upload-time = "2025-11-16T14:48:41.063Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b3/0860cdd012291dc21272895ce107f1e98e335509ba986dd83d72658b82b9/rpds_py-0.29.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:521807963971a23996ddaf764c682b3e46459b3c58ccd79fefbe16718db43154", size = 360582, upload-time = "2025-11-16T14:48:42.423Z" }, + { url = "https://files.pythonhosted.org/packages/92/8a/a18c2f4a61b3407e56175f6aab6deacdf9d360191a3d6f38566e1eaf7266/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8896986efaa243ab713c69e6491a4138410f0fe36f2f4c71e18bd5501e8014", size = 391172, upload-time = "2025-11-16T14:48:43.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/49/e93354258508c50abc15cdcd5fcf7ac4117f67bb6233ad7859f75e7372a0/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d24564a700ef41480a984c5ebed62b74e6ce5860429b98b1fede76049e953e6", size = 409586, upload-time = "2025-11-16T14:48:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8d/a27860dae1c19a6bdc901f90c81f0d581df1943355802961a57cdb5b6cd1/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6596b93c010d386ae46c9fba9bfc9fc5965fa8228edeac51576299182c2e31c", size = 516339, upload-time = "2025-11-16T14:48:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/a75e603161e79b7110c647163d130872b271c6b28712c803c65d492100f7/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5cc58aac218826d054c7da7f95821eba94125d88be673ff44267bb89d12a5866", size = 416201, upload-time = "2025-11-16T14:48:48.615Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/555b4ee17508beafac135c8b450816ace5a96194ce97fefc49d58e5652ea/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de73e40ebc04dd5d9556f50180395322193a78ec247e637e741c1b954810f295", size = 395095, upload-time = "2025-11-16T14:48:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f0/c90b671b9031e800ec45112be42ea9f027f94f9ac25faaac8770596a16a1/rpds_py-0.29.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:295ce5ac7f0cf69a651ea75c8f76d02a31f98e5698e82a50a5f4d4982fbbae3b", size = 410077, upload-time = "2025-11-16T14:48:51.515Z" }, + { url = "https://files.pythonhosted.org/packages/3d/80/9af8b640b81fe21e6f718e9dec36c0b5f670332747243130a5490f292245/rpds_py-0.29.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ea59b23ea931d494459c8338056fe7d93458c0bf3ecc061cd03916505369d55", size = 424548, upload-time = "2025-11-16T14:48:53.237Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0b/b5647446e991736e6a495ef510e6710df91e880575a586e763baeb0aa770/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f49d41559cebd608042fdcf54ba597a4a7555b49ad5c1c0c03e0af82692661cd", size = 573661, upload-time = "2025-11-16T14:48:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/1b1c9576839ff583d1428efbf59f9ee70498d8ce6c0b328ac02f1e470879/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:05a2bd42768ea988294ca328206efbcc66e220d2d9b7836ee5712c07ad6340ea", size = 600937, upload-time = "2025-11-16T14:48:56.247Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/b6cfca2f9fee4c4494ce54f7fb1b9f578867495a9aa9fc0d44f5f735c8e0/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33ca7bdfedd83339ca55da3a5e1527ee5870d4b8369456b5777b197756f3ca22", size = 564496, upload-time = "2025-11-16T14:48:57.691Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fb/ba29ec7f0f06eb801bac5a23057a9ff7670623b5e8013bd59bec4aa09de8/rpds_py-0.29.0-cp313-cp313-win32.whl", hash = "sha256:20c51ae86a0bb9accc9ad4e6cdeec58d5ebb7f1b09dd4466331fc65e1766aae7", size = 223126, upload-time = "2025-11-16T14:48:59.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/0229d3bed4ddaa409e6d90b0ae967ed4380e4bdd0dad6e59b92c17d42457/rpds_py-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:6410e66f02803600edb0b1889541f4b5cc298a5ccda0ad789cc50ef23b54813e", size = 239771, upload-time = "2025-11-16T14:49:00.872Z" }, + { url = "https://files.pythonhosted.org/packages/e4/38/d2868f058b164f8efd89754d85d7b1c08b454f5c07ac2e6cc2e9bd4bd05b/rpds_py-0.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:56838e1cd9174dc23c5691ee29f1d1be9eab357f27efef6bded1328b23e1ced2", size = 229994, upload-time = "2025-11-16T14:49:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/52/91/5de91c5ec7d41759beec9b251630824dbb8e32d20c3756da1a9a9d309709/rpds_py-0.29.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:37d94eadf764d16b9a04307f2ab1d7af6dc28774bbe0535c9323101e14877b4c", size = 365886, upload-time = "2025-11-16T14:49:04.133Z" }, + { url = "https://files.pythonhosted.org/packages/85/7c/415d8c1b016d5f47ecec5145d9d6d21002d39dce8761b30f6c88810b455a/rpds_py-0.29.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d472cf73efe5726a067dce63eebe8215b14beabea7c12606fd9994267b3cfe2b", size = 355262, upload-time = "2025-11-16T14:49:05.543Z" }, + { url = "https://files.pythonhosted.org/packages/3d/14/bf83e2daa4f980e4dc848aed9299792a8b84af95e12541d9e7562f84a6ef/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72fdfd5ff8992e4636621826371e3ac5f3e3b8323e9d0e48378e9c13c3dac9d0", size = 384826, upload-time = "2025-11-16T14:49:07.301Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/53330c50a810ae22b4fbba5e6cf961b68b9d72d9bd6780a7c0a79b070857/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2549d833abdf8275c901313b9e8ff8fba57e50f6a495035a2a4e30621a2f7cc4", size = 394234, upload-time = "2025-11-16T14:49:08.782Z" }, + { url = "https://files.pythonhosted.org/packages/cc/32/01e2e9645cef0e584f518cfde4567563e57db2257244632b603f61b40e50/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4448dad428f28a6a767c3e3b80cde3446a22a0efbddaa2360f4bb4dc836d0688", size = 520008, upload-time = "2025-11-16T14:49:10.253Z" }, + { url = "https://files.pythonhosted.org/packages/98/c3/0d1b95a81affae2b10f950782e33a1fd2edd6ce2a479966cac98c9a66f57/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:115f48170fd4296a33938d8c11f697f5f26e0472e43d28f35624764173a60e4d", size = 409569, upload-time = "2025-11-16T14:49:12.478Z" }, + { url = "https://files.pythonhosted.org/packages/fa/60/aa3b8678f3f009f675b99174fa2754302a7fbfe749162e8043d111de2d88/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5bb73ffc029820f4348e9b66b3027493ae00bca6629129cd433fd7a76308ee", size = 385188, upload-time = "2025-11-16T14:49:13.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/02/5546c1c8aa89c18d40c1fcffdcc957ba730dee53fb7c3ca3a46f114761d2/rpds_py-0.29.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b1581fcde18fcdf42ea2403a16a6b646f8eb1e58d7f90a0ce693da441f76942e", size = 398587, upload-time = "2025-11-16T14:49:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e0/ad6eeaf47e236eba052fa34c4073078b9e092bd44da6bbb35aaae9580669/rpds_py-0.29.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16e9da2bda9eb17ea318b4c335ec9ac1818e88922cbe03a5743ea0da9ecf74fb", size = 416641, upload-time = "2025-11-16T14:49:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/1a/93/0acedfd50ad9cdd3879c615a6dc8c5f1ce78d2fdf8b87727468bb5bb4077/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:28fd300326dd21198f311534bdb6d7e989dd09b3418b3a91d54a0f384c700967", size = 566683, upload-time = "2025-11-16T14:49:18.342Z" }, + { url = "https://files.pythonhosted.org/packages/62/53/8c64e0f340a9e801459fc6456821abc15b3582cb5dc3932d48705a9d9ac7/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2aba991e041d031c7939e1358f583ae405a7bf04804ca806b97a5c0e0af1ea5e", size = 592730, upload-time = "2025-11-16T14:49:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/85/ef/3109b6584f8c4b0d2490747c916df833c127ecfa82be04d9a40a376f2090/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f437026dbbc3f08c99cc41a5b2570c6e1a1ddbe48ab19a9b814254128d4ea7a", size = 557361, upload-time = "2025-11-16T14:49:21.574Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3b/61586475e82d57f01da2c16edb9115a618afe00ce86fe1b58936880b15af/rpds_py-0.29.0-cp313-cp313t-win32.whl", hash = "sha256:6e97846e9800a5d0fe7be4d008f0c93d0feeb2700da7b1f7528dabafb31dfadb", size = 211227, upload-time = "2025-11-16T14:49:23.03Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3a/12dc43f13594a54ea0c9d7e9d43002116557330e3ad45bc56097ddf266e2/rpds_py-0.29.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f49196aec7c4b406495f60e6f947ad71f317a765f956d74bbd83996b9edc0352", size = 225248, upload-time = "2025-11-16T14:49:24.841Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/0b1474e7899371d9540d3bbb2a499a3427ae1fc39c998563fe9035a1073b/rpds_py-0.29.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:394d27e4453d3b4d82bb85665dc1fcf4b0badc30fc84282defed71643b50e1a1", size = 363731, upload-time = "2025-11-16T14:49:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/28/12/3b7cf2068d0a334ed1d7b385a9c3c8509f4c2bcba3d4648ea71369de0881/rpds_py-0.29.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55d827b2ae95425d3be9bc9a5838b6c29d664924f98146557f7715e331d06df8", size = 354343, upload-time = "2025-11-16T14:49:28.24Z" }, + { url = "https://files.pythonhosted.org/packages/eb/73/5afcf8924bc02a749416eda64e17ac9c9b28f825f4737385295a0e99b0c1/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc31a07ed352e5462d3ee1b22e89285f4ce97d5266f6d1169da1142e78045626", size = 385406, upload-time = "2025-11-16T14:49:29.943Z" }, + { url = "https://files.pythonhosted.org/packages/c8/37/5db736730662508535221737a21563591b6f43c77f2e388951c42f143242/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4695dd224212f6105db7ea62197144230b808d6b2bba52238906a2762f1d1e7", size = 396162, upload-time = "2025-11-16T14:49:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/70/0d/491c1017d14f62ce7bac07c32768d209a50ec567d76d9f383b4cfad19b80/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcae1770b401167f8b9e1e3f566562e6966ffa9ce63639916248a9e25fa8a244", size = 517719, upload-time = "2025-11-16T14:49:33.804Z" }, + { url = "https://files.pythonhosted.org/packages/d7/25/b11132afcb17cd5d82db173f0c8dab270ffdfaba43e5ce7a591837ae9649/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90f30d15f45048448b8da21c41703b31c61119c06c216a1bf8c245812a0f0c17", size = 409498, upload-time = "2025-11-16T14:49:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/e6543cedfb2e6403a1845710a5ab0e0ccf8fc288e0b5af9a70bfe2c12053/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a91e0ab77bdc0004b43261a4b8cd6d6b451e8d443754cfda830002b5745b32", size = 382743, upload-time = "2025-11-16T14:49:36.704Z" }, + { url = "https://files.pythonhosted.org/packages/75/11/a4ebc9f654293ae9fefb83b2b6be7f3253e85ea42a5db2f77d50ad19aaeb/rpds_py-0.29.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4aa195e5804d32c682e453b34474f411ca108e4291c6a0f824ebdc30a91c973c", size = 400317, upload-time = "2025-11-16T14:49:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/52/18/97677a60a81c7f0e5f64e51fb3f8271c5c8fcabf3a2df18e97af53d7c2bf/rpds_py-0.29.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7971bdb7bf4ee0f7e6f67fa4c7fbc6019d9850cc977d126904392d363f6f8318", size = 416979, upload-time = "2025-11-16T14:49:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/28ab391a9968f6c746b2a2db181eaa4d16afaa859fedc9c2f682d19f7e18/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8ae33ad9ce580c7a47452c3b3f7d8a9095ef6208e0a0c7e4e2384f9fc5bf8212", size = 567288, upload-time = "2025-11-16T14:49:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d3/0c7afdcdb830eee94f5611b64e71354ffe6ac8df82d00c2faf2bfffd1d4e/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c661132ab2fb4eeede2ef69670fd60da5235209874d001a98f1542f31f2a8a94", size = 593157, upload-time = "2025-11-16T14:49:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ac/a0fcbc2feed4241cf26d32268c195eb88ddd4bd862adfc9d4b25edfba535/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb78b3a0d31ac1bde132c67015a809948db751cb4e92cdb3f0b242e430b6ed0d", size = 554741, upload-time = "2025-11-16T14:49:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f1/fcc24137c470df8588674a677f33719d5800ec053aaacd1de8a5d5d84d9e/rpds_py-0.29.0-cp314-cp314-win32.whl", hash = "sha256:f475f103488312e9bd4000bc890a95955a07b2d0b6e8884aef4be56132adbbf1", size = 215508, upload-time = "2025-11-16T14:49:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/1d169b2045512eac019918fc1021ea07c30e84a4343f9f344e3e0aa8c788/rpds_py-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:b9cf2359a4fca87cfb6801fae83a76aedf66ee1254a7a151f1341632acf67f1b", size = 228125, upload-time = "2025-11-16T14:49:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/be/36/0cec88aaba70ec4a6e381c444b0d916738497d27f0c30406e3d9fcbd3bc2/rpds_py-0.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:9ba8028597e824854f0f1733d8b964e914ae3003b22a10c2c664cb6927e0feb9", size = 221992, upload-time = "2025-11-16T14:49:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/a2e524631717c9c0eb5d90d30f648cfba6b731047821c994acacb618406c/rpds_py-0.29.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e71136fd0612556b35c575dc2726ae04a1669e6a6c378f2240312cf5d1a2ab10", size = 366425, upload-time = "2025-11-16T14:49:52.691Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/6d43ebe0746ff694a30233f63f454aed1677bd50ab7a59ff6b2bb5ac61f2/rpds_py-0.29.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:76fe96632d53f3bf0ea31ede2f53bbe3540cc2736d4aec3b3801b0458499ef3a", size = 355282, upload-time = "2025-11-16T14:49:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a7/52fd8270e0320b09eaf295766ae81dd175f65394687906709b3e75c71d06/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9459a33f077130dbb2c7c3cea72ee9932271fb3126404ba2a2661e4fe9eb7b79", size = 384968, upload-time = "2025-11-16T14:49:55.857Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/e6bc526b7a14e1ef80579a52c1d4ad39260a058a51d66c6039035d14db9d/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9546cfdd5d45e562cc0444b6dddc191e625c62e866bf567a2c69487c7ad28a", size = 394714, upload-time = "2025-11-16T14:49:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/f0ade3954e7db95c791e7eaf978aa7e08a756d2046e8bdd04d08146ed188/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12597d11d97b8f7e376c88929a6e17acb980e234547c92992f9f7c058f1a7310", size = 520136, upload-time = "2025-11-16T14:49:59.162Z" }, + { url = "https://files.pythonhosted.org/packages/87/b3/07122ead1b97009715ab9d4082be6d9bd9546099b2b03fae37c3116f72be/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28de03cf48b8a9e6ec10318f2197b83946ed91e2891f651a109611be4106ac4b", size = 409250, upload-time = "2025-11-16T14:50:00.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/dcbee61fd1dc892aedcb1b489ba661313101aa82ec84b1a015d4c63ebfda/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7951c964069039acc9d67a8ff1f0a7f34845ae180ca542b17dc1456b1f1808", size = 384940, upload-time = "2025-11-16T14:50:02.312Z" }, + { url = "https://files.pythonhosted.org/packages/47/11/914ecb6f3574cf9bf8b38aced4063e0f787d6e1eb30b181a7efbc6c1da9a/rpds_py-0.29.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:c07d107b7316088f1ac0177a7661ca0c6670d443f6fe72e836069025e6266761", size = 399392, upload-time = "2025-11-16T14:50:03.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/fd/2f4bd9433f58f816434bb934313584caa47dbc6f03ce5484df8ac8980561/rpds_py-0.29.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de2345af363d25696969befc0c1688a6cb5e8b1d32b515ef84fc245c6cddba3", size = 416796, upload-time = "2025-11-16T14:50:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/79/a5/449f0281af33efa29d5c71014399d74842342ae908d8cd38260320167692/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:00e56b12d2199ca96068057e1ae7f9998ab6e99cda82431afafd32f3ec98cca9", size = 566843, upload-time = "2025-11-16T14:50:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/0a6a1ccee2e37fcb1b7ba9afde762b77182dbb57937352a729c6cd3cf2bb/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3919a3bbecee589300ed25000b6944174e07cd20db70552159207b3f4bbb45b8", size = 593956, upload-time = "2025-11-16T14:50:09.029Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3d/eb820f95dce4306f07a495ede02fb61bef36ea201d9137d4fcd5ab94ec1e/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a", size = 557288, upload-time = "2025-11-16T14:50:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/b8ff786f40470462a252918e0836e0db903c28e88e3eec66bc4a7856ee5d/rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5", size = 211382, upload-time = "2025-11-16T14:50:12.827Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919, upload-time = "2025-11-16T14:50:14.734Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ac/b97e80bf107159e5b9ba9c91df1ab95f69e5e41b435f27bdd737f0d583ac/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:acd82a9e39082dc5f4492d15a6b6c8599aa21db5c35aaf7d6889aea16502c07d", size = 373963, upload-time = "2025-11-16T14:50:16.205Z" }, + { url = "https://files.pythonhosted.org/packages/40/5a/55e72962d5d29bd912f40c594e68880d3c7a52774b0f75542775f9250712/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:715b67eac317bf1c7657508170a3e011a1ea6ccb1c9d5f296e20ba14196be6b3", size = 364644, upload-time = "2025-11-16T14:50:18.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/6b6524d0191b7fc1351c3c0840baac42250515afb48ae40c7ed15499a6a2/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b1b87a237cb2dba4db18bcfaaa44ba4cd5936b91121b62292ff21df577fc43", size = 393847, upload-time = "2025-11-16T14:50:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b8/c5692a7df577b3c0c7faed7ac01ee3c608b81750fc5d89f84529229b6873/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c3c3e8101bb06e337c88eb0c0ede3187131f19d97d43ea0e1c5407ea74c0cbf", size = 407281, upload-time = "2025-11-16T14:50:21.64Z" }, + { url = "https://files.pythonhosted.org/packages/f0/57/0546c6f84031b7ea08b76646a8e33e45607cc6bd879ff1917dc077bb881e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8e54d6e61f3ecd3abe032065ce83ea63417a24f437e4a3d73d2f85ce7b7cfe", size = 529213, upload-time = "2025-11-16T14:50:23.219Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c1/01dd5f444233605555bc11fe5fed6a5c18f379f02013870c176c8e630a23/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fbd4e9aebf110473a420dea85a238b254cf8a15acb04b22a5a6b5ce8925b760", size = 413808, upload-time = "2025-11-16T14:50:25.262Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0a/60f98b06156ea2a7af849fb148e00fbcfdb540909a5174a5ed10c93745c7/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fdf53d36e6c72819993e35d1ebeeb8e8fc688d0c6c2b391b55e335b3afba5a", size = 394600, upload-time = "2025-11-16T14:50:26.956Z" }, + { url = "https://files.pythonhosted.org/packages/37/f1/dc9312fc9bec040ece08396429f2bd9e0977924ba7a11c5ad7056428465e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:ea7173df5d86f625f8dde6d5929629ad811ed8decda3b60ae603903839ac9ac0", size = 408634, upload-time = "2025-11-16T14:50:28.989Z" }, + { url = "https://files.pythonhosted.org/packages/ed/41/65024c9fd40c89bb7d604cf73beda4cbdbcebe92d8765345dd65855b6449/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:76054d540061eda273274f3d13a21a4abdde90e13eaefdc205db37c05230efce", size = 426064, upload-time = "2025-11-16T14:50:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e0/cf95478881fc88ca2fdbf56381d7df36567cccc39a05394beac72182cd62/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9f84c549746a5be3bc7415830747a3a0312573afc9f95785eb35228bb17742ec", size = 575871, upload-time = "2025-11-16T14:50:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c0/df88097e64339a0218b57bd5f9ca49898e4c394db756c67fccc64add850a/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:0ea962671af5cb9a260489e311fa22b2e97103e3f9f0caaea6f81390af96a9ed", size = 601702, upload-time = "2025-11-16T14:50:36.051Z" }, + { url = "https://files.pythonhosted.org/packages/87/f4/09ffb3ebd0cbb9e2c7c9b84d252557ecf434cd71584ee1e32f66013824df/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f7728653900035fb7b8d06e1e5900545d8088efc9d5d4545782da7df03ec803f", size = 564054, upload-time = "2025-11-16T14:50:37.733Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f0/62b5a1a723fe183650109407fa56abb433b00aa1c0b9ba555f9c4efec2c6/ruff-0.14.6.tar.gz", hash = "sha256:6f0c742ca6a7783a736b867a263b9a7a80a45ce9bee391eeda296895f1b4e1cc", size = 5669501, upload-time = "2025-11-21T14:26:17.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/d2/7dd544116d107fffb24a0064d41a5d2ed1c9d6372d142f9ba108c8e39207/ruff-0.14.6-py3-none-linux_armv6l.whl", hash = "sha256:d724ac2f1c240dbd01a2ae98db5d1d9a5e1d9e96eba999d1c48e30062df578a3", size = 13326119, upload-time = "2025-11-21T14:25:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/36/6a/ad66d0a3315d6327ed6b01f759d83df3c4d5f86c30462121024361137b6a/ruff-0.14.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9f7539ea257aa4d07b7ce87aed580e485c40143f2473ff2f2b75aee003186004", size = 13526007, upload-time = "2025-11-21T14:25:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9d/dae6db96df28e0a15dea8e986ee393af70fc97fd57669808728080529c37/ruff-0.14.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f6007e55b90a2a7e93083ba48a9f23c3158c433591c33ee2e99a49b889c6332", size = 12676572, upload-time = "2025-11-21T14:25:29.826Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/f319e87759949062cfee1b26245048e92e2acce900ad3a909285f9db1859/ruff-0.14.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8e7b9d73d8728b68f632aa8e824ef041d068d231d8dbc7808532d3629a6bef", size = 13140745, upload-time = "2025-11-21T14:25:32.788Z" }, + { url = "https://files.pythonhosted.org/packages/95/d3/248c1efc71a0a8ed4e8e10b4b2266845d7dfc7a0ab64354afe049eaa1310/ruff-0.14.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d50d45d4553a3ebcbd33e7c5e0fe6ca4aafd9a9122492de357205c2c48f00775", size = 13076486, upload-time = "2025-11-21T14:25:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/a5/19/b68d4563fe50eba4b8c92aa842149bb56dd24d198389c0ed12e7faff4f7d/ruff-0.14.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:118548dd121f8a21bfa8ab2c5b80e5b4aed67ead4b7567790962554f38e598ce", size = 13727563, upload-time = "2025-11-21T14:25:38.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/943169436832d4b0e867235abbdb57ce3a82367b47e0280fa7b4eabb7593/ruff-0.14.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:57256efafbfefcb8748df9d1d766062f62b20150691021f8ab79e2d919f7c11f", size = 15199755, upload-time = "2025-11-21T14:25:41.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b9/288bb2399860a36d4bb0541cb66cce3c0f4156aaff009dc8499be0c24bf2/ruff-0.14.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff18134841e5c68f8e5df1999a64429a02d5549036b394fafbe410f886e1989d", size = 14850608, upload-time = "2025-11-21T14:25:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b1/a0d549dd4364e240f37e7d2907e97ee80587480d98c7799d2d8dc7a2f605/ruff-0.14.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c4b7ec1e66a105d5c27bd57fa93203637d66a26d10ca9809dc7fc18ec58440", size = 14118754, upload-time = "2025-11-21T14:25:47.214Z" }, + { url = "https://files.pythonhosted.org/packages/13/ac/9b9fe63716af8bdfddfacd0882bc1586f29985d3b988b3c62ddce2e202c3/ruff-0.14.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167843a6f78680746d7e226f255d920aeed5e4ad9c03258094a2d49d3028b105", size = 13949214, upload-time = "2025-11-21T14:25:50.002Z" }, + { url = "https://files.pythonhosted.org/packages/12/27/4dad6c6a77fede9560b7df6802b1b697e97e49ceabe1f12baf3ea20862e9/ruff-0.14.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:16a33af621c9c523b1ae006b1b99b159bf5ac7e4b1f20b85b2572455018e0821", size = 14106112, upload-time = "2025-11-21T14:25:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/6a/db/23e322d7177873eaedea59a7932ca5084ec5b7e20cb30f341ab594130a71/ruff-0.14.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1432ab6e1ae2dc565a7eea707d3b03a0c234ef401482a6f1621bc1f427c2ff55", size = 13035010, upload-time = "2025-11-21T14:25:55.536Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9c/20e21d4d69dbb35e6a1df7691e02f363423658a20a2afacf2a2c011800dc/ruff-0.14.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c55cfbbe7abb61eb914bfd20683d14cdfb38a6d56c6c66efa55ec6570ee4e71", size = 13054082, upload-time = "2025-11-21T14:25:58.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/906ee6a0464c3125c8d673c589771a974965c2be1a1e28b5c3b96cb6ef88/ruff-0.14.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:efea3c0f21901a685fff4befda6d61a1bf4cb43de16da87e8226a281d614350b", size = 13303354, upload-time = "2025-11-21T14:26:01.816Z" }, + { url = "https://files.pythonhosted.org/packages/4c/58/60577569e198d56922b7ead07b465f559002b7b11d53f40937e95067ca1c/ruff-0.14.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:344d97172576d75dc6afc0e9243376dbe1668559c72de1864439c4fc95f78185", size = 14054487, upload-time = "2025-11-21T14:26:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/8e4e0639e4cc12547f41cb771b0b44ec8225b6b6a93393176d75fe6f7d40/ruff-0.14.6-py3-none-win32.whl", hash = "sha256:00169c0c8b85396516fdd9ce3446c7ca20c2a8f90a77aa945ba6b8f2bfe99e85", size = 13013361, upload-time = "2025-11-21T14:26:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/82240553b77fd1341f80ebb3eaae43ba011c7a91b4224a9f317d8e6591af/ruff-0.14.6-py3-none-win_amd64.whl", hash = "sha256:390e6480c5e3659f8a4c8d6a0373027820419ac14fa0d2713bd8e6c3e125b8b9", size = 14432087, upload-time = "2025-11-21T14:26:10.891Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1f/93f9b0fad9470e4c829a5bb678da4012f0c710d09331b860ee555216f4ea/ruff-0.14.6-py3-none-win_arm64.whl", hash = "sha256:d43c81fbeae52cfa8728d8766bbf46ee4298c888072105815b392da70ca836b2", size = 13520930, upload-time = "2025-11-21T14:26:13.951Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, + { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, + { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, + { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, + { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, + { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, + { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +] + +[[package]] +name = "send2trash" +version = "1.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3a/aec9b02217bb79b87bbc1a21bc6abc51e3d5dcf65c30487ac96c0908c722/Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf", size = 17394, upload-time = "2024-04-07T00:01:09.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9", size = 18072, upload-time = "2024-04-07T00:01:07.438Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-bibtex" +version = "2.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pybtex" }, + { name = "pybtex-docutils" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/83/1488c9879f2fa3c2cbd6f666c7a3a42a1fa9e08462bec73281fa6c092cba/sphinxcontrib_bibtex-2.6.5.tar.gz", hash = "sha256:9b3224dd6fece9268ebd8c905dc0a83ff2f6c54148a9235fe70e9d1e9ff149c0", size = 118462, upload-time = "2025-06-27T10:40:14.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a0/3a612da94f828f26cabb247817393e79472c32b12c49222bf85fb6d7b6c8/sphinxcontrib_bibtex-2.6.5-py3-none-any.whl", hash = "sha256:455ea4509642ea0b28ede3721550273626f85af65af01f161bfd8e19dc1edd7d", size = 40410, upload-time = "2025-06-27T10:40:12.274Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-napoleon" +version = "0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pockets" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/eb/ad89500f4cee83187596e07f43ad561f293e8e6e96996005c3319653b89f/sphinxcontrib-napoleon-0.7.tar.gz", hash = "sha256:407382beed396e9f2d7f3043fad6afda95719204a1e1a231ac865f40abcbfcf8", size = 21232, upload-time = "2018-09-23T14:16:47.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/f2/6b7627dfe7b4e418e295e254bb15c3a6455f11f8c0ad0d43113f678049c3/sphinxcontrib_napoleon-0.7-py2.py3-none-any.whl", hash = "sha256:711e41a3974bdf110a484aec4c1a556799eb0b3f3b897521a018ad7e2db13fef", size = 17151, upload-time = "2018-09-23T14:16:45.548Z" }, +] + +[[package]] +name = "sphinxcontrib-newsfeed" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/5e/8bc839b5c4ef030bf26eede24208a49f25d00033cbd4969b3895264f14db/sphinxcontrib-newsfeed-0.1.4.tar.gz", hash = "sha256:f78c865b1a32d249644a9c46f395b547df0dc21ed69b70898d64235b4755efb4", size = 10862, upload-time = "2015-02-18T23:53:38.696Z" } + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, + { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, + { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "webcolors" +version = "25.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "wepy" +version = "1.1.0" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "dill" }, + { name = "geomm" }, + { name = "h5py" }, + { name = "jinja2" }, + { name = "multiprocessing-logging" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pint" }, + { name = "scipy" }, + { name = "tabulate" }, +] + +[package.optional-dependencies] +distributed = [ + { name = "dask" }, +] +graphics = [ + { name = "matplotlib" }, + { name = "pillow" }, +] +md = [ + { name = "mdtraj" }, + { name = "openmm" }, + { name = "openmm-systems" }, +] +prometheus = [ + { name = "prometheus-client" }, + { name = "pympler" }, +] + +[package.dev-dependencies] +dev = [ + { name = "black" }, + { name = "coverage" }, + { name = "interrogate" }, + { name = "ipython" }, + { name = "isort" }, + { name = "mypy" }, + { name = "nbsphinx" }, + { name = "notebook" }, + { name = "pdbpp" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "sphinx" }, + { name = "sphinxcontrib-bibtex" }, + { name = "sphinxcontrib-napoleon" }, + { name = "sphinxcontrib-newsfeed" }, +] + +[package.metadata] +requires-dist = [ + { name = "click" }, + { name = "dask", extras = ["bag"], marker = "extra == 'distributed'" }, + { name = "dill" }, + { name = "geomm" }, + { name = "h5py", specifier = ">=3" }, + { name = "jinja2" }, + { name = "matplotlib", marker = "extra == 'graphics'" }, + { name = "mdtraj", marker = "extra == 'md'" }, + { name = "multiprocessing-logging" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "openmm", marker = "extra == 'md'" }, + { name = "openmm-systems", marker = "extra == 'md'", specifier = "==0.0.0" }, + { name = "pandas" }, + { name = "pillow", marker = "extra == 'graphics'", specifier = ">=10.0.1" }, + { name = "pint" }, + { name = "prometheus-client", marker = "extra == 'prometheus'" }, + { name = "pympler", marker = "extra == 'prometheus'" }, + { name = "scipy" }, + { name = "tabulate" }, +] +provides-extras = ["md", "distributed", "prometheus", "graphics"] + +[package.metadata.requires-dev] +dev = [ + { name = "black" }, + { name = "coverage" }, + { name = "interrogate" }, + { name = "ipython" }, + { name = "isort" }, + { name = "mypy" }, + { name = "nbsphinx" }, + { name = "notebook" }, + { name = "pdbpp" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "sphinx" }, + { name = "sphinxcontrib-bibtex" }, + { name = "sphinxcontrib-napoleon" }, + { name = "sphinxcontrib-newsfeed" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From 7d28de0184d7b55de2f27aaef5efc9bbf3c860b2 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 24 Nov 2025 16:21:17 -0500 Subject: [PATCH 004/143] formatting --- CONTRIBUTING.md | 11 +- justfile | 22 ++ sphinx/conf.py | 2 +- src/pytest_wepy/lennard_jones_pair.py | 5 +- src/pytest_wepy/test_hdf5_analysis.py | 21 +- src/wepy/__init__.py | 2 +- src/wepy/analysis/__init__.py | 1 - src/wepy/analysis/contig_tree.py | 45 +--- src/wepy/analysis/distributed.py | 47 ++-- src/wepy/analysis/network.py | 47 ++-- .../analysis/network_layouts/layout_graph.py | 14 +- src/wepy/analysis/network_layouts/tree.py | 42 +--- src/wepy/analysis/parents.py | 11 +- src/wepy/analysis/profiles.py | 77 +++--- src/wepy/analysis/rates.py | 6 - src/wepy/analysis/transitions.py | 1 - src/wepy/boundary_conditions/__init__.py | 1 - src/wepy/boundary_conditions/boundary.py | 16 +- src/wepy/boundary_conditions/randomwalk.py | 15 +- src/wepy/boundary_conditions/receptor.py | 21 +- src/wepy/hdf5.py | 223 +++++++++--------- src/wepy/orchestration/cli.py | 48 ++-- src/wepy/orchestration/configuration.py | 7 +- src/wepy/orchestration/orchestrator.py | 34 +-- src/wepy/orchestration/snapshot.py | 2 +- src/wepy/reporter/dashboard.py | 11 +- src/wepy/reporter/hdf5.py | 26 +- src/wepy/reporter/openmm.py | 6 +- src/wepy/reporter/receptor/dashboard.py | 12 +- src/wepy/reporter/reporter.py | 7 - src/wepy/reporter/restree.py | 7 +- src/wepy/reporter/revo/dashboard.py | 35 ++- src/wepy/reporter/walker.py | 9 +- src/wepy/reporter/wexplore/dashboard.py | 15 +- src/wepy/resampling/decisions/clone_merge.py | 5 +- src/wepy/resampling/decisions/decision.py | 7 - src/wepy/resampling/distances/distance.py | 6 +- src/wepy/resampling/distances/randomwalk.py | 6 +- src/wepy/resampling/distances/receptor.py | 2 - src/wepy/resampling/resamplers/clone_merge.py | 10 +- src/wepy/resampling/resamplers/resampler.py | 17 +- src/wepy/resampling/resamplers/revo.py | 21 +- src/wepy/resampling/resamplers/wexplore.py | 80 ++----- src/wepy/runners/openmm.py | 18 +- src/wepy/runners/randomwalk.py | 2 - src/wepy/runners/runner.py | 3 +- src/wepy/sim_manager.py | 23 +- src/wepy/util/__init__.py | 1 - src/wepy/util/json_top.py | 21 +- src/wepy/util/kv.py | 14 +- src/wepy/util/mdtraj.py | 20 +- src/wepy/util/util.py | 60 ++--- src/wepy/work_mapper/mapper.py | 6 - src/wepy/work_mapper/worker.py | 10 - src/wepy_test_drive.py | 6 +- src/wepy_tools/sim_makers/openmm/__init__.py | 2 +- .../sim_makers/openmm/lennard_jones.py | 2 + src/wepy_tools/sim_makers/openmm/lysozyme.py | 6 +- src/wepy_tools/sim_makers/openmm/sim_maker.py | 2 +- src/wepy_tools/sim_makers/toys/randomwalk.py | 9 - src/wepy_tools/systems/openmm/base.py | 15 +- src/wepy_tools/systems/openmm/nacl_pair.py | 9 +- src/wepy_tools/systems/receptor.py | 3 +- tests/benchmarks/test_mappers.py | 18 +- .../test_examples/test_Lennard_Jones_Pair.py | 5 +- tests/docs/test_examples/test_Lysozyme.py | 5 +- tests/docs/test_examples/test_RandomWalk.py | 5 +- tests/docs/test_pages/test_pages.py | 4 +- .../docs/test_tutorials/test_Orchestrator.py | 5 +- .../docs/test_tutorials/test_data_analysis.py | 5 +- .../test_extended_test_drive.py | 5 +- tests/docs/test_tutorials/test_tutorials.py | 5 - tests/integration/conftest.py | 1 - tests/integration/test_lj_combinations.py | 22 -- tests/integration/test_lj_fixture.py | 1 - tests/unit/test_work_mapper/test_mapper.py | 5 +- 76 files changed, 510 insertions(+), 811 deletions(-) create mode 100644 justfile diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 44f8c7b1..e826f981 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,13 @@ -# Contributing +# CONTRIBUTING + +``` +uv sync --all-extras +``` + + + + +# Contributing (OLD) Developing on wepy diff --git a/justfile b/justfile new file mode 100644 index 00000000..fc425cb8 --- /dev/null +++ b/justfile @@ -0,0 +1,22 @@ +#!/usr/bin/env just --justfile + +fmt-check: + uv run black --check src tests sphinx/conf.py + +fmt: + uv run black src tests sphinx/conf.py + +fix-check: + uv run isort --check src tests sphinx/conf.py + uv run ruff check src tests sphinx/conf.py + +fix: + uv run isort src tests sphinx/conf.py + uv run ruff check --fix src tests sphinx/conf.py + + +check: + uv run mypy src + +test: + uv run pytest --import-mode=importlib tests/unit diff --git a/sphinx/conf.py b/sphinx/conf.py index 22f8e05e..a335e25d 100644 --- a/sphinx/conf.py +++ b/sphinx/conf.py @@ -230,7 +230,7 @@ # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python': ("https://docs.python.org/", None)} +intersphinx_mapping = {"python": ("https://docs.python.org/", None)} # -- Options for todo extension ---------------------------------------------- diff --git a/src/pytest_wepy/lennard_jones_pair.py b/src/pytest_wepy/lennard_jones_pair.py index 473d73f9..6923e574 100644 --- a/src/pytest_wepy/lennard_jones_pair.py +++ b/src/pytest_wepy/lennard_jones_pair.py @@ -3,16 +3,13 @@ import pytest from openmm_systems.test_systems import LennardJonesPair +# First Party Library from wepy.runners.openmm import ( - GET_STATE_KWARG_DEFAULTS, - UNIT_NAMES, OpenMMRunner, gen_walker_state, ) - from wepy_tools.sim_makers.openmm.sim_maker import OpenMMSimMaker - ### Constants # only use the reference platform for python-only integration testing diff --git a/src/pytest_wepy/test_hdf5_analysis.py b/src/pytest_wepy/test_hdf5_analysis.py index 5be6abed..381a0674 100644 --- a/src/pytest_wepy/test_hdf5_analysis.py +++ b/src/pytest_wepy/test_hdf5_analysis.py @@ -6,11 +6,8 @@ # 4) get traces # Standard Library -import os -import unittest # Third Party Library -import mdtraj as mdj import numpy as np # First Party Library @@ -66,12 +63,12 @@ def test_H5_resampling(): n_cycles = we.num_run_cycles(0) wr_list = we.warping_records([0]) n_walkers = we.num_walkers(0, 0) - all_pos = np.array([ - we.h5[f"runs/0/trajectories/{i}/positions"] for i in range(n_walkers) - ]) - all_wts = np.array([ - we.h5[f"runs/0/trajectories/{i}/weights"] for i in range(n_walkers) - ]) + all_pos = np.array( + [we.h5[f"runs/0/trajectories/{i}/positions"] for i in range(n_walkers)] + ) + all_wts = np.array( + [we.h5[f"runs/0/trajectories/{i}/weights"] for i in range(n_walkers)] + ) rrs = we.resampling_records([0]) # @@ -135,9 +132,9 @@ def test_H5_contig(): with we: n_cycles = we.num_run_cycles(0) n_walkers = we.num_walkers(0, 0) - all_pos = np.array([ - we.h5[f"runs/0/trajectories/{i}/positions"] for i in range(n_walkers) - ]) + all_pos = np.array( + [we.h5[f"runs/0/trajectories/{i}/positions"] for i in range(n_walkers)] + ) ct = ContigTree( we, boundary_condition_class=RandomWalkBC, diff --git a/src/wepy/__init__.py b/src/wepy/__init__.py index e1c1fbb3..19536bb5 100644 --- a/src/wepy/__init__.py +++ b/src/wepy/__init__.py @@ -1,7 +1,7 @@ """Top-level package.""" # Local Modules -from .__about__ import __version__ +from .__about__ import __version__ as __version__ __author__ = "Samuel D. Lotz" __email__ = "samuel.lotz@salotz.info" diff --git a/src/wepy/analysis/__init__.py b/src/wepy/analysis/__init__.py index 98c43f35..7d2a4605 100644 --- a/src/wepy/analysis/__init__.py +++ b/src/wepy/analysis/__init__.py @@ -38,7 +38,6 @@ See Also -------- - `wepy.hdf5.WepyHDF5.compute_observable` Notes diff --git a/src/wepy/analysis/contig_tree.py b/src/wepy/analysis/contig_tree.py index b46c8ff3..0dceb30f 100644 --- a/src/wepy/analysis/contig_tree.py +++ b/src/wepy/analysis/contig_tree.py @@ -9,11 +9,11 @@ """ # Standard Library -from typing import Final import warnings from collections import deque from copy import copy from operator import attrgetter +from typing import Final # Third Party Library import networkx as nx @@ -26,13 +26,10 @@ warnings.warn("Matplotlib not installed, these features will not work") # Third Party Library +import pandas as pd from geomm.free_energy import free_energy as calc_free_energy # First Party Library -from wepy.hdf5 import WepyHDF5 -from wepy.resampling.decisions.decision import Decision -from wepy.boundary_conditions.boundary import BoundaryConditions - from wepy.analysis.network_layouts.layout_graph import LayoutGraph from wepy.analysis.network_layouts.tree import ResamplingTreeLayout from wepy.analysis.parents import ( @@ -43,9 +40,9 @@ parent_panel, sliding_window, ) - -import pandas as pd - +from wepy.boundary_conditions.boundary import BoundaryConditions +from wepy.hdf5 import WepyHDF5 +from wepy.resampling.decisions.decision import Decision # the groups of run records RESAMPLING: Final = "resampling" @@ -133,7 +130,6 @@ def __init__( Warnings -------- - Only set `continuations` if you know what you are doing. A `decision_class` must be given to be able to detect cloning and @@ -416,7 +412,6 @@ def contig_trace_to_run_trace(contig_trace, contig_walker_trace): Returns ------- - run_trace : list of tuples of ints (run_idx, traj_idx, cycle_idx) """ @@ -445,12 +440,10 @@ def walker_trace_to_run_trace(self, contig_walker_trace): Parameters ---------- - contig_walker_trace : list of tuples of ints (traj_idx, cycle_idx) Returns ------- - run_trace : list of tuples of ints (run_idx, traj_idx, cycle_idx) See Also @@ -462,18 +455,14 @@ def walker_trace_to_run_trace(self, contig_walker_trace): return self.contig_trace_to_run_trace(self.contig_trace, contig_walker_trace) def run_trace_to_contig_trace(self, run_trace): - """ - - Assumes that the run trace goes along a valid contig. + """Assumes that the run trace goes along a valid contig. Parameters ---------- - run_trace : list of tuples of ints (run_idx, traj_idx, cycle_idx) Returns ------- - contig_walker_trace : list of tuples of ints (traj_idx, contig_cycle_idx) @@ -501,7 +490,6 @@ def contig_cycle_idx(self, run_idx, cycle_idx): Returns ------- - contig_cycle_idx : int The cycle idx in the contig @@ -530,7 +518,6 @@ def get_branch_trace(self, run_idx, cycle_idx, start_contig_idx=0): Returns ------- - contig_trace : list of tuples of ints (run_idx, cycle_idx) """ @@ -825,7 +812,6 @@ def contig_sliding_windows(self, contig_trace, window_length): Returns ------- - windows : list of list of tuples of ints (traj_idx, cycle_idx) List of contig walker traces @@ -1017,7 +1003,6 @@ def _rec_spanning_paths(cls, edges, root): Returns ------- - spanning_paths : list of edges """ @@ -1079,15 +1064,12 @@ def _rec_spanning_paths(cls, edges, root): # return root_sources def _spanning_paths(self, root): - """ - - Parameters + """Parameters ---------- root : node_id Returns ------- - spanning_paths : list of list of edges """ @@ -1193,7 +1175,7 @@ def _contig_trace_to_contig_runs(cls, contig_trace): contig_runs = [] for run_idx, cycle_idx in contig_trace: - if not run_idx in contig_runs: + if run_idx not in contig_runs: contig_runs.append(run_idx) else: pass @@ -1484,7 +1466,6 @@ def resampling_trace(self, decision_id): Parameters ---------- - decision_id : int The string ID of the decision you want to match on and get lineages for. @@ -1516,7 +1497,8 @@ def final_trace(self): def lineages(self, trace, discontinuities=True): """Get the ancestry lineage for each element of the trace as a run - trace.""" + trace. + """ lines = [] # for each element of the trace we need to get it's lineage @@ -1817,13 +1799,11 @@ def parent_table(self, discontinuities=True): Notes ----- - This requires the decision class to be given to the Contig at construction. Warnings -------- - If the simulation was run with boundary conditions that result in discontinuous warping events and that class is not provided at construction time to this class these discontinuities will @@ -1854,7 +1834,8 @@ def lineages_contig(self, contig_trace, discontinuities=True): def lineages(self, contig_trace, discontinuities=True): """Get the ancestry lineage for each element of the trace as a run - trace.""" + trace. + """ return [ self.walker_trace_to_run_trace(trace) @@ -1883,7 +1864,6 @@ def resampling_contig_trace(self, decision_id): Parameters ---------- - decision_id : int The integer ID of the decision you want to match on and get lineages for. This is the integer value of the decision @@ -1928,7 +1908,6 @@ def resampling_trace(self, decision_id): Parameters ---------- - decision_id : int The string ID of the decision you want to match on and get lineages for. diff --git a/src/wepy/analysis/distributed.py b/src/wepy/analysis/distributed.py index 03305aa2..ff554db9 100644 --- a/src/wepy/analysis/distributed.py +++ b/src/wepy/analysis/distributed.py @@ -115,7 +115,6 @@ """ # Standard Library -import time from collections import defaultdict from copy import deepcopy @@ -135,9 +134,8 @@ def traj_fields_chunk_items( ): """Generate items that can be used to create a dask.bag object. - Arguments + Arguments: --------- - wepy_h5_path : str The file path to the WepyHDF5 file that will be read from. @@ -150,7 +148,7 @@ def traj_fields_chunk_items( data for which a single task will work on. Dask will also partition these chunks as it sees fit. - Returns + Returns: ------- chunk_specs : list of dict of str : value @@ -166,9 +164,9 @@ def traj_fields_chunk_items( with wepy_h5: # choose the run idxs if run_idxs is not Ellipsis: - assert all([run_idx in wepy_h5.run_idxs for run_idx in run_idxs]), ( - "run_idx not in runs" - ) + assert all( + [run_idx in wepy_h5.run_idxs for run_idx in run_idxs] + ), "run_idx not in runs" else: run_idxs = wepy_h5.run_idxs @@ -234,7 +232,7 @@ def chunk_func_funcgen( result_name = result_name def chunk_func(chunk_spec): - assert not result_name in chunk_spec.keys() + assert result_name not in chunk_spec.keys() fields = [] for key in input_keys: @@ -300,10 +298,12 @@ def chunk_concat(cum_chunk_spec, new_chunk_spec): cum_chunk_spec["fields"] = new_chunk_spec["fields"] # concatenate the frame indices in this chunk - new_chunk["frame_idxs"] = np.concatenate([ - cum_chunk_spec["frame_idxs"], - new_chunk_spec["frame_idxs"], - ]) + new_chunk["frame_idxs"] = np.concatenate( + [ + cum_chunk_spec["frame_idxs"], + new_chunk_spec["frame_idxs"], + ] + ) # for each extra concat function feed it the two chunk specs for concat_func in concat_funcs: @@ -319,10 +319,12 @@ def chunk_array_concat_funcgen(field): def func(cum_chunk_spec, new_chunk_spec): # only add it if it has been initialized in the cum_chunk if field in cum_chunk_spec: - cum_chunk_spec[field] = np.concatenate([ - cum_chunk_spec[field], - new_chunk_spec[field], - ]) + cum_chunk_spec[field] = np.concatenate( + [ + cum_chunk_spec[field], + new_chunk_spec[field], + ] + ) # otherwise set just the new chunk else: @@ -335,13 +337,16 @@ def func(cum_chunk_spec, new_chunk_spec): def chunk_traj_fields_concat(cum_chunk_spec, new_chunk_spec): """Binary operation for dask foldby reductions for concatenating chunk - specs with a traj_fields payload""" + specs with a traj_fields payload + """ # concatenate the traj fields - cum_chunk_spec["traj_fields"] = concat_traj_fields([ - cum_chunk_spec["traj_fields"], - new_chunk_spec["traj_fields"], - ]) + cum_chunk_spec["traj_fields"] = concat_traj_fields( + [ + cum_chunk_spec["traj_fields"], + new_chunk_spec["traj_fields"], + ] + ) return cum_chunk_spec diff --git a/src/wepy/analysis/network.py b/src/wepy/analysis/network.py index 7d00f343..bbba2587 100644 --- a/src/wepy/analysis/network.py +++ b/src/wepy/analysis/network.py @@ -12,7 +12,7 @@ import numpy as np # First Party Library -from wepy.analysis.transitions import counts_d_to_matrix, transition_counts +from wepy.analysis.transitions import transition_counts try: # Third Party Library @@ -129,7 +129,7 @@ def __init__( The 'transition_lag_time' must be given as an integer greater than 1. - Arguments + Arguments: --------- contig_tree : ContigTree object @@ -145,19 +145,19 @@ def __init__( arraylikes of shape (n_traj, observable_shape[0], ...). - See Also + See Also: """ self._graph = nx.DiGraph() - assert not (assg_field_key is None and assignments is None), ( - "either assg_field_key or assignments must be given" - ) + assert not ( + assg_field_key is None and assignments is None + ), "either assg_field_key or assignments must be given" - assert assg_field_key is not None or assignments is not None, ( - "one of assg_field_key or assignments must be given" - ) + assert ( + assg_field_key is not None or assignments is not None + ), "one of assg_field_key or assignments must be given" self._base_contig_tree = contig_tree.base_contigtree @@ -352,11 +352,13 @@ def _assignments_init(self, assignments): for run_idx, run in enumerate(assignments): for traj_idx, traj in enumerate(run): for frame_idx, assignment in enumerate(traj): - self._node_assignments[assignment].append(( - run_idx, - traj_idx, - frame_idx, - )) + self._node_assignments[assignment].append( + ( + run_idx, + traj_idx, + frame_idx, + ) + ) def _init_transition_counts( self, @@ -795,9 +797,9 @@ def write_gexf( exclude_node_fields = list(set(exclude_node_fields)) # exclude the layouts, we will set the viz manually for the layout - exclude_node_fields.extend([ - "_layouts/{}".format(layout_name) for layout_name in self.layouts - ]) + exclude_node_fields.extend( + ["_layouts/{}".format(layout_name) for layout_name in self.layouts] + ) for node in gexf_graph: # remove requested fields @@ -1007,7 +1009,6 @@ def node_map(self, func, map_func=map): Returns ------- - node_values : dict of node_id : values The mapping of node_ids to the values computed by the mapped func. @@ -1071,7 +1072,6 @@ def edge_attribute_to_matrix( Parameters ---------- - attribute_key : str The key of the edge attribute the matrix should be made of. @@ -1082,7 +1082,6 @@ def edge_attribute_to_matrix( Returns ------- - edge_matrix : numpy.ndarray Assymetric matrix of dim (n_macrostates, @@ -1171,7 +1170,6 @@ class MacroStateNetwork: Warnings -------- - This class is not serializable as it references a 'WepyHDF5' object. Either construct a 'BaseMacroStateNetwork' or use the attached instance in the 'base_network' attribute. @@ -1202,7 +1200,6 @@ def __init__( Parameters ---------- - base_network : BaseMacroStateNetwork object An already constructed network, which will avoid recomputing all in-memory network values again for this @@ -1424,7 +1421,6 @@ def get_node_fields(self, node_id, fields): Returns ------- - fields : dict of str: array_like A dictionary mapping the names of the fields to an array of the field. Like fields of a trace. @@ -1501,7 +1497,8 @@ def macrostate_weights(self): def set_macrostate_weights(self): """Compute the macrostate weights and set them as node attributes - 'total_weight'.""" + 'total_weight'. + """ self.base_network.set_nodes_observable( "total_weight", @@ -1542,14 +1539,12 @@ def node_fields_map(self, func, fields, map_func=map): Returns ------- - node_values : dict of node_id : values The mapping of node_ids to the values computed by the mapped func. Returns ------- - node_values : dict of node_id : values Dictionary mapping nodes to the computed values from the mapped function. diff --git a/src/wepy/analysis/network_layouts/layout_graph.py b/src/wepy/analysis/network_layouts/layout_graph.py index ba0c774f..cae7fd6f 100644 --- a/src/wepy/analysis/network_layouts/layout_graph.py +++ b/src/wepy/analysis/network_layouts/layout_graph.py @@ -58,7 +58,7 @@ def __init__(self, graph): the viz_graph, which has all node and edge attributes removed but keeps the topology. - Arguments + Arguments: --------- graph : any networkx graph @@ -252,13 +252,13 @@ def RGBA_to_hex(cls, color_vec): """ - assert all([ - True if (color <= 255 and color >= 0) else False for color in color_vec - ]), "invalid color values, must be between 0 and 255" + assert all( + [True if (color <= 255 and color >= 0) else False for color in color_vec] + ), "invalid color values, must be between 0 and 255" - return "#" + "".join(["{:02x}" for _ in color_vec]).format(*[ - color for color in color_vec - ]) + return "#" + "".join(["{:02x}" for _ in color_vec]).format( + *[color for color in color_vec] + ) # methods for setting gexf visualization attributes @classmethod diff --git a/src/wepy/analysis/network_layouts/tree.py b/src/wepy/analysis/network_layouts/tree.py index cb7306f0..25471cb9 100644 --- a/src/wepy/analysis/network_layouts/tree.py +++ b/src/wepy/analysis/network_layouts/tree.py @@ -9,17 +9,12 @@ # Standard Library import itertools as it -from collections import defaultdict from copy import copy from warnings import warn # Third Party Library -import networkx as nx import numpy as np -# First Party Library -from wepy.analysis.network_layouts.layout import LayoutError - class ResamplingTreeLayout: """Class that wraps the parameters for generating resampling tree layouts. @@ -28,7 +23,6 @@ class ResamplingTreeLayout: Attributes ---------- - node_radius : float Default node radius to use. row_spacing : float @@ -45,7 +39,7 @@ def __init__( """Constructing the object is just a setting of the parameters and collection of methods for generating layout positions. - Arguments + Arguments: --------- node_radius : float, optional Default node radius to use. @@ -70,9 +64,7 @@ def __init__( self.central_axis = central_axis def _overlaps(self, positions, node_radii, node_idx): - """ - - Parameters + """Parameters ---------- positions : @@ -118,7 +110,6 @@ def _node_row_length(self, node_positions, node_radii): Returns ------- - row_length : float """ @@ -131,9 +122,7 @@ def _node_row_length(self, node_positions, node_radii): return abs(max_edge - min_edge) def _simple_gen_distribution(self, nodes_x, node_radii): - """ - - Parameters + """Parameters ---------- nodes_x : @@ -142,7 +131,6 @@ def _simple_gen_distribution(self, nodes_x, node_radii): Returns ------- - new_nodes_positions """ @@ -349,16 +337,14 @@ def _simple_gen_distribution(self, nodes_x, node_radii): new_node_positions[node_idx] = eff_positions[n_groups + i] # sanity check that we covered them all - assert all([ - True if pos is not None else False for pos in new_node_positions - ]), "not all positions recovered from the effective nodes" + assert all( + [True if pos is not None else False for pos in new_node_positions] + ), "not all positions recovered from the effective nodes" return new_node_positions def _simple_next_gen(self, parents_x, children_parent_idxs, node_radii): - """ - - Parameters + """Parameters ---------- parents_x : @@ -369,7 +355,6 @@ def _simple_next_gen(self, parents_x, children_parent_idxs, node_radii): Returns ------- - children_x """ @@ -390,15 +375,12 @@ def _simple_next_gen(self, parents_x, children_parent_idxs, node_radii): return children_x def _initial_parent_distribution(self, node_radii): - """ - - Parameters + """Parameters ---------- node_radii : Returns ------- - positions """ @@ -447,7 +429,6 @@ def _center_row(self, positions, radii, center): Returns ------- - centered_positions """ @@ -478,7 +459,6 @@ def _layout_array(self, parent_table, radii_array): Returns ------- - node_positions """ @@ -544,9 +524,9 @@ def _layout_array(self, parent_table, radii_array): step_y = last_y + last_max_radius + self.step_spacing + this_max_radius # then generate the coordinates - node_positions[generation_idx] = np.array([ - np.array([x, step_y, 0.0]) for x in curr_gen_positions - ]) + node_positions[generation_idx] = np.array( + [np.array([x, step_y, 0.0]) for x in curr_gen_positions] + ) # set the last gen positions last_gen_positions = curr_gen_positions diff --git a/src/wepy/analysis/parents.py b/src/wepy/analysis/parents.py index 2e708436..bddf8259 100644 --- a/src/wepy/analysis/parents.py +++ b/src/wepy/analysis/parents.py @@ -62,7 +62,6 @@ # Third Party Library import networkx as nx -import numpy as np DISCONTINUITY_VALUE = -1 """Special value used to determine if a parent-child relationship has @@ -490,7 +489,7 @@ def __init__( The underlying data structure used is a parent table. However, if a contig is given a reference to it will be kept. - Arguments + Arguments: --------- contig : Conting object, optional conditional on parent_table @@ -499,7 +498,7 @@ def __init__( include metadata on discontinuities use the contig input which is preferrable. - Raises + Raises: ------ ValueError If neither parent_table nor contig is given, or if both are given. @@ -521,9 +520,9 @@ def __init__( # otherwise use the one given else: - assert not self.DISCONTINUITY_VALUE in it.chain(*parent_table), ( - "Discontinuity values in parent table are not allowed." - ) + assert self.DISCONTINUITY_VALUE not in it.chain( + *parent_table + ), "Discontinuity values in parent table are not allowed." self._parent_table = parent_table diff --git a/src/wepy/analysis/profiles.py b/src/wepy/analysis/profiles.py index 4d1aba69..bcb90a48 100644 --- a/src/wepy/analysis/profiles.py +++ b/src/wepy/analysis/profiles.py @@ -96,7 +96,6 @@ def cumulative_partitions( Parameters ---------- - ensemble_values : arraylikes of float of shape (n_cycles, n_trajs) Array of scalar values for all of the frames of an ensemble simulation. @@ -115,7 +114,6 @@ def cumulative_partitions( Yields ------ - cumulative_tranche : arraylike A slice along the cycles axis of the ensemble values starting at the beginning and including up to the end of the next @@ -146,10 +144,8 @@ def free_energy_profile( max_energy=100, zero_point_energy=1e-12, ): - """ - Parameters + """Parameters ---------- - weights : arraylikes of float of shape (n_trajs, n_cycles) The weights for all of the frames of an ensemble simulation. @@ -162,7 +158,6 @@ def free_energy_profile( Returns ------- - hist_fe : arraylike The free energies of the histogram bins @@ -171,9 +166,9 @@ def free_energy_profile( """ - assert weights.shape == observables.shape, ( - "Weights and observables must correspond in shape" - ) + assert ( + weights.shape == observables.shape + ), "Weights and observables must correspond in shape" hist_weights, bin_edges = np.histogram( observables, @@ -201,7 +196,6 @@ def contigtrees_bin_edges( Parameters ---------- - contigtrees : list of ContigTree objects The contigtrees to draw the data from. @@ -287,14 +281,14 @@ def contigtrees_bin_edges( class ContigTreeProfiler(object): """A wrapper class around a ContigTree that provides extra methods for - generating free energy profiles for observables.""" + generating free energy profiles for observables. + """ def __init__(self, contigtree, truncate_cycles=None): """Create a wrapper around a contigtree for the profiler. Parameters ---------- - contigtree : ContigTree object The contigtree you want to generate profiles for. @@ -328,7 +322,6 @@ def _get_ignore_trace(cls, contigtree, truncate_cycles): Parameters ---------- - contigtree : ContigTree truncate_cycles : int @@ -336,7 +329,6 @@ def _get_ignore_trace(cls, contigtree, truncate_cycles): Returns ------- - ignored_trace : set of (int, int) The frames to ignore given the truncation. @@ -346,10 +338,12 @@ def _get_ignore_trace(cls, contigtree, truncate_cycles): for span_idx, span_trace in contigtree.span_traces.items(): for run_idx, cycle_idx in span_trace: if cycle_idx >= truncate_cycles: - ignore_trace.add(( - run_idx, - cycle_idx, - )) + ignore_trace.add( + ( + run_idx, + cycle_idx, + ) + ) return ignore_trace @@ -369,7 +363,6 @@ def fe_profile_trace( Parameters ---------- - trace : list of tuple of ints (run_idx, traj_idx, cycle_idx) field_key : str @@ -387,7 +380,6 @@ def fe_profile_trace( Returns ------- - fe_profile : arraylike of dtype float An array of free energies for each bin. @@ -451,7 +443,6 @@ def fe_profile_all( Parameters ---------- - field_key : str The key for the trajectory field to calculate the profiles for. Must be a rank 0 (or equivalent (1,) rank) field. @@ -468,7 +459,6 @@ def fe_profile_all( Returns ------- - fe_profile : arraylike of dtype float An array of free energies for each bin. @@ -545,10 +535,12 @@ def fe_profile_all( ] # reshape to match - weights = weights.reshape(( - weights.shape[0], - weights.shape[1], - )) + weights = weights.reshape( + ( + weights.shape[0], + weights.shape[1], + ) + ) all_weights.append(weights) all_values.append(values) @@ -607,7 +599,6 @@ def fe_profile(self, span, field_key, bins=None, ignore_truncate=False): Parameters ---------- - span : int The index of the span to calculate profiles for. @@ -627,7 +618,6 @@ def fe_profile(self, span, field_key, bins=None, ignore_truncate=False): Returns ------- - fe_profile : arraylike of dtype float An array of free energies for each bin. @@ -709,7 +699,6 @@ def fe_cumulative_profiles( Parameters ---------- - span : int The index of the span to calculate profiles for. @@ -740,7 +729,6 @@ def fe_cumulative_profiles( Returns ------- - cumulative_fe_profiles : list of arraylike of dtype float A list of each cumulative free energy profile. Each profile is an array of free energies for each bin. @@ -828,7 +816,6 @@ def fe_all_cumulative_profiles( Parameters ---------- - field_key : str The key for the trajectory field to calculate the profiles for. Must be a rank 0 (or equivalent (1,) rank) field. @@ -858,7 +845,6 @@ def fe_all_cumulative_profiles( Returns ------- - cumulative_fe_profiles : list of arraylike of dtype float A list of each cumulative free energy profile. Each profile is an array of free energies for each bin. @@ -892,9 +878,9 @@ def fe_all_cumulative_profiles( # trace of all of the frames in the contigtree all_trace = list( - it.chain(*[ - span_trace for span_trace in self.contigtree.span_traces.values() - ]) + it.chain( + *[span_trace for span_trace in self.contigtree.span_traces.values()] + ) ) # filter it for the ignored fields if applicable @@ -983,10 +969,12 @@ def fe_all_cumulative_profiles( contig_values = contig.contig_fields([field_key])[field_key] # reshape to match - contig_weights = contig_weights.reshape(( - contig_weights.shape[0], - contig_weights.shape[1], - )) + contig_weights = contig_weights.reshape( + ( + contig_weights.shape[0], + contig_weights.shape[1], + ) + ) # make the cumulative generators for each contig_weights_partition_gen = cumulative_partitions( @@ -1041,7 +1029,6 @@ def bin_edges(self, bins, field_key): Parameters ---------- - bins : int or str The number of bins to make or the method to use for autobinning. @@ -1061,10 +1048,12 @@ def bin_edges(self, bins, field_key): """ - all_values = np.concatenate([ - fields[field_key] - for fields in self.contigtree.wepy_h5.iter_trajs_fields([field_key]) - ]) + all_values = np.concatenate( + [ + fields[field_key] + for fields in self.contigtree.wepy_h5.iter_trajs_fields([field_key]) + ] + ) bin_edges = np.histogram_bin_edges(all_values, bins=bins) diff --git a/src/wepy/analysis/rates.py b/src/wepy/analysis/rates.py index 7179cded..7e095789 100644 --- a/src/wepy/analysis/rates.py +++ b/src/wepy/analysis/rates.py @@ -15,7 +15,6 @@ def calc_warp_rate(warping_records, total_sampling_time): Parameters ---------- - warping_records : list of namedtuples implementing the warping interface The list of warping records for which events will be used to calculate the rates. @@ -26,7 +25,6 @@ def calc_warp_rate(warping_records, total_sampling_time): Returns ------- - target_weights_rates : dict of int : tuple of (float, float, float) A dictionary where each key is for a target present in the warping records and each value is a tuple giving the total @@ -36,7 +34,6 @@ def calc_warp_rate(warping_records, total_sampling_time): See Also -------- - wepy.boundary_conditions.boundary.Boundary : for specs on fields necessary for warping_records @@ -85,7 +82,6 @@ class (wepy.boundary_conditions.boundary.Boundary). Parameters ---------- - contig : analysis.contig_tree.Contig Underlying WepyHDF5 must be open for reading. @@ -98,7 +94,6 @@ class (wepy.boundary_conditions.boundary.Boundary). Returns ------- - run_target_weights_rates : list of dict of int : tuple of (float, float, float) List where each value of a run is a list of outputs from @@ -107,7 +102,6 @@ class (wepy.boundary_conditions.boundary.Boundary). See Also -------- - wepy.analysis.rates.calc_warp_rate wepy.boundary_conditions.boundary.Boundary : for specs on fields diff --git a/src/wepy/analysis/transitions.py b/src/wepy/analysis/transitions.py index a1bfc930..3ed791a0 100644 --- a/src/wepy/analysis/transitions.py +++ b/src/wepy/analysis/transitions.py @@ -62,7 +62,6 @@ def transition_counts(assignments, transitions, weights=None): Parameters ---------- - assignments: mixed array_like of dim (n_run, n_traj, n_cycle) type int Assignment of microstates to macrostate labels, where N_runs is the number of runs, N_traj is the number of trajectories, diff --git a/src/wepy/boundary_conditions/__init__.py b/src/wepy/boundary_conditions/__init__.py index 85db0338..5571c4c1 100644 --- a/src/wepy/boundary_conditions/__init__.py +++ b/src/wepy/boundary_conditions/__init__.py @@ -94,7 +94,6 @@ class does exactly this and modifies the walker state to restart it in Notes ----- - Boundary conditions in wepy simulations are optional. Inherit from the diff --git a/src/wepy/boundary_conditions/boundary.py b/src/wepy/boundary_conditions/boundary.py index 59af01dc..9c4e098d 100644 --- a/src/wepy/boundary_conditions/boundary.py +++ b/src/wepy/boundary_conditions/boundary.py @@ -10,7 +10,6 @@ logger = logging.getLogger(__name__) # Standard Library import random -import sys from collections import defaultdict from copy import deepcopy @@ -276,7 +275,6 @@ def bc_fields(self): Returns ------- - record_specs : list of tuple A list of the specs for each field, a spec is a tuple of type (field_name, shape_spec, dtype_spec) @@ -306,7 +304,6 @@ def warping_fields(self): Returns ------- - record_specs : list of tuple A list of the specs for each field, a spec is a tuple of type (field_name, shape_spec, dtype_spec) @@ -340,7 +337,6 @@ def progress_fields(self): Returns ------- - record_specs : list of tuple A list of the specs for each field, a spec is a tuple of type (field_name, shape_spec, dtype_spec) @@ -516,7 +512,7 @@ def __init__(self, initial_states=None, initial_weights=None, **kwargs): If the initial weights for each initial state are not given uniform weights are assigned to them. - Arguments + Arguments: --------- initial_states : list of objects implementing the State interface The list of possible states that warped walkers will assume. @@ -526,7 +522,7 @@ def __init__(self, initial_states=None, initial_weights=None, **kwargs): provided. If not given, uniform probabilities will be used. - Raises + Raises: ------ AssertionError If any of the following kwargs are not given: @@ -656,17 +652,15 @@ def warp_walkers(self, walkers, cycle): """Test the progress of all the walkers, warp if required, and update the boundary conditions. - Arguments + Arguments: --------- - walkers : list of objects implementing the Walker interface cycle : int The index of the cycle. - Returns + Returns: ------- - new_walkers : list of objects implementing the Walker interface The new set of walkers that may have been warped. @@ -745,13 +739,11 @@ def warping_discontinuity(cls, warping_record): Parameters ---------- - warping_record : tuple The WARPING type record. Returns ------- - is_discontinuous : bool True if a discontinuous warp False if continuous. diff --git a/src/wepy/boundary_conditions/randomwalk.py b/src/wepy/boundary_conditions/randomwalk.py index 3c22674d..7b7395f3 100644 --- a/src/wepy/boundary_conditions/randomwalk.py +++ b/src/wepy/boundary_conditions/randomwalk.py @@ -1,26 +1,16 @@ """Boundary conditions for random walk.""" # Standard Library -import itertools as it import logging logger = logging.getLogger(__name__) # Standard Library -import time -from collections import defaultdict # Third Party Library import numpy as np -from geomm.centering import center_around -from geomm.distance import minimum_distance -from geomm.grouping import group_pair -from geomm.rmsd import calc_rmsd -from geomm.superimpose import superimpose # First Party Library from wepy.boundary_conditions.boundary import WarpBC -from wepy.util.util import box_vectors_to_lengths_angles -from wepy.walker import WalkerState class RandomWalkBC(WarpBC): @@ -59,9 +49,8 @@ def __init__( ): """Constructor for RandomWalkBC. - Arguments + Arguments: --------- - threshold : int The threshold distance for recording a warping event. @@ -81,7 +70,7 @@ def __init__( The indices of the atom positions in the state considered the binding site. - Raises + Raises: ------ AssertionError If any of the following kwargs are not given: diff --git a/src/wepy/boundary_conditions/receptor.py b/src/wepy/boundary_conditions/receptor.py index bfcda72f..8ab7bde2 100644 --- a/src/wepy/boundary_conditions/receptor.py +++ b/src/wepy/boundary_conditions/receptor.py @@ -4,13 +4,10 @@ """ # Standard Library -import itertools as it import logging logger = logging.getLogger(__name__) # Standard Library -import time -from collections import defaultdict # Third Party Library import numpy as np @@ -90,7 +87,7 @@ def __init__( If the initial weights for each initial state are not given uniform weights are assigned to them. - Arguments + Arguments: --------- ligand_idxs : arraylike of int The indices of the atom positions in the state considered @@ -100,7 +97,7 @@ def __init__( The indices of the atom positions in the state considered the receptor. - Raises + Raises: ------ AssertionError If any of the following kwargs are not given: @@ -209,9 +206,8 @@ def __init__( ): """Constructor for RebindingBC. - Arguments + Arguments: --------- - native_state : object implementing the State interface The reference bound state. Will be automatically centered. @@ -234,7 +230,7 @@ def __init__( The indices of the atom positions in the state considered the binding site. - Raises + Raises: ------ AssertionError If any of the following kwargs are not given: @@ -400,7 +396,7 @@ def __init__( The 'initial_state' should be the initial state of your simulation for proper non-equilibrium simulations. - Arguments + Arguments: --------- initial_state : object implementing State interface The state walkers will take on after unbinding. @@ -420,7 +416,7 @@ def __init__( Indices of the atoms in the topology that correspond to the receptor for the ligand. - Raises + Raises: ------ AssertionError If any of the following are not provided: initial_state, @@ -429,7 +425,7 @@ def __init__( AssertionError If the cutoff distance is not a float. - Warnings + Warnings: -------- The 'initial_state' should be the initial state of your simulation for proper non-equilibrium simulations. @@ -463,7 +459,8 @@ def cutoff_distance(self): @property def topology(self): """JSON string topology of the system. - Note: Deprecated and will be removed in future versions.""" + Note: Deprecated and will be removed in future versions. + """ return self._topology def _calc_min_distance(self, walker): diff --git a/src/wepy/hdf5.py b/src/wepy/hdf5.py index 940dc5fe..c0af8187 100644 --- a/src/wepy/hdf5.py +++ b/src/wepy/hdf5.py @@ -399,12 +399,10 @@ # Standard Library import os.path as osp from collections import Counter, defaultdict, namedtuple -from copy import copy from warnings import warn # Third Party Library import h5py -import networkx as nx import numpy as np # First Party Library @@ -412,7 +410,6 @@ from wepy.util.json_top import json_top_atom_count, json_top_subset from wepy.util.mdtraj import ( json_to_mdtraj_topology, - mdtraj_to_json_topology, traj_fields_to_mdtraj, ) from wepy.util.util import traj_box_vectors_to_lengths_angles @@ -782,7 +779,6 @@ def __init__( Raises ------ - AssertionError If the mode is not one of the supported mode specs. @@ -791,7 +787,6 @@ def __init__( Warns ----- - If initialization data was given but the file was opened in a read mode. """ @@ -892,19 +887,21 @@ def __init__( # read only mode elif self._wepy_mode == "r": # if any data was given, warn the user - if any([ - kwarg is not None - for kwarg in [ - topology, - units, - sparse_fields, - feature_shapes, - feature_dtypes, - n_dims, - alt_reps, - main_rep_idxs, + if any( + [ + kwarg is not None + for kwarg in [ + topology, + units, + sparse_fields, + feature_shapes, + feature_dtypes, + n_dims, + alt_reps, + main_rep_idxs, + ] ] - ]): + ): warn("Data was given but opening in read-only mode", RuntimeWarning) # then run the initialization process @@ -973,9 +970,9 @@ def _create_init(self): and set with the new ones if given. """ - assert self._topology is not None, ( - "Topology must be given for a creation constructor" - ) + assert ( + self._topology is not None + ), "Topology must be given for a creation constructor" # initialize the runs group runs_grp = self._h5.create_group(RUNS) @@ -1036,8 +1033,8 @@ def _create_init(self): # any sparse field with unspecified shape and dtype must be # set to None so that it will be set at runtime for sparse_field in self.sparse_fields: - if (not sparse_field in self._field_feature_shapes) or ( - not sparse_field in self._field_feature_dtypes + if (sparse_field not in self._field_feature_shapes) or ( + sparse_field not in self._field_feature_dtypes ): self._field_feature_shapes[sparse_field] = None self._field_feature_dtypes[sparse_field] = None @@ -1470,9 +1467,7 @@ def _init_contiguous_traj_field(self, run_idx, traj_idx, field_path, shape, dtyp ) def _init_sparse_traj_field(self, run_idx, traj_idx, field_path, shape, dtype): - """ - - Parameters + """Parameters ---------- run_idx : int traj_idx : int @@ -1617,9 +1612,9 @@ def _extend_contiguous_traj_field(self, run_idx, traj_idx, field_path, field_dat field = traj_grp[field_path] # make sure this is a feature vector - assert len(field_data.shape) > 1, ( - "field_data must be a feature vector with the same number of dimensions as the number" - ) + assert ( + len(field_data.shape) > 1 + ), "field_data must be a feature vector with the same number of dimensions as the number" # of datase new frames n_new_frames = field_data.shape[0] @@ -1628,9 +1623,9 @@ def _extend_contiguous_traj_field(self, run_idx, traj_idx, field_path, field_dat if all([i == 0 for i in field.shape]): # check the feature shape against the maxshape which gives # the feature dimensions for an empty dataset - assert field_data.shape[1:] == field.maxshape[1:], ( - "field feature dimensions must be the same, i.e. all but the first dimension" - ) + assert ( + field_data.shape[1:] == field.maxshape[1:] + ), "field feature dimensions must be the same, i.e. all but the first dimension" # if it is empty resize it to make an array the size of # the new field_data with the maxshape for the feature @@ -1644,9 +1639,9 @@ def _extend_contiguous_traj_field(self, run_idx, traj_idx, field_path, field_dat else: # make sure the new data has the right dimensions against # the shape it already has - assert field_data.shape[1:] == field.shape[1:], ( - "field feature dimensions must be the same, i.e. all but the first dimension" - ) + assert ( + field_data.shape[1:] == field.shape[1:] + ), "field feature dimensions must be the same, i.e. all but the first dimension" # append to the dataset on the first dimension, keeping the # others the same, these must be feature vectors and therefore @@ -1688,10 +1683,10 @@ def _extend_sparse_traj_field( if all([i == 0 for i in field_data.shape]): # check the feature shape against the maxshape which gives # the feature dimensions for an empty dataset - assert values.shape[1:] == field_data.maxshape[1:], ( - "input value features have shape {}, expected {}".format( - values.shape[1:], field_data.maxshape[1:] - ) + assert ( + values.shape[1:] == field_data.maxshape[1:] + ), "input value features have shape {}, expected {}".format( + values.shape[1:], field_data.maxshape[1:] ) # if it is empty resize it to make an array the size of @@ -1705,25 +1700,29 @@ def _extend_sparse_traj_field( else: # make sure the new data has the right dimensions - assert values.shape[1:] == field_data.shape[1:], ( - "field feature dimensions must be the same, i.e. all but the first dimension" - ) + assert ( + values.shape[1:] == field_data.shape[1:] + ), "field feature dimensions must be the same, i.e. all but the first dimension" # append to the dataset on the first dimension, keeping the # others the same, these must be feature vectors and therefore # must exist - field_data.resize(( - field_data.shape[0] + n_new_frames, - *field_data.shape[1:], - )) + field_data.resize( + ( + field_data.shape[0] + n_new_frames, + *field_data.shape[1:], + ) + ) # add the new data field_data[-n_new_frames:, ...] = values # add the sparse idxs in the same way - field_sparse_idxs.resize(( - field_sparse_idxs.shape[0] + n_new_frames, - *field_sparse_idxs.shape[1:], - )) + field_sparse_idxs.resize( + ( + field_sparse_idxs.shape[0] + n_new_frames, + *field_sparse_idxs.shape[1:], + ) + ) # add the new data field_sparse_idxs[-n_new_frames:, ...] = sparse_idxs @@ -1839,7 +1838,6 @@ def _set_field_feature_dtype(self, field_path, field_feature_dtype): field_path, field_feature_dtype, self.field_feature_dtypes[field_path], - NONE_STR, ) ) # it was not previously set so we must create then save it @@ -1871,9 +1869,9 @@ def _extend_run_record_data_field( field = records_grp[field_name] # make sure this is a feature vector - assert len(field_data.shape) > 1, ( - "field_data must be a feature vector with the same number of dimensions as the number" - ) + assert ( + len(field_data.shape) > 1 + ), "field_data must be a feature vector with the same number of dimensions as the number" # of datase new frames n_new_frames = field_data.shape[0] @@ -1914,9 +1912,9 @@ def _extend_run_record_data_field( if all([i == 0 for i in field.shape]): # check the feature shape against the maxshape which gives # the feature dimensions for an empty dataset - assert field_data.shape[1:] == field.maxshape[1:], ( - "field feature dimensions must be the same, i.e. all but the first dimension" - ) + assert ( + field_data.shape[1:] == field.maxshape[1:] + ), "field feature dimensions must be the same, i.e. all but the first dimension" # if it is empty resize it to make an array the size of # the new field_data with the maxshape for the feature @@ -1984,13 +1982,11 @@ def _convert_record_field_to_table_column( Returns ------- - record_dset : list Table-ified values Raises ------ - TypeError If the field feature vector shape rank is greater than 1. @@ -2490,7 +2486,6 @@ def open(self, mode=None): Parameters ---------- - mode : str Valid mode spec. Opens the HDF5 file in this mode if given otherwise uses the existing mode. @@ -2893,7 +2888,6 @@ def _check_traj_field_consistency(self, field_names): Parameters ---------- - field_names : list of str The field names to check for. @@ -2956,7 +2950,8 @@ def main_rep_idxs(self): @property def alt_reps_idxs(self): """Mapping of the names of the alt reps to the indices of the atoms - from the topology that they include in their datasets.""" + from the topology that they include in their datasets. + """ idxs_grp = self.h5["{}/{}".format(SETTINGS, ALT_REPS_IDXS)] return {name: ds[:] for name, ds in idxs_grp.items()} @@ -2971,7 +2966,8 @@ def alt_reps(self): @property def field_feature_shapes(self): """Mapping of the names of the trajectory fields to their feature - vector shapes.""" + vector shapes. + """ shapes_grp = self.h5["{}/{}".format(SETTINGS, FIELD_FEATURE_SHAPES_STR)] @@ -2990,14 +2986,15 @@ def field_feature_shapes(self): @property def field_feature_dtypes(self): """Mapping of the names of the trajectory fields to their feature - vector numpy dtypes.""" + vector numpy dtypes. + """ dtypes_grp = self.h5["{}/{}".format(SETTINGS, FIELD_FEATURE_DTYPES_STR)] field_paths = _iter_field_paths(dtypes_grp) _NONE_STR = NONE_STR.encode() - + dtypes = {} for field_path in field_paths: dtype_str = dtypes_grp[field_path][()] @@ -3423,7 +3420,7 @@ def get_traj_field_cycle_idxs(self, run_idx, traj_idx, field_path): traj_path = "{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx) - if not field_path in self._h5[traj_path]: + if field_path not in self._h5[traj_path]: raise KeyError("key for field {} not found".format(field_path)) # if the field is not sparse just return the cycle indices for @@ -3940,14 +3937,18 @@ def add_continuation(self, continuation_run, base_run): """ continuations_dset = self.settings_grp[CONTINUATIONS] - continuations_dset.resize(( - continuations_dset.shape[0] + 1, - continuations_dset.shape[1], - )) - continuations_dset[continuations_dset.shape[0] - 1] = np.array([ - continuation_run, - base_run, - ]) + continuations_dset.resize( + ( + continuations_dset.shape[0] + 1, + continuations_dset.shape[1], + ) + ) + continuations_dset[continuations_dset.shape[0] - 1] = np.array( + [ + continuation_run, + base_run, + ] + ) def new_run(self, init_walkers, continue_run=None, **kwargs): """Initialize a new run. @@ -4346,9 +4347,9 @@ def add_traj(self, run_idx, data, weights=None, sparse_idxs=None, metadata=None) weights = np.ones((n_frames, 1), dtype=float) else: assert isinstance(weights, np.ndarray), "weights must be a numpy.ndarray" - assert weights.shape[0] == n_frames, ( - "weights and the number of frames must be the same length" - ) + assert ( + weights.shape[0] == n_frames + ), "weights and the number of frames must be the same length" # current traj_idx traj_idx = self.next_run_traj_idx(run_idx) @@ -4365,7 +4366,7 @@ def add_traj(self, run_idx, data, weights=None, sparse_idxs=None, metadata=None) # add the rest of the metadata if given for key, val in metadata.items(): - if not key in [RUN_IDX, TRAJ_IDX]: + if key not in [RUN_IDX, TRAJ_IDX]: traj_grp.attrs[key] = val else: warn( @@ -4374,15 +4375,15 @@ def add_traj(self, run_idx, data, weights=None, sparse_idxs=None, metadata=None) ) # check to make sure the positions are the right shape - assert traj_data[POSITIONS].shape[1] == self.num_atoms, ( - "positions given have different number of atoms: {}, should be {}".format( - traj_data[POSITIONS].shape[1], self.num_atoms - ) + assert ( + traj_data[POSITIONS].shape[1] == self.num_atoms + ), "positions given have different number of atoms: {}, should be {}".format( + traj_data[POSITIONS].shape[1], self.num_atoms ) - assert traj_data[POSITIONS].shape[2] == self.num_dims, ( - "positions given have different number of dims: {}, should be {}".format( - traj_data[POSITIONS].shape[2], self.num_dims - ) + assert ( + traj_data[POSITIONS].shape[2] == self.num_dims + ), "positions given have different number of dims: {}, should be {}".format( + traj_data[POSITIONS].shape[2], self.num_dims ) # add datasets to the traj group @@ -4453,9 +4454,9 @@ def extend_traj(self, run_idx, traj_idx, data, weights=None): """ if self._wepy_mode == "c-": - assert self._append_flags[dataset_key], ( - "dataset is not available for appending to" - ) + assert self._append_flags[ + dataset_key + ], "dataset is not available for appending to" # convenient alias traj_data = data @@ -4479,9 +4480,9 @@ def extend_traj(self, run_idx, traj_idx, data, weights=None): weights = np.ones((n_new_frames, 1), dtype=float) else: assert isinstance(weights, np.ndarray), "weights must be a numpy.ndarray" - assert weights.shape[0] == n_new_frames, ( - "weights and the number of frames must be the same length" - ) + assert ( + weights.shape[0] == n_new_frames + ), "weights and the number of frames must be the same length" # add the weights weights_ds = traj_grp[WEIGHTS] @@ -4489,10 +4490,12 @@ def extend_traj(self, run_idx, traj_idx, data, weights=None): # append to the dataset on the first dimension, keeping the # others the same, if they exist if len(weights_ds.shape) > 1: - weights_ds.resize(( - weights_ds.shape[0] + n_new_frames, - *weights_ds.shape[1:], - )) + weights_ds.resize( + ( + weights_ds.shape[0] + n_new_frames, + *weights_ds.shape[1:], + ) + ) else: weights_ds.resize((weights_ds.shape[0] + n_new_frames,)) @@ -4503,7 +4506,7 @@ def extend_traj(self, run_idx, traj_idx, data, weights=None): for field_path, field_data in traj_data.items(): # if the field hasn't been initialized yet initialize it, # unless we are in SWMR mode - if not field_path in traj_grp: + if field_path not in traj_grp: # if in SWMR mode you cannot create groups so if we # are in SWMR mode raise a warning that the data won't # be recorded @@ -4519,9 +4522,9 @@ def extend_traj(self, run_idx, traj_idx, data, weights=None): # not specified as sparse_field, no settings if ( - (not field_path in self.field_feature_shapes) - and (not field_path in self.field_feature_dtypes) - and not field_path in self.sparse_fields + (field_path not in self.field_feature_shapes) + and (field_path not in self.field_feature_dtypes) + and field_path not in self.sparse_fields ): # only save if it is an observable is_observable = False @@ -5214,7 +5217,6 @@ def compute_observable( Returns ------- - traj_id_tuples : list of tuple of int, if 'idxs' option is True A list of the tuple identifiers for each trajectory result. @@ -5319,7 +5321,7 @@ def get_traj_field(self, run_idx, traj_idx, field_path, frames=None, masked=True traj_path = "{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx) # if the field doesn't exist return None - if not field_path in self._h5[traj_path]: + if field_path not in self._h5[traj_path]: raise KeyError("key for field {} not found".format(field_path)) # return None @@ -5512,15 +5514,17 @@ def get_contig_trace_fields(self, contig_trace, fields): for run_idx, cycle_idx in contig_trace: runs_frames[run_idx].append(cycle_idx) - if not run_idx in run_idxs: + if run_idx not in run_idxs: run_idxs.append(run_idx) # (there must be the same number of trajectories in each run) n_trajs_test = self.num_run_trajs(run_idxs[0]) - assert all([ - True if n_trajs_test == self.num_run_trajs(run_idx) else False - for run_idx in run_idxs - ]) + assert all( + [ + True if n_trajs_test == self.num_run_trajs(run_idx) else False + for run_idx in run_idxs + ] + ) # then using this we go run by run and get all the # trajectories @@ -5874,14 +5878,12 @@ def _choose_rep_path(self, alt_rep): Parameters ---------- - alt_rep : str The short name (non relative path) for a representation of the positions. Returns ------- - rep_path : str The relative field path to that representation. @@ -5919,7 +5921,6 @@ def traj_fields_to_mdtraj(self, traj_fields, alt_rep=POSITIONS): Parameters ---------- - traj_fields : dict of str : arraylike Dictionary of the traj fields to their values @@ -5930,7 +5931,6 @@ def traj_fields_to_mdtraj(self, traj_fields, alt_rep=POSITIONS): Returns ------- - traj : mdtraj.Trajectory object This is mainly a convenience function to retrieve the correct @@ -5949,7 +5949,8 @@ def copy_run_slice( self, run_idx, target_file_path, target_grp_path, run_slice=None, mode="x" ): """Copy this run to another HDF5 file (target_file_path) at the group - (target_grp_path)""" + (target_grp_path) + """ assert mode in ["w", "w-", "x", "r+"], "must be opened in write mode" diff --git a/src/wepy/orchestration/cli.py b/src/wepy/orchestration/cli.py index f4039a27..0819f32d 100644 --- a/src/wepy/orchestration/cli.py +++ b/src/wepy/orchestration/cli.py @@ -34,9 +34,7 @@ def settle_run_options( configuration=None, start_hash=None, ): - """ - - Parameters + """Parameters ---------- n_workers : (Default value = None) @@ -48,6 +46,7 @@ def settle_run_options( (Default value = None) \b + Returns ------- @@ -140,9 +139,8 @@ def run_snapshot( configuration, snapshot, ): - """ + """\b - \b Parameters ---------- log : @@ -169,6 +167,7 @@ def run_snapshot( \b + Returns ------- @@ -262,9 +261,8 @@ def run_orch( start_hash, orchestrator, ): - """ + """\b - \b Parameters ---------- log : @@ -289,6 +287,7 @@ def run_orch( \b + Returns ------- @@ -344,9 +343,8 @@ def run_orch( def combine_orch_wepy_hdf5s(new_orch, new_hdf5_path, run_ids=None): - """ + """\b - \b Parameters ---------- new_orch : @@ -355,6 +353,7 @@ def combine_orch_wepy_hdf5s(new_orch, new_hdf5_path, run_ids=None): \b + Returns ------- @@ -495,9 +494,9 @@ def combine_orch_wepy_hdf5s(new_orch, new_hdf5_path, run_ids=None): # map the hash id to the new run idx created. There should # only be one run in an HDF5 if we are following the # orchestration workflow. - assert len(new_run_idxs) < 2, ( - "Cannot be more than 1 run per HDF5 file in orchestration workflow" - ) + assert ( + len(new_run_idxs) < 2 + ), "Cannot be more than 1 run per HDF5 file in orchestration workflow" run_mapping[run_id] = new_run_idxs[0] @@ -560,6 +559,7 @@ def reconcile_hdf5(orchestrator, hdf5, run_ids): files. \b + Parameters ---------- orchestrator : Path @@ -573,6 +573,7 @@ def reconcile_hdf5(orchestrator, hdf5, run_ids): e.g. 'd0cb2e6fbcc8c2d66d67c845120c7f6b,b4b96580ae57f133d5f3b6ce25affa6d' \b + Returns ------- @@ -597,9 +598,8 @@ def reconcile_hdf5(orchestrator, hdf5, run_ids): @click.argument("output", nargs=1, type=click.Path(exists=False)) @click.argument("orchestrators", nargs=-1, type=click.Path(exists=True)) def reconcile_orch(hdf5, output, orchestrators): - """ + """\b - \b Parameters ---------- hdf5 : Path @@ -610,6 +610,7 @@ def reconcile_orch(hdf5, output, orchestrators): Paths to the orchestrators to reconcile. \b + Returns ------- @@ -629,15 +630,15 @@ def reconcile_orch(hdf5, output, orchestrators): def hash_listing_formatter(hashes): - """ + """\b - \b Parameters ---------- hashes : \b + Returns ------- @@ -649,15 +650,15 @@ def hash_listing_formatter(hashes): @click.argument("orchestrator", type=click.Path(exists=True)) @click.command() def ls_snapshots(orchestrator): - """ + """\b - \b Parameters ---------- orchestrator : \b + Returns ------- @@ -675,15 +676,15 @@ def ls_snapshots(orchestrator): @click.argument("orchestrator", type=click.Path(exists=True)) @click.command() def ls_runs(orchestrator): - """ + """\b - \b Parameters ---------- orchestrator : \b + Returns ------- @@ -703,15 +704,15 @@ def ls_runs(orchestrator): @click.argument("orchestrator", type=click.Path(exists=True)) @click.command() def ls_configs(orchestrator): - """ + """\b - \b Parameters ---------- orchestrator : \b + Returns ------- @@ -732,7 +733,8 @@ def ls_configs(orchestrator): @click.argument("target", type=click.Path(exists=False)) def hdf5_copy(no_expand_external, source, target): """Copy a WepyHDF5 file, except links to other runs will optionally be - expanded and truly duplicated if symbolic inter-file links are present.""" + expanded and truly duplicated if symbolic inter-file links are present. + """ # arg clusters to pass to subprocess for the files input_f_args = ["-i", source] diff --git a/src/wepy/orchestration/configuration.py b/src/wepy/orchestration/configuration.py index f01c04ed..9ad23c83 100644 --- a/src/wepy/orchestration/configuration.py +++ b/src/wepy/orchestration/configuration.py @@ -1,7 +1,7 @@ # Standard Library -from typing import Final import itertools as it import logging +from typing import Final logger = logging.getLogger(__name__) # Standard Library @@ -10,7 +10,6 @@ # First Party Library from wepy.work_mapper.mapper import Mapper, WorkerMapper -from wepy.work_mapper.worker import Worker class Configuration: @@ -326,9 +325,7 @@ def work_mapper(self): return deepcopy(self._work_mapper) def reparametrize(self, **kwargs): - """ - - Parameters + """Parameters ---------- **kwargs : diff --git a/src/wepy/orchestration/orchestrator.py b/src/wepy/orchestration/orchestrator.py index 14d057a6..8f30c36b 100644 --- a/src/wepy/orchestration/orchestrator.py +++ b/src/wepy/orchestration/orchestrator.py @@ -8,7 +8,7 @@ import sqlite3 import time from base64 import b64decode, b64encode -from copy import copy, deepcopy +from copy import deepcopy from hashlib import md5 from zlib import compress, decompress @@ -299,9 +299,7 @@ def get_default_snapshot_hash(self): @classmethod def hash_snapshot(cls, serial_str): - """ - - Parameters + """Parameters ---------- serial_str : @@ -357,9 +355,7 @@ def configuration_hashes(self): return list(self.configuration_kv.keys()) def add_snapshot(self, snapshot): - """ - - Parameters + """Parameters ---------- snapshot : @@ -399,9 +395,7 @@ def add_serial_snapshot(self, serial_snapshot): return snaphash def gen_start_snapshot(self, init_walkers): - """ - - Parameters + """Parameters ---------- init_walkers : @@ -595,9 +589,7 @@ def _update_run_record( c.execute(self.update_run_record_query, params) def register_run(self, start_hash, end_hash, config_hash, cycle_idx): - """ - - Parameters + """Parameters ---------- start_hash : @@ -766,9 +758,7 @@ def _save_checkpoint( checkpoint_db_path, cycle_idx, ): - """ - - Parameters + """Parameters ---------- checkpoint_snapshot : @@ -886,9 +876,7 @@ def _save_checkpoint( @staticmethod def gen_sim_manager(start_snapshot, configuration): - """ - - Parameters + """Parameters ---------- start_snapshot : @@ -1084,9 +1072,7 @@ def orchestrate_snapshot_run_by_time( # configuration.reparametrize method **kwargs, ): - """ - - Parameters + """Parameters ---------- snapshot_hash : @@ -1250,9 +1236,7 @@ def orchestrate_snapshot_run_by_time( def reconcile_orchestrators(host_path, *orchestrator_paths): - """ - - Parameters + """Parameters ---------- template_orchestrator : diff --git a/src/wepy/orchestration/snapshot.py b/src/wepy/orchestration/snapshot.py index ee7e49bc..9eeb818d 100644 --- a/src/wepy/orchestration/snapshot.py +++ b/src/wepy/orchestration/snapshot.py @@ -1,5 +1,5 @@ # Standard Library -from copy import copy, deepcopy +from copy import deepcopy class SimApparatus: diff --git a/src/wepy/reporter/dashboard.py b/src/wepy/reporter/dashboard.py index fbe2d759..aa09933a 100644 --- a/src/wepy/reporter/dashboard.py +++ b/src/wepy/reporter/dashboard.py @@ -3,13 +3,11 @@ """ # Standard Library -import itertools as it import logging logger = logging.getLogger(__name__) # Standard Library import time -from collections import defaultdict from copy import copy from datetime import datetime @@ -84,8 +82,7 @@ class DashboardReporter(ProgressiveFileReporter): """ def __init__(self, resampler_dash=None, runner_dash=None, bc_dash=None, **kwargs): - """ - Parameters + """Parameters ---------- resampler_dash runner_dash @@ -459,9 +456,9 @@ def __init__(self, bc=None, discontinuities=None, name=None, **kwargs): self.bc_discontinuities = copy(bc.DISCONTINUITY_TARGET_IDXS) else: - assert discontinuities is not None, ( - "If the bc is not given must give parameter: discontinuities" - ) + assert ( + discontinuities is not None + ), "If the bc is not given must give parameter: discontinuities" self.bc_discontinuities = discontinuities self.warp_records = [] diff --git a/src/wepy/reporter/hdf5.py b/src/wepy/reporter/hdf5.py index 0d35c275..856bd254 100644 --- a/src/wepy/reporter/hdf5.py +++ b/src/wepy/reporter/hdf5.py @@ -3,7 +3,6 @@ logger = logging.getLogger(__name__) # Standard Library -from copy import deepcopy # Third Party Library import numpy as np @@ -27,7 +26,6 @@ class WepyHDF5Reporter(FileReporter): See Also -------- - wepy.hdf5.WepyHDF5 @@ -81,7 +79,6 @@ def __init__( Parameters ---------- - save_fields : tuple of str, default: None A selection of fields from the walker states to be stored. Allows for the ignoring of some states. If None all @@ -149,7 +146,6 @@ def __init__( Other Parameters ---------------- - resampling_fields : list of str The names of the fields for resampling records @@ -210,13 +206,9 @@ def __init__( # TODO: refine requirements of sparse fields. Do they need to # be in the 'save_fields'? - + self._sparse_fields = ( - { - field_name : freq - for field_name, freq - in sparse_fields.items() - } + {field_name: freq for field_name, freq in sparse_fields.items()} if sparse_fields is not None else {} ) @@ -312,13 +304,12 @@ def __init__( if alt_reps is not None: self.alt_reps_idxs = {key: list(tup[0]) for key, tup in alt_reps.items()} - # add the frequencies for these alt_reps to the # sparse_fields frequency dictionary for key, (idxs, freq) in alt_reps.items(): self.alt_reps_to_save.append(key) - + alt_rep_key = "alt_reps/{}".format(key) # if the frequency is Ellipsis or 1 then we save it @@ -397,10 +388,12 @@ def init(self, continue_run=None, init_walkers=None, **kwargs): state_fields = list(init_walkers[0].state.dict().keys()) # make sure all the save_fields are present in the state - assert all([ - True if save_field in state_fields else False - for save_field in self.save_fields - ]), "Not all specified save_fields present in walker states" + assert all( + [ + True if save_field in state_fields else False + for save_field in self.save_fields + ] + ), "Not all specified save_fields present in walker states" filtered_init_walkers = [] for walker in init_walkers: @@ -425,7 +418,6 @@ def init(self, continue_run=None, init_walkers=None, **kwargs): else: state_d[alt_rep_path] = state_d["positions"][alt_rep_idxs] - # always store a copy of the all_atoms rep for the init_walkers state_d[f"alt_reps/{self.ALL_ATOMS_REP_KEY}"] = state_d["positions"] diff --git a/src/wepy/reporter/openmm.py b/src/wepy/reporter/openmm.py index b3d16f46..2ef19e42 100644 --- a/src/wepy/reporter/openmm.py +++ b/src/wepy/reporter/openmm.py @@ -27,9 +27,9 @@ def __init__(self, runner=None, step_time=None, **kwargs): super().__init__(runner=runner, step_time=step_time, **kwargs) if runner is None: - assert step_time is not None, ( - "If no complete runner is given must give parameters: step_time" - ) + assert ( + step_time is not None + ), "If no complete runner is given must give parameters: step_time" # assume it has units self.step_time = step_time diff --git a/src/wepy/reporter/receptor/dashboard.py b/src/wepy/reporter/receptor/dashboard.py index 7729392d..2c7ff847 100644 --- a/src/wepy/reporter/receptor/dashboard.py +++ b/src/wepy/reporter/receptor/dashboard.py @@ -80,9 +80,9 @@ def __init__(self, bc=None, cutoff_distance=None, **kwargs): if bc is not None: self.cutoff_distance = bc.cutoff_distance else: - assert cutoff_distance is not None, ( - "If no bc is given must give parameters: cutoff_distance" - ) + assert ( + cutoff_distance is not None + ), "If no bc is given must give parameters: cutoff_distance" self.cutoff_distance = cutoff_distance def gen_fields(self, **kwargs): @@ -113,9 +113,9 @@ def __init__(self, bc=None, cutoff_rmsd=None, **kwargs): if bc is not None: self.cutoff_rmsd = bc.cutoff_rmsd else: - assert cutoff_rmsd is not None, ( - "If no bc is given must give parameters: cutoff_rmsd" - ) + assert ( + cutoff_rmsd is not None + ), "If no bc is given must give parameters: cutoff_rmsd" self.cutoff_rmsd = cutoff_rmsd def gen_fields(self, **kwargs): diff --git a/src/wepy/reporter/reporter.py b/src/wepy/reporter/reporter.py index 26630b82..8c0a807d 100644 --- a/src/wepy/reporter/reporter.py +++ b/src/wepy/reporter/reporter.py @@ -18,7 +18,6 @@ class Reporter: See Also -------- - wepy.sim_manager : details of calls to reporter methods. """ @@ -52,7 +51,6 @@ def init(self, **kwargs): Parameters ---------- - init_walkers : list of Walker objects The initial walkers for the simulation. @@ -92,7 +90,6 @@ def report(self, **kwargs): Parameters ---------- - cycle_idx : int new_walkers : list of Walker objects @@ -159,7 +156,6 @@ def cleanup(self, **kwargs): Parameters ---------- - runner : Runner object The runner at the end of the simulation @@ -272,7 +268,6 @@ def __init__( Parameters ---------- - file_paths : list of str The list of file paths (in order) to use. @@ -509,7 +504,6 @@ def init(self, **kwargs): Parameters ---------- - file_paths : list of str The list of file paths (in order) to use. @@ -524,7 +518,6 @@ def init(self, **kwargs): See Also -------- - wepy.reporter.reporter.FileReporter """ diff --git a/src/wepy/reporter/restree.py b/src/wepy/reporter/restree.py index 112527d8..b3541f0c 100644 --- a/src/wepy/reporter/restree.py +++ b/src/wepy/reporter/restree.py @@ -32,7 +32,8 @@ class ResTreeReporter(ProgressiveFileReporter): """Reporter that generates resampling parent trees in the GEXF - format.""" + format. + """ FILE_ORDER = ("gexf_restree_path",) @@ -62,7 +63,6 @@ def __init__( Parameters ---------- - resampler : Resampler Used to generate parental relations from resampling records. @@ -177,7 +177,6 @@ def _make_resampling_record(self, record_d, cycle_idx): Returns ------- - record : namedtuple """ @@ -211,7 +210,6 @@ def _make_warping_record(self, record_d, cycle_idx): Returns ------- - record : namedtuple """ @@ -264,7 +262,6 @@ def _make_record( Returns ------- - record : namedtuple object """ diff --git a/src/wepy/reporter/revo/dashboard.py b/src/wepy/reporter/revo/dashboard.py index 378529f5..12e3492a 100644 --- a/src/wepy/reporter/revo/dashboard.py +++ b/src/wepy/reporter/revo/dashboard.py @@ -1,19 +1,14 @@ # Standard Library -import itertools as it import logging logger = logging.getLogger(__name__) # Standard Library -import os.path as osp -from collections import defaultdict # Third Party Library import numpy as np -import pandas as pd # First Party Library from wepy.reporter.dashboard import ResamplerDashboardSection -from wepy.resampling.decisions.clone_merge import MultiCloneMergeDecision class REVODashboardSection(ResamplerDashboardSection): @@ -70,22 +65,22 @@ def __init__( self.decision = resampler.DECISION else: - assert dist_exponent is not None, ( - "if no resampler given must give parameters: dist_exponent" - ) - assert merge_dist is not None, ( - "if no resampler given must give parameters: merge_dist" - ) - assert lpmin is not None, ( - "if no resampler given must give parameters: lpmin" - ) - assert char_dist is not None, ( - "if no resampler given must give parameters: char_dist" - ) + assert ( + dist_exponent is not None + ), "if no resampler given must give parameters: dist_exponent" + assert ( + merge_dist is not None + ), "if no resampler given must give parameters: merge_dist" + assert ( + lpmin is not None + ), "if no resampler given must give parameters: lpmin" + assert ( + char_dist is not None + ), "if no resampler given must give parameters: char_dist" assert seed is not None, "if no resampler given must give parameters: seed" - assert decision is not None, ( - "if no resampler given must give parameters: decision" - ) + assert ( + decision is not None + ), "if no resampler given must give parameters: decision" self.dist_exponent = dist_exponent self.merge_dist = merge_dist diff --git a/src/wepy/reporter/walker.py b/src/wepy/reporter/walker.py index b27a0f6b..c16e9ff2 100644 --- a/src/wepy/reporter/walker.py +++ b/src/wepy/reporter/walker.py @@ -18,7 +18,7 @@ # First Party Library from wepy.reporter.reporter import ProgressiveFileReporter from wepy.util.json_top import json_top_subset -from wepy.util.mdtraj import json_to_mdtraj_topology, mdtraj_to_json_topology +from wepy.util.mdtraj import json_to_mdtraj_topology from wepy.util.util import ( box_vectors_to_lengths_angles, traj_box_vectors_to_lengths_angles, @@ -63,7 +63,6 @@ def __init__( Parameters ---------- - init_state : object implementing WalkerState An initial state, only used for writing the PDB topology. @@ -141,9 +140,9 @@ def report(self, cycle_idx=None, new_walkers=None, **kwargs): # slice off the main_rep indices because that is all we want # to write for these - main_rep_positions = np.array([ - walker.state["positions"][self.main_rep_idxs] for walker in new_walkers - ]) + main_rep_positions = np.array( + [walker.state["positions"][self.main_rep_idxs] for walker in new_walkers] + ) # convert the box vectors unitcell_lengths, unitcell_angles = traj_box_vectors_to_lengths_angles( diff --git a/src/wepy/reporter/wexplore/dashboard.py b/src/wepy/reporter/wexplore/dashboard.py index 7cfccb09..a64d694a 100644 --- a/src/wepy/reporter/wexplore/dashboard.py +++ b/src/wepy/reporter/wexplore/dashboard.py @@ -4,12 +4,9 @@ logger = logging.getLogger(__name__) # Standard Library -import os.path as osp from collections import defaultdict -from warnings import warn # Third Party Library -import numpy as np import pandas as pd from tabulate import tabulate @@ -67,12 +64,12 @@ def __init__( self.max_n_regions = resampler.max_n_regions self.max_region_sizes = resampler.max_region_sizes else: - assert max_n_regions is not None, ( - "If a resampler is not given must give parameters: max_n_regions" - ) - assert max_region_sizes is not None, ( - "If a resampler is not given must give parameters: max_n_regions" - ) + assert ( + max_n_regions is not None + ), "If a resampler is not given must give parameters: max_n_regions" + assert ( + max_region_sizes is not None + ), "If a resampler is not given must give parameters: max_n_regions" self.max_n_regions = max_n_regions self.max_region_sizes = max_region_sizes diff --git a/src/wepy/resampling/decisions/clone_merge.py b/src/wepy/resampling/decisions/clone_merge.py index c97cc584..1cc8bf42 100644 --- a/src/wepy/resampling/decisions/clone_merge.py +++ b/src/wepy/resampling/decisions/clone_merge.py @@ -3,12 +3,9 @@ logger = logging.getLogger(__name__) # Standard Library -from collections import defaultdict, namedtuple +from collections import defaultdict from enum import Enum -# Third Party Library -import numpy as np - # First Party Library from wepy.resampling.decisions.decision import Decision from wepy.walker import keep_merge, split diff --git a/src/wepy/resampling/decisions/decision.py b/src/wepy/resampling/decisions/decision.py index a7263a05..d0fde037 100644 --- a/src/wepy/resampling/decisions/decision.py +++ b/src/wepy/resampling/decisions/decision.py @@ -50,12 +50,7 @@ logger = logging.getLogger(__name__) # Standard Library -from collections import namedtuple from enum import Enum -from string import ascii_lowercase - -# Third Party Library -import numpy as np # ABC for the Decision class @@ -115,7 +110,6 @@ def fields(cls): Returns ------- - fields : list of tuples Field specs each spec is of the form (name, shape, dtype). @@ -239,7 +233,6 @@ def action(cls, walkers, decisions): Returns ------- - resampled_walkers : list of Walker objects The resampled walkers. diff --git a/src/wepy/resampling/distances/distance.py b/src/wepy/resampling/distances/distance.py index 41954eb8..530ebe94 100644 --- a/src/wepy/resampling/distances/distance.py +++ b/src/wepy/resampling/distances/distance.py @@ -29,6 +29,8 @@ # Third Party Library import numpy as np + +# First Party Library from wepy.util.util import box_vectors_to_lengths_angles @@ -73,13 +75,11 @@ def image_distance(self, image_a, image_b): Returns ------- - distance : float The distance between the two images Raises ------ - NotImplementedError : always because this is abstract """ @@ -96,7 +96,6 @@ def distance(self, state_a, state_b): Returns ------- - distance : float The distance between the two walker states @@ -130,7 +129,6 @@ def __init__(self, pair_list, periodic=True): Parameters ---------- - pair_list : arraylike of tuples The indices of the atom pairs between which to compute distances. diff --git a/src/wepy/resampling/distances/randomwalk.py b/src/wepy/resampling/distances/randomwalk.py index 81808f1b..382149f3 100644 --- a/src/wepy/resampling/distances/randomwalk.py +++ b/src/wepy/resampling/distances/randomwalk.py @@ -35,7 +35,6 @@ def image(self, state): Parameters ---------- - state : object implementing WalkerState A walker state object with positions in a numpy array of shape (N), where N is the the dimension of the random @@ -43,7 +42,6 @@ def image(self, state): Returns ------- - randomwalk_image : array of floats of shape (N) The positions of a walker in the N-dimensional space. @@ -53,9 +51,8 @@ def image(self, state): def image_distance(self, image_a, image_b): """Compute the distance between the image of the two walkers. - Parameters + Parameters ---------- - image_a : array of float of shape (1, N) Position of the first walker's state. @@ -64,7 +61,6 @@ def image_distance(self, image_a, image_b): Returns ------- - distance: float The normalized Manhattan distance. diff --git a/src/wepy/resampling/distances/receptor.py b/src/wepy/resampling/distances/receptor.py index 81c2300c..71f03954 100644 --- a/src/wepy/resampling/distances/receptor.py +++ b/src/wepy/resampling/distances/receptor.py @@ -54,7 +54,6 @@ def __init__(self, ligand_idxs, binding_site_idxs, ref_state): Parameters ---------- - ligand_idxs : arraylike of int The indices of the atoms from the 'positions' attribute of states that correspond to the ligand molecule. @@ -148,7 +147,6 @@ def image(self, state): Returns ------- - receptor_image : array of float The positions of binding site and ligand after preprocessing. diff --git a/src/wepy/resampling/resamplers/clone_merge.py b/src/wepy/resampling/resamplers/clone_merge.py index 541812d8..e92dc175 100644 --- a/src/wepy/resampling/resamplers/clone_merge.py +++ b/src/wepy/resampling/resamplers/clone_merge.py @@ -41,7 +41,6 @@ def __init__( Parameters ---------- - pmin : float The minimum probability any walker is allowed to have. @@ -71,13 +70,11 @@ def _init_walker_actions(self, n_walkers): Parameters ---------- - n_walkers : int The number of walkers to generate records for Returns ------- - decision_records : list of dict of str: value A list of default decision records for one step of resampling. @@ -159,7 +156,6 @@ def assign_clones(self, merge_groups, walker_clone_nums): Returns ------- - walker_actions : list of dict of str: values List of resampling record like dictionaries. These are not completely normalized for consumption by reporters, since @@ -217,9 +213,9 @@ def assign_clones(self, merge_groups, walker_clone_nums): # if there are any free slots, then we use those first if len(free_slots) > 0: - clone_targets.extend([ - free_slots.pop() for clone in range(num_clones) - ]) + clone_targets.extend( + [free_slots.pop() for clone in range(num_clones)] + ) # if there are more slots needed then we will have to # create them diff --git a/src/wepy/resampling/resamplers/resampler.py b/src/wepy/resampling/resamplers/resampler.py index 028ee394..b176f0b3 100644 --- a/src/wepy/resampling/resamplers/resampler.py +++ b/src/wepy/resampling/resamplers/resampler.py @@ -1,10 +1,8 @@ # Standard Library -import itertools as it import logging logger = logging.getLogger(__name__) # Standard Library -from collections import defaultdict from warnings import warn # Third Party Library @@ -16,7 +14,8 @@ class ResamplerError(Exception): """Error raised when some constraint on resampling properties is - violated.""" + violated. + """ pass @@ -290,7 +289,6 @@ def __init__( Parameters ---------- - min_num_walkers : int or None or Ellipsis The minimum number of walkers allowed to have. None is unbounded, and Ellipsis preserves whatever number of @@ -363,7 +361,6 @@ def resampling_fields(self): Returns ------- - record_specs : list of tuple A list of the specs for each field, a spec is a tuple of type (field_name, shape_spec, dtype_spec) @@ -397,7 +394,6 @@ def resampler_fields(self): Returns ------- - record_specs : list of tuple A list of the specs for each field, a spec is a tuple of type (field_name, shape_spec, dtype_spec) @@ -420,9 +416,7 @@ def is_debug_on(self): return self._debug_mode def set_debug_mode(self, mode): - """ - - Parameters + """Parameters ---------- mode @@ -601,7 +595,6 @@ def resample(self, walkers, debug_mode=False): Returns ------- - resampled_walkers : list of Walker objects The set of resampled walkers @@ -615,7 +608,7 @@ def resample(self, walkers, debug_mode=False): """ - raise NotImplemented + raise NotImplementedError self._resample_init(walkers, debug_mode=debug_mode) @@ -676,13 +669,11 @@ def _init_walker_actions(self, n_walkers): Parameters ---------- - n_walkers : int The number of walkers to generate records for Returns ------- - decision_records : list of dict of str: value A list of default decision records for one step of resampling. diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 247f0597..062f5193 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -11,9 +11,7 @@ import numpy as np # First Party Library -from wepy.resampling.decisions.clone_merge import MultiCloneMergeDecision from wepy.resampling.resamplers.clone_merge import CloneMergeResampler -from wepy.resampling.resamplers.resampler import Resampler class REVOResampler(CloneMergeResampler): @@ -144,7 +142,6 @@ def __init__( Parameters ---------- - dist_exponent : int The distance exponent that modifies distance and weight novelty relative to each other in the variation equation. @@ -235,7 +232,6 @@ def _novelty(self, walker_weight, num_walker_copy): Parameters ---------- - walker_weight : float The weight of the walker. @@ -266,7 +262,6 @@ def _calc_variation(self, walker_weights, num_walker_copies, distance_matrix): Parameters ---------- - walker_weights : list of float The weights of all walkers. The sum of all weights should be 1.0. @@ -291,10 +286,12 @@ def _calc_variation(self, walker_weights, num_walker_copies, distance_matrix): num_walkers = len(walker_weights) # set the novelty values - walker_novelties = np.array([ - self._novelty(walker_weights[i], num_walker_copies[i]) - for i in range(num_walkers) - ]) + walker_novelties = np.array( + [ + self._novelty(walker_weights[i], num_walker_copies[i]) + for i in range(num_walkers) + ] + ) # the value to be optimized variation = 0 @@ -331,7 +328,6 @@ def _calc_variation_loss(self, walker_variation, weights, eligible_pairs): Parameters ---------- - walker_variations : arraylike of shape (num_walkers) The Vi value of each walker. @@ -343,7 +339,6 @@ def _calc_variation_loss(self, walker_variation, weights, eligible_pairs): Returns ------- - variation_loss_list : tuple A tuple of the walker merge pair indicies that meet the criteria for merging and minimize variation loss. @@ -379,7 +374,6 @@ def _find_eligible_merge_pairs( Parameters ---------- - weights : list of float The weights of all walkers. The sum of all weights should be 1.0. @@ -393,9 +387,9 @@ def _find_eligible_merge_pairs( num_walker_copies : list of int The number of copies of each walker. 0 means the walker is not exists anymore. 1 means there is one of the this walker. >1 means it should be cloned to this number of walkers. + Returns ------- - eligible_pairs : list of tuples Pairs of walker indexes that meet the criteria for merging. @@ -418,7 +412,6 @@ def decide(self, walker_weights, num_walker_copies, distance_matrix): Parameters ---------- - walker_weights : list of float The weights of all walkers. The sum of all weights should be 1.0. diff --git a/src/wepy/resampling/resamplers/wexplore.py b/src/wepy/resampling/resamplers/wexplore.py index c35566c1..0e8e32df 100644 --- a/src/wepy/resampling/resamplers/wexplore.py +++ b/src/wepy/resampling/resamplers/wexplore.py @@ -47,7 +47,6 @@ def calc_squashable_walkers_single_method(walker_weights, max_weight): Returns ------- - n_squashable : int The maximum number of squashable walkers. @@ -125,7 +124,6 @@ def decide_merge_groups_single_method(walker_weights, balance, max_weight): Returns ------- - merge_groups : list of list of int The merge group solution. @@ -156,9 +154,7 @@ def decide_merge_groups_single_method(walker_weights, balance, max_weight): ## Clone methods def calc_max_num_clones(walker_weight, min_weight, max_num_walkers): - """ - - Parameters + """Parameters ---------- walker_weight : @@ -196,7 +192,8 @@ def calc_max_num_clones(walker_weight, min_weight, max_num_walkers): class RegionTree(nx.DiGraph): """Used internally in the WExploreResampler module. Not really - intended to be used outside this module.""" + intended to be used outside this module. + """ # the strings for choosing a method of solving how deciding how # many walkers can be merged together given a group of walkers and @@ -356,9 +353,7 @@ def regions(self): return self._regions def add_child(self, parent_id, image_idx): - """ - - Parameters + """Parameters ---------- parent_id : @@ -390,9 +385,7 @@ def add_child(self, parent_id, image_idx): return child_id def children(self, parent_id): - """ - - Parameters + """Parameters ---------- parent_id : @@ -429,9 +422,7 @@ def leaf_nodes(self): return self.level_nodes(self.n_levels) def branch_tree(self, parent_id, image): - """ - - Parameters + """Parameters ---------- parent_id : @@ -532,9 +523,7 @@ def min_num_walkers(self, min_num_walkers): self._min_num_walkers = None def assign(self, state): - """ - - Parameters + """Parameters ---------- state : @@ -625,9 +614,7 @@ def clear_walkers(self): self.nodes[node_id]["balance"] = 0 def place_walkers(self, walkers): - """ - - Parameters + """Parameters ---------- walkers : @@ -737,9 +724,7 @@ def place_walkers(self, walkers): @classmethod def _max_n_merges(cls, pmax, root, weights): - """ - - Parameters + """Parameters ---------- pmax : @@ -809,9 +794,7 @@ def _max_n_merges(cls, pmax, root, weights): return max_n_merges def _calc_squashable_walkers(self, walker_weights): - """ - - Parameters + """Parameters ---------- walker_weights : @@ -831,9 +814,7 @@ def _calc_squashable_walkers(self, walker_weights): return n_squashable def _calc_max_num_clones(self, walker_weight): - """ - - Parameters + """Parameters ---------- walker_weight : @@ -846,9 +827,7 @@ def _calc_max_num_clones(self, walker_weight): return calc_max_num_clones(walker_weight, self.pmin, self.max_num_walkers) def _propagate_and_balance_shares(self, parental_balance, children_node_ids): - """ - - Parameters + """Parameters ---------- parental_balance : @@ -1079,9 +1058,7 @@ def _dispense_debit_shares( def _dispense_credit_shares( self, parental_balance, children_shares, children_receivable_shares ): - """ - - Parameters + """Parameters ---------- parental_balance : @@ -1484,9 +1461,7 @@ def _calc_share_donation( donor_donatable_shares, acceptor_receivable_shares, ): - """ - - Parameters + """Parameters ---------- donor_n_shares : @@ -1521,9 +1496,7 @@ def _calc_share_donation( return actual_donation def _decide_merge_leaf(self, leaf, merge_groups): - """ - - Parameters + """Parameters ---------- leaf : @@ -1627,9 +1600,7 @@ def _decide_merge_leaf(self, leaf, merge_groups): return merge_groups def _solve_merge_groupings(self, walker_weights, balance): - """ - - Parameters + """Parameters ---------- walker_weights : @@ -1671,9 +1642,7 @@ def _solve_merge_groupings(self, walker_weights, balance): return full_merge_groups def _decide_clone_leaf(self, leaf, merge_groups, walkers_num_clones): - """ - - Parameters + """Parameters ---------- leaf : @@ -2332,7 +2301,6 @@ def __init__( Parameters ---------- - seed : None or int The random seed. If None the system (random) one will be used. @@ -2421,7 +2389,6 @@ def assign(self, walkers): Returns ------- - assignments : list of tuple of int The leaf_id for each walker that it was assigned to. @@ -2457,7 +2424,6 @@ def decide(self, delta_walkers=0): Returns ------- - resampling_data : list of dict of str: value The resampling records resulting from the decisions. @@ -2504,9 +2470,7 @@ def decide(self, delta_walkers=0): @staticmethod def _check_resampling_data(resampling_data): - """ - - Parameters + """Parameters ---------- resampling_data : @@ -2560,9 +2524,7 @@ def _check_resampling_data(resampling_data): raise ResamplerError("Not all squashes are assigned to keep_merge slots") def _resample_init(self, walkers=None): - """ - - Parameters + """Parameters ---------- walkers : @@ -2590,9 +2552,7 @@ def _resample_init(self, walkers=None): def _resample_cleanup( self, resampling_data=None, resampler_data=None, resampled_walkers=None ): - """ - - Parameters + """Parameters ---------- resampling_data : diff --git a/src/wepy/runners/openmm.py b/src/wepy/runners/openmm.py index 9edb5e21..966f565a 100644 --- a/src/wepy/runners/openmm.py +++ b/src/wepy/runners/openmm.py @@ -47,7 +47,6 @@ ) # First Party Library -from wepy.reporter.reporter import Reporter from wepy.runners.runner import Runner from wepy.util.util import box_vectors_to_lengths_angles from wepy.walker import Walker, WalkerState @@ -249,7 +248,6 @@ def __init__( Warnings -------- - Regarding the enforce_box option. When retrieving states from an OpenMM simulation Context, you @@ -303,7 +301,6 @@ def __init__( else: self.getState_kwargs = dict(GET_STATE_KWARG_DEFAULTS) - self._cycle_platform = None self._cycle_platform_kwargs = None @@ -569,7 +566,6 @@ def generate_state( Parameters ---------- - simulation : simtk.openmm.app.Simulation object A complete simulation object from which the state will be extracted. @@ -586,7 +582,6 @@ def generate_state( Returns ------- - new_state : wepy.runners.openmm.OpenMMState object A new state from the simulation state. @@ -1154,7 +1149,8 @@ def _get_nested_attr_from_compound_key(self, compound_key, compound_feat_dict): def parameters_features(self): """Returns a dictionary of the parameters with their appropriate compound keys. This can be used for placing them in the same namespace - as the rest of the attributes.""" + as the rest of the attributes. + """ parameters = self.parameters_values() if parameters is None: @@ -1165,7 +1161,8 @@ def parameters_features(self): def parameter_derivatives_features(self): """Returns a dictionary of the parameter derivatives with their appropriate compound keys. This can be used for placing them in the same namespace - as the rest of the attributes.""" + as the rest of the attributes. + """ parameter_derivatives = self.parameter_derivatives_values() if parameter_derivatives is None: @@ -1177,7 +1174,8 @@ def parameter_derivatives_features(self): def omm_state_dict(self): """Return a dictionary with all of the default keys from the wrapped - simtk.openmm.State object""" + simtk.openmm.State object + """ feature_d = { "positions": self.positions_values(), @@ -1244,7 +1242,6 @@ def gen_sim_state(positions, system, integrator, getState_kwargs=None): Parameters ---------- - positions : arraylike of float The positions for the system you want to set @@ -1254,7 +1251,6 @@ def gen_sim_state(positions, system, integrator, getState_kwargs=None): Returns ------- - sim_state : openmm.State object """ @@ -1289,7 +1285,6 @@ def gen_walker_state(positions, system, integrator, getState_kwargs=None): Parameters ---------- - positions : arraylike of float The positions for the system you want to set @@ -1299,7 +1294,6 @@ def gen_walker_state(positions, system, integrator, getState_kwargs=None): Returns ------- - walker_state : wepy.runners.openmm.OpenMMState object """ diff --git a/src/wepy/runners/randomwalk.py b/src/wepy/runners/randomwalk.py index 0a499b52..a2261956 100644 --- a/src/wepy/runners/randomwalk.py +++ b/src/wepy/runners/randomwalk.py @@ -24,7 +24,6 @@ import random as rand # Third Party Library -import numpy as np from pint import UnitRegistry # First Party Library @@ -51,7 +50,6 @@ def __init__(self, probability=0.25): Parameters ---------- - probabilty : float "Probability" is defined here as the forward-move probability only. The backward-move probability is diff --git a/src/wepy/runners/runner.py b/src/wepy/runners/runner.py index f0cb756a..23d5e62b 100644 --- a/src/wepy/runners/runner.py +++ b/src/wepy/runners/runner.py @@ -17,6 +17,7 @@ """ +# Standard Library from typing import Protocol @@ -30,7 +31,6 @@ def pre_cycle(self, **kwargs): Parameters ---------- - kwargs : key-word arguments Key-value pairs to be interpreted by each runner implementation. @@ -48,7 +48,6 @@ def post_cycle(self, **kwargs): Parameters ---------- - kwargs : key-word arguments Key-value pairs to be interpreted by each runner implementation. diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index f9413aa6..6b27bdc2 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -43,8 +43,8 @@ """ # Standard Library -from typing import Final, Any import logging +from typing import Final logger = logging.getLogger(__name__) # Standard Library @@ -52,13 +52,12 @@ from copy import deepcopy # First Party Library +from wepy.boundary_conditions.boundary import BoundaryConditions +from wepy.reporter.reporter import Reporter +from wepy.resampling.resamplers.resampler import Resampler +from wepy.runners.runner import Runner from wepy.walker import Walker from wepy.work_mapper.mapper import Mapper -from wepy.runners.runner import Runner -from wepy.resampling.resamplers.resampler import Resampler -from wepy.reporter.reporter import Reporter -from wepy.work_mapper.mapper import WorkerMapper -from wepy.boundary_conditions.boundary import BoundaryConditions class Manager: @@ -125,9 +124,8 @@ def __init__( ): """Constructor for Manager. - Arguments + Arguments: --------- - init_walkers : list of walkers The list of the initial walkers that will be run. @@ -147,15 +145,14 @@ def __init__( reporters : list of objects implenting the Reporter interface, optional Reporters to be used. You should provide these if you want to keep data. - Warnings + Warnings: -------- - While reporters are strictly optional, you probably want to provide some because the simulation manager provides no utilities for saving data from the simulations except for the walkers at the end of a cycle or simulation. - See Also + See Also: -------- wepy.reporter.hdf5 : The standard reporter for molecular simulations in wepy. @@ -220,7 +217,6 @@ def run_segment( Returns ------- - new_walkers : list[Walker] The walkers after the segment of sampling simulation. """ @@ -312,7 +308,6 @@ def run_cycle( Returns ------- - new_walkers : list of walkers The resulting walkers of the cycle @@ -350,7 +345,7 @@ def _run_cycle( runner_opts = {} if self.runner is None: - raise RuntimeError(f"'runner' is None") + raise RuntimeError("'runner' is None") # run the runner pre-cycle hook start = time.time() diff --git a/src/wepy/util/__init__.py b/src/wepy/util/__init__.py index dc17a1ca..ae051ddc 100644 --- a/src/wepy/util/__init__.py +++ b/src/wepy/util/__init__.py @@ -2,7 +2,6 @@ Warnings -------- - Deprecation warning! Try not to rely on these functions as part of your stable code. These diff --git a/src/wepy/util/json_top.py b/src/wepy/util/json_top.py index d795de10..d103e3cc 100644 --- a/src/wepy/util/json_top.py +++ b/src/wepy/util/json_top.py @@ -17,13 +17,11 @@ def json_top_chain_fields(json_topology): Parameters ---------- - json_topology : str JSON format topology Returns ------- - chain_cols : dict of str: list """ @@ -44,13 +42,11 @@ def json_top_chain_df(json_topology): Parameters ---------- - json_topology : str JSON format topology Returns ------- - chain_df : pandas.DataFrame """ @@ -63,13 +59,11 @@ def json_top_residue_fields(json_topology): Parameters ---------- - json_topology : str JSON format topology Returns ------- - residue_cols : dict of str: list """ @@ -93,13 +87,11 @@ def json_top_residue_df(json_topology): Parameters ---------- - json_topology : str JSON format topology Returns ------- - residue_df : pandas.DataFrame """ @@ -112,13 +104,11 @@ def json_top_atom_fields(json_topology): Parameters ---------- - json_topology : str JSON format topology Returns ------- - atom_cols : dict of str: list """ @@ -145,13 +135,11 @@ def json_top_atom_df(json_topology): Parameters ---------- - json_topology : str JSON format topology Returns ------- - atoms_df : pandas.DataFrame """ @@ -193,7 +181,6 @@ def json_top_subset(json_str, atom_idxs): Parameters ---------- - json_str : str A string of valid JSON in the format of JSON used in WepyHDF5 and mdtraj HDF5 format. @@ -203,7 +190,6 @@ def json_top_subset(json_str, atom_idxs): Returns ------- - subset_json_str : str JSON string of the subset of atoms. Ordering preserved. @@ -284,7 +270,6 @@ def json_top_subset(json_str, atom_idxs): residue_idx_map[old_res_idx] = new_res_idx_counter new_res_idx_counter += 1 - # do the same but for the chain old_chain_idx = res_chain_idxs[old_res_idx] @@ -332,9 +317,9 @@ def json_top_subset(json_str, atom_idxs): new_chain_res_idx = chain_res_idx_map[old_res_idx] atom_data["index"] = new_atom_idx - top_subset["chains"][new_chain_idx]["residues"][new_chain_res_idx]["atoms"].append( - atom_data - ) + top_subset["chains"][new_chain_idx]["residues"][new_chain_res_idx][ + "atoms" + ].append(atom_data) # then translate the atom indices in the bonds new_bonds = [] diff --git a/src/wepy/util/kv.py b/src/wepy/util/kv.py index d8e36408..2d963080 100644 --- a/src/wepy/util/kv.py +++ b/src/wepy/util/kv.py @@ -164,9 +164,9 @@ def gen_uri(db_url, mode_spec): # otherwise do the whole thing else: # build the query substring - query = SQLITE3_QUERY_JOIN_CHAR.join([ - "{}={}".format(key, value) for key, value in queries.items() - ]) + query = SQLITE3_QUERY_JOIN_CHAR.join( + ["{}={}".format(key, value) for key, value in queries.items()] + ) # build the URI string db_uri = SQLITE3_QUERY_URI_TEMPLATE.format( @@ -317,9 +317,9 @@ def __setitem__(self, key, value): # translation if self.value_types is not None: - assert isinstance(value, self.value_types), ( - "Value must be a value supported by this kv" - ) + assert isinstance( + value, self.value_types + ), "Value must be a value supported by this kv" self.lockless_set(key, value) @@ -366,7 +366,7 @@ def del_query(self): return query def lockless_set(self, key, value): - """an implementation of the __setitem__ without the lock context + """An implementation of the __setitem__ without the lock context manager which turns on the DEFERRED isolation level. The isolation level of the KV is set to autocommit so now lock is needed anyhow. diff --git a/src/wepy/util/mdtraj.py b/src/wepy/util/mdtraj.py index 4fc3419a..42fcc1e1 100644 --- a/src/wepy/util/mdtraj.py +++ b/src/wepy/util/mdtraj.py @@ -12,7 +12,6 @@ # Third Party Library import mdtraj as mdj import mdtraj.core.element as elem -import numpy as np # First Party Library from wepy.util.util import traj_box_vectors_to_lengths_angles @@ -112,11 +111,13 @@ def mdtraj_to_json_topology(mdj_top): except AttributeError: element_symbol_string = "" - residue_dict["atoms"].append({ - "index": int(atom.index), - "name": str(atom.name), - "element": element_symbol_string, - }) + residue_dict["atoms"].append( + { + "index": int(atom.index), + "name": str(atom.name), + "element": element_symbol_string, + } + ) chain_dict["residues"].append(residue_dict) topology_dict["chains"].append(chain_dict) @@ -270,7 +271,6 @@ def traj_fields_to_mdtraj(traj_fields, json_topology, rep_key="positions"): Parameters ---------- - traj_fields : dict of str: values The values for the trajectory, must have a positions field specified by the `rep_key` kwarg, and a 'box_vectors' field. @@ -288,9 +288,9 @@ def traj_fields_to_mdtraj(traj_fields, json_topology, rep_key="positions"): topology = json_to_mdtraj_topology(json_topology) req_fields = ["box_vectors", rep_key] - assert [field in traj_fields for field in req_fields], ( - "Fields must have the fields: {}".format(",".join(req_fields)) - ) + assert [ + field in traj_fields for field in req_fields + ], "Fields must have the fields: {}".format(",".join(req_fields)) unitcell_lengths, unitcell_angles = traj_box_vectors_to_lengths_angles( traj_fields["box_vectors"] diff --git a/src/wepy/util/util.py b/src/wepy/util/util.py index c71d1d77..64075a72 100644 --- a/src/wepy/util/util.py +++ b/src/wepy/util/util.py @@ -1,7 +1,6 @@ """Miscellaneous functions needed by wepy.""" # Standard Library -import json import logging logger = logging.getLogger(__name__) @@ -13,15 +12,15 @@ def set_loglevel(loglevel): - """ + """\b - \b Parameters ---------- loglevel : \b + Returns ------- @@ -69,15 +68,17 @@ def traj_box_vectors_to_lengths_angles(traj_box_vectors): traj_unitcell_angles = [] for vs in traj_box_vectors: - angles = np.array([ - np.degrees( - np.arccos( - np.dot(vs[i], vs[j]) - / (np.linalg.norm(vs[i]) * np.linalg.norm(vs[j])) + angles = np.array( + [ + np.degrees( + np.arccos( + np.dot(vs[i], vs[j]) + / (np.linalg.norm(vs[i]) * np.linalg.norm(vs[j])) + ) ) - ) - for i, j in [(0, 1), (1, 2), (2, 0)] - ]) + for i, j in [(0, 1), (1, 2), (2, 0)] + ] + ) traj_unitcell_angles.append(angles) @@ -112,15 +113,17 @@ def box_vectors_to_lengths_angles(box_vectors): unitcell_lengths = np.array(unitcell_lengths) # calculate the angles for the vectors - unitcell_angles = np.array([ - np.degrees( - np.arccos( - np.dot(box_vectors[i], box_vectors[j]) - / (np.linalg.norm(box_vectors[i]) * np.linalg.norm(box_vectors[j])) + unitcell_angles = np.array( + [ + np.degrees( + np.arccos( + np.dot(box_vectors[i], box_vectors[j]) + / (np.linalg.norm(box_vectors[i]) * np.linalg.norm(box_vectors[j])) + ) ) - ) - for i, j in [(0, 1), (1, 2), (2, 0)] - ]) + for i, j in [(0, 1), (1, 2), (2, 0)] + ] + ) return unitcell_lengths, unitcell_angles @@ -185,6 +188,7 @@ def lengths_and_angles_to_box_vectors(a_length, b_length, c_length, alpha, beta, Examples -------- + Notes ----- This code is adapted from gyroid, which is licensed under the BSD @@ -206,11 +210,13 @@ def lengths_and_angles_to_box_vectors(a_length, b_length, c_length, alpha, beta, gamma = gamma * np.pi / 180 a = np.array([a_length, np.zeros_like(a_length), np.zeros_like(a_length)]) - b = np.array([ - b_length * np.cos(gamma), - b_length * np.sin(gamma), - np.zeros_like(b_length), - ]) + b = np.array( + [ + b_length * np.cos(gamma), + b_length * np.sin(gamma), + np.zeros_like(b_length), + ] + ) cx = c_length * np.cos(beta) cy = c_length * (np.cos(alpha) - np.cos(beta) * np.cos(gamma)) / np.sin(gamma) cz = np.sqrt(c_length * c_length - cx * cx - cy * cy) @@ -235,8 +241,8 @@ def concat_traj_fields(trajs_fields): cum_traj_fields = {} for field in fields: - cum_traj_fields[field] = np.concatenate([ - traj_fields[field] for traj_fields in trajs_fields - ]) + cum_traj_fields[field] = np.concatenate( + [traj_fields[field] for traj_fields in trajs_fields] + ) return cum_traj_fields diff --git a/src/wepy/work_mapper/mapper.py b/src/wepy/work_mapper/mapper.py index e60f9404..b3457d0c 100644 --- a/src/wepy/work_mapper/mapper.py +++ b/src/wepy/work_mapper/mapper.py @@ -17,9 +17,6 @@ import traceback from warnings import warn -# First Party Library -from wepy.util.util import set_loglevel - PY_MAP = map @@ -123,7 +120,6 @@ def map(self, *args, **kwargs): Examples -------- - >>> Mapper(segment_func=sum).map([(0,1,2), (3,4,5)]) [3, 12] @@ -260,7 +256,6 @@ def __init__( ): """Constructor for WorkerMapper. - Parameters ---------- num_workers : int @@ -399,7 +394,6 @@ def __init__( ): """Constructor for WorkerMapper. - Parameters ---------- num_workers : int diff --git a/src/wepy/work_mapper/worker.py b/src/wepy/work_mapper/worker.py index 91fa7fb9..77ce02d5 100644 --- a/src/wepy/work_mapper/worker.py +++ b/src/wepy/work_mapper/worker.py @@ -5,21 +5,11 @@ logger = logging.getLogger(__name__) # Standard Library -import multiprocessing as mp -import time # First Party Library # we can't move the WorkerMapper here until some of the pickles I have # laying around don't expect it to be here. In the meantime, new # software can expect it to be here so we import it here. -from wepy.work_mapper.mapper import ( - ABCWorkerMapper, - TaskException, - Worker, - WorkerException, - WorkerMapper, - WrapperException, -) # this whole thing should get refactored into a better name which # should be something like ConsumerMapper because our workers act like diff --git a/src/wepy_test_drive.py b/src/wepy_test_drive.py index 50578dcd..bf05346c 100644 --- a/src/wepy_test_drive.py +++ b/src/wepy_test_drive.py @@ -50,9 +50,9 @@ def cli( """Run a pre-parametrized wepy simulation. \b + Parameters ---------- - \b SYSTEM : str Which pre-parametrized simulation to run should have the format: System/Runner-Platform @@ -119,15 +119,15 @@ def cli( REVO : Stateless and Binless algorithm that rewards in-ensemble novelty. \b + Examples -------- - python -m wepy_test_drive LennardJonesPair/OpenMM-CPU 20 10 2 4 \b + Notes ----- - When using a GPU platform your number of workers should be the number of GPUs you want to use. diff --git a/src/wepy_tools/sim_makers/openmm/__init__.py b/src/wepy_tools/sim_makers/openmm/__init__.py index d87b57e1..af34d177 100644 --- a/src/wepy_tools/sim_makers/openmm/__init__.py +++ b/src/wepy_tools/sim_makers/openmm/__init__.py @@ -1,4 +1,4 @@ -# First Party Library +# Local Modules from .lennard_jones import LennardJonesPairOpenMMSimMaker from .lysozyme import LysozymeImplicitOpenMMSimMaker from .sim_maker import ( diff --git a/src/wepy_tools/sim_makers/openmm/lennard_jones.py b/src/wepy_tools/sim_makers/openmm/lennard_jones.py index 9e7a56c7..65683243 100644 --- a/src/wepy_tools/sim_makers/openmm/lennard_jones.py +++ b/src/wepy_tools/sim_makers/openmm/lennard_jones.py @@ -7,6 +7,8 @@ from wepy.boundary_conditions.receptor import UnbindingBC from wepy.resampling.distances.distance import Distance from wepy.runners.openmm import GET_STATE_KWARG_DEFAULTS + +# Local Modules from .sim_maker import OpenMMToolsTestSysSimMaker diff --git a/src/wepy_tools/sim_makers/openmm/lysozyme.py b/src/wepy_tools/sim_makers/openmm/lysozyme.py index e5d65397..8a18da2e 100644 --- a/src/wepy_tools/sim_makers/openmm/lysozyme.py +++ b/src/wepy_tools/sim_makers/openmm/lysozyme.py @@ -1,5 +1,4 @@ # Standard Library -from copy import copy # Third Party Library import numpy as np @@ -15,9 +14,11 @@ json_top_atom_df, json_top_residue_df, ) -from .sim_maker import OpenMMToolsTestSysSimMaker from wepy_tools.systems import receptor as receptor_tools +# Local Modules +from .sim_maker import OpenMMToolsTestSysSimMaker + class LysozymeImplicitOpenMMSimMaker(OpenMMToolsTestSysSimMaker): TEST_SYS = LysozymeImplicit @@ -166,7 +167,6 @@ def binding_site_idxs(cls, cutoff): Parameters ---------- - cutoff : Quantity """ diff --git a/src/wepy_tools/sim_makers/openmm/sim_maker.py b/src/wepy_tools/sim_makers/openmm/sim_maker.py index 651de1a9..1fd5f109 100644 --- a/src/wepy_tools/sim_makers/openmm/sim_maker.py +++ b/src/wepy_tools/sim_makers/openmm/sim_maker.py @@ -403,7 +403,7 @@ def resolve_reporter_params(self, apparatus, reporter_specs, reporters_kwargs=No "WepyHDF5Reporter", "DashboardReporter", # DEBUG: this isn't compatible right now, needs refactoring - #'ResTreeReporter', + # 'ResTreeReporter', "WalkerReporter", ] diff --git a/src/wepy_tools/sim_makers/toys/randomwalk.py b/src/wepy_tools/sim_makers/toys/randomwalk.py index 0bdca0a9..670b6ee7 100644 --- a/src/wepy_tools/sim_makers/toys/randomwalk.py +++ b/src/wepy_tools/sim_makers/toys/randomwalk.py @@ -27,12 +27,8 @@ """ # Standard Library -import json -import os -import sys # Third Party Library -import h5py import mdtraj as mdj import numpy as np import pandas as pd @@ -40,7 +36,6 @@ # First Party Library from wepy.hdf5 import WepyHDF5 from wepy.reporter.hdf5 import WepyHDF5Reporter -from wepy.resampling.resamplers.resampler import NoResampler from wepy.runners.randomwalk import UNIT_NAMES, RandomWalkRunner from wepy.sim_manager import Manager from wepy.util.mdtraj import mdtraj_to_json_topology @@ -285,7 +280,6 @@ def accuracy(self, x, Px): Returns ------- - accuracy: float The value that specifies how accurate the resampler is at point x. The highest accuracy is achived when P(X) = Pt(x). @@ -305,13 +299,11 @@ def Pt(self, x): Parameters ---------- - x: int The position. Returns ------- - accuracy : float The value of the target probability when the forward-move probability p. @@ -341,7 +333,6 @@ def get_max_range(self, wepy_h5, run_idx=0): Returns ------- - max_range: int The maximum range that is visited by all walkers in all dimensions. diff --git a/src/wepy_tools/systems/openmm/base.py b/src/wepy_tools/systems/openmm/base.py index 428a3192..67163741 100644 --- a/src/wepy_tools/systems/openmm/base.py +++ b/src/wepy_tools/systems/openmm/base.py @@ -1,14 +1,10 @@ -import os -import os.path -import numpy as np - -import scipy -import scipy.special -import scipy.integrate +# Standard Library +# Third Party Library +import numpy as np import openmm -import openmm.unit as unit import openmm.app as omma +import openmm.unit as unit class TestSystem(object): @@ -28,12 +24,10 @@ class TestSystem(object): Notes ----- - Unimplemented methods will default to the base class methods, which raise a NotImplementedException. Examples -------- - Create a test system. >>> testsystem = TestSystem() @@ -118,6 +112,7 @@ def topology(self): @property def mdtraj_topology(self): """The mdtraj.Topology object corresponding to the test system (read-only).""" + # Third Party Library import mdtraj as md if self._mdtraj_topology is None: diff --git a/src/wepy_tools/systems/openmm/nacl_pair.py b/src/wepy_tools/systems/openmm/nacl_pair.py index f247d883..58922fa3 100644 --- a/src/wepy_tools/systems/openmm/nacl_pair.py +++ b/src/wepy_tools/systems/openmm/nacl_pair.py @@ -1,10 +1,11 @@ -from wepy_tools.systems.openmm.base import TestSystem - +# Third Party Library import numpy as np - import openmm -import openmm.unit as unit import openmm.app as omma +import openmm.unit as unit + +# First Party Library +from wepy_tools.systems.openmm.base import TestSystem class NaClPair(TestSystem): diff --git a/src/wepy_tools/systems/receptor.py b/src/wepy_tools/systems/receptor.py index 0ad9e4da..f163810e 100644 --- a/src/wepy_tools/systems/receptor.py +++ b/src/wepy_tools/systems/receptor.py @@ -4,7 +4,7 @@ import openmm.unit as unit # First Party Library -from wepy.util.mdtraj import json_to_mdtraj_topology, mdtraj_to_json_topology +from wepy.util.mdtraj import json_to_mdtraj_topology from wepy.util.util import box_vectors_to_lengths_angles @@ -40,7 +40,6 @@ def binding_site_idxs( Returns ------- - binding_site_idxs : arraylike (1,) """ diff --git a/tests/benchmarks/test_mappers.py b/tests/benchmarks/test_mappers.py index 305b7914..99ecc144 100644 --- a/tests/benchmarks/test_mappers.py +++ b/tests/benchmarks/test_mappers.py @@ -3,33 +3,19 @@ logger = logging.getLogger(__name__) # Standard Library -import multiprocessing as mp import time -from copy import deepcopy # Third Party Library import pytest # First Party Library -from wepy.resampling.resamplers.resampler import NoResampler -from wepy.runners.openmm import ( - OpenMMCPUWalkerTaskProcess, - OpenMMCPUWorker, - OpenMMGPUWalkerTaskProcess, - OpenMMGPUWorker, - OpenMMRunner, - OpenMMState, - OpenMMWalker, -) -from wepy.sim_manager import Manager from wepy.walker import Walker, WalkerState -from wepy.work_mapper.mapper import Mapper, TaskException +from wepy.work_mapper.mapper import Mapper from wepy.work_mapper.task_mapper import ( TaskMapper, - TaskProcessException, WalkerTaskProcess, ) -from wepy.work_mapper.worker import Worker, WorkerException, WorkerMapper +from wepy.work_mapper.worker import Worker, WorkerMapper from wepy_tools.sim_makers.openmm.lennard_jones import LennardJonesPairOpenMMSimMaker from wepy_tools.sim_makers.openmm.lysozyme import LysozymeImplicitOpenMMSimMaker diff --git a/tests/docs/test_examples/test_Lennard_Jones_Pair.py b/tests/docs/test_examples/test_Lennard_Jones_Pair.py index 29a178db..ba83e81a 100644 --- a/tests/docs/test_examples/test_Lennard_Jones_Pair.py +++ b/tests/docs/test_examples/test_Lennard_Jones_Pair.py @@ -1,12 +1,9 @@ # Standard Library import os -import os.path as osp -from pathlib import Path # Third Party Library -import pytest from pytest_shutil.cmdline import chdir -from pytest_shutil.run import run, run_as_main +from pytest_shutil.run import run ### Tests diff --git a/tests/docs/test_examples/test_Lysozyme.py b/tests/docs/test_examples/test_Lysozyme.py index 166e31af..8804c285 100644 --- a/tests/docs/test_examples/test_Lysozyme.py +++ b/tests/docs/test_examples/test_Lysozyme.py @@ -1,12 +1,9 @@ # Standard Library -import os -import os.path as osp -from pathlib import Path # Third Party Library from pytest_check import check from pytest_shutil.cmdline import chdir -from pytest_shutil.run import run, run_as_main +from pytest_shutil.run import run ### Tests diff --git a/tests/docs/test_examples/test_RandomWalk.py b/tests/docs/test_examples/test_RandomWalk.py index e8b56490..b264b983 100644 --- a/tests/docs/test_examples/test_RandomWalk.py +++ b/tests/docs/test_examples/test_RandomWalk.py @@ -1,11 +1,8 @@ # Standard Library -import os -import os.path as osp -from pathlib import Path # Third Party Library from pytest_shutil.cmdline import chdir -from pytest_shutil.run import run, run_as_main +from pytest_shutil.run import run ### Tests diff --git a/tests/docs/test_pages/test_pages.py b/tests/docs/test_pages/test_pages.py index 4368e781..7aa3d46e 100644 --- a/tests/docs/test_pages/test_pages.py +++ b/tests/docs/test_pages/test_pages.py @@ -1,13 +1,11 @@ """Test the main documentation pages.""" # Standard Library -import os -import os.path as osp from pathlib import Path # Third Party Library from pytest_shutil.cmdline import chdir -from pytest_shutil.run import run, run_as_main +from pytest_shutil.run import run def test_dir_structure(datadir_factory): diff --git a/tests/docs/test_tutorials/test_Orchestrator.py b/tests/docs/test_tutorials/test_Orchestrator.py index 46778987..ff3ef3f6 100644 --- a/tests/docs/test_tutorials/test_Orchestrator.py +++ b/tests/docs/test_tutorials/test_Orchestrator.py @@ -1,11 +1,8 @@ # Standard Library -import os -import os.path as osp -from pathlib import Path # Third Party Library from pytest_shutil.cmdline import chdir -from pytest_shutil.run import run, run_as_main +from pytest_shutil.run import run ### Tests diff --git a/tests/docs/test_tutorials/test_data_analysis.py b/tests/docs/test_tutorials/test_data_analysis.py index 0793e8c2..d7092faf 100644 --- a/tests/docs/test_tutorials/test_data_analysis.py +++ b/tests/docs/test_tutorials/test_data_analysis.py @@ -1,11 +1,8 @@ # Standard Library -import os -import os.path as osp -from pathlib import Path # Third Party Library from pytest_shutil.cmdline import chdir -from pytest_shutil.run import run, run_as_main +from pytest_shutil.run import run ### Tests diff --git a/tests/docs/test_tutorials/test_extended_test_drive.py b/tests/docs/test_tutorials/test_extended_test_drive.py index a889bbe3..469a4a95 100644 --- a/tests/docs/test_tutorials/test_extended_test_drive.py +++ b/tests/docs/test_tutorials/test_extended_test_drive.py @@ -1,12 +1,9 @@ # Standard Library -import os -import os.path as osp -from pathlib import Path # Third Party Library from pytest_check import check from pytest_shutil.cmdline import chdir -from pytest_shutil.run import run, run_as_main +from pytest_shutil.run import run ### Tests diff --git a/tests/docs/test_tutorials/test_tutorials.py b/tests/docs/test_tutorials/test_tutorials.py index 03a60d4f..b06cf702 100644 --- a/tests/docs/test_tutorials/test_tutorials.py +++ b/tests/docs/test_tutorials/test_tutorials.py @@ -1,14 +1,9 @@ """Test the examples library.""" # Standard Library -import os -import os.path as osp -from pathlib import Path # Third Party Library -import delegator # the helper modules for testing -from myutils import cd ## write one test per example diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 35fa4bc8..11d4a967 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,5 +1,4 @@ # Third Party Library -import pytest # using this to get rid of the warning without having to put it in my diff --git a/tests/integration/test_lj_combinations.py b/tests/integration/test_lj_combinations.py index d0ec7e79..a049d749 100644 --- a/tests/integration/test_lj_combinations.py +++ b/tests/integration/test_lj_combinations.py @@ -3,33 +3,11 @@ logger = logging.getLogger(__name__) # Standard Library -import multiprocessing as mp -import time -from copy import deepcopy # Third Party Library import pytest # First Party Library -from wepy.resampling.resamplers.resampler import NoResampler -from wepy.runners.openmm import ( - OpenMMCPUWalkerTaskProcess, - OpenMMCPUWorker, - OpenMMGPUWalkerTaskProcess, - OpenMMGPUWorker, - OpenMMRunner, - OpenMMState, - OpenMMWalker, -) -from wepy.sim_manager import Manager -from wepy.walker import Walker, WalkerState -from wepy.work_mapper.mapper import Mapper, TaskException -from wepy.work_mapper.task_mapper import ( - TaskMapper, - TaskProcessException, - WalkerTaskProcess, -) -from wepy.work_mapper.worker import Worker, WorkerException, WorkerMapper from wepy_tools.sim_makers.openmm.lennard_jones import LennardJonesPairOpenMMSimMaker from wepy_tools.sim_makers.openmm.lysozyme import LysozymeImplicitOpenMMSimMaker diff --git a/tests/integration/test_lj_fixture.py b/tests/integration/test_lj_fixture.py index 0664e993..27c0fb2e 100644 --- a/tests/integration/test_lj_fixture.py +++ b/tests/integration/test_lj_fixture.py @@ -3,7 +3,6 @@ logger = logging.getLogger(__name__) # Standard Library -import multiprocessing as mp import pdb # Third Party Library diff --git a/tests/unit/test_work_mapper/test_mapper.py b/tests/unit/test_work_mapper/test_mapper.py index 4364f11c..e38371cd 100644 --- a/tests/unit/test_work_mapper/test_mapper.py +++ b/tests/unit/test_work_mapper/test_mapper.py @@ -3,9 +3,7 @@ logger = logging.getLogger(__name__) # Standard Library -import multiprocessing as mp import time -from copy import deepcopy # Third Party Library import pytest @@ -15,10 +13,9 @@ from wepy.work_mapper.mapper import Mapper, TaskException from wepy.work_mapper.task_mapper import ( TaskMapper, - TaskProcessException, WalkerTaskProcess, ) -from wepy.work_mapper.worker import Worker, WorkerException, WorkerMapper +from wepy.work_mapper.worker import Worker, WorkerMapper ARGS = (0, 1, 2) From 2529f7f65c110915274b9e19872ac843d2933095 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 24 Nov 2025 16:23:29 -0500 Subject: [PATCH 005/143] get tests passing --- src/wepy/work_mapper/worker.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/wepy/work_mapper/worker.py b/src/wepy/work_mapper/worker.py index 77ce02d5..f62add2e 100644 --- a/src/wepy/work_mapper/worker.py +++ b/src/wepy/work_mapper/worker.py @@ -1,16 +1,8 @@ """Classes for workers and tasks for use with WorkerMapper.""" -# Standard Library -import logging +from .mapper import Worker, WorkerMapper -logger = logging.getLogger(__name__) -# Standard Library - -# First Party Library -# we can't move the WorkerMapper here until some of the pickles I have -# laying around don't expect it to be here. In the meantime, new -# software can expect it to be here so we import it here. - -# this whole thing should get refactored into a better name which -# should be something like ConsumerMapper because our workers act like -# consumers +__all__ = [ + "Worker", + "WorkerMapper", +] From 24a96539dd846b8990a5b924f44696a8fc6c6fcc Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 25 Nov 2025 15:06:02 -0500 Subject: [PATCH 006/143] working on README --- README.md | 62 +++++++++++++++++++++++++++++++++ README.org | 93 -------------------------------------------------- pyproject.toml | 1 - 3 files changed, 62 insertions(+), 94 deletions(-) create mode 100644 README.md delete mode 100644 README.org diff --git a/README.md b/README.md new file mode 100644 index 00000000..798f701a --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +DOI + +# Weighted Ensemble Python: wepy + +![Wepy Logo](./info/logo/wepy.svg) + + +[Documentation](https://adicksonlab.github.io/wepy/index.html) + +Modular implementation and framework for running weighted ensemble (WE) +simulations in pure python, where the aim is to have simple things +simple and complicated things possible. The latter being the priority. + +The goal of the architecture is that it should be highly modular to +allow extension, but provide a "killer app" for most uses that just +works, no questions asked. + +Comes equipped with support for +[OpenMM](https://github.com/pandegroup/openmm) molecular dynamics, +parallelization using multiprocessing, the +[WExplore](http://pubs.acs.org/doi/abs/10.1021/jp411479c) and +[REVO](https://pubmed.ncbi.nlm.nih.gov/31255090/) (Resampling +Ensembles by Variance Optimization) resampling algorithms, and an HDF5 +file format and library for storing and querying your WE datasets that +can be used from the command line. + +The deeper architecture of `wepy` is intended to be loosely coupled, +so that unforeseen use cases can be accomodated, but tightly +integrated for the most common of use cases, i.e. molecular dynamics. + +This allows freedom for fast development of new methods. + +## Installation + +Also see: [Installation Instructions](info/installation.org) + +```shell +pip install wepy + +# for openmm and MD related packages +pip install 'wepy[md]' +``` + +## Citations + +Current [Zenodo DOI](https://zenodo.org/badge/latestdoi/101077926). + +Cite software as: + +``` +Samuel D. Lotz, Nazanin Donyapour, Alex Dickson, Tom Dixon, Nicole Roussey, & Rob Hall. (2020, August 4). ADicksonLab/wepy: 1.0.0 Major version release (Version v1.0.0). Zenodo. http://doi.org/10.5281/zenodo.3973431 +``` + +Accompanying journal article: + +- [ACS Omega](https://pubs.acs.org/doi/abs/10.1021/acsomega.0c03892) article + + + + + + diff --git a/README.org b/README.org deleted file mode 100644 index 4b7edfc0..00000000 --- a/README.org +++ /dev/null @@ -1,93 +0,0 @@ -* Weighted Ensemble Python (wepy) - - #+ATTR_HTML: title="Join the chat at https://gitter.im/wepy/general" - [[https://gitter.im/wepy/general?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge][file:https://badges.gitter.im/wepy/general.svg]] - -[[./info/logo/wepy.svg]] - -# trying to make a zenodo badge but github doesn't support this -# directly. Would have to add a separate build step for this. -#+begin_export html -DOI -#+end_export - -[[https://adicksonlab.github.io/wepy/index.html][Sphinx Documentation]] - -[[https://github.com/ADicksonLab/wepy/blob/master/info/README.org][Plaintext Org-Mode Docs]] - -Modular implementation and framework for running weighted ensemble (WE) -simulations in pure python, where the aim is to have simple things -simple and complicated things possible. The latter being the priority. - -The goal of the architecture is that it should be highly modular to -allow extension, but provide a "killer app" for most uses that just -works, no questions asked. - -Comes equipped with support for [[https://github.com/pandegroup/openmm][OpenMM]] molecular dynamics, -parallelization using multiprocessing, the [[http://pubs.acs.org/doi/abs/10.1021/jp411479c][WExplore]] -and [[https://pubmed.ncbi.nlm.nih.gov/31255090/][REVO]] (Resampling Ensembles by Variance Optimization) resampling -algorithms, and an HDF5 file format and library for storing and -querying your WE datasets that can be used from the command line. - -The deeper architecture of ~wepy~ is intended to be loosely coupled, -so that unforeseen use cases can be accomodated, but tightly -integrated for the most common of use cases, i.e. molecular dynamics. - -This allows freedom for fast development of new methods. - -Full [[https://github.com/ADicksonLab/wepy/blob/master/info/introduction.org][introduction]]. - -** Installation - -Also see: [[info/installation.org][Installation Instructions]] - -We recommend running this version of `wepy` in a conda environment using `python=3.10` or greater: - -#+BEGIN_SRC bash - conda create -n wepy python=3.10 - conda activate wepy -#+END_SRC - -Next, install `wepy` with pip: - -#+BEGIN_SRC bash - pip install wepy -#+END_SRC - -which will also install most dependencies. - -Alternatively, the latest version of `wepy` can be installed from the git repo source: -#+BEGIN_SRC bash - git clone https://github.com/ADicksonLab/wepy.git - cd wepy - pip install . -#+END_SRC - -The OpenMM package can then be installed using conda: - -#+BEGIN_SRC bash - conda install -c conda-forge openmm -#+END_SRC - -Check its installed by running the command line interface: - -#+begin_src bash :tangle check_installation.bash -wepy --help -#+end_src - -** Citations - -Current [[https://zenodo.org/badge/latestdoi/101077926][Zenodo DOI]]. - -Cite software as: - -#+begin_example -Samuel D. Lotz, Nazanin Donyapour, Alex Dickson, Tom Dixon, Nicole Roussey, & Rob Hall. (2020, August 4). ADicksonLab/wepy: 1.0.0 Major version release (Version v1.0.0). Zenodo. http://doi.org/10.5281/zenodo.3973431 -#+end_example - -Accompanying journal article: - -- [[https://pubs.acs.org/doi/abs/10.1021/acsomega.0c03892][ACS Omega]] article - - - diff --git a/pyproject.toml b/pyproject.toml index 05a9bd27..931c5247 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,6 @@ graphics = [ "pillow>=10.0.1", ] - [project.urls] Documentation = "https://adicksonlab.github.io/wepy/index.html" From 3dd5e77689bcfaa9773ca3b645b0cb50233ee619 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 25 Nov 2025 15:24:27 -0500 Subject: [PATCH 007/143] fix pyproject.toml readme --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 931c5247..9d92872a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ [project] name = "wepy" description = "Weighted Ensemble Framework" -readme = { "file" = "README.org", "content-type" = "text/plain" } +readme = { "file" = "README.md", "content-type" = "text/plain" } license = "MIT" requires-python = ">=3.11" From 468d4e53e9fc6ea131dee76fa3ff30c11bfacf6d Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 25 Nov 2025 15:29:35 -0500 Subject: [PATCH 008/143] update class definitions --- info/users_guide.org | 12 +- src/wepy/analysis/profiles.py | 2 +- src/wepy/resampling/distances/distance.py | 2 +- src/wepy/walker.py | 4 +- src/wepy/work_mapper/mapper.py | 2 +- src/wepy_tools/sim_makers/toys/randomwalk.py | 2 +- src/wepy_tools/systems/openmm/base.py | 2 +- tests/unit/test_work_mapper/test_mapper.py | 113 ++++++++++++++++++- 8 files changed, 125 insertions(+), 14 deletions(-) diff --git a/info/users_guide.org b/info/users_guide.org index 19d0b93d..0d3a8fb7 100644 --- a/info/users_guide.org +++ b/info/users_guide.org @@ -583,7 +583,7 @@ We see that ReceptorDistance is inheriting from the ~Distance~ class, which is defined as such: #+BEGIN_SRC python - class Distance(object): + class Distance: """Abstract Base class for Distance classes.""" def __init__(self): @@ -615,7 +615,7 @@ as being equivalent to: We notice that the ~Distance~ class defines this method ~distance~: #+BEGIN_SRC python - class Distance(object): + class Distance: ... def distance(self, state_a, state_b): @@ -634,7 +634,7 @@ it still has access to it. We notice that ~Distance~ also defines the method ~image_distance~: #+BEGIN_SRC python - class Distance(object): + class Distance: ... @@ -1040,7 +1040,7 @@ weights of walkers in an ensemble (a simple list container). The implementation is very simple: #+begin_src python -class Walker(object): +class Walker: def __init__(self, state, weight): @@ -1077,7 +1077,7 @@ it doesn't directly inherit from the actual ~WalkerState~ class. The implementation is very simple: #+begin_src python - class WalkerState(object): + class WalkerState: def __init__(self, **kwargs): self._data = kwargs @@ -1524,7 +1524,7 @@ then using a simple for-loop to sequentially compute the segments: #+begin_src python - class Mapper(object): + class Mapper: def init(self, segment_func): diff --git a/src/wepy/analysis/profiles.py b/src/wepy/analysis/profiles.py index bcb90a48..da886361 100644 --- a/src/wepy/analysis/profiles.py +++ b/src/wepy/analysis/profiles.py @@ -279,7 +279,7 @@ def contigtrees_bin_edges( return bin_edges -class ContigTreeProfiler(object): +class ContigTreeProfiler: """A wrapper class around a ContigTree that provides extra methods for generating free energy profiles for observables. """ diff --git a/src/wepy/resampling/distances/distance.py b/src/wepy/resampling/distances/distance.py index 530ebe94..c81d7eca 100644 --- a/src/wepy/resampling/distances/distance.py +++ b/src/wepy/resampling/distances/distance.py @@ -34,7 +34,7 @@ from wepy.util.util import box_vectors_to_lengths_angles -class Distance(object): +class Distance: """Abstract Base class for Distance classes.""" def __init__(self): diff --git a/src/wepy/walker.py b/src/wepy/walker.py index 1c8119d8..c75bf1c4 100644 --- a/src/wepy/walker.py +++ b/src/wepy/walker.py @@ -125,7 +125,7 @@ def merge(walkers): return new_walker, keep_idx -class Walker(object): +class Walker: """Reference implementation of the Walker interface. A container for: @@ -211,7 +211,7 @@ def merge(self, other_walkers): return merge([self] + other_walkers) -class WalkerState(object): +class WalkerState: """Reference implementation of the WalkerState interface. Access all key-value pairs as a dictionary with the dict() method. diff --git a/src/wepy/work_mapper/mapper.py b/src/wepy/work_mapper/mapper.py index b3457d0c..0d75801d 100644 --- a/src/wepy/work_mapper/mapper.py +++ b/src/wepy/work_mapper/mapper.py @@ -20,7 +20,7 @@ PY_MAP = map -class ABCMapper(object): +class ABCMapper: """Abstract base class for a Mapper.""" def __init__(self, segment_func=None, **kwargs): diff --git a/src/wepy_tools/sim_makers/toys/randomwalk.py b/src/wepy_tools/sim_makers/toys/randomwalk.py index 670b6ee7..4de3c32d 100644 --- a/src/wepy_tools/sim_makers/toys/randomwalk.py +++ b/src/wepy_tools/sim_makers/toys/randomwalk.py @@ -51,7 +51,7 @@ np.set_printoptions(precision=PRECISION) -class RandomwalkProfiler(object): +class RandomwalkProfiler: """A class to implement RandomWalkProfilier.""" RANDOM_WALK_TEMPLATE = """* Random walk simulation: diff --git a/src/wepy_tools/systems/openmm/base.py b/src/wepy_tools/systems/openmm/base.py index 67163741..80dd9003 100644 --- a/src/wepy_tools/systems/openmm/base.py +++ b/src/wepy_tools/systems/openmm/base.py @@ -7,7 +7,7 @@ import openmm.unit as unit -class TestSystem(object): +class TestSystem: """Abstract base class for test systems, demonstrating how to implement a test system. Parameters diff --git a/tests/unit/test_work_mapper/test_mapper.py b/tests/unit/test_work_mapper/test_mapper.py index e38371cd..9a8beed9 100644 --- a/tests/unit/test_work_mapper/test_mapper.py +++ b/tests/unit/test_work_mapper/test_mapper.py @@ -10,7 +10,19 @@ # First Party Library from wepy.walker import Walker, WalkerState -from wepy.work_mapper.mapper import Mapper, TaskException +from wepy.work_mapper.mapper import ( + ABCMapper, + Mapper, + TaskException, + Task, + WrapperException, + TaskException, + ABCWorkerMapper, + WorkerException, + WorkerKilledError, + WorkerMapper, + Worker, +) from wepy.work_mapper.task_mapper import ( TaskMapper, WalkerTaskProcess, @@ -33,6 +45,35 @@ def task_pass(walker): TASK_PASS_ANSWER = [n + 1 for n in ARGS] +class TestABCMapper: + def test___init__(self): + + no_mapper = ABCMapper() + assert no_mapper.segment_func is None + assert no_mapper.attributes == {} + + mapper = ABCMapper( + segment_func=(lambda x: ) + ) + assert no_mapper.segment_func is None + assert no_mapper.attributes == {} + + + def test_init(self): + assert False + + def test_cleanup(self): + assert False + +class TestMapper: + def test___init__(self): + assert False + + def test_map(self): + assert False + def test_worker_segment_times(self): + assert False + class TestWorkMappers: def test_mapper(self): @@ -78,7 +119,77 @@ def test_task_mapper(self): time.sleep(1) +class TestTask: + + def test___init__(self): + assert False + + def test___call__(self): + assert False + +class TestWrapperException: + + def test___init__(self): + assert False + + +class TestABCWorkerMapper: + + def test___init__(self): + assert False + + def test_init(self): + assert False + + def test_cleanup(self): + assert False + + def test__make_task(self): + assert False + +class TestWorkerMapper: + + def test___init__(self): + assert False + + def test_init(self): + assert False + + def test__sigterm_shutdown(self): + assert False + + def test_force_shutdown(self): + assert False + + def test_cleanup(self): + assert False + + def test_map(self): + assert False + +class TestWorker: + + def test___init__(self): + assert False + + def test_run(self): + assert False + + def test__sigterm_shutdown(self): + assert False + + def test__shutdown(self): + assert False + + def test__run_worker(self): + assert False + + def test_run_task(self): + assert False + def test__run_task(self): + assert False + # test that task failures are passed up properly def task_fail(walker): n = walker.state["num"] From bfec483522fcc3fa62d124bdecbb3466a3889e45 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 25 Nov 2025 16:03:57 -0500 Subject: [PATCH 009/143] add attrs dependency --- pyproject.toml | 1 + uv.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 9d92872a..17d4280f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ ] dependencies = [ + "attrs", "numpy", "h5py>=3", "networkx", diff --git a/uv.lock b/uv.lock index b94fd9b3..687b6729 100644 --- a/uv.lock +++ b/uv.lock @@ -3123,6 +3123,7 @@ name = "wepy" version = "1.1.0" source = { editable = "." } dependencies = [ + { name = "attrs" }, { name = "click" }, { name = "dill" }, { name = "geomm" }, @@ -3177,6 +3178,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "attrs" }, { name = "click" }, { name = "dask", extras = ["bag"], marker = "extra == 'distributed'" }, { name = "dill" }, From e73547d0bf5481dd81bd39cb48f4fca65bda3668 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 25 Nov 2025 16:04:06 -0500 Subject: [PATCH 010/143] add walker types and tests --- src/wepy/walker.py | 222 +++++++++++++++++++------------------- tests/unit/test_walker.py | 194 +++++++++++++++++++++++++++++++++ 2 files changed, 307 insertions(+), 109 deletions(-) create mode 100644 tests/unit/test_walker.py diff --git a/src/wepy/walker.py b/src/wepy/walker.py index c75bf1c4..a8f9930c 100644 --- a/src/wepy/walker.py +++ b/src/wepy/walker.py @@ -28,103 +28,59 @@ # Standard Library import logging +from typing import Protocol, Hashable, Any, Self -logger = logging.getLogger(__name__) # Standard Library import random as rand from copy import deepcopy +import attrs -def split(walker, number=2): - """Split (AKA make multiple clones) of a single walker. +logger = logging.getLogger(__name__) - Creates multiple new walkers that have the same state as the given - walker with weight evenly divided between them. +class WalkerStateProtocol(Protocol): - Parameters - ---------- - walker : object implementing the Walker interface - The walker to split/clone - number : int - The number of clones to make of the walker - (Default value = 2) + def __getitem__(self, key: str) -> Any: + ... - Returns - ------- - cloned_walkers : list of objects implementing the Walker interface - - """ - # calculate the weight of all child walkers split uniformly - split_prob = walker.weight / (number) - # make the clones - clones = [] - for i in range(number): - clones.append(type(walker)(walker.state, split_prob)) + def dict(self) -> dict[str, Any]: + ... - return clones +class WalkerState: + """Reference implementation of the WalkerState interface. -def keep_merge(walkers, keep_idx): - """Merge a set of walkers using the state of one of them. + Access all key-value pairs as a dictionary with the dict() method. - Parameters - ---------- - walkers : list of objects implementing the Walker interface - The walkers that will be merged together - keep_idx : int - The index of the walker in the walkers list that will be used - to set the state of the new merged walker. + Access individual values using the accessor syntax similar to + dictionaries: - Returns - ------- - merged_walker : object implementing the Walker interface + >>> WalkerState(my_key='value')['my_key'] + 'value' """ - weights = [walker.weight for walker in walkers] - # but we add their weight to the new walker - new_weight = sum(weights) - # create a new walker with the keep_walker state - new_walker = type(walkers[0])(walkers[keep_idx].state, new_weight) - - return new_walker - - -def merge(walkers): - """Merge this walker with another keeping the state of one of them - and adding the weights. - - The walker that has it's state kept is a random choice weighted by - the walkers weights. - - Parameters - ---------- - walkers : list of objects implementing the Walker interface - The walkers that will be merged together - - Returns - ------- - merged_walker : object implementing the Walker interface - - """ + def __init__(self, **kwargs: dict[str, Any]) -> Self: + """Constructor for WalkerState. - weights = [walker.weight for walker in walkers] - # choose a walker according to their weights to keep its state - keep_walker = rand.choices(walkers, weights=weights) - keep_idx = walkers.index(keep_walker) + All key-word arguments passed in will be set as the key-value + pairs for the state. - # TODO do we need this? - # the others are "squashed" and we lose their state - # squashed_walkers = set(walkers).difference(keep_walker) + """ + self._data = kwargs - # but we add their weight to the new walker - new_weight = sum(weights) - # create a new walker with the keep_walker state - new_walker = type(walkers[0])(keep_walker.state, new_weight) + def __getitem__(self, key: str) -> Any: + return self._data[key] - return new_walker, keep_idx + def __eq__(self, other: Self) -> bool: + return self._data == other._data + def dict(self) -> dict[str, Any]: + """Return all key-value pairs as a dictionary.""" + return deepcopy(self._data) + +@attrs.define class Walker: """Reference implementation of the Walker interface. @@ -135,21 +91,10 @@ class Walker: """ - def __init__(self, state, weight): - """Constructor for Walker. + state: WalkerStateProtocol + weight: float - Parameters - ---------- - state : object implementing the WalkerState interface - - weight : float - - """ - - self.state = state - self.weight = weight - - def clone(self, number=1): + def clone(self, number: int = 1) -> list[Self]: """Clone this walker by making a copy with the same state and split the probability uniformly between clones. @@ -179,7 +124,7 @@ def clone(self, number=1): return clones - def squash(self, merge_target): + def squash(self, merge_target: Self) -> Self: """Add the weight of this walker to another. Parameters @@ -195,7 +140,7 @@ def squash(self, merge_target): new_weight = self.weight + merge_target.weight return type(self)(merge_target.state, new_weight) - def merge(self, other_walkers): + def merge(self, other_walkers: list[Self]) -> Self: """Merge a set of other walkers into this one using the merge function. Parameters @@ -208,34 +153,93 @@ def merge(self, other_walkers): merged_walker : object implementing the Walker interface """ - return merge([self] + other_walkers) + return merge([self] + other_walkers)[0] +def split(walker: Walker, number: int = 2) -> list[Walker]: + """Split (AKA make multiple clones) of a single walker. -class WalkerState: - """Reference implementation of the WalkerState interface. + Creates multiple new walkers that have the same state as the given + walker with weight evenly divided between them. - Access all key-value pairs as a dictionary with the dict() method. + Parameters + ---------- + walker : object implementing the Walker interface + The walker to split/clone + number : int + The number of clones to make of the walker + (Default value = 2) - Access individual values using the accessor syntax similar to - dictionaries: + Returns + ------- + cloned_walkers : list of objects implementing the Walker interface - >>> WalkerState(my_key='value')['my_key'] - 'value' + """ + # calculate the weight of all child walkers split uniformly + split_prob = walker.weight / (number) + # make the clones + clones = [] + for i in range(number): + clones.append(type(walker)(walker.state, split_prob)) + + return clones + + +def keep_merge(walkers: list[Walker], keep_idx: int) -> Walker: + """Merge a set of walkers using the state of one of them. + + Parameters + ---------- + walkers : list of objects implementing the Walker interface + The walkers that will be merged together + keep_idx : int + The index of the walker in the walkers list that will be used + to set the state of the new merged walker. + + Returns + ------- + merged_walker : object implementing the Walker interface """ - def __init__(self, **kwargs): - """Constructor for WalkerState. + weights = [walker.weight for walker in walkers] + # but we add their weight to the new walker + new_weight = sum(weights) + # create a new walker with the keep_walker state + new_walker = type(walkers[0])(walkers[keep_idx].state, new_weight) - All key-word arguments passed in will be set as the key-value - pairs for the state. + return new_walker - """ - self._data = kwargs - def __getitem__(self, key): - return self._data[key] +def merge(walkers: list[Walker]) -> Walker: + """Merge this walker with another keeping the state of one of them + and adding the weights. - def dict(self): - """Return all key-value pairs as a dictionary.""" - return deepcopy(self._data) + The walker that has it's state kept is a random choice weighted by + the walkers weights. + + Parameters + ---------- + walkers : list of objects implementing the Walker interface + The walkers that will be merged together + + Returns + ------- + merged_walker : object implementing the Walker interface + + """ + + weights = [walker.weight for walker in walkers] + # choose a walker according to their weights to keep its state + keep_walker = next(iter(rand.choices(walkers, weights=weights))) + keep_idx = walkers.index(keep_walker) + + # TODO do we need this? + # the others are "squashed" and we lose their state + # squashed_walkers = set(walkers).difference(keep_walker) + + # but we add their weight to the new walker + new_weight = sum(weights) + # create a new walker with the keep_walker state + new_walker = type(walkers[0])(keep_walker.state, new_weight) + + return new_walker, keep_idx diff --git a/tests/unit/test_walker.py b/tests/unit/test_walker.py new file mode 100644 index 00000000..d650382e --- /dev/null +++ b/tests/unit/test_walker.py @@ -0,0 +1,194 @@ + +import math +from wepy.walker import ( + split, + keep_merge, + merge, + Walker, + WalkerState, +) + +class TestWalkerState: + + def test___init__(self): + + WalkerState(a=1, b="hello") + + def test___getitem__(self): + + assert WalkerState(a=1, b="hello")['a'] == 1 + assert WalkerState(a=1, b="hello")['b'] == "hello" + + def test___eq__(self): + + assert WalkerState(a=1, b="hello") == WalkerState(a=1, b="hello") + assert WalkerState(a=1, b="hello") != WalkerState(a=100, b="hello") + + def test_dict(self): + assert WalkerState(a=1, b="hello").dict() == { + "a" : 1, + "b" : "hello", + } + +class TestWalker: + + def test___init__(self): + + state = WalkerState(a=1, b="hello") + walker = Walker( + state=state, + weight=0.1, + ) + + assert walker.weight == 0.1 + assert walker.state is state + + def test___eq__(self): + assert Walker( + state=WalkerState(a=1, b="hello"), + weight=0.1, + ) == Walker( + state=WalkerState(a=1, b="hello"), + weight=0.1, + ) + + assert Walker( + state=WalkerState(a=1, b="hello"), + weight=0.1, + ) != Walker( + state=WalkerState(a=100, b="hello"), + weight=0.1, + ) + + assert Walker( + state=WalkerState(a=1, b="hello"), + weight=0.1, + ) != Walker( + state=WalkerState(a=1, b="hello"), + weight=0.05, + ) + + def test_clone(self): + state = WalkerState(a=1, b="hello") + walker = Walker( + state=state, + weight=0.1, + ) + + clones = walker.clone(1) + assert len(clones) == 2 + + assert clones[0] == Walker( + state=state, + weight=0.05, + ) + assert clones[1] == Walker( + state=state, + weight=0.05, + ) + + + + def test_squash(self): + + walker_a = Walker( + state=WalkerState(a=1), + weight=0.1, + ) + + walker_b = Walker( + state=WalkerState(a=10), + weight=0.1, + ) + + assert walker_a.squash(walker_b) == Walker( + state=WalkerState(a=10), + weight=0.2, + ) + + assert walker_b.squash(walker_a) == Walker( + state=WalkerState(a=1), + weight=0.2, + ) + + + + + def test_merge(self): + walker_a = Walker( + state=WalkerState(a=1), + weight=0.1, + ) + + other_walkers = [ + Walker( + state=WalkerState(a=10), + weight=0.1, + ), + Walker( + state=WalkerState(a=20), + weight=0.1, + ), + ] + + assert math.isclose(walker_a.merge(other_walkers).weight, 0.3) + + + +def test_split(): + + walker = Walker( + state=WalkerState(a=1), + weight=0.1, + ) + + assert split(walker, 2) == [ + Walker( + state=WalkerState(a=1), + weight=0.05, + ), + Walker( + state=WalkerState(a=1), + weight=0.05, + ) + ] + + +def test_keep_merge(): + walkers = [ + Walker( + state=WalkerState(a=10), + weight=0.1, + ), + Walker( + state=WalkerState(a=20), + weight=0.1, + ), + ] + + assert keep_merge(walkers, 0) == Walker( + state=WalkerState(a=10), + weight=0.2, + ) + + assert keep_merge(walkers, 1) == Walker( + state=WalkerState(a=20), + weight=0.2, + ) + +def test_merge(): + walkers = [ + Walker( + state=WalkerState(a=10), + weight=0.1, + ), + Walker( + state=WalkerState(a=20), + weight=0.1, + ), + ] + + merged_walker, keep_idx = merge(walkers) + assert merged_walker.weight == 0.2 + + assert walkers[keep_idx].state == merged_walker.state From feb40cad1cd7e007ffbabeb42de6cb5ab62b8638 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 25 Nov 2025 16:07:08 -0500 Subject: [PATCH 011/143] fixup! add walker types and tests --- src/wepy/walker.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/wepy/walker.py b/src/wepy/walker.py index a8f9930c..807169de 100644 --- a/src/wepy/walker.py +++ b/src/wepy/walker.py @@ -60,7 +60,7 @@ class WalkerState: """ - def __init__(self, **kwargs: dict[str, Any]) -> Self: + def __init__(self, **kwargs: dict[str, Any]) -> None: """Constructor for WalkerState. All key-word arguments passed in will be set as the key-value @@ -72,8 +72,12 @@ def __init__(self, **kwargs: dict[str, Any]) -> Self: def __getitem__(self, key: str) -> Any: return self._data[key] - def __eq__(self, other: Self) -> bool: - return self._data == other._data + def __eq__(self, other: object) -> bool: + + if not isinstance(other, WalkerState): + return False + else: + return self._data == other._data def dict(self) -> dict[str, Any]: """Return all key-value pairs as a dictionary.""" @@ -140,7 +144,7 @@ def squash(self, merge_target: Self) -> Self: new_weight = self.weight + merge_target.weight return type(self)(merge_target.state, new_weight) - def merge(self, other_walkers: list[Self]) -> Self: + def merge(self, other_walkers: list["Walker"]) -> "Walker": """Merge a set of other walkers into this one using the merge function. Parameters @@ -210,7 +214,7 @@ def keep_merge(walkers: list[Walker], keep_idx: int) -> Walker: return new_walker -def merge(walkers: list[Walker]) -> Walker: +def merge(walkers: list[Walker]) -> tuple[Walker, int]: """Merge this walker with another keeping the state of one of them and adding the weights. @@ -219,12 +223,12 @@ def merge(walkers: list[Walker]) -> Walker: Parameters ---------- - walkers : list of objects implementing the Walker interface - The walkers that will be merged together + walkers : The walkers that will be merged together Returns ------- - merged_walker : object implementing the Walker interface + merged_walker : Final merged walker + keep_idx: Index of the walker whose state was retained. """ From 0abc5393ee513aafd587fdd8ea3a080680cac090 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 25 Nov 2025 22:02:46 -0500 Subject: [PATCH 012/143] minor fixups --- src/wepy/runners/runner.py | 41 +++++++++++++++----------- src/wepy/walker.py | 38 +++++++++++++----------- tests/unit/test_runners/test_runner.py | 21 +++++++++++++ tests/unit/test_walker.py | 40 +++++++++++-------------- 4 files changed, 84 insertions(+), 56 deletions(-) create mode 100644 tests/unit/test_runners/test_runner.py diff --git a/src/wepy/runners/runner.py b/src/wepy/runners/runner.py index 23d5e62b..534ca30a 100644 --- a/src/wepy/runners/runner.py +++ b/src/wepy/runners/runner.py @@ -18,13 +18,15 @@ """ # Standard Library -from typing import Protocol +from typing import Protocol, Any +import attrs +from wepy.walker import Walker class Runner(Protocol): """Abstract base class for the Runner interface.""" - def pre_cycle(self, **kwargs): + def pre_cycle(self) -> None: """Perform pre-cycle behavior. run_segment will be called for each walker so this allows you to perform changes of state on a per-cycle basis. @@ -35,13 +37,9 @@ def pre_cycle(self, **kwargs): Key-value pairs to be interpreted by each runner implementation. """ + ... - # by default just pass since subclasses need not implement this - # TODO: this should not be run if it is an abstract class. But it is running - # raise NotImplementedError(f"In {self.__class__.__name__}") - pass - - def post_cycle(self, **kwargs): + def post_cycle(self) -> None: """Perform post-cycle behavior. run_segment will be called for each walker so this allows you to perform changes of state on a per-cycle basis. @@ -56,15 +54,19 @@ def post_cycle(self, **kwargs): # by default just pass since subclasses need not implement this pass - def run_segment(self, walker, segment_length, **kwargs): + def run_segment( + self, + walker: Walker, + segment_length: int | float, + **kwargs: dict[str, Any], + ) -> Walker: """Run dynamics for the walker. Parameters ---------- - walker : object implementing the Walker interface - The walker for which dynamics will be propagated. - segment_length : int or float - The numerical value that specifies how much dynamics are to be run. + walker : The walker for which dynamics will be propagated. + segment_length : The numerical value that specifies how much dynamics are to be run. + Returns ------- @@ -72,16 +74,21 @@ def run_segment(self, walker, segment_length, **kwargs): Walker after dynamics was run, only the state should be modified. """ - - raise NotImplementedError + ... -class NoRunner(Runner): +@attrs.define +class NoRunner: """Stub Runner that just returns the walkers back with the same state. May be useful for testing. """ - def run_segment(self, walker, segment_length, **kwargs): + def run_segment( + self, + walker: Walker, + segment_length: int | float, + **kwargs: dict[str, Any], + ) -> Walker: # documented in superclass return walker diff --git a/src/wepy/walker.py b/src/wepy/walker.py index 807169de..b6e33546 100644 --- a/src/wepy/walker.py +++ b/src/wepy/walker.py @@ -29,6 +29,7 @@ # Standard Library import logging from typing import Protocol, Hashable, Any, Self +from abc import ABC # Standard Library import random as rand @@ -38,13 +39,12 @@ logger = logging.getLogger(__name__) + class WalkerStateProtocol(Protocol): - def __getitem__(self, key: str) -> Any: - ... + def __getitem__(self, key: str) -> Any: ... - def dict(self) -> dict[str, Any]: - ... + def dict(self) -> dict[str, Any]: ... class WalkerState: @@ -83,20 +83,8 @@ def dict(self) -> dict[str, Any]: """Return all key-value pairs as a dictionary.""" return deepcopy(self._data) - -@attrs.define -class Walker: - """Reference implementation of the Walker interface. - A container for: - - - state - - weight - - """ - - state: WalkerStateProtocol - weight: float +class WalkerABC(ABC): def clone(self, number: int = 1) -> list[Self]: """Clone this walker by making a copy with the same state and split @@ -159,6 +147,22 @@ def merge(self, other_walkers: list["Walker"]) -> "Walker": """ return merge([self] + other_walkers)[0] + +@attrs.define +class Walker(WalkerABC): + """Reference implementation of the Walker interface. + + A container for: + + - state + - weight + + """ + + state: WalkerStateProtocol + weight: float + + def split(walker: Walker, number: int = 2) -> list[Walker]: """Split (AKA make multiple clones) of a single walker. diff --git a/tests/unit/test_runners/test_runner.py b/tests/unit/test_runners/test_runner.py new file mode 100644 index 00000000..0f9e3e60 --- /dev/null +++ b/tests/unit/test_runners/test_runner.py @@ -0,0 +1,21 @@ +from wepy.runners.runner import NoRunner +from wepy.walker import Walker, WalkerState + + +class TestNoRunner: + + def test_run_segment(self): + + runner = NoRunner() + + walker = Walker( + state=WalkerState(a=1), + weight=0.1, + ) + assert ( + runner.run_segment( + walker, + 10, + ) + == walker + ) diff --git a/tests/unit/test_walker.py b/tests/unit/test_walker.py index d650382e..c0d570e2 100644 --- a/tests/unit/test_walker.py +++ b/tests/unit/test_walker.py @@ -1,4 +1,3 @@ - import math from wepy.walker import ( split, @@ -8,6 +7,7 @@ WalkerState, ) + class TestWalkerState: def test___init__(self): @@ -16,20 +16,21 @@ def test___init__(self): def test___getitem__(self): - assert WalkerState(a=1, b="hello")['a'] == 1 - assert WalkerState(a=1, b="hello")['b'] == "hello" + assert WalkerState(a=1, b="hello")["a"] == 1 + assert WalkerState(a=1, b="hello")["b"] == "hello" def test___eq__(self): assert WalkerState(a=1, b="hello") == WalkerState(a=1, b="hello") assert WalkerState(a=1, b="hello") != WalkerState(a=100, b="hello") - + def test_dict(self): assert WalkerState(a=1, b="hello").dict() == { - "a" : 1, - "b" : "hello", + "a": 1, + "b": "hello", } + class TestWalker: def test___init__(self): @@ -67,7 +68,7 @@ def test___eq__(self): state=WalkerState(a=1, b="hello"), weight=0.05, ) - + def test_clone(self): state = WalkerState(a=1, b="hello") walker = Walker( @@ -87,8 +88,6 @@ def test_clone(self): weight=0.05, ) - - def test_squash(self): walker_a = Walker( @@ -111,9 +110,6 @@ def test_squash(self): weight=0.2, ) - - - def test_merge(self): walker_a = Walker( state=WalkerState(a=1), @@ -133,7 +129,6 @@ def test_merge(self): assert math.isclose(walker_a.merge(other_walkers).weight, 0.3) - def test_split(): @@ -150,9 +145,9 @@ def test_split(): Walker( state=WalkerState(a=1), weight=0.05, - ) + ), ] - + def test_keep_merge(): walkers = [ @@ -167,15 +162,16 @@ def test_keep_merge(): ] assert keep_merge(walkers, 0) == Walker( - state=WalkerState(a=10), - weight=0.2, - ) + state=WalkerState(a=10), + weight=0.2, + ) assert keep_merge(walkers, 1) == Walker( - state=WalkerState(a=20), - weight=0.2, - ) - + state=WalkerState(a=20), + weight=0.2, + ) + + def test_merge(): walkers = [ Walker( From d7693337432307a205b6ecc093209d1c9c470894 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 25 Nov 2025 22:02:59 -0500 Subject: [PATCH 013/143] add nptyping to deps --- pyproject.toml | 1 + uv.lock | 111 +++++++++++++++---------------------------------- 2 files changed, 35 insertions(+), 77 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 17d4280f..35ea0f8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ dependencies = [ "attrs", "numpy", + "nptyping", "h5py>=3", "networkx", "pandas", diff --git a/uv.lock b/uv.lock index 687b6729..86774d38 100644 --- a/uv.lock +++ b/uv.lock @@ -1710,85 +1710,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, ] +[[package]] +name = "nptyping" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/b7/ffe533358c32506b1708feec0fb04ba0a35a959a94163fff5333671909da/nptyping-2.5.0.tar.gz", hash = "sha256:e3d35b53af967e6fb407c3016ff9abae954d3a0568f7cc13a461084224e8e20a", size = 71623, upload-time = "2023-02-20T21:57:11.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/28/92edc05378175de13a3d4986cee7531853634a22b7e5e21a988fa84fde3f/nptyping-2.5.0-py3-none-any.whl", hash = "sha256:764e51836faae33a7ae2e928af574cfb701355647accadcc89f2ad793630b7c8", size = 37602, upload-time = "2023-02-20T21:57:09.61Z" }, +] + [[package]] name = "numpy" -version = "2.3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, - { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, - { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, - { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, - { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, - { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, - { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, - { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, - { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, - { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, - { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, - { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, - { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, - { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, - { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, - { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, - { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, - { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, - { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, - { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, - { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, - { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, - { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, - { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, - { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, - { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, - { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, - { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, - { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, - { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, - { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, - { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, - { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, - { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, - { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, - { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, - { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, ] [[package]] @@ -3131,6 +3086,7 @@ dependencies = [ { name = "jinja2" }, { name = "multiprocessing-logging" }, { name = "networkx" }, + { name = "nptyping" }, { name = "numpy" }, { name = "pandas" }, { name = "pint" }, @@ -3189,6 +3145,7 @@ requires-dist = [ { name = "mdtraj", marker = "extra == 'md'" }, { name = "multiprocessing-logging" }, { name = "networkx" }, + { name = "nptyping" }, { name = "numpy" }, { name = "openmm", marker = "extra == 'md'" }, { name = "openmm-systems", marker = "extra == 'md'", specifier = "==0.0.0" }, From 24eea4f92aa8a29c92004ce31fabf30f292f9c09 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 25 Nov 2025 22:03:14 -0500 Subject: [PATCH 014/143] refactor runners/openmm and half of tests --- src/wepy/runners/openmm.py | 1748 ++++++++++++------------ tests/unit/test_runners/test_openmm.py | 436 ++++++ 2 files changed, 1305 insertions(+), 879 deletions(-) create mode 100644 tests/unit/test_runners/test_openmm.py diff --git a/src/wepy/runners/openmm.py b/src/wepy/runners/openmm.py index 966f565a..d4fc9b93 100644 --- a/src/wepy/runners/openmm.py +++ b/src/wepy/runners/openmm.py @@ -25,6 +25,7 @@ """ # Standard Library +from typing import Any, Annotated, TypedDict, NotRequired import logging logger = logging.getLogger(__name__) @@ -34,13 +35,20 @@ from warnings import warn # Third Party Library +import attrs import numpy as np +from nptyping import NDArray, Shape, Floating + +try: + import mdtraj +except ModuleNotFoundError: + warn("Module 'mdtraj' not found, those features will not be available.") try: # Third Party Library - import openmm as omm - import openmm.app as omma - import openmm.unit as unit + import openmm + import openmm.app + import openmm.unit except ModuleNotFoundError: raise ModuleNotFoundError( "OpenMM has not been installed, which this runner requires." @@ -49,13 +57,16 @@ # First Party Library from wepy.runners.runner import Runner from wepy.util.util import box_vectors_to_lengths_angles -from wepy.walker import Walker, WalkerState +from wepy.walker import Walker, WalkerState, WalkerABC from wepy.work_mapper.task_mapper import WalkerTaskProcess from wepy.work_mapper.worker import Worker +AtomNDArray = NDArray[Shape["N atoms, 3 dimensions"], Floating] +BoxVectorsNDArray = NDArray[Shape["3, 3"], Floating] + ## Constants -KEYS = ( +KEYS: tuple[str] = ( "positions", "velocities", "forces", @@ -73,7 +84,7 @@ # can pass options for what kind of data to get, this is the default # to get all the data. TODO not really sure what the 'groups' keyword # is for though -GET_STATE_KWARG_DEFAULTS = ( +GET_STATE_KWARG_DEFAULTS: tuple[tuple[str, bool]] = ( ("getPositions", True), ("getVelocities", True), ("getForces", True), @@ -89,7 +100,7 @@ """ -STATE_DATA_TYPE_ENUM_NAMES = { +STATE_DATA_TYPE_ENUM_NAMES: dict[str, str] = { "positions": "Positions", "velocities": "Velocities", "forces": "Forces", @@ -99,28 +110,17 @@ "integrator_parameters": "IntegratorParameters", } -# STATE_DATA_TYPE_ENUM_VALUES = ( -# ("positions", 1), -# ("velocities", 2), -# ("forces", 4), -# ("energy", 8), -# ("parameters", 16), -# ("parameter_derivatives", 32), -# ("integrator_parameters", 64), -# ) -# """Enum values for the state data field flags.""" - -def resolve_state_data_type_enum_values(): +def resolve_state_data_type_enum_values() -> dict[str, int]: enum_values = {} for our_name, enum_name in STATE_DATA_TYPE_ENUM_NAMES.items(): - enum_values[our_name] = getattr(omm.State, enum_name) + enum_values[our_name] = getattr(openmm.State, enum_name) return enum_values # reversed since that is the order we check them in and is a frequent operation -STATE_DATA_TYPE_ENUM_VALUES = list( +STATE_DATA_TYPE_ENUM_VALUES: list[int] = list( sorted( [(k, v) for k, v in resolve_state_data_type_enum_values().items()], key=lambda x: x[1], @@ -129,14 +129,14 @@ def resolve_state_data_type_enum_values(): ) -def get_state_fields_present(sim_state): +def get_state_fields_present(sim_state: openmm.State) -> list[str]: """For a state returns a set of the field data types present in it.""" flag_sum = sim_state.getDataTypes() - flag_fields = [] - flag_values = [] - flag_cum = flag_sum + flag_fields: list[str] = [] + flag_values: list[int] = [] + flag_cum: int = flag_sum for field_name, flag_value in STATE_DATA_TYPE_ENUM_VALUES: if flag_value > flag_cum: continue @@ -160,32 +160,35 @@ def get_state_fields_present(sim_state): # simulation data # TODO: this is never used and we only need the unit names. Its okay -# to use simtk.units here but other runners should use a units sytem +# to use openmm.units here but other runners should use a units sytem # like pint which is easier to install. So we should remove this since # its not used. -# UNITS = (('positions_unit', unit.nanometer), -# ('time_unit', unit.picosecond), -# ('box_vectors_unit', unit.nanometer), -# ('velocities_unit', unit.nanometer/unit.picosecond), -# ('forces_unit', unit.kilojoule / (unit.nanometer * unit.mole)), -# ('box_volume_unit', unit.nanometer), -# ('kinetic_energy_unit', unit.kilojoule / unit.mole), -# ('potential_energy_unit', unit.kilojoule / unit.mole), +# UNITS = (('positions_unit', openmm.unit.nanometer), +# ('time_unit', openmm.unit.picosecond), +# ('box_vectors_unit', openmm.unit.nanometer), +# ('velocities_unit', openmm.unit.nanometer/openmm.unit.picosecond), +# ('forces_unit', openmm.unit.kilojoule / (openmm.unit.nanometer * openmm.unit.mole)), +# ('box_volume_unit', openmm.unit.nanometer), +# ('kinetic_energy_unit', openmm.unit.kilojoule / openmm.unit.mole), +# ('potential_energy_unit', openmm.unit.kilojoule / openmm.unit.mole), # ) -# """Mapping of units identifiers to the corresponding simtk.units Unit objects.""" +# """Mapping of units identifiers to the corresponding openmm.units Unit objects.""" # the names of the units from the units objects above. This is used # for saving them to files -UNIT_NAMES = ( - ("positions_unit", unit.nanometer.get_name()), - ("time_unit", unit.picosecond.get_name()), - ("box_vectors_unit", unit.nanometer.get_name()), - ("velocities_unit", (unit.nanometer / unit.picosecond).get_name()), - ("forces_unit", (unit.kilojoule / (unit.nanometer * unit.mole)).get_name()), - ("box_volume_unit", unit.nanometer.get_name()), - ("kinetic_energy_unit", (unit.kilojoule / unit.mole).get_name()), - ("potential_energy_unit", (unit.kilojoule / unit.mole).get_name()), +UNIT_NAMES: tuple[tuple[str, str]] = ( + ("positions_unit", openmm.unit.nanometer.get_name()), + ("time_unit", openmm.unit.picosecond.get_name()), + ("box_vectors_unit", openmm.unit.nanometer.get_name()), + ("velocities_unit", (openmm.unit.nanometer / openmm.unit.picosecond).get_name()), + ( + "forces_unit", + (openmm.unit.kilojoule / (openmm.unit.nanometer * openmm.unit.mole)).get_name(), + ), + ("box_volume_unit", openmm.unit.nanometer.get_name()), + ("kinetic_energy_unit", (openmm.unit.kilojoule / openmm.unit.mole).get_name()), + ("potential_energy_unit", (openmm.unit.kilojoule / openmm.unit.mole).get_name()), ) """Mapping of unit identifier strings to the serialized string spec of the unit.""" @@ -197,1048 +200,1053 @@ def get_state_fields_present(sim_state): # RAND_SEED_RANGE_MAX = 1000000 -# the runner for the simulation which runs the actual dynamics -class OpenMMRunner(Runner): - """Runner for OpenMM simulations.""" +class OpenMMStateDict(TypedDict, total=False): + positions: AtomNDArray + velocities: AtomNDArray + forces: AtomNDArray + kinetic_energy: float + potential_energy: float + time: float + box_vectors: BoxVectorsNDArray + box_volume: float + # TODO: parameters - def __init__( - self, - system, - topology, - integrator, - platform=None, - platform_kwargs=None, - enforce_box=False, - get_state_kwargs=None, - ): - """Constructor for OpenMMRunner. - Parameters - ---------- - system : simtk.openmm.System object - The system (forcefields) for the simulation. +class OpenMMState(WalkerState): + """Walker state that wraps an openmm.State object. - topology : simtk.openmm.app.Topology object - The topology for you system. + The keys for which values in the state are available are given by + the KEYS module constant (accessible through the class constant of + the same name as well). - integrator : subclass simtk.openmm.Integrator object - Integrator for propagating dynamics. + Additional fields can be added to these states through passing + extra kwargs to the constructor. These will be automatically given + a suffix of "_OTHER" to avoid name clashes. - platform : str - The specification for the default computational platform - to use. Platform can also be set when run_segment is - called. If None uses OpenMM default platform, see OpenMM - documentation for all value but typical ones are: - Reference, CUDA, OpenCL. If value is None the automatic - platform determining mechanism in OpenMM will be used. + """ - platform_kwargs : dict of str : bool, optional - key-values to set for a platform with - platform.setPropertyDefaultValue as the default for this - runner. + KEYS: tuple[str] = KEYS + """The provided attribute keys for the state.""" - enforce_box : bool - Calls 'context.getState' with 'enforcePeriodicBox' if True. - (Default value = False) + OTHER_KEY_TEMPLATE: str = "{}_OTHER" + """String formatting template for attributes not set in KEYS.""" - get_state_kwargs : dict of str : bool, optional - key-values to set for getting the state from the OpenMM context. - keys not included will use the values in GET_STATE_KWARG_DEFAULTS. - Will override the enforce_box flag. + def __init__( + self, + sim_state: openmm.State, + **kwargs: dict[str, Any], + ) -> None: + """Constructor for OpenMMState. - Warnings - -------- - Regarding the enforce_box option. + Parameters + ---------- + state : openmm.State object + The simulation state retrieved from the simulation constant. - When retrieving states from an OpenMM simulation Context, you - have the option to enforce periodic boundary conditions in the - resulting atomic positions in a topology aware way that - doesn't break bonds through boundaries. This is convenient for - post-processing as this can be a complex task and is not - readily exposed in the OpenMM API as a standalone function. + kwargs : optional - However, in some types of simulations the periodic box vectors - are ignored (such as implicit solvent ones) despite there - being no option to not have periodic boundaries in the context - itself. Likely if you are running one of these kinds of - simulations you will not pay attention to the box vectors at - all and the random defaults that exist will be very wrong but - this incorrectness will not show in a non-wepy simulation with - openmm unless you are handling the context states - yourself. Then when you run in wepy the default of True to - enforce the boxes will be applied and confusingly wrong - answers will result that are difficult to find root cause of. + Additional attributes to set for the state. Will add the + "_OTHER" suffix to the keys """ - if platform is not None: - assert isinstance( - platform, str - ), f"platform should be a string, not {type(platform)}" + # save the simulation state + self._sim_state = sim_state - # we save the different components. However, if we are to make - # this runner picklable we have to convert the SWIG objects to - # a picklable form - self.system = system - self.integrator = integrator + # probe which data fields it has + self._sim_state_fields_present = get_state_fields_present(self.sim_state) - # these are not SWIG objects - self.topology = topology - self.platform_name = platform - self.platform_kwargs = platform_kwargs + # save additional data if given + self._data = {} + for key, value in kwargs.items(): + # if the key is already in the sim_state keys we need to + # modify it and raise a warning + if key in self.KEYS: + warn( + "Key {} in kwargs is already taken by this class, renaming to {}".format( + self.OTHER_KEY_TEMPLATE + ).format( + key + ) + ) - self.enforce_box = enforce_box + # make a new key + new_key = self.OTHER_KEY_TEMPLATE.format(key) - self.getState_kwargs = {} - if get_state_kwargs is not None: - for k in get_state_kwargs: - self.getState_kwargs[k] = get_state_kwargs[k] + # set it in the data + self._data[new_key] = value - # override enforce_box option if specified in get_state_kwargs - if "enforce_box" in get_state_kwargs: - self.enforce_box = get_state_kwargs["enforce_box"] + # otherwise just set it + else: + self._data[key] = value + + @property + def sim_state(self) -> openmm.State: + """The underlying openmm.State object this is wrapping.""" + return self._sim_state + + def __getitem__(self, key: str) -> Any: + # if this was a key for data not mapped from the OpenMM.State + # object we use the _data attribute + if (key not in self.KEYS) and ( + (not key.startswith("parameters")) + and (not key.startswith("parameter_derivatives")) + ): + return self._data[key] + # otherwise we have to specifically get the correct data and + # process it into an array from the OpenMM.State else: - self.getState_kwargs = dict(GET_STATE_KWARG_DEFAULTS) + if key == "positions": + return self.positions_values() + elif key == "velocities": + return self.velocities_values() + elif key == "forces": + return self.forces_values() + elif key == "kinetic_energy": + return self.kinetic_energy_value() + elif key == "potential_energy": + return self.potential_energy_value() + elif key == "time": + return self.time_value() + elif key == "box_vectors": + return self.box_vectors_values() + elif key == "box_volume": + return self.box_volume_value() - self._cycle_platform = None - self._cycle_platform_kwargs = None + # handle the parameters differently since they are dictionaries of values + elif key.startswith("parameters"): + parameters_dict = self.parameters_values() + if parameters_dict is None: + return None + else: + # TODO: this was an attempt at a general way to do + # this but it doesn't work and I only ever need + # one nested level, so for now we just implement it that way + # return self._get_nested_attr_from_compound_key(key, parameters_dict) - # for special monitoring purposes to get split times to debug - # performance - self._last_cycle_segments_split_times = [] + param_key = key.split("/")[-1] + return parameters_dict[param_key] - def pre_cycle(self, platform=None, platform_kwargs=None, **kwargs): - # choose to use the platform spec in this function call or to - # use the default one saved in the runner + elif key.startswith("parameter_derivatives"): + pd_dict = self.parameter_derivatives_values() + if pd_dict is None: + return None + else: + return self._get_nested_attr_from_compound_key(key, pd_dict) - # if the platform is given locally use this one - if platform is not None: - logger.info( - f"Setting the platform ({platform}) in the 'pre_cycle' OpenMM Runner call" - f"with platform kwargs: {platform_kwargs}" - ) - # set the platform and kwargs for this cycle - self._cycle_platform = platform - self._cycle_platform_kwargs = platform_kwargs + ## Array properties - # otherwise we just don't set this and let resolution of - # platform happen at run segment. + # Positions + @property + def positions(self) -> Annotated[ + openmm.unit.Quantity | None, + AtomNDArray, + ]: + """The positions of the state as a numpy array openmm.unit.Quantity object.""" - super().pre_cycle(**kwargs) + if "positions" in self._sim_state_fields_present: + return self.sim_state.getPositions(asNumpy=True) + else: + return None - # each segment split times will get appended to this - self._last_cycle_segments_split_times = [] + @property + def positions_unit(self) -> openmm.unit.Unit: + """The units (as a openmm.unit.Unit object) the positions are in.""" + return self.positions.unit - def post_cycle(self, **kwargs): - super().post_cycle(**kwargs) + def positions_values(self) -> AtomNDArray | None: + """The positions of the state as a numpy array in the positions_unit + openmm.unit.Unit. This is what is returned by the __getitem__ + accessor. - # remove the platform and kwargs for this cycle - self._cycle_platform = None - self._cycle_platform_kwargs = None + """ + return self.positions.value_in_unit(self.positions_unit) - def _resolve_platform( - self, - platform, - platform_kwargs, - ): - # resolve which platform to use + # Velocities + @property + def velocities(self) -> Annotated[openmm.unit.Quantity | None, AtomNDArray]: + """The velocities of the state as a numpy array openmm.unit.Quantity object.""" - # force usage of environmental one - if platform is Ellipsis: - platform_name = None - platform_kwargs = None + if "velocities" in self._sim_state_fields_present: + return self.sim_state.getVelocities(asNumpy=True) + else: + return None - # use the runtime given one - elif platform is not None: - platform_name = platform - platform_kwargs = platform_kwargs + @property + def velocities_unit(self) -> openmm.unit.Unit: + """The units (as a openmm.unit.Unit object) the velocities are in.""" + return self.velocities.unit - # if the pre_cycle configured platform is set use this over - # the default - elif self._cycle_platform is not None: - platform_name = self._cycle_platform - platform_kwargs = self._cycle_platform_kwargs + def velocities_values(self) -> AtomNDArray | None: + """The velocities of the state as a numpy array in the velocities_unit + openmm.unit.Unit. This is what is returned by the __getitem__ + accessor. - # use the default one - elif self.platform_name is not None: - platform_name = self.platform_name - platform_kwargs = self.platform_kwargs + """ - # if the default is not set fall back to the environmental one + velocities = self.velocities + if velocities is None: + return None else: - platform_name = None - platform_kwargs = None + return self.velocities.value_in_unit(self.velocities_unit) - return ( - platform_name, - platform_kwargs, - ) + # Forces + @property + def forces(self) -> Annotated[ + openmm.unit.Quantity | None, + AtomNDArray, + ]: + """The forces of the state as a numpy array openmm.unit.Quantity object.""" - def run_segment( - self, - walker, - segment_length, - getState_kwargs=None, - platform=None, - platform_kwargs=None, - **kwargs, - ): - """Run dynamics for the walker. + if "forces" in self._sim_state_fields_present: + return self.sim_state.getForces(asNumpy=True) + else: + return None - Parameters - ---------- - walker : object implementing the Walker interface - The walker for which dynamics will be propagated. + @property + def forces_unit(self) -> openmm.unit.Unit: + """The units (as a openmm.unit.Unit object) the forces are in.""" + return self.forces.unit - segment_length : int or float - The numerical value that specifies how much dynamics are to be run. + def forces_values(self) -> AtomNDArray | None: + """The forces of the state as a numpy array in the forces_unit + openmm.unit.Unit. This is what is returned by the __getitem__ + accessor. - getState_kwargs : dict of str : bool, optional - Specify the key-word arguments to pass to - simulation.context.getState when getting simulation - states. If None defaults object values. + """ - platform : str or None or Ellipsis - The specification for the computational platform to - use. If None will use the default for the runner and - ignore platform_kwargs. If Ellipsis forces the use of the - OpenMM default or environmentally defined platform. See - OpenMM documentation for all value but typical ones are: - Reference, CUDA, OpenCL. If value is None the automatic - platform determining mechanism in OpenMM will be used. + forces = self.forces + if forces is None: + return None + else: + return self.forces.value_in_unit(self.forces_unit) - platform_kwargs : dict of str : bool, optional - key-values to set for a platform with - platform.setPropertyDefaultValue for this segment only. + # Box Vectors + @property + def box_vectors(self) -> Annotated[ + openmm.unit.Quantity | None, + BoxVectorsNDArray, + ]: + """The box vectors of the state as a numpy array openmm.unit.Quantity object.""" + try: + return self.sim_state.getPeriodicBoxVectors(asNumpy=True) + except: + warn( + "Unknown exception handled from `self.sim_state.getPeriodicBoxVectors()`, " + "this is probably because this attribute is not in the State." + ) + return None + @property + def box_vectors_unit(self) -> openmm.unit.Unit: + """The units (as a openmm.unit.Unit object) the box vectors are in.""" + return self.box_vectors.unit - Returns - ------- - new_walker : object implementing the Walker interface - Walker after dynamics was run, only the state should be modified. + def box_vectors_values(self) -> AtomNDArray | None: + """The box vectors of the state as a numpy array in the + box_vectors_unit openmm.unit.Unit. This is what is returned by + the __getitem__ accessor. """ - run_segment_start = time.time() - - # set the kwargs that will be passed to getState - tmp_getState_kwargs = getState_kwargs + box_vectors = self.box_vectors + if box_vectors is None: + return None + else: + return self.box_vectors.value_in_unit(self.box_vectors_unit) - logger.info(f"Default 'getState_kwargs' in runner: {self.getState_kwargs}") + ## non-array properties - logger.info(f"'getState_kwargs' passed to 'run_segment' : {getState_kwargs}") + # Kinetic Energy + @property + def kinetic_energy(self) -> Annotated[ + openmm.unit.Quantity | None, + AtomNDArray, + ]: + """The kinetic energy of the state as a numpy array openmm.unit.Quantity object.""" + try: + return self.sim_state.getKineticEnergy() + except: + warn( + "Unknown exception handled from `self.sim_state.getKineticEnergy()`, " + "this is probably because this attribute is not in the State." + ) + return None - # start with the object value - getState_kwargs = copy(self.getState_kwargs) - if tmp_getState_kwargs is not None: - getState_kwargs.update(tmp_getState_kwargs) + @property + def kinetic_energy_unit(self) -> openmm.unit.Unit: + """The units (as a openmm.unit.Unit object) the kinetic energy is in.""" + return self.kinetic_energy.unit - logger.info( - "After resolving 'getState_kwargs' that will be used are: " - f"{getState_kwargs}" - ) + def kinetic_energy_value(self) -> AtomNDArray | None: + """The kinetic energy of the state as a numpy array in the kinetic_energy_unit + openmm.unit.Unit. This is what is returned by the __getitem__ + accessor. - gen_sim_start = time.time() + """ - # make a copy of the integrator for this particular segment - new_integrator = copy(self.integrator) - # force setting of random seed to 0, which is a special - # value that forces the integrator to choose another - # random number - new_integrator.setRandomNumberSeed(0) + kinetic_energy = self.kinetic_energy + if kinetic_energy is None: + return None + else: + return np.array( + [self.kinetic_energy.value_in_unit(self.kinetic_energy_unit)] + ) - ## Platform + # Potential Energy + @property + def potential_energy(self) -> Annotated[ + openmm.unit.Quantity | None, + AtomNDArray, + ]: + """The potential energy of the state as a numpy array openmm.unit.Quantity object.""" + try: + return self.sim_state.getPotentialEnergy() + except: + warn( + "Unknown exception handled from `self.sim_state.getPotentialEnergy()`, " + "this is probably because this attribute is not in the State." + ) + return None - logger.info(f"Default 'platform' in runner: {self.platform_name}") + @property + def potential_energy_unit(self) -> openmm.unit.Unit: + """The units (as a openmm.unit.Unit object) the potential energy is in.""" + return self.potential_energy.unit - logger.info(f"pre_cycle set 'platform' in runner: {self._cycle_platform}") + def potential_energy_value(self) -> AtomNDArray | None: + """The potential energy of the state as a numpy array in the potential_energy_unit + openmm.unit.Unit. This is what is returned by the __getitem__ + accessor. - logger.info(f"'platform' passed to 'run_segment' : {platform}") + """ - logger.info(f"Default 'platform_kwargs' in runner: {self.platform_kwargs}") + potential_energy = self.potential_energy + if potential_energy is None: + return None + else: + return np.array( + [self.potential_energy.value_in_unit(self.potential_energy_unit)] + ) - logger.info( - f"pre_cycle set 'platform_kwargs' in runner: {self._cycle_platform_kwargs}" - ) + # Time + @property + def time(self) -> openmm.unit.Quantity | None: + """The time of the state as a numpy array openmm.unit.Quantity object.""" + try: + return self.sim_state.getTime() + except: + warn( + "Unknown exception handled from `self.sim_state.getTime()`, " + "this is probably because this attribute is not in the State." + ) + return None - logger.info(f"'platform_kwargs' passed to 'run_segment' : {platform_kwargs}") + @property + def time_unit(self) -> openmm.unit.Unit: + """The units (as a openmm.unit.Unit object) the time is in.""" + return self.time.unit - platform_name, platform_kwargs = self._resolve_platform( - platform, platform_kwargs - ) + def time_value(self) -> AtomNDArray | None: + """The time of the state as a numpy array in the time_unit + openmm.unit.Unit. This is what is returned by the __getitem__ + accessor. - logger.info(f"Resolved 'platform' : {platform_name}") + """ - logger.info(f"Resolved 'platform_kwargs' : {platform_kwargs}") + time = self.time + if time is None: + return None + else: + return np.array([self.time.value_in_unit(self.time_unit)]) - # create simulation object + # Box Volume + @property + def box_volume(self) -> openmm.unit.Quantity | None: + """The box volume of the state as a numpy array openmm.unit.Quantity object.""" + try: + return self.sim_state.getPeriodicBoxVolume() + except: + warn( + "Unknown exception handled from `self.sim_state.getPeriodicBoxVolume()`, " + "this is probably because this attribute is not in the State." + ) + return None - ## create the platform and customize + @property + def box_volume_unit(self) -> openmm.unit.Unit: + """The units (as a openmm.unit.Unit object) the box volume is in.""" + return self.box_volume.unit - # if a platform was given we use it to make a Simulation object - if platform_name is not None: - logger.info("Using platform configured in code.") + def box_volume_value(self) -> NDArray[Shape["1"], Floating] | None: + """The box volume of the state as a numpy array in the box_volume_unit + openmm.unit.Unit. This is what is returned by the __getitem__ + accessor. - # get the platform by its name to use - platform = omm.Platform.getPlatformByName(platform_name) - logger.info(f"Platform object created: {platform}") + """ - if platform_kwargs is None: - platform_kwargs = {} + box_volume = self.box_volume + if box_volume is None: + return None + else: + return np.array([self.box_volume.value_in_unit(self.box_volume_unit)]) - # set properties from the kwargs if they apply to the platform - for key, value in platform_kwargs.items(): - if key in platform.getPropertyNames(): - logger.info(f"Setting platform property: {key} : {value}") - platform.setPropertyDefaultValue(key, value) + ## Dictionary properties + ## Unitless - else: - warn( - f"Platform kwargs given ({key} : {value}) " - f"but is not valid for this platform ({platform_name})" - ) + # Parameters + @property + def parameters(self) -> dict[str, openmm.unit.Quantity] | None: + """The parameters of the state as a dictionary mapping the names of + the parameters to their values which are numpy array + openmm.unit.Quantity objects. - # make a new simulation object - simulation = omma.Simulation( - self.topology, self.system, new_integrator, platform - ) + """ - # otherwise just use the default or environmentally defined one + if "parameters" in self._sim_state_fields_present: + return self.sim_state.getParameters() else: - logger.info("Using environmental platform.") - simulation = omma.Simulation(self.topology, self.system, new_integrator) + return None - # set the state to the context from the walker - simulation.context.setState(walker.state.sim_state) + @property + def parameters_unit(self) -> dict[str, openmm.unit.Unit]: + """The units for each parameter as a dictionary mapping parameter + names to their corresponding unit as a openmm.unit.Unit + object. - gen_sim_end = time.time() - gen_sim_time = gen_sim_end - gen_sim_start + """ + param_units = {key: None for key, val in self.parameters.items()} + return param_units - logger.info("Time to generate the system: {}".format(gen_sim_time)) + def parameters_values(self) -> dict[str, NDArray] | None: + """The parameters of the state as a dictionary mapping the name of the + parameter to a numpy array in the unit for the parameter of the + same name in the parameters_unit corresponding + openmm.unit.Unit object. This is what is returned by the + __getitem__ accessor using the compound key syntax with the + prefix 'parameters', e.g. state['parameter/paramA'] for the + parameter 'paramA'. - # actually run the simulation + """ - steps_start = time.time() + if self.parameters is None: + return None - # Run the simulation segment for the number of time steps - simulation.step(segment_length) + param_arrs = {key: np.array(val) for key, val in self.parameters.items()} - steps_end = time.time() - steps_time = steps_end - steps_start + # return None if there is nothing in this + if len(param_arrs) == 0: + return None + else: + return param_arrs - logger.info("Time to run {} sim steps: {}".format(segment_length, steps_time)) + # Parameter Derivatives + @property + def parameter_derivatives(self) -> dict[str, openmm.unit.Quantity] | None: + """The parameter derivatives of the state as a dictionary mapping the + names of the parameters to their values which are numpy array + openmm.unit.Quantity objects. - get_state_start = time.time() + """ - get_state_end = time.time() - get_state_time = get_state_end - get_state_start - logger.info("Getting context state time: {}".format(get_state_time)) + if "parameter_derivatives" in self._sim_state_fields_present: + return self.sim_state.getEnergyParameterDerivatives() + else: + return None - # generate the new state/walker - new_state = self.generate_state( - simulation, segment_length, walker, getState_kwargs - ) + @property + def parameter_derivatives_unit(self) -> dict[str, openmm.unit.Unit]: + """The units for each parameter derivative as a dictionary mapping + parameter names to their corresponding unit as a + openmm.unit.Unit object. - # create a new walker for this - new_walker = OpenMMWalker(new_state, walker.weight) + """ - run_segment_end = time.time() - run_segment_time = run_segment_end - run_segment_start - logger.info("Total internal run_segment time: {}".format(run_segment_time)) + param_units = {key: None for key, val in self.parameter_derivatives.items()} + return param_units - segment_split_times = { - "gen_sim_time": gen_sim_time, - "steps_time": steps_time, - "get_state_time": get_state_time, - "run_segment_time": run_segment_time, - } + def parameter_derivatives_values(self) -> dict[str, NDArray] | None: + """The parameter derivatives of the state as a dictionary mapping the + name of the parameter to a numpy array in the unit for the + parameter of the same name in the parameters_unit + corresponding openmm.unit.Unit object. This is what is + returned by the __getitem__ accessor using the compound key + syntax with the prefix 'parameter_derivatives', + e.g. state['parameter_derivatives/paramA'] for the parameter + 'paramA'. - self._last_cycle_segments_split_times.append(segment_split_times) + """ - return new_walker + if self.parameter_derivatives is None: + return None - def generate_state( - self, simulation, segment_length, starting_walker, getState_kwargs - ): - """Method for generating a wepy compliant state from an OpenMM - simulation object and data about the last segment of dynamics run. + param_arrs = { + key: np.array(val) for key, val in self.parameter_derivatives.items() + } - Parameters - ---------- - simulation : simtk.openmm.app.Simulation object - A complete simulation object from which the state will be extracted. + # return None if there is nothing in this + if len(param_arrs) == 0: + return None + else: + return param_arrs - segment_length : int - The number of integration steps run in a segment of simulation. + # for the dict attributes we need to transform the keys for making + # a proper state where all __getitem__ things are arrays + def _dict_attr_to_compound_key_dict( + self, + root_key: str, + attr_dict: dict[str:Any], + ) -> dict[str, Any]: + """Transform a dictionary of values within the compound key 'root_key' + to a dictionary mapping compound keys to values. - starting_walker : wepy.walker.Walker subclass object - The walker that was the beginning of this segment of simyulation. + For example give the root_key 'parameters' and the parameters + dictionary {'paramA' : 1.234} returns {'parameters/paramA' : 1.234}. - getState_kwargs : dict of str : bool - Specify the key-word arguments to pass to - simulation.context.getState when getting simulation - states. + Parameters + ---------- + root_key : str + The compound key prefix + attr_dict : dict of str : value + The dictionary with simple keys within the root key namespace. Returns ------- - new_state : wepy.runners.openmm.OpenMMState object - A new state from the simulation state. - - This method is meant to be called from within the - `run_segment` method during a simulation. It can be customized - in subclasses to allow for the addition of custom attributes - for a state, in addition to the base ones implemented in the - interface to the openmm simulation state in OpenMMState. - - The extra arguments to this function are data that would allow - for the calculation of integral values over the duration of - the segment, such as time elapsed and differences from the - starting state. + compound_key_dict : dict of str : value + The dictionary with the compound keys. """ - # save the state of the system with all possible values - new_sim_state = simulation.context.getState(**getState_kwargs) + key_template = "{}/{}" + cmpd_key_d = {} + for key, value in attr_dict.items(): + new_key = key_template.format(root_key, key) + # if this is a proper feature + if type(value) == np.ndarray: + cmpd_key_d[new_key] = value + elif hasattr(value, "__getitem__"): + cmpd_key_d.update(self._dict_attr_to_compound_key_dict(new_key, value)) + else: + raise TypeError("Unsupported attribute type") - # make an OpenMMState wrapper with this - new_state = OpenMMState(new_sim_state) + return cmpd_key_d - return new_state + def _get_nested_attr_from_compound_key( + self, + compound_key: str, + compound_feat_dict: dict[str, Any], + ) -> Any: + """Get arbitrarily deeply nested compound keys from the full + dictionary tree. + Parameters + ---------- + compound_key : str + Compound key separated by '/' characters -class OpenMMState(WalkerState): - """Walker state that wraps an simtk.openmm.State object. + compound_feat_dict : dict + Dictionary of arbitrary depth - The keys for which values in the state are available are given by - the KEYS module constant (accessible through the class constant of - the same name as well). + Returns + ------- + value + Value requested by the key. - Additional fields can be added to these states through passing - extra kwargs to the constructor. These will be automatically given - a suffix of "_OTHER" to avoid name clashes. + """ - """ + key_components = compound_key.split("/") - KEYS = KEYS - """The provided attribute keys for the state.""" + # if there is only one component of the key then it is not + # really compound, we won't complain just return the + # "dictionary" if it is not actually a dict like + if not hasattr(compound_feat_dict, "__getitem__"): + raise TypeError("Must provide a dict-like with the compound key") - OTHER_KEY_TEMPLATE = "{}_OTHER" - """String formatting template for attributes not set in KEYS.""" + value = compound_feat_dict[key_components[0]] - def __init__(self, sim_state, **kwargs): - """Constructor for OpenMMState. + # if the value itself is compound recursively fetch the value + if hasattr(value, "__getitem__") and len(key_components[1:]) > 0: + subgroup_key = "/".join(key_components[1:]) - Parameters - ---------- - state : simtk.openmm.State object - The simulation state retrieved from the simulation constant. + return self._get_nested_attr_from_compound_key(subgroup_key, value) - kwargs : optional + elif hasattr(value, "__getitem__") and len(key_components[1:]) < 1: + raise ValueError("Key does not reference a leaf node of attribute") - Additional attributes to set for the state. Will add the - "_OTHER" suffix to the keys + # otherwise we have the right key so return the object + else: + return value + def parameters_features(self) -> dict[str, Any] | None: + """Returns a dictionary of the parameters with their appropriate + compound keys. This can be used for placing them in the same namespace + as the rest of the attributes. """ - # save the simulation state - self._sim_state = sim_state + parameters = self.parameters_values() + if parameters is None: + return None + else: + return self._dict_attr_to_compound_key_dict("parameters", parameters) - # probe which data fields it has - self._sim_state_fields_present = get_state_fields_present(self.sim_state) + def parameter_derivatives_features(self) -> dict[str, Any] | None: + """Returns a dictionary of the parameter derivatives with their appropriate + compound keys. This can be used for placing them in the same namespace + as the rest of the attributes. + """ - # save additional data if given - self._data = {} - for key, value in kwargs.items(): - # if the key is already in the sim_state keys we need to - # modify it and raise a warning - if key in self.KEYS: - warn( - "Key {} in kwargs is already taken by this class, renaming to {}".format( - self.OTHER_KEY_TEMPLATE - ).format( - key - ) - ) + parameter_derivatives = self.parameter_derivatives_values() + if parameter_derivatives is None: + return None + else: + return self._dict_attr_to_compound_key_dict( + "parameter_derivatives", parameter_derivatives + ) - # make a new key - new_key = self.OTHER_KEY_TEMPLATE.format(key) + def omm_state_dict(self) -> OpenMMStateDict: + """Return a dictionary with all of the default keys from the wrapped + openmm.State object + """ - # set it in the data - self._data[new_key] = value + feature_d = { + "positions": self.positions_values(), + "velocities": self.velocities_values(), + "forces": self.forces_values(), + "kinetic_energy": self.kinetic_energy_value(), + "potential_energy": self.potential_energy_value(), + "time": self.time_value(), + "box_vectors": self.box_vectors_values(), + "box_volume": self.box_volume_value(), + } - # otherwise just set it - else: - self._data[key] = value + params = self.parameters_features() + if params is not None: + feature_d.update(params) - @property - def sim_state(self): - """The underlying simtk.openmm.State object this is wrapping.""" - return self._sim_state + param_derivs = self.parameter_derivatives_features() + if param_derivs is not None: + feature_d.update(param_derivs) - def __getitem__(self, key): - # if this was a key for data not mapped from the OpenMM.State - # object we use the _data attribute - if (key not in self.KEYS) and ( - (not key.startswith("parameters")) - and (not key.startswith("parameter_derivatives")) - ): - return self._data[key] + return feature_d - # otherwise we have to specifically get the correct data and - # process it into an array from the OpenMM.State - else: - if key == "positions": - return self.positions_values() - elif key == "velocities": - return self.velocities_values() - elif key == "forces": - return self.forces_values() - elif key == "kinetic_energy": - return self.kinetic_energy_value() - elif key == "potential_energy": - return self.potential_energy_value() - elif key == "time": - return self.time_value() - elif key == "box_vectors": - return self.box_vectors_values() - elif key == "box_volume": - return self.box_volume_value() + def dict(self) -> dict[str, Any]: + # documented in superclass - # handle the parameters differently since they are dictionaries of values - elif key.startswith("parameters"): - parameters_dict = self.parameters_values() - if parameters_dict is None: - return None - else: - # TODO: this was an attempt at a general way to do - # this but it doesn't work and I only ever need - # one nested level, so for now we just implement it that way - # return self._get_nested_attr_from_compound_key(key, parameters_dict) + d = {} + for key, value in self._data.items(): + d[key] = value + for key, value in self.omm_state_dict().items(): + d[key] = value + return d - param_key = key.split("/")[-1] - return parameters_dict[param_key] + def to_mdtraj(self, topology: mdtraj.Topology) -> mdtraj.Trajectory: + """Returns an mdtraj.Trajectory object from this walker's state. - elif key.startswith("parameter_derivatives"): - pd_dict = self.parameter_derivatives_values() - if pd_dict is None: - return None - else: - return self._get_nested_attr_from_compound_key(key, pd_dict) + Parameters + ---------- + topology : mdtraj.Topology object + Topology for the state. - ## Array properties + Returns + ------- + state_traj : mdtraj.Trajectory object - # Positions - @property - def positions(self): - """The positions of the state as a numpy array simtk.units.Quantity object.""" + """ - if "positions" in self._sim_state_fields_present: - return self.sim_state.getPositions(asNumpy=True) - else: - return None + # resize the time to a 1D vector + unitcell_lengths, unitcell_angles = box_vectors_to_lengths_angles( + self.box_vectors + ) + return mdj.Trajectory( + np.array([self.positions_values()]), + unitcell_lengths=[unitcell_lengths], + unitcell_angles=[unitcell_angles], + topology=topology, + ) - @property - def positions_unit(self): - """The units (as a simtk.units.Unit object) the positions are in.""" - return self.positions.unit - def positions_values(self): - """The positions of the state as a numpy array in the positions_unit - simtk.units.Unit. This is what is returned by the __getitem__ - accessor. +@attrs.define +class OpenMMWalker(WalkerABC): - """ - return self.positions.value_in_unit(self.positions_unit) + state: OpenMMState + weight: float - # Velocities - @property - def velocities(self): - """The velocities of the state as a numpy array simtk.units.Quantity object.""" - if "velocities" in self._sim_state_fields_present: - return self.sim_state.getVelocities(asNumpy=True) - else: - return None +PlatformKwargs = dict[str, str] - @property - def velocities_unit(self): - """The units (as a simtk.units.Unit object) the velocities are in.""" - return self.velocities.unit - def velocities_values(self): - """The velocities of the state as a numpy array in the velocities_unit - simtk.units.Unit. This is what is returned by the __getitem__ - accessor. +# the runner for the simulation which runs the actual dynamics +class OpenMMRunner(Runner): + """Runner for OpenMM simulations.""" - """ + system: openmm.System + topology: openmm.app.Topology + integrator: openmm.Integrator + platform_name: str + platform_kwargs: PlatformKwargs + enforce_box: bool + getState_kwargs: dict[str, bool] + # _cycle_platform: + # _cycle_platform_kwargs: + # _last_cycle_segments_split_times: list[float] - velocities = self.velocities - if velocities is None: - return None - else: - return self.velocities.value_in_unit(self.velocities_unit) - - # Forces - @property - def forces(self): - """The forces of the state as a numpy array simtk.units.Quantity object.""" - - if "forces" in self._sim_state_fields_present: - return self.sim_state.getForces(asNumpy=True) - else: - return None - - @property - def forces_unit(self): - """The units (as a simtk.units.Unit object) the forces are in.""" - return self.forces.unit - - def forces_values(self): - """The forces of the state as a numpy array in the forces_unit - simtk.units.Unit. This is what is returned by the __getitem__ - accessor. - - """ + def __init__( + self, + system: openmm.System, + topology: openmm.app.Topology, + integrator: openmm.Integrator, + platform: str | None = None, + platform_kwargs: PlatformKwargs | None = None, + enforce_box: bool = False, + get_state_kwargs: dict[str, bool] | None = None, + ) -> None: + """Constructor for OpenMMRunner. - forces = self.forces - if forces is None: - return None - else: - return self.forces.value_in_unit(self.forces_unit) + Parameters + ---------- + system : + The system (forcefields) for the simulation. - # Box Vectors - @property - def box_vectors(self): - """The box vectors of the state as a numpy array simtk.units.Quantity object.""" - try: - return self.sim_state.getPeriodicBoxVectors(asNumpy=True) - except: - warn( - "Unknown exception handled from `self.sim_state.getPeriodicBoxVectors()`, " - "this is probably because this attribute is not in the State." - ) - return None + topology : + The topology for you system. - @property - def box_vectors_unit(self): - """The units (as a simtk.units.Unit object) the box vectors are in.""" - return self.box_vectors.unit + integrator : + Integrator for propagating dynamics. - def box_vectors_values(self): - """The box vectors of the state as a numpy array in the - box_vectors_unit simtk.units.Unit. This is what is returned by - the __getitem__ accessor. + platform : + The specification for the default computational platform + to use. Platform can also be set when run_segment is + called. If None uses OpenMM default platform, see OpenMM + documentation for all value but typical ones are: + Reference, CUDA, OpenCL. If value is None the automatic + platform determining mechanism in OpenMM will be used. - """ + platform_kwargs : + key-values to set for a platform with + platform.setPropertyDefaultValue as the default for this + runner. - box_vectors = self.box_vectors - if box_vectors is None: - return None - else: - return self.box_vectors.value_in_unit(self.box_vectors_unit) + enforce_box : + Calls 'context.getState' with 'enforcePeriodicBox' if True. + (Default value = False) - ## non-array properties + get_state_kwargs : + key-values to set for getting the state from the OpenMM context. + keys not included will use the values in GET_STATE_KWARG_DEFAULTS. + Will override the enforce_box flag. - # Kinetic Energy - @property - def kinetic_energy(self): - """The kinetic energy of the state as a numpy array simtk.units.Quantity object.""" - try: - return self.sim_state.getKineticEnergy() - except: - warn( - "Unknown exception handled from `self.sim_state.getKineticEnergy()`, " - "this is probably because this attribute is not in the State." - ) - return None + Warnings + -------- + Regarding the enforce_box option. - @property - def kinetic_energy_unit(self): - """The units (as a simtk.units.Unit object) the kinetic energy is in.""" - return self.kinetic_energy.unit + When retrieving states from an OpenMM simulation Context, you + have the option to enforce periodic boundary conditions in the + resulting atomic positions in a topology aware way that + doesn't break bonds through boundaries. This is convenient for + post-processing as this can be a complex task and is not + readily exposed in the OpenMM API as a standalone function. - def kinetic_energy_value(self): - """The kinetic energy of the state as a numpy array in the kinetic_energy_unit - simtk.units.Unit. This is what is returned by the __getitem__ - accessor. + However, in some types of simulations the periodic box vectors + are ignored (such as implicit solvent ones) despite there + being no option to not have periodic boundaries in the context + itself. Likely if you are running one of these kinds of + simulations you will not pay attention to the box vectors at + all and the random defaults that exist will be very wrong but + this incorrectness will not show in a non-wepy simulation with + openmm unless you are handling the context states + yourself. Then when you run in wepy the default of True to + enforce the boxes will be applied and confusingly wrong + answers will result that are difficult to find root cause of. """ - kinetic_energy = self.kinetic_energy - if kinetic_energy is None: - return None - else: - return np.array( - [self.kinetic_energy.value_in_unit(self.kinetic_energy_unit)] - ) - - # Potential Energy - @property - def potential_energy(self): - """The potential energy of the state as a numpy array simtk.units.Quantity object.""" - try: - return self.sim_state.getPotentialEnergy() - except: - warn( - "Unknown exception handled from `self.sim_state.getPotentialEnergy()`, " - "this is probably because this attribute is not in the State." - ) - return None + if platform is not None: + assert isinstance( + platform, str + ), f"platform should be a string, not {type(platform)}" - @property - def potential_energy_unit(self): - """The units (as a simtk.units.Unit object) the potential energy is in.""" - return self.potential_energy.unit + # we save the different components. However, if we are to make + # this runner picklable we have to convert the SWIG objects to + # a picklable form + self.system = system + self.integrator = integrator - def potential_energy_value(self): - """The potential energy of the state as a numpy array in the potential_energy_unit - simtk.units.Unit. This is what is returned by the __getitem__ - accessor. + # these are not SWIG objects + self.topology = topology + self.platform_name = platform + self.platform_kwargs = platform_kwargs - """ + self.enforce_box = enforce_box - potential_energy = self.potential_energy - if potential_energy is None: - return None - else: - return np.array( - [self.potential_energy.value_in_unit(self.potential_energy_unit)] - ) + self.getState_kwargs = {} + if get_state_kwargs is not None: + for k in get_state_kwargs: + self.getState_kwargs[k] = get_state_kwargs[k] - # Time - @property - def time(self): - """The time of the state as a numpy array simtk.units.Quantity object.""" - try: - return self.sim_state.getTime() - except: - warn( - "Unknown exception handled from `self.sim_state.getTime()`, " - "this is probably because this attribute is not in the State." - ) - return None + # override enforce_box option if specified in get_state_kwargs + if "enforce_box" in get_state_kwargs: + self.enforce_box = get_state_kwargs["enforce_box"] - @property - def time_unit(self): - """The units (as a simtk.units.Unit object) the time is in.""" - return self.time.unit + else: + self.getState_kwargs = dict(GET_STATE_KWARG_DEFAULTS) - def time_value(self): - """The time of the state as a numpy array in the time_unit - simtk.units.Unit. This is what is returned by the __getitem__ - accessor. + self._cycle_platform = None + self._cycle_platform_kwargs = None - """ + # for special monitoring purposes to get split times to debug + # performance + self._last_cycle_segments_split_times = [] - time = self.time - if time is None: - return None - else: - return np.array([self.time.value_in_unit(self.time_unit)]) + def pre_cycle( + self, + platform: str | None = None, + platform_kwargs: PlatformKwargs | None = None, + ) -> None: + # choose to use the platform spec in this function call or to + # use the default one saved in the runner - # Box Volume - @property - def box_volume(self): - """The box volume of the state as a numpy array simtk.units.Quantity object.""" - try: - return self.sim_state.getPeriodicBoxVolume() - except: - warn( - "Unknown exception handled from `self.sim_state.getPeriodicBoxVolume()`, " - "this is probably because this attribute is not in the State." + # if the platform is given locally use this one + if platform is not None: + logger.info( + f"Setting the platform ({platform}) in the 'pre_cycle' OpenMM Runner call" + f"with platform kwargs: {platform_kwargs}" ) - return None + # set the platform and kwargs for this cycle + self._cycle_platform = platform + self._cycle_platform_kwargs = platform_kwargs - @property - def box_volume_unit(self): - """The units (as a simtk.units.Unit object) the box volume is in.""" - return self.box_volume.unit + # otherwise we just don't set this and let resolution of + # platform happen at run segment. + # each segment split times will get appended to this + self._last_cycle_segments_split_times = [] - def box_volume_value(self): - """The box volume of the state as a numpy array in the box_volume_unit - simtk.units.Unit. This is what is returned by the __getitem__ - accessor. + def post_cycle(self) -> None: + # remove the platform and kwargs for this cycle + self._cycle_platform = None + self._cycle_platform_kwargs = None - """ + def _resolve_platform( + self, + platform: str | type(Ellipsis) | None, + platform_kwargs: PlatformKwargs | None, + ) -> tuple[ + str | None, + PlatformKwargs | None, + ]: + # resolve which platform to use - box_volume = self.box_volume - if box_volume is None: - return None - else: - return np.array([self.box_volume.value_in_unit(self.box_volume_unit)]) + # force usage of environmental one + if platform is Ellipsis: + platform_name = None + platform_kwargs = None - ## Dictionary properties - ## Unitless + # use the runtime given one + elif platform is not None: + platform_name = platform + platform_kwargs = platform_kwargs - # Parameters - @property - def parameters(self): - """The parameters of the state as a dictionary mapping the names of - the parameters to their values which are numpy array - simtk.units.Quantity objects. + # if the pre_cycle configured platform is set use this over + # the default + elif self._cycle_platform is not None: + platform_name = self._cycle_platform + platform_kwargs = self._cycle_platform_kwargs - """ + # use the default one + elif self.platform_name is not None: + platform_name = self.platform_name + platform_kwargs = self.platform_kwargs - if "parameters" in self._sim_state_fields_present: - return self.sim_state.getParameters() + # if the default is not set fall back to the environmental one else: - return None - - @property - def parameters_unit(self): - """The units for each parameter as a dictionary mapping parameter - names to their corresponding unit as a simtk.units.Unit - object. - - """ - param_units = {key: None for key, val in self.parameters.items()} - return param_units - - def parameters_values(self): - """The parameters of the state as a dictionary mapping the name of the - parameter to a numpy array in the unit for the parameter of the - same name in the parameters_unit corresponding - simtk.units.Unit object. This is what is returned by the - __getitem__ accessor using the compound key syntax with the - prefix 'parameters', e.g. state['parameter/paramA'] for the - parameter 'paramA'. - - """ - - if self.parameters is None: - return None + platform_name = None + platform_kwargs = None - param_arrs = {key: np.array(val) for key, val in self.parameters.items()} + return ( + platform_name, + platform_kwargs, + ) - # return None if there is nothing in this - if len(param_arrs) == 0: - return None - else: - return param_arrs + def run_segment( + self, + walker: OpenMMWalker, + segment_length: int, + getState_kwargs: dict[str, bool] | None = None, + platform: str | type(Ellipsis) | None = None, + platform_kwargs: PlatformKwargs = None, + ) -> Walker: + """Run dynamics for the walker. - # Parameter Derivatives - @property - def parameter_derivatives(self): - """The parameter derivatives of the state as a dictionary mapping the - names of the parameters to their values which are numpy array - simtk.units.Quantity objects. + Parameters + ---------- + walker : The walker for which dynamics will be propagated. - """ + segment_length : The numerical value that specifies how much dynamics are to be run. - if "parameter_derivatives" in self._sim_state_fields_present: - return self.sim_state.getEnergyParameterDerivatives() - else: - return None + getState_kwargs : Specify the key-word arguments to pass to + simulation.context.getState when getting simulation + states. If None defaults object values. - @property - def parameter_derivatives_unit(self): - """The units for each parameter derivative as a dictionary mapping - parameter names to their corresponding unit as a - simtk.units.Unit object. - """ + platform : The specification for the computational platform to + use. If None will use the default for the runner and + ignore platform_kwargs. If Ellipsis forces the use of the + OpenMM default or environmentally defined platform. See + OpenMM documentation for all value but typical ones are: + Reference, CUDA, OpenCL. If value is None the automatic + platform determining mechanism in OpenMM will be used. - param_units = {key: None for key, val in self.parameter_derivatives.items()} - return param_units + platform_kwargs : Key-values to set for a platform with + platform.setPropertyDefaultValue for this segment only. - def parameter_derivatives_values(self): - """The parameter derivatives of the state as a dictionary mapping the - name of the parameter to a numpy array in the unit for the - parameter of the same name in the parameters_unit - corresponding simtk.units.Unit object. This is what is - returned by the __getitem__ accessor using the compound key - syntax with the prefix 'parameter_derivatives', - e.g. state['parameter_derivatives/paramA'] for the parameter - 'paramA'. + + Returns + ------- + new_walker : Walker after dynamics was run, only the state should be modified. """ - if self.parameter_derivatives is None: - return None + run_segment_start = time.time() - param_arrs = { - key: np.array(val) for key, val in self.parameter_derivatives.items() - } + # set the kwargs that will be passed to getState + _getState_kwargs = ( + getState_kwargs if getState_kwargs is not None else self.getState_kwargs + ) + logger.info(f"Default 'getState_kwargs' in runner: {self.getState_kwargs}") + logger.info(f"'getState_kwargs' passed to 'run_segment' : {getState_kwargs}") - # return None if there is nothing in this - if len(param_arrs) == 0: - return None - else: - return param_arrs + logger.info( + "After resolving 'getState_kwargs' that will be used are: " + f"{_getState_kwargs}" + ) - # for the dict attributes we need to transform the keys for making - # a proper state where all __getitem__ things are arrays - def _dict_attr_to_compound_key_dict(self, root_key, attr_dict): - """Transform a dictionary of values within the compound key 'root_key' - to a dictionary mapping compound keys to values. + gen_sim_start = time.time() - For example give the root_key 'parameters' and the parameters - dictionary {'paramA' : 1.234} returns {'parameters/paramA' : 1.234}. + # make a copy of the integrator for this particular segment + new_integrator = copy(self.integrator) + # force setting of random seed to 0, which is a special + # value that forces the integrator to choose another + # random number + new_integrator.setRandomNumberSeed(0) - Parameters - ---------- - root_key : str - The compound key prefix - attr_dict : dict of str : value - The dictionary with simple keys within the root key namespace. + ## Platform - Returns - ------- - compound_key_dict : dict of str : value - The dictionary with the compound keys. + logger.info(f"Default 'platform' in runner: {self.platform_name}") - """ + logger.info(f"pre_cycle set 'platform' in runner: {self._cycle_platform}") - key_template = "{}/{}" - cmpd_key_d = {} - for key, value in attr_dict.items(): - new_key = key_template.format(root_key, key) - # if this is a proper feature - if type(value) == np.ndarray: - cmpd_key_d[new_key] = value - elif hasattr(value, "__getitem__"): - cmpd_key_d.update(self._dict_attr_to_compound_key_dict(new_key, value)) - else: - raise TypeError("Unsupported attribute type") + logger.info(f"'platform' passed to 'run_segment' : {platform}") - return cmpd_key_d + logger.info(f"Default 'platform_kwargs' in runner: {self.platform_kwargs}") - def _get_nested_attr_from_compound_key(self, compound_key, compound_feat_dict): - """Get arbitrarily deeply nested compound keys from the full - dictionary tree. + logger.info( + f"pre_cycle set 'platform_kwargs' in runner: {self._cycle_platform_kwargs}" + ) - Parameters - ---------- - compound_key : str - Compound key separated by '/' characters + logger.info(f"'platform_kwargs' passed to 'run_segment' : {platform_kwargs}") - compound_feat_dict : dict - Dictionary of arbitrary depth + platform_name, platform_kwargs = self._resolve_platform( + platform, platform_kwargs + ) - Returns - ------- - value - Value requested by the key. + logger.info(f"Resolved 'platform' : {platform_name}") - """ + logger.info(f"Resolved 'platform_kwargs' : {platform_kwargs}") - key_components = compound_key.split("/") + # create simulation object - # if there is only one component of the key then it is not - # really compound, we won't complain just return the - # "dictionary" if it is not actually a dict like - if not hasattr(compound_feat_dict, "__getitem__"): - raise TypeError("Must provide a dict-like with the compound key") + ## create the platform and customize - value = compound_feat_dict[key_components[0]] + # if a platform was given we use it to make a Simulation object + if platform_name is not None: + logger.info("Using platform configured in code.") - # if the value itself is compound recursively fetch the value - if hasattr(value, "__getitem__") and len(key_components[1:]) > 0: - subgroup_key = "/".join(key_components[1:]) + # get the platform by its name to use + platform = openmm.Platform.getPlatformByName(platform_name) + logger.info(f"Platform object created: {platform}") - return self._get_nested_attr_from_compound_key(subgroup_key, value) + if platform_kwargs is None: + platform_kwargs = {} - elif hasattr(value, "__getitem__") and len(key_components[1:]) < 1: - raise ValueError("Key does not reference a leaf node of attribute") + # set properties from the kwargs if they apply to the platform + for key, value in platform_kwargs.items(): + if key in platform.getPropertyNames(): + logger.info(f"Setting platform property: {key} : {value}") + platform.setPropertyDefaultValue(key, value) - # otherwise we have the right key so return the object - else: - return value + else: + warn( + f"Platform kwargs given ({key} : {value}) " + f"but is not valid for this platform ({platform_name})" + ) - def parameters_features(self): - """Returns a dictionary of the parameters with their appropriate - compound keys. This can be used for placing them in the same namespace - as the rest of the attributes. - """ + # make a new simulation object + simulation = openmm.app.Simulation( + self.topology, self.system, new_integrator, platform + ) - parameters = self.parameters_values() - if parameters is None: - return None + # otherwise just use the default or environmentally defined one else: - return self._dict_attr_to_compound_key_dict("parameters", parameters) + logger.info("Using environmental platform.") + simulation = openmm.app.Simulation( + self.topology, self.system, new_integrator + ) - def parameter_derivatives_features(self): - """Returns a dictionary of the parameter derivatives with their appropriate - compound keys. This can be used for placing them in the same namespace - as the rest of the attributes. - """ + # set the state to the context from the walker + simulation.context.setState(walker.state.sim_state) - parameter_derivatives = self.parameter_derivatives_values() - if parameter_derivatives is None: - return None - else: - return self._dict_attr_to_compound_key_dict( - "parameter_derivatives", parameter_derivatives - ) + gen_sim_end = time.time() + gen_sim_time = gen_sim_end - gen_sim_start - def omm_state_dict(self): - """Return a dictionary with all of the default keys from the wrapped - simtk.openmm.State object - """ + logger.info("Time to generate the system: {}".format(gen_sim_time)) - feature_d = { - "positions": self.positions_values(), - "velocities": self.velocities_values(), - "forces": self.forces_values(), - "kinetic_energy": self.kinetic_energy_value(), - "potential_energy": self.potential_energy_value(), - "time": self.time_value(), - "box_vectors": self.box_vectors_values(), - "box_volume": self.box_volume_value(), - } + # actually run the simulation - params = self.parameters_features() - if params is not None: - feature_d.update(params) + steps_start = time.time() - param_derivs = self.parameter_derivatives_features() - if param_derivs is not None: - feature_d.update(param_derivs) + # Run the simulation segment for the number of time steps + simulation.step(segment_length) - return feature_d + steps_end = time.time() + steps_time = steps_end - steps_start - def dict(self): - # documented in superclass + logger.info("Time to run {} sim steps: {}".format(segment_length, steps_time)) - d = {} - for key, value in self._data.items(): - d[key] = value - for key, value in self.omm_state_dict().items(): - d[key] = value - return d + get_state_start = time.time() - def to_mdtraj(self, topology): - """Returns an mdtraj.Trajectory object from this walker's state. + get_state_end = time.time() + get_state_time = get_state_end - get_state_start + logger.info("Getting context state time: {}".format(get_state_time)) - Parameters - ---------- - topology : mdtraj.Topology object - Topology for the state. + # generate the new state/walker + new_state = OpenMMState(simulation.context.getState(**_getState_kwargs)) - Returns - ------- - state_traj : mdtraj.Trajectory object + # create a new walker for this + new_walker = OpenMMWalker(new_state, walker.weight) - """ + run_segment_end = time.time() + run_segment_time = run_segment_end - run_segment_start + logger.info("Total internal run_segment time: {}".format(run_segment_time)) - # Third Party Library - import mdtraj as mdj + segment_split_times = { + "gen_sim_time": gen_sim_time, + "steps_time": steps_time, + "get_state_time": get_state_time, + "run_segment_time": run_segment_time, + } - # resize the time to a 1D vector - unitcell_lengths, unitcell_angles = box_vectors_to_lengths_angles( - self.box_vectors - ) - return mdj.Trajectory( - np.array([self.positions_values()]), - unitcell_lengths=[unitcell_lengths], - unitcell_angles=[unitcell_angles], - topology=topology, - ) + self._last_cycle_segments_split_times.append(segment_split_times) + + return new_walker -def gen_sim_state(positions, system, integrator, getState_kwargs=None): - """Convenience function for generating an omm.State object. +def gen_sim_state( + positions: AtomNDArray, + system: openmm.System, + integrator: openmm.Integrator, + getState_kwargs: dict[str, bool] | None = None, +) -> openmm.State: + """Convenience function for generating an openmm.State object. Parameters ---------- @@ -1267,8 +1275,8 @@ def gen_sim_state(positions, system, integrator, getState_kwargs=None): # generate a throwaway context, using the reference platform so we # don't screw up other platform stuff later in the same process - platform = omm.Platform.getPlatformByName("Reference") - context = omm.Context(system, copy(integrator), platform) + platform = openmm.Platform.getPlatformByName("Reference") + context = openmm.Context(system, copy(integrator), platform) # set the positions context.setPositions(positions) @@ -1305,24 +1313,6 @@ def gen_walker_state(positions, system, integrator, getState_kwargs=None): return state -class OpenMMWalker(Walker): - """Walker for OpenMMRunner simulations. - - This simply enforces the use of an OpenMMState object for the - walker state attribute. - - """ - - def __init__(self, state, weight): - # documented in superclass - - assert isinstance( - state, OpenMMState - ), "state must be an instance of class OpenMMState not {}".format(type(state)) - - super().__init__(state, weight) - - class OpenMMCPUWorker(Worker): """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). diff --git a/tests/unit/test_runners/test_openmm.py b/tests/unit/test_runners/test_openmm.py new file mode 100644 index 00000000..2ab70430 --- /dev/null +++ b/tests/unit/test_runners/test_openmm.py @@ -0,0 +1,436 @@ +from wepy.runners.openmm import ( + resolve_state_data_type_enum_values, + GET_STATE_KWARG_DEFAULTS, + get_state_fields_present, + OpenMMRunner, + OpenMMState, + gen_sim_state, + gen_walker_state, + OpenMMWalker, + OpenMMCPUWorker, + OpenMMGPUWorker, + OpenMMCPUWalkerTaskProcess, + OpenMMGPUWalkerTaskProcess, +) + +import pytest + +import numpy as np +import openmm +import openmm.app +import openmm.unit + + +def dummy_context( + system: openmm.System, + positions: openmm.unit.Quantity, + unitcell: openmm.unit.Quantity | None = None, +) -> openmm.Context: + """Create a throwaway OpenMM context. + + This uses some hardcoded integrators, etc. to be able to get a + context which is useful for generating OpenMM objects without + running any calculations. You can also use it for a simulation but + it won't do anything meaningful. + """ + + platform = openmm.Platform.getPlatformByName("Reference") + integrator = openmm.VerletIntegrator(1.0 * openmm.unit.femtoseconds) + context = openmm.Context(system, integrator, platform) + context.setPositions(positions) + + if unitcell is not None: + bvs = unitcell.to_vec3() + context.setPeriodicBoxVectors(*bvs) + + return context + + +def n_lj_system( + num_particles: int, + mass: openmm.unit.Quantity = (39.9481 * openmm.unit.dalton), + sigma: openmm.unit.Quantity = (0.3350 * openmm.unit.nanometer), + epsilon: openmm.unit.Quantity = (0.996 * openmm.unit.kilojoules_per_mole), +) -> openmm.System: + + system = openmm.System() + + # single nonbonded force + nb_force = openmm.NonbondedForce() + nb_force.setNonbondedMethod(openmm.NonbondedForce.NoCutoff) + + # TODO: add support for cutoffs + + for idx in range(num_particles): + system.addParticle(mass) + nb_force.addParticle( + 0.0 * openmm.unit.elementary_charge, + sigma, + epsilon, + ) + + system.addForce(nb_force) + + return system + + +def particle_line(num_particles: int) -> openmm.unit.Quantity: + """Initialize a 3D coordinate array.""" + + return ( + np.array([[float(idx), 0.0, 0.0] for idx in range(num_particles)]) + * openmm.unit.angstrom + ) + + +ARGON = openmm.app.Element.getBySymbol("Ar") + + +def n_particle_topology( + num_particles: int, + element: openmm.app.Element = ARGON, +) -> openmm.app.Topology: + """Create a single particle topology from scratch. + + There will only be one chain, and each particle is it's own + residue. + + Box vectors are never set. + """ + + top = openmm.app.Topology() + + chain = top.addChain() + for idx in range(num_particles): + + residue = top.addResidue(element.symbol, chain) + top.addAtom( + element.symbol, + element, + residue, + ) + + return top + + +@pytest.fixture +def omm_context() -> openmm.Context: + + system = n_lj_system(2) + + coords = particle_line(2) + + ctx = dummy_context(system, coords) + + return ctx + + +def test_resolve_state_data_type_enum_values(): + + assert resolve_state_data_type_enum_values() == { + "positions": 1, + "velocities": 2, + "energy": 8, + "forces": 4, + "integrator_parameters": 64, + "parameter_derivatives": 32, + "parameters": 16, + } + + +def test_get_state_fields_present(omm_context): + + state = omm_context.getState(positions=True) + assert get_state_fields_present(state) == ["positions"] + + +def test_gen_sim_state(): + + state = gen_sim_state( + positions=particle_line(2), + system=n_lj_system(2), + integrator=openmm.VerletIntegrator(0.002), + ) + + +def test_gen_walker_state(): + + gen_walker_state( + positions=particle_line(2), + system=n_lj_system(2), + integrator=openmm.VerletIntegrator(0.002), + ) + + +class TestOpenMMState: + + # TODO: this whole class needs overhauled but I want the other + # tests before messing with it too much. + + def test___init__(self): + + state = gen_sim_state( + positions=particle_line(2), + system=n_lj_system(2), + integrator=openmm.VerletIntegrator(0.002), + ) + + state = OpenMMState(state) + # assert "positions" in state + # assert state._data == {} + + +class TestOpenMMRunner: + + def test___init__(self): + + system = n_lj_system(2) + topology = n_particle_topology(2) + integrator = openmm.VerletIntegrator(0.002) + + runner = OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + ) + + assert not runner.enforce_box + assert runner.getState_kwargs == dict(GET_STATE_KWARG_DEFAULTS) + + assert OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + enforce_box=True, + ).enforce_box + + assert OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + enforce_box=False, + get_state_kwargs={"enforce_box": True}, + ).enforce_box + + assert OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + enforce_box=False, + get_state_kwargs={"positions": True}, + ).getState_kwargs == {"positions": True} + + def test_pre_cycle(self): + + system = n_lj_system(2) + topology = n_particle_topology(2) + integrator = openmm.VerletIntegrator(0.002) + + runner = OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + ) + + assert runner._cycle_platform is None + assert runner._cycle_platform_kwargs is None + + runner.pre_cycle() + + assert runner._cycle_platform is None + assert runner._cycle_platform_kwargs is None + + runner = OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + ) + + assert runner._cycle_platform is None + assert runner._cycle_platform_kwargs is None + + runner.pre_cycle( + platform="CPU", + ) + + assert runner._cycle_platform == "CPU" + assert runner._cycle_platform_kwargs is None + + runner = OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + ) + + assert runner._cycle_platform is None + assert runner._cycle_platform_kwargs is None + + runner.pre_cycle( + platform="CPU", + platform_kwargs={"Threads": 1}, + ) + + assert runner._cycle_platform == "CPU" + assert runner._cycle_platform_kwargs == {"Threads": 1} + + def test_post_cycle(self): + + system = n_lj_system(2) + topology = n_particle_topology(2) + integrator = openmm.VerletIntegrator(0.002) + + runner = OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + ) + + assert runner._cycle_platform is None + assert runner._cycle_platform_kwargs is None + + runner.post_cycle() + + assert runner._cycle_platform is None + assert runner._cycle_platform_kwargs is None + + runner = OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + ) + + assert runner._cycle_platform is None + assert runner._cycle_platform_kwargs is None + + runner.pre_cycle( + platform="CPU", + platform_kwargs={"Threads": 1}, + ) + + assert runner._cycle_platform == "CPU" + assert runner._cycle_platform_kwargs == {"Threads": 1} + + runner.post_cycle() + + assert runner._cycle_platform is None + assert runner._cycle_platform_kwargs is None + + def test__resolve_platform(self): + + system = n_lj_system(2) + topology = n_particle_topology(2) + integrator = openmm.VerletIntegrator(0.002) + + runner = OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + ) + + assert runner._resolve_platform(platform=None, platform_kwargs=None) == ( + None, + None, + ) + assert runner._resolve_platform(platform=Ellipsis, platform_kwargs=None) == ( + None, + None, + ) + assert runner._resolve_platform( + platform=Ellipsis, platform_kwargs={"Threads": 1} + ) == (None, None) + + assert runner._resolve_platform(platform="CPU", platform_kwargs=None) == ( + "CPU", + None, + ) + assert runner._resolve_platform( + platform="CPU", platform_kwargs={"Threads": 1} + ) == ("CPU", {"Threads": 1}) + + runner = OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + ) + + runner.pre_cycle( + platform="CPU", + platform_kwargs={"Threads": 1}, + ) + + assert runner._resolve_platform(platform=None, platform_kwargs=None) == ( + "CPU", + {"Threads": 1}, + ) + + runner = OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + platform="CPU", + ) + + assert runner._resolve_platform(platform=None, platform_kwargs=None) == ( + "CPU", + None, + ) + + def test_run_segment(self): + + system = n_lj_system(2) + topology = n_particle_topology(2) + # TODO: only currently works with LangevinIntegrator + integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + + state = gen_sim_state( + positions=particle_line(2), + system=n_lj_system(2), + integrator=openmm.VerletIntegrator(0.002), + ) + + state = OpenMMState(state) + walker = OpenMMWalker(state, 0.1) + + runner = OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + ) + + runner.run_segment( + walker, + 2, + ) + + walker = runner.run_segment(walker, 2, getState_kwargs={"positions": True}) + assert walker.state["positions"] is not None + assert walker.state["velocities"] is None + + runner = OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + ) + + runner.run_segment( + walker, + 2, + platform="Reference", + ) + + +class TestOpenMMCPUWorker: + + pass + + +class TestOpenMMGPUWorker: + pass + + +class TestOpenMMCPUWalkerTaskProcess: + pass + + +class TestOpenMMGPUWalkerTaskProcess: + pass From 1cd7f7701b23e82b063c19b2e3316dd5d171270a Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 25 Nov 2025 22:03:55 -0500 Subject: [PATCH 015/143] fixup! minor fixups --- src/wepy/util/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wepy/util/util.py b/src/wepy/util/util.py index 64075a72..7f894046 100644 --- a/src/wepy/util/util.py +++ b/src/wepy/util/util.py @@ -11,7 +11,7 @@ import numpy as np -def set_loglevel(loglevel): +def set_loglevel(loglevel: int | str) -> None: """\b Parameters From 1e89f5898ebeedfafdb1c35aec28d5ec65fd5985 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 26 Nov 2025 12:14:06 -0500 Subject: [PATCH 016/143] improve walker weight comparison operator --- src/wepy/walker.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/wepy/walker.py b/src/wepy/walker.py index b6e33546..f66b26c5 100644 --- a/src/wepy/walker.py +++ b/src/wepy/walker.py @@ -30,6 +30,7 @@ import logging from typing import Protocol, Hashable, Any, Self from abc import ABC +import math # Standard Library import random as rand @@ -159,8 +160,8 @@ class Walker(WalkerABC): """ - state: WalkerStateProtocol - weight: float + state: WalkerState + weight: float = attrs.field(eq=attrs.cmp_using(eq=math.isclose)) def split(walker: Walker, number: int = 2) -> list[Walker]: From 840ecc29a63c2bee7e64423a82e4e5370564e2eb Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 26 Nov 2025 12:14:55 -0500 Subject: [PATCH 017/143] refactor and add tests for decisions and basic NoResampler --- src/wepy/resampling/decisions/clone_merge.py | 27 +-- src/wepy/resampling/decisions/decision.py | 126 +++++------- src/wepy/resampling/decisions/no_decision.py | 61 ++++++ src/wepy/resampling/resamplers/noresampler.py | 78 ++++++++ src/wepy/resampling/resamplers/resampler.py | 137 ++++--------- .../test_decisions/test_clone_merge.py | 188 ++++++++++++++++++ .../test_decisions/test_decision.py | 0 .../test_decisions/test_no_decision.py | 57 ++++++ .../test_resamplers/test_noresampler.py | 47 +++++ .../test_resamplers/test_resampler.py | 0 10 files changed, 530 insertions(+), 191 deletions(-) create mode 100644 src/wepy/resampling/decisions/no_decision.py create mode 100644 src/wepy/resampling/resamplers/noresampler.py create mode 100644 tests/unit/test_resampling/test_decisions/test_clone_merge.py create mode 100644 tests/unit/test_resampling/test_decisions/test_decision.py create mode 100644 tests/unit/test_resampling/test_decisions/test_no_decision.py create mode 100644 tests/unit/test_resampling/test_resamplers/test_noresampler.py create mode 100644 tests/unit/test_resampling/test_resamplers/test_resampler.py diff --git a/src/wepy/resampling/decisions/clone_merge.py b/src/wepy/resampling/decisions/clone_merge.py index 1cc8bf42..d14b43b4 100644 --- a/src/wepy/resampling/decisions/clone_merge.py +++ b/src/wepy/resampling/decisions/clone_merge.py @@ -1,19 +1,21 @@ # Standard Library +from typing import TypedDict import logging -logger = logging.getLogger(__name__) # Standard Library from collections import defaultdict -from enum import Enum +from enum import IntEnum # First Party Library from wepy.resampling.decisions.decision import Decision -from wepy.walker import keep_merge, split +from wepy.walker import keep_merge, split, Walker + +logger = logging.getLogger(__name__) # the possible types of decisions that can be made enumerated for # storage, these each correspond to specific instruction type -class CloneMergeDecisionEnum(Enum): +class CloneMergeDecisionEnum(IntEnum): """Enum definition for cloning and merging decision values." - NOTHING : 1 @@ -38,6 +40,11 @@ class CloneMergeDecisionEnum(Enum): donate their weight to it.""" +class CloneMergeDecisionRecord(TypedDict): + decision_id: int + target_idxs: list[int] + + class MultiCloneMergeDecision(Decision): """Decision encoding cloning and merging decisions for weighted ensemble. @@ -76,16 +83,10 @@ class MultiCloneMergeDecision(Decision): ENUM.CLONE.value, ) - # TODO deprecate in favor of Decision implementation - @classmethod - def record(cls, enum_value, target_idxs): - record = super().record(enum_value) - record["target_idxs"] = target_idxs - - return record - @classmethod - def action(cls, walkers, decisions): + def action( + cls, walkers: list[Walker], decisions: list[CloneMergeDecisionRecord] + ) -> list[Walker]: # list for the modified walkers mod_walkers = [None for i in range(len(walkers))] diff --git a/src/wepy/resampling/decisions/decision.py b/src/wepy/resampling/decisions/decision.py index d0fde037..80f12c7a 100644 --- a/src/wepy/resampling/decisions/decision.py +++ b/src/wepy/resampling/decisions/decision.py @@ -46,24 +46,36 @@ """ # Standard Library +from typing import TypedDict, Required, Any, Union import logging -logger = logging.getLogger(__name__) # Standard Library -from enum import Enum +from enum import IntEnum + +from wepy.walker import Walker + +logger = logging.getLogger(__name__) + + +class DecisionRecord(TypedDict): + + decision_id: int + + +DecisionFieldDtype = Union[int,] # ABC for the Decision class class Decision: """Represents and provides methods for a set of decision values.""" - ENUM = None + ENUM: IntEnum """The enumeration of the decision types. Maps them to integers.""" - DEFAULT_DECISION = None + DEFAULT_DECISION: int """The default decision to choose.""" - FIELDS = ("decision_id",) + FIELDS: tuple[str, ...] = ("decision_id",) """The names of the fields that go into the decision record.""" # suggestion for subclassing, FIELDS and others @@ -72,16 +84,16 @@ class Decision: # An Ellipsis instead of fields indicate there is a variable # number of fields. - SHAPES = ((1,),) + SHAPES: tuple[tuple[int | type(Ellipsis), ...], ...] = ((1,),) """Field data shapes.""" - DTYPES = (int,) + DTYPES: tuple[DecisionFieldDtype, ...] = (int,) """Field data types.""" - RECORD_FIELDS = ("decision_id",) + RECORD_FIELDS: tuple[str] = ("decision_id",) """The fields that could be used in a reduced table-like representation.""" - ANCESTOR_DECISION_IDS = None + ANCESTOR_DECISION_IDS: tuple[int, ...] """Specify the enum values where their walker state sample value is passed on in the next generation, i.e. after performing the action.""" @@ -177,37 +189,41 @@ def enum_by_name(cls, enum_name): d = cls.enum_dict_by_name() return d[enum_name] - @classmethod - def record(cls, enum_value, **fields): - """Generate a record for the enum_value and the other fields. + # @classmethod + # def record(cls, enum_value: int, **fields: dict[str, Any]) -> DecisionRecord: + # """Generate a record for the enum_value and the other fields. - Parameters - ---------- - enum_value : int + # Parameters + # ---------- + # enum_value : int - Returns - ------- - rec : dict of str: value + # Returns + # ------- + # rec : dict of str: value - """ + # """ - assert ( - enum_value in cls.enum_dict_by_value() - ), "value is not a valid Enumerated value" + # assert ( + # enum_value in cls.enum_dict_by_value() + # ), "value is not a valid Enumerated value" - for field_key in fields.keys(): - assert ( - field_key in cls.FIELDS - ), "The field {} is not a field for that decision".format(field_key) - assert field_key != "decision_id", "'decision_id' cannot be an extra field" + # for field_key in fields.keys(): + # assert ( + # field_key in cls.FIELDS + # ), "The field {} is not a field for that decision".format(field_key) + # assert field_key != "decision_id", "'decision_id' cannot be an extra field" - rec = {"decision_id": enum_value} - rec.update(fields) + # rec = {"decision_id": enum_value} + # rec.update(fields) - return rec + # return rec @classmethod - def action(cls, walkers, decisions): + def action( + cls, + walkers: list[Walker], + decisions: list[list[DecisionRecord]], + ) -> list[Walker]: """Perform the instructions for a set of resampling records on walkers. @@ -244,7 +260,7 @@ def action(cls, walkers, decisions): raise NotImplementedError @classmethod - def parents(cls, step): + def parents(cls, step: list[DecisionRecord]) -> list[int]: """Given a step of resampling records (for a single resampling step) returns the parents of the children of this step. @@ -277,49 +293,3 @@ def parents(cls, step): step_parents[child_idx] = parent_idx return step_parents - - -class NothingDecisionEnum(Enum): - """Enumeration of the decision values for doing nothing.""" - - NOTHING = 0 - """Do nothing with the walker.""" - - -class NoDecision(Decision): - """Decision for a resampling process that does no resampling.""" - - ENUM = NothingDecisionEnum - DEFAULT_DECISION = ENUM.NOTHING - - FIELDS = Decision.FIELDS + ("target_idxs",) - SHAPES = Decision.SHAPES + (Ellipsis,) - DTYPES = Decision.DTYPES + (int,) - - RECORD_FIELDS = Decision.RECORD_FIELDS + ("target_idxs",) - - ANCESTOR_DECISION_IDS = (ENUM.NOTHING.value,) - - @classmethod - def action(cls, walkers, decisions): - # list for the modified walkers - mod_walkers = [None for i in range(len(walkers))] - # go through each decision and perform the decision - # instructions - for walker_idx, decision in enumerate(decisions): - decision_value, instruction = decision - if decision_value == cls.ENUM.NOTHING.value: - # check to make sure a walker doesn't already exist - # where you are going to put it - if mod_walkers[instruction[0]] is not None: - raise ValueError( - "Multiple walkers assigned to position {}".format( - instruction[0] - ) - ) - - # put the walker in the position specified by the - # instruction - mod_walkers[instruction[0]] = walkers[walker_idx] - - return mod_walkers diff --git a/src/wepy/resampling/decisions/no_decision.py b/src/wepy/resampling/decisions/no_decision.py new file mode 100644 index 00000000..d2ffc24b --- /dev/null +++ b/src/wepy/resampling/decisions/no_decision.py @@ -0,0 +1,61 @@ +from typing import TypedDict +from enum import IntEnum + +from wepy.walker import Walker +from wepy.resampling.decisions.decision import Decision, DecisionRecord + + +class NothingDecisionEnum(IntEnum): + """Enumeration of the decision values for doing nothing.""" + + NOTHING = 0 + """Do nothing with the walker.""" + + +class NoDecisionRecord(TypedDict): + decision_id: int + target_idxs: list[int] + + +class NoDecision(Decision): + """Decision for a resampling process that does no resampling.""" + + ENUM = NothingDecisionEnum + DEFAULT_DECISION = ENUM.NOTHING + + FIELDS = Decision.FIELDS + ("target_idxs",) + SHAPES = Decision.SHAPES + (Ellipsis,) + DTYPES = Decision.DTYPES + (int,) + + RECORD_FIELDS = Decision.RECORD_FIELDS + ("target_idxs",) + + ANCESTOR_DECISION_IDS = (ENUM.NOTHING.value,) + + @classmethod + def action( + cls, + walkers: list[Walker], + decisions: list[NoDecisionRecord], + ) -> list[Walker]: + # list for the modified walkers + mod_walkers: list[Walker] = [None for i in range(len(walkers))] + # go through each decision and perform the decision + # instructions + for step_idx, step_recs in enumerate(decisions): + for walker_idx, decision in enumerate(step_recs): + + if decision["decision_id"] == cls.ENUM.NOTHING.value: + # check to make sure a walker doesn't already exist + # where you are going to put it + if mod_walkers[decision["target_idxs"][0]] is not None: + raise ValueError( + "Multiple walkers assigned to position {}".format( + decision["target_idxs"][0] + ) + ) + + # put the walker in the position specified by the + # instruction + mod_walkers[decision["target_idxs"][0]] = walkers[walker_idx] + + return mod_walkers diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py new file mode 100644 index 00000000..001765f9 --- /dev/null +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -0,0 +1,78 @@ +from typing import TypedDict +import numpy as np +from nptyping import NDArray, Floating, Shape, Integer +from wepy.walker import Walker +from wepy.resampling.resamplers.resampler import Resampler +from wepy.resampling.decisions.no_decision import ( + NoDecision, + NoDecisionRecord, + NothingDecisionEnum, +) + + +class NoResamplerResamplingData(TypedDict): + walker_idx: NDArray[Shape["1"], Integer] + step_idx: NDArray[Shape["1"], Integer] + decision_id: NDArray[Shape["1"], Integer] + target_idxs: NDArray[Shape["1"], Integer] + + +class NoResamplerResamplerData(TypedDict): + pass + + +class NoResampler(Resampler): + """The resampler which does nothing.""" + + DECISION = NoDecision + + # must reset these when you change the decision + RESAMPLING_FIELDS = DECISION.FIELDS + Resampler.CYCLE_FIELDS + RESAMPLING_SHAPES = DECISION.SHAPES + Resampler.CYCLE_SHAPES + RESAMPLING_DTYPES = DECISION.DTYPES + Resampler.CYCLE_DTYPES + + RESAMPLING_RECORD_FIELDS = DECISION.RECORD_FIELDS + Resampler.CYCLE_RECORD_FIELDS + + def resample( + self, + walkers: list[Walker], + ) -> tuple[ + list[Walker], + list[list[NoResamplerResamplingData]], + list[NoResamplerResamplerData], + ]: + + # TODO,REFACT: do we really need this + self._resample_init(walkers=walkers) + + # normally decide is only for a single step and so does not + # include the step_idx, so we add this to the records, and + # convert the target idxs and decision_id to feature vector + # arrays + _resampling_data: list[NoResamplerResamplingData] = [] + for walker_idx in range(len(walkers)): + + walker_record = NoResamplerResamplingData( + decision_id=NothingDecisionEnum.NOTHING.value, + target_idxs=[walker_idx], + ) + + _resampling_data.append(walker_record) + + # only a single step of decisions + resampling_data = [_resampling_data] + + # there is no change in state in the resampler so there are no + # resampler records + resampler_data: list[NoResamplerResamplerData] = [{}] + + # the resampled walkers are just the walkers + + # TODO,REFACT: do we really need this + self._resample_cleanup( + resampling_data=resampling_data, + resampler_data=resampler_data, + walkers=walkers, + ) + + return walkers, resampling_data, resampler_data diff --git a/src/wepy/resampling/resamplers/resampler.py b/src/wepy/resampling/resamplers/resampler.py index b176f0b3..48050c52 100644 --- a/src/wepy/resampling/resamplers/resampler.py +++ b/src/wepy/resampling/resamplers/resampler.py @@ -1,7 +1,7 @@ # Standard Library import logging +from typing import Any -logger = logging.getLogger(__name__) # Standard Library from warnings import warn @@ -9,7 +9,10 @@ import numpy as np # First Party Library -from wepy.resampling.decisions.decision import Decision, NoDecision +from wepy.resampling.decisions.decision import Decision, DecisionRecord +from wepy.walker import Walker + +logger = logging.getLogger(__name__) class ResamplerError(Exception): @@ -81,10 +84,10 @@ class Resampler: """ - DECISION = Decision + DECISION: Decision = Decision """The decision class for this resampler.""" - CYCLE_FIELDS = ( + CYCLE_FIELDS: tuple[str, ...] = ( "step_idx", "walker_idx", ) @@ -94,19 +97,19 @@ class Resampler: walker index. """ - CYCLE_SHAPES = ( + CYCLE_SHAPES: tuple[tuple[int, ...], ...] = ( (1,), (1,), ) """Data shapes of the cycle fields.""" - CYCLE_DTYPES = ( + CYCLE_DTYPES: tuple[int | float] = ( int, int, ) """Data types of the cycle fields """ - CYCLE_RECORD_FIELDS = ( + CYCLE_RECORD_FIELDS: tuple[str, ...] = ( "step_idx", "walker_idx", ) @@ -280,11 +283,11 @@ class Resampler: def __init__( self, - min_num_walkers=Ellipsis, - max_num_walkers=Ellipsis, - debug_mode=False, + min_num_walkers: int | None | type(Ellipsis) = Ellipsis, + max_num_walkers: int | None | type(Ellipsis) = Ellipsis, + debug_mode: bool = False, **kwargs, - ): + ) -> None: """Constructor for Resampler class Parameters @@ -340,7 +343,7 @@ def __init__( self.set_debug_mode(debug_mode) @property - def decision(self): + def decision(self) -> Decision: """The decision class for this resampler.""" return self.DECISION @@ -411,11 +414,11 @@ def resampler_record_field_names(self): return self.RESAMPLER_RECORD_FIELDS @property - def is_debug_on(self): + def is_debug_on(self) -> bool: """ """ return self._debug_mode - def set_debug_mode(self, mode): + def set_debug_mode(self, mode: bool) -> None: """Parameters ---------- mode @@ -440,14 +443,14 @@ def set_debug_mode(self, mode): "You must have ipdb installed to use the debug feature" ) - def debug_on(self): + def debug_on(self) -> None: """ """ if self.is_debug_on: warn("Debug mode is already on") self.set_debug_mode(True) - def debug_off(self): + def debug_off(self) -> None: """ """ if not self.is_debug_on: warn("Debug mode is already off") @@ -464,7 +467,7 @@ def min_num_walkers_setting(self): """The specification for the minimum number of walkers for the resampler.""" return self._min_num_walkers - def max_num_walkers(self): + def max_num_walkers(self) -> int | None: """ " Get the max number of walkers allowed currently""" # first check to make sure that a resampling is occuring and @@ -493,7 +496,7 @@ def max_num_walkers(self): else: return self.max_num_walkers_setting - def min_num_walkers(self): + def min_num_walkers(self) -> int | None: """ " Get the min number of walkers allowed currently""" # first check to make sure that a resampling is occuring and @@ -522,7 +525,7 @@ def min_num_walkers(self): else: return self.min_num_walkers_setting - def _set_resampling_num_walkers(self, num_walkers): + def _set_resampling_num_walkers(self, num_walkers: int) -> None: """Sets the concrete number of walkers constraints given a number of walkers and the settings for max and min. @@ -554,10 +557,13 @@ def _set_resampling_num_walkers(self, num_walkers): "The number of walkers given to resample is less than the maximum" ) - def _unset_resampling_num_walkers(self): + def _unset_resampling_num_walkers(self) -> None: self._resampling_num_walkers = None - def _resample_init(self, walkers, **kwargs): + def _resample_init( + self, + walkers: list[Walker], + ) -> None: """Common initialization stuff for resamplers. Sets the number of walkers in this round of resampling. @@ -571,7 +577,7 @@ def _resample_init(self, walkers, **kwargs): # first set how many walkers there are in this resampling self._set_resampling_num_walkers(len(walkers)) - def _resample_cleanup(self, **kwargs): + def _resample_cleanup(self, **kwargs) -> None: """Common cleanup stuff for resamplers. Unsets the number of walkers for this round of resampling. @@ -581,7 +587,15 @@ def _resample_cleanup(self, **kwargs): # unset the number of walkers for this resampling self._unset_resampling_num_walkers() - def resample(self, walkers, debug_mode=False): + def resample( + self, + walkers: list[Walker], + debug_mode: bool = False, + ) -> tuple[ + list[Walker], + list[dict[str, Any]], + list[dict[str, Any]], + ]: """Perform resampling on the set of walkers. Parameters @@ -609,80 +623,3 @@ def resample(self, walkers, debug_mode=False): """ raise NotImplementedError - - self._resample_init(walkers, debug_mode=debug_mode) - - -class NoResampler(Resampler): - """The resampler which does nothing.""" - - DECISION = NoDecision - - # must reset these when you change the decision - RESAMPLING_FIELDS = DECISION.FIELDS + Resampler.CYCLE_FIELDS - RESAMPLING_SHAPES = DECISION.SHAPES + Resampler.CYCLE_SHAPES - RESAMPLING_DTYPES = DECISION.DTYPES + Resampler.CYCLE_DTYPES - - RESAMPLING_RECORD_FIELDS = DECISION.RECORD_FIELDS + Resampler.CYCLE_RECORD_FIELDS - - def resample(self, walkers, **kwargs): - self._resample_init(walkers=walkers) - - n_walkers = len(walkers) - - # the walker actions are all nothings with the same walker - # index which is the default initialization - resampling_data = self._init_walker_actions(n_walkers) - - # normally decide is only for a single step and so does not - # include the step_idx, so we add this to the records, and - # convert the target idxs and decision_id to feature vector - # arrays - for walker_idx, walker_record in enumerate(resampling_data): - walker_record["walker_idx"] = np.array([walker_idx]) - walker_record["step_idx"] = np.array([0]) - walker_record["walker_idx"] = np.array([walker_record["walker_idx"]]) - walker_record["decision_id"] = np.array([walker_record["decision_id"]]) - walker_record["target_idxs"] = np.array([walker_record["walker_idx"]]) - - # we only have one step so our resampling_records are just the - # single list of walker actions - resampling_data = resampling_data - - # there is no change in state in the resampler so there are no - # resampler records - resampler_data = [{}] - - # the resampled walkers are just the walkers - - self._resample_cleanup( - resampling_data=resampling_data, - resampler_data=resampler_data, - walkers=walkers, - ) - - return walkers, resampling_data, resampler_data - - def _init_walker_actions(self, n_walkers): - """Returns a list of default resampling records for a single - resampling step. - - Parameters - ---------- - n_walkers : int - The number of walkers to generate records for - - Returns - ------- - decision_records : list of dict of str: value - A list of default decision records for one step of - resampling. - - """ - # determine resampling actions - walker_actions = [ - self.decision.record(enum_value=self.decision.default_decision().value) - for i in range(n_walkers) - ] - - return walker_actions diff --git a/tests/unit/test_resampling/test_decisions/test_clone_merge.py b/tests/unit/test_resampling/test_decisions/test_clone_merge.py new file mode 100644 index 00000000..4c93933b --- /dev/null +++ b/tests/unit/test_resampling/test_decisions/test_clone_merge.py @@ -0,0 +1,188 @@ +import pytest +import attrs + +from wepy.walker import Walker, WalkerState +from wepy.resampling.decisions.clone_merge import ( + MultiCloneMergeDecision, + CloneMergeDecisionEnum, +) + + +class TestMultiCloneMergeDecision: + + def test_action(self): + walker_1 = Walker( + state=WalkerState(a=1), + weight=1.0, + ) + + walker_2 = Walker( + state=WalkerState(a=2), + weight=1.0, + ) + walker_3 = Walker( + state=WalkerState(a=3), + weight=1.0, + ) + + walkers = [ + walker_1, + walker_2, + ] + + # unknown decision number + with pytest.raises(ValueError): + MultiCloneMergeDecision.action( + walkers, + [ + [ + { + "decision_id": 1000, + "target_idxs": [0], + }, + { + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [1], + }, + ] + ], + ) + + assert ( + MultiCloneMergeDecision.action( + walkers, + [ + [ + { + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [0], + }, + { + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [1], + }, + ] + ], + ) + == walkers + ) + + # reorder + assert MultiCloneMergeDecision.action( + walkers, + [ + [ + { + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [1], + }, + { + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [0], + }, + ] + ], + ) == [walker_2, walker_1] + + # multiple assignment to same slot + with pytest.raises(ValueError): + MultiCloneMergeDecision.action( + walkers, + [ + [ + { + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [0], + }, + { + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [0], + }, + ] + ], + ) + + # TODO: this should be a more explicit error + # + # squashing without filling a slot is an error + with pytest.raises(KeyError): + MultiCloneMergeDecision.action( + walkers, + [ + [ + { + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [0], + }, + { + "decision_id": CloneMergeDecisionEnum.SQUASH, + "target_idxs": [1], + }, + ] + ], + ) + with pytest.raises(KeyError): + MultiCloneMergeDecision.action( + walkers, + [ + [ + { + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [0], + }, + { + "decision_id": CloneMergeDecisionEnum.SQUASH, + "target_idxs": [0], + }, + ] + ], + ) + + # provide a keep merge target, but leave a slot open... + with pytest.raises(ValueError): + MultiCloneMergeDecision.action( + walkers, + [ + [ + { + "decision_id": CloneMergeDecisionEnum.KEEP_MERGE, + "target_idxs": [0], + }, + { + "decision_id": CloneMergeDecisionEnum.SQUASH, + "target_idxs": [0], + }, + ] + ], + ) + + assert MultiCloneMergeDecision.action( + [ + walker_1, + walker_2, + walker_3, + ], + [ + [ + { + "decision_id": CloneMergeDecisionEnum.CLONE, + "target_idxs": [0, 2], + }, + { + "decision_id": CloneMergeDecisionEnum.KEEP_MERGE, + "target_idxs": [1], + }, + { + "decision_id": CloneMergeDecisionEnum.SQUASH, + "target_idxs": [1], + }, + ] + ], + ) == [ + attrs.evolve(walker_1, weight=0.5), + attrs.evolve( + walker_2, + weight=2.0, + ), + attrs.evolve(walker_1, weight=0.5), + ] diff --git a/tests/unit/test_resampling/test_decisions/test_decision.py b/tests/unit/test_resampling/test_decisions/test_decision.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/test_resampling/test_decisions/test_no_decision.py b/tests/unit/test_resampling/test_decisions/test_no_decision.py new file mode 100644 index 00000000..bdaa337c --- /dev/null +++ b/tests/unit/test_resampling/test_decisions/test_no_decision.py @@ -0,0 +1,57 @@ +from wepy.walker import Walker, WalkerState +from wepy.resampling.decisions.no_decision import NoDecision, NothingDecisionEnum + + +class TestNoDecision: + + def test_action(self): + + walker_1 = Walker( + state=WalkerState(a=1), + weight=1.0, + ) + + walker_2 = Walker( + state=WalkerState(a=2), + weight=1.0, + ) + + walkers = [ + walker_1, + walker_2, + ] + + assert ( + NoDecision.action( + walkers, + [ + [ + { + "decision_id": NothingDecisionEnum.NOTHING, + "target_idxs": [0], + }, + { + "decision_id": NothingDecisionEnum.NOTHING, + "target_idxs": [1], + }, + ] + ], + ) + == walkers + ) + + NoDecision.action( + walkers, + [ + [ + { + "decision_id": NothingDecisionEnum.NOTHING, + "target_idxs": [1], + }, + { + "decision_id": NothingDecisionEnum.NOTHING, + "target_idxs": [0], + }, + ] + ], + ) == [walker_2, walker_1] diff --git a/tests/unit/test_resampling/test_resamplers/test_noresampler.py b/tests/unit/test_resampling/test_resamplers/test_noresampler.py new file mode 100644 index 00000000..31bb16d2 --- /dev/null +++ b/tests/unit/test_resampling/test_resamplers/test_noresampler.py @@ -0,0 +1,47 @@ +from wepy.resampling.resamplers.noresampler import NoResampler +from wepy.resampling.decisions.no_decision import NothingDecisionEnum +from wepy.walker import Walker, WalkerState + + +class TestNoResampler: + + def test_resample(self): + + walker_1 = Walker( + state=WalkerState(a=1), + weight=1.0, + ) + + walker_2 = Walker( + state=WalkerState(a=2), + weight=1.0, + ) + walker_3 = Walker( + state=WalkerState(a=3), + weight=1.0, + ) + + walkers = [walker_1, walker_2, walker_3] + + resampler = NoResampler() + + assert resampler.resample(walkers) == ( + walkers, + [ + [ + dict( + decision_id=NothingDecisionEnum.NOTHING.value, + target_idxs=[0], + ), + dict( + decision_id=NothingDecisionEnum.NOTHING.value, + target_idxs=[1], + ), + dict( + decision_id=NothingDecisionEnum.NOTHING.value, + target_idxs=[2], + ), + ] + ], + [{}], + ) diff --git a/tests/unit/test_resampling/test_resamplers/test_resampler.py b/tests/unit/test_resampling/test_resamplers/test_resampler.py new file mode 100644 index 00000000..e69de29b From 7ca7a302bd555a8edeb6542fe3cdb7ed591e1561 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 26 Nov 2025 12:15:43 -0500 Subject: [PATCH 018/143] fixup! refactor and add tests for decisions and basic NoResampler --- src/wepy/resampling/resamplers/noresampler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py index 001765f9..4318f6da 100644 --- a/src/wepy/resampling/resamplers/noresampler.py +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -11,7 +11,6 @@ class NoResamplerResamplingData(TypedDict): - walker_idx: NDArray[Shape["1"], Integer] step_idx: NDArray[Shape["1"], Integer] decision_id: NDArray[Shape["1"], Integer] target_idxs: NDArray[Shape["1"], Integer] From ed4637736ca9f40ffaafe6599ec7f38c94d5a9de Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 26 Nov 2025 14:53:34 -0500 Subject: [PATCH 019/143] add proc pool work mapper Various other wip refactoring --- pyproject.toml | 4 +- src/wepy/runners/openmm.py | 12 + src/wepy/work_mapper/base.py | 168 +++ src/wepy/work_mapper/mapper.py | 1007 +---------------- src/wepy/work_mapper/proc_pool_mapper.py | 111 ++ src/wepy/work_mapper/serial.py | 100 ++ src/wepy/work_mapper/task_mapper.py | 674 +++++------ src/wepy/work_mapper/worker.py | 8 - src/wepy/work_mapper/worker_mapper.py | 763 +++++++++++++ tests/unit/test_work_mapper/test_mapper.py | 235 ---- .../test_work_mapper/test_proc_pool_mapper.py | 106 ++ tests/unit/test_work_mapper/test_serial.py | 63 ++ uv.lock | 404 ++----- 13 files changed, 1756 insertions(+), 1899 deletions(-) create mode 100644 src/wepy/work_mapper/base.py create mode 100644 src/wepy/work_mapper/proc_pool_mapper.py create mode 100644 src/wepy/work_mapper/serial.py delete mode 100644 src/wepy/work_mapper/worker.py create mode 100644 src/wepy/work_mapper/worker_mapper.py delete mode 100644 tests/unit/test_work_mapper/test_mapper.py create mode 100644 tests/unit/test_work_mapper/test_proc_pool_mapper.py create mode 100644 tests/unit/test_work_mapper/test_serial.py diff --git a/pyproject.toml b/pyproject.toml index 35ea0f8c..8013fdf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ name = "wepy" description = "Weighted Ensemble Framework" readme = { "file" = "README.md", "content-type" = "text/plain" } license = "MIT" -requires-python = ">=3.11" +requires-python = ">=3.12" authors = [ { name = "Samuel Lotz", email = "samuel.lotz@salotz.info" }, @@ -23,7 +23,7 @@ classifiers = [ dependencies = [ "attrs", - "numpy", + "numpy>=2", "nptyping", "h5py>=3", "networkx", diff --git a/src/wepy/runners/openmm.py b/src/wepy/runners/openmm.py index d4fc9b93..4539f2a6 100644 --- a/src/wepy/runners/openmm.py +++ b/src/wepy/runners/openmm.py @@ -33,6 +33,7 @@ import time from copy import copy from warnings import warn +import copy # Third Party Library import attrs @@ -876,6 +877,13 @@ class OpenMMWalker(WalkerABC): PlatformKwargs = dict[str, str] +class OpenMMRunnerSegmentSplitTimes(TypedDict): + gen_sim_time: float + steps_time: float + get_state_time: float + run_segment_time: float + + # the runner for the simulation which runs the actual dynamics class OpenMMRunner(Runner): """Runner for OpenMM simulations.""" @@ -1239,6 +1247,10 @@ def run_segment( return new_walker + def last_cycle_segments_split_times(self) -> OpenMMRunnerSegmentSplitTimes: + + return copy.deepcopy(self._last_cycle_segments_split_times) + def gen_sim_state( positions: AtomNDArray, diff --git a/src/wepy/work_mapper/base.py b/src/wepy/work_mapper/base.py new file mode 100644 index 00000000..7e0d2f43 --- /dev/null +++ b/src/wepy/work_mapper/base.py @@ -0,0 +1,168 @@ +"""Base classes and definitions for all work mappers.""" + +# Standard Library +import traceback +from typing import Callable, Literal, Generic, TypeVar, Protocol, Any +import logging + +# Standard Library + +from wepy.walker import WalkerState + +logger = logging.getLogger(__name__) + +AnyWalkerState = TypeVar("AnyWalkerState", bound=WalkerState) + + +class WorkMapper(Protocol[AnyWalkerState]): + + def init( + self, + segment_func: Callable[ + tuple[ + AnyWalkerState, + ..., + ], + AnyWalkerState, + ] + ) -> None: + ... + + def map( + self, + walker_states: list[AnyWalkerState], + *args: list[list[Any]], + **kwargs: dict[str, list[Any]], + ) -> list[AnyWalkerState]: ... + + def get_worker_segment_times(self) -> dict[int, list[float]] | None: ... + + def cleanup(self) -> None: ... + +class WrapperException(Exception): + """Exception used for wrapping another exception. + + Since tracebacks can't be pickled we format it and save that + instead. + + """ + + def __init__( + self, + message, + # must be kwargs so we can pickle it (I know weird...) + wrapped_exception=None, + tb=None, + ): + super().__init__(message) + + # save the exception with the traceback + self.wrapped_exception = wrapped_exception + self.formatted_tb = traceback.format_tb(tb) + + +class TaskException(WrapperException): + pass + +class Task: + """Class that composes a function and arguments.""" + + def __init__(self, func, *args, **kwargs): + """Constructor for Task. + + Parameters + ---------- + func : callable + Function to be called on the arguments. + + *args + The arguments to pass to func + + """ + self.args = args + self.kwargs = kwargs + self.func = func + + def __call__(self, **worker_kwargs): + """Makes the Task itself callable.""" + + # run the function passing in the args for running it and any + # worker information in the worker kwargs. + return self.func(*self.args, **self.kwargs, **worker_kwargs) + + +# class ABCMapper: +# """Abstract base class for a Mapper.""" + +# def __init__( +# self, +# segment_func: SegmentFunc | None =None, +# **kwargs: dict[str, Any], +# ) -> None: +# """Constructor for the Mapper class. No arguments are required. + +# Parameters +# ---------- + +# segment_func : Set a default segment_func. Typically set at +# runtime. + +# """ + +# self._func = segment_func + +# self._attributes = kwargs + +# @property +# def attributes(self) -> dict[str, Any]: +# return self._attributes + +# def init( +# self, +# segment_func: SegmentFunc | None = None, +# **kwargs: dict[str, Any], +# ) -> None: +# """Runtime initialization and setting of function to map over walkers. + +# Parameters +# ---------- +# segment_func : callable implementing the Runner.run_segment interface + +# """ + +# if self.segment_func is not None and segment_func is not None: +# logger.info( +# "overriding default segment_func {} with {}".format( +# self._func, segment_func +# ) +# ) +# self._func = segment_func + +# elif self.segment_func is None and segment_func is None: +# ValueError("segment_func must be given since no default specified") + +# elif self.segment_func is None and segment_func is not None: +# self._func = segment_func + +# @property +# def segment_func(self) -> SegmentFunc: +# """The function that will be called for new data in the `map` method.""" +# return self._func + +# def cleanup(self, **kwargs: dict[str, Any]) -> None: +# """Runtime post-simulation tasks. + +# This is run either at the end of a successful simulation or +# upon an error in the main process of the simulation manager +# call to `run_cycle`. + +# The Mapper class performs no actions here and all arguments +# are ignored. + +# """ + +# # nothing to do +# pass + +# def map(self, walkers: list[Walker], *args: list[list[Any]], **kwargs: dict[list[Any]]) -> list[Walker]: +# raise NotImplementedError diff --git a/src/wepy/work_mapper/mapper.py b/src/wepy/work_mapper/mapper.py index 0d75801d..8ba1df6d 100644 --- a/src/wepy/work_mapper/mapper.py +++ b/src/wepy/work_mapper/mapper.py @@ -1,13 +1,8 @@ -"""Reference implementations, abstract base classes, and a production -ready worker style mapper for mapping runner dynamics to walkers for -wepy simulation cycles. +"""Reference implementation of serial work mapper.""" -""" - -# Standard Library +from typing import Callable, Literal, Generic, TypeVar, Protocol, Any import logging -logger = logging.getLogger(__name__) # Standard Library import multiprocessing as mp import queue as pyq @@ -17,243 +12,27 @@ import traceback from warnings import warn -PY_MAP = map - - -class ABCMapper: - """Abstract base class for a Mapper.""" - - def __init__(self, segment_func=None, **kwargs): - """Constructor for the Mapper class. No arguments are required. - - Parameters - ---------- - segment_func : callable, optional - Set a default segment_func. Typically set at runtime. - - """ - - self._func = segment_func - - self._attributes = kwargs - - @property - def attributes(self): - return self._attributes - - def init(self, segment_func=None, **kwargs): - """Runtime initialization and setting of function to map over walkers. - - Parameters - ---------- - segment_func : callable implementing the Runner.run_segment interface - - """ - - if self.segment_func is not None and segment_func is not None: - logger.info( - "overriding default segment_func {} with {}".format( - self._func, segment_func - ) - ) - self._func = segment_func - - elif self.segment_func is None and segment_func is None: - ValueError("segment_func must be given since no default specified") - - elif self.segment_func is None and segment_func is not None: - self._func = segment_func - - @property - def segment_func(self): - """The function that will be called for new data in the `map` method.""" - return self._func - - def cleanup(self, **kwargs): - """Runtime post-simulation tasks. - - This is run either at the end of a successful simulation or - upon an error in the main process of the simulation manager - call to `run_cycle`. - - The Mapper class performs no actions here and all arguments - are ignored. - - """ - - # nothing to do - pass - - def map(self, *args, **kwargs): - raise NotImplementedError - - -class Mapper(ABCMapper): - """Basic non-parallel reference implementation of a mapper.""" - - def __init__(self, segment_func=None, **kwargs): - """Constructor for the Mapper class. No arguments are required. - - Parameters - ---------- - segment_func : callable, optional - Set a default segment_func. Typically set at runtime. - - """ - - super().__init__(segment_func=segment_func, **kwargs) - - self._worker_segment_times = {0: []} - - def map(self, *args, **kwargs): - """Map the 'segment_func' to args. - - Parameters - ---------- - *args : list of list - Each element is the argument to one call of 'segment_func'. - - Returns - ------- - results : list - The results of each call to 'segment_func' in the same order as input. - - Examples - -------- - >>> Mapper(segment_func=sum).map([(0,1,2), (3,4,5)]) - [3, 12] - - """ - - # expand the generators for the args and kwargs - args = [list(arg) for arg in args] - kwargs = {key: list(kwarg) for key, kwarg in kwargs.items()} - - segment_times = [] - results = [] - for arg_idx in range(len(args[0])): - start = time.time() - - # get just the args for this call to func - call_args = [arg[arg_idx] for arg in args] - call_kwargs = {key: value[arg_idx] for key, value in kwargs.items()} - - # run the task, catch any errors and reraise as a - # TaskException to satisfy the pattern - try: - result = self._func(*call_args, **call_kwargs) - - except Exception as task_exception: - # get the traceback for the exception - tb = sys.exc_info()[2] - - msg = "Exception '{}({})' caught in a task.".format( - type(task_exception).__name__, task_exception - ) - traceback_log_msg = """Traceback: --------------------------------------------------------------------------------- -{} --------------------------------------------------------------------------------- - """.format( - "".join( - traceback.format_exception( - type(task_exception), task_exception, tb - ) - ), - ) - - logger.critical(msg + "\n" + traceback_log_msg) - - # raise a TaskException to distinguish it from the worker - # errors with the metadata about the original exception - - raise TaskException( - "Error occured during task execution, recovery not possible.", - wrapped_exception=task_exception, - tb=tb, - ) - - end = time.time() - segment_time = end - start - segment_times.append(segment_time) - - results.append(result) - - self._worker_segment_times[0] = segment_times - - return results - - @property - def worker_segment_times(self): - """The run timings for each segment for each walker. - - Returns - ------- - worker_seg_times : dict of int : list of float - Dictionary mapping worker indices to a list of times in - seconds for each segment run. - - """ - return self._worker_segment_times - - -class Task: - """Class that composes a function and arguments.""" - - def __init__(self, func, *args, **kwargs): - """Constructor for Task. - - Parameters - ---------- - func : callable - Function to be called on the arguments. - - *args - The arguments to pass to func - - """ - self.args = args - self.kwargs = kwargs - self.func = func - - def __call__(self, **worker_kwargs): - """Makes the Task itself callable.""" - - # run the function passing in the args for running it and any - # worker information in the worker kwargs. - return self.func(*self.args, **self.kwargs, **worker_kwargs) +from wepy.walker import Walker +from wepy.work_mapper.base import WorkMapper, AnyWalker +logger = logging.getLogger(__name__) -class WrapperException(Exception): - """Exception used for wrapping another exception. - Since tracebacks can't be pickled we format it and save that - instead. - """ - def __init__( - self, - message, - # must be kwargs so we can pickle it (I know weird...) - wrapped_exception=None, - tb=None, - ): - super().__init__(message) - # save the exception with the traceback - self.wrapped_exception = wrapped_exception - self.formatted_tb = traceback.format_tb(tb) -class TaskException(WrapperException): - pass class ABCWorkerMapper(ABCMapper): def __init__( - self, num_workers=None, segment_func=None, proc_start_method="fork", **kwargs - ): + self, + num_workers: int | None = None, + segment_func: SegmentFunc = None, + proc_start_method: Literal["fork", "spawn", "forkserver"] = "fork", + **kwargs, + ) -> None: """Constructor for WorkerMapper. Parameters @@ -357,767 +136,3 @@ def _make_task(self, *args, **kwargs): """ return Task(self._func, *args, **kwargs) - - -# ---------------------------------- -# everything below this logically belongs in worker.py and should be imported from there - - -class WorkerException(WrapperException): - pass - - -class WorkerKilledError(ChildProcessError): - pass - - -# TODO: move this class to the wepy.work_mapper.worker class where it -# belongs. It shouldn't be in this namespace, but we will leave it -# here. Furthermore I would like to rename it since we now have -# different worker mapper implementations with different concurrency -# models -class WorkerMapper(ABCWorkerMapper): - """Work mapper implementation using multiple worker processes and task - queue. - - Uses the python multiprocessing module to spawn multiple worker - processes which watch a task queue of walker segments. - """ - - def __init__( - self, - num_workers=None, - worker_type=None, - worker_attributes=None, - segment_func=None, - **kwargs, - ): - """Constructor for WorkerMapper. - - Parameters - ---------- - num_workers : int - The number of worker processes to spawn. - - worker_type : callable, optional - Callable that generates an object implementing the Worker - interface, typically a type from a Worker class. - - worker_attributes : dictionary - A dictionary of values that are passed to the worker - constructor as key-word arguments. - - segment_func : callable, optional - Set a default segment_func. Typically set at runtime. - - """ - - super().__init__(num_workers=num_workers, segment_func=segment_func, **kwargs) - - # since the workers will be their own process classes we - # handle this data - - # attributes that will be passed to the worker constructors - if worker_attributes is not None: - self._worker_attributes = worker_attributes - else: - self._worker_attributes = {} - - # choose the type of the worker - if worker_type is None: - self._worker_type = Worker - warn("worker_type not given using the default base class") - logger.warn("worker_type not given using the default base class") - else: - self._worker_type = worker_type - - @property - def worker_type(self): - """The callable that generates a worker object. - - Typically this is just the type from the class definition of - the Worker where the constructor is called. - - """ - return self._worker_type - - def init(self, num_workers=None, segment_func=None, **kwargs): - """Runtime initialization and setting of function to map over walkers. - - Parameters - ---------- - num_workers : int - The number of worker processes to spawn - - segment_func : callable implementing the Runner.run_segment interface - - """ - - super().init(num_workers=num_workers, segment_func=segment_func, **kwargs) - - manager = self._mp_ctx.Manager() - - # Establish communication queues - - # A queue for errors - self._exception_queue = manager.Queue() - - # queue for the tasks we know the batch size so we don't need - # a JoinableQueue - self._task_queue = manager.Queue() - - # results queue - self._result_queue = manager.Queue() - - # use pipes for communication channels between this parent - # process and the children for sending specific interrupts - # such as the signal to kill them. Note that the clean way to - # end the process is to send poison pills on the task queue, - # this is for other stuff. IRQ is a common abbreviation for - # interrupts - self._irq_parent_conns = [] - - # Start workers, giving them all the queues - self._workers = [] - for i in range(self.num_workers): - # make a pipe to communicate with this worker for the int - parent_conn, child_conn = self._mp_ctx.Pipe() - self._irq_parent_conns.append(parent_conn) - - # create the worker giving it all of the communication - # channels - worker = self.worker_type( - i, - self._task_queue, - self._result_queue, - self._exception_queue, - child_conn, - mapper_attributes=self._attributes, - **self._worker_attributes, - ) - self._workers.append(worker) - - # start the worker processes - for worker in self._workers: - worker.start() - - logger.info( - "Worker process started as name: {}; PID: {}".format( - worker.name, worker.pid - ) - ) - - # now that we have started the processes register the handler - # for SIGTERM signals that will clean up our children cleanly - signal.signal(signal.SIGTERM, self._sigterm_shutdown) - - def _sigterm_shutdown(self, signum, frame): - logger.critical("Received external SIGTERM, forcing shutdown.") - - self.force_shutdown() - - def force_shutdown(self, **kwargs): - logger.critical("Forcing shutdown") - - # our primary job is to shut down all of the running processes - # without just shutting down the queues and breaking the pipes - - # to do this we send the kill signals to them on the kill - # channel. - - for worker_idx, worker in enumerate(self._workers): - logger.critical( - "Sending SIGTERM message on {} to worker {}".format( - self._irq_parent_conns[worker_idx].fileno(), worker_idx - ) - ) - - # send a kill message to the worker - self._irq_parent_conns[worker_idx].send(signal.SIGTERM) - - logger.critical("All kill messages sent to workers") - - # check that all have exited - alive_workers = [worker.is_alive() for worker in self._workers] - worker_acks = {} - worker_exitcodes = {} - premature_exit = False - while any(alive_workers) and not premature_exit: - for worker_idx, worker in enumerate(self._workers): - # ignore already known dead workers - if not alive_workers[worker_idx]: - continue - - if worker.is_alive(): - # if it is still alive and we have an ack from it - # just terminate. There is a bug in the code and - # is out of our control - if worker_idx in worker_acks: - logger.debug( - "Ack received from {} but has not shut down".format( - worker.name - ) - ) - premature_exit = True - - # otherwise we need to try and receive the ack - elif self._irq_parent_conns[worker_idx].poll(1): - # receive the acknowledgement - ack = self._irq_parent_conns[worker_idx].recv() - - logger.debug( - "Received {} acknowledgement from {}".format( - ack, worker.name - ) - ) - - # make sure the ack is affirmative - if ack is True: - worker_acks[worker_idx] = ack - - # if it is an exeption wrap it as a worker - # error and use the os to kill the process - elif issubclass(type(ack), Exception): - # wrap it as a worker exception - exception = WorkerException(wrapped_exception=ack) - worker_acks[worker_idx] = exception - - logger.critical( - "{} not responding, terminating with SIGTERM".format( - worker.name - ) - ) - - worker.terminate() - - else: - alive_workers[worker_idx] = False - worker_exitcodes[worker_idx] = worker.exitcode - - if any(alive_workers): - logger.critical( - "Terminating main process with running workers {}".format( - ",".join( - [ - str(worker_idx) - for worker_idx in range(len(self._workers)) - if alive_workers[worker_idx] - ] - ) - ) - ) - - def cleanup(self, **kwargs): - """Runtime post-simulation tasks. - - This is run either at the end of a successful simulation or - upon an error in the main process of the simulation manager - call to `run_cycle`. - - The Mapper class performs no actions here and all arguments - are ignored. - - """ - - super().cleanup(**kwargs) - - # send poison pills (Stop signals) to the queues to stop them in a nice way - # and let them finish up - for i in range(self.num_workers): - self._task_queue.put((None, None)) - - # delete the queues and workers - self._task_queue = None - self._result_queue = None - self._workers = None - - def map(self, *args, **kwargs): - # docstring in superclass - - map_process = self._mp_ctx.current_process() - logger.info( - "Mapping from process {}; PID {}".format(map_process.name, map_process.pid) - ) - - # make tuples for the arguments to each function call - task_args = zip(*args) - kwargs = {key: list(kwarg) for key, kwarg in kwargs.items()} - - num_tasks = len(args[0]) - # Enqueue the jobs - for task_idx, task_arg in enumerate(task_args): - task_kwargs = {key: value[task_idx] for key, value in kwargs.items()} - - # a task will be the actual task and its task idx so we can - # sort them later - self._task_queue.put((task_idx, self._make_task(*task_arg, **task_kwargs))) - - logger.info("Waiting for tasks to be run") - - # poll the exception and result queues for results - n_results_left = num_tasks - results = [] - while n_results_left > 0: - # first check if any errors came back but don't wait, - # since the methods for querying whether it is empty or - # not are not reliable we just try and if we don't get - # anything we will come back around - try: - proc_name, pid, exception = self._exception_queue.get_nowait() - except pyq.Empty: - pass - - else: - logger.error( - "Exception occured in process {}; pid {}.".format(proc_name, pid) - ) - - # we can handle Task and Worker exceptions differently - if type(exception) == TaskException: - logger.critical( - "Exception encountered in a task which is unrecoverable." - "You will need to reconfigure your components in a stable manner." - ) - - self.force_shutdown() - - logger.critical("Shutdown complete.") - raise exception - - elif type(exception) == WorkerException: - # we make just an error message to say that errors - # in the worker may be due to the network or - # something and could recover - logger.error( - "Exception encountered in the work mapper worker process." - "Recovery possible, see further messages." - ) - - # However, the current implementation doesn't - # support retries or whatever so we issue a - # critical log informing that it has been elevated - # to critical and will force shutdown - logger.critical( - "Worker error mode resiliency not supported at this time." - "Performing force shutdown and simulation ending." - ) - - self.force_shutdown() - - logger.critical("Shutdown complete.") - raise exception - - else: - logger.critical("Unknown exception encountered.") - - self.force_shutdown() - - logger.critical("Shutdown complete.") - - raise exception - - # attempt to get something off of the results queue - try: - result = self._result_queue.get_nowait() - except pyq.Empty: - pass - - # if we get something handle it - else: - logger.info("Retrieved result: {}".format(result)) - results.append(result) - - # reduce the counter so we know when we are done - n_results_left -= 1 - - # sort the results according to their task_idx - results.sort() - - # save the task run times, so they can be accessed if desired, - # after clearing the task times from the last mapping - - # DEBUG: removing this because it should be set on init() - # self._worker_segment_times = {i : [] for i in range(self.num_workers)} - - for task_idx, worker_idx, task_time, result in results: - self._worker_segment_times[worker_idx].append(task_time) - - # then just return the values of the function - return [result for task_idx, worker_idx, task_time, result in results] - - -# same for the worker in terms of refactoring -class Worker(mp.Process): - """Worker process. - - This is a subclass of process with an overriden `__init__` - constructor that will automatically generate the Process. - - When this class is constructed a new process will be formed. - - """ - - NAME_TEMPLATE = "Worker-{}" - """A string formatting template to identify worker processes in - logs. The field will be filled with the worker index.""" - - def __init__( - self, - worker_idx, - task_queue, - result_queue, - exception_queue, - interrupt_connection, - mapper_attributes=None, - log_level="INFO", - **kwargs, - ): - """Constructor for the Worker class. - - Parameters - ---------- - worker_idx : int - The index of the worker. Should be unique. - - task_queue : multiprocessing.JoinableQueue - The shared task queue the worker will watch for new tasks to complete. - - result_queue : multiprocessing.Queue - The shared queue that completed task results will be placed on. - - interrupt_connection : multiprocessing.Connection - One end of a pipe to listen for messages specific to this worker. - - mapper_attributes : None or dict - A dictionary of the attributes of the mapper for reference in workers. - - kwargs : - The worker specific attributes - - """ - - # call the Process constructor - mp.Process.__init__(self, name=self.NAME_TEMPLATE.format(worker_idx)) - - self._exception_queue = exception_queue - self._exception = None - self._traceback = None - - # the queue that will trigger a shutdown in the event of failure - self._irq_channel = interrupt_connection - - # also register the SIGTERM signal handler for graceful - # shutdown with reporting to mapper - signal.signal(signal.SIGTERM, self._sigterm_shutdown) - - self._worker_idx = worker_idx - - self._mapper_attributes = mapper_attributes - - # set all the kwargs into an attributes dictionary - self._attributes = kwargs - - # the queues for work to be done and work done - self._task_queue = task_queue - self._result_queue = result_queue - - logger.debug("{} process created".format(self.name)) - - @property - def worker_idx(self): - """Dictionary of attributes of the worker.""" - return self._worker_idx - - @property - def attributes(self): - """Dictionary of attributes of the worker.""" - return self._attributes - - @property - def mapper_attributes(self): - """Dictionary of attributes of the worker.""" - return self._mapper_attributes - - def run(self): - logger.debug("{}: starting to run".format(self.name)) - - # try to run the worker and it's task, except either class of - # error that can come from it either from the worker - # (WorkerException) or the task (TaskException) and communicate it - # back to the main process - - # if we get an exception there is some cleanup logic - run_exception = None - - try: - # run the worker, which will retrieve its task from the - # queue attempt to run the task, and if it succeeds will - # put the results on the result queue, if the task fails - # it will catch it and wrap it as a task exception - self._run_worker() - - except TaskException as task_exception: - logger.error("{}: TaskException caught".format(self.name)) - - run_exception = task_exception - - # anything else is considered a WorkerException so take the - # original exception and generate a worker exception from that - except Exception as exception: - logger.debug("{}: WorkerError caught".format(self.name)) - - # get the traceback - tb = sys.exc_info()[2] - - msg = "Exception '{}({})' caught in a worker.".format( - type(exception).__name__, exception - ) - traceback_log_msg = """Traceback: --------------------------------------------------------------------------------- -{} --------------------------------------------------------------------------------- - """.format( - "".join(traceback.format_exception(type(exception), exception, tb)), - ) - - logger.error("{}:".format(self.name) + msg + "\n" + traceback_log_msg) - - # raise a TaskError to distinguish it from the worker - # errors with the metadata about the original exception - - worker_exception = WorkerException( - "Error occured during worker execution.", - wrapped_exception=exception, - tb=tb, - ) - - run_exception = worker_exception - - # raise worker_exception - if run_exception is not None: - logger.debug("{}: Putting exception on exception queue".format(self.name)) - - # then put the exception and the traceback onto the queue - # so we can communicate back to the parent process - try: - self._exception_queue.put((self.name, self.pid, run_exception)) - except BrokenPipeError as exc: - logger.error( - "Pipe is broken indicating the root process has already exited:\n{}".format( - exc - ) - ) - - # TODO: not sure if this is good or not - # then reraise the exception so it can be caught - # raise run_exception - - def _sigterm_shutdown(self, signum, frame): - logger.debug("Received external SIGTERM kill command.") - - logger.debug("Alerting mapper that this will be honored.") - - # send an error to the mapper that the worker has been killed - self._irq_channel.send( - WorkerKilledError( - "{} (pid: {}) killed by external SIGTERM signal".format( - self.name, self.pid - ) - ) - ) - - logger.debug("Acknowledgment sent") - - logger.debug("Shutting down process") - - def _shutdown(self): - logger.debug("Received SIGTERM kill command from mapper") - - logger.debug("Acknowledging kill request will be honored") - - # report back that we are shutting down with a True - self._irq_channel.send(True) - - logger.debug("Acknowledgment sent") - - logger.debug("Shutting down process") - - def _run_worker(self): - # run the logic associated with communication and liveness of - # the worker process itself, this is not necessarily a fatal - # (critical) error and restarting a worker might resolve the - # problem. This calls the _run_task method though which is - # always critical since the logic in the code cannot be - # disputed - - # TODO remove when confirmed that this works - # worker_process = mp.current_process() - logger.info( - "{}: Worker process started as name: {}; PID: {}".format( - self.name, self.name, self.pid - ) - ) - - while True: - # check to see if there is any signals in the interrupt channel - if self._irq_channel.poll(): - # get the message - message = self._irq_channel.recv() - - logger.debug( - "{}: Received message from mapper on filehandle {}: {}".format( - self.name, self._irq_channel.fileno(), message - ) - ) - - # handle the message - - # a SIGTERM is a signal to kill the process - # unconditionally - if message is signal.SIGTERM: - self._shutdown() - - # break from the event (while) loop and shut down - break - - # anything is not recognized and we will continue and - # report back that we don't recognize the message with - # a ValueError object - else: - logger.error( - "{}: Message not recognized, continuing operations and" - " sending error to mapper".format(self.name) - ) - self._irq_channel.send( - ValueError( - "Message: {} not recognized continuing operations".format( - message - ) - ) - ) - - # get the next task - try: - task_idx, next_task = self._task_queue.get(block=False, timeout=None) - - logger.debug("{}: Got task {}".format(self.name, task_idx)) - - except pyq.Empty: - task_idx = None - next_task = Ellipsis - - # # check for the poison pill which is the signal to stop - if next_task is None: - logger.info( - "{}: received {} {}: FINISHED".format( - self.name, task_idx, next_task - ) - ) - - # TODO remove since we aren't using joinble queue anymore - # mark the poison pill task as done - # self.task_queue.task_done() - - # and exit the loop - break - - # only execute this if a task was actually receieved from - # the queue; an Ellipsis indicates continue the loop - elif next_task is not Ellipsis: - logger.info( - "{}; task_idx : {}; args : {} ".format( - self.name, task_idx, next_task.args - ) - ) - - # run the task - start = time.time() - - answer = self._run_task(next_task) - - end = time.time() - task_time = end - start - - logger.info( - "{}: task_idx : {}; COMPLETED in {} s".format( - self.name, task_idx, task_time - ) - ) - - # put the results into the results queue with it's task - # index so we can sort them later - self._result_queue.put((task_idx, self.worker_idx, task_time, answer)) - - def run_task(self, task): - """Actually executes the task. - - This default runner simply executes the task thunk. - - This can be customized by subclasses in order to allow for - injection of worker specific data. - - Parameters - ---------- - task : Task object - The partially evaluated task; function plus arguments - - Returns - ------- - task_result - Results of running the task. - - """ - - return task() - - def _run_task(self, task): - """Runs the given task and returns the results. - - This manages handling exceptions and tracebacks from the - actual `run_task` function which is intended to be specialized - by different workers to inject worker specific arguments to - tasks. Such as node and device identification. - - Parameters - ---------- - task : Task object - The partially evaluated task; function plus arguments - - Returns - ------- - task_result - Results of running the task. - - """ - - logger.info("Running task") - try: - return self.run_task(task) - - except Exception as task_exception: - # get the traceback for the exception - tb = sys.exc_info()[2] - - msg = "Exception '{}({})' caught in a task.".format( - type(task_exception).__name__, task_exception - ) - traceback_log_msg = """Traceback: --------------------------------------------------------------------------------- -{} --------------------------------------------------------------------------------- - """.format( - "".join( - traceback.format_exception(type(task_exception), task_exception, tb) - ), - ) - - logger.critical(msg + "\n" + traceback_log_msg) - - # raise a TaskException to distinguish it from the worker - # errors with the metadata about the original exception - - raise TaskException( - "Error occured during task execution, recovery not possible.", - wrapped_exception=task_exception, - tb=tb, - ) diff --git a/src/wepy/work_mapper/proc_pool_mapper.py b/src/wepy/work_mapper/proc_pool_mapper.py new file mode 100644 index 00000000..b2f35d44 --- /dev/null +++ b/src/wepy/work_mapper/proc_pool_mapper.py @@ -0,0 +1,111 @@ +import itertools +import multiprocessing as mp +from typing import Any, Callable, Literal +import logging + +import attrs + +from wepy.work_mapper.base import AnyWalkerState + +logger = logging.getLogger(__name__) + +class ProcPoolMapper: + + num_workers: int + + def __init__( + self, + num_workers: int, + proc_start_method: Literal["fork", "spawn", "forkserver"] = "spawn", + ) -> None: + self.num_workers = num_workers + self.proc_start_method = proc_start_method + + def init( + self, + segment_func: Callable[ + [ + AnyWalkerState, + Any, + ..., + ], + AnyWalkerState, + ], + ) -> None: + + logger.info("Initializing ProcPoolMapper") + + self._func = segment_func + + logger.info(f"Initializing local multiprocessing context with start method: {self.proc_start_method}") + self._mp_ctx = mp.get_context(method=self.proc_start_method) + + def cleanup(self) -> None: + + logger.info("Running ProcPoolMapper cleanup") + logger.info("Nothing to do") + + def map( + self, + walker_states: list[AnyWalkerState], + *args: list[list[Any]], + ) -> list[AnyWalkerState]: + + logger.info(f"Running map on {len(walker_states)} in batches of {self.num_workers}") + + # spin up a new pool for each map + logger.info(f"Starting process Pool with {self.num_workers}") + with self._mp_ctx.Pool( + processes=self.num_workers, + # only run one thing per task, just to make sure + # everything is cleaned up + maxtasksperchild=1, + ) as pool: + + results = [] + for batch_idx, batch in enumerate(itertools.batched( + zip(walker_states, *args, strict=True), + self.num_workers, + strict=False, + )): + + logger.info(f"Submitting batch: {batch_idx}") + + batch_results = [] + for batch_task_idx, batch_args in enumerate(batch): + + task_idx = batch_idx + batch_task_idx + + logger.info(f"Submitting task {task_idx}") + result = pool.apply_async( + self._func, + batch_args, + ) + logger.info(f"Task {task_idx} submitted") + batch_results.append(result) + + logger.info(f"Batch {batch_idx} submitted, awaiting results.") + for batch_task_idx, task_result in enumerate(batch_results): + + task_idx = batch_idx + batch_task_idx + logger.info(f"Awaiting task {task_idx}") + + + try: + real_result = task_result.get() + # TODO: add timeouts and retries + except TimeoutError as exc: + raise exc + except Exception as exc: + raise exc + + results.append(real_result) + + logger.info(f"Retrieved completed results for task: {task_idx}") + + logger.info(f"Batch {batch_idx} completed") + + logger.info(f"Completed all batches, terminating Pool") + + + return results diff --git a/src/wepy/work_mapper/serial.py b/src/wepy/work_mapper/serial.py new file mode 100644 index 00000000..4bbee2d7 --- /dev/null +++ b/src/wepy/work_mapper/serial.py @@ -0,0 +1,100 @@ +"""Reference implementation of a serial WorkMapper""" +import time +import sys +import traceback + +from typing import Callable, Literal, Generic, TypeVar, Protocol, Any +import logging + +from wepy.walker import Walker +from wepy.work_mapper.base import WorkMapper, AnyWalkerState, TaskException + +logger = logging.getLogger(__name__) + + + +class SerialMapper(WorkMapper[AnyWalkerState]): + """Basic non-parallel reference implementation of a mapper.""" + + def __init__( + self, + ) -> None: + """Constructor for the Mapper class. No arguments are required.""" + + self._worker_segment_times: dict[int, list[float]] = {0: []} + + + def get_worker_segment_times(self) -> dict[int, list[float]]: + """The run timings for each segment for each walker. + + Returns + ------- + worker_seg_times : Dictionary mapping worker indices to a list of times in + seconds for each segment run. + + """ + return self._worker_segment_times + + def init( + self, + segment_func: Callable[ + [ + AnyWalkerState, + Any, + ..., + ], + AnyWalkerState, + ], + ) -> None: + + self._func = segment_func + + def cleanup(self) -> None: + pass + + def map( + self, + walker_states: list[AnyWalkerState], + *args: list[list[Any]], + **kwargs: dict[str, list[Any]], + ) -> list[AnyWalkerState]: + """Map the 'segment_func' to args. + + Parameters + ---------- + *args : list of list + Each element is the argument to one call of 'segment_func'. + + Returns + ------- + results : list + The results of each call to 'segment_func' in the same order as input. + + Examples + -------- + >>> Mapper(segment_func=sum).map([(0,1,2), (3,4,5)]) + [3, 12] + + """ + + segment_times: list[float] = [] + results: list[AnyWalkerState] = [] + + for arg_idx, (walker_state, *call_args) in enumerate(zip(walker_states, *args, strict=True)): + + call_kwargs = { + k : values[arg_idx] + for k, values + in kwargs.items() + } + + tic = time.time() + result = self._func(walker_state, *call_args, **call_kwargs) + toc = time.time() + + segment_times.append(toc - tic) + results.append(result) + + self._worker_segment_times[0] = segment_times + + return results diff --git a/src/wepy/work_mapper/task_mapper.py b/src/wepy/work_mapper/task_mapper.py index faeb7c5c..3cac5f4a 100644 --- a/src/wepy/work_mapper/task_mapper.py +++ b/src/wepy/work_mapper/task_mapper.py @@ -1,7 +1,7 @@ # Standard Library import logging +from typing import Literal, Any -logger = logging.getLogger(__name__) # Standard Library import multiprocessing as mp import pickle @@ -13,13 +13,13 @@ from warnings import warn # First Party Library -from wepy.work_mapper.mapper import ( - ABCWorkerMapper, +from wepy.work_mapper.base import ( Task, TaskException, WrapperException, ) +logger = logging.getLogger(__name__) class TaskProcessException(WrapperException): pass @@ -28,327 +28,12 @@ class TaskProcessException(WrapperException): class TaskProcessKilledError(ChildProcessError): pass - -class TaskMapper(ABCWorkerMapper): - """Process-per-task mapper. - - This method of work mapper starts new processes for each runner - segment task that needs to be run. This allows cheap copying of - shared state using the operating system primitives. On linux this - would be either 'fork' (default) or 'spawn'. Fork is cheap but - doesn't initialize certain process namespace things, whereas spawn - is much more expensive but properly cleans things up. Fork should - be sufficient in most cases, however spawn may be needed when you - have some special contexts in the parent process. This is the case - with starting CUDA contexts in the main parent process and then - forking new processes from it. We suggest using fork and avoiding - making these kinds of contexts in the main process. - - This method avoids using shared memory or sending objects through - interprocess communication (that has a serialization and - deserialization cost associated with them) by using OS copying - mechanism. However, a new process will be created each cycle for - each walker in the simulation. So if you want a large number of - walkers you may experience a large overhead. If your walker states - are very small or a very fast serializer is available you may also - not benefit from full process address space copies. Instead the - WorkerMapper may be better suited. - - """ - - def __init__( - self, walker_task_type=None, num_workers=None, segment_func=None, **kwargs - ): - super().__init__(num_workers=num_workers, segment_func=segment_func, **kwargs) - - # choose the type of the worker - if walker_task_type is None: - self._walker_task_type = WalkerTaskProcess - warn("walker_task_type not given using the default base class") - logger.warning("walker_task_type not given using the default base class") - else: - self._walker_task_type = walker_task_type - - # initialize a list to put results in - self.results = None - - # this is meant to be a transient variable, will be initialized and deinitialized - self._walker_processes = None - - def init(self, **kwargs): - super().init(**kwargs) - - # now that we have started the processes register the handler - # for SIGTERM signals that will clean up our children cleanly - signal.signal(signal.SIGTERM, self._sigterm_shutdown) - - def _sigterm_shutdown(self, signum, frame): - logger.critical("Received external SIGTERM, forcing shutdown.") - - self.force_shutdown() - - logger.critical("Shutdown complete.") - - @property - def walker_task_type(self): - """The callable that generates a worker object. - - Typically this is just the type from the class definition of - the Worker where the constructor is called. - - """ - return self._walker_task_type - - def force_shutdown(self): - # send sigterm signals to processes to kill them - for walker_idx, walker_process in enumerate(self._walker_processes): - logger.critical( - "Sending SIGTERM message on {} to worker {}".format( - self._irq_parent_conns[walker_idx].fileno(), walker_idx - ) - ) - - # send a kill message to the worker - self._irq_parent_conns[walker_idx].send(signal.SIGTERM) - - logger.critical("All kill messages sent to workers") - - # wait for the walkers to finish and handle errors in them - # appropriately - alive_walkers = [walker.is_alive() for walker in self._walker_processes] - walker_exitcodes = {} - premature_exit = False - while any(alive_walkers): - for walker_idx, walker in enumerate(self._walker_processes): - if not alive_walkers[walker_idx]: - continue - - if walker.is_alive(): - pass - - # otherwise the walker is done - else: - alive_walkers[walker_idx] = False - walker_exitcodes[walker_idx] = walker.exitcode - - def map(self, *args, **kwargs): - # run computations in a Manager context - with self._mp_ctx.Manager() as manager: - num_walkers = len(args[0]) - - # to manage access to worker resources we use a queue with - # the index of the worker - worker_queue = manager.Queue() - - # put the workers onto the queue - for worker_idx in range(self.num_workers): - worker_queue.put(worker_idx) - - # initialize segment times for workers to - # fill in - worker_segment_times = manager.dict() - - # initialize for the number of workers, since these will be - # the slots to put timing results in - for i in range(self.num_workers): - worker_segment_times[i] = [] - - # make a shared list for the walker results - results = manager.list() - - # since this will be indexed by walker index initialize the - # length of the array - for walker in range(num_walkers): - results.append(None) - - # use pipes for communication channels between this parent - # process and the children for sending specific interrupts - # such as the signal to kill them. Note that the clean way to - # end the process is to send poison pills on the task queue, - # this is for other stuff. IRQ is a common abbreviation for - # interrupts - self._irq_parent_conns = [] - - # unpack the generator for the kwargs - kwargs = {key: list(kwarg) for key, kwarg in kwargs.items()} - - # create the task based processes - self._walker_processes = [] - for walker_idx, task_args in enumerate(zip(*args)): - task_kwargs = {key: value[walker_idx] for key, value in kwargs.items()} - - # make the interrupt pipe - parent_conn, child_conn = self._mp_ctx.Pipe() - self._irq_parent_conns.append(parent_conn) - - # start a process for this walker - walker_process = self.walker_task_type( - walker_idx, - self._attributes, - self._func, - task_args, - task_kwargs, - worker_queue, - results, - worker_segment_times, - child_conn, - ) - - walker_process.start() - - self._walker_processes.append(walker_process) - - new_walkers = [None for _ in range(num_walkers)] - results_found = [False for _ in range(num_walkers)] - while not all(results_found): - # go through the results list and handle the values that may be there - for walker_idx, result in enumerate(results): - if results_found[walker_idx]: - continue - - # logger.info("Checking for walker {}".format(walker_idx)) - - # first check to see if any of the task processes were - # terminated from the system - if self._irq_parent_conns[walker_idx].poll(): - irq = self._irq_parent_conns[walker_idx].recv() - - if issubclass(type(irq), TaskProcessKilledError): - # just terminate if a worker goes down. We - # could handle this better but it is not implemented now - logger.critical( - "Process {} was killed by sigterm, shutting down.".format( - walker_process[walker_idx].name - ) - ) - - logger.info( - "Recovery is possible here, but is not implemented " - "so we opt to fail fast and let you know a problem exists." - "Please use checkpointing to avoid lost data." - ) - - self.force_shutdown() - logger.critical("Shutdown complete.") - - logger.debug( - "Received {} acknowledgement from {}".format( - ack, worker.name - ) - ) - - # if no interrupts were handled we continue - - # if it is None no response has been made at all - # yet, this is the initialized value - if result is None: - pass - - # walker results are returned serialized as - # pickles, they are packed into a tuple so that we - # can associate them with an explicit marker, if - # we have a tuple then we can handle that - # appropriately - elif type(result) == tuple: - logger.debug("Received a results tuple") - - assert ( - len(result) == 2 - ), "Result tuples should be only be (ID, pickle)" - - result_id, payload = result - - # there was a walker successfully returned - if result_id == "Walker": - logger.debug("Received a serialized results walker") - - # deserialize - logger.debug("deserializing") - new_walker = pickle.loads(payload) - - logger.info("Got result for walker {}".format(walker_idx)) - - new_walkers[walker_idx] = new_walker - results_found[walker_idx] = True - - else: - raise ValueError("Unkown result ID: {}".format(result_id)) - - elif issubclass(type(result), TaskException): - logger.critical( - "Exception encountered in a task which is unrecoverable." - "You will need to reconfigure your components in a stable manner." - ) - - self.force_shutdown() - - logger.critical("Shutdown complete.") - raise result - - elif issubclass(type(result), TaskProcessException): - # we make just an error message to say that errors - # in the worker may be due to the network or - # something and could recover - logger.error( - "Exception encountered in the work mapper task process." - "Recovery possible, see further messages." - ) - - # However, the current implementation doesn't - # support retries or whatever so we issue a - # critical log informing that it has been elevated - # to critical and will force shutdown - logger.critical( - "Task process error mode resiliency not supported at this time." - "Performing force shutdown and simulation ending." - ) - - self.force_shutdown() - - logger.critical("Shutdown complete.") - raise result - - elif issubclass(type(result), Exception): - logger.critical( - "Unknown exception {} encountered.".format(result) - ) - - self.force_shutdown() - - logger.critical("Shutdown complete.") - - raise result - - else: - logger.critical( - "Unknown result value {} encountered.".format(result) - ) - - self.force_shutdown() - - logger.critical("Shutdown complete.") - - # save the managed list of the recorded worker times locally - for key, val in worker_segment_times.items(): - self._worker_segment_times[key] = val - - # wait for the processes to end - # for walker in self._walker_processes: - # walker.join() - # logger.info("Joined {}".format(walker.name)) - - # deinitialize the current walker processes - self._walker_processes = None - - return new_walkers - - class WalkerTaskProcess(mp.Process): NAME_TEMPLATE = "Walker-{}" def __init__( self, - walker_idx, + walker_idx: int, mapper_attributes, func, task_args, @@ -664,3 +349,354 @@ def _run_walker(self): self._worker_segment_times[worker_idx] = seg_times logger.info("{}: Exiting normally having completed the task".format(self.name)) + + +# class TaskMapper(ABCWorkerMapper): +# """Process-per-task mapper. + +# This method of work mapper starts new processes for each runner +# segment task that needs to be run. This allows cheap copying of +# shared state using the operating system primitives. On linux this +# would be either 'fork' (default) or 'spawn'. Fork is cheap but +# doesn't initialize certain process namespace things, whereas spawn +# is much more expensive but properly cleans things up. Fork should +# be sufficient in most cases, however spawn may be needed when you +# have some special contexts in the parent process. This is the case +# with starting CUDA contexts in the main parent process and then +# forking new processes from it. We suggest using fork and avoiding +# making these kinds of contexts in the main process. + +# This method avoids using shared memory or sending objects through +# interprocess communication (that has a serialization and +# deserialization cost associated with them) by using OS copying +# mechanism. However, a new process will be created each cycle for +# each walker in the simulation. So if you want a large number of +# walkers you may experience a large overhead. If your walker states +# are very small or a very fast serializer is available you may also +# not benefit from full process address space copies. Instead the +# WorkerMapper may be better suited. + +# """ + +# def __init__( +# self, +# walker_task_type=None, +# num_workers: int | None = None, +# proc_start_method: Literal["fork", "spawn", "forkserver"] = "fork", +# **kwargs +# ): + +# self._attributes = kwargs + +# self._proc_start_method = proc_start_method + +# self._num_workers = num_workers +# self._worker_segment_times = None + +# if num_workers is not None: +# self._worker_segment_times = {i: [] for i in range(self.num_workers)} + +# # choose the type of the worker +# if walker_task_type is None: +# self._walker_task_type = WalkerTaskProcess +# warn("walker_task_type not given using the default base class") +# logger.warning("walker_task_type not given using the default base class") +# else: +# self._walker_task_type = walker_task_type + +# # initialize a list to put results in +# self.results = None + +# # this is meant to be a transient variable, will be initialized and deinitialized +# self._walker_processes = None + +# def init( +# self, +# segment_func, +# num_workers: int | None = None, +# ): + +# # create the multiprocessing context to use for spawning +# # processes here +# self._mp_ctx = mp.get_context(method=self._proc_start_method) + +# # the number of workers must be given here or set as an object attribute +# if num_workers is None and self.num_workers is None: +# raise ValueError( +# "The number of workers must be given, received {}".format(num_workers) +# ) + +# # if the number of walkers was given for this init() call use +# # that, otherwise we use the default that was specified when +# # the object was created +# elif num_workers is not None and self.num_workers is None: +# self._num_workers = num_workers + +# # update the worker segment times +# self._worker_segment_times = {i: [] for i in range(self.num_workers)} + +# # now that we have started the processes register the handler +# # for SIGTERM signals that will clean up our children cleanly +# signal.signal(signal.SIGTERM, self._sigterm_shutdown) + +# def _sigterm_shutdown(self, signum, frame): +# logger.critical("Received external SIGTERM, forcing shutdown.") + +# self.force_shutdown() + +# logger.critical("Shutdown complete.") + +# @property +# def walker_task_type(self): +# """The callable that generates a worker object. + +# Typically this is just the type from the class definition of +# the Worker where the constructor is called. + +# """ +# return self._walker_task_type + +# def force_shutdown(self): +# # send sigterm signals to processes to kill them +# for walker_idx, walker_process in enumerate(self._walker_processes): +# logger.critical( +# "Sending SIGTERM message on {} to worker {}".format( +# self._irq_parent_conns[walker_idx].fileno(), walker_idx +# ) +# ) + +# # send a kill message to the worker +# self._irq_parent_conns[walker_idx].send(signal.SIGTERM) + +# logger.critical("All kill messages sent to workers") + +# # wait for the walkers to finish and handle errors in them +# # appropriately +# alive_walkers = [walker.is_alive() for walker in self._walker_processes] +# walker_exitcodes = {} +# premature_exit = False +# while any(alive_walkers): +# for walker_idx, walker in enumerate(self._walker_processes): +# if not alive_walkers[walker_idx]: +# continue + +# if walker.is_alive(): +# pass + +# # otherwise the walker is done +# else: +# alive_walkers[walker_idx] = False +# walker_exitcodes[walker_idx] = walker.exitcode + +# def map(self, *args, **kwargs): +# # run computations in a Manager context +# with self._mp_ctx.Manager() as manager: +# num_walkers = len(args[0]) + +# # to manage access to worker resources we use a queue with +# # the index of the worker +# worker_queue = manager.Queue() + +# # put the workers onto the queue +# for worker_idx in range(self.num_workers): +# worker_queue.put(worker_idx) + +# # initialize segment times for workers to +# # fill in +# worker_segment_times = manager.dict() + +# # initialize for the number of workers, since these will be +# # the slots to put timing results in +# for i in range(self.num_workers): +# worker_segment_times[i] = [] + +# # make a shared list for the walker results +# results = manager.list() + +# # since this will be indexed by walker index initialize the +# # length of the array +# for walker in range(num_walkers): +# results.append(None) + +# # use pipes for communication channels between this parent +# # process and the children for sending specific interrupts +# # such as the signal to kill them. Note that the clean way to +# # end the process is to send poison pills on the task queue, +# # this is for other stuff. IRQ is a common abbreviation for +# # interrupts +# self._irq_parent_conns = [] + +# # unpack the generator for the kwargs +# kwargs = {key: list(kwarg) for key, kwarg in kwargs.items()} + +# # create the task based processes +# self._walker_processes = [] +# for walker_idx, task_args in enumerate(zip(*args)): +# task_kwargs = {key: value[walker_idx] for key, value in kwargs.items()} + +# # make the interrupt pipe +# parent_conn, child_conn = self._mp_ctx.Pipe() +# self._irq_parent_conns.append(parent_conn) + +# # start a process for this walker +# walker_process = self.walker_task_type( +# walker_idx, +# self._attributes, +# self._func, +# task_args, +# task_kwargs, +# worker_queue, +# results, +# worker_segment_times, +# child_conn, +# ) + +# walker_process.start() + +# self._walker_processes.append(walker_process) + +# new_walkers = [None for _ in range(num_walkers)] +# results_found = [False for _ in range(num_walkers)] +# while not all(results_found): +# # go through the results list and handle the values that may be there +# for walker_idx, result in enumerate(results): +# if results_found[walker_idx]: +# continue + +# # logger.info("Checking for walker {}".format(walker_idx)) + +# # first check to see if any of the task processes were +# # terminated from the system +# if self._irq_parent_conns[walker_idx].poll(): +# irq = self._irq_parent_conns[walker_idx].recv() + +# if issubclass(type(irq), TaskProcessKilledError): +# # just terminate if a worker goes down. We +# # could handle this better but it is not implemented now +# logger.critical( +# "Process {} was killed by sigterm, shutting down.".format( +# walker_process[walker_idx].name +# ) +# ) + +# logger.info( +# "Recovery is possible here, but is not implemented " +# "so we opt to fail fast and let you know a problem exists." +# "Please use checkpointing to avoid lost data." +# ) + +# self.force_shutdown() +# logger.critical("Shutdown complete.") + +# logger.debug( +# "Received {} acknowledgement from {}".format( +# ack, worker.name +# ) +# ) + +# # if no interrupts were handled we continue + +# # if it is None no response has been made at all +# # yet, this is the initialized value +# if result is None: +# pass + +# # walker results are returned serialized as +# # pickles, they are packed into a tuple so that we +# # can associate them with an explicit marker, if +# # we have a tuple then we can handle that +# # appropriately +# elif type(result) == tuple: +# logger.debug("Received a results tuple") + +# assert ( +# len(result) == 2 +# ), "Result tuples should be only be (ID, pickle)" + +# result_id, payload = result + +# # there was a walker successfully returned +# if result_id == "Walker": +# logger.debug("Received a serialized results walker") + +# # deserialize +# logger.debug("deserializing") +# new_walker = pickle.loads(payload) + +# logger.info("Got result for walker {}".format(walker_idx)) + +# new_walkers[walker_idx] = new_walker +# results_found[walker_idx] = True + +# else: +# raise ValueError("Unkown result ID: {}".format(result_id)) + +# elif issubclass(type(result), TaskException): +# logger.critical( +# "Exception encountered in a task which is unrecoverable." +# "You will need to reconfigure your components in a stable manner." +# ) + +# self.force_shutdown() + +# logger.critical("Shutdown complete.") +# raise result + +# elif issubclass(type(result), TaskProcessException): +# # we make just an error message to say that errors +# # in the worker may be due to the network or +# # something and could recover +# logger.error( +# "Exception encountered in the work mapper task process." +# "Recovery possible, see further messages." +# ) + +# # However, the current implementation doesn't +# # support retries or whatever so we issue a +# # critical log informing that it has been elevated +# # to critical and will force shutdown +# logger.critical( +# "Task process error mode resiliency not supported at this time." +# "Performing force shutdown and simulation ending." +# ) + +# self.force_shutdown() + +# logger.critical("Shutdown complete.") +# raise result + +# elif issubclass(type(result), Exception): +# logger.critical( +# "Unknown exception {} encountered.".format(result) +# ) + +# self.force_shutdown() + +# logger.critical("Shutdown complete.") + +# raise result + +# else: +# logger.critical( +# "Unknown result value {} encountered.".format(result) +# ) + +# self.force_shutdown() + +# logger.critical("Shutdown complete.") + +# # save the managed list of the recorded worker times locally +# for key, val in worker_segment_times.items(): +# self._worker_segment_times[key] = val + +# # wait for the processes to end +# # for walker in self._walker_processes: +# # walker.join() +# # logger.info("Joined {}".format(walker.name)) + +# # deinitialize the current walker processes +# self._walker_processes = None + +# return new_walkers + + diff --git a/src/wepy/work_mapper/worker.py b/src/wepy/work_mapper/worker.py deleted file mode 100644 index f62add2e..00000000 --- a/src/wepy/work_mapper/worker.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Classes for workers and tasks for use with WorkerMapper.""" - -from .mapper import Worker, WorkerMapper - -__all__ = [ - "Worker", - "WorkerMapper", -] diff --git a/src/wepy/work_mapper/worker_mapper.py b/src/wepy/work_mapper/worker_mapper.py new file mode 100644 index 00000000..c3fc772a --- /dev/null +++ b/src/wepy/work_mapper/worker_mapper.py @@ -0,0 +1,763 @@ +"""Classes for workers and tasks for use with WorkerMapper.""" + +from .mapper import WrapperException + + +class WorkerException(WrapperException): + pass + + +class WorkerKilledError(ChildProcessError): + pass + + +# TODO: move this class to the wepy.work_mapper.worker class where it +# belongs. It shouldn't be in this namespace, but we will leave it +# here. Furthermore I would like to rename it since we now have +# different worker mapper implementations with different concurrency +# models +class WorkerMapper(ABCWorkerMapper): + """Work mapper implementation using multiple worker processes and task + queue. + + Uses the python multiprocessing module to spawn multiple worker + processes which watch a task queue of walker segments. + """ + + def __init__( + self, + num_workers=None, + worker_type=None, + worker_attributes=None, + segment_func=None, + **kwargs, + ): + """Constructor for WorkerMapper. + + Parameters + ---------- + num_workers : int + The number of worker processes to spawn. + + worker_type : callable, optional + Callable that generates an object implementing the Worker + interface, typically a type from a Worker class. + + worker_attributes : dictionary + A dictionary of values that are passed to the worker + constructor as key-word arguments. + + segment_func : callable, optional + Set a default segment_func. Typically set at runtime. + + """ + + super().__init__(num_workers=num_workers, segment_func=segment_func, **kwargs) + + # since the workers will be their own process classes we + # handle this data + + # attributes that will be passed to the worker constructors + if worker_attributes is not None: + self._worker_attributes = worker_attributes + else: + self._worker_attributes = {} + + # choose the type of the worker + if worker_type is None: + self._worker_type = Worker + warn("worker_type not given using the default base class") + logger.warn("worker_type not given using the default base class") + else: + self._worker_type = worker_type + + @property + def worker_type(self): + """The callable that generates a worker object. + + Typically this is just the type from the class definition of + the Worker where the constructor is called. + + """ + return self._worker_type + + def init(self, num_workers=None, segment_func=None, **kwargs): + """Runtime initialization and setting of function to map over walkers. + + Parameters + ---------- + num_workers : int + The number of worker processes to spawn + + segment_func : callable implementing the Runner.run_segment interface + + """ + + super().init(num_workers=num_workers, segment_func=segment_func, **kwargs) + + manager = self._mp_ctx.Manager() + + # Establish communication queues + + # A queue for errors + self._exception_queue = manager.Queue() + + # queue for the tasks we know the batch size so we don't need + # a JoinableQueue + self._task_queue = manager.Queue() + + # results queue + self._result_queue = manager.Queue() + + # use pipes for communication channels between this parent + # process and the children for sending specific interrupts + # such as the signal to kill them. Note that the clean way to + # end the process is to send poison pills on the task queue, + # this is for other stuff. IRQ is a common abbreviation for + # interrupts + self._irq_parent_conns = [] + + # Start workers, giving them all the queues + self._workers = [] + for i in range(self.num_workers): + # make a pipe to communicate with this worker for the int + parent_conn, child_conn = self._mp_ctx.Pipe() + self._irq_parent_conns.append(parent_conn) + + # create the worker giving it all of the communication + # channels + worker = self.worker_type( + i, + self._task_queue, + self._result_queue, + self._exception_queue, + child_conn, + mapper_attributes=self._attributes, + **self._worker_attributes, + ) + self._workers.append(worker) + + # start the worker processes + for worker in self._workers: + worker.start() + + logger.info( + "Worker process started as name: {}; PID: {}".format( + worker.name, worker.pid + ) + ) + + # now that we have started the processes register the handler + # for SIGTERM signals that will clean up our children cleanly + signal.signal(signal.SIGTERM, self._sigterm_shutdown) + + def _sigterm_shutdown(self, signum, frame): + logger.critical("Received external SIGTERM, forcing shutdown.") + + self.force_shutdown() + + def force_shutdown(self, **kwargs): + logger.critical("Forcing shutdown") + + # our primary job is to shut down all of the running processes + # without just shutting down the queues and breaking the pipes + + # to do this we send the kill signals to them on the kill + # channel. + + for worker_idx, worker in enumerate(self._workers): + logger.critical( + "Sending SIGTERM message on {} to worker {}".format( + self._irq_parent_conns[worker_idx].fileno(), worker_idx + ) + ) + + # send a kill message to the worker + self._irq_parent_conns[worker_idx].send(signal.SIGTERM) + + logger.critical("All kill messages sent to workers") + + # check that all have exited + alive_workers = [worker.is_alive() for worker in self._workers] + worker_acks = {} + worker_exitcodes = {} + premature_exit = False + while any(alive_workers) and not premature_exit: + for worker_idx, worker in enumerate(self._workers): + # ignore already known dead workers + if not alive_workers[worker_idx]: + continue + + if worker.is_alive(): + # if it is still alive and we have an ack from it + # just terminate. There is a bug in the code and + # is out of our control + if worker_idx in worker_acks: + logger.debug( + "Ack received from {} but has not shut down".format( + worker.name + ) + ) + premature_exit = True + + # otherwise we need to try and receive the ack + elif self._irq_parent_conns[worker_idx].poll(1): + # receive the acknowledgement + ack = self._irq_parent_conns[worker_idx].recv() + + logger.debug( + "Received {} acknowledgement from {}".format( + ack, worker.name + ) + ) + + # make sure the ack is affirmative + if ack is True: + worker_acks[worker_idx] = ack + + # if it is an exeption wrap it as a worker + # error and use the os to kill the process + elif issubclass(type(ack), Exception): + # wrap it as a worker exception + exception = WorkerException(wrapped_exception=ack) + worker_acks[worker_idx] = exception + + logger.critical( + "{} not responding, terminating with SIGTERM".format( + worker.name + ) + ) + + worker.terminate() + + else: + alive_workers[worker_idx] = False + worker_exitcodes[worker_idx] = worker.exitcode + + if any(alive_workers): + logger.critical( + "Terminating main process with running workers {}".format( + ",".join( + [ + str(worker_idx) + for worker_idx in range(len(self._workers)) + if alive_workers[worker_idx] + ] + ) + ) + ) + + def cleanup(self, **kwargs): + """Runtime post-simulation tasks. + + This is run either at the end of a successful simulation or + upon an error in the main process of the simulation manager + call to `run_cycle`. + + The Mapper class performs no actions here and all arguments + are ignored. + + """ + + super().cleanup(**kwargs) + + # send poison pills (Stop signals) to the queues to stop them in a nice way + # and let them finish up + for i in range(self.num_workers): + self._task_queue.put((None, None)) + + # delete the queues and workers + self._task_queue = None + self._result_queue = None + self._workers = None + + def map(self, *args, **kwargs): + # docstring in superclass + + map_process = self._mp_ctx.current_process() + logger.info( + "Mapping from process {}; PID {}".format(map_process.name, map_process.pid) + ) + + # make tuples for the arguments to each function call + task_args = zip(*args) + kwargs = {key: list(kwarg) for key, kwarg in kwargs.items()} + + num_tasks = len(args[0]) + # Enqueue the jobs + for task_idx, task_arg in enumerate(task_args): + task_kwargs = {key: value[task_idx] for key, value in kwargs.items()} + + # a task will be the actual task and its task idx so we can + # sort them later + self._task_queue.put((task_idx, self._make_task(*task_arg, **task_kwargs))) + + logger.info("Waiting for tasks to be run") + + # poll the exception and result queues for results + n_results_left = num_tasks + results = [] + while n_results_left > 0: + # first check if any errors came back but don't wait, + # since the methods for querying whether it is empty or + # not are not reliable we just try and if we don't get + # anything we will come back around + try: + proc_name, pid, exception = self._exception_queue.get_nowait() + except pyq.Empty: + pass + + else: + logger.error( + "Exception occured in process {}; pid {}.".format(proc_name, pid) + ) + + # we can handle Task and Worker exceptions differently + if type(exception) == TaskException: + logger.critical( + "Exception encountered in a task which is unrecoverable." + "You will need to reconfigure your components in a stable manner." + ) + + self.force_shutdown() + + logger.critical("Shutdown complete.") + raise exception + + elif type(exception) == WorkerException: + # we make just an error message to say that errors + # in the worker may be due to the network or + # something and could recover + logger.error( + "Exception encountered in the work mapper worker process." + "Recovery possible, see further messages." + ) + + # However, the current implementation doesn't + # support retries or whatever so we issue a + # critical log informing that it has been elevated + # to critical and will force shutdown + logger.critical( + "Worker error mode resiliency not supported at this time." + "Performing force shutdown and simulation ending." + ) + + self.force_shutdown() + + logger.critical("Shutdown complete.") + raise exception + + else: + logger.critical("Unknown exception encountered.") + + self.force_shutdown() + + logger.critical("Shutdown complete.") + + raise exception + + # attempt to get something off of the results queue + try: + result = self._result_queue.get_nowait() + except pyq.Empty: + pass + + # if we get something handle it + else: + logger.info("Retrieved result: {}".format(result)) + results.append(result) + + # reduce the counter so we know when we are done + n_results_left -= 1 + + # sort the results according to their task_idx + results.sort() + + # save the task run times, so they can be accessed if desired, + # after clearing the task times from the last mapping + + # DEBUG: removing this because it should be set on init() + # self._worker_segment_times = {i : [] for i in range(self.num_workers)} + + for task_idx, worker_idx, task_time, result in results: + self._worker_segment_times[worker_idx].append(task_time) + + # then just return the values of the function + return [result for task_idx, worker_idx, task_time, result in results] + + +# same for the worker in terms of refactoring +class Worker(mp.Process): + """Worker process. + + This is a subclass of process with an overriden `__init__` + constructor that will automatically generate the Process. + + When this class is constructed a new process will be formed. + + """ + + NAME_TEMPLATE = "Worker-{}" + """A string formatting template to identify worker processes in + logs. The field will be filled with the worker index.""" + + def __init__( + self, + worker_idx, + task_queue, + result_queue, + exception_queue, + interrupt_connection, + mapper_attributes=None, + log_level="INFO", + **kwargs, + ): + """Constructor for the Worker class. + + Parameters + ---------- + worker_idx : int + The index of the worker. Should be unique. + + task_queue : multiprocessing.JoinableQueue + The shared task queue the worker will watch for new tasks to complete. + + result_queue : multiprocessing.Queue + The shared queue that completed task results will be placed on. + + interrupt_connection : multiprocessing.Connection + One end of a pipe to listen for messages specific to this worker. + + mapper_attributes : None or dict + A dictionary of the attributes of the mapper for reference in workers. + + kwargs : + The worker specific attributes + + """ + + # call the Process constructor + mp.Process.__init__(self, name=self.NAME_TEMPLATE.format(worker_idx)) + + self._exception_queue = exception_queue + self._exception = None + self._traceback = None + + # the queue that will trigger a shutdown in the event of failure + self._irq_channel = interrupt_connection + + # also register the SIGTERM signal handler for graceful + # shutdown with reporting to mapper + signal.signal(signal.SIGTERM, self._sigterm_shutdown) + + self._worker_idx = worker_idx + + self._mapper_attributes = mapper_attributes + + # set all the kwargs into an attributes dictionary + self._attributes = kwargs + + # the queues for work to be done and work done + self._task_queue = task_queue + self._result_queue = result_queue + + logger.debug("{} process created".format(self.name)) + + @property + def worker_idx(self): + """Dictionary of attributes of the worker.""" + return self._worker_idx + + @property + def attributes(self): + """Dictionary of attributes of the worker.""" + return self._attributes + + @property + def mapper_attributes(self): + """Dictionary of attributes of the worker.""" + return self._mapper_attributes + + def run(self): + logger.debug("{}: starting to run".format(self.name)) + + # try to run the worker and it's task, except either class of + # error that can come from it either from the worker + # (WorkerException) or the task (TaskException) and communicate it + # back to the main process + + # if we get an exception there is some cleanup logic + run_exception = None + + try: + # run the worker, which will retrieve its task from the + # queue attempt to run the task, and if it succeeds will + # put the results on the result queue, if the task fails + # it will catch it and wrap it as a task exception + self._run_worker() + + except TaskException as task_exception: + logger.error("{}: TaskException caught".format(self.name)) + + run_exception = task_exception + + # anything else is considered a WorkerException so take the + # original exception and generate a worker exception from that + except Exception as exception: + logger.debug("{}: WorkerError caught".format(self.name)) + + # get the traceback + tb = sys.exc_info()[2] + + msg = "Exception '{}({})' caught in a worker.".format( + type(exception).__name__, exception + ) + traceback_log_msg = """Traceback: +-------------------------------------------------------------------------------- +{} +-------------------------------------------------------------------------------- + """.format( + "".join(traceback.format_exception(type(exception), exception, tb)), + ) + + logger.error("{}:".format(self.name) + msg + "\n" + traceback_log_msg) + + # raise a TaskError to distinguish it from the worker + # errors with the metadata about the original exception + + worker_exception = WorkerException( + "Error occured during worker execution.", + wrapped_exception=exception, + tb=tb, + ) + + run_exception = worker_exception + + # raise worker_exception + if run_exception is not None: + logger.debug("{}: Putting exception on exception queue".format(self.name)) + + # then put the exception and the traceback onto the queue + # so we can communicate back to the parent process + try: + self._exception_queue.put((self.name, self.pid, run_exception)) + except BrokenPipeError as exc: + logger.error( + "Pipe is broken indicating the root process has already exited:\n{}".format( + exc + ) + ) + + # TODO: not sure if this is good or not + # then reraise the exception so it can be caught + # raise run_exception + + def _sigterm_shutdown(self, signum, frame): + logger.debug("Received external SIGTERM kill command.") + + logger.debug("Alerting mapper that this will be honored.") + + # send an error to the mapper that the worker has been killed + self._irq_channel.send( + WorkerKilledError( + "{} (pid: {}) killed by external SIGTERM signal".format( + self.name, self.pid + ) + ) + ) + + logger.debug("Acknowledgment sent") + + logger.debug("Shutting down process") + + def _shutdown(self): + logger.debug("Received SIGTERM kill command from mapper") + + logger.debug("Acknowledging kill request will be honored") + + # report back that we are shutting down with a True + self._irq_channel.send(True) + + logger.debug("Acknowledgment sent") + + logger.debug("Shutting down process") + + def _run_worker(self): + # run the logic associated with communication and liveness of + # the worker process itself, this is not necessarily a fatal + # (critical) error and restarting a worker might resolve the + # problem. This calls the _run_task method though which is + # always critical since the logic in the code cannot be + # disputed + + # TODO remove when confirmed that this works + # worker_process = mp.current_process() + logger.info( + "{}: Worker process started as name: {}; PID: {}".format( + self.name, self.name, self.pid + ) + ) + + while True: + # check to see if there is any signals in the interrupt channel + if self._irq_channel.poll(): + # get the message + message = self._irq_channel.recv() + + logger.debug( + "{}: Received message from mapper on filehandle {}: {}".format( + self.name, self._irq_channel.fileno(), message + ) + ) + + # handle the message + + # a SIGTERM is a signal to kill the process + # unconditionally + if message is signal.SIGTERM: + self._shutdown() + + # break from the event (while) loop and shut down + break + + # anything is not recognized and we will continue and + # report back that we don't recognize the message with + # a ValueError object + else: + logger.error( + "{}: Message not recognized, continuing operations and" + " sending error to mapper".format(self.name) + ) + self._irq_channel.send( + ValueError( + "Message: {} not recognized continuing operations".format( + message + ) + ) + ) + + # get the next task + try: + task_idx, next_task = self._task_queue.get(block=False, timeout=None) + + logger.debug("{}: Got task {}".format(self.name, task_idx)) + + except pyq.Empty: + task_idx = None + next_task = Ellipsis + + # # check for the poison pill which is the signal to stop + if next_task is None: + logger.info( + "{}: received {} {}: FINISHED".format( + self.name, task_idx, next_task + ) + ) + + # TODO remove since we aren't using joinble queue anymore + # mark the poison pill task as done + # self.task_queue.task_done() + + # and exit the loop + break + + # only execute this if a task was actually receieved from + # the queue; an Ellipsis indicates continue the loop + elif next_task is not Ellipsis: + logger.info( + "{}; task_idx : {}; args : {} ".format( + self.name, task_idx, next_task.args + ) + ) + + # run the task + start = time.time() + + answer = self._run_task(next_task) + + end = time.time() + task_time = end - start + + logger.info( + "{}: task_idx : {}; COMPLETED in {} s".format( + self.name, task_idx, task_time + ) + ) + + # put the results into the results queue with it's task + # index so we can sort them later + self._result_queue.put((task_idx, self.worker_idx, task_time, answer)) + + def run_task(self, task): + """Actually executes the task. + + This default runner simply executes the task thunk. + + This can be customized by subclasses in order to allow for + injection of worker specific data. + + Parameters + ---------- + task : Task object + The partially evaluated task; function plus arguments + + Returns + ------- + task_result + Results of running the task. + + """ + + return task() + + def _run_task(self, task): + """Runs the given task and returns the results. + + This manages handling exceptions and tracebacks from the + actual `run_task` function which is intended to be specialized + by different workers to inject worker specific arguments to + tasks. Such as node and device identification. + + Parameters + ---------- + task : Task object + The partially evaluated task; function plus arguments + + Returns + ------- + task_result + Results of running the task. + + """ + + logger.info("Running task") + try: + return self.run_task(task) + + except Exception as task_exception: + # get the traceback for the exception + tb = sys.exc_info()[2] + + msg = "Exception '{}({})' caught in a task.".format( + type(task_exception).__name__, task_exception + ) + traceback_log_msg = """Traceback: +-------------------------------------------------------------------------------- +{} +-------------------------------------------------------------------------------- + """.format( + "".join( + traceback.format_exception(type(task_exception), task_exception, tb) + ), + ) + + logger.critical(msg + "\n" + traceback_log_msg) + + # raise a TaskException to distinguish it from the worker + # errors with the metadata about the original exception + + raise TaskException( + "Error occured during task execution, recovery not possible.", + wrapped_exception=task_exception, + tb=tb, + ) diff --git a/tests/unit/test_work_mapper/test_mapper.py b/tests/unit/test_work_mapper/test_mapper.py deleted file mode 100644 index 9a8beed9..00000000 --- a/tests/unit/test_work_mapper/test_mapper.py +++ /dev/null @@ -1,235 +0,0 @@ -# Standard Library -import logging - -logger = logging.getLogger(__name__) -# Standard Library -import time - -# Third Party Library -import pytest - -# First Party Library -from wepy.walker import Walker, WalkerState -from wepy.work_mapper.mapper import ( - ABCMapper, - Mapper, - TaskException, - Task, - WrapperException, - TaskException, - ABCWorkerMapper, - WorkerException, - WorkerKilledError, - WorkerMapper, - Worker, -) -from wepy.work_mapper.task_mapper import ( - TaskMapper, - WalkerTaskProcess, -) -from wepy.work_mapper.worker import Worker, WorkerMapper - -ARGS = (0, 1, 2) - - -def gen_walkers(): - return [Walker(WalkerState(**{"num": arg}), 1 / len(ARGS)) for arg in ARGS] - - -# test basic functionality -def task_pass(walker): - # simulate it actually taking some time - n = walker.state["num"] - return Walker(WalkerState(**{"num": n + 1}), walker.weight) - - -TASK_PASS_ANSWER = [n + 1 for n in ARGS] - -class TestABCMapper: - def test___init__(self): - - no_mapper = ABCMapper() - assert no_mapper.segment_func is None - assert no_mapper.attributes == {} - - mapper = ABCMapper( - segment_func=(lambda x: ) - ) - assert no_mapper.segment_func is None - assert no_mapper.attributes == {} - - - def test_init(self): - assert False - - def test_cleanup(self): - assert False - -class TestMapper: - def test___init__(self): - assert False - - def test_map(self): - assert False - def test_worker_segment_times(self): - assert False - - -class TestWorkMappers: - def test_mapper(self): - mapper = Mapper(segment_func=task_pass) - - mapper.init() - - results = mapper.map(gen_walkers()) - - assert all( - [res.state["num"] == TASK_PASS_ANSWER[i] for i, res in enumerate(results)] - ) - - mapper.cleanup() - - def test_worker_mapper(self): - mapper = WorkerMapper(segment_func=task_pass, num_workers=3, worker_type=Worker) - - mapper.init() - - results = mapper.map(gen_walkers()) - - assert all( - [res.state["num"] == TASK_PASS_ANSWER[i] for i, res in enumerate(results)] - ) - - mapper.cleanup() - - def test_task_mapper(self): - mapper = TaskMapper( - segment_func=task_pass, num_workers=3, walker_task_type=WalkerTaskProcess - ) - - mapper.init() - - results = mapper.map(gen_walkers()) - - assert all( - [res.state["num"] == TASK_PASS_ANSWER[i] for i, res in enumerate(results)] - ) - - mapper.cleanup() - - time.sleep(1) - -class TestTask: - - def test___init__(self): - assert False - - def test___call__(self): - assert False - -class TestWrapperException: - - def test___init__(self): - assert False - - -class TestABCWorkerMapper: - - def test___init__(self): - assert False - - def test_init(self): - assert False - - def test_cleanup(self): - assert False - - def test__make_task(self): - assert False - -class TestWorkerMapper: - - def test___init__(self): - assert False - - def test_init(self): - assert False - - def test__sigterm_shutdown(self): - assert False - - def test_force_shutdown(self): - assert False - - def test_cleanup(self): - assert False - - def test_map(self): - assert False - -class TestWorker: - - def test___init__(self): - assert False - - def test_run(self): - assert False - - def test__sigterm_shutdown(self): - assert False - - def test__shutdown(self): - assert False - - def test__run_worker(self): - assert False - - def test_run_task(self): - assert False - - def test__run_task(self): - assert False - -# test that task failures are passed up properly -def task_fail(walker): - n = walker.state["num"] - if n == 1: - raise ValueError("No soup for you!!") - else: - return Walker(WalkerState(**{"num": n + 1}), walker.weight) - - -class TestTaskFail: - ARGS = ((0, 1, 2),) - - def test_mapper(self): - mapper = Mapper(segment_func=task_fail) - - mapper.init() - - with pytest.raises(TaskException) as task_exc_info: - results = mapper.map(gen_walkers()) - - mapper.cleanup() - - def test_worker_mapper(self): - mapper = WorkerMapper(segment_func=task_fail, num_workers=3, worker_type=Worker) - - mapper.init() - - with pytest.raises(TaskException) as task_exc_info: - results = mapper.map(gen_walkers()) - - mapper.cleanup() - - def test_task_mapper(self): - mapper = TaskMapper( - segment_func=task_fail, num_workers=3, walker_task_type=WalkerTaskProcess - ) - - mapper.init() - - with pytest.raises(TaskException) as task_exc_info: - results = mapper.map(gen_walkers()) - - mapper.cleanup() diff --git a/tests/unit/test_work_mapper/test_proc_pool_mapper.py b/tests/unit/test_work_mapper/test_proc_pool_mapper.py new file mode 100644 index 00000000..266c126e --- /dev/null +++ b/tests/unit/test_work_mapper/test_proc_pool_mapper.py @@ -0,0 +1,106 @@ +import attrs +from wepy.walker import Walker, WalkerState +from wepy.work_mapper.proc_pool_mapper import ProcPoolMapper + +# some minimal definitions for testing a concrete work mapper + +@attrs.define +class RizzWalkerState: + rizz: int + +def rizz_run(walker_state: RizzWalkerState, delta: int, multiple: int) -> RizzWalkerState: + + return attrs.evolve( + walker_state, + rizz=(walker_state.rizz + delta) * multiple + ) + +def test_rizz_walker(): + assert rizz_run( + RizzWalkerState(rizz=1), + 1, + 2, + ) == RizzWalkerState(rizz=4) + +def test_ProcPoolMapper(): + + poolmapper = ProcPoolMapper(num_workers=1) + + poolmapper.init(rizz_run) + + results = poolmapper.map( + [ + RizzWalkerState(1), + RizzWalkerState(1), + RizzWalkerState(2), + ], + [1, 2, 2], + [2, 2, 2], + ) + + assert results == [ + RizzWalkerState(4), + RizzWalkerState(6), + RizzWalkerState(8), + ] + + + poolmapper = ProcPoolMapper(num_workers=2) + + poolmapper.init(rizz_run) + + results = poolmapper.map( + [ + RizzWalkerState(1), + RizzWalkerState(1), + RizzWalkerState(2), + ], + [1, 2, 2], + [2, 2, 2], + ) + + assert results == [ + RizzWalkerState(4), + RizzWalkerState(6), + RizzWalkerState(8), + ] + + poolmapper = ProcPoolMapper(num_workers=3) + + poolmapper.init(rizz_run) + + results = poolmapper.map( + [ + RizzWalkerState(1), + RizzWalkerState(1), + RizzWalkerState(2), + ], + [1, 2, 2], + [2, 2, 2], + ) + + assert results == [ + RizzWalkerState(4), + RizzWalkerState(6), + RizzWalkerState(8), + ] + + poolmapper = ProcPoolMapper(num_workers=3, proc_start_method="fork") + + poolmapper.init(rizz_run) + + results = poolmapper.map( + [ + RizzWalkerState(1), + RizzWalkerState(1), + RizzWalkerState(2), + ], + [1, 2, 2], + [2, 2, 2], + ) + + assert results == [ + RizzWalkerState(4), + RizzWalkerState(6), + RizzWalkerState(8), + ] diff --git a/tests/unit/test_work_mapper/test_serial.py b/tests/unit/test_work_mapper/test_serial.py new file mode 100644 index 00000000..ee111d1a --- /dev/null +++ b/tests/unit/test_work_mapper/test_serial.py @@ -0,0 +1,63 @@ +import attrs +from wepy.walker import Walker, WalkerState +from wepy.work_mapper.serial import SerialMapper + +# some minimal definitions for testing a concrete work mapper + +@attrs.define +class RizzWalkerState: + rizz: int + +def rizz_run(walker_state: RizzWalkerState, delta: int, multiple: int) -> RizzWalkerState: + + return attrs.evolve( + walker_state, + rizz=(walker_state.rizz + delta) * multiple + ) + +def test_rizz_walker(): + assert rizz_run( + RizzWalkerState(rizz=1), + 1, + 2, + ) == RizzWalkerState(rizz=4) + + +class TestMapper: + + def test_map(self): + + mapper = SerialMapper() + + mapper.init(segment_func=rizz_run) + + assert mapper.map( + [ + RizzWalkerState(1), + RizzWalkerState(1), + RizzWalkerState(2), + ], + [1, 2, 2], + [2, 2, 2], + ) == [ + RizzWalkerState(4), + RizzWalkerState(6), + RizzWalkerState(8), + ] + + assert len(mapper.get_worker_segment_times()[0]) == 3 + + assert mapper.map( + [ + RizzWalkerState(1), + RizzWalkerState(1), + RizzWalkerState(2), + ], + [1, 2, 2], + multiple=[2, 2, 2], + ) == [ + RizzWalkerState(4), + RizzWalkerState(6), + RizzWalkerState(8), + ] + diff --git a/uv.lock b/uv.lock index 86774d38..e3c8234d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.11" +requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14'", - "python_full_version >= '3.12' and python_full_version < '3.14'", - "python_full_version < '3.12'", + "python_full_version < '3.14'", ] [[package]] @@ -158,10 +157,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, - { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, @@ -212,19 +207,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, @@ -279,22 +261,6 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, @@ -394,17 +360,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, - { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, - { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, - { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, - { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, - { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, - { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, - { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, @@ -460,11 +415,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, - { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, - { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, - { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] [[package]] @@ -473,19 +423,6 @@ version = "7.12.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, - { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, - { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, - { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, - { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, - { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, @@ -554,11 +491,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, ] -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - [[package]] name = "cycler" version = "0.12.1" @@ -576,7 +508,6 @@ dependencies = [ { name = "click" }, { name = "cloudpickle" }, { name = "fsspec" }, - { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "partd" }, { name = "pyyaml" }, @@ -593,10 +524,6 @@ version = "1.8.17" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129, upload-time = "2025-09-17T16:33:20.633Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/53/3af72b5c159278c4a0cf4cffa518675a0e73bdb7d1cac0239b815502d2ce/debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840", size = 2207154, upload-time = "2025-09-17T16:33:29.457Z" }, - { url = "https://files.pythonhosted.org/packages/8f/6d/204f407df45600e2245b4a39860ed4ba32552330a0b3f5f160ae4cc30072/debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f", size = 3170322, upload-time = "2025-09-17T16:33:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/f2/13/1b8f87d39cf83c6b713de2620c31205299e6065622e7dd37aff4808dd410/debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da", size = 5155078, upload-time = "2025-09-17T16:33:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c5/c012c60a2922cc91caa9675d0ddfbb14ba59e1e36228355f41cab6483469/debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4", size = 5179011, upload-time = "2025-09-17T16:33:35.711Z" }, { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" }, { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" }, { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" }, @@ -709,14 +636,6 @@ version = "4.60.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" }, - { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" }, - { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" }, - { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" }, { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" }, { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" }, { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" }, @@ -802,14 +721,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/4d/6a/0d79de0b025aa85dc8864de8e97659c94cf3d23148394a954dc5ca52f8c8/h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69", size = 426236, upload-time = "2025-10-16T10:35:27.404Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243", size = 3434135, upload-time = "2025-10-16T10:33:47.954Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509", size = 2870958, upload-time = "2025-10-16T10:33:50.907Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e3/c255cafc9b85e6ea04e2ad1bba1416baa1d7f57fc98a214be1144087690c/h5py-3.15.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80e5bb5b9508d5d9da09f81fd00abbb3f85da8143e56b1585d59bc8ceb1dba8b", size = 4504770, upload-time = "2025-10-16T10:33:54.357Z" }, - { url = "https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf", size = 4700329, upload-time = "2025-10-16T10:33:57.616Z" }, - { url = "https://files.pythonhosted.org/packages/a4/e4/932a3a8516e4e475b90969bf250b1924dbe3612a02b897e426613aed68f4/h5py-3.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f6c841efd4e6e5b7e82222eaf90819927b6d256ab0f3aca29675601f654f3c", size = 4152456, upload-time = "2025-10-16T10:34:00.843Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0a/f74d589883b13737021b2049ac796328f188dbb60c2ed35b101f5b95a3fc/h5py-3.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8a3a22458956ee7b40d8e39c9a9dc01f82933e4c030c964f8b875592f4d831", size = 4617295, upload-time = "2025-10-16T10:34:04.154Z" }, - { url = "https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878", size = 2882129, upload-time = "2025-10-16T10:34:06.886Z" }, - { url = "https://files.pythonhosted.org/packages/ce/bb/cfcc70b8a42222ba3ad4478bcef1791181ea908e2adbd7d53c66395edad5/h5py-3.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:b39239947cb36a819147fc19e86b618dcb0953d1cd969f5ed71fc0de60392427", size = 2477121, upload-time = "2025-10-16T10:34:09.579Z" }, { url = "https://files.pythonhosted.org/packages/62/b8/c0d9aa013ecfa8b7057946c080c0c07f6fa41e231d2e9bd306a2f8110bdc/h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c", size = 3399089, upload-time = "2025-10-16T10:34:12.135Z" }, { url = "https://files.pythonhosted.org/packages/a4/5e/3c6f6e0430813c7aefe784d00c6711166f46225f5d229546eb53032c3707/h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76", size = 2847803, upload-time = "2025-10-16T10:34:14.564Z" }, { url = "https://files.pythonhosted.org/packages/00/69/ba36273b888a4a48d78f9268d2aee05787e4438557450a8442946ab8f3ec/h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e", size = 4914884, upload-time = "2025-10-16T10:34:18.452Z" }, @@ -882,18 +793,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -958,7 +857,6 @@ dependencies = [ { name = "pygments" }, { name = "stack-data" }, { name = "traitlets" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/e6/48c74d54039241a456add616464ea28c6ebf782e4110d419411b83dae06f/ipython-9.7.0.tar.gz", hash = "sha256:5f6de88c905a566c6a9d6c400a8fed54a638e1f7543d17aae2551133216b1e4e", size = 4422115, upload-time = "2025-11-05T12:18:54.646Z" } wheels = [ @@ -1154,7 +1052,6 @@ dependencies = [ { name = "jupyter-server-terminals" }, { name = "nbconvert" }, { name = "nbformat" }, - { name = "overrides", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "prometheus-client" }, { name = "pywinpty", marker = "os_name == 'nt'" }, @@ -1240,19 +1137,6 @@ version = "1.4.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, - { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, - { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, - { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, - { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, @@ -1317,11 +1201,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, - { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] [[package]] @@ -1357,17 +1236,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, @@ -1442,13 +1310,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" }, - { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" }, - { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" }, - { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" }, { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" }, { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" }, { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" }, @@ -1484,9 +1345,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" }, { url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" }, { url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" }, - { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" }, ] [[package]] @@ -1513,9 +1371,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/49/87/04f21b321e7bb6f3e09b5339df51699b8caae4846796175845bbc44f298b/mdtraj-1.11.0.tar.gz", hash = "sha256:2cf0ed2ee9a603dc4599743b1806e1387c655e2a1e9d013841b99fb137ad852e", size = 2860049, upload-time = "2025-06-25T02:06:19.716Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/bc/1a6d273467a24dad5048bf527288a7bbe997ecee8342f6e8fdd7465c9132/mdtraj-1.11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a6bbf2bc03a2ed23fd1fe696f6f6f970f495e843f3baaa71a0e99da086420de1", size = 2463123, upload-time = "2025-06-25T02:03:06.602Z" }, - { url = "https://files.pythonhosted.org/packages/09/dc/37bd8a22c66d05a293979d8d8cf1de5e6c03d21ebd6950b6a839c8d8a16b/mdtraj-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c34002c49282d208fdd34cd1b2578859bc7377a22ebd66a194efa1fc17395e5", size = 8023237, upload-time = "2025-06-25T02:03:28.425Z" }, - { url = "https://files.pythonhosted.org/packages/4c/48/6fc195907c056df9b6ff6442bb39c5dd433b971b9deaecf6d18a0761efa8/mdtraj-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:4c794aac3db13937ed7e4f8c2b773564a89b2b539c15f43bae9dab6e1ffa5de1", size = 1355146, upload-time = "2025-06-25T02:03:31.524Z" }, { url = "https://files.pythonhosted.org/packages/21/71/56adb24577052f2cd6f9b678da908e8e2d7963db3a88faff39792be43903/mdtraj-1.11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:878fb8e62962b881ba75d4200637280b518e891e4aa5f213052ddb1905704558", size = 2448664, upload-time = "2025-06-25T02:03:43.052Z" }, { url = "https://files.pythonhosted.org/packages/50/af/7d65be36619befb29b51b6118a97121de6685720af7f4cfd6ff1902ca7eb/mdtraj-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01eb00d9b62f22056303fb8e4be0f85257a2ce1003f1e6274393bbcc977badaf", size = 8028112, upload-time = "2025-06-25T02:04:12.949Z" }, { url = "https://files.pythonhosted.org/packages/d2/8f/fd05aa3613fad29b3a779e3384797d2b64aa329bcfd18d25f8fb9c0d725e/mdtraj-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:f87117f7e6742b11a3a39e55fe5c9b4fc013d27691ac671bcc6dfb8193beae0f", size = 1341985, upload-time = "2025-06-25T02:04:19.133Z" }, @@ -1556,12 +1411,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, - { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, @@ -1712,38 +1561,76 @@ wheels = [ [[package]] name = "nptyping" -version = "2.5.0" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/b7/ffe533358c32506b1708feec0fb04ba0a35a959a94163fff5333671909da/nptyping-2.5.0.tar.gz", hash = "sha256:e3d35b53af967e6fb407c3016ff9abae954d3a0568f7cc13a461084224e8e20a", size = 71623, upload-time = "2023-02-20T21:57:11.814Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/28/92edc05378175de13a3d4986cee7531853634a22b7e5e21a988fa84fde3f/nptyping-2.5.0-py3-none-any.whl", hash = "sha256:764e51836faae33a7ae2e928af574cfb701355647accadcc89f2ad793630b7c8", size = 37602, upload-time = "2023-02-20T21:57:09.61Z" }, + { url = "https://files.pythonhosted.org/packages/48/03/3f5af42a8858c223f1a4b1d5a773f3a4f7cb9ae9dcbd1ad61bdb8539e633/nptyping-2.0.1-py3-none-any.whl", hash = "sha256:0fc5c4d76c65e12a77e750b9e2701dab6468d00926c8c4f383867bd70598a532", size = 19435, upload-time = "2022-04-28T21:02:07.815Z" }, ] [[package]] name = "numpy" -version = "1.26.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, - { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, - { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, - { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, - { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, - { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, - { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, - { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, - { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, - { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, - { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, - { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, - { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, ] [[package]] @@ -1754,10 +1641,6 @@ dependencies = [ { name = "numpy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/35/9a51d07fc60fc267a241a2199deb31502baa2e438801d4223a256b898c8a/openmm-8.4.0.post1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:eb5d3da43b7ac4b3e41e3a29005982b1f87de1879d2d7a97b982d20022321383", size = 13228166, upload-time = "2025-11-07T18:03:58.349Z" }, - { url = "https://files.pythonhosted.org/packages/95/50/772848b1b81c7609cbe119c5456996488ce2efd4863e782c257e42829be4/openmm-8.4.0.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef172c5ab504f99a7346c3e4b35e29703be803a610ab72e6b2d16e39f754ebea", size = 12741362, upload-time = "2025-11-07T18:04:04.236Z" }, - { url = "https://files.pythonhosted.org/packages/7b/b2/8acf418560b6fe29635f4f992f3a1a71d9f017cca19ebe4ccff9f86fae90/openmm-8.4.0.post1-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:2b5dcbc789d9d16fbe4fcc7326532499f45143a2bec05c869fa98af0c23c3d80", size = 14248283, upload-time = "2025-11-07T18:04:10.516Z" }, - { url = "https://files.pythonhosted.org/packages/38/1f/976e1fae88a6ff4ba74310dc663faf80e58e9f740ebfc12c890309706032/openmm-8.4.0.post1-cp311-cp311-win_amd64.whl", hash = "sha256:a49c498564bdaeb3bad936440ed402dca4b0718a6578147142f2c3ffe4580559", size = 13096103, upload-time = "2025-11-07T18:04:16.787Z" }, { url = "https://files.pythonhosted.org/packages/6d/c1/63de7b93c6b57323cb3524f1038fbb75749d44f087a9c443de4a5d35be33/openmm-8.4.0.post1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:117165ce53a465389f05249cae9feb90276874dc7ac5bf0cc8b9151031b06fa6", size = 13224758, upload-time = "2025-11-07T18:04:22.948Z" }, { url = "https://files.pythonhosted.org/packages/dc/17/ddae4b246950b68a45a5521f925a90db332e0761c28194a3b7533e2e4495/openmm-8.4.0.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:226d462ea3e1b36daa5276b931979d36db72c9daf0e3502f7cfd5528ade8861f", size = 12739396, upload-time = "2025-11-07T18:04:29.885Z" }, { url = "https://files.pythonhosted.org/packages/3b/ca/2063be344cefbeaec480d9d44662551f41f27097c0df825cc2bcbea221cc/openmm-8.4.0.post1-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:a503c2b860e21364f1c49bf23c5bb8b6fa04e1a4227bc577d543d4137e99eaf5", size = 14253643, upload-time = "2025-11-07T18:04:37.888Z" }, @@ -1786,15 +1669,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/e1/02debe3a6f49490d2fc40c3fc5a96ab902ad96a66e5b8173254d3ef900b5/openmm_systems-0.0.0-py2.py3-none-any.whl", hash = "sha256:9e9ce3157a1034baf9fae1f652dc5187f6b36e6d66b2871521a4ef98f3c4b64b", size = 11327692, upload-time = "2020-04-30T19:51:18.139Z" }, ] -[[package]] -name = "overrides" -version = "7.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, -] - [[package]] name = "packaging" version = "25.0" @@ -1816,13 +1690,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, - { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, - { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, - { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, - { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, @@ -1929,17 +1796,6 @@ version = "12.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, - { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, - { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, - { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, - { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, - { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, - { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, - { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, @@ -2001,13 +1857,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, - { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, - { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, - { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, ] [[package]] @@ -2233,7 +2082,7 @@ name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] @@ -2286,9 +2135,6 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, @@ -2306,7 +2152,6 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/bb/a7cc2967c5c4eceb6cc49cfe39447d4bfc56e6c865e7c2249b6eb978935f/pywinpty-3.0.2.tar.gz", hash = "sha256:1505cc4cb248af42cb6285a65c9c2086ee9e7e574078ee60933d5d7fa86fb004", size = 30669, upload-time = "2025-10-03T21:16:29.205Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/a1/409c1651c9f874d598c10f51ff586c416625601df4bca315d08baec4c3e3/pywinpty-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:327790d70e4c841ebd9d0f295a780177149aeb405bca44c7115a3de5c2054b23", size = 2050304, upload-time = "2025-10-03T21:19:29.466Z" }, { url = "https://files.pythonhosted.org/packages/02/4e/1098484e042c9485f56f16eb2b69b43b874bd526044ee401512234cf9e04/pywinpty-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:99fdd9b455f0ad6419aba6731a7a0d2f88ced83c3c94a80ff9533d95fa8d8a9e", size = 2050391, upload-time = "2025-10-03T21:19:01.642Z" }, { url = "https://files.pythonhosted.org/packages/fc/19/b757fe28008236a4a713e813283721b8a40aa60cd7d3f83549f2e25a3155/pywinpty-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:18f78b81e4cfee6aabe7ea8688441d30247b73e52cd9657138015c5f4ee13a51", size = 2050057, upload-time = "2025-10-03T21:19:26.732Z" }, { url = "https://files.pythonhosted.org/packages/cb/44/cbae12ecf6f4fa4129c36871fd09c6bef4f98d5f625ecefb5e2449765508/pywinpty-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:663383ecfab7fc382cc97ea5c4f7f0bb32c2f889259855df6ea34e5df42d305b", size = 2049874, upload-time = "2025-10-03T21:18:53.923Z" }, @@ -2320,15 +2165,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, @@ -2378,16 +2214,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, - { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, - { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, - { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, - { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, - { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, - { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, @@ -2420,11 +2246,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, - { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, - { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, - { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] [[package]] @@ -2495,21 +2316,6 @@ version = "0.29.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/ab/7fb95163a53ab122c74a7c42d2d2f012819af2cf3deb43fb0d5acf45cc1a/rpds_py-0.29.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b9c764a11fd637e0322a488560533112837f5334ffeb48b1be20f6d98a7b437", size = 372344, upload-time = "2025-11-16T14:47:57.279Z" }, - { url = "https://files.pythonhosted.org/packages/b3/45/f3c30084c03b0d0f918cb4c5ae2c20b0a148b51ba2b3f6456765b629bedd/rpds_py-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fd2164d73812026ce970d44c3ebd51e019d2a26a4425a5dcbdfa93a34abc383", size = 363041, upload-time = "2025-11-16T14:47:58.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e9/4d044a1662608c47a87cbb37b999d4d5af54c6d6ebdda93a4d8bbf8b2a10/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a097b7f7f7274164566ae90a221fd725363c0e9d243e2e9ed43d195ccc5495c", size = 391775, upload-time = "2025-11-16T14:48:00.197Z" }, - { url = "https://files.pythonhosted.org/packages/50/c9/7616d3ace4e6731aeb6e3cd85123e03aec58e439044e214b9c5c60fd8eb1/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cdc0490374e31cedefefaa1520d5fe38e82fde8748cbc926e7284574c714d6b", size = 405624, upload-time = "2025-11-16T14:48:01.496Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e2/6d7d6941ca0843609fd2d72c966a438d6f22617baf22d46c3d2156c31350/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89ca2e673ddd5bde9b386da9a0aac0cab0e76f40c8f0aaf0d6311b6bbf2aa311", size = 527894, upload-time = "2025-11-16T14:48:03.167Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f7/aee14dc2db61bb2ae1e3068f134ca9da5f28c586120889a70ff504bb026f/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5d9da3ff5af1ca1249b1adb8ef0573b94c76e6ae880ba1852f033bf429d4588", size = 412720, upload-time = "2025-11-16T14:48:04.413Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e2/2293f236e887c0360c2723d90c00d48dee296406994d6271faf1712e94ec/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8238d1d310283e87376c12f658b61e1ee23a14c0e54c7c0ce953efdbdc72deed", size = 392945, upload-time = "2025-11-16T14:48:06.252Z" }, - { url = "https://files.pythonhosted.org/packages/14/cd/ceea6147acd3bd1fd028d1975228f08ff19d62098078d5ec3eed49703797/rpds_py-0.29.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2d6fb2ad1c36f91c4646989811e84b1ea5e0c3cf9690b826b6e32b7965853a63", size = 406385, upload-time = "2025-11-16T14:48:07.575Z" }, - { url = "https://files.pythonhosted.org/packages/52/36/fe4dead19e45eb77a0524acfdbf51e6cda597b26fc5b6dddbff55fbbb1a5/rpds_py-0.29.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:534dc9df211387547267ccdb42253aa30527482acb38dd9b21c5c115d66a96d2", size = 423943, upload-time = "2025-11-16T14:48:10.175Z" }, - { url = "https://files.pythonhosted.org/packages/a1/7b/4551510803b582fa4abbc8645441a2d15aa0c962c3b21ebb380b7e74f6a1/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d456e64724a075441e4ed648d7f154dc62e9aabff29bcdf723d0c00e9e1d352f", size = 574204, upload-time = "2025-11-16T14:48:11.499Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/071ccdd7b171e727a6ae079f02c26f75790b41555f12ca8f1151336d2124/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a738f2da2f565989401bd6fd0b15990a4d1523c6d7fe83f300b7e7d17212feca", size = 600587, upload-time = "2025-11-16T14:48:12.822Z" }, - { url = "https://files.pythonhosted.org/packages/03/09/96983d48c8cf5a1e03c7d9cc1f4b48266adfb858ae48c7c2ce978dbba349/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a110e14508fd26fd2e472bb541f37c209409876ba601cf57e739e87d8a53cf95", size = 562287, upload-time = "2025-11-16T14:48:14.108Z" }, - { url = "https://files.pythonhosted.org/packages/40/f0/8c01aaedc0fa92156f0391f39ea93b5952bc0ec56b897763858f95da8168/rpds_py-0.29.0-cp311-cp311-win32.whl", hash = "sha256:923248a56dd8d158389a28934f6f69ebf89f218ef96a6b216a9be6861804d3f4", size = 221394, upload-time = "2025-11-16T14:48:15.374Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/a8b21c54c7d234efdc83dc034a4d7cd9668e3613b6316876a29b49dece71/rpds_py-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:539eb77eb043afcc45314d1be09ea6d6cafb3addc73e0547c171c6d636957f60", size = 235713, upload-time = "2025-11-16T14:48:16.636Z" }, - { url = "https://files.pythonhosted.org/packages/a7/1f/df3c56219523947b1be402fa12e6323fe6d61d883cf35d6cb5d5bb6db9d9/rpds_py-0.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:bdb67151ea81fcf02d8f494703fb728d4d34d24556cbff5f417d74f6f5792e7c", size = 229157, upload-time = "2025-11-16T14:48:17.891Z" }, { url = "https://files.pythonhosted.org/packages/3c/50/bc0e6e736d94e420df79be4deb5c9476b63165c87bb8f19ef75d100d21b3/rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954", size = 376000, upload-time = "2025-11-16T14:48:19.141Z" }, { url = "https://files.pythonhosted.org/packages/3e/3a/46676277160f014ae95f24de53bed0e3b7ea66c235e7de0b9df7bd5d68ba/rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c", size = 360575, upload-time = "2025-11-16T14:48:20.443Z" }, { url = "https://files.pythonhosted.org/packages/75/ba/411d414ed99ea1afdd185bbabeeaac00624bd1e4b22840b5e9967ade6337/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d", size = 392159, upload-time = "2025-11-16T14:48:22.12Z" }, @@ -2583,18 +2389,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/3d/eb820f95dce4306f07a495ede02fb61bef36ea201d9137d4fcd5ab94ec1e/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a", size = 557288, upload-time = "2025-11-16T14:50:10.73Z" }, { url = "https://files.pythonhosted.org/packages/e9/f8/b8ff786f40470462a252918e0836e0db903c28e88e3eec66bc4a7856ee5d/rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5", size = 211382, upload-time = "2025-11-16T14:50:12.827Z" }, { url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919, upload-time = "2025-11-16T14:50:14.734Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ac/b97e80bf107159e5b9ba9c91df1ab95f69e5e41b435f27bdd737f0d583ac/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:acd82a9e39082dc5f4492d15a6b6c8599aa21db5c35aaf7d6889aea16502c07d", size = 373963, upload-time = "2025-11-16T14:50:16.205Z" }, - { url = "https://files.pythonhosted.org/packages/40/5a/55e72962d5d29bd912f40c594e68880d3c7a52774b0f75542775f9250712/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:715b67eac317bf1c7657508170a3e011a1ea6ccb1c9d5f296e20ba14196be6b3", size = 364644, upload-time = "2025-11-16T14:50:18.22Z" }, - { url = "https://files.pythonhosted.org/packages/99/2a/6b6524d0191b7fc1351c3c0840baac42250515afb48ae40c7ed15499a6a2/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b1b87a237cb2dba4db18bcfaaa44ba4cd5936b91121b62292ff21df577fc43", size = 393847, upload-time = "2025-11-16T14:50:20.012Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b8/c5692a7df577b3c0c7faed7ac01ee3c608b81750fc5d89f84529229b6873/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c3c3e8101bb06e337c88eb0c0ede3187131f19d97d43ea0e1c5407ea74c0cbf", size = 407281, upload-time = "2025-11-16T14:50:21.64Z" }, - { url = "https://files.pythonhosted.org/packages/f0/57/0546c6f84031b7ea08b76646a8e33e45607cc6bd879ff1917dc077bb881e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8e54d6e61f3ecd3abe032065ce83ea63417a24f437e4a3d73d2f85ce7b7cfe", size = 529213, upload-time = "2025-11-16T14:50:23.219Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c1/01dd5f444233605555bc11fe5fed6a5c18f379f02013870c176c8e630a23/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fbd4e9aebf110473a420dea85a238b254cf8a15acb04b22a5a6b5ce8925b760", size = 413808, upload-time = "2025-11-16T14:50:25.262Z" }, - { url = "https://files.pythonhosted.org/packages/aa/0a/60f98b06156ea2a7af849fb148e00fbcfdb540909a5174a5ed10c93745c7/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fdf53d36e6c72819993e35d1ebeeb8e8fc688d0c6c2b391b55e335b3afba5a", size = 394600, upload-time = "2025-11-16T14:50:26.956Z" }, - { url = "https://files.pythonhosted.org/packages/37/f1/dc9312fc9bec040ece08396429f2bd9e0977924ba7a11c5ad7056428465e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:ea7173df5d86f625f8dde6d5929629ad811ed8decda3b60ae603903839ac9ac0", size = 408634, upload-time = "2025-11-16T14:50:28.989Z" }, - { url = "https://files.pythonhosted.org/packages/ed/41/65024c9fd40c89bb7d604cf73beda4cbdbcebe92d8765345dd65855b6449/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:76054d540061eda273274f3d13a21a4abdde90e13eaefdc205db37c05230efce", size = 426064, upload-time = "2025-11-16T14:50:30.674Z" }, - { url = "https://files.pythonhosted.org/packages/a2/e0/cf95478881fc88ca2fdbf56381d7df36567cccc39a05394beac72182cd62/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9f84c549746a5be3bc7415830747a3a0312573afc9f95785eb35228bb17742ec", size = 575871, upload-time = "2025-11-16T14:50:33.428Z" }, - { url = "https://files.pythonhosted.org/packages/ea/c0/df88097e64339a0218b57bd5f9ca49898e4c394db756c67fccc64add850a/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:0ea962671af5cb9a260489e311fa22b2e97103e3f9f0caaea6f81390af96a9ed", size = 601702, upload-time = "2025-11-16T14:50:36.051Z" }, - { url = "https://files.pythonhosted.org/packages/87/f4/09ffb3ebd0cbb9e2c7c9b84d252557ecf434cd71584ee1e32f66013824df/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f7728653900035fb7b8d06e1e5900545d8088efc9d5d4545782da7df03ec803f", size = 564054, upload-time = "2025-11-16T14:50:37.733Z" }, ] [[package]] @@ -2632,16 +2426,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, - { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, - { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, - { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, - { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, - { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, @@ -2915,55 +2699,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, ] -[[package]] -name = "tomli" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, - { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, - { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, - { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, - { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, - { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, - { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, - { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, - { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, - { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, - { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, - { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, - { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, - { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, - { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, - { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, - { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, - { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, - { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, - { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, - { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, - { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, -] - [[package]] name = "toolz" version = "1.1.0" @@ -3146,7 +2881,7 @@ requires-dist = [ { name = "multiprocessing-logging" }, { name = "networkx" }, { name = "nptyping" }, - { name = "numpy" }, + { name = "numpy", specifier = ">=2" }, { name = "openmm", marker = "extra == 'md'" }, { name = "openmm-systems", marker = "extra == 'md'", specifier = "==0.0.0" }, { name = "pandas" }, @@ -3178,12 +2913,3 @@ dev = [ { name = "sphinxcontrib-napoleon" }, { name = "sphinxcontrib-newsfeed" }, ] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -] From bd774b4491c72123c441151dac6724e1678021fa Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:09:18 -0500 Subject: [PATCH 020/143] remove orchestration code paths --- pyproject.toml | 5 - src/wepy/orchestration/__init__.py | 6 - src/wepy/orchestration/cli.py | 1016 ----------------- src/wepy/orchestration/configuration.py | 375 ------ src/wepy/orchestration/orchestrator.py | 1376 ----------------------- src/wepy/orchestration/snapshot.py | 73 -- src/wepy/util/kv.py | 436 ------- uv.lock | 11 - 8 files changed, 3298 deletions(-) delete mode 100644 src/wepy/orchestration/__init__.py delete mode 100644 src/wepy/orchestration/cli.py delete mode 100644 src/wepy/orchestration/configuration.py delete mode 100644 src/wepy/orchestration/orchestrator.py delete mode 100644 src/wepy/orchestration/snapshot.py delete mode 100644 src/wepy/util/kv.py diff --git a/pyproject.toml b/pyproject.toml index 8013fdf4..6cfa4745 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ dependencies = [ "h5py>=3", "networkx", "pandas", - "dill", "click", "scipy", "geomm", @@ -71,10 +70,6 @@ Documentation = "https://adicksonlab.github.io/wepy/index.html" Source = "https://github.com/ADicksonLab/wepy" Issues = "https://github.com/ADicksonLab/wepy/issues" -[project.scripts] - -wepy = "wepy.__main__:cli" - [build-system] requires = ["uv_build>=0.9.11,<0.10.0"] build-backend = "uv_build" diff --git a/src/wepy/orchestration/__init__.py b/src/wepy/orchestration/__init__.py deleted file mode 100644 index 2821c70d..00000000 --- a/src/wepy/orchestration/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -if __name__ == "__main__": - # entry-point to the orchestration CLI - # First Party Library - from wepy.orchestration.cli import cli - - cli() diff --git a/src/wepy/orchestration/cli.py b/src/wepy/orchestration/cli.py deleted file mode 100644 index 0819f32d..00000000 --- a/src/wepy/orchestration/cli.py +++ /dev/null @@ -1,1016 +0,0 @@ -# Standard Library -import logging - -logger = logging.getLogger(__name__) -# Standard Library -import os.path as osp -import subprocess -from copy import deepcopy - -# Third Party Library -import click - -# First Party Library -from wepy.hdf5 import WepyHDF5 -from wepy.orchestration.orchestrator import Orchestrator, reconcile_orchestrators -from wepy.reporter.hdf5 import WepyHDF5Reporter -from wepy.util.util import set_loglevel - -ORCHESTRATOR_DEFAULT_FILENAME = Orchestrator.ORCH_FILENAME_TEMPLATE.format( - config=Orchestrator.DEFAULT_CONFIG_NAME, narration=Orchestrator.DEFAULT_NARRATION -) - -START_HASH = "" -CURDIR = "" - - -def settle_run_options( - n_workers=None, - job_dir=None, - job_name=None, - narration=None, - monitor_http_port=None, - tag=None, - configuration=None, - start_hash=None, -): - """Parameters - ---------- - n_workers : - (Default value = None) - job_dir : - (Default value = None) - job_name : - (Default value = None) - narration : - (Default value = None) - - \b - - Returns - ------- - - """ - - # the default for the job name is the start hash if none is given - if job_name == START_HASH: - job_name = start_hash - - # if the job_name is given and the default value for the job_dir - # is given (i.e. not specified by the user) we set the job-dir as - # the job_name - if job_name is not None and job_dir == CURDIR: - job_dir = job_name - - # if the special value for curdir is given we get the systems - # current directory, this is the default. - if job_dir == CURDIR: - job_dir = osp.curdir - - # normalize the job_dir - job_dir = osp.realpath(job_dir) - - # if a path for a configuration was given we want to use it so we - # unpickle it and return it, otherwise return None and use the - # default one in the orchestrator - config = None - if configuration is not None: - with open(configuration, "rb") as rf: - config = Orchestrator.deserialize(rf.read()) - - ## Monitoring - - # nothing to do, just use the port or tag if its given - monitor_pkwargs = { - "tag": tag, - "port": monitor_http_port, - } - - # we need to reparametrize the configuration here since the - # orchestrator API will ignore reparametrization values if a - # concrete Configuration is given. - if config is not None: - # if there is a change in the number of workers we need to - # recalculate all of the partial kwargs - - work_mapper_pkwargs = deepcopy(config.work_mapper_partial_kwargs) - - # if the number of workers has changed update all the relevant - # fields, otherwise leave it alone - if work_mapper_pkwargs["num_workers"] != n_workers: - work_mapper_pkwargs["num_workers"] = n_workers - work_mapper_pkwargs["device_ids"] = [str(i) for i in range(n_workers)] - - config = config.reparametrize( - work_dir=job_dir, - config_name=job_name, - narration=narration, - work_mapper_partial_kwargs=work_mapper_pkwargs, - monitor_partial_kwargs=monitor_pkwargs, - ) - - return job_dir, job_name, narration, config - - -@click.option("--log", default="WARNING") -@click.option("--n-workers", type=click.INT) -@click.option("--checkpoint-freq", default=None, type=click.INT) -@click.option("--job-dir", default=CURDIR, type=click.Path(writable=True)) -@click.option("--job-name", default=START_HASH) -@click.option("--narration", default="") -@click.option("--monitor-http-port", default=9001) -@click.option("--tag", default="None") -@click.argument("n_cycle_steps", type=click.INT) -@click.argument("run_time", type=click.FLOAT) -@click.argument("configuration", type=click.Path(exists=True)) -@click.argument("snapshot", type=click.File("rb")) -@click.command() -def run_snapshot( - log, - n_workers, - checkpoint_freq, - job_dir, - job_name, - narration, - monitor_http_port, - tag, - n_cycle_steps, - run_time, - configuration, - snapshot, -): - """\b - - Parameters - ---------- - log : - - n_workers : - - checkpoint_freq : - - job_dir : - - job_name : - - narration : - - monitor_http_port : - - n_cycle_steps : - - run_time : - - start_hash : - - orchestrator : - - - \b - - Returns - ------- - - """ - - set_loglevel(log) - - logger.info("Loading the starting snapshot file") - # read the config and snapshot in - serial_snapshot = snapshot.read() - - logger.info("Creating orchestrating orch database") - # make the orchestrator for this simulation in memory to start - orch = Orchestrator() - logger.info("Adding the starting snapshot to database") - start_hash = orch.add_serial_snapshot(serial_snapshot) - - # settle what the defaults etc. are for the different options as they are interdependent - job_dir, job_name, narration, config = settle_run_options( - # work mapper - n_workers=n_workers, - # reporters - job_dir=job_dir, - job_name=job_name, - narration=narration, - # monitoring - tag=tag, - monitor_http_port=monitor_http_port, - # other - configuration=configuration, - start_hash=start_hash, - ) - - # add the parametrized configuration to the orchestrator - # config_hash = orch.add_serial_configuration(config) - - logger.info("Orchestrator loaded") - logger.info("Running snapshot by time") - run_orch = orch.orchestrate_snapshot_run_by_time( - start_hash, - run_time, - n_cycle_steps, - checkpoint_freq=checkpoint_freq, - work_dir=job_dir, - config_name=job_name, - narration=narration, - configuration=config, - ) - logger.info("Finished running snapshot by time") - - start_hash, end_hash = run_orch.run_hashes()[0] - - run_orch.close() - logger.info("Closed the resultant orch") - - # write the run tuple out to the log - run_line_str = "Run start and end hashes: {}, {}".format(start_hash, end_hash) - - # log it - logger.info(run_line_str) - - # also put it to the terminal - click.echo(run_line_str) - - orch.close() - logger.info("closed the orchestrating orch database") - - -@click.option("--log", default="WARNING") -@click.option("--n-workers", type=click.INT) -@click.option("--checkpoint-freq", default=None, type=click.INT) -@click.option("--job-dir", default=CURDIR, type=click.Path(writable=True)) -@click.option("--job-name", default=START_HASH) -@click.option("--narration", default="") -@click.option("--configuration", type=click.Path(exists=True), default=None) -@click.argument("n_cycle_steps", type=click.INT) -@click.argument("run_time", type=click.FLOAT) -@click.argument("start_hash") -@click.argument("orchestrator", type=click.Path(exists=True)) -@click.command() -def run_orch( - log, - n_workers, - checkpoint_freq, - job_dir, - job_name, - narration, - configuration, - n_cycle_steps, - run_time, - start_hash, - orchestrator, -): - """\b - - Parameters - ---------- - log : - - n_workers : - - checkpoint_freq : - - job_dir : - - job_name : - - narration : - - n_cycle_steps : - - run_time : - - start_hash : - - orchestrator : - - - \b - - Returns - ------- - - """ - - set_loglevel(log) - - # settle what the defaults etc. are for the different options as they are interdependent - job_dir, job_name, narration, config = settle_run_options( - n_workers=n_workers, - job_dir=job_dir, - job_name=job_name, - narration=narration, - configuration=configuration, - start_hash=start_hash, - ) - - # Open a wrapper around the orchestrator database that provides - # the inputs for the simulation - orch = Orchestrator(orchestrator, mode="r") - - logger.info("Orchestrator loaded") - - logger.info("Running snapshot by time") - run_orch = orch.orchestrate_snapshot_run_by_time( - start_hash, - run_time, - n_cycle_steps, - checkpoint_freq=checkpoint_freq, - work_dir=job_dir, - config_name=job_name, - narration=narration, - configuration=config, - ) - logger.info("Finished running snapshot by time") - - start_hash, end_hash = run_orch.run_hashes()[0] - - logger.info("Closing the resultant orchestrator") - run_orch.close() - - # write the run tuple out to the log - run_line_str = "Run start and end hashes: {}, {}".format(start_hash, end_hash) - - # log it - logger.info(run_line_str) - - # also put it to the terminal - click.echo(run_line_str) - - logger.info("Closing the orchestrating orch") - orch.close() - - -def combine_orch_wepy_hdf5s(new_orch, new_hdf5_path, run_ids=None): - """\b - - Parameters - ---------- - new_orch : - - new_hdf5_path : - - - \b - - Returns - ------- - - """ - - if run_ids is None: - run_ids = new_orch.run_hashes() - - # we assume that the run we are interested in is the only run in - # the WepyHDF5 file so it is index 0 - singleton_run_idx = 0 - - # a key-value for the paths for each run - hdf5_paths = {} - - # go through each run in the new orchestrator - for run_id in run_ids: - # get the configuration used for this run - run_config = new_orch.run_configuration(*run_id) - - # from that configuration find the WepyHDF5Reporters - for reporter in run_config.reporters: - if isinstance(reporter, WepyHDF5Reporter): - # and save the path for that run - hdf5_paths[run_id] = reporter.file_path - - click.echo("Combining these HDF5 files:") - click.echo("\n".join(hdf5_paths.values())) - - # now that we have the paths (or lack of paths) for all - # the runs we need to start linking them all - # together. - - # first we need a master linker HDF5 to do this with - - # so load a template WepyHDF5 - template_wepy_h5_path = hdf5_paths[run_ids[singleton_run_idx]] - template_wepy_h5 = WepyHDF5(template_wepy_h5_path, mode="r") - - # clone it - with template_wepy_h5: - master_wepy_h5 = template_wepy_h5.clone(new_hdf5_path, mode="x") - - click.echo("Into a single master hdf5 file: {}".format(new_hdf5_path)) - - # then link all the files to it - run_mapping = {} - for run_id, wepy_h5_path in hdf5_paths.items(): - # in the case where continuations were done from - # checkpoints then the runs data will potentially (and - # most likely) contain extra cycles since checkpoints are - # typically produced on some interval of cycles. So, in - # order for us to actually piece together contigs we need - # to take care of this. - - # There are two ways to deal with this which can both be - # done at the same time. The first is to keep the "nubs", - # which are the small leftover pieces after the checkpoint - # that ended up getting continued, and make a new run from - # the last checkpoint to the end of the nub, in both the - # WepyHDF5 and the orchestrator run collections. - - # The second is to generate a WepyHDF5 run that - # corresponds to the run in the checkpoint orchestrator. - - # To avoid complexity (for now) we opt to simply dispose - # of the nubs and assume that not much will be lost from - # this. For the typical use case of making multiple - # independent and linear contigs this is also the simplest - # mode, since the addition of multiple nubs will introduce - # an extra spanning contig in the contig tree. - - # furthermore the nubs provide a source of problems if - # rnus were abruptly stopped and data is not written some - # of the frames can be corrupted. SO until we know how to - # stop this (probably SWMR mode will help) this is also a - # reason not to deal with nubs. - - # TODO: add option to keep nubs in HDF5, and deal with in - # orch (you won't be able to have an end snapshot...). - - # to do this we simply check whether or not the number of - # cycles for the run_id are less than the number of cycles - # in the corresponding WepyHDF5 run dataset. - orch_run_num_cycles = new_orch.run_last_cycle_idx(*run_id) - - # get the number of cycles that are in the data for the run in - # the HDF5 to compare to the number in the orchestrator run - # record - wepy_h5 = WepyHDF5(wepy_h5_path, mode="r") - with wepy_h5: - h5_run_num_cycles = wepy_h5.num_run_cycles(singleton_run_idx) - - # sanity check for if the number of cycles in the - # orchestrator is greater than the HDF5 - if orch_run_num_cycles > h5_run_num_cycles: - raise ValueError( - "Number of cycles in orch run is more than HDF5." - "This implies missing data" - ) - - # copy the run (with the slice) - with master_wepy_h5: - # TODO: this was the old way of combining where we would - # just link, however due to the above discussion this is - # not tenable now. In the future there might be some more - # complex options taking linking into account but for now - # we just don't use it and all runs will be copied by this - # operation - - # # we just link the whole file then sort out the - # # continuations later since we aren't necessarily doing - # # this in a logical order - # new_run_idxs = master_wepy_h5.link_file_runs(wepy_h5_path) - - # extract the runs from the file (there should only be - # one). This means copy the run, but if we only want a - # truncation of it we will use the run slice to only get - # part of it - - # so first we generate the run slices for this file using - # the number of cycles recorded in the orchestrator - run_slices = {singleton_run_idx: (0, orch_run_num_cycles)} - - click.echo("Extracting Run: {}".format(run_id)) - click.echo( - "Frames 0 to {} out of {}".format( - orch_run_num_cycles, h5_run_num_cycles - ) - ) - - # then perform the extraction, which will open the other - # file on its own - new_run_idxs = master_wepy_h5.extract_file_runs( - wepy_h5_path, run_slices=run_slices - ) - - # map the hash id to the new run idx created. There should - # only be one run in an HDF5 if we are following the - # orchestration workflow. - assert ( - len(new_run_idxs) < 2 - ), "Cannot be more than 1 run per HDF5 file in orchestration workflow" - - run_mapping[run_id] = new_run_idxs[0] - - click.echo("Set as run: {}".format(new_run_idxs[0])) - - click.echo("Done extracting runs, setting continuations") - - with master_wepy_h5: - # now that they are all linked we need to add the snapshot - # hashes identifying the runs as metadata. This is so we can - # map the simple run indices in the HDF5 back to the - # orchestrator defined runs. This will be saved as metadata on - # the run. Also: - - # We need to set the continuations correctly betwen the runs - # in different files, so for each run we find the run it - # continues in the orchestrator - for run_id, run_idx in run_mapping.items(): - # set the run snapshot hash metadata except for if we have - # already done it - try: - master_wepy_h5.set_run_start_snapshot_hash(run_idx, run_id[0]) - except AttributeError: - # it was already set so just move on - pass - try: - master_wepy_h5.set_run_end_snapshot_hash(run_idx, run_id[1]) - except AttributeError: - # it was already set so just move on - pass - - # find the run_id that this one continues - continued_run_id = new_orch.run_continues(*run_id) - - # if a None is returned then there was no continuation - if continued_run_id is None: - # so we go to the next run_id and don't log any - # continuation - continue - - # get the run_idx in the HDF5 that corresponds to this run - continued_run_idx = run_mapping[continued_run_id] - - click.echo("Run {} continued by {}".format(continued_run_id, run_idx)) - - # add the continuation - master_wepy_h5.add_continuation(run_idx, continued_run_idx) - - -@click.command() -@click.argument("orchestrator", nargs=1, type=click.Path(exists=True)) -@click.argument("hdf5", nargs=1, type=click.Path(exists=False)) -@click.argument("run_ids", nargs=-1) -def reconcile_hdf5(orchestrator, hdf5, run_ids): - """For an orchestrator with multiple runs combine the HDF5 results - into a single one. - - This requires that the paths inside of the reporters for the - configurations used for a run still have valid paths to the HDF5 - files. - - \b - - Parameters - ---------- - orchestrator : Path - The orchestrator to retrieve HDF5s for - - hdf5 : Path - Path to the resultant HDF5. - - run_ids : str - String specifying a run as start and end hash - e.g. 'd0cb2e6fbcc8c2d66d67c845120c7f6b,b4b96580ae57f133d5f3b6ce25affa6d' - - \b - - Returns - ------- - - """ - - # parse the run ids - run_ids = [tuple(run_id.split(",")) for run_id in run_ids] - - orch = Orchestrator(orchestrator, mode="r") - - hdf5_path = osp.realpath(hdf5) - - click.echo("Combining the HDF5s together, saving to:") - click.echo(hdf5_path) - - # combine the HDF5 files from those orchestrators - combine_orch_wepy_hdf5s(orch, hdf5_path, run_ids=run_ids) - - -@click.command() -@click.option("--hdf5", type=click.Path(exists=False)) -@click.argument("output", nargs=1, type=click.Path(exists=False)) -@click.argument("orchestrators", nargs=-1, type=click.Path(exists=True)) -def reconcile_orch(hdf5, output, orchestrators): - """\b - - Parameters - ---------- - hdf5 : Path - Path to the resultant HDF5. - output : Path - Path to the resultant orchestrator that is created. - orchestrators : Path - Paths to the orchestrators to reconcile. - - \b - - Returns - ------- - - """ - - new_orch = reconcile_orchestrators(output, *orchestrators) - - # if a path for an HDF5 file is given - if hdf5 is not None: - hdf5_path = osp.realpath(hdf5) - - click.echo("Combining the HDF5s together, saving to:") - click.echo(hdf5_path) - - # combine the HDF5 files from those orchestrators - combine_orch_wepy_hdf5s(new_orch, hdf5_path) - - -def hash_listing_formatter(hashes): - """\b - - Parameters - ---------- - hashes : - - - \b - - Returns - ------- - - """ - hash_listing_str = "\n".join(hashes) - return hash_listing_str - - -@click.argument("orchestrator", type=click.Path(exists=True)) -@click.command() -def ls_snapshots(orchestrator): - """\b - - Parameters - ---------- - orchestrator : - - - \b - - Returns - ------- - - """ - - orch = Orchestrator(orch_path=orchestrator, mode="r") - - message = hash_listing_formatter(orch.snapshot_hashes) - - orch.close() - - click.echo(message) - - -@click.argument("orchestrator", type=click.Path(exists=True)) -@click.command() -def ls_runs(orchestrator): - """\b - - Parameters - ---------- - orchestrator : - - - \b - - Returns - ------- - - """ - - orch = Orchestrator(orch_path=orchestrator, mode="r") - - runs = orch.run_hashes() - - orch.close() - - hash_listing_str = "\n".join(["{}, {}".format(start, end) for start, end in runs]) - - click.echo(hash_listing_str) - - -@click.argument("orchestrator", type=click.Path(exists=True)) -@click.command() -def ls_configs(orchestrator): - """\b - - Parameters - ---------- - orchestrator : - - - \b - - Returns - ------- - - """ - - orch = Orchestrator(orch_path=orchestrator, mode="r") - - message = hash_listing_formatter(orch.configuration_hashes) - - orch.close() - - click.echo(message) - - -@click.command() -@click.option("--no-expand-external", is_flag=True) -@click.argument("source", type=click.Path(exists=True)) -@click.argument("target", type=click.Path(exists=False)) -def hdf5_copy(no_expand_external, source, target): - """Copy a WepyHDF5 file, except links to other runs will optionally be - expanded and truly duplicated if symbolic inter-file links are present. - """ - - # arg clusters to pass to subprocess for the files - input_f_args = ["-i", source] - output_f_args = ["-o", target] - - # each invocation calls a different group since we can't call the - # toplevel '/' directly - settings_args = ["-s", "/units", "-d", "/units"] - settings_args = ["-s", "/_settings", "-d", "/_settings"] - topology_args = ["-s", "/topology", "-d", "/topology"] - runs_args = ["-s", "/runs", "-d", "/runs"] - - # by default expand the external links - flags_args = ["-f", "ext"] - - # if the not expand external flag is given get rid of those args - if no_expand_external: - flags_args = [] - - common_args = input_f_args + output_f_args + flags_args - - settings_output = subprocess.check_output(["h5copy"] + common_args + settings_args) - - topology_output = subprocess.check_output(["h5copy"] + common_args + topology_args) - - runs_output = subprocess.check_output(["h5copy"] + common_args + runs_args) - - -@click.option("-O", "--output", type=click.Path(exists=False), default=None) -@click.argument("snapshot_hash") -@click.argument("orchestrator", type=click.Path(exists=True)) -@click.command() -def get_snapshot(output, snapshot_hash, orchestrator): - # first check if the output is None, if it is we automatically - # generate a file in the cwd that is the hash of the snapshot - if output is None: - output = "{}.snap.dill.pkl".format(snapshot_hash) - - # check that it doesn't exist, and fail if it does, since we - # don't want to implicitly overwrite stuff - if osp.exists(output): - raise OSError( - "No output path was specified and default alredy exists, exiting." - ) - - orch = Orchestrator(orchestrator, mode="r") - - serial_snapshot = orch.snapshot_kv[snapshot_hash] - - with open(output, "wb") as wf: - wf.write(serial_snapshot) - - orch.close() - - -@click.option("-O", "--output", type=click.Path(exists=False), default=None) -@click.argument("config_hash") -@click.argument("orchestrator", type=click.Path(exists=True)) -@click.command() -def get_config(output, config_hash, orchestrator): - # first check if the output is None, if it is we automatically - # generate a file in the cwd that is the hash of the snapshot - if output is None: - output = "{}.config.dill.pkl".format(config_hash) - - # check that it doesn't exist, and fail if it does, since we - # don't want to implicitly overwrite stuff - if osp.exists(output): - raise OSError( - "No output path was specified and default alredy exists, exiting." - ) - - orch = Orchestrator(orchestrator, mode="r") - - serial_snapshot = orch.configuration_kv[config_hash] - - with open(output, "wb") as wf: - wf.write(serial_snapshot) - - orch.close() - - -@click.option("-O", "--output", type=click.Path(exists=False), default=None) -@click.argument("end_hash") -@click.argument("start_hash") -@click.argument("orchestrator", type=click.Path(exists=True)) -@click.command() -def get_run(output, end_hash, start_hash, orchestrator): - # first check if the output is None, if it is we automatically - # generate a file in the cwd that is the hash of the snapshot - if output is None: - output = "{}-{}.orch.sqlite".format(start_hash, end_hash) - - # check that it doesn't exist, and fail if it does, since we - # don't want to implicitly overwrite stuff - if osp.exists(output): - raise OSError( - "No output path was specified and default alredy exists, exiting." - ) - - orch = Orchestrator(orchestrator, mode="r") - - start_serial_snapshot = orch.snapshot_kv[start_hash] - end_serial_snapshot = orch.snapshot_kv[end_hash] - - # get the records values for this run - rec_d = { - field: value - for field, value in zip( - Orchestrator.RUN_SELECT_FIELDS, orch.get_run_record(start_hash, end_hash) - ) - } - - config = orch.configuration_kv[rec_d["config_hash"]] - - # create a new orchestrator at the output location - new_orch = Orchestrator(output, mode="w") - - _ = new_orch.add_serial_snapshot(start_serial_snapshot) - _ = new_orch.add_serial_snapshot(end_serial_snapshot) - config_hash = new_orch.add_serial_configuration(config) - - new_orch.register_run(start_hash, end_hash, config_hash, rec_d["last_cycle_idx"]) - - orch.close() - new_orch.close() - - -@click.argument("end_hash") -@click.argument("start_hash") -@click.argument("orchestrator", type=click.Path(exists=True)) -@click.command() -def get_run_cycles(end_hash, start_hash, orchestrator): - orch = Orchestrator(orchestrator, mode="r") - - start_serial_snapshot = orch.snapshot_kv[start_hash] - end_serial_snapshot = orch.snapshot_kv[end_hash] - - # get the records values for this run - rec_d = { - field: value - for field, value in zip( - Orchestrator.RUN_SELECT_FIELDS, orch.get_run_record(start_hash, end_hash) - ) - } - - click.echo(rec_d["last_cycle_idx"]) - - -@click.argument("orchestrator", type=click.Path(exists=False)) -@click.command() -def create_orch(orchestrator): - orch = Orchestrator(orchestrator, mode="x") - - orch.close() - - -@click.argument("snapshot", type=click.File("rb")) -@click.argument("orchestrator", type=click.Path(exists=True)) -@click.command() -def add_snapshot(snapshot, orchestrator): - orch = Orchestrator(orchestrator, mode="r+") - - serial_snapshot = snapshot.read() - - snaphash = orch.add_serial_snapshot(serial_snapshot) - - orch.close() - - click.echo(snaphash) - - -@click.argument("configuration", type=click.File("rb")) -@click.argument("orchestrator", type=click.Path(exists=True)) -@click.command() -def add_config(configuration, orchestrator): - orch = Orchestrator(orchestrator, mode="r+") - - serial_config = configuration.read() - - config_hash = orch.add_serial_snapshot(serial_config) - - orch.close() - - click.echo(config_hash) - - -@click.group() -def cli(): - """ """ - pass - - -@click.group() -def run(): - """ """ - pass - - -@click.group() -def get(): - """ """ - pass - - -@click.group() -def add(): - """ """ - pass - - -@click.group() -def create(): - """ """ - pass - - -@click.group() -def ls(): - """ """ - pass - - -@click.group() -def reconcile(): - """ """ - pass - - -@click.group() -def hdf5(): - """ """ - pass - - -# command groupings - -# run -run.add_command(run_orch, name="orch") -run.add_command(run_snapshot, name="snapshot") - -# ls -ls.add_command(ls_snapshots, name="snapshots") -ls.add_command(ls_runs, name="runs") -ls.add_command(ls_configs, name="configs") - -# get -get.add_command(get_snapshot, name="snapshot") -get.add_command(get_config, name="config") -get.add_command(get_run, name="run") -get.add_command(get_run_cycles, name="run-cycles") - -# add -add.add_command(add_snapshot, name="snapshot") -add.add_command(add_config, name="config") - -# create -create.add_command(create_orch, name="orch") - -# reconcile -reconcile.add_command(reconcile_orch, name="orch") -reconcile.add_command(reconcile_hdf5, name="hdf5") - -# hdf5 -hdf5.add_command(hdf5_copy, name="copy") -# desired commands -# hdf5.add_command(hdf5_copy, name='copy-run') -# hdf5.add_command(hdf5_copy, name='copy-traj') -# hdf5.add_command(hdf5_copy, name='ls-runs') -# hdf5.add_command(hdf5_copy, name='ls-run-hashes') - -# subgroups -subgroups = [run, get, add, create, ls, reconcile, hdf5] - -for subgroup in subgroups: - cli.add_command(subgroup) - -if __name__ == "__main__": - cli() diff --git a/src/wepy/orchestration/configuration.py b/src/wepy/orchestration/configuration.py deleted file mode 100644 index 9ad23c83..00000000 --- a/src/wepy/orchestration/configuration.py +++ /dev/null @@ -1,375 +0,0 @@ -# Standard Library -import itertools as it -import logging -from typing import Final - -logger = logging.getLogger(__name__) -# Standard Library -import os.path as osp -from copy import deepcopy - -# First Party Library -from wepy.work_mapper.mapper import Mapper, WorkerMapper - - -class Configuration: - """ """ - - DEFAULT_WORKDIR: Final = osp.realpath(osp.curdir) - DEFAULT_CONFIG_NAME: Final = "root" - DEFAULT_NARRATION: Final = "" - DEFAULT_REPORTER_CLASS: Final = "" - - # if there is to be reporter class in filenames use this template - # to put it into the filename - REPORTER_CLASS_SEG_TEMPLATE: Final = ".{}" - DEFAULT_MODE: Final = "x" - - def __init__( - self, - # reporters - config_name=None, - work_dir=None, - mode=None, - narration=None, - reporter_classes=None, - reporter_partial_kwargs=None, - # work mappers - work_mapper_class=None, - work_mapper_partial_kwargs=None, - # monitors - monitor_class=None, - monitor_partial_kwargs=None, - # apparatus configuration options - apparatus_opts=None, - ): - ## reporter stuff - - # reporters and partial kwargs - if reporter_classes is not None: - self._reporter_classes = reporter_classes - else: - self._reporter_classes = [] - - if reporter_partial_kwargs is not None: - self._reporter_partial_kwargs = reporter_partial_kwargs - else: - self._reporter_partial_kwargs = [] - - # file path localization variables - - # config string - if config_name is not None: - self._config_name = config_name - else: - self._config_name = self.DEFAULT_CONFIG_NAME - - if work_dir is not None: - self._work_dir = work_dir - else: - self._work_dir = self.DEFAULT_WORKDIR - - # narration - if narration is not None: - narration = "_{}".format(narration) if len(narration) > 0 else "" - self._narration = narration - else: - self._narration = self.DEFAULT_NARRATION - - # file modes, if none are given we set to the default, this - # needs to be done before generating the reporters - if mode is not None: - self._mode = mode - else: - self._mode = self.DEFAULT_MODE - - # generate the reporters for this configuration - self._reporters = self._gen_reporters() - - ## work mapper - - # the partial kwargs that will be passed for reparametrization - if work_mapper_partial_kwargs is None: - self._work_mapper_partial_kwargs = {} - else: - self._work_mapper_partial_kwargs = work_mapper_partial_kwargs - - # if the number of workers is not given set it to None - if "num_workers" not in self._work_mapper_partial_kwargs: - self._work_mapper_partial_kwargs["num_workers"] = None - - # same for the worker type - if "worker_type" not in self._work_mapper_partial_kwargs: - self._work_mapper_partial_kwargs["worker_type"] = None - - # if the number of workers was sepcified and no work_mapper - # class was specified default to the WorkerMapper - if (self._work_mapper_partial_kwargs["num_workers"] is not None) and ( - work_mapper_class is None - ): - self._work_mapper_class = WorkerMapper - - # if no number of workers was specified and no work_mapper - # class was specified we default to the serial mapper - elif (self._work_mapper_partial_kwargs["num_workers"] is None) and ( - work_mapper_class is None - ): - self._work_mapper_class = Mapper - - # otherwise if the work_mapper class was given we use it and - # whatever the number of workers was - else: - self._work_mapper_class = work_mapper_class - - # then generate a work mapper - self._work_mapper = self._work_mapper_class(**self._work_mapper_partial_kwargs) - print( - "config mapper ---->", - self._work_mapper.__class__.__name__, - self._work_mapper._attributes, - self._work_mapper_partial_kwargs, - ) - - ### Monitor options - - # get the names of the reporters in the order they are - reporter_order = tuple( - [str(reporter_class.__name__) for reporter_class in self._reporter_classes] - ) - - # init the kwargs for the monitor - if monitor_partial_kwargs is None: - self._monitor_partial_kwargs = {} - else: - self._monitor_partial_kwargs = monitor_partial_kwargs - - # choose the monitor class (None is okay) - self._monitor_class = monitor_class - - # generate the object - if self._monitor_class is not None: - self._monitor = self._monitor_class( - reporter_order=reporter_order, - **self._monitor_partial_kwargs, - ) - - else: - self._monitor = None - - ### Apparatus options - - # the runtime configuration of the apparatus can be configured - # via these options. - self._apparatus_opts = apparatus_opts if apparatus_opts is not None else {} - - @property - def reporter_classes(self): - """ """ - return self._reporter_classes - - @property - def reporter_partial_kwargs(self): - """ """ - return self._reporter_partial_kwargs - - @property - def config_name(self): - """ """ - return self._config_name - - @property - def work_dir(self): - """ """ - return self._work_dir - - @property - def narration(self): - """ """ - return self._narration - - @property - def mode(self): - """ """ - return self._mode - - @property - def reporters(self): - """ """ - return self._reporters - - @property - def work_mapper_class(self): - """ """ - return self._work_mapper_class - - @property - def work_mapper_partial_kwargs(self): - """ """ - return self._work_mapper_partial_kwargs - - @property - def work_mapper(self): - """ """ - return self._work_mapper - - @property - def monitor_class(self): - """ """ - return self._monitor_class - - @property - def monitor_partial_kwargs(self): - """ """ - return self._monitor_partial_kwargs - - @property - def monitor(self): - """ """ - return self._monitor - - @property - def apparatus_opts(self): - """ """ - return self._apparatus_opts - - def _gen_reporters(self): - """ """ - - # check the extensions of all the reporters. If any of them - # are the same raise a flag to add the reporter names to the - # filenames - - # the number of filenames - all_exts = list( - it.chain( - *[ - [ext for ext in rep.SUGGESTED_EXTENSIONS] - for rep in self.reporter_classes - ] - ) - ) - n_exts = len(all_exts) - - # the number of unique ones - n_unique_exts = len(set(all_exts)) - - duplicates = False - if n_unique_exts < n_exts: - duplicates = True - - # then go through and make the inputs for each reporter - reporters = [] - for idx, reporter_class in enumerate(self.reporter_classes): - # first we have to generate the filenames for all the - # files this reporter needs. The number of file names the - # reporter needs is given by the number of suggested - # extensions it has - file_paths = [] - for extension in reporter_class.SUGGESTED_EXTENSIONS: - # if previously found that there are duplicates in the - # extensions we need to name with the reporter class string - if duplicates: - # use the __name__ attribute of the class and put - # it into the template to make a segment out of it - reporter_class_seg_str = self.REPORTER_CLASS_SEG_TEMPLATE.format( - reporter_class.__name__ - ) - - # then make the filename with this - filename = reporter_class.SUGGESTED_FILENAME_TEMPLATE.format( - narration=self.narration, - config=self.config_name, - reporter_class=reporter_class_seg_str, - ext=extension, - ) - - # otherwise don't use the reporter class names to keep it clean - else: - filename = reporter_class.SUGGESTED_FILENAME_TEMPLATE.format( - narration=self.narration, - config=self.config_name, - reporter_class=self.DEFAULT_REPORTER_CLASS, - ext=extension, - ) - - file_path = osp.join(self.work_dir, filename) - - file_paths.append(file_path) - - modes = [self.mode for i in range(len(file_paths))] - - reporter = reporter_class( - file_paths=file_paths, modes=modes, **self.reporter_partial_kwargs[idx] - ) - - reporters.append(reporter) - - return reporters - - # TODO: remove, not used - def _gen_work_mapper(self): - """ """ - - work_mapper = self._work_mapper_class(n_workers=self._default_n_workers) - - return work_mapper - - @property - def reporters(self): - """ """ - return deepcopy(self._reporters) - - @property - def work_mapper(self): - """ """ - return deepcopy(self._work_mapper) - - def reparametrize(self, **kwargs): - """Parameters - ---------- - **kwargs : - - - Returns - ------- - - """ - - # dictionary of the possible reparametrizations from the - # current configuration - params = { - # related to the work mapper - "work_mapper_class": self.work_mapper_class, - "work_mapper_partial_kwargs": self.work_mapper_partial_kwargs, - # monitor - "monitor_class": self.monitor_class, - "monitor_partial_kwargs": self.monitor_partial_kwargs, - # those related to the reporters - "mode": self.mode, - "config_name": self.config_name, - "work_dir": self.work_dir, - "narration": self.narration, - "reporter_classes": self.reporter_classes, - "reporter_partial_kwargs": self.reporter_partial_kwargs, - # apparatus - "apparatus_opts": self.apparatus_opts, - } - - for key, value in kwargs.items(): - # for the partial kwargs we need to update them not - # completely overwrite - if key in [ - "work_mapper_partial_kwargs", - "reporter_partial_kwargs", - "monitor_partial_kwargs", - ]: - if value is not None: - params[key].update(value) - - # if the value is given we replace the old one with it - elif value is not None: - params[key] = value - - new_configuration = type(self)(**params) - - return new_configuration diff --git a/src/wepy/orchestration/orchestrator.py b/src/wepy/orchestration/orchestrator.py deleted file mode 100644 index 8f30c36b..00000000 --- a/src/wepy/orchestration/orchestrator.py +++ /dev/null @@ -1,1376 +0,0 @@ -# Standard Library -import logging - -logger = logging.getLogger(__name__) -# Standard Library -import os -import os.path as osp -import sqlite3 -import time -from base64 import b64decode, b64encode -from copy import deepcopy -from hashlib import md5 -from zlib import compress, decompress - -# Third Party Library -# instead of pickle we use dill, so we can save dynamically defined -# classes -import dill - -# First Party Library -from wepy.orchestration.configuration import Configuration -from wepy.orchestration.snapshot import SimApparatus, SimSnapshot -from wepy.sim_manager import Manager -from wepy.util.kv import KV, SQLITE3_INMEMORY_URI, gen_uri - - -class OrchestratorError(Exception): - """ """ - - pass - - -class Orchestrator: - """ """ - - # we freeze the pickle protocol for making hashes, because we care - # more about stability than efficiency of newer versions - HASH_PICKLE_PROTOCOL = 3 - - DEFAULT_WORKDIR = Configuration.DEFAULT_WORKDIR - DEFAULT_CONFIG_NAME = Configuration.DEFAULT_CONFIG_NAME - DEFAULT_NARRATION = Configuration.DEFAULT_NARRATION - DEFAULT_MODE = Configuration.DEFAULT_MODE - - DEFAULT_CHECKPOINT_FILENAME = "checkpoint.orch.sqlite" - ORCH_FILENAME_TEMPLATE = "{config}{narration}.orch.sqlite" - - # the default way to oepn up the whole parent database - DEFAULT_ORCHESTRATION_MODE = "x" - - # mode to open the individual kv stores on the parent database - KV_MODE = "r+" - - # default timeout for connecting to a database - SQLITE3_DEFAULT_TIMEOUT = 5 - - # the fields to return (and their order) as a record for a run - # query - RUN_SELECT_FIELDS = ("last_cycle_idx", "config_hash") - - def __init__( - self, - orch_path=None, - mode="x", - append_only=False, - ): - self._mode = mode - self._append_only = append_only - - # handle the path and convert to a proper URI for the database - # given the path and the mode - self._db_uri = gen_uri(orch_path, mode) - - # run table: start_hash, end_hash, num_cycles, configuration_id - - # get a raw connection to the database - self._db = sqlite3.connect( - self.db_uri, uri=True, timeout=self.SQLITE3_DEFAULT_TIMEOUT - ) - self._closed = False - - # set isolation level to autocommit - self._db.isolation_level = None - - # we can use read_uncommited only in append_only mode (no - # updates) because you never have to worry about dirty reads - # since you can't update - if self.append_only: - self._db.execute("PRAGMA read_uncommited=1") - - # we make a table for the run data, if it doesn't already - # exist - c = self._db.cursor().execute(self.create_run_table_query) - - # initialize or open each of the separate KV-stores (tables in - # the same SQLite3 database) - - # change the mode for the KV stores since we already created the database - - # metadata: default init walkers, default apparatus, default - # configuration - self.metadata_kv = KV( - db_url=self.db_uri, - table="meta", - mode="a", - value_types=None, - append_only=self.append_only, - ) - - # snapshots - self.snapshot_kv = KV( - db_url=self.db_uri, - table="snapshots", - primary_key="snaphash", - value_name="snapshot", - mode="a", - append_only=self.append_only, - ) - - # configurations - self.configuration_kv = KV( - db_url=self.db_uri, - table="configurations", - primary_key="config_hash", - value_name="config", - mode="a", - append_only=self.append_only, - ) - - @property - def mode(self): - return self._mode - - @property - def append_only(self): - return self._append_only - - def close(self): - if self._closed == True: - raise IOError("The database connection is already closed") - - else: - # close all the connections - self.metadata_kv.close() - self.configuration_kv.close() - self.snapshot_kv.close() - self._db.close() - self._closed = True - - @property - def db_uri(self): - return self._db_uri - - @property - def orch_path(self): - # if it is not an in-memory database we parse off the path and - # return that - if self.db_uri == SQLITE3_INMEMORY_URI: - return None - else: - # URIs have the following form: protocol:url?query - # destructure the URI - _, tail = self.db_uri.split(":") - - if len(tail.split("?")) > 1: - url, _ = tail.split("?") - else: - url = tail - - return url - - @classmethod - def serialize(cls, snapshot): - """Serialize a snapshot to a compressed, encoded, pickle string - representation. - - Currently uses the dill module for pickling because the base - pickle module is inadequate. However, it is mostly compatible - and can be read natively with pickle but this usage is - officially not supported. Instead use the deserialize_snapshot. - - Also compresses with default zlib compression and is encoded - in base64. - - The object will always have a deepcopy performed on it so that - all of the extraneous references to it are avoided since there - is no (AFAIK) way to make sure all references to an object are - deleted. - - NOTE: Perhaps there is a way and that should be done (and - tested) to see if it provides stable pickles (i.e. pickles - that always hash to the same value). To avoid the overhead of - copying large objects. - - Parameters - ---------- - snapshot : SimSnapshot object - The snapshot of the simulation you want to serialize. - - Returns - ------- - serial_str : str - Serialized string of the snapshot object - - """ - - serial_str = b64encode( - compress( - dill.dumps( - deepcopy(snapshot), protocol=cls.HASH_PICKLE_PROTOCOL, recurse=True - ) - ) - ) - - return serial_str - - # core methods for serializing python objects, used for snapshots, - # apparatuses, configurations, and the initial walker list - - @classmethod - def deserialize(cls, serial_str): - """Deserialize an unencoded string snapshot to an object. - - Parameters - ---------- - serial_str : str - Serialized string of the snapshot object - - Returns - ------- - snapshot : SimSnapshot object - Simulation snapshot object - - """ - - return dill.loads(decompress(b64decode(serial_str))) - - # defaults getters and setters - def set_default_sim_apparatus(self, sim_apparatus): - # serialize the apparatus and then set it - serial_app = self.serialize(sim_apparatus) - - self.metadata_kv["default_sim_apparatus"] = serial_app - - def set_default_init_walkers(self, init_walkers): - # serialize the apparatus and then set it - serial_walkers = self.serialize(init_walkers) - - self.metadata_kv["default_init_walkers"] = serial_walkers - - def set_default_configuration(self, configuration): - # serialize the apparatus and then set it - serial_config = self.serialize(configuration) - - config_hash = self.hash_snapshot(serial_config) - - self.metadata_kv["default_configuration_hash"] = config_hash - - self.configuration_kv[config_hash] = serial_config - - def set_default_snapshot(self, snapshot): - snaphash = self.add_snapshot(snapshot) - - # then save the hash in the metadata - self.metadata_kv["default_snapshot_hash"] = snaphash - - return snaphash - - def gen_default_snapshot(self): - # generate the snapshot - sim_start_hash = self.gen_start_snapshot(self.get_default_init_walkers()) - - # then save the hash in the metadata - self.metadata_kv["default_snapshot_hash"] = sim_start_hash - - return sim_start_hash - - def get_default_sim_apparatus(self): - return self.deserialize(self.metadata_kv["default_sim_apparatus"]) - - def get_default_init_walkers(self): - return self.deserialize(self.metadata_kv["default_init_walkers"]) - - def get_default_configuration(self): - config_hash = self.metadata_kv["default_configuration_hash"] - - return self.get_configuration(config_hash) - - def get_default_configuration_hash(self): - return self.metadata_kv["default_configuration_hash"] - - def get_default_snapshot(self): - start_hash = self.metadata_kv["default_snapshot_hash"] - - return self.get_snapshot(start_hash) - - def get_default_snapshot_hash(self): - return self.metadata_kv["default_snapshot_hash"] - - @classmethod - def hash_snapshot(cls, serial_str): - """Parameters - ---------- - serial_str : - - - Returns - ------- - - """ - return md5(serial_str).hexdigest() - - def get_snapshot(self, snapshot_hash): - """Returns a copy of a snapshot. - - Parameters - ---------- - snapshot_hash : - - - Returns - ------- - - """ - - return self.deserialize(self.snapshot_kv[snapshot_hash]) - - def get_configuration(self, config_hash): - """Returns a copy of a snapshot. - - Parameters - ---------- - config_hash : - - - Returns - ------- - - """ - - return self.deserialize(self.configuration_kv[config_hash]) - - @property - def snapshot_hashes(self): - """ """ - - # iterate over the snapshot kv - return list(self.snapshot_kv.keys()) - - @property - def configuration_hashes(self): - """ """ - - # iterate over the snapshot kv - return list(self.configuration_kv.keys()) - - def add_snapshot(self, snapshot): - """Parameters - ---------- - snapshot : - - Returns - ------- - - """ - - # serialize the snapshot using the protocol for doing so - serialized_snapshot = self.serialize(snapshot) - - # get the hash of the snapshot - snaphash = self.hash_snapshot(serialized_snapshot) - - # check that the hash is not already in the snapshots - if any([True if snaphash == md5 else False for md5 in self.snapshot_hashes]): - # just skip the rest of the function and return the hash - return snaphash - - # save the snapshot in the KV store - self.snapshot_kv[snaphash] = serialized_snapshot - - return snaphash - - def add_serial_snapshot(self, serial_snapshot): - # get the hash of the snapshot - snaphash = self.hash_snapshot(serial_snapshot) - - # check that the hash is not already in the snapshots - if any([True if snaphash == md5 else False for md5 in self.snapshot_hashes]): - # just skip the rest of the function and return the hash - return snaphash - - # save the snapshot in the KV store - self.snapshot_kv[snaphash] = serial_snapshot - - return snaphash - - def gen_start_snapshot(self, init_walkers): - """Parameters - ---------- - init_walkers : - - - Returns - ------- - - """ - - # make a SimSnapshot object using the initial walkers and - start_snapshot = SimSnapshot(init_walkers, self.get_default_sim_apparatus()) - - # save the snapshot, and generate its hash - sim_start_md5 = self.add_snapshot(start_snapshot) - - return sim_start_md5 - - @property - def default_snapshot_hash(self): - """ """ - return self.metadata_kv["default_snapshot_hash"] - - @property - def default_snapshot(self): - """ """ - return self.get_snapshot(self.default_snapshot_hash) - - def snapshot_registered(self, snapshot): - """Check whether a snapshot is already in the database, based on the - hash of it. - - This serializes the snapshot so may be slow. - - Parameters - ---------- - snapshot : SimSnapshot object - The snapshot object you want to query for. - - Returns - ------- - - """ - - # serialize and hash the snapshot - snaphash = self.hash_snapshot(self.serialize(snapshot)) - - # then check it - return self.snapshot_hash_registered(snaphash) - - def snapshot_hash_registered(self, snapshot_hash): - """Check whether a snapshot hash is already in the database. - - Parameters - ---------- - snapshot_hash : str - The string hash of the snapshot. - - Returns - ------- - - """ - - if any([True if snapshot_hash == h else False for h in self.snapshot_hashes]): - return True - else: - return False - - def configuration_hash_registered(self, config_hash): - """Check whether a snapshot hash is already in the database. - - Parameters - ---------- - snapshot_hash : str - The string hash of the snapshot. - - Returns - ------- - - """ - - if any( - [True if config_hash == h else False for h in self.configuration_hashes] - ): - return True - else: - return False - - ### run methods - - def add_configuration(self, configuration): - serialized_config = self.serialize(configuration) - - config_hash = self.hash_snapshot(serialized_config) - - # check that the hash is not already in the snapshots - if any( - [True if config_hash == md5 else False for md5 in self.configuration_hashes] - ): - # just skip the rest of the function and return the hash - return config_hash - - # save the snapshot in the KV store - self.configuration_kv[config_hash] = serialized_config - - return config_hash - - def add_serial_configuration(self, serial_configuration): - # get the hash of the configuration - snaphash = self.hash_snapshot(serial_configuration) - - # check that the hash is not already in the configurations - if any( - [True if snaphash == md5 else False for md5 in self.configuration_hashes] - ): - # just skip the rest of the function and return the hash - return snaphash - - # save the configuration in the KV store - self.configuration_kv[snaphash] = serial_configuration - - return snaphash - - @property - def create_run_table_query(self): - create_run_table_query = """ - CREATE TABLE IF NOT EXISTS runs - (start_hash TEXT NOT NULL, - end_hash TEXT NOT NULL, - config_hash NOT NULL, - last_cycle_idx INTEGER NOT NULL, - PRIMARY KEY (start_hash, end_hash)) - - """ - - return create_run_table_query - - @property - def add_run_record_query(self): - add_run_row_query = """ - INSERT INTO runs (start_hash, end_hash, config_hash, last_cycle_idx) - VALUES (?, ?, ?, ?) - """ - - return add_run_row_query - - @property - def update_run_record_query(self): - q = """ - UPDATE runs - SET config_hash = ?, - last_cycle_idx = ? - WHERE start_hash=? AND end_hash=? - """ - - return q - - @property - def delete_run_record_query(self): - q = """ - DELETE FROM runs - WHERE start_hash=? AND end_hash=? - """ - - return q - - def _add_run_record(self, start_hash, end_hash, configuration_hash, cycle_idx): - params = (start_hash, end_hash, configuration_hash, cycle_idx) - - # do it as a transaction - c = self._db.cursor() - - # run the insert - c.execute(self.add_run_record_query, params) - - def _delete_run_record(self, start_hash, end_hash): - params = (start_hash, end_hash) - - cursor = self._db.cursor() - - cursor.execute(self.delete_run_record_query, params) - - def _update_run_record( - self, start_hash, end_hash, new_config_hash, new_last_cycle_idx - ): - params = (new_config_hash, new_last_cycle_idx, start_hash, end_hash) - - # do it as a transaction - c = self._db.cursor() - - # run the update - c.execute(self.update_run_record_query, params) - - def register_run(self, start_hash, end_hash, config_hash, cycle_idx): - """Parameters - ---------- - start_hash : - - end_hash : - - config_hash : - - cycle_idx : int - The cycle of the simulation run the checkpoint was generated for. - - Returns - ------- - - """ - - # check that the hashes are for snapshots in the orchestrator - # if one is not registered raise an error - if not self.snapshot_hash_registered(start_hash): - raise OrchestratorError( - "snapshot start_hash {} is not registered with the orchestrator".format( - start_hash - ) - ) - - if not self.snapshot_hash_registered(end_hash): - raise OrchestratorError( - "snapshot end_hash {} is not registered with the orchestrator".format( - end_hash - ) - ) - - if not self.configuration_hash_registered(config_hash): - raise OrchestratorError( - "config hash {} is not registered with the orchestrator".format( - config_hash - ) - ) - - # save the configuration and get it's id - - self._add_run_record(start_hash, end_hash, config_hash, cycle_idx) - - def get_run_records(self): - get_run_record_query = """ - SELECT * - FROM runs - """.format( - fields=", ".join(self.RUN_SELECT_FIELDS) - ) - - cursor = self._db.cursor() - cursor.execute(get_run_record_query) - records = cursor.fetchall() - - return records - - def get_run_record(self, start_hash, end_hash): - get_run_record_query = """ - SELECT {fields} - FROM runs - WHERE start_hash=? AND end_hash=? - """.format( - fields=", ".join(self.RUN_SELECT_FIELDS) - ) - - params = (start_hash, end_hash) - - cursor = self._db.cursor() - cursor.execute(get_run_record_query, params) - record = cursor.fetchone() - - return record - - def run_last_cycle_idx(self, start_hash, end_hash): - record = self.get_run_record(start_hash, end_hash) - - last_cycle_idx = record[self.RUN_SELECT_FIELDS.index("last_cycle_idx")] - - return last_cycle_idx - - def run_configuration(self, start_hash, end_hash): - record = self.get_run_record(start_hash, end_hash) - - config_hash = record[self.RUN_SELECT_FIELDS.index("config_hash")] - - # get the configuration object and deserialize it - return self.deserialize(self.configuration_kv[config_hash]) - - def run_configuration_hash(self, start_hash, end_hash): - record = self.get_run_record(start_hash, end_hash) - - config_hash = record[self.RUN_SELECT_FIELDS.index("config_hash")] - - return config_hash - - def run_hashes(self): - return [(rec[0], rec[1]) for rec in self.get_run_records()] - - def run_continues(self, start_hash, end_hash): - """Given a start hash and end hash for a run, find the run that this - continues. - - Parameters - ---------- - start_hash : - - end_hash : - - - Returns - ------- - run_id - - """ - - # loop through the runs in this orchestrator until we find one - # where the start_hash matches the end hash - runs = self.run_hashes() - run_idx = 0 - while True: - run_start_hash, run_end_hash = runs[run_idx] - - # if the start hash of the queried run is the same as the - # end hash for this run we have found it - if start_hash == run_end_hash: - return (run_start_hash, run_end_hash) - - run_idx += 1 - - # if the index is over the number of runs we quit and - # return None as no match - if run_idx >= len(runs): - return None - - def _init_checkpoint_db(self, start_hash, configuration, checkpoint_dir, mode="x"): - logger.debug("Initializing checkpoint orch database") - - # make the checkpoint with the default filename at the checkpoint directory - checkpoint_path = osp.join(checkpoint_dir, self.DEFAULT_CHECKPOINT_FILENAME) - - # create a new database in the mode specified - logger.debug("Creating checkpoint database") - checkpoint_orch = Orchestrator(checkpoint_path, mode=mode) - - # add the starting snapshot, bypassing the serialization stuff - logger.debug("Setting the starting snapshot") - checkpoint_orch.snapshot_kv[start_hash] = self.snapshot_kv[start_hash] - - # if we have a new configuration at runtime serialize and - # hash it - serialized_config = self.serialize(configuration) - config_hash = self.hash_snapshot(serialized_config) - - # save the configuration as well - checkpoint_orch.configuration_kv[config_hash] = serialized_config - - checkpoint_orch.close() - logger.debug("closing connection to checkpoint database") - - return checkpoint_path, config_hash - - def _save_checkpoint( - self, - checkpoint_snapshot, - config_hash, - checkpoint_db_path, - cycle_idx, - ): - """Parameters - ---------- - checkpoint_snapshot : - - config_hash : - - checkpoint_db_path : - - mode : - (Default value = 'wb') - - Returns - ------- - - """ - - # orchestrator wrapper to the db - logger.debug("Opening the checkpoint orch database") - checkpoint_orch = Orchestrator(checkpoint_db_path, mode="r+") - - # connection to the db - cursor = checkpoint_orch._db.cursor() - - # we replicate the code for adding the snapshot here because - # we want it to occur transactionally the delete and add - - # serialize the snapshot using the protocol for doing so - serialized_snapshot = self.serialize(checkpoint_snapshot) - - # get the hash of the snapshot - snaphash = self.hash_snapshot(serialized_snapshot) - - # the queries for deleting and inserting the new run record - delete_query = """ - DELETE FROM runs - WHERE start_hash=? - AND end_hash=? - """ - - insert_query = """ - INSERT INTO runs (start_hash, end_hash, config_hash, last_cycle_idx) - VALUES (?, ?, ?, ?) - """ - - # if there are any runs in the checkpoint orch remove the - # final snapshot - delete_params = None - if len(checkpoint_orch.run_hashes()) > 0: - start_hash, old_checkpoint_hash = checkpoint_orch.run_hashes()[0] - - delete_params = (start_hash, old_checkpoint_hash) - else: - start_hash = list(checkpoint_orch.snapshot_kv.keys())[0] - - # the config should already be in the orchestrator db - insert_params = (start_hash, snaphash, config_hash, cycle_idx) - - # start this whole process as a transaction so we don't get - # something weird in between - logger.debug("Starting transaction for updating run table in checkpoint") - cursor.execute("BEGIN TRANSACTION") - - # add the new one, using a special method for setting inside - # of a transaction - logger.debug("setting the new checkpoint snapshot into the KV") - cursor = checkpoint_orch.snapshot_kv.set_in_tx( - cursor, snaphash, serialized_snapshot - ) - logger.debug("finished") - - # if we need to delete the old end of the run snapshot and the - # run record for it - if delete_params is not None: - logger.debug("Old run record needs to be removed") - - # remove the old run from the run table - logger.debug("Deleting the old run record") - cursor.execute(delete_query, delete_params) - logger.debug("finished") - - # register the new run in the run table - logger.debug("Inserting the new run record") - cursor.execute(insert_query, insert_params) - logger.debug("finished") - - # end the transaction - logger.debug("Finishing transaction") - cursor.execute("COMMIT") - logger.debug("Transaction committed") - - # we do the removal of the old snapshot outside of the - # transaction since it is slow and can cause timeouts to - # occur. Furthermore, it is okay if it is in the checkpoint as - # the run record is what matters as long as the new checkpoint - # is there. - - # delete the old snapshot if we need to - if delete_params is not None: - # WARN: occasionally and for unknown reasons we have found - # that the final checkpoint hash is the same as the one - # before. (The case where the last snapshot is on the same - # cycle as a backup is already covered). So as a last - # resort, we check that they don't have the same hash. If - # they do we don't delete it! - if snaphash != old_checkpoint_hash: - logger.debug("Deleting the old snapshot") - del checkpoint_orch.snapshot_kv[old_checkpoint_hash] - logger.debug("finished") - else: - logger.warn( - "Final snapshot has same hash as the previous checkpoint. Not deleting the previous one." - ) - - checkpoint_orch.close() - logger.debug("closed the checkpoint orch connection") - - @staticmethod - def gen_sim_manager(start_snapshot, configuration): - """Parameters - ---------- - start_snapshot : - - configuration : - - - Returns - ------- - - """ - - # construct the sim manager, in a wepy specific way - sim_manager = Manager( - start_snapshot.walkers, - runner=start_snapshot.apparatus.filters[0], - boundary_conditions=start_snapshot.apparatus.filters[1], - resampler=start_snapshot.apparatus.filters[2], - # configuration options - work_mapper=configuration.work_mapper, - reporters=configuration.reporters, - sim_monitor=configuration.monitor, - ) - - return sim_manager - - def run_snapshot_by_time( - self, - start_hash, - run_time, - n_steps, - checkpoint_freq=None, - checkpoint_dir=None, - configuration=None, - configuration_hash=None, - checkpoint_mode="x", - ): - """For a finished run continue it but resetting all the state of the - resampler and boundary conditions - - Parameters - ---------- - start_hash : - - run_time : - - n_steps : - - checkpoint_freq : - (Default value = None) - checkpoint_dir : - (Default value = None) - configuration : - (Default value = None) - configuration_hash : - (Default value = None) - checkpoint_mode : - (Default value = None) - - Returns - ------- - - """ - - # you must have a checkpoint dir if you ask for a checkpoint - # frequency - if checkpoint_freq is not None and checkpoint_dir is None: - raise ValueError( - "Must provide a directory for the checkpoint file " - "is a frequency is specified" - ) - - if configuration_hash is not None and configuration is not None: - raise ValueError( - "Cannot specify both a hash of an existing configuration" - "and provide a runtime configuration" - ) - - # if no configuration was specified we use the default one, oth - elif (configuration is None) and (configuration_hash is None): - configuration = self.get_default_configuration() - - # if a configuration hash was given only then we retrieve that - # configuration since we must pass configurations to the - # checkpoint DB initialization - elif configuration_hash is not None: - configuration = self.configuration_kv[configuration_hash] - - # check that the directory for checkpoints exists, and create - # it if it doesn't and isn't already created - if checkpoint_dir is not None: - checkpoint_dir = osp.realpath(checkpoint_dir) - os.makedirs(checkpoint_dir, exist_ok=True) - - # if the checkpoint dir is not specified don't create a - # checkpoint db orch - checkpoint_db_path = None - if checkpoint_dir is not None: - logger.debug("Initialization of checkpoint database is requested") - checkpoint_db_path, configuration_hash = self._init_checkpoint_db( - start_hash, configuration, checkpoint_dir, mode=checkpoint_mode - ) - logger.debug("finished initializing checkpoint database") - - # get the snapshot and the configuration to use for the sim_manager - start_snapshot = self.get_snapshot(start_hash) - - # generate the simulation manager given the snapshot and the - # configuration - sim_manager = self.gen_sim_manager(start_snapshot, configuration) - - # handle and process the optional arguments for running simulation - if "runner" in configuration.apparatus_opts: - runner_opts = configuration.apparatus_opts["runner"] - else: - runner_opts = None - - # run the init subroutine for the simulation manager - logger.debug("Running sim_manager.init") - sim_manager.init() - - # run each cycle manually creating checkpoints when necessary - logger.debug("Starting run loop") - walkers = sim_manager.init_walkers - cycle_idx = 0 - start_time = time.time() - while time.time() - start_time < run_time: - logger.debug("Running cycle {}".format(cycle_idx)) - # run the cycle - walkers, filters = sim_manager.run_cycle( - walkers, - n_steps, - cycle_idx, - runner_opts=runner_opts, - ) - - # check to see if a checkpoint is necessary - if checkpoint_freq is not None: - if cycle_idx % checkpoint_freq == 0: - logger.debug("Checkpoint is required for this cycle") - - # make the checkpoint snapshot - logger.debug("Generating the simulation snapshot") - checkpoint_snapshot = SimSnapshot(walkers, SimApparatus(filters)) - - # save the checkpoint (however that is implemented) - logger.debug("saving the checkpoint to the database") - self._save_checkpoint( - checkpoint_snapshot, - configuration_hash, - checkpoint_db_path, - cycle_idx, - ) - logger.debug("finished saving the checkpoint to the database") - - # increase the cycle index for the next cycle - cycle_idx += 1 - - logger.debug("Finished the run cycle") - - # the cycle index was set for the next cycle which didn't run - # so we decrement it - last_cycle_idx = cycle_idx - 1 - - logger.debug("Running sim_manager.cleanup") - # run the cleanup subroutine - sim_manager.cleanup() - - # run the segment given the sim manager and run parameters - end_snapshot = SimSnapshot(walkers, SimApparatus(filters)) - - logger.debug("Run finished") - # return the things necessary for saving to the checkpoint if - # that is what is wanted later on - return end_snapshot, configuration_hash, checkpoint_db_path, last_cycle_idx - - def orchestrate_snapshot_run_by_time( - self, - snapshot_hash, - run_time, - n_steps, - checkpoint_freq=None, - checkpoint_dir=None, - orchestrator_path=None, - configuration=None, - # these can reparametrize the paths - # for both the orchestrator produced - # files as well as the configuration - work_dir=None, - config_name=None, - narration=None, - mode=None, - # extra kwargs will be passed to the - # configuration.reparametrize method - **kwargs, - ): - """Parameters - ---------- - snapshot_hash : - - run_time : - - n_steps : - - checkpoint_freq : - (Default value = None) - checkpoint_dir : - (Default value = None) - orchestrator_path : - (Default value = None) - configuration : - (Default value = None) - # these can reparametrize the paths# for both the orchestrator produced# files as well as the configurationwork_dir : - (Default value = None) - config_name : - (Default value = None) - narration : - (Default value = None) - mode : - (Default value = None) - # extra kwargs will be passed to the# configuration.reparametrize method**kwargs : - - - Returns - ------- - - """ - - # for writing the orchestration files we set the default mode - # if mode is not given - if mode is None: - # the orchestrator mode is used for pickling the - # orchestrator and so must be in bytes mode - orch_mode = self.DEFAULT_ORCHESTRATION_MODE - - # there are two possible uses for the path reparametrizations: - # the configuration and the orchestrator file paths. If both - # of those are explicitly specified by passing in the whole - # configuration object or both of checkpoint_dir, - # orchestrator_path then those reparametrization kwargs will - # not be used. As this is likely not the intention of the user - # we will raise an error. If there is even one use for them no - # error will be raised. - - # first check if any reparametrizations were even requested - parametrizations_requested = ( - True if work_dir is not None else False, - True if config_name is not None else False, - True if narration is not None else False, - True if mode is not None else False, - ) - - # check if there are any available targets for reparametrization - reparametrization_targets = ( - True if configuration is None else False, - True if checkpoint_dir is None else False, - True if orchestrator_path is None else False, - ) - - # if paramatrizations were requested and there are no targets - # we need to raise an error - if any(parametrizations_requested) and not any(reparametrization_targets): - raise OrchestratorError( - "Reparametrizations were requested but none are possible," - " due to all possible targets being already explicitly given" - ) - - # if any paths were not given and no defaults for path - # parameters we want to fill in the defaults for them. This - # will also fill in any missing parametrizations with defaults - - # we do this by just setting the path parameters if they - # aren't set, then later the parametrization targets will be - # tested for if they have been set or not, and if they haven't - # then these will be used to generate paths for them. - if work_dir is None: - work_dir = self.DEFAULT_WORKDIR - if config_name is None: - config_name = self.DEFAULT_CONFIG_NAME - if narration is None: - narration = self.DEFAULT_NARRATION - if mode is None: - mode = self.DEFAULT_MODE - - # if no configuration was specified use the default one - if configuration is None: - configuration = self.get_default_configuration() - - # reparametrize the configuration with the given path - # parameters and anything else in kwargs. If they are none - # this will have no effect anyhow - logger.debug("Reparametrizing the configuration") - configuration = configuration.reparametrize( - work_dir=work_dir, - config_name=config_name, - narration=narration, - mode=mode, - **kwargs, - ) - - # make parametric paths for the checkpoint directory and the - # orchestrator pickle to be made, unless they are explicitly given - - if checkpoint_dir is None: - # the checkpoint directory will be in the work dir - logger.debug("checkpoint directory defaulted to the work_dir") - checkpoint_dir = work_dir - - logger.debug("In the orchestrate run, calling to run_snapshot by time") - # then actually run the simulation with checkpointing. This - # returns the end snapshot and doesn't write out anything to - # orchestrators other than the checkpointing - ( - end_snapshot, - configuration_hash, - checkpoint_db_path, - last_cycle_idx, - ) = self.run_snapshot_by_time( - snapshot_hash, - run_time, - n_steps, - checkpoint_freq=checkpoint_freq, - checkpoint_dir=checkpoint_dir, - configuration=configuration, - checkpoint_mode=orch_mode, - ) - - logger.debug("Finished running snapshot by time") - - # if the last cycle in the run was a checkpoint skip this step - # of saving a checkpoint - do_final_checkpoint = True - - # make sure the checkpoint_freq is defined before testing it - if checkpoint_freq is not None: - if checkpoint_freq % last_cycle_idx == 0: - logger.debug("Last cycle saved a checkpoint, no need to save one") - do_final_checkpoint = False - - if do_final_checkpoint: - logger.debug("Saving a final checkpoint for the end of the run") - # now that it is finished we save the final snapshot to the - # checkpoint file. This is done transactionally using the - # SQLite transaction functionality (either succeeds or doesn't - # happen) that way we don't have worry about data integrity - # loss. Here we also don't have to worry about other processes - # interacting with the checkpoint which makes it isolated. - self._save_checkpoint( - end_snapshot, configuration_hash, checkpoint_db_path, last_cycle_idx - ) - logger.debug("Finished saving the final checkpoint for the run") - - # then return the final orchestrator - logger.debug("Getting a connection to that orch to retun") - checkpoint_orch = Orchestrator(checkpoint_db_path, mode="r+", append_only=True) - - return checkpoint_orch - - -def reconcile_orchestrators(host_path, *orchestrator_paths): - """Parameters - ---------- - template_orchestrator : - - *orchestrators : - - - Returns - ------- - - """ - - if not osp.exists(host_path): - assert ( - len(orchestrator_paths) > 1 - ), "If the host path is a new orchestrator, must give at least 2 orchestrators to merge." - - # open the host orchestrator at the location which will have all - # of the new things put into it from the other orchestrators. If - # it doesn't already exist it will be created otherwise open - # read-write. - new_orch = Orchestrator(orch_path=host_path, mode="a", append_only=True) - - # TODO deprecate, if there is no defaults we can't set them since - # the mode is append only, we don't really care about these so - # don't set them, otherwise do some mode logic to figure this out - # and open in write mode and set defaults, then change to append - # only - - # # if this is an existing orchestrator copy the default - # # sim_apparatus and init_walkers - # try: - # default_app = new_orch.get_default_sim_apparatus() - # except KeyError: - # # no default apparatus, that is okay - # pass - # else: - # # set it - # new_orch.set_default_sim_apparatus(default_app) - - # # same for the initial walkers - # try: - # default_walkers = new_orch.get_default_init_walkers() - # except KeyError: - # # no default apparatus, that is okay - # pass - # else: - # # set it - # new_orch.set_default_sim_apparatus(default_walkers) - - for orch_path in orchestrator_paths: - # open it in read-write fail if doesn't exist - orch = Orchestrator(orch_path=orch_path, mode="r+", append_only=True) - - # add in all snapshots from each orchestrator, by the hash not the - # snapshots themselves, we trust they are correct - for snaphash in orch.snapshot_hashes: - # check that the hash is not already in the snapshots - if any( - [True if snaphash == md5 else False for md5 in new_orch.snapshot_hashes] - ): - # skip it and move on - continue - - # if it is not copy it over without deserializing - new_orch.snapshot_kv[snaphash] = orch.snapshot_kv[snaphash] - - # add in the configurations for the runs from each - # orchestrator, by the hash not the snapshots themselves, we - # trust they are correct - for run_id in orch.run_hashes(): - config_hash = orch.run_configuration_hash(*run_id) - - # check that the hash is not already in the snapshots - if any( - [ - True if config_hash == md5 else False - for md5 in new_orch.configuration_hashes - ] - ): - # skip it and move on - continue - - # if it is not set it - new_orch.configuration_kv[config_hash] = orch.configuration_kv[config_hash] - - # concatenate the run table with an SQL union from an attached - # database - - attached_table_name = "other" - - # query to attach the foreign database - attach_query = """ - ATTACH '{}' AS {} - """.format( - orch_path, attached_table_name - ) - - # query to update the runs tabel with new unique runs - union_query = """ - INSERT INTO runs - SELECT * FROM ( - SELECT * FROM {}.runs - EXCEPT - SELECT * FROM runs - ) - """.format( - attached_table_name - ) - - # query to detach the table - detach_query = """ - DETACH {} - """.format( - attached_table_name - ) - - # then run the queries - - cursor = new_orch._db.cursor() - try: - cursor.execute("BEGIN TRANSACTION") - cursor.execute(attach_query) - cursor.execute(union_query) - cursor.execute("COMMIT") - cursor.execute(detach_query) - except: - cursor.execute("COMMIT") - # Standard Library - import pdb - - pdb.set_trace() - cursor.execute( - "SELECT * FROM (SELECT * FROM other.runs EXCEPT SELECT * FROM runs)" - ) - recs = cursor.fetchall() - - return new_orch diff --git a/src/wepy/orchestration/snapshot.py b/src/wepy/orchestration/snapshot.py deleted file mode 100644 index 9eeb818d..00000000 --- a/src/wepy/orchestration/snapshot.py +++ /dev/null @@ -1,73 +0,0 @@ -# Standard Library -from copy import deepcopy - - -class SimApparatus: - """The simulation apparatus are the components needed for running a - simulation without the initial conditions for starting the simulation. - - A runner is strictly necessary but a resampler and boundary - conditions are not. - - Parameters - ---------- - - Returns - ------- - - """ - - def __init__(self, filters): - self._filters = deepcopy(filters) - - @property - def filters(self): - """ """ - return self._filters - - -class WepySimApparatus(SimApparatus): - """ """ - - RUNNER_IDX = 0 - BC_IDX = 1 - RESAMPLER_IDX = 2 - - def __init__(self, runner, resampler=None, boundary_conditions=None): - if resampler is None: - raise ValueError("must provide a resampler") - - # add them in the order they are done in Wepy - filters = [runner, boundary_conditions, resampler] - - super().__init__(filters) - - @property - def runner(self): - return self.filters[self.RUNNER_IDX] - - @property - def boundary_conditions(self): - return self.filters[self.BC_IDX] - - @property - def resampler(self): - return self.filters[self.RESAMPLER_IDX] - - -class SimSnapshot: - """ """ - - def __init__(self, walkers, apparatus): - self._walkers = deepcopy(walkers) - self._apparatus = deepcopy(apparatus) - - @property - def walkers(self): - """ """ - return self._walkers - - @property - def apparatus(self): - """ """ - return self._apparatus diff --git a/src/wepy/util/kv.py b/src/wepy/util/kv.py deleted file mode 100644 index 2d963080..00000000 --- a/src/wepy/util/kv.py +++ /dev/null @@ -1,436 +0,0 @@ -"""Implement a key-value store on top of sqlite3 database.""" - -# Copyright (c) 2012, Alex Morega -# All rights reserved. - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: - -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. - -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. - -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# Software copied and modified heavily from this source - -# Standard Library -import logging - -logger = logging.getLogger(__name__) -# Standard Library -import os -import os.path as osp -import sqlite3 -from collections.abc import MutableMapping -from contextlib import contextmanager - -# mapping of the modes we support and the modes that SQLite provides -# KV mode -> sqlite3 mode -MODE_MAPPING = ( - ("r", "ro"), - ("r+", "rw"), - ("a", "rwc"), - ("x", None), - ("w", None), - ("w-", None), -) - -# modes that sqlite3 itself doesn't support and have to manual -# processing for -TRUNCATE_MODES = ("w",) -FAIL_IF_EXISTS_CREATE_MODES = ("w-", "x") - -SQLITE3_URI_TEMPLATE = "{protocol}:{url}" -SQLITE3_QUERY_URI_TEMPLATE = "{protocol}:{url}?{query}" - -SQLITE3_QUERY_JOIN_CHAR = "&" - -SQLITE3_INMEMORY_URI = "file::memory:?cache=shared" - - -# the default types for the values that is checked, is bytes -DEFAULT_VALUE_TYPES = (bytes, bytearray) - - -def gen_uri(db_url, mode_spec): - # if the db url is the in memory special string or None or the - # :memory: identifier, use the full in-memory URI - if db_url == SQLITE3_INMEMORY_URI or db_url is None or db_url == ":memory:": - db_uri = SQLITE3_INMEMORY_URI - - return db_uri - - # check for and split the URI into the components: protocol, URL, query - if len(db_url.split(":")) > 1: - # for a query - if len(db_url.split("?")) > 1: - # protocol and queries - ((protocol,), (url, query)) = [ - comp.split("?") for comp in db_url.split(":") - ] - - else: - # no query - protocol, url = db_url.split(":") - query = "" - else: - protocol = "" - if len(db_url.split("?")) > 1: - url, query = db_url.split("?") - else: - query = "" - url = db_url - - # process and build the URI given this information - - # if the protocol is not given set it to the default - if len(protocol) == 0: - protocol = "file" - - # split the query up into sections if it has them - if len(query.split("?")) > 1: - queries = query.split("?") - - # split the "key=value" pairs in each query - queries = {q.split("=")[0]: q.split("=")[0] for q in queries} - else: - queries = {} - - # now handle the mode. If it was given as an argument then we have - # to set the appropriate query in the URI. If it was given in the - # URI that takes precedence however. - - # check for a mode option in the given query section, if no mode - # is given then we set it depending on what mode_spec was given - if "mode" not in queries: - # default to rwc mode - mode_query_value = "rwc" - - if mode_spec is not None: - # if the mode is one of the modes in the mode mapping use that to - # generate the URI, otherwise raise an error - if mode_spec not in dict(MODE_MAPPING): - raise ValueError("kv mode spec '{}' not recognized".format(mode_spec)) - - else: - sqlite_mode = dict(MODE_MAPPING)[mode_spec] - - # if the sqlite_mode is recognized as a mode that can - # be given in the query section do that - if sqlite_mode is not None: - mode_query_value = sqlite_mode - - # otherwise we handle these special modes ourselves - # here, checking file properties and raising errors as - # necessary - else: - # if the mode is either 'x' or 'w-' check to see if the db - # already exists. If it does raise an error. - if mode_spec in FAIL_IF_EXISTS_CREATE_MODES: - if osp.exists(url): - raise OSError("File exists") - - # if it is 'w' we want to delete the old file - elif mode_spec in TRUNCATE_MODES: - # if it exists remove it - if osp.exists(url): - os.remove(url) - - # the sqlite_mode for these is rwc (a), since we - # need to create it - mode_query_value = "rwc" - - # add the mode query to queries list - queries["mode"] = mode_query_value - - # if thw queries are empty just use the protocol and URL - if not queries: - db_uri = SQLITE3_URI_TEMPLATE.format(protocol=protocol, url=url) - # otherwise do the whole thing - else: - # build the query substring - query = SQLITE3_QUERY_JOIN_CHAR.join( - ["{}={}".format(key, value) for key, value in queries.items()] - ) - - # build the URI string - db_uri = SQLITE3_QUERY_URI_TEMPLATE.format( - protocol=protocol, url=url, query=query - ) - - return db_uri - - -class KV(MutableMapping): - def __init__( - self, - db_url=None, - table="data", - primary_key="key", - value_name="value", - timeout=5, - mode="x", - append_only=False, - value_types=DEFAULT_VALUE_TYPES, - ): - # generate a good URI from the url and the mode - db_uri = gen_uri(db_url, mode) - - self._mode = mode - self._append_only = append_only - - # set the value types for this kv - self._kv_types = value_types - - self._db_uri = db_uri - - # connect to the db - self._db = sqlite3.connect(self._db_uri, timeout=timeout, uri=True) - self._closed = False - - # set the isolation level to autocommit - self._db.isolation_level = None - - # we can use read_uncommited only in append_only mode (no - # updates) because you never have to worry about dirty reads - # since you can't update - if self.append_only: - self._execute("PRAGMA read_uncommited=1") - - self._table = table - self._primary_key = primary_key - self._value_name = value_name - - # create the table if it doesn't exist and set the key names - create_table_query = """ - CREATE TABLE IF NOT EXISTS {table_name} - ({key_name} PRIMARY KEY, {value_name}) - """.format( - table_name=self.table, key_name=self.primary_key, value_name=self.value_name - ) - self._execute(create_table_query) - - self._locks = 0 - - @property - def mode(self): - return self._mode - - @property - def append_only(self): - return self._append_only - - def close(self): - if self._closed == True: - raise IOError("The database connection is already closed") - - else: - self._db.close() - self._closed = True - - @property - def db_uri(self): - return self._db_uri - - @property - def db(self): - return self._db - - @property - def table(self): - return self._table - - @property - def primary_key(self): - return self._primary_key - - @property - def value_name(self): - return self._value_name - - @property - def value_types(self): - return self._kv_types - - def _execute(self, *args): - return self._db.cursor().execute(*args) - - def __len__(self): - [[n]] = self._execute("SELECT COUNT(*) FROM {table}".format(table=self.table)) - return n - - def __getitem__(self, key): - if key is None: - query = ( - "SELECT {value} FROM {table} WHERE {key} is NULL".format( - value=self.value_name, table=self.table, key=self.primary_key - ), - (), - ) - else: - query = ( - "SELECT {value} FROM {table} WHERE {key}=?".format( - value=self.value_name, table=self.table, key=self.primary_key - ), - (key,), - ) - - cursor = self._execute(*query) - result = cursor.fetchone() - - if result is None: - raise KeyError - else: - return result[0] - - def __iter__(self): - return ( - key - for [key] in self._execute( - "SELECT {key} FROM {table}".format( - key=self.primary_key, table=self.table - ), - (), - ) - ) - - def __setitem__(self, key, value): - """Set a value, must be in bytes format.""" - - # check the type of the value to make sure it is what this KV - # supports, if it is None then it is the standard python type - # translation - - if self.value_types is not None: - assert isinstance( - value, self.value_types - ), "Value must be a value supported by this kv" - - self.lockless_set(key, value) - - def __delitem__(self, key): - logger.debug("Deleting the snapshot {}".format(key)) - - # no deletions in append only mode - if self.append_only: - raise sqlite3.IntegrityError( - "DB is opened in append only mode, and {} has already been set".format( - key - ) - ) - - # delete it if it exists - elif key in self: - logger.debug("executing delete query") - self._execute(self.del_query, (key,)) - logger.debug("finished") - - else: - raise KeyError - - @property - def insert_query(self): - query = "INSERT INTO {table} VALUES (?, ?)".format(table=self.table) - - return query - - @property - def update_query(self): - query = "UPDATE {table} SET {value}=? WHERE {key}=?".format( - key=self.primary_key, value=self.value_name, table=self.table - ) - - return query - - @property - def del_query(self): - query = "DELETE FROM {table} WHERE {key}=?".format( - key=self.primary_key, table=self.table - ) - - return query - - def lockless_set(self, key, value): - """An implementation of the __setitem__ without the lock context - manager which turns on the DEFERRED isolation level. The - isolation level of the KV is set to autocommit so now lock is - needed anyhow. - - """ - - # insert the key-value pair if the key isn't in the db - try: - self._execute(self.insert_query, (key, value)) - - # otherwise update the keys value - except sqlite3.IntegrityError: - # if we are in append only mode don't allow updates - if self.append_only: - raise sqlite3.IntegrityError( - "DB is opened in append only mode, " - "and {} has already been set".format(key) - ) - else: - self._execute(self.update_query, (value, key)) - - def set_in_tx(self, cursor, key, value): - """Do a set with a cursor, this allows it to be done in a transaction.""" - - try: - cursor.execute(self.insert_query, (key, value)) - except sqlite3.IntegrityError: - # if we are in append only mode don't allow updates - if self.append_only: - raise sqlite3.IntegrityError( - "DB is opened in append only mode, " - "and {} has already been set".format(key) - ) - - else: - cursor.execute(self.update_query, (key, value)) - - return cursor - - def del_in_tx(self, cursor, key): - # no deletions in append only mode - if self.append_only: - raise sqlite3.IntegrityError( - "DB is opened in append only mode, and {} has already been set".format( - key - ) - ) - - elif key in self: - cursor.execute(self.del_query, (key,)) - - else: - raise KeyError - - return cursor - - @contextmanager - def lock(self): - if not self._locks: - self._execute("BEGIN TRANSACTION") - self._locks = True - try: - yield - finally: - self._locks = False - if not self._locks: - self._execute("COMMIT") diff --git a/uv.lock b/uv.lock index e3c8234d..89678769 100644 --- a/uv.lock +++ b/uv.lock @@ -557,15 +557,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] -[[package]] -name = "dill" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, -] - [[package]] name = "docutils" version = "0.21.2" @@ -2815,7 +2806,6 @@ source = { editable = "." } dependencies = [ { name = "attrs" }, { name = "click" }, - { name = "dill" }, { name = "geomm" }, { name = "h5py" }, { name = "jinja2" }, @@ -2872,7 +2862,6 @@ requires-dist = [ { name = "attrs" }, { name = "click" }, { name = "dask", extras = ["bag"], marker = "extra == 'distributed'" }, - { name = "dill" }, { name = "geomm" }, { name = "h5py", specifier = ">=3" }, { name = "jinja2" }, From 2f443b8a594ccfe7b7862efcf9a9e80571e6b83e Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:16:40 -0500 Subject: [PATCH 021/143] add Monitor interface --- src/wepy/monitor.py | 12 ++++++++++++ src/wepy_tools/monitoring/prometheus.py | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/wepy/monitor.py diff --git a/src/wepy/monitor.py b/src/wepy/monitor.py new file mode 100644 index 00000000..6cd5f75e --- /dev/null +++ b/src/wepy/monitor.py @@ -0,0 +1,12 @@ +"""Interface definition for simulation monitors.""" + +from wepy.walker import Walker + + +class Monitor: + + def init(self) -> None: ... + + def cycle_monitor(self, walkers: list[Walker]) -> None: ... + + def cleanup(self) -> None: ... diff --git a/src/wepy_tools/monitoring/prometheus.py b/src/wepy_tools/monitoring/prometheus.py index 8a925fb5..722e8ae3 100644 --- a/src/wepy_tools/monitoring/prometheus.py +++ b/src/wepy_tools/monitoring/prometheus.py @@ -4,11 +4,12 @@ logger = logging.getLogger(__name__) # Third Party Library +from wepy.monitor import Monitor import prometheus_client as prom from pympler.asizeof import asizeof -class SimMonitor: +class SimMonitor(Monitor): """A simulation monitor using a prometheus http server""" DEFAULT_PORT = 9001 From 9367520b791afd1aa2342ffc9320e88d25f9c613 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:17:38 -0500 Subject: [PATCH 022/143] add lennard jones system maker for testing --- src/wepy_tools/systems/lennard_jones.py | 92 +++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/wepy_tools/systems/lennard_jones.py b/src/wepy_tools/systems/lennard_jones.py index 1e8bdeb7..5f151714 100644 --- a/src/wepy_tools/systems/lennard_jones.py +++ b/src/wepy_tools/systems/lennard_jones.py @@ -1,10 +1,102 @@ # Third Party Library import numpy as np +import openmm +import openmm.app +import openmm.unit from scipy.spatial.distance import euclidean # First Party Library from wepy.resampling.distances.distance import Distance +class LennardJonesPair: + + """Create a pair of Lennard-Jones particles. + + Parameters + ---------- + mass : simtk.unit.Quantity with units compatible with amu, optional, default=39.9*amu + The mass of each particle. + epsilon : simtk.unit.Quantity with units compatible with kilojoules_per_mole, optional, default=1.0*kilocalories_per_mole + The effective Lennard-Jones sigma parameter. + sigma : simtk.unit.Quantity with units compatible with nanometers, optional, default=3.350*angstroms + The effective Lennard-Jones sigma parameter. + + Examples + -------- + + Create Lennard-Jones pair. + + >>> test = LennardJonesPair() + >>> system, positions = test.system, test.positions + >>> thermodynamic_state = ThermodynamicState(temperature=300.0*unit.kelvin) + >>> binding_free_energy = test.get_binding_free_energy(thermodynamic_state) + + Create Lennard-Jones pair with different well depth. + + >>> test = LennardJonesPair(epsilon=11.0*unit.kilocalories_per_mole) + >>> system, positions = test.system, test.positions + >>> thermodynamic_state = ThermodynamicState(temperature=300.0*unit.kelvin) + >>> binding_free_energy = test.get_binding_free_energy(thermodynamic_state) + + Create Lennard-Jones pair with different well depth and sigma. + + >>> test = LennardJonesPair(epsilon=7.0*unit.kilocalories_per_mole, sigma=4.5*unit.angstroms) + >>> system, positions = test.system, test.positions + >>> thermodynamic_state = ThermodynamicState(temperature=300.0*unit.kelvin) + >>> binding_free_energy = test.get_binding_free_energy(thermodynamic_state) + + """ + + def __init__(self, mass=39.9 * openmm.unit.amu, sigma=3.350 * openmm.unit.angstrom, epsilon=10.0 * openmm.unit.kilocalories_per_mole, **kwargs): + + # Store parameters + self.mass = mass + self.sigma = sigma + self.epsilon = epsilon + + # Charge must be zero. + charge = 0.0 * openmm.unit.elementary_charge + + # Create an empty system object. + system = openmm.System() + + # Create a NonbondedForce object with no cutoff. + force = openmm.NonbondedForce() + force.setNonbondedMethod(openmm.NonbondedForce.NoCutoff) + + # Create positions. + positions = openmm.unit.Quantity(np.zeros([2, 3], np.float32), openmm.unit.angstrom) + # Move the second particle along the x axis to be at the potential minimum. + positions[1, 0] = 2.0**(1.0 / 6.0) * sigma + + # Create first particle. + system.addParticle(mass) + force.addParticle(charge, sigma, epsilon) + + # Create second particle. + system.addParticle(mass) + force.addParticle(charge, sigma, epsilon) + + # Add the nonbonded force. + system.addForce(force) + + # Store system and positions. + self.system, self.positions = system, positions + + # Store ligand and receptor particle indices. + self.ligand_indices = [0] + self.receptor_indices = [1] + + # Create topology. + topology = openmm.app.Topology() + element = openmm.app.Element.getBySymbol('Ar') + chain = topology.addChain() + residue = topology.addResidue('Ar', chain) + topology.addAtom('Ar', element, residue) + residue = topology.addResidue('Ar', chain) + topology.addAtom('Ar', element, residue) + self.topology = topology + class PairDistance(Distance): def __init__(self, metric=euclidean): From d342cc9df350c7d3d98ebb92cc3544b4cd98df67 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:21:02 -0500 Subject: [PATCH 023/143] make Walker generic wrt to the state --- src/wepy/walker.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/wepy/walker.py b/src/wepy/walker.py index f66b26c5..b53aa20e 100644 --- a/src/wepy/walker.py +++ b/src/wepy/walker.py @@ -28,7 +28,7 @@ # Standard Library import logging -from typing import Protocol, Hashable, Any, Self +from typing import Protocol, Hashable, Any, Self, TypeVar, Generic from abc import ABC import math @@ -40,6 +40,7 @@ logger = logging.getLogger(__name__) +T = TypeVar("T") class WalkerStateProtocol(Protocol): @@ -84,6 +85,8 @@ def dict(self) -> dict[str, Any]: """Return all key-value pairs as a dictionary.""" return deepcopy(self._data) +# TODO: move all cloning and merging methods and functions to a +# standalone module or in the clone_merge decision module class WalkerABC(ABC): @@ -150,7 +153,7 @@ def merge(self, other_walkers: list["Walker"]) -> "Walker": @attrs.define -class Walker(WalkerABC): +class Walker(WalkerABC, Generic[T]): """Reference implementation of the Walker interface. A container for: @@ -160,7 +163,7 @@ class Walker(WalkerABC): """ - state: WalkerState + state: T weight: float = attrs.field(eq=attrs.cmp_using(eq=math.isclose)) From 17069b9f4ae84c49028118430ca6a36e0946d71b Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:21:22 -0500 Subject: [PATCH 024/143] adds mock runner and update runner interface Runners are now generic wrt to the state type they accept --- src/wepy/runners/mock.py | 44 ++++++++++++++++++++++++++++ src/wepy/runners/runner.py | 35 ++++++++++++++-------- tests/unit/test_runners/test_mock.py | 18 ++++++++++++ 3 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 src/wepy/runners/mock.py create mode 100644 tests/unit/test_runners/test_mock.py diff --git a/src/wepy/runners/mock.py b/src/wepy/runners/mock.py new file mode 100644 index 00000000..4491a4b2 --- /dev/null +++ b/src/wepy/runners/mock.py @@ -0,0 +1,44 @@ +"""Realistic mock runners useful mostly for testing.""" + +import attrs +from wepy.walker import Walker +from wepy.runners.runner import Runner + +@attrs.define +class MockState: + a: 1 + +class MockError(Exception): + pass + +@attrs.define +class MockRunner(Runner[MockState]): + + fail: bool = False + + def pre_cycle(self) -> None: + pass + + def post_cycle(self) -> None: + pass + + def run_segment( + self, + state: MockState, + segment_length: int, + worker_id: int = 0, + # UGLY: here to satisfy the interface + cycle_idx: int = 0, + walker_idx: int = 0 + ) -> MockState: + + if self.fail: + raise MockError("Error requested") + + return attrs.evolve( + state, + a=(state.a + segment_length), + ) + + def get_last_cycle_segments_split_times(self) -> None: + return None diff --git a/src/wepy/runners/runner.py b/src/wepy/runners/runner.py index 534ca30a..ccd63553 100644 --- a/src/wepy/runners/runner.py +++ b/src/wepy/runners/runner.py @@ -18,15 +18,15 @@ """ # Standard Library -from typing import Protocol, Any +from typing import Protocol, Any, TypedDict, TypeVar import attrs from wepy.walker import Walker - -class Runner(Protocol): +State_ = TypeVar("State_") +class Runner(Protocol[State_]): """Abstract base class for the Runner interface.""" - def pre_cycle(self) -> None: + def pre_cycle(self, **kwargs: dict[str, Any]) -> None: """Perform pre-cycle behavior. run_segment will be called for each walker so this allows you to perform changes of state on a per-cycle basis. @@ -57,8 +57,10 @@ def post_cycle(self) -> None: def run_segment( self, walker: Walker, - segment_length: int | float, - **kwargs: dict[str, Any], + segment_length: int, + # UGLY: here to satisfy the interface + cycle_idx: int = 0, + walker_idx: int = 0 ) -> Walker: """Run dynamics for the walker. @@ -76,19 +78,28 @@ def run_segment( """ ... + def get_last_cycle_segments_split_times(self) -> list[dict[str, float]] | None: ... + @attrs.define -class NoRunner: +class NoRunner[State_]: """Stub Runner that just returns the walkers back with the same state. May be useful for testing. """ + def pre_cycle(self) -> None: + pass + def post_cycle(self) -> None: + pass + def get_last_cycle_segments_split_times(self) -> None: + return None def run_segment( self, - walker: Walker, + state: State_, segment_length: int | float, - **kwargs: dict[str, Any], - ) -> Walker: - # documented in superclass - return walker + # UGLY: here to satisfy the interface + cycle_idx: int = 0, + walker_idx: int = 0 + ) -> State_: + return state diff --git a/tests/unit/test_runners/test_mock.py b/tests/unit/test_runners/test_mock.py new file mode 100644 index 00000000..e675d0ac --- /dev/null +++ b/tests/unit/test_runners/test_mock.py @@ -0,0 +1,18 @@ +from wepy.runners.mock import MockRunner, MockState + +class TestMockRunner: + + def test_pre_cycle(self): + MockRunner().pre_cycle() + + def test_post_cycle(self): + MockRunner().post_cycle() + + + def test_run_segment(self): + + assert MockRunner().run_segment( + MockState(0), + 10, + 0, + ) == MockState(10) From 278b7f871f4430451ed8d4ab15b75afea0fcda8d Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:23:25 -0500 Subject: [PATCH 025/143] update work mapper interface --- src/wepy/work_mapper/base.py | 15 ++++++++------- src/wepy/work_mapper/serial.py | 3 +++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/wepy/work_mapper/base.py b/src/wepy/work_mapper/base.py index 7e0d2f43..0578d9e0 100644 --- a/src/wepy/work_mapper/base.py +++ b/src/wepy/work_mapper/base.py @@ -17,14 +17,15 @@ class WorkMapper(Protocol[AnyWalkerState]): def init( - self, - segment_func: Callable[ - tuple[ - AnyWalkerState, - ..., - ], + self, + segment_func: Callable[ + tuple[ AnyWalkerState, - ] + ..., + ], + AnyWalkerState, + ], + num_workers: int | None = None, ) -> None: ... diff --git a/src/wepy/work_mapper/serial.py b/src/wepy/work_mapper/serial.py index 4bbee2d7..c7522b8a 100644 --- a/src/wepy/work_mapper/serial.py +++ b/src/wepy/work_mapper/serial.py @@ -45,6 +45,9 @@ def init( ], AnyWalkerState, ], + # UGLY: these are here for compatibility with the + # interface but not used + num_workers: int | None = None, ) -> None: self._func = segment_func From 24670012d4ae5fc05b9ed172c13494f697f82e0c Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:23:43 -0500 Subject: [PATCH 026/143] clean up sim manager interface and add tests --- src/wepy/sim_manager.py | 608 ++++++++++++++++----------------- tests/unit/test_sim_manager.py | 143 ++++++++ 2 files changed, 430 insertions(+), 321 deletions(-) create mode 100644 tests/unit/test_sim_manager.py diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index 6b27bdc2..6efb2d80 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -44,7 +44,7 @@ # Standard Library import logging -from typing import Final +from typing import Final, Any, TypedDict, Generic, TypeVar logger = logging.getLogger(__name__) # Standard Library @@ -57,10 +57,33 @@ from wepy.resampling.resamplers.resampler import Resampler from wepy.runners.runner import Runner from wepy.walker import Walker -from wepy.work_mapper.mapper import Mapper - - -class Manager: +from wepy.work_mapper.base import WorkMapper +from wepy.work_mapper.serial import SerialMapper +from wepy.monitor import Monitor + +class CycleReportDict(TypedDict): + cycle_idx: int + new_walkers: list[Walker] + # TODO: types for all the Anys + warp_data: list[Any] + bc_data: list[Any] + progress_data: dict[Any] + resampling_data: Any + resampler_data: Any + n_segment_steps: int + resampled_walkers: list[Walker] + runner_precycle_time: float + runner_postcycle_time: float + sim_manager_segment_overhead_time: float + runner_splits_time: dict[str, float] | None + worker_segment_times: dict[int, list[float]] | None + cycle_sim_manager_segment_time: float + cycle_runner_time: float + cycle_bc_time: float + cycle_resampling_time: float + +State_ = TypeVar("State_") +class Manager(Generic[State_]): """The class that coordinates wepy simulations. The Manager class is the lynchpin of wepy simulations and is where @@ -92,7 +115,17 @@ class Manager: """ - REPORT_ITEM_KEYS: Final = ( + init_walkers: list[Walker[State_]] + n_init_walkers: int + runner: Runner + resampler: Resampler + boundary_conditions: BoundaryConditions | None + work_mapper: WorkMapper + reporters: list[Reporter] + monitor: Monitor | None + + + REPORT_ITEM_KEYS: Final[tuple[str, ...]] = ( "cycle_idx", "n_segment_steps", "new_walkers", @@ -114,14 +147,14 @@ class Manager: def __init__( self, - init_walkers: list[Walker], - runner: Runner | None = None, - work_mapper=None, - resampler: Resampler | None = None, + init_walkers: list[Walker[State_]], + runner: Runner, + resampler: Resampler, + work_mapper: WorkMapper | None = None, boundary_conditions: BoundaryConditions | None = None, - reporters: Reporter | None = None, - sim_monitor=None, - ): + reporters: list[Reporter] | None = None, + sim_monitor: Monitor | None = None, + ) -> None: """Constructor for Manager. Arguments: @@ -145,6 +178,9 @@ def __init__( reporters : list of objects implenting the Reporter interface, optional Reporters to be used. You should provide these if you want to keep data. + sim_monitor: Monitoring object. Can be used to report metrics + outside of simulation data flow. + Warnings: -------- While reporters are strictly optional, you probably want to @@ -156,10 +192,6 @@ def __init__( -------- wepy.reporter.hdf5 : The standard reporter for molecular simulations in wepy. - wepy.orchestration.orchestrator.Orchestrator : for running simulations with - checkpointing, restarting, reporter localization, and configuration hotswapping - with command line interface. - """ self.init_walkers = init_walkers @@ -179,24 +211,150 @@ def __init__( self.reporters = reporters if work_mapper is None: - self.work_mapper = Mapper() + self.work_mapper = SerialMapper() else: self.work_mapper = work_mapper ## Monitor - self.monitor = None + self.monitor = sim_monitor # used to have a record of the last report for the simulation # monitor without breaking the API. Ugly but I don't want to # break it and no one cares about this anyhow - self._last_report = None + self._last_report: CycleReportDict | None = None + + + def init( + self, + num_workers: int | None = None, + continue_run: int | None = None, + ) -> None: + """Initialize wepy configuration components for use at runtime. + + This `init` method is different than the constructor + `__init__` method and instead calls the special `init` method + on all wepy components (runner, resampler, boundary + conditions, and reporters) at runtime. + + This allows for a things that need to be done at runtime before a + simulation begins, e.g. opening files, that you don't want done at + construction time. + + It calls the `init` methods on: + + - work_mapper + - reporters + + Passes the segment_func of the runner and the number of + workers to the work_mapper. + + Passes the following things to each reporter `init` method: + + - init_walkers + - runner + - resampler + - boundary_conditions + - work_mapper + - reporters + - continue_run + + Parameters + ---------- + num_workers : int + The number of workers to use in the work mapper. + (Default value = None) + continue_run : int + Index of a run this one is continuing. + (Default value = None) + + """ + + logger.info("Starting simulation") + + # initialize the monitoring object + + # TODO: do we need to supply the port here? I don't want to + # add it to the interface... Should be pre-parametrized + if self.monitor is not None: + logger.info("Initializing monitoring") + self.monitor.init() + + # initialize the work_mapper with the function it will be + # mapping and the number of workers, this may include things like starting processes + # etc. + logger.info("Initializing work_mapper") + self.work_mapper.init( + segment_func=self.runner.run_segment, + num_workers=num_workers, + ) + + # init the reporter + for reporter in self.reporters: + logger.info(f"Initializing reporter: {reporter}") + reporter.init( + init_walkers=self.init_walkers, + runner=self.runner, + resampler=self.resampler, + boundary_conditions=self.boundary_conditions, + work_mapper=self.work_mapper, + reporters=self.reporters, + continue_run=continue_run, + ) + + logger.info("Finished sim_manager initialization") + + def cleanup(self) -> None: + """Perform cleanup actions for wepy configuration components. + + Allow components to perform actions before ending the main + simulation manager process. + + Calls the `cleanup` method on: + + - work_mapper + - reporters + + Passes nothing to the work mapper. + + Passes the following to each reporter: + + - runner + - work_mapper + - resampler + - boundary_conditions + - reporters + + """ + + logger.info("Running cleanup") + + if self.monitor is not None: + logger.info("Cleaning up monitoring") + self.monitor.cleanup() + + # cleanup the mapper + logger.info("Cleaning up work_mapper") + self.work_mapper.cleanup() + + # cleanup things associated with the reporter + for reporter in self.reporters: + logger.info(f"Cleaning up reporter: {reporter}") + reporter.cleanup( + runner=self.runner, + work_mapper=self.work_mapper, + resampler=self.resampler, + boundary_conditions=self.boundary_conditions, + reporters=self.reporters, + ) + + logger.info("Finished cleanup") def run_segment( self, - walkers: list[Walker], + states: list[State_], segment_length: int, cycle_idx: int, - ) -> list[Walker]: + ) -> list[State_]: """Run a time segment for all walkers using the available workers. Maps the work for running each segment for each walker using @@ -221,41 +379,49 @@ def run_segment( The walkers after the segment of sampling simulation. """ - num_walkers = len(walkers) + num_walkers = len(states) logger.info("Starting segment") + segment_lengths = [segment_length for i in range(num_walkers)] + cycle_idxs = [cycle_idx for i in range(num_walkers)] + walker_idxs = [walker_idx for walker_idx in range(num_walkers)] try: - new_walkers = list( + new_states = list( self.work_mapper.map( # args, which must be supported by the map function - walkers, - (segment_length for i in range(num_walkers)), + states, + segment_lengths, # kwargs which are optionally recognized by the map function - cycle_idx=(cycle_idx for i in range(num_walkers)), - walker_idx=(walker_idx for walker_idx in range(num_walkers)), + cycle_idx=cycle_idxs, + walker_idx=walker_idxs, ) ) except Exception as exception: + logger.info("Exception encountered in segment calculations. Cleaning up before raising.") # get the errors from the work mapper error queue self.cleanup() + logger.info("Failure cleanup complete, reraising error.") + # report on all of the errors that occured raise exception logger.info("Ending segment") - return new_walkers + return new_states def run_cycle( self, - walkers: list[Walker], + walkers: list[Walker[State_]], n_segment_steps: int, cycle_idx: int, runner_opts=None, - ) -> tuple[list[Walker], list[Runner | BoundaryConditions | Resampler]]: - # TODO: Replace list of Runner | BoundaryConditions | Resampler with tuple + ) -> tuple[ + list[Walker[State_]], + tuple[Runner, BoundaryConditions | None, Resampler], + ]: """Run a full cycle of weighted ensemble simulation using each component. @@ -322,59 +488,50 @@ def run_cycle( """ - # this one is called to just easily be able to catch all the - # errors from it so we can cleanup if an error is caught - - return self._run_cycle( - walkers, - n_segment_steps, - cycle_idx, - runner_opts=runner_opts, - ) - - def _run_cycle( - self, - walkers, - n_segment_steps, - cycle_idx, - runner_opts=None, - ): - """See run_cycle.""" + logger.info("Running simulation cycle") if runner_opts is None: runner_opts = {} - if self.runner is None: - raise RuntimeError("'runner' is None") - # run the runner pre-cycle hook start = time.time() + logger.info("Running Runner.pre_cycle hook") self.runner.pre_cycle( - walkers=walkers, - n_segment_steps=n_segment_steps, - cycle_idx=cycle_idx, **runner_opts, ) end = time.time() runner_precycle_time = end - start + logger.info(f"Precycle time: {runner_precycle_time}") # run the segment start = time.time() - new_walkers = self.run_segment(walkers, n_segment_steps, cycle_idx) + logger.info("Running state propagation segment") + new_states = self.run_segment( + [walker.state for walker in walkers], + n_segment_steps, + cycle_idx, + ) + logger.info("Finished state propagation segment") + + new_walkers = [ + Walker( + state=new_state, + weight=walker.weight + ) + for walker, new_state + in zip(walkers, new_states, strict=True) + ] end = time.time() sim_manager_segment_time = end - start + logger.info(f"Segment duration: {sim_manager_segment_time}") - if hasattr(self.runner, "_last_cycle_segments_split_times"): - runner_splits = deepcopy(self.runner._last_cycle_segments_split_times) + runner_splits = self.runner.get_last_cycle_segments_split_times() - else: - runner_splits = None - - logger.info("Starting post cycle") + logger.info("Running Runner.post_cycle hook") # run post-cycle hook start = time.time() @@ -383,7 +540,7 @@ def _run_cycle( end = time.time() runner_postcycle_time = end - start - logger.info("End cycle {}".format(cycle_idx)) + logger.info(f"Post cycle duration: {runner_postcycle_time}") # boundary conditions should be optional; @@ -395,9 +552,10 @@ def _run_cycle( progress_data = {} bc_time = 0.0 if self.boundary_conditions is not None: + logger.info("Boundary conditions were provided, applying.") # apply rules of boundary conditions and warp walkers through space start = time.time() - logger.info("Starting boundary conditions") + logger.info("Starting boundary conditions calculations") bc_results = self.boundary_conditions.warp_walkers(new_walkers, cycle_idx) end = time.time() bc_time = end - start @@ -408,60 +566,53 @@ def _run_cycle( bc_data = bc_results[2] progress_data = bc_results[3] + logger.info(f"Boundary condition duration: {bc_time}") + if len(warp_data) > 0: - logger.info("Returned warp record in cycle {}".format(cycle_idx)) + logger.info(f"Returned warp record in cycle {cycle_idx}") # resample walkers + logger.info("Starting resampler phase.") start = time.time() - logger.info("Starting resampler") resampling_results = self.resampler.resample(warped_walkers) end = time.time() resampling_time = end - start + logger.info(f"Resampling duration: {resampling_time}") + resampled_walkers = resampling_results[0] resampling_data = resampling_results[1] resampler_data = resampling_results[2] - # log the weights of the walkers after resampling - - # DEBUG: commenting this out to make mocking easier. But really I - # don't even care if it stays. as its mostly noise. - # result_template_str = "|".join(["{:^5}" for i in range(self.n_init_walkers + 1)]) - # walker_weight_str = result_template_str.format("weight", - # *[round(walker.weight, 3) for walker in resampled_walkers]) - # logger.info(walker_weight_str) - # make a dictionary of all the results that will be reported seg_times = {} sampling_time = None - if hasattr(self.work_mapper, "worker_segment_times"): - seg_times = deepcopy(self.work_mapper.worker_segment_times) + if (seg_times := self.work_mapper.get_worker_segment_times()) is not None: + logger.info("Segment timings provided by work mapper, recording.") # count up the total sampling time from the segments - sampling_time = 0 + sampling_time = 0. for ( worker_id, segments_times, - ) in self.work_mapper.worker_segment_times.items(): + ) in seg_times.items(): for seg_time in segments_times: sampling_time += seg_time # calculate the overhead for logging sim_manager_segment_overhead_time = sim_manager_segment_time - sampling_time - # logger.info( - # "Runner time = {}; Sampling = ({}); Overhead = ({})".format( - # runner_time, sampling_time, overhead_time)) + logger.info(f"Simulation manager overhead time: {sim_manager_segment_overhead_time}") + else: + logger.info("Worker segment times not provided") sim_manager_segment_overhead_time = 0.0 - # logger.info("No worker segment times given") - # logger.info("Runner time = {}".format(runner_time)) - report = { + report = CycleReportDict({ "cycle_idx": cycle_idx, "new_walkers": new_walkers, "warp_data": warp_data, @@ -481,159 +632,33 @@ def _run_cycle( "cycle_runner_time": sim_manager_segment_time, "cycle_bc_time": bc_time, "cycle_resampling_time": resampling_time, - } + }) self._last_report = report - # check that all of the keys that are specified for this sim - # manager are present - assert all( - [True if rep_key in report else False for rep_key in self.REPORT_ITEM_KEYS] - ) - logger.info("Starting reporting") # report results to the reporters for reporter in self.reporters: logger.info(f"Reporting with reporter: {reporter}") reporter.report(**report) - # prepare resampled walkers for running new state changes - walkers = resampled_walkers - # run the simulation monitor to get metrics on everything if self.monitor is not None: - logger.info("Running monitoring") - self.monitor.cycle_monitor(self, walkers) - - # we also return a list of the "filters" which are the - # classes that are run on the initial walkers to produce - # the final walkers. THis is to satisfy a future looking - # interface in which the order and components of these - # filters are completely parametrizable. This may or may - # not be implemented in a future release of wepy but this - # interface is assumed by the orchestration classes for - # making snapshots of the simulations. The receiver of - # these should perform the copy to make sure they aren't - # mutated. We don't do this here for efficiency. - filters = [self.runner, self.boundary_conditions, self.resampler] + logger.info("Running cycle monitoring") + self.monitor.cycle_monitor(self, resampled_walkers) logger.info("Done: returning walkers") - return walkers, filters - - def init( - self, - num_workers=None, - continue_run=None, - ): - """Initialize wepy configuration components for use at runtime. - - This `init` method is different than the constructor - `__init__` method and instead calls the special `init` method - on all wepy components (runner, resampler, boundary - conditions, and reporters) at runtime. - - This allows for a things that need to be done at runtime before a - simulation begins, e.g. opening files, that you don't want done at - construction time. - - It calls the `init` methods on: - - - work_mapper - - reporters - - Passes the segment_func of the runner and the number of - workers to the work_mapper. - - Passes the following things to each reporter `init` method: - - - init_walkers - - runner - - resampler - - boundary_conditions - - work_mapper - - reporters - - continue_run - - Parameters - ---------- - num_workers : int - The number of workers to use in the work mapper. - (Default value = None) - continue_run : int - Index of a run this one is continuing. - (Default value = None) - - """ - - logger.info("Starting simulation") - - # initialize the monitoring object - - # TODO: do we need to supply the port here? I don't want to - # add it to the interface... Should be pre-parametrized - if self.monitor is not None: - self.monitor.init() - - # initialize the work_mapper with the function it will be - # mapping and the number of workers, this may include things like starting processes - # etc. - self.work_mapper.init( - segment_func=self.runner.run_segment, - num_workers=num_workers, - ) - - # init the reporter - for reporter in self.reporters: - reporter.init( - init_walkers=self.init_walkers, - runner=self.runner, - resampler=self.resampler, - boundary_conditions=self.boundary_conditions, - work_mapper=self.work_mapper, - reporters=self.reporters, - continue_run=continue_run, - ) - - def cleanup(self): - """Perform cleanup actions for wepy configuration components. - - Allow components to perform actions before ending the main - simulation manager process. - - Calls the `cleanup` method on: - - - work_mapper - - reporters - - Passes nothing to the work mapper. - - Passes the following to each reporter: - - - runner - - work_mapper - - resampler - - boundary_conditions - - reporters - - """ - - if self.monitor is not None: - self.monitor.cleanup() - - # cleanup the mapper - self.work_mapper.cleanup() - - # cleanup things associated with the reporter - for reporter in self.reporters: - reporter.cleanup( - runner=self.runner, - work_mapper=self.work_mapper, - resampler=self.resampler, - boundary_conditions=self.boundary_conditions, - reporters=self.reporters, - ) - - def run_simulation_by_time(self, run_time, segments_length, num_workers=None): + return resampled_walkers, (self.runner, self.boundary_conditions, self.resampler) + + def run_simulation_by_time( + self, + run_time: float, + segments_length: int, + num_workers: int | None = None, + ) -> tuple[ + list[Walker[State_]], + tuple[Runner, BoundaryConditions | None, Resampler], + ]: """Run a simulation for a certain amount of time. This starts timing as soon as this is called. If the time @@ -667,11 +692,6 @@ def run_simulation_by_time(self, run_time, segments_length, num_workers=None): Deep copies of the runner, resampler, and boundary conditions objects at the end of the cycle. - See Also - -------- - wepy.orchestration.orchestrator.Orchestrator : for running simulations with - checkpointing, restarting, reporter localization, and configuration hotswapping - with command line interface. """ start_time = time.time() @@ -700,10 +720,14 @@ def run_simulation_by_time(self, run_time, segments_length, num_workers=None): def run_simulation( self, - n_cycles, - segment_lengths, - num_workers=None, - ): + n_cycles: int, + segment_lengths: int, + num_workers: int | None = None, + continue_run_idx: int | None = None, + ) -> tuple[ + list[Walker[State_]], + tuple[Runner, BoundaryConditions, Resampler], + ]: """Run a simulation for an explicit number of cycles. Parameters @@ -718,6 +742,8 @@ def run_simulation( The number of workers to use for the work mapper. (Default value = None) + continue_run_idx: Index of the run you are continuing, optional. + Returns ------- @@ -728,99 +754,47 @@ def run_simulation( Deep copies of the runner, boundary conditions, and resampler objects at the end of the simulation. - See Also - -------- - wepy.orchestration.orchestrator.Orchestrator : for running simulations with - checkpointing, restarting, reporter localization, and configuration hotswapping - with command line interface. - """ - self.init(num_workers=num_workers) + logger.info("Running simulation init hook") + self.init(num_workers=num_workers, continue_run=continue_run_idx) if type(segment_lengths) == int: + logger.info("Single number of steps provided for simulation, using this for all cycles.") segment_lengths = [segment_lengths for _ in range(n_cycles)] walkers = self.init_walkers + logger.info("Starting main simulation loop over cycles") # the main cycle loop for cycle_idx in range(n_cycles): + logger.info(f"Running cycle: {cycle_idx}") walkers, filters = self.run_cycle( - walkers, segment_lengths[cycle_idx], cycle_idx + walkers, segment_lengths[cycle_idx], cycle_idx, ) + logger.info(f"Finished running cycle: {cycle_idx}") # run the simulation monitor to get metrics on everything if self.monitor is not None: + logger.info("Running monitoring cycle_monitor hook") self.monitor.cycle_monitor(self, walkers) + logger.info("Running simulation cleanup") self.cleanup() + logger.info("Simulation cleanup complete") - return walkers, deepcopy(filters) + return walkers, deepcopy(tuple(filters)) - def continue_run_simulation( + def run_simulation_by_time( self, - run_idx, - n_cycles, - segment_lengths, - num_workers=None, - ): - """Continue a simulation. All this does is provide a run idx to the - reporters, which is the run that is intended to be - continued. This simulation manager knows no details and is - left up to the reporters to handle this appropriately. - - Parameters - ---------- - run_idx : int - Index of the run you are continuing. - - n_cycles : int - Number of cycles to perform. - - segment_lengths : int - The number of steps for each runner segment. - - num_workers : int - The number of workers to use for the work mapper. - (Default value = None) - - Returns - ------- - new_walkers : list of walkers - The resulting walkers of the cycle - - sim_components : list - Deep copies of the runner, resampler, and boundary - conditions objects at the end of the cycle. - - See Also - -------- - wepy.orchestration.orchestrator.Orchestrator : for running simulations with - checkpointing, restarting, reporter localization, and configuration hotswapping - with command line interface. - - """ - - self.init(num_workers=num_workers, continue_run=run_idx) - - walkers = self.init_walkers - # the main cycle loop - for cycle_idx in range(n_cycles): - walkers, filters = self.run_cycle( - walkers, segment_lengths[cycle_idx], cycle_idx - ) - - # run the simulation monitor to get metrics on everything - if self.monitor is not None: - self.monitor.cycle_monitor(self, walkers) - - self.cleanup() - - return walkers, filters - - def continue_run_simulation_by_time( - self, run_idx, run_time, segments_length, num_workers=None - ): + run_time: int, + segments_length: int, + num_workers: int | None = None, + continue_run_idx: int | None = None, + ) -> tuple[ + list[Walker[State_]], + tuple[Runner, BoundaryConditions | None, Resampler], + ]: """Continue a simulation with a separate run by time. This starts timing as soon as this is called. If the time @@ -833,24 +807,13 @@ def continue_run_simulation_by_time( manager knows no details and is left up to the reporters to handle this appropriately. - Parameters - ---------- - run_idx : int - Deep copies of the runner, resampler, and boundary - conditions objects at the end of the cycle. - - - See Also - -------- - wepy.orchestration.orchestrator.Orchestrator : for running simulations with - checkpointing, restarting, reporter localization, and configuration hotswapping - with command line interface. - """ start_time = time.time() + logger.info(f"Simulation start time: {start_time}") - self.init(num_workers=num_workers, continue_run=run_idx) + logger.info("Running simulation init hook") + self.init(num_workers=num_workers, continue_run=continue_run_idx) cycle_idx = 0 walkers = self.init_walkers @@ -869,10 +832,13 @@ def continue_run_simulation_by_time( # run the simulation monitor to get metrics on everything if self.monitor is not None: + logger.info("Running cycle_monitor hook") self.monitor.cycle_monitor(self, walkers) cycle_idx += 1 + logger.info("Running simulation cleanup") self.cleanup() + logger.info("Simulation cleanup complete") return walkers, filters diff --git a/tests/unit/test_sim_manager.py b/tests/unit/test_sim_manager.py new file mode 100644 index 00000000..e13bd95f --- /dev/null +++ b/tests/unit/test_sim_manager.py @@ -0,0 +1,143 @@ +import pytest +import attrs +from wepy.walker import Walker +from wepy.work_mapper.serial import SerialMapper +from wepy.resampling.resamplers.noresampler import NoResampler +from wepy.runners.runner import NoRunner +from wepy.runners.mock import MockRunner, MockState, MockError + +from wepy.sim_manager import Manager + +@pytest.fixture +def sim_components() -> tuple[ + list[Walker], + NoRunner, + MockRunner, + SerialMapper, +]: + + num_walkers = 4 + + init_walker_weight = 1 / num_walkers + init_walkers = [ + Walker( + state=MockState(1), + weight=init_walker_weight, + ) + for walker_state + in range(num_walkers) + ] + + return init_walkers, MockRunner(), NoResampler(), SerialMapper() + +class TestManager: + + def test___init__(self, sim_components): + + manager = Manager(*sim_components) + + assert len(manager.reporters) == 0 + assert manager.work_mapper == sim_components[3] + + def test_init(self, sim_components): + + manager = Manager(*sim_components) + + manager.init() + + def test_cleanup(self, sim_components): + + manager = Manager(*sim_components) + + manager.cleanup() + + def test_run_segment(self, sim_components): + + init_walkers, runner, resampler, mapper = sim_components + + manager = Manager(*sim_components) + manager.init() + + new_states = manager.run_segment( + [walker.state for walker in init_walkers], + 1, + 0, + ) + + # test if something fails + manager = Manager( + init_walkers, + MockRunner(fail=True), + resampler, + mapper, + ) + manager.init() + + with pytest.raises(MockError): + manager.run_segment( + [walker.state for walker in init_walkers], + 1, + 0, + ) + + def test_run_cycle(self, sim_components): + + init_walkers, runner, resampler, mapper = sim_components + + manager = Manager(*sim_components) + manager.init() + + new_states = manager.run_cycle( + init_walkers, + 1, + 0, + ) + + # test if something fails + manager = Manager( + init_walkers, + MockRunner(fail=True), + resampler, + mapper, + ) + manager.init() + + with pytest.raises(MockError): + manager.run_cycle( + init_walkers, + 1, + 0, + ) + + def test_run_simulation(self, sim_components): + + manager = Manager(*sim_components) + manager.init() + + new_walkers, _ = manager.run_simulation(2, 2, num_workers=None) + + + new_walkers, _ = manager.run_simulation( + 2, + 2, + num_workers=None, + continue_run_idx=0, + ) + + def test_run_simulation_by_time(self, sim_components): + + manager = Manager(*sim_components) + manager.init() + + new_walkers, _ = manager.run_simulation_by_time( + 1, + 2, + num_workers=None, + ) + + new_walkers, _ = manager.run_simulation_by_time( + 1, + 2, + num_workers=None, + continue_run_idx=0, + ) From e2b1cfff6315ce00d85e9a1fb84f304cd4a197c2 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:24:37 -0500 Subject: [PATCH 027/143] update types; removing nptyping nptyping doesn't work with numpy 2 --- src/wepy/resampling/resamplers/noresampler.py | 6 +- src/wepy/resampling/resamplers/wexplore.py | 2 +- src/wepy/runners/openmm.py | 262 ++++++++---------- tests/unit/test_runners/test_openmm.py | 64 ++--- 4 files changed, 140 insertions(+), 194 deletions(-) diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py index 4318f6da..78543adc 100644 --- a/src/wepy/resampling/resamplers/noresampler.py +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -1,6 +1,5 @@ from typing import TypedDict import numpy as np -from nptyping import NDArray, Floating, Shape, Integer from wepy.walker import Walker from wepy.resampling.resamplers.resampler import Resampler from wepy.resampling.decisions.no_decision import ( @@ -11,9 +10,8 @@ class NoResamplerResamplingData(TypedDict): - step_idx: NDArray[Shape["1"], Integer] - decision_id: NDArray[Shape["1"], Integer] - target_idxs: NDArray[Shape["1"], Integer] + decision_id: int + target_idxs: tuple[int, ...] class NoResamplerResamplerData(TypedDict): diff --git a/src/wepy/resampling/resamplers/wexplore.py b/src/wepy/resampling/resamplers/wexplore.py index 0e8e32df..d675fb3c 100644 --- a/src/wepy/resampling/resamplers/wexplore.py +++ b/src/wepy/resampling/resamplers/wexplore.py @@ -2413,7 +2413,7 @@ def assign(self, walkers): # resampler state, which is addition of new regions return assignments, resampler_data - def decide(self, delta_walkers=0): + def decide(self, delta_walkers: int = 0): """Make decisions for resampling for a single step. Parameters diff --git a/src/wepy/runners/openmm.py b/src/wepy/runners/openmm.py index 4539f2a6..04b5107a 100644 --- a/src/wepy/runners/openmm.py +++ b/src/wepy/runners/openmm.py @@ -31,14 +31,12 @@ logger = logging.getLogger(__name__) # Standard Library import time -from copy import copy from warnings import warn import copy # Third Party Library import attrs import numpy as np -from nptyping import NDArray, Shape, Floating try: import mdtraj @@ -58,12 +56,12 @@ # First Party Library from wepy.runners.runner import Runner from wepy.util.util import box_vectors_to_lengths_angles -from wepy.walker import Walker, WalkerState, WalkerABC -from wepy.work_mapper.task_mapper import WalkerTaskProcess -from wepy.work_mapper.worker import Worker +from wepy.walker import WalkerState +# from wepy.work_mapper.task_mapper import WalkerTaskProcess +# from wepy.work_mapper.worker import Worker -AtomNDArray = NDArray[Shape["N atoms, 3 dimensions"], Floating] -BoxVectorsNDArray = NDArray[Shape["3, 3"], Floating] +# AtomNDArray = NDArray[Shape["N atoms, 3 dimensions"], Floating] +# BoxVectorsNDArray = NDArray[Shape["3, 3"], Floating] ## Constants @@ -202,13 +200,13 @@ def get_state_fields_present(sim_state: openmm.State) -> list[str]: class OpenMMStateDict(TypedDict, total=False): - positions: AtomNDArray - velocities: AtomNDArray - forces: AtomNDArray + positions: np.typing.ArrayLike + velocities: np.typing.ArrayLike + forces: np.typing.ArrayLike kinetic_energy: float potential_energy: float time: float - box_vectors: BoxVectorsNDArray + box_vectors: np.typing.ArrayLike box_volume: float # TODO: parameters @@ -342,7 +340,7 @@ def __getitem__(self, key: str) -> Any: @property def positions(self) -> Annotated[ openmm.unit.Quantity | None, - AtomNDArray, + np.typing.ArrayLike, ]: """The positions of the state as a numpy array openmm.unit.Quantity object.""" @@ -356,7 +354,7 @@ def positions_unit(self) -> openmm.unit.Unit: """The units (as a openmm.unit.Unit object) the positions are in.""" return self.positions.unit - def positions_values(self) -> AtomNDArray | None: + def positions_values(self) -> np.typing.ArrayLike | None: """The positions of the state as a numpy array in the positions_unit openmm.unit.Unit. This is what is returned by the __getitem__ accessor. @@ -366,7 +364,7 @@ def positions_values(self) -> AtomNDArray | None: # Velocities @property - def velocities(self) -> Annotated[openmm.unit.Quantity | None, AtomNDArray]: + def velocities(self) -> Annotated[openmm.unit.Quantity | None, np.typing.ArrayLike]: """The velocities of the state as a numpy array openmm.unit.Quantity object.""" if "velocities" in self._sim_state_fields_present: @@ -379,7 +377,7 @@ def velocities_unit(self) -> openmm.unit.Unit: """The units (as a openmm.unit.Unit object) the velocities are in.""" return self.velocities.unit - def velocities_values(self) -> AtomNDArray | None: + def velocities_values(self) -> np.typing.ArrayLike | None: """The velocities of the state as a numpy array in the velocities_unit openmm.unit.Unit. This is what is returned by the __getitem__ accessor. @@ -396,7 +394,7 @@ def velocities_values(self) -> AtomNDArray | None: @property def forces(self) -> Annotated[ openmm.unit.Quantity | None, - AtomNDArray, + np.typing.ArrayLike, ]: """The forces of the state as a numpy array openmm.unit.Quantity object.""" @@ -410,7 +408,7 @@ def forces_unit(self) -> openmm.unit.Unit: """The units (as a openmm.unit.Unit object) the forces are in.""" return self.forces.unit - def forces_values(self) -> AtomNDArray | None: + def forces_values(self) -> np.typing.ArrayLike | None: """The forces of the state as a numpy array in the forces_unit openmm.unit.Unit. This is what is returned by the __getitem__ accessor. @@ -427,7 +425,7 @@ def forces_values(self) -> AtomNDArray | None: @property def box_vectors(self) -> Annotated[ openmm.unit.Quantity | None, - BoxVectorsNDArray, + np.typing.ArrayLike, ]: """The box vectors of the state as a numpy array openmm.unit.Quantity object.""" try: @@ -444,7 +442,7 @@ def box_vectors_unit(self) -> openmm.unit.Unit: """The units (as a openmm.unit.Unit object) the box vectors are in.""" return self.box_vectors.unit - def box_vectors_values(self) -> AtomNDArray | None: + def box_vectors_values(self) -> np.typing.ArrayLike | None: """The box vectors of the state as a numpy array in the box_vectors_unit openmm.unit.Unit. This is what is returned by the __getitem__ accessor. @@ -463,7 +461,7 @@ def box_vectors_values(self) -> AtomNDArray | None: @property def kinetic_energy(self) -> Annotated[ openmm.unit.Quantity | None, - AtomNDArray, + np.typing.ArrayLike, ]: """The kinetic energy of the state as a numpy array openmm.unit.Quantity object.""" try: @@ -480,7 +478,7 @@ def kinetic_energy_unit(self) -> openmm.unit.Unit: """The units (as a openmm.unit.Unit object) the kinetic energy is in.""" return self.kinetic_energy.unit - def kinetic_energy_value(self) -> AtomNDArray | None: + def kinetic_energy_value(self) -> np.typing.ArrayLike | None: """The kinetic energy of the state as a numpy array in the kinetic_energy_unit openmm.unit.Unit. This is what is returned by the __getitem__ accessor. @@ -499,7 +497,7 @@ def kinetic_energy_value(self) -> AtomNDArray | None: @property def potential_energy(self) -> Annotated[ openmm.unit.Quantity | None, - AtomNDArray, + np.typing.ArrayLike, ]: """The potential energy of the state as a numpy array openmm.unit.Quantity object.""" try: @@ -516,7 +514,7 @@ def potential_energy_unit(self) -> openmm.unit.Unit: """The units (as a openmm.unit.Unit object) the potential energy is in.""" return self.potential_energy.unit - def potential_energy_value(self) -> AtomNDArray | None: + def potential_energy_value(self) -> np.typing.ArrayLike | None: """The potential energy of the state as a numpy array in the potential_energy_unit openmm.unit.Unit. This is what is returned by the __getitem__ accessor. @@ -549,7 +547,7 @@ def time_unit(self) -> openmm.unit.Unit: """The units (as a openmm.unit.Unit object) the time is in.""" return self.time.unit - def time_value(self) -> AtomNDArray | None: + def time_value(self) -> np.typing.ArrayLike | None: """The time of the state as a numpy array in the time_unit openmm.unit.Unit. This is what is returned by the __getitem__ accessor. @@ -580,7 +578,7 @@ def box_volume_unit(self) -> openmm.unit.Unit: """The units (as a openmm.unit.Unit object) the box volume is in.""" return self.box_volume.unit - def box_volume_value(self) -> NDArray[Shape["1"], Floating] | None: + def box_volume_value(self) -> np.typing.ArrayLike | None: """The box volume of the state as a numpy array in the box_volume_unit openmm.unit.Unit. This is what is returned by the __getitem__ accessor. @@ -620,7 +618,7 @@ def parameters_unit(self) -> dict[str, openmm.unit.Unit]: param_units = {key: None for key, val in self.parameters.items()} return param_units - def parameters_values(self) -> dict[str, NDArray] | None: + def parameters_values(self) -> dict[str, np.typing.ArrayLike] | None: """The parameters of the state as a dictionary mapping the name of the parameter to a numpy array in the unit for the parameter of the same name in the parameters_unit corresponding @@ -667,7 +665,7 @@ def parameter_derivatives_unit(self) -> dict[str, openmm.unit.Unit]: param_units = {key: None for key, val in self.parameter_derivatives.items()} return param_units - def parameter_derivatives_values(self) -> dict[str, NDArray] | None: + def parameter_derivatives_values(self) -> dict[str, np.typing.ArrayLike] | None: """The parameter derivatives of the state as a dictionary mapping the name of the parameter to a numpy array in the unit for the parameter of the same name in the parameters_unit @@ -866,17 +864,8 @@ def to_mdtraj(self, topology: mdtraj.Topology) -> mdtraj.Trajectory: topology=topology, ) - -@attrs.define -class OpenMMWalker(WalkerABC): - - state: OpenMMState - weight: float - - PlatformKwargs = dict[str, str] - class OpenMMRunnerSegmentSplitTimes(TypedDict): gen_sim_time: float steps_time: float @@ -885,7 +874,7 @@ class OpenMMRunnerSegmentSplitTimes(TypedDict): # the runner for the simulation which runs the actual dynamics -class OpenMMRunner(Runner): +class OpenMMRunner(Runner[OpenMMState]): """Runner for OpenMM simulations.""" system: openmm.System @@ -1077,12 +1066,12 @@ def _resolve_platform( def run_segment( self, - walker: OpenMMWalker, + walker_state: OpenMMState, segment_length: int, getState_kwargs: dict[str, bool] | None = None, platform: str | type(Ellipsis) | None = None, platform_kwargs: PlatformKwargs = None, - ) -> Walker: + ) -> OpenMMState: """Run dynamics for the walker. Parameters @@ -1110,7 +1099,7 @@ def run_segment( Returns ------- - new_walker : Walker after dynamics was run, only the state should be modified. + new_walker_state : Walker after dynamics was run, only the state should be modified. """ @@ -1131,7 +1120,7 @@ def run_segment( gen_sim_start = time.time() # make a copy of the integrator for this particular segment - new_integrator = copy(self.integrator) + new_integrator = copy.copy(self.integrator) # force setting of random seed to 0, which is a special # value that forces the integrator to choose another # random number @@ -1201,7 +1190,7 @@ def run_segment( ) # set the state to the context from the walker - simulation.context.setState(walker.state.sim_state) + simulation.context.setState(walker_state.sim_state) gen_sim_end = time.time() gen_sim_time = gen_sim_end - gen_sim_start @@ -1226,12 +1215,9 @@ def run_segment( get_state_time = get_state_end - get_state_start logger.info("Getting context state time: {}".format(get_state_time)) - # generate the new state/walker + # generate the new state new_state = OpenMMState(simulation.context.getState(**_getState_kwargs)) - # create a new walker for this - new_walker = OpenMMWalker(new_state, walker.weight) - run_segment_end = time.time() run_segment_time = run_segment_end - run_segment_start logger.info("Total internal run_segment time: {}".format(run_segment_time)) @@ -1245,7 +1231,7 @@ def run_segment( self._last_cycle_segments_split_times.append(segment_split_times) - return new_walker + return new_state def last_cycle_segments_split_times(self) -> OpenMMRunnerSegmentSplitTimes: @@ -1253,7 +1239,7 @@ def last_cycle_segments_split_times(self) -> OpenMMRunnerSegmentSplitTimes: def gen_sim_state( - positions: AtomNDArray, + positions: np.typing.ArrayLike, system: openmm.System, integrator: openmm.Integrator, getState_kwargs: dict[str, bool] | None = None, @@ -1288,7 +1274,7 @@ def gen_sim_state( # generate a throwaway context, using the reference platform so we # don't screw up other platform stuff later in the same process platform = openmm.Platform.getPlatformByName("Reference") - context = openmm.Context(system, copy(integrator), platform) + context = openmm.Context(system, copy.copy(integrator), platform) # set the positions context.setPositions(positions) @@ -1299,141 +1285,115 @@ def gen_sim_state( return sim_state -def gen_walker_state(positions, system, integrator, getState_kwargs=None): - """Convenience function for generating a wepy walker State object for - an openmm simulation state. - - Parameters - ---------- - positions : arraylike of float - The positions for the system you want to set - - system : openmm.app.System object - - integrator : openmm.Integrator object - - Returns - ------- - walker_state : wepy.runners.openmm.OpenMMState object - - """ - - state = OpenMMState( - gen_sim_state(positions, system, integrator, getState_kwargs=getState_kwargs) - ) +# class OpenMMCPUWorker(Worker): +# """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). - return state +# This is intended to be used with the wepy.work_mapper.WorkerMapper +# work mapper class. +# This class must be used in order to ensure OpenMM runs jobs on the +# appropriate GPU device. -class OpenMMCPUWorker(Worker): - """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). +# """ - This is intended to be used with the wepy.work_mapper.WorkerMapper - work mapper class. +# NAME_TEMPLATE = "OpenMMCPUWorker-{}" +# """The name template the worker processes are named to substituting in +# the process number.""" - This class must be used in order to ensure OpenMM runs jobs on the - appropriate GPU device. +# DEFAULT_NUM_THREADS = 1 - """ - - NAME_TEMPLATE = "OpenMMCPUWorker-{}" - """The name template the worker processes are named to substituting in - the process number.""" - - DEFAULT_NUM_THREADS = 1 - - def __init__(self, *args, **kwargs): - if "num_threads" not in kwargs: - num_threads = self.DEFAULT_NUM_THREADS - else: - num_threads = kwargs.pop("num_threads") +# def __init__(self, *args, **kwargs): +# if "num_threads" not in kwargs: +# num_threads = self.DEFAULT_NUM_THREADS +# else: +# num_threads = kwargs.pop("num_threads") - super().__init__(*args, num_threads=num_threads, **kwargs) +# super().__init__(*args, num_threads=num_threads, **kwargs) - def run_task(self, task): - # documented in superclass +# def run_task(self, task): +# # documented in superclass - # make the platform kwargs dictionary - platform_options = {"Threads": str(self.attributes["num_threads"])} +# # make the platform kwargs dictionary +# platform_options = {"Threads": str(self.attributes["num_threads"])} - # run the task and pass in the DeviceIndex for OpenMM to - # assign work to the correct GPU - return task(platform_kwargs=platform_options) +# # run the task and pass in the DeviceIndex for OpenMM to +# # assign work to the correct GPU +# return task(platform_kwargs=platform_options) -class OpenMMGPUWorker(Worker): - """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). +# class OpenMMGPUWorker(Worker): +# """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). - This is intended to be used with the wepy.work_mapper.WorkerMapper - work mapper class. +# This is intended to be used with the wepy.work_mapper.WorkerMapper +# work mapper class. - This class must be used in order to ensure OpenMM runs jobs on the - appropriate GPU device. +# This class must be used in order to ensure OpenMM runs jobs on the +# appropriate GPU device. - """ +# """ - NAME_TEMPLATE = "OpenMMGPUWorker-{}" - """The name template the worker processes are named to substituting in - the process number.""" +# NAME_TEMPLATE = "OpenMMGPUWorker-{}" +# """The name template the worker processes are named to substituting in +# the process number.""" - def run_task(self, task): - # get the platform - platform = self.mapper_attributes["platform"] +# def run_task(self, task): +# # get the platform +# platform = self.mapper_attributes["platform"] - # get the device index from the attributes - device_id = self.mapper_attributes["device_ids"][self._worker_idx] +# # get the device index from the attributes +# device_id = self.mapper_attributes["device_ids"][self._worker_idx] - # make the platform kwargs dictionary - platform_options = {"DeviceIndex": str(device_id)} +# # make the platform kwargs dictionary +# platform_options = {"DeviceIndex": str(device_id)} - logger.info(f"platform={platform}, platform_options={platform_options}") +# logger.info(f"platform={platform}, platform_options={platform_options}") - return task( - platform=platform, - platform_kwargs=platform_options, - ) +# return task( +# platform=platform, +# platform_kwargs=platform_options, +# ) -class OpenMMCPUWalkerTaskProcess(WalkerTaskProcess): - NAME_TEMPLATE = "OpenMM_CPU_Walker_Task-{}" +# class OpenMMCPUWalkerTaskProcess(WalkerTaskProcess): +# NAME_TEMPLATE = "OpenMM_CPU_Walker_Task-{}" - def run_task(self, task): - print("CPU Walker Task ---->", self.mapper_attributes, task, task.func) - if "num_threads" in self.mapper_attributes: - num_threads = self.mapper_attributes["num_threads"] +# def run_task(self, task): +# print("CPU Walker Task ---->", self.mapper_attributes, task, task.func) +# if "num_threads" in self.mapper_attributes: +# num_threads = self.mapper_attributes["num_threads"] - # make the platform kwargs dictionary - platform_options = {"Threads": str(num_threads)} +# # make the platform kwargs dictionary +# platform_options = {"Threads": str(num_threads)} - logger.info(f"Threads={num_threads}") +# logger.info(f"Threads={num_threads}") - else: - platform_options = {} +# else: +# platform_options = {} - return task( - platform_kwargs=platform_options, - ) +# return task( +# platform_kwargs=platform_options, +# ) -class OpenMMGPUWalkerTaskProcess(WalkerTaskProcess): - NAME_TEMPLATE = "OpenMM_GPU_Walker_Task-{}" +# class OpenMMGPUWalkerTaskProcess(WalkerTaskProcess): +# NAME_TEMPLATE = "OpenMM_GPU_Walker_Task-{}" - def run_task(self, task): - logger.info(f"Starting to run a task as worker {self._worker_idx}") +# def run_task(self, task): +# logger.info(f"Starting to run a task as worker {self._worker_idx}") - logger.info(f"GPU Walker Task ----> {self.mapper_attributes}") - # get the platform - platform = self.mapper_attributes["platform"] +# logger.info(f"GPU Walker Task ----> {self.mapper_attributes}") +# # get the platform +# platform = self.mapper_attributes["platform"] - # get the device index from the attributes - device_id = self.mapper_attributes["device_ids"][self._worker_idx] +# # get the device index from the attributes +# device_id = self.mapper_attributes["device_ids"][self._worker_idx] - # make the platform kwargs dictionary - platform_options = {"DeviceIndex": str(device_id)} +# # make the platform kwargs dictionary +# platform_options = {"DeviceIndex": str(device_id)} - logger.info(f"platform={platform}, platform_options={platform_options}") +# logger.info(f"platform={platform}, platform_options={platform_options}") - return task( - platform=platform, - platform_kwargs=platform_options, - ) +# return task( +# platform=platform, +# platform_kwargs=platform_options, +# ) diff --git a/tests/unit/test_runners/test_openmm.py b/tests/unit/test_runners/test_openmm.py index 2ab70430..0bacd72b 100644 --- a/tests/unit/test_runners/test_openmm.py +++ b/tests/unit/test_runners/test_openmm.py @@ -5,12 +5,10 @@ OpenMMRunner, OpenMMState, gen_sim_state, - gen_walker_state, - OpenMMWalker, - OpenMMCPUWorker, - OpenMMGPUWorker, - OpenMMCPUWalkerTaskProcess, - OpenMMGPUWalkerTaskProcess, + # OpenMMCPUWorker, + # OpenMMGPUWorker, + # OpenMMCPUWalkerTaskProcess, + # OpenMMGPUWalkerTaskProcess, ) import pytest @@ -153,15 +151,6 @@ def test_gen_sim_state(): ) -def test_gen_walker_state(): - - gen_walker_state( - positions=particle_line(2), - system=n_lj_system(2), - integrator=openmm.VerletIntegrator(0.002), - ) - - class TestOpenMMState: # TODO: this whole class needs overhauled but I want the other @@ -267,11 +256,11 @@ def test_pre_cycle(self): runner.pre_cycle( platform="CPU", - platform_kwargs={"Threads": 1}, + platform_kwargs={"Threads": "1"}, ) assert runner._cycle_platform == "CPU" - assert runner._cycle_platform_kwargs == {"Threads": 1} + assert runner._cycle_platform_kwargs == {"Threads": "1"} def test_post_cycle(self): @@ -304,11 +293,11 @@ def test_post_cycle(self): runner.pre_cycle( platform="CPU", - platform_kwargs={"Threads": 1}, + platform_kwargs={"Threads": "1"}, ) assert runner._cycle_platform == "CPU" - assert runner._cycle_platform_kwargs == {"Threads": 1} + assert runner._cycle_platform_kwargs == {"Threads": "1"} runner.post_cycle() @@ -336,7 +325,7 @@ def test__resolve_platform(self): None, ) assert runner._resolve_platform( - platform=Ellipsis, platform_kwargs={"Threads": 1} + platform=Ellipsis, platform_kwargs={"Threads": "1"} ) == (None, None) assert runner._resolve_platform(platform="CPU", platform_kwargs=None) == ( @@ -344,8 +333,8 @@ def test__resolve_platform(self): None, ) assert runner._resolve_platform( - platform="CPU", platform_kwargs={"Threads": 1} - ) == ("CPU", {"Threads": 1}) + platform="CPU", platform_kwargs={"Threads": "1"} + ) == ("CPU", {"Threads": "1"}) runner = OpenMMRunner( system=system, @@ -355,12 +344,12 @@ def test__resolve_platform(self): runner.pre_cycle( platform="CPU", - platform_kwargs={"Threads": 1}, + platform_kwargs={"Threads": "1"}, ) assert runner._resolve_platform(platform=None, platform_kwargs=None) == ( "CPU", - {"Threads": 1}, + {"Threads": "1"}, ) runner = OpenMMRunner( @@ -389,7 +378,6 @@ def test_run_segment(self): ) state = OpenMMState(state) - walker = OpenMMWalker(state, 0.1) runner = OpenMMRunner( system=system, @@ -398,13 +386,13 @@ def test_run_segment(self): ) runner.run_segment( - walker, + state, 2, ) - walker = runner.run_segment(walker, 2, getState_kwargs={"positions": True}) - assert walker.state["positions"] is not None - assert walker.state["velocities"] is None + new_state = runner.run_segment(state, 2, getState_kwargs={"positions": True}) + assert new_state["positions"] is not None + assert new_state["velocities"] is None runner = OpenMMRunner( system=system, @@ -413,24 +401,24 @@ def test_run_segment(self): ) runner.run_segment( - walker, + new_state, 2, platform="Reference", ) -class TestOpenMMCPUWorker: +# class TestOpenMMCPUWorker: - pass +# pass -class TestOpenMMGPUWorker: - pass +# class TestOpenMMGPUWorker: +# pass -class TestOpenMMCPUWalkerTaskProcess: - pass +# class TestOpenMMCPUWalkerTaskProcess: +# pass -class TestOpenMMGPUWalkerTaskProcess: - pass +# class TestOpenMMGPUWalkerTaskProcess: +# pass From 9159437abe880e6b7c7b438031b94312ca2334b6 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:28:38 -0500 Subject: [PATCH 028/143] add some functional tests --- src/wepy/runners/openmm.py | 3 + .../test_openmm/test_serial_mapper.py | 44 +++++++++++++ .../test_openmm/test_sim_manager.py | 62 +++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 tests/functional/test_openmm/test_serial_mapper.py create mode 100644 tests/functional/test_openmm/test_sim_manager.py diff --git a/src/wepy/runners/openmm.py b/src/wepy/runners/openmm.py index 04b5107a..4dcea531 100644 --- a/src/wepy/runners/openmm.py +++ b/src/wepy/runners/openmm.py @@ -1071,6 +1071,9 @@ def run_segment( getState_kwargs: dict[str, bool] | None = None, platform: str | type(Ellipsis) | None = None, platform_kwargs: PlatformKwargs = None, + # UGLY: here to satisfy the interface + cycle_idx: int = 0, + walker_idx: int = 0 ) -> OpenMMState: """Run dynamics for the walker. diff --git a/tests/functional/test_openmm/test_serial_mapper.py b/tests/functional/test_openmm/test_serial_mapper.py new file mode 100644 index 00000000..9c5702b0 --- /dev/null +++ b/tests/functional/test_openmm/test_serial_mapper.py @@ -0,0 +1,44 @@ +import functools +import openmm +from wepy.runners.openmm import OpenMMRunner, OpenMMState, gen_sim_state +from wepy.work_mapper.serial import SerialMapper + +from wepy_tools.systems.lennard_jones import LennardJonesPair + + +def test_serial_mapper(): + + lj_sys = LennardJonesPair() + integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + + + runner = OpenMMRunner( + system=lj_sys.system, + topology=lj_sys.topology, + integrator=integrator, + ) + + run_func = functools.partial( + runner.run_segment, + platform="Reference", + ) + + num_walkers = 4 + + walker_states = [ + OpenMMState(gen_sim_state( + lj_sys.positions, + system=lj_sys.system, + integrator=integrator, + )) + for _ + in range(num_walkers) + ] + + mapper = SerialMapper() + mapper.init(run_func) + + mapper.map( + walker_states, + [10 for _ in range(num_walkers)], + ) diff --git a/tests/functional/test_openmm/test_sim_manager.py b/tests/functional/test_openmm/test_sim_manager.py new file mode 100644 index 00000000..082ce59d --- /dev/null +++ b/tests/functional/test_openmm/test_sim_manager.py @@ -0,0 +1,62 @@ +import functools +import openmm +from wepy.walker import Walker +from wepy.runners.openmm import OpenMMRunner, OpenMMState, gen_sim_state +from wepy.work_mapper.serial import SerialMapper +from wepy.sim_manager import Manager +from wepy.resampling.resamplers.noresampler import NoResampler + +from wepy_tools.systems.lennard_jones import LennardJonesPair + + +def test_serial_mapper(): + + lj_sys = LennardJonesPair() + integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + + + runner = OpenMMRunner( + system=lj_sys.system, + topology=lj_sys.topology, + integrator=integrator, + ) + + run_func = functools.partial( + runner.run_segment, + platform="Reference", + ) + + num_walkers = 4 + + walker_states = [ + OpenMMState(gen_sim_state( + lj_sys.positions, + system=lj_sys.system, + integrator=integrator, + )) + for _ + in range(num_walkers) + ] + + init_walker_weight = 1 / num_walkers + init_walkers = [ + Walker( + state=walker_state, + weight=init_walker_weight, + ) + for walker_state + in walker_states + ] + + sim_manager = Manager( + init_walkers=init_walkers, + runner=runner, + resampler=NoResampler(), + work_mapper=SerialMapper(), + ) + + new_walkers, sim_components = sim_manager.run_simulation( + n_cycles=1, + segment_lengths=10, + num_workers=None, + ) From 40caf116e0b32572d2e8aea9c988d3d245dc4ff0 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:28:50 -0500 Subject: [PATCH 029/143] remove junk .keep files --- tests/docs/.keep | 0 tests/integration/.keep | 0 tests/unit/.keep | 0 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/docs/.keep delete mode 100644 tests/integration/.keep delete mode 100644 tests/unit/.keep diff --git a/tests/docs/.keep b/tests/docs/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/integration/.keep b/tests/integration/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/unit/.keep b/tests/unit/.keep deleted file mode 100644 index e69de29b..00000000 From fd44a0b63662f2a11aeaf4c3d3d2de70718d2817 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:47:03 -0500 Subject: [PATCH 030/143] removing more junk, get doc tests running doc tests still fail --- .coveragerc | 19 - .gitattributes | 1 - MANIFEST.in | 24 -- noxfile.py | 440 ---------------------- pyproject.toml | 10 +- scrapyard/wexplore_image_reporter.py | 131 ------- tests/integration/conftest.py | 7 - tests/integration/test_cli.py | 214 ----------- tests/integration/test_lj_combinations.py | 161 -------- tests/integration/test_lj_fixture.py | 343 ----------------- tests/utils/README.org | 3 - tests/utils/myutils.py | 17 - uv.lock | 73 ++++ 13 files changed, 81 insertions(+), 1362 deletions(-) delete mode 100644 .coveragerc delete mode 100644 .gitattributes delete mode 100644 MANIFEST.in delete mode 100644 noxfile.py delete mode 100644 scrapyard/wexplore_image_reporter.py delete mode 100644 tests/integration/conftest.py delete mode 100644 tests/integration/test_cli.py delete mode 100644 tests/integration/test_lj_combinations.py delete mode 100644 tests/integration/test_lj_fixture.py delete mode 100644 tests/utils/README.org delete mode 100644 tests/utils/myutils.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index c5941d49..00000000 --- a/.coveragerc +++ /dev/null @@ -1,19 +0,0 @@ -[run] -branch = True -parallel = True -omit = - tests/* - -source = src/ - -[report] -exclude_lines = - @overload - pragma: no cover - raise NotImplementedError - if TYPE_CHECKING: - pass - if __name__ == "__main__": - -# SNIPPET: use this to fail CI for missing test coverage -# fail_under = 100 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index c14939b4..00000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -src/wepy/_version.py export-subst diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 9ebaaf81..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,24 +0,0 @@ -graft src -graft info -graft envs - -prune info/examples/*/_env -prune info/examples/*/_output -prune info/examples/*/_tangle_source - -prune info/tutorials/*/_env -prune info/tutorials/*/_output -prune info/tutorials/*/_tangle_source - -include AUTHORS.org -include CHANGELOG.org -include LICENSE -include README.org - -include pyproject.toml - -include requirements.in -include versioneer.py - -global-exclude *.py[co] __pycache__ *.so *~ - diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index c2ca4827..00000000 --- a/noxfile.py +++ /dev/null @@ -1,440 +0,0 @@ -# NOTE: A quick note on what Nox is used for specifically. Nox is used for -# anything that requires some sort of special virtual environment in order to -# operate. That includes creating standalone virtualenvs, and for single tasks -# requiring a specific environment. Nox should be considered an implementation -# detail of this however and all relevant high level targets should be still -# created in the Makefile. Furthermore, git hooks should reference those -# Makefile targets rather than the nox targets directly; keeping them decoupled. -# Of course feel free to use the nox targets if its easier. - -# Standard Library -import itertools as it -import os -from pathlib import Path - -# Third Party Library -import nox - -# exclude the 'dev' session here so its not run automatically -nox.options.sessions = [] - -# NOTE: that with 3.11 mdtraj fails to build -DEFAULT_PYTHON_VERSION = "3.10" - -PROJECT_ROOT_DIR = Path(__file__).parent - -SRC_DIR = PROJECT_ROOT_DIR / "src" - -SPHINX_SOURCE_DIR = PROJECT_ROOT_DIR / "sphinx" -SPHINX_BUILD_DIR = SPHINX_SOURCE_DIR / "_build" - -# listing of things to be formatted and checked -FORMAT_TARGETS = [ - "src", - "tests", - "noxfile.py", - SPHINX_SOURCE_DIR / "conf.py", -] - -LINT_TARGETS = FORMAT_TARGETS - -TYPECHECK_TARGETS = [] - -UNIT_TEST_DIRNAME = "unit" - -### Helpers - - -def install_requirements(requirements_paths: list[str]) -> list[str]: - """Given a list of requirements files generate the subprocess string for - installing all of them.""" - - return list( - it.chain( - *it.zip_longest( - [], - requirements_paths, - fillvalue="-r", - ) - ) - ) - - -def install_interactive(session: nox.Session) -> None: - """Install the standard set of interactive work dependencies, not useful in CI typically.""" - - session.install("-r", "dev/interactive.requirements.txt") - - -### Pinning - -# which extras to generate pin files for -PIN_EXTRAS = [ - "md", - "distributed", - "prometheus", - "graphics", -] - -EXTRAS_REQUIREMENTS_MAP = { - "md": "requirements-md.txt", - "distributed": "requirements-distributed.txt", - "prometheus": "requirements-prometheus.txt", -} - - -def resolve_extras_reqfiles(extras: str) -> list[str]: - extras_items = extras.split(",") - - extras_reqfiles = [] - for extra in extras_items: - extras_reqfiles.append(EXTRAS_REQUIREMENTS_MAP[extra]) - - return extras_reqfiles - - -@nox.session -@nox.parametrize("extras", PIN_EXTRAS) -def pin(session, extras): - session.install("pip-deepfreeze") - session.run("pip-df", "sync", "--extras", ",".join(PIN_EXTRAS)) - - -### Development Environment - -# this VENV_DIR constant specifies the name of the dir that the `dev` -# session will create, containing the virtualenv; -# the `resolve()` makes it portable -DEV_VENV_DIR = Path("./.venv").resolve() - - -def external_venv( - session, - requirements_txt_list: list[str], - venv_path: Path = DEV_VENV_DIR, -): - session.install("virtualenv") - session.run("virtualenv", os.fsdecode(venv_path), silent=True) - - python = os.fsdecode(venv_path.joinpath("bin/python")) - - install_spec = install_requirements(requirements_txt_list) - - session.run( - python, - "-m", - "pip", - "install", - *install_spec, - "-e", - ".", - external=True, - ) - - -@nox.session(python=DEFAULT_PYTHON_VERSION) -def dev_external(session: nox.Session) -> None: - """Set up a development environment in the '.venv' top-level folder. - - This development environment contains all dependencies needed for - development. - - """ - - mandatory_reqs = [ - "requirements.txt", - "dev/qa.requirements.txt", - "dev/typechecking.requirements.txt", - "dev/testing.requirements.txt", - ] - - # we use all the extras for the dev environment - extras_reqs = list(EXTRAS_REQUIREMENTS_MAP.values()) - - base_reqs = mandatory_reqs + extras_reqs - - if session.interactive: - reqs = base_reqs + ["dev/interactive.requirements.txt"] - - else: - reqs = base_reqs - - external_venv( - session, - reqs, - ) - - -@nox.session(python=DEFAULT_PYTHON_VERSION) -@nox.parametrize( - "extras", - [ - "postgres", - "postgres-async", - "postgres,postgres-async", - ], -) -def prod_external( - session: nox.Session, - extras: str, -) -> None: - extra_reqs = [f"requirements-{extra}.txt" for extra in extras.split(",")] - - external_venv( - session, - ["requirements.txt"] + extra_reqs, - ) - - -### QA - -## Base Functions - - -def _black_format(session): - session.run("black", *FORMAT_TARGETS) - - -def _isort_format(session): - session.run("isort", *FORMAT_TARGETS) - - -def _format(session): - _black_format(session) - _isort_format(session) - - -def _black_check(session): - session.run("black", "--check", *FORMAT_TARGETS) - - -def _isort_check(session): - session.run("isort", "--check", *FORMAT_TARGETS) - - -def _format_check(session): - _black_check(session) - _isort_check(session) - - -def _flake8(session): - session.run("flake8", *LINT_TARGETS) - - -def _interrogate(session): - session.run("interrogate", *LINT_TARGETS) - - -def _docstring_lint(session): - _interrogate(session) - - -def _lint(session): - _flake8(session) - - -def _typecheck(session): - session.run("mypy", "--strict", *TYPECHECK_TARGETS) - - -def qa_install(session): - install_spec = install_requirements( - [ - "dev/qa.requirements.txt", - ] - ) - - session.install(*install_spec) - - -## Fine Grained Sessions -@nox.session -def black_check(session): - qa_install(session) - _black_check(session) - - -@nox.session -def isort_check(session): - qa_install(session) - _isort_check(session) - - -@nox.session -def format_check(session): - qa_install(session) - _format_check(session) - - -@nox.session -def flake8(session): - qa_install(session) - _flake8(session) - - -@nox.session -def interrogate(session): - qa_install(session) - _interrogate(session) - - -@nox.session -def docstring_lint(session): - qa_install(session) - _docstring_lint(session) - - -@nox.session -def lint(session): - qa_install(session) - _lint(session) - - -## Top-Level Targets -@nox.session -def validate(session: nox.Session) -> None: - """Run all static analysis QA checks.""" - qa_install(session) - - _format_check(session) - _lint(session) - - -@nox.session(python=DEFAULT_PYTHON_VERSION) -def typecheck(session): - """Run typechecking for the project.""" - - install_spec = install_requirements( - [ - "dev/typechecking.requirements.txt", - ] - ) - - session.install(*install_spec, "-e", ".") - _typecheck(session) - - -@nox.session -def format(session): - """Run formatting on the code.""" - qa_install(session) - - _format(session) - - -### Tests - - -@nox.session(python=DEFAULT_PYTHON_VERSION) -def tests_unit( - session: nox.Session, -) -> None: - """Run the unit tests, generate the coverage database and the HTML report.""" - - install_spec = install_requirements( - [ - "dev/testing.requirements.txt", - "requirements.txt", - # we add all the extras in as well, we don't use them - # inappropriately though! - "requirements-distributed.txt", - "requirements-md.txt", - "requirements-prometheus.txt", - ] - ) - - session.install(*install_spec, "-e", ".") - - if session.interactive: - install_interactive(session) - - session.run( - "pytest", - "-s", - # modern way of importing stuff, use with `pythonpath` option in pytest.ini - "--import-mode=importlib", - "--cov-report=term-missing:skip-covered", - "--cov=wepy", - # for this stage don't fail on missing coverage - "--cov-fail-under=0", - # the pointer plugin, collect covered modules - # "--pointers-collect=src", - # "--pointers-report", - # "--pointers-func-min-pass=1", - # "--pointers-fail-under=100", - # # block the loading of the integration test plugins - # "-p", - # "no:local_test_utils.plugins.database", - # f"tests/{UNIT_TEST_DIRNAME}", - "tests/unit/test_work_mapper", - ) - - session.run("coverage", "html", "--fail-under=100", "--skip-covered") - - -@nox.session(python=DEFAULT_PYTHON_VERSION) -def coverage(session): - """Check that the coverage generated by unit tests passes.""" - - # TODO: add a way to report the unit coverage as well - - session.install("coverage") - - session.run("coverage", "html", "--skip-covered") - session.run( - "coverage", - "report", - "--fail-under=100", - "--data-file=.coverage", - "--show-missing", - ) - - -# TODO: integration, benchmark, acceptance, and docs tests -# TODO: build documentation - -## Builds & Releases - - -@nox.session(python=DEFAULT_PYTHON_VERSION) -def build(session): - session.install("hatch") - - session.run("hatch", "build") - - -@nox.session(python=DEFAULT_PYTHON_VERSION) -def bumpversion(session): - session.install("hatch") - - if session.posargs: - assert len(session.posargs) == 1, "Too many arguments only need 1." - part = session.posargs[0] - assert part in ( - "major", - "minor", - "patch", - ), "Must choose a valid bump part ('major', 'minor', 'patch')" - else: - part = "patch" - - session.run("hatch", "version", part) - - -# NOTE: this is the current owner of the PyPI package contact my email above for -# more info -PYPI_USER = "salotz" - - -@nox.session(python=DEFAULT_PYTHON_VERSION) -def publish(session): - session.install("hatch") - - session.run( - "hatch", - "-v", - "publish", - env={ - "HATCH_INDEX_USER": PYPI_USER, - }, - ) diff --git a/pyproject.toml b/pyproject.toml index 6cfa4745..30d6cc2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,11 @@ dev = [ "pytest", "coverage", "pytest-cov", + # for documentation testing + "pytest-shutil", + "pytest-check", + "pytest-datadir @ git+https://github.com/salotz/pytest-datadir-extras.git@41e9a1e94ba27efe28ce9d0019b1e0a73be34400", + "pytest-print", # qa "black", "isort", @@ -106,5 +111,6 @@ dev = [ fail-under = 100 verbose = 2 -[tool.hatch.version] -path = "src/wepy/__about__.py" +# [tool.uv.sources] + +# pytest-datadir = { git = "https://github.com/salotz/pytest-datadir-e"} \ No newline at end of file diff --git a/scrapyard/wexplore_image_reporter.py b/scrapyard/wexplore_image_reporter.py deleted file mode 100644 index bb3c95e2..00000000 --- a/scrapyard/wexplore_image_reporter.py +++ /dev/null @@ -1,131 +0,0 @@ -import logging -logger = logging.getLogger(__name__) - -import numpy as np - -import mdtraj as mdj - -from wepy.reporter.reporter import ProgressiveFileReporter -from wepy.util.mdtraj import json_to_mdtraj_topology, mdtraj_to_json_topology -from wepy.util.json_top import json_top_subset - -class WExploreAtomImageReporter(ProgressiveFileReporter): - """Reporter for generating 3D molecular structures from WExplore - region images. - - This will only be meaningful for WExplore simulations where the - region images are actually 3D coordinates. - - """ - - FILE_ORDER = ("init_state_path", "image_path") - SUGGESTED_EXTENSIONS = ("image_top.pdb", "wexplore_images.dcd") - - - def __init__(self, - init_image=None, - image_atom_idxs=None, - json_topology=None, - **kwargs): - """Constructor for the WExploreAtomImageReporter. - - Parameters - ---------- - - init_image : numpy.array, optional - The initial region image. Used for generating the topology - as well. If not given will be eventually generated. - (Default = None) - - image_atom_idxs : list of int - The indices of the atoms that are part of the topology - subset that comprises the image. - - json_topology : str - JSON format topology for the whole system. A subset of the - atoms will be taken using the image_atom_idxs. - - """ - - super().__init__(**kwargs) - - assert json_topology is not None, "must give a JSON format topology" - assert image_atom_idxs is not None, \ - "must give the indices of the atoms for the subset of the topology that is the image" - - self.image_atom_idxs = image_atom_idxs - - self.json_main_rep_top = json_top_subset(json_topology, self.image_atom_idxs) - - self.init_image = None - self._top_pdb_written = False - self.image_traj_positions = [] - - # if an initial image was given use it, otherwise just don't - # worry about it, the reason for this is that there is no - # interface for getting image indices from distance metrics as - # of now. - if init_image is not None: - self.init_image = init_image - self.image_traj_positions.append(self.init_image) - - # and times - self.times = [0] - - - def init(self, **kwargs): - - super().init(**kwargs) - - if self.init_image is not None: - - image_mdj_topology = json_to_mdtraj_topology(self.json_main_rep_top) - - # initialize the initial image into the image traj - init_image_traj = mdj.Trajectory([self.init_image], - time=self.times, - topology=image_mdj_topology) - - - - # save this as a PDB for a topology to view in VMD etc. to go - # along with the trajectory we will make - logger.info("Writing initial image to {}".format(self.init_state_path)) - init_image_traj.save_pdb(self.init_state_path) - - self._top_pdb_written = True - - def report(self, cycle_idx=None, resampler_data=None, - **kwargs): - - # load the json topology as an mdtraj one - image_mdj_topology = json_to_mdtraj_topology(self.json_main_rep_top) - - # collect the new images defined - new_images = [] - for resampler_rec in resampler_data: - image = resampler_rec['image'] - new_images.append(image) - - times = np.array([cycle_idx + 1 for _ in range(len(new_images))]) - - - # combine the new image positions and times with the old - self.image_traj_positions.extend(new_images) - self.times.extend(times) - - # only save if we have an image yet - if len(self.image_traj_positions) > 0: - - # make a trajectory of the new images, using the cycle_idx as the time - new_image_traj = mdj.Trajectory(self.image_traj_positions, - time=self.times, - topology=image_mdj_topology) - - # if we haven't already written a topology PDB write it now - if not self._top_pdb_written: - new_image_traj[0].save_pdb(self.init_state_path) - self._top_pdb_written = True - - # then the images to the trajectory file - new_image_traj.save_dcd(self.image_path) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py deleted file mode 100644 index 11d4a967..00000000 --- a/tests/integration/conftest.py +++ /dev/null @@ -1,7 +0,0 @@ -# Third Party Library - - -# using this to get rid of the warning without having to put it in my -# config file -def pytest_configure(config): - config.addinivalue_line("markers", "interactive: tests which give you the debugger") diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py deleted file mode 100644 index 5b3c043d..00000000 --- a/tests/integration/test_cli.py +++ /dev/null @@ -1,214 +0,0 @@ -# Standard Library -import os -import os.path as osp -import pdb - -# Third Party Library -import pytest -from click.testing import CliRunner - -# First Party Library -from wepy.orchestration.cli import cli as wepy_cli -from wepy.orchestration.orchestrator import Orchestrator - -lj_fixtures = [ - "lj_orchestrator_defaults_file", - "lj_orch_file_orchestrated_run", - "lj_orchestrator_defaults_file_other", - "lj_orch_file_other_orchestrated_run", -] - - -@pytest.mark.interactive -def test_orch_workdir(lj_orchestrator_defaults_file): - pdb.set_trace() - - -@pytest.mark.usefixtures(*lj_fixtures) -class TestCLI: - def test_ls_runs(self, lj_orch_file_orchestrated_run): - orch_path = lj_orch_file_orchestrated_run.orch_path - - runner = CliRunner() - - result = runner.invoke(wepy_cli, ["ls", "runs", orch_path]) - - assert result.exit_code == 0 - - def test_ls_snapshots(self, lj_orch_file_orchestrated_run): - orch_path = lj_orch_file_orchestrated_run.orch_path - - runner = CliRunner() - - result = runner.invoke(wepy_cli, ["ls", "snapshots", orch_path]) - - assert result.exit_code == 0 - - def test_ls_configs(self, lj_orch_file_orchestrated_run): - orch_path = lj_orch_file_orchestrated_run.orch_path - - runner = CliRunner() - - result = runner.invoke(wepy_cli, ["ls", "configs", orch_path]) - - assert result.exit_code == 0 - - def test_run_orch(self, function_tmp_path_factory, lj_orchestrator_defaults_file): - workdir = str(function_tmp_path_factory.mktemp("test_run")) - - n_steps = str(100) - n_seconds = str(5) - - orch_path = lj_orchestrator_defaults_file.orch_path - start_hash = lj_orchestrator_defaults_file.get_default_snapshot_hash() - - runner = CliRunner() - - result = runner.invoke( - wepy_cli, - [ - "run", - "orch", - "--job-dir", - workdir, - orch_path, - start_hash, - n_seconds, - n_steps, - ], - catch_exceptions=False, - ) - - assert result.exit_code == 0 - - def test_run_snapshot( - self, function_tmp_path_factory, lj_snapshot, lj_configuration - ): - workdir = str(function_tmp_path_factory.mktemp("test_run")) - - # write the snapshot to the file system - serial_snap = Orchestrator.serialize(lj_snapshot) - snap_path = osp.join(workdir, "snapshot.snap.dill.pkl") - with open(snap_path, "wb") as wf: - wf.write(serial_snap) - - # write the configuration to the file system - serial_config = Orchestrator.serialize(lj_configuration) - config_path = osp.join(workdir, "config.config.dill.pkl") - with open(config_path, "wb") as wf: - wf.write(serial_config) - - n_steps = str(100) - n_seconds = str(5) - - runner = CliRunner() - - result = runner.invoke( - wepy_cli, - [ - "run", - "snapshot", - "--job-dir", - workdir, - snap_path, - config_path, - n_seconds, - n_steps, - ], - catch_exceptions=False, - ) - - assert result.exit_code == 0 - - def test_get_snapshot( - self, function_tmp_path_factory, lj_orchestrator_defaults_file - ): - workdir = str(function_tmp_path_factory.mktemp("test_run")) - - orch_path = lj_orchestrator_defaults_file.orch_path - start_hash = lj_orchestrator_defaults_file.get_default_snapshot_hash() - - os.chdir(workdir) - - runner = CliRunner() - - result = runner.invoke( - wepy_cli, - ["get", "snapshot", orch_path, start_hash], - catch_exceptions=False, - ) - - assert result.exit_code == 0 - assert osp.exists("{}.snap.dill.pkl".format(start_hash)) - - def test_get_config(self, function_tmp_path_factory, lj_orchestrator_defaults_file): - workdir = str(function_tmp_path_factory.mktemp("test_run")) - - orch_path = lj_orchestrator_defaults_file.orch_path - config_hash = lj_orchestrator_defaults_file.get_default_configuration_hash() - - os.chdir(workdir) - - runner = CliRunner() - - result = runner.invoke( - wepy_cli, - ["get", "config", orch_path, config_hash], - catch_exceptions=False, - ) - - assert result.exit_code == 0 - assert osp.exists("{}.config.dill.pkl".format(config_hash)) - - def test_get_run(self, function_tmp_path_factory, lj_orch_file_orchestrated_run): - workdir = str(function_tmp_path_factory.mktemp("test_run")) - - orch_path = lj_orch_file_orchestrated_run.orch_path - start_hash, end_hash = lj_orch_file_orchestrated_run.run_hashes()[0] - - os.chdir(workdir) - - runner = CliRunner() - - result = runner.invoke( - wepy_cli, - ["get", "run", orch_path, start_hash, end_hash], - catch_exceptions=False, - ) - - assert result.exit_code == 0 - assert osp.exists("{}-{}.orch.sqlite".format(start_hash, end_hash)) - - def test_reconcile( - self, - function_tmp_path_factory, - lj_orch_file_orchestrated_run, - lj_orch_file_other_orchestrated_run, - ): - savedir = function_tmp_path_factory.mktemp("reconciliation") - h5_target = str(savedir / "reconciled.wepy.h5") - orch_target = str(savedir / "reconciled.orch.sqlite") - - orch_path = lj_orch_file_orchestrated_run.orch_path - other_orch_path = lj_orch_file_other_orchestrated_run.orch_path - - runner = CliRunner() - - result = runner.invoke( - wepy_cli, - [ - "reconcile", - "orch", - "--hdf5", - h5_target, - orch_target, - orch_path, - other_orch_path, - ], - catch_exceptions=False, - ) - - assert result.exit_code == 0 - - def test_copy_h5(self, lj_orch_file_orchestrated_run): - assert False diff --git a/tests/integration/test_lj_combinations.py b/tests/integration/test_lj_combinations.py deleted file mode 100644 index a049d749..00000000 --- a/tests/integration/test_lj_combinations.py +++ /dev/null @@ -1,161 +0,0 @@ -# Standard Library -import logging - -logger = logging.getLogger(__name__) -# Standard Library - -# Third Party Library -import pytest - -# First Party Library -from wepy_tools.sim_makers.openmm.lennard_jones import LennardJonesPairOpenMMSimMaker -from wepy_tools.sim_makers.openmm.lysozyme import LysozymeImplicitOpenMMSimMaker - - -def get_sim_maker(spec): - if spec == "LennardJonesPair": - sim_maker = LennardJonesPairOpenMMSimMaker() - elif spec == "LysozymeImplicit": - sim_maker = LysozymeImplicitOpenMMSimMaker() - else: - raise ValueError("Unknown system spec: {}".format(spec)) - - return sim_maker - - -# testing on a node in HPCC means we will have 8 GPUS -BIG_NODE_N_WORKERS = 8 -DEV_NODE_N_WORKERS = 1 - -# number of walkers in multiples of 8 since that is how many GPUs we -# have -BIG_NODE_N_WALKER_TESTS = [i * BIG_NODE_N_WORKERS for i in (1, 2, 4, 8)] -DEV_NODE_N_WALKER_TESTS = [i * DEV_NODE_N_WORKERS for i in (5, 10, 20)] -DEV_NODE_N_WALKER_TESTS = [i for i in (10,)] - -# 1 ps, 5 ps, 10 ps, #20 ps -N_STEPS_TEST = [10, 50, 100, 200] # [1000, 5000, 10000] -N_CYCLES_TEST = [1, 10, 100] -SYSTEMS_TEST = [ - "LennardJonesPair", -] # 'LysozymeImplicit',] -PLATFORMS_TEST = ["OpenCL"] -RESAMPLERS_TEST = ["NoResampler", "REVOResampler", "WExploreResampler"] -WORK_MAPPERS_TEST = ["WorkerMapper", "TaskMapper"] - - -class TestCombinationsMinorNode: - @pytest.mark.parametrize( - "n_walkers", - [ - 5, - ], - ) - @pytest.mark.parametrize( - "n_cycles", - [ - 3, - ], - ) - @pytest.mark.parametrize( - "n_steps", - [ - 10, - ], - ) - @pytest.mark.parametrize( - "platform", - [ - "CPU", - ], - ) - @pytest.mark.parametrize("system", SYSTEMS_TEST) - @pytest.mark.parametrize("resampler", RESAMPLERS_TEST) - @pytest.mark.parametrize("work_mapper", WORK_MAPPERS_TEST) - def test_combinations( - self, n_walkers, n_cycles, n_steps, platform, system, resampler, work_mapper - ): - sim_maker = get_sim_maker(system) - - apparatus = sim_maker.make_apparatus(platform=platform, resampler=resampler) - - config = sim_maker.make_configuration( - apparatus, work_mapper_spec=work_mapper, platform=platform, reporters=None - ) - - sim_manager = sim_maker.make_sim_manager(n_walkers, apparatus, config) - - result = sim_manager.run_simulation( - n_cycles, n_steps, num_workers=DEV_NODE_N_WORKERS - ) - - -class TestCombinationsDevNode: - @pytest.mark.parametrize( - "n_walkers", - [ - 5, - ], - ) - @pytest.mark.parametrize( - "n_cycles", - [ - 3, - ], - ) - @pytest.mark.parametrize( - "n_steps", - [ - 10, - ], - ) - @pytest.mark.parametrize( - "platform", - [ - "OpenCL", - ], - ) - @pytest.mark.parametrize("system", SYSTEMS_TEST) - @pytest.mark.parametrize("resampler", RESAMPLERS_TEST) - @pytest.mark.parametrize("work_mapper", WORK_MAPPERS_TEST) - def test_combinations( - self, n_walkers, n_cycles, n_steps, platform, system, resampler, work_mapper - ): - sim_maker = get_sim_maker(system) - - apparatus = sim_maker.make_apparatus(platform=platform, resampler=resampler) - - config = sim_maker.make_configuration( - apparatus, work_mapper_spec="TaskMapper", platform=platform, reporters=None - ) - - sim_manager = sim_maker.make_sim_manager(n_walkers, apparatus, config) - - result = sim_manager.run_simulation( - n_cycles, n_steps, num_workers=DEV_NODE_N_WORKERS - ) - - -class TestCombinationsBigNode: - @pytest.mark.parametrize("n_walkers", BIG_NODE_N_WALKER_TESTS) - @pytest.mark.parametrize("n_cycles", N_CYCLES_TEST) - @pytest.mark.parametrize("n_steps", N_STEPS_TEST) - @pytest.mark.parametrize("platform", PLATFORMS_TEST) - @pytest.mark.parametrize("system", SYSTEMS_TEST) - @pytest.mark.parametrize("resampler", RESAMPLERS_TEST) - def test_combinations( - self, n_walkers, n_cycles, n_steps, platform, system, resampler - ): - sim_maker = get_sim_maker(system) - - apparatus = sim_maker.make_apparatus(platform=platform, resampler=resampler) - - config = sim_maker.make_configuration( - work_mapper_spec="TaskMapper", platform=platform - ) - - sim_manager = sim_maker.make_sim_manager(n_walkers, apparatus, config) - - result = sim_manager.run_simulation( - n_cycles, n_steps, num_workers=BIG_NODE_N_WORKERS - ) diff --git a/tests/integration/test_lj_fixture.py b/tests/integration/test_lj_fixture.py deleted file mode 100644 index 27c0fb2e..00000000 --- a/tests/integration/test_lj_fixture.py +++ /dev/null @@ -1,343 +0,0 @@ -# Standard Library -import logging - -logger = logging.getLogger(__name__) -# Standard Library -import pdb - -# Third Party Library -import pytest - -# testing helpers -from multiprocessing_logging import install_mp_handler - -# First Party Library -from wepy.sim_manager import Manager - -# we define a fixture for a fixture for all the components of a -# simulation of the openmmtools Lennard-Jones pair. We test the -# fixtures by requiring them one by one. - -# the fixtures are class scoped so we make a class for this - -lj_fixtures = [ - "lj_params", - "lj_omm_sys", - "lj_integrator", - "lj_init_sim_state", - "lj_init_state", - "lj_openmm_runner", - "lj_distance_metric", - "lj_resampler", - "lj_topology", - "lj_boundary_condition", - "lj_reporter_kwargs", - "lj_reporter_classes", - "lj_init_walkers", - "lj_apparatus", - "lj_snapshot", - "lj_configuration", - "lj_work_mapper", - "lj_reporters", - "lj_orchestrator", - "lj_orchestrator_defaults", - "lj_orchestrator_file", - "lj_orchestrator_file_other", - "lj_orchestrator_defaults_file", - "lj_sim_manager", - "lj_sim_manager_run_results", - "lj_orch_run_by_time_results", - "lj_orch_run_end_snapshot", - "lj_orch_orchestrated_run", - "lj_orch_file_orchestrated_run", - "lj_orch_file_other_orchestrated_run", - "lj_orch_reconciled_orchs", - "lj_sim_manager_null_run_results", -] - - -@pytest.mark.interactive -def test_init_state(lj_init_state): - pdb.set_trace() - pass - - -@pytest.mark.usefixtures(*lj_fixtures) -class TestLJPairNewOrch: - # just an empty thing to get the fixtures made and catch errors - # there - def test_fixtures(self): - pass - - @pytest.mark.interactive - def test_orch_interactive(self, lj_orchestrator_defaults): - pdb.set_trace() - - pass - - @pytest.mark.interactive - def test_reconciled_orch(self, lj_orch_reconciled_orchs): - host_orch, other_orch, reconciled_orch = lj_orch_reconciled_orchs - pdb.set_trace() - - pass - - -@pytest.mark.usefixtures( - "lj_reporters", - "lj_init_walkers", - "lj_openmm_runner", - "lj_unbinding_bc", - "lj_wexplore_resampler", - "lj_revo_resampler", - "lj_work_mapper", - "lj_work_mapper_worker", - "lj_work_mapper_task", -) -class TestLJSimIntegration: - # TODO: add revo back in after all combinations with WExplore are passing - # @pytest.mark.parametrize('resampler_class', ['WExploreResampler', 'REVOResampler',]) - - # NOTE: CUDA has issues but OpenCL tests the code path that we - # need so we will just use it here - - # order matters here for the platforms and the work mapper classes - # since there is issues with that and typically aren't being all - # used in the same place like we do here. Basically do the - # 'Mapper' last since it doesn't use it's own multiprocessing - # context. - @pytest.mark.parametrize( - "boundary_condition_class", - [ - "UnbindingBC", - ], - ) - @pytest.mark.parametrize( - "resampler_class", - [ - "WExploreResampler", - ], - ) - @pytest.mark.parametrize( - "platform", - [ - "Reference", - "CPU", - "OpenCL", - ], - ) # 'CUDA' - @pytest.mark.parametrize( - "work_mapper_class", - [ - "WorkerMapper", - "TaskMapper", - "Mapper", - ], - ) - def test_lj_sim_manager_openmm_integration_run( - self, - class_tmp_path_factory, - boundary_condition_class, - resampler_class, - work_mapper_class, - platform, - lj_params, - lj_omm_sys, - lj_integrator, - lj_reporter_classes, - lj_reporter_kwargs, - lj_init_walkers, - lj_openmm_runner, - lj_unbinding_bc, - lj_wexplore_resampler, - lj_revo_resampler, - ): - """Run all combinations of components in the fixtures for the smallest - amount of time, just to make sure they all work together and don't give errors. - """ - - logger = logging.getLogger("testing").setLevel(logging.DEBUG) - install_mp_handler() - logger.debug("Starting the test") - - print("starting the test") - - # the configuration class gives us a convenient way to - # parametrize our reporters for the locale - # First Party Library - from wepy.orchestration.configuration import Configuration - - # the runner - from wepy.runners.openmm import ( - OpenMMCPUWalkerTaskProcess, - OpenMMCPUWorker, - OpenMMGPUWalkerTaskProcess, - OpenMMGPUWorker, - OpenMMRunner, - ) - - # mappers - from wepy.work_mapper.mapper import Mapper - - # the walker task types for the TaskMapper - from wepy.work_mapper.task_mapper import TaskMapper, WalkerTaskProcess - - # the worker types for the WorkerMapper - from wepy.work_mapper.worker import Worker, WorkerMapper - - n_cycles = 1 - n_steps = 2 - num_workers = 2 - - # generate the reporters and temporary directory for this test - # combination - - tmpdir_template = "lj_fixture_{plat}-{wm}-{res}-{bc}" - tmpdir_name = tmpdir_template.format( - plat=platform, - wm=work_mapper_class, - res=resampler_class, - bc=boundary_condition_class, - ) - - # make a temporary directory for this configuration to work with - tmpdir = str(class_tmp_path_factory.mktemp(tmpdir_name)) - - # make a config so that the reporters get parametrized properly - reporters = Configuration( - work_dir=tmpdir, - reporter_classes=lj_reporter_classes, - reporter_partial_kwargs=lj_reporter_kwargs, - ).reporters - - steps = [n_steps for _ in range(n_cycles)] - - # choose the components based on the parametrization - boundary_condition = None - resampler = None - - walker_fixtures = [lj_init_walkers] - runner_fixtures = [lj_openmm_runner] - boundary_condition_fixtures = [lj_unbinding_bc] - resampler_fixtures = [lj_wexplore_resampler, lj_revo_resampler] - - walkers = lj_init_walkers - - boundary_condition = [ - boundary_condition - for boundary_condition in boundary_condition_fixtures - if type(boundary_condition).__name__ == boundary_condition_class - ][0] - resampler = [ - resampler - for resampler in resampler_fixtures - if type(resampler).__name__ == resampler_class - ][0] - - assert boundary_condition is not None - assert resampler is not None - - # generate the work mapper given the type and the platform - - work_mapper_classes = { - mapper_class.__name__: mapper_class - for mapper_class in [Mapper, WorkerMapper, TaskMapper] - } - - # # select the right one given the option - # work_mapper_type = [mapper_type for mapper_type in work_mapper_classes - # if type(mapper_type).__name__ == work_mapper_class][0] - - # decide based on the platform and the work mapper which - # platform dependent components to build - if work_mapper_class == "Mapper": - # then there is no settings - work_mapper = Mapper() - - elif work_mapper_class == "WorkerMapper": - if platform == "CUDA" or platform == "OpenCL": - work_mapper = WorkerMapper( - num_workers=num_workers, - worker_type=OpenMMGPUWorker, - device_ids={"0": 0, "1": 1}, - proc_start_method="spawn", - ) - if platform == "OpenCL": - work_mapper = WorkerMapper( - num_workers=num_workers, - worker_type=OpenMMGPUWorker, - device_ids={"0": 0, "1": 1}, - ) - - elif platform == "CPU": - work_mapper = WorkerMapper( - num_workers=num_workers, - worker_type=OpenMMCPUWorker, - worker_attributes={"num_threads": 1}, - ) - - elif platform == "Reference": - work_mapper = WorkerMapper( - num_workers=num_workers, - worker_type=Worker, - ) - - elif work_mapper_class == "TaskMapper": - if platform == "CUDA": - work_mapper = TaskMapper( - num_workers=num_workers, - walker_task_type=OpenMMGPUWalkerTaskProcess, - device_ids={"0": 0, "1": 1}, - proc_start_method="spawn", - ) - - elif platform == "OpenCL": - work_mapper = TaskMapper( - num_workers=num_workers, - walker_task_type=OpenMMGPUWalkerTaskProcess, - device_ids={"0": 0, "1": 1}, - ) - - elif platform == "CPU": - work_mapper = TaskMapper( - num_workers=num_workers, - walker_task_type=OpenMMCPUWalkerTaskProcess, - worker_attributes={"num_threads": 1}, - ) - - elif platform == "Reference": - work_mapper = TaskMapper( - num_workers=num_workers, - worker_type=WalkerTaskProcess, - ) - - else: - raise ValueError("Platform {} not recognized".format(platform)) - - # initialize the runner with the platform - runner = OpenMMRunner( - lj_omm_sys.system, lj_omm_sys.topology, lj_integrator, platform=platform - ) - - logger.debug("Constructing the manager") - - manager = Manager( - walkers, - runner=runner, - boundary_conditions=boundary_condition, - resampler=resampler, - worker_mapper=work_mapper, - reporters=reporters, - ) - - # since different work mappers need different process start - # methods for different platforms i.e. CUDA and linux fork - # vs. spawn we choose the appropriate one for each method. - - logger.debug("Starting the simulation") - - walkers, filters = manager.run_simulation( - n_cycles, steps, num_workers=num_workers - ) - - # no assert if it runs we are happy for now diff --git a/tests/utils/README.org b/tests/utils/README.org deleted file mode 100644 index d284d43e..00000000 --- a/tests/utils/README.org +++ /dev/null @@ -1,3 +0,0 @@ -Put individual modules here that can be imported into tests. Choose -names that don't clash. Typically you can just add a 'my' to the -beginning of them. diff --git a/tests/utils/myutils.py b/tests/utils/myutils.py deleted file mode 100644 index f7164364..00000000 --- a/tests/utils/myutils.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Generic modules that help with running tests more smoothly.""" - -# Standard Library -import os -import os.path as osp -from contextlib import contextmanager - - -@contextmanager -def cd(newdir): - """Change directories use as a context manager.""" - prevdir = os.getcwd() - os.chdir(osp.expanduser(newdir)) - try: - yield - finally: - os.chdir(prevdir) diff --git a/uv.lock b/uv.lock index 89678769..6261c0b9 100644 --- a/uv.lock +++ b/uv.lock @@ -566,6 +566,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -2068,6 +2077,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, ] +[[package]] +name = "pytest-check" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/b9/9b1636f27a75fbc613037020a148f6410c6f973170fa2189ee0059ae6e5b/pytest_check-2.6.2.tar.gz", hash = "sha256:5e2128405b14978ad74f21f3225fcc52bbb1cc7c41a55d5eaad9f1e3cd27e42e", size = 27988, upload-time = "2025-11-29T02:51:15.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e2/26113dc55d59b2364e01afa25270eec29470dd52bf117ad857a3e64e688b/pytest_check-2.6.2-py3-none-any.whl", hash = "sha256:71762eb58f6b8a2d75afb9e6c8b800af7f634f67359d899a65ee48aaee087215", size = 16060, upload-time = "2025-11-29T02:51:14.521Z" }, +] + [[package]] name = "pytest-cov" version = "7.0.0" @@ -2082,6 +2103,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] +[[package]] +name = "pytest-datadir" +version = "1.3.1.dev9+g41e9a1e94" +source = { git = "https://github.com/salotz/pytest-datadir-extras.git?rev=41e9a1e94ba27efe28ce9d0019b1e0a73be34400#41e9a1e94ba27efe28ce9d0019b1e0a73be34400" } +dependencies = [ + { name = "pytest" }, +] + +[[package]] +name = "pytest-print" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/0d/7167bc3a83d6862241b367bce92119c338929a46408b4ca9555c15cb5b40/pytest_print-1.2.0.tar.gz", hash = "sha256:62514752d788206789d04dde2e69218d04138068377e5355d389fcc186a8e51e", size = 11898, upload-time = "2025-10-09T19:19:55.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/80/02c87f95140e3621dde9a599c0b5684464f16acc0786ab8c8c5e929330c2/pytest_print-1.2.0-py3-none-any.whl", hash = "sha256:12969d579e72d549b71a68cef8a2d10ec47afdbaf92e32c8777d5d93d5a1cd42", size = 7151, upload-time = "2025-10-09T19:19:54.924Z" }, +] + +[[package]] +name = "pytest-shutil" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, + { name = "six" }, + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/8d/5a60eeced74608fd2304dafc9e26815988714f8a2cfa72da99c8c3c85515/pytest-shutil-1.8.1.tar.gz", hash = "sha256:7dcc02e8a372098d51a98737e7f662e6edfd75cec7070a11e904141de49d866b", size = 37554, upload-time = "2024-11-29T19:34:17.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/17/1b161657385982134b723dcd463483d498e54e286c5632d42602407e606f/pytest_shutil-1.8.1-py3-none-any.whl", hash = "sha256:0793e347e07b9296d814ce33377ed348ce376ffac76c0e57b28bf84235499f51", size = 15948, upload-time = "2024-11-29T19:33:26.349Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2664,6 +2720,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, ] +[[package]] +name = "termcolor" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/56/ab275c2b56a5e2342568838f0d5e3e66a32354adcc159b495e374cda43f5/termcolor-3.2.0.tar.gz", hash = "sha256:610e6456feec42c4bcd28934a8c87a06c3fa28b01561d46aa09a9881b8622c58", size = 14423, upload-time = "2025-10-25T19:11:42.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/d5/141f53d7c1eb2a80e6d3e9a390228c3222c27705cbe7f048d3623053f3ca/termcolor-3.2.0-py3-none-any.whl", hash = "sha256:a10343879eba4da819353c55cb8049b0933890c2ebf9ad5d3ecd2bb32ea96ea6", size = 7698, upload-time = "2025-10-25T19:11:41.536Z" }, +] + [[package]] name = "terminado" version = "0.18.1" @@ -2849,7 +2914,11 @@ dev = [ { name = "notebook" }, { name = "pdbpp" }, { name = "pytest" }, + { name = "pytest-check" }, { name = "pytest-cov" }, + { name = "pytest-datadir" }, + { name = "pytest-print" }, + { name = "pytest-shutil" }, { name = "ruff" }, { name = "sphinx" }, { name = "sphinxcontrib-bibtex" }, @@ -2895,7 +2964,11 @@ dev = [ { name = "notebook" }, { name = "pdbpp" }, { name = "pytest" }, + { name = "pytest-check" }, { name = "pytest-cov" }, + { name = "pytest-datadir", git = "https://github.com/salotz/pytest-datadir-extras.git?rev=41e9a1e94ba27efe28ce9d0019b1e0a73be34400" }, + { name = "pytest-print" }, + { name = "pytest-shutil" }, { name = "ruff" }, { name = "sphinx" }, { name = "sphinxcontrib-bibtex" }, From 3a9f3fdd22c807bb48ffb07198a286de795ce459 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:48:15 -0500 Subject: [PATCH 031/143] initial implementation of proc pool mapper --- src/wepy/work_mapper/proc_pool_mapper.py | 56 +++++++++---- .../test_openmm/test_proc_pool_mapper.py | 84 +++++++++++++++++++ .../test_work_mapper/test_proc_pool_mapper.py | 36 ++++++-- 3 files changed, 150 insertions(+), 26 deletions(-) create mode 100644 tests/functional/test_openmm/test_proc_pool_mapper.py diff --git a/src/wepy/work_mapper/proc_pool_mapper.py b/src/wepy/work_mapper/proc_pool_mapper.py index b2f35d44..9767d959 100644 --- a/src/wepy/work_mapper/proc_pool_mapper.py +++ b/src/wepy/work_mapper/proc_pool_mapper.py @@ -11,16 +11,6 @@ class ProcPoolMapper: - num_workers: int - - def __init__( - self, - num_workers: int, - proc_start_method: Literal["fork", "spawn", "forkserver"] = "spawn", - ) -> None: - self.num_workers = num_workers - self.proc_start_method = proc_start_method - def init( self, segment_func: Callable[ @@ -31,14 +21,40 @@ def init( ], AnyWalkerState, ], + num_workers: int, + worker_args: list[dict[str, Any]] | None = None, + proc_start_method: Literal["fork", "spawn", "forkserver"] = "spawn", ) -> None: + """l.. + + worker_func_args: This is an arbitrary set of key-values that + for each worker will be passed into the segment_func call + if that worker is used. Useful for injecting things like + device IDs. + + """ logger.info("Initializing ProcPoolMapper") + self._num_workers = num_workers + self._proc_start_method = proc_start_method + + if worker_args is not None and len(worker_args) != num_workers: + raise ValueError("If worker_args are given they must match the number of workers.") + + elif worker_args is None: + logger.info("No worker arguments given.") + self._worker_args = [{} for _ in range(self._num_workers)] + + else: + logger.info(f"Configured workers with the following function arguments: {worker_args}") + self._worker_args = worker_args + + self._func = segment_func - logger.info(f"Initializing local multiprocessing context with start method: {self.proc_start_method}") - self._mp_ctx = mp.get_context(method=self.proc_start_method) + logger.info(f"Initializing local multiprocessing context with start method: {self._proc_start_method}") + self._mp_ctx = mp.get_context(method=self._proc_start_method) def cleanup(self) -> None: @@ -51,12 +67,12 @@ def map( *args: list[list[Any]], ) -> list[AnyWalkerState]: - logger.info(f"Running map on {len(walker_states)} in batches of {self.num_workers}") + logger.info(f"Running map on {len(walker_states)} in batches of {self._num_workers}") # spin up a new pool for each map - logger.info(f"Starting process Pool with {self.num_workers}") + logger.info(f"Starting process Pool with {self._num_workers}") with self._mp_ctx.Pool( - processes=self.num_workers, + processes=self._num_workers, # only run one thing per task, just to make sure # everything is cleaned up maxtasksperchild=1, @@ -65,7 +81,7 @@ def map( results = [] for batch_idx, batch in enumerate(itertools.batched( zip(walker_states, *args, strict=True), - self.num_workers, + self._num_workers, strict=False, )): @@ -75,11 +91,15 @@ def map( for batch_task_idx, batch_args in enumerate(batch): task_idx = batch_idx + batch_task_idx + # for our purposes each element in this batch + # should be associated with a worker. + worker_idx = batch_task_idx - logger.info(f"Submitting task {task_idx}") + logger.info(f"Submitting task {task_idx} to worker {worker_idx}") result = pool.apply_async( self._func, - batch_args, + args=batch_args, + kwds=self._worker_args[worker_idx], ) logger.info(f"Task {task_idx} submitted") batch_results.append(result) diff --git a/tests/functional/test_openmm/test_proc_pool_mapper.py b/tests/functional/test_openmm/test_proc_pool_mapper.py new file mode 100644 index 00000000..b884e70f --- /dev/null +++ b/tests/functional/test_openmm/test_proc_pool_mapper.py @@ -0,0 +1,84 @@ +import functools +import openmm +import numpy as np +from wepy.runners.openmm import OpenMMRunner, OpenMMState, gen_sim_state +from wepy.work_mapper.proc_pool_mapper import ProcPoolMapper + +from wepy_tools.systems.lennard_jones import LennardJonesPair + + +def test_proc_pool_mapper(): + + lj_sys = LennardJonesPair() + integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + + + runner = OpenMMRunner( + system=lj_sys.system, + topology=lj_sys.topology, + integrator=integrator, + ) + + num_walkers = 4 + + walker_states = [ + OpenMMState(gen_sim_state( + lj_sys.positions, + system=lj_sys.system, + integrator=integrator, + )) + for _ + in range(num_walkers) + ] + + run_func = functools.partial( + runner.run_segment, + platform="Reference", + ) + + poolmapper = ProcPoolMapper() + poolmapper.init(run_func, num_workers=2) + + results = poolmapper.map( + walker_states, + [10 for _ in range(num_walkers)], + ) + assert len(results) == 4 + + # try different platform with some global platform settings + run_func = functools.partial( + runner.run_segment, + platform="CPU", + platform_kwargs={"Threads" : "1"}, + ) + + poolmapper = ProcPoolMapper() + poolmapper.init(run_func, num_workers=2) + + poolmapper.map( + walker_states, + [10 for _ in range(num_walkers)], + ) + + # Different platform args per "worker" + + run_func = functools.partial( + runner.run_segment, + platform="CPU", + ) + + poolmapper = ProcPoolMapper() + poolmapper.init( + run_func, + num_workers=2, + worker_args=[ + {"platform_kwargs" : {"Threads" : "2"}}, + {"platform_kwargs" : {"Threads" : "3"}}, + ] + ) + + poolmapper.map( + walker_states, + [10 for _ in range(num_walkers)], + ) + diff --git a/tests/unit/test_work_mapper/test_proc_pool_mapper.py b/tests/unit/test_work_mapper/test_proc_pool_mapper.py index 266c126e..32fba3a2 100644 --- a/tests/unit/test_work_mapper/test_proc_pool_mapper.py +++ b/tests/unit/test_work_mapper/test_proc_pool_mapper.py @@ -24,9 +24,9 @@ def test_rizz_walker(): def test_ProcPoolMapper(): - poolmapper = ProcPoolMapper(num_workers=1) + poolmapper = ProcPoolMapper() - poolmapper.init(rizz_run) + poolmapper.init(rizz_run, num_workers=1) results = poolmapper.map( [ @@ -45,9 +45,9 @@ def test_ProcPoolMapper(): ] - poolmapper = ProcPoolMapper(num_workers=2) + poolmapper = ProcPoolMapper() - poolmapper.init(rizz_run) + poolmapper.init(rizz_run, num_workers=2) results = poolmapper.map( [ @@ -65,9 +65,9 @@ def test_ProcPoolMapper(): RizzWalkerState(8), ] - poolmapper = ProcPoolMapper(num_workers=3) + poolmapper = ProcPoolMapper() - poolmapper.init(rizz_run) + poolmapper.init(rizz_run, num_workers=3) results = poolmapper.map( [ @@ -85,9 +85,9 @@ def test_ProcPoolMapper(): RizzWalkerState(8), ] - poolmapper = ProcPoolMapper(num_workers=3, proc_start_method="fork") + poolmapper = ProcPoolMapper() - poolmapper.init(rizz_run) + poolmapper.init(rizz_run, num_workers=3, proc_start_method="fork") results = poolmapper.map( [ @@ -104,3 +104,23 @@ def test_ProcPoolMapper(): RizzWalkerState(6), RizzWalkerState(8), ] + + # use worker args to fill in one of the args + poolmapper = ProcPoolMapper() + + poolmapper.init( + rizz_run, + num_workers=2, + worker_args=[ + {"multiple" : 2}, + {"multiple" : 3}, + ]) + + results = poolmapper.map( + [ + RizzWalkerState(1), + RizzWalkerState(1), + RizzWalkerState(1), + ], + [1, 1, 1], + ) From 09ad9b70d7454c9080c66bd7be4ca9c814ef387b Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 10:50:23 -0500 Subject: [PATCH 032/143] move openmm integration tests --- tests/integration/test_openmm/conftest.py | 0 .../test_openmm/test_proc_pool_mapper.py | 0 .../{functional => integration}/test_openmm/test_serial_mapper.py | 0 tests/{functional => integration}/test_openmm/test_sim_manager.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/integration/test_openmm/conftest.py rename tests/{functional => integration}/test_openmm/test_proc_pool_mapper.py (100%) rename tests/{functional => integration}/test_openmm/test_serial_mapper.py (100%) rename tests/{functional => integration}/test_openmm/test_sim_manager.py (100%) diff --git a/tests/integration/test_openmm/conftest.py b/tests/integration/test_openmm/conftest.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/functional/test_openmm/test_proc_pool_mapper.py b/tests/integration/test_openmm/test_proc_pool_mapper.py similarity index 100% rename from tests/functional/test_openmm/test_proc_pool_mapper.py rename to tests/integration/test_openmm/test_proc_pool_mapper.py diff --git a/tests/functional/test_openmm/test_serial_mapper.py b/tests/integration/test_openmm/test_serial_mapper.py similarity index 100% rename from tests/functional/test_openmm/test_serial_mapper.py rename to tests/integration/test_openmm/test_serial_mapper.py diff --git a/tests/functional/test_openmm/test_sim_manager.py b/tests/integration/test_openmm/test_sim_manager.py similarity index 100% rename from tests/functional/test_openmm/test_sim_manager.py rename to tests/integration/test_openmm/test_sim_manager.py From 23a00c572adfe43cb877f555bcf596c06bddab57 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 11:51:32 -0500 Subject: [PATCH 033/143] remove pytest_wepy module --- src/pytest_wepy/__init__.py | 7 - src/pytest_wepy/lennard_jones_pair.py | 610 -------------------------- src/pytest_wepy/openmm.py | 0 src/pytest_wepy/test_hdf5.py | 215 --------- src/pytest_wepy/test_hdf5_analysis.py | 199 --------- 5 files changed, 1031 deletions(-) delete mode 100644 src/pytest_wepy/__init__.py delete mode 100644 src/pytest_wepy/lennard_jones_pair.py delete mode 100644 src/pytest_wepy/openmm.py delete mode 100644 src/pytest_wepy/test_hdf5.py delete mode 100644 src/pytest_wepy/test_hdf5_analysis.py diff --git a/src/pytest_wepy/__init__.py b/src/pytest_wepy/__init__.py deleted file mode 100644 index a349c178..00000000 --- a/src/pytest_wepy/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Third Party Library -import pytest - - -@pytest.fixture(scope="class") -def test_wepy_fixture(): - return "Hello" diff --git a/src/pytest_wepy/lennard_jones_pair.py b/src/pytest_wepy/lennard_jones_pair.py deleted file mode 100644 index 6923e574..00000000 --- a/src/pytest_wepy/lennard_jones_pair.py +++ /dev/null @@ -1,610 +0,0 @@ -# Third Party Library -import openmm as omm -import pytest -from openmm_systems.test_systems import LennardJonesPair - -# First Party Library -from wepy.runners.openmm import ( - OpenMMRunner, - gen_walker_state, -) -from wepy_tools.sim_makers.openmm.sim_maker import OpenMMSimMaker - -### Constants - -# only use the reference platform for python-only integration testing -# purposes -PLATFORM = "Reference" - - -### Sanity Test -@pytest.fixture(scope="class") -def lj_sanity_test(): - """Sanity test to make sure we even have the plugin fixtures installed.""" - return "sanity" - - -### Fixtures - - -## OpenMM Misc. - - -@pytest.fixture(scope="class") -def lj_omm_sys(): - return LennardJonesPair() - - -@pytest.fixture(scope="class") -def langevin_integrator(): - integrator = omm.LangevinIntegrator( - *OpenMMSimMaker.DEFAULT_INTEGRATOR_PARAMS["LangevinIntegrator"] - ) - - return integrator - - -integrators = [ - langevin_integrator, -] - - -@pytest.fixture( - scope="class", - params=[ - "LangevinIntegrator", - ], -) -def lj_integrator( - request, - *integrators, -): - intgr_spec = request.param - if intgr_spec == "LangevinIntegrator": - return langevin_integrator - else: - raise ValueError("Unkown integrator") - - -## Runner - - -@pytest.fixture( - scope="class", - params=[ - "Reference", - ], -) -def lj_openmm_runner(request, lj_omm_sys, lj_integrator): - # parametrize the platform - platform = request.param - - positions = test_sys.positions.value_in_unit(test_sys.positions.unit) - - init_state = gen_walker_state(positions, test_sys.system, integrator) - - # initialize the runner - runner = OpenMMRunner( - lj_omm_sys.system, lj_omm_sys.topology, lj_integrator, platform=platform - ) - - return runner - - -## Resampler - -# @pytest.fixture(scope='class') -# def lj_distance_metric(): -# return PairDistance() - - -# @pytest.fixture(scope='class') -# def lj_wexplore_resampler(lj_params, lj_distance_metric, lj_init_state): -# resampler = WExploreResampler(distance=lj_distance_metric, -# init_state=lj_init_state, -# max_region_sizes=lj_params['max_region_sizes'], -# max_n_regions=lj_params['max_n_regions'], -# pmin=lj_params['pmin'], pmax=lj_params['pmax']) - -# return resampler - -# @pytest.fixture(scope='class') -# def lj_revo_resampler(lj_params, lj_distance_metric, lj_init_state): -# resampler = REVOResampler(distance=lj_distance_metric, -# merge_dist=2.5, -# char_dist=1.0, -# init_state=lj_init_state, -# pmin=lj_params['pmin'], pmax=lj_params['pmax']) - -# return resampler - -# @pytest.fixture(scope='class') -# def lj_topology(lj_omm_sys): - -# # the mdtraj here is needed for the distance function -# mdtraj_topology = mdj.Topology.from_openmm(lj_omm_sys.topology) - -# ## Reporters if we want them -# json_str_top = mdtraj_to_json_topology(mdtraj_topology) - -# return json_str_top - - -# @pytest.fixture(scope='class') -# def lj_unbinding_bc(lj_params, lj_init_state, lj_topology, lj_omm_sys): - -# # initialize the unbinding boundary condition -# ubc = UnbindingBC(cutoff_distance=lj_params['cutoff_distance'], -# initial_state=lj_init_state, -# topology=lj_topology, -# ligand_idxs=np.array(lj_omm_sys.ligand_indices), -# receptor_idxs=np.array(lj_omm_sys.receptor_indices)) - -# return ubc - -# @pytest.fixture(scope='class') -# def lj_reporter_kwargs(lj_params, lj_topology, lj_wexplore_resampler, lj_unbinding_bc): -# """Reporters that work for all of the components.""" - -# # make a dictionary of units for adding to the HDF5 -# units = dict(UNIT_NAMES) - -# hdf5_reporter_kwargs = {'save_fields' : lj_params['save_fields'], -# 'resampler' : lj_wexplore_resampler, -# 'boundary_conditions' : lj_unbinding_bc, -# 'topology' : lj_topology, -# 'units' : units, -# } - -# dashboard_reporter_kwargs = {'step_time' : lj_params['step_size'].value_in_unit(unit.second), -# 'bc_cutoff_distance' : lj_unbinding_bc.cutoff_distance} - -# # Resampling Tree -# restree_reporter_kwargs = {'resampler' : lj_wexplore_resampler, -# 'boundary_condition' : lj_unbinding_bc, -# 'node_radius' : 3.0, -# 'row_spacing' : 5.0, -# 'step_spacing' : 20.0, -# 'progress_key' : 'min_distances', -# 'max_progress_value' : lj_unbinding_bc.cutoff_distance, -# 'colormap_name' : 'plasma'} - - -# reporter_kwargs = [hdf5_reporter_kwargs, dashboard_reporter_kwargs, -# restree_reporter_kwargs] - -# return reporter_kwargs - - -# @pytest.fixture(scope='class') -# def lj_reporter_classes(): -# reporter_classes = [WepyHDF5Reporter, DashboardReporter, -# ResTreeReporter] - -# return reporter_classes - -# @pytest.fixture(scope='class') -# def lj_init_walkers(lj_params, lj_init_sim_state): -# init_weight = 1.0 / lj_params['n_walkers'] -# init_walkers = [OpenMMWalker(OpenMMState(lj_init_sim_state), init_weight) -# for i in range(lj_params['n_walkers'])] - -# return init_walkers - - -# @pytest.fixture(scope='class') -# def lj_apparatus(lj_openmm_runner, lj_wexplore_resampler, lj_unbinding_bc): - -# sim_apparatus = WepySimApparatus(lj_openmm_runner, resampler=lj_wexplore_resampler, -# boundary_conditions=lj_unbinding_bc) - -# return sim_apparatus - -# @pytest.fixture(scope='class') -# def lj_null_apparatus(lj_openmm_runner): - -# sim_apparatus = WepySimApparatus(lj_openmm_runner, resampler=NoResampler()) - -# return sim_apparatus - -# @pytest.fixture(scope='class') -# def lj_snapshot(lj_init_walkers, lj_apparatus): - -# return SimSnapshot(lj_init_walkers, lj_apparatus) - - -# @pytest.fixture(scope='class') -# def lj_configuration(tmp_path_factory, lj_reporter_classes, lj_reporter_kwargs): - -# # make a temporary directory for this configuration to work with -# tmpdir = str(tmp_path_factory.mktemp('lj_fixture')) -# # tmpdir = tmp_path_factory.mktemp('lj_fixture/work_dir') - -# configuration = Configuration(work_dir=tmpdir, -# reporter_classes=lj_reporter_classes, -# reporter_partial_kwargs=lj_reporter_kwargs) - -# return configuration - -# @pytest.fixture(scope='class') -# def lj_null_configuration(tmp_path_factory, lj_reporter_classes, lj_reporter_kwargs, -# lj_params, lj_wexplore_resampler, lj_topology): - -# reporter_classes = [WepyHDF5Reporter] - -# # make a dictionary of units for adding to the HDF5 -# units = dict(UNIT_NAMES) - -# hdf5_reporter_kwargs = {'save_fields' : lj_params['save_fields'], -# 'resampler' : lj_wexplore_resampler, -# 'topology' : lj_topology, -# 'units' : units, -# } - -# reporter_kwargs = [hdf5_reporter_kwargs] - - -# # make a temporary directory for this configuration to work with -# tmpdir = str(tmp_path_factory.mktemp('lj_fixture')) -# # tmpdir = tmp_path_factory.mktemp('lj_fixture/work_dir') - -# configuration = Configuration(work_dir=tmpdir, -# reporter_classes=reporter_classes, -# reporter_partial_kwargs=reporter_kwargs) - -# return configuration - -# @pytest.fixture(scope='class') -# def lj_inmem_configuration(tmp_path_factory): - -# # make a temporary directory for this configuration to work with -# tmpdir = str(tmp_path_factory.mktemp('lj_fixture')) -# # tmpdir = tmp_path_factory.mktemp('lj_fixture/work_dir') - -# configuration = Configuration(work_dir=tmpdir) - -# return configuration - - -# @pytest.fixture(scope='class') -# def lj_work_mapper(lj_configuration): - -# work_mapper = Mapper() - -# return work_mapper - - -# @pytest.fixture(scope='class') -# def lj_work_mapper_worker(): - -# work_mapper = WorkerMapper(num_workers=1) - -# return work_mapper - -# @pytest.fixture(scope='class') -# def lj_work_mapper_task(): - -# work_mapper = TaskMapper(num_workers=1) - -# return work_mapper - - -# @pytest.fixture(scope='class') -# def lj_reporters(tmp_path_factory, lj_reporter_classes, lj_reporter_kwargs): - - -# # make a temporary directory for this configuration to work with -# tmpdir = str(tmp_path_factory.mktemp('lj_fixture')) - -# # make a config so that the reporters get parametrized properly -# config = Configuration(work_dir=tmpdir, -# reporter_classes=lj_reporter_classes, -# reporter_partial_kwargs=lj_reporter_kwargs) - -# return config.reporters - - -# @pytest.fixture(scope='class') -# def lj_orchestrator(lj_apparatus, lj_init_walkers, lj_configuration): - -# # use an in memory database with sqlite - -# # make a path to the temporary directory for this orchestrator -# # orch_path = tmp_path_factory.mktemp('lj_fixture/lj.orch.sqlite') - -# # then create the seed/root/master orchestrator which will be used -# # from here on out -# orch = Orchestrator() - -# return orch - -# @pytest.fixture(scope='class') -# def lj_orchestrator_defaults(lj_orchestrator, -# lj_apparatus, lj_init_walkers, lj_configuration): - - -# lj_orchestrator.set_default_sim_apparatus(lj_apparatus) -# lj_orchestrator.set_default_init_walkers(lj_init_walkers) -# lj_orchestrator.set_default_configuration(lj_configuration) - -# lj_orchestrator.gen_default_snapshot() - - -# return lj_orchestrator - -# @pytest.fixture(scope='class') -# def lj_orchestrator_defaults_inmem(lj_orchestrator, -# lj_apparatus, lj_init_walkers, lj_inmem_configuration): - - -# lj_orchestrator.set_default_sim_apparatus(lj_apparatus) -# lj_orchestrator.set_default_init_walkers(lj_init_walkers) -# lj_orchestrator.set_default_configuration(lj_configuration) - -# lj_orchestrator.gen_default_snapshot() - - -# return lj_orchestrator - -# @pytest.fixture(scope='class') -# def lj_orchestrator_defaults_null(lj_orchestrator, -# lj_null_apparatus, lj_init_walkers, -# lj_null_configuration): - -# lj_orchestrator.set_default_sim_apparatus(lj_null_apparatus) -# lj_orchestrator.set_default_init_walkers(lj_init_walkers) -# lj_orchestrator.set_default_configuration(lj_null_configuration) - -# lj_orchestrator.gen_default_snapshot() - - -# return lj_orchestrator - - -# @pytest.fixture(scope='class') -# def lj_orchestrator_file(tmp_path_factory, lj_apparatus, lj_init_walkers, lj_configuration): - -# # use an in memory database with sqlite - -# # make a path to the temporary directory for this orchestrator -# orch_path = str(tmp_path_factory.mktemp('lj_fixture') / "lj.orch.sqlite") - -# # then create the seed/root/master orchestrator which will be used -# # from here on out -# orch = Orchestrator(orch_path) - -# return orch - -# @pytest.fixture(scope='class') -# def lj_orchestrator_file_other(tmp_path_factory, -# lj_apparatus, lj_init_walkers, lj_configuration): - -# # use an in memory database with sqlite - -# # make a path to the temporary directory for this orchestrator -# orch_path = str(tmp_path_factory.mktemp('lj_fixture') / "lj_other.orch.sqlite") - -# # then create the seed/root/master orchestrator which will be used -# # from here on out -# orch = Orchestrator(orch_path) - -# return orch - -# @pytest.fixture(scope='class') -# def lj_orchestrator_defaults_file(lj_orchestrator_file, -# lj_apparatus, lj_init_walkers, lj_configuration): - - -# lj_orchestrator_file.set_default_sim_apparatus(lj_apparatus) -# lj_orchestrator_file.set_default_init_walkers(lj_init_walkers) -# lj_orchestrator_file.set_default_configuration(lj_configuration) - -# lj_orchestrator_file.gen_default_snapshot() - - -# return lj_orchestrator_file - -# @pytest.fixture(scope='class') -# def lj_orchestrator_defaults_file_other(lj_orchestrator_file_other, -# lj_apparatus, lj_init_walkers, lj_configuration): - -# lj_orchestrator_file_other.set_default_sim_apparatus(lj_apparatus) -# lj_orchestrator_file_other.set_default_init_walkers(lj_init_walkers) -# lj_orchestrator_file_other.set_default_configuration(lj_configuration) - -# lj_orchestrator_file_other.gen_default_snapshot() - - -# return lj_orchestrator_file_other - - -# @pytest.fixture(scope='class') -# def lj_sim_manager(tmp_path_factory, lj_orchestrator_defaults): - -# start_snapshot = lj_orchestrator_defaults.get_default_snapshot() -# configuration = lj_orchestrator_defaults.get_default_configuration() - -# # make a new temp dir for this configuration -# tempdir = str(tmp_path_factory.mktemp('lj_sim_manager')) -# configuration = configuration.reparametrize(work_dir=tempdir) - -# sim_manager = lj_orchestrator_defaults.gen_sim_manager(start_snapshot, -# configuration=configuration) - -# return sim_manager - - -# @pytest.fixture(scope='class') -# def lj_sim_manager_inmem(tmp_path_factory, lj_orchestrator_defaults_inmem): - -# start_snapshot = lj_orchestrator_defaults_inmem.get_default_snapshot() -# configuration = lj_orchestrator_defaults_inmem.get_default_configuration() - -# # make a new temp dir for this configuration -# tempdir = str(tmp_path_factory.mktemp('lj_sim_manager')) -# configuration = configuration.reparametrize(work_dir=tempdir) - -# sim_manager = lj_orchestrator_defaults.gen_sim_manager(start_snapshot, -# configuration=configuration) - -# return sim_manager - -# @pytest.fixture(scope='class') -# def lj_sim_manager_null(tmp_path_factory, lj_orchestrator_defaults_null): - -# start_snapshot = lj_orchestrator_defaults_null.get_default_snapshot() -# configuration = lj_orchestrator_defaults_null.get_default_configuration() - -# # make a new temp dir for this configuration -# tempdir = str(tmp_path_factory.mktemp('lj_sim_manager')) -# configuration = configuration.reparametrize(work_dir=tempdir) - -# sim_manager = lj_orchestrator_defaults_null.gen_sim_manager(start_snapshot, -# configuration=configuration) - -# return sim_manager - - -# @pytest.fixture(scope='class') -# def lj_sim_manager_run_results(lj_sim_manager): - -# n_cycles = 10 -# n_steps = 100 - -# steps = [n_steps for _ in range(n_cycles)] - -# return lj_sim_manager.run_simulation(n_cycles, steps) - -# @pytest.fixture(scope='class') -# def lj_sim_manager_null_run_results(lj_sim_manager_null): - -# n_cycles = 10 -# n_steps = 100 - -# steps = [n_steps for _ in range(n_cycles)] - -# return lj_sim_manager_null.run_simulation(n_cycles, steps) - -# @pytest.fixture(scope='class') -# def lj_orch_run_by_time_results(tmp_path_factory, lj_orchestrator_defaults): - -# runtime = 20 # seconds -# n_steps = 100 - -# start_snaphash = lj_orchestrator_defaults.get_default_snapshot_hash() - -# # make a new temp dir for this configuration -# configuration = lj_orchestrator_defaults.get_default_configuration() -# tempdir = str(tmp_path_factory.mktemp('lj_sim_manager')) -# configuration = configuration.reparametrize(work_dir=tempdir) - - -# return lj_orchestrator_defaults.run_snapshot_by_time(start_snaphash, -# runtime, n_steps, -# configuration=configuration) - -# @pytest.fixture(scope='class') -# def lj_orch_run_end_snapshot(lj_orch_run_by_time_results): - -# end_snapshot, _, _, _ = lj_orch_run_by_time_results - -# return end_snapshot - -# @pytest.fixture(scope='class') -# def lj_orch_orchestrated_run(tmp_path_factory, lj_orchestrator_defaults): - -# run_time = 20 # seconds -# n_steps = 100 - -# start_snaphash = lj_orchestrator_defaults.get_default_snapshot_hash() - -# tempdir = str(tmp_path_factory.mktemp('orchestrate_run')) - -# run_orch = lj_orchestrator_defaults.orchestrate_snapshot_run_by_time(start_snaphash, -# run_time, n_steps, -# work_dir=tempdir) - -# return run_orch - -# @pytest.fixture(scope='class') -# def lj_orch_file_orchestrated_run(tmp_path_factory, lj_orchestrator_defaults_file): - -# run_time = 20 # seconds -# n_steps = 100 - -# start_snaphash = lj_orchestrator_defaults_file.get_default_snapshot_hash() - -# tempdir = str(tmp_path_factory.mktemp('orchestrate_run')) - -# run_orch = lj_orchestrator_defaults_file.orchestrate_snapshot_run_by_time(start_snaphash, -# run_time, n_steps, -# work_dir=tempdir) - -# return run_orch - -# @pytest.fixture(scope='class') -# def lj_orch_file_other_orchestrated_run(tmp_path_factory, -# lj_orchestrator_defaults_file_other): - -# run_time = 20 # seconds -# n_steps = 100 - -# start_snaphash = lj_orchestrator_defaults_file_other.get_default_snapshot_hash() - -# tempdir = str(tmp_path_factory.mktemp('orchestrate_run_other')) - -# run_orch = lj_orchestrator_defaults_file_other.orchestrate_snapshot_run_by_time(start_snaphash, -# run_time, n_steps, -# work_dir=tempdir) - -# return run_orch - - -# @pytest.fixture(scope='class') -# def lj_orch_reconciled_orchs(tmp_path_factory, lj_apparatus, lj_init_walkers, lj_configuration): - -# run_time = 20 # seconds -# n_steps = 100 - -# # tempdirs for the orchestrators and configuration output -# first_tempdir = str(tmp_path_factory.mktemp('reconcile_first_run')) -# second_tempdir = str(tmp_path_factory.mktemp('reconcile_second_run')) - -# first_orch_path = osp.join(first_tempdir, "first.orch.sqlite") -# second_orch_path = osp.join(second_tempdir, "second.orch.sqlite") - -# # make two orchestrators in their directories -# first_orch = Orchestrator(orch_path=first_orch_path) -# second_orch = Orchestrator(orch_path=second_orch_path) - -# # configure them -# # 1 -# first_orch.set_default_sim_apparatus(lj_apparatus) -# first_orch.set_default_init_walkers(lj_init_walkers) -# first_orch.set_default_configuration(lj_configuration) -# first_orch.gen_default_snapshot() -# # 2 -# second_orch.set_default_sim_apparatus(lj_apparatus) -# second_orch.set_default_init_walkers(lj_init_walkers) -# second_orch.set_default_configuration(lj_configuration) -# second_orch.gen_default_snapshot() - -# # do independent runs for each of them - -# # start snapshot hashes -# first_starthash = first_orch.get_default_snapshot_hash() -# second_starthash = second_orch.get_default_snapshot_hash() - -# # then orchestrate the runs -# first_run_orch = first_orch.orchestrate_snapshot_run_by_time(first_starthash, -# run_time, n_steps, -# work_dir=first_tempdir) - -# second_run_orch = second_orch.orchestrate_snapshot_run_by_time(second_starthash, -# run_time, n_steps, -# work_dir=second_tempdir) - -# # then reconcile them -# reconciled_orch = reconcile_orchestrators(first_run_orch.orch_path, second_run_orch.orch_path) - -# return first_run_orch, second_run_orch, reconciled_orch diff --git a/src/pytest_wepy/openmm.py b/src/pytest_wepy/openmm.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pytest_wepy/test_hdf5.py b/src/pytest_wepy/test_hdf5.py deleted file mode 100644 index 0e7bd452..00000000 --- a/src/pytest_wepy/test_hdf5.py +++ /dev/null @@ -1,215 +0,0 @@ -# Testing hdf5 functionality -# -# 1) writing to HDF5 during simulation (boundary conditions, resampling) -# 2) reading in HDF5 (tests on sensible data) -# 3) compute observable -# 4) get traces - -# Standard Library -import os - -# Third Party Library -import mdtraj as mdj -import numpy as np - -# First Party Library -from wepy.boundary_conditions.boundary import NoBC -from wepy.boundary_conditions.randomwalk import RandomWalkBC -from wepy.hdf5 import WepyHDF5 -from wepy.reporter.hdf5 import WepyHDF5Reporter -from wepy.resampling.distances.randomwalk import RandomWalkDistance -from wepy.resampling.resamplers.resampler import NoResampler -from wepy.resampling.resamplers.revo import REVOResampler -from wepy.runners.randomwalk import UNIT_NAMES, RandomWalkRunner -from wepy.sim_manager import Manager -from wepy.util.mdtraj import mdtraj_to_json_topology -from wepy.walker import Walker, WalkerState -from wepy.work_mapper.mapper import Mapper - -num_walkers = 20 -hdf5_filename = "test.h5" -segment_length = 1 -threshold = 5 - - -def generate_topology(N): - """Creates an N-atom, dummy trajectory and topology for - the randomwalk system using the mdtraj package. Then creates a - JSON format for the topology. This JSON string is used in making - the WepyHDF5 reporter. - - Returns - ------- - topology: str - JSON string representing the topology of system being simulated. - """ - data = [] - top = mdj.Topology() - c = top.add_chain() - r = top.add_residue("test", c) - - for i in range(N): - at = top.add_atom(f"a{i}", mdj.element.argon, r, i) - - json_top_str = mdtraj_to_json_topology(top) - return json_top_str - - -def test_WriteReadH5(): - cleanup = True - - # 1D random walk - positions = np.zeros((1, 1)) - - init_state = WalkerState(positions=positions, time=0.0) - - # create list of init_walkers - initial_weight = 1 / num_walkers - init_walkers = [] - - init_walkers = [Walker(init_state, initial_weight) for i in range(num_walkers)] - - # set up runner for system - runner = RandomWalkRunner(probability=0.5) - - units = dict(UNIT_NAMES) - # instantiate a revo resampler and unbindingboundarycondition - - rw_distance = RandomWalkDistance() - - resampler = REVOResampler( - merge_dist=100, - char_dist=0.1, - distance=rw_distance, - init_state=init_state, - weights=True, - pmax=0.5, - dist_exponent=4, - ) - - json_top = generate_topology(1) - - rw_bc = RandomWalkBC(threshold=threshold, initial_states=[init_state]) - - hdf5_reporter = WepyHDF5Reporter( - file_path=hdf5_filename, - mode="w", - save_fields=["positions"], - boundary_conditions=rw_bc, - topology=json_top, - resampler=resampler, - n_dims=1, - ) - - sim_manager = Manager( - init_walkers, - runner=runner, - resampler=resampler, - boundary_conditions=rw_bc, - work_mapper=Mapper(), - reporters=[hdf5_reporter], - ) - - n_cycles = 20 - steps = [segment_length for i in range(n_cycles)] - - sim_manager.run_simulation(n_cycles, steps) - - # ------ - # data collected! now analyze - # ------ - - we = WepyHDF5(hdf5_filename, mode="r") - - initial_trace = [(i, 0) for i in range(num_walkers)] - final_trace = [(i, n_cycles - 1) for i in range(num_walkers)] - with we: - initial_pos = we.get_run_trace_fields(0, initial_trace, ["positions"]) - final_pos = we.get_run_trace_fields(0, final_trace, ["positions"]) - - # test that you haven't moved more than segment_length positions in the first cycle - assert initial_pos["positions"].max() <= segment_length - assert initial_pos["positions"].min() >= 0 - - # test that some of the trajectories have moved in the final pos - assert final_pos["positions"].max() >= 0 - - # test that no positions are further than the boundary condition - assert final_pos["positions"].max() <= int(threshold) - assert final_pos["positions"].min() >= 0 - - if cleanup: - os.remove(hdf5_filename) - - -def makeRandomWalkH5(h5name, resampling=True, warping=True): - # 1D random walk - positions = np.zeros((1, 1)) - - init_state = WalkerState(positions=positions, time=0.0) - - # create list of init_walkers - initial_weight = 1 / num_walkers - init_walkers = [] - - init_walkers = [Walker(init_state, initial_weight) for i in range(num_walkers)] - - # set up runner for system - runner = RandomWalkRunner(probability=0.5) - - units = dict(UNIT_NAMES) - # instantiate a revo resampler and unbindingboundarycondition - - rw_distance = RandomWalkDistance() - - if resampling: - resampler = REVOResampler( - merge_dist=100, - char_dist=0.1, - distance=rw_distance, - init_state=init_state, - weights=True, - pmax=0.5, - dist_exponent=4, - ) - else: - resampler = NoResampler() - - json_top = generate_topology(1) - - if warping: - rw_bc = RandomWalkBC(threshold=threshold, initial_states=[init_state]) - else: - rw_bc = NoBC() - - hdf5_reporter = WepyHDF5Reporter( - file_path=h5name, - mode="w", - save_fields=["positions"], - boundary_conditions=rw_bc, - topology=json_top, - resampler=resampler, - n_dims=1, - ) - - sim_manager = Manager( - init_walkers, - runner=runner, - resampler=resampler, - boundary_conditions=rw_bc, - work_mapper=Mapper(), - reporters=[hdf5_reporter], - ) - - n_cycles = 20 - steps = [segment_length for i in range(n_cycles)] - - sim_manager.run_simulation(n_cycles, steps) - - -if __name__ == "__main__": - makeRandomWalkH5("test_data/rw.h5", resampling=True, warping=True) - makeRandomWalkH5("test_data/rw_noresampling.h5", resampling=False, warping=True) - makeRandomWalkH5("test_data/rw_nowarping.h5", resampling=True, warping=False) - - print("HDF5s written") diff --git a/src/pytest_wepy/test_hdf5_analysis.py b/src/pytest_wepy/test_hdf5_analysis.py deleted file mode 100644 index 381a0674..00000000 --- a/src/pytest_wepy/test_hdf5_analysis.py +++ /dev/null @@ -1,199 +0,0 @@ -# Testing hdf5 functionality -# -# 1) testing if warping events are written correctly -# 2) reading in HDF5 (tests on sensible data) -# 3) compute observable -# 4) get traces - -# Standard Library - -# Third Party Library -import numpy as np - -# First Party Library -from wepy.analysis.contig_tree import ContigTree -from wepy.boundary_conditions.randomwalk import RandomWalkBC -from wepy.hdf5 import WepyHDF5 -from wepy.resampling.decisions.clone_merge import MultiCloneMergeDecision - -hdf5_filename = "test_data/rw.h5" -hdf5_filename_nores = "test_data/rw_noresampling.h5" -hdf5_filename_nowarp = "test_data/rw_nowarping.h5" -segment_length = 1 -boundary_position = 5 - - -def test_H5_warping(): - # ------------------------------------------------------------------ - # test_data/rw_noresampling.h5 holds 1D random walk data (generated by test_hdf5.py) - # includes warping, but no resampling - # ------ - we = WepyHDF5(hdf5_filename_nores, mode="r") - with we: - n_cycles = we.num_run_cycles(0) - wr_list = we.warping_records([0]) - warp_trace = [(wr.walker_idx, wr.cycle_idx) for wr in wr_list] - warp_pos = we.get_run_trace_fields(0, warp_trace, ["positions"]) - - # all of the warp positions should be at x = boundary_position - if len(warp_pos["positions"]) > 0: - assert warp_pos["positions"].max() == boundary_position - assert warp_pos["positions"].min() == boundary_position - - # make sure that the next cycle they get warped back to zero - warp_next_trace = [ - (wr.walker_idx, wr.cycle_idx + 1) - for wr in wr_list - if wr.cycle_idx + 1 < n_cycles - ] - with we: - warp_next_pos = we.get_run_trace_fields(0, warp_next_trace, ["positions"]) - - # test if the walkers were warped back to the beginning - assert warp_next_pos["positions"].max() <= segment_length - - -def test_H5_resampling(): - # ------------------------------------------------------------------ - # test_data/rw_nowarping.h5 holds 1D random walk data (generated by test_hdf5.py) - # includes resampling, but no warping - # ------ - we = WepyHDF5(hdf5_filename_nowarp, mode="r") - with we: - n_cycles = we.num_run_cycles(0) - wr_list = we.warping_records([0]) - n_walkers = we.num_walkers(0, 0) - all_pos = np.array( - [we.h5[f"runs/0/trajectories/{i}/positions"] for i in range(n_walkers)] - ) - all_wts = np.array( - [we.h5[f"runs/0/trajectories/{i}/weights"] for i in range(n_walkers)] - ) - - rrs = we.resampling_records([0]) - # - # find all cloning events - # - # MultiCloneMergeDecision: (1: Nothing; 2: Clone; 3: Squash; 4: Keep_Merge) - # - clone_rrs = [rr for rr in rrs if rr.decision_id == 2] - for cr in clone_rrs: - parent = cr.walker_idx - targets = cr.target_idxs - cycle_idx = cr.cycle_idx - if cycle_idx + 1 < n_cycles: - for target in targets: - # test if cloned walkers have the appropriate weights - assert ( - all_wts[parent][cycle_idx] / len(targets) - == all_wts[target][cycle_idx + 1] - ) - - # test if the children are within segment_length of the parent - assert ( - np.sum( - np.abs( - all_pos[parent][cycle_idx] - all_pos[target][cycle_idx + 1] - ) - ) - <= segment_length - ) - - # - # find all merging events - # - squash_rrs = [rr for rr in rrs if rr.decision_id == 3] - keep_merge_rrs = [rr for rr in rrs if rr.decision_id == 4] - for km in keep_merge_rrs: - cycle = km.cycle_idx - walker = km.walker_idx - if cycle + 1 < n_cycles: - # get all the squash records that correspond to this - squashed_walkers = [ - sr.walker_idx - for sr in squash_rrs - if (sr.target_idxs[0] == walker and sr.cycle_idx == cycle) - ] + [walker] - - # check if the sum of their weights (on this cycle) equals the km weight (on the next cycle) - np.testing.assert_almost_equal( - np.sum(all_wts[walker][cycle + 1]), - np.sum(all_wts[squashed_walkers, cycle]), - decimal=5, - ) - - -def test_H5_contig(): - # ------------------------------------------------------------------ - # test_data/rw.h5 holds 1D random walk data (generated by test_hdf5.py) - # includes both warping and resampling - # ------ - we = WepyHDF5(hdf5_filename, mode="r") - with we: - n_cycles = we.num_run_cycles(0) - n_walkers = we.num_walkers(0, 0) - all_pos = np.array( - [we.h5[f"runs/0/trajectories/{i}/positions"] for i in range(n_walkers)] - ) - ct = ContigTree( - we, - boundary_condition_class=RandomWalkBC, - decision_class=MultiCloneMergeDecision, - ) - - sw = ct.sliding_windows(3) - for trace in sw: - # test if traces are from adjacent cycles - assert trace[1][2] - trace[0][2] == 1 - assert trace[2][2] - trace[1][2] == 1 - - # test if positions are adjacent - assert ( - np.sum( - np.abs( - all_pos[trace[1][1], trace[1][2]] - - all_pos[trace[0][1], trace[0][2]] - ) - ) - <= 1 - ) - assert ( - np.sum( - np.abs( - all_pos[trace[2][1], trace[2][2]] - - all_pos[trace[1][1], trace[1][2]] - ) - ) - <= 1 - ) - - final_trace = [(0, i, n_cycles - 1) for i in range(n_walkers)] - lineages = ct.lineages(final_trace, discontinuities=True) - for lin in lineages: - for i in range(len(lin) - 1): - # test if positions are adjacent - assert ( - np.sum( - np.abs( - all_pos[lin[i][1], lin[i][2]] - - all_pos[lin[i + 1][1], lin[i + 1][2]] - ) - ) - <= 1 - ) - - disc_lineages = ct.lineages(final_trace, discontinuities=False) - for lin in disc_lineages: - for i in range(len(lin) - 1): - # if positions aren't adjacent, check that they correspond to warping events - if ( - np.sum( - np.abs( - all_pos[lin[i][1], lin[i][2]] - - all_pos[lin[i + 1][1], lin[i + 1][2]] - ) - ) - > 1 - ): - assert all_pos[lin[i][1], lin[i][2]] == boundary_position - assert all_pos[lin[i + 1][1], lin[i + 1][2]] <= segment_length From 3036dca689a9e99df0b732706b53329602dca186 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 11:51:59 -0500 Subject: [PATCH 034/143] refine Walker interfaces even further - The WalkerState is now pure protocol. - Walker is a generic concrete class - all clone/merge methods are now just functions --- src/wepy/missing.py | 10 +++ src/wepy/walker.py | 147 ++++++++++++----------------------- tests/unit/test_walker.py | 158 ++++++++++++++++++++------------------ 3 files changed, 141 insertions(+), 174 deletions(-) create mode 100644 src/wepy/missing.py diff --git a/src/wepy/missing.py b/src/wepy/missing.py new file mode 100644 index 00000000..89948a32 --- /dev/null +++ b/src/wepy/missing.py @@ -0,0 +1,10 @@ +class Missing: + # no data allowed + __slots__ = () + + def __repr__(self): + return "" + + +# Single instance +MISSING = Missing() diff --git a/src/wepy/walker.py b/src/wepy/walker.py index b53aa20e..17139c19 100644 --- a/src/wepy/walker.py +++ b/src/wepy/walker.py @@ -28,13 +28,11 @@ # Standard Library import logging -from typing import Protocol, Hashable, Any, Self, TypeVar, Generic -from abc import ABC +from typing import Protocol, Any, TypeVar, Generic import math # Standard Library import random as rand -from copy import deepcopy import attrs @@ -42,129 +40,80 @@ T = TypeVar("T") -class WalkerStateProtocol(Protocol): - def __getitem__(self, key: str) -> Any: ... +class WalkerState(Protocol[T]): - def dict(self) -> dict[str, Any]: ... + def __getitem__(self, key: str) -> T: ... + def __eq__(self, other: Any) -> bool: ... -class WalkerState: - """Reference implementation of the WalkerState interface. + def dict(self) -> dict[str, T]: ... - Access all key-value pairs as a dictionary with the dict() method. - Access individual values using the accessor syntax similar to - dictionaries: +WalkerState_ = TypeVar("WalkerState_") - >>> WalkerState(my_key='value')['my_key'] - 'value' - """ - - def __init__(self, **kwargs: dict[str, Any]) -> None: - """Constructor for WalkerState. - - All key-word arguments passed in will be set as the key-value - pairs for the state. - - """ - self._data = kwargs - - def __getitem__(self, key: str) -> Any: - return self._data[key] - - def __eq__(self, other: object) -> bool: - - if not isinstance(other, WalkerState): - return False - else: - return self._data == other._data - - def dict(self) -> dict[str, Any]: - """Return all key-value pairs as a dictionary.""" - return deepcopy(self._data) - -# TODO: move all cloning and merging methods and functions to a -# standalone module or in the clone_merge decision module - -class WalkerABC(ABC): - - def clone(self, number: int = 1) -> list[Self]: - """Clone this walker by making a copy with the same state and split - the probability uniformly between clones. - - The number is the increase in the number of walkers. - - e.g. number=1 will return 2 walkers with the same state as - this object but with probability split 50/50 between them +@attrs.define +class Walker(Generic[WalkerState_]): + """Reference implementation of the Walker interface. - Parameters - ---------- - number : int - Number of extra clones to make - (Default value = 1) + A container for: - Returns - ------- - cloned_walkers : list of objects implementing the Walker interface + - state + - weight - """ + """ - # calculate the weight of all child walkers split uniformly - split_prob = self.weight / (number + 1) - # make the clones - clones = [] - for i in range(number + 1): - clones.append(type(self)(self.state, split_prob)) + state: WalkerState_ + weight: float = attrs.field(eq=attrs.cmp_using(eq=math.isclose)) - return clones - def squash(self, merge_target: Self) -> Self: - """Add the weight of this walker to another. +def clone(walker: Walker, number: int = 1) -> list[Walker]: + """Clone this walker by making a copy with the same state and split + the probability uniformly between clones. - Parameters - ---------- - merge_target : object implementing the Walker interface - The walker to add this one's weight to. + The number is the increase in the number of walkers. - Returns - ------- - merged_walker : object implementing the Walker interface + e.g. number=1 will return 2 walkers with the same state as + this object but with probability split 50/50 between them - """ - new_weight = self.weight + merge_target.weight - return type(self)(merge_target.state, new_weight) + Parameters + ---------- + number : int + Number of extra clones to make + (Default value = 1) - def merge(self, other_walkers: list["Walker"]) -> "Walker": - """Merge a set of other walkers into this one using the merge function. + Returns + ------- + cloned_walkers : list of objects implementing the Walker interface - Parameters - ---------- - other_walkers : list of objects implementing the Walker interface - The walkers that will be merged together + """ - Returns - ------- - merged_walker : object implementing the Walker interface + # calculate the weight of all child walkers split uniformly + split_prob = walker.weight / (number + 1) + # make the clones + clones = [] + for i in range(number + 1): + clones.append(Walker(walker.state, split_prob)) - """ - return merge([self] + other_walkers)[0] + return clones -@attrs.define -class Walker(WalkerABC, Generic[T]): - """Reference implementation of the Walker interface. +def squash(walker: Walker, merge_target: Walker) -> Walker: + """Add the weight of this walker to another. - A container for: + Parameters + ---------- + merge_target : object implementing the Walker interface + The walker to add this one's weight to. - - state - - weight + Returns + ------- + merged_walker : object implementing the Walker interface """ - - state: T - weight: float = attrs.field(eq=attrs.cmp_using(eq=math.isclose)) + new_weight = walker.weight + merge_target.weight + return Walker(merge_target.state, new_weight) def split(walker: Walker, number: int = 2) -> list[Walker]: diff --git a/tests/unit/test_walker.py b/tests/unit/test_walker.py index c0d570e2..314ea0fe 100644 --- a/tests/unit/test_walker.py +++ b/tests/unit/test_walker.py @@ -1,5 +1,9 @@ import math +from typing import Literal, TypedDict +from wepy.missing import MISSING from wepy.walker import ( + clone, + squash, split, keep_merge, merge, @@ -7,25 +11,49 @@ WalkerState, ) +import attrs + +# attrs provide the __eq__ method + +MockKeys = Literal["a", "b"] +MockDataValue = int | str +class MockData(TypedDict): + a: int + b: str + +@attrs.define +class MockWalkerState(WalkerState): + a: int + b: str + + def __getitem__(self, key: MockKeys) -> MockDataValue: + if (value := getattr(self, key, MISSING)) is MISSING: + raise KeyError(f"'key' '{key}' not found") + else: + return value + + def dict(self) -> MockData: + return attrs.asdict(self) + class TestWalkerState: def test___init__(self): - WalkerState(a=1, b="hello") + MockWalkerState(a=1, b="hello") def test___getitem__(self): - assert WalkerState(a=1, b="hello")["a"] == 1 - assert WalkerState(a=1, b="hello")["b"] == "hello" + assert MockWalkerState(a=1, b="hello")["a"] == 1 + assert MockWalkerState(a=1, b="hello")["b"] == "hello" def test___eq__(self): - assert WalkerState(a=1, b="hello") == WalkerState(a=1, b="hello") - assert WalkerState(a=1, b="hello") != WalkerState(a=100, b="hello") + assert MockWalkerState(a=1, b="hello") == MockWalkerState(a=1, b="hello") + assert MockWalkerState(a=1, b="hello") != MockWalkerState(a=100, b="hello") def test_dict(self): - assert WalkerState(a=1, b="hello").dict() == { + assert MockWalkerState(a=1, b="hello").dict() == { "a": 1, "b": "hello", } @@ -35,7 +63,7 @@ class TestWalker: def test___init__(self): - state = WalkerState(a=1, b="hello") + state = MockWalkerState(a=1, b="hello") walker = Walker( state=state, weight=0.1, @@ -46,104 +74,84 @@ def test___init__(self): def test___eq__(self): assert Walker( - state=WalkerState(a=1, b="hello"), + state=MockWalkerState(a=1, b="hello"), weight=0.1, ) == Walker( - state=WalkerState(a=1, b="hello"), + state=MockWalkerState(a=1, b="hello"), weight=0.1, ) assert Walker( - state=WalkerState(a=1, b="hello"), + state=MockWalkerState(a=1, b="hello"), weight=0.1, ) != Walker( - state=WalkerState(a=100, b="hello"), + state=MockWalkerState(a=100, b="hello"), weight=0.1, ) assert Walker( - state=WalkerState(a=1, b="hello"), + state=MockWalkerState(a=1, b="hello"), weight=0.1, ) != Walker( - state=WalkerState(a=1, b="hello"), - weight=0.05, - ) - - def test_clone(self): - state = WalkerState(a=1, b="hello") - walker = Walker( - state=state, - weight=0.1, - ) - - clones = walker.clone(1) - assert len(clones) == 2 - - assert clones[0] == Walker( - state=state, - weight=0.05, - ) - assert clones[1] == Walker( - state=state, + state=MockWalkerState(a=1, b="hello"), weight=0.05, ) - def test_squash(self): - - walker_a = Walker( - state=WalkerState(a=1), - weight=0.1, - ) +def test_clone(): + state = MockWalkerState(a=1, b="hello") + walker = Walker( + state=state, + weight=0.1, + ) - walker_b = Walker( - state=WalkerState(a=10), - weight=0.1, - ) + clones = clone(walker, 1) + assert len(clones) == 2 - assert walker_a.squash(walker_b) == Walker( - state=WalkerState(a=10), - weight=0.2, - ) + assert clones[0] == Walker( + state=state, + weight=0.05, + ) + assert clones[1] == Walker( + state=state, + weight=0.05, + ) - assert walker_b.squash(walker_a) == Walker( - state=WalkerState(a=1), - weight=0.2, - ) +def test_squash(): - def test_merge(self): - walker_a = Walker( - state=WalkerState(a=1), - weight=0.1, - ) + walker_a = Walker( + state=MockWalkerState(a=1, b="hello"), + weight=0.1, + ) - other_walkers = [ - Walker( - state=WalkerState(a=10), - weight=0.1, - ), - Walker( - state=WalkerState(a=20), - weight=0.1, - ), - ] + walker_b = Walker( + state=MockWalkerState(a=10, b="hello"), + weight=0.1, + ) - assert math.isclose(walker_a.merge(other_walkers).weight, 0.3) + assert squash(walker_a, walker_b) == Walker( + state=MockWalkerState(a=10, b="hello"), + weight=0.2, + ) + assert squash(walker_b, walker_a) == Walker( + state=MockWalkerState(a=1, b="hello"), + weight=0.2, + ) def test_split(): walker = Walker( - state=WalkerState(a=1), + state=MockWalkerState(a=1, b="hello"), weight=0.1, ) assert split(walker, 2) == [ Walker( - state=WalkerState(a=1), + state=MockWalkerState(a=1, b="hello"), weight=0.05, ), Walker( - state=WalkerState(a=1), + state=MockWalkerState(a=1, b="hello"), weight=0.05, ), ] @@ -152,22 +160,22 @@ def test_split(): def test_keep_merge(): walkers = [ Walker( - state=WalkerState(a=10), + state=MockWalkerState(a=10, b="hello"), weight=0.1, ), Walker( - state=WalkerState(a=20), + state=MockWalkerState(a=20, b="hello"), weight=0.1, ), ] assert keep_merge(walkers, 0) == Walker( - state=WalkerState(a=10), + state=MockWalkerState(a=10, b="hello"), weight=0.2, ) assert keep_merge(walkers, 1) == Walker( - state=WalkerState(a=20), + state=MockWalkerState(a=20, b="hello"), weight=0.2, ) @@ -175,11 +183,11 @@ def test_keep_merge(): def test_merge(): walkers = [ Walker( - state=WalkerState(a=10), + state=MockWalkerState(a=10, b="hello"), weight=0.1, ), Walker( - state=WalkerState(a=20), + state=MockWalkerState(a=20, b="hello"), weight=0.1, ), ] From 9fab914dd84fac049ff1a246f0086d28ab33dd13 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 1 Dec 2025 16:56:03 -0500 Subject: [PATCH 035/143] remove multiprocessing_logging This can be a source of problems with pools so we will not use it anymore. --- pyproject.toml | 1 - uv.lock | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 30d6cc2d..d9bb7d4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,6 @@ dependencies = [ "tabulate", "jinja2", "pint", - "multiprocessing_logging", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 6261c0b9..498e32b6 100644 --- a/uv.lock +++ b/uv.lock @@ -1392,14 +1392,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d", size = 53481, upload-time = "2025-08-29T07:20:42.218Z" }, ] -[[package]] -name = "multiprocessing-logging" -version = "0.3.4" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/fe/32bd864bcb604b0607924a4cf618ed267a0ef21ac9c3e255109256046e1f/multiprocessing_logging-0.3.4-py2.py3-none-any.whl", hash = "sha256:8a5be02b02edbd6fa6e3e89499af7680db69db9e2d8707fcd28d445fa248f23e", size = 8770, upload-time = "2023-02-05T17:06:20.632Z" }, -] - [[package]] name = "mypy" version = "1.18.2" @@ -2874,7 +2866,6 @@ dependencies = [ { name = "geomm" }, { name = "h5py" }, { name = "jinja2" }, - { name = "multiprocessing-logging" }, { name = "networkx" }, { name = "nptyping" }, { name = "numpy" }, @@ -2936,7 +2927,6 @@ requires-dist = [ { name = "jinja2" }, { name = "matplotlib", marker = "extra == 'graphics'" }, { name = "mdtraj", marker = "extra == 'md'" }, - { name = "multiprocessing-logging" }, { name = "networkx" }, { name = "nptyping" }, { name = "numpy", specifier = ">=2" }, From 5494a92685e4c3eb3cdf4f55132623c403061e82 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 2 Dec 2025 10:43:49 -0500 Subject: [PATCH 036/143] minor cleanups --- .gitignore | 3 ++- justfile | 3 +++ src/wepy/py.typed | 0 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 src/wepy/py.typed diff --git a/.gitignore b/.gitignore index fbcf6960..102d2696 100644 --- a/.gitignore +++ b/.gitignore @@ -288,4 +288,5 @@ tags # End of https://www.gitignore.io/api/vim _output -.envrc.local \ No newline at end of file +.envrc.local +_tmp \ No newline at end of file diff --git a/justfile b/justfile index fc425cb8..25d1b13d 100644 --- a/justfile +++ b/justfile @@ -20,3 +20,6 @@ check: test: uv run pytest --import-mode=importlib tests/unit + +clean: + find . -type d -name "__pycache__" -prune -exec rm -rf {} + diff --git a/src/wepy/py.typed b/src/wepy/py.typed new file mode 100644 index 00000000..e69de29b From 8af87cfd514af99b3643c3b6b2dfa3891e2f7d95 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 2 Dec 2025 10:47:42 -0500 Subject: [PATCH 037/143] add pytest options --- pyproject.toml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d9bb7d4f..3c0e834c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,6 +110,19 @@ dev = [ fail-under = 100 verbose = 2 -# [tool.uv.sources] +[tool.pytest] -# pytest-datadir = { git = "https://github.com/salotz/pytest-datadir-e"} \ No newline at end of file +minversion = "9.0" +strict = true + +addopts = ["--import-mode=importlib"] + +python_classes = ["Test*", "Test_*"] +python_functions = ["test_*"] +python_files = ["test_*.py"] + +markers = [ + "ray", # mark a test as running ray +] + +log_level = "INFO" \ No newline at end of file From ffdf2f3053088e392624872e6e5887108c1d5bb3 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 3 Dec 2025 11:12:56 -0500 Subject: [PATCH 038/143] add ray to dependencies --- pyproject.toml | 2 +- uv.lock | 163 ++++++++++++++++++++++++++++--------------------- 2 files changed, 95 insertions(+), 70 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3c0e834c..44df0bec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ md = [ ] distributed = [ - "dask[bag]", + "ray", ] prometheus = [ diff --git a/uv.lock b/uv.lock index 498e32b6..42ae9105 100644 --- a/uv.lock +++ b/uv.lock @@ -324,15 +324,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] -[[package]] -name = "cloudpickle" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -500,24 +491,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] -[[package]] -name = "dask" -version = "2025.11.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "cloudpickle" }, - { name = "fsspec" }, - { name = "packaging" }, - { name = "partd" }, - { name = "pyyaml" }, - { name = "toolz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/db/33/eacaa72731f7fc64868caaf2d35060d50049eff889bd217263e68f76472f/dask-2025.11.0.tar.gz", hash = "sha256:23d59e624b80ee05b7cc8df858682cca58262c4c3b197ccf61da0f6543c8f7c3", size = 10984781, upload-time = "2025-11-06T16:56:51.535Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/54/a46920229d12c3a6e9f0081d1bdaeffad23c1826353ace95714faee926e5/dask-2025.11.0-py3-none-any.whl", hash = "sha256:08c35a8146c05c93b34f83cf651009129c42ee71762da7ca452fb7308641c2b8", size = 1477108, upload-time = "2025-11-06T16:56:44.892Z" }, -] - [[package]] name = "debugpy" version = "1.8.17" @@ -606,6 +579,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, ] +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, +] + [[package]] name = "flexcache" version = "0.3" @@ -680,15 +662,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, ] -[[package]] -name = "fsspec" -version = "2025.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, -] - [[package]] name = "geomm" version = "0.3" @@ -1221,15 +1194,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/40/23569737873cc9637fd488606347e9dd92b9fa37ba4fcda1f98ee5219a97/latexcodec-3.0.1-py3-none-any.whl", hash = "sha256:a9eb8200bff693f0437a69581f7579eb6bca25c4193515c09900ce76451e452e", size = 18532, upload-time = "2025-06-17T18:47:30.726Z" }, ] -[[package]] -name = "locket" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, -] - [[package]] name = "markupsafe" version = "3.0.3" @@ -1392,6 +1356,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d", size = 53481, upload-time = "2025-08-29T07:20:42.218Z" }, ] +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +] + [[package]] name = "mypy" version = "1.18.2" @@ -1735,19 +1743,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, ] -[[package]] -name = "partd" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "locket" }, - { name = "toolz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, -] - [[package]] name = "pathspec" version = "0.12.1" @@ -1917,6 +1912,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] +[[package]] +name = "protobuf" +version = "6.33.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432, upload-time = "2025-11-13T16:44:18.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593, upload-time = "2025-11-13T16:44:06.275Z" }, + { url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883, upload-time = "2025-11-13T16:44:09.222Z" }, + { url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522, upload-time = "2025-11-13T16:44:10.475Z" }, + { url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445, upload-time = "2025-11-13T16:44:11.869Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161, upload-time = "2025-11-13T16:44:12.778Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171, upload-time = "2025-11-13T16:44:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" }, +] + [[package]] name = "psutil" version = "7.1.3" @@ -2287,6 +2297,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, ] +[[package]] +name = "ray" +version = "2.51.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "jsonschema" }, + { name = "msgpack" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "requests" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/54/66fcfebd26c9747d908e2ac24f3a8a5502e84f19ea1e7a9b7f4d4a12bc34/ray-2.51.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:461b0e711f73cebc68128bca7202bef8db2c0e14dc6d49140f96549e5e752eb1", size = 68030141, upload-time = "2025-11-29T00:29:08.67Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9e/7add3c78a5a3d05f9c702d247da83a8a3e30d57eae153985f48ec3309c82/ray-2.51.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:5c97f29574072e3568a2714a84e6948fb457ce09eefd251c919221584b2d458d", size = 70506728, upload-time = "2025-11-29T00:29:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8e/5d1325619399d7eb9563e2f883f8e782fb26b39a122d6d629e54c8989a5a/ray-2.51.2-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:7b2a842744a1d4b47af8f3c0665a319736139518dd2e26fb9e18114281d8f9ea", size = 71359570, upload-time = "2025-11-29T00:29:19.508Z" }, + { url = "https://files.pythonhosted.org/packages/ba/96/ec1ee03fb1731d9e09d94d7ba6d9e47fce886d7cc79aac47e8422fe9c528/ray-2.51.2-cp312-cp312-win_amd64.whl", hash = "sha256:6b04ca7dccf540da2ab07fd7073009dfe04d9d084d705e337572272fa3e56485", size = 26675734, upload-time = "2025-11-29T00:29:24.27Z" }, + { url = "https://files.pythonhosted.org/packages/70/89/255ac2a70928a1d439c98fca9f3437cabbbebd3ac767523df608cce39197/ray-2.51.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c9ed290667868c809eb467ad8830d887fdce10dac2c674b3d43d3b3b5f9c7b07", size = 67975149, upload-time = "2025-11-29T00:29:28.995Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/1e3bb04e263a2bc1eacd762b37a0013d18f76341de0a7199d84a5a00b372/ray-2.51.2-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:554bd393e97bed9dfa5f73f47e4fbf42aa35d81b1228081aa93ccb7cdd5d4b34", size = 70414911, upload-time = "2025-11-29T00:29:34.286Z" }, + { url = "https://files.pythonhosted.org/packages/c4/85/f6994a74cf5e6fa6ebc959c27ff6f1f5352b78e71b947b4b302c6bb0a203/ray-2.51.2-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:e3bf004ed23971ec5d324ed9748aed23f6645d56696a44cdbe35d331f66c4619", size = 71275062, upload-time = "2025-11-29T00:29:39.379Z" }, +] + [[package]] name = "referencing" version = "0.37.0" @@ -2747,15 +2781,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, ] -[[package]] -name = "toolz" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, -] - [[package]] name = "tornado" version = "6.5.2" @@ -2877,7 +2902,7 @@ dependencies = [ [package.optional-dependencies] distributed = [ - { name = "dask" }, + { name = "ray" }, ] graphics = [ { name = "matplotlib" }, @@ -2921,7 +2946,6 @@ dev = [ requires-dist = [ { name = "attrs" }, { name = "click" }, - { name = "dask", extras = ["bag"], marker = "extra == 'distributed'" }, { name = "geomm" }, { name = "h5py", specifier = ">=3" }, { name = "jinja2" }, @@ -2937,6 +2961,7 @@ requires-dist = [ { name = "pint" }, { name = "prometheus-client", marker = "extra == 'prometheus'" }, { name = "pympler", marker = "extra == 'prometheus'" }, + { name = "ray", marker = "extra == 'distributed'" }, { name = "scipy" }, { name = "tabulate" }, ] From 70a3966ae25d970248c51fc189b487d8f6153a19 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 3 Dec 2025 13:27:45 -0500 Subject: [PATCH 039/143] refactoring openmm state and runner Creates a OpenMMStateWrapper (wraps an openmm.State) and OpenMMState (plain data) --- pyproject.toml | 20 +- src/wepy/core.py | 2 + src/wepy/runners/openmm.py | 1402 --------------- src/wepy/runners/openmm/__init__.py | 1 + src/wepy/runners/openmm/runner.py | 1506 +++++++++++++++++ src/wepy/runners/openmm/state.py | 977 +++++++++++ src/wepy/util/openmm.py | 24 + .../test_runner.py} | 170 +- .../test_runners/test_openmm/test_state.py | 852 ++++++++++ tests/unit/test_util/test_openmm.py | 40 + uv.lock | 110 ++ 11 files changed, 3581 insertions(+), 1523 deletions(-) create mode 100644 src/wepy/core.py delete mode 100644 src/wepy/runners/openmm.py create mode 100644 src/wepy/runners/openmm/__init__.py create mode 100644 src/wepy/runners/openmm/runner.py create mode 100644 src/wepy/runners/openmm/state.py create mode 100644 src/wepy/util/openmm.py rename tests/unit/test_runners/{test_openmm.py => test_openmm/test_runner.py} (69%) create mode 100644 tests/unit/test_runners/test_openmm/test_state.py create mode 100644 tests/unit/test_util/test_openmm.py diff --git a/pyproject.toml b/pyproject.toml index 44df0bec..a1fe0ca8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ dependencies = [ "attrs", + "immutables", "numpy>=2", "nptyping", "h5py>=3", @@ -44,6 +45,8 @@ md = [ # NOTE: you should just install this yourself to get it the compute # runtime you need. "openmm", + # for dealing with openmm serialized objects + "lxml", ] distributed = [ @@ -85,6 +88,8 @@ dev = [ "pytest-check", "pytest-datadir @ git+https://github.com/salotz/pytest-datadir-extras.git@41e9a1e94ba27efe28ce9d0019b1e0a73be34400", "pytest-print", + # for testing openmm wrappers + "lxml", # qa "black", "isort", @@ -125,4 +130,17 @@ markers = [ "ray", # mark a test as running ray ] -log_level = "INFO" \ No newline at end of file +log_level = "INFO" + +[tool.mypy] + +pretty = true +color_output= true + +[[tool.mypy.overrides]] +module = "mdtraj.*" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "openmm.*" +ignore_missing_imports = true diff --git a/src/wepy/core.py b/src/wepy/core.py new file mode 100644 index 00000000..3c3dc324 --- /dev/null +++ b/src/wepy/core.py @@ -0,0 +1,2 @@ +class BugError(Exception): + pass diff --git a/src/wepy/runners/openmm.py b/src/wepy/runners/openmm.py deleted file mode 100644 index 4dcea531..00000000 --- a/src/wepy/runners/openmm.py +++ /dev/null @@ -1,1402 +0,0 @@ -"""OpenMM molecular dynamics runner with accessory classes. - -OpenMM is a library with support for running molecular dynamics -simulations with specific support for fast GPU calculations. The -component based architecture of OpenMM makes it a perfect fit with -wepy. - -In addition to the principle OpenMMRunner class there are a few -classes here that make using OpenMM runner more efficient. - -First is a WalkerState class (OpenMMState) that wraps the openmm state -object directly, itself is a wrapper around the C++ -datastructures. This gives better performance by not performing copies -to a WalkerState dictionary. - -Second, is the OpenMMWalker which is identical to the Walker class -except that it enforces the state is an actual instantiation of -OpenMMState. Use of this is optional. - -Finally, is the OpenMMGPUWorker class. This is to be used as the -worker type for the WorkerMapper work mapper. This is necessary to -allow passing of the device index to OpenMM for which GPU device to -use. - -""" - -# Standard Library -from typing import Any, Annotated, TypedDict, NotRequired -import logging - -logger = logging.getLogger(__name__) -# Standard Library -import time -from warnings import warn -import copy - -# Third Party Library -import attrs -import numpy as np - -try: - import mdtraj -except ModuleNotFoundError: - warn("Module 'mdtraj' not found, those features will not be available.") - -try: - # Third Party Library - import openmm - import openmm.app - import openmm.unit -except ModuleNotFoundError: - raise ModuleNotFoundError( - "OpenMM has not been installed, which this runner requires." - ) - -# First Party Library -from wepy.runners.runner import Runner -from wepy.util.util import box_vectors_to_lengths_angles -from wepy.walker import WalkerState -# from wepy.work_mapper.task_mapper import WalkerTaskProcess -# from wepy.work_mapper.worker import Worker - -# AtomNDArray = NDArray[Shape["N atoms, 3 dimensions"], Floating] -# BoxVectorsNDArray = NDArray[Shape["3, 3"], Floating] - -## Constants - -KEYS: tuple[str] = ( - "positions", - "velocities", - "forces", - "kinetic_energy", - "potential_energy", - "time", - "box_vectors", - "box_volume", - "parameters", - "parameter_derivatives", -) -"""Names of the fields of the OpenMMState.""" - -# when we use the get_state function from the simulation context we -# can pass options for what kind of data to get, this is the default -# to get all the data. TODO not really sure what the 'groups' keyword -# is for though -GET_STATE_KWARG_DEFAULTS: tuple[tuple[str, bool]] = ( - ("getPositions", True), - ("getVelocities", True), - ("getForces", True), - ("getEnergy", True), - ("getParameters", True), - ("getParameterDerivatives", False), - ("enforcePeriodicBox", True), -) -"""Mapping of key word arguments to the simulation.context.getState -method for retrieving data for a simulation state. By default we set -each as True to retrieve all information. The presence or absence of -them is handled by the OpenMMState. - -""" - -STATE_DATA_TYPE_ENUM_NAMES: dict[str, str] = { - "positions": "Positions", - "velocities": "Velocities", - "forces": "Forces", - "energy": "Energy", - "parameters": "Parameters", - "parameter_derivatives": "ParameterDerivatives", - "integrator_parameters": "IntegratorParameters", -} - - -def resolve_state_data_type_enum_values() -> dict[str, int]: - enum_values = {} - for our_name, enum_name in STATE_DATA_TYPE_ENUM_NAMES.items(): - enum_values[our_name] = getattr(openmm.State, enum_name) - - return enum_values - - -# reversed since that is the order we check them in and is a frequent operation -STATE_DATA_TYPE_ENUM_VALUES: list[int] = list( - sorted( - [(k, v) for k, v in resolve_state_data_type_enum_values().items()], - key=lambda x: x[1], - reverse=True, - ) -) - - -def get_state_fields_present(sim_state: openmm.State) -> list[str]: - """For a state returns a set of the field data types present in it.""" - - flag_sum = sim_state.getDataTypes() - - flag_fields: list[str] = [] - flag_values: list[int] = [] - flag_cum: int = flag_sum - for field_name, flag_value in STATE_DATA_TYPE_ENUM_VALUES: - if flag_value > flag_cum: - continue - elif flag_value == flag_cum: - flag_fields.append(field_name) - flag_values.append(flag_value) - break - - else: - flag_fields.append(field_name) - flag_values.append(flag_value) - flag_cum -= flag_value - - # double check they sum up - assert sum(flag_values) == flag_sum - - return flag_fields - - -# the Units objects that OpenMM uses internally and are returned from -# simulation data - -# TODO: this is never used and we only need the unit names. Its okay -# to use openmm.units here but other runners should use a units sytem -# like pint which is easier to install. So we should remove this since -# its not used. - -# UNITS = (('positions_unit', openmm.unit.nanometer), -# ('time_unit', openmm.unit.picosecond), -# ('box_vectors_unit', openmm.unit.nanometer), -# ('velocities_unit', openmm.unit.nanometer/openmm.unit.picosecond), -# ('forces_unit', openmm.unit.kilojoule / (openmm.unit.nanometer * openmm.unit.mole)), -# ('box_volume_unit', openmm.unit.nanometer), -# ('kinetic_energy_unit', openmm.unit.kilojoule / openmm.unit.mole), -# ('potential_energy_unit', openmm.unit.kilojoule / openmm.unit.mole), -# ) -# """Mapping of units identifiers to the corresponding openmm.units Unit objects.""" - -# the names of the units from the units objects above. This is used -# for saving them to files -UNIT_NAMES: tuple[tuple[str, str]] = ( - ("positions_unit", openmm.unit.nanometer.get_name()), - ("time_unit", openmm.unit.picosecond.get_name()), - ("box_vectors_unit", openmm.unit.nanometer.get_name()), - ("velocities_unit", (openmm.unit.nanometer / openmm.unit.picosecond).get_name()), - ( - "forces_unit", - (openmm.unit.kilojoule / (openmm.unit.nanometer * openmm.unit.mole)).get_name(), - ), - ("box_volume_unit", openmm.unit.nanometer.get_name()), - ("kinetic_energy_unit", (openmm.unit.kilojoule / openmm.unit.mole).get_name()), - ("potential_energy_unit", (openmm.unit.kilojoule / openmm.unit.mole).get_name()), -) -"""Mapping of unit identifier strings to the serialized string spec of the unit.""" - -# a random seed will be chosen from 1 to RAND_SEED_RANGE_MAX when the -# Langevin integrator is created. 0 is the default and special value -# which will then choose a random value when the integrator is created - -# TODO: test this isn't needed -# RAND_SEED_RANGE_MAX = 1000000 - - -class OpenMMStateDict(TypedDict, total=False): - positions: np.typing.ArrayLike - velocities: np.typing.ArrayLike - forces: np.typing.ArrayLike - kinetic_energy: float - potential_energy: float - time: float - box_vectors: np.typing.ArrayLike - box_volume: float - # TODO: parameters - - -class OpenMMState(WalkerState): - """Walker state that wraps an openmm.State object. - - The keys for which values in the state are available are given by - the KEYS module constant (accessible through the class constant of - the same name as well). - - Additional fields can be added to these states through passing - extra kwargs to the constructor. These will be automatically given - a suffix of "_OTHER" to avoid name clashes. - - """ - - KEYS: tuple[str] = KEYS - """The provided attribute keys for the state.""" - - OTHER_KEY_TEMPLATE: str = "{}_OTHER" - """String formatting template for attributes not set in KEYS.""" - - def __init__( - self, - sim_state: openmm.State, - **kwargs: dict[str, Any], - ) -> None: - """Constructor for OpenMMState. - - Parameters - ---------- - state : openmm.State object - The simulation state retrieved from the simulation constant. - - kwargs : optional - - Additional attributes to set for the state. Will add the - "_OTHER" suffix to the keys - - """ - - # save the simulation state - self._sim_state = sim_state - - # probe which data fields it has - self._sim_state_fields_present = get_state_fields_present(self.sim_state) - - # save additional data if given - self._data = {} - for key, value in kwargs.items(): - # if the key is already in the sim_state keys we need to - # modify it and raise a warning - if key in self.KEYS: - warn( - "Key {} in kwargs is already taken by this class, renaming to {}".format( - self.OTHER_KEY_TEMPLATE - ).format( - key - ) - ) - - # make a new key - new_key = self.OTHER_KEY_TEMPLATE.format(key) - - # set it in the data - self._data[new_key] = value - - # otherwise just set it - else: - self._data[key] = value - - @property - def sim_state(self) -> openmm.State: - """The underlying openmm.State object this is wrapping.""" - return self._sim_state - - def __getitem__(self, key: str) -> Any: - # if this was a key for data not mapped from the OpenMM.State - # object we use the _data attribute - if (key not in self.KEYS) and ( - (not key.startswith("parameters")) - and (not key.startswith("parameter_derivatives")) - ): - return self._data[key] - - # otherwise we have to specifically get the correct data and - # process it into an array from the OpenMM.State - else: - if key == "positions": - return self.positions_values() - elif key == "velocities": - return self.velocities_values() - elif key == "forces": - return self.forces_values() - elif key == "kinetic_energy": - return self.kinetic_energy_value() - elif key == "potential_energy": - return self.potential_energy_value() - elif key == "time": - return self.time_value() - elif key == "box_vectors": - return self.box_vectors_values() - elif key == "box_volume": - return self.box_volume_value() - - # handle the parameters differently since they are dictionaries of values - elif key.startswith("parameters"): - parameters_dict = self.parameters_values() - if parameters_dict is None: - return None - else: - # TODO: this was an attempt at a general way to do - # this but it doesn't work and I only ever need - # one nested level, so for now we just implement it that way - # return self._get_nested_attr_from_compound_key(key, parameters_dict) - - param_key = key.split("/")[-1] - return parameters_dict[param_key] - - elif key.startswith("parameter_derivatives"): - pd_dict = self.parameter_derivatives_values() - if pd_dict is None: - return None - else: - return self._get_nested_attr_from_compound_key(key, pd_dict) - - ## Array properties - - # Positions - @property - def positions(self) -> Annotated[ - openmm.unit.Quantity | None, - np.typing.ArrayLike, - ]: - """The positions of the state as a numpy array openmm.unit.Quantity object.""" - - if "positions" in self._sim_state_fields_present: - return self.sim_state.getPositions(asNumpy=True) - else: - return None - - @property - def positions_unit(self) -> openmm.unit.Unit: - """The units (as a openmm.unit.Unit object) the positions are in.""" - return self.positions.unit - - def positions_values(self) -> np.typing.ArrayLike | None: - """The positions of the state as a numpy array in the positions_unit - openmm.unit.Unit. This is what is returned by the __getitem__ - accessor. - - """ - return self.positions.value_in_unit(self.positions_unit) - - # Velocities - @property - def velocities(self) -> Annotated[openmm.unit.Quantity | None, np.typing.ArrayLike]: - """The velocities of the state as a numpy array openmm.unit.Quantity object.""" - - if "velocities" in self._sim_state_fields_present: - return self.sim_state.getVelocities(asNumpy=True) - else: - return None - - @property - def velocities_unit(self) -> openmm.unit.Unit: - """The units (as a openmm.unit.Unit object) the velocities are in.""" - return self.velocities.unit - - def velocities_values(self) -> np.typing.ArrayLike | None: - """The velocities of the state as a numpy array in the velocities_unit - openmm.unit.Unit. This is what is returned by the __getitem__ - accessor. - - """ - - velocities = self.velocities - if velocities is None: - return None - else: - return self.velocities.value_in_unit(self.velocities_unit) - - # Forces - @property - def forces(self) -> Annotated[ - openmm.unit.Quantity | None, - np.typing.ArrayLike, - ]: - """The forces of the state as a numpy array openmm.unit.Quantity object.""" - - if "forces" in self._sim_state_fields_present: - return self.sim_state.getForces(asNumpy=True) - else: - return None - - @property - def forces_unit(self) -> openmm.unit.Unit: - """The units (as a openmm.unit.Unit object) the forces are in.""" - return self.forces.unit - - def forces_values(self) -> np.typing.ArrayLike | None: - """The forces of the state as a numpy array in the forces_unit - openmm.unit.Unit. This is what is returned by the __getitem__ - accessor. - - """ - - forces = self.forces - if forces is None: - return None - else: - return self.forces.value_in_unit(self.forces_unit) - - # Box Vectors - @property - def box_vectors(self) -> Annotated[ - openmm.unit.Quantity | None, - np.typing.ArrayLike, - ]: - """The box vectors of the state as a numpy array openmm.unit.Quantity object.""" - try: - return self.sim_state.getPeriodicBoxVectors(asNumpy=True) - except: - warn( - "Unknown exception handled from `self.sim_state.getPeriodicBoxVectors()`, " - "this is probably because this attribute is not in the State." - ) - return None - - @property - def box_vectors_unit(self) -> openmm.unit.Unit: - """The units (as a openmm.unit.Unit object) the box vectors are in.""" - return self.box_vectors.unit - - def box_vectors_values(self) -> np.typing.ArrayLike | None: - """The box vectors of the state as a numpy array in the - box_vectors_unit openmm.unit.Unit. This is what is returned by - the __getitem__ accessor. - - """ - - box_vectors = self.box_vectors - if box_vectors is None: - return None - else: - return self.box_vectors.value_in_unit(self.box_vectors_unit) - - ## non-array properties - - # Kinetic Energy - @property - def kinetic_energy(self) -> Annotated[ - openmm.unit.Quantity | None, - np.typing.ArrayLike, - ]: - """The kinetic energy of the state as a numpy array openmm.unit.Quantity object.""" - try: - return self.sim_state.getKineticEnergy() - except: - warn( - "Unknown exception handled from `self.sim_state.getKineticEnergy()`, " - "this is probably because this attribute is not in the State." - ) - return None - - @property - def kinetic_energy_unit(self) -> openmm.unit.Unit: - """The units (as a openmm.unit.Unit object) the kinetic energy is in.""" - return self.kinetic_energy.unit - - def kinetic_energy_value(self) -> np.typing.ArrayLike | None: - """The kinetic energy of the state as a numpy array in the kinetic_energy_unit - openmm.unit.Unit. This is what is returned by the __getitem__ - accessor. - - """ - - kinetic_energy = self.kinetic_energy - if kinetic_energy is None: - return None - else: - return np.array( - [self.kinetic_energy.value_in_unit(self.kinetic_energy_unit)] - ) - - # Potential Energy - @property - def potential_energy(self) -> Annotated[ - openmm.unit.Quantity | None, - np.typing.ArrayLike, - ]: - """The potential energy of the state as a numpy array openmm.unit.Quantity object.""" - try: - return self.sim_state.getPotentialEnergy() - except: - warn( - "Unknown exception handled from `self.sim_state.getPotentialEnergy()`, " - "this is probably because this attribute is not in the State." - ) - return None - - @property - def potential_energy_unit(self) -> openmm.unit.Unit: - """The units (as a openmm.unit.Unit object) the potential energy is in.""" - return self.potential_energy.unit - - def potential_energy_value(self) -> np.typing.ArrayLike | None: - """The potential energy of the state as a numpy array in the potential_energy_unit - openmm.unit.Unit. This is what is returned by the __getitem__ - accessor. - - """ - - potential_energy = self.potential_energy - if potential_energy is None: - return None - else: - return np.array( - [self.potential_energy.value_in_unit(self.potential_energy_unit)] - ) - - # Time - @property - def time(self) -> openmm.unit.Quantity | None: - """The time of the state as a numpy array openmm.unit.Quantity object.""" - try: - return self.sim_state.getTime() - except: - warn( - "Unknown exception handled from `self.sim_state.getTime()`, " - "this is probably because this attribute is not in the State." - ) - return None - - @property - def time_unit(self) -> openmm.unit.Unit: - """The units (as a openmm.unit.Unit object) the time is in.""" - return self.time.unit - - def time_value(self) -> np.typing.ArrayLike | None: - """The time of the state as a numpy array in the time_unit - openmm.unit.Unit. This is what is returned by the __getitem__ - accessor. - - """ - - time = self.time - if time is None: - return None - else: - return np.array([self.time.value_in_unit(self.time_unit)]) - - # Box Volume - @property - def box_volume(self) -> openmm.unit.Quantity | None: - """The box volume of the state as a numpy array openmm.unit.Quantity object.""" - try: - return self.sim_state.getPeriodicBoxVolume() - except: - warn( - "Unknown exception handled from `self.sim_state.getPeriodicBoxVolume()`, " - "this is probably because this attribute is not in the State." - ) - return None - - @property - def box_volume_unit(self) -> openmm.unit.Unit: - """The units (as a openmm.unit.Unit object) the box volume is in.""" - return self.box_volume.unit - - def box_volume_value(self) -> np.typing.ArrayLike | None: - """The box volume of the state as a numpy array in the box_volume_unit - openmm.unit.Unit. This is what is returned by the __getitem__ - accessor. - - """ - - box_volume = self.box_volume - if box_volume is None: - return None - else: - return np.array([self.box_volume.value_in_unit(self.box_volume_unit)]) - - ## Dictionary properties - ## Unitless - - # Parameters - @property - def parameters(self) -> dict[str, openmm.unit.Quantity] | None: - """The parameters of the state as a dictionary mapping the names of - the parameters to their values which are numpy array - openmm.unit.Quantity objects. - - """ - - if "parameters" in self._sim_state_fields_present: - return self.sim_state.getParameters() - else: - return None - - @property - def parameters_unit(self) -> dict[str, openmm.unit.Unit]: - """The units for each parameter as a dictionary mapping parameter - names to their corresponding unit as a openmm.unit.Unit - object. - - """ - param_units = {key: None for key, val in self.parameters.items()} - return param_units - - def parameters_values(self) -> dict[str, np.typing.ArrayLike] | None: - """The parameters of the state as a dictionary mapping the name of the - parameter to a numpy array in the unit for the parameter of the - same name in the parameters_unit corresponding - openmm.unit.Unit object. This is what is returned by the - __getitem__ accessor using the compound key syntax with the - prefix 'parameters', e.g. state['parameter/paramA'] for the - parameter 'paramA'. - - """ - - if self.parameters is None: - return None - - param_arrs = {key: np.array(val) for key, val in self.parameters.items()} - - # return None if there is nothing in this - if len(param_arrs) == 0: - return None - else: - return param_arrs - - # Parameter Derivatives - @property - def parameter_derivatives(self) -> dict[str, openmm.unit.Quantity] | None: - """The parameter derivatives of the state as a dictionary mapping the - names of the parameters to their values which are numpy array - openmm.unit.Quantity objects. - - """ - - if "parameter_derivatives" in self._sim_state_fields_present: - return self.sim_state.getEnergyParameterDerivatives() - else: - return None - - @property - def parameter_derivatives_unit(self) -> dict[str, openmm.unit.Unit]: - """The units for each parameter derivative as a dictionary mapping - parameter names to their corresponding unit as a - openmm.unit.Unit object. - - """ - - param_units = {key: None for key, val in self.parameter_derivatives.items()} - return param_units - - def parameter_derivatives_values(self) -> dict[str, np.typing.ArrayLike] | None: - """The parameter derivatives of the state as a dictionary mapping the - name of the parameter to a numpy array in the unit for the - parameter of the same name in the parameters_unit - corresponding openmm.unit.Unit object. This is what is - returned by the __getitem__ accessor using the compound key - syntax with the prefix 'parameter_derivatives', - e.g. state['parameter_derivatives/paramA'] for the parameter - 'paramA'. - - """ - - if self.parameter_derivatives is None: - return None - - param_arrs = { - key: np.array(val) for key, val in self.parameter_derivatives.items() - } - - # return None if there is nothing in this - if len(param_arrs) == 0: - return None - else: - return param_arrs - - # for the dict attributes we need to transform the keys for making - # a proper state where all __getitem__ things are arrays - def _dict_attr_to_compound_key_dict( - self, - root_key: str, - attr_dict: dict[str:Any], - ) -> dict[str, Any]: - """Transform a dictionary of values within the compound key 'root_key' - to a dictionary mapping compound keys to values. - - For example give the root_key 'parameters' and the parameters - dictionary {'paramA' : 1.234} returns {'parameters/paramA' : 1.234}. - - Parameters - ---------- - root_key : str - The compound key prefix - attr_dict : dict of str : value - The dictionary with simple keys within the root key namespace. - - Returns - ------- - compound_key_dict : dict of str : value - The dictionary with the compound keys. - - """ - - key_template = "{}/{}" - cmpd_key_d = {} - for key, value in attr_dict.items(): - new_key = key_template.format(root_key, key) - # if this is a proper feature - if type(value) == np.ndarray: - cmpd_key_d[new_key] = value - elif hasattr(value, "__getitem__"): - cmpd_key_d.update(self._dict_attr_to_compound_key_dict(new_key, value)) - else: - raise TypeError("Unsupported attribute type") - - return cmpd_key_d - - def _get_nested_attr_from_compound_key( - self, - compound_key: str, - compound_feat_dict: dict[str, Any], - ) -> Any: - """Get arbitrarily deeply nested compound keys from the full - dictionary tree. - - Parameters - ---------- - compound_key : str - Compound key separated by '/' characters - - compound_feat_dict : dict - Dictionary of arbitrary depth - - Returns - ------- - value - Value requested by the key. - - """ - - key_components = compound_key.split("/") - - # if there is only one component of the key then it is not - # really compound, we won't complain just return the - # "dictionary" if it is not actually a dict like - if not hasattr(compound_feat_dict, "__getitem__"): - raise TypeError("Must provide a dict-like with the compound key") - - value = compound_feat_dict[key_components[0]] - - # if the value itself is compound recursively fetch the value - if hasattr(value, "__getitem__") and len(key_components[1:]) > 0: - subgroup_key = "/".join(key_components[1:]) - - return self._get_nested_attr_from_compound_key(subgroup_key, value) - - elif hasattr(value, "__getitem__") and len(key_components[1:]) < 1: - raise ValueError("Key does not reference a leaf node of attribute") - - # otherwise we have the right key so return the object - else: - return value - - def parameters_features(self) -> dict[str, Any] | None: - """Returns a dictionary of the parameters with their appropriate - compound keys. This can be used for placing them in the same namespace - as the rest of the attributes. - """ - - parameters = self.parameters_values() - if parameters is None: - return None - else: - return self._dict_attr_to_compound_key_dict("parameters", parameters) - - def parameter_derivatives_features(self) -> dict[str, Any] | None: - """Returns a dictionary of the parameter derivatives with their appropriate - compound keys. This can be used for placing them in the same namespace - as the rest of the attributes. - """ - - parameter_derivatives = self.parameter_derivatives_values() - if parameter_derivatives is None: - return None - else: - return self._dict_attr_to_compound_key_dict( - "parameter_derivatives", parameter_derivatives - ) - - def omm_state_dict(self) -> OpenMMStateDict: - """Return a dictionary with all of the default keys from the wrapped - openmm.State object - """ - - feature_d = { - "positions": self.positions_values(), - "velocities": self.velocities_values(), - "forces": self.forces_values(), - "kinetic_energy": self.kinetic_energy_value(), - "potential_energy": self.potential_energy_value(), - "time": self.time_value(), - "box_vectors": self.box_vectors_values(), - "box_volume": self.box_volume_value(), - } - - params = self.parameters_features() - if params is not None: - feature_d.update(params) - - param_derivs = self.parameter_derivatives_features() - if param_derivs is not None: - feature_d.update(param_derivs) - - return feature_d - - def dict(self) -> dict[str, Any]: - # documented in superclass - - d = {} - for key, value in self._data.items(): - d[key] = value - for key, value in self.omm_state_dict().items(): - d[key] = value - return d - - def to_mdtraj(self, topology: mdtraj.Topology) -> mdtraj.Trajectory: - """Returns an mdtraj.Trajectory object from this walker's state. - - Parameters - ---------- - topology : mdtraj.Topology object - Topology for the state. - - Returns - ------- - state_traj : mdtraj.Trajectory object - - """ - - # resize the time to a 1D vector - unitcell_lengths, unitcell_angles = box_vectors_to_lengths_angles( - self.box_vectors - ) - return mdj.Trajectory( - np.array([self.positions_values()]), - unitcell_lengths=[unitcell_lengths], - unitcell_angles=[unitcell_angles], - topology=topology, - ) - -PlatformKwargs = dict[str, str] - -class OpenMMRunnerSegmentSplitTimes(TypedDict): - gen_sim_time: float - steps_time: float - get_state_time: float - run_segment_time: float - - -# the runner for the simulation which runs the actual dynamics -class OpenMMRunner(Runner[OpenMMState]): - """Runner for OpenMM simulations.""" - - system: openmm.System - topology: openmm.app.Topology - integrator: openmm.Integrator - platform_name: str - platform_kwargs: PlatformKwargs - enforce_box: bool - getState_kwargs: dict[str, bool] - # _cycle_platform: - # _cycle_platform_kwargs: - # _last_cycle_segments_split_times: list[float] - - def __init__( - self, - system: openmm.System, - topology: openmm.app.Topology, - integrator: openmm.Integrator, - platform: str | None = None, - platform_kwargs: PlatformKwargs | None = None, - enforce_box: bool = False, - get_state_kwargs: dict[str, bool] | None = None, - ) -> None: - """Constructor for OpenMMRunner. - - Parameters - ---------- - system : - The system (forcefields) for the simulation. - - topology : - The topology for you system. - - integrator : - Integrator for propagating dynamics. - - platform : - The specification for the default computational platform - to use. Platform can also be set when run_segment is - called. If None uses OpenMM default platform, see OpenMM - documentation for all value but typical ones are: - Reference, CUDA, OpenCL. If value is None the automatic - platform determining mechanism in OpenMM will be used. - - platform_kwargs : - key-values to set for a platform with - platform.setPropertyDefaultValue as the default for this - runner. - - enforce_box : - Calls 'context.getState' with 'enforcePeriodicBox' if True. - (Default value = False) - - get_state_kwargs : - key-values to set for getting the state from the OpenMM context. - keys not included will use the values in GET_STATE_KWARG_DEFAULTS. - Will override the enforce_box flag. - - Warnings - -------- - Regarding the enforce_box option. - - When retrieving states from an OpenMM simulation Context, you - have the option to enforce periodic boundary conditions in the - resulting atomic positions in a topology aware way that - doesn't break bonds through boundaries. This is convenient for - post-processing as this can be a complex task and is not - readily exposed in the OpenMM API as a standalone function. - - However, in some types of simulations the periodic box vectors - are ignored (such as implicit solvent ones) despite there - being no option to not have periodic boundaries in the context - itself. Likely if you are running one of these kinds of - simulations you will not pay attention to the box vectors at - all and the random defaults that exist will be very wrong but - this incorrectness will not show in a non-wepy simulation with - openmm unless you are handling the context states - yourself. Then when you run in wepy the default of True to - enforce the boxes will be applied and confusingly wrong - answers will result that are difficult to find root cause of. - - """ - - if platform is not None: - assert isinstance( - platform, str - ), f"platform should be a string, not {type(platform)}" - - # we save the different components. However, if we are to make - # this runner picklable we have to convert the SWIG objects to - # a picklable form - self.system = system - self.integrator = integrator - - # these are not SWIG objects - self.topology = topology - self.platform_name = platform - self.platform_kwargs = platform_kwargs - - self.enforce_box = enforce_box - - self.getState_kwargs = {} - if get_state_kwargs is not None: - for k in get_state_kwargs: - self.getState_kwargs[k] = get_state_kwargs[k] - - # override enforce_box option if specified in get_state_kwargs - if "enforce_box" in get_state_kwargs: - self.enforce_box = get_state_kwargs["enforce_box"] - - else: - self.getState_kwargs = dict(GET_STATE_KWARG_DEFAULTS) - - self._cycle_platform = None - self._cycle_platform_kwargs = None - - # for special monitoring purposes to get split times to debug - # performance - self._last_cycle_segments_split_times = [] - - def pre_cycle( - self, - platform: str | None = None, - platform_kwargs: PlatformKwargs | None = None, - ) -> None: - # choose to use the platform spec in this function call or to - # use the default one saved in the runner - - # if the platform is given locally use this one - if platform is not None: - logger.info( - f"Setting the platform ({platform}) in the 'pre_cycle' OpenMM Runner call" - f"with platform kwargs: {platform_kwargs}" - ) - # set the platform and kwargs for this cycle - self._cycle_platform = platform - self._cycle_platform_kwargs = platform_kwargs - - # otherwise we just don't set this and let resolution of - # platform happen at run segment. - # each segment split times will get appended to this - self._last_cycle_segments_split_times = [] - - def post_cycle(self) -> None: - # remove the platform and kwargs for this cycle - self._cycle_platform = None - self._cycle_platform_kwargs = None - - def _resolve_platform( - self, - platform: str | type(Ellipsis) | None, - platform_kwargs: PlatformKwargs | None, - ) -> tuple[ - str | None, - PlatformKwargs | None, - ]: - # resolve which platform to use - - # force usage of environmental one - if platform is Ellipsis: - platform_name = None - platform_kwargs = None - - # use the runtime given one - elif platform is not None: - platform_name = platform - platform_kwargs = platform_kwargs - - # if the pre_cycle configured platform is set use this over - # the default - elif self._cycle_platform is not None: - platform_name = self._cycle_platform - platform_kwargs = self._cycle_platform_kwargs - - # use the default one - elif self.platform_name is not None: - platform_name = self.platform_name - platform_kwargs = self.platform_kwargs - - # if the default is not set fall back to the environmental one - else: - platform_name = None - platform_kwargs = None - - return ( - platform_name, - platform_kwargs, - ) - - def run_segment( - self, - walker_state: OpenMMState, - segment_length: int, - getState_kwargs: dict[str, bool] | None = None, - platform: str | type(Ellipsis) | None = None, - platform_kwargs: PlatformKwargs = None, - # UGLY: here to satisfy the interface - cycle_idx: int = 0, - walker_idx: int = 0 - ) -> OpenMMState: - """Run dynamics for the walker. - - Parameters - ---------- - walker : The walker for which dynamics will be propagated. - - segment_length : The numerical value that specifies how much dynamics are to be run. - - getState_kwargs : Specify the key-word arguments to pass to - simulation.context.getState when getting simulation - states. If None defaults object values. - - - platform : The specification for the computational platform to - use. If None will use the default for the runner and - ignore platform_kwargs. If Ellipsis forces the use of the - OpenMM default or environmentally defined platform. See - OpenMM documentation for all value but typical ones are: - Reference, CUDA, OpenCL. If value is None the automatic - platform determining mechanism in OpenMM will be used. - - platform_kwargs : Key-values to set for a platform with - platform.setPropertyDefaultValue for this segment only. - - - Returns - ------- - new_walker_state : Walker after dynamics was run, only the state should be modified. - - """ - - run_segment_start = time.time() - - # set the kwargs that will be passed to getState - _getState_kwargs = ( - getState_kwargs if getState_kwargs is not None else self.getState_kwargs - ) - logger.info(f"Default 'getState_kwargs' in runner: {self.getState_kwargs}") - logger.info(f"'getState_kwargs' passed to 'run_segment' : {getState_kwargs}") - - logger.info( - "After resolving 'getState_kwargs' that will be used are: " - f"{_getState_kwargs}" - ) - - gen_sim_start = time.time() - - # make a copy of the integrator for this particular segment - new_integrator = copy.copy(self.integrator) - # force setting of random seed to 0, which is a special - # value that forces the integrator to choose another - # random number - new_integrator.setRandomNumberSeed(0) - - ## Platform - - logger.info(f"Default 'platform' in runner: {self.platform_name}") - - logger.info(f"pre_cycle set 'platform' in runner: {self._cycle_platform}") - - logger.info(f"'platform' passed to 'run_segment' : {platform}") - - logger.info(f"Default 'platform_kwargs' in runner: {self.platform_kwargs}") - - logger.info( - f"pre_cycle set 'platform_kwargs' in runner: {self._cycle_platform_kwargs}" - ) - - logger.info(f"'platform_kwargs' passed to 'run_segment' : {platform_kwargs}") - - platform_name, platform_kwargs = self._resolve_platform( - platform, platform_kwargs - ) - - logger.info(f"Resolved 'platform' : {platform_name}") - - logger.info(f"Resolved 'platform_kwargs' : {platform_kwargs}") - - # create simulation object - - ## create the platform and customize - - # if a platform was given we use it to make a Simulation object - if platform_name is not None: - logger.info("Using platform configured in code.") - - # get the platform by its name to use - platform = openmm.Platform.getPlatformByName(platform_name) - logger.info(f"Platform object created: {platform}") - - if platform_kwargs is None: - platform_kwargs = {} - - # set properties from the kwargs if they apply to the platform - for key, value in platform_kwargs.items(): - if key in platform.getPropertyNames(): - logger.info(f"Setting platform property: {key} : {value}") - platform.setPropertyDefaultValue(key, value) - - else: - warn( - f"Platform kwargs given ({key} : {value}) " - f"but is not valid for this platform ({platform_name})" - ) - - # make a new simulation object - simulation = openmm.app.Simulation( - self.topology, self.system, new_integrator, platform - ) - - # otherwise just use the default or environmentally defined one - else: - logger.info("Using environmental platform.") - simulation = openmm.app.Simulation( - self.topology, self.system, new_integrator - ) - - # set the state to the context from the walker - simulation.context.setState(walker_state.sim_state) - - gen_sim_end = time.time() - gen_sim_time = gen_sim_end - gen_sim_start - - logger.info("Time to generate the system: {}".format(gen_sim_time)) - - # actually run the simulation - - steps_start = time.time() - - # Run the simulation segment for the number of time steps - simulation.step(segment_length) - - steps_end = time.time() - steps_time = steps_end - steps_start - - logger.info("Time to run {} sim steps: {}".format(segment_length, steps_time)) - - get_state_start = time.time() - - get_state_end = time.time() - get_state_time = get_state_end - get_state_start - logger.info("Getting context state time: {}".format(get_state_time)) - - # generate the new state - new_state = OpenMMState(simulation.context.getState(**_getState_kwargs)) - - run_segment_end = time.time() - run_segment_time = run_segment_end - run_segment_start - logger.info("Total internal run_segment time: {}".format(run_segment_time)) - - segment_split_times = { - "gen_sim_time": gen_sim_time, - "steps_time": steps_time, - "get_state_time": get_state_time, - "run_segment_time": run_segment_time, - } - - self._last_cycle_segments_split_times.append(segment_split_times) - - return new_state - - def last_cycle_segments_split_times(self) -> OpenMMRunnerSegmentSplitTimes: - - return copy.deepcopy(self._last_cycle_segments_split_times) - - -def gen_sim_state( - positions: np.typing.ArrayLike, - system: openmm.System, - integrator: openmm.Integrator, - getState_kwargs: dict[str, bool] | None = None, -) -> openmm.State: - """Convenience function for generating an openmm.State object. - - Parameters - ---------- - positions : arraylike of float - The positions for the system you want to set - - system : openmm.app.System object - - integrator : openmm.Integrator object - - Returns - ------- - sim_state : openmm.State object - - """ - - # handle the getState_kwargs - tmp_getState_kwargs = getState_kwargs - - # start with the defaults - getState_kwargs = dict(GET_STATE_KWARG_DEFAULTS) - - # if there were customizations use them - if tmp_getState_kwargs is not None: - getState_kwargs.update(tmp_getState_kwargs) - - # generate a throwaway context, using the reference platform so we - # don't screw up other platform stuff later in the same process - platform = openmm.Platform.getPlatformByName("Reference") - context = openmm.Context(system, copy.copy(integrator), platform) - - # set the positions - context.setPositions(positions) - - # then just retrieve it as a state using the default kwargs - sim_state = context.getState(**getState_kwargs) - - return sim_state - - -# class OpenMMCPUWorker(Worker): -# """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). - -# This is intended to be used with the wepy.work_mapper.WorkerMapper -# work mapper class. - -# This class must be used in order to ensure OpenMM runs jobs on the -# appropriate GPU device. - -# """ - -# NAME_TEMPLATE = "OpenMMCPUWorker-{}" -# """The name template the worker processes are named to substituting in -# the process number.""" - -# DEFAULT_NUM_THREADS = 1 - -# def __init__(self, *args, **kwargs): -# if "num_threads" not in kwargs: -# num_threads = self.DEFAULT_NUM_THREADS -# else: -# num_threads = kwargs.pop("num_threads") - -# super().__init__(*args, num_threads=num_threads, **kwargs) - -# def run_task(self, task): -# # documented in superclass - -# # make the platform kwargs dictionary -# platform_options = {"Threads": str(self.attributes["num_threads"])} - -# # run the task and pass in the DeviceIndex for OpenMM to -# # assign work to the correct GPU -# return task(platform_kwargs=platform_options) - - -# class OpenMMGPUWorker(Worker): -# """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). - -# This is intended to be used with the wepy.work_mapper.WorkerMapper -# work mapper class. - -# This class must be used in order to ensure OpenMM runs jobs on the -# appropriate GPU device. - -# """ - -# NAME_TEMPLATE = "OpenMMGPUWorker-{}" -# """The name template the worker processes are named to substituting in -# the process number.""" - -# def run_task(self, task): -# # get the platform -# platform = self.mapper_attributes["platform"] - -# # get the device index from the attributes -# device_id = self.mapper_attributes["device_ids"][self._worker_idx] - -# # make the platform kwargs dictionary -# platform_options = {"DeviceIndex": str(device_id)} - -# logger.info(f"platform={platform}, platform_options={platform_options}") - -# return task( -# platform=platform, -# platform_kwargs=platform_options, -# ) - - -# class OpenMMCPUWalkerTaskProcess(WalkerTaskProcess): -# NAME_TEMPLATE = "OpenMM_CPU_Walker_Task-{}" - -# def run_task(self, task): -# print("CPU Walker Task ---->", self.mapper_attributes, task, task.func) -# if "num_threads" in self.mapper_attributes: -# num_threads = self.mapper_attributes["num_threads"] - -# # make the platform kwargs dictionary -# platform_options = {"Threads": str(num_threads)} - -# logger.info(f"Threads={num_threads}") - -# else: -# platform_options = {} - -# return task( -# platform_kwargs=platform_options, -# ) - - -# class OpenMMGPUWalkerTaskProcess(WalkerTaskProcess): -# NAME_TEMPLATE = "OpenMM_GPU_Walker_Task-{}" - -# def run_task(self, task): -# logger.info(f"Starting to run a task as worker {self._worker_idx}") - -# logger.info(f"GPU Walker Task ----> {self.mapper_attributes}") -# # get the platform -# platform = self.mapper_attributes["platform"] - -# # get the device index from the attributes -# device_id = self.mapper_attributes["device_ids"][self._worker_idx] - -# # make the platform kwargs dictionary -# platform_options = {"DeviceIndex": str(device_id)} - -# logger.info(f"platform={platform}, platform_options={platform_options}") - -# return task( -# platform=platform, -# platform_kwargs=platform_options, -# ) diff --git a/src/wepy/runners/openmm/__init__.py b/src/wepy/runners/openmm/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/wepy/runners/openmm/__init__.py @@ -0,0 +1 @@ + diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py new file mode 100644 index 00000000..d07296ed --- /dev/null +++ b/src/wepy/runners/openmm/runner.py @@ -0,0 +1,1506 @@ +"""OpenMM molecular dynamics runner with accessory classes. + +OpenMM is a library with support for running molecular dynamics +simulations with specific support for fast GPU calculations. The +component based architecture of OpenMM makes it a perfect fit with +wepy. + +In addition to the principle OpenMMRunner class there are a few +classes here that make using OpenMM runner more efficient. + +First is a WalkerState class (OpenMMState) that wraps the openmm state +object directly, itself is a wrapper around the C++ +datastructures. This gives better performance by not performing copies +to a WalkerState dictionary. + +Second, is the OpenMMWalker which is identical to the Walker class +except that it enforces the state is an actual instantiation of +OpenMMState. Use of this is optional. + +Finally, is the OpenMMGPUWorker class. This is to be used as the +worker type for the WorkerMapper work mapper. This is necessary to +allow passing of the device index to OpenMM for which GPU device to +use. + +""" + +# Standard Library +from typing import Any, Annotated, TypedDict, NotRequired, Literal, Final, Self, get_args, TypeAlias +import logging +import multiprocessing as mp +import itertools +import time +from warnings import warn +import copy + +# Third Party Library +from immutables import Map as frozenmap +import attrs +import numpy as np + +logger = logging.getLogger(__name__) + +try: + import mdtraj +except ModuleNotFoundError: + warn("Module 'mdtraj' not found, those features will not be available.") + +try: + # Third Party Library + import openmm + import openmm.app + import openmm.unit +except ModuleNotFoundError: + raise ModuleNotFoundError( + "OpenMM has not been installed, which this runner requires." + ) + +# First Party Library +from wepy.runners.runner import Runner +from wepy.util.util import box_vectors_to_lengths_angles +from wepy.walker import WalkerState +# from wepy.work_mapper.task_mapper import WalkerTaskProcess +# from wepy.work_mapper.worker import Worker + +# AtomNDArray = NDArray[Shape["N atoms, 3 dimensions"], Floating] +# BoxVectorsNDArray = NDArray[Shape["3, 3"], Floating] + +## Constants + +StateFieldName = Literal[ + "positions", + "velocities", + "forces", + "kinetic_energy", + "potential_energy", + "time", + "box_vectors", + "box_volume", + "parameters", + "parameter_derivatives", +] + +# OPENMM_STATE_FIELD_KEYS: frozenset[str, ...] = frozenset(get_args(OpenMMStateFields)) +# """Names of the fields of the OpenMMState.""" +STATE_FIELD_NAMES: frozenset[StateFieldName] = frozenset(get_args(StateFieldName)) + +FieldDataType: TypeAlias = openmm.UnitQuantity | frozenmap[str, Any] + +StateDataTypeName: TypeAlias = Literal[ + "positions", + "velocities", + "forces", + "energy", + "parameters", + "parameter_derivatives", + # NOTE: integrator_parameters show up here but are not accessible + # as fields on the state + "integrator_parameters", +] + +STATE_DATA_TYPE_ENUM_NAMES = frozenmap( + { + "positions": "Positions", + "velocities": "Velocities", + "forces": "Forces", + "energy": "Energy", + "parameters": "Parameters", + "parameter_derivatives": "ParameterDerivatives", + "integrator_parameters": "IntegratorParameters", + } +) + +FIELD_GETTER_NAMES = frozenmap( + { + "positions": "getPositions", + "velocities": "getVelocities", + "forces": "getForces", + "kinetic_energy": "getKineticEnergy", + "potential_energy": "getPotentialEnergy", + "time": "getTime", + "box_vectors": "getPeriodicBoxVectors", + "box_volume": "getPeriodicBoxVolume", + "parameters": "getParameters", + "parameter_derivatives": "getEnergyParameterDerivatives", + } +) + +GetStateKeyWords = Literal[ + "positions", + "velocities", + "forces", + "energy", + "parameters", + "parameterDerivatives", +] + +GET_STATE_KEYWORDS: frozenmap[StateFieldName, GetStateKeyWords | None] = frozenmap({ + "positions": "positions", + "velocities": "velocities", + "forces": "forces", + "kinetic_energy": "energy", + "potential_energy": "energy", + "time": None, + "box_vectors": None, + "box_volume": None, + "parameters": "parameters", + "parameter_derivatives": "parameterDerivatives", +}) + +GET_STATE_DEFAULT_ENFORCE_PERIODIC_BOX = False + + +# when we use the get_state function from the simulation context we +# can pass options for what kind of data to get, this is the default +# to get all the data. TODO not really sure what the 'groups' keyword +# is for though +GET_STATE_KWARG_DEFAULTS = frozenmap({ + "getPositions" : True, + "getVelocities" : True, + "getForces" : True, + "getEnergy" : True, + "getParameters" : True, + "getParameterDerivatives" : False, + "enforcePeriodicBox" : True, +}) +"""Mapping of key word arguments to the simulation.context.getState +method for retrieving data for a simulation state. By default we set +each as True to retrieve all information. The presence or absence of +them is handled by the OpenMMState. + +""" + +def get_context_state( + context: openmm.Context, + fields: frozenset[StateFieldName] | None = None, +) -> openmm.State: + """Retrieve a state from a context using field names.""" + + if fields is None: + _fields = STATE_FIELD_NAMES + else: + _fields = fields + + kwargs = {} + for field_name in _fields: + + kwarg = GET_STATE_KEYWORDS[field_name] + if kwarg is not None: + kwargs[kwarg] = True + + return context.getState( + **kwargs, + enforcePeriodicBox=GET_STATE_DEFAULT_ENFORCE_PERIODIC_BOX, + ) + + +def resolve_state_data_type_enum_values() -> frozenmap[StateDataTypeName, int]: + """Gets the enum values for each field in the state. + + These are int values which are used for bitflag operations. + """ + enum_values = {} + for our_name, enum_name in STATE_DATA_TYPE_ENUM_NAMES.items(): + enum_values[our_name] = getattr(openmm.State, enum_name) + + return frozenmap(enum_values) + + +# reversed since that is the order we check them in and is a frequent operation +STATE_DATA_TYPE_ENUM_VALUES: tuple[tuple[StateDataTypeName, int], ...] = tuple( + sorted( + [(k, v) for k, v in resolve_state_data_type_enum_values().items()], + key=lambda x: x[1], + reverse=True, + ) +) + + +def get_state_core_fields_present( + sim_state: openmm.State, +) -> frozenset[StateDataTypeName]: + """Figure out which core data fields are present in the State. + + This does not include accessory attributes: + - time + - box_vectors and box volume + - energies + + Note that this also includes the 'integrator_parameters' which are + not accessible from the state getters. + """ + + flag_sum = sim_state.getDataTypes() + + flag_fields = [] + flag_values = [] + flag_cum = flag_sum + for field_name, flag_value in STATE_DATA_TYPE_ENUM_VALUES: + if flag_value > flag_cum: + continue + elif flag_value == flag_cum: + flag_fields.append(field_name) + flag_values.append(flag_value) + break + + else: + flag_fields.append(field_name) + flag_values.append(flag_value) + flag_cum -= flag_value + + # double check they sum up + assert sum(flag_values) == flag_sum + + return frozenset(flag_fields) + +def get_state_fields_present(sim_state: openmm.State) -> frozenset[StateFieldName]: + """Figure out which accessible state fields are present in a State. + + This includes all the accessible fields that have getters + associated with them. + + Notably this excludes the 'integrator_parameters'. + """ + + # get which core fields are present + present_core_fields = get_state_core_fields_present(sim_state) + + present_fields = set() + + # core fields + for field in { + "positions", + "velocities", + "forces", + "parameters", + "parameter_derivatives", + }: + + if field in present_core_fields: + present_fields.add(field) + + # energy is a little different + if "energy" in present_core_fields: + present_fields = present_fields | ENERGY_FIELDS + + # handle box fields efficiently + try: + sim_state.getPeriodicBoxVectors() + except openmm.OpenMMException: + pass + else: + present_fields.add("box_vectors") + present_fields.add("box_volume") + + # then get whether the rest of the accessory fields are available + try: + sim_state.getTime() + except openmm.OpenMMException: + pass + else: + present_fields.add("time") + + return frozenset(present_fields) + + +# def resolve_state_data_type_enum_values() -> dict[str, int]: +# enum_values = {} +# for our_name, enum_name in STATE_DATA_TYPE_ENUM_NAMES.items(): +# enum_values[our_name] = getattr(openmm.State, enum_name) + +# return enum_values + + +# # reversed since that is the order we check them in and is a frequent operation +# STATE_DATA_TYPE_ENUM_VALUES: list[tuple[str, int]] = list( +# sorted( +# [(k, v) for k, v in resolve_state_data_type_enum_values().items()], +# key=lambda x: x[1], +# reverse=True, +# ) +# ) + + +# def get_state_fields_present(sim_state: openmm.State) -> list[str]: +# """For a state returns a set of the field data types present in it.""" + +# flag_sum = sim_state.getDataTypes() + +# flag_fields: list[str] = [] +# flag_values: list[int] = [] +# flag_cum: int = flag_sum +# for field_name, flag_value in STATE_DATA_TYPE_ENUM_VALUES: +# if flag_value > flag_cum: +# continue +# elif flag_value == flag_cum: +# flag_fields.append(field_name) +# flag_values.append(flag_value) +# break + +# else: +# flag_fields.append(field_name) +# flag_values.append(flag_value) +# flag_cum -= flag_value + +# # double check they sum up +# assert sum(flag_values) == flag_sum + +# return flag_fields + + +# the Units objects that OpenMM uses internally and are returned from +# simulation data + +# TODO: this is never used and we only need the unit names. Its okay +# to use openmm.units here but other runners should use a units sytem +# like pint which is easier to install. So we should remove this since +# its not used. + +# UNITS = (('positions_unit', openmm.unit.nanometer), +# ('time_unit', openmm.unit.picosecond), +# ('box_vectors_unit', openmm.unit.nanometer), +# ('velocities_unit', openmm.unit.nanometer/openmm.unit.picosecond), +# ('forces_unit', openmm.unit.kilojoule / (openmm.unit.nanometer * openmm.unit.mole)), +# ('box_volume_unit', openmm.unit.nanometer), +# ('kinetic_energy_unit', openmm.unit.kilojoule / openmm.unit.mole), +# ('potential_energy_unit', openmm.unit.kilojoule / openmm.unit.mole), +# ) +# """Mapping of units identifiers to the corresponding openmm.units Unit objects.""" + +# the names of the units from the units objects above. This is used +# for saving them to files +UNIT_NAMES: tuple[tuple[str, str], ...] = ( + ("positions_unit", openmm.unit.nanometer.get_name()), + ("time_unit", openmm.unit.picosecond.get_name()), + ("box_vectors_unit", openmm.unit.nanometer.get_name()), + ("velocities_unit", (openmm.unit.nanometer / openmm.unit.picosecond).get_name()), + ( + "forces_unit", + (openmm.unit.kilojoule / (openmm.unit.nanometer * openmm.unit.mole)).get_name(), + ), + ("box_volume_unit", openmm.unit.nanometer.get_name()), + ("kinetic_energy_unit", (openmm.unit.kilojoule / openmm.unit.mole).get_name()), + ("potential_energy_unit", (openmm.unit.kilojoule / openmm.unit.mole).get_name()), +) +"""Mapping of unit identifier strings to the serialized string spec of the unit.""" + +# a random seed will be chosen from 1 to RAND_SEED_RANGE_MAX when the +# Langevin integrator is created. 0 is the default and special value +# which will then choose a random value when the integrator is created + +# TODO: test this isn't needed +# RAND_SEED_RANGE_MAX = 1000000 + + +class OpenMMStateDict(TypedDict, total=False): + positions: np.typing.ArrayLike + velocities: np.typing.ArrayLike + forces: np.typing.ArrayLike + kinetic_energy: float + potential_energy: float + time: float + box_vectors: np.typing.ArrayLike + box_volume: float + # TODO: parameters + +"""OpenMM molecular dynamics runner with accessory classes. + +OpenMM is a library with support for running molecular dynamics +simulations with specific support for fast GPU calculations. The +component based architecture of OpenMM makes it a perfect fit with +wepy. + +In addition to the principle OpenMMRunner class there are a few +classes here that make using OpenMM runner more efficient. + +First is a WalkerState class (OpenMMState) that wraps the openmm state +object directly, itself is a wrapper around the C++ +datastructures. This gives better performance by not performing copies +to a WalkerState dictionary. + +Second, is the OpenMMWalker which is identical to the Walker class +except that it enforces the state is an actual instantiation of +OpenMMState. Use of this is optional. + +Finally, is the OpenMMGPUWorker class. This is to be used as the +worker type for the WorkerMapper work mapper. This is necessary to +allow passing of the device index to OpenMM for which GPU device to +use. + +""" + +# Standard Library +from typing import Any, Annotated, TypedDict, NotRequired, Literal, Final, Self, get_args, TypeAlias +import logging +import multiprocessing as mp +import itertools +import time +from warnings import warn +import copy + +# Third Party Library +from immutables import Map as frozenmap +import attrs +import numpy as np + +logger = logging.getLogger(__name__) + +try: + import mdtraj +except ModuleNotFoundError: + warn("Module 'mdtraj' not found, those features will not be available.") + +try: + # Third Party Library + import openmm + import openmm.app + import openmm.unit +except ModuleNotFoundError: + raise ModuleNotFoundError( + "OpenMM has not been installed, which this runner requires." + ) + +# First Party Library +from wepy.runners.runner import Runner +from wepy.util.util import box_vectors_to_lengths_angles +from wepy.walker import WalkerState +# from wepy.work_mapper.task_mapper import WalkerTaskProcess +# from wepy.work_mapper.worker import Worker + +# AtomNDArray = NDArray[Shape["N atoms, 3 dimensions"], Floating] +# BoxVectorsNDArray = NDArray[Shape["3, 3"], Floating] + +## Constants + + +# a random seed will be chosen from 1 to RAND_SEED_RANGE_MAX when the +# Langevin integrator is created. 0 is the default and special value +# which will then choose a random value when the integrator is created + +# TODO: test this isn't needed +# RAND_SEED_RANGE_MAX = 1000000 + +PlatformKwargs = dict[str, str] + +class OpenMMRunnerSegmentSplitTimes(TypedDict): + gen_sim_time: float + steps_time: float + get_state_time: float + run_segment_time: float + + +# the runner for the simulation which runs the actual dynamics +class OpenMMRunner(Runner[OpenMMStateWrapper]): + """Runner for OpenMM simulations.""" + + system: openmm.System + topology: openmm.app.Topology + integrator: openmm.Integrator + platform_name: str + platform_kwargs: PlatformKwargs + enforce_box: bool + getState_kwargs: dict[str, bool] + # _cycle_platform: + # _cycle_platform_kwargs: + _last_cycle_segments_split_times: list[OpenMMRunnerSegmentSplitTimes] + + def __init__( + self, + system: openmm.System, + topology: openmm.app.Topology, + integrator: openmm.Integrator, + platform: str | None = None, + platform_kwargs: PlatformKwargs | None = None, + enforce_box: bool = False, + get_state_kwargs: dict[str, bool] | None = None, + ) -> None: + """Constructor for OpenMMRunner. + + Parameters + ---------- + system : + The system (forcefields) for the simulation. + + topology : + The topology for you system. + + integrator : + Integrator for propagating dynamics. + + platform : + The specification for the default computational platform + to use. Platform can also be set when run_segment is + called. If None uses OpenMM default platform, see OpenMM + documentation for all value but typical ones are: + Reference, CUDA, OpenCL. If value is None the automatic + platform determining mechanism in OpenMM will be used. + + platform_kwargs : + key-values to set for a platform with + platform.setPropertyDefaultValue as the default for this + runner. + + enforce_box : + Calls 'context.getState' with 'enforcePeriodicBox' if True. + (Default value = False) + + get_state_kwargs : + key-values to set for getting the state from the OpenMM context. + keys not included will use the values in GET_STATE_KWARG_DEFAULTS. + Will override the enforce_box flag. + + Warnings + -------- + Regarding the enforce_box option. + + When retrieving states from an OpenMM simulation Context, you + have the option to enforce periodic boundary conditions in the + resulting atomic positions in a topology aware way that + doesn't break bonds through boundaries. This is convenient for + post-processing as this can be a complex task and is not + readily exposed in the OpenMM API as a standalone function. + + However, in some types of simulations the periodic box vectors + are ignored (such as implicit solvent ones) despite there + being no option to not have periodic boundaries in the context + itself. Likely if you are running one of these kinds of + simulations you will not pay attention to the box vectors at + all and the random defaults that exist will be very wrong but + this incorrectness will not show in a non-wepy simulation with + openmm unless you are handling the context states + yourself. Then when you run in wepy the default of True to + enforce the boxes will be applied and confusingly wrong + answers will result that are difficult to find root cause of. + + """ + + if platform is not None: + assert isinstance( + platform, str + ), f"platform should be a string, not {type(platform)}" + + # we save the different components. However, if we are to make + # this runner picklable we have to convert the SWIG objects to + # a picklable form + self.system = system + self.integrator = integrator + + # these are not SWIG objects + self.topology = topology + self.platform_name = platform + self.platform_kwargs = platform_kwargs + + self.enforce_box = enforce_box + + self.getState_kwargs = {} + if get_state_kwargs is not None: + for k in get_state_kwargs: + self.getState_kwargs[k] = get_state_kwargs[k] + + # override enforce_box option if specified in get_state_kwargs + if "enforce_box" in get_state_kwargs: + self.enforce_box = get_state_kwargs["enforce_box"] + + else: + self.getState_kwargs = dict(GET_STATE_KWARG_DEFAULTS) + + self._cycle_platform = None + self._cycle_platform_kwargs = None + + # for special monitoring purposes to get split times to debug + # performance + self._last_cycle_segments_split_times = [] + + def pre_cycle( + self, + platform: str | None = None, + platform_kwargs: PlatformKwargs | None = None, + ) -> None: + # choose to use the platform spec in this function call or to + # use the default one saved in the runner + + # if the platform is given locally use this one + if platform is not None: + logger.info( + f"Setting the platform ({platform}) in the 'pre_cycle' OpenMM Runner call" + f"with platform kwargs: {platform_kwargs}" + ) + # set the platform and kwargs for this cycle + self._cycle_platform = platform + self._cycle_platform_kwargs = platform_kwargs + + # otherwise we just don't set this and let resolution of + # platform happen at run segment. + # each segment split times will get appended to this + self._last_cycle_segments_split_times = [] + + def post_cycle(self) -> None: + # remove the platform and kwargs for this cycle + self._cycle_platform = None + self._cycle_platform_kwargs = None + + def _resolve_platform( + self, + platform: str | Literal[Ellipsis] | None, + platform_kwargs: PlatformKwargs | None, + ) -> tuple[ + str | None, + PlatformKwargs | None, + ]: + # resolve which platform to use + + # force usage of environmental one + if platform is Ellipsis: + platform_name = None + platform_kwargs = None + + # use the runtime given one + elif platform is not None: + platform_name = platform + platform_kwargs = platform_kwargs + + # if the pre_cycle configured platform is set use this over + # the default + elif self._cycle_platform is not None: + platform_name = self._cycle_platform + platform_kwargs = self._cycle_platform_kwargs + + # use the default one + elif self.platform_name is not None: + platform_name = self.platform_name + platform_kwargs = self.platform_kwargs + + # if the default is not set fall back to the environmental one + else: + platform_name = None + platform_kwargs = None + + return ( + platform_name, + platform_kwargs, + ) + + def run_segment( + self, + walker_state: OpenMMStateWrapper, + segment_length: int, + getState_kwargs: dict[str, bool] | None = None, + platform: str | Literal[Ellipsis] | None = None, + platform_kwargs: PlatformKwargs | None = None, + ) -> OpenMMStateWrapper: + """Run dynamics for the walker. + + Parameters + ---------- + walker : The walker for which dynamics will be propagated. + + segment_length : The numerical value that specifies how much dynamics are to be run. + + getState_kwargs : Specify the key-word arguments to pass to + simulation.context.getState when getting simulation + states. If None defaults object values. + + + platform : The specification for the computational platform to + use. If None will use the default for the runner and + ignore platform_kwargs. If Ellipsis forces the use of the + OpenMM default or environmentally defined platform. See + OpenMM documentation for all value but typical ones are: + Reference, CUDA, OpenCL. If value is None the automatic + platform determining mechanism in OpenMM will be used. + + platform_kwargs : Key-values to set for a platform with + platform.setPropertyDefaultValue for this segment only. + + + Returns + ------- + new_walker_state : Walker after dynamics was run, only the state should be modified. + + """ + + run_segment_start = time.time() + + # set the kwargs that will be passed to getState + _getState_kwargs = ( + getState_kwargs if getState_kwargs is not None else self.getState_kwargs + ) + logger.info(f"Default 'getState_kwargs' in runner: {self.getState_kwargs}") + logger.info(f"'getState_kwargs' passed to 'run_segment' : {getState_kwargs}") + + logger.info( + "After resolving 'getState_kwargs' that will be used are: " + f"{_getState_kwargs}" + ) + + gen_sim_start = time.time() + + # make a copy of the integrator for this particular segment + new_integrator = copy.copy(self.integrator) + # force setting of random seed to 0, which is a special + # value that forces the integrator to choose another + # random number + new_integrator.setRandomNumberSeed(0) + + ## Platform + + logger.info(f"Default 'platform' in runner: {self.platform_name}") + + logger.info(f"pre_cycle set 'platform' in runner: {self._cycle_platform}") + + logger.info(f"'platform' passed to 'run_segment' : {platform}") + + logger.info(f"Default 'platform_kwargs' in runner: {self.platform_kwargs}") + + logger.info( + f"pre_cycle set 'platform_kwargs' in runner: {self._cycle_platform_kwargs}" + ) + + logger.info(f"'platform_kwargs' passed to 'run_segment' : {platform_kwargs}") + + platform_name, platform_kwargs = self._resolve_platform( + platform, platform_kwargs + ) + + logger.info(f"Resolved 'platform' : {platform_name}") + + logger.info(f"Resolved 'platform_kwargs' : {platform_kwargs}") + + # create simulation object + + ## create the platform and customize + + # if a platform was given we use it to make a Simulation object + if platform_name is not None: + logger.info("Using platform configured in code.") + + # get the platform by its name to use + platform = openmm.Platform.getPlatformByName(platform_name) + logger.info(f"Platform object created: {platform}") + + if platform_kwargs is None: + platform_kwargs = {} + + # set properties from the kwargs if they apply to the platform + for key, value in platform_kwargs.items(): + if key in platform.getPropertyNames(): + logger.info(f"Setting platform property: {key} : {value}") + platform.setPropertyDefaultValue(key, value) + + else: + warn( + f"Platform kwargs given ({key} : {value}) " + f"but is not valid for this platform ({platform_name})" + ) + + # make a new simulation object + simulation = openmm.app.Simulation( + self.topology, self.system, new_integrator, platform + ) + + # otherwise just use the default or environmentally defined one + else: + logger.info("Using environmental platform.") + simulation = openmm.app.Simulation( + self.topology, self.system, new_integrator + ) + + # set the state to the context from the walker + simulation.context.setState(walker_state.sim_state) + + gen_sim_end = time.time() + gen_sim_time = gen_sim_end - gen_sim_start + + logger.info("Time to generate the system: {}".format(gen_sim_time)) + + # actually run the simulation + + steps_start = time.time() + + # Run the simulation segment for the number of time steps + simulation.step(segment_length) + + steps_end = time.time() + steps_time = steps_end - steps_start + + logger.info("Time to run {} sim steps: {}".format(segment_length, steps_time)) + + get_state_start = time.time() + + get_state_end = time.time() + get_state_time = get_state_end - get_state_start + logger.info("Getting context state time: {}".format(get_state_time)) + + # generate the new state + new_state = OpenMMStateWrapper(simulation.context.getState(**_getState_kwargs)) + + run_segment_end = time.time() + run_segment_time = run_segment_end - run_segment_start + logger.info("Total internal run_segment time: {}".format(run_segment_time)) + + segment_split_times = OpenMMRunnerSegmentSplitTimes({ + "gen_sim_time": gen_sim_time, + "steps_time": steps_time, + "get_state_time": get_state_time, + "run_segment_time": run_segment_time, + }) + + self._last_cycle_segments_split_times.append(segment_split_times) + + return new_state + + def last_cycle_segments_split_times(self) -> list[OpenMMRunnerSegmentSplitTimes]: + + return copy.deepcopy(self._last_cycle_segments_split_times) + + + + +# class OpenMMCPUWorker(Worker): +# """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). + +# This is intended to be used with the wepy.work_mapper.WorkerMapper +# work mapper class. + +# This class must be used in order to ensure OpenMM runs jobs on the +# appropriate GPU device. + +# """ + +# NAME_TEMPLATE = "OpenMMCPUWorker-{}" +# """The name template the worker processes are named to substituting in +# the process number.""" + +# DEFAULT_NUM_THREADS = 1 + +# def __init__(self, *args, **kwargs): +# if "num_threads" not in kwargs: +# num_threads = self.DEFAULT_NUM_THREADS +# else: +# num_threads = kwargs.pop("num_threads") + +# super().__init__(*args, num_threads=num_threads, **kwargs) + +# def run_task(self, task): +# # documented in superclass + +# # make the platform kwargs dictionary +# platform_options = {"Threads": str(self.attributes["num_threads"])} + +# # run the task and pass in the DeviceIndex for OpenMM to +# # assign work to the correct GPU +# return task(platform_kwargs=platform_options) + + +# class OpenMMGPUWorker(Worker): +# """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). + +# This is intended to be used with the wepy.work_mapper.WorkerMapper +# work mapper class. + +# This class must be used in order to ensure OpenMM runs jobs on the +# appropriate GPU device. + +# """ + +# NAME_TEMPLATE = "OpenMMGPUWorker-{}" +# """The name template the worker processes are named to substituting in +# the process number.""" + +# def run_task(self, task): +# # get the platform +# platform = self.mapper_attributes["platform"] + +# # get the device index from the attributes +# device_id = self.mapper_attributes["device_ids"][self._worker_idx] + +# # make the platform kwargs dictionary +# platform_options = {"DeviceIndex": str(device_id)} + +# logger.info(f"platform={platform}, platform_options={platform_options}") + +# return task( +# platform=platform, +# platform_kwargs=platform_options, +# ) + + +# class OpenMMCPUWalkerTaskProcess(WalkerTaskProcess): +# NAME_TEMPLATE = "OpenMM_CPU_Walker_Task-{}" + +# def run_task(self, task): +# print("CPU Walker Task ---->", self.mapper_attributes, task, task.func) +# if "num_threads" in self.mapper_attributes: +# num_threads = self.mapper_attributes["num_threads"] + +# # make the platform kwargs dictionary +# platform_options = {"Threads": str(num_threads)} + +# logger.info(f"Threads={num_threads}") + +# else: +# platform_options = {} + +# return task( +# platform_kwargs=platform_options, +# ) + + +# class OpenMMGPUWalkerTaskProcess(WalkerTaskProcess): +# NAME_TEMPLATE = "OpenMM_GPU_Walker_Task-{}" + +# def run_task(self, task): +# logger.info(f"Starting to run a task as worker {self._worker_idx}") + +# logger.info(f"GPU Walker Task ----> {self.mapper_attributes}") +# # get the platform +# platform = self.mapper_attributes["platform"] + +# # get the device index from the attributes +# device_id = self.mapper_attributes["device_ids"][self._worker_idx] + +# # make the platform kwargs dictionary +# platform_options = {"DeviceIndex": str(device_id)} + +# logger.info(f"platform={platform}, platform_options={platform_options}") + +# return task( +# platform=platform, +# platform_kwargs=platform_options, +# ) + + +PlatformKwargs = dict[str, str] + +class OpenMMRunnerSegmentSplitTimes(TypedDict): + gen_sim_time: float + steps_time: float + get_state_time: float + run_segment_time: float + + +# the runner for the simulation which runs the actual dynamics +class OpenMMRunner(Runner[OpenMMStateWrapper]): + """Runner for OpenMM simulations.""" + + system: openmm.System + topology: openmm.app.Topology + integrator: openmm.Integrator + platform_name: str + platform_kwargs: PlatformKwargs + enforce_box: bool + getState_kwargs: dict[str, bool] + # _cycle_platform: + # _cycle_platform_kwargs: + _last_cycle_segments_split_times: list[OpenMMRunnerSegmentSplitTimes] + + def __init__( + self, + system: openmm.System, + topology: openmm.app.Topology, + integrator: openmm.Integrator, + platform: str | None = None, + platform_kwargs: PlatformKwargs | None = None, + enforce_box: bool = False, + get_state_kwargs: dict[str, bool] | None = None, + ) -> None: + """Constructor for OpenMMRunner. + + Parameters + ---------- + system : + The system (forcefields) for the simulation. + + topology : + The topology for you system. + + integrator : + Integrator for propagating dynamics. + + platform : + The specification for the default computational platform + to use. Platform can also be set when run_segment is + called. If None uses OpenMM default platform, see OpenMM + documentation for all value but typical ones are: + Reference, CUDA, OpenCL. If value is None the automatic + platform determining mechanism in OpenMM will be used. + + platform_kwargs : + key-values to set for a platform with + platform.setPropertyDefaultValue as the default for this + runner. + + enforce_box : + Calls 'context.getState' with 'enforcePeriodicBox' if True. + (Default value = False) + + get_state_kwargs : + key-values to set for getting the state from the OpenMM context. + keys not included will use the values in GET_STATE_KWARG_DEFAULTS. + Will override the enforce_box flag. + + Warnings + -------- + Regarding the enforce_box option. + + When retrieving states from an OpenMM simulation Context, you + have the option to enforce periodic boundary conditions in the + resulting atomic positions in a topology aware way that + doesn't break bonds through boundaries. This is convenient for + post-processing as this can be a complex task and is not + readily exposed in the OpenMM API as a standalone function. + + However, in some types of simulations the periodic box vectors + are ignored (such as implicit solvent ones) despite there + being no option to not have periodic boundaries in the context + itself. Likely if you are running one of these kinds of + simulations you will not pay attention to the box vectors at + all and the random defaults that exist will be very wrong but + this incorrectness will not show in a non-wepy simulation with + openmm unless you are handling the context states + yourself. Then when you run in wepy the default of True to + enforce the boxes will be applied and confusingly wrong + answers will result that are difficult to find root cause of. + + """ + + if platform is not None: + assert isinstance( + platform, str + ), f"platform should be a string, not {type(platform)}" + + # we save the different components. However, if we are to make + # this runner picklable we have to convert the SWIG objects to + # a picklable form + self.system = system + self.integrator = integrator + + # these are not SWIG objects + self.topology = topology + self.platform_name = platform + self.platform_kwargs = platform_kwargs + + self.enforce_box = enforce_box + + self.getState_kwargs = {} + if get_state_kwargs is not None: + for k in get_state_kwargs: + self.getState_kwargs[k] = get_state_kwargs[k] + + # override enforce_box option if specified in get_state_kwargs + if "enforce_box" in get_state_kwargs: + self.enforce_box = get_state_kwargs["enforce_box"] + + else: + self.getState_kwargs = dict(GET_STATE_KWARG_DEFAULTS) + + self._cycle_platform = None + self._cycle_platform_kwargs = None + + # for special monitoring purposes to get split times to debug + # performance + self._last_cycle_segments_split_times = [] + + def pre_cycle( + self, + platform: str | None = None, + platform_kwargs: PlatformKwargs | None = None, + ) -> None: + # choose to use the platform spec in this function call or to + # use the default one saved in the runner + + # if the platform is given locally use this one + if platform is not None: + logger.info( + f"Setting the platform ({platform}) in the 'pre_cycle' OpenMM Runner call" + f"with platform kwargs: {platform_kwargs}" + ) + # set the platform and kwargs for this cycle + self._cycle_platform = platform + self._cycle_platform_kwargs = platform_kwargs + + # otherwise we just don't set this and let resolution of + # platform happen at run segment. + # each segment split times will get appended to this + self._last_cycle_segments_split_times = [] + + def post_cycle(self) -> None: + # remove the platform and kwargs for this cycle + self._cycle_platform = None + self._cycle_platform_kwargs = None + + def _resolve_platform( + self, + platform: str | Literal[Ellipsis] | None, + platform_kwargs: PlatformKwargs | None, + ) -> tuple[ + str | None, + PlatformKwargs | None, + ]: + # resolve which platform to use + + # force usage of environmental one + if platform is Ellipsis: + platform_name = None + platform_kwargs = None + + # use the runtime given one + elif platform is not None: + platform_name = platform + platform_kwargs = platform_kwargs + + # if the pre_cycle configured platform is set use this over + # the default + elif self._cycle_platform is not None: + platform_name = self._cycle_platform + platform_kwargs = self._cycle_platform_kwargs + + # use the default one + elif self.platform_name is not None: + platform_name = self.platform_name + platform_kwargs = self.platform_kwargs + + # if the default is not set fall back to the environmental one + else: + platform_name = None + platform_kwargs = None + + return ( + platform_name, + platform_kwargs, + ) + + def run_segment( + self, + walker_state: OpenMMStateWrapper, + segment_length: int, + getState_kwargs: dict[str, bool] | None = None, + platform: str | Literal[Ellipsis] | None = None, + platform_kwargs: PlatformKwargs | None = None, + ) -> OpenMMStateWrapper: + """Run dynamics for the walker. + + Parameters + ---------- + walker : The walker for which dynamics will be propagated. + + segment_length : The numerical value that specifies how much dynamics are to be run. + + getState_kwargs : Specify the key-word arguments to pass to + simulation.context.getState when getting simulation + states. If None defaults object values. + + + platform : The specification for the computational platform to + use. If None will use the default for the runner and + ignore platform_kwargs. If Ellipsis forces the use of the + OpenMM default or environmentally defined platform. See + OpenMM documentation for all value but typical ones are: + Reference, CUDA, OpenCL. If value is None the automatic + platform determining mechanism in OpenMM will be used. + + platform_kwargs : Key-values to set for a platform with + platform.setPropertyDefaultValue for this segment only. + + + Returns + ------- + new_walker_state : Walker after dynamics was run, only the state should be modified. + + """ + + run_segment_start = time.time() + + # set the kwargs that will be passed to getState + _getState_kwargs = ( + getState_kwargs if getState_kwargs is not None else self.getState_kwargs + ) + logger.info(f"Default 'getState_kwargs' in runner: {self.getState_kwargs}") + logger.info(f"'getState_kwargs' passed to 'run_segment' : {getState_kwargs}") + + logger.info( + "After resolving 'getState_kwargs' that will be used are: " + f"{_getState_kwargs}" + ) + + gen_sim_start = time.time() + + # make a copy of the integrator for this particular segment + new_integrator = copy.copy(self.integrator) + # force setting of random seed to 0, which is a special + # value that forces the integrator to choose another + # random number + new_integrator.setRandomNumberSeed(0) + + ## Platform + + logger.info(f"Default 'platform' in runner: {self.platform_name}") + + logger.info(f"pre_cycle set 'platform' in runner: {self._cycle_platform}") + + logger.info(f"'platform' passed to 'run_segment' : {platform}") + + logger.info(f"Default 'platform_kwargs' in runner: {self.platform_kwargs}") + + logger.info( + f"pre_cycle set 'platform_kwargs' in runner: {self._cycle_platform_kwargs}" + ) + + logger.info(f"'platform_kwargs' passed to 'run_segment' : {platform_kwargs}") + + platform_name, platform_kwargs = self._resolve_platform( + platform, platform_kwargs + ) + + logger.info(f"Resolved 'platform' : {platform_name}") + + logger.info(f"Resolved 'platform_kwargs' : {platform_kwargs}") + + # create simulation object + + ## create the platform and customize + + # if a platform was given we use it to make a Simulation object + if platform_name is not None: + logger.info("Using platform configured in code.") + + # get the platform by its name to use + platform = openmm.Platform.getPlatformByName(platform_name) + logger.info(f"Platform object created: {platform}") + + if platform_kwargs is None: + platform_kwargs = {} + + # set properties from the kwargs if they apply to the platform + for key, value in platform_kwargs.items(): + if key in platform.getPropertyNames(): + logger.info(f"Setting platform property: {key} : {value}") + platform.setPropertyDefaultValue(key, value) + + else: + warn( + f"Platform kwargs given ({key} : {value}) " + f"but is not valid for this platform ({platform_name})" + ) + + # make a new simulation object + simulation = openmm.app.Simulation( + self.topology, self.system, new_integrator, platform + ) + + # otherwise just use the default or environmentally defined one + else: + logger.info("Using environmental platform.") + simulation = openmm.app.Simulation( + self.topology, self.system, new_integrator + ) + + # set the state to the context from the walker + simulation.context.setState(walker_state.sim_state) + + gen_sim_end = time.time() + gen_sim_time = gen_sim_end - gen_sim_start + + logger.info("Time to generate the system: {}".format(gen_sim_time)) + + # actually run the simulation + + steps_start = time.time() + + # Run the simulation segment for the number of time steps + simulation.step(segment_length) + + steps_end = time.time() + steps_time = steps_end - steps_start + + logger.info("Time to run {} sim steps: {}".format(segment_length, steps_time)) + + get_state_start = time.time() + + get_state_end = time.time() + get_state_time = get_state_end - get_state_start + logger.info("Getting context state time: {}".format(get_state_time)) + + # generate the new state + new_state = OpenMMStateWrapper(simulation.context.getState(**_getState_kwargs)) + + run_segment_end = time.time() + run_segment_time = run_segment_end - run_segment_start + logger.info("Total internal run_segment time: {}".format(run_segment_time)) + + segment_split_times = OpenMMRunnerSegmentSplitTimes({ + "gen_sim_time": gen_sim_time, + "steps_time": steps_time, + "get_state_time": get_state_time, + "run_segment_time": run_segment_time, + }) + + self._last_cycle_segments_split_times.append(segment_split_times) + + return new_state + + def last_cycle_segments_split_times(self) -> list[OpenMMRunnerSegmentSplitTimes]: + + return copy.deepcopy(self._last_cycle_segments_split_times) + + +def gen_sim_state( + positions: np.typing.ArrayLike, + system: openmm.System, + integrator: openmm.Integrator, + getState_kwargs: dict[str, bool] | None = None, +) -> openmm.State: + """Convenience function for generating an openmm.State object. + + Parameters + ---------- + positions : arraylike of float + The positions for the system you want to set + + system : openmm.app.System object + + integrator : openmm.Integrator object + + Returns + ------- + sim_state : openmm.State object + + """ + + # handle the getState_kwargs + tmp_getState_kwargs = getState_kwargs + + # start with the defaults + getState_kwargs = dict(GET_STATE_KWARG_DEFAULTS) + + # if there were customizations use them + if tmp_getState_kwargs is not None: + getState_kwargs.update(tmp_getState_kwargs) + + # generate a throwaway context, using the reference platform so we + # don't screw up other platform stuff later in the same process + platform = openmm.Platform.getPlatformByName("Reference") + context = openmm.Context(system, copy.copy(integrator), platform) + + # set the positions + context.setPositions(positions) + + # then just retrieve it as a state using the default kwargs + sim_state = context.getState(**getState_kwargs) + + return sim_state + + +# class OpenMMCPUWorker(Worker): +# """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). + +# This is intended to be used with the wepy.work_mapper.WorkerMapper +# work mapper class. + +# This class must be used in order to ensure OpenMM runs jobs on the +# appropriate GPU device. + +# """ + +# NAME_TEMPLATE = "OpenMMCPUWorker-{}" +# """The name template the worker processes are named to substituting in +# the process number.""" + +# DEFAULT_NUM_THREADS = 1 + +# def __init__(self, *args, **kwargs): +# if "num_threads" not in kwargs: +# num_threads = self.DEFAULT_NUM_THREADS +# else: +# num_threads = kwargs.pop("num_threads") + +# super().__init__(*args, num_threads=num_threads, **kwargs) + +# def run_task(self, task): +# # documented in superclass + +# # make the platform kwargs dictionary +# platform_options = {"Threads": str(self.attributes["num_threads"])} + +# # run the task and pass in the DeviceIndex for OpenMM to +# # assign work to the correct GPU +# return task(platform_kwargs=platform_options) + + +# class OpenMMGPUWorker(Worker): +# """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). + +# This is intended to be used with the wepy.work_mapper.WorkerMapper +# work mapper class. + +# This class must be used in order to ensure OpenMM runs jobs on the +# appropriate GPU device. + +# """ + +# NAME_TEMPLATE = "OpenMMGPUWorker-{}" +# """The name template the worker processes are named to substituting in +# the process number.""" + +# def run_task(self, task): +# # get the platform +# platform = self.mapper_attributes["platform"] + +# # get the device index from the attributes +# device_id = self.mapper_attributes["device_ids"][self._worker_idx] + +# # make the platform kwargs dictionary +# platform_options = {"DeviceIndex": str(device_id)} + +# logger.info(f"platform={platform}, platform_options={platform_options}") + +# return task( +# platform=platform, +# platform_kwargs=platform_options, +# ) + + +# class OpenMMCPUWalkerTaskProcess(WalkerTaskProcess): +# NAME_TEMPLATE = "OpenMM_CPU_Walker_Task-{}" + +# def run_task(self, task): +# print("CPU Walker Task ---->", self.mapper_attributes, task, task.func) +# if "num_threads" in self.mapper_attributes: +# num_threads = self.mapper_attributes["num_threads"] + +# # make the platform kwargs dictionary +# platform_options = {"Threads": str(num_threads)} + +# logger.info(f"Threads={num_threads}") + +# else: +# platform_options = {} + +# return task( +# platform_kwargs=platform_options, +# ) + + +# class OpenMMGPUWalkerTaskProcess(WalkerTaskProcess): +# NAME_TEMPLATE = "OpenMM_GPU_Walker_Task-{}" + +# def run_task(self, task): +# logger.info(f"Starting to run a task as worker {self._worker_idx}") + +# logger.info(f"GPU Walker Task ----> {self.mapper_attributes}") +# # get the platform +# platform = self.mapper_attributes["platform"] + +# # get the device index from the attributes +# device_id = self.mapper_attributes["device_ids"][self._worker_idx] + +# # make the platform kwargs dictionary +# platform_options = {"DeviceIndex": str(device_id)} + +# logger.info(f"platform={platform}, platform_options={platform_options}") + +# return task( +# platform=platform, +# platform_kwargs=platform_options, +# ) + diff --git a/src/wepy/runners/openmm/state.py b/src/wepy/runners/openmm/state.py new file mode 100644 index 00000000..84569c1a --- /dev/null +++ b/src/wepy/runners/openmm/state.py @@ -0,0 +1,977 @@ +import copy +from typing import Literal, get_args, TypeAlias, Union, Any, Self, TypedDict, NotRequired, ClassVar +from collections.abc import Mapping, Collection +import openmm +import openmm.unit +import numpy as np + +import attrs + +from lxml import etree +from immutables import Map as frozenmap + +from wepy.walker import WalkerState +from wepy.core import BugError +from wepy.util.openmm import array3d_to_vec3 + +class OpenMMStateValidationError(Exception): + pass + +PREFERRED_UNITS_LUT = frozenmap( + { + "length": openmm.unit.nanometer, + "time": openmm.unit.picosecond, + "temperature": openmm.unit.kelvin, + "angle": openmm.unit.degrees, + "molar_mass": openmm.unit.amu, + "charge": openmm.unit.elementary_charge, + "molar_energy": openmm.unit.kilojoule / openmm.unit.mole, + "energy": openmm.unit.kilojoule, + "subtance": openmm.unit.mole, + "velocity": openmm.unit.nanometer / openmm.unit.picosecond, + "molar_force": (openmm.unit.kilojoule / openmm.unit.nanometer) / openmm.unit.mole, + "molar_energy_density": openmm.unit.kilojoule / openmm.unit.mole, + } +) + +PREFERRED_UNITS = [val for val in PREFERRED_UNITS_LUT.values()] + + +StateFieldName: TypeAlias = Literal[ + "time", + "box_vectors", + "box_volume", + "positions", + "velocities", + "forces", + "parameters", + "parameter_derivatives", + "kinetic_energy", + "potential_energy", +] + +STATE_FIELD_NAMES: frozenset[StateFieldName] = frozenset(get_args(StateFieldName)) + +CORE_FIELDS: frozenset[StateFieldName] = frozenset( + { + "positions", + "velocities", + "forces", + "parameters", + "parameter_derivatives", + } +) + +BOX_FIELDS: frozenset[StateFieldName] = frozenset( + { + "box_vectors", + "box_volume", + } +) + +ACCESSORY_FIELDS: frozenset[StateFieldName] = frozenset( + { + "time", + } +) + +FieldDataType: TypeAlias = openmm.unit.Quantity | frozenmap[str, Any] + +STATE_FIELD_TYPES: frozenmap[str, type[FieldDataType]] = frozenmap( + time = openmm.unit.Quantity, + box_vectors = openmm.unit.Quantity, + box_volume = openmm.unit.Quantity, + positions = openmm.unit.Quantity, + velocities = openmm.unit.Quantity, + forces = openmm.unit.Quantity, + kinetic_energy = openmm.unit.Quantity, + potential_energy = openmm.unit.Quantity, + parameters = frozenmap[str, Any], + parameter_derivatives = frozenmap[str, Any], +) + +StateDataTypeName: TypeAlias = Literal[ + "positions", + "velocities", + "forces", + "energy", + "parameters", + "parameter_derivatives", + # NOTE: integrator_parameters show up here but are not accessible + # as fields on the state + "integrator_parameters", +] + +STATE_DATA_TYPE_ENUM_NAMES: frozenmap[str, str] = frozenmap( + positions = "Positions", + velocities = "Velocities", + forces = "Forces", + energy = "Energy", + parameters = "Parameters", + parameter_derivatives = "ParameterDerivatives", + integrator_parameters = "IntegratorParameters", +) + +FIELD_GETTER_NAMES: frozenmap[str, str] = frozenmap( + positions = "getPositions", + velocities = "getVelocities", + forces = "getForces", + kinetic_energy = "getKineticEnergy", + potential_energy = "getPotentialEnergy", + time = "getTime", + box_vectors = "getPeriodicBoxVectors", + box_volume = "getPeriodicBoxVolume", + parameters = "getParameters", + parameter_derivatives = "getEnergyParameterDerivatives", +) + +UNREQUESTED_FIELDS: frozenset[StateFieldName] = frozenset( + { + "time", + "box_volume", + "box_vectors", + } +) + +ARRAYLIKE_FIELDS: frozenset[StateFieldName] = frozenset( + { + "positions", + "velocities", + "forces", + "box_vectors", + } +) + +SCALAR_FIELDS: frozenset[StateFieldName] = frozenset( + { + "kinetic_energy", + "potential_energy", + "time", + "box_volume", + } +) + +MAPPING_FIELDS: frozenset[StateFieldName] = frozenset( + { + "parameters", + "parameter_derivatives", + } +) + +ENERGY_FIELDS: frozenset[StateFieldName] = frozenset( + { + "kinetic_energy", + "potential_energy", + } +) + +GetStateKeyWords = Literal[ + "positions", + "velocities", + "forces", + "energy", + "parameters", + "parameterDerivatives", +] + +GET_STATE_KEYWORDS: frozenmap[str, GetStateKeyWords | None] = frozenmap( + positions = "positions", + velocities = "velocities", + forces = "forces", + kinetic_energy = "energy", + potential_energy = "energy", + time = None, + box_vectors = None, + box_volume = None, + parameters = "parameters", + parameter_derivatives = "parameterDerivatives", +) + + + +GET_STATE_DEFAULT_ENFORCE_PERIODIC_BOX = False + +OPENMM_DEFAULT_DIMENSION_UNITS: frozenmap[str, openmm.unit.Unit] = frozenmap( + length = openmm.unit.nanometer, + time = openmm.unit.picosecond, + energy = openmm.unit.kilojoule, + substance = openmm.unit.mole, +) + +OPENMM_DEFAULT_UNITS: frozenmap[str, openmm.unit.Unit] = frozenmap( + positions = openmm.unit.nanometer, + time = openmm.unit.picosecond, + box_vectors = openmm.unit.nanometer, + box_volume = openmm.unit.nanometer**3, + velocities = openmm.unit.nanometer / openmm.unit.picosecond, + forces = openmm.unit.kilojoule / openmm.unit.nanometer, + kinetic_energy = openmm.unit.kilojoule, + potential_energy = openmm.unit.kilojoule, +) + +class StateFieldData(TypedDict): + time: openmm.unit.Quantity + box_vectors: openmm.unit.Quantity + box_volume: openmm.unit.Quantity + + positions: NotRequired[openmm.unit.Quantity] + velocities: NotRequired[openmm.unit.Quantity] + forces: NotRequired[openmm.unit.Quantity] + kinetic_energy: NotRequired[openmm.unit.Quantity] + potential_energy: NotRequired[openmm.unit.Quantity] + parameters: NotRequired[frozenmap[str, Any]] + parameter_derivatives: NotRequired[frozenmap[str, Any]] + +class StateFieldDataInput(TypedDict): + time: openmm.unit.Quantity + box_vectors: openmm.unit.Quantity + + positions: NotRequired[openmm.unit.Quantity] + velocities: NotRequired[openmm.unit.Quantity] + parameters: NotRequired[frozenmap[str, Any]] + +STATE_REQUIRED_INPUT_FIELDS: frozenset[StateFieldName] = frozenset( + {"time", "box_vectors"} +) + +def dummy_context( + system: openmm.System, + positions: openmm.unit.Quantity, + unitcell: openmm.unit.Quantity | None = None, +) -> openmm.Context: + """Create a throwaway OpenMM context. + + This uses some hardcoded integrators, etc. to be able to get a + context which is useful for generating OpenMM objects without + running any calculations. You can also use it for a simulation but + it won't do anything meaningful. + """ + + platform = openmm.Platform.getPlatformByName("Reference") + integrator = openmm.VerletIntegrator(1.0 * openmm.unit.femtoseconds) + context = openmm.Context(system, integrator, platform) + context.setPositions(positions) + + if unitcell is not None: + context.setPeriodicBoxVectors(*unitcell) + + return context + +def get_context_state( + context: openmm.Context, + fields: frozenset[StateFieldName] | None = None, +) -> openmm.State: + """Retrieve a state from a context using field names.""" + + if fields is None: + _fields = STATE_FIELD_NAMES + else: + _fields = fields + + kwargs = {} + for field_name in _fields: + + kwarg = GET_STATE_KEYWORDS[field_name] + if kwarg is not None: + kwargs[kwarg] = True + + return context.getState( + **kwargs, + enforcePeriodicBox=GET_STATE_DEFAULT_ENFORCE_PERIODIC_BOX, + ) + + +def resolve_state_data_type_enum_values() -> frozenmap[str, int]: + """Gets the enum values for each field in the state. + + These are int values which are used for bitflag operations. + """ + enum_values = {} + for our_name, enum_name in STATE_DATA_TYPE_ENUM_NAMES.items(): + enum_values[our_name] = getattr(openmm.State, enum_name) + + return frozenmap(enum_values) + + +# reversed since that is the order we check them in and is a frequent operation +STATE_DATA_TYPE_ENUM_VALUES: tuple[tuple[str, int], ...] = tuple( + sorted( + [(k, v) for k, v in resolve_state_data_type_enum_values().items()], + key=lambda x: x[1], + reverse=True, + ) +) + + +def get_state_core_fields_present( + sim_state: openmm.State, +) -> frozenset[str]: + """Figure out which core data fields are present in the State. + + This does not include accessory attributes: + - time + - box_vectors and box volume + - energies + + Note that this also includes the 'integrator_parameters' which are + not accessible from the state getters. + """ + + flag_sum = sim_state.getDataTypes() + + flag_fields = [] + flag_values = [] + flag_cum = flag_sum + for field_name, flag_value in STATE_DATA_TYPE_ENUM_VALUES: + if flag_value > flag_cum: + continue + elif flag_value == flag_cum: + flag_fields.append(field_name) + flag_values.append(flag_value) + break + + else: + flag_fields.append(field_name) + flag_values.append(flag_value) + flag_cum -= flag_value + + # double check they sum up + assert sum(flag_values) == flag_sum + + return frozenset(flag_fields) + +def get_state_fields_present(sim_state: openmm.State) -> frozenset[StateFieldName]: + """Figure out which accessible state fields are present in a State. + + This includes all the accessible fields that have getters + associated with them. + + Notably this excludes the 'integrator_parameters'. + """ + + # get which core fields are present + present_core_fields = get_state_core_fields_present(sim_state) + + present_fields = set() + + # core fields + for field in { + "positions", + "velocities", + "forces", + "parameters", + "parameter_derivatives", + }: + + if field in present_core_fields: + present_fields.add(field) + + # energy is a little different + if "energy" in present_core_fields: + present_fields = present_fields | ENERGY_FIELDS + + # handle box fields efficiently + try: + sim_state.getPeriodicBoxVectors() + except openmm.OpenMMException: + pass + else: + present_fields.add("box_vectors") + present_fields.add("box_volume") + + # then get whether the rest of the accessory fields are available + try: + sim_state.getTime() + except openmm.OpenMMException: + pass + else: + present_fields.add("time") + + return frozenset(present_fields) + + +# def resolve_state_data_type_enum_values() -> dict[str, int]: +# enum_values = {} +# for our_name, enum_name in STATE_DATA_TYPE_ENUM_NAMES.items(): +# enum_values[our_name] = getattr(openmm.State, enum_name) + +# return enum_values + + +# # reversed since that is the order we check them in and is a frequent operation +# STATE_DATA_TYPE_ENUM_VALUES: list[tuple[str, int]] = list( +# sorted( +# [(k, v) for k, v in resolve_state_data_type_enum_values().items()], +# key=lambda x: x[1], +# reverse=True, +# ) +# ) + + +# def get_state_fields_present(sim_state: openmm.State) -> list[str]: +# """For a state returns a set of the field data types present in it.""" + +# flag_sum = sim_state.getDataTypes() + +# flag_fields: list[str] = [] +# flag_values: list[int] = [] +# flag_cum: int = flag_sum +# for field_name, flag_value in STATE_DATA_TYPE_ENUM_VALUES: +# if flag_value > flag_cum: +# continue +# elif flag_value == flag_cum: +# flag_fields.append(field_name) +# flag_values.append(flag_value) +# break + +# else: +# flag_fields.append(field_name) +# flag_values.append(flag_value) +# flag_cum -= flag_value + +# # double check they sum up +# assert sum(flag_values) == flag_sum + +# return flag_fields + + +# the Units objects that OpenMM uses internally and are returned from +# simulation data + +# TODO: this is never used and we only need the unit names. Its okay +# to use openmm.units here but other runners should use a units sytem +# like pint which is easier to install. So we should remove this since +# its not used. + +# UNITS = (('positions_unit', openmm.unit.nanometer), +# ('time_unit', openmm.unit.picosecond), +# ('box_vectors_unit', openmm.unit.nanometer), +# ('velocities_unit', openmm.unit.nanometer/openmm.unit.picosecond), +# ('forces_unit', openmm.unit.kilojoule / (openmm.unit.nanometer * openmm.unit.mole)), +# ('box_volume_unit', openmm.unit.nanometer), +# ('kinetic_energy_unit', openmm.unit.kilojoule / openmm.unit.mole), +# ('potential_energy_unit', openmm.unit.kilojoule / openmm.unit.mole), +# ) +# """Mapping of units identifiers to the corresponding openmm.units Unit objects.""" + +# the names of the units from the units objects above. This is used +# for saving them to files +UNIT_NAMES: tuple[tuple[str, str], ...] = ( + ("positions_unit", openmm.unit.nanometer.get_name()), + ("time_unit", openmm.unit.picosecond.get_name()), + ("box_vectors_unit", openmm.unit.nanometer.get_name()), + ("velocities_unit", (openmm.unit.nanometer / openmm.unit.picosecond).get_name()), + ( + "forces_unit", + (openmm.unit.kilojoule / (openmm.unit.nanometer * openmm.unit.mole)).get_name(), + ), + ("box_volume_unit", openmm.unit.nanometer.get_name()), + ("kinetic_energy_unit", (openmm.unit.kilojoule / openmm.unit.mole).get_name()), + ("potential_energy_unit", (openmm.unit.kilojoule / openmm.unit.mole).get_name()), +) +"""Mapping of unit identifier strings to the serialized string spec of the unit.""" + + + + + +## Wrapper for a openmm.State + +class OpenMMStateWrapper(WalkerState): + """Useful wrapper around an openmm.State object. + + This object is meant to be an alternative interface to the + openmm.State object and thus supports the same use case of efficient + and fine grained data transfer between the openmm.Context (possible + GPU memory) and local memory. + """ + + state: openmm.State + core_fields: frozenset[StateDataTypeName] + fields: frozenset[StateFieldName] + + def __init__(self, state: openmm.State) -> None: + + self.state = state + + # probe which data fields it has + self.core_fields = get_state_core_fields_present(self.state) + self.fields = get_state_fields_present(self.state) + + def fields_in(self, fields: Collection[StateFieldName]) -> bool: + """Checks if all the fields specified are in the state.""" + + return len(set(fields) - self.fields) == 0 + + def __str__(self) -> str: + + fields_s = ", ".join(self.fields) + return f"StateWrapper[{fields_s}]" + + def __len__(self) -> int: + return len(self.fields) + + def __contains__(self, item: str) -> bool: + return self.fields_in((item,)) + + def __getitem__(self, key) -> FieldDataType: + # if this was a key for data not mapped from the OpenMM.State + # object we use the _data attribute + if key not in STATE_FIELD_NAMES: + raise KeyError(f"Key '{key}' is not a valid key for a state.") + + elif key in STATE_DATA_TYPE_ENUM_NAMES and key not in self.fields: + raise KeyError(f"Core data field '{key}' is not present in this state") + else: + + # resolve the getter function for the field + getter_name = FIELD_GETTER_NAMES[key] + + getter_method = getattr(self.state, getter_name, None) + + if getter_method is None: + raise BugError( + f"No getter ('{getter_name}') for key '{key}'" + ) + + elif key in ARRAYLIKE_FIELDS: + field_val = getter_method(asNumpy=True) + elif key in SCALAR_FIELDS: + field_val = getter_method() + elif key in MAPPING_FIELDS: + field_val = frozenmap(dict(getter_method())) + else: + raise BugError( + f"Unhandled field key {key}, getter '{getter_name}'" + ) + + return field_val + + def __eq__(self, other: Any) -> bool: + + if not isinstance(other, type(self)): + return False + + if self.fields == other.fields: + for field_name in self.fields & (SCALAR_FIELDS | MAPPING_FIELDS): + if self[field_name] != other[field_name]: + return False + + for field_name in self.fields & (ARRAYLIKE_FIELDS - {"box_vectors"}): + if not np.array_equal(self[field_name], other[field_name]): + return False + + else: + return False + + return True + + # Methods for the OpenMMBasicStateProtocol + def get_positions(self) -> openmm.unit.Quantity | None: + return self["positions"] + + def get_unitcell(self) -> openmm.unit.Quantity | None: + return self["unitcell"] + + def to_dict(self) -> StateFieldData: + + return StateFieldData({field: self[field] for field in self.fields}) + + def serialize_xml(self) -> str: + """Serialize the openmm.State to openmm XML format.""" + + return openmm.XmlSerializer.serialize(self.state) + + @classmethod + def from_xml(cls, xml_str: str) -> Self: + """Serialize the openmm.State to openmm XML format.""" + + state = openmm.XmlSerializer.deserialize(xml_str) + return cls(state) + + @classmethod + def from_dict( + cls, + system: openmm.System, + state_dict: StateFieldDataInput, + ) -> Self: + """Convert an input dictionary to a state wrapper and State object. + + Note that the input data structure is slightly different than + the state output. + + For example the box_volume cannot be provided as an input + since it is a derived value from the box vectors. + + See the data structure type for inputs. + + For parameters the parameter must be defined in the system forces. + """ + + if ( + len(missing_fields := STATE_REQUIRED_INPUT_FIELDS - set(state_dict.keys())) + > 0 + ): + + missing_fields_str = ", ".join(missing_fields) + + raise OpenMMStateValidationError( + f"Missing required fields: {missing_fields_str}" + ) + + # a dummy context used to generate a state only + ctx = openmm.Context( + system, + openmm.VerletIntegrator(1.0 * openmm.unit.femtoseconds), + openmm.Platform.getPlatformByName("Reference"), + ) + + # the fields which are always in a state dict + ctx.setTime(state_dict["time"].in_units_of(OPENMM_DEFAULT_UNITS["time"])) + + bvs_vec3 = tuple(v for v in array3d_to_vec3(state_dict["box_vectors"])) * state_dict["box_vectors"].unit + ctx.setPeriodicBoxVectors(*bvs_vec3) + + if "positions" in state_dict: + + ctx.setPositions(state_dict["positions"]) + + if "velocities" in state_dict: + + ctx.setVelocities(state_dict["velocities"]) + + if "parameters" in state_dict: + for name, value in state_dict["parameters"].items(): + try: + ctx.setParameter(name, value) + except openmm.OpenMMException: + raise OpenMMStateValidationError( + f"Could not set parameter '{name}' as there is no matching parameter in the system forces." + ) + + state = get_context_state(ctx, frozenset(state_dict.keys())) + + return cls(state) + +# A plain data structure state + +def _maybe_array_equal( + arr0: openmm.unit.Quantity | None, + arr1: openmm.unit.Quantity | None, +) -> bool: + """Similar to numpy.array_equal for quantities.""" + + if arr0 is None and arr1 is None: + return True + elif arr0 is None or arr1 is None: + return False + else: + return np.array_equal(arr0, arr1) + +def _gen_unit_cube() -> np.typing.ArrayLike: + return np.array([ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.],] + ) + +@attrs.define +class OpenMMState(WalkerState): + """Pure data type for an OpenMM state. + + This type does no wrapping and should be serializable as is. + + This is not mapped to a particular openmm.State object but can be + converted to it. + """ + + time: openmm.unit.Quantity = attrs.field( + eq=attrs.cmp_using(np.array_equal, require_same_type=False), + ) + box_vectors: openmm.unit.Quantity = attrs.field( + eq=attrs.cmp_using(np.array_equal, require_same_type=False), + ) + + box_volume: openmm.unit.Quantity | None = None + + positions: openmm.unit.Quantity | None = attrs.field( + default=None, + eq=attrs.cmp_using(_maybe_array_equal, require_same_type=False), + ) + velocities: openmm.unit.Quantity | None = attrs.field( + default=None, + eq=attrs.cmp_using(_maybe_array_equal, require_same_type=False), + ) + forces: openmm.unit.Quantity | None = attrs.field( + default=None, + eq=attrs.cmp_using(_maybe_array_equal, require_same_type=False), + ) + kinetic_energy: openmm.unit.Quantity | None = None + potential_energy: openmm.unit.Quantity | None = None + parameters: frozenmap[str, Any] | None = None + parameter_derivatives: frozenmap[str, Any] | None = None + + # DWIM: "do what I mean" which corresponds to a common default + # that is somewhat arbitrary but typically the ergonomic single + # way to do something + DWIM_DEFAULT_TIME: ClassVar[openmm.unit.Quantity] = 0 * openmm.unit.picosecond + DWIM_DEFAULT_UNITCELL: ClassVar[openmm.unit.Quantity] = _gen_unit_cube() * openmm.unit.nanometer + + @staticmethod + def _validate_array3ds( + positions: openmm.unit.Quantity | None, + velocities: openmm.unit.Quantity | None, + forces: openmm.unit.Quantity | None, + ) -> bool: + + maybe_nums = { + "positions": positions.shape[0] if positions is not None else None, + "velocities": velocities.shape[0] if velocities is not None else None, + "forces": forces.shape[0] if forces is not None else None, + } + + # check dependending on which attributes are actually given + which_given = [name for name, e in maybe_nums.items() if e is not None] + if len(which_given) < 2: + return True + + elif ( + len(which_given) == 2 + and maybe_nums[which_given[0]] != maybe_nums[which_given[1]] + ): + report = ", ".join(f"{name}={maybe_nums[name]}" for name in which_given) + + raise OpenMMStateValidationError( + f"The number of particles do not match: {report}" + ) + + elif len(which_given) == 3 and ( + maybe_nums[which_given[0]] != maybe_nums[which_given[1]] + or maybe_nums[which_given[0]] != maybe_nums[which_given[2]] + ): + + report = ", ".join(f"{name}={maybe_nums[name]}" for name in which_given) + + raise OpenMMStateValidationError( + f"The number of particles do not match: {report}" + ) + + else: + return True + + def __attrs_post_init__(self) -> None: + + # validate that at least one field is given + if not any(f is not None for f in attrs.astuple(self)): + raise OpenMMStateValidationError( + "At least one field must be provided to construct the object." + ) + + # validate that coordinates all have the same number of particles + self._validate_array3ds(self.positions, self.velocities, self.forces) + + # TODO: These aren't critical so avoiding this for now. + # + # validate that the box_volume matches the vectors + # + # validate that the parameters and derivatives have the same keys + + # Methods for the protocol + def get_positions(self) -> openmm.unit.Quantity | None: + return self.positions + + def get_unitcell(self) -> openmm.unit.Quantity: + return self.unitcell + + def to_dict(self) -> StateFieldData: + return StateFieldData( + { + k: v + for k, v in attrs.asdict(self, recurse=False).items() + if v is not None + } + ) + + @classmethod + def from_dict(cls, data_dict: StateFieldData) -> Self: + return cls(**data_dict) + + @classmethod + def from_state_wrapper(cls, state_wrapper: OpenMMStateWrapper) -> Self: + return cls.from_dict(state_wrapper.to_dict()) + + @classmethod + def from_state(cls, state: openmm.State) -> Self: + return cls.from_state_wrapper(OpenMMStateWrapper(state)) + + def to_state_wrapper( + self, + system: openmm.System | None = None, + ) -> OpenMMStateWrapper: + """Convert this state to a real wrapped openmm.State. + + Notes: + If the optional system is provided this provides a more direct + route for translation. + + If not the state will be serialized and deserialized in memory + to get a state. + """ + + if system is not None: + wrapper = OpenMMStateWrapper.from_dict(system, self.to_dict()) + + else: + + xml_str = state_to_xml(self) + state = openmm.XmlSerializer.deserialize(xml_str) + wrapper = OpenMMStateWrapper(state) + + return wrapper + +def _gen_vec3_element( + name: str, parent: etree.Element, vec: tuple[int, int, int] +) -> etree.Element: + + return etree.SubElement( + parent, + name, + x=str(vec[0]), + y=str(vec[1]), + z=str(vec[2]), + ) + + +def state_to_xml( + state: OpenMMState, + step_count: int = 0, +) -> str: + """Convert to an OpenMM State XML without the need of a system/context.""" + + _time_str = str(state.time.in_units_of(PREFERRED_UNITS_LUT["time"])) + _step_count_str = str(step_count) + + state_el = etree.Element( + "State", + openmmVersion=openmm.version.short_version, + stepCount=_step_count_str, + time=_time_str, + type="State", + version="1", + ) + + # box vectors + box_vectors_el = etree.SubElement( + state_el, + "PeriodicBoxVectors", + ) + + _bvecs = [ + tuple(vec.in_units_of(PREFERRED_UNITS_LUT["length"])) + for vec in tuple(state.box_vectors[i] for i in range(3)) + ] + + for name, vec in zip(("A", "B", "C"), _bvecs, strict=True): + + _gen_vec3_element( + name, + box_vectors_el, + vec, + ) + + if state.positions is not None: + + positions_el = etree.SubElement( + state_el, + "Positions", + ) + + _positions = state.positions.in_units_of(PREFERRED_UNITS_LUT["length"]) + + for atom_vec in _positions: + + _gen_vec3_element( + "Position", + positions_el, + tuple(atom_vec), + ) + + if state.velocities is not None: + + velocities_el = etree.SubElement( + state_el, + "Velocities", + ) + + _velocities = state.velocities.in_units_of(PREFERRED_UNITS_LUT["velocity"]) + + for atom_vec in _velocities: + + _gen_vec3_element( + "Velocity", + velocities_el, + tuple(atom_vec), + ) + + if state.forces is not None: + + forces_el = etree.SubElement( + state_el, + "Forces", + ) + + _forces = state.forces.in_units_of(PREFERRED_UNITS_LUT["molar_force"]) + + for atom_vec in _forces: + + _gen_vec3_element( + "Force", + forces_el, + tuple(atom_vec), + ) + + if state.kinetic_energy is not None or state.potential_energy is not None: + + energies_el = etree.SubElement( + state_el, + "Energies", + **( + { + "KineticEnergy": str( + state.kinetic_energy.in_units_of( + PREFERRED_UNITS_LUT["molar_energy_density"] + ) + ) + } + if state.kinetic_energy is not None + else {} + ), + **( + { + "PotentialEnergy": str( + state.potential_energy.in_units_of( + PREFERRED_UNITS_LUT["molar_energy_density"] + ) + ) + } + if state.potential_energy is not None + else {} + ), + ) + + # TODO: parameters, parameter derivatives. I have no working + # example on how to do this so I am eliding them. + if state.parameters is not None or state.parameter_derivatives is not None: + + warnings.warn( + "A state was provided to the XML serializer with parameters or parameter_derivatives, but these are currently not serialized." + ) + + xml_str = etree.tostring( + state_el, + xml_declaration=True, + pretty_print=True, + ) + + return xml_str.decode() + diff --git a/src/wepy/util/openmm.py b/src/wepy/util/openmm.py new file mode 100644 index 00000000..7f442cf8 --- /dev/null +++ b/src/wepy/util/openmm.py @@ -0,0 +1,24 @@ +from typing import Generator +from collections.abc import Iterable +import numpy as np +import openmm + +def array3d_to_vec3(array: np.typing.ArrayLike) -> Generator[openmm.Vec3, None, None]: + + for row in array: + yield openmm.Vec3(*row.tolist()) + +def vec3_to_array3d(vec3s: Iterable[openmm.Vec3]) -> np.typing.ArrayLike: + + vs = [] + for vec3 in vec3s: + vs.append( + ( + vec3.x, + vec3.y, + vec3.z, + ) + ) + + return np.array(vs) + diff --git a/tests/unit/test_runners/test_openmm.py b/tests/unit/test_runners/test_openmm/test_runner.py similarity index 69% rename from tests/unit/test_runners/test_openmm.py rename to tests/unit/test_runners/test_openmm/test_runner.py index 0bacd72b..570aef8f 100644 --- a/tests/unit/test_runners/test_openmm.py +++ b/tests/unit/test_runners/test_openmm/test_runner.py @@ -1,14 +1,15 @@ -from wepy.runners.openmm import ( +from wepy_tools.systems.lennard_jones import LennardJonesPair +from wepy.runners.openmm.state import ( + dummy_context, resolve_state_data_type_enum_values, GET_STATE_KWARG_DEFAULTS, get_state_fields_present, - OpenMMRunner, OpenMMState, gen_sim_state, - # OpenMMCPUWorker, - # OpenMMGPUWorker, - # OpenMMCPUWalkerTaskProcess, - # OpenMMGPUWalkerTaskProcess, +) + +from wepy.runner.openmm.runner import ( + OpenMMRunner, ) import pytest @@ -18,138 +19,66 @@ import openmm.app import openmm.unit - -def dummy_context( - system: openmm.System, - positions: openmm.unit.Quantity, - unitcell: openmm.unit.Quantity | None = None, -) -> openmm.Context: - """Create a throwaway OpenMM context. - - This uses some hardcoded integrators, etc. to be able to get a - context which is useful for generating OpenMM objects without - running any calculations. You can also use it for a simulation but - it won't do anything meaningful. - """ - - platform = openmm.Platform.getPlatformByName("Reference") - integrator = openmm.VerletIntegrator(1.0 * openmm.unit.femtoseconds) - context = openmm.Context(system, integrator, platform) - context.setPositions(positions) - - if unitcell is not None: - bvs = unitcell.to_vec3() - context.setPeriodicBoxVectors(*bvs) - - return context - - -def n_lj_system( - num_particles: int, - mass: openmm.unit.Quantity = (39.9481 * openmm.unit.dalton), - sigma: openmm.unit.Quantity = (0.3350 * openmm.unit.nanometer), - epsilon: openmm.unit.Quantity = (0.996 * openmm.unit.kilojoules_per_mole), -) -> openmm.System: - - system = openmm.System() - - # single nonbonded force - nb_force = openmm.NonbondedForce() - nb_force.setNonbondedMethod(openmm.NonbondedForce.NoCutoff) - - # TODO: add support for cutoffs - - for idx in range(num_particles): - system.addParticle(mass) - nb_force.addParticle( - 0.0 * openmm.unit.elementary_charge, - sigma, - epsilon, +UNIT_CUBE = np.array( + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] ) - system.addForce(nb_force) - - return system +def test_dummy_context(): + lj_sys = LennardJonesPair() - -def particle_line(num_particles: int) -> openmm.unit.Quantity: - """Initialize a 3D coordinate array.""" - - return ( - np.array([[float(idx), 0.0, 0.0] for idx in range(num_particles)]) - * openmm.unit.angstrom + dummy_context( + lj_sys.system, + np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer ) - -ARGON = openmm.app.Element.getBySymbol("Ar") - - -def n_particle_topology( - num_particles: int, - element: openmm.app.Element = ARGON, -) -> openmm.app.Topology: - """Create a single particle topology from scratch. - - There will only be one chain, and each particle is it's own - residue. - - Box vectors are never set. - """ - - top = openmm.app.Topology() - - chain = top.addChain() - for idx in range(num_particles): - - residue = top.addResidue(element.symbol, chain) - top.addAtom( - element.symbol, - element, - residue, + dummy_context( + lj_sys.system, + np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] ) + * openmm.unit.angstrom, + unitcell=UNIT_CUBE * openmm.unit.nanometer, + ) - return top +def test_resolve_state_data_type_enum_values(): + + assert resolve_state_data_type_enum_values() == frozenmap( + { + "positions": 1, + "velocities": 2, + "forces": 4, + "energy": 8, + "parameters": 16, + "parameter_derivatives": 32, + "integrator_parameters": 64, + } + ) @pytest.fixture def omm_context() -> openmm.Context: - system = n_lj_system(2) + lj_sys = LennardJonesPair() - coords = particle_line(2) - ctx = dummy_context(system, coords) + ctx = dummy_context(lj_sys.system, lj_sys.positions) return ctx -def test_resolve_state_data_type_enum_values(): - - assert resolve_state_data_type_enum_values() == { - "positions": 1, - "velocities": 2, - "energy": 8, - "forces": 4, - "integrator_parameters": 64, - "parameter_derivatives": 32, - "parameters": 16, - } - - -def test_get_state_fields_present(omm_context): - - state = omm_context.getState(positions=True) - assert get_state_fields_present(state) == ["positions"] - - -def test_gen_sim_state(): - - state = gen_sim_state( - positions=particle_line(2), - system=n_lj_system(2), - integrator=openmm.VerletIntegrator(0.002), - ) - class TestOpenMMState: @@ -422,3 +351,4 @@ def test_run_segment(self): # class TestOpenMMGPUWalkerTaskProcess: # pass + diff --git a/tests/unit/test_runners/test_openmm/test_state.py b/tests/unit/test_runners/test_openmm/test_state.py new file mode 100644 index 00000000..72b99df9 --- /dev/null +++ b/tests/unit/test_runners/test_openmm/test_state.py @@ -0,0 +1,852 @@ +# Third Party Library +import numpy as np +import openmm +import openmm.unit +import pytest +from immutables import Map as frozenmap +from lxml import etree + +from wepy_tools.systems.lennard_jones import LennardJonesPair + +from wepy.runners.openmm.state import ( + dummy_context, + resolve_state_data_type_enum_values, + get_context_state, + get_state_core_fields_present, + get_state_fields_present, + OpenMMStateWrapper, + OpenMMStateValidationError, + OpenMMState, + _gen_vec3_element, + state_to_xml, +) + +# First Party Library + +UNIT_CUBE = np.array( + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + ) + +def test_dummy_context(): + lj_sys = LennardJonesPair() + + dummy_context( + lj_sys.system, + np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + ) + + dummy_context( + lj_sys.system, + np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.angstrom, + unitcell=UNIT_CUBE * openmm.unit.nanometer, + ) + +def test_resolve_state_data_type_enum_values(): + + assert resolve_state_data_type_enum_values() == frozenmap( + { + "positions": 1, + "velocities": 2, + "forces": 4, + "energy": 8, + "parameters": 16, + "parameter_derivatives": 32, + "integrator_parameters": 64, + } + ) + + +@pytest.fixture +def omm_context() -> openmm.Context: + + lj_sys = LennardJonesPair() + + + ctx = dummy_context(lj_sys.system, lj_sys.positions) + + return ctx + + +def test_get_context_state(omm_context): + + get_context_state(omm_context, {}) + get_context_state( + omm_context, + { + "positions", + "velocities", + "forces", + "parameters", + "parameter_derivatives", + "kinetic_energy", + "potential_energy", + "time", + "box_vectors", + "box_volume", + }, + ) + + +def test_get_state_core_fields_present(omm_context): + + state = omm_context.getState(positions=True) + assert get_state_core_fields_present(state) == frozenset({"positions"}) + + state = omm_context.getState(positions=True, velocities=True, forces=True) + assert get_state_core_fields_present(state) == frozenset( + {"positions", "velocities", "forces"} + ) + + state = omm_context.getState(parameters=True, parameterDerivatives=True) + assert get_state_core_fields_present(state) == frozenset( + { + "parameters", + "parameter_derivatives", + } + ) + + state = omm_context.getState(energy=True) + assert get_state_core_fields_present(state) == frozenset( + { + "energy", + } + ) + + +def test_get_state_fields_present(omm_context): + + state = omm_context.getState() + assert get_state_fields_present(state) == frozenset( + { + "time", + "box_vectors", + "box_volume", + } + ) + + state = omm_context.getState(positions=True) + assert get_state_fields_present(state) == frozenset( + { + "time", + "box_vectors", + "box_volume", + "positions", + } + ) + + state = omm_context.getState(positions=True, velocities=True, forces=True) + assert get_state_fields_present(state) == frozenset( + { + "time", + "box_vectors", + "box_volume", + "positions", + "velocities", + "forces", + } + ) + + state = omm_context.getState(parameters=True, parameterDerivatives=True) + assert get_state_fields_present(state) == frozenset( + { + "time", + "box_vectors", + "box_volume", + "parameters", + "parameter_derivatives", + } + ) + + state = omm_context.getState(energy=True) + assert get_state_fields_present(state) == frozenset( + { + "time", + "box_vectors", + "box_volume", + "kinetic_energy", + "potential_energy", + } + ) + + +@pytest.mark.usefixtures("omm_context") +class Test_OpenMMStateWrapper: + + def test___init__(self, omm_context): + + state = omm_context.getState( + positions=True, + velocities=True, + forces=True, + ) + + state_wrapper = OpenMMStateWrapper(state) + + assert state_wrapper.core_fields == frozenset( + { + "positions", + "velocities", + "forces", + } + ) + + assert state_wrapper.fields == frozenset( + { + "time", + "box_vectors", + "box_volume", + "positions", + "velocities", + "forces", + } + ) + + # test after running some MD + omm_context.getIntegrator().step(1) + + state = omm_context.getState( + positions=True, + velocities=True, + forces=True, + energy=True, + parameters=True, + parameterDerivatives=True, + ) + + state_wrapper = OpenMMStateWrapper(state) + + assert state_wrapper.fields == frozenset( + { + "time", + "box_vectors", + "box_volume", + "positions", + "velocities", + "forces", + "kinetic_energy", + "potential_energy", + "parameters", + "parameter_derivatives", + } + ) + + def test_fields_in(self, omm_context): + + state = omm_context.getState( + positions=True, + velocities=True, + forces=True, + ) + + state_wrapper = OpenMMStateWrapper(state) + + assert state_wrapper.fields_in( + { + "time", + "box_vectors", + "box_volume", + "positions", + "velocities", + "forces", + } + ) + + assert not state_wrapper.fields_in({"something"}) + assert not state_wrapper.fields_in({"parameters"}) + + def test___contains__(self, omm_context): + + state = omm_context.getState( + positions=True, + velocities=True, + forces=True, + ) + + state_wrapper = OpenMMStateWrapper(state) + + assert "time" in state_wrapper + assert "box_vectors" in state_wrapper + assert "positions" in state_wrapper + + def test___len__(self, omm_context): + + state = omm_context.getState( + positions=True, + velocities=True, + forces=True, + ) + + state_wrapper = OpenMMStateWrapper(state) + assert len(state_wrapper) == 6 + + def test___getitem__(self, omm_context): + + omm_context.getIntegrator().step(1) + + state = omm_context.getState( + positions=True, + velocities=True, + forces=True, + energy=True, + parameters=True, + parameterDerivatives=True, + integratorParameters=True, + ) + + state_wrapper = OpenMMStateWrapper(state) + + # always there + + assert isinstance(state_wrapper["time"], openmm.unit.Quantity) + + # core data + assert isinstance(state_wrapper["positions"], openmm.unit.Quantity) + assert isinstance(state_wrapper["velocities"], openmm.unit.Quantity) + assert isinstance(state_wrapper["forces"], openmm.unit.Quantity) + assert isinstance(state_wrapper["parameters"], frozenmap) + assert isinstance(state_wrapper["parameter_derivatives"], frozenmap) + + # extras + assert isinstance(state_wrapper["potential_energy"], openmm.unit.Quantity) + assert isinstance(state_wrapper["kinetic_energy"], openmm.unit.Quantity) + + # only when there is a box + assert isinstance(state_wrapper["box_vectors"], openmm.unit.Quantity) + assert isinstance(state_wrapper["box_volume"], openmm.unit.Quantity) + + def test___eq__(self, omm_context): + state1 = omm_context.getState() + state_wrapper1 = OpenMMStateWrapper(state1) + state2 = omm_context.getState() + state_wrapper2 = OpenMMStateWrapper(state2) + + assert state_wrapper1 == state_wrapper2 + + def test_to_dict(self, omm_context): + + state = omm_context.getState() + + state_wrapper = OpenMMStateWrapper(state) + assert set(state_wrapper.to_dict().keys()) == { + "time", + "box_vectors", + "box_volume", + } + + state = omm_context.getState( + positions=True, + velocities=True, + forces=True, + energy=True, + parameters=True, + parameterDerivatives=True, + integratorParameters=True, + ) + + state_wrapper = OpenMMStateWrapper(state) + assert set(state_wrapper.to_dict().keys()) == { + "time", + "box_vectors", + "box_volume", + "positions", + "velocities", + "forces", + "parameters", + "parameter_derivatives", + "kinetic_energy", + "potential_energy", + } + + def test_serialize_xml(self, omm_context): + + state = omm_context.getState() + + state_wrapper = OpenMMStateWrapper(state) + + assert openmm.XmlSerializer.serialize(state) == state_wrapper.serialize_xml() + + def test_from_xml(self, omm_context): + + state = omm_context.getState() + + state_xml = openmm.XmlSerializer.serialize(state) + + state_wrapper = OpenMMStateWrapper.from_xml(state_xml) + + assert state_wrapper == OpenMMStateWrapper(state) + + def test_from_dict(self, omm_context): + + lj_sys = LennardJonesPair() + system = lj_sys.system + + state_d = { + "time": 0.0 * openmm.unit.seconds, + "box_vectors": UNIT_CUBE * openmm.unit.nanometer, + } + + sw = OpenMMStateWrapper.from_dict(system, state_d) + assert sw is not None + assert "time" in sw + assert "box_vectors" in sw + assert "box_volume" in sw + + state_d = { + "time": 0.0 * openmm.unit.seconds, + "box_vectors": UNIT_CUBE * openmm.unit.nanometer, + "positions": lj_sys.positions, + "velocities": np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond, + # TODO: to tes this I need a system with a parametrizable + # force + # + # "parameters" : { + # "a" : 1.0, + # "b" : 2.0 + # } + } + + sw = OpenMMStateWrapper.from_dict(system, state_d) + assert sw is not None + assert "time" in sw + assert "box_vectors" in sw + assert "box_volume" in sw + assert "positions" in sw + + with pytest.raises(OpenMMStateValidationError): + OpenMMStateWrapper.from_dict(system, {}) + + # invalid parameter + with pytest.raises(OpenMMStateValidationError): + OpenMMStateWrapper.from_dict( + system, + { + "parameters": { + "a": 1.0, + } + }, + ) + +class Test_OpenMMState: + + def test__validate_array3ds(self): + + pos1 = ( + np.array( + [ + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + ) + vel1 = ( + np.array( + [ + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond + ) + force1 = ( + np.array( + [ + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.kilojoule + / (openmm.unit.nanometer * openmm.unit.mole) + ) + + good_cases = [ + (None, None, None), + (pos1, None, None), + (None, vel1, None), + (None, None, force1), + (pos1, vel1, None), + (pos1, None, force1), + (None, vel1, force1), + (pos1, vel1, force1), + ] + + for c in good_cases: + assert OpenMMState._validate_array3ds(*c) is True + + pos2 = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + ) + vel2 = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond + ) + force2 = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.kilojoule + / (openmm.unit.nanometer * openmm.unit.mole) + ) + + bad_cases = [ + (pos1, vel2, None), + (pos1, None, force2), + (pos1, vel2, force2), + ] + + for c in bad_cases: + with pytest.raises(OpenMMStateValidationError): + OpenMMState._validate_array3ds(*c) + + def test___init__(self): + + bvs = UNIT_CUBE * openmm.unit.nanometer + + OpenMMState( + time=0.0 * openmm.unit.picosecond, + box_vectors=bvs, + ) + OpenMMState( + time=0.0 * openmm.unit.picosecond, + box_vectors=bvs, + positions=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer, + velocities=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond, + forces=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.kilojoule + / (openmm.unit.nanometer * openmm.unit.mole), + ) + + def test_from_state(self, omm_context): + s = OpenMMState.from_state(omm_context.getState()) + + assert s.time is not None + assert s.box_vectors is not None + assert s.box_volume is not None + + s = OpenMMState.from_state(omm_context.getState(positions=True)) + assert s.positions is not None + + omm_context.setVelocitiesToTemperature(300.0 * openmm.unit.kelvin) + s = OpenMMState.from_state( + omm_context.getState( + positions=True, + velocities=True, + ) + ) + + assert s.velocities is not None + + # get some forces to actually use + omm_context.getIntegrator().step(0) + + s = OpenMMState.from_state( + omm_context.getState( + positions=True, + velocities=True, + forces=True, + ) + ) + assert s.forces is not None + + s = OpenMMState.from_state( + omm_context.getState( + positions=True, + velocities=True, + forces=True, + energy=True, + parameters=True, + parameterDerivatives=True, + ) + ) + + assert s.kinetic_energy is not None + assert s.potential_energy is not None + + assert s.parameters is not None + assert s.parameter_derivatives is not None + + def test_from_state_wrapper(self, omm_context): + + sw = OpenMMStateWrapper( + omm_context.getState( + positions=True, + velocities=True, + forces=True, + energy=True, + parameters=True, + parameterDerivatives=True, + ) + ) + OpenMMState.from_state_wrapper(sw) + + def test_to_dict(self): + + time = 0.0 * openmm.unit.picosecond + bvs = UNIT_CUBE * openmm.unit.nanometer + + positions = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + ) + velocities = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond + ) + forces = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.kilojoule + / (openmm.unit.nanometer * openmm.unit.mole) + ) + + os = OpenMMState( + time=time, + box_vectors=bvs, + positions=positions, + velocities=velocities, + forces=forces, + ) + + osd = os.to_dict() + assert set(osd.keys()) == { + "time", + "box_vectors", + "positions", + "velocities", + "forces", + } + + def test_from_dict(self): + + time = 0.0 * openmm.unit.picosecond + bvs = UNIT_CUBE * openmm.unit.nanometer + + positions = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + ) + velocities = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond + ) + forces = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.kilojoule + / (openmm.unit.nanometer * openmm.unit.mole) + ) + + d = dict( + time=time, + box_vectors=bvs, + positions=positions, + velocities=velocities, + forces=forces, + ) + assert OpenMMState.from_dict(d) == OpenMMState(**d) + + def test_to_state_wrapper(self): + + time = 0.0 * openmm.unit.picosecond + positions = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + ) + velocities = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond + ) + forces = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.kilojoule + / (openmm.unit.nanometer * openmm.unit.mole) + ) + + unitcell = UNIT_CUBE * openmm.unit.nanometer + + # with optional system + lj_sys = LennardJonesPair() + system = lj_sys.system + os = OpenMMState( + time=time, + box_vectors=unitcell, + positions=positions, + velocities=velocities, + forces=forces, + ) + + os.to_state_wrapper(system) + + # without the system + os = OpenMMState( + time=time, + box_vectors=unitcell, + positions=positions, + velocities=velocities, + forces=forces, + ) + + os.to_state_wrapper() + + +def test__gen_vec3_element(): + + root = etree.Element("root") + + el = _gen_vec3_element("position", root, (0, 1, 2)) + + assert el.tag == "position" + assert dict(el.attrib) == { + "x": "0", + "y": "1", + "z": "2", + } + + +def test_state_to_xml(): + + bvs = UNIT_CUBE * openmm.unit.nanometer + + simple_state = OpenMMState( + time=0.0 * openmm.unit.picosecond, + box_vectors=bvs, + ) + + simple_xml = state_to_xml(simple_state) + + assert isinstance(openmm.XmlSerializer.deserialize(simple_xml), openmm.State) + + full_state = OpenMMState( + time=0.0 * openmm.unit.picosecond, + box_vectors=bvs, + positions=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer, + velocities=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond, + forces=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.kilojoule + / (openmm.unit.nanometer * openmm.unit.mole), + potential_energy=(1.0 * (openmm.unit.kilojoule / openmm.unit.mole)), + kinetic_energy=(1.0 * (openmm.unit.kilojoule / openmm.unit.mole)), + ) + + full_xml = state_to_xml(full_state) + + assert isinstance(openmm.XmlSerializer.deserialize(full_xml), openmm.State) + diff --git a/tests/unit/test_util/test_openmm.py b/tests/unit/test_util/test_openmm.py new file mode 100644 index 00000000..ec4f5273 --- /dev/null +++ b/tests/unit/test_util/test_openmm.py @@ -0,0 +1,40 @@ +import numpy as np +import openmm +from wepy.util.openmm import array3d_to_vec3, vec3_to_array3d + +def test_array3d_to_vec3(): + assert tuple( + array3d_to_vec3( + np.array( + [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ] + ) + ) + ) == tuple( + [ + openmm.Vec3(0.0, 0.0, 0.0), + openmm.Vec3(0.0, 0.0, 0.0), + ] + ) + +def test_vec3_to_array3d(): + + assert np.array_equal( + vec3_to_array3d( + tuple( + [ + openmm.Vec3(0.0, 0.0, 0.0), + openmm.Vec3(0.0, 0.0, 0.0), + ] + ) + ), + np.array( + [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ] + ), + ) + diff --git a/uv.lock b/uv.lock index 42ae9105..2d60c3ee 100644 --- a/uv.lock +++ b/uv.lock @@ -766,6 +766,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] +[[package]] +name = "immutables" +version = "0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/41/0ccaa6ef9943c0609ec5aa663a3b3e681c1712c1007147b84590cec706a0/immutables-0.21.tar.gz", hash = "sha256:b55ffaf0449790242feb4c56ab799ea7af92801a0a43f9e2f4f8af2ab24dfc4a", size = 89008, upload-time = "2024-10-10T00:55:01.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/f9/0c46f600702b815182212453f5514c0070ee168b817cdf7c3767554c8489/immutables-0.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1ed262094b755903122c3c3a83ad0e0d5c3ab7887cda12b2fe878769d1ee0d", size = 31885, upload-time = "2024-10-10T00:54:19.406Z" }, + { url = "https://files.pythonhosted.org/packages/29/34/7608d2eab6179aa47e8f59ab0fbd5b3eeb2333d78c9dc2da0de8de4ed322/immutables-0.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce604f81d9d8f26e60b52ebcb56bb5c0462c8ea50fb17868487d15f048a2f13e", size = 31537, upload-time = "2024-10-10T00:54:20.998Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/cb9e2bb7a69338155ffabbd2f993c968c750dd2d5c6c6eaa6ebb7bfcbdfa/immutables-0.21-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b48b116aaca4500398058b5a87814857a60c4cb09417fecc12d7da0f5639b73d", size = 104270, upload-time = "2024-10-10T00:54:21.912Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a4/25df835a9b9b372a4a869a8a1ac30a32199f2b3f581ad0e249f7e3d19eed/immutables-0.21-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dad7c0c74b285cc0e555ec0e97acbdc6f1862fcd16b99abd612df3243732e741", size = 104864, upload-time = "2024-10-10T00:54:22.956Z" }, + { url = "https://files.pythonhosted.org/packages/4a/51/b548fbc657134d658e179ee8d201ae82d9049aba5c3cb2d858ed2ecb7e3f/immutables-0.21-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e44346e2221a5a676c880ca8e0e6429fa24d1a4ae562573f5c04d7f2e759b030", size = 99733, upload-time = "2024-10-10T00:54:23.99Z" }, + { url = "https://files.pythonhosted.org/packages/47/db/d7b1e0e88faf07fe9a88579a86f58078a9a37fff871f4b3dbcf28cad9a12/immutables-0.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b10139b529a460e53fe8be699ebd848c54c8a33ebe67763bcfcc809a475a26f", size = 101698, upload-time = "2024-10-10T00:54:25.734Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/6fe42a1a053dd8cfb9f45e91d5246522637c7287dc6bd347f67aedf7aedb/immutables-0.21-cp312-cp312-win32.whl", hash = "sha256:fc512d808662614feb17d2d92e98f611d69669a98c7af15910acf1dc72737038", size = 30977, upload-time = "2024-10-10T00:54:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/63/45/d062aca6971e99454ce3ae42a7430037227fee961644ed1f8b6c9b99e0a5/immutables-0.21-cp312-cp312-win_amd64.whl", hash = "sha256:461dcb0f58a131045155e52a2c43de6ec2fe5ba19bdced6858a3abb63cee5111", size = 35088, upload-time = "2024-10-10T00:54:28.388Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/60da6f5a3c3f64e0b3940c4ad86e1971d9d2eb8b13a179c26eda5ec6a298/immutables-0.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:79674b51aa8dd983f9ac55f7f67b433b1df84a6b4f28ab860588389a5659485b", size = 31922, upload-time = "2024-10-10T00:54:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/9b/89/5420f1d16a652024fcccc9c07d46d4157fcaf33ff37c82412c83fc16ef36/immutables-0.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93c8350f8f7d0d9693f708229d9d0578e6f3b785ce6da4bced1da97137aacfad", size = 31552, upload-time = "2024-10-10T00:54:30.282Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d0/a5fb7c164ddb298ec37537e618b70dfa30c7cae9fac01de374c36489cbc9/immutables-0.21-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:583d2a63e444ce1538cc2bda56ae1f4a1a11473dbc0377c82b516bc7eec3b81e", size = 104334, upload-time = "2024-10-10T00:54:31.284Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a5/5fda0ee4a261a85124011ac0750fec678f00e1b2d4a5502b149a3b4d86d9/immutables-0.21-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b274a52da9b106db55eceb93fc1aea858c4e6f4740189e3548e38613eafc2021", size = 104898, upload-time = "2024-10-10T00:54:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/93/fa/d46bfe92f2c66d35916344176ff87fa839aac9c16849652947e722b7a15f/immutables-0.21-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:338bede057250b33716a3e4892e15df0bf5a5ddbf1d67ead996b3e680b49ef9e", size = 99966, upload-time = "2024-10-10T00:54:34.046Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f5/2a19e2e095f7a39d8d77dcc10669734d2d99773ce00c99bdcfeeb7d714e6/immutables-0.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8781c89583b68f604cf30f0978b722165824c3075888639fde771bf1a3e12dc0", size = 101773, upload-time = "2024-10-10T00:54:35.851Z" }, + { url = "https://files.pythonhosted.org/packages/86/80/5b6ee53f836cf2067ced997efbf2ce20890627f150c3089ea50cf607e783/immutables-0.21-cp313-cp313-win32.whl", hash = "sha256:e97ea83befad873712f283c0cccd630f70cba753e207b4868af28d5b85e9dc54", size = 30988, upload-time = "2024-10-10T00:54:37.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/07/f623e6da78368fc0b1772f4877afbf60f34c4cc93f1a8f1006507afa21ec/immutables-0.21-cp313-cp313-win_amd64.whl", hash = "sha256:cfcb23bd898f5a4ef88692b42c51f52ca7373a35ba4dcc215060a668639eb5da", size = 35147, upload-time = "2024-10-10T00:54:38.558Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -1194,6 +1218,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/40/23569737873cc9637fd488606347e9dd92b9fa37ba4fcda1f98ee5219a97/latexcodec-3.0.1-py3-none-any.whl", hash = "sha256:a9eb8200bff693f0437a69581f7579eb6bca25c4193515c09900ce76451e452e", size = 18532, upload-time = "2025-06-17T18:47:30.726Z" }, ] +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -2890,6 +2994,7 @@ dependencies = [ { name = "click" }, { name = "geomm" }, { name = "h5py" }, + { name = "immutables" }, { name = "jinja2" }, { name = "networkx" }, { name = "nptyping" }, @@ -2909,6 +3014,7 @@ graphics = [ { name = "pillow" }, ] md = [ + { name = "lxml" }, { name = "mdtraj" }, { name = "openmm" }, { name = "openmm-systems" }, @@ -2925,6 +3031,7 @@ dev = [ { name = "interrogate" }, { name = "ipython" }, { name = "isort" }, + { name = "lxml" }, { name = "mypy" }, { name = "nbsphinx" }, { name = "notebook" }, @@ -2948,7 +3055,9 @@ requires-dist = [ { name = "click" }, { name = "geomm" }, { name = "h5py", specifier = ">=3" }, + { name = "immutables" }, { name = "jinja2" }, + { name = "lxml", marker = "extra == 'md'" }, { name = "matplotlib", marker = "extra == 'graphics'" }, { name = "mdtraj", marker = "extra == 'md'" }, { name = "networkx" }, @@ -2974,6 +3083,7 @@ dev = [ { name = "interrogate" }, { name = "ipython" }, { name = "isort" }, + { name = "lxml" }, { name = "mypy" }, { name = "nbsphinx" }, { name = "notebook" }, From f8c8b7bcdd6355b18881965cf2cf73348737b326 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 3 Dec 2025 14:47:56 -0500 Subject: [PATCH 040/143] finish reimplementing the openmm runner --- src/wepy/runners/openmm/runner.py | 1272 +---------------- src/wepy/runners/openmm/state.py | 99 +- .../test_runners/test_openmm/test_runner.py | 307 +--- .../test_runners/test_openmm/test_state.py | 243 +++- 4 files changed, 397 insertions(+), 1524 deletions(-) diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index d07296ed..3a9eba3a 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -1,434 +1,3 @@ -"""OpenMM molecular dynamics runner with accessory classes. - -OpenMM is a library with support for running molecular dynamics -simulations with specific support for fast GPU calculations. The -component based architecture of OpenMM makes it a perfect fit with -wepy. - -In addition to the principle OpenMMRunner class there are a few -classes here that make using OpenMM runner more efficient. - -First is a WalkerState class (OpenMMState) that wraps the openmm state -object directly, itself is a wrapper around the C++ -datastructures. This gives better performance by not performing copies -to a WalkerState dictionary. - -Second, is the OpenMMWalker which is identical to the Walker class -except that it enforces the state is an actual instantiation of -OpenMMState. Use of this is optional. - -Finally, is the OpenMMGPUWorker class. This is to be used as the -worker type for the WorkerMapper work mapper. This is necessary to -allow passing of the device index to OpenMM for which GPU device to -use. - -""" - -# Standard Library -from typing import Any, Annotated, TypedDict, NotRequired, Literal, Final, Self, get_args, TypeAlias -import logging -import multiprocessing as mp -import itertools -import time -from warnings import warn -import copy - -# Third Party Library -from immutables import Map as frozenmap -import attrs -import numpy as np - -logger = logging.getLogger(__name__) - -try: - import mdtraj -except ModuleNotFoundError: - warn("Module 'mdtraj' not found, those features will not be available.") - -try: - # Third Party Library - import openmm - import openmm.app - import openmm.unit -except ModuleNotFoundError: - raise ModuleNotFoundError( - "OpenMM has not been installed, which this runner requires." - ) - -# First Party Library -from wepy.runners.runner import Runner -from wepy.util.util import box_vectors_to_lengths_angles -from wepy.walker import WalkerState -# from wepy.work_mapper.task_mapper import WalkerTaskProcess -# from wepy.work_mapper.worker import Worker - -# AtomNDArray = NDArray[Shape["N atoms, 3 dimensions"], Floating] -# BoxVectorsNDArray = NDArray[Shape["3, 3"], Floating] - -## Constants - -StateFieldName = Literal[ - "positions", - "velocities", - "forces", - "kinetic_energy", - "potential_energy", - "time", - "box_vectors", - "box_volume", - "parameters", - "parameter_derivatives", -] - -# OPENMM_STATE_FIELD_KEYS: frozenset[str, ...] = frozenset(get_args(OpenMMStateFields)) -# """Names of the fields of the OpenMMState.""" -STATE_FIELD_NAMES: frozenset[StateFieldName] = frozenset(get_args(StateFieldName)) - -FieldDataType: TypeAlias = openmm.UnitQuantity | frozenmap[str, Any] - -StateDataTypeName: TypeAlias = Literal[ - "positions", - "velocities", - "forces", - "energy", - "parameters", - "parameter_derivatives", - # NOTE: integrator_parameters show up here but are not accessible - # as fields on the state - "integrator_parameters", -] - -STATE_DATA_TYPE_ENUM_NAMES = frozenmap( - { - "positions": "Positions", - "velocities": "Velocities", - "forces": "Forces", - "energy": "Energy", - "parameters": "Parameters", - "parameter_derivatives": "ParameterDerivatives", - "integrator_parameters": "IntegratorParameters", - } -) - -FIELD_GETTER_NAMES = frozenmap( - { - "positions": "getPositions", - "velocities": "getVelocities", - "forces": "getForces", - "kinetic_energy": "getKineticEnergy", - "potential_energy": "getPotentialEnergy", - "time": "getTime", - "box_vectors": "getPeriodicBoxVectors", - "box_volume": "getPeriodicBoxVolume", - "parameters": "getParameters", - "parameter_derivatives": "getEnergyParameterDerivatives", - } -) - -GetStateKeyWords = Literal[ - "positions", - "velocities", - "forces", - "energy", - "parameters", - "parameterDerivatives", -] - -GET_STATE_KEYWORDS: frozenmap[StateFieldName, GetStateKeyWords | None] = frozenmap({ - "positions": "positions", - "velocities": "velocities", - "forces": "forces", - "kinetic_energy": "energy", - "potential_energy": "energy", - "time": None, - "box_vectors": None, - "box_volume": None, - "parameters": "parameters", - "parameter_derivatives": "parameterDerivatives", -}) - -GET_STATE_DEFAULT_ENFORCE_PERIODIC_BOX = False - - -# when we use the get_state function from the simulation context we -# can pass options for what kind of data to get, this is the default -# to get all the data. TODO not really sure what the 'groups' keyword -# is for though -GET_STATE_KWARG_DEFAULTS = frozenmap({ - "getPositions" : True, - "getVelocities" : True, - "getForces" : True, - "getEnergy" : True, - "getParameters" : True, - "getParameterDerivatives" : False, - "enforcePeriodicBox" : True, -}) -"""Mapping of key word arguments to the simulation.context.getState -method for retrieving data for a simulation state. By default we set -each as True to retrieve all information. The presence or absence of -them is handled by the OpenMMState. - -""" - -def get_context_state( - context: openmm.Context, - fields: frozenset[StateFieldName] | None = None, -) -> openmm.State: - """Retrieve a state from a context using field names.""" - - if fields is None: - _fields = STATE_FIELD_NAMES - else: - _fields = fields - - kwargs = {} - for field_name in _fields: - - kwarg = GET_STATE_KEYWORDS[field_name] - if kwarg is not None: - kwargs[kwarg] = True - - return context.getState( - **kwargs, - enforcePeriodicBox=GET_STATE_DEFAULT_ENFORCE_PERIODIC_BOX, - ) - - -def resolve_state_data_type_enum_values() -> frozenmap[StateDataTypeName, int]: - """Gets the enum values for each field in the state. - - These are int values which are used for bitflag operations. - """ - enum_values = {} - for our_name, enum_name in STATE_DATA_TYPE_ENUM_NAMES.items(): - enum_values[our_name] = getattr(openmm.State, enum_name) - - return frozenmap(enum_values) - - -# reversed since that is the order we check them in and is a frequent operation -STATE_DATA_TYPE_ENUM_VALUES: tuple[tuple[StateDataTypeName, int], ...] = tuple( - sorted( - [(k, v) for k, v in resolve_state_data_type_enum_values().items()], - key=lambda x: x[1], - reverse=True, - ) -) - - -def get_state_core_fields_present( - sim_state: openmm.State, -) -> frozenset[StateDataTypeName]: - """Figure out which core data fields are present in the State. - - This does not include accessory attributes: - - time - - box_vectors and box volume - - energies - - Note that this also includes the 'integrator_parameters' which are - not accessible from the state getters. - """ - - flag_sum = sim_state.getDataTypes() - - flag_fields = [] - flag_values = [] - flag_cum = flag_sum - for field_name, flag_value in STATE_DATA_TYPE_ENUM_VALUES: - if flag_value > flag_cum: - continue - elif flag_value == flag_cum: - flag_fields.append(field_name) - flag_values.append(flag_value) - break - - else: - flag_fields.append(field_name) - flag_values.append(flag_value) - flag_cum -= flag_value - - # double check they sum up - assert sum(flag_values) == flag_sum - - return frozenset(flag_fields) - -def get_state_fields_present(sim_state: openmm.State) -> frozenset[StateFieldName]: - """Figure out which accessible state fields are present in a State. - - This includes all the accessible fields that have getters - associated with them. - - Notably this excludes the 'integrator_parameters'. - """ - - # get which core fields are present - present_core_fields = get_state_core_fields_present(sim_state) - - present_fields = set() - - # core fields - for field in { - "positions", - "velocities", - "forces", - "parameters", - "parameter_derivatives", - }: - - if field in present_core_fields: - present_fields.add(field) - - # energy is a little different - if "energy" in present_core_fields: - present_fields = present_fields | ENERGY_FIELDS - - # handle box fields efficiently - try: - sim_state.getPeriodicBoxVectors() - except openmm.OpenMMException: - pass - else: - present_fields.add("box_vectors") - present_fields.add("box_volume") - - # then get whether the rest of the accessory fields are available - try: - sim_state.getTime() - except openmm.OpenMMException: - pass - else: - present_fields.add("time") - - return frozenset(present_fields) - - -# def resolve_state_data_type_enum_values() -> dict[str, int]: -# enum_values = {} -# for our_name, enum_name in STATE_DATA_TYPE_ENUM_NAMES.items(): -# enum_values[our_name] = getattr(openmm.State, enum_name) - -# return enum_values - - -# # reversed since that is the order we check them in and is a frequent operation -# STATE_DATA_TYPE_ENUM_VALUES: list[tuple[str, int]] = list( -# sorted( -# [(k, v) for k, v in resolve_state_data_type_enum_values().items()], -# key=lambda x: x[1], -# reverse=True, -# ) -# ) - - -# def get_state_fields_present(sim_state: openmm.State) -> list[str]: -# """For a state returns a set of the field data types present in it.""" - -# flag_sum = sim_state.getDataTypes() - -# flag_fields: list[str] = [] -# flag_values: list[int] = [] -# flag_cum: int = flag_sum -# for field_name, flag_value in STATE_DATA_TYPE_ENUM_VALUES: -# if flag_value > flag_cum: -# continue -# elif flag_value == flag_cum: -# flag_fields.append(field_name) -# flag_values.append(flag_value) -# break - -# else: -# flag_fields.append(field_name) -# flag_values.append(flag_value) -# flag_cum -= flag_value - -# # double check they sum up -# assert sum(flag_values) == flag_sum - -# return flag_fields - - -# the Units objects that OpenMM uses internally and are returned from -# simulation data - -# TODO: this is never used and we only need the unit names. Its okay -# to use openmm.units here but other runners should use a units sytem -# like pint which is easier to install. So we should remove this since -# its not used. - -# UNITS = (('positions_unit', openmm.unit.nanometer), -# ('time_unit', openmm.unit.picosecond), -# ('box_vectors_unit', openmm.unit.nanometer), -# ('velocities_unit', openmm.unit.nanometer/openmm.unit.picosecond), -# ('forces_unit', openmm.unit.kilojoule / (openmm.unit.nanometer * openmm.unit.mole)), -# ('box_volume_unit', openmm.unit.nanometer), -# ('kinetic_energy_unit', openmm.unit.kilojoule / openmm.unit.mole), -# ('potential_energy_unit', openmm.unit.kilojoule / openmm.unit.mole), -# ) -# """Mapping of units identifiers to the corresponding openmm.units Unit objects.""" - -# the names of the units from the units objects above. This is used -# for saving them to files -UNIT_NAMES: tuple[tuple[str, str], ...] = ( - ("positions_unit", openmm.unit.nanometer.get_name()), - ("time_unit", openmm.unit.picosecond.get_name()), - ("box_vectors_unit", openmm.unit.nanometer.get_name()), - ("velocities_unit", (openmm.unit.nanometer / openmm.unit.picosecond).get_name()), - ( - "forces_unit", - (openmm.unit.kilojoule / (openmm.unit.nanometer * openmm.unit.mole)).get_name(), - ), - ("box_volume_unit", openmm.unit.nanometer.get_name()), - ("kinetic_energy_unit", (openmm.unit.kilojoule / openmm.unit.mole).get_name()), - ("potential_energy_unit", (openmm.unit.kilojoule / openmm.unit.mole).get_name()), -) -"""Mapping of unit identifier strings to the serialized string spec of the unit.""" - -# a random seed will be chosen from 1 to RAND_SEED_RANGE_MAX when the -# Langevin integrator is created. 0 is the default and special value -# which will then choose a random value when the integrator is created - -# TODO: test this isn't needed -# RAND_SEED_RANGE_MAX = 1000000 - - -class OpenMMStateDict(TypedDict, total=False): - positions: np.typing.ArrayLike - velocities: np.typing.ArrayLike - forces: np.typing.ArrayLike - kinetic_energy: float - potential_energy: float - time: float - box_vectors: np.typing.ArrayLike - box_volume: float - # TODO: parameters - -"""OpenMM molecular dynamics runner with accessory classes. - -OpenMM is a library with support for running molecular dynamics -simulations with specific support for fast GPU calculations. The -component based architecture of OpenMM makes it a perfect fit with -wepy. - -In addition to the principle OpenMMRunner class there are a few -classes here that make using OpenMM runner more efficient. - -First is a WalkerState class (OpenMMState) that wraps the openmm state -object directly, itself is a wrapper around the C++ -datastructures. This gives better performance by not performing copies -to a WalkerState dictionary. - -Second, is the OpenMMWalker which is identical to the Walker class -except that it enforces the state is an actual instantiation of -OpenMMState. Use of this is optional. - -Finally, is the OpenMMGPUWorker class. This is to be used as the -worker type for the WorkerMapper work mapper. This is necessary to -allow passing of the device index to OpenMM for which GPU device to -use. - -""" - # Standard Library from typing import Any, Annotated, TypedDict, NotRequired, Literal, Final, Self, get_args, TypeAlias import logging @@ -464,21 +33,7 @@ class OpenMMStateDict(TypedDict, total=False): from wepy.runners.runner import Runner from wepy.util.util import box_vectors_to_lengths_angles from wepy.walker import WalkerState -# from wepy.work_mapper.task_mapper import WalkerTaskProcess -# from wepy.work_mapper.worker import Worker - -# AtomNDArray = NDArray[Shape["N atoms, 3 dimensions"], Floating] -# BoxVectorsNDArray = NDArray[Shape["3, 3"], Floating] - -## Constants - - -# a random seed will be chosen from 1 to RAND_SEED_RANGE_MAX when the -# Langevin integrator is created. 0 is the default and special value -# which will then choose a random value when the integrator is created - -# TODO: test this isn't needed -# RAND_SEED_RANGE_MAX = 1000000 +from .state import OpenMMState, OpenMMStateWrapper, get_context_state PlatformKwargs = dict[str, str] @@ -488,206 +43,50 @@ class OpenMMRunnerSegmentSplitTimes(TypedDict): get_state_time: float run_segment_time: float +GET_STATE_DEFAULT_KEYS = frozenset({ + "positions", + "velocities", + "forces", + "parameters", + "parameter_derivatives", + "kinetic_energy", + "potential_energy", + "time", + "box_vectors", + "box_volume", +}) # the runner for the simulation which runs the actual dynamics -class OpenMMRunner(Runner[OpenMMStateWrapper]): +@attrs.define +class OpenMMRunner(Runner): """Runner for OpenMM simulations.""" + # TODO: should probably have these as the serialized versions for + # going across process boundaries system: openmm.System topology: openmm.app.Topology integrator: openmm.Integrator - platform_name: str - platform_kwargs: PlatformKwargs - enforce_box: bool - getState_kwargs: dict[str, bool] - # _cycle_platform: - # _cycle_platform_kwargs: - _last_cycle_segments_split_times: list[OpenMMRunnerSegmentSplitTimes] - - def __init__( - self, - system: openmm.System, - topology: openmm.app.Topology, - integrator: openmm.Integrator, - platform: str | None = None, - platform_kwargs: PlatformKwargs | None = None, - enforce_box: bool = False, - get_state_kwargs: dict[str, bool] | None = None, - ) -> None: - """Constructor for OpenMMRunner. - - Parameters - ---------- - system : - The system (forcefields) for the simulation. - - topology : - The topology for you system. - - integrator : - Integrator for propagating dynamics. - - platform : - The specification for the default computational platform - to use. Platform can also be set when run_segment is - called. If None uses OpenMM default platform, see OpenMM - documentation for all value but typical ones are: - Reference, CUDA, OpenCL. If value is None the automatic - platform determining mechanism in OpenMM will be used. - - platform_kwargs : - key-values to set for a platform with - platform.setPropertyDefaultValue as the default for this - runner. - - enforce_box : - Calls 'context.getState' with 'enforcePeriodicBox' if True. - (Default value = False) - - get_state_kwargs : - key-values to set for getting the state from the OpenMM context. - keys not included will use the values in GET_STATE_KWARG_DEFAULTS. - Will override the enforce_box flag. - - Warnings - -------- - Regarding the enforce_box option. - - When retrieving states from an OpenMM simulation Context, you - have the option to enforce periodic boundary conditions in the - resulting atomic positions in a topology aware way that - doesn't break bonds through boundaries. This is convenient for - post-processing as this can be a complex task and is not - readily exposed in the OpenMM API as a standalone function. - - However, in some types of simulations the periodic box vectors - are ignored (such as implicit solvent ones) despite there - being no option to not have periodic boundaries in the context - itself. Likely if you are running one of these kinds of - simulations you will not pay attention to the box vectors at - all and the random defaults that exist will be very wrong but - this incorrectness will not show in a non-wepy simulation with - openmm unless you are handling the context states - yourself. Then when you run in wepy the default of True to - enforce the boxes will be applied and confusingly wrong - answers will result that are difficult to find root cause of. - - """ - - if platform is not None: - assert isinstance( - platform, str - ), f"platform should be a string, not {type(platform)}" - - # we save the different components. However, if we are to make - # this runner picklable we have to convert the SWIG objects to - # a picklable form - self.system = system - self.integrator = integrator - - # these are not SWIG objects - self.topology = topology - self.platform_name = platform - self.platform_kwargs = platform_kwargs + platform_name: str | None = None + global_platform_kwargs: PlatformKwargs | None = None + enforce_box: bool = False + get_state_keys: frozenset[str] = attrs.field(default=GET_STATE_DEFAULT_KEYS) - self.enforce_box = enforce_box - - self.getState_kwargs = {} - if get_state_kwargs is not None: - for k in get_state_kwargs: - self.getState_kwargs[k] = get_state_kwargs[k] - - # override enforce_box option if specified in get_state_kwargs - if "enforce_box" in get_state_kwargs: - self.enforce_box = get_state_kwargs["enforce_box"] - - else: - self.getState_kwargs = dict(GET_STATE_KWARG_DEFAULTS) - - self._cycle_platform = None - self._cycle_platform_kwargs = None - - # for special monitoring purposes to get split times to debug - # performance - self._last_cycle_segments_split_times = [] + _last_cycle_segments_split_times: list[OpenMMRunnerSegmentSplitTimes] = attrs.field(default=[]) def pre_cycle( self, - platform: str | None = None, - platform_kwargs: PlatformKwargs | None = None, ) -> None: - # choose to use the platform spec in this function call or to - # use the default one saved in the runner - - # if the platform is given locally use this one - if platform is not None: - logger.info( - f"Setting the platform ({platform}) in the 'pre_cycle' OpenMM Runner call" - f"with platform kwargs: {platform_kwargs}" - ) - # set the platform and kwargs for this cycle - self._cycle_platform = platform - self._cycle_platform_kwargs = platform_kwargs - - # otherwise we just don't set this and let resolution of - # platform happen at run segment. - # each segment split times will get appended to this self._last_cycle_segments_split_times = [] def post_cycle(self) -> None: - # remove the platform and kwargs for this cycle - self._cycle_platform = None - self._cycle_platform_kwargs = None - - def _resolve_platform( - self, - platform: str | Literal[Ellipsis] | None, - platform_kwargs: PlatformKwargs | None, - ) -> tuple[ - str | None, - PlatformKwargs | None, - ]: - # resolve which platform to use - - # force usage of environmental one - if platform is Ellipsis: - platform_name = None - platform_kwargs = None - - # use the runtime given one - elif platform is not None: - platform_name = platform - platform_kwargs = platform_kwargs - - # if the pre_cycle configured platform is set use this over - # the default - elif self._cycle_platform is not None: - platform_name = self._cycle_platform - platform_kwargs = self._cycle_platform_kwargs - - # use the default one - elif self.platform_name is not None: - platform_name = self.platform_name - platform_kwargs = self.platform_kwargs - - # if the default is not set fall back to the environmental one - else: - platform_name = None - platform_kwargs = None - - return ( - platform_name, - platform_kwargs, - ) + pass def run_segment( self, - walker_state: OpenMMStateWrapper, + walker_state: OpenMMState, segment_length: int, - getState_kwargs: dict[str, bool] | None = None, - platform: str | Literal[Ellipsis] | None = None, platform_kwargs: PlatformKwargs | None = None, - ) -> OpenMMStateWrapper: + ) -> OpenMMState: """Run dynamics for the walker. Parameters @@ -696,19 +95,6 @@ def run_segment( segment_length : The numerical value that specifies how much dynamics are to be run. - getState_kwargs : Specify the key-word arguments to pass to - simulation.context.getState when getting simulation - states. If None defaults object values. - - - platform : The specification for the computational platform to - use. If None will use the default for the runner and - ignore platform_kwargs. If Ellipsis forces the use of the - OpenMM default or environmentally defined platform. See - OpenMM documentation for all value but typical ones are: - Reference, CUDA, OpenCL. If value is None the automatic - platform determining mechanism in OpenMM will be used. - platform_kwargs : Key-values to set for a platform with platform.setPropertyDefaultValue for this segment only. @@ -722,17 +108,7 @@ def run_segment( run_segment_start = time.time() # set the kwargs that will be passed to getState - _getState_kwargs = ( - getState_kwargs if getState_kwargs is not None else self.getState_kwargs - ) - logger.info(f"Default 'getState_kwargs' in runner: {self.getState_kwargs}") - logger.info(f"'getState_kwargs' passed to 'run_segment' : {getState_kwargs}") - - logger.info( - "After resolving 'getState_kwargs' that will be used are: " - f"{_getState_kwargs}" - ) - + gen_sim_start = time.time() # make a copy of the integrator for this particular segment @@ -740,57 +116,49 @@ def run_segment( # force setting of random seed to 0, which is a special # value that forces the integrator to choose another # random number + logger.info("Setting random seed to special value: 0") new_integrator.setRandomNumberSeed(0) ## Platform - logger.info(f"Default 'platform' in runner: {self.platform_name}") - - logger.info(f"pre_cycle set 'platform' in runner: {self._cycle_platform}") - - logger.info(f"'platform' passed to 'run_segment' : {platform}") - - logger.info(f"Default 'platform_kwargs' in runner: {self.platform_kwargs}") - - logger.info( - f"pre_cycle set 'platform_kwargs' in runner: {self._cycle_platform_kwargs}" - ) + logger.info(f"'global_platform_kwargs' in runner: {self.global_platform_kwargs}") logger.info(f"'platform_kwargs' passed to 'run_segment' : {platform_kwargs}") - platform_name, platform_kwargs = self._resolve_platform( - platform, platform_kwargs - ) + match (self.global_platform_kwargs, platform_kwargs): + case (None, None): + _platform_kwargs = {} + case (global_kwargs, None): + _platform_kwargs = global_kwargs + case (None, local_kwargs): + _platform_kwargs = local_kwargs + case (global_kwargs, local_kwargs): + _platform_kwargs = self.global_platform_kwargs | platform_kwargs - logger.info(f"Resolved 'platform' : {platform_name}") - - logger.info(f"Resolved 'platform_kwargs' : {platform_kwargs}") + logger.info(f"Resolved 'platform_kwargs' : {_platform_kwargs}") # create simulation object ## create the platform and customize # if a platform was given we use it to make a Simulation object - if platform_name is not None: + if self.platform_name is not None: logger.info("Using platform configured in code.") # get the platform by its name to use - platform = openmm.Platform.getPlatformByName(platform_name) + platform = openmm.Platform.getPlatformByName(self.platform_name) logger.info(f"Platform object created: {platform}") - if platform_kwargs is None: - platform_kwargs = {} - # set properties from the kwargs if they apply to the platform - for key, value in platform_kwargs.items(): + for key, value in _platform_kwargs.items(): if key in platform.getPropertyNames(): logger.info(f"Setting platform property: {key} : {value}") platform.setPropertyDefaultValue(key, value) else: - warn( + logger.warning( f"Platform kwargs given ({key} : {value}) " - f"but is not valid for this platform ({platform_name})" + f"but is not valid for this platform ({self.platform_name})" ) # make a new simulation object @@ -800,13 +168,19 @@ def run_segment( # otherwise just use the default or environmentally defined one else: - logger.info("Using environmental platform.") + logger.info("Using OpenMM default platform resolution.") simulation = openmm.app.Simulation( self.topology, self.system, new_integrator ) - # set the state to the context from the walker - simulation.context.setState(walker_state.sim_state) + # generate a sim state + logger.info("Generating openmm.State from input OpenMMState") + state_wrapper = walker_state.to_state_wrapper() + + + # set in the context + logger.info("Setting openmm.State into current context") + simulation.context.setState(state_wrapper.state) gen_sim_end = time.time() gen_sim_time = gen_sim_end - gen_sim_start @@ -827,503 +201,22 @@ def run_segment( get_state_start = time.time() - get_state_end = time.time() - get_state_time = get_state_end - get_state_start - logger.info("Getting context state time: {}".format(get_state_time)) - # generate the new state - new_state = OpenMMStateWrapper(simulation.context.getState(**_getState_kwargs)) - - run_segment_end = time.time() - run_segment_time = run_segment_end - run_segment_start - logger.info("Total internal run_segment time: {}".format(run_segment_time)) - - segment_split_times = OpenMMRunnerSegmentSplitTimes({ - "gen_sim_time": gen_sim_time, - "steps_time": steps_time, - "get_state_time": get_state_time, - "run_segment_time": run_segment_time, - }) - - self._last_cycle_segments_split_times.append(segment_split_times) - - return new_state - - def last_cycle_segments_split_times(self) -> list[OpenMMRunnerSegmentSplitTimes]: - - return copy.deepcopy(self._last_cycle_segments_split_times) - + logger.info(f"Fetching fields {self.get_state_keys} from context state") - -# class OpenMMCPUWorker(Worker): -# """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). - -# This is intended to be used with the wepy.work_mapper.WorkerMapper -# work mapper class. - -# This class must be used in order to ensure OpenMM runs jobs on the -# appropriate GPU device. - -# """ - -# NAME_TEMPLATE = "OpenMMCPUWorker-{}" -# """The name template the worker processes are named to substituting in -# the process number.""" - -# DEFAULT_NUM_THREADS = 1 - -# def __init__(self, *args, **kwargs): -# if "num_threads" not in kwargs: -# num_threads = self.DEFAULT_NUM_THREADS -# else: -# num_threads = kwargs.pop("num_threads") - -# super().__init__(*args, num_threads=num_threads, **kwargs) - -# def run_task(self, task): -# # documented in superclass - -# # make the platform kwargs dictionary -# platform_options = {"Threads": str(self.attributes["num_threads"])} - -# # run the task and pass in the DeviceIndex for OpenMM to -# # assign work to the correct GPU -# return task(platform_kwargs=platform_options) - - -# class OpenMMGPUWorker(Worker): -# """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). - -# This is intended to be used with the wepy.work_mapper.WorkerMapper -# work mapper class. - -# This class must be used in order to ensure OpenMM runs jobs on the -# appropriate GPU device. - -# """ - -# NAME_TEMPLATE = "OpenMMGPUWorker-{}" -# """The name template the worker processes are named to substituting in -# the process number.""" - -# def run_task(self, task): -# # get the platform -# platform = self.mapper_attributes["platform"] - -# # get the device index from the attributes -# device_id = self.mapper_attributes["device_ids"][self._worker_idx] - -# # make the platform kwargs dictionary -# platform_options = {"DeviceIndex": str(device_id)} - -# logger.info(f"platform={platform}, platform_options={platform_options}") - -# return task( -# platform=platform, -# platform_kwargs=platform_options, -# ) - - -# class OpenMMCPUWalkerTaskProcess(WalkerTaskProcess): -# NAME_TEMPLATE = "OpenMM_CPU_Walker_Task-{}" - -# def run_task(self, task): -# print("CPU Walker Task ---->", self.mapper_attributes, task, task.func) -# if "num_threads" in self.mapper_attributes: -# num_threads = self.mapper_attributes["num_threads"] - -# # make the platform kwargs dictionary -# platform_options = {"Threads": str(num_threads)} - -# logger.info(f"Threads={num_threads}") - -# else: -# platform_options = {} - -# return task( -# platform_kwargs=platform_options, -# ) - - -# class OpenMMGPUWalkerTaskProcess(WalkerTaskProcess): -# NAME_TEMPLATE = "OpenMM_GPU_Walker_Task-{}" - -# def run_task(self, task): -# logger.info(f"Starting to run a task as worker {self._worker_idx}") - -# logger.info(f"GPU Walker Task ----> {self.mapper_attributes}") -# # get the platform -# platform = self.mapper_attributes["platform"] - -# # get the device index from the attributes -# device_id = self.mapper_attributes["device_ids"][self._worker_idx] - -# # make the platform kwargs dictionary -# platform_options = {"DeviceIndex": str(device_id)} - -# logger.info(f"platform={platform}, platform_options={platform_options}") - -# return task( -# platform=platform, -# platform_kwargs=platform_options, -# ) - - -PlatformKwargs = dict[str, str] - -class OpenMMRunnerSegmentSplitTimes(TypedDict): - gen_sim_time: float - steps_time: float - get_state_time: float - run_segment_time: float - - -# the runner for the simulation which runs the actual dynamics -class OpenMMRunner(Runner[OpenMMStateWrapper]): - """Runner for OpenMM simulations.""" - - system: openmm.System - topology: openmm.app.Topology - integrator: openmm.Integrator - platform_name: str - platform_kwargs: PlatformKwargs - enforce_box: bool - getState_kwargs: dict[str, bool] - # _cycle_platform: - # _cycle_platform_kwargs: - _last_cycle_segments_split_times: list[OpenMMRunnerSegmentSplitTimes] - - def __init__( - self, - system: openmm.System, - topology: openmm.app.Topology, - integrator: openmm.Integrator, - platform: str | None = None, - platform_kwargs: PlatformKwargs | None = None, - enforce_box: bool = False, - get_state_kwargs: dict[str, bool] | None = None, - ) -> None: - """Constructor for OpenMMRunner. - - Parameters - ---------- - system : - The system (forcefields) for the simulation. - - topology : - The topology for you system. - - integrator : - Integrator for propagating dynamics. - - platform : - The specification for the default computational platform - to use. Platform can also be set when run_segment is - called. If None uses OpenMM default platform, see OpenMM - documentation for all value but typical ones are: - Reference, CUDA, OpenCL. If value is None the automatic - platform determining mechanism in OpenMM will be used. - - platform_kwargs : - key-values to set for a platform with - platform.setPropertyDefaultValue as the default for this - runner. - - enforce_box : - Calls 'context.getState' with 'enforcePeriodicBox' if True. - (Default value = False) - - get_state_kwargs : - key-values to set for getting the state from the OpenMM context. - keys not included will use the values in GET_STATE_KWARG_DEFAULTS. - Will override the enforce_box flag. - - Warnings - -------- - Regarding the enforce_box option. - - When retrieving states from an OpenMM simulation Context, you - have the option to enforce periodic boundary conditions in the - resulting atomic positions in a topology aware way that - doesn't break bonds through boundaries. This is convenient for - post-processing as this can be a complex task and is not - readily exposed in the OpenMM API as a standalone function. - - However, in some types of simulations the periodic box vectors - are ignored (such as implicit solvent ones) despite there - being no option to not have periodic boundaries in the context - itself. Likely if you are running one of these kinds of - simulations you will not pay attention to the box vectors at - all and the random defaults that exist will be very wrong but - this incorrectness will not show in a non-wepy simulation with - openmm unless you are handling the context states - yourself. Then when you run in wepy the default of True to - enforce the boxes will be applied and confusingly wrong - answers will result that are difficult to find root cause of. - - """ - - if platform is not None: - assert isinstance( - platform, str - ), f"platform should be a string, not {type(platform)}" - - # we save the different components. However, if we are to make - # this runner picklable we have to convert the SWIG objects to - # a picklable form - self.system = system - self.integrator = integrator - - # these are not SWIG objects - self.topology = topology - self.platform_name = platform - self.platform_kwargs = platform_kwargs - - self.enforce_box = enforce_box - - self.getState_kwargs = {} - if get_state_kwargs is not None: - for k in get_state_kwargs: - self.getState_kwargs[k] = get_state_kwargs[k] - - # override enforce_box option if specified in get_state_kwargs - if "enforce_box" in get_state_kwargs: - self.enforce_box = get_state_kwargs["enforce_box"] - - else: - self.getState_kwargs = dict(GET_STATE_KWARG_DEFAULTS) - - self._cycle_platform = None - self._cycle_platform_kwargs = None - - # for special monitoring purposes to get split times to debug - # performance - self._last_cycle_segments_split_times = [] - - def pre_cycle( - self, - platform: str | None = None, - platform_kwargs: PlatformKwargs | None = None, - ) -> None: - # choose to use the platform spec in this function call or to - # use the default one saved in the runner - - # if the platform is given locally use this one - if platform is not None: - logger.info( - f"Setting the platform ({platform}) in the 'pre_cycle' OpenMM Runner call" - f"with platform kwargs: {platform_kwargs}" - ) - # set the platform and kwargs for this cycle - self._cycle_platform = platform - self._cycle_platform_kwargs = platform_kwargs - - # otherwise we just don't set this and let resolution of - # platform happen at run segment. - # each segment split times will get appended to this - self._last_cycle_segments_split_times = [] - - def post_cycle(self) -> None: - # remove the platform and kwargs for this cycle - self._cycle_platform = None - self._cycle_platform_kwargs = None - - def _resolve_platform( - self, - platform: str | Literal[Ellipsis] | None, - platform_kwargs: PlatformKwargs | None, - ) -> tuple[ - str | None, - PlatformKwargs | None, - ]: - # resolve which platform to use - - # force usage of environmental one - if platform is Ellipsis: - platform_name = None - platform_kwargs = None - - # use the runtime given one - elif platform is not None: - platform_name = platform - platform_kwargs = platform_kwargs - - # if the pre_cycle configured platform is set use this over - # the default - elif self._cycle_platform is not None: - platform_name = self._cycle_platform - platform_kwargs = self._cycle_platform_kwargs - - # use the default one - elif self.platform_name is not None: - platform_name = self.platform_name - platform_kwargs = self.platform_kwargs - - # if the default is not set fall back to the environmental one - else: - platform_name = None - platform_kwargs = None - - return ( - platform_name, - platform_kwargs, - ) - - def run_segment( - self, - walker_state: OpenMMStateWrapper, - segment_length: int, - getState_kwargs: dict[str, bool] | None = None, - platform: str | Literal[Ellipsis] | None = None, - platform_kwargs: PlatformKwargs | None = None, - ) -> OpenMMStateWrapper: - """Run dynamics for the walker. - - Parameters - ---------- - walker : The walker for which dynamics will be propagated. - - segment_length : The numerical value that specifies how much dynamics are to be run. - - getState_kwargs : Specify the key-word arguments to pass to - simulation.context.getState when getting simulation - states. If None defaults object values. - - - platform : The specification for the computational platform to - use. If None will use the default for the runner and - ignore platform_kwargs. If Ellipsis forces the use of the - OpenMM default or environmentally defined platform. See - OpenMM documentation for all value but typical ones are: - Reference, CUDA, OpenCL. If value is None the automatic - platform determining mechanism in OpenMM will be used. - - platform_kwargs : Key-values to set for a platform with - platform.setPropertyDefaultValue for this segment only. - - - Returns - ------- - new_walker_state : Walker after dynamics was run, only the state should be modified. - - """ - - run_segment_start = time.time() - - # set the kwargs that will be passed to getState - _getState_kwargs = ( - getState_kwargs if getState_kwargs is not None else self.getState_kwargs - ) - logger.info(f"Default 'getState_kwargs' in runner: {self.getState_kwargs}") - logger.info(f"'getState_kwargs' passed to 'run_segment' : {getState_kwargs}") - - logger.info( - "After resolving 'getState_kwargs' that will be used are: " - f"{_getState_kwargs}" + new_omm_state = get_context_state( + simulation.context, + self.get_state_keys, ) - gen_sim_start = time.time() - - # make a copy of the integrator for this particular segment - new_integrator = copy.copy(self.integrator) - # force setting of random seed to 0, which is a special - # value that forces the integrator to choose another - # random number - new_integrator.setRandomNumberSeed(0) - - ## Platform - - logger.info(f"Default 'platform' in runner: {self.platform_name}") - - logger.info(f"pre_cycle set 'platform' in runner: {self._cycle_platform}") - - logger.info(f"'platform' passed to 'run_segment' : {platform}") - - logger.info(f"Default 'platform_kwargs' in runner: {self.platform_kwargs}") - - logger.info( - f"pre_cycle set 'platform_kwargs' in runner: {self._cycle_platform_kwargs}" - ) - - logger.info(f"'platform_kwargs' passed to 'run_segment' : {platform_kwargs}") - - platform_name, platform_kwargs = self._resolve_platform( - platform, platform_kwargs - ) - - logger.info(f"Resolved 'platform' : {platform_name}") - - logger.info(f"Resolved 'platform_kwargs' : {platform_kwargs}") - - # create simulation object - - ## create the platform and customize - - # if a platform was given we use it to make a Simulation object - if platform_name is not None: - logger.info("Using platform configured in code.") - - # get the platform by its name to use - platform = openmm.Platform.getPlatformByName(platform_name) - logger.info(f"Platform object created: {platform}") - - if platform_kwargs is None: - platform_kwargs = {} - - # set properties from the kwargs if they apply to the platform - for key, value in platform_kwargs.items(): - if key in platform.getPropertyNames(): - logger.info(f"Setting platform property: {key} : {value}") - platform.setPropertyDefaultValue(key, value) - - else: - warn( - f"Platform kwargs given ({key} : {value}) " - f"but is not valid for this platform ({platform_name})" - ) - - # make a new simulation object - simulation = openmm.app.Simulation( - self.topology, self.system, new_integrator, platform - ) - - # otherwise just use the default or environmentally defined one - else: - logger.info("Using environmental platform.") - simulation = openmm.app.Simulation( - self.topology, self.system, new_integrator - ) - - # set the state to the context from the walker - simulation.context.setState(walker_state.sim_state) - - gen_sim_end = time.time() - gen_sim_time = gen_sim_end - gen_sim_start - - logger.info("Time to generate the system: {}".format(gen_sim_time)) - - # actually run the simulation - - steps_start = time.time() - - # Run the simulation segment for the number of time steps - simulation.step(segment_length) - - steps_end = time.time() - steps_time = steps_end - steps_start - - logger.info("Time to run {} sim steps: {}".format(segment_length, steps_time)) - - get_state_start = time.time() - + new_state_wrapper = OpenMMStateWrapper(new_omm_state) + new_state = OpenMMState.from_state_wrapper(new_state_wrapper) + get_state_end = time.time() get_state_time = get_state_end - get_state_start logger.info("Getting context state time: {}".format(get_state_time)) - # generate the new state - new_state = OpenMMStateWrapper(simulation.context.getState(**_getState_kwargs)) - run_segment_end = time.time() run_segment_time = run_segment_end - run_segment_start logger.info("Total internal run_segment time: {}".format(run_segment_time)) @@ -1344,53 +237,6 @@ def last_cycle_segments_split_times(self) -> list[OpenMMRunnerSegmentSplitTimes] return copy.deepcopy(self._last_cycle_segments_split_times) -def gen_sim_state( - positions: np.typing.ArrayLike, - system: openmm.System, - integrator: openmm.Integrator, - getState_kwargs: dict[str, bool] | None = None, -) -> openmm.State: - """Convenience function for generating an openmm.State object. - - Parameters - ---------- - positions : arraylike of float - The positions for the system you want to set - - system : openmm.app.System object - - integrator : openmm.Integrator object - - Returns - ------- - sim_state : openmm.State object - - """ - - # handle the getState_kwargs - tmp_getState_kwargs = getState_kwargs - - # start with the defaults - getState_kwargs = dict(GET_STATE_KWARG_DEFAULTS) - - # if there were customizations use them - if tmp_getState_kwargs is not None: - getState_kwargs.update(tmp_getState_kwargs) - - # generate a throwaway context, using the reference platform so we - # don't screw up other platform stuff later in the same process - platform = openmm.Platform.getPlatformByName("Reference") - context = openmm.Context(system, copy.copy(integrator), platform) - - # set the positions - context.setPositions(positions) - - # then just retrieve it as a state using the default kwargs - sim_state = context.getState(**getState_kwargs) - - return sim_state - - # class OpenMMCPUWorker(Worker): # """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). diff --git a/src/wepy/runners/openmm/state.py b/src/wepy/runners/openmm/state.py index 84569c1a..f1638091 100644 --- a/src/wepy/runners/openmm/state.py +++ b/src/wepy/runners/openmm/state.py @@ -1,4 +1,5 @@ import copy +import logging from typing import Literal, get_args, TypeAlias, Union, Any, Self, TypedDict, NotRequired, ClassVar from collections.abc import Mapping, Collection import openmm @@ -10,10 +11,13 @@ from lxml import etree from immutables import Map as frozenmap +from wepy.missing import MISSING from wepy.walker import WalkerState from wepy.core import BugError from wepy.util.openmm import array3d_to_vec3 +logger = logging.getLogger(__name__) + class OpenMMStateValidationError(Exception): pass @@ -716,6 +720,7 @@ class OpenMMState(WalkerState): # way to do something DWIM_DEFAULT_TIME: ClassVar[openmm.unit.Quantity] = 0 * openmm.unit.picosecond DWIM_DEFAULT_UNITCELL: ClassVar[openmm.unit.Quantity] = _gen_unit_cube() * openmm.unit.nanometer + DWIM_DEFAULT_BOX_VOLUME: ClassVar[openmm.unit.Quantity] = 0. * (openmm.unit.nanometer ** 3) @staticmethod def _validate_array3ds( @@ -776,26 +781,82 @@ def __attrs_post_init__(self) -> None: # # validate that the parameters and derivatives have the same keys - # Methods for the protocol - def get_positions(self) -> openmm.unit.Quantity | None: - return self.positions + def __len__(self) -> int: - def get_unitcell(self) -> openmm.unit.Quantity: - return self.unitcell + count = 0 + for key in STATE_FIELD_NAMES: + if getattr(self, key, MISSING) is not None: + count += 1 - def to_dict(self) -> StateFieldData: - return StateFieldData( - { - k: v - for k, v in attrs.asdict(self, recurse=False).items() - if v is not None - } - ) + return count + + def __contains__(self, key: str) -> bool: + + if key not in STATE_FIELD_NAMES: + return False + + elif getattr(self, key, MISSING) is None: + return False + + else: + return True + + def __getitem__(self, key: str) -> openmm.unit.Quantity: + + if key not in STATE_FIELD_NAMES: + raise KeyError(f"Field {key} is not a valid OpenMMState key") + + elif getattr(self, key, MISSING) is None: + raise ValueError(f"Field {key} has no value.") + + else: + return getattr(self, key, None) @classmethod def from_dict(cls, data_dict: StateFieldData) -> Self: return cls(**data_dict) + + @classmethod + def from_dwim( + cls, + positions: openmm.unit.Quantity, + time: openmm.unit.Quantity | None = None, + box_volume: openmm.unit.Quantity | None = None, + box_vectors: openmm.unit.Quantity | None = None, + velocities: openmm.unit.Quantity | None = None, + forces: openmm.unit.Quantity | None = None, + kinetic_energy: openmm.unit.Quantity | None = None, + potential_energy: openmm.unit.Quantity | None = None, + parameters: frozenmap[str, Any] | None = None, + parameter_derivatives: frozenmap[str, Any] | None = None, + ): + + return cls( + time=( + cls.DWIM_DEFAULT_TIME + if time is None + else time + ), + box_volume=( + cls.DWIM_DEFAULT_BOX_VOLUME + if box_volume is None + else box_volume + ), + positions=positions, + box_vectors=( + cls.DWIM_DEFAULT_UNITCELL + if box_vectors is None + else box_vectors + ), + velocities=velocities, + forces=forces, + kinetic_energy=kinetic_energy, + potential_energy=potential_energy, + parameters=parameters, + parameter_derivatives=parameter_derivatives, + ) + @classmethod def from_state_wrapper(cls, state_wrapper: OpenMMStateWrapper) -> Self: return cls.from_dict(state_wrapper.to_dict()) @@ -804,6 +865,16 @@ def from_state_wrapper(cls, state_wrapper: OpenMMStateWrapper) -> Self: def from_state(cls, state: openmm.State) -> Self: return cls.from_state_wrapper(OpenMMStateWrapper(state)) + + def to_dict(self) -> StateFieldData: + return StateFieldData( + { + k: v + for k, v in attrs.asdict(self, recurse=False).items() + if v is not None + } + ) + def to_state_wrapper( self, system: openmm.System | None = None, @@ -963,7 +1034,7 @@ def state_to_xml( # example on how to do this so I am eliding them. if state.parameters is not None or state.parameter_derivatives is not None: - warnings.warn( + logger.warning( "A state was provided to the XML serializer with parameters or parameter_derivatives, but these are currently not serialized." ) diff --git a/tests/unit/test_runners/test_openmm/test_runner.py b/tests/unit/test_runners/test_openmm/test_runner.py index 570aef8f..4264ec08 100644 --- a/tests/unit/test_runners/test_openmm/test_runner.py +++ b/tests/unit/test_runners/test_openmm/test_runner.py @@ -1,14 +1,10 @@ from wepy_tools.systems.lennard_jones import LennardJonesPair from wepy.runners.openmm.state import ( dummy_context, - resolve_state_data_type_enum_values, - GET_STATE_KWARG_DEFAULTS, - get_state_fields_present, OpenMMState, - gen_sim_state, ) -from wepy.runner.openmm.runner import ( +from wepy.runners.openmm.runner import ( OpenMMRunner, ) @@ -27,328 +23,81 @@ ] ) -def test_dummy_context(): - lj_sys = LennardJonesPair() - - dummy_context( - lj_sys.system, - np.array( - [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - ] - ) - * openmm.unit.nanometer - ) - - dummy_context( - lj_sys.system, - np.array( - [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - ] - ) - * openmm.unit.angstrom, - unitcell=UNIT_CUBE * openmm.unit.nanometer, - ) +@pytest.fixture +def runner_components() -> tuple[openmm.System, openmm.app.Topology, openmm.LangevinIntegrator]: -def test_resolve_state_data_type_enum_values(): + lj_sys = LennardJonesPair() - assert resolve_state_data_type_enum_values() == frozenmap( - { - "positions": 1, - "velocities": 2, - "forces": 4, - "energy": 8, - "parameters": 16, - "parameter_derivatives": 32, - "integrator_parameters": 64, - } - ) + integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + return lj_sys.system, lj_sys.topology, integrator @pytest.fixture def omm_context() -> openmm.Context: lj_sys = LennardJonesPair() - ctx = dummy_context(lj_sys.system, lj_sys.positions) return ctx -class TestOpenMMState: - - # TODO: this whole class needs overhauled but I want the other - # tests before messing with it too much. - - def test___init__(self): - - state = gen_sim_state( - positions=particle_line(2), - system=n_lj_system(2), - integrator=openmm.VerletIntegrator(0.002), - ) - - state = OpenMMState(state) - # assert "positions" in state - # assert state._data == {} - - class TestOpenMMRunner: - def test___init__(self): - - system = n_lj_system(2) - topology = n_particle_topology(2) - integrator = openmm.VerletIntegrator(0.002) + def test___init__(self, runner_components): runner = OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, + *runner_components ) - assert not runner.enforce_box - assert runner.getState_kwargs == dict(GET_STATE_KWARG_DEFAULTS) - - assert OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, - enforce_box=True, - ).enforce_box - - assert OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, - enforce_box=False, - get_state_kwargs={"enforce_box": True}, - ).enforce_box - - assert OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, - enforce_box=False, - get_state_kwargs={"positions": True}, - ).getState_kwargs == {"positions": True} - - def test_pre_cycle(self): - - system = n_lj_system(2) - topology = n_particle_topology(2) - integrator = openmm.VerletIntegrator(0.002) + def test_pre_cycle(self, runner_components): runner = OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, + *runner_components, ) - assert runner._cycle_platform is None - assert runner._cycle_platform_kwargs is None - + assert runner._last_cycle_segments_split_times == [] runner.pre_cycle() - - assert runner._cycle_platform is None - assert runner._cycle_platform_kwargs is None + assert runner._last_cycle_segments_split_times == [] runner = OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, - ) - - assert runner._cycle_platform is None - assert runner._cycle_platform_kwargs is None - - runner.pre_cycle( - platform="CPU", + *runner_components, + last_cycle_segments_split_times=[{"something" : 1}] ) - assert runner._cycle_platform == "CPU" - assert runner._cycle_platform_kwargs is None - - runner = OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, - ) - - assert runner._cycle_platform is None - assert runner._cycle_platform_kwargs is None - - runner.pre_cycle( - platform="CPU", - platform_kwargs={"Threads": "1"}, - ) - - assert runner._cycle_platform == "CPU" - assert runner._cycle_platform_kwargs == {"Threads": "1"} - - def test_post_cycle(self): - - system = n_lj_system(2) - topology = n_particle_topology(2) - integrator = openmm.VerletIntegrator(0.002) - - runner = OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, - ) - - assert runner._cycle_platform is None - assert runner._cycle_platform_kwargs is None - - runner.post_cycle() - - assert runner._cycle_platform is None - assert runner._cycle_platform_kwargs is None - - runner = OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, - ) - - assert runner._cycle_platform is None - assert runner._cycle_platform_kwargs is None - - runner.pre_cycle( - platform="CPU", - platform_kwargs={"Threads": "1"}, - ) - - assert runner._cycle_platform == "CPU" - assert runner._cycle_platform_kwargs == {"Threads": "1"} - - runner.post_cycle() - - assert runner._cycle_platform is None - assert runner._cycle_platform_kwargs is None - - def test__resolve_platform(self): - - system = n_lj_system(2) - topology = n_particle_topology(2) - integrator = openmm.VerletIntegrator(0.002) - - runner = OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, - ) - - assert runner._resolve_platform(platform=None, platform_kwargs=None) == ( - None, - None, - ) - assert runner._resolve_platform(platform=Ellipsis, platform_kwargs=None) == ( - None, - None, - ) - assert runner._resolve_platform( - platform=Ellipsis, platform_kwargs={"Threads": "1"} - ) == (None, None) - - assert runner._resolve_platform(platform="CPU", platform_kwargs=None) == ( - "CPU", - None, - ) - assert runner._resolve_platform( - platform="CPU", platform_kwargs={"Threads": "1"} - ) == ("CPU", {"Threads": "1"}) - - runner = OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, - ) - - runner.pre_cycle( - platform="CPU", - platform_kwargs={"Threads": "1"}, - ) - - assert runner._resolve_platform(platform=None, platform_kwargs=None) == ( - "CPU", - {"Threads": "1"}, - ) - - runner = OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, - platform="CPU", - ) - - assert runner._resolve_platform(platform=None, platform_kwargs=None) == ( - "CPU", - None, - ) + runner.pre_cycle() + assert runner._last_cycle_segments_split_times == [] - def test_run_segment(self): + def test_run_segment(self, runner_components): - system = n_lj_system(2) - topology = n_particle_topology(2) - # TODO: only currently works with LangevinIntegrator - integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + system, topology, integrator = runner_components - state = gen_sim_state( - positions=particle_line(2), - system=n_lj_system(2), - integrator=openmm.VerletIntegrator(0.002), - ) + lj_sys = LennardJonesPair() - state = OpenMMState(state) + state = OpenMMState.from_dwim(positions=lj_sys.positions) runner = OpenMMRunner( system=system, topology=topology, integrator=integrator, + platform_name="Reference", ) - runner.run_segment( - state, - 2, - ) - - new_state = runner.run_segment(state, 2, getState_kwargs={"positions": True}) - assert new_state["positions"] is not None - assert new_state["velocities"] is None + new_state = runner.run_segment(state, 2) + assert "positions" in new_state + assert "velocities" in new_state runner = OpenMMRunner( system=system, topology=topology, integrator=integrator, + get_state_keys={"positions",} ) - runner.run_segment( + new_state = runner.run_segment( new_state, 2, - platform="Reference", ) - - -# class TestOpenMMCPUWorker: - -# pass - - -# class TestOpenMMGPUWorker: -# pass - - -# class TestOpenMMCPUWalkerTaskProcess: -# pass - - -# class TestOpenMMGPUWalkerTaskProcess: -# pass - + assert new_state["positions"] is not None + assert "velocities" not in new_state diff --git a/tests/unit/test_runners/test_openmm/test_state.py b/tests/unit/test_runners/test_openmm/test_state.py index 72b99df9..037df60a 100644 --- a/tests/unit/test_runners/test_openmm/test_state.py +++ b/tests/unit/test_runners/test_openmm/test_state.py @@ -381,6 +381,51 @@ def test_serialize_xml(self, omm_context): assert openmm.XmlSerializer.serialize(state) == state_wrapper.serialize_xml() + def test_from_dict(self): + + time = 0.0 * openmm.unit.picosecond + bvs = UNIT_CUBE * openmm.unit.nanometer + + positions = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + ) + velocities = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond + ) + forces = ( + np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.kilojoule + / (openmm.unit.nanometer * openmm.unit.mole) + ) + + d = dict( + time=time, + box_vectors=bvs, + positions=positions, + velocities=velocities, + forces=forces, + ) + assert OpenMMState.from_dict(d) == OpenMMState(**d) + + def test_from_xml(self, omm_context): state = omm_context.getState() @@ -570,6 +615,148 @@ def test___init__(self): / (openmm.unit.nanometer * openmm.unit.mole), ) + def test___len__(self): + assert len(OpenMMState( + time=0.0 * openmm.unit.picosecond, + box_vectors=UNIT_CUBE * openmm.unit.nanometer, + )) == 2 + + assert len(OpenMMState( + time=0.0 * openmm.unit.picosecond, + box_vectors=UNIT_CUBE * openmm.unit.nanometer, + positions=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer, + velocities=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond, + forces=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.kilojoule + / (openmm.unit.nanometer * openmm.unit.mole), + )) == 5 + + def test___contains__(self): + small_state = OpenMMState( + time=0.0 * openmm.unit.picosecond, + box_vectors=UNIT_CUBE * openmm.unit.nanometer, + ) + + assert "time" in small_state + assert "box_vectors" in small_state + assert "box_volume" not in small_state + assert "positions" not in small_state + assert "velocities" not in small_state + assert "forces" not in small_state + assert "kinetic_energy" not in small_state + assert "potential_energy" not in small_state + assert "parameters" not in small_state + assert "parameter_derivatives" not in small_state + + large_state = OpenMMState( + time=0.0 * openmm.unit.picosecond, + box_vectors=UNIT_CUBE * openmm.unit.nanometer, + positions=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer, + velocities=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond, + forces=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.kilojoule + / (openmm.unit.nanometer * openmm.unit.mole), + ) + + assert "time" in large_state + assert "box_vectors" in large_state + assert "box_volume" not in large_state + assert "positions" in large_state + assert "velocities" in large_state + assert "forces" in large_state + assert "kinetic_energy" not in large_state + assert "potential_energy" not in large_state + assert "parameters" not in large_state + assert "parameter_derivatives" not in large_state + + + def test___getitem__(self): + + bvs = UNIT_CUBE * openmm.unit.nanometer + + state = OpenMMState( + time=0.0 * openmm.unit.picosecond, + box_vectors=bvs, + ) + + assert state["time"] == 0.0 * openmm.unit.picosecond + assert state["box_vectors"] is not None + + with pytest.raises(KeyError): + state["invalid"] + + with pytest.raises(ValueError): + state["positions"] + + state = OpenMMState( + time=0.0 * openmm.unit.picosecond, + box_vectors=bvs, + positions=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer, + velocities=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond, + forces=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.kilojoule + / (openmm.unit.nanometer * openmm.unit.mole), + ) + + assert state["positions"] is not None + assert state["velocities"] is not None + assert state["forces"] is not None + + def test_from_state(self, omm_context): s = OpenMMState.from_state(omm_context.getState()) @@ -633,11 +820,8 @@ def test_from_state_wrapper(self, omm_context): ) OpenMMState.from_state_wrapper(sw) - def test_to_dict(self): - - time = 0.0 * openmm.unit.picosecond - bvs = UNIT_CUBE * openmm.unit.nanometer + def test_from_dwim(self): positions = ( np.array( [ @@ -668,24 +852,39 @@ def test_to_dict(self): / (openmm.unit.nanometer * openmm.unit.mole) ) - os = OpenMMState( - time=time, - box_vectors=bvs, + assert OpenMMState.from_dwim( + box_vectors=None, + positions=positions, + velocities=velocities, + forces=forces, + ) == OpenMMState( + time=OpenMMState.DWIM_DEFAULT_TIME, + box_volume=OpenMMState.DWIM_DEFAULT_BOX_VOLUME, + box_vectors=OpenMMState.DWIM_DEFAULT_UNITCELL, positions=positions, velocities=velocities, forces=forces, ) - osd = os.to_dict() - assert set(osd.keys()) == { - "time", - "box_vectors", - "positions", - "velocities", - "forces", - } + bv = np.array( + [ + [2.0, 0.0, 0.0], + [0., 2.0, 0.0], + [0., 0.0, 2.0], + ] + ) - def test_from_dict(self): + assert OpenMMState.from_dwim( + box_vectors=bv, + positions=positions, + ) == OpenMMState( + time=OpenMMState.DWIM_DEFAULT_TIME, + box_volume=OpenMMState.DWIM_DEFAULT_BOX_VOLUME, + box_vectors=bv, + positions=positions, + ) + + def test_to_dict(self): time = 0.0 * openmm.unit.picosecond bvs = UNIT_CUBE * openmm.unit.nanometer @@ -720,15 +919,23 @@ def test_from_dict(self): / (openmm.unit.nanometer * openmm.unit.mole) ) - d = dict( + os = OpenMMState( time=time, box_vectors=bvs, positions=positions, velocities=velocities, forces=forces, ) - assert OpenMMState.from_dict(d) == OpenMMState(**d) + osd = os.to_dict() + assert set(osd.keys()) == { + "time", + "box_vectors", + "positions", + "velocities", + "forces", + } + def test_to_state_wrapper(self): time = 0.0 * openmm.unit.picosecond From 5910c7d099e2e7c55005fb58b9264908a68a63db Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 3 Dec 2025 14:49:15 -0500 Subject: [PATCH 041/143] mock runner implementation updates --- src/wepy/runners/mock.py | 35 ++++++++++++++++++-------- src/wepy/runners/runner.py | 29 ++++++++++----------- tests/unit/test_runners/test_mock.py | 18 ++++++++++++- tests/unit/test_runners/test_runner.py | 18 ++++++++++++- 4 files changed, 71 insertions(+), 29 deletions(-) diff --git a/src/wepy/runners/mock.py b/src/wepy/runners/mock.py index 4491a4b2..ab86bdd2 100644 --- a/src/wepy/runners/mock.py +++ b/src/wepy/runners/mock.py @@ -1,20 +1,23 @@ """Realistic mock runners useful mostly for testing.""" +import logging import attrs -from wepy.walker import Walker +from wepy.walker import Walker, WalkerState from wepy.runners.runner import Runner +from wepy.work_mapper.base import Task + +logger = logging.getLogger(__name__) @attrs.define -class MockState: - a: 1 +class MockState(WalkerState): + a: int class MockError(Exception): pass -@attrs.define -class MockRunner(Runner[MockState]): - fail: bool = False +@attrs.define +class MockRunner(Runner): def pre_cycle(self) -> None: pass @@ -26,15 +29,14 @@ def run_segment( self, state: MockState, segment_length: int, - worker_id: int = 0, - # UGLY: here to satisfy the interface - cycle_idx: int = 0, - walker_idx: int = 0 + fail: bool, ) -> MockState: - if self.fail: + if fail: + logger.critical("Error requested in MockRuner.run_segment, raising.") raise MockError("Error requested") + logger.info("Evolving the MockState in MockRunner.run_segment") return attrs.evolve( state, a=(state.a + segment_length), @@ -42,3 +44,14 @@ def run_segment( def get_last_cycle_segments_split_times(self) -> None: return None + +@attrs.define +class MockTask(Task): + + runner: MockRunner + segment_length: int + fail: bool + + def __call__(self, state: MockState) -> MockState: + logger.info("Running MockTask segment") + return self.runner.run_segment(state, self.segment_length, self.fail) diff --git a/src/wepy/runners/runner.py b/src/wepy/runners/runner.py index ccd63553..d7899adc 100644 --- a/src/wepy/runners/runner.py +++ b/src/wepy/runners/runner.py @@ -18,15 +18,16 @@ """ # Standard Library -from typing import Protocol, Any, TypedDict, TypeVar +from typing import Protocol, Any, TypedDict, TypeVar, ParamSpec import attrs -from wepy.walker import Walker +from wepy.walker import Walker, WalkerState -State_ = TypeVar("State_") -class Runner(Protocol[State_]): +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) +SegmentParams_ = ParamSpec("SegmentParams_") +class Runner(Protocol[WalkerState_, SegmentParams_]): """Abstract base class for the Runner interface.""" - def pre_cycle(self, **kwargs: dict[str, Any]) -> None: + def pre_cycle(self) -> None: """Perform pre-cycle behavior. run_segment will be called for each walker so this allows you to perform changes of state on a per-cycle basis. @@ -56,12 +57,11 @@ def post_cycle(self) -> None: def run_segment( self, - walker: Walker, + walker: WalkerState_, segment_length: int, - # UGLY: here to satisfy the interface - cycle_idx: int = 0, - walker_idx: int = 0 - ) -> Walker: + *args: SegmentParams_.args, + **kwargs: SegmentParams_.kwargs, + ) -> WalkerState_: """Run dynamics for the walker. Parameters @@ -82,7 +82,7 @@ def get_last_cycle_segments_split_times(self) -> list[dict[str, float]] | None: @attrs.define -class NoRunner[State_]: +class NoRunner(Runner): """Stub Runner that just returns the walkers back with the same state. May be useful for testing. @@ -96,10 +96,7 @@ def get_last_cycle_segments_split_times(self) -> None: return None def run_segment( self, - state: State_, + state: WalkerState_, segment_length: int | float, - # UGLY: here to satisfy the interface - cycle_idx: int = 0, - walker_idx: int = 0 - ) -> State_: + ) -> WalkerState_: return state diff --git a/tests/unit/test_runners/test_mock.py b/tests/unit/test_runners/test_mock.py index e675d0ac..4cafc76a 100644 --- a/tests/unit/test_runners/test_mock.py +++ b/tests/unit/test_runners/test_mock.py @@ -1,5 +1,21 @@ -from wepy.runners.mock import MockRunner, MockState +import pytest +from wepy.runners.mock import MockRunner, MockState, MockTask, MockError +def test_MockTask(): + + assert MockTask( + MockRunner(), + 10, + fail=False, + )(MockState(10)) == MockState(20) + + with pytest.raises(MockError): + MockTask( + MockRunner(), + 10, + fail=True, + )(MockState(10)) + class TestMockRunner: def test_pre_cycle(self): diff --git a/tests/unit/test_runners/test_runner.py b/tests/unit/test_runners/test_runner.py index 0f9e3e60..038ee606 100644 --- a/tests/unit/test_runners/test_runner.py +++ b/tests/unit/test_runners/test_runner.py @@ -1,3 +1,4 @@ +import attrs from wepy.runners.runner import NoRunner from wepy.walker import Walker, WalkerState @@ -6,10 +7,25 @@ class TestNoRunner: def test_run_segment(self): + # concrete state to use + @attrs.define + class SomeState(WalkerState): + a: int + + def __getitem__(self, key: str) -> int: + + if key != "a": + raise KeyError(f"Invalid key '{key}'") + + return self.a + + def dict(self) -> dict[str, int]: + return {"a" : self.a} + runner = NoRunner() walker = Walker( - state=WalkerState(a=1), + state=SomeState(a=1), weight=0.1, ) assert ( From a786d22eb8f824ab9bebd3aa7f5fd43310ba26f1 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 3 Dec 2025 14:50:28 -0500 Subject: [PATCH 042/143] working tests for work mappers --- src/wepy/work_mapper/base.py | 144 ++++-------------- src/wepy/work_mapper/mapper.py | 8 - src/wepy/work_mapper/proc_pool_mapper.py | 100 +++++++----- src/wepy/work_mapper/serial.py | 81 ++++------ .../test_work_mapper/test_proc_pool_mapper.py | 132 +++------------- tests/unit/test_work_mapper/test_serial.py | 41 ++--- 6 files changed, 157 insertions(+), 349 deletions(-) diff --git a/src/wepy/work_mapper/base.py b/src/wepy/work_mapper/base.py index 0578d9e0..bae5bcce 100644 --- a/src/wepy/work_mapper/base.py +++ b/src/wepy/work_mapper/base.py @@ -2,7 +2,7 @@ # Standard Library import traceback -from typing import Callable, Literal, Generic, TypeVar, Protocol, Any +from typing import Callable, Literal, Generic, TypeVar, Protocol, Any, ParamSpec, Concatenate import logging # Standard Library @@ -11,30 +11,46 @@ logger = logging.getLogger(__name__) -AnyWalkerState = TypeVar("AnyWalkerState", bound=WalkerState) +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) +Task_ = TypeVar("Task_") +WorkMapperParams_ = ParamSpec("WorkMapperParams_") +InnerTask_ = TypeVar("InnerTask_") +class Task(Protocol[WalkerState_]): -class WorkMapper(Protocol[AnyWalkerState]): + def __call__( + self, + walker_state: WalkerState_, + ) -> WalkerState_: + ... + + +class WorkMapper(Protocol[WalkerState_, WorkMapperParams_, Task_, InnerTask_]): - def init( + def __init__( self, segment_func: Callable[ - tuple[ - AnyWalkerState, - ..., + [ + WalkerState_, + Task_, ], - AnyWalkerState, + WalkerState_, ], - num_workers: int | None = None, + *args: WorkMapperParams_.args, + **kwargs: WorkMapperParams_.kwargs, ) -> None: ... + def init(self) -> None: + ... + + def gen_task(self, outer_task: Task_) -> Task_ | InnerTask_: ... + def map( self, - walker_states: list[AnyWalkerState], - *args: list[list[Any]], - **kwargs: dict[str, list[Any]], - ) -> list[AnyWalkerState]: ... + tasks: list[Task_], + walker_states: list[WalkerState_], + ) -> list[WalkerState_]: ... def get_worker_segment_times(self) -> dict[int, list[float]] | None: ... @@ -61,109 +77,7 @@ def __init__( self.wrapped_exception = wrapped_exception self.formatted_tb = traceback.format_tb(tb) - class TaskException(WrapperException): pass -class Task: - """Class that composes a function and arguments.""" - - def __init__(self, func, *args, **kwargs): - """Constructor for Task. - - Parameters - ---------- - func : callable - Function to be called on the arguments. - - *args - The arguments to pass to func - - """ - self.args = args - self.kwargs = kwargs - self.func = func - - def __call__(self, **worker_kwargs): - """Makes the Task itself callable.""" - - # run the function passing in the args for running it and any - # worker information in the worker kwargs. - return self.func(*self.args, **self.kwargs, **worker_kwargs) - - -# class ABCMapper: -# """Abstract base class for a Mapper.""" - -# def __init__( -# self, -# segment_func: SegmentFunc | None =None, -# **kwargs: dict[str, Any], -# ) -> None: -# """Constructor for the Mapper class. No arguments are required. - -# Parameters -# ---------- - -# segment_func : Set a default segment_func. Typically set at -# runtime. - -# """ - -# self._func = segment_func - -# self._attributes = kwargs - -# @property -# def attributes(self) -> dict[str, Any]: -# return self._attributes - -# def init( -# self, -# segment_func: SegmentFunc | None = None, -# **kwargs: dict[str, Any], -# ) -> None: -# """Runtime initialization and setting of function to map over walkers. - -# Parameters -# ---------- -# segment_func : callable implementing the Runner.run_segment interface - -# """ - -# if self.segment_func is not None and segment_func is not None: -# logger.info( -# "overriding default segment_func {} with {}".format( -# self._func, segment_func -# ) -# ) -# self._func = segment_func - -# elif self.segment_func is None and segment_func is None: -# ValueError("segment_func must be given since no default specified") - -# elif self.segment_func is None and segment_func is not None: -# self._func = segment_func - -# @property -# def segment_func(self) -> SegmentFunc: -# """The function that will be called for new data in the `map` method.""" -# return self._func - -# def cleanup(self, **kwargs: dict[str, Any]) -> None: -# """Runtime post-simulation tasks. - -# This is run either at the end of a successful simulation or -# upon an error in the main process of the simulation manager -# call to `run_cycle`. - -# The Mapper class performs no actions here and all arguments -# are ignored. - -# """ - -# # nothing to do -# pass -# def map(self, walkers: list[Walker], *args: list[list[Any]], **kwargs: dict[list[Any]]) -> list[Walker]: -# raise NotImplementedError diff --git a/src/wepy/work_mapper/mapper.py b/src/wepy/work_mapper/mapper.py index 8ba1df6d..40e94e08 100644 --- a/src/wepy/work_mapper/mapper.py +++ b/src/wepy/work_mapper/mapper.py @@ -17,14 +17,6 @@ logger = logging.getLogger(__name__) - - - - - - - - class ABCWorkerMapper(ABCMapper): def __init__( self, diff --git a/src/wepy/work_mapper/proc_pool_mapper.py b/src/wepy/work_mapper/proc_pool_mapper.py index 9767d959..3a37b096 100644 --- a/src/wepy/work_mapper/proc_pool_mapper.py +++ b/src/wepy/work_mapper/proc_pool_mapper.py @@ -1,29 +1,50 @@ import itertools import multiprocessing as mp -from typing import Any, Callable, Literal +from typing import Any, Callable, Literal, Generic, TypeVar, Never import logging import attrs -from wepy.work_mapper.base import AnyWalkerState +from wepy.work_mapper.base import Task, WalkerState, WorkMapper + +# log_safe.initialize_safe_logging() logger = logging.getLogger(__name__) -class ProcPoolMapper: +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) +Task_ = TypeVar("Task_", bound=Task) + +def _force_error(exc: Exception) -> Never: + raise exc + +@attrs.define +class ProcPoolTask(Task, Generic[Task_]): + + wrapped_task: Task_ + + def __call__(self, walker_state: WalkerState_) -> WalkerState_: + + logging.basicConfig(level="INFO") + logging.getLogger("ProcPoolTask").info("Configured logging in task process") + + return self.wrapped_task(walker_state) + +class ProcPoolMapper( + WorkMapper, + Generic[ + WalkerState_, + Task_, + ], +): + + def __init__( + self, + num_workers: int, + ) -> None: + self._num_workers = num_workers def init( self, - segment_func: Callable[ - [ - AnyWalkerState, - Any, - ..., - ], - AnyWalkerState, - ], - num_workers: int, - worker_args: list[dict[str, Any]] | None = None, - proc_start_method: Literal["fork", "spawn", "forkserver"] = "spawn", ) -> None: """l.. @@ -36,25 +57,12 @@ def init( logger.info("Initializing ProcPoolMapper") - self._num_workers = num_workers - self._proc_start_method = proc_start_method - - if worker_args is not None and len(worker_args) != num_workers: - raise ValueError("If worker_args are given they must match the number of workers.") - - elif worker_args is None: - logger.info("No worker arguments given.") - self._worker_args = [{} for _ in range(self._num_workers)] + logger.info(f"Initializing local multiprocessing context with 'spawn' process start method") - else: - logger.info(f"Configured workers with the following function arguments: {worker_args}") - self._worker_args = worker_args - - - self._func = segment_func - - logger.info(f"Initializing local multiprocessing context with start method: {self._proc_start_method}") - self._mp_ctx = mp.get_context(method=self._proc_start_method) + # NOTE: always require "spawn" as this is the safest and + # changing to "fork" can have lots of other effects that we + # don't want to test + self._mp_ctx = mp.get_context(method="spawn") def cleanup(self) -> None: @@ -63,24 +71,28 @@ def cleanup(self) -> None: def map( self, - walker_states: list[AnyWalkerState], - *args: list[list[Any]], - ) -> list[AnyWalkerState]: + tasks: list[Task_], + walker_states: list[WalkerState_], + ) -> list[WalkerState_]: logger.info(f"Running map on {len(walker_states)} in batches of {self._num_workers}") # spin up a new pool for each map logger.info(f"Starting process Pool with {self._num_workers}") + with self._mp_ctx.Pool( processes=self._num_workers, - # only run one thing per task, just to make sure - # everything is cleaned up + # NOTE: only run one thing per task, just to make sure + # everything is cleaned up which is an issue with + # OpenMM contexts. Also note that this is why we use a + # multiprocessing.Pool and not a + # concurrent.futures.ProcessPoolExecutor maxtasksperchild=1, ) as pool: results = [] for batch_idx, batch in enumerate(itertools.batched( - zip(walker_states, *args, strict=True), + zip(walker_states, tasks, strict=True), self._num_workers, strict=False, )): @@ -88,18 +100,22 @@ def map( logger.info(f"Submitting batch: {batch_idx}") batch_results = [] - for batch_task_idx, batch_args in enumerate(batch): + for batch_task_idx, (walker_state, task) in enumerate(batch): task_idx = batch_idx + batch_task_idx # for our purposes each element in this batch # should be associated with a worker. worker_idx = batch_task_idx + proc_pool_task = ProcPoolTask(task) + logger.info(f"Submitting task {task_idx} to worker {worker_idx}") result = pool.apply_async( - self._func, - args=batch_args, - kwds=self._worker_args[worker_idx], + proc_pool_task, + (walker_state,), + # NOTE: this must be provided or in some cases when a + # worker crashes on startup it will hang + error_callback=_force_error, ) logger.info(f"Task {task_idx} submitted") batch_results.append(result) diff --git a/src/wepy/work_mapper/serial.py b/src/wepy/work_mapper/serial.py index c7522b8a..210739b4 100644 --- a/src/wepy/work_mapper/serial.py +++ b/src/wepy/work_mapper/serial.py @@ -3,24 +3,29 @@ import sys import traceback -from typing import Callable, Literal, Generic, TypeVar, Protocol, Any +from typing import Callable, Literal, Generic, TypeVar, Protocol, Any, ParamSpec, Concatenate, Sequence import logging -from wepy.walker import Walker -from wepy.work_mapper.base import WorkMapper, AnyWalkerState, TaskException +from wepy.walker import Walker, WalkerState +from wepy.work_mapper.base import WorkMapper, TaskException, Task logger = logging.getLogger(__name__) +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) +Task_ = TypeVar("Task_", bound=Task) -class SerialMapper(WorkMapper[AnyWalkerState]): +class SerialMapper( + WorkMapper, + Generic[ + WalkerState_, + Task_, + ]): """Basic non-parallel reference implementation of a mapper.""" def __init__( self, ) -> None: - """Constructor for the Mapper class. No arguments are required.""" - self._worker_segment_times: dict[int, list[float]] = {0: []} @@ -34,65 +39,33 @@ def get_worker_segment_times(self) -> dict[int, list[float]]: """ return self._worker_segment_times - - def init( - self, - segment_func: Callable[ - [ - AnyWalkerState, - Any, - ..., - ], - AnyWalkerState, - ], - # UGLY: these are here for compatibility with the - # interface but not used - num_workers: int | None = None, - ) -> None: - self._func = segment_func + def init(self) -> None: + pass def cleanup(self) -> None: pass - def map( + def gen_task( self, - walker_states: list[AnyWalkerState], - *args: list[list[Any]], - **kwargs: dict[str, list[Any]], - ) -> list[AnyWalkerState]: - """Map the 'segment_func' to args. - - Parameters - ---------- - *args : list of list - Each element is the argument to one call of 'segment_func'. - - Returns - ------- - results : list - The results of each call to 'segment_func' in the same order as input. - - Examples - -------- - >>> Mapper(segment_func=sum).map([(0,1,2), (3,4,5)]) - [3, 12] - - """ + outer_task: Task_, + task_idx: int, + ) -> Task_: + return outer_task + def map( + self, + tasks: list[Task_], + walker_states: list[WalkerState_], + ) -> list[WalkerState_]: segment_times: list[float] = [] - results: list[AnyWalkerState] = [] - - for arg_idx, (walker_state, *call_args) in enumerate(zip(walker_states, *args, strict=True)): + results: list[WalkerState_] = [] + for task_idx, (task, walker_state) in enumerate(zip(tasks, walker_states, strict=True)): - call_kwargs = { - k : values[arg_idx] - for k, values - in kwargs.items() - } + _task = self.gen_task(task, task_idx=task_idx) tic = time.time() - result = self._func(walker_state, *call_args, **call_kwargs) + result = _task(walker_state) toc = time.time() segment_times.append(toc - tic) diff --git a/tests/unit/test_work_mapper/test_proc_pool_mapper.py b/tests/unit/test_work_mapper/test_proc_pool_mapper.py index 32fba3a2..13591245 100644 --- a/tests/unit/test_work_mapper/test_proc_pool_mapper.py +++ b/tests/unit/test_work_mapper/test_proc_pool_mapper.py @@ -1,126 +1,34 @@ -import attrs -from wepy.walker import Walker, WalkerState +# from wepy.walker import Walker, WalkerState from wepy.work_mapper.proc_pool_mapper import ProcPoolMapper - -# some minimal definitions for testing a concrete work mapper - -@attrs.define -class RizzWalkerState: - rizz: int - -def rizz_run(walker_state: RizzWalkerState, delta: int, multiple: int) -> RizzWalkerState: - - return attrs.evolve( - walker_state, - rizz=(walker_state.rizz + delta) * multiple - ) - -def test_rizz_walker(): - assert rizz_run( - RizzWalkerState(rizz=1), - 1, - 2, - ) == RizzWalkerState(rizz=4) +# NOTE: that you must import from a module (as opposed to inline in +# the test file) or you have problems with importing modules when +# using "spawn" process start method +from wepy.runners.mock import MockState, MockError, MockRunner, MockTask def test_ProcPoolMapper(): - poolmapper = ProcPoolMapper() + poolmapper = ProcPoolMapper(num_workers=2) - poolmapper.init(rizz_run, num_workers=1) + poolmapper.init() results = poolmapper.map( [ - RizzWalkerState(1), - RizzWalkerState(1), - RizzWalkerState(2), + MockTask( + MockRunner(), + segment_length=10, + fail=False, + ) + for _ in range(3) ], - [1, 2, 2], - [2, 2, 2], - ) - - assert results == [ - RizzWalkerState(4), - RizzWalkerState(6), - RizzWalkerState(8), - ] - - - poolmapper = ProcPoolMapper() - - poolmapper.init(rizz_run, num_workers=2) - - results = poolmapper.map( [ - RizzWalkerState(1), - RizzWalkerState(1), - RizzWalkerState(2), + MockState(0), + MockState(1), + MockState(2), ], - [1, 2, 2], - [2, 2, 2], ) assert results == [ - RizzWalkerState(4), - RizzWalkerState(6), - RizzWalkerState(8), - ] - - poolmapper = ProcPoolMapper() - - poolmapper.init(rizz_run, num_workers=3) - - results = poolmapper.map( - [ - RizzWalkerState(1), - RizzWalkerState(1), - RizzWalkerState(2), - ], - [1, 2, 2], - [2, 2, 2], - ) - - assert results == [ - RizzWalkerState(4), - RizzWalkerState(6), - RizzWalkerState(8), - ] - - poolmapper = ProcPoolMapper() - - poolmapper.init(rizz_run, num_workers=3, proc_start_method="fork") - - results = poolmapper.map( - [ - RizzWalkerState(1), - RizzWalkerState(1), - RizzWalkerState(2), - ], - [1, 2, 2], - [2, 2, 2], - ) - - assert results == [ - RizzWalkerState(4), - RizzWalkerState(6), - RizzWalkerState(8), - ] - - # use worker args to fill in one of the args - poolmapper = ProcPoolMapper() - - poolmapper.init( - rizz_run, - num_workers=2, - worker_args=[ - {"multiple" : 2}, - {"multiple" : 3}, - ]) - - results = poolmapper.map( - [ - RizzWalkerState(1), - RizzWalkerState(1), - RizzWalkerState(1), - ], - [1, 1, 1], - ) + MockState(10), + MockState(11), + MockState(12), + ] diff --git a/tests/unit/test_work_mapper/test_serial.py b/tests/unit/test_work_mapper/test_serial.py index ee111d1a..ad2d9f72 100644 --- a/tests/unit/test_work_mapper/test_serial.py +++ b/tests/unit/test_work_mapper/test_serial.py @@ -1,3 +1,4 @@ +import functools import attrs from wepy.walker import Walker, WalkerState from wepy.work_mapper.serial import SerialMapper @@ -8,6 +9,7 @@ class RizzWalkerState: rizz: int + def rizz_run(walker_state: RizzWalkerState, delta: int, multiple: int) -> RizzWalkerState: return attrs.evolve( @@ -15,6 +17,16 @@ def rizz_run(walker_state: RizzWalkerState, delta: int, multiple: int) -> RizzWa rizz=(walker_state.rizz + delta) * multiple ) +@attrs.define +class RizzTask: + + delta: int + multiple: int + + def __call__(self, state: RizzWalkerState) -> RizzWalkerState: + + return rizz_run(state, delta=self.delta, multiple=self.multiple) + def test_rizz_walker(): assert rizz_run( RizzWalkerState(rizz=1), @@ -29,35 +41,28 @@ def test_map(self): mapper = SerialMapper() - mapper.init(segment_func=rizz_run) + mapper.init() assert mapper.map( [ - RizzWalkerState(1), - RizzWalkerState(1), - RizzWalkerState(2), + RizzTask( + *args, + ) + for args + in zip( + [1, 2, 2], + [2, 2, 2], + ) ], - [1, 2, 2], - [2, 2, 2], - ) == [ - RizzWalkerState(4), - RizzWalkerState(6), - RizzWalkerState(8), - ] - - assert len(mapper.get_worker_segment_times()[0]) == 3 - - assert mapper.map( [ RizzWalkerState(1), RizzWalkerState(1), RizzWalkerState(2), ], - [1, 2, 2], - multiple=[2, 2, 2], ) == [ RizzWalkerState(4), RizzWalkerState(6), RizzWalkerState(8), ] - + + assert len(mapper.get_worker_segment_times()[0]) == 3 From cb96c0c8a07a62c7a085a4547cb99ea412b721b4 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 3 Dec 2025 15:51:22 -0500 Subject: [PATCH 043/143] refactor work mapper runner interfaces Start to reify some of the implicit interfaces the sim manager expects from the runners/work mappers. Secondly experimenting with having the runners be responsible for the work mapper task generation. This makes sense since the runner knows the specifics of how the tasks need to be parametrized. Seems to make sense. --- src/wepy/interface.py | 25 + src/wepy/runners/mock.py | 39 +- src/wepy/runners/runner.py | 31 +- src/wepy/sim_manager.py | 127 +-- src/wepy/work_mapper/base.py | 22 +- src/wepy/work_mapper/mapper.py | 130 --- src/wepy/work_mapper/proc_pool_mapper.py | 7 +- src/wepy/work_mapper/serial.py | 15 +- src/wepy/work_mapper/task_mapper.py | 702 ---------------- src/wepy/work_mapper/worker_mapper.py | 763 ------------------ .../test_openmm/test_serial_mapper.py | 133 ++- tests/unit/test_sim_manager.py | 30 +- .../test_work_mapper/test_proc_pool_mapper.py | 7 +- tests/unit/test_work_mapper/test_serial.py | 5 +- 14 files changed, 283 insertions(+), 1753 deletions(-) create mode 100644 src/wepy/interface.py delete mode 100644 src/wepy/work_mapper/mapper.py delete mode 100644 src/wepy/work_mapper/task_mapper.py delete mode 100644 src/wepy/work_mapper/worker_mapper.py diff --git a/src/wepy/interface.py b/src/wepy/interface.py new file mode 100644 index 00000000..bb6dc50c --- /dev/null +++ b/src/wepy/interface.py @@ -0,0 +1,25 @@ +from typing import Generic, TypeVar, Protocol + +import attrs + +from wepy.walker import WalkerState + +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) + +@attrs.define +class WorkMapperFactoryArgs: + num_workers: int + +@attrs.define +class RunnerGenTaskArgs(Generic[WalkerState_]): + segment_length: int + cycle_idx: int + states: list[WalkerState_] + +class Task(Protocol[WalkerState_]): + + def __call__( + self, + walker_state: WalkerState_, + ) -> WalkerState_: + ... diff --git a/src/wepy/runners/mock.py b/src/wepy/runners/mock.py index ab86bdd2..5423c1ef 100644 --- a/src/wepy/runners/mock.py +++ b/src/wepy/runners/mock.py @@ -2,6 +2,7 @@ import logging import attrs +from wepy.interface import Task, RunnerGenTaskArgs from wepy.walker import Walker, WalkerState from wepy.runners.runner import Runner from wepy.work_mapper.base import Task @@ -15,16 +16,44 @@ class MockState(WalkerState): class MockError(Exception): pass +@attrs.define +class MockTask(Task): + + runner: "MockRunner" + segment_length: int + fail: bool + + def __call__(self, state: MockState) -> MockState: + logger.info("Running MockTask segment") + return self.runner.run_segment(state, self.segment_length, self.fail) @attrs.define class MockRunner(Runner): + fail_walker_idxs: set[int] = attrs.field(default={}) + def pre_cycle(self) -> None: pass def post_cycle(self) -> None: pass + def gen_tasks(self, segment_spec: RunnerGenTaskArgs[MockState]) -> list[MockTask]: + + return [ + MockTask( + runner=self, + segment_length=segment_spec.segment_length, + fail=( + True + if walker_idx in self.fail_walker_idxs + else False + ) + ) + for walker_idx, state + in enumerate(segment_spec.states) + ] + def run_segment( self, state: MockState, @@ -45,13 +74,3 @@ def run_segment( def get_last_cycle_segments_split_times(self) -> None: return None -@attrs.define -class MockTask(Task): - - runner: MockRunner - segment_length: int - fail: bool - - def __call__(self, state: MockState) -> MockState: - logger.info("Running MockTask segment") - return self.runner.run_segment(state, self.segment_length, self.fail) diff --git a/src/wepy/runners/runner.py b/src/wepy/runners/runner.py index d7899adc..b81bc6cd 100644 --- a/src/wepy/runners/runner.py +++ b/src/wepy/runners/runner.py @@ -21,10 +21,13 @@ from typing import Protocol, Any, TypedDict, TypeVar, ParamSpec import attrs from wepy.walker import Walker, WalkerState +from wepy.interface import Task, RunnerGenTaskArgs WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) -SegmentParams_ = ParamSpec("SegmentParams_") -class Runner(Protocol[WalkerState_, SegmentParams_]): + +Task_ = TypeVar("Task_", bound=Task) + +class Runner(Protocol[WalkerState_, Task_]): """Abstract base class for the Runner interface.""" def pre_cycle(self) -> None: @@ -52,15 +55,15 @@ def post_cycle(self) -> None: """ - # by default just pass since subclasses need not implement this - pass + ... + + def gen_tasks(self, segment_spec: RunnerGenTaskArgs[WalkerState_]) -> list[Task_]: + ... def run_segment( self, walker: WalkerState_, segment_length: int, - *args: SegmentParams_.args, - **kwargs: SegmentParams_.kwargs, ) -> WalkerState_: """Run dynamics for the walker. @@ -80,6 +83,13 @@ def run_segment( def get_last_cycle_segments_split_times(self) -> list[dict[str, float]] | None: ... +@attrs.define +class IdentityTask(Task[WalkerState_]): + + runner: "NoRunner" + + def __call__(self, walker_state: WalkerState_) -> WalkerState_: + return self.runner.run_segment(walker_state) @attrs.define class NoRunner(Runner): @@ -94,6 +104,15 @@ def post_cycle(self) -> None: pass def get_last_cycle_segments_split_times(self) -> None: return None + + def gen_tasks(self, segment_spec: RunnerGenTaskArgs[WalkerState_]) -> list[IdentityTask[WalkerState_]]: + + return [ + IdentityTask(self) + for state + in segment_spec.states + ] + def run_segment( self, state: WalkerState_, diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index 6efb2d80..1663c7f6 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -52,6 +52,10 @@ from copy import deepcopy # First Party Library +from wepy.interface import ( + WorkMapperFactoryArgs, + RunnerGenTaskArgs, +) from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.reporter.reporter import Reporter from wepy.resampling.resamplers.resampler import Resampler @@ -120,6 +124,7 @@ class Manager(Generic[State_]): runner: Runner resampler: Resampler boundary_conditions: BoundaryConditions | None + work_mapper_class: type[WorkMapper] work_mapper: WorkMapper reporters: list[Reporter] monitor: Monitor | None @@ -150,7 +155,7 @@ def __init__( init_walkers: list[Walker[State_]], runner: Runner, resampler: Resampler, - work_mapper: WorkMapper | None = None, + work_mapper_class: type[WorkMapper] | None = None, boundary_conditions: BoundaryConditions | None = None, reporters: list[Reporter] | None = None, sim_monitor: Monitor | None = None, @@ -165,9 +170,9 @@ def __init__( runner : object implementing the Runner interface The runner to be used for propagating sampling segments of walkers. - work_mapper : object implementing the WorkMapper interface - The object that will be used to perform a set of runner - segments in a cycle. + work_mapper_class : Class for a work mapper, will be + instantiated by simulation manager. If None will default + to a serial mapper. resampler : object implementing the Resampler interface The resampler to be used in the simulation @@ -210,10 +215,10 @@ def __init__( else: self.reporters = reporters - if work_mapper is None: - self.work_mapper = SerialMapper() + if work_mapper_class is None: + self.work_mapper_class = SerialMapper else: - self.work_mapper = work_mapper + self.work_mapper_class = work_mapper_class ## Monitor self.monitor = sim_monitor @@ -282,11 +287,15 @@ def init( # initialize the work_mapper with the function it will be # mapping and the number of workers, this may include things like starting processes # etc. - logger.info("Initializing work_mapper") - self.work_mapper.init( - segment_func=self.runner.run_segment, - num_workers=num_workers, + logger.info("Instantiating work mapper") + self.work_mapper = self.work_mapper_class( + WorkMapperFactoryArgs( + num_workers=num_workers, + ) ) + logger.info("Running WorkMapper.init hook") + self.work_mapper.init() + logger.info("Finished WorkMapper.init hook") # init the reporter for reporter in self.reporters: @@ -364,9 +373,7 @@ def run_segment( Parameters ---------- - walkers : list[Walker] - List of walkers - + states segment_length : int Number of steps to run in each segment. @@ -379,22 +386,21 @@ def run_segment( The walkers after the segment of sampling simulation. """ - num_walkers = len(states) - - logger.info("Starting segment") + logger.info("Generating tasks for walker states") + tasks = self.runner.gen_tasks( + RunnerGenTaskArgs( + segment_length=segment_length, + cycle_idx=cycle_idx, + states=states, + ) + ) - segment_lengths = [segment_length for i in range(num_walkers)] - cycle_idxs = [cycle_idx for i in range(num_walkers)] - walker_idxs = [walker_idx for walker_idx in range(num_walkers)] + logger.info("Starting segment runs") try: new_states = list( self.work_mapper.map( - # args, which must be supported by the map function + tasks, states, - segment_lengths, - # kwargs which are optionally recognized by the map function - cycle_idx=cycle_idxs, - walker_idx=walker_idxs, ) ) @@ -650,74 +656,6 @@ def run_cycle( logger.info("Done: returning walkers") return resampled_walkers, (self.runner, self.boundary_conditions, self.resampler) - def run_simulation_by_time( - self, - run_time: float, - segments_length: int, - num_workers: int | None = None, - ) -> tuple[ - list[Walker[State_]], - tuple[Runner, BoundaryConditions | None, Resampler], - ]: - """Run a simulation for a certain amount of time. - - This starts timing as soon as this is called. If the time - before running a new cycle is greater than the runtime the run - will exit after cleaning up. Once a cycle is started it may - also run over the wall time. - - All this does is provide a run idx to the reporters, which is - the run that is intended to be continued. This simulation - manager knows no details and is left up to the reporters to - handle this appropriately. - - Parameters - ---------- - run_time : float - The time to run in seconds. - - segments_length : int - The number of steps for each runner segment. - - num_workers : int - The number of workers to use for the work mapper. - (Default value = None) - - Returns - ------- - new_walkers : list of walkers - The resulting walkers of the cycle - - sim_components : list - Deep copies of the runner, resampler, and boundary - conditions objects at the end of the cycle. - - - """ - start_time = time.time() - self.init(num_workers=num_workers) - cycle_idx = 0 - walkers = self.init_walkers - while time.time() - start_time < run_time: - logger.info( - "starting cycle {} at time {}".format( - cycle_idx, time.time() - start_time - ) - ) - - walkers, filters = self.run_cycle(walkers, segments_length, cycle_idx) - - logger.info( - "ending cycle {} at time {}".format(cycle_idx, time.time() - start_time) - ) - - cycle_idx += 1 - - logger.info("Cleaning up simulation") - self.cleanup() - - return walkers, deepcopy(filters) - def run_simulation( self, n_cycles: int, @@ -817,7 +755,8 @@ def run_simulation_by_time( cycle_idx = 0 walkers = self.init_walkers - while time.time() - start_time < run_time: + # run until time is elapsed, but guarantee to run at least one cycle + while time.time() - start_time < run_time or cycle_idx < 1: logger.info( "starting cycle {} at time {}".format( cycle_idx, time.time() - start_time diff --git a/src/wepy/work_mapper/base.py b/src/wepy/work_mapper/base.py index bae5bcce..f43f678a 100644 --- a/src/wepy/work_mapper/base.py +++ b/src/wepy/work_mapper/base.py @@ -7,25 +7,20 @@ # Standard Library +from wepy.interface import ( + WorkMapperFactoryArgs, + Task, +) from wepy.walker import WalkerState logger = logging.getLogger(__name__) WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) -Task_ = TypeVar("Task_") -WorkMapperParams_ = ParamSpec("WorkMapperParams_") -InnerTask_ = TypeVar("InnerTask_") +Task_ = TypeVar("Task_", bound=Task) -class Task(Protocol[WalkerState_]): - - def __call__( - self, - walker_state: WalkerState_, - ) -> WalkerState_: - ... -class WorkMapper(Protocol[WalkerState_, WorkMapperParams_, Task_, InnerTask_]): +class WorkMapper(Protocol[WalkerState_, Task_]): def __init__( self, @@ -36,16 +31,13 @@ def __init__( ], WalkerState_, ], - *args: WorkMapperParams_.args, - **kwargs: WorkMapperParams_.kwargs, + wm_args: WorkMapperFactoryArgs | None, ) -> None: ... def init(self) -> None: ... - def gen_task(self, outer_task: Task_) -> Task_ | InnerTask_: ... - def map( self, tasks: list[Task_], diff --git a/src/wepy/work_mapper/mapper.py b/src/wepy/work_mapper/mapper.py deleted file mode 100644 index 40e94e08..00000000 --- a/src/wepy/work_mapper/mapper.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Reference implementation of serial work mapper.""" - -from typing import Callable, Literal, Generic, TypeVar, Protocol, Any -import logging - -# Standard Library -import multiprocessing as mp -import queue as pyq -import signal -import sys -import time -import traceback -from warnings import warn - -from wepy.walker import Walker -from wepy.work_mapper.base import WorkMapper, AnyWalker - -logger = logging.getLogger(__name__) - -class ABCWorkerMapper(ABCMapper): - def __init__( - self, - num_workers: int | None = None, - segment_func: SegmentFunc = None, - proc_start_method: Literal["fork", "spawn", "forkserver"] = "fork", - **kwargs, - ) -> None: - """Constructor for WorkerMapper. - - Parameters - ---------- - num_workers : int - The number of worker processes to spawn. - - segment_func : callable, optional - Set a default segment_func. Typically set at runtime. - - proc_start_method : str or None - A string indicating the type of process start method to - use from python multiprocessing typically 'fork', 'spawn', - or 'forkserver', or the platform default for None. See - documentation. Generates a context with the method - multiprocessing.get_context(proc_start_method) on `init`. - - """ - - super().__init__(segment_func=segment_func, **kwargs) - - self._proc_start_method = proc_start_method - - self._num_workers = num_workers - self._worker_segment_times = None - - if num_workers is not None: - self._worker_segment_times = {i: [] for i in range(self.num_workers)} - - def init(self, num_workers=None, segment_func=None, **kwargs): - """Runtime initialization and setting of function to map over walkers. - - Parameters - ---------- - num_workers : int - The number of worker processes to spawn - - segment_func : callable implementing the Runner.run_segment interface - - """ - - super().init(segment_func=segment_func) - - # create the multiprocessing context to use for spawning - # processes here - self._mp_ctx = mp.get_context(method=self._proc_start_method) - - # the number of workers must be given here or set as an object attribute - if num_workers is None and self.num_workers is None: - raise ValueError( - "The number of workers must be given, received {}".format(num_workers) - ) - - # if the number of walkers was given for this init() call use - # that, otherwise we use the default that was specified when - # the object was created - elif num_workers is not None and self.num_workers is None: - self._num_workers = num_workers - - # update the worker segment times - self._worker_segment_times = {i: [] for i in range(self.num_workers)} - - def cleanup(self, **kwargs): - # ALERT: is this all we need to do? I have a hunch there is - # more caveats, but these context objects are not really - # documented - - # make sure the context for this work mapper is destroyed - del self._mp_ctx - - @property - def num_workers(self): - """The number of worker processes.""" - return self._num_workers - - @property - def worker_segment_times(self): - """The run timings for each segment for each walker. - - Returns - ------- - worker_seg_times : dict of int : list of float - Dictionary mapping worker indices to a list of times in - seconds for each segment run. - - """ - return self._worker_segment_times - - def _make_task(self, *args, **kwargs): - """Generate a task from 'segment_func' attribute. - - Similar to partial evaluation (or currying). - - Args will be eventually used as the arguments to the call of - 'segment_func' by the worker processes when they receive the - task from the queue. - - Returns - ------- - task : Task object - - """ - return Task(self._func, *args, **kwargs) diff --git a/src/wepy/work_mapper/proc_pool_mapper.py b/src/wepy/work_mapper/proc_pool_mapper.py index 3a37b096..b00b22f3 100644 --- a/src/wepy/work_mapper/proc_pool_mapper.py +++ b/src/wepy/work_mapper/proc_pool_mapper.py @@ -5,6 +5,9 @@ import attrs +from wepy.interface import ( + WorkMapperFactoryArgs, +) from wepy.work_mapper.base import Task, WalkerState, WorkMapper # log_safe.initialize_safe_logging() @@ -39,9 +42,9 @@ class ProcPoolMapper( def __init__( self, - num_workers: int, + wm_args: WorkMapperFactoryArgs, ) -> None: - self._num_workers = num_workers + self._num_workers = wm_args.num_workers def init( self, diff --git a/src/wepy/work_mapper/serial.py b/src/wepy/work_mapper/serial.py index 210739b4..92e302c6 100644 --- a/src/wepy/work_mapper/serial.py +++ b/src/wepy/work_mapper/serial.py @@ -6,6 +6,9 @@ from typing import Callable, Literal, Generic, TypeVar, Protocol, Any, ParamSpec, Concatenate, Sequence import logging +from wepy.interface import ( + WorkMapperFactoryArgs, +) from wepy.walker import Walker, WalkerState from wepy.work_mapper.base import WorkMapper, TaskException, Task @@ -25,6 +28,7 @@ class SerialMapper( def __init__( self, + wm_args: WorkMapperFactoryArgs, ) -> None: self._worker_segment_times: dict[int, list[float]] = {0: []} @@ -46,13 +50,6 @@ def init(self) -> None: def cleanup(self) -> None: pass - def gen_task( - self, - outer_task: Task_, - task_idx: int, - ) -> Task_: - return outer_task - def map( self, tasks: list[Task_], @@ -62,10 +59,8 @@ def map( results: list[WalkerState_] = [] for task_idx, (task, walker_state) in enumerate(zip(tasks, walker_states, strict=True)): - _task = self.gen_task(task, task_idx=task_idx) - tic = time.time() - result = _task(walker_state) + result = task(walker_state) toc = time.time() segment_times.append(toc - tic) diff --git a/src/wepy/work_mapper/task_mapper.py b/src/wepy/work_mapper/task_mapper.py deleted file mode 100644 index 3cac5f4a..00000000 --- a/src/wepy/work_mapper/task_mapper.py +++ /dev/null @@ -1,702 +0,0 @@ -# Standard Library -import logging -from typing import Literal, Any - -# Standard Library -import multiprocessing as mp -import pickle -import queue as pyq -import signal -import sys -import time -import traceback -from warnings import warn - -# First Party Library -from wepy.work_mapper.base import ( - Task, - TaskException, - WrapperException, -) - -logger = logging.getLogger(__name__) - -class TaskProcessException(WrapperException): - pass - - -class TaskProcessKilledError(ChildProcessError): - pass - -class WalkerTaskProcess(mp.Process): - NAME_TEMPLATE = "Walker-{}" - - def __init__( - self, - walker_idx: int, - mapper_attributes, - func, - task_args, - task_kwargs, - worker_queue, - results_list, - worker_segment_times, - interrupt_connection, - **kwargs, - ): - # initialize the process customizing the name - mp.Process.__init__(self, name=self.NAME_TEMPLATE.format(walker_idx), **kwargs) - - # the idea with this TaskProcess thing is that we pass in all - # the data to the constructor to create a "thunk" (a closure - # that is ready to be run without arguments) and then when run - # is called there will be no arguments to be passed. This - # simplifies the flow of data and underscores that the task is - # the process. - - # task arguments - self._func = func - self._task_args = task_args - self._task_kwargs = task_kwargs - - self.walker_idx = walker_idx - self._worker_idx = None - self.mapper_attributes = mapper_attributes - - # set the managed datastructure proxies as an attribute so we - self._worker_queue = worker_queue - self._results_list = results_list - self._worker_segment_times = worker_segment_times - self._irq_channel = interrupt_connection - - # also register the SIGTERM signal handler for graceful - # shutdown with reporting to mapper - signal.signal(signal.SIGTERM, self._external_sigterm_shutdown) - - def _external_sigterm_shutdown(self, signum, frame): - logger.debug("Received external SIGTERM kill command.") - - logger.debug("Alerting mapper that this will be honored.") - - # send an error to the mapper that the worker has been killed - self._irq_channel.send( - TaskProcessKilledError( - "{} (pid: {}) killed by external SIGTERM signal".format( - self.name, self.pid - ) - ) - ) - - logger.debug("Acknowledgment sent") - - logger.debug("Shutting down process") - - def _shutdown(self): - """The normal shutdown which can be ordered by the work mapper.""" - - logger.debug("Received SIGTERM kill command from mapper") - - logger.debug("Acknowledging kill request will be honored") - - # report back that we are shutting down with a True - self._irq_channel.send(True) - - logger.debug("Acknowledgment sent") - - logger.debug("Shutting down process") - - @property - def attributes(self, key): - return self._attributes - - @attributes.getter - def attributes(self, key): - return self._attributes[key] - - def _run_task(self, task): - # run the task thunk - logger.info("{}: Running task".format(self.name)) - try: - result = self.run_task(task) - except Exception as task_exception: - # get the traceback for the exception - tb = sys.exc_info()[2] - - msg = "Exception '{}({})' caught in a task.".format( - type(task_exception).__name__, task_exception - ) - traceback_log_msg = """Traceback: --------------------------------------------------------------------------------- -{} --------------------------------------------------------------------------------- - """.format( - "".join( - traceback.format_exception(type(task_exception), task_exception, tb) - ), - ) - - logger.critical("{}: ".format(self.name) + msg + "\n" + traceback_log_msg) - - # raise a TaskException to distinguish it from the worker - # errors with the metadata about the original exception - - raise TaskException( - "Error occured during task execution, recovery not possible.", - wrapped_exception=task_exception, - tb=tb, - ) - - return result - - def run_task(self, task): - logger.info("Running an unspecialized task") - - return task() - - def run(self): - logger.debug("{}: starting to run".format(self.name)) - - # try to run the worker and it's task, except either class of - # error that can come from it either from the worker - # (WorkerException) or the task (TaskException) and communicate it - # back to the main process - - # if we get an exception there is some cleanup logic - run_exception = None - - try: - # run the worker, which will retrieve its task from the - # queue attempt to run the task, and if it succeeds will - # put the results on the result queue, if the task fails - # it will catch it and wrap it as a task exception - self._run_walker() - - except TaskException as task_exception: - logger.error("{}: TaskException caught".format(self.name)) - - run_exception = task_exception - - # anything else is considered a WorkerException so take the - # original exception and generate a worker exception from that - except Exception as exception: - logger.debug("{}: TaskProcessException Error caught".format(self.name)) - - # get the traceback - tb = sys.exc_info()[2] - - msg = "Exception '{}({})' caught in a task process.".format( - type(exception).__name__, exception - ) - traceback_log_msg = """Traceback: --------------------------------------------------------------------------------- -{} --------------------------------------------------------------------------------- - """.format( - "".join(traceback.format_exception(type(exception), exception, tb)), - ) - - logger.error(msg + "\n" + traceback_log_msg) - - # raise a TaskError to distinguish it from the worker - # errors with the metadata about the original exception - - walker_exception = TaskProcessException( - "Error occured during task process execution.", - wrapped_exception=exception, - tb=tb, - ) - - run_exception = walker_exception - - # raise worker_exception - if run_exception is not None: - logger.debug( - "{}: Putting exception in managed results list".format(self.name) - ) - - # then put the exception and the traceback onto the queue - # so we can communicate back to the parent process - try: - self._results_list[self.walker_idx] = run_exception - except BrokenPipeError as exc: - logger.error( - "{}: Pipe is broken indicating the root process has already exited:\n{}".format( - self.name, exc - ) - ) - - def _run_walker(self): - logger.info( - "Walker process started as name: {}; PID: {}".format(self.name, self.pid) - ) - - # lock a worker, then pop it off the queue so no other process - # tries to use it - worker_received = False - while not worker_received: - # pop off a worker to use it, this will block until it - # receives a worker - try: - worker_idx = self._worker_queue.get_nowait() - except pyq.Empty: - pass - - # always do on a successful get - else: - if type(worker_idx) == int: - worker_received = True - logger.info("{}: acquired worker {}".format(self.name, worker_idx)) - - # if it is a shutdown signal we do so - elif worker_idx is signal.SIGTERM: - logger.info( - "{}: SIGTERM signal received from mapper. Shutting down.".format( - self.name - ) - ) - - self._shutdown() - - return None - - # check to see if there is any signals on the interrupt channel - if self._irq_channel.poll(): - # get the message - message = self._irq_channel.recv() - - logger.debug( - "{}: Received message from mapper on filehandle {}: {}".format( - self.name, self._irq_channel.fileno(), message - ) - ) - - # handle the message - - # check for signals to die - if message is signal.SIGTERM: - logger.critical( - f"{self.name}: SIGTERM signal received from mapper. Shutting down." - ) - - self._shutdown() - - return None - - else: - logger.error( - "{}: Message not recognized, continuing operations and" - " sending error to mapper".format(self.name) - ) - self._irq_channel.send( - ValueError( - "Message: {} not recognized continuing operations".format( - message - ) - ) - ) - - # after the wait loop we can now perform our work - - self._worker_idx = worker_idx - - # generate the task thunk - task = Task(self._func, *self._task_args, **self._task_kwargs) - - # run the task - start = time.time() - logger.info("{}: running function. Time: {}".format(self.name, time.time())) - - # run the task doing the proper handling of the task - # exception, this can raise a task exception - result = self._run_task(task) - - logger.info( - "{}: finished running function. Time {}".format(self.name, time.time()) - ) - end = time.time() - - # we separately set the result to the results list. This is so - # we can debug performance problems, and dissect what is due - # to computation time and what is due to communication - logger.info( - "{}: Setting value to results list. Time {}".format(self.name, time.time()) - ) - - logger.debug("Serializing the result walker") - - serial_result = pickle.dumps(result) - logger.debug("Putting tagged serialized walker tuple on managed results list") - self._results_list[self.walker_idx] = ("Walker", serial_result) - - logger.info( - "{}: Finished setting value to results list. Time {}".format( - self.name, time.time() - ) - ) - - # put the worker back onto the queue since we are done using it - self._worker_queue.put(worker_idx) - - logger.info("{}: released worker {}".format(self.name, worker_idx)) - - # add the time for this segment to the collection of the worker times - segment_time = end - start - seg_times = self._worker_segment_times[worker_idx] + [segment_time] - - # we must explicitly set the new value in total to trigger an - # update of the real dictionary. In place modification of - # proxy objects has no effect - self._worker_segment_times[worker_idx] = seg_times - - logger.info("{}: Exiting normally having completed the task".format(self.name)) - - -# class TaskMapper(ABCWorkerMapper): -# """Process-per-task mapper. - -# This method of work mapper starts new processes for each runner -# segment task that needs to be run. This allows cheap copying of -# shared state using the operating system primitives. On linux this -# would be either 'fork' (default) or 'spawn'. Fork is cheap but -# doesn't initialize certain process namespace things, whereas spawn -# is much more expensive but properly cleans things up. Fork should -# be sufficient in most cases, however spawn may be needed when you -# have some special contexts in the parent process. This is the case -# with starting CUDA contexts in the main parent process and then -# forking new processes from it. We suggest using fork and avoiding -# making these kinds of contexts in the main process. - -# This method avoids using shared memory or sending objects through -# interprocess communication (that has a serialization and -# deserialization cost associated with them) by using OS copying -# mechanism. However, a new process will be created each cycle for -# each walker in the simulation. So if you want a large number of -# walkers you may experience a large overhead. If your walker states -# are very small or a very fast serializer is available you may also -# not benefit from full process address space copies. Instead the -# WorkerMapper may be better suited. - -# """ - -# def __init__( -# self, -# walker_task_type=None, -# num_workers: int | None = None, -# proc_start_method: Literal["fork", "spawn", "forkserver"] = "fork", -# **kwargs -# ): - -# self._attributes = kwargs - -# self._proc_start_method = proc_start_method - -# self._num_workers = num_workers -# self._worker_segment_times = None - -# if num_workers is not None: -# self._worker_segment_times = {i: [] for i in range(self.num_workers)} - -# # choose the type of the worker -# if walker_task_type is None: -# self._walker_task_type = WalkerTaskProcess -# warn("walker_task_type not given using the default base class") -# logger.warning("walker_task_type not given using the default base class") -# else: -# self._walker_task_type = walker_task_type - -# # initialize a list to put results in -# self.results = None - -# # this is meant to be a transient variable, will be initialized and deinitialized -# self._walker_processes = None - -# def init( -# self, -# segment_func, -# num_workers: int | None = None, -# ): - -# # create the multiprocessing context to use for spawning -# # processes here -# self._mp_ctx = mp.get_context(method=self._proc_start_method) - -# # the number of workers must be given here or set as an object attribute -# if num_workers is None and self.num_workers is None: -# raise ValueError( -# "The number of workers must be given, received {}".format(num_workers) -# ) - -# # if the number of walkers was given for this init() call use -# # that, otherwise we use the default that was specified when -# # the object was created -# elif num_workers is not None and self.num_workers is None: -# self._num_workers = num_workers - -# # update the worker segment times -# self._worker_segment_times = {i: [] for i in range(self.num_workers)} - -# # now that we have started the processes register the handler -# # for SIGTERM signals that will clean up our children cleanly -# signal.signal(signal.SIGTERM, self._sigterm_shutdown) - -# def _sigterm_shutdown(self, signum, frame): -# logger.critical("Received external SIGTERM, forcing shutdown.") - -# self.force_shutdown() - -# logger.critical("Shutdown complete.") - -# @property -# def walker_task_type(self): -# """The callable that generates a worker object. - -# Typically this is just the type from the class definition of -# the Worker where the constructor is called. - -# """ -# return self._walker_task_type - -# def force_shutdown(self): -# # send sigterm signals to processes to kill them -# for walker_idx, walker_process in enumerate(self._walker_processes): -# logger.critical( -# "Sending SIGTERM message on {} to worker {}".format( -# self._irq_parent_conns[walker_idx].fileno(), walker_idx -# ) -# ) - -# # send a kill message to the worker -# self._irq_parent_conns[walker_idx].send(signal.SIGTERM) - -# logger.critical("All kill messages sent to workers") - -# # wait for the walkers to finish and handle errors in them -# # appropriately -# alive_walkers = [walker.is_alive() for walker in self._walker_processes] -# walker_exitcodes = {} -# premature_exit = False -# while any(alive_walkers): -# for walker_idx, walker in enumerate(self._walker_processes): -# if not alive_walkers[walker_idx]: -# continue - -# if walker.is_alive(): -# pass - -# # otherwise the walker is done -# else: -# alive_walkers[walker_idx] = False -# walker_exitcodes[walker_idx] = walker.exitcode - -# def map(self, *args, **kwargs): -# # run computations in a Manager context -# with self._mp_ctx.Manager() as manager: -# num_walkers = len(args[0]) - -# # to manage access to worker resources we use a queue with -# # the index of the worker -# worker_queue = manager.Queue() - -# # put the workers onto the queue -# for worker_idx in range(self.num_workers): -# worker_queue.put(worker_idx) - -# # initialize segment times for workers to -# # fill in -# worker_segment_times = manager.dict() - -# # initialize for the number of workers, since these will be -# # the slots to put timing results in -# for i in range(self.num_workers): -# worker_segment_times[i] = [] - -# # make a shared list for the walker results -# results = manager.list() - -# # since this will be indexed by walker index initialize the -# # length of the array -# for walker in range(num_walkers): -# results.append(None) - -# # use pipes for communication channels between this parent -# # process and the children for sending specific interrupts -# # such as the signal to kill them. Note that the clean way to -# # end the process is to send poison pills on the task queue, -# # this is for other stuff. IRQ is a common abbreviation for -# # interrupts -# self._irq_parent_conns = [] - -# # unpack the generator for the kwargs -# kwargs = {key: list(kwarg) for key, kwarg in kwargs.items()} - -# # create the task based processes -# self._walker_processes = [] -# for walker_idx, task_args in enumerate(zip(*args)): -# task_kwargs = {key: value[walker_idx] for key, value in kwargs.items()} - -# # make the interrupt pipe -# parent_conn, child_conn = self._mp_ctx.Pipe() -# self._irq_parent_conns.append(parent_conn) - -# # start a process for this walker -# walker_process = self.walker_task_type( -# walker_idx, -# self._attributes, -# self._func, -# task_args, -# task_kwargs, -# worker_queue, -# results, -# worker_segment_times, -# child_conn, -# ) - -# walker_process.start() - -# self._walker_processes.append(walker_process) - -# new_walkers = [None for _ in range(num_walkers)] -# results_found = [False for _ in range(num_walkers)] -# while not all(results_found): -# # go through the results list and handle the values that may be there -# for walker_idx, result in enumerate(results): -# if results_found[walker_idx]: -# continue - -# # logger.info("Checking for walker {}".format(walker_idx)) - -# # first check to see if any of the task processes were -# # terminated from the system -# if self._irq_parent_conns[walker_idx].poll(): -# irq = self._irq_parent_conns[walker_idx].recv() - -# if issubclass(type(irq), TaskProcessKilledError): -# # just terminate if a worker goes down. We -# # could handle this better but it is not implemented now -# logger.critical( -# "Process {} was killed by sigterm, shutting down.".format( -# walker_process[walker_idx].name -# ) -# ) - -# logger.info( -# "Recovery is possible here, but is not implemented " -# "so we opt to fail fast and let you know a problem exists." -# "Please use checkpointing to avoid lost data." -# ) - -# self.force_shutdown() -# logger.critical("Shutdown complete.") - -# logger.debug( -# "Received {} acknowledgement from {}".format( -# ack, worker.name -# ) -# ) - -# # if no interrupts were handled we continue - -# # if it is None no response has been made at all -# # yet, this is the initialized value -# if result is None: -# pass - -# # walker results are returned serialized as -# # pickles, they are packed into a tuple so that we -# # can associate them with an explicit marker, if -# # we have a tuple then we can handle that -# # appropriately -# elif type(result) == tuple: -# logger.debug("Received a results tuple") - -# assert ( -# len(result) == 2 -# ), "Result tuples should be only be (ID, pickle)" - -# result_id, payload = result - -# # there was a walker successfully returned -# if result_id == "Walker": -# logger.debug("Received a serialized results walker") - -# # deserialize -# logger.debug("deserializing") -# new_walker = pickle.loads(payload) - -# logger.info("Got result for walker {}".format(walker_idx)) - -# new_walkers[walker_idx] = new_walker -# results_found[walker_idx] = True - -# else: -# raise ValueError("Unkown result ID: {}".format(result_id)) - -# elif issubclass(type(result), TaskException): -# logger.critical( -# "Exception encountered in a task which is unrecoverable." -# "You will need to reconfigure your components in a stable manner." -# ) - -# self.force_shutdown() - -# logger.critical("Shutdown complete.") -# raise result - -# elif issubclass(type(result), TaskProcessException): -# # we make just an error message to say that errors -# # in the worker may be due to the network or -# # something and could recover -# logger.error( -# "Exception encountered in the work mapper task process." -# "Recovery possible, see further messages." -# ) - -# # However, the current implementation doesn't -# # support retries or whatever so we issue a -# # critical log informing that it has been elevated -# # to critical and will force shutdown -# logger.critical( -# "Task process error mode resiliency not supported at this time." -# "Performing force shutdown and simulation ending." -# ) - -# self.force_shutdown() - -# logger.critical("Shutdown complete.") -# raise result - -# elif issubclass(type(result), Exception): -# logger.critical( -# "Unknown exception {} encountered.".format(result) -# ) - -# self.force_shutdown() - -# logger.critical("Shutdown complete.") - -# raise result - -# else: -# logger.critical( -# "Unknown result value {} encountered.".format(result) -# ) - -# self.force_shutdown() - -# logger.critical("Shutdown complete.") - -# # save the managed list of the recorded worker times locally -# for key, val in worker_segment_times.items(): -# self._worker_segment_times[key] = val - -# # wait for the processes to end -# # for walker in self._walker_processes: -# # walker.join() -# # logger.info("Joined {}".format(walker.name)) - -# # deinitialize the current walker processes -# self._walker_processes = None - -# return new_walkers - - diff --git a/src/wepy/work_mapper/worker_mapper.py b/src/wepy/work_mapper/worker_mapper.py deleted file mode 100644 index c3fc772a..00000000 --- a/src/wepy/work_mapper/worker_mapper.py +++ /dev/null @@ -1,763 +0,0 @@ -"""Classes for workers and tasks for use with WorkerMapper.""" - -from .mapper import WrapperException - - -class WorkerException(WrapperException): - pass - - -class WorkerKilledError(ChildProcessError): - pass - - -# TODO: move this class to the wepy.work_mapper.worker class where it -# belongs. It shouldn't be in this namespace, but we will leave it -# here. Furthermore I would like to rename it since we now have -# different worker mapper implementations with different concurrency -# models -class WorkerMapper(ABCWorkerMapper): - """Work mapper implementation using multiple worker processes and task - queue. - - Uses the python multiprocessing module to spawn multiple worker - processes which watch a task queue of walker segments. - """ - - def __init__( - self, - num_workers=None, - worker_type=None, - worker_attributes=None, - segment_func=None, - **kwargs, - ): - """Constructor for WorkerMapper. - - Parameters - ---------- - num_workers : int - The number of worker processes to spawn. - - worker_type : callable, optional - Callable that generates an object implementing the Worker - interface, typically a type from a Worker class. - - worker_attributes : dictionary - A dictionary of values that are passed to the worker - constructor as key-word arguments. - - segment_func : callable, optional - Set a default segment_func. Typically set at runtime. - - """ - - super().__init__(num_workers=num_workers, segment_func=segment_func, **kwargs) - - # since the workers will be their own process classes we - # handle this data - - # attributes that will be passed to the worker constructors - if worker_attributes is not None: - self._worker_attributes = worker_attributes - else: - self._worker_attributes = {} - - # choose the type of the worker - if worker_type is None: - self._worker_type = Worker - warn("worker_type not given using the default base class") - logger.warn("worker_type not given using the default base class") - else: - self._worker_type = worker_type - - @property - def worker_type(self): - """The callable that generates a worker object. - - Typically this is just the type from the class definition of - the Worker where the constructor is called. - - """ - return self._worker_type - - def init(self, num_workers=None, segment_func=None, **kwargs): - """Runtime initialization and setting of function to map over walkers. - - Parameters - ---------- - num_workers : int - The number of worker processes to spawn - - segment_func : callable implementing the Runner.run_segment interface - - """ - - super().init(num_workers=num_workers, segment_func=segment_func, **kwargs) - - manager = self._mp_ctx.Manager() - - # Establish communication queues - - # A queue for errors - self._exception_queue = manager.Queue() - - # queue for the tasks we know the batch size so we don't need - # a JoinableQueue - self._task_queue = manager.Queue() - - # results queue - self._result_queue = manager.Queue() - - # use pipes for communication channels between this parent - # process and the children for sending specific interrupts - # such as the signal to kill them. Note that the clean way to - # end the process is to send poison pills on the task queue, - # this is for other stuff. IRQ is a common abbreviation for - # interrupts - self._irq_parent_conns = [] - - # Start workers, giving them all the queues - self._workers = [] - for i in range(self.num_workers): - # make a pipe to communicate with this worker for the int - parent_conn, child_conn = self._mp_ctx.Pipe() - self._irq_parent_conns.append(parent_conn) - - # create the worker giving it all of the communication - # channels - worker = self.worker_type( - i, - self._task_queue, - self._result_queue, - self._exception_queue, - child_conn, - mapper_attributes=self._attributes, - **self._worker_attributes, - ) - self._workers.append(worker) - - # start the worker processes - for worker in self._workers: - worker.start() - - logger.info( - "Worker process started as name: {}; PID: {}".format( - worker.name, worker.pid - ) - ) - - # now that we have started the processes register the handler - # for SIGTERM signals that will clean up our children cleanly - signal.signal(signal.SIGTERM, self._sigterm_shutdown) - - def _sigterm_shutdown(self, signum, frame): - logger.critical("Received external SIGTERM, forcing shutdown.") - - self.force_shutdown() - - def force_shutdown(self, **kwargs): - logger.critical("Forcing shutdown") - - # our primary job is to shut down all of the running processes - # without just shutting down the queues and breaking the pipes - - # to do this we send the kill signals to them on the kill - # channel. - - for worker_idx, worker in enumerate(self._workers): - logger.critical( - "Sending SIGTERM message on {} to worker {}".format( - self._irq_parent_conns[worker_idx].fileno(), worker_idx - ) - ) - - # send a kill message to the worker - self._irq_parent_conns[worker_idx].send(signal.SIGTERM) - - logger.critical("All kill messages sent to workers") - - # check that all have exited - alive_workers = [worker.is_alive() for worker in self._workers] - worker_acks = {} - worker_exitcodes = {} - premature_exit = False - while any(alive_workers) and not premature_exit: - for worker_idx, worker in enumerate(self._workers): - # ignore already known dead workers - if not alive_workers[worker_idx]: - continue - - if worker.is_alive(): - # if it is still alive and we have an ack from it - # just terminate. There is a bug in the code and - # is out of our control - if worker_idx in worker_acks: - logger.debug( - "Ack received from {} but has not shut down".format( - worker.name - ) - ) - premature_exit = True - - # otherwise we need to try and receive the ack - elif self._irq_parent_conns[worker_idx].poll(1): - # receive the acknowledgement - ack = self._irq_parent_conns[worker_idx].recv() - - logger.debug( - "Received {} acknowledgement from {}".format( - ack, worker.name - ) - ) - - # make sure the ack is affirmative - if ack is True: - worker_acks[worker_idx] = ack - - # if it is an exeption wrap it as a worker - # error and use the os to kill the process - elif issubclass(type(ack), Exception): - # wrap it as a worker exception - exception = WorkerException(wrapped_exception=ack) - worker_acks[worker_idx] = exception - - logger.critical( - "{} not responding, terminating with SIGTERM".format( - worker.name - ) - ) - - worker.terminate() - - else: - alive_workers[worker_idx] = False - worker_exitcodes[worker_idx] = worker.exitcode - - if any(alive_workers): - logger.critical( - "Terminating main process with running workers {}".format( - ",".join( - [ - str(worker_idx) - for worker_idx in range(len(self._workers)) - if alive_workers[worker_idx] - ] - ) - ) - ) - - def cleanup(self, **kwargs): - """Runtime post-simulation tasks. - - This is run either at the end of a successful simulation or - upon an error in the main process of the simulation manager - call to `run_cycle`. - - The Mapper class performs no actions here and all arguments - are ignored. - - """ - - super().cleanup(**kwargs) - - # send poison pills (Stop signals) to the queues to stop them in a nice way - # and let them finish up - for i in range(self.num_workers): - self._task_queue.put((None, None)) - - # delete the queues and workers - self._task_queue = None - self._result_queue = None - self._workers = None - - def map(self, *args, **kwargs): - # docstring in superclass - - map_process = self._mp_ctx.current_process() - logger.info( - "Mapping from process {}; PID {}".format(map_process.name, map_process.pid) - ) - - # make tuples for the arguments to each function call - task_args = zip(*args) - kwargs = {key: list(kwarg) for key, kwarg in kwargs.items()} - - num_tasks = len(args[0]) - # Enqueue the jobs - for task_idx, task_arg in enumerate(task_args): - task_kwargs = {key: value[task_idx] for key, value in kwargs.items()} - - # a task will be the actual task and its task idx so we can - # sort them later - self._task_queue.put((task_idx, self._make_task(*task_arg, **task_kwargs))) - - logger.info("Waiting for tasks to be run") - - # poll the exception and result queues for results - n_results_left = num_tasks - results = [] - while n_results_left > 0: - # first check if any errors came back but don't wait, - # since the methods for querying whether it is empty or - # not are not reliable we just try and if we don't get - # anything we will come back around - try: - proc_name, pid, exception = self._exception_queue.get_nowait() - except pyq.Empty: - pass - - else: - logger.error( - "Exception occured in process {}; pid {}.".format(proc_name, pid) - ) - - # we can handle Task and Worker exceptions differently - if type(exception) == TaskException: - logger.critical( - "Exception encountered in a task which is unrecoverable." - "You will need to reconfigure your components in a stable manner." - ) - - self.force_shutdown() - - logger.critical("Shutdown complete.") - raise exception - - elif type(exception) == WorkerException: - # we make just an error message to say that errors - # in the worker may be due to the network or - # something and could recover - logger.error( - "Exception encountered in the work mapper worker process." - "Recovery possible, see further messages." - ) - - # However, the current implementation doesn't - # support retries or whatever so we issue a - # critical log informing that it has been elevated - # to critical and will force shutdown - logger.critical( - "Worker error mode resiliency not supported at this time." - "Performing force shutdown and simulation ending." - ) - - self.force_shutdown() - - logger.critical("Shutdown complete.") - raise exception - - else: - logger.critical("Unknown exception encountered.") - - self.force_shutdown() - - logger.critical("Shutdown complete.") - - raise exception - - # attempt to get something off of the results queue - try: - result = self._result_queue.get_nowait() - except pyq.Empty: - pass - - # if we get something handle it - else: - logger.info("Retrieved result: {}".format(result)) - results.append(result) - - # reduce the counter so we know when we are done - n_results_left -= 1 - - # sort the results according to their task_idx - results.sort() - - # save the task run times, so they can be accessed if desired, - # after clearing the task times from the last mapping - - # DEBUG: removing this because it should be set on init() - # self._worker_segment_times = {i : [] for i in range(self.num_workers)} - - for task_idx, worker_idx, task_time, result in results: - self._worker_segment_times[worker_idx].append(task_time) - - # then just return the values of the function - return [result for task_idx, worker_idx, task_time, result in results] - - -# same for the worker in terms of refactoring -class Worker(mp.Process): - """Worker process. - - This is a subclass of process with an overriden `__init__` - constructor that will automatically generate the Process. - - When this class is constructed a new process will be formed. - - """ - - NAME_TEMPLATE = "Worker-{}" - """A string formatting template to identify worker processes in - logs. The field will be filled with the worker index.""" - - def __init__( - self, - worker_idx, - task_queue, - result_queue, - exception_queue, - interrupt_connection, - mapper_attributes=None, - log_level="INFO", - **kwargs, - ): - """Constructor for the Worker class. - - Parameters - ---------- - worker_idx : int - The index of the worker. Should be unique. - - task_queue : multiprocessing.JoinableQueue - The shared task queue the worker will watch for new tasks to complete. - - result_queue : multiprocessing.Queue - The shared queue that completed task results will be placed on. - - interrupt_connection : multiprocessing.Connection - One end of a pipe to listen for messages specific to this worker. - - mapper_attributes : None or dict - A dictionary of the attributes of the mapper for reference in workers. - - kwargs : - The worker specific attributes - - """ - - # call the Process constructor - mp.Process.__init__(self, name=self.NAME_TEMPLATE.format(worker_idx)) - - self._exception_queue = exception_queue - self._exception = None - self._traceback = None - - # the queue that will trigger a shutdown in the event of failure - self._irq_channel = interrupt_connection - - # also register the SIGTERM signal handler for graceful - # shutdown with reporting to mapper - signal.signal(signal.SIGTERM, self._sigterm_shutdown) - - self._worker_idx = worker_idx - - self._mapper_attributes = mapper_attributes - - # set all the kwargs into an attributes dictionary - self._attributes = kwargs - - # the queues for work to be done and work done - self._task_queue = task_queue - self._result_queue = result_queue - - logger.debug("{} process created".format(self.name)) - - @property - def worker_idx(self): - """Dictionary of attributes of the worker.""" - return self._worker_idx - - @property - def attributes(self): - """Dictionary of attributes of the worker.""" - return self._attributes - - @property - def mapper_attributes(self): - """Dictionary of attributes of the worker.""" - return self._mapper_attributes - - def run(self): - logger.debug("{}: starting to run".format(self.name)) - - # try to run the worker and it's task, except either class of - # error that can come from it either from the worker - # (WorkerException) or the task (TaskException) and communicate it - # back to the main process - - # if we get an exception there is some cleanup logic - run_exception = None - - try: - # run the worker, which will retrieve its task from the - # queue attempt to run the task, and if it succeeds will - # put the results on the result queue, if the task fails - # it will catch it and wrap it as a task exception - self._run_worker() - - except TaskException as task_exception: - logger.error("{}: TaskException caught".format(self.name)) - - run_exception = task_exception - - # anything else is considered a WorkerException so take the - # original exception and generate a worker exception from that - except Exception as exception: - logger.debug("{}: WorkerError caught".format(self.name)) - - # get the traceback - tb = sys.exc_info()[2] - - msg = "Exception '{}({})' caught in a worker.".format( - type(exception).__name__, exception - ) - traceback_log_msg = """Traceback: --------------------------------------------------------------------------------- -{} --------------------------------------------------------------------------------- - """.format( - "".join(traceback.format_exception(type(exception), exception, tb)), - ) - - logger.error("{}:".format(self.name) + msg + "\n" + traceback_log_msg) - - # raise a TaskError to distinguish it from the worker - # errors with the metadata about the original exception - - worker_exception = WorkerException( - "Error occured during worker execution.", - wrapped_exception=exception, - tb=tb, - ) - - run_exception = worker_exception - - # raise worker_exception - if run_exception is not None: - logger.debug("{}: Putting exception on exception queue".format(self.name)) - - # then put the exception and the traceback onto the queue - # so we can communicate back to the parent process - try: - self._exception_queue.put((self.name, self.pid, run_exception)) - except BrokenPipeError as exc: - logger.error( - "Pipe is broken indicating the root process has already exited:\n{}".format( - exc - ) - ) - - # TODO: not sure if this is good or not - # then reraise the exception so it can be caught - # raise run_exception - - def _sigterm_shutdown(self, signum, frame): - logger.debug("Received external SIGTERM kill command.") - - logger.debug("Alerting mapper that this will be honored.") - - # send an error to the mapper that the worker has been killed - self._irq_channel.send( - WorkerKilledError( - "{} (pid: {}) killed by external SIGTERM signal".format( - self.name, self.pid - ) - ) - ) - - logger.debug("Acknowledgment sent") - - logger.debug("Shutting down process") - - def _shutdown(self): - logger.debug("Received SIGTERM kill command from mapper") - - logger.debug("Acknowledging kill request will be honored") - - # report back that we are shutting down with a True - self._irq_channel.send(True) - - logger.debug("Acknowledgment sent") - - logger.debug("Shutting down process") - - def _run_worker(self): - # run the logic associated with communication and liveness of - # the worker process itself, this is not necessarily a fatal - # (critical) error and restarting a worker might resolve the - # problem. This calls the _run_task method though which is - # always critical since the logic in the code cannot be - # disputed - - # TODO remove when confirmed that this works - # worker_process = mp.current_process() - logger.info( - "{}: Worker process started as name: {}; PID: {}".format( - self.name, self.name, self.pid - ) - ) - - while True: - # check to see if there is any signals in the interrupt channel - if self._irq_channel.poll(): - # get the message - message = self._irq_channel.recv() - - logger.debug( - "{}: Received message from mapper on filehandle {}: {}".format( - self.name, self._irq_channel.fileno(), message - ) - ) - - # handle the message - - # a SIGTERM is a signal to kill the process - # unconditionally - if message is signal.SIGTERM: - self._shutdown() - - # break from the event (while) loop and shut down - break - - # anything is not recognized and we will continue and - # report back that we don't recognize the message with - # a ValueError object - else: - logger.error( - "{}: Message not recognized, continuing operations and" - " sending error to mapper".format(self.name) - ) - self._irq_channel.send( - ValueError( - "Message: {} not recognized continuing operations".format( - message - ) - ) - ) - - # get the next task - try: - task_idx, next_task = self._task_queue.get(block=False, timeout=None) - - logger.debug("{}: Got task {}".format(self.name, task_idx)) - - except pyq.Empty: - task_idx = None - next_task = Ellipsis - - # # check for the poison pill which is the signal to stop - if next_task is None: - logger.info( - "{}: received {} {}: FINISHED".format( - self.name, task_idx, next_task - ) - ) - - # TODO remove since we aren't using joinble queue anymore - # mark the poison pill task as done - # self.task_queue.task_done() - - # and exit the loop - break - - # only execute this if a task was actually receieved from - # the queue; an Ellipsis indicates continue the loop - elif next_task is not Ellipsis: - logger.info( - "{}; task_idx : {}; args : {} ".format( - self.name, task_idx, next_task.args - ) - ) - - # run the task - start = time.time() - - answer = self._run_task(next_task) - - end = time.time() - task_time = end - start - - logger.info( - "{}: task_idx : {}; COMPLETED in {} s".format( - self.name, task_idx, task_time - ) - ) - - # put the results into the results queue with it's task - # index so we can sort them later - self._result_queue.put((task_idx, self.worker_idx, task_time, answer)) - - def run_task(self, task): - """Actually executes the task. - - This default runner simply executes the task thunk. - - This can be customized by subclasses in order to allow for - injection of worker specific data. - - Parameters - ---------- - task : Task object - The partially evaluated task; function plus arguments - - Returns - ------- - task_result - Results of running the task. - - """ - - return task() - - def _run_task(self, task): - """Runs the given task and returns the results. - - This manages handling exceptions and tracebacks from the - actual `run_task` function which is intended to be specialized - by different workers to inject worker specific arguments to - tasks. Such as node and device identification. - - Parameters - ---------- - task : Task object - The partially evaluated task; function plus arguments - - Returns - ------- - task_result - Results of running the task. - - """ - - logger.info("Running task") - try: - return self.run_task(task) - - except Exception as task_exception: - # get the traceback for the exception - tb = sys.exc_info()[2] - - msg = "Exception '{}({})' caught in a task.".format( - type(task_exception).__name__, task_exception - ) - traceback_log_msg = """Traceback: --------------------------------------------------------------------------------- -{} --------------------------------------------------------------------------------- - """.format( - "".join( - traceback.format_exception(type(task_exception), task_exception, tb) - ), - ) - - logger.critical(msg + "\n" + traceback_log_msg) - - # raise a TaskException to distinguish it from the worker - # errors with the metadata about the original exception - - raise TaskException( - "Error occured during task execution, recovery not possible.", - wrapped_exception=task_exception, - tb=tb, - ) diff --git a/tests/integration/test_openmm/test_serial_mapper.py b/tests/integration/test_openmm/test_serial_mapper.py index 9c5702b0..c8ccc39e 100644 --- a/tests/integration/test_openmm/test_serial_mapper.py +++ b/tests/integration/test_openmm/test_serial_mapper.py @@ -1,10 +1,36 @@ +from typing import Literal import functools import openmm -from wepy.runners.openmm import OpenMMRunner, OpenMMState, gen_sim_state + +import attrs + +from wepy.runners.openmm import OpenMMRunner, OpenMMState, gen_sim_state, PlatformKwargs from wepy.work_mapper.serial import SerialMapper +from wepy.work_mapper.base import Task from wepy_tools.systems.lennard_jones import LennardJonesPair +@attrs.define +class OpenMMTask(Task): + + runner: OpenMMRunner + segment_length: int + getState_kwargs: dict[str, bool] | None = None + + def __call__( + self, + state: OpenMMState, + platform: str | type(Ellipsis) | None = None, + platform_kwargs: PlatformKwargs | None = None, + ) -> OpenMMState: + + self.runner.run_segment( + state, + segment_length=self.segment_length, + getState_kwargs=self.getState_kwargs, + platform=platform, + platform_kwargs=platform_kwargs + ) def test_serial_mapper(): @@ -18,9 +44,95 @@ def test_serial_mapper(): integrator=integrator, ) - run_func = functools.partial( - runner.run_segment, - platform="Reference", + num_walkers = 4 + + walker_states = [ + OpenMMState(gen_sim_state( + lj_sys.positions, + system=lj_sys.system, + integrator=integrator, + )) + for _ + in range(num_walkers) + ] + + mapper = SerialMapper() + mapper.init() + + mapper.map( + [ + OpenMMTask( + runner=runner, + segment_length=10, + ) + for _ + in range(num_walkers) + ], + walker_states, + ) + +class OpenMMWorkerTask(Task): + + runner: OpenMMRunner + segment_length: int + getState_kwargs: dict[str, bool] | None = None + platform: str | type(Ellipsis) | None = None + platform_kwargs: PlatformKwargs | None = None + + def __call__(self, state: OpenMMState) -> OpenMMState: + + self.runner.run_segment( + state, + segment_length=self.segment_length, + getState_kwargs=self.getState_kwargs, + platform=self.platform, + platform_kwargs=self.platform_kwargs + ) + +OpenMMPlatformName = Literal[ + "Reference", + "CPU", + "OpenCL", + "CUDA", + "HIP", +] + +class OpenMMPlatformSpec: + name: OpenMMPlatformName + properties: dict[str, str] + +class OpenMMWorkerSpec: + platform: OpenMMPlatformSpec + +class OpenMMSerialMapper(SerialMapper): + + def __init__( + self, + worker_specs: dict[int, OpenMMWorkerSpec]| None = None, + ) -> None: + + self._worker_segment_times: dict[int, list[float]] = {0: []} + self._worker_specs = worker_specs + + + def gen_task(self, outer_task: OpenMMTask, task_idx: int) -> OpenMMWorkerTask: + + return OpenMMWorkerTask( + runner=outer_task.runner, + segment_length=outer_task.segment_length, + getState_kwargs=outer_task.getState_kwargs, + platform + ) + +def test_serial_devices(): + + lj_sys = LennardJonesPair() + integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + + runner = OpenMMRunner( + system=lj_sys.system, + topology=lj_sys.topology, + integrator=integrator, ) num_walkers = 4 @@ -35,10 +147,17 @@ def test_serial_mapper(): in range(num_walkers) ] - mapper = SerialMapper() - mapper.init(run_func) + mapper = OpenMMSerialMapper() + mapper.init() mapper.map( + [ + OpenMMTask( + runner=runner, + segment_length=10, + ) + for _ + in range(num_walkers) + ], walker_states, - [10 for _ in range(num_walkers)], ) diff --git a/tests/unit/test_sim_manager.py b/tests/unit/test_sim_manager.py index e13bd95f..666d1cbb 100644 --- a/tests/unit/test_sim_manager.py +++ b/tests/unit/test_sim_manager.py @@ -13,7 +13,6 @@ def sim_components() -> tuple[ list[Walker], NoRunner, MockRunner, - SerialMapper, ]: num_walkers = 4 @@ -28,7 +27,7 @@ def sim_components() -> tuple[ in range(num_walkers) ] - return init_walkers, MockRunner(), NoResampler(), SerialMapper() + return init_walkers, MockRunner(), NoResampler() class TestManager: @@ -37,23 +36,26 @@ def test___init__(self, sim_components): manager = Manager(*sim_components) assert len(manager.reporters) == 0 - assert manager.work_mapper == sim_components[3] + assert manager.work_mapper_class == SerialMapper + assert not hasattr(manager, "work_mapper") def test_init(self, sim_components): manager = Manager(*sim_components) manager.init() + assert hasattr(manager, "work_mapper") def test_cleanup(self, sim_components): manager = Manager(*sim_components) + manager.init() manager.cleanup() def test_run_segment(self, sim_components): - init_walkers, runner, resampler, mapper = sim_components + init_walkers, runner, resampler = sim_components manager = Manager(*sim_components) manager.init() @@ -67,9 +69,8 @@ def test_run_segment(self, sim_components): # test if something fails manager = Manager( init_walkers, - MockRunner(fail=True), + MockRunner(fail_walker_idxs={0,}), resampler, - mapper, ) manager.init() @@ -82,7 +83,7 @@ def test_run_segment(self, sim_components): def test_run_cycle(self, sim_components): - init_walkers, runner, resampler, mapper = sim_components + init_walkers, runner, resampler = sim_components manager = Manager(*sim_components) manager.init() @@ -96,9 +97,8 @@ def test_run_cycle(self, sim_components): # test if something fails manager = Manager( init_walkers, - MockRunner(fail=True), + MockRunner(fail_walker_idxs={0,}), resampler, - mapper, ) manager.init() @@ -130,13 +130,21 @@ def test_run_simulation_by_time(self, sim_components): manager.init() new_walkers, _ = manager.run_simulation_by_time( - 1, + 0.001, 2, num_workers=None, ) new_walkers, _ = manager.run_simulation_by_time( - 1, + 0.001, + 2, + num_workers=None, + continue_run_idx=0, + ) + + # make sure it runs at least one cycle + new_walkers, _ = manager.run_simulation_by_time( + 0.0000001, 2, num_workers=None, continue_run_idx=0, diff --git a/tests/unit/test_work_mapper/test_proc_pool_mapper.py b/tests/unit/test_work_mapper/test_proc_pool_mapper.py index 13591245..3f5bdc34 100644 --- a/tests/unit/test_work_mapper/test_proc_pool_mapper.py +++ b/tests/unit/test_work_mapper/test_proc_pool_mapper.py @@ -1,4 +1,7 @@ -# from wepy.walker import Walker, WalkerState + +from wepy.interface import ( + WorkMapperFactoryArgs, +) from wepy.work_mapper.proc_pool_mapper import ProcPoolMapper # NOTE: that you must import from a module (as opposed to inline in # the test file) or you have problems with importing modules when @@ -7,7 +10,7 @@ def test_ProcPoolMapper(): - poolmapper = ProcPoolMapper(num_workers=2) + poolmapper = ProcPoolMapper(WorkMapperFactoryArgs(num_workers=2)) poolmapper.init() diff --git a/tests/unit/test_work_mapper/test_serial.py b/tests/unit/test_work_mapper/test_serial.py index ad2d9f72..b2ab79ee 100644 --- a/tests/unit/test_work_mapper/test_serial.py +++ b/tests/unit/test_work_mapper/test_serial.py @@ -1,6 +1,9 @@ import functools import attrs from wepy.walker import Walker, WalkerState +from wepy.interface import ( + WorkMapperFactoryArgs, +) from wepy.work_mapper.serial import SerialMapper # some minimal definitions for testing a concrete work mapper @@ -39,7 +42,7 @@ class TestMapper: def test_map(self): - mapper = SerialMapper() + mapper = SerialMapper(WorkMapperFactoryArgs(num_workers=0)) mapper.init() From e2bfd653e63ecdcdf514f0d4c780fb862a2294fc Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 3 Dec 2025 15:57:47 -0500 Subject: [PATCH 044/143] fix resampling tests --- .../test_resampling/test_decisions/test_clone_merge.py | 9 +++++---- .../test_resampling/test_decisions/test_decision.py | 0 .../test_resampling/test_decisions/test_no_decision.py | 5 +++-- .../test_resamplers/test_noresampler.py | 10 ++++++---- .../test_resampling/test_resamplers/test_resampler.py | 0 5 files changed, 14 insertions(+), 10 deletions(-) delete mode 100644 tests/unit/test_resampling/test_decisions/test_decision.py delete mode 100644 tests/unit/test_resampling/test_resamplers/test_resampler.py diff --git a/tests/unit/test_resampling/test_decisions/test_clone_merge.py b/tests/unit/test_resampling/test_decisions/test_clone_merge.py index 4c93933b..0c57aaca 100644 --- a/tests/unit/test_resampling/test_decisions/test_clone_merge.py +++ b/tests/unit/test_resampling/test_decisions/test_clone_merge.py @@ -1,7 +1,8 @@ import pytest import attrs -from wepy.walker import Walker, WalkerState +from wepy.walker import Walker +from wepy.runners.mock import MockState from wepy.resampling.decisions.clone_merge import ( MultiCloneMergeDecision, CloneMergeDecisionEnum, @@ -12,16 +13,16 @@ class TestMultiCloneMergeDecision: def test_action(self): walker_1 = Walker( - state=WalkerState(a=1), + state=MockState(a=1), weight=1.0, ) walker_2 = Walker( - state=WalkerState(a=2), + state=MockState(a=2), weight=1.0, ) walker_3 = Walker( - state=WalkerState(a=3), + state=MockState(a=3), weight=1.0, ) diff --git a/tests/unit/test_resampling/test_decisions/test_decision.py b/tests/unit/test_resampling/test_decisions/test_decision.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/unit/test_resampling/test_decisions/test_no_decision.py b/tests/unit/test_resampling/test_decisions/test_no_decision.py index bdaa337c..f8878525 100644 --- a/tests/unit/test_resampling/test_decisions/test_no_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_no_decision.py @@ -1,4 +1,5 @@ from wepy.walker import Walker, WalkerState +from wepy.runners.mock import MockState from wepy.resampling.decisions.no_decision import NoDecision, NothingDecisionEnum @@ -7,12 +8,12 @@ class TestNoDecision: def test_action(self): walker_1 = Walker( - state=WalkerState(a=1), + state=MockState(a=1), weight=1.0, ) walker_2 = Walker( - state=WalkerState(a=2), + state=MockState(a=2), weight=1.0, ) diff --git a/tests/unit/test_resampling/test_resamplers/test_noresampler.py b/tests/unit/test_resampling/test_resamplers/test_noresampler.py index 31bb16d2..d6280196 100644 --- a/tests/unit/test_resampling/test_resamplers/test_noresampler.py +++ b/tests/unit/test_resampling/test_resamplers/test_noresampler.py @@ -1,6 +1,8 @@ from wepy.resampling.resamplers.noresampler import NoResampler from wepy.resampling.decisions.no_decision import NothingDecisionEnum -from wepy.walker import Walker, WalkerState +from wepy.walker import Walker +from wepy.runners.mock import MockState + class TestNoResampler: @@ -8,16 +10,16 @@ class TestNoResampler: def test_resample(self): walker_1 = Walker( - state=WalkerState(a=1), + state=MockState(a=1), weight=1.0, ) walker_2 = Walker( - state=WalkerState(a=2), + state=MockState(a=2), weight=1.0, ) walker_3 = Walker( - state=WalkerState(a=3), + state=MockState(a=3), weight=1.0, ) diff --git a/tests/unit/test_resampling/test_resamplers/test_resampler.py b/tests/unit/test_resampling/test_resamplers/test_resampler.py deleted file mode 100644 index e69de29b..00000000 From f371918f1aea4f4b4e217f1c8b442f3a9c038743 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 4 Dec 2025 10:31:55 -0500 Subject: [PATCH 045/143] wip! --- src/wepy/runners/openmm/__init__.py | 19 +++++++++++++++++ src/wepy/runners/openmm/runner.py | 21 +++++++++++++++++++ tests/integration/test_openmm/conftest.py | 0 .../test_openmm/test_sim_manager.py | 5 ----- 4 files changed, 40 insertions(+), 5 deletions(-) delete mode 100644 tests/integration/test_openmm/conftest.py diff --git a/src/wepy/runners/openmm/__init__.py b/src/wepy/runners/openmm/__init__.py index 8b137891..2837e35d 100644 --- a/src/wepy/runners/openmm/__init__.py +++ b/src/wepy/runners/openmm/__init__.py @@ -1 +1,20 @@ +from .state import ( + OpenMMState, + OpenMMStateWrapper, + OpenMMStateValidationError, + dummy_context, + get_context_state, + state_to_xml, +) +from .runner import OpenMMRunner + +__all__ = [ + "OpenMMRunner", + "OpenMMState", + "OpenMMStateWrapper", + "OpenMMStateValidationError", + "dummy_context", + "get_context_state", + "state_to_xml", +] diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index 3a9eba3a..a2321b81 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -33,9 +33,12 @@ from wepy.runners.runner import Runner from wepy.util.util import box_vectors_to_lengths_angles from wepy.walker import WalkerState +from wepy.interface import Task, RunnerGenTaskArgs from .state import OpenMMState, OpenMMStateWrapper, get_context_state PlatformKwargs = dict[str, str] +GPU_PLATFORMS = {"CUDA", "OpenCL", "HIP"} + class OpenMMRunnerSegmentSplitTimes(TypedDict): gen_sim_time: float @@ -56,6 +59,23 @@ class OpenMMRunnerSegmentSplitTimes(TypedDict): "box_volume", }) +@attrs.define +class OpenMMTask(Task): + + runner: "OpenMMRunner" + segment_length: int + platform_kwargs: PlatformKwargs | None = None + + def __call___(self, state: OpenMMState) -> OpenMMState: + + logger.info("Running OpenMMTask") + + return self.runner.run_segment( + state, + segment_length=self.segment_length, + platform_kwargs=self.platform_kwargs, + ) + # the runner for the simulation which runs the actual dynamics @attrs.define class OpenMMRunner(Runner): @@ -81,6 +101,7 @@ def pre_cycle( def post_cycle(self) -> None: pass + def run_segment( self, walker_state: OpenMMState, diff --git a/tests/integration/test_openmm/conftest.py b/tests/integration/test_openmm/conftest.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/integration/test_openmm/test_sim_manager.py b/tests/integration/test_openmm/test_sim_manager.py index 082ce59d..d6fd4f17 100644 --- a/tests/integration/test_openmm/test_sim_manager.py +++ b/tests/integration/test_openmm/test_sim_manager.py @@ -21,11 +21,6 @@ def test_serial_mapper(): integrator=integrator, ) - run_func = functools.partial( - runner.run_segment, - platform="Reference", - ) - num_walkers = 4 walker_states = [ From 33fc351d9cb35ebe909936df905ae144f54f75b0 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 4 Dec 2025 17:19:56 -0500 Subject: [PATCH 046/143] writing openmm specific work mapper --- pyproject.toml | 1 + src/wepy/interface.py | 25 - src/wepy/runners/openmm/__init__.py | 4 +- src/wepy/runners/openmm/runner.py | 45 +- src/wepy/runners/runner.py | 24 +- src/wepy/sim_manager.py | 51 +- src/wepy/work_mapper/base.py | 51 +- src/wepy/work_mapper/openmm.py | 471 ++++++++++++++++++ src/wepy/work_mapper/serial.py | 27 +- .../test_openmm/test_sim_manager.py | 196 +++++++- tests/unit/test_work_mapper/test_openmm.py | 165 ++++++ uv.lock | 2 + 12 files changed, 870 insertions(+), 192 deletions(-) delete mode 100644 src/wepy/interface.py create mode 100644 src/wepy/work_mapper/openmm.py create mode 100644 tests/unit/test_work_mapper/test_openmm.py diff --git a/pyproject.toml b/pyproject.toml index a1fe0ca8..00b14553 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,7 @@ dev = [ "pytest-print", # for testing openmm wrappers "lxml", + "psutil", # qa "black", "isort", diff --git a/src/wepy/interface.py b/src/wepy/interface.py deleted file mode 100644 index bb6dc50c..00000000 --- a/src/wepy/interface.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import Generic, TypeVar, Protocol - -import attrs - -from wepy.walker import WalkerState - -WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) - -@attrs.define -class WorkMapperFactoryArgs: - num_workers: int - -@attrs.define -class RunnerGenTaskArgs(Generic[WalkerState_]): - segment_length: int - cycle_idx: int - states: list[WalkerState_] - -class Task(Protocol[WalkerState_]): - - def __call__( - self, - walker_state: WalkerState_, - ) -> WalkerState_: - ... diff --git a/src/wepy/runners/openmm/__init__.py b/src/wepy/runners/openmm/__init__.py index 2837e35d..427348fc 100644 --- a/src/wepy/runners/openmm/__init__.py +++ b/src/wepy/runners/openmm/__init__.py @@ -7,9 +7,10 @@ get_context_state, state_to_xml, ) -from .runner import OpenMMRunner +from .runner import OpenMMRunner, PlatformKwargs, OpenMMPlatformName __all__ = [ + "OpenMMPlatformName", "OpenMMRunner", "OpenMMState", "OpenMMStateWrapper", @@ -17,4 +18,5 @@ "dummy_context", "get_context_state", "state_to_xml", + "PlatformKwargs", ] diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index a2321b81..a32c9729 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -33,12 +33,11 @@ from wepy.runners.runner import Runner from wepy.util.util import box_vectors_to_lengths_angles from wepy.walker import WalkerState -from wepy.interface import Task, RunnerGenTaskArgs from .state import OpenMMState, OpenMMStateWrapper, get_context_state PlatformKwargs = dict[str, str] -GPU_PLATFORMS = {"CUDA", "OpenCL", "HIP"} +OpenMMPlatformName = Literal["Reference", "CPU", "CUDA", "OpenCL", "HIP"] class OpenMMRunnerSegmentSplitTimes(TypedDict): gen_sim_time: float @@ -59,23 +58,6 @@ class OpenMMRunnerSegmentSplitTimes(TypedDict): "box_volume", }) -@attrs.define -class OpenMMTask(Task): - - runner: "OpenMMRunner" - segment_length: int - platform_kwargs: PlatformKwargs | None = None - - def __call___(self, state: OpenMMState) -> OpenMMState: - - logger.info("Running OpenMMTask") - - return self.runner.run_segment( - state, - segment_length=self.segment_length, - platform_kwargs=self.platform_kwargs, - ) - # the runner for the simulation which runs the actual dynamics @attrs.define class OpenMMRunner(Runner): @@ -106,6 +88,7 @@ def run_segment( self, walker_state: OpenMMState, segment_length: int, + platform_name: OpenMMPlatformName | None = None, platform_kwargs: PlatformKwargs | None = None, ) -> OpenMMState: """Run dynamics for the walker. @@ -126,6 +109,8 @@ def run_segment( """ + logger.info("Running OpenMM MD segment") + run_segment_start = time.time() # set the kwargs that will be passed to getState @@ -142,36 +127,22 @@ def run_segment( ## Platform - logger.info(f"'global_platform_kwargs' in runner: {self.global_platform_kwargs}") - logger.info(f"'platform_kwargs' passed to 'run_segment' : {platform_kwargs}") - match (self.global_platform_kwargs, platform_kwargs): - case (None, None): - _platform_kwargs = {} - case (global_kwargs, None): - _platform_kwargs = global_kwargs - case (None, local_kwargs): - _platform_kwargs = local_kwargs - case (global_kwargs, local_kwargs): - _platform_kwargs = self.global_platform_kwargs | platform_kwargs - - logger.info(f"Resolved 'platform_kwargs' : {_platform_kwargs}") - # create simulation object ## create the platform and customize # if a platform was given we use it to make a Simulation object - if self.platform_name is not None: + if platform_name is not None: logger.info("Using platform configured in code.") # get the platform by its name to use - platform = openmm.Platform.getPlatformByName(self.platform_name) + platform = openmm.Platform.getPlatformByName(platform_name) logger.info(f"Platform object created: {platform}") # set properties from the kwargs if they apply to the platform - for key, value in _platform_kwargs.items(): + for key, value in platform_kwargs.items(): if key in platform.getPropertyNames(): logger.info(f"Setting platform property: {key} : {value}") platform.setPropertyDefaultValue(key, value) @@ -179,7 +150,7 @@ def run_segment( else: logger.warning( f"Platform kwargs given ({key} : {value}) " - f"but is not valid for this platform ({self.platform_name})" + f"but is not valid for this platform ({platform_name})" ) # make a new simulation object diff --git a/src/wepy/runners/runner.py b/src/wepy/runners/runner.py index b81bc6cd..c44afd9e 100644 --- a/src/wepy/runners/runner.py +++ b/src/wepy/runners/runner.py @@ -21,13 +21,10 @@ from typing import Protocol, Any, TypedDict, TypeVar, ParamSpec import attrs from wepy.walker import Walker, WalkerState -from wepy.interface import Task, RunnerGenTaskArgs WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) -Task_ = TypeVar("Task_", bound=Task) - -class Runner(Protocol[WalkerState_, Task_]): +class Runner(Protocol[WalkerState_]): """Abstract base class for the Runner interface.""" def pre_cycle(self) -> None: @@ -57,9 +54,6 @@ def post_cycle(self) -> None: ... - def gen_tasks(self, segment_spec: RunnerGenTaskArgs[WalkerState_]) -> list[Task_]: - ... - def run_segment( self, walker: WalkerState_, @@ -83,14 +77,6 @@ def run_segment( def get_last_cycle_segments_split_times(self) -> list[dict[str, float]] | None: ... -@attrs.define -class IdentityTask(Task[WalkerState_]): - - runner: "NoRunner" - - def __call__(self, walker_state: WalkerState_) -> WalkerState_: - return self.runner.run_segment(walker_state) - @attrs.define class NoRunner(Runner): """Stub Runner that just returns the walkers back with the same state. @@ -105,14 +91,6 @@ def post_cycle(self) -> None: def get_last_cycle_segments_split_times(self) -> None: return None - def gen_tasks(self, segment_spec: RunnerGenTaskArgs[WalkerState_]) -> list[IdentityTask[WalkerState_]]: - - return [ - IdentityTask(self) - for state - in segment_spec.states - ] - def run_segment( self, state: WalkerState_, diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index 1663c7f6..82399372 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -44,7 +44,7 @@ # Standard Library import logging -from typing import Final, Any, TypedDict, Generic, TypeVar +from typing import Final, Any, TypedDict, Generic, TypeVar, Callable logger = logging.getLogger(__name__) # Standard Library @@ -52,10 +52,6 @@ from copy import deepcopy # First Party Library -from wepy.interface import ( - WorkMapperFactoryArgs, - RunnerGenTaskArgs, -) from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.reporter.reporter import Reporter from wepy.resampling.resamplers.resampler import Resampler @@ -124,7 +120,7 @@ class Manager(Generic[State_]): runner: Runner resampler: Resampler boundary_conditions: BoundaryConditions | None - work_mapper_class: type[WorkMapper] + work_mapper_factory: type[WorkMapper] work_mapper: WorkMapper reporters: list[Reporter] monitor: Monitor | None @@ -155,7 +151,7 @@ def __init__( init_walkers: list[Walker[State_]], runner: Runner, resampler: Resampler, - work_mapper_class: type[WorkMapper] | None = None, + work_mapper_factory: Callable[[], WorkMapper] | None = None, boundary_conditions: BoundaryConditions | None = None, reporters: list[Reporter] | None = None, sim_monitor: Monitor | None = None, @@ -170,7 +166,7 @@ def __init__( runner : object implementing the Runner interface The runner to be used for propagating sampling segments of walkers. - work_mapper_class : Class for a work mapper, will be + work_mapper_factory : Class for a work mapper, will be instantiated by simulation manager. If None will default to a serial mapper. @@ -215,10 +211,10 @@ def __init__( else: self.reporters = reporters - if work_mapper_class is None: - self.work_mapper_class = SerialMapper + if work_mapper_factory is None: + self.work_mapper_factory = SerialMapper else: - self.work_mapper_class = work_mapper_class + self.work_mapper_factory = work_mapper_factory ## Monitor self.monitor = sim_monitor @@ -231,7 +227,6 @@ def __init__( def init( self, - num_workers: int | None = None, continue_run: int | None = None, ) -> None: """Initialize wepy configuration components for use at runtime. @@ -265,9 +260,6 @@ def init( Parameters ---------- - num_workers : int - The number of workers to use in the work mapper. - (Default value = None) continue_run : int Index of a run this one is continuing. (Default value = None) @@ -288,11 +280,7 @@ def init( # mapping and the number of workers, this may include things like starting processes # etc. logger.info("Instantiating work mapper") - self.work_mapper = self.work_mapper_class( - WorkMapperFactoryArgs( - num_workers=num_workers, - ) - ) + self.work_mapper = self.work_mapper_factory() logger.info("Running WorkMapper.init hook") self.work_mapper.init() logger.info("Finished WorkMapper.init hook") @@ -386,21 +374,14 @@ def run_segment( The walkers after the segment of sampling simulation. """ - logger.info("Generating tasks for walker states") - tasks = self.runner.gen_tasks( - RunnerGenTaskArgs( - segment_length=segment_length, - cycle_idx=cycle_idx, - states=states, - ) - ) - + segment_lengths = [segment_length for i in range(len(states))] logger.info("Starting segment runs") try: new_states = list( self.work_mapper.map( - tasks, + self.runner.run_segment, states, + segment_lengths, ) ) @@ -660,7 +641,6 @@ def run_simulation( self, n_cycles: int, segment_lengths: int, - num_workers: int | None = None, continue_run_idx: int | None = None, ) -> tuple[ list[Walker[State_]], @@ -676,10 +656,6 @@ def run_simulation( segment_lengths : int The number of steps for each runner segment. - num_workers : int - The number of workers to use for the work mapper. - (Default value = None) - continue_run_idx: Index of the run you are continuing, optional. @@ -695,7 +671,7 @@ def run_simulation( """ logger.info("Running simulation init hook") - self.init(num_workers=num_workers, continue_run=continue_run_idx) + self.init(continue_run=continue_run_idx) if type(segment_lengths) == int: logger.info("Single number of steps provided for simulation, using this for all cycles.") @@ -727,7 +703,6 @@ def run_simulation_by_time( self, run_time: int, segments_length: int, - num_workers: int | None = None, continue_run_idx: int | None = None, ) -> tuple[ list[Walker[State_]], @@ -751,7 +726,7 @@ def run_simulation_by_time( logger.info(f"Simulation start time: {start_time}") logger.info("Running simulation init hook") - self.init(num_workers=num_workers, continue_run=continue_run_idx) + self.init(continue_run=continue_run_idx) cycle_idx = 0 walkers = self.init_walkers diff --git a/src/wepy/work_mapper/base.py b/src/wepy/work_mapper/base.py index f43f678a..bc57ecd0 100644 --- a/src/wepy/work_mapper/base.py +++ b/src/wepy/work_mapper/base.py @@ -7,69 +7,24 @@ # Standard Library -from wepy.interface import ( - WorkMapperFactoryArgs, - Task, -) from wepy.walker import WalkerState logger = logging.getLogger(__name__) WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) -Task_ = TypeVar("Task_", bound=Task) - - -class WorkMapper(Protocol[WalkerState_, Task_]): - - def __init__( - self, - segment_func: Callable[ - [ - WalkerState_, - Task_, - ], - WalkerState_, - ], - wm_args: WorkMapperFactoryArgs | None, - ) -> None: - ... +class WorkMapper(Protocol[WalkerState_]): def init(self) -> None: ... def map( self, - tasks: list[Task_], + task: Callable[[WalkerState_, int], WalkerState_], walker_states: list[WalkerState_], + segment_lengths: list[int], ) -> list[WalkerState_]: ... def get_worker_segment_times(self) -> dict[int, list[float]] | None: ... def cleanup(self) -> None: ... - -class WrapperException(Exception): - """Exception used for wrapping another exception. - - Since tracebacks can't be pickled we format it and save that - instead. - - """ - - def __init__( - self, - message, - # must be kwargs so we can pickle it (I know weird...) - wrapped_exception=None, - tb=None, - ): - super().__init__(message) - - # save the exception with the traceback - self.wrapped_exception = wrapped_exception - self.formatted_tb = traceback.format_tb(tb) - -class TaskException(WrapperException): - pass - - diff --git a/src/wepy/work_mapper/openmm.py b/src/wepy/work_mapper/openmm.py new file mode 100644 index 00000000..104412a8 --- /dev/null +++ b/src/wepy/work_mapper/openmm.py @@ -0,0 +1,471 @@ +"""Special OpenMM mappers.""" +import logging +from typing import Literal, Any, Callable +import time +import multiprocessing as mp +import itertools + +import attrs +import ray +import ray.util.multiprocessing + +from wepy.work_mapper.base import WorkMapper +from wepy.runners.openmm import OpenMMState, OpenMMRunner, PlatformKwargs, OpenMMPlatformName + +logger = logging.getLogger(__name__) + +GPU_PLATFORMS = {"CUDA", "OpenCL", "HIP"} + +class OpenMMSerialWorkMapper(WorkMapper): + + def __init__( + self, + platform: OpenMMPlatformName, + global_platform_properties: dict[str, str] | None = None, + ) -> None: + self._worker_segment_times: dict[int, list[float]] = {0: []} + + self._platform = platform + self._global_platform_properties = global_platform_properties if global_platform_properties is not None else {} + + def get_worker_segment_times(self) -> dict[int, list[float]]: + """The run timings for each segment for each walker. + + Returns + ------- + worker_seg_times : Dictionary mapping worker indices to a list of times in + seconds for each segment run. + + """ + return self._worker_segment_times + + def init(self) -> None: + pass + + def cleanup(self) -> None: + pass + + def map( + self, + task: Callable[[OpenMMState, int], OpenMMState], + walker_states: list[OpenMMState], + segment_lengths: list[int], + ) -> list[OpenMMState]: + + segment_times: list[float] = [] + results: list[OpenMMState] = [] + for task_idx, task_args in enumerate(zip(walker_states, segment_lengths, strict=True)): + + tic = time.time() + result = task( + *task_args, + platform_name=self._platform, + platform_kwargs=self._global_platform_properties, + ) + toc = time.time() + + segment_times.append(toc - tic) + results.append(result) + + self._worker_segment_times[0] = segment_times + + return results + +@attrs.define +class OpenMMSerialWorkMapperFactory: + + platform: OpenMMPlatformName + global_platform_properties: dict[str, str] | None = None + + def __call__(self) -> OpenMMSerialWorkMapper: + + return OpenMMSerialWorkMapper( + platform=self.platform, + global_platform_properties=self.global_platform_properties, + ) + + +class OpenMMProcPoolWorkMapper(WorkMapper): + + def __init__( + self, + platform: OpenMMPlatformName, + num_procs: int, + device_ids: list[int] | None = None, + global_platform_properties: dict[str, str] | None = None, + device_platform_properties: list[dict[str, str]] | None = None, + proc_start_method: Literal["fork", "spawn", "forkserver"] = "spawn", + ): + + + if platform in {"CUDA", "HIP", "OpenCL"}: + if device_ids is None: + raise ValueError(f"For accelerator platforms ({platform} requested) device_ids must be given.") + + if device_platform_properties is not None: + + if len(device_platform_properties) != len(device_ids): + raise ValueError(f"{len(device_ids)} devices requested, but only {len(device_platform_properties)} device platform property dicts given.") + + else: + self._device_platform_properties = { + idx : props + for idx, props + in enumerate(device_platform_properties) + } + + + else: + self._device_platform_properties = None + + self._platform = platform + self._global_platform_properties = global_platform_properties + + if device_ids is not None and num_procs != len(device_ids): + + raise ValueError(f"When device_ids is given ({device_ids}) it must be the same length as the number of processes: {num_procs}") + + self._device_ids: dict[int,int] = { + idx: device_id + for idx, device_id + in enumerate(device_ids) + } if device_ids is not None else None + self._num_procs = num_procs + + self._proc_start_method = proc_start_method + + def init( + self, + ) -> None: + + logger.info("Initializing ProcPoolMapper") + + logger.info(f"Initializing local multiprocessing context with start method: {self._proc_start_method}") + self._mp_ctx = mp.get_context(method=self._proc_start_method) + + def cleanup(self) -> None: + + logger.info("Running ProcPoolMapper cleanup") + logger.info("Nothing to do") + + + def map( + self, + task: Callable[[OpenMMState, int], OpenMMState], + walker_states: list[OpenMMState], + segment_lengths: list[int], + ) -> list[OpenMMState]: + + logger.info(f"Running map on {len(walker_states)} in batches of {self._num_procs}") + + # spin up a new pool for each map + logger.info(f"Starting process Pool with {self._num_procs}") + with self._mp_ctx.Pool( + processes=self._num_procs, + # only run one thing per task, just to make sure + # everything is cleaned up + maxtasksperchild=1, + ) as pool: + + results = [] + for batch_idx, batch in enumerate(itertools.batched( + zip(walker_states, segment_lengths, strict=True), + self._num_procs, + strict=False, + )): + + logger.info(f"Submitting batch: {batch_idx}") + + batch_results = [] + for batch_task_idx, task_args in enumerate(batch): + + task_idx = batch_idx + batch_task_idx + # for our purposes each element in this batch + # should be associated with a worker. + worker_idx = batch_task_idx + + if self._device_ids is not None: + logger.info("device_ids have been given, setting up special platform properties for each task.") + device_id = str(self._device_ids[worker_idx]) + worker_platform_props = { + **( + {"DeviceIndex" : device_id} + if self._platform in GPU_PLATFORMS + else {} + ), + **( + self._device_platform_properties[worker_idx] + if ( + self._device_platform_properties is not None and + worker_idx in self._device_platform_properties + ) + else {} + ) + } + logger.info( + f"Device IDs given, resolved to using worker specific platform properties: {worker_platform_props}" + ) + else: + logger.info("No Device IDs given.") + worker_platform_props = None + + match (self._global_platform_properties, worker_platform_props): + case (None, None): + logger.info("No platform properties provided") + _platform_kwargs = {} + case (global_kwargs, None): + logger.info("Only global platform properties provided") + _platform_kwargs = global_kwargs + case (None, local_kwargs): + logger.info("Only device specific platform properties provided") + _platform_kwargs = worker_platform_props + case (global_kwargs, local_kwargs): + logger.info("Both global and device specific platform properties provided") + _platform_kwargs = global_kwargs | worker_platform_props + + logger.info(f"Resolved 'platform_kwargs' : {_platform_kwargs}") + + logger.info(f"Submitting task {task_idx} to worker {worker_idx}") + result = pool.apply_async( + task, + args=task_args, + kwds=dict( + platform_name=self._platform, + platform_kwargs=_platform_kwargs, + ) + ) + logger.info(f"Task {task_idx} submitted") + batch_results.append(result) + + logger.info(f"Batch {batch_idx} submitted, awaiting results.") + for batch_task_idx, task_result in enumerate(batch_results): + + task_idx = batch_idx + batch_task_idx + logger.info(f"Awaiting task {task_idx}") + + + try: + real_result = task_result.get() + # TODO: add timeouts and retries + except TimeoutError as exc: + raise exc + except Exception as exc: + raise exc + + results.append(real_result) + + logger.info(f"Retrieved completed results for task: {task_idx}") + + logger.info(f"Batch {batch_idx} completed") + + logger.info(f"Completed all batches, terminating Pool") + + + return results + +@attrs.define +class OpenMMProcPoolWorkMapperFactory: + + platform: OpenMMPlatformName + num_procs: int + device_ids: list[int] | None = None + global_platform_properties: dict[str, str] | None = None + device_platform_properties: list[dict[str, str]] | None = None + proc_start_method: Literal["fork", "spawn", "forkserver"] = "spawn" + + def __attrs_post_init__(self) -> None: + + if self.platform in {"CUDA", "HIP", "OpenCL"}: + if self.device_ids is None: + raise ValueError(f"For accelerator platforms ({self.platform} requested) device_ids must be given.") + + if self.device_platform_properties is not None: + + if len(self.device_platform_properties) != len(self.device_ids): + raise ValueError(f"{len(self.device_ids)} devices requested, but only {len(self.device_platform_properties)} device platform property dicts given.") + + if self.device_ids is not None and self.num_procs != len(self.device_ids): + + raise ValueError(f"When device_ids is given ({self.device_ids}) it must be the same length as the number of processes: {self.num_procs}") + + + def __call__(self) -> OpenMMProcPoolWorkMapper: + + return OpenMMProcPoolWorkMapper( + platform=self.platform, + num_procs=self.num_procs, + device_ids=self.device_ids, + global_platform_properties=self.global_platform_properties, + device_platform_properties=self.device_platform_properties, + proc_start_method=self.proc_start_method, + ) + + +# @attrs.define +# class OpenMMRayTask: + +# openmm_task: OpenMMTask + +# def __call__( +# self, +# *args, +# **kwargs, +# ) -> OpenMMState: + +# logging.basicConfig(level=logging.INFO) +# logging.getLogger("OpenMMRayTask").info("Configured logging in OpenMMRayTask process") + +# return self.openmm_task(*args, **kwargs) + + +# class OpenMMRayPoolWorkMapper: + + +# def __init__( +# self, +# platform: str, +# device_ids: list[int] | None = None, +# device_platform_properties: list[dict[str, str]] | None = None, +# global_platform_properties: dict[str, str] | None = None, +# ray_init_args: dict[str,Any] | None = None, +# ): + +# if platform in {"CUDA", "HIP", "OpenCL"}: +# if device_ids is None: +# raise ValueError(f"For accelerator platforms ({platform} requested) device_ids must be given.") + +# if device_platform_properties is not None: + +# if len(device_platform_properties) != device_ids: +# raise ValueError(f"{len(device_ids)} requested, but only {len(device_platform_properties)} given.") + +# else: +# self._device_platform_properties = { +# idx : props +# for idx, props +# in enumerate(device_platform_properties) +# } + + +# else: +# self._device_platform_properties = None + +# self._platform = platform +# self._device_ids: dict[int,int] = { +# idx: device_id +# for idx, device_id +# in enumerate(device_ids) +# } +# self._num_workers = len(device_ids) + +# if ray_init_args is not None: +# self._ray_init_args = ray_init_args + +# else: +# self._ray_init_args = None + + +# self._global_platform_properties = global_platform_properties + +# def init( +# self, +# ) -> None: + +# logger.info("Initializing RayPoolMapper") + +# self._ray_ctx = ray.init(**(self._ray_init_args if self._ray_init_args is not None else {})) + + +# def cleanup(self) -> None: + +# logger.info("Running RayPoolMapper cleanup") + +# ray.shutdown() + +# def map( +# self, +# tasks: list[OpenMMTask], +# walker_states: list[OpenMMState], +# ) -> list[OpenMMState]: + +# logger.info(f"Running map on {len(walker_states)} in batches of {self._num_workers}") + +# # spin up a new pool for each map +# logger.info(f"Starting ray Pool with {self._num_workers} workers") +# with ray.util.multiprocessing.Pool( +# processes=self._num_workers, +# # only run one thing per task, just to make sure +# # everything is cleaned up +# maxtasksperchild=1, +# ) as pool: + +# results = [] +# for batch_idx, batch in enumerate(itertools.batched( +# zip(walker_states, tasks, strict=True), +# self._num_workers, +# strict=False, +# )): + +# logger.info(f"Submitting batch: {batch_idx}") + +# batch_results = [] +# for batch_task_idx, (walker_state, task) in enumerate(batch): + +# task_idx = batch_idx + batch_task_idx +# # for our purposes each element in this batch +# # should be associated with a worker. +# worker_idx = batch_task_idx + +# if self._device_ids is not None and self._platform in GPU_PLATFORMS: +# logger.info("Worker platform configured and resolving platform properties.") +# platform_kwargs = self._global_platform_properties | { +# "DeviceIndex" : str(self._device_ids[worker_idx]), +# **( +# self._device_platform_properties[worker_idx] +# if self._device_platform_properties is not None +# else {} +# ), +# } +# else: +# logger.info("Non-worker platform, only using global properties") +# platform_kwargs = self._global_platform_properties + +# _task = OpenMMRayTask(task) +# logger.info(f"Submitting task {task_idx} to worker {worker_idx}") +# logger.info(f"Injecting: platform={self._platform}, platform_kwargs={platform_kwargs}") +# result = pool.apply_async( +# _task, +# args=(walker_state,), +# kwargs=dict( +# platform=self._platform, +# platform_kwargs=platform_kwargs +# ) +# ) +# logger.info(f"Task {task_idx} submitted") +# batch_results.append(result) + +# logger.info(f"Batch {batch_idx} submitted, awaiting results.") +# for batch_task_idx, task_result in enumerate(batch_results): + +# task_idx = batch_idx + batch_task_idx +# logger.info(f"Awaiting task {task_idx}") + + +# try: +# real_result = task_result.get() +# # TODO: add timeouts and retries +# except TimeoutError as exc: +# raise exc +# except Exception as exc: +# raise exc + +# results.append(real_result) + +# logger.info(f"Retrieved completed results for task: {task_idx}") + +# logger.info(f"Batch {batch_idx} completed") + +# logger.info(f"Completed all batches, terminating Pool") + + +# return results diff --git a/src/wepy/work_mapper/serial.py b/src/wepy/work_mapper/serial.py index 92e302c6..a6fa2ce9 100644 --- a/src/wepy/work_mapper/serial.py +++ b/src/wepy/work_mapper/serial.py @@ -6,29 +6,21 @@ from typing import Callable, Literal, Generic, TypeVar, Protocol, Any, ParamSpec, Concatenate, Sequence import logging -from wepy.interface import ( - WorkMapperFactoryArgs, -) from wepy.walker import Walker, WalkerState -from wepy.work_mapper.base import WorkMapper, TaskException, Task logger = logging.getLogger(__name__) WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) -Task_ = TypeVar("Task_", bound=Task) class SerialMapper( - WorkMapper, Generic[ WalkerState_, - Task_, ]): """Basic non-parallel reference implementation of a mapper.""" def __init__( self, - wm_args: WorkMapperFactoryArgs, ) -> None: self._worker_segment_times: dict[int, list[float]] = {0: []} @@ -52,15 +44,28 @@ def cleanup(self) -> None: def map( self, - tasks: list[Task_], + task: Callable[ + [ + WalkerState_, + int, + ], + WalkerState_ + ], walker_states: list[WalkerState_], + segment_lengths: list[int], ) -> list[WalkerState_]: segment_times: list[float] = [] results: list[WalkerState_] = [] - for task_idx, (task, walker_state) in enumerate(zip(tasks, walker_states, strict=True)): + for task_idx, task_args in enumerate( + zip( + walker_states, + segment_lengths, + strict=True, + ) + ): tic = time.time() - result = task(walker_state) + result = task(*task_args) toc = time.time() segment_times.append(toc - tic) diff --git a/tests/integration/test_openmm/test_sim_manager.py b/tests/integration/test_openmm/test_sim_manager.py index d6fd4f17..ec537db6 100644 --- a/tests/integration/test_openmm/test_sim_manager.py +++ b/tests/integration/test_openmm/test_sim_manager.py @@ -1,8 +1,13 @@ import functools +import psutil import openmm from wepy.walker import Walker -from wepy.runners.openmm import OpenMMRunner, OpenMMState, gen_sim_state +from wepy.runners.openmm import OpenMMRunner, OpenMMState from wepy.work_mapper.serial import SerialMapper +from wepy.work_mapper.openmm import ( + OpenMMSerialWorkMapperFactory, + OpenMMProcPoolWorkMapperFactory, +) from wepy.sim_manager import Manager from wepy.resampling.resamplers.noresampler import NoResampler @@ -24,11 +29,9 @@ def test_serial_mapper(): num_walkers = 4 walker_states = [ - OpenMMState(gen_sim_state( - lj_sys.positions, - system=lj_sys.system, - integrator=integrator, - )) + OpenMMState.from_dwim( + positions=lj_sys.positions, + ) for _ in range(num_walkers) ] @@ -47,11 +50,186 @@ def test_serial_mapper(): init_walkers=init_walkers, runner=runner, resampler=NoResampler(), - work_mapper=SerialMapper(), + work_mapper_factory=OpenMMSerialWorkMapperFactory( + platform="Reference", + ) + ) + + new_walkers, sim_components = sim_manager.run_simulation( + n_cycles=1, + segment_lengths=100, + ) + + sim_manager = Manager( + init_walkers=init_walkers, + runner=runner, + resampler=NoResampler(), + work_mapper_factory=OpenMMSerialWorkMapperFactory( + platform="CPU", + global_platform_properties={"Threads" : "1"}, + ) + ) + + new_walkers, sim_components = sim_manager.run_simulation( + n_cycles=1, + segment_lengths=100, + ) + + sim_manager = Manager( + init_walkers=init_walkers, + runner=runner, + resampler=NoResampler(), + work_mapper_factory=OpenMMSerialWorkMapperFactory( + platform="CPU", + global_platform_properties={"Threads" : "4"}, + ) ) new_walkers, sim_components = sim_manager.run_simulation( n_cycles=1, - segment_lengths=10, - num_workers=None, + segment_lengths=10000000000, + ) + +def test_proc_pool_mapper(): + + lj_sys = LennardJonesPair() + integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + + + runner = OpenMMRunner( + system=lj_sys.system, + topology=lj_sys.topology, + integrator=integrator, ) + + num_walkers = 4 + + walker_states = [ + OpenMMState.from_dwim( + positions=lj_sys.positions, + ) + for _ + in range(num_walkers) + ] + + init_walker_weight = 1 / num_walkers + init_walkers = [ + Walker( + state=walker_state, + weight=init_walker_weight, + ) + for walker_state + in walker_states + ] + + # As an example of a useful configuration. There are 4 walkers in + # each cycle so that is the max number of processes that should be + # used. + sim_manager = Manager( + init_walkers=init_walkers, + runner=runner, + resampler=NoResampler(), + work_mapper_factory=OpenMMProcPoolWorkMapperFactory( + platform="Reference", + num_procs=4, + ), + ) + + new_walkers, sim_components = sim_manager.run_simulation( + n_cycles=1, + segment_lengths=100, + ) + + # As an example of a useful configuration. There are 4 walkers in + # each cycle so that is the max number of processes that should be + # used, however for each worker we can assign more threads based + # on how many CPUs you have. Here we use psutil to reliably get + # the number of cores and divide that by the number of walkers. + num_cores = len(psutil.Process().cpu_affinity()) + cores_per_worker = (num_cores // num_walkers) + sim_manager = Manager( + init_walkers=init_walkers, + runner=runner, + resampler=NoResampler(), + work_mapper_factory=OpenMMProcPoolWorkMapperFactory( + platform="CPU", + num_procs=len(walker_states), + global_platform_properties={"Threads" : str(cores_per_worker)}, + ), + ) + + new_walkers, sim_components = sim_manager.run_simulation( + n_cycles=2, + segment_lengths=100, + ) + + # Just as way of example of how GPU device specific arguments + # would work, we assign 1 thread as global but then override that + # for one specific worker. Note that you need to explicitly + # enumerate the device IDs then. + sim_manager = Manager( + init_walkers=init_walkers, + runner=runner, + resampler=NoResampler(), + work_mapper_factory=OpenMMProcPoolWorkMapperFactory( + platform="CPU", + num_procs=len(walker_states), + global_platform_properties={"Threads" : "1"}, + device_ids=[0,1,2,3], + device_platform_properties=[ + {}, {}, {}, + {"Threads" : "3"} + ] + ), + ) + + new_walkers, sim_components = sim_manager.run_simulation( + n_cycles=2, + segment_lengths=100, + ) + +# def test_ray_mapper(): +# lj_sys = LennardJonesPair() +# integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + + +# runner = OpenMMRunner( +# system=lj_sys.system, +# topology=lj_sys.topology, +# integrator=integrator, +# ) + +# num_walkers = 4 + +# walker_states = [ +# OpenMMState.from_dwim( +# positions=lj_sys.positions, +# ) +# for _ +# in range(num_walkers) +# ] + +# init_walker_weight = 1 / num_walkers +# init_walkers = [ +# Walker( +# state=walker_state, +# weight=init_walker_weight, +# ) +# for walker_state +# in walker_states +# ] + +# sim_manager = Manager( +# init_walkers=init_walkers, +# runner=runner, +# resampler=NoResampler(), +# work_mapper_factory=OpenMMRayWorkMapperFactory( +# platform="Reference", +# num_procs=1, +# ), +# ) + +# new_walkers, sim_components = sim_manager.run_simulation( +# n_cycles=2, +# segment_lengths=10, +# ) diff --git a/tests/unit/test_work_mapper/test_openmm.py b/tests/unit/test_work_mapper/test_openmm.py new file mode 100644 index 00000000..d0594d5c --- /dev/null +++ b/tests/unit/test_work_mapper/test_openmm.py @@ -0,0 +1,165 @@ +from wepy.runners.openmm import ( + OpenMMRunner, + OpenMMState, + gen_sim_state, +) +from wepy.work_mapper.openmm import ( + OpenMMTask, + OpenMMSerialWorkMapper, + OpenMMProcPoolWorkMapper, + OpenMMRayPoolWorkMapper, +) + +import pytest + +import numpy as np +import openmm +import openmm.app +import openmm.unit + +from wepy_tools.systems.lennard_jones import LennardJonesPair + +@pytest.fixture(scope="function") +def openmm_runner() -> OpenMMRunner: + + lj_sys = LennardJonesPair() + integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + + + runner = OpenMMRunner( + system=lj_sys.system, + topology=lj_sys.topology, + integrator=integrator, + ) + return runner + +@pytest.fixture(scope="function") +def openmm_state() -> OpenMMState: + + lj_sys = LennardJonesPair() + + state = OpenMMState(gen_sim_state( + lj_sys.positions, + system=lj_sys.system, + integrator=openmm.VerletIntegrator(0.002), + )) + + return state + + + +class TestOpenMMTask: + + def test___call__(self, openmm_runner, openmm_state): + + task = OpenMMTask( + runner=openmm_runner, + segment_length=2, + ) + + new_state = task(openmm_state) + +class TestOpenMMSerialWorkMapper: + + def test_all(self, openmm_runner): + + lj_sys = LennardJonesPair() + + init_states = [ + OpenMMState(gen_sim_state( + lj_sys.positions, + system=lj_sys.system, + integrator=openmm.VerletIntegrator(1), + )) + for _ + in range(4) + ] + + mapper = OpenMMSerialWorkMapper( + platform="CPU", + global_platform_properties={"Threads" : "2"}, + ) + + mapper.init() + new_states = mapper.map( + [ + OpenMMTask( + runner=openmm_runner, + segment_length=2, + ) + for _ in range(len(init_states)) + ], + init_states, + ) + +class TestOpenMMProcPoolWorkMapper: + def test_all(self, openmm_runner): + + lj_sys = LennardJonesPair() + + init_states = [ + OpenMMState(gen_sim_state( + lj_sys.positions, + system=lj_sys.system, + integrator=openmm.VerletIntegrator(1), + )) + for _ + in range(4) + ] + + mapper = OpenMMProcPoolWorkMapper( + platform="CPU", + device_ids=[0,1], + global_platform_properties={"Threads" : "1"}, + ) + + mapper.init() + new_states = mapper.map( + [ + OpenMMTask( + runner=openmm_runner, + segment_length=2, + ) + for _ in range(len(init_states)) + ], + init_states, + ) + +class TestOpenMMRayPoolWorkMapper: + + @pytest.mark.ray + def test_all(self, openmm_runner): + + lj_sys = LennardJonesPair() + + init_states = [ + OpenMMState(gen_sim_state( + lj_sys.positions, + system=lj_sys.system, + integrator=openmm.VerletIntegrator(1), + )) + for _ + in range(4) + ] + + mapper = OpenMMRayPoolWorkMapper( + platform="CPU", + device_ids=[0,1], + global_platform_properties={"Threads" : "1"}, + # ray_init_args={ + + # } + ) + + mapper.init() + new_states = mapper.map( + [ + OpenMMTask( + runner=openmm_runner, + segment_length=100000, + ) + for _ in range(len(init_states)) + ], + init_states, + ) + diff --git a/uv.lock b/uv.lock index 2d60c3ee..fd55971d 100644 --- a/uv.lock +++ b/uv.lock @@ -3036,6 +3036,7 @@ dev = [ { name = "nbsphinx" }, { name = "notebook" }, { name = "pdbpp" }, + { name = "psutil" }, { name = "pytest" }, { name = "pytest-check" }, { name = "pytest-cov" }, @@ -3088,6 +3089,7 @@ dev = [ { name = "nbsphinx" }, { name = "notebook" }, { name = "pdbpp" }, + { name = "psutil" }, { name = "pytest" }, { name = "pytest-check" }, { name = "pytest-cov" }, From b1444ee95cb3bb071c4ae97d3550fe7bb8e82db3 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 4 Dec 2025 22:28:31 -0500 Subject: [PATCH 047/143] finished implementation of the openmm procpool mapper includes proper handling of logging --- src/wepy/runners/openmm/__init__.py | 3 +- src/wepy/runners/openmm/runner.py | 1 + src/wepy/work_mapper/openmm/__init__.py | 7 + .../{openmm.py => openmm/proc_pool.py} | 294 ++++-------------- src/wepy/work_mapper/openmm/ray.py | 183 +++++++++++ src/wepy/work_mapper/openmm/serial.py | 79 +++++ 6 files changed, 324 insertions(+), 243 deletions(-) create mode 100644 src/wepy/work_mapper/openmm/__init__.py rename src/wepy/work_mapper/{openmm.py => openmm/proc_pool.py} (52%) create mode 100644 src/wepy/work_mapper/openmm/ray.py create mode 100644 src/wepy/work_mapper/openmm/serial.py diff --git a/src/wepy/runners/openmm/__init__.py b/src/wepy/runners/openmm/__init__.py index 427348fc..83bc1dc6 100644 --- a/src/wepy/runners/openmm/__init__.py +++ b/src/wepy/runners/openmm/__init__.py @@ -7,9 +7,10 @@ get_context_state, state_to_xml, ) -from .runner import OpenMMRunner, PlatformKwargs, OpenMMPlatformName +from .runner import OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS __all__ = [ + "GPU_PLATFORMS", "OpenMMPlatformName", "OpenMMRunner", "OpenMMState", diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index a32c9729..95608e7d 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -38,6 +38,7 @@ PlatformKwargs = dict[str, str] OpenMMPlatformName = Literal["Reference", "CPU", "CUDA", "OpenCL", "HIP"] +GPU_PLATFORMS = frozenset({"CUDA", "OpenCL", "HIP"}) class OpenMMRunnerSegmentSplitTimes(TypedDict): gen_sim_time: float diff --git a/src/wepy/work_mapper/openmm/__init__.py b/src/wepy/work_mapper/openmm/__init__.py new file mode 100644 index 00000000..45d0c702 --- /dev/null +++ b/src/wepy/work_mapper/openmm/__init__.py @@ -0,0 +1,7 @@ +from .serial import OpenMMSerialWorkMapperFactory +from .proc_pool import OpenMMProcPoolWorkMapperFactory + +__all__ = [ + "OpenMMSerialWorkMapperFactory", + "OpenMMProcPoolWorkMapperFactory", +] diff --git a/src/wepy/work_mapper/openmm.py b/src/wepy/work_mapper/openmm/proc_pool.py similarity index 52% rename from src/wepy/work_mapper/openmm.py rename to src/wepy/work_mapper/openmm/proc_pool.py index 104412a8..9c292584 100644 --- a/src/wepy/work_mapper/openmm.py +++ b/src/wepy/work_mapper/openmm/proc_pool.py @@ -1,89 +1,55 @@ """Special OpenMM mappers.""" import logging +import logging.handlers +import logging.config from typing import Literal, Any, Callable import time +import copy import multiprocessing as mp import itertools import attrs -import ray -import ray.util.multiprocessing from wepy.work_mapper.base import WorkMapper -from wepy.runners.openmm import OpenMMState, OpenMMRunner, PlatformKwargs, OpenMMPlatformName +from wepy.runners.openmm import OpenMMState, OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS logger = logging.getLogger(__name__) -GPU_PLATFORMS = {"CUDA", "OpenCL", "HIP"} - -class OpenMMSerialWorkMapper(WorkMapper): - - def __init__( - self, - platform: OpenMMPlatformName, - global_platform_properties: dict[str, str] | None = None, - ) -> None: - self._worker_segment_times: dict[int, list[float]] = {0: []} - - self._platform = platform - self._global_platform_properties = global_platform_properties if global_platform_properties is not None else {} - - def get_worker_segment_times(self) -> dict[int, list[float]]: - """The run timings for each segment for each walker. - - Returns - ------- - worker_seg_times : Dictionary mapping worker indices to a list of times in - seconds for each segment run. - - """ - return self._worker_segment_times - - def init(self) -> None: - pass - - def cleanup(self) -> None: - pass - - def map( - self, - task: Callable[[OpenMMState, int], OpenMMState], - walker_states: list[OpenMMState], - segment_lengths: list[int], - ) -> list[OpenMMState]: - - segment_times: list[float] = [] - results: list[OpenMMState] = [] - for task_idx, task_args in enumerate(zip(walker_states, segment_lengths, strict=True)): - - tic = time.time() - result = task( - *task_args, - platform_name=self._platform, - platform_kwargs=self._global_platform_properties, - ) - toc = time.time() - - segment_times.append(toc - tic) - results.append(result) - - self._worker_segment_times[0] = segment_times - - return results - -@attrs.define -class OpenMMSerialWorkMapperFactory: - - platform: OpenMMPlatformName - global_platform_properties: dict[str, str] | None = None - - def __call__(self) -> OpenMMSerialWorkMapper: - - return OpenMMSerialWorkMapper( - platform=self.platform, - global_platform_properties=self.global_platform_properties, - ) - +BASE_WORKER_LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "queue": { + "class": "logging.handlers.QueueHandler", + "queue": None, # injected at runtime + } + }, + "root": { + "handlers": ["queue"], + "level": "NOTSET", # defer filtering to parent + }, +} + +def _worker_setup(log_queue: mp.Queue) -> None: + + config = copy.deepcopy(BASE_WORKER_LOGGING_CONFIG) + + config["handlers"]["queue"]["queue"] = log_queue + parent_loggers = logging.root.manager.loggerDict + for name, logger in parent_loggers.items(): + if isinstance(logger, logging.Logger): + config.setdefault("loggers", {})[name] = { + "level" : logging.getLevelName(logger.level), + "propagate" : logger.propagate, + "handlers" : [], + "filters" : [ + f.__class__.__name__ + for f + in logger.filters + ], + } + + logging.config.dictConfig(config) class OpenMMProcPoolWorkMapper(WorkMapper): @@ -160,11 +126,23 @@ def map( # spin up a new pool for each map logger.info(f"Starting process Pool with {self._num_procs}") + + # set up the log queue and listener for getting logs from + # processes + log_queue = self._mp_ctx.Queue() + # handler = logging.StreamHandler() + + handlers = list(logging.getLogger().handlers) + listener = logging.handlers.QueueListener(log_queue, *handlers) + listener.start() + with self._mp_ctx.Pool( processes=self._num_procs, # only run one thing per task, just to make sure # everything is cleaned up maxtasksperchild=1, + initializer=_worker_setup, + initargs=(log_queue,), ) as pool: results = [] @@ -260,6 +238,7 @@ def map( logger.info(f"Completed all batches, terminating Pool") + listener.stop() return results @@ -300,172 +279,3 @@ def __call__(self) -> OpenMMProcPoolWorkMapper: proc_start_method=self.proc_start_method, ) - -# @attrs.define -# class OpenMMRayTask: - -# openmm_task: OpenMMTask - -# def __call__( -# self, -# *args, -# **kwargs, -# ) -> OpenMMState: - -# logging.basicConfig(level=logging.INFO) -# logging.getLogger("OpenMMRayTask").info("Configured logging in OpenMMRayTask process") - -# return self.openmm_task(*args, **kwargs) - - -# class OpenMMRayPoolWorkMapper: - - -# def __init__( -# self, -# platform: str, -# device_ids: list[int] | None = None, -# device_platform_properties: list[dict[str, str]] | None = None, -# global_platform_properties: dict[str, str] | None = None, -# ray_init_args: dict[str,Any] | None = None, -# ): - -# if platform in {"CUDA", "HIP", "OpenCL"}: -# if device_ids is None: -# raise ValueError(f"For accelerator platforms ({platform} requested) device_ids must be given.") - -# if device_platform_properties is not None: - -# if len(device_platform_properties) != device_ids: -# raise ValueError(f"{len(device_ids)} requested, but only {len(device_platform_properties)} given.") - -# else: -# self._device_platform_properties = { -# idx : props -# for idx, props -# in enumerate(device_platform_properties) -# } - - -# else: -# self._device_platform_properties = None - -# self._platform = platform -# self._device_ids: dict[int,int] = { -# idx: device_id -# for idx, device_id -# in enumerate(device_ids) -# } -# self._num_workers = len(device_ids) - -# if ray_init_args is not None: -# self._ray_init_args = ray_init_args - -# else: -# self._ray_init_args = None - - -# self._global_platform_properties = global_platform_properties - -# def init( -# self, -# ) -> None: - -# logger.info("Initializing RayPoolMapper") - -# self._ray_ctx = ray.init(**(self._ray_init_args if self._ray_init_args is not None else {})) - - -# def cleanup(self) -> None: - -# logger.info("Running RayPoolMapper cleanup") - -# ray.shutdown() - -# def map( -# self, -# tasks: list[OpenMMTask], -# walker_states: list[OpenMMState], -# ) -> list[OpenMMState]: - -# logger.info(f"Running map on {len(walker_states)} in batches of {self._num_workers}") - -# # spin up a new pool for each map -# logger.info(f"Starting ray Pool with {self._num_workers} workers") -# with ray.util.multiprocessing.Pool( -# processes=self._num_workers, -# # only run one thing per task, just to make sure -# # everything is cleaned up -# maxtasksperchild=1, -# ) as pool: - -# results = [] -# for batch_idx, batch in enumerate(itertools.batched( -# zip(walker_states, tasks, strict=True), -# self._num_workers, -# strict=False, -# )): - -# logger.info(f"Submitting batch: {batch_idx}") - -# batch_results = [] -# for batch_task_idx, (walker_state, task) in enumerate(batch): - -# task_idx = batch_idx + batch_task_idx -# # for our purposes each element in this batch -# # should be associated with a worker. -# worker_idx = batch_task_idx - -# if self._device_ids is not None and self._platform in GPU_PLATFORMS: -# logger.info("Worker platform configured and resolving platform properties.") -# platform_kwargs = self._global_platform_properties | { -# "DeviceIndex" : str(self._device_ids[worker_idx]), -# **( -# self._device_platform_properties[worker_idx] -# if self._device_platform_properties is not None -# else {} -# ), -# } -# else: -# logger.info("Non-worker platform, only using global properties") -# platform_kwargs = self._global_platform_properties - -# _task = OpenMMRayTask(task) -# logger.info(f"Submitting task {task_idx} to worker {worker_idx}") -# logger.info(f"Injecting: platform={self._platform}, platform_kwargs={platform_kwargs}") -# result = pool.apply_async( -# _task, -# args=(walker_state,), -# kwargs=dict( -# platform=self._platform, -# platform_kwargs=platform_kwargs -# ) -# ) -# logger.info(f"Task {task_idx} submitted") -# batch_results.append(result) - -# logger.info(f"Batch {batch_idx} submitted, awaiting results.") -# for batch_task_idx, task_result in enumerate(batch_results): - -# task_idx = batch_idx + batch_task_idx -# logger.info(f"Awaiting task {task_idx}") - - -# try: -# real_result = task_result.get() -# # TODO: add timeouts and retries -# except TimeoutError as exc: -# raise exc -# except Exception as exc: -# raise exc - -# results.append(real_result) - -# logger.info(f"Retrieved completed results for task: {task_idx}") - -# logger.info(f"Batch {batch_idx} completed") - -# logger.info(f"Completed all batches, terminating Pool") - - -# return results diff --git a/src/wepy/work_mapper/openmm/ray.py b/src/wepy/work_mapper/openmm/ray.py new file mode 100644 index 00000000..f29b978c --- /dev/null +++ b/src/wepy/work_mapper/openmm/ray.py @@ -0,0 +1,183 @@ +"""Special OpenMM mappers.""" +import logging +from typing import Literal, Any, Callable +import time +import itertools + +import attrs +import ray +import ray.util.multiprocessing + +from wepy.work_mapper.base import WorkMapper +from wepy.runners.openmm import OpenMMState, OpenMMRunner, PlatformKwargs, OpenMMPlatformName + +logger = logging.getLogger(__name__) + +@attrs.define +class OpenMMRayTask: + + openmm_task: OpenMMTask + + def __call__( + self, + *args, + **kwargs, + ) -> OpenMMState: + + logging.basicConfig(level=logging.INFO) + logging.getLogger("OpenMMRayTask").info("Configured logging in OpenMMRayTask process") + + return self.openmm_task(*args, **kwargs) + + +class OpenMMRayPoolWorkMapper: + + + def __init__( + self, + platform: str, + device_ids: list[int] | None = None, + device_platform_properties: list[dict[str, str]] | None = None, + global_platform_properties: dict[str, str] | None = None, + ray_init_args: dict[str,Any] | None = None, + ): + + if platform in {"CUDA", "HIP", "OpenCL"}: + if device_ids is None: + raise ValueError(f"For accelerator platforms ({platform} requested) device_ids must be given.") + + if device_platform_properties is not None: + + if len(device_platform_properties) != device_ids: + raise ValueError(f"{len(device_ids)} requested, but only {len(device_platform_properties)} given.") + + else: + self._device_platform_properties = { + idx : props + for idx, props + in enumerate(device_platform_properties) + } + + + else: + self._device_platform_properties = None + + self._platform = platform + self._device_ids: dict[int,int] = { + idx: device_id + for idx, device_id + in enumerate(device_ids) + } + self._num_workers = len(device_ids) + + if ray_init_args is not None: + self._ray_init_args = ray_init_args + + else: + self._ray_init_args = None + + + self._global_platform_properties = global_platform_properties + + def init( + self, + ) -> None: + + logger.info("Initializing RayPoolMapper") + + self._ray_ctx = ray.init(**(self._ray_init_args if self._ray_init_args is not None else {})) + + + def cleanup(self) -> None: + + logger.info("Running RayPoolMapper cleanup") + + ray.shutdown() + + def map( + self, + tasks: list[OpenMMTask], + walker_states: list[OpenMMState], + ) -> list[OpenMMState]: + + logger.info(f"Running map on {len(walker_states)} in batches of {self._num_workers}") + + # spin up a new pool for each map + logger.info(f"Starting ray Pool with {self._num_workers} workers") + with ray.util.multiprocessing.Pool( + processes=self._num_workers, + # only run one thing per task, just to make sure + # everything is cleaned up + maxtasksperchild=1, + ) as pool: + + results = [] + for batch_idx, batch in enumerate(itertools.batched( + zip(walker_states, tasks, strict=True), + self._num_workers, + strict=False, + )): + + logger.info(f"Submitting batch: {batch_idx}") + + batch_results = [] + for batch_task_idx, (walker_state, task) in enumerate(batch): + + task_idx = batch_idx + batch_task_idx + # for our purposes each element in this batch + # should be associated with a worker. + worker_idx = batch_task_idx + + if self._device_ids is not None and self._platform in GPU_PLATFORMS: + logger.info("Worker platform configured and resolving platform properties.") + platform_kwargs = self._global_platform_properties | { + "DeviceIndex" : str(self._device_ids[worker_idx]), + **( + self._device_platform_properties[worker_idx] + if self._device_platform_properties is not None + else {} + ), + } + else: + logger.info("Non-worker platform, only using global properties") + platform_kwargs = self._global_platform_properties + + _task = OpenMMRayTask(task) + logger.info(f"Submitting task {task_idx} to worker {worker_idx}") + logger.info(f"Injecting: platform={self._platform}, platform_kwargs={platform_kwargs}") + result = pool.apply_async( + _task, + args=(walker_state,), + kwargs=dict( + platform=self._platform, + platform_kwargs=platform_kwargs + ) + ) + logger.info(f"Task {task_idx} submitted") + batch_results.append(result) + + logger.info(f"Batch {batch_idx} submitted, awaiting results.") + for batch_task_idx, task_result in enumerate(batch_results): + + task_idx = batch_idx + batch_task_idx + logger.info(f"Awaiting task {task_idx}") + + + try: + real_result = task_result.get() + # TODO: add timeouts and retries + except TimeoutError as exc: + raise exc + except Exception as exc: + raise exc + + results.append(real_result) + + logger.info(f"Retrieved completed results for task: {task_idx}") + + logger.info(f"Batch {batch_idx} completed") + + logger.info(f"Completed all batches, terminating Pool") + + + return results diff --git a/src/wepy/work_mapper/openmm/serial.py b/src/wepy/work_mapper/openmm/serial.py new file mode 100644 index 00000000..2c15a70d --- /dev/null +++ b/src/wepy/work_mapper/openmm/serial.py @@ -0,0 +1,79 @@ +import logging +from typing import Literal, Any, Callable +import time +import itertools + +import attrs + +from wepy.work_mapper.base import WorkMapper +from wepy.runners.openmm import OpenMMState, OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS + +logger = logging.getLogger(__name__) + +class OpenMMSerialWorkMapper(WorkMapper): + + def __init__( + self, + platform: OpenMMPlatformName, + global_platform_properties: dict[str, str] | None = None, + ) -> None: + self._worker_segment_times: dict[int, list[float]] = {0: []} + + self._platform = platform + self._global_platform_properties = global_platform_properties if global_platform_properties is not None else {} + + def get_worker_segment_times(self) -> dict[int, list[float]]: + """The run timings for each segment for each walker. + + Returns + ------- + worker_seg_times : Dictionary mapping worker indices to a list of times in + seconds for each segment run. + + """ + return self._worker_segment_times + + def init(self) -> None: + pass + + def cleanup(self) -> None: + pass + + def map( + self, + task: Callable[[OpenMMState, int], OpenMMState], + walker_states: list[OpenMMState], + segment_lengths: list[int], + ) -> list[OpenMMState]: + + segment_times: list[float] = [] + results: list[OpenMMState] = [] + for task_idx, task_args in enumerate(zip(walker_states, segment_lengths, strict=True)): + + tic = time.time() + result = task( + *task_args, + platform_name=self._platform, + platform_kwargs=self._global_platform_properties, + ) + toc = time.time() + + segment_times.append(toc - tic) + results.append(result) + + self._worker_segment_times[0] = segment_times + + return results + +@attrs.define +class OpenMMSerialWorkMapperFactory: + + platform: OpenMMPlatformName + global_platform_properties: dict[str, str] | None = None + + def __call__(self) -> OpenMMSerialWorkMapper: + + return OpenMMSerialWorkMapper( + platform=self.platform, + global_platform_properties=self.global_platform_properties, + ) From f1c174a1f65d585a9e667eeee8132ebc50d2d437 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 4 Dec 2025 22:45:46 -0500 Subject: [PATCH 048/143] refactor log pool setup function and test --- src/wepy/util/multiprocessing.py | 70 ++++++++++++++++++++ src/wepy/work_mapper/openmm/proc_pool.py | 41 +----------- tests/unit/test_util/test_multiprocessing.py | 34 ++++++++++ 3 files changed, 107 insertions(+), 38 deletions(-) create mode 100644 src/wepy/util/multiprocessing.py create mode 100644 tests/unit/test_util/test_multiprocessing.py diff --git a/src/wepy/util/multiprocessing.py b/src/wepy/util/multiprocessing.py new file mode 100644 index 00000000..3ff7d4d3 --- /dev/null +++ b/src/wepy/util/multiprocessing.py @@ -0,0 +1,70 @@ +import copy +import logging +import logging.handlers +import logging.config +import multiprocessing as mp + +logger = logging.getLogger(__name__) + +_BASE_WORKER_LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "queue": { + "class": "logging.handlers.QueueHandler", + "queue": None, # injected at runtime + } + }, + "root": { + "handlers": ["queue"], + "level": "NOTSET", # defer filtering to parent + }, +} + +def proc_pool_worker_setup(log_queue: mp.Queue) -> None: + """Pool(initializer=) function that handles logging properly. + + Does its best to inherit log settings from a parent + process. Requires a logging queue that the log messages are sent + on. + + Works properly with 'spawn' start method. + + + + """ + + config = copy.deepcopy(_BASE_WORKER_LOGGING_CONFIG) + + config["handlers"]["queue"]["queue"] = log_queue + parent_loggers = logging.root.manager.loggerDict + for name, logger in parent_loggers.items(): + if isinstance(logger, logging.Logger): + config.setdefault("loggers", {})[name] = { + "level" : logging.getLevelName(logger.level), + "propagate" : logger.propagate, + "handlers" : [], + "filters" : [ + f.__class__.__name__ + for f + in logger.filters + ], + } + + logging.config.dictConfig(config) + + logger.info("Configured logging in worker process") + + +def _dummy_task(foo: int) -> int: + """Just a dummy function used for testing. + + For 'spawn' we need to have it importable thus it is defined here + and not in a test. + + """ + + logger = logging.getLogger("dummy-task") + logger.info("Executing dummy task") + + return foo + 1 diff --git a/src/wepy/work_mapper/openmm/proc_pool.py b/src/wepy/work_mapper/openmm/proc_pool.py index 9c292584..f32037d2 100644 --- a/src/wepy/work_mapper/openmm/proc_pool.py +++ b/src/wepy/work_mapper/openmm/proc_pool.py @@ -10,47 +10,12 @@ import attrs +from wepy.util.multiprocessing import proc_pool_worker_setup from wepy.work_mapper.base import WorkMapper from wepy.runners.openmm import OpenMMState, OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS logger = logging.getLogger(__name__) -BASE_WORKER_LOGGING_CONFIG = { - "version": 1, - "disable_existing_loggers": False, - "handlers": { - "queue": { - "class": "logging.handlers.QueueHandler", - "queue": None, # injected at runtime - } - }, - "root": { - "handlers": ["queue"], - "level": "NOTSET", # defer filtering to parent - }, -} - -def _worker_setup(log_queue: mp.Queue) -> None: - - config = copy.deepcopy(BASE_WORKER_LOGGING_CONFIG) - - config["handlers"]["queue"]["queue"] = log_queue - parent_loggers = logging.root.manager.loggerDict - for name, logger in parent_loggers.items(): - if isinstance(logger, logging.Logger): - config.setdefault("loggers", {})[name] = { - "level" : logging.getLevelName(logger.level), - "propagate" : logger.propagate, - "handlers" : [], - "filters" : [ - f.__class__.__name__ - for f - in logger.filters - ], - } - - logging.config.dictConfig(config) - class OpenMMProcPoolWorkMapper(WorkMapper): def __init__( @@ -135,13 +100,13 @@ def map( handlers = list(logging.getLogger().handlers) listener = logging.handlers.QueueListener(log_queue, *handlers) listener.start() - + with self._mp_ctx.Pool( processes=self._num_procs, # only run one thing per task, just to make sure # everything is cleaned up maxtasksperchild=1, - initializer=_worker_setup, + initializer=proc_pool_worker_setup, initargs=(log_queue,), ) as pool: diff --git a/tests/unit/test_util/test_multiprocessing.py b/tests/unit/test_util/test_multiprocessing.py new file mode 100644 index 00000000..06e801a3 --- /dev/null +++ b/tests/unit/test_util/test_multiprocessing.py @@ -0,0 +1,34 @@ +import logging +import multiprocessing as mp + +from wepy.util.multiprocessing import proc_pool_worker_setup, _dummy_task + +def test__dummy_task(): + + assert _dummy_task(1) == 2 + +def test_proc_pool_worker_setup(caplog): + + log_queue = mp.Queue() + handlers = list(logging.getLogger().handlers) + listener = logging.handlers.QueueListener(log_queue, *handlers) + listener.start() + + + with mp.Pool( + processes=1, + initializer=proc_pool_worker_setup, + initargs=(log_queue,), + ) as pool: + + # NOTE: this is the hardcoded logger in the dummy function + with caplog.at_level(logging.INFO, logger="dummy-task"): + result = pool.apply(_dummy_task, (1,)) + + assert result == 2 + + assert len(caplog.records) == 2 + assert caplog.records[0].levelname == "INFO" + assert caplog.records[0].msg == "Configured logging in worker process" + assert caplog.records[1].levelname == "INFO" + assert caplog.records[1].msg == "Executing dummy task" From 0821838854adce76a0051d0bce0ccf38fe657d96 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 4 Dec 2025 22:47:21 -0500 Subject: [PATCH 049/143] add log handling to REVO proc pool --- src/wepy/resampling/resamplers/revo.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 062f5193..9ba75574 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -4,13 +4,14 @@ logger = logging.getLogger(__name__) # Standard Library -import multiprocessing as mulproc +import multiprocessing as mp import random as rand # Third Party Library import numpy as np # First Party Library +from wepy.util.multiprocessing import proc_pool_worker_setup from wepy.resampling.resamplers.clone_merge import CloneMergeResampler @@ -655,7 +656,16 @@ def _all_to_all_distance(self, walkers): # make images for all the walker states for us to compute distances on if self.num_proc > 1: - with mulproc.Pool(self.num_proc) as pool: + log_queue = mp.Queue() + handlers = list(logging.getLogger().handlers) + listener = logging.handlers.QueueListener(log_queue, *handlers) + listener.start() + + with mp.Pool( + self.num_proc, + initializer=proc_pool_worker_setup, + initargs=(log_queue,), + ) as pool: images = pool.map(self.distance.image, [walker.state for walker in walkers]) else: images = [] From 289d6dc1cd854d3a7d0a367906d810b8a42ca6c1 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 5 Dec 2025 11:18:54 -0500 Subject: [PATCH 050/143] add alanine dipeptide example system Add tests for test systems --- src/wepy_tools/systems/alanine_dipeptide.py | 90 +++++++++++ .../systems/{openmm => data}/__init__.py | 0 .../alanine_dipeptide_explicit/.gitignore | 4 + .../data/alanine_dipeptide_explicit/BUILD | 3 + .../alanine_dipeptide_explicit/__init__.py | 0 .../alanine-dipeptide.crd.dvc | 5 + .../alanine-dipeptide.pdb.dvc | 5 + .../alanine-dipeptide.prmtop.dvc | 5 + .../generate-pdb.py | 21 +++ .../alanine_dipeptide_explicit/leap.log.dvc | 5 + .../data/alanine_dipeptide_explicit/run.sh | 15 ++ .../alanine_dipeptide_explicit/setup.leap.in | 23 +++ src/wepy_tools/systems/lennard_jones.py | 2 +- src/wepy_tools/systems/openmm/base.py | 134 ---------------- src/wepy_tools/systems/openmm/nacl_pair.py | 145 ------------------ .../test_systems/test_alanine_dipeptide.py | 5 + .../test_systems/test_lennard_jones.py | 5 + 17 files changed, 187 insertions(+), 280 deletions(-) create mode 100644 src/wepy_tools/systems/alanine_dipeptide.py rename src/wepy_tools/systems/{openmm => data}/__init__.py (100%) create mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/.gitignore create mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/BUILD create mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/__init__.py create mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.crd.dvc create mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.pdb.dvc create mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.prmtop.dvc create mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/generate-pdb.py create mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/leap.log.dvc create mode 100755 src/wepy_tools/systems/data/alanine_dipeptide_explicit/run.sh create mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/setup.leap.in delete mode 100644 src/wepy_tools/systems/openmm/base.py delete mode 100644 src/wepy_tools/systems/openmm/nacl_pair.py create mode 100644 tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py create mode 100644 tests/unit/test_wepy_tools/test_systems/test_lennard_jones.py diff --git a/src/wepy_tools/systems/alanine_dipeptide.py b/src/wepy_tools/systems/alanine_dipeptide.py new file mode 100644 index 00000000..08bb90d0 --- /dev/null +++ b/src/wepy_tools/systems/alanine_dipeptide.py @@ -0,0 +1,90 @@ +from importlib.resources import files + +import openmm +import openmm.app +import openmm.unit + +DEFAULT_EWALD_ERROR_TOLERANCE = 1.0e-5 +DEFAULT_CUTOFF_DISTANCE = 10.0 * openmm.unit.angstroms +DEFAULT_SWITCH_WIDTH = 1.5 * openmm.unit.angstroms + +class AlanineDipeptideExplicit: + """Alanine dipeptide ff96 in TIP3P explicit solvent. + + Parameters + ---------- + constraints : optional, default=openmm.app.HBonds + rigid_water : bool, optional, default=True + nonbondedCutoff : Quantity, optional, default=9.0 * unit.angstroms + use_dispersion_correction : bool, optional, default=True + If True, the long-range disperson correction will be used. + nonbondedMethod : openmm.app nonbonded method, optional, default=app.PME + Sets the nonbonded method to use for the water box (one of app.CutoffPeriodic, app.Ewald, app.PME). + hydrogenMass : unit, optional, default=None + If set, will pass along a modified hydrogen mass for OpenMM to + use mass repartitioning. + cutoff : openmm.unit.Quantity with units compatible with angstroms, optional, default = DEFAULT_CUTOFF_DISTANCE + Cutoff distance + switch_width : openmm.unit.Quantity with units compatible with angstroms, optional, default = DEFAULT_SWITCH_WIDTH + switching function is turned on at cutoff - switch_width + If None, no switch will be applied (e.g. hard cutoff). + ewaldErrorTolerance : float, optional, default=DEFAULT_EWALD_ERROR_TOLERANCE + The Ewald or PME tolerance. + + Examples + -------- + + >>> alanine = AlanineDipeptideExplicit() + >>> (system, positions) = alanine.system, alanine.positions + """ + + def __init__( + self, + constraints=openmm.app.HBonds, + rigid_water=True, + nonbondedCutoff=DEFAULT_CUTOFF_DISTANCE, + use_dispersion_correction=True, + nonbondedMethod=openmm.app.PME, + hydrogenMass=None, + switch_width=DEFAULT_SWITCH_WIDTH, + ewaldErrorTolerance=DEFAULT_EWALD_ERROR_TOLERANCE, + ): + + prmtop_filename = files("wepy_tools.systems.data.alanine_dipeptide_explicit") / "alanine-dipeptide.prmtop" + crd_filename = files("wepy_tools.systems.data.alanine_dipeptide_explicit") / "alanine-dipeptide.crd" + + # Initialize system. + prmtop = openmm.app.AmberPrmtopFile(prmtop_filename) + system = prmtop.createSystem( + constraints=constraints, + nonbondedMethod=nonbondedMethod, + rigidWater=rigid_water, + nonbondedCutoff=nonbondedCutoff, + hydrogenMass=hydrogenMass, + ) + + # Extract topology + self.topology = prmtop.topology + + # Set dispersion correction use. + forces = { + system.getForce(index).__class__.__name__: system.getForce(index) + for index + in range(system.getNumForces()) + } + forces['NonbondedForce'].setUseDispersionCorrection(use_dispersion_correction) + forces['NonbondedForce'].setEwaldErrorTolerance(ewaldErrorTolerance) + + if switch_width is not None: + forces['NonbondedForce'].setUseSwitchingFunction(True) + forces['NonbondedForce'].setSwitchingDistance(nonbondedCutoff - switch_width) + + # Read positions. + inpcrd = openmm.app.AmberInpcrdFile(crd_filename) + positions = inpcrd.getPositions(asNumpy=True) + + # Set box vectors. + box_vectors = inpcrd.getBoxVectors(asNumpy=True) + system.setDefaultPeriodicBoxVectors(box_vectors[0], box_vectors[1], box_vectors[2]) + + self.system, self.positions = system, positions diff --git a/src/wepy_tools/systems/openmm/__init__.py b/src/wepy_tools/systems/data/__init__.py similarity index 100% rename from src/wepy_tools/systems/openmm/__init__.py rename to src/wepy_tools/systems/data/__init__.py diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/.gitignore b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/.gitignore new file mode 100644 index 00000000..9730052f --- /dev/null +++ b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/.gitignore @@ -0,0 +1,4 @@ +/alanine-dipeptide.crd +/alanine-dipeptide.pdb +/alanine-dipeptide.prmtop +/leap.log diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/BUILD b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/BUILD new file mode 100644 index 00000000..a01f53ef --- /dev/null +++ b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/BUILD @@ -0,0 +1,3 @@ +resources( + sources=["*.crd", "*.pdb", "*.prmtop", "setup.leap.in"] +) diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/__init__.py b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.crd.dvc b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.crd.dvc new file mode 100644 index 00000000..eb5e06fb --- /dev/null +++ b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.crd.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 567e2fd21891f7a3b6d62aab422a3538 + size: 82903 + hash: md5 + path: alanine-dipeptide.crd diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.pdb.dvc b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.pdb.dvc new file mode 100644 index 00000000..e0424930 --- /dev/null +++ b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.pdb.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 6e51f11350240325b11408b6ba782158 + size: 184227 + hash: md5 + path: alanine-dipeptide.pdb diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.prmtop.dvc b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.prmtop.dvc new file mode 100644 index 00000000..7ae1039d --- /dev/null +++ b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.prmtop.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 099b49adfcc8da32792ea5efdffe9f5b + size: 357642 + hash: md5 + path: alanine-dipeptide.prmtop diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/generate-pdb.py b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/generate-pdb.py new file mode 100644 index 00000000..583c761c --- /dev/null +++ b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/generate-pdb.py @@ -0,0 +1,21 @@ +""" +Generate PDB file containing periodic box data. + +""" + +from simtk import openmm, unit +from simtk.openmm import app + +prmtop_filename = 'alanine-dipeptide.prmtop' +crd_filename = 'alanine-dipeptide.crd' +pdb_filename = 'alanine-dipeptide.pdb' + +# Read topology and positions. +prmtop = app.AmberPrmtopFile(prmtop_filename) +inpcrd = app.AmberInpcrdFile(crd_filename) + +# Write PDB. +outfile = open(pdb_filename, 'w') +app.PDBFile.writeFile(prmtop.topology, inpcrd.positions, file=outfile, keepIds=False) +outfile.close() + diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/leap.log.dvc b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/leap.log.dvc new file mode 100644 index 00000000..593e3928 --- /dev/null +++ b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/leap.log.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 6a49ab75254c368f73beff887d7cac8b + size: 9517 + hash: md5 + path: leap.log diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/run.sh b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/run.sh new file mode 100755 index 00000000..bddcda4d --- /dev/null +++ b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/run.sh @@ -0,0 +1,15 @@ +#!/bin/tcsh + +# Name of system +setenv SYSTEM alanine-dipeptide + +# Clean up old files, if present. +rm -f leap.log ${SYSTEM}.{crd,prmtop,pdb} + +# Create prmtop/crd files. +tleap -f setup.leap.in + +# Create PDB file. +#cat ${SYSTEM}.crd | ambpdb -p ${SYSTEM}.prmtop > ${SYSTEM}.pdb +python generate-pdb.py + diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/setup.leap.in b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/setup.leap.in new file mode 100644 index 00000000..78ffb798 --- /dev/null +++ b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/setup.leap.in @@ -0,0 +1,23 @@ +# Create terminally-blocked alanine peptide model with AMBER ff96 and OBC GBSA. + +# Load AMBER '96 forcefield for protein. +source leaprc.ff96 + +# Create sequence. +peptide = sequence { ACE ALA NME } + +# Check peptide. +check peptide + +# Report on net charge. +charge peptide + +# Solvate in water box. +solvateBox peptide TIP3PBOX 9.0 iso + +# Write parameters. +saveAmberParm peptide alanine-dipeptide.prmtop alanine-dipeptide.crd + +# Exit +quit + diff --git a/src/wepy_tools/systems/lennard_jones.py b/src/wepy_tools/systems/lennard_jones.py index 5f151714..3f9d25ee 100644 --- a/src/wepy_tools/systems/lennard_jones.py +++ b/src/wepy_tools/systems/lennard_jones.py @@ -47,7 +47,7 @@ class LennardJonesPair: """ - def __init__(self, mass=39.9 * openmm.unit.amu, sigma=3.350 * openmm.unit.angstrom, epsilon=10.0 * openmm.unit.kilocalories_per_mole, **kwargs): + def __init__(self, mass=39.9 * openmm.unit.amu, sigma=3.350 * openmm.unit.angstrom, epsilon=10.0 * openmm.unit.kilocalories_per_mole): # Store parameters self.mass = mass diff --git a/src/wepy_tools/systems/openmm/base.py b/src/wepy_tools/systems/openmm/base.py deleted file mode 100644 index 80dd9003..00000000 --- a/src/wepy_tools/systems/openmm/base.py +++ /dev/null @@ -1,134 +0,0 @@ -# Standard Library - -# Third Party Library -import numpy as np -import openmm -import openmm.app as omma -import openmm.unit as unit - - -class TestSystem: - """Abstract base class for test systems, demonstrating how to implement a test system. - - Parameters - ---------- - - Attributes - ---------- - system : openmm.System - System object for the test system - positions : list - positions of test system - topology : list - topology of the test system - - Notes - ----- - Unimplemented methods will default to the base class methods, which raise a NotImplementedException. - - Examples - -------- - Create a test system. - - >>> testsystem = TestSystem() - - Retrieve a deep copy of the System object. - - >>> system = testsystem.system - - Retrieve a deep copy of the positions. - - >>> positions = testsystem.positions - - Retrieve a deep copy of the topology. - - >>> topology = testsystem.topology - - Serialize system and positions to XML (to aid in debugging). - - >>> (system_xml, positions_xml) = testsystem.serialize() - - """ - - def __init__(self, **kwargs): - """Abstract base class for test system. - - Parameters - ---------- - - """ - - # Create an empty system object. - self._system = openmm.System() - - # Store positions. - self._positions = unit.Quantity(np.zeros([0, 3], float), unit.nanometers) - - # Empty topology. - self._topology = omma.Topology() - # MDTraj Topology is built on demand. - self._mdtraj_topology = None - - @property - def system(self): - """The openmm.System object corresponding to the test system.""" - return self._system - - @system.setter - def system(self, value): - self._system = value - - @system.deleter - def system(self): - del self._system - - @property - def positions(self): - """The openmm.unit.Quantity object containing the particle positions, with units compatible with openmm.unit.nanometers.""" - return self._positions - - @positions.setter - def positions(self, value): - self._positions = value - - @positions.deleter - def positions(self): - del self._positions - - @property - def topology(self): - """The openmm.app.Topology object corresponding to the test system.""" - return self._topology - - @topology.setter - def topology(self, value): - self._topology = value - self._mdtraj_topology = None - - @topology.deleter - def topology(self): - del self._topology - - @property - def mdtraj_topology(self): - """The mdtraj.Topology object corresponding to the test system (read-only).""" - # Third Party Library - import mdtraj as md - - if self._mdtraj_topology is None: - self._mdtraj_topology = md.Topology.from_openmm(self._topology) - return self._mdtraj_topology - - def construct_restraining_potential(self, particle_indices, K): - """Make a CustomExternalForce that puts an origin-centered spring on the chosen particles""" - - # Add a restraining potential centered at the origin. - energy_expression = "(K/2.0) * (x^2 + y^2 + z^2);" - energy_expression += "K = %f;" % ( - K / (unit.kilojoules_per_mole / unit.nanometers**2) - ) # in OpenMM units - force = openmm.CustomExternalForce(energy_expression) - for particle_index in particle_indices: - force.addParticle(particle_index, []) - - return force diff --git a/src/wepy_tools/systems/openmm/nacl_pair.py b/src/wepy_tools/systems/openmm/nacl_pair.py deleted file mode 100644 index 58922fa3..00000000 --- a/src/wepy_tools/systems/openmm/nacl_pair.py +++ /dev/null @@ -1,145 +0,0 @@ -# Third Party Library -import numpy as np -import openmm -import openmm.app as omma -import openmm.unit as unit - -# First Party Library -from wepy_tools.systems.openmm.base import TestSystem - - -class NaClPair(TestSystem): - """Create a non-periodic rectilinear grid of NaCl pair in a harmonic restraining potential. - - Parameters - ---------- - nx : int, optional, default=3 - number of particles in the x direction - ny : int, optional, default=3 - number of particles in the y direction - nz : int, optional, default=3 - number of particles in the z direction - - Attributes - ---------- - MASS_Na : openmm.unit.Quantity - Mass of a Na atom. - MASS_Cl : openmm.unit.Quantity - Mass of a Cl atom. - Q_Na : openmm.unit.Quantity - Charge of a Na ion. - Q_Cl : openmm.unit.Quantity - Charge of a Cl ion. - SIGMA_Na : openmm.unit.Quantity - Lennard-Jones sigma parameter for Na. - SIGMA_Cl : openmm.unit.Quantity - Lennard-Jones sigma parameter for Cl. - EPSILON_Na : openmm.unit.Quantity - Lennard-Jones epsilon parameter for Na. - EPSILON_Cl : openmm.unit.Quantity - Lennard-Jones epsilon parameter for Cl. - CUTOFF : openmm.unit.Quantity or None - Class-level default cutoff distance. If None, no cutoff is used. - SWITCH_WIDTH : openmm.unit.Quantity or None - Class-level default switching width. - SCALE_STEP_SIZE_X : float - Step size scaling in the x dimension (default=1.0). - SCALE_STEP_SIZE_Y : float - Step size scaling in the y dimension (default=1.0). - SCALE_STEP_SIZE_Z : float - Step size scaling in the z dimension (default=1.0). - - """ - - MASS_Na = 22.99 * unit.amu - MASS_Cl = 35.45 * unit.amu - Q_Na = 1.0 * unit.elementary_charge - Q_Cl = -1.0 * unit.elementary_charge - SIGMA_Na = 2.0 * unit.angstrom - SIGMA_Cl = 4.0 * unit.angstrom - EPSILON_Na = 0.1 * unit.kilojoule_per_mole - EPSILON_Cl = 0.2 * unit.kilojoule_per_mole - K = 1.0 * unit.kilojoules_per_mole / unit.nanometer**2 - - CUTOFF = None - SWITCH_WIDTH = None - - SCALE_STEP_SIZE_X = 1.0 - SCALE_STEP_SIZE_Y = 1.0 - SCALE_STEP_SIZE_Z = 1.0 - - def __init__(self, nx=3, ny=3, nz=3, **kwargs): - super().__init__(**kwargs) - - self.nx = nx - self.ny = ny - self.nz = nz - self.natoms = nx * ny * nz - - self.construct_system() - - def construct_system(self): - system = openmm.System() - - nb = openmm.NonbondedForce() - - if self.CUTOFF is None: - nb.setNonbondedMethod(openmm.NonbondedForce.NoCutoff) - else: - nb.setNonbondedMethod(openmm.NonbondedForce.CutoffNonPeriodic) - nb.setCutoffDistance(self.CUTOFF) - nb.setUseDispersionCorrection(False) - nb.setUseSwitchingFunction(False) - if self.SWITCH_WIDTH is not None: - nb.setUseSwitchingFunction(True) - nb.setSwitchingDistance(self.CUTOFF - self.SWITCH_WIDTH) - - positions = unit.Quantity(np.zeros([self.natoms, 3], np.float32), unit.angstrom) - - atom_index = 0 - for ii in range(self.nx): - for jj in range(self.ny): - for kk in range(self.nz): - if (atom_index % 2) == 0: # Alternating Na and Cl - mass = self.MASS_Na - q = self.Q_Na - sigma = self.SIGMA_Na - epsilon = self.EPSILON_Na - element = omma.Element.getBySymbol("Na") - atom_name = "Na" - else: - mass = self.MASS_Cl - q = self.Q_Cl - sigma = self.SIGMA_Cl - epsilon = self.EPSILON_Cl - element = omma.Element.getBySymbol("Cl") - atom_name = "Cl" - - system.addParticle(mass) - nb.addParticle(q, sigma, epsilon) - x = sigma * self.SCALE_STEP_SIZE_X * (ii - self.nx / 2.0) - y = sigma * self.SCALE_STEP_SIZE_Y * (jj - self.ny / 2.0) - z = sigma * self.SCALE_STEP_SIZE_Z * (kk - self.nz / 2.0) - - positions[atom_index, 0] = x - positions[atom_index, 1] = y - positions[atom_index, 2] = z - atom_index += 1 - - system.addForce(nb) - - topology = omma.Topology() - chain = topology.addChain() - for _ in range(system.getNumParticles()): - residue = topology.addResidue(atom_name, chain) - topology.addAtom(atom_name, element, residue) - self.topology = topology - - # Add a restraining potential centered at the origin. - system.addForce( - self.construct_restraining_potential( - particle_indices=range(self.natoms), K=self.K - ) - ) - - self.system, self.positions = system, positions diff --git a/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py b/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py new file mode 100644 index 00000000..3f10d897 --- /dev/null +++ b/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py @@ -0,0 +1,5 @@ +from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideExplicit + +def test_AlanineDipeptideExplicit(): + + AlanineDipeptideExplicit() diff --git a/tests/unit/test_wepy_tools/test_systems/test_lennard_jones.py b/tests/unit/test_wepy_tools/test_systems/test_lennard_jones.py new file mode 100644 index 00000000..16116432 --- /dev/null +++ b/tests/unit/test_wepy_tools/test_systems/test_lennard_jones.py @@ -0,0 +1,5 @@ +from wepy_tools.systems.lennard_jones import LennardJonesPair + +def test_LennardJonesPair(): + + LennardJonesPair() From bd50847c0fe46982bc93c574c862cc90fa694755 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 5 Dec 2025 12:29:02 -0500 Subject: [PATCH 051/143] add alanine dipeptide ramachandran distance metric --- src/wepy_tools/systems/alanine_dipeptide.py | 87 ++++++++++++++++++- .../test_systems/test_alanine_dipeptide.py | 83 +++++++++++++++++- 2 files changed, 166 insertions(+), 4 deletions(-) diff --git a/src/wepy_tools/systems/alanine_dipeptide.py b/src/wepy_tools/systems/alanine_dipeptide.py index 08bb90d0..f6fb86d6 100644 --- a/src/wepy_tools/systems/alanine_dipeptide.py +++ b/src/wepy_tools/systems/alanine_dipeptide.py @@ -1,8 +1,17 @@ +from typing import Any from importlib.resources import files +import attrs +import numpy as np import openmm import openmm.app import openmm.unit +import mdtraj + +from wepy.walker import WalkerState +from wepy.runners.openmm import OpenMMState +from wepy.resampling.distances.distance import Distance +from wepy.util.mdtraj import traj_fields_to_mdtraj DEFAULT_EWALD_ERROR_TOLERANCE = 1.0e-5 DEFAULT_CUTOFF_DISTANCE = 10.0 * openmm.unit.angstroms @@ -48,7 +57,7 @@ def __init__( hydrogenMass=None, switch_width=DEFAULT_SWITCH_WIDTH, ewaldErrorTolerance=DEFAULT_EWALD_ERROR_TOLERANCE, - ): + ) -> None: prmtop_filename = files("wepy_tools.systems.data.alanine_dipeptide_explicit") / "alanine-dipeptide.prmtop" crd_filename = files("wepy_tools.systems.data.alanine_dipeptide_explicit") / "alanine-dipeptide.crd" @@ -84,7 +93,79 @@ def __init__( positions = inpcrd.getPositions(asNumpy=True) # Set box vectors. - box_vectors = inpcrd.getBoxVectors(asNumpy=True) - system.setDefaultPeriodicBoxVectors(box_vectors[0], box_vectors[1], box_vectors[2]) + _box_vectors = inpcrd.getBoxVectors(asNumpy=True) + system.setDefaultPeriodicBoxVectors(_box_vectors[0], _box_vectors[1], _box_vectors[2]) + + + self.box_vectors = np.array( + [ + vec_q.value_in_unit(vec_q.unit) + for vec_q + in _box_vectors + ] + ) * _box_vectors[0].unit self.system, self.positions = system, positions + +@attrs.define +class AlanineDipeptideRamachandranDistanceImage(WalkerState): + + phis: np.typing.ArrayLike + psis: np.typing.ArrayLike + +@attrs.define +class AlanineDipeptideRamachandranDistance(Distance): + # the parsed JSON topology of plain python objects + topology: str + + def image(self, state: OpenMMState) -> AlanineDipeptideRamachandranDistanceImage: + + + _unit = state["positions"].unit + state_dict = { + # traj shape to match interface requirements + key : np.array([quantity.value_in_unit(_unit)]) + for key, quantity + in state.to_dict().items() + if key in {"positions", "box_vectors"} + } + traj = traj_fields_to_mdtraj( + state_dict, + self.topology, + ) + + is_periodic = "box_vectors" in state + + _, phis = mdtraj.compute_phi( + traj, + periodic=is_periodic, + opt=True, + ) + _, psis = mdtraj.compute_psi( + traj, + periodic=is_periodic, + opt=True, + ) + + return AlanineDipeptideRamachandranDistanceImage( + phis=phis, + psis=psis, + ) + + + def image_distance( + self, + image_a: AlanineDipeptideRamachandranDistanceImage, + image_b: AlanineDipeptideRamachandranDistanceImage, + ) -> float: + + angles_a = np.concatenate((image_a.phis, image_a.psis)) + angles_b = np.concatenate((image_b.phis, image_b.psis)) + + # compute the circular difference + deltas = np.atan2( + np.sin(angles_a - angles_b), + np.cos(angles_a - angles_b), + ) + + return np.sqrt(np.sum(deltas**2)) diff --git a/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py b/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py index 3f10d897..d41a9296 100644 --- a/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py +++ b/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py @@ -1,5 +1,86 @@ -from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideExplicit +import json + +import numpy as np +import mdtraj + +from wepy.util.mdtraj import mdtraj_to_json_topology +from wepy.runners.openmm import OpenMMState +from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideExplicit, AlanineDipeptideRamachandranDistance + def test_AlanineDipeptideExplicit(): AlanineDipeptideExplicit() + +class Test_AlanineDipeptideRamachandranDistance: + + def test_image(self): + + ala_sys = AlanineDipeptideExplicit() + + json_top = mdtraj_to_json_topology( + mdtraj.Topology.from_openmm(ala_sys.topology) + ) + + distance = AlanineDipeptideRamachandranDistance(topology=json_top) + + state = OpenMMState.from_dwim( + positions=ala_sys.positions, + box_vectors=ala_sys.box_vectors, + ) + + image = distance.image(state) + + def test_image_distance(self): + + ala_sys = AlanineDipeptideExplicit() + + json_top = mdtraj_to_json_topology( + mdtraj.Topology.from_openmm(ala_sys.topology) + ) + + distance = AlanineDipeptideRamachandranDistance(topology=json_top) + + state = OpenMMState.from_dwim( + positions=ala_sys.positions, + box_vectors=ala_sys.box_vectors, + ) + + image_a = distance.image(state) + + assert np.isclose(distance.image_distance(image_a, image_a), 0.) + + # then make a jittered atom positions to get something a little + # different to compare + jitter_positions = ala_sys.positions + np.random.uniform( + -0.01, + 0.01, + size=ala_sys.positions.shape, + ) * ala_sys.positions.unit + + jitter_state = OpenMMState.from_dwim( + positions=jitter_positions, + box_vectors=ala_sys.box_vectors, + ) + + jitter_image = distance.image(jitter_state) + + assert not np.isclose( + distance.image_distance( + image_a, + jitter_image, + ), + 0., + ) + + # test it is symmetric + assert np.isclose( + distance.image_distance( + image_a, + jitter_image, + ), + distance.image_distance( + jitter_image, + image_a, + ), + ) From 110dfcd481e655deb9bbf593e8ba55b27c270034 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 5 Dec 2025 16:34:35 -0500 Subject: [PATCH 052/143] adds more helpers for dealing with logging This sets up a temporary logging context which does its best to reuse all the existing handlers and formatters and just inject the processName and process ID into the log message, so the logs don't look drastically different from the already configured loggers. --- src/wepy/util/multiprocessing.py | 71 ++++++++++++++++++++++++ src/wepy/work_mapper/openmm/proc_pool.py | 31 ++++++----- 2 files changed, 87 insertions(+), 15 deletions(-) diff --git a/src/wepy/util/multiprocessing.py b/src/wepy/util/multiprocessing.py index 3ff7d4d3..c9d07671 100644 --- a/src/wepy/util/multiprocessing.py +++ b/src/wepy/util/multiprocessing.py @@ -1,8 +1,11 @@ +import os +import contextlib import copy import logging import logging.handlers import logging.config import multiprocessing as mp +from typing import Generator logger = logging.getLogger(__name__) @@ -53,6 +56,7 @@ def proc_pool_worker_setup(log_queue: mp.Queue) -> None: logging.config.dictConfig(config) + logger = logging.getLogger(__name__) logger.info("Configured logging in worker process") @@ -68,3 +72,70 @@ def _dummy_task(foo: int) -> int: logger.info("Executing dummy task") return foo + 1 + +class WorkerFormatter(logging.Formatter): + def __init__(self, base_formatter: logging.Formatter): + self.base_formatter = base_formatter + + def format(self, record): + # Ensure process info exists + if not hasattr(record, "processName"): + record.processName = mp.current_process().name + if not hasattr(record, "process"): + record.process = mp.current_process().pid + + # Prepend process info to the actual message + + # the formatted msg string + original_msg = record.getMessage() + record.msg = f"[{record.processName} | PID {record.process}] {original_msg}" + # ensure no old args are re-applied + record.args = () + + # Use the base formatter for the rest + formatted = self.base_formatter.format(record) + + # Restore original message so we don't mutate it permanently + record.msg = original_msg + return formatted + +@contextlib.contextmanager +def queue_listener_context(log_queue: mp.Queue) -> Generator[None, None, None]: + + root_logger = logging.getLogger() + + old_factory = logging.getLogRecordFactory() + + def record_factory(*args, **kwargs): + + record = old_factory(*args, **kwargs) + if not hasattr(record, "processName"): + record.processName = mp.current_process().name + + if not hasattr(record, "process"): + record.process = os.getpid() + + old_formatters = [ + handler.formatter + for handler + in root_logger.handlers + ] + + listener_handlers = [] + for handler in root_logger.handlers: + new_handler = copy.copy(handler) + new_handler.setFormatter(WorkerFormatter(handler.formatter)) + listener_handlers.append(new_handler) + + listener = logging.handlers.QueueListener(log_queue, *listener_handlers) + listener.start() + + try: + yield + finally: + listener.stop() + + for handler, formatter in zip(root_logger.handlers, old_formatters, strict=True): + handler.setFormatter(formatter) + + logging.setLogRecordFactory(old_factory) diff --git a/src/wepy/work_mapper/openmm/proc_pool.py b/src/wepy/work_mapper/openmm/proc_pool.py index f32037d2..cef08428 100644 --- a/src/wepy/work_mapper/openmm/proc_pool.py +++ b/src/wepy/work_mapper/openmm/proc_pool.py @@ -1,8 +1,10 @@ """Special OpenMM mappers.""" +import os import logging import logging.handlers import logging.config -from typing import Literal, Any, Callable +from typing import Literal, Any, Callable, Generator +import contextlib import time import copy import multiprocessing as mp @@ -10,12 +12,13 @@ import attrs -from wepy.util.multiprocessing import proc_pool_worker_setup +from wepy.util.multiprocessing import proc_pool_worker_setup, queue_listener_context from wepy.work_mapper.base import WorkMapper from wepy.runners.openmm import OpenMMState, OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS logger = logging.getLogger(__name__) + class OpenMMProcPoolWorkMapper(WorkMapper): def __init__( @@ -97,18 +100,18 @@ def map( log_queue = self._mp_ctx.Queue() # handler = logging.StreamHandler() - handlers = list(logging.getLogger().handlers) - listener = logging.handlers.QueueListener(log_queue, *handlers) - listener.start() - with self._mp_ctx.Pool( - processes=self._num_procs, - # only run one thing per task, just to make sure - # everything is cleaned up - maxtasksperchild=1, - initializer=proc_pool_worker_setup, - initargs=(log_queue,), - ) as pool: + with ( + queue_listener_context(log_queue), + self._mp_ctx.Pool( + processes=self._num_procs, + # only run one thing per task, just to make sure + # everything is cleaned up + maxtasksperchild=1, + initializer=proc_pool_worker_setup, + initargs=(log_queue,), + ) as pool, + ): results = [] for batch_idx, batch in enumerate(itertools.batched( @@ -203,8 +206,6 @@ def map( logger.info(f"Completed all batches, terminating Pool") - listener.stop() - return results @attrs.define From 8b861859e04ce370a5eebf65a234e4770d6f43ca Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 5 Dec 2025 16:35:49 -0500 Subject: [PATCH 053/143] cleaning and new tests --- src/wepy/runners/openmm/runner.py | 1 + src/wepy_tools/systems/mock.py | 1 - .../test_openmm/test_proc_pool_mapper.py | 84 --------- .../integration/test_openmm/test_realistic.py | 99 +++++++++++ .../test_openmm/test_serial_mapper.py | 163 ------------------ .../test_openmm/test_sim_manager.py | 6 +- 6 files changed, 103 insertions(+), 251 deletions(-) delete mode 100644 src/wepy_tools/systems/mock.py delete mode 100644 tests/integration/test_openmm/test_proc_pool_mapper.py create mode 100644 tests/integration/test_openmm/test_realistic.py delete mode 100644 tests/integration/test_openmm/test_serial_mapper.py diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index 95608e7d..cf3d13b0 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -185,6 +185,7 @@ def run_segment( steps_start = time.time() # Run the simulation segment for the number of time steps + logger.info("Running MD steps") simulation.step(segment_length) steps_end = time.time() diff --git a/src/wepy_tools/systems/mock.py b/src/wepy_tools/systems/mock.py deleted file mode 100644 index b69406cd..00000000 --- a/src/wepy_tools/systems/mock.py +++ /dev/null @@ -1 +0,0 @@ -"""Mock systems""" diff --git a/tests/integration/test_openmm/test_proc_pool_mapper.py b/tests/integration/test_openmm/test_proc_pool_mapper.py deleted file mode 100644 index b884e70f..00000000 --- a/tests/integration/test_openmm/test_proc_pool_mapper.py +++ /dev/null @@ -1,84 +0,0 @@ -import functools -import openmm -import numpy as np -from wepy.runners.openmm import OpenMMRunner, OpenMMState, gen_sim_state -from wepy.work_mapper.proc_pool_mapper import ProcPoolMapper - -from wepy_tools.systems.lennard_jones import LennardJonesPair - - -def test_proc_pool_mapper(): - - lj_sys = LennardJonesPair() - integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) - - - runner = OpenMMRunner( - system=lj_sys.system, - topology=lj_sys.topology, - integrator=integrator, - ) - - num_walkers = 4 - - walker_states = [ - OpenMMState(gen_sim_state( - lj_sys.positions, - system=lj_sys.system, - integrator=integrator, - )) - for _ - in range(num_walkers) - ] - - run_func = functools.partial( - runner.run_segment, - platform="Reference", - ) - - poolmapper = ProcPoolMapper() - poolmapper.init(run_func, num_workers=2) - - results = poolmapper.map( - walker_states, - [10 for _ in range(num_walkers)], - ) - assert len(results) == 4 - - # try different platform with some global platform settings - run_func = functools.partial( - runner.run_segment, - platform="CPU", - platform_kwargs={"Threads" : "1"}, - ) - - poolmapper = ProcPoolMapper() - poolmapper.init(run_func, num_workers=2) - - poolmapper.map( - walker_states, - [10 for _ in range(num_walkers)], - ) - - # Different platform args per "worker" - - run_func = functools.partial( - runner.run_segment, - platform="CPU", - ) - - poolmapper = ProcPoolMapper() - poolmapper.init( - run_func, - num_workers=2, - worker_args=[ - {"platform_kwargs" : {"Threads" : "2"}}, - {"platform_kwargs" : {"Threads" : "3"}}, - ] - ) - - poolmapper.map( - walker_states, - [10 for _ in range(num_walkers)], - ) - diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py new file mode 100644 index 00000000..98c3d146 --- /dev/null +++ b/tests/integration/test_openmm/test_realistic.py @@ -0,0 +1,99 @@ +"""Tests for a realistic end to end use case. + +OpenMM Runner, REVO and WExplore resamplers, paralell work mappers. + +Configurable platforms. + +""" +import copy +import openmm +import psutil + +import mdtraj +# TODO: use the high-level API imports +from wepy.walker import Walker +from wepy.runners.openmm import OpenMMRunner, OpenMMState +from wepy.sim_manager import Manager +from wepy.work_mapper.openmm import OpenMMProcPoolWorkMapperFactory +from wepy.resampling.resamplers.revo import REVOResampler +from wepy.util.mdtraj import mdtraj_to_json_topology + +from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideExplicit, AlanineDipeptideRamachandranDistance + + +def test_alanine_dipeptide_revo_procpool(): + + ala_sys = AlanineDipeptideExplicit() + + integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + + runner = OpenMMRunner( + system=ala_sys.system, + topology=ala_sys.topology, + integrator=integrator, + ) + + num_walkers = 4 + + init_state = OpenMMState.from_dwim( + positions=ala_sys.positions, + ) + + # TODO: remove the need to deepcopy and have the components make + # their own copies if necessary + walker_states = [ + copy.deepcopy(init_state) + for _ + in range(num_walkers) + ] + + init_walker_weight = 1 / num_walkers + init_walkers = [ + Walker( + state=walker_state, + weight=init_walker_weight, + ) + for walker_state + in walker_states + ] + + # number of walkers if less then total cores, otherwise the total + # number of cores + num_cores = len(psutil.Process().cpu_affinity()) + if num_cores < num_walkers: + num_workers = num_cores + cores_per_worker = 1 + else: + num_workers = num_walkers + cores_per_worker = (num_workers // num_walkers) + + json_top = mdtraj_to_json_topology( + mdtraj.Topology.from_openmm(ala_sys.topology) + ) + + distance_metric = AlanineDipeptideRamachandranDistance(json_top) + + resampler = REVOResampler( + merge_dist=4, + char_dist=0.1, + distance=distance_metric, + # DEBUG + # num_proc=num_workers, + num_proc=1, + ) + + sim_manager = Manager( + init_walkers=init_walkers, + runner=runner, + resampler=resampler, + work_mapper_factory=OpenMMProcPoolWorkMapperFactory( + platform="CPU", + num_procs=num_workers, + global_platform_properties={"Threads" : str(cores_per_worker)}, + ), + ) + + new_walkers, sim_components = sim_manager.run_simulation( + n_cycles=1, + segment_lengths=100, + ) diff --git a/tests/integration/test_openmm/test_serial_mapper.py b/tests/integration/test_openmm/test_serial_mapper.py deleted file mode 100644 index c8ccc39e..00000000 --- a/tests/integration/test_openmm/test_serial_mapper.py +++ /dev/null @@ -1,163 +0,0 @@ -from typing import Literal -import functools -import openmm - -import attrs - -from wepy.runners.openmm import OpenMMRunner, OpenMMState, gen_sim_state, PlatformKwargs -from wepy.work_mapper.serial import SerialMapper -from wepy.work_mapper.base import Task - -from wepy_tools.systems.lennard_jones import LennardJonesPair - -@attrs.define -class OpenMMTask(Task): - - runner: OpenMMRunner - segment_length: int - getState_kwargs: dict[str, bool] | None = None - - def __call__( - self, - state: OpenMMState, - platform: str | type(Ellipsis) | None = None, - platform_kwargs: PlatformKwargs | None = None, - ) -> OpenMMState: - - self.runner.run_segment( - state, - segment_length=self.segment_length, - getState_kwargs=self.getState_kwargs, - platform=platform, - platform_kwargs=platform_kwargs - ) - -def test_serial_mapper(): - - lj_sys = LennardJonesPair() - integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) - - - runner = OpenMMRunner( - system=lj_sys.system, - topology=lj_sys.topology, - integrator=integrator, - ) - - num_walkers = 4 - - walker_states = [ - OpenMMState(gen_sim_state( - lj_sys.positions, - system=lj_sys.system, - integrator=integrator, - )) - for _ - in range(num_walkers) - ] - - mapper = SerialMapper() - mapper.init() - - mapper.map( - [ - OpenMMTask( - runner=runner, - segment_length=10, - ) - for _ - in range(num_walkers) - ], - walker_states, - ) - -class OpenMMWorkerTask(Task): - - runner: OpenMMRunner - segment_length: int - getState_kwargs: dict[str, bool] | None = None - platform: str | type(Ellipsis) | None = None - platform_kwargs: PlatformKwargs | None = None - - def __call__(self, state: OpenMMState) -> OpenMMState: - - self.runner.run_segment( - state, - segment_length=self.segment_length, - getState_kwargs=self.getState_kwargs, - platform=self.platform, - platform_kwargs=self.platform_kwargs - ) - -OpenMMPlatformName = Literal[ - "Reference", - "CPU", - "OpenCL", - "CUDA", - "HIP", -] - -class OpenMMPlatformSpec: - name: OpenMMPlatformName - properties: dict[str, str] - -class OpenMMWorkerSpec: - platform: OpenMMPlatformSpec - -class OpenMMSerialMapper(SerialMapper): - - def __init__( - self, - worker_specs: dict[int, OpenMMWorkerSpec]| None = None, - ) -> None: - - self._worker_segment_times: dict[int, list[float]] = {0: []} - self._worker_specs = worker_specs - - - def gen_task(self, outer_task: OpenMMTask, task_idx: int) -> OpenMMWorkerTask: - - return OpenMMWorkerTask( - runner=outer_task.runner, - segment_length=outer_task.segment_length, - getState_kwargs=outer_task.getState_kwargs, - platform - ) - -def test_serial_devices(): - - lj_sys = LennardJonesPair() - integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) - - runner = OpenMMRunner( - system=lj_sys.system, - topology=lj_sys.topology, - integrator=integrator, - ) - - num_walkers = 4 - - walker_states = [ - OpenMMState(gen_sim_state( - lj_sys.positions, - system=lj_sys.system, - integrator=integrator, - )) - for _ - in range(num_walkers) - ] - - mapper = OpenMMSerialMapper() - mapper.init() - - mapper.map( - [ - OpenMMTask( - runner=runner, - segment_length=10, - ) - for _ - in range(num_walkers) - ], - walker_states, - ) diff --git a/tests/integration/test_openmm/test_sim_manager.py b/tests/integration/test_openmm/test_sim_manager.py index ec537db6..ccce788e 100644 --- a/tests/integration/test_openmm/test_sim_manager.py +++ b/tests/integration/test_openmm/test_sim_manager.py @@ -1,4 +1,5 @@ -import functools +import logging +import pytest import psutil import openmm from wepy.walker import Walker @@ -19,7 +20,6 @@ def test_serial_mapper(): lj_sys = LennardJonesPair() integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) - runner = OpenMMRunner( system=lj_sys.system, topology=lj_sys.topology, @@ -89,7 +89,7 @@ def test_serial_mapper(): n_cycles=1, segment_lengths=10000000000, ) - + def test_proc_pool_mapper(): lj_sys = LennardJonesPair() From d9ff9cec7e275cb95e9856e8e4a84e128160d33a Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 5 Dec 2025 20:15:07 -0500 Subject: [PATCH 054/143] implement OpenMM logging reporter bases Includes abstract interfaces to document them (which is otherwise a bit confusing). Also implements two reusable loggers that trigger based on step counts or sampling times. The logging function is then a callback that can be customized. --- src/wepy/runners/openmm/logger.py | 143 +++++++ src/wepy/runners/openmm/reporter.py | 57 +++ .../test_runners/test_openmm/test_logger.py | 365 ++++++++++++++++++ 3 files changed, 565 insertions(+) create mode 100644 src/wepy/runners/openmm/logger.py create mode 100644 src/wepy/runners/openmm/reporter.py create mode 100644 tests/unit/test_runners/test_openmm/test_logger.py diff --git a/src/wepy/runners/openmm/logger.py b/src/wepy/runners/openmm/logger.py new file mode 100644 index 00000000..bf12bf21 --- /dev/null +++ b/src/wepy/runners/openmm/logger.py @@ -0,0 +1,143 @@ +import logging +from typing import Callable +from collections.abc import Collection +import openmm.app +import openmm +import openmm.unit + +from .reporter import ( + OpenMMReporter, + OpenMMGetStateKeys, + OpenMMReporterNextReport, +) + +LoggingReporterCallback = Callable[ + [ + logging.Logger, + openmm.app.Simulation, + openmm.State, + ], + None, +] + +class LoggingReporter(OpenMMReporter): + logger: logging.Logger + callback: LoggingReporterCallback + state_includes: list[OpenMMGetStateKeys] + + def __init__( + self, + logger: logging.Logger, + callback: LoggingReporterCallback, + state_includes: Collection[OpenMMGetStateKeys], + ) -> None: + + self.logger = logger + self.callback = callback + self.state_includes = list(state_includes) + + def report( + self, + simulation: openmm.app.Simulation, + state: openmm.State, + ) -> None: + + self.callback( + self.logger, + simulation, + state, + ) + + +class StepIntervalLoggingReporter(LoggingReporter): + """Reporter that reports at intervals in steps.""" + + logger: logging.Logger + callback: LoggingReporterCallback + state_includes: list[OpenMMGetStateKeys] + step_interval: int + + def __init__( + self, + logger: logging.Logger, + callback: LoggingReporterCallback, + state_includes: Collection[OpenMMGetStateKeys], + step_interval: int, + ) -> None: + + super().__init__( + logger=logger, + callback=callback, + state_includes=state_includes, + ) + self.step_interval = step_interval + + def describeNextReport( + self, simulation: openmm.app.Simulation + ) -> OpenMMReporterNextReport: + + steps_left = self.step_interval - simulation.currentStep % self.step_interval + + return OpenMMReporterNextReport( + steps=steps_left, + include=list(self.state_includes), + periodic=False, + ) + + +class SamplingTimeIntervalLoggingReporter(LoggingReporter): + """Reporter that reports at intervals in sampling time. + + Does not work for variable step integrators currently. + + """ + + logger: logging.Logger + callback: LoggingReporterCallback + state_includes: list[OpenMMGetStateKeys] + step_size: openmm.unit.Quantity + sampling_time_interval: openmm.unit.Quantity + + def __init__( + self, + logger: logging.Logger, + callback: LoggingReporterCallback, + state_includes: Collection[OpenMMGetStateKeys], + step_size: openmm.unit.Quantity, + sampling_time_interval: openmm.unit.Quantity, + ) -> None: + + super().__init__(logger, callback, state_includes) + self.sampling_time_interval = sampling_time_interval + self.step_size = step_size + + def describeNextReport( + self, simulation: openmm.app.Simulation + ) -> OpenMMReporterNextReport: + + _unit = openmm.unit.attosecond + + curr_sampling_time: openmm.unit.Quantity = simulation.context.getTime() + + # avg_step_time: openmm.unit.Quantity = curr_sampling_time / simulation.currentStep + + sampling_time_left = ( + self.sampling_time_interval.value_in_unit(_unit) - ( + curr_sampling_time.value_in_unit(_unit) + % self.sampling_time_interval.value_in_unit(_unit) + ) + ) * _unit + + if sampling_time_left < (0. * _unit): + estimated_steps_left = 0 + + else: + estimated_steps_left = ( + round(sampling_time_left.value_in_unit(_unit)) // round(self.step_size.value_in_unit(_unit)) + ) + + return OpenMMReporterNextReport( + steps=estimated_steps_left, + include=list(self.state_includes), + periodic=False, + ) diff --git a/src/wepy/runners/openmm/reporter.py b/src/wepy/runners/openmm/reporter.py new file mode 100644 index 00000000..ecee4060 --- /dev/null +++ b/src/wepy/runners/openmm/reporter.py @@ -0,0 +1,57 @@ +"""OpenMM reporters used in wepy.""" + +# Standard Library +import abc +import logging +from collections.abc import Callable, Collection +from typing import Literal, NotRequired, TypedDict, get_args + +# Third Party Library +import openmm as omm +import openmm.app as omma +import openmm.unit as unit + +logger = logging.getLogger(__name__) + +class OpenMMReporterNextReport(TypedDict): + + steps: int + include: list[str] + periodic: NotRequired[bool | None] = False + + +OpenMMGetStateKeys = Literal[ + "positions", + "velocities", + "forces", + "energy", + "parameters", + "parameterDerivatives", + "integratorParameters", +] +OPENMM_GET_STATE_KEYS: frozenset[OpenMMGetStateKeys] = frozenset( + get_args(OpenMMGetStateKeys) +) + + + +class OpenMMReporter(metaclass=abc.ABCMeta): + """ABC for openmm.app Reporter. + + Documents the interface for openmm.app Reporter compatible classes. + """ + + def describeNextReport( + self, simulation: omma.Simulation + ) -> OpenMMReporterNextReport: + raise NotImplementedError + + def report( + self, + simulation: omma.Simulation, + state: omm.State, + ) -> None: + + raise NotImplementedError + + diff --git a/tests/unit/test_runners/test_openmm/test_logger.py b/tests/unit/test_runners/test_openmm/test_logger.py new file mode 100644 index 00000000..ae1f8f58 --- /dev/null +++ b/tests/unit/test_runners/test_openmm/test_logger.py @@ -0,0 +1,365 @@ +import logging +import copy +import pytest +import openmm +import openmm.app +import openmm.unit +from wepy.runners.openmm.state import OpenMMState +from wepy.runners.openmm.reporter import OpenMMReporterNextReport +from wepy.runners.openmm.logger import LoggingReporter, StepIntervalLoggingReporter, SamplingTimeIntervalLoggingReporter +from wepy_tools.systems.lennard_jones import LennardJonesPair + +STEP_TIME = 1 * openmm.unit.femtosecond + +@pytest.fixture(scope="function") +def sim_components() -> tuple[ + openmm.State, + openmm.app.Topology, + openmm.System, + openmm.LangevinIntegrator, + openmm.Platform, +]: + + lj_sys = LennardJonesPair() + integrator = openmm.VerletIntegrator(STEP_TIME) + omm_state = OpenMMState.from_dwim(positions=lj_sys.positions).to_state_wrapper().state + + + platform = openmm.Platform.getPlatformByName("Reference") + + return omm_state, lj_sys.topology, lj_sys.system, integrator, platform + + + +class Test_LoggingReporter: + + def test_report(self, caplog): + + logger = logging.getLogger("test-LoggingReporter") + + def hello_log( + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, + ) -> None: + + logger.info("Hello") + + hello_log_reporter = LoggingReporter( + logger, + callback=hello_log, + state_includes=["energy"], + ) + + # NOTE: dummy inputs since they aren't used in the hello_log callback + with caplog.at_level(logging.INFO, logger="test-LoggingReporter"): + hello_log_reporter.report(None, None) + + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "INFO" + assert caplog.records[0].msg == "Hello" + + caplog.clear() + +class Test_StepIntervalLoggingReporter: + + def test_describeNextReport(self, sim_components): + + omm_state, sim_args = sim_components[0], sim_components[1:] + + logger = logging.getLogger("test-StepIntervalLoggingReporter") + + def hello_log( + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, + ) -> None: + + logger.info("Hello") + + state_includes = ["energy"] + + step_logger = StepIntervalLoggingReporter( + logger, + callback=hello_log, + state_includes=state_includes, + step_interval=10, + ) + + + simulation = openmm.app.Simulation( + *sim_args + ) + simulation.context.setState(omm_state) + + assert step_logger.describeNextReport( + simulation + ) == OpenMMReporterNextReport( + steps=10, + include=list(state_includes), + periodic=False, + ) + + simulation.step(1) + assert step_logger.describeNextReport( + simulation + ) == OpenMMReporterNextReport( + steps=9, + include=list(state_includes), + periodic=False, + ) + + simulation.step(2) + assert step_logger.describeNextReport( + simulation + ) == OpenMMReporterNextReport( + steps=7, + include=list(state_includes), + periodic=False, + ) + + # wraps back around at 0 + simulation.step(7) + assert step_logger.describeNextReport( + simulation + ) == OpenMMReporterNextReport( + steps=10, + include=list(state_includes), + periodic=False, + ) + + + def test_simulation(self, sim_components, caplog): + omm_state, sim_args = sim_components[0], sim_components[1:] + + simulation = openmm.app.Simulation( + *sim_args + ) + simulation.context.setState(omm_state) + + logger_name = "test-StepIntervalLoggingReporter" + logger = logging.getLogger(logger_name) + + def hello_log( + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, + ) -> None: + + logger.info("Hello") + + state_includes = ["energy"] + + step_logger = StepIntervalLoggingReporter( + logger, + callback=hello_log, + state_includes=state_includes, + step_interval=10, + ) + + simulation.reporters.append(step_logger) + + with caplog.at_level(logging.INFO, logger_name): + simulation.step(1) + + assert len(caplog.records) == 0 + caplog.clear() + + with caplog.at_level(logging.INFO, logger_name): + simulation.step(9) + + assert len(caplog.records) == 1 + assert caplog.records[0].msg == "Hello" + + caplog.clear() + + +class Test_SamplingTimeIntervalLoggingReporter: + + def test_describeNextReport(self, sim_components): + + omm_state, sim_args = sim_components[0], sim_components[1:] + topology, system, integrator, platform = sim_args + + + logger = logging.getLogger("test-SamplingTimeIntervalLoggingReporter") + + def hello_log( + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, + ) -> None: + + logger.info("Hello") + + state_includes = ["energy"] + + step_logger = SamplingTimeIntervalLoggingReporter( + logger, + callback=hello_log, + state_includes=state_includes, + step_size=STEP_TIME, + sampling_time_interval=(10 * openmm.unit.femtosecond), + ) + + simulation = openmm.app.Simulation( + topology, + system, + copy.deepcopy(integrator), + platform, + ) + simulation.context.setState(omm_state) + + # at step 0 returns the interval + assert step_logger.describeNextReport( + simulation + ) == OpenMMReporterNextReport( + steps=10, + include=list(state_includes), + periodic=False, + ) + + simulation.step(1) + assert step_logger.describeNextReport( + simulation + ) == OpenMMReporterNextReport( + steps=9, + include=list(state_includes), + periodic=False, + ) + + simulation.step(2) + assert step_logger.describeNextReport( + simulation + ) == OpenMMReporterNextReport( + steps=7, + include=list(state_includes), + periodic=False, + ) + + simulation.step(7) + assert step_logger.describeNextReport( + simulation + ) == OpenMMReporterNextReport( + steps=10, + include=list(state_includes), + periodic=False, + ) + + simulation.step(3) + assert step_logger.describeNextReport( + simulation + ) == OpenMMReporterNextReport( + steps=7, + include=list(state_includes), + periodic=False, + ) + + simulation = openmm.app.Simulation( + topology, + system, + copy.deepcopy(integrator), + platform, + ) + simulation.context.setState(omm_state) + + simulation.step(10) + assert step_logger.describeNextReport( + simulation + ) == OpenMMReporterNextReport( + steps=10, + include=list(state_includes), + periodic=False, + ) + + # test wrapping around behavior + simulation = openmm.app.Simulation( + topology, + system, + copy.deepcopy(integrator), + platform, + ) + simulation.context.setState(omm_state) + + simulation.step(11) + assert step_logger.describeNextReport( + simulation + ) == OpenMMReporterNextReport( + steps=9, + include=list(state_includes), + periodic=False, + ) + + + def test_simulation(self, sim_components, caplog): + omm_state, sim_args = sim_components[0], sim_components[1:] + + topology, system, integrator, platform = sim_args + + logger_name = "test-SamplingTimeIntevalLoggingReporter" + logger = logging.getLogger(logger_name) + + def hello_log( + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, + ) -> None: + + logger.info("Hello") + + state_includes = ["energy"] + + time_logger = SamplingTimeIntervalLoggingReporter( + logger, + callback=hello_log, + state_includes=state_includes, + step_size=STEP_TIME, + sampling_time_interval=(10 * openmm.unit.femtosecond), + ) + + simulation = openmm.app.Simulation( + topology, + system, + copy.deepcopy(integrator), + platform, + ) + simulation.context.setState(omm_state) + simulation.reporters.append(time_logger) + + with caplog.at_level(logging.INFO, logger_name): + simulation.step(1) + + assert len(caplog.records) == 0 + caplog.clear() + + with caplog.at_level(logging.INFO, logger_name): + simulation.step(9) + + assert len(caplog.records) == 1 + assert caplog.records[0].msg == "Hello" + + caplog.clear() + + # test something longer + time_logger = SamplingTimeIntervalLoggingReporter( + logger, + callback=hello_log, + state_includes=state_includes, + step_size=STEP_TIME, + sampling_time_interval=(2 * openmm.unit.femtosecond), + ) + + simulation = openmm.app.Simulation( + topology, + system, + copy.deepcopy(integrator), + platform, + ) + simulation.context.setState(omm_state) + simulation.reporters.append(time_logger) + + with caplog.at_level(logging.INFO, logger_name): + simulation.step(10) + + assert len(caplog.records) == 5 + caplog.clear() From 85b8f238d0452b8eb790625f31476bd01f0e6833 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 5 Dec 2025 20:31:24 -0500 Subject: [PATCH 055/143] adds heart beat logger just reports steps every interval. Meant to just be a quick sign that the simulation is still running and making progress. Preconfigured and you only need the interval. --- src/wepy/runners/openmm/logger.py | 50 +++++++++++ .../test_runners/test_openmm/test_logger.py | 82 ++++++++++++++++++- 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/src/wepy/runners/openmm/logger.py b/src/wepy/runners/openmm/logger.py index bf12bf21..a99f86d6 100644 --- a/src/wepy/runners/openmm/logger.py +++ b/src/wepy/runners/openmm/logger.py @@ -4,6 +4,7 @@ import openmm.app import openmm import openmm.unit +import attrs from .reporter import ( OpenMMReporter, @@ -20,6 +21,7 @@ None, ] + class LoggingReporter(OpenMMReporter): logger: logging.Logger callback: LoggingReporterCallback @@ -48,6 +50,7 @@ def report( state, ) +LoggingReporterFactory = Callable[[logging.Logger], LoggingReporter] class StepIntervalLoggingReporter(LoggingReporter): """Reporter that reports at intervals in steps.""" @@ -141,3 +144,50 @@ def describeNextReport( include=list(self.state_includes), periodic=False, ) + + +class HeartBeatLoggingReporter(StepIntervalLoggingReporter): + + def __init__( + self, + logger: logging.Logger, + step_interval: int + ) -> None: + + super().__init__( + logger=logger, + callback=self.logging_callback, + state_includes=[], + step_interval=step_interval, + ) + + @staticmethod + def logging_callback( + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, + ) -> None: + + # TODO: make this adaptive to reduce zeros etc. + sim_time = simulation.context.getTime() + sim_time_mag = sim_time.value_in_unit(openmm.unit.picosecond) + sim_steps = simulation.context.getStepCount() + + logger.info(f"OpenMM simulation progress: sim_time={sim_time_mag:.4f} ps, sim_steps={sim_steps}") + + + + +@attrs.define +class HeartBeatLoggingReporterFactory: + + step_interval: int + + def __call__(self, logger: logging.Logger) -> HeartBeatLoggingReporter: + + return HeartBeatLoggingReporter( + logger=logger, + step_interval=self.step_interval, + ) +# TODO: +# class EnergyLoggingReporter() diff --git a/tests/unit/test_runners/test_openmm/test_logger.py b/tests/unit/test_runners/test_openmm/test_logger.py index ae1f8f58..128d5930 100644 --- a/tests/unit/test_runners/test_openmm/test_logger.py +++ b/tests/unit/test_runners/test_openmm/test_logger.py @@ -6,7 +6,12 @@ import openmm.unit from wepy.runners.openmm.state import OpenMMState from wepy.runners.openmm.reporter import OpenMMReporterNextReport -from wepy.runners.openmm.logger import LoggingReporter, StepIntervalLoggingReporter, SamplingTimeIntervalLoggingReporter +from wepy.runners.openmm.logger import ( + LoggingReporter, + StepIntervalLoggingReporter, + SamplingTimeIntervalLoggingReporter, + HeartBeatLoggingReporter, +) from wepy_tools.systems.lennard_jones import LennardJonesPair STEP_TIME = 1 * openmm.unit.femtosecond @@ -363,3 +368,78 @@ def hello_log( assert len(caplog.records) == 5 caplog.clear() + +class Test_HeartBeatLoggingReporter: + + def test_logging_callback(self, sim_components, caplog): + + omm_state, sim_args = sim_components[0], sim_components[1:] + + topology, system, integrator, platform = sim_args + + logger_name = "test-HeartBeatLoggingReporter" + logger = logging.getLogger(logger_name) + + time_logger = HeartBeatLoggingReporter( + logger, + step_interval=2, + ) + + simulation = openmm.app.Simulation( + topology, + system, + copy.deepcopy(integrator), + platform, + ) + simulation.context.setState(omm_state) + + with caplog.at_level(logging.INFO, logger_name): + HeartBeatLoggingReporter.logging_callback( + logger, + simulation, + omm_state, + ) + + assert len(caplog.records) == 1 + + + def test_simulation(self, sim_components, caplog): + + omm_state, sim_args = sim_components[0], sim_components[1:] + + topology, system, integrator, platform = sim_args + + logger_name = "test-HeartBeatLoggingReporter" + logger = logging.getLogger(logger_name) + + time_logger = HeartBeatLoggingReporter( + logger, + step_interval=2, + ) + + simulation = openmm.app.Simulation( + topology, + system, + copy.deepcopy(integrator), + platform, + ) + simulation.context.setState(omm_state) + simulation.reporters.append(time_logger) + + with caplog.at_level(logging.INFO, logger_name): + simulation.step(1) + + assert len(caplog.records) == 0 + caplog.clear() + + with caplog.at_level(logging.INFO, logger_name): + simulation.step(1) + + assert len(caplog.records) == 1 + caplog.clear() + + with caplog.at_level(logging.INFO, logger_name): + simulation.step(10) + + assert len(caplog.records) == 5 + caplog.clear() From ee5f73b6e96c4e5b0576f8c4644949d9ab44529c Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sat, 6 Dec 2025 11:17:30 -0500 Subject: [PATCH 056/143] add support in OpenMM runner for simulation reporters - Pass in reporter factories to allow for later injection of the logger to use - `init` hook to initialize reporters from factories - default loggers for OpenMMRunner --- src/wepy/runners/openmm/runner.py | 21 +++++- .../test_runners/test_openmm/test_runner.py | 67 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index cf3d13b0..77761d4e 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -1,5 +1,5 @@ # Standard Library -from typing import Any, Annotated, TypedDict, NotRequired, Literal, Final, Self, get_args, TypeAlias +from typing import Any, Annotated, TypedDict, NotRequired, Literal, Final, Self, get_args, TypeAlias, Callable import logging import multiprocessing as mp import itertools @@ -12,6 +12,8 @@ import attrs import numpy as np +from .reporter import OpenMMReporter + logger = logging.getLogger(__name__) try: @@ -34,6 +36,7 @@ from wepy.util.util import box_vectors_to_lengths_angles from wepy.walker import WalkerState from .state import OpenMMState, OpenMMStateWrapper, get_context_state +from .logger import HeartBeatLoggingReporterFactory, LoggingReporterFactory PlatformKwargs = dict[str, str] @@ -59,6 +62,11 @@ class OpenMMRunnerSegmentSplitTimes(TypedDict): "box_volume", }) +DEFAULT_OPENMM_REPORTER_FACTORIES = [ + # default heart beat every 500 steps + HeartBeatLoggingReporterFactory(step_interval=500), +] + # the runner for the simulation which runs the actual dynamics @attrs.define class OpenMMRunner(Runner): @@ -73,9 +81,16 @@ class OpenMMRunner(Runner): global_platform_kwargs: PlatformKwargs | None = None enforce_box: bool = False get_state_keys: frozenset[str] = attrs.field(default=GET_STATE_DEFAULT_KEYS) + openmm_reporter_factories: list[LoggingReporterFactory] = attrs.field(default=DEFAULT_OPENMM_REPORTER_FACTORIES) + _openmm_reporters: list[OpenMMReporter] = attrs.field(default=[]) _last_cycle_segments_split_times: list[OpenMMRunnerSegmentSplitTimes] = attrs.field(default=[]) + def init(self) -> None: + + for omm_reporter_factory in self.openmm_reporter_factories: + self._openmm_reporters.append(omm_reporter_factory(logger)) + def pre_cycle( self, ) -> None: @@ -155,6 +170,7 @@ def run_segment( ) # make a new simulation object + logger.info("Construction Simulation and context") simulation = openmm.app.Simulation( self.topology, self.system, new_integrator, platform ) @@ -166,6 +182,9 @@ def run_segment( self.topology, self.system, new_integrator ) + logger.info("Registering OpenMM Simulation reporters") + simulation.reporters.extend(self._openmm_reporters) + # generate a sim state logger.info("Generating openmm.State from input OpenMMState") state_wrapper = walker_state.to_state_wrapper() diff --git a/tests/unit/test_runners/test_openmm/test_runner.py b/tests/unit/test_runners/test_openmm/test_runner.py index 4264ec08..a081cf33 100644 --- a/tests/unit/test_runners/test_openmm/test_runner.py +++ b/tests/unit/test_runners/test_openmm/test_runner.py @@ -1,3 +1,4 @@ +import logging from wepy_tools.systems.lennard_jones import LennardJonesPair from wepy.runners.openmm.state import ( dummy_context, @@ -8,6 +9,7 @@ OpenMMRunner, ) +from wepy.runners.openmm.logger import StepIntervalLoggingReporter import pytest import numpy as np @@ -51,6 +53,20 @@ def test___init__(self, runner_components): *runner_components ) + def test_init(self, runner_components): + + runner = OpenMMRunner( + *runner_components + ) + + assert runner._openmm_reporters == [] + + runner.init() + + # check the default openmm reporters were constructed + assert len(runner._openmm_reporters) > 0 + + def test_pre_cycle(self, runner_components): runner = OpenMMRunner( @@ -101,3 +117,54 @@ def test_run_segment(self, runner_components): ) assert new_state["positions"] is not None assert "velocities" not in new_state + + # test that openmm reporters are being called + class Spy: + def __init__(self) -> None: + self.touched = False + + def touch(self) -> None: + self.touched = True + + SPY = Spy() + + class TouchGlobalStepIntervalLoggingReporter(StepIntervalLoggingReporter): + + def __init__( + self, + logger: logging.Logger, + ) -> None: + + self.spy = SPY + + super().__init__( + logger=logger, + callback=self.touch, + state_includes=[], + # NOTE: hardcoded + step_interval=1, + ) + + def touch(self, *args) -> None: + self.spy.touch() + + + def _mock_factory(logger: logging.Logger) -> TouchGlobalStepIntervalLoggingReporter: + return TouchGlobalStepIntervalLoggingReporter(logger=logger) + + runner = OpenMMRunner( + system=system, + topology=topology, + integrator=integrator, + get_state_keys={}, + openmm_reporter_factories=[_mock_factory], + ) + runner.init() + + assert not SPY.touched + new_state = runner.run_segment( + state, + 2, + ) + + assert SPY.touched From 021250d522885a3475a2862362aa4d6ded1f16b3 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sat, 6 Dec 2025 11:31:26 -0500 Subject: [PATCH 057/143] add support for Runner.init in sim manager lifecycle --- src/wepy/runners/mock.py | 40 +++++----------------------- src/wepy/runners/runner.py | 5 ++++ src/wepy/sim_manager.py | 6 +++-- tests/unit/test_runners/test_mock.py | 24 ++++++----------- tests/unit/test_sim_manager.py | 14 +++++----- 5 files changed, 30 insertions(+), 59 deletions(-) diff --git a/src/wepy/runners/mock.py b/src/wepy/runners/mock.py index 5423c1ef..6a03dd1f 100644 --- a/src/wepy/runners/mock.py +++ b/src/wepy/runners/mock.py @@ -1,11 +1,10 @@ """Realistic mock runners useful mostly for testing.""" import logging +from typing import Literal import attrs -from wepy.interface import Task, RunnerGenTaskArgs from wepy.walker import Walker, WalkerState from wepy.runners.runner import Runner -from wepy.work_mapper.base import Task logger = logging.getLogger(__name__) @@ -17,51 +16,27 @@ class MockError(Exception): pass @attrs.define -class MockTask(Task): - - runner: "MockRunner" - segment_length: int - fail: bool +class MockRunner(Runner): - def __call__(self, state: MockState) -> MockState: - logger.info("Running MockTask segment") - return self.runner.run_segment(state, self.segment_length, self.fail) + fail: bool = False -@attrs.define -class MockRunner(Runner): + _initialized: bool = False - fail_walker_idxs: set[int] = attrs.field(default={}) + def init(self) -> None: + self._initialized = True def pre_cycle(self) -> None: pass - def post_cycle(self) -> None: pass - def gen_tasks(self, segment_spec: RunnerGenTaskArgs[MockState]) -> list[MockTask]: - - return [ - MockTask( - runner=self, - segment_length=segment_spec.segment_length, - fail=( - True - if walker_idx in self.fail_walker_idxs - else False - ) - ) - for walker_idx, state - in enumerate(segment_spec.states) - ] - def run_segment( self, state: MockState, segment_length: int, - fail: bool, ) -> MockState: - if fail: + if self.fail: logger.critical("Error requested in MockRuner.run_segment, raising.") raise MockError("Error requested") @@ -73,4 +48,3 @@ def run_segment( def get_last_cycle_segments_split_times(self) -> None: return None - diff --git a/src/wepy/runners/runner.py b/src/wepy/runners/runner.py index c44afd9e..a1dea3ee 100644 --- a/src/wepy/runners/runner.py +++ b/src/wepy/runners/runner.py @@ -27,6 +27,8 @@ class Runner(Protocol[WalkerState_]): """Abstract base class for the Runner interface.""" + def init(self) -> None: ... + def pre_cycle(self) -> None: """Perform pre-cycle behavior. run_segment will be called for each walker so this allows you to perform changes of state on a @@ -84,6 +86,9 @@ class NoRunner(Runner): May be useful for testing. """ + def init(self) -> None: + pass + def pre_cycle(self) -> None: pass def post_cycle(self) -> None: diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index 82399372..284ec68b 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -125,7 +125,6 @@ class Manager(Generic[State_]): reporters: list[Reporter] monitor: Monitor | None - REPORT_ITEM_KEYS: Final[tuple[str, ...]] = ( "cycle_idx", "n_segment_steps", @@ -266,7 +265,10 @@ def init( """ - logger.info("Starting simulation") + logger.info("Running sim_manager.init hooks") + + logger.info("Running runner.init hook") + self.runner.init() # initialize the monitoring object diff --git a/tests/unit/test_runners/test_mock.py b/tests/unit/test_runners/test_mock.py index 4cafc76a..dc4ffb77 100644 --- a/tests/unit/test_runners/test_mock.py +++ b/tests/unit/test_runners/test_mock.py @@ -1,23 +1,15 @@ import pytest -from wepy.runners.mock import MockRunner, MockState, MockTask, MockError - -def test_MockTask(): - - assert MockTask( - MockRunner(), - 10, - fail=False, - )(MockState(10)) == MockState(20) - - with pytest.raises(MockError): - MockTask( - MockRunner(), - 10, - fail=True, - )(MockState(10)) +from wepy.runners.mock import MockRunner, MockState, MockError class TestMockRunner: + def test_init(self): + + runner = MockRunner() + assert not runner._initialized + runner.init() + assert runner._initialized + def test_pre_cycle(self): MockRunner().pre_cycle() diff --git a/tests/unit/test_sim_manager.py b/tests/unit/test_sim_manager.py index 666d1cbb..366d5779 100644 --- a/tests/unit/test_sim_manager.py +++ b/tests/unit/test_sim_manager.py @@ -36,15 +36,17 @@ def test___init__(self, sim_components): manager = Manager(*sim_components) assert len(manager.reporters) == 0 - assert manager.work_mapper_class == SerialMapper + assert manager.work_mapper_factory == SerialMapper assert not hasattr(manager, "work_mapper") def test_init(self, sim_components): manager = Manager(*sim_components) + assert not manager.runner._initialized manager.init() assert hasattr(manager, "work_mapper") + assert manager.runner._initialized def test_cleanup(self, sim_components): @@ -69,7 +71,7 @@ def test_run_segment(self, sim_components): # test if something fails manager = Manager( init_walkers, - MockRunner(fail_walker_idxs={0,}), + MockRunner(fail=True), resampler, ) manager.init() @@ -97,7 +99,7 @@ def test_run_cycle(self, sim_components): # test if something fails manager = Manager( init_walkers, - MockRunner(fail_walker_idxs={0,}), + MockRunner(fail=True), resampler, ) manager.init() @@ -114,13 +116,12 @@ def test_run_simulation(self, sim_components): manager = Manager(*sim_components) manager.init() - new_walkers, _ = manager.run_simulation(2, 2, num_workers=None) + new_walkers, _ = manager.run_simulation(2, 2) new_walkers, _ = manager.run_simulation( 2, 2, - num_workers=None, continue_run_idx=0, ) @@ -132,13 +133,11 @@ def test_run_simulation_by_time(self, sim_components): new_walkers, _ = manager.run_simulation_by_time( 0.001, 2, - num_workers=None, ) new_walkers, _ = manager.run_simulation_by_time( 0.001, 2, - num_workers=None, continue_run_idx=0, ) @@ -146,6 +145,5 @@ def test_run_simulation_by_time(self, sim_components): new_walkers, _ = manager.run_simulation_by_time( 0.0000001, 2, - num_workers=None, continue_run_idx=0, ) From 3e48d68e086771edb2722bdcf6fe3f9a4c9ff7c8 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 8 Dec 2025 11:26:08 -0500 Subject: [PATCH 058/143] export HeartBeatLoggingReporterFactory for openmm runner --- src/wepy/runners/openmm/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wepy/runners/openmm/__init__.py b/src/wepy/runners/openmm/__init__.py index 83bc1dc6..258c5273 100644 --- a/src/wepy/runners/openmm/__init__.py +++ b/src/wepy/runners/openmm/__init__.py @@ -8,8 +8,10 @@ state_to_xml, ) from .runner import OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS +from .logger import HeartBeatLoggingReporterFactory __all__ = [ + "HeartBeatLoggingReporterFactory", "GPU_PLATFORMS", "OpenMMPlatformName", "OpenMMRunner", From 53587009a08842e6c7e54359bd088df409782a93 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 8 Dec 2025 11:26:25 -0500 Subject: [PATCH 059/143] copy all components for sim manager This was needed because certain things get mutated so the sim manager must own all the components. --- src/wepy/sim_manager.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index 284ec68b..3f422933 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -45,11 +45,8 @@ # Standard Library import logging from typing import Final, Any, TypedDict, Generic, TypeVar, Callable - -logger = logging.getLogger(__name__) -# Standard Library +import copy import time -from copy import deepcopy # First Party Library from wepy.boundary_conditions.boundary import BoundaryConditions @@ -61,6 +58,8 @@ from wepy.work_mapper.serial import SerialMapper from wepy.monitor import Monitor +logger = logging.getLogger(__name__) + class CycleReportDict(TypedDict): cycle_idx: int new_walkers: list[Walker] @@ -194,21 +193,21 @@ def __init__( """ - self.init_walkers = init_walkers + self.init_walkers = copy.deepcopy(init_walkers) self.n_init_walkers = len(init_walkers) # the runner is the object that runs dynamics - self.runner = runner + self.runner = copy.deepcopy(runner) # the resampler - self.resampler = resampler + self.resampler = copy.deepcopy(resampler) # object for boundary conditions - self.boundary_conditions = boundary_conditions + self.boundary_conditions = copy.deepcopy(boundary_conditions) # the method for writing output if reporters is None: self.reporters = [] else: - self.reporters = reporters + self.reporters = copy.deepcopy(reporters) if work_mapper_factory is None: self.work_mapper_factory = SerialMapper @@ -699,7 +698,7 @@ def run_simulation( self.cleanup() logger.info("Simulation cleanup complete") - return walkers, deepcopy(tuple(filters)) + return walkers, copy.deepcopy(tuple(filters)) def run_simulation_by_time( self, From 0defc97fdde21780c7510cc737bd48c013a8f2ff Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 8 Dec 2025 11:27:02 -0500 Subject: [PATCH 060/143] add heart beat openmm simulation logger to test --- .../test_openmm/test_sim_manager.py | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/tests/integration/test_openmm/test_sim_manager.py b/tests/integration/test_openmm/test_sim_manager.py index ccce788e..b421fc38 100644 --- a/tests/integration/test_openmm/test_sim_manager.py +++ b/tests/integration/test_openmm/test_sim_manager.py @@ -2,8 +2,9 @@ import pytest import psutil import openmm +import openmm.unit from wepy.walker import Walker -from wepy.runners.openmm import OpenMMRunner, OpenMMState +from wepy.runners.openmm import OpenMMRunner, OpenMMState, HeartBeatLoggingReporterFactory from wepy.work_mapper.serial import SerialMapper from wepy.work_mapper.openmm import ( OpenMMSerialWorkMapperFactory, @@ -14,16 +15,27 @@ from wepy_tools.systems.lennard_jones import LennardJonesPair +STEP_SIZE = 2 * openmm.unit.femtosecond + def test_serial_mapper(): lj_sys = LennardJonesPair() - integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + integrator = openmm.LangevinIntegrator( + 300.0, + 0.1, + STEP_SIZE, + ) runner = OpenMMRunner( system=lj_sys.system, topology=lj_sys.topology, integrator=integrator, + # specialized reporters for testing + openmm_reporter_factories=[ + # heart beat every step + HeartBeatLoggingReporterFactory(step_interval=1) + ] ) num_walkers = 4 @@ -93,13 +105,20 @@ def test_serial_mapper(): def test_proc_pool_mapper(): lj_sys = LennardJonesPair() - integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) - + integrator = openmm.LangevinIntegrator( + 300.0, + 0.1, + STEP_SIZE, + ) runner = OpenMMRunner( system=lj_sys.system, topology=lj_sys.topology, integrator=integrator, + openmm_reporter_factories=[ + # heart beat every step + HeartBeatLoggingReporterFactory(step_interval=10) + ] ) num_walkers = 4 @@ -122,9 +141,9 @@ def test_proc_pool_mapper(): in walker_states ] - # As an example of a useful configuration. There are 4 walkers in - # each cycle so that is the max number of processes that should be - # used. + # As an example of a useful configuration for Reference + # platform. There are 4 walkers in each cycle so that is the max + # number of processes that should be used. sim_manager = Manager( init_walkers=init_walkers, runner=runner, From 48d62be4343b0a00b94e30b5311e877b2e176e57 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 8 Dec 2025 11:42:10 -0500 Subject: [PATCH 061/143] add some more clock time info to heart beat openmm logger --- src/wepy/runners/openmm/logger.py | 50 +++++++++++++++++++++++++------ src/wepy/runners/openmm/runner.py | 12 +++++++- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/wepy/runners/openmm/logger.py b/src/wepy/runners/openmm/logger.py index a99f86d6..86f7851d 100644 --- a/src/wepy/runners/openmm/logger.py +++ b/src/wepy/runners/openmm/logger.py @@ -1,3 +1,4 @@ +import time import logging from typing import Callable from collections.abc import Collection @@ -50,7 +51,14 @@ def report( state, ) -LoggingReporterFactory = Callable[[logging.Logger], LoggingReporter] +LoggingReporterFactory = Callable[ + [ + logging.Logger, + # start_time + int, + ], + LoggingReporter, +] class StepIntervalLoggingReporter(LoggingReporter): """Reporter that reports at intervals in steps.""" @@ -66,6 +74,7 @@ def __init__( callback: LoggingReporterCallback, state_includes: Collection[OpenMMGetStateKeys], step_interval: int, + start_time: int, ) -> None: super().__init__( @@ -74,6 +83,7 @@ def __init__( state_includes=state_includes, ) self.step_interval = step_interval + self.start_time = start_time def describeNextReport( self, simulation: openmm.app.Simulation @@ -108,22 +118,31 @@ def __init__( state_includes: Collection[OpenMMGetStateKeys], step_size: openmm.unit.Quantity, sampling_time_interval: openmm.unit.Quantity, + start_time: int, ) -> None: super().__init__(logger, callback, state_includes) self.sampling_time_interval = sampling_time_interval self.step_size = step_size + self.start_time = start_time def describeNextReport( - self, simulation: openmm.app.Simulation + self, + simulation: openmm.app.Simulation, ) -> OpenMMReporterNextReport: + # Special case for first step + if simulation.context.getStepCount() == 0: + return OpenMMReporterNextReport( + steps=0, + include=list(self.state_includes), + periodic=False, + ) + _unit = openmm.unit.attosecond curr_sampling_time: openmm.unit.Quantity = simulation.context.getTime() - # avg_step_time: openmm.unit.Quantity = curr_sampling_time / simulation.currentStep - sampling_time_left = ( self.sampling_time_interval.value_in_unit(_unit) - ( curr_sampling_time.value_in_unit(_unit) @@ -151,7 +170,8 @@ class HeartBeatLoggingReporter(StepIntervalLoggingReporter): def __init__( self, logger: logging.Logger, - step_interval: int + step_interval: int, + start_time: int, ) -> None: super().__init__( @@ -159,21 +179,28 @@ def __init__( callback=self.logging_callback, state_includes=[], step_interval=step_interval, + start_time=start_time, ) - @staticmethod def logging_callback( + self, logger: logging.Logger, simulation: openmm.app.Simulation, state: openmm.State, ) -> None: - # TODO: make this adaptive to reduce zeros etc. + current_time = time.time() + elapsed_time = current_time - self.start_time + + # TODO: make this adaptive to reduce zeros etc. Currently just + # padded to the standard 1-2 fs step time shown in picoseconds sim_time = simulation.context.getTime() sim_time_mag = sim_time.value_in_unit(openmm.unit.picosecond) sim_steps = simulation.context.getStepCount() - logger.info(f"OpenMM simulation progress: sim_time={sim_time_mag:.4f} ps, sim_steps={sim_steps}") + logger.info( + f"OpenMM simulation progress: clock_time={current_time:.4f} s, elapsed_time={elapsed_time:.4f} s, sim_time={sim_time_mag:.4f} ps, sim_steps={sim_steps}", + ) @@ -183,11 +210,16 @@ class HeartBeatLoggingReporterFactory: step_interval: int - def __call__(self, logger: logging.Logger) -> HeartBeatLoggingReporter: + def __call__( + self, + logger: logging.Logger, + start_time: int, + ) -> HeartBeatLoggingReporter: return HeartBeatLoggingReporter( logger=logger, step_interval=self.step_interval, + start_time=start_time, ) # TODO: # class EnergyLoggingReporter() diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index 77761d4e..2433cc98 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -83,13 +83,23 @@ class OpenMMRunner(Runner): get_state_keys: frozenset[str] = attrs.field(default=GET_STATE_DEFAULT_KEYS) openmm_reporter_factories: list[LoggingReporterFactory] = attrs.field(default=DEFAULT_OPENMM_REPORTER_FACTORIES) + # TODO: figure out a better way to do this, probably by separating + # runner into a factory and concrete, stateful, implementation _openmm_reporters: list[OpenMMReporter] = attrs.field(default=[]) _last_cycle_segments_split_times: list[OpenMMRunnerSegmentSplitTimes] = attrs.field(default=[]) + _init_time: int = attrs.field(init=False) def init(self) -> None: + self._init_time = time.time() + for omm_reporter_factory in self.openmm_reporter_factories: - self._openmm_reporters.append(omm_reporter_factory(logger)) + self._openmm_reporters.append( + omm_reporter_factory( + logger, + start_time=self._init_time, + ) + ) def pre_cycle( self, From 4e354fb5244b94cfa82d324284d6b774471043ee Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 8 Dec 2025 14:12:11 -0500 Subject: [PATCH 062/143] implement state machine protocol for runners This centralizes this behavior and makes it independently testable. --- src/wepy/runners/mock.py | 56 ++++++-- src/wepy/runners/runner.py | 178 ++++++++++++++++++++----- tests/unit/test_runners/test_mock.py | 54 +++++++- tests/unit/test_runners/test_runner.py | 98 +++++++++++++- 4 files changed, 327 insertions(+), 59 deletions(-) diff --git a/src/wepy/runners/mock.py b/src/wepy/runners/mock.py index 6a03dd1f..1ebad91f 100644 --- a/src/wepy/runners/mock.py +++ b/src/wepy/runners/mock.py @@ -1,10 +1,11 @@ """Realistic mock runners useful mostly for testing.""" +import time import logging from typing import Literal import attrs from wepy.walker import Walker, WalkerState -from wepy.runners.runner import Runner +from wepy.runners.runner import Runner, RunnerStatus, RunSegmentData, RunnerStateMachine, RunnerEvent, RunnerStateError logger = logging.getLogger(__name__) @@ -17,34 +18,65 @@ class MockError(Exception): @attrs.define class MockRunner(Runner): - fail: bool = False - _initialized: bool = False + state_machine: RunnerStateMachine = attrs.field( + default=attrs.Factory( + RunnerStateMachine, + ) + ) + + @property + def status(self) -> RunnerStatus: + return self.state_machine.state def init(self) -> None: - self._initialized = True + + # NOTE: showing example of validating the event before doing + # potentially expensive calculations and then transitioning + # the actual state when it is done + self.state_machine.validate_event(RunnerEvent.INIT) + # do something... + logger.info("INIT stuff") + self.state_machine.send(RunnerEvent.INIT) def pre_cycle(self) -> None: - pass - def post_cycle(self) -> None: - pass + self.state_machine.send(RunnerEvent.PRE_CYCLE) + + def post_cycle(self, segments_data: list[RunSegmentData]) -> None: + self.state_machine.send(RunnerEvent.POST_SEGMENT) + self.state_machine.send(RunnerEvent.POST_CYCLE) def run_segment( self, state: MockState, segment_length: int, - ) -> MockState: + ) -> tuple[MockState, RunSegmentData]: + + if self.status != RunnerStatus.PRE_CYCLE: + raise RunnerStateError( + f"Cannot run a segment in state ({self.status.name}:{self.status.value})" + ) + + seg_start_time = time.time() if self.fail: - logger.critical("Error requested in MockRuner.run_segment, raising.") + logger.critical("Error requested in MockRunner.run_segment, raising.") raise MockError("Error requested") logger.info("Evolving the MockState in MockRunner.run_segment") - return attrs.evolve( + new_state = attrs.evolve( state, a=(state.a + segment_length), ) - def get_last_cycle_segments_split_times(self) -> None: - return None + seg_end_time = time.time() + + split_time = seg_end_time - seg_start_time + + segment_data = RunSegmentData(segment_split_time=split_time) + + return new_state, segment_data + +# @attrs.define +# class MockRunnerFactory: diff --git a/src/wepy/runners/runner.py b/src/wepy/runners/runner.py index a1dea3ee..09fbb8f3 100644 --- a/src/wepy/runners/runner.py +++ b/src/wepy/runners/runner.py @@ -18,49 +18,121 @@ """ # Standard Library -from typing import Protocol, Any, TypedDict, TypeVar, ParamSpec +import logging +from typing import Protocol, Any, TypedDict, TypeVar, ParamSpec, Callable, Literal +from enum import IntEnum import attrs +from immutables import Map as frozenmap from wepy.walker import Walker, WalkerState -WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) - -class Runner(Protocol[WalkerState_]): - """Abstract base class for the Runner interface.""" +logger = logging.getLogger(__name__) - def init(self) -> None: ... +@attrs.define +class RunSegmentData: + segment_split_time: float - def pre_cycle(self) -> None: - """Perform pre-cycle behavior. run_segment will be called for each - walker so this allows you to perform changes of state on a - per-cycle basis. +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) +RunSegmentData_ = TypeVar("RunSegmentData_", bound=RunSegmentData) + +class RunnerStatus(IntEnum): + PRE_INITIALIZATION = 0 + INITIALIZED = 1 + PRE_CYCLE = 2 + POST_SEGMENT = 3 + POST_CYCLE = 4 + +class RunnerEvent(IntEnum): + INIT = 0 + PRE_CYCLE = 1 + POST_SEGMENT = 2 + POST_CYCLE = 3 + +class RunnerStateMachineError(Exception): + pass + +class RunnerStateTransitionError(RunnerStateMachineError): + """Indicates an error with the runner state machine transition.""" + pass + +class RunnerStateError(RunnerStateMachineError): + """Indicates an error relating to the current state of the runner.""" + pass + +# State machine table that defines what are the valid states to +# transition to another state. None for the initial states +RUNNER_STATE_TRANSITION_TABLE: frozenmap[ + RunnerStatus, + frozenmap[RunnerEvent, RunnerStatus], +] = frozenmap({ + RunnerStatus.PRE_INITIALIZATION: frozenmap({ + RunnerEvent.INIT : RunnerStatus.INITIALIZED, + }), + RunnerStatus.INITIALIZED: frozenmap({ + RunnerEvent.PRE_CYCLE : RunnerStatus.PRE_CYCLE, + }), + RunnerStatus.PRE_CYCLE: frozenmap({ + RunnerEvent.POST_SEGMENT : RunnerStatus.POST_SEGMENT, + }), + RunnerStatus.POST_SEGMENT: frozenmap({ + RunnerEvent.POST_CYCLE : RunnerStatus.POST_CYCLE, + }), + RunnerStatus.POST_CYCLE: frozenmap({ + RunnerEvent.PRE_CYCLE : RunnerStatus.PRE_CYCLE, + }), +}) - Parameters - ---------- - kwargs : key-word arguments - Key-value pairs to be interpreted by each runner implementation. +@attrs.define +class RunnerStateMachine: + state: RunnerStatus = attrs.field( + default=RunnerStatus.PRE_INITIALIZATION, + ) + + def validate_event(self, event: RunnerEvent) -> Literal[True]: + state_transitions = RUNNER_STATE_TRANSITION_TABLE[self.state] + if event not in state_transitions: + raise RunnerStateTransitionError( + f"Runner is in state {self.state.name}:{self.state.value}," + f"event {event.name}:{event.value} is not a valid." + f" Choose from: {set(state_transitions.keys())}" + ) + else: + return True + + def send(self, event: RunnerEvent) -> RunnerStatus: + + logger.info(f"Received event: {event.name}:{event.value}") + self.validate_event(event) + + state_transitions = RUNNER_STATE_TRANSITION_TABLE[self.state] + new_state = state_transitions[event] + + logger.info( + "Transitioning runner state:" + f" {self.state.name}:{self.state.value} -> {new_state.name}:{new_state.value}" + ) + self.state = new_state + + return self.state + +class Runner(Protocol[WalkerState_, RunSegmentData_]): + """Abstract base class for the Runner interface.""" - """ + @property + def status(self) -> RunnerStatus: ... - def post_cycle(self) -> None: - """Perform post-cycle behavior. run_segment will be called for each - walker so this allows you to perform changes of state on a - per-cycle basis. - - Parameters - ---------- - kwargs : key-word arguments - Key-value pairs to be interpreted by each runner implementation. - """ + def init(self) -> None: ... + def pre_cycle(self) -> None: + """Perform pre-cycle behavior.""" ... def run_segment( self, walker: WalkerState_, segment_length: int, - ) -> WalkerState_: + ) -> tuple[WalkerState_, RunSegmentData_ | None]: """Run dynamics for the walker. Parameters @@ -71,13 +143,26 @@ def run_segment( Returns ------- - new_walker : object implementing the Walker interface - Walker after dynamics was run, only the state should be modified. + new_walker : Walker after dynamics was run, only the state should be modified. + run_segment_data: Arbitrary data type that is used internally + in the runner and managers for runner specific data, + e.g. segment performance metrics. """ ... - def get_last_cycle_segments_split_times(self) -> list[dict[str, float]] | None: ... + def post_cycle( + self, + segments_data: list[RunSegmentData_] | None, + ) -> None: + """Perform post-cycle behavior.""" + ... + + + +RunnerFactory = Callable[ + [], Runner, +] @attrs.define class NoRunner(Runner): @@ -86,19 +171,38 @@ class NoRunner(Runner): May be useful for testing. """ + state_machine: RunnerStateMachine = attrs.field( + default=attrs.Factory( + RunnerStateMachine, + ) + ) + + @property + def status(self) -> RunnerStatus: + return self.state_machine.state + def init(self) -> None: - pass + self.state_machine.send(RunnerEvent.INIT) def pre_cycle(self) -> None: - pass - def post_cycle(self) -> None: - pass - def get_last_cycle_segments_split_times(self) -> None: - return None + self.state_machine.send(RunnerEvent.PRE_CYCLE) + def run_segment( self, state: WalkerState_, segment_length: int | float, - ) -> WalkerState_: - return state + ) -> tuple[WalkerState_, None]: + + if self.status != RunnerStatus.PRE_CYCLE: + raise RunnerStateError( + f"Cannot run a segment in state ({self.status.name}:{self.status.value})" + ) + + return state, None + + def post_cycle(self, segments_data: None) -> None: + + self.state_machine.send(RunnerEvent.POST_SEGMENT) + self.state_machine.send(RunnerEvent.POST_CYCLE) + diff --git a/tests/unit/test_runners/test_mock.py b/tests/unit/test_runners/test_mock.py index dc4ffb77..21f76174 100644 --- a/tests/unit/test_runners/test_mock.py +++ b/tests/unit/test_runners/test_mock.py @@ -1,26 +1,66 @@ import pytest from wepy.runners.mock import MockRunner, MockState, MockError +from wepy.runners.runner import RunnerStatus, Runner, RunnerStateTransitionError, RunnerStateMachineError class TestMockRunner: def test_init(self): runner = MockRunner() - assert not runner._initialized + assert runner.status == RunnerStatus.PRE_INITIALIZATION runner.init() - assert runner._initialized + assert runner.status == RunnerStatus.INITIALIZED + + with pytest.raises(RunnerStateTransitionError): + runner.init() def test_pre_cycle(self): - MockRunner().pre_cycle() + runner = MockRunner() + + with pytest.raises(RunnerStateTransitionError): + runner.pre_cycle() + + runner.init() + runner.pre_cycle() + + with pytest.raises(RunnerStateTransitionError): + runner.pre_cycle() + + runner.post_cycle(None) + runner.pre_cycle() def test_post_cycle(self): - MockRunner().post_cycle() + runner = MockRunner() + + with pytest.raises(RunnerStateTransitionError): + runner.pre_cycle() + + runner.init() + runner.pre_cycle() + + with pytest.raises(RunnerStateTransitionError): + runner.pre_cycle() + + runner.post_cycle(None) + runner.pre_cycle() + runner.post_cycle(None) def test_run_segment(self): - assert MockRunner().run_segment( + runner = MockRunner() + + with pytest.raises(RunnerStateMachineError): + runner.run_segment( + MockState(0), + 10, + ) + + runner.init() + runner.pre_cycle() + + assert runner.run_segment( MockState(0), 10, - 0, - ) == MockState(10) + )[0] == MockState(10) + diff --git a/tests/unit/test_runners/test_runner.py b/tests/unit/test_runners/test_runner.py index 038ee606..f3362055 100644 --- a/tests/unit/test_runners/test_runner.py +++ b/tests/unit/test_runners/test_runner.py @@ -1,9 +1,77 @@ +import pytest import attrs -from wepy.runners.runner import NoRunner +from wepy.runners.runner import NoRunner, RunnerStateMachine, RunnerStatus, RunnerEvent, RunnerStateTransitionError, RunnerStateError from wepy.walker import Walker, WalkerState +class Test_RunnerStateMachine: -class TestNoRunner: + def test_validate_event(self): + + sm = RunnerStateMachine(state=RunnerStatus.PRE_INITIALIZATION) + # make sure the return and state are consistent + assert sm.validate_event(RunnerEvent.INIT) + + sm = RunnerStateMachine(state=RunnerStatus.PRE_INITIALIZATION) + with pytest.raises(RunnerStateTransitionError): + sm.validate_event(RunnerEvent.PRE_CYCLE) + + def test_send(self): + + sm = RunnerStateMachine(state=RunnerStatus.PRE_INITIALIZATION) + # make sure the return and state are consistent + assert sm.send(RunnerEvent.INIT) == RunnerStatus.INITIALIZED + assert sm.state == RunnerStatus.INITIALIZED + + # default construction + assert RunnerStateMachine().state == RunnerStatus.PRE_INITIALIZATION + + sm = RunnerStateMachine(state=RunnerStatus.PRE_INITIALIZATION) + with pytest.raises(RunnerStateTransitionError): + sm.send(RunnerEvent.PRE_CYCLE) + + # test the rest of the transitions + assert RunnerStateMachine( + RunnerStatus.INITIALIZED + ).send(RunnerEvent.PRE_CYCLE) == RunnerStatus.PRE_CYCLE + + assert RunnerStateMachine( + RunnerStatus.PRE_CYCLE + ).send(RunnerEvent.POST_SEGMENT) == RunnerStatus.POST_SEGMENT + + assert RunnerStateMachine( + RunnerStatus.POST_SEGMENT + ).send(RunnerEvent.POST_CYCLE) == RunnerStatus.POST_CYCLE + + assert RunnerStateMachine( + RunnerStatus.POST_CYCLE + ).send(RunnerEvent.PRE_CYCLE) == RunnerStatus.PRE_CYCLE + +class Test_NoRunner: + + def test___init__(self): + + assert NoRunner().state_machine.state == RunnerStatus.PRE_INITIALIZATION + + def test_status(self): + assert NoRunner().status == RunnerStatus.PRE_INITIALIZATION + + def test_init(self): + runner = NoRunner() + runner.init() + assert runner.status == RunnerStatus.INITIALIZED + + def test_pre_cycle(self): + runner = NoRunner() + runner.init() + runner.pre_cycle() + assert runner.status == RunnerStatus.PRE_CYCLE + + def test_post_cycle(self): + runner = NoRunner() + runner.init() + runner.pre_cycle() + runner.post_cycle(None) + assert runner.status == RunnerStatus.POST_CYCLE def test_run_segment(self): @@ -28,10 +96,34 @@ def dict(self) -> dict[str, int]: state=SomeState(a=1), weight=0.1, ) + + with pytest.raises(RunnerStateError): + runner.run_segment( + walker, + 10, + ) + + runner.init() + + with pytest.raises(RunnerStateError): + runner.run_segment( + walker, + 10, + ) + + runner.pre_cycle() assert ( runner.run_segment( walker, 10, ) - == walker + == (walker, None) ) + + runner.post_cycle(None) + with pytest.raises(RunnerStateError): + runner.run_segment( + walker, + 10, + ) + From 2975ed8c6a05241d63b722a6f725f675ff04c391 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 8 Dec 2025 14:38:09 -0500 Subject: [PATCH 063/143] implement state machine for openmm runner --- src/wepy/runners/openmm/__init__.py | 4 +- src/wepy/runners/openmm/runner.py | 177 ++++++++++---- .../test_runners/test_openmm/test_runner.py | 225 +++++++++++++----- 3 files changed, 306 insertions(+), 100 deletions(-) diff --git a/src/wepy/runners/openmm/__init__.py b/src/wepy/runners/openmm/__init__.py index 258c5273..3f5a8c80 100644 --- a/src/wepy/runners/openmm/__init__.py +++ b/src/wepy/runners/openmm/__init__.py @@ -7,12 +7,14 @@ get_context_state, state_to_xml, ) -from .runner import OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS +from .runner import ( + OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS, OpenMMRunnerFactory) from .logger import HeartBeatLoggingReporterFactory __all__ = [ "HeartBeatLoggingReporterFactory", "GPU_PLATFORMS", + "OpenMMRunnerFactory", "OpenMMPlatformName", "OpenMMRunner", "OpenMMState", diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index 2433cc98..56296ffe 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -32,7 +32,7 @@ ) # First Party Library -from wepy.runners.runner import Runner +from wepy.runners.runner import Runner, RunnerStatus, RunSegmentData, RunnerStateMachine, RunnerEvent, RunnerStateError from wepy.util.util import box_vectors_to_lengths_angles from wepy.walker import WalkerState from .state import OpenMMState, OpenMMStateWrapper, get_context_state @@ -43,11 +43,7 @@ OpenMMPlatformName = Literal["Reference", "CPU", "CUDA", "OpenCL", "HIP"] GPU_PLATFORMS = frozenset({"CUDA", "OpenCL", "HIP"}) -class OpenMMRunnerSegmentSplitTimes(TypedDict): - gen_sim_time: float - steps_time: float - get_state_time: float - run_segment_time: float + GET_STATE_DEFAULT_KEYS = frozenset({ "positions", @@ -67,47 +63,88 @@ class OpenMMRunnerSegmentSplitTimes(TypedDict): HeartBeatLoggingReporterFactory(step_interval=500), ] -# the runner for the simulation which runs the actual dynamics @attrs.define +class OpenMMRunnerSegmentSplitTime: + gen_sim_time: float + steps_time: float + get_state_time: float + +@attrs.define +class OpenMMRunnerSegmentData(RunSegmentData): + segment_split_time: float + openmm_segment_split_time: OpenMMRunnerSegmentSplitTime + +# the runner for the simulation which runs the actual dynamics class OpenMMRunner(Runner): """Runner for OpenMM simulations.""" - # TODO: should probably have these as the serialized versions for - # going across process boundaries system: openmm.System topology: openmm.app.Topology integrator: openmm.Integrator - platform_name: str | None = None - global_platform_kwargs: PlatformKwargs | None = None - enforce_box: bool = False - get_state_keys: frozenset[str] = attrs.field(default=GET_STATE_DEFAULT_KEYS) - openmm_reporter_factories: list[LoggingReporterFactory] = attrs.field(default=DEFAULT_OPENMM_REPORTER_FACTORIES) + platform_name: str | None + global_platform_kwargs: PlatformKwargs | None + enforce_box: bool + get_state_keys: frozenset[str] + openmm_reporter_factories: list[LoggingReporterFactory] | None + + state_machine: RunnerStateMachine + + _openmm_reporters: list[OpenMMReporter] | None + _init_time: int | None + _pre_cycle_time: int | None + + def __init__( + self, + system: openmm.System, + topology: openmm.app.Topology, + integrator: openmm.Integrator, + platform_name: str | None = None, + global_platform_kwargs: PlatformKwargs | None = None, + enforce_box: bool = False, + get_state_keys: frozenset[str] = GET_STATE_DEFAULT_KEYS, + openmm_reporter_factories: list[LoggingReporterFactory] | None = None, + ) -> None: - # TODO: figure out a better way to do this, probably by separating - # runner into a factory and concrete, stateful, implementation - _openmm_reporters: list[OpenMMReporter] = attrs.field(default=[]) - _last_cycle_segments_split_times: list[OpenMMRunnerSegmentSplitTimes] = attrs.field(default=[]) - _init_time: int = attrs.field(init=False) + self.system = system + self.topology = topology + self.integrator = integrator + self.platform_name = platform_name + self.global_platform_args = global_platform_kwargs + self.enforce_box = enforce_box + self.get_state_keys = get_state_keys + + logger.warning("No OpenMM reporter factories configured.") + self.openmm_reporter_factories = openmm_reporter_factories if openmm_reporter_factories is not None else [] + + self._openmm_reporters = None + self._init_time = None + self._pre_cycle_time = None + + self.state_machine = RunnerStateMachine() + + @property + def status(self) -> RunnerStatus: + return self.state_machine.state def init(self) -> None: + self.state_machine.validate_event(RunnerEvent.INIT) + self._init_time = time.time() + logger.info(f"Initialized runner at time: {self._init_time} s") - for omm_reporter_factory in self.openmm_reporter_factories: - self._openmm_reporters.append( - omm_reporter_factory( - logger, - start_time=self._init_time, - ) - ) + self.state_machine.send(RunnerEvent.INIT) def pre_cycle( self, ) -> None: - self._last_cycle_segments_split_times = [] - def post_cycle(self) -> None: - pass + self.state_machine.validate_event(RunnerEvent.PRE_CYCLE) + + self._pre_cycle_time = time.time() + logger.info(f"Runner pre_cycle time: {self._pre_cycle_time} s") + + self.state_machine.send(RunnerEvent.PRE_CYCLE) def run_segment( @@ -116,7 +153,10 @@ def run_segment( segment_length: int, platform_name: OpenMMPlatformName | None = None, platform_kwargs: PlatformKwargs | None = None, - ) -> OpenMMState: + ) -> tuple[ + OpenMMState, + OpenMMRunnerSegmentData, + ]: """Run dynamics for the walker. Parameters @@ -135,15 +175,24 @@ def run_segment( """ + if self.status != RunnerStatus.PRE_CYCLE: + raise RunnerStateError( + f"Cannot run a segment in state ({self.status.name}:{self.status.value})" + ) + logger.info("Running OpenMM MD segment") run_segment_start = time.time() # set the kwargs that will be passed to getState - gen_sim_start = time.time() - # make a copy of the integrator for this particular segment + # TODO: refactor this as an integrator spec as the object + # attribute to avoid needing to do this and make this + # interface explicit + + # make a copy of the integrator for this particular segment, + # otherwise the object attribute will get bound to the context new_integrator = copy.copy(self.integrator) # force setting of random seed to 0, which is a special # value that forces the integrator to choose another @@ -192,8 +241,21 @@ def run_segment( self.topology, self.system, new_integrator ) + # Generate new reporters for each segment so they don't step + # on each other's state + logger.info("Generating OpenMM reporters for this segment.") + openmm_reporters = [] + for omm_reporter_factory in self.openmm_reporter_factories: + logger.info(f"Generating and configuring reporter for factory: {omm_reporter_factory}") + openmm_reporters.append( + omm_reporter_factory( + logger, + start_time=run_segment_start, + ) + ) + logger.info("Registering OpenMM Simulation reporters") - simulation.reporters.extend(self._openmm_reporters) + simulation.reporters = openmm_reporters # generate a sim state logger.info("Generating openmm.State from input OpenMMState") @@ -220,7 +282,7 @@ def run_segment( steps_end = time.time() steps_time = steps_end - steps_start - logger.info("Time to run {} sim steps: {}".format(segment_length, steps_time)) + logger.info(f"Time to run {segment_length} sim steps: {steps_time} s") get_state_start = time.time() @@ -244,20 +306,47 @@ def run_segment( run_segment_time = run_segment_end - run_segment_start logger.info("Total internal run_segment time: {}".format(run_segment_time)) - segment_split_times = OpenMMRunnerSegmentSplitTimes({ - "gen_sim_time": gen_sim_time, - "steps_time": steps_time, - "get_state_time": get_state_time, - "run_segment_time": run_segment_time, - }) + segment_data = OpenMMRunnerSegmentData( + segment_split_time=run_segment_time, + openmm_segment_split_time=OpenMMRunnerSegmentSplitTime( + gen_sim_time=gen_sim_time, + steps_time=steps_time, + get_state_time=get_state_time, + ), + ) - self._last_cycle_segments_split_times.append(segment_split_times) + return new_state, segment_data - return new_state + def post_cycle(self, segments_data: list[OpenMMRunnerSegmentData]) -> None: - def last_cycle_segments_split_times(self) -> list[OpenMMRunnerSegmentSplitTimes]: + self.state_machine.send(RunnerEvent.POST_SEGMENT) + logger.info("Nothing to do") + self.state_machine.send(RunnerEvent.POST_CYCLE) - return copy.deepcopy(self._last_cycle_segments_split_times) +@attrs.define +class OpenMMRunnerFactory: + + system: openmm.System + topology: openmm.app.Topology + integrator: openmm.Integrator + platform_name: str | None = None + global_platform_kwargs: PlatformKwargs | None = None + enforce_box: bool = False + get_state_keys: frozenset[str] = attrs.field(default=GET_STATE_DEFAULT_KEYS) + openmm_reporter_factories: list[LoggingReporterFactory] | None = attrs.field(default=DEFAULT_OPENMM_REPORTER_FACTORIES) + + def __call__(self) -> OpenMMRunner: + + return OpenMMRunner( + system=copy.deepcopy(self.system), + topology=copy.deepcopy(self.topology), + integrator=copy.deepcopy(self.integrator), + platform_name=self.platform_name, + global_platform_kwargs=self.global_platform_kwargs, + enforce_box=self.enforce_box, + get_state_keys=self.get_state_keys, + openmm_reporter_factories=self.openmm_reporter_factories, + ) # class OpenMMCPUWorker(Worker): diff --git a/tests/unit/test_runners/test_openmm/test_runner.py b/tests/unit/test_runners/test_openmm/test_runner.py index a081cf33..33e6097f 100644 --- a/tests/unit/test_runners/test_openmm/test_runner.py +++ b/tests/unit/test_runners/test_openmm/test_runner.py @@ -1,12 +1,20 @@ import logging +import copy +import attrs from wepy_tools.systems.lennard_jones import LennardJonesPair from wepy.runners.openmm.state import ( dummy_context, OpenMMState, ) +from wepy.runners.runner import ( + RunnerStatus, + RunnerStateTransitionError, + RunnerStateError, +) from wepy.runners.openmm.runner import ( OpenMMRunner, + OpenMMRunnerSegmentData, ) from wepy.runners.openmm.logger import StepIntervalLoggingReporter @@ -30,7 +38,7 @@ def runner_components() -> tuple[openmm.System, openmm.app.Topology, openmm.Lang lj_sys = LennardJonesPair() - integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + integrator = openmm.LangevinIntegrator(300.0, 0.1, 0.002) return lj_sys.system, lj_sys.topology, integrator @@ -43,47 +51,112 @@ def omm_context() -> openmm.Context: return ctx +@attrs.define +class Spy: + touched: bool = False + + def touch(self) -> None: + self.touched = True + +class TouchGlobalStepIntervalLoggingReporter(StepIntervalLoggingReporter): + def __init__( + self, + logger: logging.Logger, + start_time: int, + spy: Spy, + ) -> None: -class TestOpenMMRunner: + self.spy = spy + + super().__init__( + logger=logger, + callback=self.touch, + state_includes=[], + # NOTE: hardcoded + step_interval=1, + start_time=start_time, + ) + + def touch(self, *args) -> None: + self.spy.touch() + +class Test_OpenMMRunner: def test___init__(self, runner_components): + system, topology, integrator = runner_components + + runner = OpenMMRunner( + system=copy.deepcopy(system), + topology=copy.deepcopy(topology), + integrator=copy.deepcopy(integrator), + openmm_reporter_factories=None, + ) + assert runner.openmm_reporter_factories == [] + assert runner._openmm_reporters is None + assert runner._init_time is None + + runner = OpenMMRunner( + system=copy.deepcopy(system), + topology=copy.deepcopy(topology), + integrator=copy.deepcopy(integrator), + ) + assert runner.openmm_reporter_factories == [] + + assert runner.status == RunnerStatus.PRE_INITIALIZATION + + SPY = Spy() + def _mock_factory(logger: logging.Logger, start_time: int) -> TouchGlobalStepIntervalLoggingReporter: + return TouchGlobalStepIntervalLoggingReporter(logger=logger, start_time=start_time, spy=SPY) + runner = OpenMMRunner( - *runner_components + system=copy.deepcopy(system), + topology=copy.deepcopy(topology), + integrator=copy.deepcopy(integrator), + openmm_reporter_factories=[_mock_factory], ) + assert len(runner.openmm_reporter_factories) == 1 def test_init(self, runner_components): + system, topology, integrator = runner_components runner = OpenMMRunner( - *runner_components + system=copy.deepcopy(system), + topology=copy.deepcopy(topology), + integrator=copy.deepcopy(integrator), ) - assert runner._openmm_reporters == [] + assert runner._openmm_reporters is None runner.init() + assert runner.status == RunnerStatus.INITIALIZED # check the default openmm reporters were constructed - assert len(runner._openmm_reporters) > 0 + assert runner._init_time is not None + # test status, can't init twice + with pytest.raises(RunnerStateTransitionError): + runner.init() def test_pre_cycle(self, runner_components): + system, topology, integrator = runner_components runner = OpenMMRunner( - *runner_components, + system=copy.deepcopy(system), + topology=copy.deepcopy(topology), + integrator=copy.deepcopy(integrator), ) - assert runner._last_cycle_segments_split_times == [] - runner.pre_cycle() - assert runner._last_cycle_segments_split_times == [] + with pytest.raises(RunnerStateTransitionError): + runner.pre_cycle() - runner = OpenMMRunner( - *runner_components, - last_cycle_segments_split_times=[{"something" : 1}] - ) + runner.init() + assert runner.status == RunnerStatus.INITIALIZED runner.pre_cycle() - assert runner._last_cycle_segments_split_times == [] + assert runner.status == RunnerStatus.PRE_CYCLE + assert runner._pre_cycle_time is not None def test_run_segment(self, runner_components): @@ -94,77 +167,119 @@ def test_run_segment(self, runner_components): state = OpenMMState.from_dwim(positions=lj_sys.positions) runner = OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, + system=copy.deepcopy(system), + topology=copy.deepcopy(topology), + integrator=copy.deepcopy(integrator), platform_name="Reference", ) - new_state = runner.run_segment(state, 2) + with pytest.raises(RunnerStateError): + runner.run_segment(state, 2) + + runner.init() + with pytest.raises(RunnerStateError): + runner.run_segment(state, 2) + + runner.pre_cycle() + + new_state, segment_data = runner.run_segment(state, 2) + assert "positions" in new_state assert "velocities" in new_state runner = OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, + system=copy.deepcopy(system), + topology=copy.deepcopy(topology), + integrator=copy.deepcopy(integrator), get_state_keys={"positions",} ) + runner.init() + runner.pre_cycle() - new_state = runner.run_segment( + new_state, segment_data = runner.run_segment( new_state, 2, ) assert new_state["positions"] is not None assert "velocities" not in new_state + assert isinstance(segment_data, OpenMMRunnerSegmentData) + # test that openmm reporters are being called - class Spy: - def __init__(self) -> None: - self.touched = False + SPY = Spy() + def _mock_factory(logger: logging.Logger, start_time: int) -> TouchGlobalStepIntervalLoggingReporter: + return TouchGlobalStepIntervalLoggingReporter(logger=logger, start_time=start_time, spy=SPY) - def touch(self) -> None: - self.touched = True + runner = OpenMMRunner( + system=copy.deepcopy(system), + topology=copy.deepcopy(topology), + integrator=copy.deepcopy(integrator), + openmm_reporter_factories=[_mock_factory], + ) + runner.init() + runner.pre_cycle() - SPY = Spy() + assert not SPY.touched + new_state, segment_data = runner.run_segment( + state, + 10, + ) - class TouchGlobalStepIntervalLoggingReporter(StepIntervalLoggingReporter): + assert SPY.touched - def __init__( - self, - logger: logging.Logger, - ) -> None: + def test_post_cycle(self, runner_components): - self.spy = SPY + system, topology, integrator = runner_components - super().__init__( - logger=logger, - callback=self.touch, - state_includes=[], - # NOTE: hardcoded - step_interval=1, - ) + lj_sys = LennardJonesPair() - def touch(self, *args) -> None: - self.spy.touch() + state = OpenMMState.from_dwim(positions=lj_sys.positions) + runner = OpenMMRunner( + system=copy.deepcopy(system), + topology=copy.deepcopy(topology), + integrator=copy.deepcopy(integrator), + platform_name="Reference", + ) - def _mock_factory(logger: logging.Logger) -> TouchGlobalStepIntervalLoggingReporter: - return TouchGlobalStepIntervalLoggingReporter(logger=logger) + with pytest.raises(RunnerStateTransitionError): + runner.post_cycle(None) + runner.init() + with pytest.raises(RunnerStateTransitionError): + runner.post_cycle(None) + + runner.pre_cycle() + + # NOTE: that you don't need to call run_segment, because in a + # standard use case this would be done in a subprocess. Any + # state changes must be reified in the RunSegmentData + + runner.post_cycle(None) + + assert runner.status == RunnerStatus.POST_CYCLE + + # with a run_segment runner = OpenMMRunner( - system=system, - topology=topology, - integrator=integrator, - get_state_keys={}, - openmm_reporter_factories=[_mock_factory], + system=copy.deepcopy(system), + topology=copy.deepcopy(topology), + integrator=copy.deepcopy(integrator), + platform_name="Reference", ) + + with pytest.raises(RunnerStateTransitionError): + runner.post_cycle(None) + runner.init() + with pytest.raises(RunnerStateTransitionError): + runner.post_cycle(None) - assert not SPY.touched - new_state = runner.run_segment( + runner.pre_cycle() + new_state, segment_data = runner.run_segment( state, - 2, + 10, ) - assert SPY.touched + runner.post_cycle([segment_data]) + + assert runner.status == RunnerStatus.POST_CYCLE From 8e5b00df31d0629a1c66ec9d2f65b23ee38682a9 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 8 Dec 2025 22:38:49 -0500 Subject: [PATCH 064/143] implement state machine for Manager Also refined some other interfaces and added `pre_segment` and `post_segment` instead of directly calling the `runner.pre_cycle` etc. hooks in the main state machine flow. Also implements the runner factory generation, which was what spurred adding the state machine because state mgmt. was getting bad. --- src/wepy/runners/mock.py | 8 +- src/wepy/sim_manager.py | 373 +++++++++++++++--- .../test_openmm/test_sim_manager.py | 22 +- tests/unit/test_runners/test_mock.py | 7 +- tests/unit/test_runners/test_runner.py | 1 - tests/unit/test_sim_manager.py | 167 +++++++- 6 files changed, 490 insertions(+), 88 deletions(-) diff --git a/src/wepy/runners/mock.py b/src/wepy/runners/mock.py index 1ebad91f..d4636b0e 100644 --- a/src/wepy/runners/mock.py +++ b/src/wepy/runners/mock.py @@ -78,5 +78,9 @@ def run_segment( return new_state, segment_data -# @attrs.define -# class MockRunnerFactory: +@attrs.define +class MockRunnerFactory: + fail: bool = False + + def __call__(self) -> MockRunner: + return MockRunner(fail=self.fail) diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index 3f422933..af2d4365 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -44,15 +44,19 @@ # Standard Library import logging -from typing import Final, Any, TypedDict, Generic, TypeVar, Callable +from typing import Final, Any, TypedDict, Generic, TypeVar, Callable, Literal import copy import time +import enum + +import attrs +from immutables import Map as frozenmap # First Party Library from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.reporter.reporter import Reporter from wepy.resampling.resamplers.resampler import Resampler -from wepy.runners.runner import Runner +from wepy.runners.runner import Runner, RunnerFactory, RunSegmentData from wepy.walker import Walker from wepy.work_mapper.base import WorkMapper from wepy.work_mapper.serial import SerialMapper @@ -80,8 +84,231 @@ class CycleReportDict(TypedDict): cycle_runner_time: float cycle_bc_time: float cycle_resampling_time: float + +class ManagerStatus(enum.IntEnum): + CONSTRUCTED = enum.auto() + PRE_SIMULATION = enum.auto() + INITIALIZING = enum.auto() + INITIALIZED = enum.auto() + SIM_STARTED = enum.auto() + RUNNING_CYCLE = enum.auto() + RUNNING_PRE_SEGMENT = enum.auto() + PRE_SEGMENT_FINISHED = enum.auto() + RUNNING_SEGMENT = enum.auto() + SEGMENT_FINISHED = enum.auto() + RUNNING_POST_SEGMENT = enum.auto() + POST_SEGMENT_FINISHED = enum.auto() + BC_WARPING = enum.auto() + POST_BC_WARPING = enum.auto() + RESAMPLING = enum.auto() + POST_RESAMPLING = enum.auto() + REPORT_GENERATION = enum.auto() + REPORTING = enum.auto() + POST_REPORTING = enum.auto() + CYCLE_MONITORING = enum.auto() + POST_CYCLE_MONITORING = enum.auto() + POST_CYCLE = enum.auto() + POST_SIMULATION = enum.auto() + CLEANING = enum.auto() + CLEANUP_FINISHED = enum.auto() + FINISHED = enum.auto() + +class ManagerEvent(enum.IntEnum): + START_PRE_SIM = enum.auto() + START_INITIALIZATION = enum.auto() + FINISH_INITIALIZATION = enum.auto() + START_SIM = enum.auto() + START_CYCLE = enum.auto() + START_PRE_SEGMENT = enum.auto() + FINISH_PRE_SEGMENT = enum.auto() + START_SEGMENT = enum.auto() + FINISH_SEGMENT = enum.auto() + START_POST_SEGMENT = enum.auto() + FINISH_POST_SEGMENT = enum.auto() + START_BC_WARPING = enum.auto() + FINISH_BC_WARPING = enum.auto() + START_RESAMPLING = enum.auto() + FINISH_RESAMPLING = enum.auto() + GENERATE_REPORT = enum.auto() + START_REPORTING = enum.auto() + FINISH_REPORTING = enum.auto() + START_CYCLE_MONITORING = enum.auto() + FINISH_CYCLE_MONITORING = enum.auto() + FINISH_CYCLE = enum.auto() + FINISH_SIMULATION = enum.auto() + START_CLEANUP = enum.auto() + FINISH_CLEANUP = enum.auto() + SHUTDOWN = enum.auto() + + +# State machine table that defines what are the valid states to +# transition to another state. None for the initial states +MANAGER_STATE_TRANSITION_TABLE: frozenmap[ + ManagerStatus, + frozenmap[ManagerEvent, ManagerStatus], +] = frozenmap({ + ManagerStatus.CONSTRUCTED: frozenmap({ + ManagerEvent.START_PRE_SIM : ManagerStatus.PRE_SIMULATION, + }), + ManagerStatus.PRE_SIMULATION: frozenmap({ + ManagerEvent.START_INITIALIZATION : ManagerStatus.INITIALIZING, + }), + ManagerStatus.INITIALIZING: frozenmap({ + ManagerEvent.FINISH_INITIALIZATION : ManagerStatus.INITIALIZED, + }), + ManagerStatus.INITIALIZED: frozenmap({ + ManagerEvent.START_SIM : ManagerStatus.SIM_STARTED, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + ManagerStatus.SIM_STARTED: frozenmap({ + ManagerEvent.START_CYCLE : ManagerStatus.RUNNING_CYCLE, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + ManagerStatus.RUNNING_CYCLE: frozenmap({ + ManagerEvent.START_PRE_SEGMENT : ManagerStatus.RUNNING_PRE_SEGMENT, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + ManagerStatus.RUNNING_PRE_SEGMENT: frozenmap({ + ManagerEvent.FINISH_PRE_SEGMENT : ManagerStatus.PRE_SEGMENT_FINISHED, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + ManagerStatus.PRE_SEGMENT_FINISHED: frozenmap({ + ManagerEvent.START_SEGMENT : ManagerStatus.RUNNING_SEGMENT, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + ManagerStatus.RUNNING_SEGMENT: frozenmap({ + ManagerEvent.FINISH_SEGMENT : ManagerStatus.SEGMENT_FINISHED, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + ManagerStatus.SEGMENT_FINISHED: frozenmap({ + ManagerEvent.START_POST_SEGMENT : ManagerStatus.RUNNING_POST_SEGMENT, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + + ManagerStatus.RUNNING_POST_SEGMENT: frozenmap({ + ManagerEvent.FINISH_POST_SEGMENT : ManagerStatus.POST_SEGMENT_FINISHED, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + + ManagerStatus.POST_SEGMENT_FINISHED: frozenmap({ + ManagerEvent.START_BC_WARPING : ManagerStatus.BC_WARPING, + ManagerEvent.START_RESAMPLING : ManagerStatus.RESAMPLING, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + ManagerStatus.BC_WARPING: frozenmap({ + ManagerEvent.FINISH_BC_WARPING : ManagerStatus.POST_BC_WARPING, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + ManagerStatus.POST_BC_WARPING: frozenmap({ + ManagerEvent.START_RESAMPLING : ManagerStatus.RESAMPLING, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + + ManagerStatus.RESAMPLING: frozenmap({ + ManagerEvent.FINISH_RESAMPLING : ManagerStatus.POST_RESAMPLING, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + ManagerStatus.POST_RESAMPLING: frozenmap({ + ManagerEvent.GENERATE_REPORT : ManagerStatus.REPORT_GENERATION, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + + + ManagerStatus.REPORT_GENERATION: frozenmap({ + ManagerEvent.START_REPORTING : ManagerStatus.REPORTING, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + + ManagerStatus.REPORTING: frozenmap({ + ManagerEvent.FINISH_REPORTING : ManagerStatus.POST_REPORTING, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + + ManagerStatus.POST_REPORTING: frozenmap({ + ManagerEvent.START_CYCLE_MONITORING : ManagerStatus.CYCLE_MONITORING, + ManagerEvent.FINISH_CYCLE : ManagerStatus.POST_CYCLE, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + ManagerStatus.CYCLE_MONITORING: frozenmap({ + ManagerEvent.FINISH_CYCLE_MONITORING : ManagerStatus.POST_CYCLE_MONITORING, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + + ManagerStatus.CYCLE_MONITORING: frozenmap({ + ManagerEvent.FINISH_CYCLE_MONITORING : ManagerStatus.POST_CYCLE_MONITORING, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + + ManagerStatus.POST_CYCLE_MONITORING: frozenmap({ + ManagerEvent.FINISH_CYCLE : ManagerStatus.POST_CYCLE, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + + ManagerStatus.POST_CYCLE: frozenmap({ + ManagerEvent.START_CYCLE : ManagerStatus.RUNNING_CYCLE, + ManagerEvent.FINISH_SIMULATION : ManagerStatus.POST_SIMULATION, + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + ManagerStatus.POST_SIMULATION: frozenmap({ + ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, + }), + ManagerStatus.CLEANING: frozenmap({ + ManagerEvent.FINISH_CLEANUP : ManagerStatus.CLEANUP_FINISHED, + }), + ManagerStatus.CLEANUP_FINISHED: frozenmap({ + ManagerEvent.SHUTDOWN : ManagerStatus.FINISHED, + }), + ManagerStatus.FINISHED: frozenmap({}), +}) + +class ManagerStateMachineError(Exception): + pass + +class ManagerStateTransitionError(ManagerStateMachineError): + """Indicates an error with the runner state machine transition.""" + pass + +class ManagerStateError(ManagerStateMachineError): + """Indicates an error relating to the current state of the runner.""" + pass + + +@attrs.define +class ManagerStateMachine: + state: ManagerStatus = attrs.field( + default=ManagerStatus.CONSTRUCTED, + ) + + def validate_event(self, event: ManagerEvent) -> Literal[True]: + state_transitions = MANAGER_STATE_TRANSITION_TABLE[self.state] + if event not in state_transitions: + raise ManagerStateTransitionError( + f"Manager is in state {self.state.name}:{self.state.value}," + f" event {event.name}:{event.value} is not a valid." + f" Choose from: {set(state_transitions.keys())}" + ) + else: + return True + + def send(self, event: ManagerEvent) -> ManagerStatus: + + logger.info(f"Received event: {event.name}:{event.value}") + self.validate_event(event) + + state_transitions = MANAGER_STATE_TRANSITION_TABLE[self.state] + new_state = state_transitions[event] + + logger.info( + "Transitioning runner state:" + f" {self.state.name}:{self.state.value} -> {new_state.name}:{new_state.value}" + ) + self.state = new_state + + return self.state + State_ = TypeVar("State_") +RunSegmentData_ = TypeVar("RunSegmentData_", bound=RunSegmentData, covariant=True) class Manager(Generic[State_]): """The class that coordinates wepy simulations. @@ -114,9 +341,10 @@ class Manager(Generic[State_]): """ + state_machine: ManagerStateMachine init_walkers: list[Walker[State_]] n_init_walkers: int - runner: Runner + runner_factory: RunnerFactory resampler: Resampler boundary_conditions: BoundaryConditions | None work_mapper_factory: type[WorkMapper] @@ -147,7 +375,7 @@ class Manager(Generic[State_]): def __init__( self, init_walkers: list[Walker[State_]], - runner: Runner, + runner_factory: RunnerFactory, resampler: Resampler, work_mapper_factory: Callable[[], WorkMapper] | None = None, boundary_conditions: BoundaryConditions | None = None, @@ -193,11 +421,12 @@ def __init__( """ + self.init_walkers = copy.deepcopy(init_walkers) self.n_init_walkers = len(init_walkers) # the runner is the object that runs dynamics - self.runner = copy.deepcopy(runner) + self.runner_factory = runner_factory # the resampler self.resampler = copy.deepcopy(resampler) # object for boundary conditions @@ -222,6 +451,11 @@ def __init__( # break it and no one cares about this anyhow self._last_report: CycleReportDict | None = None + self.state_machine = ManagerStateMachine() + + @property + def status(self) -> ManagerStatus: + return self.state_machine.state def init( self, @@ -263,9 +497,12 @@ def init( (Default value = None) """ + self.state_machine.send(ManagerEvent.START_INITIALIZATION) logger.info("Running sim_manager.init hooks") + logger.info("Generating runner from factory") + self.runner = self.runner_factory() logger.info("Running runner.init hook") self.runner.init() @@ -300,6 +537,7 @@ def init( ) logger.info("Finished sim_manager initialization") + self.state_machine.send(ManagerEvent.FINISH_INITIALIZATION) def cleanup(self) -> None: """Perform cleanup actions for wepy configuration components. @@ -324,7 +562,7 @@ def cleanup(self) -> None: """ - logger.info("Running cleanup") + self.state_machine.send(ManagerEvent.START_CLEANUP) if self.monitor is not None: logger.info("Cleaning up monitoring") @@ -345,14 +583,17 @@ def cleanup(self) -> None: reporters=self.reporters, ) - logger.info("Finished cleanup") + self.state_machine.send(ManagerEvent.FINISH_CLEANUP) def run_segment( self, states: list[State_], segment_length: int, cycle_idx: int, - ) -> list[State_]: + ) -> tuple[ + list[State_], + list[RunSegmentData_], + ]: """Run a time segment for all walkers using the available workers. Maps the work for running each segment for each walker using @@ -375,10 +616,11 @@ def run_segment( The walkers after the segment of sampling simulation. """ + self.state_machine.send(ManagerEvent.START_SEGMENT) + segment_lengths = [segment_length for i in range(len(states))] - logger.info("Starting segment runs") try: - new_states = list( + map_results = list( self.work_mapper.map( self.runner.run_segment, states, @@ -396,9 +638,24 @@ def run_segment( # report on all of the errors that occured raise exception - logger.info("Ending segment") + self.state_machine.send(ManagerEvent.FINISH_SEGMENT) - return new_states + # transpose + new_states, segments_data = zip(*map_results, strict=True) + + return new_states, segments_data + + def pre_segment(self) -> None: + + self.state_machine.send(ManagerEvent.START_PRE_SEGMENT) + self.runner.pre_cycle() + self.state_machine.send(ManagerEvent.FINISH_PRE_SEGMENT) + + def post_segment(self, segments_data: RunSegmentData_) -> None: + self.state_machine.send(ManagerEvent.START_POST_SEGMENT) + self.runner.post_cycle(segments_data) + self.state_machine.send(ManagerEvent.FINISH_POST_SEGMENT) + def run_cycle( self, @@ -476,33 +733,24 @@ def run_cycle( """ - logger.info("Running simulation cycle") - - if runner_opts is None: - runner_opts = {} + self.state_machine.send(ManagerEvent.START_CYCLE) + # run the runner pre-cycle hook start = time.time() - - logger.info("Running Runner.pre_cycle hook") - self.runner.pre_cycle( - **runner_opts, - ) - + self.pre_segment() end = time.time() - runner_precycle_time = end - start - logger.info(f"Precycle time: {runner_precycle_time}") + presegment_time = end - start + logger.info(f"Presegment time: {presegment_time}") # run the segment start = time.time() - logger.info("Running state propagation segment") - new_states = self.run_segment( + new_states, segments_data = self.run_segment( [walker.state for walker in walkers], n_segment_steps, cycle_idx, ) - logger.info("Finished state propagation segment") new_walkers = [ Walker( @@ -517,18 +765,14 @@ def run_cycle( sim_manager_segment_time = end - start logger.info(f"Segment duration: {sim_manager_segment_time}") - runner_splits = self.runner.get_last_cycle_segments_split_times() - - logger.info("Running Runner.post_cycle hook") # run post-cycle hook start = time.time() - self.runner.post_cycle() + self.post_segment(segments_data) end = time.time() - runner_postcycle_time = end - start - - logger.info(f"Post cycle duration: {runner_postcycle_time}") + post_segment_time = end - start + logger.info(f"Post segment duration: {post_segment_time}") # boundary conditions should be optional; @@ -541,12 +785,15 @@ def run_cycle( bc_time = 0.0 if self.boundary_conditions is not None: logger.info("Boundary conditions were provided, applying.") + + self.state_machine.send(ManagerEvent.START_BC_WARPING) + # apply rules of boundary conditions and warp walkers through space start = time.time() - logger.info("Starting boundary conditions calculations") bc_results = self.boundary_conditions.warp_walkers(new_walkers, cycle_idx) end = time.time() bc_time = end - start + self.state_machine.send(ManagerEvent.FINISH_BC_WARPING) # warping results warped_walkers = bc_results[0] @@ -560,16 +807,20 @@ def run_cycle( logger.info(f"Returned warp record in cycle {cycle_idx}") # resample walkers - logger.info("Starting resampler phase.") + self.state_machine.send(ManagerEvent.START_RESAMPLING) start = time.time() resampling_results = self.resampler.resample(warped_walkers) + self.state_machine.send(ManagerEvent.FINISH_RESAMPLING) + end = time.time() resampling_time = end - start logger.info(f"Resampling duration: {resampling_time}") + self.state_machine.send(ManagerEvent.GENERATE_REPORT) + resampled_walkers = resampling_results[0] resampling_data = resampling_results[1] resampler_data = resampling_results[2] @@ -611,10 +862,11 @@ def run_cycle( "n_segment_steps": n_segment_steps, "resampled_walkers": resampled_walkers, # timings - "runner_precycle_time": runner_precycle_time, - "runner_postcycle_time": runner_postcycle_time, + "runner_precycle_time": presegment_time, + "runner_postcycle_time": post_segment_time, "sim_manager_segment_overhead_time": sim_manager_segment_overhead_time, - "runner_splits_time": runner_splits, + # TODO: fix this + "runner_splits_time": segments_data, "worker_segment_times": seg_times, "cycle_sim_manager_segment_time": sim_manager_segment_time, "cycle_runner_time": sim_manager_segment_time, @@ -624,18 +876,23 @@ def run_cycle( self._last_report = report - logger.info("Starting reporting") + self.state_machine.send(ManagerEvent.START_REPORTING) + # report results to the reporters for reporter in self.reporters: logger.info(f"Reporting with reporter: {reporter}") reporter.report(**report) + self.state_machine.send(ManagerEvent.FINISH_REPORTING) + # run the simulation monitor to get metrics on everything if self.monitor is not None: - logger.info("Running cycle monitoring") + self.state_machine.send(ManagerEvent.START_CYCLE_MONITORING) self.monitor.cycle_monitor(self, resampled_walkers) + self.state_machine.send(ManagerEvent.FINISH_CYCLE_MONITORING) + + self.state_machine.send(ManagerEvent.FINISH_CYCLE) - logger.info("Done: returning walkers") return resampled_walkers, (self.runner, self.boundary_conditions, self.resampler) def run_simulation( @@ -671,8 +928,8 @@ def run_simulation( """ - logger.info("Running simulation init hook") - self.init(continue_run=continue_run_idx) + self.state_machine.send(ManagerEvent.START_PRE_SIM) + if type(segment_lengths) == int: logger.info("Single number of steps provided for simulation, using this for all cycles.") @@ -680,23 +937,23 @@ def run_simulation( walkers = self.init_walkers - logger.info("Starting main simulation loop over cycles") + self.init(continue_run=continue_run_idx) + + self.state_machine.send(ManagerEvent.START_SIM) # the main cycle loop for cycle_idx in range(n_cycles): + logger.info(f"Running cycle: {cycle_idx}") walkers, filters = self.run_cycle( walkers, segment_lengths[cycle_idx], cycle_idx, ) logger.info(f"Finished running cycle: {cycle_idx}") - # run the simulation monitor to get metrics on everything - if self.monitor is not None: - logger.info("Running monitoring cycle_monitor hook") - self.monitor.cycle_monitor(self, walkers) + self.state_machine.send(ManagerEvent.FINISH_SIMULATION) - logger.info("Running simulation cleanup") self.cleanup() - logger.info("Simulation cleanup complete") + + self.state_machine.send(ManagerEvent.SHUTDOWN) return walkers, copy.deepcopy(tuple(filters)) @@ -723,12 +980,15 @@ def run_simulation_by_time( """ + self.state_machine.send(ManagerEvent.START_PRE_SIM) + start_time = time.time() logger.info(f"Simulation start time: {start_time}") - logger.info("Running simulation init hook") self.init(continue_run=continue_run_idx) + self.state_machine.send(ManagerEvent.START_SIM) + cycle_idx = 0 walkers = self.init_walkers # run until time is elapsed, but guarantee to run at least one cycle @@ -745,15 +1005,14 @@ def run_simulation_by_time( "ending cycle {} at time {}".format(cycle_idx, time.time() - start_time) ) - # run the simulation monitor to get metrics on everything - if self.monitor is not None: - logger.info("Running cycle_monitor hook") - self.monitor.cycle_monitor(self, walkers) - cycle_idx += 1 + self.state_machine.send(ManagerEvent.FINISH_SIMULATION) + logger.info("Running simulation cleanup") self.cleanup() logger.info("Simulation cleanup complete") + self.state_machine.send(ManagerEvent.SHUTDOWN) + return walkers, filters diff --git a/tests/integration/test_openmm/test_sim_manager.py b/tests/integration/test_openmm/test_sim_manager.py index b421fc38..8e27c4be 100644 --- a/tests/integration/test_openmm/test_sim_manager.py +++ b/tests/integration/test_openmm/test_sim_manager.py @@ -4,7 +4,7 @@ import openmm import openmm.unit from wepy.walker import Walker -from wepy.runners.openmm import OpenMMRunner, OpenMMState, HeartBeatLoggingReporterFactory +from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState, HeartBeatLoggingReporterFactory from wepy.work_mapper.serial import SerialMapper from wepy.work_mapper.openmm import ( OpenMMSerialWorkMapperFactory, @@ -27,7 +27,7 @@ def test_serial_mapper(): STEP_SIZE, ) - runner = OpenMMRunner( + runner_factory = OpenMMRunnerFactory( system=lj_sys.system, topology=lj_sys.topology, integrator=integrator, @@ -60,7 +60,7 @@ def test_serial_mapper(): sim_manager = Manager( init_walkers=init_walkers, - runner=runner, + runner_factory=runner_factory, resampler=NoResampler(), work_mapper_factory=OpenMMSerialWorkMapperFactory( platform="Reference", @@ -74,7 +74,7 @@ def test_serial_mapper(): sim_manager = Manager( init_walkers=init_walkers, - runner=runner, + runner_factory=runner_factory, resampler=NoResampler(), work_mapper_factory=OpenMMSerialWorkMapperFactory( platform="CPU", @@ -89,7 +89,7 @@ def test_serial_mapper(): sim_manager = Manager( init_walkers=init_walkers, - runner=runner, + runner_factory=runner_factory, resampler=NoResampler(), work_mapper_factory=OpenMMSerialWorkMapperFactory( platform="CPU", @@ -111,7 +111,7 @@ def test_proc_pool_mapper(): STEP_SIZE, ) - runner = OpenMMRunner( + runner_factory = OpenMMRunnerFactory( system=lj_sys.system, topology=lj_sys.topology, integrator=integrator, @@ -146,7 +146,7 @@ def test_proc_pool_mapper(): # number of processes that should be used. sim_manager = Manager( init_walkers=init_walkers, - runner=runner, + runner_factory=runner_factory, resampler=NoResampler(), work_mapper_factory=OpenMMProcPoolWorkMapperFactory( platform="Reference", @@ -168,7 +168,7 @@ def test_proc_pool_mapper(): cores_per_worker = (num_cores // num_walkers) sim_manager = Manager( init_walkers=init_walkers, - runner=runner, + runner_factory=runner_factory, resampler=NoResampler(), work_mapper_factory=OpenMMProcPoolWorkMapperFactory( platform="CPU", @@ -188,7 +188,7 @@ def test_proc_pool_mapper(): # enumerate the device IDs then. sim_manager = Manager( init_walkers=init_walkers, - runner=runner, + runner_factory=runner_factory, resampler=NoResampler(), work_mapper_factory=OpenMMProcPoolWorkMapperFactory( platform="CPU", @@ -212,7 +212,7 @@ def test_proc_pool_mapper(): # integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) -# runner = OpenMMRunner( +# runner_factory = OpenMMRunnerFactory( # system=lj_sys.system, # topology=lj_sys.topology, # integrator=integrator, @@ -240,7 +240,7 @@ def test_proc_pool_mapper(): # sim_manager = Manager( # init_walkers=init_walkers, -# runner=runner, +# runner_factory=runner_factory, # resampler=NoResampler(), # work_mapper_factory=OpenMMRayWorkMapperFactory( # platform="Reference", diff --git a/tests/unit/test_runners/test_mock.py b/tests/unit/test_runners/test_mock.py index 21f76174..0257cbb0 100644 --- a/tests/unit/test_runners/test_mock.py +++ b/tests/unit/test_runners/test_mock.py @@ -1,6 +1,6 @@ import pytest from wepy.runners.mock import MockRunner, MockState, MockError -from wepy.runners.runner import RunnerStatus, Runner, RunnerStateTransitionError, RunnerStateMachineError +from wepy.runners.runner import RunnerStatus, Runner, RunnerStateTransitionError, RunnerStateMachineError, RunnerEvent class TestMockRunner: @@ -22,12 +22,16 @@ def test_pre_cycle(self): runner.init() runner.pre_cycle() + assert runner.status == RunnerStatus.PRE_CYCLE with pytest.raises(RunnerStateTransitionError): runner.pre_cycle() runner.post_cycle(None) + + assert runner.status == RunnerStatus.POST_CYCLE runner.pre_cycle() + assert runner.status == RunnerStatus.PRE_CYCLE def test_post_cycle(self): runner = MockRunner() @@ -63,4 +67,3 @@ def test_run_segment(self): MockState(0), 10, )[0] == MockState(10) - diff --git a/tests/unit/test_runners/test_runner.py b/tests/unit/test_runners/test_runner.py index f3362055..146389a3 100644 --- a/tests/unit/test_runners/test_runner.py +++ b/tests/unit/test_runners/test_runner.py @@ -8,7 +8,6 @@ class Test_RunnerStateMachine: def test_validate_event(self): sm = RunnerStateMachine(state=RunnerStatus.PRE_INITIALIZATION) - # make sure the return and state are consistent assert sm.validate_event(RunnerEvent.INIT) sm = RunnerStateMachine(state=RunnerStatus.PRE_INITIALIZATION) diff --git a/tests/unit/test_sim_manager.py b/tests/unit/test_sim_manager.py index 366d5779..071fed21 100644 --- a/tests/unit/test_sim_manager.py +++ b/tests/unit/test_sim_manager.py @@ -1,18 +1,27 @@ +import copy + import pytest import attrs from wepy.walker import Walker from wepy.work_mapper.serial import SerialMapper from wepy.resampling.resamplers.noresampler import NoResampler -from wepy.runners.runner import NoRunner -from wepy.runners.mock import MockRunner, MockState, MockError - -from wepy.sim_manager import Manager +from wepy.runners.runner import NoRunner, RunnerStatus, RunSegmentData +from wepy.runners.mock import MockRunnerFactory, MockState, MockError + +from wepy.sim_manager import ( + Manager, + ManagerStateMachine, + ManagerStateTransitionError, + ManagerStateError, + ManagerStatus, + ManagerEvent, +) @pytest.fixture def sim_components() -> tuple[ list[Walker], - NoRunner, - MockRunner, + MockRunnerFactory, + NoResampler, ]: num_walkers = 4 @@ -27,14 +36,43 @@ def sim_components() -> tuple[ in range(num_walkers) ] - return init_walkers, MockRunner(), NoResampler() + return init_walkers, MockRunnerFactory(fail=False), NoResampler() + +class Test_ManagerStateMachine: + + def test_validate_event(self): + + sm = ManagerStateMachine() + assert sm.validate_event(ManagerEvent.START_PRE_SIM) + + sm = ManagerStateMachine() + with pytest.raises(ManagerStateTransitionError): + sm.validate_event(ManagerEvent.FINISH_SIMULATION) + + def test_send(self): + + sm = ManagerStateMachine(state=ManagerStatus.CONSTRUCTED) + # make sure the return and state are consistent + assert sm.send(ManagerEvent.START_PRE_SIM) == ManagerStatus.PRE_SIMULATION + assert sm.state == ManagerStatus.PRE_SIMULATION + + # default construction + assert ManagerStateMachine().state == ManagerStatus.CONSTRUCTED + + # TODO: this should be its own test as it is a behavioural test + + # Test all the transitions are what we expect + # sm = ManagerStateMachine(state=ManagerStatus.PRE_INITIALIZATION) + + -class TestManager: +class Test_Manager: def test___init__(self, sim_components): manager = Manager(*sim_components) + assert manager.status == ManagerStatus.CONSTRUCTED assert len(manager.reporters) == 0 assert manager.work_mapper_factory == SerialMapper assert not hasattr(manager, "work_mapper") @@ -42,39 +80,122 @@ def test___init__(self, sim_components): def test_init(self, sim_components): manager = Manager(*sim_components) - assert not manager.runner._initialized + + with pytest.raises(ManagerStateTransitionError): + manager.init() + + manager.state_machine.send(ManagerEvent.START_PRE_SIM) manager.init() + assert manager.status == ManagerStatus.INITIALIZED assert hasattr(manager, "work_mapper") - assert manager.runner._initialized + assert manager.runner.status == RunnerStatus.INITIALIZED + + with pytest.raises(ManagerStateTransitionError): + manager.init() + + def test_pre_segment(self, sim_components): + + manager = Manager(*sim_components) + + with pytest.raises(ManagerStateTransitionError): + manager.pre_segment() + + manager.state_machine.send(ManagerEvent.START_PRE_SIM) + + manager.init() + + manager.state_machine.send(ManagerEvent.START_SIM) + manager.state_machine.send(ManagerEvent.START_CYCLE) + + manager.pre_segment() + + assert manager.status == ManagerStatus.PRE_SEGMENT_FINISHED + assert manager.runner.status == RunnerStatus.PRE_CYCLE + def test_post_segment(self, sim_components): + + manager = Manager(*sim_components) + + with pytest.raises(ManagerStateTransitionError): + manager.post_segment(None) + + manager.state_machine.send(ManagerEvent.START_PRE_SIM) + manager.init() + manager.state_machine.send(ManagerEvent.START_SIM) + manager.state_machine.send(ManagerEvent.START_CYCLE) + manager.pre_segment() + + manager.run_segment( + [copy.deepcopy(walker.state) for walker in sim_components[0]], + 5, + 0, + ) + + manager.post_segment([ + RunSegmentData( + segment_split_time=2., + ) + for _ + in range(len(sim_components[0])) + ]) + + assert manager.status == ManagerStatus.POST_SEGMENT_FINISHED + assert manager.runner.status == RunnerStatus.POST_CYCLE + def test_cleanup(self, sim_components): manager = Manager(*sim_components) + # cleanup can be run in any state after init, but not after cleanup + + with pytest.raises(ManagerStateTransitionError): + manager.cleanup() + + manager.state_machine.send(ManagerEvent.START_PRE_SIM) manager.init() + assert manager.runner.status == RunnerStatus.INITIALIZED manager.cleanup() + + with pytest.raises(ManagerStateTransitionError): + manager.cleanup() def test_run_segment(self, sim_components): - init_walkers, runner, resampler = sim_components + init_walkers, runner_factory, resampler = sim_components manager = Manager(*sim_components) + manager.state_machine.send(ManagerEvent.START_PRE_SIM) manager.init() + # UGLY,TODO: that this is ugly as there might be some implicit + # state around that needs to go in tandem. So there should be + # some state coupled data that should be introduced to make + # this more robust + + # all the state changes needed for this to work + manager.state_machine.send(ManagerEvent.START_SIM) + manager.state_machine.send(ManagerEvent.START_CYCLE) + manager.pre_segment() + new_states = manager.run_segment( [walker.state for walker in init_walkers], 1, 0, ) + assert manager.status == ManagerStatus.SEGMENT_FINISHED # test if something fails manager = Manager( init_walkers, - MockRunner(fail=True), + MockRunnerFactory(fail=True), resampler, ) + manager.state_machine.send(ManagerEvent.START_PRE_SIM) manager.init() + manager.state_machine.send(ManagerEvent.START_SIM) + manager.state_machine.send(ManagerEvent.START_CYCLE) + manager.pre_segment() with pytest.raises(MockError): manager.run_segment( @@ -83,12 +204,16 @@ def test_run_segment(self, sim_components): 0, ) + assert manager.status == ManagerStatus.CLEANUP_FINISHED + def test_run_cycle(self, sim_components): init_walkers, runner, resampler = sim_components manager = Manager(*sim_components) + manager.state_machine.send(ManagerEvent.START_PRE_SIM) manager.init() + manager.state_machine.send(ManagerEvent.START_SIM) new_states = manager.run_cycle( init_walkers, @@ -96,13 +221,17 @@ def test_run_cycle(self, sim_components): 0, ) + assert manager.status == ManagerStatus.POST_CYCLE + # test if something fails manager = Manager( init_walkers, - MockRunner(fail=True), + MockRunnerFactory(fail=True), resampler, ) + manager.state_machine.send(ManagerEvent.START_PRE_SIM) manager.init() + manager.state_machine.send(ManagerEvent.START_SIM) with pytest.raises(MockError): manager.run_cycle( @@ -111,39 +240,47 @@ def test_run_cycle(self, sim_components): 0, ) + assert manager.status == ManagerStatus.CLEANUP_FINISHED + def test_run_simulation(self, sim_components): manager = Manager(*sim_components) - manager.init() new_walkers, _ = manager.run_simulation(2, 2) + assert manager.status == ManagerStatus.FINISHED + manager = Manager(*sim_components) new_walkers, _ = manager.run_simulation( 2, 2, continue_run_idx=0, ) + assert manager.status == ManagerStatus.FINISHED def test_run_simulation_by_time(self, sim_components): manager = Manager(*sim_components) - manager.init() new_walkers, _ = manager.run_simulation_by_time( 0.001, 2, ) + assert manager.status == ManagerStatus.FINISHED + manager = Manager(*sim_components) new_walkers, _ = manager.run_simulation_by_time( 0.001, 2, continue_run_idx=0, ) + assert manager.status == ManagerStatus.FINISHED # make sure it runs at least one cycle + manager = Manager(*sim_components) new_walkers, _ = manager.run_simulation_by_time( 0.0000001, 2, continue_run_idx=0, ) + assert manager.status == ManagerStatus.FINISHED From 045353df0411e8cc34e08c3bf0ed3e7a34d70a64 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 8 Dec 2025 23:32:28 -0500 Subject: [PATCH 065/143] update work mappers --- src/wepy/work_mapper/base.py | 13 +++++-- src/wepy/work_mapper/serial.py | 5 ++- .../test_openmm/test_sim_manager.py | 2 +- tests/unit/test_work_mapper/test_serial.py | 35 ++++++------------- 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/wepy/work_mapper/base.py b/src/wepy/work_mapper/base.py index bc57ecd0..bdf5301f 100644 --- a/src/wepy/work_mapper/base.py +++ b/src/wepy/work_mapper/base.py @@ -8,22 +8,29 @@ # Standard Library from wepy.walker import WalkerState +from wepy.runners.runner import RunSegmentData logger = logging.getLogger(__name__) WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) +RunSegmentData_ = TypeVar("RunSegmentData_", bound=RunSegmentData) -class WorkMapper(Protocol[WalkerState_]): +class WorkMapper(Protocol[WalkerState_, RunSegmentData_]): def init(self) -> None: ... def map( self, - task: Callable[[WalkerState_, int], WalkerState_], + task: Callable[[WalkerState_, int], tuple[WalkerState_, RunSegmentData_]], walker_states: list[WalkerState_], segment_lengths: list[int], - ) -> list[WalkerState_]: ... + ) -> list[ + tuple[ + WalkerState_, + RunSegmentData_, + ] + ]: ... def get_worker_segment_times(self) -> dict[int, list[float]] | None: ... diff --git a/src/wepy/work_mapper/serial.py b/src/wepy/work_mapper/serial.py index a6fa2ce9..c6a9fe62 100644 --- a/src/wepy/work_mapper/serial.py +++ b/src/wepy/work_mapper/serial.py @@ -7,15 +7,18 @@ import logging from wepy.walker import Walker, WalkerState +from wepy.runners.runner import RunSegmentData logger = logging.getLogger(__name__) WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) +RunSegmentData_ = TypeVar("RunSegmentData_", bound=RunSegmentData) class SerialMapper( Generic[ WalkerState_, + RunSegmentData_, ]): """Basic non-parallel reference implementation of a mapper.""" @@ -53,7 +56,7 @@ def map( ], walker_states: list[WalkerState_], segment_lengths: list[int], - ) -> list[WalkerState_]: + ) -> list[tuple[WalkerState_, RunSegmentData_]]: segment_times: list[float] = [] results: list[WalkerState_] = [] for task_idx, task_args in enumerate( diff --git a/tests/integration/test_openmm/test_sim_manager.py b/tests/integration/test_openmm/test_sim_manager.py index 8e27c4be..098efb48 100644 --- a/tests/integration/test_openmm/test_sim_manager.py +++ b/tests/integration/test_openmm/test_sim_manager.py @@ -99,7 +99,7 @@ def test_serial_mapper(): new_walkers, sim_components = sim_manager.run_simulation( n_cycles=1, - segment_lengths=10000000000, + segment_lengths=100, ) def test_proc_pool_mapper(): diff --git a/tests/unit/test_work_mapper/test_serial.py b/tests/unit/test_work_mapper/test_serial.py index b2ab79ee..66919473 100644 --- a/tests/unit/test_work_mapper/test_serial.py +++ b/tests/unit/test_work_mapper/test_serial.py @@ -1,9 +1,6 @@ import functools import attrs from wepy.walker import Walker, WalkerState -from wepy.interface import ( - WorkMapperFactoryArgs, -) from wepy.work_mapper.serial import SerialMapper # some minimal definitions for testing a concrete work mapper @@ -13,59 +10,49 @@ class RizzWalkerState: rizz: int -def rizz_run(walker_state: RizzWalkerState, delta: int, multiple: int) -> RizzWalkerState: +def rizz_run(walker_state: RizzWalkerState, segment_length: int) -> RizzWalkerState: return attrs.evolve( walker_state, - rizz=(walker_state.rizz + delta) * multiple + rizz=(walker_state.rizz + segment_length), ) @attrs.define class RizzTask: - delta: int multiple: int - def __call__(self, state: RizzWalkerState) -> RizzWalkerState: + def __call__(self, state: RizzWalkerState, segment_length: int) -> RizzWalkerState: - return rizz_run(state, delta=self.delta, multiple=self.multiple) + return rizz_run(state, segment_length=segment_length, multiple=self.multiple) def test_rizz_walker(): assert rizz_run( RizzWalkerState(rizz=1), 1, - 2, - ) == RizzWalkerState(rizz=4) + ) == RizzWalkerState(rizz=2) class TestMapper: def test_map(self): - mapper = SerialMapper(WorkMapperFactoryArgs(num_workers=0)) + mapper = SerialMapper() mapper.init() - + assert mapper.map( - [ - RizzTask( - *args, - ) - for args - in zip( - [1, 2, 2], - [2, 2, 2], - ) - ], + rizz_run, [ RizzWalkerState(1), RizzWalkerState(1), RizzWalkerState(2), ], + [1, 2, 2], ) == [ + RizzWalkerState(2), + RizzWalkerState(3), RizzWalkerState(4), - RizzWalkerState(6), - RizzWalkerState(8), ] assert len(mapper.get_worker_segment_times()[0]) == 3 From 96c9d03794ea94ac6c746e8c8dd658ac0a099eac Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 8 Dec 2025 23:32:38 -0500 Subject: [PATCH 066/143] use spawn for revo multiprocessing Pool using fork can cause deadlocks in subprocesses and reliability is key so we stop that. --- src/wepy/resampling/resamplers/revo.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 9ba75574..012cdfe2 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -129,7 +129,6 @@ def __init__( merge_dist=None, char_dist=None, distance=None, - init_state=None, weights=True, merge_alg="pairs", pmin=1e-12, @@ -202,7 +201,6 @@ def __init__( assert merge_dist is not None, "Merge distance must be given." assert distance is not None, "Distance object must be given." assert char_dist is not None, "Characteristic distance value (d0) must be given" - assert init_state is not None, "An initial state must be given." # ln(probability_min) self.lpmin = np.log(self.pmin / 100) @@ -660,8 +658,11 @@ def _all_to_all_distance(self, walkers): handlers = list(logging.getLogger().handlers) listener = logging.handlers.QueueListener(log_queue, *handlers) listener.start() - - with mp.Pool( + + # NOTE: Must use spawn here, otherwise there are problems + # with deadlocking in the sub-processes + mp_ctx = mp.get_context(method="spawn") + with mp_ctx.Pool( self.num_proc, initializer=proc_pool_worker_setup, initargs=(log_queue,), From a78c88f4c35dc038441b83e00353f9ed7e187fd4 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 8 Dec 2025 23:33:26 -0500 Subject: [PATCH 067/143] add OpenMMRunnerFactory test --- src/wepy/runners/openmm/runner.py | 115 ------------------ .../test_runners/test_openmm/test_runner.py | 16 +++ 2 files changed, 16 insertions(+), 115 deletions(-) diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index 56296ffe..b63c8669 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -347,118 +347,3 @@ def __call__(self) -> OpenMMRunner: get_state_keys=self.get_state_keys, openmm_reporter_factories=self.openmm_reporter_factories, ) - - -# class OpenMMCPUWorker(Worker): -# """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). - -# This is intended to be used with the wepy.work_mapper.WorkerMapper -# work mapper class. - -# This class must be used in order to ensure OpenMM runs jobs on the -# appropriate GPU device. - -# """ - -# NAME_TEMPLATE = "OpenMMCPUWorker-{}" -# """The name template the worker processes are named to substituting in -# the process number.""" - -# DEFAULT_NUM_THREADS = 1 - -# def __init__(self, *args, **kwargs): -# if "num_threads" not in kwargs: -# num_threads = self.DEFAULT_NUM_THREADS -# else: -# num_threads = kwargs.pop("num_threads") - -# super().__init__(*args, num_threads=num_threads, **kwargs) - -# def run_task(self, task): -# # documented in superclass - -# # make the platform kwargs dictionary -# platform_options = {"Threads": str(self.attributes["num_threads"])} - -# # run the task and pass in the DeviceIndex for OpenMM to -# # assign work to the correct GPU -# return task(platform_kwargs=platform_options) - - -# class OpenMMGPUWorker(Worker): -# """Worker for OpenMM GPU simulations (CUDA or OpenCL platforms). - -# This is intended to be used with the wepy.work_mapper.WorkerMapper -# work mapper class. - -# This class must be used in order to ensure OpenMM runs jobs on the -# appropriate GPU device. - -# """ - -# NAME_TEMPLATE = "OpenMMGPUWorker-{}" -# """The name template the worker processes are named to substituting in -# the process number.""" - -# def run_task(self, task): -# # get the platform -# platform = self.mapper_attributes["platform"] - -# # get the device index from the attributes -# device_id = self.mapper_attributes["device_ids"][self._worker_idx] - -# # make the platform kwargs dictionary -# platform_options = {"DeviceIndex": str(device_id)} - -# logger.info(f"platform={platform}, platform_options={platform_options}") - -# return task( -# platform=platform, -# platform_kwargs=platform_options, -# ) - - -# class OpenMMCPUWalkerTaskProcess(WalkerTaskProcess): -# NAME_TEMPLATE = "OpenMM_CPU_Walker_Task-{}" - -# def run_task(self, task): -# print("CPU Walker Task ---->", self.mapper_attributes, task, task.func) -# if "num_threads" in self.mapper_attributes: -# num_threads = self.mapper_attributes["num_threads"] - -# # make the platform kwargs dictionary -# platform_options = {"Threads": str(num_threads)} - -# logger.info(f"Threads={num_threads}") - -# else: -# platform_options = {} - -# return task( -# platform_kwargs=platform_options, -# ) - - -# class OpenMMGPUWalkerTaskProcess(WalkerTaskProcess): -# NAME_TEMPLATE = "OpenMM_GPU_Walker_Task-{}" - -# def run_task(self, task): -# logger.info(f"Starting to run a task as worker {self._worker_idx}") - -# logger.info(f"GPU Walker Task ----> {self.mapper_attributes}") -# # get the platform -# platform = self.mapper_attributes["platform"] - -# # get the device index from the attributes -# device_id = self.mapper_attributes["device_ids"][self._worker_idx] - -# # make the platform kwargs dictionary -# platform_options = {"DeviceIndex": str(device_id)} - -# logger.info(f"platform={platform}, platform_options={platform_options}") - -# return task( -# platform=platform, -# platform_kwargs=platform_options, -# ) - diff --git a/tests/unit/test_runners/test_openmm/test_runner.py b/tests/unit/test_runners/test_openmm/test_runner.py index 33e6097f..2299101d 100644 --- a/tests/unit/test_runners/test_openmm/test_runner.py +++ b/tests/unit/test_runners/test_openmm/test_runner.py @@ -11,10 +11,12 @@ RunnerStatus, RunnerStateTransitionError, RunnerStateError, + ) from wepy.runners.openmm.runner import ( OpenMMRunner, OpenMMRunnerSegmentData, + OpenMMRunnerFactory, ) from wepy.runners.openmm.logger import StepIntervalLoggingReporter @@ -283,3 +285,17 @@ def test_post_cycle(self, runner_components): runner.post_cycle([segment_data]) assert runner.status == RunnerStatus.POST_CYCLE + +def test_OpenMMRunnerFactory(runner_components): + + system, topology, integrator = runner_components + + # NOTE: no need to copy at this level because the factory handles + # that for you + omm_factory = OpenMMRunnerFactory( + system=system, + topology=topology, + integrator=integrator, + ) + + assert isinstance(omm_factory, OpenMMRunner) From de35c926965b9882e2a0d69663d879512760051c Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 8 Dec 2025 23:33:45 -0500 Subject: [PATCH 068/143] update LJ Pair test system metric --- src/wepy_tools/systems/lennard_jones.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/wepy_tools/systems/lennard_jones.py b/src/wepy_tools/systems/lennard_jones.py index 3f9d25ee..1f61692b 100644 --- a/src/wepy_tools/systems/lennard_jones.py +++ b/src/wepy_tools/systems/lennard_jones.py @@ -5,8 +5,11 @@ import openmm.unit from scipy.spatial.distance import euclidean +import attrs + # First Party Library from wepy.resampling.distances.distance import Distance +from wepy.runners.openmm import OpenMMState class LennardJonesPair: @@ -97,15 +100,18 @@ def __init__(self, mass=39.9 * openmm.unit.amu, sigma=3.350 * openmm.unit.angstr topology.addAtom('Ar', element, residue) self.topology = topology +@attrs.define +class PairDistanceImage: + positions: np.typing.ArrayLike class PairDistance(Distance): def __init__(self, metric=euclidean): self.metric = metric - def image(self, state): - return state["positions"] + def image(self, state: OpenMMState) -> PairDistanceImage: + return state.positions - def image_distance(self, image_a, image_b): + def image_distance(self, image_a: PairDistanceImage, image_b: PairDistanceImage) -> float: dist_a = self.metric(image_a[0], image_a[1]) dist_b = self.metric(image_b[0], image_b[1]) From 6fe98030618175e581339206ca6014fd17871e02 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 8 Dec 2025 23:33:53 -0500 Subject: [PATCH 069/143] add realistic example of LJ pair The alanine dipeptide case is failing due to some problem with that system so I needed one that won't fail for those reasons. --- .../integration/test_openmm/test_realistic.py | 123 ++++++++++++++++-- 1 file changed, 109 insertions(+), 14 deletions(-) diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index 98c3d146..53d5100a 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -12,31 +12,127 @@ import mdtraj # TODO: use the high-level API imports from wepy.walker import Walker -from wepy.runners.openmm import OpenMMRunner, OpenMMState +from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState, HeartBeatLoggingReporterFactory from wepy.sim_manager import Manager from wepy.work_mapper.openmm import OpenMMProcPoolWorkMapperFactory from wepy.resampling.resamplers.revo import REVOResampler from wepy.util.mdtraj import mdtraj_to_json_topology +from wepy.runners.openmm.logger import HeartBeatLoggingReporter +from wepy.resampling.resamplers.noresampler import NoResampler from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideExplicit, AlanineDipeptideRamachandranDistance +from wepy_tools.systems.lennard_jones import LennardJonesPair, PairDistance +def test_lennard_jones_revo_procpool(): + + test_sys = LennardJonesPair() + + integrator = openmm.LangevinIntegrator(300.0, 0.1, 0.002) + + runner_factory = OpenMMRunnerFactory( + system=test_sys.system, + topology=test_sys.topology, + integrator=integrator, + # For this test we do want heart beat at shorter interval + openmm_reporter_factories=[ + # heart beat every step + HeartBeatLoggingReporterFactory(step_interval=2) + ] + ) + + num_walkers = 4 + + init_state = OpenMMState.from_dwim( + positions=test_sys.positions, + ) + + # TODO: remove the need to deepcopy and have the components make + # their own copies if necessary + walker_states = [ + copy.deepcopy(init_state) + for _ + in range(num_walkers) + ] + + init_walker_weight = 1 / num_walkers + init_walkers = [ + Walker( + state=walker_state, + weight=init_walker_weight, + ) + for walker_state + in walker_states + ] + + # number of walkers if less then total cores, otherwise the total + # number of cores + num_cores = len(psutil.Process().cpu_affinity()) + if num_cores < num_walkers: + num_workers = num_cores + cores_per_worker = 1 + else: + num_workers = num_walkers + cores_per_worker = (num_workers // num_walkers) + + json_top = mdtraj_to_json_topology( + mdtraj.Topology.from_openmm(test_sys.topology) + ) + + distance_metric = PairDistance() + + resampler = REVOResampler( + merge_dist=4, + char_dist=0.1, + distance=distance_metric, + num_proc=num_workers, + # num_proc=1, + ) + + sim_manager = Manager( + init_walkers=init_walkers, + runner_factory=runner_factory, + # DEBUG + # resampler=resampler, + resampler=NoResampler(), + work_mapper_factory=OpenMMProcPoolWorkMapperFactory( + # DEBUG + platform="Reference", + num_procs=1, + # platform="CPU", + # num_procs=num_workers, + # global_platform_properties={"Threads" : "1"}, + # # global_platform_properties={"Threads" : str(cores_per_worker)}, + ), + ) + + new_walkers, sim_components = sim_manager.run_simulation( + n_cycles=1, + segment_lengths=10, + ) + def test_alanine_dipeptide_revo_procpool(): - ala_sys = AlanineDipeptideExplicit() + # ALERT: Using this system will cause the simulation to stall + test_sys = AlanineDipeptideExplicit() - integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) + integrator = openmm.LangevinIntegrator(300.0, 0.1, 0.002) - runner = OpenMMRunner( - system=ala_sys.system, - topology=ala_sys.topology, + runner_factory = OpenMMRunnerFactory( + system=test_sys.system, + topology=test_sys.topology, integrator=integrator, + # For this test we do want heart beat at shorter interval + openmm_reporter_factories=[ + # heart beat every step + HeartBeatLoggingReporterFactory(step_interval=2) + ] ) num_walkers = 4 init_state = OpenMMState.from_dwim( - positions=ala_sys.positions, + positions=test_sys.positions, ) # TODO: remove the need to deepcopy and have the components make @@ -68,7 +164,7 @@ def test_alanine_dipeptide_revo_procpool(): cores_per_worker = (num_workers // num_walkers) json_top = mdtraj_to_json_topology( - mdtraj.Topology.from_openmm(ala_sys.topology) + mdtraj.Topology.from_openmm(test_sys.topology) ) distance_metric = AlanineDipeptideRamachandranDistance(json_top) @@ -77,15 +173,13 @@ def test_alanine_dipeptide_revo_procpool(): merge_dist=4, char_dist=0.1, distance=distance_metric, - # DEBUG - # num_proc=num_workers, - num_proc=1, + num_proc=num_workers, ) sim_manager = Manager( init_walkers=init_walkers, - runner=runner, - resampler=resampler, + runner_factory=runner_factory, + resampler=NoResampler(), work_mapper_factory=OpenMMProcPoolWorkMapperFactory( platform="CPU", num_procs=num_workers, @@ -95,5 +189,6 @@ def test_alanine_dipeptide_revo_procpool(): new_walkers, sim_components = sim_manager.run_simulation( n_cycles=1, - segment_lengths=100, + segment_lengths=10, ) + From c4bb360545b303a991aafb277f757c9515fc461f Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 9 Dec 2025 15:42:06 -0500 Subject: [PATCH 070/143] new ala test system, cleanup The Alanine dipeptide system builder from openmmtools was causing issues with the simulation. It probably generated contexts or something. Switched to using some XML serialized systems I had laying around. Added a lot of extra logging in the runner on the configuration and simulation system state during the lifecycle for simpler debugging during the simulations. --- src/wepy/runners/openmm/runner.py | 184 +- src/wepy/runners/openmm/state.py | 67 - src/wepy/util/openmm.py | 21 + src/wepy_tools/systems/alanine_dipeptide.py | 94 - .../alanine_dipeptide_explicit/.gitignore | 4 - .../data/alanine_dipeptide_explicit/BUILD | 3 - .../alanine-dipeptide-explicit.state.omm.xml | 2279 ++++ .../alanine-dipeptide-explicit.system.omm.xml | 9275 +++++++++++++++++ .../alanine-dipeptide-explicit.top.json | 1 + .../alanine-dipeptide.crd.dvc | 5 - .../alanine-dipeptide.pdb.dvc | 5 - .../alanine-dipeptide.prmtop.dvc | 5 - .../generate-pdb.py | 21 - .../alanine_dipeptide_explicit/leap.log.dvc | 5 - .../data/alanine_dipeptide_explicit/run.sh | 15 - .../alanine_dipeptide_explicit/setup.leap.in | 23 - .../integration/test_openmm/test_realistic.py | 57 +- 17 files changed, 11781 insertions(+), 283 deletions(-) delete mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/.gitignore delete mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/BUILD create mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide-explicit.state.omm.xml create mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide-explicit.system.omm.xml create mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide-explicit.top.json delete mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.crd.dvc delete mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.pdb.dvc delete mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.prmtop.dvc delete mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/generate-pdb.py delete mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/leap.log.dvc delete mode 100755 src/wepy_tools/systems/data/alanine_dipeptide_explicit/run.sh delete mode 100644 src/wepy_tools/systems/data/alanine_dipeptide_explicit/setup.leap.in diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index b63c8669..6e18e051 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -34,6 +34,7 @@ # First Party Library from wepy.runners.runner import Runner, RunnerStatus, RunSegmentData, RunnerStateMachine, RunnerEvent, RunnerStateError from wepy.util.util import box_vectors_to_lengths_angles +from wepy.util.openmm import triclinic_volume_vec3_quantity, format_box_vectors_line from wepy.walker import WalkerState from .state import OpenMMState, OpenMMStateWrapper, get_context_state from .logger import HeartBeatLoggingReporterFactory, LoggingReporterFactory @@ -74,6 +75,65 @@ class OpenMMRunnerSegmentData(RunSegmentData): segment_split_time: float openmm_segment_split_time: OpenMMRunnerSegmentSplitTime +def _report_simulation(simulation: openmm.app.Simulation) -> tuple[ + openmm.unit.Quantity, + tuple[openmm.unit.Quantity, openmm.unit.Quantity, openmm.unit.Quantity], + openmm.unit.Quantity, + openmm.unit.Quantity, + openmm.unit.Quantity, +]: + + # log some info on the constructed simulation + _step_count = simulation.context.getStepCount() + _time = simulation.context.getTime() + _num_molecules = len(simulation.context.getMolecules()) + _platform = simulation.context.getPlatform() + _platform_name = _platform.getName() + _platform_prop_names = _platform.getPropertyNames() + _props = { + name: _platform.getPropertyValue( + simulation.context, + name, + ) + for name in _platform_prop_names + } + _openmm_version = _platform.getOpenMMVersion() + logger.info(f"Simulation Context: current_step={_step_count}, sampling_time={_time}, num_molecules={_num_molecules}") + logger.info(f"Simulation Context Platform: openmm_version={_openmm_version}, name={_platform_name}, properties={_props}") + + # report on the initial state as well + _init_state = simulation.context.getState( + positions=False, + velocities=True, + forces=False, + energy=True, + ) + _velocities = _init_state.getVelocities() + _vel0 = _velocities[0] + _vel0_mag = _vel0.value_in_unit(_vel0.unit) + _vels_zeroed = ( + np.isclose(_vel0_mag[0], 0.) and np.isclose(_vel0_mag[1], 0.) and np.isclose(_vel0_mag[2], 0.) + ) + + if _vels_zeroed: + logger.info("Context state velocities are zeroed.") + else: + logger.info("Context state velocities are set.") + + _pot_e = _init_state.getPotentialEnergy() + _kin_e = _init_state.getKineticEnergy() + _tot_e = _pot_e + _kin_e + + logger.info(f"Context state energies: kinetic={_kin_e}, potential={_pot_e}, total={_tot_e}") + + _box_volume = _init_state.getPeriodicBoxVolume() + _bvs = _init_state.getPeriodicBoxVectors() + _bvs_line = format_box_vectors_line(_bvs) + + logger.info(f"Context state box: volume={_box_volume}, vectors={_bvs_line}") + + return _box_volume, _bvs, _pot_e, _kin_e, _tot_e + # the runner for the simulation which runs the actual dynamics class OpenMMRunner(Runner): """Runner for OpenMM simulations.""" @@ -81,8 +141,6 @@ class OpenMMRunner(Runner): system: openmm.System topology: openmm.app.Topology integrator: openmm.Integrator - platform_name: str | None - global_platform_kwargs: PlatformKwargs | None enforce_box: bool get_state_keys: frozenset[str] openmm_reporter_factories: list[LoggingReporterFactory] | None @@ -98,8 +156,6 @@ def __init__( system: openmm.System, topology: openmm.app.Topology, integrator: openmm.Integrator, - platform_name: str | None = None, - global_platform_kwargs: PlatformKwargs | None = None, enforce_box: bool = False, get_state_keys: frozenset[str] = GET_STATE_DEFAULT_KEYS, openmm_reporter_factories: list[LoggingReporterFactory] | None = None, @@ -108,8 +164,6 @@ def __init__( self.system = system self.topology = topology self.integrator = integrator - self.platform_name = platform_name - self.global_platform_args = global_platform_kwargs self.enforce_box = enforce_box self.get_state_keys = get_state_keys @@ -122,6 +176,70 @@ def __init__( self.state_machine = RunnerStateMachine() + self._report_configuration() + + def _report_configuration(self) -> None: + logger.info("Details of OpenMMRunner initial configuration") + + logger.info( + f"OpenMM logger reporters: {','.join(str(v) for v in self.openmm_reporter_factories)}" + ) + + logger.info(f"Enforce PBCs in getState: {self.enforce_box}") + logger.info(f"Get state keys: {self.get_state_keys}") + + # system + num_particles = self.system.getNumParticles() + num_forces = self.system.getNumForces() + uses_pbcs = self.system.usesPeriodicBoundaryConditions() + default_bvs = self.system.getDefaultPeriodicBoxVectors() + + default_bv_volume = triclinic_volume_vec3_quantity(default_bvs) + default_bv_line = format_box_vectors_line(default_bvs) + + logger.info(f"System: num_particles={num_particles}, num_forces={num_forces}, uses_pbcs={uses_pbcs}") + logger.info(f"System default box vectors: volume={default_bv_volume}, vectors={default_bv_line}") + + # topology + num_chains = self.topology.getNumChains() + num_residues = self.topology.getNumResidues() + num_atoms = self.topology.getNumAtoms() + num_bonds = self.topology.getNumBonds() + + top_bvs = self.topology.getPeriodicBoxVectors() + + + logger.info( + f"Topology: num_chains={num_chains}, num_residues={num_residues}, num_atoms={num_atoms}, num_bonds={num_bonds}" + ) + if top_bvs is not None: + top_bv_volume = triclinic_volume_vec3_quantity(default_bvs) + bv_line = format_box_vectors_line(default_bvs) + logger.info( + f"Topology box vectors: volume={top_bv_volume} vectors={bv_line}" + ) + else: + logger.info("Topology box vectors not set.") + + chain_ids = [ + chain.id + for chain + in self.topology.chains() + ] + logger.info(f"Topology Chains (IDs): {','.join(chain_ids)}") + + for chain in self.topology.chains(): + num_residues = len(list(chain.residues())) + num_atoms = len(list(chain.atoms())) + logger.info( + f"Chain {chain.index}: id={chain.id}, num_residues={num_residues}, num_atoms={num_atoms}" + ) + + # integrator + # UGLY: just dump the XML for simplicity + integrator_xml = openmm.XmlSerializer.serialize(self.integrator).replace("\n", " ") + logger.info(f"Integrator: {integrator_xml}") + @property def status(self) -> RunnerStatus: return self.state_machine.state @@ -146,6 +264,7 @@ def pre_cycle( self.state_machine.send(RunnerEvent.PRE_CYCLE) + def run_segment( self, @@ -214,7 +333,7 @@ def run_segment( # get the platform by its name to use platform = openmm.Platform.getPlatformByName(platform_name) - logger.info(f"Platform object created: {platform}") + logger.info(f"Platform instantiated.") # set properties from the kwargs if they apply to the platform for key, value in platform_kwargs.items(): @@ -261,7 +380,6 @@ def run_segment( logger.info("Generating openmm.State from input OpenMMState") state_wrapper = walker_state.to_state_wrapper() - # set in the context logger.info("Setting openmm.State into current context") simulation.context.setState(state_wrapper.state) @@ -269,7 +387,10 @@ def run_segment( gen_sim_end = time.time() gen_sim_time = gen_sim_end - gen_sim_start - logger.info("Time to generate the system: {}".format(gen_sim_time)) + logger.info(f"Time to generate the system: {gen_sim_time:.4f} s") + + logger.info("Information on initial simulation state") + before_volume, before_bvs, before_pot_e, before_kin_e, before_tot_e = _report_simulation(simulation) # actually run the simulation @@ -282,7 +403,42 @@ def run_segment( steps_end = time.time() steps_time = steps_end - steps_start - logger.info(f"Time to run {segment_length} sim steps: {steps_time} s") + logger.info(f"Time to run {segment_length} sim steps: {steps_time:.4f} s") + + logger.info("Information on final simulation state") + after_volume, after_bvs, after_pot_e, after_kin_e, after_tot_e = _report_simulation(simulation) + + _before_lengths = ( + np.linalg.norm(before_bvs[0]), + np.linalg.norm(before_bvs[1]), + np.linalg.norm(before_bvs[2]), + ) + _after_lengths = ( + np.linalg.norm(after_bvs[0]), + np.linalg.norm(after_bvs[1]), + np.linalg.norm(after_bvs[2]), + ) + + _delta_lengths = [ + after_length - before_length + for after_length, before_length + in zip(_after_lengths, _before_lengths, strict=True) + ] + _delta_lengths_line = f"({_delta_lengths[0]}, {_delta_lengths[1]}, {_delta_lengths[2]})" + + # report on the change in energies and box volume + _delta_volume = after_volume - before_volume + _delta_pot_e = after_pot_e - before_pot_e + _delta_kin_e = after_kin_e - before_kin_e + _delta_tot_e = after_tot_e - before_tot_e + + logger.info( + f"State changes in Unitcell: box_volume={_delta_volume}, lengths={_delta_lengths_line}, " + ) + + logger.info( + f"State changes in Energy: potential_E={_delta_pot_e}, kinetic_E={_delta_kin_e}, total_E={_delta_tot_e}" + ) get_state_start = time.time() @@ -300,11 +456,11 @@ def run_segment( get_state_end = time.time() get_state_time = get_state_end - get_state_start - logger.info("Getting context state time: {}".format(get_state_time)) + logger.info(f"Getting context state time: {get_state_time:.4f} s") run_segment_end = time.time() run_segment_time = run_segment_end - run_segment_start - logger.info("Total internal run_segment time: {}".format(run_segment_time)) + logger.info(f"Total internal run_segment time: {run_segment_time:.4f} s") segment_data = OpenMMRunnerSegmentData( segment_split_time=run_segment_time, @@ -329,8 +485,6 @@ class OpenMMRunnerFactory: system: openmm.System topology: openmm.app.Topology integrator: openmm.Integrator - platform_name: str | None = None - global_platform_kwargs: PlatformKwargs | None = None enforce_box: bool = False get_state_keys: frozenset[str] = attrs.field(default=GET_STATE_DEFAULT_KEYS) openmm_reporter_factories: list[LoggingReporterFactory] | None = attrs.field(default=DEFAULT_OPENMM_REPORTER_FACTORIES) @@ -341,8 +495,6 @@ def __call__(self) -> OpenMMRunner: system=copy.deepcopy(self.system), topology=copy.deepcopy(self.topology), integrator=copy.deepcopy(self.integrator), - platform_name=self.platform_name, - global_platform_kwargs=self.global_platform_kwargs, enforce_box=self.enforce_box, get_state_keys=self.get_state_keys, openmm_reporter_factories=self.openmm_reporter_factories, diff --git a/src/wepy/runners/openmm/state.py b/src/wepy/runners/openmm/state.py index f1638091..a25aa139 100644 --- a/src/wepy/runners/openmm/state.py +++ b/src/wepy/runners/openmm/state.py @@ -394,70 +394,6 @@ def get_state_fields_present(sim_state: openmm.State) -> frozenset[StateFieldNam return frozenset(present_fields) -# def resolve_state_data_type_enum_values() -> dict[str, int]: -# enum_values = {} -# for our_name, enum_name in STATE_DATA_TYPE_ENUM_NAMES.items(): -# enum_values[our_name] = getattr(openmm.State, enum_name) - -# return enum_values - - -# # reversed since that is the order we check them in and is a frequent operation -# STATE_DATA_TYPE_ENUM_VALUES: list[tuple[str, int]] = list( -# sorted( -# [(k, v) for k, v in resolve_state_data_type_enum_values().items()], -# key=lambda x: x[1], -# reverse=True, -# ) -# ) - - -# def get_state_fields_present(sim_state: openmm.State) -> list[str]: -# """For a state returns a set of the field data types present in it.""" - -# flag_sum = sim_state.getDataTypes() - -# flag_fields: list[str] = [] -# flag_values: list[int] = [] -# flag_cum: int = flag_sum -# for field_name, flag_value in STATE_DATA_TYPE_ENUM_VALUES: -# if flag_value > flag_cum: -# continue -# elif flag_value == flag_cum: -# flag_fields.append(field_name) -# flag_values.append(flag_value) -# break - -# else: -# flag_fields.append(field_name) -# flag_values.append(flag_value) -# flag_cum -= flag_value - -# # double check they sum up -# assert sum(flag_values) == flag_sum - -# return flag_fields - - -# the Units objects that OpenMM uses internally and are returned from -# simulation data - -# TODO: this is never used and we only need the unit names. Its okay -# to use openmm.units here but other runners should use a units sytem -# like pint which is easier to install. So we should remove this since -# its not used. - -# UNITS = (('positions_unit', openmm.unit.nanometer), -# ('time_unit', openmm.unit.picosecond), -# ('box_vectors_unit', openmm.unit.nanometer), -# ('velocities_unit', openmm.unit.nanometer/openmm.unit.picosecond), -# ('forces_unit', openmm.unit.kilojoule / (openmm.unit.nanometer * openmm.unit.mole)), -# ('box_volume_unit', openmm.unit.nanometer), -# ('kinetic_energy_unit', openmm.unit.kilojoule / openmm.unit.mole), -# ('potential_energy_unit', openmm.unit.kilojoule / openmm.unit.mole), -# ) -# """Mapping of units identifiers to the corresponding openmm.units Unit objects.""" - # the names of the units from the units objects above. This is used # for saving them to files UNIT_NAMES: tuple[tuple[str, str], ...] = ( @@ -476,9 +412,6 @@ def get_state_fields_present(sim_state: openmm.State) -> frozenset[StateFieldNam """Mapping of unit identifier strings to the serialized string spec of the unit.""" - - - ## Wrapper for a openmm.State class OpenMMStateWrapper(WalkerState): diff --git a/src/wepy/util/openmm.py b/src/wepy/util/openmm.py index 7f442cf8..d5f89a13 100644 --- a/src/wepy/util/openmm.py +++ b/src/wepy/util/openmm.py @@ -22,3 +22,24 @@ def vec3_to_array3d(vec3s: Iterable[openmm.Vec3]) -> np.typing.ArrayLike: return np.array(vs) + + +def triclinic_volume_vec3_quantity(box_vectors: list[openmm.unit.Quantity]) -> openmm.unit.Quantity: + + return np.dot(box_vectors[0], np.cross(box_vectors[1], box_vectors[2])) + +def format_box_vectors_line(box_vectors: list[openmm.unit.Quantity]) -> str: + unit = box_vectors[0].unit + + vec_strs = [] + for vec in box_vectors: + mags = [ + q.value_in_unit(q.unit) + for q in vec + ] + vec_s = f"{mags[0]:.3f}, {mags[1]:.3f}, {mags[2]:.3f}" + vec_strs.append(vec_s) + + s = f"({vec_strs[0]}) ({vec_strs[1]}) ({vec_strs[2]}) {unit}" + + return s diff --git a/src/wepy_tools/systems/alanine_dipeptide.py b/src/wepy_tools/systems/alanine_dipeptide.py index f6fb86d6..08adddb4 100644 --- a/src/wepy_tools/systems/alanine_dipeptide.py +++ b/src/wepy_tools/systems/alanine_dipeptide.py @@ -13,100 +13,6 @@ from wepy.resampling.distances.distance import Distance from wepy.util.mdtraj import traj_fields_to_mdtraj -DEFAULT_EWALD_ERROR_TOLERANCE = 1.0e-5 -DEFAULT_CUTOFF_DISTANCE = 10.0 * openmm.unit.angstroms -DEFAULT_SWITCH_WIDTH = 1.5 * openmm.unit.angstroms - -class AlanineDipeptideExplicit: - """Alanine dipeptide ff96 in TIP3P explicit solvent. - - Parameters - ---------- - constraints : optional, default=openmm.app.HBonds - rigid_water : bool, optional, default=True - nonbondedCutoff : Quantity, optional, default=9.0 * unit.angstroms - use_dispersion_correction : bool, optional, default=True - If True, the long-range disperson correction will be used. - nonbondedMethod : openmm.app nonbonded method, optional, default=app.PME - Sets the nonbonded method to use for the water box (one of app.CutoffPeriodic, app.Ewald, app.PME). - hydrogenMass : unit, optional, default=None - If set, will pass along a modified hydrogen mass for OpenMM to - use mass repartitioning. - cutoff : openmm.unit.Quantity with units compatible with angstroms, optional, default = DEFAULT_CUTOFF_DISTANCE - Cutoff distance - switch_width : openmm.unit.Quantity with units compatible with angstroms, optional, default = DEFAULT_SWITCH_WIDTH - switching function is turned on at cutoff - switch_width - If None, no switch will be applied (e.g. hard cutoff). - ewaldErrorTolerance : float, optional, default=DEFAULT_EWALD_ERROR_TOLERANCE - The Ewald or PME tolerance. - - Examples - -------- - - >>> alanine = AlanineDipeptideExplicit() - >>> (system, positions) = alanine.system, alanine.positions - """ - - def __init__( - self, - constraints=openmm.app.HBonds, - rigid_water=True, - nonbondedCutoff=DEFAULT_CUTOFF_DISTANCE, - use_dispersion_correction=True, - nonbondedMethod=openmm.app.PME, - hydrogenMass=None, - switch_width=DEFAULT_SWITCH_WIDTH, - ewaldErrorTolerance=DEFAULT_EWALD_ERROR_TOLERANCE, - ) -> None: - - prmtop_filename = files("wepy_tools.systems.data.alanine_dipeptide_explicit") / "alanine-dipeptide.prmtop" - crd_filename = files("wepy_tools.systems.data.alanine_dipeptide_explicit") / "alanine-dipeptide.crd" - - # Initialize system. - prmtop = openmm.app.AmberPrmtopFile(prmtop_filename) - system = prmtop.createSystem( - constraints=constraints, - nonbondedMethod=nonbondedMethod, - rigidWater=rigid_water, - nonbondedCutoff=nonbondedCutoff, - hydrogenMass=hydrogenMass, - ) - - # Extract topology - self.topology = prmtop.topology - - # Set dispersion correction use. - forces = { - system.getForce(index).__class__.__name__: system.getForce(index) - for index - in range(system.getNumForces()) - } - forces['NonbondedForce'].setUseDispersionCorrection(use_dispersion_correction) - forces['NonbondedForce'].setEwaldErrorTolerance(ewaldErrorTolerance) - - if switch_width is not None: - forces['NonbondedForce'].setUseSwitchingFunction(True) - forces['NonbondedForce'].setSwitchingDistance(nonbondedCutoff - switch_width) - - # Read positions. - inpcrd = openmm.app.AmberInpcrdFile(crd_filename) - positions = inpcrd.getPositions(asNumpy=True) - - # Set box vectors. - _box_vectors = inpcrd.getBoxVectors(asNumpy=True) - system.setDefaultPeriodicBoxVectors(_box_vectors[0], _box_vectors[1], _box_vectors[2]) - - - self.box_vectors = np.array( - [ - vec_q.value_in_unit(vec_q.unit) - for vec_q - in _box_vectors - ] - ) * _box_vectors[0].unit - - self.system, self.positions = system, positions - @attrs.define class AlanineDipeptideRamachandranDistanceImage(WalkerState): diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/.gitignore b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/.gitignore deleted file mode 100644 index 9730052f..00000000 --- a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/alanine-dipeptide.crd -/alanine-dipeptide.pdb -/alanine-dipeptide.prmtop -/leap.log diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/BUILD b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/BUILD deleted file mode 100644 index a01f53ef..00000000 --- a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/BUILD +++ /dev/null @@ -1,3 +0,0 @@ -resources( - sources=["*.crd", "*.pdb", "*.prmtop", "setup.leap.in"] -) diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide-explicit.state.omm.xml b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide-explicit.state.omm.xml new file mode 100644 index 00000000..d8de0d4f --- /dev/null +++ b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide-explicit.state.omm.xml @@ -0,0 +1,2279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide-explicit.system.omm.xml b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide-explicit.system.omm.xml new file mode 100644 index 00000000..84874379 --- /dev/null +++ b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide-explicit.system.omm.xml @@ -0,0 +1,9275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide-explicit.top.json b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide-explicit.top.json new file mode 100644 index 00000000..368b22c2 --- /dev/null +++ b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide-explicit.top.json @@ -0,0 +1 @@ +{"chains": [{"residues": [{"index": 0, "name": "ACE", "atoms": [{"index": 0, "name": "H1", "element": "H"}, {"index": 1, "name": "CH3", "element": "C"}, {"index": 2, "name": "H2", "element": "H"}, {"index": 3, "name": "H3", "element": "H"}, {"index": 4, "name": "C", "element": "C"}, {"index": 5, "name": "O", "element": "O"}], "resSeq": 1, "segmentID": ""}, {"index": 1, "name": "ALA", "atoms": [{"index": 6, "name": "N", "element": "N"}, {"index": 7, "name": "H", "element": "H"}, {"index": 8, "name": "CA", "element": "C"}, {"index": 9, "name": "HA", "element": "H"}, {"index": 10, "name": "CB", "element": "C"}, {"index": 11, "name": "HB1", "element": "H"}, {"index": 12, "name": "HB2", "element": "H"}, {"index": 13, "name": "HB3", "element": "H"}, {"index": 14, "name": "C", "element": "C"}, {"index": 15, "name": "O", "element": "O"}], "resSeq": 2, "segmentID": ""}, {"index": 2, "name": "NME", "atoms": [{"index": 16, "name": "N", "element": "N"}, {"index": 17, "name": "H", "element": "H"}, {"index": 18, "name": "C", "element": "C"}, {"index": 19, "name": "H1", "element": "H"}, {"index": 20, "name": "H2", "element": "H"}, {"index": 21, "name": "H3", "element": "H"}], "resSeq": 3, "segmentID": ""}, {"index": 3, "name": "HOH", "atoms": [{"index": 22, "name": "O", "element": "O"}, {"index": 23, "name": "H1", "element": "H"}, {"index": 24, "name": "H2", "element": "H"}], "resSeq": 4, "segmentID": ""}, {"index": 4, "name": "HOH", "atoms": [{"index": 25, "name": "O", "element": "O"}, {"index": 26, "name": "H1", "element": "H"}, {"index": 27, "name": "H2", "element": "H"}], "resSeq": 5, "segmentID": ""}, {"index": 5, "name": "HOH", "atoms": [{"index": 28, "name": "O", "element": "O"}, {"index": 29, "name": "H1", "element": "H"}, {"index": 30, "name": "H2", "element": "H"}], "resSeq": 6, "segmentID": ""}, {"index": 6, "name": "HOH", "atoms": [{"index": 31, "name": "O", "element": "O"}, {"index": 32, "name": "H1", "element": "H"}, {"index": 33, "name": "H2", "element": "H"}], "resSeq": 7, "segmentID": ""}, {"index": 7, "name": "HOH", "atoms": [{"index": 34, "name": "O", "element": "O"}, {"index": 35, "name": "H1", "element": "H"}, {"index": 36, "name": "H2", "element": "H"}], "resSeq": 8, "segmentID": ""}, {"index": 8, "name": "HOH", "atoms": [{"index": 37, "name": "O", "element": "O"}, {"index": 38, "name": "H1", "element": "H"}, {"index": 39, "name": "H2", "element": "H"}], "resSeq": 9, "segmentID": ""}, {"index": 9, "name": "HOH", "atoms": [{"index": 40, "name": "O", "element": "O"}, {"index": 41, "name": "H1", "element": "H"}, {"index": 42, "name": "H2", "element": "H"}], "resSeq": 10, "segmentID": ""}, {"index": 10, "name": "HOH", "atoms": [{"index": 43, "name": "O", "element": "O"}, {"index": 44, "name": "H1", "element": "H"}, {"index": 45, "name": "H2", "element": "H"}], "resSeq": 11, "segmentID": ""}, {"index": 11, "name": "HOH", "atoms": [{"index": 46, "name": "O", "element": "O"}, {"index": 47, "name": "H1", "element": "H"}, {"index": 48, "name": "H2", "element": "H"}], "resSeq": 12, "segmentID": ""}, {"index": 12, "name": "HOH", "atoms": [{"index": 49, "name": "O", "element": "O"}, {"index": 50, "name": "H1", "element": "H"}, {"index": 51, "name": "H2", "element": "H"}], "resSeq": 13, "segmentID": ""}, {"index": 13, "name": "HOH", "atoms": [{"index": 52, "name": "O", "element": "O"}, {"index": 53, "name": "H1", "element": "H"}, {"index": 54, "name": "H2", "element": "H"}], "resSeq": 14, "segmentID": ""}, {"index": 14, "name": "HOH", "atoms": [{"index": 55, "name": "O", "element": "O"}, {"index": 56, "name": "H1", "element": "H"}, {"index": 57, "name": "H2", "element": "H"}], "resSeq": 15, "segmentID": ""}, {"index": 15, "name": "HOH", "atoms": [{"index": 58, "name": "O", "element": "O"}, {"index": 59, "name": "H1", "element": "H"}, {"index": 60, "name": "H2", "element": "H"}], "resSeq": 16, "segmentID": ""}, {"index": 16, "name": "HOH", "atoms": [{"index": 61, "name": "O", "element": "O"}, {"index": 62, "name": "H1", "element": "H"}, {"index": 63, "name": "H2", "element": "H"}], "resSeq": 17, "segmentID": ""}, {"index": 17, "name": "HOH", "atoms": [{"index": 64, "name": "O", "element": "O"}, {"index": 65, "name": "H1", "element": "H"}, {"index": 66, "name": "H2", "element": "H"}], "resSeq": 18, "segmentID": ""}, {"index": 18, "name": "HOH", "atoms": [{"index": 67, "name": "O", "element": "O"}, {"index": 68, "name": "H1", "element": "H"}, {"index": 69, "name": "H2", "element": "H"}], "resSeq": 19, "segmentID": ""}, {"index": 19, "name": "HOH", "atoms": [{"index": 70, "name": "O", "element": "O"}, {"index": 71, "name": "H1", "element": "H"}, {"index": 72, "name": "H2", "element": "H"}], "resSeq": 20, "segmentID": ""}, {"index": 20, "name": "HOH", "atoms": [{"index": 73, "name": "O", "element": "O"}, {"index": 74, "name": "H1", "element": "H"}, {"index": 75, "name": "H2", "element": "H"}], "resSeq": 21, "segmentID": ""}, {"index": 21, "name": "HOH", "atoms": [{"index": 76, "name": "O", "element": "O"}, {"index": 77, "name": "H1", "element": "H"}, {"index": 78, "name": "H2", "element": "H"}], "resSeq": 22, "segmentID": ""}, {"index": 22, "name": "HOH", "atoms": [{"index": 79, "name": "O", "element": "O"}, {"index": 80, "name": "H1", "element": "H"}, {"index": 81, "name": "H2", "element": "H"}], "resSeq": 23, "segmentID": ""}, {"index": 23, "name": "HOH", "atoms": [{"index": 82, "name": "O", "element": "O"}, {"index": 83, "name": "H1", "element": "H"}, {"index": 84, "name": "H2", "element": "H"}], "resSeq": 24, "segmentID": ""}, {"index": 24, "name": "HOH", "atoms": [{"index": 85, "name": "O", "element": "O"}, {"index": 86, "name": "H1", "element": "H"}, {"index": 87, "name": "H2", "element": "H"}], "resSeq": 25, "segmentID": ""}, {"index": 25, "name": "HOH", "atoms": [{"index": 88, "name": "O", "element": "O"}, {"index": 89, "name": "H1", "element": "H"}, {"index": 90, "name": "H2", "element": "H"}], "resSeq": 26, "segmentID": ""}, {"index": 26, "name": "HOH", "atoms": [{"index": 91, "name": "O", "element": "O"}, {"index": 92, "name": "H1", "element": "H"}, {"index": 93, "name": "H2", "element": "H"}], "resSeq": 27, "segmentID": ""}, {"index": 27, "name": "HOH", "atoms": [{"index": 94, "name": "O", "element": "O"}, {"index": 95, "name": "H1", "element": "H"}, {"index": 96, "name": "H2", "element": "H"}], "resSeq": 28, "segmentID": ""}, {"index": 28, "name": "HOH", "atoms": [{"index": 97, "name": "O", "element": "O"}, {"index": 98, "name": "H1", "element": "H"}, {"index": 99, "name": "H2", "element": "H"}], "resSeq": 29, "segmentID": ""}, {"index": 29, "name": "HOH", "atoms": [{"index": 100, "name": "O", "element": "O"}, {"index": 101, "name": "H1", "element": "H"}, {"index": 102, "name": "H2", "element": "H"}], "resSeq": 30, "segmentID": ""}, {"index": 30, "name": "HOH", "atoms": [{"index": 103, "name": "O", "element": "O"}, {"index": 104, "name": "H1", "element": "H"}, {"index": 105, "name": "H2", "element": "H"}], "resSeq": 31, "segmentID": ""}, {"index": 31, "name": "HOH", "atoms": [{"index": 106, "name": "O", "element": "O"}, {"index": 107, "name": "H1", "element": "H"}, {"index": 108, "name": "H2", "element": "H"}], "resSeq": 32, "segmentID": ""}, {"index": 32, "name": "HOH", "atoms": [{"index": 109, "name": "O", "element": "O"}, {"index": 110, "name": "H1", "element": "H"}, {"index": 111, "name": "H2", "element": "H"}], "resSeq": 33, "segmentID": ""}, {"index": 33, "name": "HOH", "atoms": [{"index": 112, "name": "O", "element": "O"}, {"index": 113, "name": "H1", "element": "H"}, {"index": 114, "name": "H2", "element": "H"}], "resSeq": 34, "segmentID": ""}, {"index": 34, "name": "HOH", "atoms": [{"index": 115, "name": "O", "element": "O"}, {"index": 116, "name": "H1", "element": "H"}, {"index": 117, "name": "H2", "element": "H"}], "resSeq": 35, "segmentID": ""}, {"index": 35, "name": "HOH", "atoms": [{"index": 118, "name": "O", "element": "O"}, {"index": 119, "name": "H1", "element": "H"}, {"index": 120, "name": "H2", "element": "H"}], "resSeq": 36, "segmentID": ""}, {"index": 36, "name": "HOH", "atoms": [{"index": 121, "name": "O", "element": "O"}, {"index": 122, "name": "H1", "element": "H"}, {"index": 123, "name": "H2", "element": "H"}], "resSeq": 37, "segmentID": ""}, {"index": 37, "name": "HOH", "atoms": [{"index": 124, "name": "O", "element": "O"}, {"index": 125, "name": "H1", "element": "H"}, {"index": 126, "name": "H2", "element": "H"}], "resSeq": 38, "segmentID": ""}, {"index": 38, "name": "HOH", "atoms": [{"index": 127, "name": "O", "element": "O"}, {"index": 128, "name": "H1", "element": "H"}, {"index": 129, "name": "H2", "element": "H"}], "resSeq": 39, "segmentID": ""}, {"index": 39, "name": "HOH", "atoms": [{"index": 130, "name": "O", "element": "O"}, {"index": 131, "name": "H1", "element": "H"}, {"index": 132, "name": "H2", "element": "H"}], "resSeq": 40, "segmentID": ""}, {"index": 40, "name": "HOH", "atoms": [{"index": 133, "name": "O", "element": "O"}, {"index": 134, "name": "H1", "element": "H"}, {"index": 135, "name": "H2", "element": "H"}], "resSeq": 41, "segmentID": ""}, {"index": 41, "name": "HOH", "atoms": [{"index": 136, "name": "O", "element": "O"}, {"index": 137, "name": "H1", "element": "H"}, {"index": 138, "name": "H2", "element": "H"}], "resSeq": 42, "segmentID": ""}, {"index": 42, "name": "HOH", "atoms": [{"index": 139, "name": "O", "element": "O"}, {"index": 140, "name": "H1", "element": "H"}, {"index": 141, "name": "H2", "element": "H"}], "resSeq": 43, "segmentID": ""}, {"index": 43, "name": "HOH", "atoms": [{"index": 142, "name": "O", "element": "O"}, {"index": 143, "name": "H1", "element": "H"}, {"index": 144, "name": "H2", "element": "H"}], "resSeq": 44, "segmentID": ""}, {"index": 44, "name": "HOH", "atoms": [{"index": 145, "name": "O", "element": "O"}, {"index": 146, "name": "H1", "element": "H"}, {"index": 147, "name": "H2", "element": "H"}], "resSeq": 45, "segmentID": ""}, {"index": 45, "name": "HOH", "atoms": [{"index": 148, "name": "O", "element": "O"}, {"index": 149, "name": "H1", "element": "H"}, {"index": 150, "name": "H2", "element": "H"}], "resSeq": 46, "segmentID": ""}, {"index": 46, "name": "HOH", "atoms": [{"index": 151, "name": "O", "element": "O"}, {"index": 152, "name": "H1", "element": "H"}, {"index": 153, "name": "H2", "element": "H"}], "resSeq": 47, "segmentID": ""}, {"index": 47, "name": "HOH", "atoms": [{"index": 154, "name": "O", "element": "O"}, {"index": 155, "name": "H1", "element": "H"}, {"index": 156, "name": "H2", "element": "H"}], "resSeq": 48, "segmentID": ""}, {"index": 48, "name": "HOH", "atoms": [{"index": 157, "name": "O", "element": "O"}, {"index": 158, "name": "H1", "element": "H"}, {"index": 159, "name": "H2", "element": "H"}], "resSeq": 49, "segmentID": ""}, {"index": 49, "name": "HOH", "atoms": [{"index": 160, "name": "O", "element": "O"}, {"index": 161, "name": "H1", "element": "H"}, {"index": 162, "name": "H2", "element": "H"}], "resSeq": 50, "segmentID": ""}, {"index": 50, "name": "HOH", "atoms": [{"index": 163, "name": "O", "element": "O"}, {"index": 164, "name": "H1", "element": "H"}, {"index": 165, "name": "H2", "element": "H"}], "resSeq": 51, "segmentID": ""}, {"index": 51, "name": "HOH", "atoms": [{"index": 166, "name": "O", "element": "O"}, {"index": 167, "name": "H1", "element": "H"}, {"index": 168, "name": "H2", "element": "H"}], "resSeq": 52, "segmentID": ""}, {"index": 52, "name": "HOH", "atoms": [{"index": 169, "name": "O", "element": "O"}, {"index": 170, "name": "H1", "element": "H"}, {"index": 171, "name": "H2", "element": "H"}], "resSeq": 53, "segmentID": ""}, {"index": 53, "name": "HOH", "atoms": [{"index": 172, "name": "O", "element": "O"}, {"index": 173, "name": "H1", "element": "H"}, {"index": 174, "name": "H2", "element": "H"}], "resSeq": 54, "segmentID": ""}, {"index": 54, "name": "HOH", "atoms": [{"index": 175, "name": "O", "element": "O"}, {"index": 176, "name": "H1", "element": "H"}, {"index": 177, "name": "H2", "element": "H"}], "resSeq": 55, "segmentID": ""}, {"index": 55, "name": "HOH", "atoms": [{"index": 178, "name": "O", "element": "O"}, {"index": 179, "name": "H1", "element": "H"}, {"index": 180, "name": "H2", "element": "H"}], "resSeq": 56, "segmentID": ""}, {"index": 56, "name": "HOH", "atoms": [{"index": 181, "name": "O", "element": "O"}, {"index": 182, "name": "H1", "element": "H"}, {"index": 183, "name": "H2", "element": "H"}], "resSeq": 57, "segmentID": ""}, {"index": 57, "name": "HOH", "atoms": [{"index": 184, "name": "O", "element": "O"}, {"index": 185, "name": "H1", "element": "H"}, {"index": 186, "name": "H2", "element": "H"}], "resSeq": 58, "segmentID": ""}, {"index": 58, "name": "HOH", "atoms": [{"index": 187, "name": "O", "element": "O"}, {"index": 188, "name": "H1", "element": "H"}, {"index": 189, "name": "H2", "element": "H"}], "resSeq": 59, "segmentID": ""}, {"index": 59, "name": "HOH", "atoms": [{"index": 190, "name": "O", "element": "O"}, {"index": 191, "name": "H1", "element": "H"}, {"index": 192, "name": "H2", "element": "H"}], "resSeq": 60, "segmentID": ""}, {"index": 60, "name": "HOH", "atoms": [{"index": 193, "name": "O", "element": "O"}, {"index": 194, "name": "H1", "element": "H"}, {"index": 195, "name": "H2", "element": "H"}], "resSeq": 61, "segmentID": ""}, {"index": 61, "name": "HOH", "atoms": [{"index": 196, "name": "O", "element": "O"}, {"index": 197, "name": "H1", "element": "H"}, {"index": 198, "name": "H2", "element": "H"}], "resSeq": 62, "segmentID": ""}, {"index": 62, "name": "HOH", "atoms": [{"index": 199, "name": "O", "element": "O"}, {"index": 200, "name": "H1", "element": "H"}, {"index": 201, "name": "H2", "element": "H"}], "resSeq": 63, "segmentID": ""}, {"index": 63, "name": "HOH", "atoms": [{"index": 202, "name": "O", "element": "O"}, {"index": 203, "name": "H1", "element": "H"}, {"index": 204, "name": "H2", "element": "H"}], "resSeq": 64, "segmentID": ""}, {"index": 64, "name": "HOH", "atoms": [{"index": 205, "name": "O", "element": "O"}, {"index": 206, "name": "H1", "element": "H"}, {"index": 207, "name": "H2", "element": "H"}], "resSeq": 65, "segmentID": ""}, {"index": 65, "name": "HOH", "atoms": [{"index": 208, "name": "O", "element": "O"}, {"index": 209, "name": "H1", "element": "H"}, {"index": 210, "name": "H2", "element": "H"}], "resSeq": 66, "segmentID": ""}, {"index": 66, "name": "HOH", "atoms": [{"index": 211, "name": "O", "element": "O"}, {"index": 212, "name": "H1", "element": "H"}, {"index": 213, "name": "H2", "element": "H"}], "resSeq": 67, "segmentID": ""}, {"index": 67, "name": "HOH", "atoms": [{"index": 214, "name": "O", "element": "O"}, {"index": 215, "name": "H1", "element": "H"}, {"index": 216, "name": "H2", "element": "H"}], "resSeq": 68, "segmentID": ""}, {"index": 68, "name": "HOH", "atoms": [{"index": 217, "name": "O", "element": "O"}, {"index": 218, "name": "H1", "element": "H"}, {"index": 219, "name": "H2", "element": "H"}], "resSeq": 69, "segmentID": ""}, {"index": 69, "name": "HOH", "atoms": [{"index": 220, "name": "O", "element": "O"}, {"index": 221, "name": "H1", "element": "H"}, {"index": 222, "name": "H2", "element": "H"}], "resSeq": 70, "segmentID": ""}, {"index": 70, "name": "HOH", "atoms": [{"index": 223, "name": "O", "element": "O"}, {"index": 224, "name": "H1", "element": "H"}, {"index": 225, "name": "H2", "element": "H"}], "resSeq": 71, "segmentID": ""}, {"index": 71, "name": "HOH", "atoms": [{"index": 226, "name": "O", "element": "O"}, {"index": 227, "name": "H1", "element": "H"}, {"index": 228, "name": "H2", "element": "H"}], "resSeq": 72, "segmentID": ""}, {"index": 72, "name": "HOH", "atoms": [{"index": 229, "name": "O", "element": "O"}, {"index": 230, "name": "H1", "element": "H"}, {"index": 231, "name": "H2", "element": "H"}], "resSeq": 73, "segmentID": ""}, {"index": 73, "name": "HOH", "atoms": [{"index": 232, "name": "O", "element": "O"}, {"index": 233, "name": "H1", "element": "H"}, {"index": 234, "name": "H2", "element": "H"}], "resSeq": 74, "segmentID": ""}, {"index": 74, "name": "HOH", "atoms": [{"index": 235, "name": "O", "element": "O"}, {"index": 236, "name": "H1", "element": "H"}, {"index": 237, "name": "H2", "element": "H"}], "resSeq": 75, "segmentID": ""}, {"index": 75, "name": "HOH", "atoms": [{"index": 238, "name": "O", "element": "O"}, {"index": 239, "name": "H1", "element": "H"}, {"index": 240, "name": "H2", "element": "H"}], "resSeq": 76, "segmentID": ""}, {"index": 76, "name": "HOH", "atoms": [{"index": 241, "name": "O", "element": "O"}, {"index": 242, "name": "H1", "element": "H"}, {"index": 243, "name": "H2", "element": "H"}], "resSeq": 77, "segmentID": ""}, {"index": 77, "name": "HOH", "atoms": [{"index": 244, "name": "O", "element": "O"}, {"index": 245, "name": "H1", "element": "H"}, {"index": 246, "name": "H2", "element": "H"}], "resSeq": 78, "segmentID": ""}, {"index": 78, "name": "HOH", "atoms": [{"index": 247, "name": "O", "element": "O"}, {"index": 248, "name": "H1", "element": "H"}, {"index": 249, "name": "H2", "element": "H"}], "resSeq": 79, "segmentID": ""}, {"index": 79, "name": "HOH", "atoms": [{"index": 250, "name": "O", "element": "O"}, {"index": 251, "name": "H1", "element": "H"}, {"index": 252, "name": "H2", "element": "H"}], "resSeq": 80, "segmentID": ""}, {"index": 80, "name": "HOH", "atoms": [{"index": 253, "name": "O", "element": "O"}, {"index": 254, "name": "H1", "element": "H"}, {"index": 255, "name": "H2", "element": "H"}], "resSeq": 81, "segmentID": ""}, {"index": 81, "name": "HOH", "atoms": [{"index": 256, "name": "O", "element": "O"}, {"index": 257, "name": "H1", "element": "H"}, {"index": 258, "name": "H2", "element": "H"}], "resSeq": 82, "segmentID": ""}, {"index": 82, "name": "HOH", "atoms": [{"index": 259, "name": "O", "element": "O"}, {"index": 260, "name": "H1", "element": "H"}, {"index": 261, "name": "H2", "element": "H"}], "resSeq": 83, "segmentID": ""}, {"index": 83, "name": "HOH", "atoms": [{"index": 262, "name": "O", "element": "O"}, {"index": 263, "name": "H1", "element": "H"}, {"index": 264, "name": "H2", "element": "H"}], "resSeq": 84, "segmentID": ""}, {"index": 84, "name": "HOH", "atoms": [{"index": 265, "name": "O", "element": "O"}, {"index": 266, "name": "H1", "element": "H"}, {"index": 267, "name": "H2", "element": "H"}], "resSeq": 85, "segmentID": ""}, {"index": 85, "name": "HOH", "atoms": [{"index": 268, "name": "O", "element": "O"}, {"index": 269, "name": "H1", "element": "H"}, {"index": 270, "name": "H2", "element": "H"}], "resSeq": 86, "segmentID": ""}, {"index": 86, "name": "HOH", "atoms": [{"index": 271, "name": "O", "element": "O"}, {"index": 272, "name": "H1", "element": "H"}, {"index": 273, "name": "H2", "element": "H"}], "resSeq": 87, "segmentID": ""}, {"index": 87, "name": "HOH", "atoms": [{"index": 274, "name": "O", "element": "O"}, {"index": 275, "name": "H1", "element": "H"}, {"index": 276, "name": "H2", "element": "H"}], "resSeq": 88, "segmentID": ""}, {"index": 88, "name": "HOH", "atoms": [{"index": 277, "name": "O", "element": "O"}, {"index": 278, "name": "H1", "element": "H"}, {"index": 279, "name": "H2", "element": "H"}], "resSeq": 89, "segmentID": ""}, {"index": 89, "name": "HOH", "atoms": [{"index": 280, "name": "O", "element": "O"}, {"index": 281, "name": "H1", "element": "H"}, {"index": 282, "name": "H2", "element": "H"}], "resSeq": 90, "segmentID": ""}, {"index": 90, "name": "HOH", "atoms": [{"index": 283, "name": "O", "element": "O"}, {"index": 284, "name": "H1", "element": "H"}, {"index": 285, "name": "H2", "element": "H"}], "resSeq": 91, "segmentID": ""}, {"index": 91, "name": "HOH", "atoms": [{"index": 286, "name": "O", "element": "O"}, {"index": 287, "name": "H1", "element": "H"}, {"index": 288, "name": "H2", "element": "H"}], "resSeq": 92, "segmentID": ""}, {"index": 92, "name": "HOH", "atoms": [{"index": 289, "name": "O", "element": "O"}, {"index": 290, "name": "H1", "element": "H"}, {"index": 291, "name": "H2", "element": "H"}], "resSeq": 93, "segmentID": ""}, {"index": 93, "name": "HOH", "atoms": [{"index": 292, "name": "O", "element": "O"}, {"index": 293, "name": "H1", "element": "H"}, {"index": 294, "name": "H2", "element": "H"}], "resSeq": 94, "segmentID": ""}, {"index": 94, "name": "HOH", "atoms": [{"index": 295, "name": "O", "element": "O"}, {"index": 296, "name": "H1", "element": "H"}, {"index": 297, "name": "H2", "element": "H"}], "resSeq": 95, "segmentID": ""}, {"index": 95, "name": "HOH", "atoms": [{"index": 298, "name": "O", "element": "O"}, {"index": 299, "name": "H1", "element": "H"}, {"index": 300, "name": "H2", "element": "H"}], "resSeq": 96, "segmentID": ""}, {"index": 96, "name": "HOH", "atoms": [{"index": 301, "name": "O", "element": "O"}, {"index": 302, "name": "H1", "element": "H"}, {"index": 303, "name": "H2", "element": "H"}], "resSeq": 97, "segmentID": ""}, {"index": 97, "name": "HOH", "atoms": [{"index": 304, "name": "O", "element": "O"}, {"index": 305, "name": "H1", "element": "H"}, {"index": 306, "name": "H2", "element": "H"}], "resSeq": 98, "segmentID": ""}, {"index": 98, "name": "HOH", "atoms": [{"index": 307, "name": "O", "element": "O"}, {"index": 308, "name": "H1", "element": "H"}, {"index": 309, "name": "H2", "element": "H"}], "resSeq": 99, "segmentID": ""}, {"index": 99, "name": "HOH", "atoms": [{"index": 310, "name": "O", "element": "O"}, {"index": 311, "name": "H1", "element": "H"}, {"index": 312, "name": "H2", "element": "H"}], "resSeq": 100, "segmentID": ""}, {"index": 100, "name": "HOH", "atoms": [{"index": 313, "name": "O", "element": "O"}, {"index": 314, "name": "H1", "element": "H"}, {"index": 315, "name": "H2", "element": "H"}], "resSeq": 101, "segmentID": ""}, {"index": 101, "name": "HOH", "atoms": [{"index": 316, "name": "O", "element": "O"}, {"index": 317, "name": "H1", "element": "H"}, {"index": 318, "name": "H2", "element": "H"}], "resSeq": 102, "segmentID": ""}, {"index": 102, "name": "HOH", "atoms": [{"index": 319, "name": "O", "element": "O"}, {"index": 320, "name": "H1", "element": "H"}, {"index": 321, "name": "H2", "element": "H"}], "resSeq": 103, "segmentID": ""}, {"index": 103, "name": "HOH", "atoms": [{"index": 322, "name": "O", "element": "O"}, {"index": 323, "name": "H1", "element": "H"}, {"index": 324, "name": "H2", "element": "H"}], "resSeq": 104, "segmentID": ""}, {"index": 104, "name": "HOH", "atoms": [{"index": 325, "name": "O", "element": "O"}, {"index": 326, "name": "H1", "element": "H"}, {"index": 327, "name": "H2", "element": "H"}], "resSeq": 105, "segmentID": ""}, {"index": 105, "name": "HOH", "atoms": [{"index": 328, "name": "O", "element": "O"}, {"index": 329, "name": "H1", "element": "H"}, {"index": 330, "name": "H2", "element": "H"}], "resSeq": 106, "segmentID": ""}, {"index": 106, "name": "HOH", "atoms": [{"index": 331, "name": "O", "element": "O"}, {"index": 332, "name": "H1", "element": "H"}, {"index": 333, "name": "H2", "element": "H"}], "resSeq": 107, "segmentID": ""}, {"index": 107, "name": "HOH", "atoms": [{"index": 334, "name": "O", "element": "O"}, {"index": 335, "name": "H1", "element": "H"}, {"index": 336, "name": "H2", "element": "H"}], "resSeq": 108, "segmentID": ""}, {"index": 108, "name": "HOH", "atoms": [{"index": 337, "name": "O", "element": "O"}, {"index": 338, "name": "H1", "element": "H"}, {"index": 339, "name": "H2", "element": "H"}], "resSeq": 109, "segmentID": ""}, {"index": 109, "name": "HOH", "atoms": [{"index": 340, "name": "O", "element": "O"}, {"index": 341, "name": "H1", "element": "H"}, {"index": 342, "name": "H2", "element": "H"}], "resSeq": 110, "segmentID": ""}, {"index": 110, "name": "HOH", "atoms": [{"index": 343, "name": "O", "element": "O"}, {"index": 344, "name": "H1", "element": "H"}, {"index": 345, "name": "H2", "element": "H"}], "resSeq": 111, "segmentID": ""}, {"index": 111, "name": "HOH", "atoms": [{"index": 346, "name": "O", "element": "O"}, {"index": 347, "name": "H1", "element": "H"}, {"index": 348, "name": "H2", "element": "H"}], "resSeq": 112, "segmentID": ""}, {"index": 112, "name": "HOH", "atoms": [{"index": 349, "name": "O", "element": "O"}, {"index": 350, "name": "H1", "element": "H"}, {"index": 351, "name": "H2", "element": "H"}], "resSeq": 113, "segmentID": ""}, {"index": 113, "name": "HOH", "atoms": [{"index": 352, "name": "O", "element": "O"}, {"index": 353, "name": "H1", "element": "H"}, {"index": 354, "name": "H2", "element": "H"}], "resSeq": 114, "segmentID": ""}, {"index": 114, "name": "HOH", "atoms": [{"index": 355, "name": "O", "element": "O"}, {"index": 356, "name": "H1", "element": "H"}, {"index": 357, "name": "H2", "element": "H"}], "resSeq": 115, "segmentID": ""}, {"index": 115, "name": "HOH", "atoms": [{"index": 358, "name": "O", "element": "O"}, {"index": 359, "name": "H1", "element": "H"}, {"index": 360, "name": "H2", "element": "H"}], "resSeq": 116, "segmentID": ""}, {"index": 116, "name": "HOH", "atoms": [{"index": 361, "name": "O", "element": "O"}, {"index": 362, "name": "H1", "element": "H"}, {"index": 363, "name": "H2", "element": "H"}], "resSeq": 117, "segmentID": ""}, {"index": 117, "name": "HOH", "atoms": [{"index": 364, "name": "O", "element": "O"}, {"index": 365, "name": "H1", "element": "H"}, {"index": 366, "name": "H2", "element": "H"}], "resSeq": 118, "segmentID": ""}, {"index": 118, "name": "HOH", "atoms": [{"index": 367, "name": "O", "element": "O"}, {"index": 368, "name": "H1", "element": "H"}, {"index": 369, "name": "H2", "element": "H"}], "resSeq": 119, "segmentID": ""}, {"index": 119, "name": "HOH", "atoms": [{"index": 370, "name": "O", "element": "O"}, {"index": 371, "name": "H1", "element": "H"}, {"index": 372, "name": "H2", "element": "H"}], "resSeq": 120, "segmentID": ""}, {"index": 120, "name": "HOH", "atoms": [{"index": 373, "name": "O", "element": "O"}, {"index": 374, "name": "H1", "element": "H"}, {"index": 375, "name": "H2", "element": "H"}], "resSeq": 121, "segmentID": ""}, {"index": 121, "name": "HOH", "atoms": [{"index": 376, "name": "O", "element": "O"}, {"index": 377, "name": "H1", "element": "H"}, {"index": 378, "name": "H2", "element": "H"}], "resSeq": 122, "segmentID": ""}, {"index": 122, "name": "HOH", "atoms": [{"index": 379, "name": "O", "element": "O"}, {"index": 380, "name": "H1", "element": "H"}, {"index": 381, "name": "H2", "element": "H"}], "resSeq": 123, "segmentID": ""}, {"index": 123, "name": "HOH", "atoms": [{"index": 382, "name": "O", "element": "O"}, {"index": 383, "name": "H1", "element": "H"}, {"index": 384, "name": "H2", "element": "H"}], "resSeq": 124, "segmentID": ""}, {"index": 124, "name": "HOH", "atoms": [{"index": 385, "name": "O", "element": "O"}, {"index": 386, "name": "H1", "element": "H"}, {"index": 387, "name": "H2", "element": "H"}], "resSeq": 125, "segmentID": ""}, {"index": 125, "name": "HOH", "atoms": [{"index": 388, "name": "O", "element": "O"}, {"index": 389, "name": "H1", "element": "H"}, {"index": 390, "name": "H2", "element": "H"}], "resSeq": 126, "segmentID": ""}, {"index": 126, "name": "HOH", "atoms": [{"index": 391, "name": "O", "element": "O"}, {"index": 392, "name": "H1", "element": "H"}, {"index": 393, "name": "H2", "element": "H"}], "resSeq": 127, "segmentID": ""}, {"index": 127, "name": "HOH", "atoms": [{"index": 394, "name": "O", "element": "O"}, {"index": 395, "name": "H1", "element": "H"}, {"index": 396, "name": "H2", "element": "H"}], "resSeq": 128, "segmentID": ""}, {"index": 128, "name": "HOH", "atoms": [{"index": 397, "name": "O", "element": "O"}, {"index": 398, "name": "H1", "element": "H"}, {"index": 399, "name": "H2", "element": "H"}], "resSeq": 129, "segmentID": ""}, {"index": 129, "name": "HOH", "atoms": [{"index": 400, "name": "O", "element": "O"}, {"index": 401, "name": "H1", "element": "H"}, {"index": 402, "name": "H2", "element": "H"}], "resSeq": 130, "segmentID": ""}, {"index": 130, "name": "HOH", "atoms": [{"index": 403, "name": "O", "element": "O"}, {"index": 404, "name": "H1", "element": "H"}, {"index": 405, "name": "H2", "element": "H"}], "resSeq": 131, "segmentID": ""}, {"index": 131, "name": "HOH", "atoms": [{"index": 406, "name": "O", "element": "O"}, {"index": 407, "name": "H1", "element": "H"}, {"index": 408, "name": "H2", "element": "H"}], "resSeq": 132, "segmentID": ""}, {"index": 132, "name": "HOH", "atoms": [{"index": 409, "name": "O", "element": "O"}, {"index": 410, "name": "H1", "element": "H"}, {"index": 411, "name": "H2", "element": "H"}], "resSeq": 133, "segmentID": ""}, {"index": 133, "name": "HOH", "atoms": [{"index": 412, "name": "O", "element": "O"}, {"index": 413, "name": "H1", "element": "H"}, {"index": 414, "name": "H2", "element": "H"}], "resSeq": 134, "segmentID": ""}, {"index": 134, "name": "HOH", "atoms": [{"index": 415, "name": "O", "element": "O"}, {"index": 416, "name": "H1", "element": "H"}, {"index": 417, "name": "H2", "element": "H"}], "resSeq": 135, "segmentID": ""}, {"index": 135, "name": "HOH", "atoms": [{"index": 418, "name": "O", "element": "O"}, {"index": 419, "name": "H1", "element": "H"}, {"index": 420, "name": "H2", "element": "H"}], "resSeq": 136, "segmentID": ""}, {"index": 136, "name": "HOH", "atoms": [{"index": 421, "name": "O", "element": "O"}, {"index": 422, "name": "H1", "element": "H"}, {"index": 423, "name": "H2", "element": "H"}], "resSeq": 137, "segmentID": ""}, {"index": 137, "name": "HOH", "atoms": [{"index": 424, "name": "O", "element": "O"}, {"index": 425, "name": "H1", "element": "H"}, {"index": 426, "name": "H2", "element": "H"}], "resSeq": 138, "segmentID": ""}, {"index": 138, "name": "HOH", "atoms": [{"index": 427, "name": "O", "element": "O"}, {"index": 428, "name": "H1", "element": "H"}, {"index": 429, "name": "H2", "element": "H"}], "resSeq": 139, "segmentID": ""}, {"index": 139, "name": "HOH", "atoms": [{"index": 430, "name": "O", "element": "O"}, {"index": 431, "name": "H1", "element": "H"}, {"index": 432, "name": "H2", "element": "H"}], "resSeq": 140, "segmentID": ""}, {"index": 140, "name": "HOH", "atoms": [{"index": 433, "name": "O", "element": "O"}, {"index": 434, "name": "H1", "element": "H"}, {"index": 435, "name": "H2", "element": "H"}], "resSeq": 141, "segmentID": ""}, {"index": 141, "name": "HOH", "atoms": [{"index": 436, "name": "O", "element": "O"}, {"index": 437, "name": "H1", "element": "H"}, {"index": 438, "name": "H2", "element": "H"}], "resSeq": 142, "segmentID": ""}, {"index": 142, "name": "HOH", "atoms": [{"index": 439, "name": "O", "element": "O"}, {"index": 440, "name": "H1", "element": "H"}, {"index": 441, "name": "H2", "element": "H"}], "resSeq": 143, "segmentID": ""}, {"index": 143, "name": "HOH", "atoms": [{"index": 442, "name": "O", "element": "O"}, {"index": 443, "name": "H1", "element": "H"}, {"index": 444, "name": "H2", "element": "H"}], "resSeq": 144, "segmentID": ""}, {"index": 144, "name": "HOH", "atoms": [{"index": 445, "name": "O", "element": "O"}, {"index": 446, "name": "H1", "element": "H"}, {"index": 447, "name": "H2", "element": "H"}], "resSeq": 145, "segmentID": ""}, {"index": 145, "name": "HOH", "atoms": [{"index": 448, "name": "O", "element": "O"}, {"index": 449, "name": "H1", "element": "H"}, {"index": 450, "name": "H2", "element": "H"}], "resSeq": 146, "segmentID": ""}, {"index": 146, "name": "HOH", "atoms": [{"index": 451, "name": "O", "element": "O"}, {"index": 452, "name": "H1", "element": "H"}, {"index": 453, "name": "H2", "element": "H"}], "resSeq": 147, "segmentID": ""}, {"index": 147, "name": "HOH", "atoms": [{"index": 454, "name": "O", "element": "O"}, {"index": 455, "name": "H1", "element": "H"}, {"index": 456, "name": "H2", "element": "H"}], "resSeq": 148, "segmentID": ""}, {"index": 148, "name": "HOH", "atoms": [{"index": 457, "name": "O", "element": "O"}, {"index": 458, "name": "H1", "element": "H"}, {"index": 459, "name": "H2", "element": "H"}], "resSeq": 149, "segmentID": ""}, {"index": 149, "name": "HOH", "atoms": [{"index": 460, "name": "O", "element": "O"}, {"index": 461, "name": "H1", "element": "H"}, {"index": 462, "name": "H2", "element": "H"}], "resSeq": 150, "segmentID": ""}, {"index": 150, "name": "HOH", "atoms": [{"index": 463, "name": "O", "element": "O"}, {"index": 464, "name": "H1", "element": "H"}, {"index": 465, "name": "H2", "element": "H"}], "resSeq": 151, "segmentID": ""}, {"index": 151, "name": "HOH", "atoms": [{"index": 466, "name": "O", "element": "O"}, {"index": 467, "name": "H1", "element": "H"}, {"index": 468, "name": "H2", "element": "H"}], "resSeq": 152, "segmentID": ""}, {"index": 152, "name": "HOH", "atoms": [{"index": 469, "name": "O", "element": "O"}, {"index": 470, "name": "H1", "element": "H"}, {"index": 471, "name": "H2", "element": "H"}], "resSeq": 153, "segmentID": ""}, {"index": 153, "name": "HOH", "atoms": [{"index": 472, "name": "O", "element": "O"}, {"index": 473, "name": "H1", "element": "H"}, {"index": 474, "name": "H2", "element": "H"}], "resSeq": 154, "segmentID": ""}, {"index": 154, "name": "HOH", "atoms": [{"index": 475, "name": "O", "element": "O"}, {"index": 476, "name": "H1", "element": "H"}, {"index": 477, "name": "H2", "element": "H"}], "resSeq": 155, "segmentID": ""}, {"index": 155, "name": "HOH", "atoms": [{"index": 478, "name": "O", "element": "O"}, {"index": 479, "name": "H1", "element": "H"}, {"index": 480, "name": "H2", "element": "H"}], "resSeq": 156, "segmentID": ""}, {"index": 156, "name": "HOH", "atoms": [{"index": 481, "name": "O", "element": "O"}, {"index": 482, "name": "H1", "element": "H"}, {"index": 483, "name": "H2", "element": "H"}], "resSeq": 157, "segmentID": ""}, {"index": 157, "name": "HOH", "atoms": [{"index": 484, "name": "O", "element": "O"}, {"index": 485, "name": "H1", "element": "H"}, {"index": 486, "name": "H2", "element": "H"}], "resSeq": 158, "segmentID": ""}, {"index": 158, "name": "HOH", "atoms": [{"index": 487, "name": "O", "element": "O"}, {"index": 488, "name": "H1", "element": "H"}, {"index": 489, "name": "H2", "element": "H"}], "resSeq": 159, "segmentID": ""}, {"index": 159, "name": "HOH", "atoms": [{"index": 490, "name": "O", "element": "O"}, {"index": 491, "name": "H1", "element": "H"}, {"index": 492, "name": "H2", "element": "H"}], "resSeq": 160, "segmentID": ""}, {"index": 160, "name": "HOH", "atoms": [{"index": 493, "name": "O", "element": "O"}, {"index": 494, "name": "H1", "element": "H"}, {"index": 495, "name": "H2", "element": "H"}], "resSeq": 161, "segmentID": ""}, {"index": 161, "name": "HOH", "atoms": [{"index": 496, "name": "O", "element": "O"}, {"index": 497, "name": "H1", "element": "H"}, {"index": 498, "name": "H2", "element": "H"}], "resSeq": 162, "segmentID": ""}, {"index": 162, "name": "HOH", "atoms": [{"index": 499, "name": "O", "element": "O"}, {"index": 500, "name": "H1", "element": "H"}, {"index": 501, "name": "H2", "element": "H"}], "resSeq": 163, "segmentID": ""}, {"index": 163, "name": "HOH", "atoms": [{"index": 502, "name": "O", "element": "O"}, {"index": 503, "name": "H1", "element": "H"}, {"index": 504, "name": "H2", "element": "H"}], "resSeq": 164, "segmentID": ""}, {"index": 164, "name": "HOH", "atoms": [{"index": 505, "name": "O", "element": "O"}, {"index": 506, "name": "H1", "element": "H"}, {"index": 507, "name": "H2", "element": "H"}], "resSeq": 165, "segmentID": ""}, {"index": 165, "name": "HOH", "atoms": [{"index": 508, "name": "O", "element": "O"}, {"index": 509, "name": "H1", "element": "H"}, {"index": 510, "name": "H2", "element": "H"}], "resSeq": 166, "segmentID": ""}, {"index": 166, "name": "HOH", "atoms": [{"index": 511, "name": "O", "element": "O"}, {"index": 512, "name": "H1", "element": "H"}, {"index": 513, "name": "H2", "element": "H"}], "resSeq": 167, "segmentID": ""}, {"index": 167, "name": "HOH", "atoms": [{"index": 514, "name": "O", "element": "O"}, {"index": 515, "name": "H1", "element": "H"}, {"index": 516, "name": "H2", "element": "H"}], "resSeq": 168, "segmentID": ""}, {"index": 168, "name": "HOH", "atoms": [{"index": 517, "name": "O", "element": "O"}, {"index": 518, "name": "H1", "element": "H"}, {"index": 519, "name": "H2", "element": "H"}], "resSeq": 169, "segmentID": ""}, {"index": 169, "name": "HOH", "atoms": [{"index": 520, "name": "O", "element": "O"}, {"index": 521, "name": "H1", "element": "H"}, {"index": 522, "name": "H2", "element": "H"}], "resSeq": 170, "segmentID": ""}, {"index": 170, "name": "HOH", "atoms": [{"index": 523, "name": "O", "element": "O"}, {"index": 524, "name": "H1", "element": "H"}, {"index": 525, "name": "H2", "element": "H"}], "resSeq": 171, "segmentID": ""}, {"index": 171, "name": "HOH", "atoms": [{"index": 526, "name": "O", "element": "O"}, {"index": 527, "name": "H1", "element": "H"}, {"index": 528, "name": "H2", "element": "H"}], "resSeq": 172, "segmentID": ""}, {"index": 172, "name": "HOH", "atoms": [{"index": 529, "name": "O", "element": "O"}, {"index": 530, "name": "H1", "element": "H"}, {"index": 531, "name": "H2", "element": "H"}], "resSeq": 173, "segmentID": ""}, {"index": 173, "name": "HOH", "atoms": [{"index": 532, "name": "O", "element": "O"}, {"index": 533, "name": "H1", "element": "H"}, {"index": 534, "name": "H2", "element": "H"}], "resSeq": 174, "segmentID": ""}, {"index": 174, "name": "HOH", "atoms": [{"index": 535, "name": "O", "element": "O"}, {"index": 536, "name": "H1", "element": "H"}, {"index": 537, "name": "H2", "element": "H"}], "resSeq": 175, "segmentID": ""}, {"index": 175, "name": "HOH", "atoms": [{"index": 538, "name": "O", "element": "O"}, {"index": 539, "name": "H1", "element": "H"}, {"index": 540, "name": "H2", "element": "H"}], "resSeq": 176, "segmentID": ""}, {"index": 176, "name": "HOH", "atoms": [{"index": 541, "name": "O", "element": "O"}, {"index": 542, "name": "H1", "element": "H"}, {"index": 543, "name": "H2", "element": "H"}], "resSeq": 177, "segmentID": ""}, {"index": 177, "name": "HOH", "atoms": [{"index": 544, "name": "O", "element": "O"}, {"index": 545, "name": "H1", "element": "H"}, {"index": 546, "name": "H2", "element": "H"}], "resSeq": 178, "segmentID": ""}, {"index": 178, "name": "HOH", "atoms": [{"index": 547, "name": "O", "element": "O"}, {"index": 548, "name": "H1", "element": "H"}, {"index": 549, "name": "H2", "element": "H"}], "resSeq": 179, "segmentID": ""}, {"index": 179, "name": "HOH", "atoms": [{"index": 550, "name": "O", "element": "O"}, {"index": 551, "name": "H1", "element": "H"}, {"index": 552, "name": "H2", "element": "H"}], "resSeq": 180, "segmentID": ""}, {"index": 180, "name": "HOH", "atoms": [{"index": 553, "name": "O", "element": "O"}, {"index": 554, "name": "H1", "element": "H"}, {"index": 555, "name": "H2", "element": "H"}], "resSeq": 181, "segmentID": ""}, {"index": 181, "name": "HOH", "atoms": [{"index": 556, "name": "O", "element": "O"}, {"index": 557, "name": "H1", "element": "H"}, {"index": 558, "name": "H2", "element": "H"}], "resSeq": 182, "segmentID": ""}, {"index": 182, "name": "HOH", "atoms": [{"index": 559, "name": "O", "element": "O"}, {"index": 560, "name": "H1", "element": "H"}, {"index": 561, "name": "H2", "element": "H"}], "resSeq": 183, "segmentID": ""}, {"index": 183, "name": "HOH", "atoms": [{"index": 562, "name": "O", "element": "O"}, {"index": 563, "name": "H1", "element": "H"}, {"index": 564, "name": "H2", "element": "H"}], "resSeq": 184, "segmentID": ""}, {"index": 184, "name": "HOH", "atoms": [{"index": 565, "name": "O", "element": "O"}, {"index": 566, "name": "H1", "element": "H"}, {"index": 567, "name": "H2", "element": "H"}], "resSeq": 185, "segmentID": ""}, {"index": 185, "name": "HOH", "atoms": [{"index": 568, "name": "O", "element": "O"}, {"index": 569, "name": "H1", "element": "H"}, {"index": 570, "name": "H2", "element": "H"}], "resSeq": 186, "segmentID": ""}, {"index": 186, "name": "HOH", "atoms": [{"index": 571, "name": "O", "element": "O"}, {"index": 572, "name": "H1", "element": "H"}, {"index": 573, "name": "H2", "element": "H"}], "resSeq": 187, "segmentID": ""}, {"index": 187, "name": "HOH", "atoms": [{"index": 574, "name": "O", "element": "O"}, {"index": 575, "name": "H1", "element": "H"}, {"index": 576, "name": "H2", "element": "H"}], "resSeq": 188, "segmentID": ""}, {"index": 188, "name": "HOH", "atoms": [{"index": 577, "name": "O", "element": "O"}, {"index": 578, "name": "H1", "element": "H"}, {"index": 579, "name": "H2", "element": "H"}], "resSeq": 189, "segmentID": ""}, {"index": 189, "name": "HOH", "atoms": [{"index": 580, "name": "O", "element": "O"}, {"index": 581, "name": "H1", "element": "H"}, {"index": 582, "name": "H2", "element": "H"}], "resSeq": 190, "segmentID": ""}, {"index": 190, "name": "HOH", "atoms": [{"index": 583, "name": "O", "element": "O"}, {"index": 584, "name": "H1", "element": "H"}, {"index": 585, "name": "H2", "element": "H"}], "resSeq": 191, "segmentID": ""}, {"index": 191, "name": "HOH", "atoms": [{"index": 586, "name": "O", "element": "O"}, {"index": 587, "name": "H1", "element": "H"}, {"index": 588, "name": "H2", "element": "H"}], "resSeq": 192, "segmentID": ""}, {"index": 192, "name": "HOH", "atoms": [{"index": 589, "name": "O", "element": "O"}, {"index": 590, "name": "H1", "element": "H"}, {"index": 591, "name": "H2", "element": "H"}], "resSeq": 193, "segmentID": ""}, {"index": 193, "name": "HOH", "atoms": [{"index": 592, "name": "O", "element": "O"}, {"index": 593, "name": "H1", "element": "H"}, {"index": 594, "name": "H2", "element": "H"}], "resSeq": 194, "segmentID": ""}, {"index": 194, "name": "HOH", "atoms": [{"index": 595, "name": "O", "element": "O"}, {"index": 596, "name": "H1", "element": "H"}, {"index": 597, "name": "H2", "element": "H"}], "resSeq": 195, "segmentID": ""}, {"index": 195, "name": "HOH", "atoms": [{"index": 598, "name": "O", "element": "O"}, {"index": 599, "name": "H1", "element": "H"}, {"index": 600, "name": "H2", "element": "H"}], "resSeq": 196, "segmentID": ""}, {"index": 196, "name": "HOH", "atoms": [{"index": 601, "name": "O", "element": "O"}, {"index": 602, "name": "H1", "element": "H"}, {"index": 603, "name": "H2", "element": "H"}], "resSeq": 197, "segmentID": ""}, {"index": 197, "name": "HOH", "atoms": [{"index": 604, "name": "O", "element": "O"}, {"index": 605, "name": "H1", "element": "H"}, {"index": 606, "name": "H2", "element": "H"}], "resSeq": 198, "segmentID": ""}, {"index": 198, "name": "HOH", "atoms": [{"index": 607, "name": "O", "element": "O"}, {"index": 608, "name": "H1", "element": "H"}, {"index": 609, "name": "H2", "element": "H"}], "resSeq": 199, "segmentID": ""}, {"index": 199, "name": "HOH", "atoms": [{"index": 610, "name": "O", "element": "O"}, {"index": 611, "name": "H1", "element": "H"}, {"index": 612, "name": "H2", "element": "H"}], "resSeq": 200, "segmentID": ""}, {"index": 200, "name": "HOH", "atoms": [{"index": 613, "name": "O", "element": "O"}, {"index": 614, "name": "H1", "element": "H"}, {"index": 615, "name": "H2", "element": "H"}], "resSeq": 201, "segmentID": ""}, {"index": 201, "name": "HOH", "atoms": [{"index": 616, "name": "O", "element": "O"}, {"index": 617, "name": "H1", "element": "H"}, {"index": 618, "name": "H2", "element": "H"}], "resSeq": 202, "segmentID": ""}, {"index": 202, "name": "HOH", "atoms": [{"index": 619, "name": "O", "element": "O"}, {"index": 620, "name": "H1", "element": "H"}, {"index": 621, "name": "H2", "element": "H"}], "resSeq": 203, "segmentID": ""}, {"index": 203, "name": "HOH", "atoms": [{"index": 622, "name": "O", "element": "O"}, {"index": 623, "name": "H1", "element": "H"}, {"index": 624, "name": "H2", "element": "H"}], "resSeq": 204, "segmentID": ""}, {"index": 204, "name": "HOH", "atoms": [{"index": 625, "name": "O", "element": "O"}, {"index": 626, "name": "H1", "element": "H"}, {"index": 627, "name": "H2", "element": "H"}], "resSeq": 205, "segmentID": ""}, {"index": 205, "name": "HOH", "atoms": [{"index": 628, "name": "O", "element": "O"}, {"index": 629, "name": "H1", "element": "H"}, {"index": 630, "name": "H2", "element": "H"}], "resSeq": 206, "segmentID": ""}, {"index": 206, "name": "HOH", "atoms": [{"index": 631, "name": "O", "element": "O"}, {"index": 632, "name": "H1", "element": "H"}, {"index": 633, "name": "H2", "element": "H"}], "resSeq": 207, "segmentID": ""}, {"index": 207, "name": "HOH", "atoms": [{"index": 634, "name": "O", "element": "O"}, {"index": 635, "name": "H1", "element": "H"}, {"index": 636, "name": "H2", "element": "H"}], "resSeq": 208, "segmentID": ""}, {"index": 208, "name": "HOH", "atoms": [{"index": 637, "name": "O", "element": "O"}, {"index": 638, "name": "H1", "element": "H"}, {"index": 639, "name": "H2", "element": "H"}], "resSeq": 209, "segmentID": ""}, {"index": 209, "name": "HOH", "atoms": [{"index": 640, "name": "O", "element": "O"}, {"index": 641, "name": "H1", "element": "H"}, {"index": 642, "name": "H2", "element": "H"}], "resSeq": 210, "segmentID": ""}, {"index": 210, "name": "HOH", "atoms": [{"index": 643, "name": "O", "element": "O"}, {"index": 644, "name": "H1", "element": "H"}, {"index": 645, "name": "H2", "element": "H"}], "resSeq": 211, "segmentID": ""}, {"index": 211, "name": "HOH", "atoms": [{"index": 646, "name": "O", "element": "O"}, {"index": 647, "name": "H1", "element": "H"}, {"index": 648, "name": "H2", "element": "H"}], "resSeq": 212, "segmentID": ""}, {"index": 212, "name": "HOH", "atoms": [{"index": 649, "name": "O", "element": "O"}, {"index": 650, "name": "H1", "element": "H"}, {"index": 651, "name": "H2", "element": "H"}], "resSeq": 213, "segmentID": ""}, {"index": 213, "name": "HOH", "atoms": [{"index": 652, "name": "O", "element": "O"}, {"index": 653, "name": "H1", "element": "H"}, {"index": 654, "name": "H2", "element": "H"}], "resSeq": 214, "segmentID": ""}, {"index": 214, "name": "HOH", "atoms": [{"index": 655, "name": "O", "element": "O"}, {"index": 656, "name": "H1", "element": "H"}, {"index": 657, "name": "H2", "element": "H"}], "resSeq": 215, "segmentID": ""}, {"index": 215, "name": "HOH", "atoms": [{"index": 658, "name": "O", "element": "O"}, {"index": 659, "name": "H1", "element": "H"}, {"index": 660, "name": "H2", "element": "H"}], "resSeq": 216, "segmentID": ""}, {"index": 216, "name": "HOH", "atoms": [{"index": 661, "name": "O", "element": "O"}, {"index": 662, "name": "H1", "element": "H"}, {"index": 663, "name": "H2", "element": "H"}], "resSeq": 217, "segmentID": ""}, {"index": 217, "name": "HOH", "atoms": [{"index": 664, "name": "O", "element": "O"}, {"index": 665, "name": "H1", "element": "H"}, {"index": 666, "name": "H2", "element": "H"}], "resSeq": 218, "segmentID": ""}, {"index": 218, "name": "HOH", "atoms": [{"index": 667, "name": "O", "element": "O"}, {"index": 668, "name": "H1", "element": "H"}, {"index": 669, "name": "H2", "element": "H"}], "resSeq": 219, "segmentID": ""}, {"index": 219, "name": "HOH", "atoms": [{"index": 670, "name": "O", "element": "O"}, {"index": 671, "name": "H1", "element": "H"}, {"index": 672, "name": "H2", "element": "H"}], "resSeq": 220, "segmentID": ""}, {"index": 220, "name": "HOH", "atoms": [{"index": 673, "name": "O", "element": "O"}, {"index": 674, "name": "H1", "element": "H"}, {"index": 675, "name": "H2", "element": "H"}], "resSeq": 221, "segmentID": ""}, {"index": 221, "name": "HOH", "atoms": [{"index": 676, "name": "O", "element": "O"}, {"index": 677, "name": "H1", "element": "H"}, {"index": 678, "name": "H2", "element": "H"}], "resSeq": 222, "segmentID": ""}, {"index": 222, "name": "HOH", "atoms": [{"index": 679, "name": "O", "element": "O"}, {"index": 680, "name": "H1", "element": "H"}, {"index": 681, "name": "H2", "element": "H"}], "resSeq": 223, "segmentID": ""}, {"index": 223, "name": "HOH", "atoms": [{"index": 682, "name": "O", "element": "O"}, {"index": 683, "name": "H1", "element": "H"}, {"index": 684, "name": "H2", "element": "H"}], "resSeq": 224, "segmentID": ""}, {"index": 224, "name": "HOH", "atoms": [{"index": 685, "name": "O", "element": "O"}, {"index": 686, "name": "H1", "element": "H"}, {"index": 687, "name": "H2", "element": "H"}], "resSeq": 225, "segmentID": ""}, {"index": 225, "name": "HOH", "atoms": [{"index": 688, "name": "O", "element": "O"}, {"index": 689, "name": "H1", "element": "H"}, {"index": 690, "name": "H2", "element": "H"}], "resSeq": 226, "segmentID": ""}, {"index": 226, "name": "HOH", "atoms": [{"index": 691, "name": "O", "element": "O"}, {"index": 692, "name": "H1", "element": "H"}, {"index": 693, "name": "H2", "element": "H"}], "resSeq": 227, "segmentID": ""}, {"index": 227, "name": "HOH", "atoms": [{"index": 694, "name": "O", "element": "O"}, {"index": 695, "name": "H1", "element": "H"}, {"index": 696, "name": "H2", "element": "H"}], "resSeq": 228, "segmentID": ""}, {"index": 228, "name": "HOH", "atoms": [{"index": 697, "name": "O", "element": "O"}, {"index": 698, "name": "H1", "element": "H"}, {"index": 699, "name": "H2", "element": "H"}], "resSeq": 229, "segmentID": ""}, {"index": 229, "name": "HOH", "atoms": [{"index": 700, "name": "O", "element": "O"}, {"index": 701, "name": "H1", "element": "H"}, {"index": 702, "name": "H2", "element": "H"}], "resSeq": 230, "segmentID": ""}, {"index": 230, "name": "HOH", "atoms": [{"index": 703, "name": "O", "element": "O"}, {"index": 704, "name": "H1", "element": "H"}, {"index": 705, "name": "H2", "element": "H"}], "resSeq": 231, "segmentID": ""}, {"index": 231, "name": "HOH", "atoms": [{"index": 706, "name": "O", "element": "O"}, {"index": 707, "name": "H1", "element": "H"}, {"index": 708, "name": "H2", "element": "H"}], "resSeq": 232, "segmentID": ""}, {"index": 232, "name": "HOH", "atoms": [{"index": 709, "name": "O", "element": "O"}, {"index": 710, "name": "H1", "element": "H"}, {"index": 711, "name": "H2", "element": "H"}], "resSeq": 233, "segmentID": ""}, {"index": 233, "name": "HOH", "atoms": [{"index": 712, "name": "O", "element": "O"}, {"index": 713, "name": "H1", "element": "H"}, {"index": 714, "name": "H2", "element": "H"}], "resSeq": 234, "segmentID": ""}, {"index": 234, "name": "HOH", "atoms": [{"index": 715, "name": "O", "element": "O"}, {"index": 716, "name": "H1", "element": "H"}, {"index": 717, "name": "H2", "element": "H"}], "resSeq": 235, "segmentID": ""}, {"index": 235, "name": "HOH", "atoms": [{"index": 718, "name": "O", "element": "O"}, {"index": 719, "name": "H1", "element": "H"}, {"index": 720, "name": "H2", "element": "H"}], "resSeq": 236, "segmentID": ""}, {"index": 236, "name": "HOH", "atoms": [{"index": 721, "name": "O", "element": "O"}, {"index": 722, "name": "H1", "element": "H"}, {"index": 723, "name": "H2", "element": "H"}], "resSeq": 237, "segmentID": ""}, {"index": 237, "name": "HOH", "atoms": [{"index": 724, "name": "O", "element": "O"}, {"index": 725, "name": "H1", "element": "H"}, {"index": 726, "name": "H2", "element": "H"}], "resSeq": 238, "segmentID": ""}, {"index": 238, "name": "HOH", "atoms": [{"index": 727, "name": "O", "element": "O"}, {"index": 728, "name": "H1", "element": "H"}, {"index": 729, "name": "H2", "element": "H"}], "resSeq": 239, "segmentID": ""}, {"index": 239, "name": "HOH", "atoms": [{"index": 730, "name": "O", "element": "O"}, {"index": 731, "name": "H1", "element": "H"}, {"index": 732, "name": "H2", "element": "H"}], "resSeq": 240, "segmentID": ""}, {"index": 240, "name": "HOH", "atoms": [{"index": 733, "name": "O", "element": "O"}, {"index": 734, "name": "H1", "element": "H"}, {"index": 735, "name": "H2", "element": "H"}], "resSeq": 241, "segmentID": ""}, {"index": 241, "name": "HOH", "atoms": [{"index": 736, "name": "O", "element": "O"}, {"index": 737, "name": "H1", "element": "H"}, {"index": 738, "name": "H2", "element": "H"}], "resSeq": 242, "segmentID": ""}, {"index": 242, "name": "HOH", "atoms": [{"index": 739, "name": "O", "element": "O"}, {"index": 740, "name": "H1", "element": "H"}, {"index": 741, "name": "H2", "element": "H"}], "resSeq": 243, "segmentID": ""}, {"index": 243, "name": "HOH", "atoms": [{"index": 742, "name": "O", "element": "O"}, {"index": 743, "name": "H1", "element": "H"}, {"index": 744, "name": "H2", "element": "H"}], "resSeq": 244, "segmentID": ""}, {"index": 244, "name": "HOH", "atoms": [{"index": 745, "name": "O", "element": "O"}, {"index": 746, "name": "H1", "element": "H"}, {"index": 747, "name": "H2", "element": "H"}], "resSeq": 245, "segmentID": ""}, {"index": 245, "name": "HOH", "atoms": [{"index": 748, "name": "O", "element": "O"}, {"index": 749, "name": "H1", "element": "H"}, {"index": 750, "name": "H2", "element": "H"}], "resSeq": 246, "segmentID": ""}, {"index": 246, "name": "HOH", "atoms": [{"index": 751, "name": "O", "element": "O"}, {"index": 752, "name": "H1", "element": "H"}, {"index": 753, "name": "H2", "element": "H"}], "resSeq": 247, "segmentID": ""}, {"index": 247, "name": "HOH", "atoms": [{"index": 754, "name": "O", "element": "O"}, {"index": 755, "name": "H1", "element": "H"}, {"index": 756, "name": "H2", "element": "H"}], "resSeq": 248, "segmentID": ""}, {"index": 248, "name": "HOH", "atoms": [{"index": 757, "name": "O", "element": "O"}, {"index": 758, "name": "H1", "element": "H"}, {"index": 759, "name": "H2", "element": "H"}], "resSeq": 249, "segmentID": ""}, {"index": 249, "name": "HOH", "atoms": [{"index": 760, "name": "O", "element": "O"}, {"index": 761, "name": "H1", "element": "H"}, {"index": 762, "name": "H2", "element": "H"}], "resSeq": 250, "segmentID": ""}, {"index": 250, "name": "HOH", "atoms": [{"index": 763, "name": "O", "element": "O"}, {"index": 764, "name": "H1", "element": "H"}, {"index": 765, "name": "H2", "element": "H"}], "resSeq": 251, "segmentID": ""}, {"index": 251, "name": "HOH", "atoms": [{"index": 766, "name": "O", "element": "O"}, {"index": 767, "name": "H1", "element": "H"}, {"index": 768, "name": "H2", "element": "H"}], "resSeq": 252, "segmentID": ""}, {"index": 252, "name": "HOH", "atoms": [{"index": 769, "name": "O", "element": "O"}, {"index": 770, "name": "H1", "element": "H"}, {"index": 771, "name": "H2", "element": "H"}], "resSeq": 253, "segmentID": ""}, {"index": 253, "name": "HOH", "atoms": [{"index": 772, "name": "O", "element": "O"}, {"index": 773, "name": "H1", "element": "H"}, {"index": 774, "name": "H2", "element": "H"}], "resSeq": 254, "segmentID": ""}, {"index": 254, "name": "HOH", "atoms": [{"index": 775, "name": "O", "element": "O"}, {"index": 776, "name": "H1", "element": "H"}, {"index": 777, "name": "H2", "element": "H"}], "resSeq": 255, "segmentID": ""}, {"index": 255, "name": "HOH", "atoms": [{"index": 778, "name": "O", "element": "O"}, {"index": 779, "name": "H1", "element": "H"}, {"index": 780, "name": "H2", "element": "H"}], "resSeq": 256, "segmentID": ""}, {"index": 256, "name": "HOH", "atoms": [{"index": 781, "name": "O", "element": "O"}, {"index": 782, "name": "H1", "element": "H"}, {"index": 783, "name": "H2", "element": "H"}], "resSeq": 257, "segmentID": ""}, {"index": 257, "name": "HOH", "atoms": [{"index": 784, "name": "O", "element": "O"}, {"index": 785, "name": "H1", "element": "H"}, {"index": 786, "name": "H2", "element": "H"}], "resSeq": 258, "segmentID": ""}, {"index": 258, "name": "HOH", "atoms": [{"index": 787, "name": "O", "element": "O"}, {"index": 788, "name": "H1", "element": "H"}, {"index": 789, "name": "H2", "element": "H"}], "resSeq": 259, "segmentID": ""}, {"index": 259, "name": "HOH", "atoms": [{"index": 790, "name": "O", "element": "O"}, {"index": 791, "name": "H1", "element": "H"}, {"index": 792, "name": "H2", "element": "H"}], "resSeq": 260, "segmentID": ""}, {"index": 260, "name": "HOH", "atoms": [{"index": 793, "name": "O", "element": "O"}, {"index": 794, "name": "H1", "element": "H"}, {"index": 795, "name": "H2", "element": "H"}], "resSeq": 261, "segmentID": ""}, {"index": 261, "name": "HOH", "atoms": [{"index": 796, "name": "O", "element": "O"}, {"index": 797, "name": "H1", "element": "H"}, {"index": 798, "name": "H2", "element": "H"}], "resSeq": 262, "segmentID": ""}, {"index": 262, "name": "HOH", "atoms": [{"index": 799, "name": "O", "element": "O"}, {"index": 800, "name": "H1", "element": "H"}, {"index": 801, "name": "H2", "element": "H"}], "resSeq": 263, "segmentID": ""}, {"index": 263, "name": "HOH", "atoms": [{"index": 802, "name": "O", "element": "O"}, {"index": 803, "name": "H1", "element": "H"}, {"index": 804, "name": "H2", "element": "H"}], "resSeq": 264, "segmentID": ""}, {"index": 264, "name": "HOH", "atoms": [{"index": 805, "name": "O", "element": "O"}, {"index": 806, "name": "H1", "element": "H"}, {"index": 807, "name": "H2", "element": "H"}], "resSeq": 265, "segmentID": ""}, {"index": 265, "name": "HOH", "atoms": [{"index": 808, "name": "O", "element": "O"}, {"index": 809, "name": "H1", "element": "H"}, {"index": 810, "name": "H2", "element": "H"}], "resSeq": 266, "segmentID": ""}, {"index": 266, "name": "HOH", "atoms": [{"index": 811, "name": "O", "element": "O"}, {"index": 812, "name": "H1", "element": "H"}, {"index": 813, "name": "H2", "element": "H"}], "resSeq": 267, "segmentID": ""}, {"index": 267, "name": "HOH", "atoms": [{"index": 814, "name": "O", "element": "O"}, {"index": 815, "name": "H1", "element": "H"}, {"index": 816, "name": "H2", "element": "H"}], "resSeq": 268, "segmentID": ""}, {"index": 268, "name": "HOH", "atoms": [{"index": 817, "name": "O", "element": "O"}, {"index": 818, "name": "H1", "element": "H"}, {"index": 819, "name": "H2", "element": "H"}], "resSeq": 269, "segmentID": ""}, {"index": 269, "name": "HOH", "atoms": [{"index": 820, "name": "O", "element": "O"}, {"index": 821, "name": "H1", "element": "H"}, {"index": 822, "name": "H2", "element": "H"}], "resSeq": 270, "segmentID": ""}, {"index": 270, "name": "HOH", "atoms": [{"index": 823, "name": "O", "element": "O"}, {"index": 824, "name": "H1", "element": "H"}, {"index": 825, "name": "H2", "element": "H"}], "resSeq": 271, "segmentID": ""}, {"index": 271, "name": "HOH", "atoms": [{"index": 826, "name": "O", "element": "O"}, {"index": 827, "name": "H1", "element": "H"}, {"index": 828, "name": "H2", "element": "H"}], "resSeq": 272, "segmentID": ""}, {"index": 272, "name": "HOH", "atoms": [{"index": 829, "name": "O", "element": "O"}, {"index": 830, "name": "H1", "element": "H"}, {"index": 831, "name": "H2", "element": "H"}], "resSeq": 273, "segmentID": ""}, {"index": 273, "name": "HOH", "atoms": [{"index": 832, "name": "O", "element": "O"}, {"index": 833, "name": "H1", "element": "H"}, {"index": 834, "name": "H2", "element": "H"}], "resSeq": 274, "segmentID": ""}, {"index": 274, "name": "HOH", "atoms": [{"index": 835, "name": "O", "element": "O"}, {"index": 836, "name": "H1", "element": "H"}, {"index": 837, "name": "H2", "element": "H"}], "resSeq": 275, "segmentID": ""}, {"index": 275, "name": "HOH", "atoms": [{"index": 838, "name": "O", "element": "O"}, {"index": 839, "name": "H1", "element": "H"}, {"index": 840, "name": "H2", "element": "H"}], "resSeq": 276, "segmentID": ""}, {"index": 276, "name": "HOH", "atoms": [{"index": 841, "name": "O", "element": "O"}, {"index": 842, "name": "H1", "element": "H"}, {"index": 843, "name": "H2", "element": "H"}], "resSeq": 277, "segmentID": ""}, {"index": 277, "name": "HOH", "atoms": [{"index": 844, "name": "O", "element": "O"}, {"index": 845, "name": "H1", "element": "H"}, {"index": 846, "name": "H2", "element": "H"}], "resSeq": 278, "segmentID": ""}, {"index": 278, "name": "HOH", "atoms": [{"index": 847, "name": "O", "element": "O"}, {"index": 848, "name": "H1", "element": "H"}, {"index": 849, "name": "H2", "element": "H"}], "resSeq": 279, "segmentID": ""}, {"index": 279, "name": "HOH", "atoms": [{"index": 850, "name": "O", "element": "O"}, {"index": 851, "name": "H1", "element": "H"}, {"index": 852, "name": "H2", "element": "H"}], "resSeq": 280, "segmentID": ""}, {"index": 280, "name": "HOH", "atoms": [{"index": 853, "name": "O", "element": "O"}, {"index": 854, "name": "H1", "element": "H"}, {"index": 855, "name": "H2", "element": "H"}], "resSeq": 281, "segmentID": ""}, {"index": 281, "name": "HOH", "atoms": [{"index": 856, "name": "O", "element": "O"}, {"index": 857, "name": "H1", "element": "H"}, {"index": 858, "name": "H2", "element": "H"}], "resSeq": 282, "segmentID": ""}, {"index": 282, "name": "HOH", "atoms": [{"index": 859, "name": "O", "element": "O"}, {"index": 860, "name": "H1", "element": "H"}, {"index": 861, "name": "H2", "element": "H"}], "resSeq": 283, "segmentID": ""}, {"index": 283, "name": "HOH", "atoms": [{"index": 862, "name": "O", "element": "O"}, {"index": 863, "name": "H1", "element": "H"}, {"index": 864, "name": "H2", "element": "H"}], "resSeq": 284, "segmentID": ""}, {"index": 284, "name": "HOH", "atoms": [{"index": 865, "name": "O", "element": "O"}, {"index": 866, "name": "H1", "element": "H"}, {"index": 867, "name": "H2", "element": "H"}], "resSeq": 285, "segmentID": ""}, {"index": 285, "name": "HOH", "atoms": [{"index": 868, "name": "O", "element": "O"}, {"index": 869, "name": "H1", "element": "H"}, {"index": 870, "name": "H2", "element": "H"}], "resSeq": 286, "segmentID": ""}, {"index": 286, "name": "HOH", "atoms": [{"index": 871, "name": "O", "element": "O"}, {"index": 872, "name": "H1", "element": "H"}, {"index": 873, "name": "H2", "element": "H"}], "resSeq": 287, "segmentID": ""}, {"index": 287, "name": "HOH", "atoms": [{"index": 874, "name": "O", "element": "O"}, {"index": 875, "name": "H1", "element": "H"}, {"index": 876, "name": "H2", "element": "H"}], "resSeq": 288, "segmentID": ""}, {"index": 288, "name": "HOH", "atoms": [{"index": 877, "name": "O", "element": "O"}, {"index": 878, "name": "H1", "element": "H"}, {"index": 879, "name": "H2", "element": "H"}], "resSeq": 289, "segmentID": ""}, {"index": 289, "name": "HOH", "atoms": [{"index": 880, "name": "O", "element": "O"}, {"index": 881, "name": "H1", "element": "H"}, {"index": 882, "name": "H2", "element": "H"}], "resSeq": 290, "segmentID": ""}, {"index": 290, "name": "HOH", "atoms": [{"index": 883, "name": "O", "element": "O"}, {"index": 884, "name": "H1", "element": "H"}, {"index": 885, "name": "H2", "element": "H"}], "resSeq": 291, "segmentID": ""}, {"index": 291, "name": "HOH", "atoms": [{"index": 886, "name": "O", "element": "O"}, {"index": 887, "name": "H1", "element": "H"}, {"index": 888, "name": "H2", "element": "H"}], "resSeq": 292, "segmentID": ""}, {"index": 292, "name": "HOH", "atoms": [{"index": 889, "name": "O", "element": "O"}, {"index": 890, "name": "H1", "element": "H"}, {"index": 891, "name": "H2", "element": "H"}], "resSeq": 293, "segmentID": ""}, {"index": 293, "name": "HOH", "atoms": [{"index": 892, "name": "O", "element": "O"}, {"index": 893, "name": "H1", "element": "H"}, {"index": 894, "name": "H2", "element": "H"}], "resSeq": 294, "segmentID": ""}, {"index": 294, "name": "HOH", "atoms": [{"index": 895, "name": "O", "element": "O"}, {"index": 896, "name": "H1", "element": "H"}, {"index": 897, "name": "H2", "element": "H"}], "resSeq": 295, "segmentID": ""}, {"index": 295, "name": "HOH", "atoms": [{"index": 898, "name": "O", "element": "O"}, {"index": 899, "name": "H1", "element": "H"}, {"index": 900, "name": "H2", "element": "H"}], "resSeq": 296, "segmentID": ""}, {"index": 296, "name": "HOH", "atoms": [{"index": 901, "name": "O", "element": "O"}, {"index": 902, "name": "H1", "element": "H"}, {"index": 903, "name": "H2", "element": "H"}], "resSeq": 297, "segmentID": ""}, {"index": 297, "name": "HOH", "atoms": [{"index": 904, "name": "O", "element": "O"}, {"index": 905, "name": "H1", "element": "H"}, {"index": 906, "name": "H2", "element": "H"}], "resSeq": 298, "segmentID": ""}, {"index": 298, "name": "HOH", "atoms": [{"index": 907, "name": "O", "element": "O"}, {"index": 908, "name": "H1", "element": "H"}, {"index": 909, "name": "H2", "element": "H"}], "resSeq": 299, "segmentID": ""}, {"index": 299, "name": "HOH", "atoms": [{"index": 910, "name": "O", "element": "O"}, {"index": 911, "name": "H1", "element": "H"}, {"index": 912, "name": "H2", "element": "H"}], "resSeq": 300, "segmentID": ""}, {"index": 300, "name": "HOH", "atoms": [{"index": 913, "name": "O", "element": "O"}, {"index": 914, "name": "H1", "element": "H"}, {"index": 915, "name": "H2", "element": "H"}], "resSeq": 301, "segmentID": ""}, {"index": 301, "name": "HOH", "atoms": [{"index": 916, "name": "O", "element": "O"}, {"index": 917, "name": "H1", "element": "H"}, {"index": 918, "name": "H2", "element": "H"}], "resSeq": 302, "segmentID": ""}, {"index": 302, "name": "HOH", "atoms": [{"index": 919, "name": "O", "element": "O"}, {"index": 920, "name": "H1", "element": "H"}, {"index": 921, "name": "H2", "element": "H"}], "resSeq": 303, "segmentID": ""}, {"index": 303, "name": "HOH", "atoms": [{"index": 922, "name": "O", "element": "O"}, {"index": 923, "name": "H1", "element": "H"}, {"index": 924, "name": "H2", "element": "H"}], "resSeq": 304, "segmentID": ""}, {"index": 304, "name": "HOH", "atoms": [{"index": 925, "name": "O", "element": "O"}, {"index": 926, "name": "H1", "element": "H"}, {"index": 927, "name": "H2", "element": "H"}], "resSeq": 305, "segmentID": ""}, {"index": 305, "name": "HOH", "atoms": [{"index": 928, "name": "O", "element": "O"}, {"index": 929, "name": "H1", "element": "H"}, {"index": 930, "name": "H2", "element": "H"}], "resSeq": 306, "segmentID": ""}, {"index": 306, "name": "HOH", "atoms": [{"index": 931, "name": "O", "element": "O"}, {"index": 932, "name": "H1", "element": "H"}, {"index": 933, "name": "H2", "element": "H"}], "resSeq": 307, "segmentID": ""}, {"index": 307, "name": "HOH", "atoms": [{"index": 934, "name": "O", "element": "O"}, {"index": 935, "name": "H1", "element": "H"}, {"index": 936, "name": "H2", "element": "H"}], "resSeq": 308, "segmentID": ""}, {"index": 308, "name": "HOH", "atoms": [{"index": 937, "name": "O", "element": "O"}, {"index": 938, "name": "H1", "element": "H"}, {"index": 939, "name": "H2", "element": "H"}], "resSeq": 309, "segmentID": ""}, {"index": 309, "name": "HOH", "atoms": [{"index": 940, "name": "O", "element": "O"}, {"index": 941, "name": "H1", "element": "H"}, {"index": 942, "name": "H2", "element": "H"}], "resSeq": 310, "segmentID": ""}, {"index": 310, "name": "HOH", "atoms": [{"index": 943, "name": "O", "element": "O"}, {"index": 944, "name": "H1", "element": "H"}, {"index": 945, "name": "H2", "element": "H"}], "resSeq": 311, "segmentID": ""}, {"index": 311, "name": "HOH", "atoms": [{"index": 946, "name": "O", "element": "O"}, {"index": 947, "name": "H1", "element": "H"}, {"index": 948, "name": "H2", "element": "H"}], "resSeq": 312, "segmentID": ""}, {"index": 312, "name": "HOH", "atoms": [{"index": 949, "name": "O", "element": "O"}, {"index": 950, "name": "H1", "element": "H"}, {"index": 951, "name": "H2", "element": "H"}], "resSeq": 313, "segmentID": ""}, {"index": 313, "name": "HOH", "atoms": [{"index": 952, "name": "O", "element": "O"}, {"index": 953, "name": "H1", "element": "H"}, {"index": 954, "name": "H2", "element": "H"}], "resSeq": 314, "segmentID": ""}, {"index": 314, "name": "HOH", "atoms": [{"index": 955, "name": "O", "element": "O"}, {"index": 956, "name": "H1", "element": "H"}, {"index": 957, "name": "H2", "element": "H"}], "resSeq": 315, "segmentID": ""}, {"index": 315, "name": "HOH", "atoms": [{"index": 958, "name": "O", "element": "O"}, {"index": 959, "name": "H1", "element": "H"}, {"index": 960, "name": "H2", "element": "H"}], "resSeq": 316, "segmentID": ""}, {"index": 316, "name": "HOH", "atoms": [{"index": 961, "name": "O", "element": "O"}, {"index": 962, "name": "H1", "element": "H"}, {"index": 963, "name": "H2", "element": "H"}], "resSeq": 317, "segmentID": ""}, {"index": 317, "name": "HOH", "atoms": [{"index": 964, "name": "O", "element": "O"}, {"index": 965, "name": "H1", "element": "H"}, {"index": 966, "name": "H2", "element": "H"}], "resSeq": 318, "segmentID": ""}, {"index": 318, "name": "HOH", "atoms": [{"index": 967, "name": "O", "element": "O"}, {"index": 968, "name": "H1", "element": "H"}, {"index": 969, "name": "H2", "element": "H"}], "resSeq": 319, "segmentID": ""}, {"index": 319, "name": "HOH", "atoms": [{"index": 970, "name": "O", "element": "O"}, {"index": 971, "name": "H1", "element": "H"}, {"index": 972, "name": "H2", "element": "H"}], "resSeq": 320, "segmentID": ""}, {"index": 320, "name": "HOH", "atoms": [{"index": 973, "name": "O", "element": "O"}, {"index": 974, "name": "H1", "element": "H"}, {"index": 975, "name": "H2", "element": "H"}], "resSeq": 321, "segmentID": ""}, {"index": 321, "name": "HOH", "atoms": [{"index": 976, "name": "O", "element": "O"}, {"index": 977, "name": "H1", "element": "H"}, {"index": 978, "name": "H2", "element": "H"}], "resSeq": 322, "segmentID": ""}, {"index": 322, "name": "HOH", "atoms": [{"index": 979, "name": "O", "element": "O"}, {"index": 980, "name": "H1", "element": "H"}, {"index": 981, "name": "H2", "element": "H"}], "resSeq": 323, "segmentID": ""}, {"index": 323, "name": "HOH", "atoms": [{"index": 982, "name": "O", "element": "O"}, {"index": 983, "name": "H1", "element": "H"}, {"index": 984, "name": "H2", "element": "H"}], "resSeq": 324, "segmentID": ""}, {"index": 324, "name": "HOH", "atoms": [{"index": 985, "name": "O", "element": "O"}, {"index": 986, "name": "H1", "element": "H"}, {"index": 987, "name": "H2", "element": "H"}], "resSeq": 325, "segmentID": ""}, {"index": 325, "name": "HOH", "atoms": [{"index": 988, "name": "O", "element": "O"}, {"index": 989, "name": "H1", "element": "H"}, {"index": 990, "name": "H2", "element": "H"}], "resSeq": 326, "segmentID": ""}, {"index": 326, "name": "HOH", "atoms": [{"index": 991, "name": "O", "element": "O"}, {"index": 992, "name": "H1", "element": "H"}, {"index": 993, "name": "H2", "element": "H"}], "resSeq": 327, "segmentID": ""}, {"index": 327, "name": "HOH", "atoms": [{"index": 994, "name": "O", "element": "O"}, {"index": 995, "name": "H1", "element": "H"}, {"index": 996, "name": "H2", "element": "H"}], "resSeq": 328, "segmentID": ""}, {"index": 328, "name": "HOH", "atoms": [{"index": 997, "name": "O", "element": "O"}, {"index": 998, "name": "H1", "element": "H"}, {"index": 999, "name": "H2", "element": "H"}], "resSeq": 329, "segmentID": ""}, {"index": 329, "name": "HOH", "atoms": [{"index": 1000, "name": "O", "element": "O"}, {"index": 1001, "name": "H1", "element": "H"}, {"index": 1002, "name": "H2", "element": "H"}], "resSeq": 330, "segmentID": ""}, {"index": 330, "name": "HOH", "atoms": [{"index": 1003, "name": "O", "element": "O"}, {"index": 1004, "name": "H1", "element": "H"}, {"index": 1005, "name": "H2", "element": "H"}], "resSeq": 331, "segmentID": ""}, {"index": 331, "name": "HOH", "atoms": [{"index": 1006, "name": "O", "element": "O"}, {"index": 1007, "name": "H1", "element": "H"}, {"index": 1008, "name": "H2", "element": "H"}], "resSeq": 332, "segmentID": ""}, {"index": 332, "name": "HOH", "atoms": [{"index": 1009, "name": "O", "element": "O"}, {"index": 1010, "name": "H1", "element": "H"}, {"index": 1011, "name": "H2", "element": "H"}], "resSeq": 333, "segmentID": ""}, {"index": 333, "name": "HOH", "atoms": [{"index": 1012, "name": "O", "element": "O"}, {"index": 1013, "name": "H1", "element": "H"}, {"index": 1014, "name": "H2", "element": "H"}], "resSeq": 334, "segmentID": ""}, {"index": 334, "name": "HOH", "atoms": [{"index": 1015, "name": "O", "element": "O"}, {"index": 1016, "name": "H1", "element": "H"}, {"index": 1017, "name": "H2", "element": "H"}], "resSeq": 335, "segmentID": ""}, {"index": 335, "name": "HOH", "atoms": [{"index": 1018, "name": "O", "element": "O"}, {"index": 1019, "name": "H1", "element": "H"}, {"index": 1020, "name": "H2", "element": "H"}], "resSeq": 336, "segmentID": ""}, {"index": 336, "name": "HOH", "atoms": [{"index": 1021, "name": "O", "element": "O"}, {"index": 1022, "name": "H1", "element": "H"}, {"index": 1023, "name": "H2", "element": "H"}], "resSeq": 337, "segmentID": ""}, {"index": 337, "name": "HOH", "atoms": [{"index": 1024, "name": "O", "element": "O"}, {"index": 1025, "name": "H1", "element": "H"}, {"index": 1026, "name": "H2", "element": "H"}], "resSeq": 338, "segmentID": ""}, {"index": 338, "name": "HOH", "atoms": [{"index": 1027, "name": "O", "element": "O"}, {"index": 1028, "name": "H1", "element": "H"}, {"index": 1029, "name": "H2", "element": "H"}], "resSeq": 339, "segmentID": ""}, {"index": 339, "name": "HOH", "atoms": [{"index": 1030, "name": "O", "element": "O"}, {"index": 1031, "name": "H1", "element": "H"}, {"index": 1032, "name": "H2", "element": "H"}], "resSeq": 340, "segmentID": ""}, {"index": 340, "name": "HOH", "atoms": [{"index": 1033, "name": "O", "element": "O"}, {"index": 1034, "name": "H1", "element": "H"}, {"index": 1035, "name": "H2", "element": "H"}], "resSeq": 341, "segmentID": ""}, {"index": 341, "name": "HOH", "atoms": [{"index": 1036, "name": "O", "element": "O"}, {"index": 1037, "name": "H1", "element": "H"}, {"index": 1038, "name": "H2", "element": "H"}], "resSeq": 342, "segmentID": ""}, {"index": 342, "name": "HOH", "atoms": [{"index": 1039, "name": "O", "element": "O"}, {"index": 1040, "name": "H1", "element": "H"}, {"index": 1041, "name": "H2", "element": "H"}], "resSeq": 343, "segmentID": ""}, {"index": 343, "name": "HOH", "atoms": [{"index": 1042, "name": "O", "element": "O"}, {"index": 1043, "name": "H1", "element": "H"}, {"index": 1044, "name": "H2", "element": "H"}], "resSeq": 344, "segmentID": ""}, {"index": 344, "name": "HOH", "atoms": [{"index": 1045, "name": "O", "element": "O"}, {"index": 1046, "name": "H1", "element": "H"}, {"index": 1047, "name": "H2", "element": "H"}], "resSeq": 345, "segmentID": ""}, {"index": 345, "name": "HOH", "atoms": [{"index": 1048, "name": "O", "element": "O"}, {"index": 1049, "name": "H1", "element": "H"}, {"index": 1050, "name": "H2", "element": "H"}], "resSeq": 346, "segmentID": ""}, {"index": 346, "name": "HOH", "atoms": [{"index": 1051, "name": "O", "element": "O"}, {"index": 1052, "name": "H1", "element": "H"}, {"index": 1053, "name": "H2", "element": "H"}], "resSeq": 347, "segmentID": ""}, {"index": 347, "name": "HOH", "atoms": [{"index": 1054, "name": "O", "element": "O"}, {"index": 1055, "name": "H1", "element": "H"}, {"index": 1056, "name": "H2", "element": "H"}], "resSeq": 348, "segmentID": ""}, {"index": 348, "name": "HOH", "atoms": [{"index": 1057, "name": "O", "element": "O"}, {"index": 1058, "name": "H1", "element": "H"}, {"index": 1059, "name": "H2", "element": "H"}], "resSeq": 349, "segmentID": ""}, {"index": 349, "name": "HOH", "atoms": [{"index": 1060, "name": "O", "element": "O"}, {"index": 1061, "name": "H1", "element": "H"}, {"index": 1062, "name": "H2", "element": "H"}], "resSeq": 350, "segmentID": ""}, {"index": 350, "name": "HOH", "atoms": [{"index": 1063, "name": "O", "element": "O"}, {"index": 1064, "name": "H1", "element": "H"}, {"index": 1065, "name": "H2", "element": "H"}], "resSeq": 351, "segmentID": ""}, {"index": 351, "name": "HOH", "atoms": [{"index": 1066, "name": "O", "element": "O"}, {"index": 1067, "name": "H1", "element": "H"}, {"index": 1068, "name": "H2", "element": "H"}], "resSeq": 352, "segmentID": ""}, {"index": 352, "name": "HOH", "atoms": [{"index": 1069, "name": "O", "element": "O"}, {"index": 1070, "name": "H1", "element": "H"}, {"index": 1071, "name": "H2", "element": "H"}], "resSeq": 353, "segmentID": ""}, {"index": 353, "name": "HOH", "atoms": [{"index": 1072, "name": "O", "element": "O"}, {"index": 1073, "name": "H1", "element": "H"}, {"index": 1074, "name": "H2", "element": "H"}], "resSeq": 354, "segmentID": ""}, {"index": 354, "name": "HOH", "atoms": [{"index": 1075, "name": "O", "element": "O"}, {"index": 1076, "name": "H1", "element": "H"}, {"index": 1077, "name": "H2", "element": "H"}], "resSeq": 355, "segmentID": ""}, {"index": 355, "name": "HOH", "atoms": [{"index": 1078, "name": "O", "element": "O"}, {"index": 1079, "name": "H1", "element": "H"}, {"index": 1080, "name": "H2", "element": "H"}], "resSeq": 356, "segmentID": ""}, {"index": 356, "name": "HOH", "atoms": [{"index": 1081, "name": "O", "element": "O"}, {"index": 1082, "name": "H1", "element": "H"}, {"index": 1083, "name": "H2", "element": "H"}], "resSeq": 357, "segmentID": ""}, {"index": 357, "name": "HOH", "atoms": [{"index": 1084, "name": "O", "element": "O"}, {"index": 1085, "name": "H1", "element": "H"}, {"index": 1086, "name": "H2", "element": "H"}], "resSeq": 358, "segmentID": ""}, {"index": 358, "name": "HOH", "atoms": [{"index": 1087, "name": "O", "element": "O"}, {"index": 1088, "name": "H1", "element": "H"}, {"index": 1089, "name": "H2", "element": "H"}], "resSeq": 359, "segmentID": ""}, {"index": 359, "name": "HOH", "atoms": [{"index": 1090, "name": "O", "element": "O"}, {"index": 1091, "name": "H1", "element": "H"}, {"index": 1092, "name": "H2", "element": "H"}], "resSeq": 360, "segmentID": ""}, {"index": 360, "name": "HOH", "atoms": [{"index": 1093, "name": "O", "element": "O"}, {"index": 1094, "name": "H1", "element": "H"}, {"index": 1095, "name": "H2", "element": "H"}], "resSeq": 361, "segmentID": ""}, {"index": 361, "name": "HOH", "atoms": [{"index": 1096, "name": "O", "element": "O"}, {"index": 1097, "name": "H1", "element": "H"}, {"index": 1098, "name": "H2", "element": "H"}], "resSeq": 362, "segmentID": ""}, {"index": 362, "name": "HOH", "atoms": [{"index": 1099, "name": "O", "element": "O"}, {"index": 1100, "name": "H1", "element": "H"}, {"index": 1101, "name": "H2", "element": "H"}], "resSeq": 363, "segmentID": ""}, {"index": 363, "name": "HOH", "atoms": [{"index": 1102, "name": "O", "element": "O"}, {"index": 1103, "name": "H1", "element": "H"}, {"index": 1104, "name": "H2", "element": "H"}], "resSeq": 364, "segmentID": ""}, {"index": 364, "name": "HOH", "atoms": [{"index": 1105, "name": "O", "element": "O"}, {"index": 1106, "name": "H1", "element": "H"}, {"index": 1107, "name": "H2", "element": "H"}], "resSeq": 365, "segmentID": ""}, {"index": 365, "name": "HOH", "atoms": [{"index": 1108, "name": "O", "element": "O"}, {"index": 1109, "name": "H1", "element": "H"}, {"index": 1110, "name": "H2", "element": "H"}], "resSeq": 366, "segmentID": ""}, {"index": 366, "name": "HOH", "atoms": [{"index": 1111, "name": "O", "element": "O"}, {"index": 1112, "name": "H1", "element": "H"}, {"index": 1113, "name": "H2", "element": "H"}], "resSeq": 367, "segmentID": ""}, {"index": 367, "name": "HOH", "atoms": [{"index": 1114, "name": "O", "element": "O"}, {"index": 1115, "name": "H1", "element": "H"}, {"index": 1116, "name": "H2", "element": "H"}], "resSeq": 368, "segmentID": ""}, {"index": 368, "name": "HOH", "atoms": [{"index": 1117, "name": "O", "element": "O"}, {"index": 1118, "name": "H1", "element": "H"}, {"index": 1119, "name": "H2", "element": "H"}], "resSeq": 369, "segmentID": ""}, {"index": 369, "name": "HOH", "atoms": [{"index": 1120, "name": "O", "element": "O"}, {"index": 1121, "name": "H1", "element": "H"}, {"index": 1122, "name": "H2", "element": "H"}], "resSeq": 370, "segmentID": ""}, {"index": 370, "name": "HOH", "atoms": [{"index": 1123, "name": "O", "element": "O"}, {"index": 1124, "name": "H1", "element": "H"}, {"index": 1125, "name": "H2", "element": "H"}], "resSeq": 371, "segmentID": ""}, {"index": 371, "name": "HOH", "atoms": [{"index": 1126, "name": "O", "element": "O"}, {"index": 1127, "name": "H1", "element": "H"}, {"index": 1128, "name": "H2", "element": "H"}], "resSeq": 372, "segmentID": ""}, {"index": 372, "name": "HOH", "atoms": [{"index": 1129, "name": "O", "element": "O"}, {"index": 1130, "name": "H1", "element": "H"}, {"index": 1131, "name": "H2", "element": "H"}], "resSeq": 373, "segmentID": ""}, {"index": 373, "name": "HOH", "atoms": [{"index": 1132, "name": "O", "element": "O"}, {"index": 1133, "name": "H1", "element": "H"}, {"index": 1134, "name": "H2", "element": "H"}], "resSeq": 374, "segmentID": ""}, {"index": 374, "name": "HOH", "atoms": [{"index": 1135, "name": "O", "element": "O"}, {"index": 1136, "name": "H1", "element": "H"}, {"index": 1137, "name": "H2", "element": "H"}], "resSeq": 375, "segmentID": ""}, {"index": 375, "name": "HOH", "atoms": [{"index": 1138, "name": "O", "element": "O"}, {"index": 1139, "name": "H1", "element": "H"}, {"index": 1140, "name": "H2", "element": "H"}], "resSeq": 376, "segmentID": ""}, {"index": 376, "name": "HOH", "atoms": [{"index": 1141, "name": "O", "element": "O"}, {"index": 1142, "name": "H1", "element": "H"}, {"index": 1143, "name": "H2", "element": "H"}], "resSeq": 377, "segmentID": ""}, {"index": 377, "name": "HOH", "atoms": [{"index": 1144, "name": "O", "element": "O"}, {"index": 1145, "name": "H1", "element": "H"}, {"index": 1146, "name": "H2", "element": "H"}], "resSeq": 378, "segmentID": ""}, {"index": 378, "name": "HOH", "atoms": [{"index": 1147, "name": "O", "element": "O"}, {"index": 1148, "name": "H1", "element": "H"}, {"index": 1149, "name": "H2", "element": "H"}], "resSeq": 379, "segmentID": ""}, {"index": 379, "name": "HOH", "atoms": [{"index": 1150, "name": "O", "element": "O"}, {"index": 1151, "name": "H1", "element": "H"}, {"index": 1152, "name": "H2", "element": "H"}], "resSeq": 380, "segmentID": ""}, {"index": 380, "name": "HOH", "atoms": [{"index": 1153, "name": "O", "element": "O"}, {"index": 1154, "name": "H1", "element": "H"}, {"index": 1155, "name": "H2", "element": "H"}], "resSeq": 381, "segmentID": ""}, {"index": 381, "name": "HOH", "atoms": [{"index": 1156, "name": "O", "element": "O"}, {"index": 1157, "name": "H1", "element": "H"}, {"index": 1158, "name": "H2", "element": "H"}], "resSeq": 382, "segmentID": ""}, {"index": 382, "name": "HOH", "atoms": [{"index": 1159, "name": "O", "element": "O"}, {"index": 1160, "name": "H1", "element": "H"}, {"index": 1161, "name": "H2", "element": "H"}], "resSeq": 383, "segmentID": ""}, {"index": 383, "name": "HOH", "atoms": [{"index": 1162, "name": "O", "element": "O"}, {"index": 1163, "name": "H1", "element": "H"}, {"index": 1164, "name": "H2", "element": "H"}], "resSeq": 384, "segmentID": ""}, {"index": 384, "name": "HOH", "atoms": [{"index": 1165, "name": "O", "element": "O"}, {"index": 1166, "name": "H1", "element": "H"}, {"index": 1167, "name": "H2", "element": "H"}], "resSeq": 385, "segmentID": ""}, {"index": 385, "name": "HOH", "atoms": [{"index": 1168, "name": "O", "element": "O"}, {"index": 1169, "name": "H1", "element": "H"}, {"index": 1170, "name": "H2", "element": "H"}], "resSeq": 386, "segmentID": ""}, {"index": 386, "name": "HOH", "atoms": [{"index": 1171, "name": "O", "element": "O"}, {"index": 1172, "name": "H1", "element": "H"}, {"index": 1173, "name": "H2", "element": "H"}], "resSeq": 387, "segmentID": ""}, {"index": 387, "name": "HOH", "atoms": [{"index": 1174, "name": "O", "element": "O"}, {"index": 1175, "name": "H1", "element": "H"}, {"index": 1176, "name": "H2", "element": "H"}], "resSeq": 388, "segmentID": ""}, {"index": 388, "name": "HOH", "atoms": [{"index": 1177, "name": "O", "element": "O"}, {"index": 1178, "name": "H1", "element": "H"}, {"index": 1179, "name": "H2", "element": "H"}], "resSeq": 389, "segmentID": ""}, {"index": 389, "name": "HOH", "atoms": [{"index": 1180, "name": "O", "element": "O"}, {"index": 1181, "name": "H1", "element": "H"}, {"index": 1182, "name": "H2", "element": "H"}], "resSeq": 390, "segmentID": ""}, {"index": 390, "name": "HOH", "atoms": [{"index": 1183, "name": "O", "element": "O"}, {"index": 1184, "name": "H1", "element": "H"}, {"index": 1185, "name": "H2", "element": "H"}], "resSeq": 391, "segmentID": ""}, {"index": 391, "name": "HOH", "atoms": [{"index": 1186, "name": "O", "element": "O"}, {"index": 1187, "name": "H1", "element": "H"}, {"index": 1188, "name": "H2", "element": "H"}], "resSeq": 392, "segmentID": ""}, {"index": 392, "name": "HOH", "atoms": [{"index": 1189, "name": "O", "element": "O"}, {"index": 1190, "name": "H1", "element": "H"}, {"index": 1191, "name": "H2", "element": "H"}], "resSeq": 393, "segmentID": ""}, {"index": 393, "name": "HOH", "atoms": [{"index": 1192, "name": "O", "element": "O"}, {"index": 1193, "name": "H1", "element": "H"}, {"index": 1194, "name": "H2", "element": "H"}], "resSeq": 394, "segmentID": ""}, {"index": 394, "name": "HOH", "atoms": [{"index": 1195, "name": "O", "element": "O"}, {"index": 1196, "name": "H1", "element": "H"}, {"index": 1197, "name": "H2", "element": "H"}], "resSeq": 395, "segmentID": ""}, {"index": 395, "name": "HOH", "atoms": [{"index": 1198, "name": "O", "element": "O"}, {"index": 1199, "name": "H1", "element": "H"}, {"index": 1200, "name": "H2", "element": "H"}], "resSeq": 396, "segmentID": ""}, {"index": 396, "name": "HOH", "atoms": [{"index": 1201, "name": "O", "element": "O"}, {"index": 1202, "name": "H1", "element": "H"}, {"index": 1203, "name": "H2", "element": "H"}], "resSeq": 397, "segmentID": ""}, {"index": 397, "name": "HOH", "atoms": [{"index": 1204, "name": "O", "element": "O"}, {"index": 1205, "name": "H1", "element": "H"}, {"index": 1206, "name": "H2", "element": "H"}], "resSeq": 398, "segmentID": ""}, {"index": 398, "name": "HOH", "atoms": [{"index": 1207, "name": "O", "element": "O"}, {"index": 1208, "name": "H1", "element": "H"}, {"index": 1209, "name": "H2", "element": "H"}], "resSeq": 399, "segmentID": ""}, {"index": 399, "name": "HOH", "atoms": [{"index": 1210, "name": "O", "element": "O"}, {"index": 1211, "name": "H1", "element": "H"}, {"index": 1212, "name": "H2", "element": "H"}], "resSeq": 400, "segmentID": ""}, {"index": 400, "name": "HOH", "atoms": [{"index": 1213, "name": "O", "element": "O"}, {"index": 1214, "name": "H1", "element": "H"}, {"index": 1215, "name": "H2", "element": "H"}], "resSeq": 401, "segmentID": ""}, {"index": 401, "name": "HOH", "atoms": [{"index": 1216, "name": "O", "element": "O"}, {"index": 1217, "name": "H1", "element": "H"}, {"index": 1218, "name": "H2", "element": "H"}], "resSeq": 402, "segmentID": ""}, {"index": 402, "name": "HOH", "atoms": [{"index": 1219, "name": "O", "element": "O"}, {"index": 1220, "name": "H1", "element": "H"}, {"index": 1221, "name": "H2", "element": "H"}], "resSeq": 403, "segmentID": ""}, {"index": 403, "name": "HOH", "atoms": [{"index": 1222, "name": "O", "element": "O"}, {"index": 1223, "name": "H1", "element": "H"}, {"index": 1224, "name": "H2", "element": "H"}], "resSeq": 404, "segmentID": ""}, {"index": 404, "name": "HOH", "atoms": [{"index": 1225, "name": "O", "element": "O"}, {"index": 1226, "name": "H1", "element": "H"}, {"index": 1227, "name": "H2", "element": "H"}], "resSeq": 405, "segmentID": ""}, {"index": 405, "name": "HOH", "atoms": [{"index": 1228, "name": "O", "element": "O"}, {"index": 1229, "name": "H1", "element": "H"}, {"index": 1230, "name": "H2", "element": "H"}], "resSeq": 406, "segmentID": ""}, {"index": 406, "name": "HOH", "atoms": [{"index": 1231, "name": "O", "element": "O"}, {"index": 1232, "name": "H1", "element": "H"}, {"index": 1233, "name": "H2", "element": "H"}], "resSeq": 407, "segmentID": ""}, {"index": 407, "name": "HOH", "atoms": [{"index": 1234, "name": "O", "element": "O"}, {"index": 1235, "name": "H1", "element": "H"}, {"index": 1236, "name": "H2", "element": "H"}], "resSeq": 408, "segmentID": ""}, {"index": 408, "name": "HOH", "atoms": [{"index": 1237, "name": "O", "element": "O"}, {"index": 1238, "name": "H1", "element": "H"}, {"index": 1239, "name": "H2", "element": "H"}], "resSeq": 409, "segmentID": ""}, {"index": 409, "name": "HOH", "atoms": [{"index": 1240, "name": "O", "element": "O"}, {"index": 1241, "name": "H1", "element": "H"}, {"index": 1242, "name": "H2", "element": "H"}], "resSeq": 410, "segmentID": ""}, {"index": 410, "name": "HOH", "atoms": [{"index": 1243, "name": "O", "element": "O"}, {"index": 1244, "name": "H1", "element": "H"}, {"index": 1245, "name": "H2", "element": "H"}], "resSeq": 411, "segmentID": ""}, {"index": 411, "name": "HOH", "atoms": [{"index": 1246, "name": "O", "element": "O"}, {"index": 1247, "name": "H1", "element": "H"}, {"index": 1248, "name": "H2", "element": "H"}], "resSeq": 412, "segmentID": ""}, {"index": 412, "name": "HOH", "atoms": [{"index": 1249, "name": "O", "element": "O"}, {"index": 1250, "name": "H1", "element": "H"}, {"index": 1251, "name": "H2", "element": "H"}], "resSeq": 413, "segmentID": ""}, {"index": 413, "name": "HOH", "atoms": [{"index": 1252, "name": "O", "element": "O"}, {"index": 1253, "name": "H1", "element": "H"}, {"index": 1254, "name": "H2", "element": "H"}], "resSeq": 414, "segmentID": ""}, {"index": 414, "name": "HOH", "atoms": [{"index": 1255, "name": "O", "element": "O"}, {"index": 1256, "name": "H1", "element": "H"}, {"index": 1257, "name": "H2", "element": "H"}], "resSeq": 415, "segmentID": ""}, {"index": 415, "name": "HOH", "atoms": [{"index": 1258, "name": "O", "element": "O"}, {"index": 1259, "name": "H1", "element": "H"}, {"index": 1260, "name": "H2", "element": "H"}], "resSeq": 416, "segmentID": ""}, {"index": 416, "name": "HOH", "atoms": [{"index": 1261, "name": "O", "element": "O"}, {"index": 1262, "name": "H1", "element": "H"}, {"index": 1263, "name": "H2", "element": "H"}], "resSeq": 417, "segmentID": ""}, {"index": 417, "name": "HOH", "atoms": [{"index": 1264, "name": "O", "element": "O"}, {"index": 1265, "name": "H1", "element": "H"}, {"index": 1266, "name": "H2", "element": "H"}], "resSeq": 418, "segmentID": ""}, {"index": 418, "name": "HOH", "atoms": [{"index": 1267, "name": "O", "element": "O"}, {"index": 1268, "name": "H1", "element": "H"}, {"index": 1269, "name": "H2", "element": "H"}], "resSeq": 419, "segmentID": ""}, {"index": 419, "name": "HOH", "atoms": [{"index": 1270, "name": "O", "element": "O"}, {"index": 1271, "name": "H1", "element": "H"}, {"index": 1272, "name": "H2", "element": "H"}], "resSeq": 420, "segmentID": ""}, {"index": 420, "name": "HOH", "atoms": [{"index": 1273, "name": "O", "element": "O"}, {"index": 1274, "name": "H1", "element": "H"}, {"index": 1275, "name": "H2", "element": "H"}], "resSeq": 421, "segmentID": ""}, {"index": 421, "name": "HOH", "atoms": [{"index": 1276, "name": "O", "element": "O"}, {"index": 1277, "name": "H1", "element": "H"}, {"index": 1278, "name": "H2", "element": "H"}], "resSeq": 422, "segmentID": ""}, {"index": 422, "name": "HOH", "atoms": [{"index": 1279, "name": "O", "element": "O"}, {"index": 1280, "name": "H1", "element": "H"}, {"index": 1281, "name": "H2", "element": "H"}], "resSeq": 423, "segmentID": ""}, {"index": 423, "name": "HOH", "atoms": [{"index": 1282, "name": "O", "element": "O"}, {"index": 1283, "name": "H1", "element": "H"}, {"index": 1284, "name": "H2", "element": "H"}], "resSeq": 424, "segmentID": ""}, {"index": 424, "name": "HOH", "atoms": [{"index": 1285, "name": "O", "element": "O"}, {"index": 1286, "name": "H1", "element": "H"}, {"index": 1287, "name": "H2", "element": "H"}], "resSeq": 425, "segmentID": ""}, {"index": 425, "name": "HOH", "atoms": [{"index": 1288, "name": "O", "element": "O"}, {"index": 1289, "name": "H1", "element": "H"}, {"index": 1290, "name": "H2", "element": "H"}], "resSeq": 426, "segmentID": ""}, {"index": 426, "name": "HOH", "atoms": [{"index": 1291, "name": "O", "element": "O"}, {"index": 1292, "name": "H1", "element": "H"}, {"index": 1293, "name": "H2", "element": "H"}], "resSeq": 427, "segmentID": ""}, {"index": 427, "name": "HOH", "atoms": [{"index": 1294, "name": "O", "element": "O"}, {"index": 1295, "name": "H1", "element": "H"}, {"index": 1296, "name": "H2", "element": "H"}], "resSeq": 428, "segmentID": ""}, {"index": 428, "name": "HOH", "atoms": [{"index": 1297, "name": "O", "element": "O"}, {"index": 1298, "name": "H1", "element": "H"}, {"index": 1299, "name": "H2", "element": "H"}], "resSeq": 429, "segmentID": ""}, {"index": 429, "name": "HOH", "atoms": [{"index": 1300, "name": "O", "element": "O"}, {"index": 1301, "name": "H1", "element": "H"}, {"index": 1302, "name": "H2", "element": "H"}], "resSeq": 430, "segmentID": ""}, {"index": 430, "name": "HOH", "atoms": [{"index": 1303, "name": "O", "element": "O"}, {"index": 1304, "name": "H1", "element": "H"}, {"index": 1305, "name": "H2", "element": "H"}], "resSeq": 431, "segmentID": ""}, {"index": 431, "name": "HOH", "atoms": [{"index": 1306, "name": "O", "element": "O"}, {"index": 1307, "name": "H1", "element": "H"}, {"index": 1308, "name": "H2", "element": "H"}], "resSeq": 432, "segmentID": ""}, {"index": 432, "name": "HOH", "atoms": [{"index": 1309, "name": "O", "element": "O"}, {"index": 1310, "name": "H1", "element": "H"}, {"index": 1311, "name": "H2", "element": "H"}], "resSeq": 433, "segmentID": ""}, {"index": 433, "name": "HOH", "atoms": [{"index": 1312, "name": "O", "element": "O"}, {"index": 1313, "name": "H1", "element": "H"}, {"index": 1314, "name": "H2", "element": "H"}], "resSeq": 434, "segmentID": ""}, {"index": 434, "name": "HOH", "atoms": [{"index": 1315, "name": "O", "element": "O"}, {"index": 1316, "name": "H1", "element": "H"}, {"index": 1317, "name": "H2", "element": "H"}], "resSeq": 435, "segmentID": ""}, {"index": 435, "name": "HOH", "atoms": [{"index": 1318, "name": "O", "element": "O"}, {"index": 1319, "name": "H1", "element": "H"}, {"index": 1320, "name": "H2", "element": "H"}], "resSeq": 436, "segmentID": ""}, {"index": 436, "name": "HOH", "atoms": [{"index": 1321, "name": "O", "element": "O"}, {"index": 1322, "name": "H1", "element": "H"}, {"index": 1323, "name": "H2", "element": "H"}], "resSeq": 437, "segmentID": ""}, {"index": 437, "name": "HOH", "atoms": [{"index": 1324, "name": "O", "element": "O"}, {"index": 1325, "name": "H1", "element": "H"}, {"index": 1326, "name": "H2", "element": "H"}], "resSeq": 438, "segmentID": ""}, {"index": 438, "name": "HOH", "atoms": [{"index": 1327, "name": "O", "element": "O"}, {"index": 1328, "name": "H1", "element": "H"}, {"index": 1329, "name": "H2", "element": "H"}], "resSeq": 439, "segmentID": ""}, {"index": 439, "name": "HOH", "atoms": [{"index": 1330, "name": "O", "element": "O"}, {"index": 1331, "name": "H1", "element": "H"}, {"index": 1332, "name": "H2", "element": "H"}], "resSeq": 440, "segmentID": ""}, {"index": 440, "name": "HOH", "atoms": [{"index": 1333, "name": "O", "element": "O"}, {"index": 1334, "name": "H1", "element": "H"}, {"index": 1335, "name": "H2", "element": "H"}], "resSeq": 441, "segmentID": ""}, {"index": 441, "name": "HOH", "atoms": [{"index": 1336, "name": "O", "element": "O"}, {"index": 1337, "name": "H1", "element": "H"}, {"index": 1338, "name": "H2", "element": "H"}], "resSeq": 442, "segmentID": ""}, {"index": 442, "name": "HOH", "atoms": [{"index": 1339, "name": "O", "element": "O"}, {"index": 1340, "name": "H1", "element": "H"}, {"index": 1341, "name": "H2", "element": "H"}], "resSeq": 443, "segmentID": ""}, {"index": 443, "name": "HOH", "atoms": [{"index": 1342, "name": "O", "element": "O"}, {"index": 1343, "name": "H1", "element": "H"}, {"index": 1344, "name": "H2", "element": "H"}], "resSeq": 444, "segmentID": ""}, {"index": 444, "name": "HOH", "atoms": [{"index": 1345, "name": "O", "element": "O"}, {"index": 1346, "name": "H1", "element": "H"}, {"index": 1347, "name": "H2", "element": "H"}], "resSeq": 445, "segmentID": ""}, {"index": 445, "name": "HOH", "atoms": [{"index": 1348, "name": "O", "element": "O"}, {"index": 1349, "name": "H1", "element": "H"}, {"index": 1350, "name": "H2", "element": "H"}], "resSeq": 446, "segmentID": ""}, {"index": 446, "name": "HOH", "atoms": [{"index": 1351, "name": "O", "element": "O"}, {"index": 1352, "name": "H1", "element": "H"}, {"index": 1353, "name": "H2", "element": "H"}], "resSeq": 447, "segmentID": ""}, {"index": 447, "name": "HOH", "atoms": [{"index": 1354, "name": "O", "element": "O"}, {"index": 1355, "name": "H1", "element": "H"}, {"index": 1356, "name": "H2", "element": "H"}], "resSeq": 448, "segmentID": ""}, {"index": 448, "name": "HOH", "atoms": [{"index": 1357, "name": "O", "element": "O"}, {"index": 1358, "name": "H1", "element": "H"}, {"index": 1359, "name": "H2", "element": "H"}], "resSeq": 449, "segmentID": ""}, {"index": 449, "name": "HOH", "atoms": [{"index": 1360, "name": "O", "element": "O"}, {"index": 1361, "name": "H1", "element": "H"}, {"index": 1362, "name": "H2", "element": "H"}], "resSeq": 450, "segmentID": ""}, {"index": 450, "name": "HOH", "atoms": [{"index": 1363, "name": "O", "element": "O"}, {"index": 1364, "name": "H1", "element": "H"}, {"index": 1365, "name": "H2", "element": "H"}], "resSeq": 451, "segmentID": ""}, {"index": 451, "name": "HOH", "atoms": [{"index": 1366, "name": "O", "element": "O"}, {"index": 1367, "name": "H1", "element": "H"}, {"index": 1368, "name": "H2", "element": "H"}], "resSeq": 452, "segmentID": ""}, {"index": 452, "name": "HOH", "atoms": [{"index": 1369, "name": "O", "element": "O"}, {"index": 1370, "name": "H1", "element": "H"}, {"index": 1371, "name": "H2", "element": "H"}], "resSeq": 453, "segmentID": ""}, {"index": 453, "name": "HOH", "atoms": [{"index": 1372, "name": "O", "element": "O"}, {"index": 1373, "name": "H1", "element": "H"}, {"index": 1374, "name": "H2", "element": "H"}], "resSeq": 454, "segmentID": ""}, {"index": 454, "name": "HOH", "atoms": [{"index": 1375, "name": "O", "element": "O"}, {"index": 1376, "name": "H1", "element": "H"}, {"index": 1377, "name": "H2", "element": "H"}], "resSeq": 455, "segmentID": ""}, {"index": 455, "name": "HOH", "atoms": [{"index": 1378, "name": "O", "element": "O"}, {"index": 1379, "name": "H1", "element": "H"}, {"index": 1380, "name": "H2", "element": "H"}], "resSeq": 456, "segmentID": ""}, {"index": 456, "name": "HOH", "atoms": [{"index": 1381, "name": "O", "element": "O"}, {"index": 1382, "name": "H1", "element": "H"}, {"index": 1383, "name": "H2", "element": "H"}], "resSeq": 457, "segmentID": ""}, {"index": 457, "name": "HOH", "atoms": [{"index": 1384, "name": "O", "element": "O"}, {"index": 1385, "name": "H1", "element": "H"}, {"index": 1386, "name": "H2", "element": "H"}], "resSeq": 458, "segmentID": ""}, {"index": 458, "name": "HOH", "atoms": [{"index": 1387, "name": "O", "element": "O"}, {"index": 1388, "name": "H1", "element": "H"}, {"index": 1389, "name": "H2", "element": "H"}], "resSeq": 459, "segmentID": ""}, {"index": 459, "name": "HOH", "atoms": [{"index": 1390, "name": "O", "element": "O"}, {"index": 1391, "name": "H1", "element": "H"}, {"index": 1392, "name": "H2", "element": "H"}], "resSeq": 460, "segmentID": ""}, {"index": 460, "name": "HOH", "atoms": [{"index": 1393, "name": "O", "element": "O"}, {"index": 1394, "name": "H1", "element": "H"}, {"index": 1395, "name": "H2", "element": "H"}], "resSeq": 461, "segmentID": ""}, {"index": 461, "name": "HOH", "atoms": [{"index": 1396, "name": "O", "element": "O"}, {"index": 1397, "name": "H1", "element": "H"}, {"index": 1398, "name": "H2", "element": "H"}], "resSeq": 462, "segmentID": ""}, {"index": 462, "name": "HOH", "atoms": [{"index": 1399, "name": "O", "element": "O"}, {"index": 1400, "name": "H1", "element": "H"}, {"index": 1401, "name": "H2", "element": "H"}], "resSeq": 463, "segmentID": ""}, {"index": 463, "name": "HOH", "atoms": [{"index": 1402, "name": "O", "element": "O"}, {"index": 1403, "name": "H1", "element": "H"}, {"index": 1404, "name": "H2", "element": "H"}], "resSeq": 464, "segmentID": ""}, {"index": 464, "name": "HOH", "atoms": [{"index": 1405, "name": "O", "element": "O"}, {"index": 1406, "name": "H1", "element": "H"}, {"index": 1407, "name": "H2", "element": "H"}], "resSeq": 465, "segmentID": ""}, {"index": 465, "name": "HOH", "atoms": [{"index": 1408, "name": "O", "element": "O"}, {"index": 1409, "name": "H1", "element": "H"}, {"index": 1410, "name": "H2", "element": "H"}], "resSeq": 466, "segmentID": ""}, {"index": 466, "name": "HOH", "atoms": [{"index": 1411, "name": "O", "element": "O"}, {"index": 1412, "name": "H1", "element": "H"}, {"index": 1413, "name": "H2", "element": "H"}], "resSeq": 467, "segmentID": ""}, {"index": 467, "name": "HOH", "atoms": [{"index": 1414, "name": "O", "element": "O"}, {"index": 1415, "name": "H1", "element": "H"}, {"index": 1416, "name": "H2", "element": "H"}], "resSeq": 468, "segmentID": ""}, {"index": 468, "name": "HOH", "atoms": [{"index": 1417, "name": "O", "element": "O"}, {"index": 1418, "name": "H1", "element": "H"}, {"index": 1419, "name": "H2", "element": "H"}], "resSeq": 469, "segmentID": ""}, {"index": 469, "name": "HOH", "atoms": [{"index": 1420, "name": "O", "element": "O"}, {"index": 1421, "name": "H1", "element": "H"}, {"index": 1422, "name": "H2", "element": "H"}], "resSeq": 470, "segmentID": ""}, {"index": 470, "name": "HOH", "atoms": [{"index": 1423, "name": "O", "element": "O"}, {"index": 1424, "name": "H1", "element": "H"}, {"index": 1425, "name": "H2", "element": "H"}], "resSeq": 471, "segmentID": ""}, {"index": 471, "name": "HOH", "atoms": [{"index": 1426, "name": "O", "element": "O"}, {"index": 1427, "name": "H1", "element": "H"}, {"index": 1428, "name": "H2", "element": "H"}], "resSeq": 472, "segmentID": ""}, {"index": 472, "name": "HOH", "atoms": [{"index": 1429, "name": "O", "element": "O"}, {"index": 1430, "name": "H1", "element": "H"}, {"index": 1431, "name": "H2", "element": "H"}], "resSeq": 473, "segmentID": ""}, {"index": 473, "name": "HOH", "atoms": [{"index": 1432, "name": "O", "element": "O"}, {"index": 1433, "name": "H1", "element": "H"}, {"index": 1434, "name": "H2", "element": "H"}], "resSeq": 474, "segmentID": ""}, {"index": 474, "name": "HOH", "atoms": [{"index": 1435, "name": "O", "element": "O"}, {"index": 1436, "name": "H1", "element": "H"}, {"index": 1437, "name": "H2", "element": "H"}], "resSeq": 475, "segmentID": ""}, {"index": 475, "name": "HOH", "atoms": [{"index": 1438, "name": "O", "element": "O"}, {"index": 1439, "name": "H1", "element": "H"}, {"index": 1440, "name": "H2", "element": "H"}], "resSeq": 476, "segmentID": ""}, {"index": 476, "name": "HOH", "atoms": [{"index": 1441, "name": "O", "element": "O"}, {"index": 1442, "name": "H1", "element": "H"}, {"index": 1443, "name": "H2", "element": "H"}], "resSeq": 477, "segmentID": ""}, {"index": 477, "name": "HOH", "atoms": [{"index": 1444, "name": "O", "element": "O"}, {"index": 1445, "name": "H1", "element": "H"}, {"index": 1446, "name": "H2", "element": "H"}], "resSeq": 478, "segmentID": ""}, {"index": 478, "name": "HOH", "atoms": [{"index": 1447, "name": "O", "element": "O"}, {"index": 1448, "name": "H1", "element": "H"}, {"index": 1449, "name": "H2", "element": "H"}], "resSeq": 479, "segmentID": ""}, {"index": 479, "name": "HOH", "atoms": [{"index": 1450, "name": "O", "element": "O"}, {"index": 1451, "name": "H1", "element": "H"}, {"index": 1452, "name": "H2", "element": "H"}], "resSeq": 480, "segmentID": ""}, {"index": 480, "name": "HOH", "atoms": [{"index": 1453, "name": "O", "element": "O"}, {"index": 1454, "name": "H1", "element": "H"}, {"index": 1455, "name": "H2", "element": "H"}], "resSeq": 481, "segmentID": ""}, {"index": 481, "name": "HOH", "atoms": [{"index": 1456, "name": "O", "element": "O"}, {"index": 1457, "name": "H1", "element": "H"}, {"index": 1458, "name": "H2", "element": "H"}], "resSeq": 482, "segmentID": ""}, {"index": 482, "name": "HOH", "atoms": [{"index": 1459, "name": "O", "element": "O"}, {"index": 1460, "name": "H1", "element": "H"}, {"index": 1461, "name": "H2", "element": "H"}], "resSeq": 483, "segmentID": ""}, {"index": 483, "name": "HOH", "atoms": [{"index": 1462, "name": "O", "element": "O"}, {"index": 1463, "name": "H1", "element": "H"}, {"index": 1464, "name": "H2", "element": "H"}], "resSeq": 484, "segmentID": ""}, {"index": 484, "name": "HOH", "atoms": [{"index": 1465, "name": "O", "element": "O"}, {"index": 1466, "name": "H1", "element": "H"}, {"index": 1467, "name": "H2", "element": "H"}], "resSeq": 485, "segmentID": ""}, {"index": 485, "name": "HOH", "atoms": [{"index": 1468, "name": "O", "element": "O"}, {"index": 1469, "name": "H1", "element": "H"}, {"index": 1470, "name": "H2", "element": "H"}], "resSeq": 486, "segmentID": ""}, {"index": 486, "name": "HOH", "atoms": [{"index": 1471, "name": "O", "element": "O"}, {"index": 1472, "name": "H1", "element": "H"}, {"index": 1473, "name": "H2", "element": "H"}], "resSeq": 487, "segmentID": ""}, {"index": 487, "name": "HOH", "atoms": [{"index": 1474, "name": "O", "element": "O"}, {"index": 1475, "name": "H1", "element": "H"}, {"index": 1476, "name": "H2", "element": "H"}], "resSeq": 488, "segmentID": ""}, {"index": 488, "name": "HOH", "atoms": [{"index": 1477, "name": "O", "element": "O"}, {"index": 1478, "name": "H1", "element": "H"}, {"index": 1479, "name": "H2", "element": "H"}], "resSeq": 489, "segmentID": ""}, {"index": 489, "name": "HOH", "atoms": [{"index": 1480, "name": "O", "element": "O"}, {"index": 1481, "name": "H1", "element": "H"}, {"index": 1482, "name": "H2", "element": "H"}], "resSeq": 490, "segmentID": ""}, {"index": 490, "name": "HOH", "atoms": [{"index": 1483, "name": "O", "element": "O"}, {"index": 1484, "name": "H1", "element": "H"}, {"index": 1485, "name": "H2", "element": "H"}], "resSeq": 491, "segmentID": ""}, {"index": 491, "name": "HOH", "atoms": [{"index": 1486, "name": "O", "element": "O"}, {"index": 1487, "name": "H1", "element": "H"}, {"index": 1488, "name": "H2", "element": "H"}], "resSeq": 492, "segmentID": ""}, {"index": 492, "name": "HOH", "atoms": [{"index": 1489, "name": "O", "element": "O"}, {"index": 1490, "name": "H1", "element": "H"}, {"index": 1491, "name": "H2", "element": "H"}], "resSeq": 493, "segmentID": ""}, {"index": 493, "name": "HOH", "atoms": [{"index": 1492, "name": "O", "element": "O"}, {"index": 1493, "name": "H1", "element": "H"}, {"index": 1494, "name": "H2", "element": "H"}], "resSeq": 494, "segmentID": ""}, {"index": 494, "name": "HOH", "atoms": [{"index": 1495, "name": "O", "element": "O"}, {"index": 1496, "name": "H1", "element": "H"}, {"index": 1497, "name": "H2", "element": "H"}], "resSeq": 495, "segmentID": ""}, {"index": 495, "name": "HOH", "atoms": [{"index": 1498, "name": "O", "element": "O"}, {"index": 1499, "name": "H1", "element": "H"}, {"index": 1500, "name": "H2", "element": "H"}], "resSeq": 496, "segmentID": ""}, {"index": 496, "name": "HOH", "atoms": [{"index": 1501, "name": "O", "element": "O"}, {"index": 1502, "name": "H1", "element": "H"}, {"index": 1503, "name": "H2", "element": "H"}], "resSeq": 497, "segmentID": ""}, {"index": 497, "name": "HOH", "atoms": [{"index": 1504, "name": "O", "element": "O"}, {"index": 1505, "name": "H1", "element": "H"}, {"index": 1506, "name": "H2", "element": "H"}], "resSeq": 498, "segmentID": ""}, {"index": 498, "name": "HOH", "atoms": [{"index": 1507, "name": "O", "element": "O"}, {"index": 1508, "name": "H1", "element": "H"}, {"index": 1509, "name": "H2", "element": "H"}], "resSeq": 499, "segmentID": ""}, {"index": 499, "name": "HOH", "atoms": [{"index": 1510, "name": "O", "element": "O"}, {"index": 1511, "name": "H1", "element": "H"}, {"index": 1512, "name": "H2", "element": "H"}], "resSeq": 500, "segmentID": ""}, {"index": 500, "name": "HOH", "atoms": [{"index": 1513, "name": "O", "element": "O"}, {"index": 1514, "name": "H1", "element": "H"}, {"index": 1515, "name": "H2", "element": "H"}], "resSeq": 501, "segmentID": ""}, {"index": 501, "name": "HOH", "atoms": [{"index": 1516, "name": "O", "element": "O"}, {"index": 1517, "name": "H1", "element": "H"}, {"index": 1518, "name": "H2", "element": "H"}], "resSeq": 502, "segmentID": ""}, {"index": 502, "name": "HOH", "atoms": [{"index": 1519, "name": "O", "element": "O"}, {"index": 1520, "name": "H1", "element": "H"}, {"index": 1521, "name": "H2", "element": "H"}], "resSeq": 503, "segmentID": ""}, {"index": 503, "name": "HOH", "atoms": [{"index": 1522, "name": "O", "element": "O"}, {"index": 1523, "name": "H1", "element": "H"}, {"index": 1524, "name": "H2", "element": "H"}], "resSeq": 504, "segmentID": ""}, {"index": 504, "name": "HOH", "atoms": [{"index": 1525, "name": "O", "element": "O"}, {"index": 1526, "name": "H1", "element": "H"}, {"index": 1527, "name": "H2", "element": "H"}], "resSeq": 505, "segmentID": ""}, {"index": 505, "name": "HOH", "atoms": [{"index": 1528, "name": "O", "element": "O"}, {"index": 1529, "name": "H1", "element": "H"}, {"index": 1530, "name": "H2", "element": "H"}], "resSeq": 506, "segmentID": ""}, {"index": 506, "name": "HOH", "atoms": [{"index": 1531, "name": "O", "element": "O"}, {"index": 1532, "name": "H1", "element": "H"}, {"index": 1533, "name": "H2", "element": "H"}], "resSeq": 507, "segmentID": ""}, {"index": 507, "name": "HOH", "atoms": [{"index": 1534, "name": "O", "element": "O"}, {"index": 1535, "name": "H1", "element": "H"}, {"index": 1536, "name": "H2", "element": "H"}], "resSeq": 508, "segmentID": ""}, {"index": 508, "name": "HOH", "atoms": [{"index": 1537, "name": "O", "element": "O"}, {"index": 1538, "name": "H1", "element": "H"}, {"index": 1539, "name": "H2", "element": "H"}], "resSeq": 509, "segmentID": ""}, {"index": 509, "name": "HOH", "atoms": [{"index": 1540, "name": "O", "element": "O"}, {"index": 1541, "name": "H1", "element": "H"}, {"index": 1542, "name": "H2", "element": "H"}], "resSeq": 510, "segmentID": ""}, {"index": 510, "name": "HOH", "atoms": [{"index": 1543, "name": "O", "element": "O"}, {"index": 1544, "name": "H1", "element": "H"}, {"index": 1545, "name": "H2", "element": "H"}], "resSeq": 511, "segmentID": ""}, {"index": 511, "name": "HOH", "atoms": [{"index": 1546, "name": "O", "element": "O"}, {"index": 1547, "name": "H1", "element": "H"}, {"index": 1548, "name": "H2", "element": "H"}], "resSeq": 512, "segmentID": ""}, {"index": 512, "name": "HOH", "atoms": [{"index": 1549, "name": "O", "element": "O"}, {"index": 1550, "name": "H1", "element": "H"}, {"index": 1551, "name": "H2", "element": "H"}], "resSeq": 513, "segmentID": ""}, {"index": 513, "name": "HOH", "atoms": [{"index": 1552, "name": "O", "element": "O"}, {"index": 1553, "name": "H1", "element": "H"}, {"index": 1554, "name": "H2", "element": "H"}], "resSeq": 514, "segmentID": ""}, {"index": 514, "name": "HOH", "atoms": [{"index": 1555, "name": "O", "element": "O"}, {"index": 1556, "name": "H1", "element": "H"}, {"index": 1557, "name": "H2", "element": "H"}], "resSeq": 515, "segmentID": ""}, {"index": 515, "name": "HOH", "atoms": [{"index": 1558, "name": "O", "element": "O"}, {"index": 1559, "name": "H1", "element": "H"}, {"index": 1560, "name": "H2", "element": "H"}], "resSeq": 516, "segmentID": ""}, {"index": 516, "name": "HOH", "atoms": [{"index": 1561, "name": "O", "element": "O"}, {"index": 1562, "name": "H1", "element": "H"}, {"index": 1563, "name": "H2", "element": "H"}], "resSeq": 517, "segmentID": ""}, {"index": 517, "name": "HOH", "atoms": [{"index": 1564, "name": "O", "element": "O"}, {"index": 1565, "name": "H1", "element": "H"}, {"index": 1566, "name": "H2", "element": "H"}], "resSeq": 518, "segmentID": ""}, {"index": 518, "name": "HOH", "atoms": [{"index": 1567, "name": "O", "element": "O"}, {"index": 1568, "name": "H1", "element": "H"}, {"index": 1569, "name": "H2", "element": "H"}], "resSeq": 519, "segmentID": ""}, {"index": 519, "name": "HOH", "atoms": [{"index": 1570, "name": "O", "element": "O"}, {"index": 1571, "name": "H1", "element": "H"}, {"index": 1572, "name": "H2", "element": "H"}], "resSeq": 520, "segmentID": ""}, {"index": 520, "name": "HOH", "atoms": [{"index": 1573, "name": "O", "element": "O"}, {"index": 1574, "name": "H1", "element": "H"}, {"index": 1575, "name": "H2", "element": "H"}], "resSeq": 521, "segmentID": ""}, {"index": 521, "name": "HOH", "atoms": [{"index": 1576, "name": "O", "element": "O"}, {"index": 1577, "name": "H1", "element": "H"}, {"index": 1578, "name": "H2", "element": "H"}], "resSeq": 522, "segmentID": ""}, {"index": 522, "name": "HOH", "atoms": [{"index": 1579, "name": "O", "element": "O"}, {"index": 1580, "name": "H1", "element": "H"}, {"index": 1581, "name": "H2", "element": "H"}], "resSeq": 523, "segmentID": ""}, {"index": 523, "name": "HOH", "atoms": [{"index": 1582, "name": "O", "element": "O"}, {"index": 1583, "name": "H1", "element": "H"}, {"index": 1584, "name": "H2", "element": "H"}], "resSeq": 524, "segmentID": ""}, {"index": 524, "name": "HOH", "atoms": [{"index": 1585, "name": "O", "element": "O"}, {"index": 1586, "name": "H1", "element": "H"}, {"index": 1587, "name": "H2", "element": "H"}], "resSeq": 525, "segmentID": ""}, {"index": 525, "name": "HOH", "atoms": [{"index": 1588, "name": "O", "element": "O"}, {"index": 1589, "name": "H1", "element": "H"}, {"index": 1590, "name": "H2", "element": "H"}], "resSeq": 526, "segmentID": ""}, {"index": 526, "name": "HOH", "atoms": [{"index": 1591, "name": "O", "element": "O"}, {"index": 1592, "name": "H1", "element": "H"}, {"index": 1593, "name": "H2", "element": "H"}], "resSeq": 527, "segmentID": ""}, {"index": 527, "name": "HOH", "atoms": [{"index": 1594, "name": "O", "element": "O"}, {"index": 1595, "name": "H1", "element": "H"}, {"index": 1596, "name": "H2", "element": "H"}], "resSeq": 528, "segmentID": ""}, {"index": 528, "name": "HOH", "atoms": [{"index": 1597, "name": "O", "element": "O"}, {"index": 1598, "name": "H1", "element": "H"}, {"index": 1599, "name": "H2", "element": "H"}], "resSeq": 529, "segmentID": ""}, {"index": 529, "name": "HOH", "atoms": [{"index": 1600, "name": "O", "element": "O"}, {"index": 1601, "name": "H1", "element": "H"}, {"index": 1602, "name": "H2", "element": "H"}], "resSeq": 530, "segmentID": ""}, {"index": 530, "name": "HOH", "atoms": [{"index": 1603, "name": "O", "element": "O"}, {"index": 1604, "name": "H1", "element": "H"}, {"index": 1605, "name": "H2", "element": "H"}], "resSeq": 531, "segmentID": ""}, {"index": 531, "name": "HOH", "atoms": [{"index": 1606, "name": "O", "element": "O"}, {"index": 1607, "name": "H1", "element": "H"}, {"index": 1608, "name": "H2", "element": "H"}], "resSeq": 532, "segmentID": ""}, {"index": 532, "name": "HOH", "atoms": [{"index": 1609, "name": "O", "element": "O"}, {"index": 1610, "name": "H1", "element": "H"}, {"index": 1611, "name": "H2", "element": "H"}], "resSeq": 533, "segmentID": ""}, {"index": 533, "name": "HOH", "atoms": [{"index": 1612, "name": "O", "element": "O"}, {"index": 1613, "name": "H1", "element": "H"}, {"index": 1614, "name": "H2", "element": "H"}], "resSeq": 534, "segmentID": ""}, {"index": 534, "name": "HOH", "atoms": [{"index": 1615, "name": "O", "element": "O"}, {"index": 1616, "name": "H1", "element": "H"}, {"index": 1617, "name": "H2", "element": "H"}], "resSeq": 535, "segmentID": ""}, {"index": 535, "name": "HOH", "atoms": [{"index": 1618, "name": "O", "element": "O"}, {"index": 1619, "name": "H1", "element": "H"}, {"index": 1620, "name": "H2", "element": "H"}], "resSeq": 536, "segmentID": ""}, {"index": 536, "name": "HOH", "atoms": [{"index": 1621, "name": "O", "element": "O"}, {"index": 1622, "name": "H1", "element": "H"}, {"index": 1623, "name": "H2", "element": "H"}], "resSeq": 537, "segmentID": ""}, {"index": 537, "name": "HOH", "atoms": [{"index": 1624, "name": "O", "element": "O"}, {"index": 1625, "name": "H1", "element": "H"}, {"index": 1626, "name": "H2", "element": "H"}], "resSeq": 538, "segmentID": ""}, {"index": 538, "name": "HOH", "atoms": [{"index": 1627, "name": "O", "element": "O"}, {"index": 1628, "name": "H1", "element": "H"}, {"index": 1629, "name": "H2", "element": "H"}], "resSeq": 539, "segmentID": ""}, {"index": 539, "name": "HOH", "atoms": [{"index": 1630, "name": "O", "element": "O"}, {"index": 1631, "name": "H1", "element": "H"}, {"index": 1632, "name": "H2", "element": "H"}], "resSeq": 540, "segmentID": ""}, {"index": 540, "name": "HOH", "atoms": [{"index": 1633, "name": "O", "element": "O"}, {"index": 1634, "name": "H1", "element": "H"}, {"index": 1635, "name": "H2", "element": "H"}], "resSeq": 541, "segmentID": ""}, {"index": 541, "name": "HOH", "atoms": [{"index": 1636, "name": "O", "element": "O"}, {"index": 1637, "name": "H1", "element": "H"}, {"index": 1638, "name": "H2", "element": "H"}], "resSeq": 542, "segmentID": ""}, {"index": 542, "name": "HOH", "atoms": [{"index": 1639, "name": "O", "element": "O"}, {"index": 1640, "name": "H1", "element": "H"}, {"index": 1641, "name": "H2", "element": "H"}], "resSeq": 543, "segmentID": ""}, {"index": 543, "name": "HOH", "atoms": [{"index": 1642, "name": "O", "element": "O"}, {"index": 1643, "name": "H1", "element": "H"}, {"index": 1644, "name": "H2", "element": "H"}], "resSeq": 544, "segmentID": ""}, {"index": 544, "name": "HOH", "atoms": [{"index": 1645, "name": "O", "element": "O"}, {"index": 1646, "name": "H1", "element": "H"}, {"index": 1647, "name": "H2", "element": "H"}], "resSeq": 545, "segmentID": ""}, {"index": 545, "name": "HOH", "atoms": [{"index": 1648, "name": "O", "element": "O"}, {"index": 1649, "name": "H1", "element": "H"}, {"index": 1650, "name": "H2", "element": "H"}], "resSeq": 546, "segmentID": ""}, {"index": 546, "name": "HOH", "atoms": [{"index": 1651, "name": "O", "element": "O"}, {"index": 1652, "name": "H1", "element": "H"}, {"index": 1653, "name": "H2", "element": "H"}], "resSeq": 547, "segmentID": ""}, {"index": 547, "name": "HOH", "atoms": [{"index": 1654, "name": "O", "element": "O"}, {"index": 1655, "name": "H1", "element": "H"}, {"index": 1656, "name": "H2", "element": "H"}], "resSeq": 548, "segmentID": ""}, {"index": 548, "name": "HOH", "atoms": [{"index": 1657, "name": "O", "element": "O"}, {"index": 1658, "name": "H1", "element": "H"}, {"index": 1659, "name": "H2", "element": "H"}], "resSeq": 549, "segmentID": ""}, {"index": 549, "name": "HOH", "atoms": [{"index": 1660, "name": "O", "element": "O"}, {"index": 1661, "name": "H1", "element": "H"}, {"index": 1662, "name": "H2", "element": "H"}], "resSeq": 550, "segmentID": ""}, {"index": 550, "name": "HOH", "atoms": [{"index": 1663, "name": "O", "element": "O"}, {"index": 1664, "name": "H1", "element": "H"}, {"index": 1665, "name": "H2", "element": "H"}], "resSeq": 551, "segmentID": ""}, {"index": 551, "name": "HOH", "atoms": [{"index": 1666, "name": "O", "element": "O"}, {"index": 1667, "name": "H1", "element": "H"}, {"index": 1668, "name": "H2", "element": "H"}], "resSeq": 552, "segmentID": ""}, {"index": 552, "name": "HOH", "atoms": [{"index": 1669, "name": "O", "element": "O"}, {"index": 1670, "name": "H1", "element": "H"}, {"index": 1671, "name": "H2", "element": "H"}], "resSeq": 553, "segmentID": ""}, {"index": 553, "name": "HOH", "atoms": [{"index": 1672, "name": "O", "element": "O"}, {"index": 1673, "name": "H1", "element": "H"}, {"index": 1674, "name": "H2", "element": "H"}], "resSeq": 554, "segmentID": ""}, {"index": 554, "name": "HOH", "atoms": [{"index": 1675, "name": "O", "element": "O"}, {"index": 1676, "name": "H1", "element": "H"}, {"index": 1677, "name": "H2", "element": "H"}], "resSeq": 555, "segmentID": ""}, {"index": 555, "name": "HOH", "atoms": [{"index": 1678, "name": "O", "element": "O"}, {"index": 1679, "name": "H1", "element": "H"}, {"index": 1680, "name": "H2", "element": "H"}], "resSeq": 556, "segmentID": ""}, {"index": 556, "name": "HOH", "atoms": [{"index": 1681, "name": "O", "element": "O"}, {"index": 1682, "name": "H1", "element": "H"}, {"index": 1683, "name": "H2", "element": "H"}], "resSeq": 557, "segmentID": ""}, {"index": 557, "name": "HOH", "atoms": [{"index": 1684, "name": "O", "element": "O"}, {"index": 1685, "name": "H1", "element": "H"}, {"index": 1686, "name": "H2", "element": "H"}], "resSeq": 558, "segmentID": ""}, {"index": 558, "name": "HOH", "atoms": [{"index": 1687, "name": "O", "element": "O"}, {"index": 1688, "name": "H1", "element": "H"}, {"index": 1689, "name": "H2", "element": "H"}], "resSeq": 559, "segmentID": ""}, {"index": 559, "name": "HOH", "atoms": [{"index": 1690, "name": "O", "element": "O"}, {"index": 1691, "name": "H1", "element": "H"}, {"index": 1692, "name": "H2", "element": "H"}], "resSeq": 560, "segmentID": ""}, {"index": 560, "name": "HOH", "atoms": [{"index": 1693, "name": "O", "element": "O"}, {"index": 1694, "name": "H1", "element": "H"}, {"index": 1695, "name": "H2", "element": "H"}], "resSeq": 561, "segmentID": ""}, {"index": 561, "name": "HOH", "atoms": [{"index": 1696, "name": "O", "element": "O"}, {"index": 1697, "name": "H1", "element": "H"}, {"index": 1698, "name": "H2", "element": "H"}], "resSeq": 562, "segmentID": ""}, {"index": 562, "name": "HOH", "atoms": [{"index": 1699, "name": "O", "element": "O"}, {"index": 1700, "name": "H1", "element": "H"}, {"index": 1701, "name": "H2", "element": "H"}], "resSeq": 563, "segmentID": ""}, {"index": 563, "name": "HOH", "atoms": [{"index": 1702, "name": "O", "element": "O"}, {"index": 1703, "name": "H1", "element": "H"}, {"index": 1704, "name": "H2", "element": "H"}], "resSeq": 564, "segmentID": ""}, {"index": 564, "name": "HOH", "atoms": [{"index": 1705, "name": "O", "element": "O"}, {"index": 1706, "name": "H1", "element": "H"}, {"index": 1707, "name": "H2", "element": "H"}], "resSeq": 565, "segmentID": ""}, {"index": 565, "name": "HOH", "atoms": [{"index": 1708, "name": "O", "element": "O"}, {"index": 1709, "name": "H1", "element": "H"}, {"index": 1710, "name": "H2", "element": "H"}], "resSeq": 566, "segmentID": ""}, {"index": 566, "name": "HOH", "atoms": [{"index": 1711, "name": "O", "element": "O"}, {"index": 1712, "name": "H1", "element": "H"}, {"index": 1713, "name": "H2", "element": "H"}], "resSeq": 567, "segmentID": ""}, {"index": 567, "name": "HOH", "atoms": [{"index": 1714, "name": "O", "element": "O"}, {"index": 1715, "name": "H1", "element": "H"}, {"index": 1716, "name": "H2", "element": "H"}], "resSeq": 568, "segmentID": ""}, {"index": 568, "name": "HOH", "atoms": [{"index": 1717, "name": "O", "element": "O"}, {"index": 1718, "name": "H1", "element": "H"}, {"index": 1719, "name": "H2", "element": "H"}], "resSeq": 569, "segmentID": ""}, {"index": 569, "name": "HOH", "atoms": [{"index": 1720, "name": "O", "element": "O"}, {"index": 1721, "name": "H1", "element": "H"}, {"index": 1722, "name": "H2", "element": "H"}], "resSeq": 570, "segmentID": ""}, {"index": 570, "name": "HOH", "atoms": [{"index": 1723, "name": "O", "element": "O"}, {"index": 1724, "name": "H1", "element": "H"}, {"index": 1725, "name": "H2", "element": "H"}], "resSeq": 571, "segmentID": ""}, {"index": 571, "name": "HOH", "atoms": [{"index": 1726, "name": "O", "element": "O"}, {"index": 1727, "name": "H1", "element": "H"}, {"index": 1728, "name": "H2", "element": "H"}], "resSeq": 572, "segmentID": ""}, {"index": 572, "name": "HOH", "atoms": [{"index": 1729, "name": "O", "element": "O"}, {"index": 1730, "name": "H1", "element": "H"}, {"index": 1731, "name": "H2", "element": "H"}], "resSeq": 573, "segmentID": ""}, {"index": 573, "name": "HOH", "atoms": [{"index": 1732, "name": "O", "element": "O"}, {"index": 1733, "name": "H1", "element": "H"}, {"index": 1734, "name": "H2", "element": "H"}], "resSeq": 574, "segmentID": ""}, {"index": 574, "name": "HOH", "atoms": [{"index": 1735, "name": "O", "element": "O"}, {"index": 1736, "name": "H1", "element": "H"}, {"index": 1737, "name": "H2", "element": "H"}], "resSeq": 575, "segmentID": ""}, {"index": 575, "name": "HOH", "atoms": [{"index": 1738, "name": "O", "element": "O"}, {"index": 1739, "name": "H1", "element": "H"}, {"index": 1740, "name": "H2", "element": "H"}], "resSeq": 576, "segmentID": ""}, {"index": 576, "name": "HOH", "atoms": [{"index": 1741, "name": "O", "element": "O"}, {"index": 1742, "name": "H1", "element": "H"}, {"index": 1743, "name": "H2", "element": "H"}], "resSeq": 577, "segmentID": ""}, {"index": 577, "name": "HOH", "atoms": [{"index": 1744, "name": "O", "element": "O"}, {"index": 1745, "name": "H1", "element": "H"}, {"index": 1746, "name": "H2", "element": "H"}], "resSeq": 578, "segmentID": ""}, {"index": 578, "name": "HOH", "atoms": [{"index": 1747, "name": "O", "element": "O"}, {"index": 1748, "name": "H1", "element": "H"}, {"index": 1749, "name": "H2", "element": "H"}], "resSeq": 579, "segmentID": ""}, {"index": 579, "name": "HOH", "atoms": [{"index": 1750, "name": "O", "element": "O"}, {"index": 1751, "name": "H1", "element": "H"}, {"index": 1752, "name": "H2", "element": "H"}], "resSeq": 580, "segmentID": ""}, {"index": 580, "name": "HOH", "atoms": [{"index": 1753, "name": "O", "element": "O"}, {"index": 1754, "name": "H1", "element": "H"}, {"index": 1755, "name": "H2", "element": "H"}], "resSeq": 581, "segmentID": ""}, {"index": 581, "name": "HOH", "atoms": [{"index": 1756, "name": "O", "element": "O"}, {"index": 1757, "name": "H1", "element": "H"}, {"index": 1758, "name": "H2", "element": "H"}], "resSeq": 582, "segmentID": ""}, {"index": 582, "name": "HOH", "atoms": [{"index": 1759, "name": "O", "element": "O"}, {"index": 1760, "name": "H1", "element": "H"}, {"index": 1761, "name": "H2", "element": "H"}], "resSeq": 583, "segmentID": ""}, {"index": 583, "name": "HOH", "atoms": [{"index": 1762, "name": "O", "element": "O"}, {"index": 1763, "name": "H1", "element": "H"}, {"index": 1764, "name": "H2", "element": "H"}], "resSeq": 584, "segmentID": ""}, {"index": 584, "name": "HOH", "atoms": [{"index": 1765, "name": "O", "element": "O"}, {"index": 1766, "name": "H1", "element": "H"}, {"index": 1767, "name": "H2", "element": "H"}], "resSeq": 585, "segmentID": ""}, {"index": 585, "name": "HOH", "atoms": [{"index": 1768, "name": "O", "element": "O"}, {"index": 1769, "name": "H1", "element": "H"}, {"index": 1770, "name": "H2", "element": "H"}], "resSeq": 586, "segmentID": ""}, {"index": 586, "name": "HOH", "atoms": [{"index": 1771, "name": "O", "element": "O"}, {"index": 1772, "name": "H1", "element": "H"}, {"index": 1773, "name": "H2", "element": "H"}], "resSeq": 587, "segmentID": ""}, {"index": 587, "name": "HOH", "atoms": [{"index": 1774, "name": "O", "element": "O"}, {"index": 1775, "name": "H1", "element": "H"}, {"index": 1776, "name": "H2", "element": "H"}], "resSeq": 588, "segmentID": ""}, {"index": 588, "name": "HOH", "atoms": [{"index": 1777, "name": "O", "element": "O"}, {"index": 1778, "name": "H1", "element": "H"}, {"index": 1779, "name": "H2", "element": "H"}], "resSeq": 589, "segmentID": ""}, {"index": 589, "name": "HOH", "atoms": [{"index": 1780, "name": "O", "element": "O"}, {"index": 1781, "name": "H1", "element": "H"}, {"index": 1782, "name": "H2", "element": "H"}], "resSeq": 590, "segmentID": ""}, {"index": 590, "name": "HOH", "atoms": [{"index": 1783, "name": "O", "element": "O"}, {"index": 1784, "name": "H1", "element": "H"}, {"index": 1785, "name": "H2", "element": "H"}], "resSeq": 591, "segmentID": ""}, {"index": 591, "name": "HOH", "atoms": [{"index": 1786, "name": "O", "element": "O"}, {"index": 1787, "name": "H1", "element": "H"}, {"index": 1788, "name": "H2", "element": "H"}], "resSeq": 592, "segmentID": ""}, {"index": 592, "name": "HOH", "atoms": [{"index": 1789, "name": "O", "element": "O"}, {"index": 1790, "name": "H1", "element": "H"}, {"index": 1791, "name": "H2", "element": "H"}], "resSeq": 593, "segmentID": ""}, {"index": 593, "name": "HOH", "atoms": [{"index": 1792, "name": "O", "element": "O"}, {"index": 1793, "name": "H1", "element": "H"}, {"index": 1794, "name": "H2", "element": "H"}], "resSeq": 594, "segmentID": ""}, {"index": 594, "name": "HOH", "atoms": [{"index": 1795, "name": "O", "element": "O"}, {"index": 1796, "name": "H1", "element": "H"}, {"index": 1797, "name": "H2", "element": "H"}], "resSeq": 595, "segmentID": ""}, {"index": 595, "name": "HOH", "atoms": [{"index": 1798, "name": "O", "element": "O"}, {"index": 1799, "name": "H1", "element": "H"}, {"index": 1800, "name": "H2", "element": "H"}], "resSeq": 596, "segmentID": ""}, {"index": 596, "name": "HOH", "atoms": [{"index": 1801, "name": "O", "element": "O"}, {"index": 1802, "name": "H1", "element": "H"}, {"index": 1803, "name": "H2", "element": "H"}], "resSeq": 597, "segmentID": ""}, {"index": 597, "name": "HOH", "atoms": [{"index": 1804, "name": "O", "element": "O"}, {"index": 1805, "name": "H1", "element": "H"}, {"index": 1806, "name": "H2", "element": "H"}], "resSeq": 598, "segmentID": ""}, {"index": 598, "name": "HOH", "atoms": [{"index": 1807, "name": "O", "element": "O"}, {"index": 1808, "name": "H1", "element": "H"}, {"index": 1809, "name": "H2", "element": "H"}], "resSeq": 599, "segmentID": ""}, {"index": 599, "name": "HOH", "atoms": [{"index": 1810, "name": "O", "element": "O"}, {"index": 1811, "name": "H1", "element": "H"}, {"index": 1812, "name": "H2", "element": "H"}], "resSeq": 600, "segmentID": ""}, {"index": 600, "name": "HOH", "atoms": [{"index": 1813, "name": "O", "element": "O"}, {"index": 1814, "name": "H1", "element": "H"}, {"index": 1815, "name": "H2", "element": "H"}], "resSeq": 601, "segmentID": ""}, {"index": 601, "name": "HOH", "atoms": [{"index": 1816, "name": "O", "element": "O"}, {"index": 1817, "name": "H1", "element": "H"}, {"index": 1818, "name": "H2", "element": "H"}], "resSeq": 602, "segmentID": ""}, {"index": 602, "name": "HOH", "atoms": [{"index": 1819, "name": "O", "element": "O"}, {"index": 1820, "name": "H1", "element": "H"}, {"index": 1821, "name": "H2", "element": "H"}], "resSeq": 603, "segmentID": ""}, {"index": 603, "name": "HOH", "atoms": [{"index": 1822, "name": "O", "element": "O"}, {"index": 1823, "name": "H1", "element": "H"}, {"index": 1824, "name": "H2", "element": "H"}], "resSeq": 604, "segmentID": ""}, {"index": 604, "name": "HOH", "atoms": [{"index": 1825, "name": "O", "element": "O"}, {"index": 1826, "name": "H1", "element": "H"}, {"index": 1827, "name": "H2", "element": "H"}], "resSeq": 605, "segmentID": ""}, {"index": 605, "name": "HOH", "atoms": [{"index": 1828, "name": "O", "element": "O"}, {"index": 1829, "name": "H1", "element": "H"}, {"index": 1830, "name": "H2", "element": "H"}], "resSeq": 606, "segmentID": ""}, {"index": 606, "name": "HOH", "atoms": [{"index": 1831, "name": "O", "element": "O"}, {"index": 1832, "name": "H1", "element": "H"}, {"index": 1833, "name": "H2", "element": "H"}], "resSeq": 607, "segmentID": ""}, {"index": 607, "name": "HOH", "atoms": [{"index": 1834, "name": "O", "element": "O"}, {"index": 1835, "name": "H1", "element": "H"}, {"index": 1836, "name": "H2", "element": "H"}], "resSeq": 608, "segmentID": ""}, {"index": 608, "name": "HOH", "atoms": [{"index": 1837, "name": "O", "element": "O"}, {"index": 1838, "name": "H1", "element": "H"}, {"index": 1839, "name": "H2", "element": "H"}], "resSeq": 609, "segmentID": ""}, {"index": 609, "name": "HOH", "atoms": [{"index": 1840, "name": "O", "element": "O"}, {"index": 1841, "name": "H1", "element": "H"}, {"index": 1842, "name": "H2", "element": "H"}], "resSeq": 610, "segmentID": ""}, {"index": 610, "name": "HOH", "atoms": [{"index": 1843, "name": "O", "element": "O"}, {"index": 1844, "name": "H1", "element": "H"}, {"index": 1845, "name": "H2", "element": "H"}], "resSeq": 611, "segmentID": ""}, {"index": 611, "name": "HOH", "atoms": [{"index": 1846, "name": "O", "element": "O"}, {"index": 1847, "name": "H1", "element": "H"}, {"index": 1848, "name": "H2", "element": "H"}], "resSeq": 612, "segmentID": ""}, {"index": 612, "name": "HOH", "atoms": [{"index": 1849, "name": "O", "element": "O"}, {"index": 1850, "name": "H1", "element": "H"}, {"index": 1851, "name": "H2", "element": "H"}], "resSeq": 613, "segmentID": ""}, {"index": 613, "name": "HOH", "atoms": [{"index": 1852, "name": "O", "element": "O"}, {"index": 1853, "name": "H1", "element": "H"}, {"index": 1854, "name": "H2", "element": "H"}], "resSeq": 614, "segmentID": ""}, {"index": 614, "name": "HOH", "atoms": [{"index": 1855, "name": "O", "element": "O"}, {"index": 1856, "name": "H1", "element": "H"}, {"index": 1857, "name": "H2", "element": "H"}], "resSeq": 615, "segmentID": ""}, {"index": 615, "name": "HOH", "atoms": [{"index": 1858, "name": "O", "element": "O"}, {"index": 1859, "name": "H1", "element": "H"}, {"index": 1860, "name": "H2", "element": "H"}], "resSeq": 616, "segmentID": ""}, {"index": 616, "name": "HOH", "atoms": [{"index": 1861, "name": "O", "element": "O"}, {"index": 1862, "name": "H1", "element": "H"}, {"index": 1863, "name": "H2", "element": "H"}], "resSeq": 617, "segmentID": ""}, {"index": 617, "name": "HOH", "atoms": [{"index": 1864, "name": "O", "element": "O"}, {"index": 1865, "name": "H1", "element": "H"}, {"index": 1866, "name": "H2", "element": "H"}], "resSeq": 618, "segmentID": ""}, {"index": 618, "name": "HOH", "atoms": [{"index": 1867, "name": "O", "element": "O"}, {"index": 1868, "name": "H1", "element": "H"}, {"index": 1869, "name": "H2", "element": "H"}], "resSeq": 619, "segmentID": ""}, {"index": 619, "name": "HOH", "atoms": [{"index": 1870, "name": "O", "element": "O"}, {"index": 1871, "name": "H1", "element": "H"}, {"index": 1872, "name": "H2", "element": "H"}], "resSeq": 620, "segmentID": ""}, {"index": 620, "name": "HOH", "atoms": [{"index": 1873, "name": "O", "element": "O"}, {"index": 1874, "name": "H1", "element": "H"}, {"index": 1875, "name": "H2", "element": "H"}], "resSeq": 621, "segmentID": ""}, {"index": 621, "name": "HOH", "atoms": [{"index": 1876, "name": "O", "element": "O"}, {"index": 1877, "name": "H1", "element": "H"}, {"index": 1878, "name": "H2", "element": "H"}], "resSeq": 622, "segmentID": ""}, {"index": 622, "name": "HOH", "atoms": [{"index": 1879, "name": "O", "element": "O"}, {"index": 1880, "name": "H1", "element": "H"}, {"index": 1881, "name": "H2", "element": "H"}], "resSeq": 623, "segmentID": ""}, {"index": 623, "name": "HOH", "atoms": [{"index": 1882, "name": "O", "element": "O"}, {"index": 1883, "name": "H1", "element": "H"}, {"index": 1884, "name": "H2", "element": "H"}], "resSeq": 624, "segmentID": ""}, {"index": 624, "name": "HOH", "atoms": [{"index": 1885, "name": "O", "element": "O"}, {"index": 1886, "name": "H1", "element": "H"}, {"index": 1887, "name": "H2", "element": "H"}], "resSeq": 625, "segmentID": ""}, {"index": 625, "name": "HOH", "atoms": [{"index": 1888, "name": "O", "element": "O"}, {"index": 1889, "name": "H1", "element": "H"}, {"index": 1890, "name": "H2", "element": "H"}], "resSeq": 626, "segmentID": ""}, {"index": 626, "name": "HOH", "atoms": [{"index": 1891, "name": "O", "element": "O"}, {"index": 1892, "name": "H1", "element": "H"}, {"index": 1893, "name": "H2", "element": "H"}], "resSeq": 627, "segmentID": ""}, {"index": 627, "name": "HOH", "atoms": [{"index": 1894, "name": "O", "element": "O"}, {"index": 1895, "name": "H1", "element": "H"}, {"index": 1896, "name": "H2", "element": "H"}], "resSeq": 628, "segmentID": ""}, {"index": 628, "name": "HOH", "atoms": [{"index": 1897, "name": "O", "element": "O"}, {"index": 1898, "name": "H1", "element": "H"}, {"index": 1899, "name": "H2", "element": "H"}], "resSeq": 629, "segmentID": ""}, {"index": 629, "name": "HOH", "atoms": [{"index": 1900, "name": "O", "element": "O"}, {"index": 1901, "name": "H1", "element": "H"}, {"index": 1902, "name": "H2", "element": "H"}], "resSeq": 630, "segmentID": ""}, {"index": 630, "name": "HOH", "atoms": [{"index": 1903, "name": "O", "element": "O"}, {"index": 1904, "name": "H1", "element": "H"}, {"index": 1905, "name": "H2", "element": "H"}], "resSeq": 631, "segmentID": ""}, {"index": 631, "name": "HOH", "atoms": [{"index": 1906, "name": "O", "element": "O"}, {"index": 1907, "name": "H1", "element": "H"}, {"index": 1908, "name": "H2", "element": "H"}], "resSeq": 632, "segmentID": ""}, {"index": 632, "name": "HOH", "atoms": [{"index": 1909, "name": "O", "element": "O"}, {"index": 1910, "name": "H1", "element": "H"}, {"index": 1911, "name": "H2", "element": "H"}], "resSeq": 633, "segmentID": ""}, {"index": 633, "name": "HOH", "atoms": [{"index": 1912, "name": "O", "element": "O"}, {"index": 1913, "name": "H1", "element": "H"}, {"index": 1914, "name": "H2", "element": "H"}], "resSeq": 634, "segmentID": ""}, {"index": 634, "name": "HOH", "atoms": [{"index": 1915, "name": "O", "element": "O"}, {"index": 1916, "name": "H1", "element": "H"}, {"index": 1917, "name": "H2", "element": "H"}], "resSeq": 635, "segmentID": ""}, {"index": 635, "name": "HOH", "atoms": [{"index": 1918, "name": "O", "element": "O"}, {"index": 1919, "name": "H1", "element": "H"}, {"index": 1920, "name": "H2", "element": "H"}], "resSeq": 636, "segmentID": ""}, {"index": 636, "name": "HOH", "atoms": [{"index": 1921, "name": "O", "element": "O"}, {"index": 1922, "name": "H1", "element": "H"}, {"index": 1923, "name": "H2", "element": "H"}], "resSeq": 637, "segmentID": ""}, {"index": 637, "name": "HOH", "atoms": [{"index": 1924, "name": "O", "element": "O"}, {"index": 1925, "name": "H1", "element": "H"}, {"index": 1926, "name": "H2", "element": "H"}], "resSeq": 638, "segmentID": ""}, {"index": 638, "name": "HOH", "atoms": [{"index": 1927, "name": "O", "element": "O"}, {"index": 1928, "name": "H1", "element": "H"}, {"index": 1929, "name": "H2", "element": "H"}], "resSeq": 639, "segmentID": ""}, {"index": 639, "name": "HOH", "atoms": [{"index": 1930, "name": "O", "element": "O"}, {"index": 1931, "name": "H1", "element": "H"}, {"index": 1932, "name": "H2", "element": "H"}], "resSeq": 640, "segmentID": ""}, {"index": 640, "name": "HOH", "atoms": [{"index": 1933, "name": "O", "element": "O"}, {"index": 1934, "name": "H1", "element": "H"}, {"index": 1935, "name": "H2", "element": "H"}], "resSeq": 641, "segmentID": ""}, {"index": 641, "name": "HOH", "atoms": [{"index": 1936, "name": "O", "element": "O"}, {"index": 1937, "name": "H1", "element": "H"}, {"index": 1938, "name": "H2", "element": "H"}], "resSeq": 642, "segmentID": ""}, {"index": 642, "name": "HOH", "atoms": [{"index": 1939, "name": "O", "element": "O"}, {"index": 1940, "name": "H1", "element": "H"}, {"index": 1941, "name": "H2", "element": "H"}], "resSeq": 643, "segmentID": ""}, {"index": 643, "name": "HOH", "atoms": [{"index": 1942, "name": "O", "element": "O"}, {"index": 1943, "name": "H1", "element": "H"}, {"index": 1944, "name": "H2", "element": "H"}], "resSeq": 644, "segmentID": ""}, {"index": 644, "name": "HOH", "atoms": [{"index": 1945, "name": "O", "element": "O"}, {"index": 1946, "name": "H1", "element": "H"}, {"index": 1947, "name": "H2", "element": "H"}], "resSeq": 645, "segmentID": ""}, {"index": 645, "name": "HOH", "atoms": [{"index": 1948, "name": "O", "element": "O"}, {"index": 1949, "name": "H1", "element": "H"}, {"index": 1950, "name": "H2", "element": "H"}], "resSeq": 646, "segmentID": ""}, {"index": 646, "name": "HOH", "atoms": [{"index": 1951, "name": "O", "element": "O"}, {"index": 1952, "name": "H1", "element": "H"}, {"index": 1953, "name": "H2", "element": "H"}], "resSeq": 647, "segmentID": ""}, {"index": 647, "name": "HOH", "atoms": [{"index": 1954, "name": "O", "element": "O"}, {"index": 1955, "name": "H1", "element": "H"}, {"index": 1956, "name": "H2", "element": "H"}], "resSeq": 648, "segmentID": ""}, {"index": 648, "name": "HOH", "atoms": [{"index": 1957, "name": "O", "element": "O"}, {"index": 1958, "name": "H1", "element": "H"}, {"index": 1959, "name": "H2", "element": "H"}], "resSeq": 649, "segmentID": ""}, {"index": 649, "name": "HOH", "atoms": [{"index": 1960, "name": "O", "element": "O"}, {"index": 1961, "name": "H1", "element": "H"}, {"index": 1962, "name": "H2", "element": "H"}], "resSeq": 650, "segmentID": ""}, {"index": 650, "name": "HOH", "atoms": [{"index": 1963, "name": "O", "element": "O"}, {"index": 1964, "name": "H1", "element": "H"}, {"index": 1965, "name": "H2", "element": "H"}], "resSeq": 651, "segmentID": ""}, {"index": 651, "name": "HOH", "atoms": [{"index": 1966, "name": "O", "element": "O"}, {"index": 1967, "name": "H1", "element": "H"}, {"index": 1968, "name": "H2", "element": "H"}], "resSeq": 652, "segmentID": ""}, {"index": 652, "name": "HOH", "atoms": [{"index": 1969, "name": "O", "element": "O"}, {"index": 1970, "name": "H1", "element": "H"}, {"index": 1971, "name": "H2", "element": "H"}], "resSeq": 653, "segmentID": ""}, {"index": 653, "name": "HOH", "atoms": [{"index": 1972, "name": "O", "element": "O"}, {"index": 1973, "name": "H1", "element": "H"}, {"index": 1974, "name": "H2", "element": "H"}], "resSeq": 654, "segmentID": ""}, {"index": 654, "name": "HOH", "atoms": [{"index": 1975, "name": "O", "element": "O"}, {"index": 1976, "name": "H1", "element": "H"}, {"index": 1977, "name": "H2", "element": "H"}], "resSeq": 655, "segmentID": ""}, {"index": 655, "name": "HOH", "atoms": [{"index": 1978, "name": "O", "element": "O"}, {"index": 1979, "name": "H1", "element": "H"}, {"index": 1980, "name": "H2", "element": "H"}], "resSeq": 656, "segmentID": ""}, {"index": 656, "name": "HOH", "atoms": [{"index": 1981, "name": "O", "element": "O"}, {"index": 1982, "name": "H1", "element": "H"}, {"index": 1983, "name": "H2", "element": "H"}], "resSeq": 657, "segmentID": ""}, {"index": 657, "name": "HOH", "atoms": [{"index": 1984, "name": "O", "element": "O"}, {"index": 1985, "name": "H1", "element": "H"}, {"index": 1986, "name": "H2", "element": "H"}], "resSeq": 658, "segmentID": ""}, {"index": 658, "name": "HOH", "atoms": [{"index": 1987, "name": "O", "element": "O"}, {"index": 1988, "name": "H1", "element": "H"}, {"index": 1989, "name": "H2", "element": "H"}], "resSeq": 659, "segmentID": ""}, {"index": 659, "name": "HOH", "atoms": [{"index": 1990, "name": "O", "element": "O"}, {"index": 1991, "name": "H1", "element": "H"}, {"index": 1992, "name": "H2", "element": "H"}], "resSeq": 660, "segmentID": ""}, {"index": 660, "name": "HOH", "atoms": [{"index": 1993, "name": "O", "element": "O"}, {"index": 1994, "name": "H1", "element": "H"}, {"index": 1995, "name": "H2", "element": "H"}], "resSeq": 661, "segmentID": ""}, {"index": 661, "name": "HOH", "atoms": [{"index": 1996, "name": "O", "element": "O"}, {"index": 1997, "name": "H1", "element": "H"}, {"index": 1998, "name": "H2", "element": "H"}], "resSeq": 662, "segmentID": ""}, {"index": 662, "name": "HOH", "atoms": [{"index": 1999, "name": "O", "element": "O"}, {"index": 2000, "name": "H1", "element": "H"}, {"index": 2001, "name": "H2", "element": "H"}], "resSeq": 663, "segmentID": ""}, {"index": 663, "name": "HOH", "atoms": [{"index": 2002, "name": "O", "element": "O"}, {"index": 2003, "name": "H1", "element": "H"}, {"index": 2004, "name": "H2", "element": "H"}], "resSeq": 664, "segmentID": ""}, {"index": 664, "name": "HOH", "atoms": [{"index": 2005, "name": "O", "element": "O"}, {"index": 2006, "name": "H1", "element": "H"}, {"index": 2007, "name": "H2", "element": "H"}], "resSeq": 665, "segmentID": ""}, {"index": 665, "name": "HOH", "atoms": [{"index": 2008, "name": "O", "element": "O"}, {"index": 2009, "name": "H1", "element": "H"}, {"index": 2010, "name": "H2", "element": "H"}], "resSeq": 666, "segmentID": ""}, {"index": 666, "name": "HOH", "atoms": [{"index": 2011, "name": "O", "element": "O"}, {"index": 2012, "name": "H1", "element": "H"}, {"index": 2013, "name": "H2", "element": "H"}], "resSeq": 667, "segmentID": ""}, {"index": 667, "name": "HOH", "atoms": [{"index": 2014, "name": "O", "element": "O"}, {"index": 2015, "name": "H1", "element": "H"}, {"index": 2016, "name": "H2", "element": "H"}], "resSeq": 668, "segmentID": ""}, {"index": 668, "name": "HOH", "atoms": [{"index": 2017, "name": "O", "element": "O"}, {"index": 2018, "name": "H1", "element": "H"}, {"index": 2019, "name": "H2", "element": "H"}], "resSeq": 669, "segmentID": ""}, {"index": 669, "name": "HOH", "atoms": [{"index": 2020, "name": "O", "element": "O"}, {"index": 2021, "name": "H1", "element": "H"}, {"index": 2022, "name": "H2", "element": "H"}], "resSeq": 670, "segmentID": ""}, {"index": 670, "name": "HOH", "atoms": [{"index": 2023, "name": "O", "element": "O"}, {"index": 2024, "name": "H1", "element": "H"}, {"index": 2025, "name": "H2", "element": "H"}], "resSeq": 671, "segmentID": ""}, {"index": 671, "name": "HOH", "atoms": [{"index": 2026, "name": "O", "element": "O"}, {"index": 2027, "name": "H1", "element": "H"}, {"index": 2028, "name": "H2", "element": "H"}], "resSeq": 672, "segmentID": ""}, {"index": 672, "name": "HOH", "atoms": [{"index": 2029, "name": "O", "element": "O"}, {"index": 2030, "name": "H1", "element": "H"}, {"index": 2031, "name": "H2", "element": "H"}], "resSeq": 673, "segmentID": ""}, {"index": 673, "name": "HOH", "atoms": [{"index": 2032, "name": "O", "element": "O"}, {"index": 2033, "name": "H1", "element": "H"}, {"index": 2034, "name": "H2", "element": "H"}], "resSeq": 674, "segmentID": ""}, {"index": 674, "name": "HOH", "atoms": [{"index": 2035, "name": "O", "element": "O"}, {"index": 2036, "name": "H1", "element": "H"}, {"index": 2037, "name": "H2", "element": "H"}], "resSeq": 675, "segmentID": ""}, {"index": 675, "name": "HOH", "atoms": [{"index": 2038, "name": "O", "element": "O"}, {"index": 2039, "name": "H1", "element": "H"}, {"index": 2040, "name": "H2", "element": "H"}], "resSeq": 676, "segmentID": ""}, {"index": 676, "name": "HOH", "atoms": [{"index": 2041, "name": "O", "element": "O"}, {"index": 2042, "name": "H1", "element": "H"}, {"index": 2043, "name": "H2", "element": "H"}], "resSeq": 677, "segmentID": ""}, {"index": 677, "name": "HOH", "atoms": [{"index": 2044, "name": "O", "element": "O"}, {"index": 2045, "name": "H1", "element": "H"}, {"index": 2046, "name": "H2", "element": "H"}], "resSeq": 678, "segmentID": ""}, {"index": 678, "name": "HOH", "atoms": [{"index": 2047, "name": "O", "element": "O"}, {"index": 2048, "name": "H1", "element": "H"}, {"index": 2049, "name": "H2", "element": "H"}], "resSeq": 679, "segmentID": ""}, {"index": 679, "name": "HOH", "atoms": [{"index": 2050, "name": "O", "element": "O"}, {"index": 2051, "name": "H1", "element": "H"}, {"index": 2052, "name": "H2", "element": "H"}], "resSeq": 680, "segmentID": ""}, {"index": 680, "name": "HOH", "atoms": [{"index": 2053, "name": "O", "element": "O"}, {"index": 2054, "name": "H1", "element": "H"}, {"index": 2055, "name": "H2", "element": "H"}], "resSeq": 681, "segmentID": ""}, {"index": 681, "name": "HOH", "atoms": [{"index": 2056, "name": "O", "element": "O"}, {"index": 2057, "name": "H1", "element": "H"}, {"index": 2058, "name": "H2", "element": "H"}], "resSeq": 682, "segmentID": ""}, {"index": 682, "name": "HOH", "atoms": [{"index": 2059, "name": "O", "element": "O"}, {"index": 2060, "name": "H1", "element": "H"}, {"index": 2061, "name": "H2", "element": "H"}], "resSeq": 683, "segmentID": ""}, {"index": 683, "name": "HOH", "atoms": [{"index": 2062, "name": "O", "element": "O"}, {"index": 2063, "name": "H1", "element": "H"}, {"index": 2064, "name": "H2", "element": "H"}], "resSeq": 684, "segmentID": ""}, {"index": 684, "name": "HOH", "atoms": [{"index": 2065, "name": "O", "element": "O"}, {"index": 2066, "name": "H1", "element": "H"}, {"index": 2067, "name": "H2", "element": "H"}], "resSeq": 685, "segmentID": ""}, {"index": 685, "name": "HOH", "atoms": [{"index": 2068, "name": "O", "element": "O"}, {"index": 2069, "name": "H1", "element": "H"}, {"index": 2070, "name": "H2", "element": "H"}], "resSeq": 686, "segmentID": ""}, {"index": 686, "name": "HOH", "atoms": [{"index": 2071, "name": "O", "element": "O"}, {"index": 2072, "name": "H1", "element": "H"}, {"index": 2073, "name": "H2", "element": "H"}], "resSeq": 687, "segmentID": ""}, {"index": 687, "name": "HOH", "atoms": [{"index": 2074, "name": "O", "element": "O"}, {"index": 2075, "name": "H1", "element": "H"}, {"index": 2076, "name": "H2", "element": "H"}], "resSeq": 688, "segmentID": ""}, {"index": 688, "name": "HOH", "atoms": [{"index": 2077, "name": "O", "element": "O"}, {"index": 2078, "name": "H1", "element": "H"}, {"index": 2079, "name": "H2", "element": "H"}], "resSeq": 689, "segmentID": ""}, {"index": 689, "name": "HOH", "atoms": [{"index": 2080, "name": "O", "element": "O"}, {"index": 2081, "name": "H1", "element": "H"}, {"index": 2082, "name": "H2", "element": "H"}], "resSeq": 690, "segmentID": ""}, {"index": 690, "name": "HOH", "atoms": [{"index": 2083, "name": "O", "element": "O"}, {"index": 2084, "name": "H1", "element": "H"}, {"index": 2085, "name": "H2", "element": "H"}], "resSeq": 691, "segmentID": ""}, {"index": 691, "name": "HOH", "atoms": [{"index": 2086, "name": "O", "element": "O"}, {"index": 2087, "name": "H1", "element": "H"}, {"index": 2088, "name": "H2", "element": "H"}], "resSeq": 692, "segmentID": ""}, {"index": 692, "name": "HOH", "atoms": [{"index": 2089, "name": "O", "element": "O"}, {"index": 2090, "name": "H1", "element": "H"}, {"index": 2091, "name": "H2", "element": "H"}], "resSeq": 693, "segmentID": ""}, {"index": 693, "name": "HOH", "atoms": [{"index": 2092, "name": "O", "element": "O"}, {"index": 2093, "name": "H1", "element": "H"}, {"index": 2094, "name": "H2", "element": "H"}], "resSeq": 694, "segmentID": ""}, {"index": 694, "name": "HOH", "atoms": [{"index": 2095, "name": "O", "element": "O"}, {"index": 2096, "name": "H1", "element": "H"}, {"index": 2097, "name": "H2", "element": "H"}], "resSeq": 695, "segmentID": ""}, {"index": 695, "name": "HOH", "atoms": [{"index": 2098, "name": "O", "element": "O"}, {"index": 2099, "name": "H1", "element": "H"}, {"index": 2100, "name": "H2", "element": "H"}], "resSeq": 696, "segmentID": ""}, {"index": 696, "name": "HOH", "atoms": [{"index": 2101, "name": "O", "element": "O"}, {"index": 2102, "name": "H1", "element": "H"}, {"index": 2103, "name": "H2", "element": "H"}], "resSeq": 697, "segmentID": ""}, {"index": 697, "name": "HOH", "atoms": [{"index": 2104, "name": "O", "element": "O"}, {"index": 2105, "name": "H1", "element": "H"}, {"index": 2106, "name": "H2", "element": "H"}], "resSeq": 698, "segmentID": ""}, {"index": 698, "name": "HOH", "atoms": [{"index": 2107, "name": "O", "element": "O"}, {"index": 2108, "name": "H1", "element": "H"}, {"index": 2109, "name": "H2", "element": "H"}], "resSeq": 699, "segmentID": ""}, {"index": 699, "name": "HOH", "atoms": [{"index": 2110, "name": "O", "element": "O"}, {"index": 2111, "name": "H1", "element": "H"}, {"index": 2112, "name": "H2", "element": "H"}], "resSeq": 700, "segmentID": ""}, {"index": 700, "name": "HOH", "atoms": [{"index": 2113, "name": "O", "element": "O"}, {"index": 2114, "name": "H1", "element": "H"}, {"index": 2115, "name": "H2", "element": "H"}], "resSeq": 701, "segmentID": ""}, {"index": 701, "name": "HOH", "atoms": [{"index": 2116, "name": "O", "element": "O"}, {"index": 2117, "name": "H1", "element": "H"}, {"index": 2118, "name": "H2", "element": "H"}], "resSeq": 702, "segmentID": ""}, {"index": 702, "name": "HOH", "atoms": [{"index": 2119, "name": "O", "element": "O"}, {"index": 2120, "name": "H1", "element": "H"}, {"index": 2121, "name": "H2", "element": "H"}], "resSeq": 703, "segmentID": ""}, {"index": 703, "name": "HOH", "atoms": [{"index": 2122, "name": "O", "element": "O"}, {"index": 2123, "name": "H1", "element": "H"}, {"index": 2124, "name": "H2", "element": "H"}], "resSeq": 704, "segmentID": ""}, {"index": 704, "name": "HOH", "atoms": [{"index": 2125, "name": "O", "element": "O"}, {"index": 2126, "name": "H1", "element": "H"}, {"index": 2127, "name": "H2", "element": "H"}], "resSeq": 705, "segmentID": ""}, {"index": 705, "name": "HOH", "atoms": [{"index": 2128, "name": "O", "element": "O"}, {"index": 2129, "name": "H1", "element": "H"}, {"index": 2130, "name": "H2", "element": "H"}], "resSeq": 706, "segmentID": ""}, {"index": 706, "name": "HOH", "atoms": [{"index": 2131, "name": "O", "element": "O"}, {"index": 2132, "name": "H1", "element": "H"}, {"index": 2133, "name": "H2", "element": "H"}], "resSeq": 707, "segmentID": ""}, {"index": 707, "name": "HOH", "atoms": [{"index": 2134, "name": "O", "element": "O"}, {"index": 2135, "name": "H1", "element": "H"}, {"index": 2136, "name": "H2", "element": "H"}], "resSeq": 708, "segmentID": ""}, {"index": 708, "name": "HOH", "atoms": [{"index": 2137, "name": "O", "element": "O"}, {"index": 2138, "name": "H1", "element": "H"}, {"index": 2139, "name": "H2", "element": "H"}], "resSeq": 709, "segmentID": ""}, {"index": 709, "name": "HOH", "atoms": [{"index": 2140, "name": "O", "element": "O"}, {"index": 2141, "name": "H1", "element": "H"}, {"index": 2142, "name": "H2", "element": "H"}], "resSeq": 710, "segmentID": ""}, {"index": 710, "name": "HOH", "atoms": [{"index": 2143, "name": "O", "element": "O"}, {"index": 2144, "name": "H1", "element": "H"}, {"index": 2145, "name": "H2", "element": "H"}], "resSeq": 711, "segmentID": ""}, {"index": 711, "name": "HOH", "atoms": [{"index": 2146, "name": "O", "element": "O"}, {"index": 2147, "name": "H1", "element": "H"}, {"index": 2148, "name": "H2", "element": "H"}], "resSeq": 712, "segmentID": ""}, {"index": 712, "name": "HOH", "atoms": [{"index": 2149, "name": "O", "element": "O"}, {"index": 2150, "name": "H1", "element": "H"}, {"index": 2151, "name": "H2", "element": "H"}], "resSeq": 713, "segmentID": ""}, {"index": 713, "name": "HOH", "atoms": [{"index": 2152, "name": "O", "element": "O"}, {"index": 2153, "name": "H1", "element": "H"}, {"index": 2154, "name": "H2", "element": "H"}], "resSeq": 714, "segmentID": ""}, {"index": 714, "name": "HOH", "atoms": [{"index": 2155, "name": "O", "element": "O"}, {"index": 2156, "name": "H1", "element": "H"}, {"index": 2157, "name": "H2", "element": "H"}], "resSeq": 715, "segmentID": ""}, {"index": 715, "name": "HOH", "atoms": [{"index": 2158, "name": "O", "element": "O"}, {"index": 2159, "name": "H1", "element": "H"}, {"index": 2160, "name": "H2", "element": "H"}], "resSeq": 716, "segmentID": ""}, {"index": 716, "name": "HOH", "atoms": [{"index": 2161, "name": "O", "element": "O"}, {"index": 2162, "name": "H1", "element": "H"}, {"index": 2163, "name": "H2", "element": "H"}], "resSeq": 717, "segmentID": ""}, {"index": 717, "name": "HOH", "atoms": [{"index": 2164, "name": "O", "element": "O"}, {"index": 2165, "name": "H1", "element": "H"}, {"index": 2166, "name": "H2", "element": "H"}], "resSeq": 718, "segmentID": ""}, {"index": 718, "name": "HOH", "atoms": [{"index": 2167, "name": "O", "element": "O"}, {"index": 2168, "name": "H1", "element": "H"}, {"index": 2169, "name": "H2", "element": "H"}], "resSeq": 719, "segmentID": ""}, {"index": 719, "name": "HOH", "atoms": [{"index": 2170, "name": "O", "element": "O"}, {"index": 2171, "name": "H1", "element": "H"}, {"index": 2172, "name": "H2", "element": "H"}], "resSeq": 720, "segmentID": ""}, {"index": 720, "name": "HOH", "atoms": [{"index": 2173, "name": "O", "element": "O"}, {"index": 2174, "name": "H1", "element": "H"}, {"index": 2175, "name": "H2", "element": "H"}], "resSeq": 721, "segmentID": ""}, {"index": 721, "name": "HOH", "atoms": [{"index": 2176, "name": "O", "element": "O"}, {"index": 2177, "name": "H1", "element": "H"}, {"index": 2178, "name": "H2", "element": "H"}], "resSeq": 722, "segmentID": ""}, {"index": 722, "name": "HOH", "atoms": [{"index": 2179, "name": "O", "element": "O"}, {"index": 2180, "name": "H1", "element": "H"}, {"index": 2181, "name": "H2", "element": "H"}], "resSeq": 723, "segmentID": ""}, {"index": 723, "name": "HOH", "atoms": [{"index": 2182, "name": "O", "element": "O"}, {"index": 2183, "name": "H1", "element": "H"}, {"index": 2184, "name": "H2", "element": "H"}], "resSeq": 724, "segmentID": ""}, {"index": 724, "name": "HOH", "atoms": [{"index": 2185, "name": "O", "element": "O"}, {"index": 2186, "name": "H1", "element": "H"}, {"index": 2187, "name": "H2", "element": "H"}], "resSeq": 725, "segmentID": ""}, {"index": 725, "name": "HOH", "atoms": [{"index": 2188, "name": "O", "element": "O"}, {"index": 2189, "name": "H1", "element": "H"}, {"index": 2190, "name": "H2", "element": "H"}], "resSeq": 726, "segmentID": ""}, {"index": 726, "name": "HOH", "atoms": [{"index": 2191, "name": "O", "element": "O"}, {"index": 2192, "name": "H1", "element": "H"}, {"index": 2193, "name": "H2", "element": "H"}], "resSeq": 727, "segmentID": ""}, {"index": 727, "name": "HOH", "atoms": [{"index": 2194, "name": "O", "element": "O"}, {"index": 2195, "name": "H1", "element": "H"}, {"index": 2196, "name": "H2", "element": "H"}], "resSeq": 728, "segmentID": ""}, {"index": 728, "name": "HOH", "atoms": [{"index": 2197, "name": "O", "element": "O"}, {"index": 2198, "name": "H1", "element": "H"}, {"index": 2199, "name": "H2", "element": "H"}], "resSeq": 729, "segmentID": ""}, {"index": 729, "name": "HOH", "atoms": [{"index": 2200, "name": "O", "element": "O"}, {"index": 2201, "name": "H1", "element": "H"}, {"index": 2202, "name": "H2", "element": "H"}], "resSeq": 730, "segmentID": ""}, {"index": 730, "name": "HOH", "atoms": [{"index": 2203, "name": "O", "element": "O"}, {"index": 2204, "name": "H1", "element": "H"}, {"index": 2205, "name": "H2", "element": "H"}], "resSeq": 731, "segmentID": ""}, {"index": 731, "name": "HOH", "atoms": [{"index": 2206, "name": "O", "element": "O"}, {"index": 2207, "name": "H1", "element": "H"}, {"index": 2208, "name": "H2", "element": "H"}], "resSeq": 732, "segmentID": ""}, {"index": 732, "name": "HOH", "atoms": [{"index": 2209, "name": "O", "element": "O"}, {"index": 2210, "name": "H1", "element": "H"}, {"index": 2211, "name": "H2", "element": "H"}], "resSeq": 733, "segmentID": ""}, {"index": 733, "name": "HOH", "atoms": [{"index": 2212, "name": "O", "element": "O"}, {"index": 2213, "name": "H1", "element": "H"}, {"index": 2214, "name": "H2", "element": "H"}], "resSeq": 734, "segmentID": ""}, {"index": 734, "name": "HOH", "atoms": [{"index": 2215, "name": "O", "element": "O"}, {"index": 2216, "name": "H1", "element": "H"}, {"index": 2217, "name": "H2", "element": "H"}], "resSeq": 735, "segmentID": ""}, {"index": 735, "name": "HOH", "atoms": [{"index": 2218, "name": "O", "element": "O"}, {"index": 2219, "name": "H1", "element": "H"}, {"index": 2220, "name": "H2", "element": "H"}], "resSeq": 736, "segmentID": ""}, {"index": 736, "name": "HOH", "atoms": [{"index": 2221, "name": "O", "element": "O"}, {"index": 2222, "name": "H1", "element": "H"}, {"index": 2223, "name": "H2", "element": "H"}], "resSeq": 737, "segmentID": ""}, {"index": 737, "name": "HOH", "atoms": [{"index": 2224, "name": "O", "element": "O"}, {"index": 2225, "name": "H1", "element": "H"}, {"index": 2226, "name": "H2", "element": "H"}], "resSeq": 738, "segmentID": ""}, {"index": 738, "name": "HOH", "atoms": [{"index": 2227, "name": "O", "element": "O"}, {"index": 2228, "name": "H1", "element": "H"}, {"index": 2229, "name": "H2", "element": "H"}], "resSeq": 739, "segmentID": ""}, {"index": 739, "name": "HOH", "atoms": [{"index": 2230, "name": "O", "element": "O"}, {"index": 2231, "name": "H1", "element": "H"}, {"index": 2232, "name": "H2", "element": "H"}], "resSeq": 740, "segmentID": ""}, {"index": 740, "name": "HOH", "atoms": [{"index": 2233, "name": "O", "element": "O"}, {"index": 2234, "name": "H1", "element": "H"}, {"index": 2235, "name": "H2", "element": "H"}], "resSeq": 741, "segmentID": ""}, {"index": 741, "name": "HOH", "atoms": [{"index": 2236, "name": "O", "element": "O"}, {"index": 2237, "name": "H1", "element": "H"}, {"index": 2238, "name": "H2", "element": "H"}], "resSeq": 742, "segmentID": ""}, {"index": 742, "name": "HOH", "atoms": [{"index": 2239, "name": "O", "element": "O"}, {"index": 2240, "name": "H1", "element": "H"}, {"index": 2241, "name": "H2", "element": "H"}], "resSeq": 743, "segmentID": ""}, {"index": 743, "name": "HOH", "atoms": [{"index": 2242, "name": "O", "element": "O"}, {"index": 2243, "name": "H1", "element": "H"}, {"index": 2244, "name": "H2", "element": "H"}], "resSeq": 744, "segmentID": ""}, {"index": 744, "name": "HOH", "atoms": [{"index": 2245, "name": "O", "element": "O"}, {"index": 2246, "name": "H1", "element": "H"}, {"index": 2247, "name": "H2", "element": "H"}], "resSeq": 745, "segmentID": ""}, {"index": 745, "name": "HOH", "atoms": [{"index": 2248, "name": "O", "element": "O"}, {"index": 2249, "name": "H1", "element": "H"}, {"index": 2250, "name": "H2", "element": "H"}], "resSeq": 746, "segmentID": ""}, {"index": 746, "name": "HOH", "atoms": [{"index": 2251, "name": "O", "element": "O"}, {"index": 2252, "name": "H1", "element": "H"}, {"index": 2253, "name": "H2", "element": "H"}], "resSeq": 747, "segmentID": ""}, {"index": 747, "name": "HOH", "atoms": [{"index": 2254, "name": "O", "element": "O"}, {"index": 2255, "name": "H1", "element": "H"}, {"index": 2256, "name": "H2", "element": "H"}], "resSeq": 748, "segmentID": ""}, {"index": 748, "name": "HOH", "atoms": [{"index": 2257, "name": "O", "element": "O"}, {"index": 2258, "name": "H1", "element": "H"}, {"index": 2259, "name": "H2", "element": "H"}], "resSeq": 749, "segmentID": ""}, {"index": 749, "name": "HOH", "atoms": [{"index": 2260, "name": "O", "element": "O"}, {"index": 2261, "name": "H1", "element": "H"}, {"index": 2262, "name": "H2", "element": "H"}], "resSeq": 750, "segmentID": ""}, {"index": 750, "name": "HOH", "atoms": [{"index": 2263, "name": "O", "element": "O"}, {"index": 2264, "name": "H1", "element": "H"}, {"index": 2265, "name": "H2", "element": "H"}], "resSeq": 751, "segmentID": ""}, {"index": 751, "name": "HOH", "atoms": [{"index": 2266, "name": "O", "element": "O"}, {"index": 2267, "name": "H1", "element": "H"}, {"index": 2268, "name": "H2", "element": "H"}], "resSeq": 752, "segmentID": ""}], "index": 0}], "bonds": [[1, 2], [1, 3], [0, 1], [10, 11], [10, 12], [10, 13], [8, 9], [6, 7], [18, 19], [18, 20], [18, 21], [16, 17], [22, 23], [22, 24], [25, 26], [25, 27], [28, 29], [28, 30], [31, 32], [31, 33], [34, 35], [34, 36], [37, 38], [37, 39], [40, 41], [40, 42], [43, 44], [43, 45], [46, 47], [46, 48], [49, 50], [49, 51], [52, 53], [52, 54], [55, 56], [55, 57], [58, 59], [58, 60], [61, 62], [61, 63], [64, 65], [64, 66], [67, 68], [67, 69], [70, 71], [70, 72], [73, 74], [73, 75], [76, 77], [76, 78], [79, 80], [79, 81], [82, 83], [82, 84], [85, 86], [85, 87], [88, 89], [88, 90], [91, 92], [91, 93], [94, 95], [94, 96], [97, 98], [97, 99], [100, 101], [100, 102], [103, 104], [103, 105], [106, 107], [106, 108], [109, 110], [109, 111], [112, 113], [112, 114], [115, 116], [115, 117], [118, 119], [118, 120], [121, 122], [121, 123], [124, 125], [124, 126], [127, 128], [127, 129], [130, 131], [130, 132], [133, 134], [133, 135], [136, 137], [136, 138], [139, 140], [139, 141], [142, 143], [142, 144], [145, 146], [145, 147], [148, 149], [148, 150], [151, 152], [151, 153], [154, 155], [154, 156], [157, 158], [157, 159], [160, 161], [160, 162], [163, 164], [163, 165], [166, 167], [166, 168], [169, 170], [169, 171], [172, 173], [172, 174], [175, 176], [175, 177], [178, 179], [178, 180], [181, 182], [181, 183], [184, 185], [184, 186], [187, 188], [187, 189], [190, 191], [190, 192], [193, 194], [193, 195], [196, 197], [196, 198], [199, 200], [199, 201], [202, 203], [202, 204], [205, 206], [205, 207], [208, 209], [208, 210], [211, 212], [211, 213], [214, 215], [214, 216], [217, 218], [217, 219], [220, 221], [220, 222], [223, 224], [223, 225], [226, 227], [226, 228], [229, 230], [229, 231], [232, 233], [232, 234], [235, 236], [235, 237], [238, 239], [238, 240], [241, 242], [241, 243], [244, 245], [244, 246], [247, 248], [247, 249], [250, 251], [250, 252], [253, 254], [253, 255], [256, 257], [256, 258], [259, 260], [259, 261], [262, 263], [262, 264], [265, 266], [265, 267], [268, 269], [268, 270], [271, 272], [271, 273], [274, 275], [274, 276], [277, 278], [277, 279], [280, 281], [280, 282], [283, 284], [283, 285], [286, 287], [286, 288], [289, 290], [289, 291], [292, 293], [292, 294], [295, 296], [295, 297], [298, 299], [298, 300], [301, 302], [301, 303], [304, 305], [304, 306], [307, 308], [307, 309], [310, 311], [310, 312], [313, 314], [313, 315], [316, 317], [316, 318], [319, 320], [319, 321], [322, 323], [322, 324], [325, 326], [325, 327], [328, 329], [328, 330], [331, 332], [331, 333], [334, 335], [334, 336], [337, 338], [337, 339], [340, 341], [340, 342], [343, 344], [343, 345], [346, 347], [346, 348], [349, 350], [349, 351], [352, 353], [352, 354], [355, 356], [355, 357], [358, 359], [358, 360], [361, 362], [361, 363], [364, 365], [364, 366], [367, 368], [367, 369], [370, 371], [370, 372], [373, 374], [373, 375], [376, 377], [376, 378], [379, 380], [379, 381], [382, 383], [382, 384], [385, 386], [385, 387], [388, 389], [388, 390], [391, 392], [391, 393], [394, 395], [394, 396], [397, 398], [397, 399], [400, 401], [400, 402], [403, 404], [403, 405], [406, 407], [406, 408], [409, 410], [409, 411], [412, 413], [412, 414], [415, 416], [415, 417], [418, 419], [418, 420], [421, 422], [421, 423], [424, 425], [424, 426], [427, 428], [427, 429], [430, 431], [430, 432], [433, 434], [433, 435], [436, 437], [436, 438], [439, 440], [439, 441], [442, 443], [442, 444], [445, 446], [445, 447], [448, 449], [448, 450], [451, 452], [451, 453], [454, 455], [454, 456], [457, 458], [457, 459], [460, 461], [460, 462], [463, 464], [463, 465], [466, 467], [466, 468], [469, 470], [469, 471], [472, 473], [472, 474], [475, 476], [475, 477], [478, 479], [478, 480], [481, 482], [481, 483], [484, 485], [484, 486], [487, 488], [487, 489], [490, 491], [490, 492], [493, 494], [493, 495], [496, 497], [496, 498], [499, 500], [499, 501], [502, 503], [502, 504], [505, 506], [505, 507], [508, 509], [508, 510], [511, 512], [511, 513], [514, 515], [514, 516], [517, 518], [517, 519], [520, 521], [520, 522], [523, 524], [523, 525], [526, 527], [526, 528], [529, 530], [529, 531], [532, 533], [532, 534], [535, 536], [535, 537], [538, 539], [538, 540], [541, 542], [541, 543], [544, 545], [544, 546], [547, 548], [547, 549], [550, 551], [550, 552], [553, 554], [553, 555], [556, 557], [556, 558], [559, 560], [559, 561], [562, 563], [562, 564], [565, 566], [565, 567], [568, 569], [568, 570], [571, 572], [571, 573], [574, 575], [574, 576], [577, 578], [577, 579], [580, 581], [580, 582], [583, 584], [583, 585], [586, 587], [586, 588], [589, 590], [589, 591], [592, 593], [592, 594], [595, 596], [595, 597], [598, 599], [598, 600], [601, 602], [601, 603], [604, 605], [604, 606], [607, 608], [607, 609], [610, 611], [610, 612], [613, 614], [613, 615], [616, 617], [616, 618], [619, 620], [619, 621], [622, 623], [622, 624], [625, 626], [625, 627], [628, 629], [628, 630], [631, 632], [631, 633], [634, 635], [634, 636], [637, 638], [637, 639], [640, 641], [640, 642], [643, 644], [643, 645], [646, 647], [646, 648], [649, 650], [649, 651], [652, 653], [652, 654], [655, 656], [655, 657], [658, 659], [658, 660], [661, 662], [661, 663], [664, 665], [664, 666], [667, 668], [667, 669], [670, 671], [670, 672], [673, 674], [673, 675], [676, 677], [676, 678], [679, 680], [679, 681], [682, 683], [682, 684], [685, 686], [685, 687], [688, 689], [688, 690], [691, 692], [691, 693], [694, 695], [694, 696], [697, 698], [697, 699], [700, 701], [700, 702], [703, 704], [703, 705], [706, 707], [706, 708], [709, 710], [709, 711], [712, 713], [712, 714], [715, 716], [715, 717], [718, 719], [718, 720], [721, 722], [721, 723], [724, 725], [724, 726], [727, 728], [727, 729], [730, 731], [730, 732], [733, 734], [733, 735], [736, 737], [736, 738], [739, 740], [739, 741], [742, 743], [742, 744], [745, 746], [745, 747], [748, 749], [748, 750], [751, 752], [751, 753], [754, 755], [754, 756], [757, 758], [757, 759], [760, 761], [760, 762], [763, 764], [763, 765], [766, 767], [766, 768], [769, 770], [769, 771], [772, 773], [772, 774], [775, 776], [775, 777], [778, 779], [778, 780], [781, 782], [781, 783], [784, 785], [784, 786], [787, 788], [787, 789], [790, 791], [790, 792], [793, 794], [793, 795], [796, 797], [796, 798], [799, 800], [799, 801], [802, 803], [802, 804], [805, 806], [805, 807], [808, 809], [808, 810], [811, 812], [811, 813], [814, 815], [814, 816], [817, 818], [817, 819], [820, 821], [820, 822], [823, 824], [823, 825], [826, 827], [826, 828], [829, 830], [829, 831], [832, 833], [832, 834], [835, 836], [835, 837], [838, 839], [838, 840], [841, 842], [841, 843], [844, 845], [844, 846], [847, 848], [847, 849], [850, 851], [850, 852], [853, 854], [853, 855], [856, 857], [856, 858], [859, 860], [859, 861], [862, 863], [862, 864], [865, 866], [865, 867], [868, 869], [868, 870], [871, 872], [871, 873], [874, 875], [874, 876], [877, 878], [877, 879], [880, 881], [880, 882], [883, 884], [883, 885], [886, 887], [886, 888], [889, 890], [889, 891], [892, 893], [892, 894], [895, 896], [895, 897], [898, 899], [898, 900], [901, 902], [901, 903], [904, 905], [904, 906], [907, 908], [907, 909], [910, 911], [910, 912], [913, 914], [913, 915], [916, 917], [916, 918], [919, 920], [919, 921], [922, 923], [922, 924], [925, 926], [925, 927], [928, 929], [928, 930], [931, 932], [931, 933], [934, 935], [934, 936], [937, 938], [937, 939], [940, 941], [940, 942], [943, 944], [943, 945], [946, 947], [946, 948], [949, 950], [949, 951], [952, 953], [952, 954], [955, 956], [955, 957], [958, 959], [958, 960], [961, 962], [961, 963], [964, 965], [964, 966], [967, 968], [967, 969], [970, 971], [970, 972], [973, 974], [973, 975], [976, 977], [976, 978], [979, 980], [979, 981], [982, 983], [982, 984], [985, 986], [985, 987], [988, 989], [988, 990], [991, 992], [991, 993], [994, 995], [994, 996], [997, 998], [997, 999], [1000, 1001], [1000, 1002], [1003, 1004], [1003, 1005], [1006, 1007], [1006, 1008], [1009, 1010], [1009, 1011], [1012, 1013], [1012, 1014], [1015, 1016], [1015, 1017], [1018, 1019], [1018, 1020], [1021, 1022], [1021, 1023], [1024, 1025], [1024, 1026], [1027, 1028], [1027, 1029], [1030, 1031], [1030, 1032], [1033, 1034], [1033, 1035], [1036, 1037], [1036, 1038], [1039, 1040], [1039, 1041], [1042, 1043], [1042, 1044], [1045, 1046], [1045, 1047], [1048, 1049], [1048, 1050], [1051, 1052], [1051, 1053], [1054, 1055], [1054, 1056], [1057, 1058], [1057, 1059], [1060, 1061], [1060, 1062], [1063, 1064], [1063, 1065], [1066, 1067], [1066, 1068], [1069, 1070], [1069, 1071], [1072, 1073], [1072, 1074], [1075, 1076], [1075, 1077], [1078, 1079], [1078, 1080], [1081, 1082], [1081, 1083], [1084, 1085], [1084, 1086], [1087, 1088], [1087, 1089], [1090, 1091], [1090, 1092], [1093, 1094], [1093, 1095], [1096, 1097], [1096, 1098], [1099, 1100], [1099, 1101], [1102, 1103], [1102, 1104], [1105, 1106], [1105, 1107], [1108, 1109], [1108, 1110], [1111, 1112], [1111, 1113], [1114, 1115], [1114, 1116], [1117, 1118], [1117, 1119], [1120, 1121], [1120, 1122], [1123, 1124], [1123, 1125], [1126, 1127], [1126, 1128], [1129, 1130], [1129, 1131], [1132, 1133], [1132, 1134], [1135, 1136], [1135, 1137], [1138, 1139], [1138, 1140], [1141, 1142], [1141, 1143], [1144, 1145], [1144, 1146], [1147, 1148], [1147, 1149], [1150, 1151], [1150, 1152], [1153, 1154], [1153, 1155], [1156, 1157], [1156, 1158], [1159, 1160], [1159, 1161], [1162, 1163], [1162, 1164], [1165, 1166], [1165, 1167], [1168, 1169], [1168, 1170], [1171, 1172], [1171, 1173], [1174, 1175], [1174, 1176], [1177, 1178], [1177, 1179], [1180, 1181], [1180, 1182], [1183, 1184], [1183, 1185], [1186, 1187], [1186, 1188], [1189, 1190], [1189, 1191], [1192, 1193], [1192, 1194], [1195, 1196], [1195, 1197], [1198, 1199], [1198, 1200], [1201, 1202], [1201, 1203], [1204, 1205], [1204, 1206], [1207, 1208], [1207, 1209], [1210, 1211], [1210, 1212], [1213, 1214], [1213, 1215], [1216, 1217], [1216, 1218], [1219, 1220], [1219, 1221], [1222, 1223], [1222, 1224], [1225, 1226], [1225, 1227], [1228, 1229], [1228, 1230], [1231, 1232], [1231, 1233], [1234, 1235], [1234, 1236], [1237, 1238], [1237, 1239], [1240, 1241], [1240, 1242], [1243, 1244], [1243, 1245], [1246, 1247], [1246, 1248], [1249, 1250], [1249, 1251], [1252, 1253], [1252, 1254], [1255, 1256], [1255, 1257], [1258, 1259], [1258, 1260], [1261, 1262], [1261, 1263], [1264, 1265], [1264, 1266], [1267, 1268], [1267, 1269], [1270, 1271], [1270, 1272], [1273, 1274], [1273, 1275], [1276, 1277], [1276, 1278], [1279, 1280], [1279, 1281], [1282, 1283], [1282, 1284], [1285, 1286], [1285, 1287], [1288, 1289], [1288, 1290], [1291, 1292], [1291, 1293], [1294, 1295], [1294, 1296], [1297, 1298], [1297, 1299], [1300, 1301], [1300, 1302], [1303, 1304], [1303, 1305], [1306, 1307], [1306, 1308], [1309, 1310], [1309, 1311], [1312, 1313], [1312, 1314], [1315, 1316], [1315, 1317], [1318, 1319], [1318, 1320], [1321, 1322], [1321, 1323], [1324, 1325], [1324, 1326], [1327, 1328], [1327, 1329], [1330, 1331], [1330, 1332], [1333, 1334], [1333, 1335], [1336, 1337], [1336, 1338], [1339, 1340], [1339, 1341], [1342, 1343], [1342, 1344], [1345, 1346], [1345, 1347], [1348, 1349], [1348, 1350], [1351, 1352], [1351, 1353], [1354, 1355], [1354, 1356], [1357, 1358], [1357, 1359], [1360, 1361], [1360, 1362], [1363, 1364], [1363, 1365], [1366, 1367], [1366, 1368], [1369, 1370], [1369, 1371], [1372, 1373], [1372, 1374], [1375, 1376], [1375, 1377], [1378, 1379], [1378, 1380], [1381, 1382], [1381, 1383], [1384, 1385], [1384, 1386], [1387, 1388], [1387, 1389], [1390, 1391], [1390, 1392], [1393, 1394], [1393, 1395], [1396, 1397], [1396, 1398], [1399, 1400], [1399, 1401], [1402, 1403], [1402, 1404], [1405, 1406], [1405, 1407], [1408, 1409], [1408, 1410], [1411, 1412], [1411, 1413], [1414, 1415], [1414, 1416], [1417, 1418], [1417, 1419], [1420, 1421], [1420, 1422], [1423, 1424], [1423, 1425], [1426, 1427], [1426, 1428], [1429, 1430], [1429, 1431], [1432, 1433], [1432, 1434], [1435, 1436], [1435, 1437], [1438, 1439], [1438, 1440], [1441, 1442], [1441, 1443], [1444, 1445], [1444, 1446], [1447, 1448], [1447, 1449], [1450, 1451], [1450, 1452], [1453, 1454], [1453, 1455], [1456, 1457], [1456, 1458], [1459, 1460], [1459, 1461], [1462, 1463], [1462, 1464], [1465, 1466], [1465, 1467], [1468, 1469], [1468, 1470], [1471, 1472], [1471, 1473], [1474, 1475], [1474, 1476], [1477, 1478], [1477, 1479], [1480, 1481], [1480, 1482], [1483, 1484], [1483, 1485], [1486, 1487], [1486, 1488], [1489, 1490], [1489, 1491], [1492, 1493], [1492, 1494], [1495, 1496], [1495, 1497], [1498, 1499], [1498, 1500], [1501, 1502], [1501, 1503], [1504, 1505], [1504, 1506], [1507, 1508], [1507, 1509], [1510, 1511], [1510, 1512], [1513, 1514], [1513, 1515], [1516, 1517], [1516, 1518], [1519, 1520], [1519, 1521], [1522, 1523], [1522, 1524], [1525, 1526], [1525, 1527], [1528, 1529], [1528, 1530], [1531, 1532], [1531, 1533], [1534, 1535], [1534, 1536], [1537, 1538], [1537, 1539], [1540, 1541], [1540, 1542], [1543, 1544], [1543, 1545], [1546, 1547], [1546, 1548], [1549, 1550], [1549, 1551], [1552, 1553], [1552, 1554], [1555, 1556], [1555, 1557], [1558, 1559], [1558, 1560], [1561, 1562], [1561, 1563], [1564, 1565], [1564, 1566], [1567, 1568], [1567, 1569], [1570, 1571], [1570, 1572], [1573, 1574], [1573, 1575], [1576, 1577], [1576, 1578], [1579, 1580], [1579, 1581], [1582, 1583], [1582, 1584], [1585, 1586], [1585, 1587], [1588, 1589], [1588, 1590], [1591, 1592], [1591, 1593], [1594, 1595], [1594, 1596], [1597, 1598], [1597, 1599], [1600, 1601], [1600, 1602], [1603, 1604], [1603, 1605], [1606, 1607], [1606, 1608], [1609, 1610], [1609, 1611], [1612, 1613], [1612, 1614], [1615, 1616], [1615, 1617], [1618, 1619], [1618, 1620], [1621, 1622], [1621, 1623], [1624, 1625], [1624, 1626], [1627, 1628], [1627, 1629], [1630, 1631], [1630, 1632], [1633, 1634], [1633, 1635], [1636, 1637], [1636, 1638], [1639, 1640], [1639, 1641], [1642, 1643], [1642, 1644], [1645, 1646], [1645, 1647], [1648, 1649], [1648, 1650], [1651, 1652], [1651, 1653], [1654, 1655], [1654, 1656], [1657, 1658], [1657, 1659], [1660, 1661], [1660, 1662], [1663, 1664], [1663, 1665], [1666, 1667], [1666, 1668], [1669, 1670], [1669, 1671], [1672, 1673], [1672, 1674], [1675, 1676], [1675, 1677], [1678, 1679], [1678, 1680], [1681, 1682], [1681, 1683], [1684, 1685], [1684, 1686], [1687, 1688], [1687, 1689], [1690, 1691], [1690, 1692], [1693, 1694], [1693, 1695], [1696, 1697], [1696, 1698], [1699, 1700], [1699, 1701], [1702, 1703], [1702, 1704], [1705, 1706], [1705, 1707], [1708, 1709], [1708, 1710], [1711, 1712], [1711, 1713], [1714, 1715], [1714, 1716], [1717, 1718], [1717, 1719], [1720, 1721], [1720, 1722], [1723, 1724], [1723, 1725], [1726, 1727], [1726, 1728], [1729, 1730], [1729, 1731], [1732, 1733], [1732, 1734], [1735, 1736], [1735, 1737], [1738, 1739], [1738, 1740], [1741, 1742], [1741, 1743], [1744, 1745], [1744, 1746], [1747, 1748], [1747, 1749], [1750, 1751], [1750, 1752], [1753, 1754], [1753, 1755], [1756, 1757], [1756, 1758], [1759, 1760], [1759, 1761], [1762, 1763], [1762, 1764], [1765, 1766], [1765, 1767], [1768, 1769], [1768, 1770], [1771, 1772], [1771, 1773], [1774, 1775], [1774, 1776], [1777, 1778], [1777, 1779], [1780, 1781], [1780, 1782], [1783, 1784], [1783, 1785], [1786, 1787], [1786, 1788], [1789, 1790], [1789, 1791], [1792, 1793], [1792, 1794], [1795, 1796], [1795, 1797], [1798, 1799], [1798, 1800], [1801, 1802], [1801, 1803], [1804, 1805], [1804, 1806], [1807, 1808], [1807, 1809], [1810, 1811], [1810, 1812], [1813, 1814], [1813, 1815], [1816, 1817], [1816, 1818], [1819, 1820], [1819, 1821], [1822, 1823], [1822, 1824], [1825, 1826], [1825, 1827], [1828, 1829], [1828, 1830], [1831, 1832], [1831, 1833], [1834, 1835], [1834, 1836], [1837, 1838], [1837, 1839], [1840, 1841], [1840, 1842], [1843, 1844], [1843, 1845], [1846, 1847], [1846, 1848], [1849, 1850], [1849, 1851], [1852, 1853], [1852, 1854], [1855, 1856], [1855, 1857], [1858, 1859], [1858, 1860], [1861, 1862], [1861, 1863], [1864, 1865], [1864, 1866], [1867, 1868], [1867, 1869], [1870, 1871], [1870, 1872], [1873, 1874], [1873, 1875], [1876, 1877], [1876, 1878], [1879, 1880], [1879, 1881], [1882, 1883], [1882, 1884], [1885, 1886], [1885, 1887], [1888, 1889], [1888, 1890], [1891, 1892], [1891, 1893], [1894, 1895], [1894, 1896], [1897, 1898], [1897, 1899], [1900, 1901], [1900, 1902], [1903, 1904], [1903, 1905], [1906, 1907], [1906, 1908], [1909, 1910], [1909, 1911], [1912, 1913], [1912, 1914], [1915, 1916], [1915, 1917], [1918, 1919], [1918, 1920], [1921, 1922], [1921, 1923], [1924, 1925], [1924, 1926], [1927, 1928], [1927, 1929], [1930, 1931], [1930, 1932], [1933, 1934], [1933, 1935], [1936, 1937], [1936, 1938], [1939, 1940], [1939, 1941], [1942, 1943], [1942, 1944], [1945, 1946], [1945, 1947], [1948, 1949], [1948, 1950], [1951, 1952], [1951, 1953], [1954, 1955], [1954, 1956], [1957, 1958], [1957, 1959], [1960, 1961], [1960, 1962], [1963, 1964], [1963, 1965], [1966, 1967], [1966, 1968], [1969, 1970], [1969, 1971], [1972, 1973], [1972, 1974], [1975, 1976], [1975, 1977], [1978, 1979], [1978, 1980], [1981, 1982], [1981, 1983], [1984, 1985], [1984, 1986], [1987, 1988], [1987, 1989], [1990, 1991], [1990, 1992], [1993, 1994], [1993, 1995], [1996, 1997], [1996, 1998], [1999, 2000], [1999, 2001], [2002, 2003], [2002, 2004], [2005, 2006], [2005, 2007], [2008, 2009], [2008, 2010], [2011, 2012], [2011, 2013], [2014, 2015], [2014, 2016], [2017, 2018], [2017, 2019], [2020, 2021], [2020, 2022], [2023, 2024], [2023, 2025], [2026, 2027], [2026, 2028], [2029, 2030], [2029, 2031], [2032, 2033], [2032, 2034], [2035, 2036], [2035, 2037], [2038, 2039], [2038, 2040], [2041, 2042], [2041, 2043], [2044, 2045], [2044, 2046], [2047, 2048], [2047, 2049], [2050, 2051], [2050, 2052], [2053, 2054], [2053, 2055], [2056, 2057], [2056, 2058], [2059, 2060], [2059, 2061], [2062, 2063], [2062, 2064], [2065, 2066], [2065, 2067], [2068, 2069], [2068, 2070], [2071, 2072], [2071, 2073], [2074, 2075], [2074, 2076], [2077, 2078], [2077, 2079], [2080, 2081], [2080, 2082], [2083, 2084], [2083, 2085], [2086, 2087], [2086, 2088], [2089, 2090], [2089, 2091], [2092, 2093], [2092, 2094], [2095, 2096], [2095, 2097], [2098, 2099], [2098, 2100], [2101, 2102], [2101, 2103], [2104, 2105], [2104, 2106], [2107, 2108], [2107, 2109], [2110, 2111], [2110, 2112], [2113, 2114], [2113, 2115], [2116, 2117], [2116, 2118], [2119, 2120], [2119, 2121], [2122, 2123], [2122, 2124], [2125, 2126], [2125, 2127], [2128, 2129], [2128, 2130], [2131, 2132], [2131, 2133], [2134, 2135], [2134, 2136], [2137, 2138], [2137, 2139], [2140, 2141], [2140, 2142], [2143, 2144], [2143, 2145], [2146, 2147], [2146, 2148], [2149, 2150], [2149, 2151], [2152, 2153], [2152, 2154], [2155, 2156], [2155, 2157], [2158, 2159], [2158, 2160], [2161, 2162], [2161, 2163], [2164, 2165], [2164, 2166], [2167, 2168], [2167, 2169], [2170, 2171], [2170, 2172], [2173, 2174], [2173, 2175], [2176, 2177], [2176, 2178], [2179, 2180], [2179, 2181], [2182, 2183], [2182, 2184], [2185, 2186], [2185, 2187], [2188, 2189], [2188, 2190], [2191, 2192], [2191, 2193], [2194, 2195], [2194, 2196], [2197, 2198], [2197, 2199], [2200, 2201], [2200, 2202], [2203, 2204], [2203, 2205], [2206, 2207], [2206, 2208], [2209, 2210], [2209, 2211], [2212, 2213], [2212, 2214], [2215, 2216], [2215, 2217], [2218, 2219], [2218, 2220], [2221, 2222], [2221, 2223], [2224, 2225], [2224, 2226], [2227, 2228], [2227, 2229], [2230, 2231], [2230, 2232], [2233, 2234], [2233, 2235], [2236, 2237], [2236, 2238], [2239, 2240], [2239, 2241], [2242, 2243], [2242, 2244], [2245, 2246], [2245, 2247], [2248, 2249], [2248, 2250], [2251, 2252], [2251, 2253], [2254, 2255], [2254, 2256], [2257, 2258], [2257, 2259], [2260, 2261], [2260, 2262], [2263, 2264], [2263, 2265], [2266, 2267], [2266, 2268], [4, 5], [4, 6], [1, 4], [14, 15], [14, 16], [8, 10], [8, 14], [6, 8], [16, 18]]} \ No newline at end of file diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.crd.dvc b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.crd.dvc deleted file mode 100644 index eb5e06fb..00000000 --- a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.crd.dvc +++ /dev/null @@ -1,5 +0,0 @@ -outs: -- md5: 567e2fd21891f7a3b6d62aab422a3538 - size: 82903 - hash: md5 - path: alanine-dipeptide.crd diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.pdb.dvc b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.pdb.dvc deleted file mode 100644 index e0424930..00000000 --- a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.pdb.dvc +++ /dev/null @@ -1,5 +0,0 @@ -outs: -- md5: 6e51f11350240325b11408b6ba782158 - size: 184227 - hash: md5 - path: alanine-dipeptide.pdb diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.prmtop.dvc b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.prmtop.dvc deleted file mode 100644 index 7ae1039d..00000000 --- a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/alanine-dipeptide.prmtop.dvc +++ /dev/null @@ -1,5 +0,0 @@ -outs: -- md5: 099b49adfcc8da32792ea5efdffe9f5b - size: 357642 - hash: md5 - path: alanine-dipeptide.prmtop diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/generate-pdb.py b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/generate-pdb.py deleted file mode 100644 index 583c761c..00000000 --- a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/generate-pdb.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -Generate PDB file containing periodic box data. - -""" - -from simtk import openmm, unit -from simtk.openmm import app - -prmtop_filename = 'alanine-dipeptide.prmtop' -crd_filename = 'alanine-dipeptide.crd' -pdb_filename = 'alanine-dipeptide.pdb' - -# Read topology and positions. -prmtop = app.AmberPrmtopFile(prmtop_filename) -inpcrd = app.AmberInpcrdFile(crd_filename) - -# Write PDB. -outfile = open(pdb_filename, 'w') -app.PDBFile.writeFile(prmtop.topology, inpcrd.positions, file=outfile, keepIds=False) -outfile.close() - diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/leap.log.dvc b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/leap.log.dvc deleted file mode 100644 index 593e3928..00000000 --- a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/leap.log.dvc +++ /dev/null @@ -1,5 +0,0 @@ -outs: -- md5: 6a49ab75254c368f73beff887d7cac8b - size: 9517 - hash: md5 - path: leap.log diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/run.sh b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/run.sh deleted file mode 100755 index bddcda4d..00000000 --- a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/run.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/tcsh - -# Name of system -setenv SYSTEM alanine-dipeptide - -# Clean up old files, if present. -rm -f leap.log ${SYSTEM}.{crd,prmtop,pdb} - -# Create prmtop/crd files. -tleap -f setup.leap.in - -# Create PDB file. -#cat ${SYSTEM}.crd | ambpdb -p ${SYSTEM}.prmtop > ${SYSTEM}.pdb -python generate-pdb.py - diff --git a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/setup.leap.in b/src/wepy_tools/systems/data/alanine_dipeptide_explicit/setup.leap.in deleted file mode 100644 index 78ffb798..00000000 --- a/src/wepy_tools/systems/data/alanine_dipeptide_explicit/setup.leap.in +++ /dev/null @@ -1,23 +0,0 @@ -# Create terminally-blocked alanine peptide model with AMBER ff96 and OBC GBSA. - -# Load AMBER '96 forcefield for protein. -source leaprc.ff96 - -# Create sequence. -peptide = sequence { ACE ALA NME } - -# Check peptide. -check peptide - -# Report on net charge. -charge peptide - -# Solvate in water box. -solvateBox peptide TIP3PBOX 9.0 iso - -# Write parameters. -saveAmberParm peptide alanine-dipeptide.prmtop alanine-dipeptide.crd - -# Exit -quit - diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index 53d5100a..503d60b1 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -5,6 +5,7 @@ Configurable platforms. """ +import importlib.resources import copy import openmm import psutil @@ -12,17 +13,17 @@ import mdtraj # TODO: use the high-level API imports from wepy.walker import Walker -from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState, HeartBeatLoggingReporterFactory +from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState, HeartBeatLoggingReporterFactory, OpenMMStateWrapper from wepy.sim_manager import Manager from wepy.work_mapper.openmm import OpenMMProcPoolWorkMapperFactory from wepy.resampling.resamplers.revo import REVOResampler from wepy.util.mdtraj import mdtraj_to_json_topology from wepy.runners.openmm.logger import HeartBeatLoggingReporter from wepy.resampling.resamplers.noresampler import NoResampler +from wepy.util.mdtraj import json_to_mdtraj_topology -from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideExplicit, AlanineDipeptideRamachandranDistance from wepy_tools.systems.lennard_jones import LennardJonesPair, PairDistance - +from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideRamachandranDistance def test_lennard_jones_revo_procpool(): @@ -113,14 +114,35 @@ def test_lennard_jones_revo_procpool(): def test_alanine_dipeptide_revo_procpool(): - # ALERT: Using this system will cause the simulation to stall - test_sys = AlanineDipeptideExplicit() + TEMPERATURE = 300. * openmm.unit.kelvin - integrator = openmm.LangevinIntegrator(300.0, 0.1, 0.002) + # load the system and state information for the simulation + ala_files = importlib.resources.files("wepy_tools.systems.data.alanine_dipeptide_explicit") + system_xml_path = ala_files / "alanine-dipeptide-explicit.system.omm.xml" + state_xml_path = ala_files / "alanine-dipeptide-explicit.state.omm.xml" + top_json_path = ala_files / "alanine-dipeptide-explicit.top.json" + + system = openmm.XmlSerializer.deserialize(system_xml_path.read_text()) + + init_state_wrapper = OpenMMStateWrapper.from_xml(state_xml_path.read_text()) + init_state = OpenMMState.from_state_wrapper(init_state_wrapper) + + json_top_str = top_json_path.read_text() + mdj_top = json_to_mdtraj_topology(json_top_str) + topology = mdj_top.to_openmm() + + integrator = openmm.LangevinIntegrator(TEMPERATURE, 0.1, 0.002) + + # add the pseudo forces like barostat + barostat = openmm.MonteCarloBarostat( + 1. * openmm.unit.atmosphere, + TEMPERATURE, + ) + system.addForce(barostat) runner_factory = OpenMMRunnerFactory( - system=test_sys.system, - topology=test_sys.topology, + system=system, + topology=topology, integrator=integrator, # For this test we do want heart beat at shorter interval openmm_reporter_factories=[ @@ -131,10 +153,6 @@ def test_alanine_dipeptide_revo_procpool(): num_walkers = 4 - init_state = OpenMMState.from_dwim( - positions=test_sys.positions, - ) - # TODO: remove the need to deepcopy and have the components make # their own copies if necessary walker_states = [ @@ -163,11 +181,7 @@ def test_alanine_dipeptide_revo_procpool(): num_workers = num_walkers cores_per_worker = (num_workers // num_walkers) - json_top = mdtraj_to_json_topology( - mdtraj.Topology.from_openmm(test_sys.topology) - ) - - distance_metric = AlanineDipeptideRamachandranDistance(json_top) + distance_metric = AlanineDipeptideRamachandranDistance(json_top_str) resampler = REVOResampler( merge_dist=4, @@ -179,8 +193,11 @@ def test_alanine_dipeptide_revo_procpool(): sim_manager = Manager( init_walkers=init_walkers, runner_factory=runner_factory, - resampler=NoResampler(), + # resampler=NoResampler(), + resampler=resampler, work_mapper_factory=OpenMMProcPoolWorkMapperFactory( + # num_procs=2, + # platform="Reference", platform="CPU", num_procs=num_workers, global_platform_properties={"Threads" : str(cores_per_worker)}, @@ -188,7 +205,7 @@ def test_alanine_dipeptide_revo_procpool(): ) new_walkers, sim_components = sim_manager.run_simulation( - n_cycles=1, - segment_lengths=10, + n_cycles=2, + segment_lengths=100, ) From 9d5dacec618d5a86da9ff6f976e0d425b89cb459 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 9 Dec 2025 15:43:57 -0500 Subject: [PATCH 071/143] fix REVO Pool and force spawn Fixed a bug in the multiprocessing context for REVO. Also just hardcoded the proc pool mapper to use spawn always and not have an option. --- src/wepy/resampling/resamplers/revo.py | 14 ++++++++++---- src/wepy/work_mapper/openmm/proc_pool.py | 8 ++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 012cdfe2..83d81500 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -654,18 +654,24 @@ def _all_to_all_distance(self, walkers): # make images for all the walker states for us to compute distances on if self.num_proc > 1: - log_queue = mp.Queue() - handlers = list(logging.getLogger().handlers) - listener = logging.handlers.QueueListener(log_queue, *handlers) - listener.start() # NOTE: Must use spawn here, otherwise there are problems # with deadlocking in the sub-processes mp_ctx = mp.get_context(method="spawn") + log_queue = mp_ctx.Queue() + handlers = list(logging.getLogger().handlers) + listener = logging.handlers.QueueListener(log_queue, *handlers) + listener.start() + + # TODO: This should be part of some setup period + with mp_ctx.Pool( self.num_proc, initializer=proc_pool_worker_setup, initargs=(log_queue,), + # Set some upper bound so that it gets cleaned up + # in case of leaks + maxtasksperchild=4 ) as pool: images = pool.map(self.distance.image, [walker.state for walker in walkers]) else: diff --git a/src/wepy/work_mapper/openmm/proc_pool.py b/src/wepy/work_mapper/openmm/proc_pool.py index cef08428..c0062a01 100644 --- a/src/wepy/work_mapper/openmm/proc_pool.py +++ b/src/wepy/work_mapper/openmm/proc_pool.py @@ -28,7 +28,6 @@ def __init__( device_ids: list[int] | None = None, global_platform_properties: dict[str, str] | None = None, device_platform_properties: list[dict[str, str]] | None = None, - proc_start_method: Literal["fork", "spawn", "forkserver"] = "spawn", ): @@ -66,7 +65,6 @@ def __init__( } if device_ids is not None else None self._num_procs = num_procs - self._proc_start_method = proc_start_method def init( self, @@ -74,8 +72,8 @@ def init( logger.info("Initializing ProcPoolMapper") - logger.info(f"Initializing local multiprocessing context with start method: {self._proc_start_method}") - self._mp_ctx = mp.get_context(method=self._proc_start_method) + logger.info(f"Initializing local multiprocessing context with start method: spawn") + self._mp_ctx = mp.get_context(method="spawn") def cleanup(self) -> None: @@ -216,7 +214,6 @@ class OpenMMProcPoolWorkMapperFactory: device_ids: list[int] | None = None global_platform_properties: dict[str, str] | None = None device_platform_properties: list[dict[str, str]] | None = None - proc_start_method: Literal["fork", "spawn", "forkserver"] = "spawn" def __attrs_post_init__(self) -> None: @@ -242,6 +239,5 @@ def __call__(self) -> OpenMMProcPoolWorkMapper: device_ids=self.device_ids, global_platform_properties=self.global_platform_properties, device_platform_properties=self.device_platform_properties, - proc_start_method=self.proc_start_method, ) From a15882ef6e4b2f10e07e71d2fe730d3d81ace264 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 9 Dec 2025 16:23:19 -0500 Subject: [PATCH 072/143] get all unit tests passing --- src/wepy/runners/openmm/logger.py | 8 - src/wepy/work_mapper/openmm/__init__.py | 2 - src/wepy/work_mapper/openmm/serial.py | 79 --------- src/wepy/work_mapper/proc_pool_mapper.py | 150 ------------------ src/wepy_tools/systems/alanine_dipeptide.py | 32 +++- .../integration/test_openmm/test_realistic.py | 27 +--- .../test_runners/test_openmm/test_logger.py | 30 +++- .../test_runners/test_openmm/test_runner.py | 5 +- tests/unit/test_util/test_multiprocessing.py | 7 +- .../test_systems/test_alanine_dipeptide.py | 44 ++--- tests/unit/test_work_mapper/test_openmm.py | 150 ++++++------------ .../test_work_mapper/test_proc_pool_mapper.py | 37 ----- 12 files changed, 128 insertions(+), 443 deletions(-) delete mode 100644 src/wepy/work_mapper/openmm/serial.py delete mode 100644 src/wepy/work_mapper/proc_pool_mapper.py delete mode 100644 tests/unit/test_work_mapper/test_proc_pool_mapper.py diff --git a/src/wepy/runners/openmm/logger.py b/src/wepy/runners/openmm/logger.py index 86f7851d..d92d6727 100644 --- a/src/wepy/runners/openmm/logger.py +++ b/src/wepy/runners/openmm/logger.py @@ -131,14 +131,6 @@ def describeNextReport( simulation: openmm.app.Simulation, ) -> OpenMMReporterNextReport: - # Special case for first step - if simulation.context.getStepCount() == 0: - return OpenMMReporterNextReport( - steps=0, - include=list(self.state_includes), - periodic=False, - ) - _unit = openmm.unit.attosecond curr_sampling_time: openmm.unit.Quantity = simulation.context.getTime() diff --git a/src/wepy/work_mapper/openmm/__init__.py b/src/wepy/work_mapper/openmm/__init__.py index 45d0c702..7413913b 100644 --- a/src/wepy/work_mapper/openmm/__init__.py +++ b/src/wepy/work_mapper/openmm/__init__.py @@ -1,7 +1,5 @@ -from .serial import OpenMMSerialWorkMapperFactory from .proc_pool import OpenMMProcPoolWorkMapperFactory __all__ = [ - "OpenMMSerialWorkMapperFactory", "OpenMMProcPoolWorkMapperFactory", ] diff --git a/src/wepy/work_mapper/openmm/serial.py b/src/wepy/work_mapper/openmm/serial.py deleted file mode 100644 index 2c15a70d..00000000 --- a/src/wepy/work_mapper/openmm/serial.py +++ /dev/null @@ -1,79 +0,0 @@ -import logging -from typing import Literal, Any, Callable -import time -import itertools - -import attrs - -from wepy.work_mapper.base import WorkMapper -from wepy.runners.openmm import OpenMMState, OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS - -logger = logging.getLogger(__name__) - -class OpenMMSerialWorkMapper(WorkMapper): - - def __init__( - self, - platform: OpenMMPlatformName, - global_platform_properties: dict[str, str] | None = None, - ) -> None: - self._worker_segment_times: dict[int, list[float]] = {0: []} - - self._platform = platform - self._global_platform_properties = global_platform_properties if global_platform_properties is not None else {} - - def get_worker_segment_times(self) -> dict[int, list[float]]: - """The run timings for each segment for each walker. - - Returns - ------- - worker_seg_times : Dictionary mapping worker indices to a list of times in - seconds for each segment run. - - """ - return self._worker_segment_times - - def init(self) -> None: - pass - - def cleanup(self) -> None: - pass - - def map( - self, - task: Callable[[OpenMMState, int], OpenMMState], - walker_states: list[OpenMMState], - segment_lengths: list[int], - ) -> list[OpenMMState]: - - segment_times: list[float] = [] - results: list[OpenMMState] = [] - for task_idx, task_args in enumerate(zip(walker_states, segment_lengths, strict=True)): - - tic = time.time() - result = task( - *task_args, - platform_name=self._platform, - platform_kwargs=self._global_platform_properties, - ) - toc = time.time() - - segment_times.append(toc - tic) - results.append(result) - - self._worker_segment_times[0] = segment_times - - return results - -@attrs.define -class OpenMMSerialWorkMapperFactory: - - platform: OpenMMPlatformName - global_platform_properties: dict[str, str] | None = None - - def __call__(self) -> OpenMMSerialWorkMapper: - - return OpenMMSerialWorkMapper( - platform=self.platform, - global_platform_properties=self.global_platform_properties, - ) diff --git a/src/wepy/work_mapper/proc_pool_mapper.py b/src/wepy/work_mapper/proc_pool_mapper.py deleted file mode 100644 index b00b22f3..00000000 --- a/src/wepy/work_mapper/proc_pool_mapper.py +++ /dev/null @@ -1,150 +0,0 @@ -import itertools -import multiprocessing as mp -from typing import Any, Callable, Literal, Generic, TypeVar, Never -import logging - -import attrs - -from wepy.interface import ( - WorkMapperFactoryArgs, -) -from wepy.work_mapper.base import Task, WalkerState, WorkMapper - -# log_safe.initialize_safe_logging() - -logger = logging.getLogger(__name__) - -WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) -Task_ = TypeVar("Task_", bound=Task) - -def _force_error(exc: Exception) -> Never: - raise exc - -@attrs.define -class ProcPoolTask(Task, Generic[Task_]): - - wrapped_task: Task_ - - def __call__(self, walker_state: WalkerState_) -> WalkerState_: - - logging.basicConfig(level="INFO") - logging.getLogger("ProcPoolTask").info("Configured logging in task process") - - return self.wrapped_task(walker_state) - -class ProcPoolMapper( - WorkMapper, - Generic[ - WalkerState_, - Task_, - ], -): - - def __init__( - self, - wm_args: WorkMapperFactoryArgs, - ) -> None: - self._num_workers = wm_args.num_workers - - def init( - self, - ) -> None: - """l.. - - worker_func_args: This is an arbitrary set of key-values that - for each worker will be passed into the segment_func call - if that worker is used. Useful for injecting things like - device IDs. - - """ - - logger.info("Initializing ProcPoolMapper") - - logger.info(f"Initializing local multiprocessing context with 'spawn' process start method") - - # NOTE: always require "spawn" as this is the safest and - # changing to "fork" can have lots of other effects that we - # don't want to test - self._mp_ctx = mp.get_context(method="spawn") - - def cleanup(self) -> None: - - logger.info("Running ProcPoolMapper cleanup") - logger.info("Nothing to do") - - def map( - self, - tasks: list[Task_], - walker_states: list[WalkerState_], - ) -> list[WalkerState_]: - - logger.info(f"Running map on {len(walker_states)} in batches of {self._num_workers}") - - # spin up a new pool for each map - logger.info(f"Starting process Pool with {self._num_workers}") - - with self._mp_ctx.Pool( - processes=self._num_workers, - # NOTE: only run one thing per task, just to make sure - # everything is cleaned up which is an issue with - # OpenMM contexts. Also note that this is why we use a - # multiprocessing.Pool and not a - # concurrent.futures.ProcessPoolExecutor - maxtasksperchild=1, - ) as pool: - - results = [] - for batch_idx, batch in enumerate(itertools.batched( - zip(walker_states, tasks, strict=True), - self._num_workers, - strict=False, - )): - - logger.info(f"Submitting batch: {batch_idx}") - - batch_results = [] - for batch_task_idx, (walker_state, task) in enumerate(batch): - - task_idx = batch_idx + batch_task_idx - # for our purposes each element in this batch - # should be associated with a worker. - worker_idx = batch_task_idx - - proc_pool_task = ProcPoolTask(task) - - logger.info(f"Submitting task {task_idx} to worker {worker_idx}") - result = pool.apply_async( - proc_pool_task, - (walker_state,), - # NOTE: this must be provided or in some cases when a - # worker crashes on startup it will hang - error_callback=_force_error, - ) - logger.info(f"Task {task_idx} submitted") - batch_results.append(result) - - logger.info(f"Batch {batch_idx} submitted, awaiting results.") - for batch_task_idx, task_result in enumerate(batch_results): - - task_idx = batch_idx + batch_task_idx - logger.info(f"Awaiting task {task_idx}") - - - try: - real_result = task_result.get() - # TODO: add timeouts and retries - except TimeoutError as exc: - raise exc - except Exception as exc: - raise exc - - results.append(real_result) - - logger.info(f"Retrieved completed results for task: {task_idx}") - - logger.info(f"Batch {batch_idx} completed") - - logger.info(f"Completed all batches, terminating Pool") - - - return results diff --git a/src/wepy_tools/systems/alanine_dipeptide.py b/src/wepy_tools/systems/alanine_dipeptide.py index 08adddb4..50a1e6ba 100644 --- a/src/wepy_tools/systems/alanine_dipeptide.py +++ b/src/wepy_tools/systems/alanine_dipeptide.py @@ -1,5 +1,5 @@ from typing import Any -from importlib.resources import files +import importlib.resources import attrs import numpy as np @@ -8,10 +8,38 @@ import openmm.unit import mdtraj +from wepy.runners.openmm import OpenMMState, OpenMMStateWrapper from wepy.walker import WalkerState from wepy.runners.openmm import OpenMMState from wepy.resampling.distances.distance import Distance -from wepy.util.mdtraj import traj_fields_to_mdtraj +from wepy.util.mdtraj import traj_fields_to_mdtraj, json_to_mdtraj_topology + +class AlanineDipeptideExplicitSystem: + + system: openmm.System + topology: openmm.app.Topology + mdtraj_top: mdtraj.Topology + json_top: str + state_wrapper: OpenMMStateWrapper + state: OpenMMState + + def __init__(self) -> None: + + # load the system and state information for the simulation + ala_files = importlib.resources.files("wepy_tools.systems.data.alanine_dipeptide_explicit") + system_xml_path = ala_files / "alanine-dipeptide-explicit.system.omm.xml" + state_xml_path = ala_files / "alanine-dipeptide-explicit.state.omm.xml" + top_json_path = ala_files / "alanine-dipeptide-explicit.top.json" + + self.system = openmm.XmlSerializer.deserialize(system_xml_path.read_text()) + + self.state_wrapper = OpenMMStateWrapper.from_xml(state_xml_path.read_text()) + self.state = OpenMMState.from_state_wrapper(self.state_wrapper) + + self.json_top = top_json_path.read_text() + self.mdj_top = json_to_mdtraj_topology(self.json_top) + self.topology = self.mdj_top.to_openmm() + @attrs.define class AlanineDipeptideRamachandranDistanceImage(WalkerState): diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index 503d60b1..9b29fc32 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -23,7 +23,7 @@ from wepy.util.mdtraj import json_to_mdtraj_topology from wepy_tools.systems.lennard_jones import LennardJonesPair, PairDistance -from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideRamachandranDistance +from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideRamachandranDistance, AlanineDipeptideExplicitSystem def test_lennard_jones_revo_procpool(): @@ -116,20 +116,7 @@ def test_alanine_dipeptide_revo_procpool(): TEMPERATURE = 300. * openmm.unit.kelvin - # load the system and state information for the simulation - ala_files = importlib.resources.files("wepy_tools.systems.data.alanine_dipeptide_explicit") - system_xml_path = ala_files / "alanine-dipeptide-explicit.system.omm.xml" - state_xml_path = ala_files / "alanine-dipeptide-explicit.state.omm.xml" - top_json_path = ala_files / "alanine-dipeptide-explicit.top.json" - - system = openmm.XmlSerializer.deserialize(system_xml_path.read_text()) - - init_state_wrapper = OpenMMStateWrapper.from_xml(state_xml_path.read_text()) - init_state = OpenMMState.from_state_wrapper(init_state_wrapper) - - json_top_str = top_json_path.read_text() - mdj_top = json_to_mdtraj_topology(json_top_str) - topology = mdj_top.to_openmm() + ala_sys = AlanineDipeptideExplicitSystem() integrator = openmm.LangevinIntegrator(TEMPERATURE, 0.1, 0.002) @@ -138,11 +125,11 @@ def test_alanine_dipeptide_revo_procpool(): 1. * openmm.unit.atmosphere, TEMPERATURE, ) - system.addForce(barostat) + ala_sys.system.addForce(barostat) runner_factory = OpenMMRunnerFactory( - system=system, - topology=topology, + system=ala_sys.system, + topology=ala_sys.topology, integrator=integrator, # For this test we do want heart beat at shorter interval openmm_reporter_factories=[ @@ -156,7 +143,7 @@ def test_alanine_dipeptide_revo_procpool(): # TODO: remove the need to deepcopy and have the components make # their own copies if necessary walker_states = [ - copy.deepcopy(init_state) + copy.deepcopy(ala_sys.state) for _ in range(num_walkers) ] @@ -181,7 +168,7 @@ def test_alanine_dipeptide_revo_procpool(): num_workers = num_walkers cores_per_worker = (num_workers // num_walkers) - distance_metric = AlanineDipeptideRamachandranDistance(json_top_str) + distance_metric = AlanineDipeptideRamachandranDistance(ala_sys.json_top) resampler = REVOResampler( merge_dist=4, diff --git a/tests/unit/test_runners/test_openmm/test_logger.py b/tests/unit/test_runners/test_openmm/test_logger.py index 128d5930..0e07857e 100644 --- a/tests/unit/test_runners/test_openmm/test_logger.py +++ b/tests/unit/test_runners/test_openmm/test_logger.py @@ -1,3 +1,4 @@ +import time import logging import copy import pytest @@ -89,6 +90,7 @@ def hello_log( callback=hello_log, state_includes=state_includes, step_interval=10, + start_time=time.time(), ) @@ -160,6 +162,7 @@ def hello_log( callback=hello_log, state_includes=state_includes, step_interval=10, + start_time=time.time(), ) simulation.reporters.append(step_logger) @@ -205,6 +208,7 @@ def hello_log( state_includes=state_includes, step_size=STEP_TIME, sampling_time_interval=(10 * openmm.unit.femtosecond), + start_time=time.time(), ) simulation = openmm.app.Simulation( @@ -215,7 +219,6 @@ def hello_log( ) simulation.context.setState(omm_state) - # at step 0 returns the interval assert step_logger.describeNextReport( simulation ) == OpenMMReporterNextReport( @@ -310,7 +313,7 @@ def hello_log( state: openmm.State, ) -> None: - logger.info("Hello") + logger.info(f"Step: {state.getStepCount()}") state_includes = ["energy"] @@ -320,6 +323,7 @@ def hello_log( state_includes=state_includes, step_size=STEP_TIME, sampling_time_interval=(10 * openmm.unit.femtosecond), + start_time=time.time(), ) simulation = openmm.app.Simulation( @@ -335,14 +339,16 @@ def hello_log( simulation.step(1) assert len(caplog.records) == 0 + logger.info("Next test") caplog.clear() with caplog.at_level(logging.INFO, logger_name): simulation.step(9) assert len(caplog.records) == 1 - assert caplog.records[0].msg == "Hello" + assert caplog.records[0].msg == "Step: 10" + logger.info("Next test") caplog.clear() # test something longer @@ -352,6 +358,7 @@ def hello_log( state_includes=state_includes, step_size=STEP_TIME, sampling_time_interval=(2 * openmm.unit.femtosecond), + start_time=time.time(), ) simulation = openmm.app.Simulation( @@ -364,7 +371,12 @@ def hello_log( simulation.reporters.append(time_logger) with caplog.at_level(logging.INFO, logger_name): - simulation.step(10) + simulation.step(2) + assert len(caplog.records) == 1 + simulation.step(2) + simulation.step(2) + simulation.step(2) + simulation.step(2) assert len(caplog.records) == 5 caplog.clear() @@ -383,6 +395,7 @@ def test_logging_callback(self, sim_components, caplog): time_logger = HeartBeatLoggingReporter( logger, step_interval=2, + start_time=time.time(), ) simulation = openmm.app.Simulation( @@ -393,15 +406,19 @@ def test_logging_callback(self, sim_components, caplog): ) simulation.context.setState(omm_state) + reporter = HeartBeatLoggingReporter( + logger, + step_interval=2, + start_time=time.time(), + ) with caplog.at_level(logging.INFO, logger_name): - HeartBeatLoggingReporter.logging_callback( + reporter.logging_callback( logger, simulation, omm_state, ) assert len(caplog.records) == 1 - def test_simulation(self, sim_components, caplog): @@ -415,6 +432,7 @@ def test_simulation(self, sim_components, caplog): time_logger = HeartBeatLoggingReporter( logger, step_interval=2, + start_time=time.time(), ) simulation = openmm.app.Simulation( diff --git a/tests/unit/test_runners/test_openmm/test_runner.py b/tests/unit/test_runners/test_openmm/test_runner.py index 2299101d..8a7404d7 100644 --- a/tests/unit/test_runners/test_openmm/test_runner.py +++ b/tests/unit/test_runners/test_openmm/test_runner.py @@ -172,7 +172,6 @@ def test_run_segment(self, runner_components): system=copy.deepcopy(system), topology=copy.deepcopy(topology), integrator=copy.deepcopy(integrator), - platform_name="Reference", ) with pytest.raises(RunnerStateError): @@ -241,7 +240,6 @@ def test_post_cycle(self, runner_components): system=copy.deepcopy(system), topology=copy.deepcopy(topology), integrator=copy.deepcopy(integrator), - platform_name="Reference", ) with pytest.raises(RunnerStateTransitionError): @@ -266,7 +264,6 @@ def test_post_cycle(self, runner_components): system=copy.deepcopy(system), topology=copy.deepcopy(topology), integrator=copy.deepcopy(integrator), - platform_name="Reference", ) with pytest.raises(RunnerStateTransitionError): @@ -298,4 +295,4 @@ def test_OpenMMRunnerFactory(runner_components): integrator=integrator, ) - assert isinstance(omm_factory, OpenMMRunner) + assert isinstance(omm_factory(), OpenMMRunner) diff --git a/tests/unit/test_util/test_multiprocessing.py b/tests/unit/test_util/test_multiprocessing.py index 06e801a3..971df11e 100644 --- a/tests/unit/test_util/test_multiprocessing.py +++ b/tests/unit/test_util/test_multiprocessing.py @@ -9,13 +9,14 @@ def test__dummy_task(): def test_proc_pool_worker_setup(caplog): - log_queue = mp.Queue() + mp_ctx = mp.get_context(method="spawn") + + log_queue = mp_ctx.Queue() handlers = list(logging.getLogger().handlers) listener = logging.handlers.QueueListener(log_queue, *handlers) listener.start() - - with mp.Pool( + with mp_ctx.Pool( processes=1, initializer=proc_pool_worker_setup, initargs=(log_queue,), diff --git a/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py b/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py index d41a9296..2bb7dbbd 100644 --- a/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py +++ b/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py @@ -5,62 +5,44 @@ from wepy.util.mdtraj import mdtraj_to_json_topology from wepy.runners.openmm import OpenMMState -from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideExplicit, AlanineDipeptideRamachandranDistance +from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideExplicitSystem, AlanineDipeptideRamachandranDistance -def test_AlanineDipeptideExplicit(): +def test_AlanineDipeptideExplicitSystem(): - AlanineDipeptideExplicit() + AlanineDipeptideExplicitSystem() class Test_AlanineDipeptideRamachandranDistance: def test_image(self): - ala_sys = AlanineDipeptideExplicit() + ala_sys = AlanineDipeptideExplicitSystem() - json_top = mdtraj_to_json_topology( - mdtraj.Topology.from_openmm(ala_sys.topology) - ) + distance = AlanineDipeptideRamachandranDistance(topology=ala_sys.json_top) - distance = AlanineDipeptideRamachandranDistance(topology=json_top) - - state = OpenMMState.from_dwim( - positions=ala_sys.positions, - box_vectors=ala_sys.box_vectors, - ) - - image = distance.image(state) + image = distance.image(ala_sys.state) def test_image_distance(self): - ala_sys = AlanineDipeptideExplicit() + ala_sys = AlanineDipeptideExplicitSystem() - json_top = mdtraj_to_json_topology( - mdtraj.Topology.from_openmm(ala_sys.topology) - ) - - distance = AlanineDipeptideRamachandranDistance(topology=json_top) - - state = OpenMMState.from_dwim( - positions=ala_sys.positions, - box_vectors=ala_sys.box_vectors, - ) + distance = AlanineDipeptideRamachandranDistance(topology=ala_sys.json_top) - image_a = distance.image(state) + image_a = distance.image(ala_sys.state) assert np.isclose(distance.image_distance(image_a, image_a), 0.) # then make a jittered atom positions to get something a little # different to compare - jitter_positions = ala_sys.positions + np.random.uniform( + jitter_positions = ala_sys.state.positions + np.random.uniform( -0.01, 0.01, - size=ala_sys.positions.shape, - ) * ala_sys.positions.unit + size=ala_sys.state.positions.shape, + ) * ala_sys.state.positions.unit jitter_state = OpenMMState.from_dwim( positions=jitter_positions, - box_vectors=ala_sys.box_vectors, + box_vectors=ala_sys.state.box_vectors, ) jitter_image = distance.image(jitter_state) diff --git a/tests/unit/test_work_mapper/test_openmm.py b/tests/unit/test_work_mapper/test_openmm.py index d0594d5c..e0eca1c6 100644 --- a/tests/unit/test_work_mapper/test_openmm.py +++ b/tests/unit/test_work_mapper/test_openmm.py @@ -1,13 +1,10 @@ from wepy.runners.openmm import ( OpenMMRunner, OpenMMState, - gen_sim_state, ) -from wepy.work_mapper.openmm import ( - OpenMMTask, - OpenMMSerialWorkMapper, +from wepy.work_mapper.openmm.proc_pool import ( OpenMMProcPoolWorkMapper, - OpenMMRayPoolWorkMapper, + OpenMMProcPoolWorkMapperFactory, ) import pytest @@ -37,129 +34,80 @@ def openmm_runner() -> OpenMMRunner: def openmm_state() -> OpenMMState: lj_sys = LennardJonesPair() - - state = OpenMMState(gen_sim_state( - lj_sys.positions, - system=lj_sys.system, - integrator=openmm.VerletIntegrator(0.002), - )) - return state - - - -class TestOpenMMTask: - - def test___call__(self, openmm_runner, openmm_state): - - task = OpenMMTask( - runner=openmm_runner, - segment_length=2, - ) - - new_state = task(openmm_state) - -class TestOpenMMSerialWorkMapper: - - def test_all(self, openmm_runner): - - lj_sys = LennardJonesPair() + state = OpenMMState.from_dwim( + positions=lj_sys.positions, + ) - init_states = [ - OpenMMState(gen_sim_state( - lj_sys.positions, - system=lj_sys.system, - integrator=openmm.VerletIntegrator(1), - )) - for _ - in range(4) - ] + return state - mapper = OpenMMSerialWorkMapper( - platform="CPU", - global_platform_properties={"Threads" : "2"}, - ) - mapper.init() - new_states = mapper.map( - [ - OpenMMTask( - runner=openmm_runner, - segment_length=2, - ) - for _ in range(len(init_states)) - ], - init_states, - ) - -class TestOpenMMProcPoolWorkMapper: +class Test_OpenMMProcPoolWorkMapper: def test_all(self, openmm_runner): lj_sys = LennardJonesPair() init_states = [ - OpenMMState(gen_sim_state( - lj_sys.positions, - system=lj_sys.system, - integrator=openmm.VerletIntegrator(1), - )) + OpenMMState.from_dwim( + positions=lj_sys.positions, + ) for _ in range(4) ] mapper = OpenMMProcPoolWorkMapper( platform="CPU", + num_procs=2, device_ids=[0,1], global_platform_properties={"Threads" : "1"}, ) - mapper.init() + + openmm_runner.init() + openmm_runner.pre_cycle() + new_states = mapper.map( + openmm_runner.run_segment, + init_states, [ - OpenMMTask( - runner=openmm_runner, - segment_length=2, - ) + 10 for _ in range(len(init_states)) ], - init_states, ) -class TestOpenMMRayPoolWorkMapper: +# class TestOpenMMRayPoolWorkMapper: - @pytest.mark.ray - def test_all(self, openmm_runner): +# @pytest.mark.ray +# def test_all(self, openmm_runner): - lj_sys = LennardJonesPair() +# lj_sys = LennardJonesPair() - init_states = [ - OpenMMState(gen_sim_state( - lj_sys.positions, - system=lj_sys.system, - integrator=openmm.VerletIntegrator(1), - )) - for _ - in range(4) - ] - - mapper = OpenMMRayPoolWorkMapper( - platform="CPU", - device_ids=[0,1], - global_platform_properties={"Threads" : "1"}, - # ray_init_args={ +# init_states = [ +# OpenMMState.from_dwim( +# positions=lj_sys.positions, +# ) +# for _ +# in range(4) +# ] + +# mapper = OpenMMRayPoolWorkMapper( +# platform="CPU", +# device_ids=[0,1], +# global_platform_properties={"Threads" : "1"}, +# # ray_init_args={ - # } - ) - - mapper.init() - new_states = mapper.map( - [ - OpenMMTask( - runner=openmm_runner, - segment_length=100000, - ) - for _ in range(len(init_states)) - ], - init_states, - ) +# # } +# ) + +# mapper.init() +# new_states = mapper.map( +# [ +# OpenMMTask( +# runner=openmm_runner, +# segment_length=100000, +# ) +# for _ in range(len(init_states)) +# ], +# init_states, +# ) diff --git a/tests/unit/test_work_mapper/test_proc_pool_mapper.py b/tests/unit/test_work_mapper/test_proc_pool_mapper.py deleted file mode 100644 index 3f5bdc34..00000000 --- a/tests/unit/test_work_mapper/test_proc_pool_mapper.py +++ /dev/null @@ -1,37 +0,0 @@ - -from wepy.interface import ( - WorkMapperFactoryArgs, -) -from wepy.work_mapper.proc_pool_mapper import ProcPoolMapper -# NOTE: that you must import from a module (as opposed to inline in -# the test file) or you have problems with importing modules when -# using "spawn" process start method -from wepy.runners.mock import MockState, MockError, MockRunner, MockTask - -def test_ProcPoolMapper(): - - poolmapper = ProcPoolMapper(WorkMapperFactoryArgs(num_workers=2)) - - poolmapper.init() - - results = poolmapper.map( - [ - MockTask( - MockRunner(), - segment_length=10, - fail=False, - ) - for _ in range(3) - ], - [ - MockState(0), - MockState(1), - MockState(2), - ], - ) - - assert results == [ - MockState(10), - MockState(11), - MockState(12), - ] From b4ebb032e64b91ae8c65337e818fdfb861c55eb4 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 9 Dec 2025 16:58:39 -0500 Subject: [PATCH 073/143] implement energy and unitcell logging reporters --- src/wepy/runners/openmm/logger.py | 84 +++++++++- .../test_runners/test_openmm/test_logger.py | 153 +++++++++++++++++- 2 files changed, 226 insertions(+), 11 deletions(-) diff --git a/src/wepy/runners/openmm/logger.py b/src/wepy/runners/openmm/logger.py index d92d6727..e81f4d4f 100644 --- a/src/wepy/runners/openmm/logger.py +++ b/src/wepy/runners/openmm/logger.py @@ -6,6 +6,7 @@ import openmm import openmm.unit import attrs +from wepy.util.openmm import format_box_vectors_line from .reporter import ( OpenMMReporter, @@ -194,9 +195,6 @@ def logging_callback( f"OpenMM simulation progress: clock_time={current_time:.4f} s, elapsed_time={elapsed_time:.4f} s, sim_time={sim_time_mag:.4f} ps, sim_steps={sim_steps}", ) - - - @attrs.define class HeartBeatLoggingReporterFactory: @@ -213,5 +211,81 @@ def __call__( step_interval=self.step_interval, start_time=start_time, ) -# TODO: -# class EnergyLoggingReporter() + +class EnergyLoggingReporter(SamplingTimeIntervalLoggingReporter): + + def __init__( + self, + logger: logging.Logger, + step_size: openmm.unit.Quantity, + sampling_time_interval: openmm.unit.Quantity, + start_time: int, + ) -> None: + + super().__init__( + logger=logger, + callback=self.logging_callback, + state_includes=["energy"], + sampling_time_interval=sampling_time_interval, + step_size=step_size, + start_time=start_time, + ) + + def logging_callback( + self, + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State + ) -> None: + + sim_time = simulation.context.getTime() + sim_time_mag = sim_time.value_in_unit(openmm.unit.picosecond) + sim_steps = simulation.context.getStepCount() + + _pot_e = state.getPotentialEnergy() + _kin_e = state.getKineticEnergy() + _tot_e = _pot_e + _kin_e + + logger.info( + f"OpenMM simulation energy (steps={sim_steps}, sim_time={sim_time_mag:.4f} ps): " + f"kinetic={_kin_e}, potential={_pot_e}, total={_tot_e}" + ) + +class UnitCellLoggingReporter(SamplingTimeIntervalLoggingReporter): + + def __init__( + self, + logger: logging.Logger, + step_size: openmm.unit.Quantity, + sampling_time_interval: openmm.unit.Quantity, + start_time: int, + ) -> None: + + super().__init__( + logger=logger, + callback=self.logging_callback, + state_includes=[], + sampling_time_interval=sampling_time_interval, + step_size=step_size, + start_time=start_time, + ) + + def logging_callback( + self, + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State + ) -> None: + + sim_time = simulation.context.getTime() + sim_time_mag = sim_time.value_in_unit(openmm.unit.picosecond) + sim_steps = simulation.context.getStepCount() + + _box_volume = state.getPeriodicBoxVolume() + _bvs = state.getPeriodicBoxVectors() + _bvs_line = format_box_vectors_line(_bvs) + + logger.info( + f"OpenMM simulation unitcell (steps={sim_steps}, sim_time={sim_time_mag:.4f} ps): " + f"volume={_box_volume}, vectors={_bvs_line}" + ) diff --git a/tests/unit/test_runners/test_openmm/test_logger.py b/tests/unit/test_runners/test_openmm/test_logger.py index 0e07857e..985cd6d9 100644 --- a/tests/unit/test_runners/test_openmm/test_logger.py +++ b/tests/unit/test_runners/test_openmm/test_logger.py @@ -12,6 +12,8 @@ StepIntervalLoggingReporter, SamplingTimeIntervalLoggingReporter, HeartBeatLoggingReporter, + EnergyLoggingReporter, + UnitCellLoggingReporter, ) from wepy_tools.systems.lennard_jones import LennardJonesPair @@ -392,12 +394,6 @@ def test_logging_callback(self, sim_components, caplog): logger_name = "test-HeartBeatLoggingReporter" logger = logging.getLogger(logger_name) - time_logger = HeartBeatLoggingReporter( - logger, - step_interval=2, - start_time=time.time(), - ) - simulation = openmm.app.Simulation( topology, system, @@ -461,3 +457,148 @@ def test_simulation(self, sim_components, caplog): assert len(caplog.records) == 5 caplog.clear() + +class Test_EnergyLoggingReporter: + + def test_logging_callback(self, sim_components, caplog): + + omm_state, sim_args = sim_components[0], sim_components[1:] + + topology, system, integrator, platform = sim_args + + logger_name = "test-EnergyLoggingReporter" + logger = logging.getLogger(logger_name) + + simulation = openmm.app.Simulation( + topology, + system, + copy.deepcopy(integrator), + platform, + ) + simulation.context.setState(omm_state) + # do a step to compute energies + simulation.context.getIntegrator().step(1) + _omm_state = simulation.context.getState(energy=True) + + reporter = EnergyLoggingReporter( + logger, + sampling_time_interval=(1 * openmm.unit.femtosecond), + step_size=STEP_TIME, + start_time=time.time(), + ) + with caplog.at_level(logging.INFO, logger_name): + reporter.logging_callback( + logger, + simulation, + _omm_state, + ) + + assert len(caplog.records) == 1 + + def test_simulation(self, sim_components, caplog): + omm_state, sim_args = sim_components[0], sim_components[1:] + + topology, system, integrator, platform = sim_args + + logger_name = "test-EnergyLoggingReporter" + logger = logging.getLogger(logger_name) + + energy_logger = EnergyLoggingReporter( + logger, + sampling_time_interval=(STEP_TIME * 2), + step_size=STEP_TIME, + start_time=time.time(), + ) + + simulation = openmm.app.Simulation( + topology, + system, + copy.deepcopy(integrator), + platform, + ) + simulation.context.setState(omm_state) + simulation.reporters.append(energy_logger) + + with caplog.at_level(logging.INFO, logger_name): + simulation.step(2) + assert len(caplog.records) == 1 + simulation.step(2) + simulation.step(2) + simulation.step(2) + simulation.step(2) + + assert len(caplog.records) == 5 + caplog.clear() + +class Test_UnitCellLoggingReporter: + + def test_logging_callback(self, sim_components, caplog): + + omm_state, sim_args = sim_components[0], sim_components[1:] + + topology, system, integrator, platform = sim_args + + logger_name = "test-UnitCellLoggingReporter" + logger = logging.getLogger(logger_name) + + simulation = openmm.app.Simulation( + topology, + system, + copy.deepcopy(integrator), + platform, + ) + simulation.context.setState(omm_state) + # do a step to compute energies + simulation.context.getIntegrator().step(1) + _omm_state = simulation.context.getState(energy=True) + + reporter = UnitCellLoggingReporter( + logger, + sampling_time_interval=(1 * openmm.unit.femtosecond), + step_size=STEP_TIME, + start_time=time.time(), + ) + with caplog.at_level(logging.INFO, logger_name): + reporter.logging_callback( + logger, + simulation, + _omm_state, + ) + + assert len(caplog.records) == 1 + + def test_simulation(self, sim_components, caplog): + omm_state, sim_args = sim_components[0], sim_components[1:] + + topology, system, integrator, platform = sim_args + + logger_name = "test-UnitCellLoggingReporter" + logger = logging.getLogger(logger_name) + + energy_logger = UnitCellLoggingReporter( + logger, + sampling_time_interval=(STEP_TIME * 2), + step_size=STEP_TIME, + start_time=time.time(), + ) + + simulation = openmm.app.Simulation( + topology, + system, + copy.deepcopy(integrator), + platform, + ) + simulation.context.setState(omm_state) + simulation.reporters.append(energy_logger) + + with caplog.at_level(logging.INFO, logger_name): + simulation.step(2) + assert len(caplog.records) == 1 + simulation.step(2) + simulation.step(2) + simulation.step(2) + simulation.step(2) + + assert len(caplog.records) == 5 + caplog.clear() + From 00e234ac1b3a3a7f14c14475faf0ea48f77a31bf Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 9 Dec 2025 17:05:31 -0500 Subject: [PATCH 074/143] removes step_size from logging reporter inputs Can be gotten from the integrator directly dynamically --- src/wepy/runners/openmm/__init__.py | 4 +- src/wepy/runners/openmm/logger.py | 47 +++++++++++++++---- .../test_runners/test_openmm/test_logger.py | 7 --- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/wepy/runners/openmm/__init__.py b/src/wepy/runners/openmm/__init__.py index 3f5a8c80..ace04c7d 100644 --- a/src/wepy/runners/openmm/__init__.py +++ b/src/wepy/runners/openmm/__init__.py @@ -9,7 +9,7 @@ ) from .runner import ( OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS, OpenMMRunnerFactory) -from .logger import HeartBeatLoggingReporterFactory +from .logger import HeartBeatLoggingReporterFactory, UnitCellLoggingReporterFactory, EnergyLoggingReporterFactory __all__ = [ "HeartBeatLoggingReporterFactory", @@ -24,4 +24,6 @@ "get_context_state", "state_to_xml", "PlatformKwargs", + "UnitCellLoggingReporterFactory", + "EnergyLoggingReporterFactory", ] diff --git a/src/wepy/runners/openmm/logger.py b/src/wepy/runners/openmm/logger.py index e81f4d4f..90529ea2 100644 --- a/src/wepy/runners/openmm/logger.py +++ b/src/wepy/runners/openmm/logger.py @@ -109,7 +109,6 @@ class SamplingTimeIntervalLoggingReporter(LoggingReporter): logger: logging.Logger callback: LoggingReporterCallback state_includes: list[OpenMMGetStateKeys] - step_size: openmm.unit.Quantity sampling_time_interval: openmm.unit.Quantity def __init__( @@ -117,14 +116,12 @@ def __init__( logger: logging.Logger, callback: LoggingReporterCallback, state_includes: Collection[OpenMMGetStateKeys], - step_size: openmm.unit.Quantity, sampling_time_interval: openmm.unit.Quantity, start_time: int, ) -> None: super().__init__(logger, callback, state_includes) self.sampling_time_interval = sampling_time_interval - self.step_size = step_size self.start_time = start_time def describeNextReport( @@ -136,6 +133,9 @@ def describeNextReport( curr_sampling_time: openmm.unit.Quantity = simulation.context.getTime() + # get the step size from the integrator + step_size = simulation.context.getIntegrator().getStepSize() + sampling_time_left = ( self.sampling_time_interval.value_in_unit(_unit) - ( curr_sampling_time.value_in_unit(_unit) @@ -148,7 +148,7 @@ def describeNextReport( else: estimated_steps_left = ( - round(sampling_time_left.value_in_unit(_unit)) // round(self.step_size.value_in_unit(_unit)) + round(sampling_time_left.value_in_unit(_unit)) // round(step_size.value_in_unit(_unit)) ) return OpenMMReporterNextReport( @@ -217,7 +217,6 @@ class EnergyLoggingReporter(SamplingTimeIntervalLoggingReporter): def __init__( self, logger: logging.Logger, - step_size: openmm.unit.Quantity, sampling_time_interval: openmm.unit.Quantity, start_time: int, ) -> None: @@ -227,7 +226,6 @@ def __init__( callback=self.logging_callback, state_includes=["energy"], sampling_time_interval=sampling_time_interval, - step_size=step_size, start_time=start_time, ) @@ -251,12 +249,29 @@ def logging_callback( f"kinetic={_kin_e}, potential={_pot_e}, total={_tot_e}" ) +@attrs.define +class EnergyLoggingReporterFactory: + + sampling_time_interval: int + + def __call__( + self, + logger: logging.Logger, + start_time: int, + ) -> EnergyLoggingReporter: + + return EnergyLoggingReporter( + logger=logger, + sampling_time_interval=self.sampling_time_interval, + start_time=start_time, + ) + + class UnitCellLoggingReporter(SamplingTimeIntervalLoggingReporter): def __init__( self, logger: logging.Logger, - step_size: openmm.unit.Quantity, sampling_time_interval: openmm.unit.Quantity, start_time: int, ) -> None: @@ -266,7 +281,6 @@ def __init__( callback=self.logging_callback, state_includes=[], sampling_time_interval=sampling_time_interval, - step_size=step_size, start_time=start_time, ) @@ -289,3 +303,20 @@ def logging_callback( f"OpenMM simulation unitcell (steps={sim_steps}, sim_time={sim_time_mag:.4f} ps): " f"volume={_box_volume}, vectors={_bvs_line}" ) + +@attrs.define +class UnitCellLoggingReporterFactory: + + sampling_time_interval: int + + def __call__( + self, + logger: logging.Logger, + start_time: int, + ) -> EnergyLoggingReporter: + + return UnitCellLoggingReporter( + logger=logger, + sampling_time_interval=self.sampling_time_interval, + start_time=start_time, + ) diff --git a/tests/unit/test_runners/test_openmm/test_logger.py b/tests/unit/test_runners/test_openmm/test_logger.py index 985cd6d9..7ce78caf 100644 --- a/tests/unit/test_runners/test_openmm/test_logger.py +++ b/tests/unit/test_runners/test_openmm/test_logger.py @@ -208,7 +208,6 @@ def hello_log( logger, callback=hello_log, state_includes=state_includes, - step_size=STEP_TIME, sampling_time_interval=(10 * openmm.unit.femtosecond), start_time=time.time(), ) @@ -323,7 +322,6 @@ def hello_log( logger, callback=hello_log, state_includes=state_includes, - step_size=STEP_TIME, sampling_time_interval=(10 * openmm.unit.femtosecond), start_time=time.time(), ) @@ -358,7 +356,6 @@ def hello_log( logger, callback=hello_log, state_includes=state_includes, - step_size=STEP_TIME, sampling_time_interval=(2 * openmm.unit.femtosecond), start_time=time.time(), ) @@ -483,7 +480,6 @@ def test_logging_callback(self, sim_components, caplog): reporter = EnergyLoggingReporter( logger, sampling_time_interval=(1 * openmm.unit.femtosecond), - step_size=STEP_TIME, start_time=time.time(), ) with caplog.at_level(logging.INFO, logger_name): @@ -506,7 +502,6 @@ def test_simulation(self, sim_components, caplog): energy_logger = EnergyLoggingReporter( logger, sampling_time_interval=(STEP_TIME * 2), - step_size=STEP_TIME, start_time=time.time(), ) @@ -555,7 +550,6 @@ def test_logging_callback(self, sim_components, caplog): reporter = UnitCellLoggingReporter( logger, sampling_time_interval=(1 * openmm.unit.femtosecond), - step_size=STEP_TIME, start_time=time.time(), ) with caplog.at_level(logging.INFO, logger_name): @@ -578,7 +572,6 @@ def test_simulation(self, sim_components, caplog): energy_logger = UnitCellLoggingReporter( logger, sampling_time_interval=(STEP_TIME * 2), - step_size=STEP_TIME, start_time=time.time(), ) From 768699fef82747baf876483941ba7c2d1bbc42de Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 9 Dec 2025 17:21:18 -0500 Subject: [PATCH 075/143] add new logging reporters to default OpenMMRunnerFactory configuration --- src/wepy/runners/openmm/runner.py | 16 ++++++--- .../test_runners/test_openmm/test_runner.py | 34 ++++++++++++++++++- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index 6e18e051..b593a01a 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -37,7 +37,7 @@ from wepy.util.openmm import triclinic_volume_vec3_quantity, format_box_vectors_line from wepy.walker import WalkerState from .state import OpenMMState, OpenMMStateWrapper, get_context_state -from .logger import HeartBeatLoggingReporterFactory, LoggingReporterFactory +from .logger import HeartBeatLoggingReporterFactory, LoggingReporterFactory, EnergyLoggingReporterFactory, UnitCellLoggingReporterFactory PlatformKwargs = dict[str, str] @@ -59,9 +59,14 @@ "box_volume", }) +# default heart beat every 500 steps, should be around 0.5 - 1 picoseconds +_DEFAULT_HEARTBEAT_INTERVAL = 500 +_DEFAULT_STATE_TIME_INTERVAL = (10 * openmm.unit.picosecond) + DEFAULT_OPENMM_REPORTER_FACTORIES = [ - # default heart beat every 500 steps - HeartBeatLoggingReporterFactory(step_interval=500), + HeartBeatLoggingReporterFactory(step_interval=_DEFAULT_HEARTBEAT_INTERVAL), + UnitCellLoggingReporterFactory(sampling_time_interval=_DEFAULT_STATE_TIME_INTERVAL), + EnergyLoggingReporterFactory(sampling_time_interval=_DEFAULT_STATE_TIME_INTERVAL), ] @attrs.define @@ -167,7 +172,8 @@ def __init__( self.enforce_box = enforce_box self.get_state_keys = get_state_keys - logger.warning("No OpenMM reporter factories configured.") + if openmm_reporter_factories is None or len(openmm_reporter_factories) == 0: + logger.warning("No OpenMM reporter factories configured.") self.openmm_reporter_factories = openmm_reporter_factories if openmm_reporter_factories is not None else [] self._openmm_reporters = None @@ -182,7 +188,7 @@ def _report_configuration(self) -> None: logger.info("Details of OpenMMRunner initial configuration") logger.info( - f"OpenMM logger reporters: {','.join(str(v) for v in self.openmm_reporter_factories)}" + f"OpenMM logging reporters: {', '.join(str(v) for v in self.openmm_reporter_factories)}" ) logger.info(f"Enforce PBCs in getState: {self.enforce_box}") diff --git a/tests/unit/test_runners/test_openmm/test_runner.py b/tests/unit/test_runners/test_openmm/test_runner.py index 8a7404d7..38e19aae 100644 --- a/tests/unit/test_runners/test_openmm/test_runner.py +++ b/tests/unit/test_runners/test_openmm/test_runner.py @@ -17,6 +17,8 @@ OpenMMRunner, OpenMMRunnerSegmentData, OpenMMRunnerFactory, + _DEFAULT_HEARTBEAT_INTERVAL, + _DEFAULT_STATE_TIME_INTERVAL, ) from wepy.runners.openmm.logger import StepIntervalLoggingReporter @@ -35,12 +37,14 @@ ] ) +STEP_SIZE = 2 * openmm.unit.femtoseconds + @pytest.fixture def runner_components() -> tuple[openmm.System, openmm.app.Topology, openmm.LangevinIntegrator]: lj_sys = LennardJonesPair() - integrator = openmm.LangevinIntegrator(300.0, 0.1, 0.002) + integrator = openmm.LangevinIntegrator(300.0, 0.1, STEP_SIZE) return lj_sys.system, lj_sys.topology, integrator @@ -296,3 +300,31 @@ def test_OpenMMRunnerFactory(runner_components): ) assert isinstance(omm_factory(), OpenMMRunner) + + +def test_defaults(runner_components): + """A test that exercises the default settings of the logging reporters.""" + + system, topology, integrator = runner_components + + lj_sys = LennardJonesPair() + + state = OpenMMState.from_dwim(positions=lj_sys.positions) + + runner = OpenMMRunnerFactory( + system=system, + topology=topology, + integrator=integrator, + )() + + time_interval_steps = _DEFAULT_STATE_TIME_INTERVAL / STEP_SIZE + + _steps = ( + time_interval_steps + if time_interval_steps > _DEFAULT_HEARTBEAT_INTERVAL + else _DEFAULT_HEARTBEAT_INTERVAL + ) + + runner.init() + runner.pre_cycle() + new_state, segment_data = runner.run_segment(state, 2 * _steps) From 3d1fbd63ec6fb08f83f38313ad33681a870f5eee Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 11:56:43 -0500 Subject: [PATCH 076/143] update and tests for all decisions --- src/wepy/resampling/decisions/clone_merge.py | 51 ++++++++++-- src/wepy/resampling/decisions/decision.py | 83 ++++++------------- src/wepy/resampling/decisions/no_decision.py | 25 ++++-- .../test_decisions/test_clone_merge.py | 81 ++++++++++-------- .../test_decisions/test_decision.py | 80 ++++++++++++++++++ .../test_decisions/test_no_decision.py | 54 +++++++++--- 6 files changed, 256 insertions(+), 118 deletions(-) create mode 100644 tests/unit/test_resampling/test_decisions/test_decision.py diff --git a/src/wepy/resampling/decisions/clone_merge.py b/src/wepy/resampling/decisions/clone_merge.py index d14b43b4..a1fed170 100644 --- a/src/wepy/resampling/decisions/clone_merge.py +++ b/src/wepy/resampling/decisions/clone_merge.py @@ -5,9 +5,10 @@ # Standard Library from collections import defaultdict from enum import IntEnum +import attrs # First Party Library -from wepy.resampling.decisions.decision import Decision +from wepy.resampling.decisions.decision import Decision, DecisionRecord from wepy.walker import keep_merge, split, Walker logger = logging.getLogger(__name__) @@ -39,8 +40,8 @@ class CloneMergeDecisionEnum(IntEnum): """Do nothing with the sample value (state) but squashed walkers will donate their weight to it.""" - -class CloneMergeDecisionRecord(TypedDict): +@attrs.define +class CloneMergeDecisionRecord(DecisionRecord): decision_id: int target_idxs: list[int] @@ -85,7 +86,9 @@ class MultiCloneMergeDecision(Decision): @classmethod def action( - cls, walkers: list[Walker], decisions: list[CloneMergeDecisionRecord] + cls, + walkers: list[Walker], + decisions: list[CloneMergeDecisionRecord], ) -> list[Walker]: # list for the modified walkers mod_walkers = [None for i in range(len(walkers))] @@ -99,8 +102,8 @@ def action( # go through each decision and perform the decision # instructions for walker_idx, walker_rec in enumerate(step_recs): - decision_value = walker_rec["decision_id"] - instruction = walker_rec["target_idxs"] + decision_value = walker_rec.decision_id + instruction = walker_rec.target_idxs if decision_value == cls.ENUM.NOTHING.value: # check to make sure a walker doesn't already exist @@ -181,3 +184,39 @@ def action( raise ValueError("Some walkers were not created") return mod_walkers + + @classmethod + def parents(cls, step: list[CloneMergeDecisionRecord]) -> list[int]: + """Given a step of resampling records (for a single resampling step) + returns the parents of the children of this step. + + Parameters + ---------- + step : list of decision records + The decision records for a step of resampling for each walker. + + Returns + ------- + walker_step_parents : list of int + For each element, the index of it in the list corresponds + to the child index and the value of the element is the + index of it's parent before the decision action. + + """ + + # initialize a list for the parents of this stages walkers + step_parents = [None for i in range(len(step))] + + # the rest of the stages parents are based on the previous stage + for parent_idx, parent_rec in enumerate(step): + # if the decision is an ancestor then the instruction + # values will be the children + if parent_rec.decision_id in cls.ANCESTOR_DECISION_IDS: + # the first value of the parent record is the target + # idxs + child_idxs = parent_rec.target_idxs + for child_idx in child_idxs: + step_parents[child_idx] = parent_idx + + return step_parents + diff --git a/src/wepy/resampling/decisions/decision.py b/src/wepy/resampling/decisions/decision.py index 80f12c7a..98961186 100644 --- a/src/wepy/resampling/decisions/decision.py +++ b/src/wepy/resampling/decisions/decision.py @@ -52,13 +52,14 @@ # Standard Library from enum import IntEnum +import attrs + from wepy.walker import Walker logger = logging.getLogger(__name__) - -class DecisionRecord(TypedDict): - +@attrs.define +class DecisionRecord: decision_id: int @@ -189,34 +190,34 @@ def enum_by_name(cls, enum_name): d = cls.enum_dict_by_name() return d[enum_name] - # @classmethod - # def record(cls, enum_value: int, **fields: dict[str, Any]) -> DecisionRecord: - # """Generate a record for the enum_value and the other fields. + @classmethod + def record(cls, enum_value: int, **fields: dict[str, Any]) -> DecisionRecord: + """Generate a record for the enum_value and the other fields. - # Parameters - # ---------- - # enum_value : int + Parameters + ---------- + enum_value : int - # Returns - # ------- - # rec : dict of str: value + Returns + ------- + rec : dict of str: value - # """ + """ - # assert ( - # enum_value in cls.enum_dict_by_value() - # ), "value is not a valid Enumerated value" + assert ( + enum_value in cls.enum_dict_by_value() + ), "value is not a valid Enumerated value" - # for field_key in fields.keys(): - # assert ( - # field_key in cls.FIELDS - # ), "The field {} is not a field for that decision".format(field_key) - # assert field_key != "decision_id", "'decision_id' cannot be an extra field" + for field_key in fields.keys(): + assert ( + field_key in cls.FIELDS + ), "The field {} is not a field for that decision".format(field_key) + assert field_key != "decision_id", "'decision_id' cannot be an extra field" - # rec = {"decision_id": enum_value} - # rec.update(fields) + rec = {"decision_id": enum_value} + rec.update(fields) - # return rec + return rec @classmethod def action( @@ -259,37 +260,3 @@ def action( """ raise NotImplementedError - @classmethod - def parents(cls, step: list[DecisionRecord]) -> list[int]: - """Given a step of resampling records (for a single resampling step) - returns the parents of the children of this step. - - Parameters - ---------- - step : list of decision records - The decision records for a step of resampling for each walker. - - Returns - ------- - walker_step_parents : list of int - For each element, the index of it in the list corresponds - to the child index and the value of the element is the - index of it's parent before the decision action. - - """ - - # initialize a list for the parents of this stages walkers - step_parents = [None for i in range(len(step))] - - # the rest of the stages parents are based on the previous stage - for parent_idx, parent_rec in enumerate(step): - # if the decision is an ancestor then the instruction - # values will be the children - if parent_rec[0] in cls.ANCESTOR_DECISION_IDS: - # the first value of the parent record is the target - # idxs - child_idxs = parent_rec[1] - for child_idx in child_idxs: - step_parents[child_idx] = parent_idx - - return step_parents diff --git a/src/wepy/resampling/decisions/no_decision.py b/src/wepy/resampling/decisions/no_decision.py index d2ffc24b..ba86cbf7 100644 --- a/src/wepy/resampling/decisions/no_decision.py +++ b/src/wepy/resampling/decisions/no_decision.py @@ -1,6 +1,6 @@ from typing import TypedDict from enum import IntEnum - +import attrs from wepy.walker import Walker from wepy.resampling.decisions.decision import Decision, DecisionRecord @@ -12,9 +12,10 @@ class NothingDecisionEnum(IntEnum): """Do nothing with the walker.""" -class NoDecisionRecord(TypedDict): +@attrs.define +class NoDecisionRecord(DecisionRecord): decision_id: int - target_idxs: list[int] + target_idx: int class NoDecision(Decision): @@ -44,18 +45,28 @@ def action( for step_idx, step_recs in enumerate(decisions): for walker_idx, decision in enumerate(step_recs): - if decision["decision_id"] == cls.ENUM.NOTHING.value: + if decision.decision_id == cls.ENUM.NOTHING.value: # check to make sure a walker doesn't already exist # where you are going to put it - if mod_walkers[decision["target_idxs"][0]] is not None: + if mod_walkers[decision.target_idx] is not None: raise ValueError( "Multiple walkers assigned to position {}".format( - decision["target_idxs"][0] + decision.target_idx ) ) # put the walker in the position specified by the # instruction - mod_walkers[decision["target_idxs"][0]] = walkers[walker_idx] + mod_walkers[decision.target_idx] = walkers[walker_idx] return mod_walkers + + @classmethod + def parents(cls, step: list[NoDecisionRecord]) -> list[int]: + + step_parents = [None for i in range(len(step))] + for parent_idx, parent_rec in enumerate(step): + + step_parents[parent_rec.target_idx] = parent_idx + + return step_parents diff --git a/tests/unit/test_resampling/test_decisions/test_clone_merge.py b/tests/unit/test_resampling/test_decisions/test_clone_merge.py index 0c57aaca..380db07a 100644 --- a/tests/unit/test_resampling/test_decisions/test_clone_merge.py +++ b/tests/unit/test_resampling/test_decisions/test_clone_merge.py @@ -6,6 +6,7 @@ from wepy.resampling.decisions.clone_merge import ( MultiCloneMergeDecision, CloneMergeDecisionEnum, + CloneMergeDecisionRecord, ) @@ -37,14 +38,14 @@ def test_action(self): walkers, [ [ - { + CloneMergeDecisionRecord(**{ "decision_id": 1000, "target_idxs": [0], - }, - { + }), + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.NOTHING, "target_idxs": [1], - }, + }), ] ], ) @@ -54,14 +55,14 @@ def test_action(self): walkers, [ [ - { + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.NOTHING, "target_idxs": [0], - }, - { + }), + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.NOTHING, "target_idxs": [1], - }, + }), ] ], ) @@ -73,14 +74,14 @@ def test_action(self): walkers, [ [ - { + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.NOTHING, "target_idxs": [1], - }, - { + }), + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.NOTHING, "target_idxs": [0], - }, + }), ] ], ) == [walker_2, walker_1] @@ -91,14 +92,14 @@ def test_action(self): walkers, [ [ - { + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.NOTHING, "target_idxs": [0], - }, - { + }), + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.NOTHING, "target_idxs": [0], - }, + }), ] ], ) @@ -111,14 +112,14 @@ def test_action(self): walkers, [ [ - { + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.NOTHING, "target_idxs": [0], - }, - { + }), + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.SQUASH, "target_idxs": [1], - }, + }), ] ], ) @@ -127,14 +128,14 @@ def test_action(self): walkers, [ [ - { + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.NOTHING, "target_idxs": [0], - }, - { + }), + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.SQUASH, "target_idxs": [0], - }, + }), ] ], ) @@ -145,14 +146,14 @@ def test_action(self): walkers, [ [ - { + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.KEEP_MERGE, "target_idxs": [0], - }, - { + }), + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.SQUASH, "target_idxs": [0], - }, + }), ] ], ) @@ -165,18 +166,18 @@ def test_action(self): ], [ [ - { + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.CLONE, "target_idxs": [0, 2], - }, - { + }), + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.KEEP_MERGE, "target_idxs": [1], - }, - { + }), + CloneMergeDecisionRecord(**{ "decision_id": CloneMergeDecisionEnum.SQUASH, "target_idxs": [1], - }, + }), ] ], ) == [ @@ -187,3 +188,15 @@ def test_action(self): ), attrs.evolve(walker_1, weight=0.5), ] + + def test_parents(self): + + MultiCloneMergeDecision.parents( + [ + CloneMergeDecisionRecord( + decision_id=0, + target_idxs=idx, + ) + for idx in range(4) + ], + ) diff --git a/tests/unit/test_resampling/test_decisions/test_decision.py b/tests/unit/test_resampling/test_decisions/test_decision.py new file mode 100644 index 00000000..b90e308e --- /dev/null +++ b/tests/unit/test_resampling/test_decisions/test_decision.py @@ -0,0 +1,80 @@ +import pytest +from typing import TypedDict +from enum import IntEnum +import attrs +from wepy.resampling.decisions.decision import Decision +from wepy.walker import Walker +from wepy.runners.mock import MockState + +# minimal implementation of the ABC for testing +class MockDecisionEnum(IntEnum): + NOTHING = 0 + +class MockDecisionRecord(TypedDict): + decision_id: int + +class MockDecision(Decision): + + ENUM = MockDecisionEnum + DEFAULT_DECISION = ENUM.NOTHING + ANCESTOR_DECISION_IDS = (ENUM.NOTHING.value,) + +class Test_Decision: + + def test_default_decision(self): + assert MockDecision.default_decision() == MockDecisionEnum.NOTHING + + def test_field_names(self): + assert MockDecision.field_names() == ("decision_id",) + + def test_field_shapes(self): + assert MockDecision.field_shapes() == ((1,),) + + def test_field_dtypes(self): + assert MockDecision.field_dtypes() == (int,) + + def test_fields(self): + assert MockDecision.fields() == [ + ("decision_id", (1,), int,) + ] + def test_record_field_names(self): + assert MockDecision.record_field_names() == ("decision_id",) + + def test_enum_dict_by_name(self): + assert MockDecision.enum_dict_by_name() == { + "NOTHING" : 0, + } + + def test_enum_dict_by_value(self): + assert MockDecision.enum_dict_by_value() == { + 0 : MockDecisionEnum.NOTHING, + } + + def test_enum_by_value(self): + assert MockDecision.enum_by_value(0) == MockDecisionEnum.NOTHING + def test_enum_by_name(self): + assert MockDecision.enum_by_name("NOTHING") == MockDecisionEnum.NOTHING + + def test_record(self): + + assert MockDecision.record(0) == {"decision_id" : 0} + + def test_action(self): + + with pytest.raises(NotImplementedError): + MockDecision.action( + [ + Walker( + MockState(1), + 0.1 + ) + for _ in range(4) + ], + [ + MockDecisionRecord( + decision_id=0 + ) + for _ in range(4) + ], + ) + diff --git a/tests/unit/test_resampling/test_decisions/test_no_decision.py b/tests/unit/test_resampling/test_decisions/test_no_decision.py index f8878525..056746ae 100644 --- a/tests/unit/test_resampling/test_decisions/test_no_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_no_decision.py @@ -1,6 +1,6 @@ from wepy.walker import Walker, WalkerState from wepy.runners.mock import MockState -from wepy.resampling.decisions.no_decision import NoDecision, NothingDecisionEnum +from wepy.resampling.decisions.no_decision import NoDecision, NothingDecisionEnum, NoDecisionRecord class TestNoDecision: @@ -27,14 +27,14 @@ def test_action(self): walkers, [ [ - { + NoDecisionRecord(**{ "decision_id": NothingDecisionEnum.NOTHING, - "target_idxs": [0], - }, - { + "target_idx": 0, + }), + NoDecisionRecord(**{ "decision_id": NothingDecisionEnum.NOTHING, - "target_idxs": [1], - }, + "target_idx": 1, + }), ] ], ) @@ -45,14 +45,42 @@ def test_action(self): walkers, [ [ - { + NoDecisionRecord(**{ "decision_id": NothingDecisionEnum.NOTHING, - "target_idxs": [1], - }, - { + "target_idx": 1, + }), + NoDecisionRecord(**{ "decision_id": NothingDecisionEnum.NOTHING, - "target_idxs": [0], - }, + "target_idx": 0, + }), ] ], ) == [walker_2, walker_1] + + def test_parents(self): + + assert NoDecision.parents( + [ + NoDecisionRecord(**{ + "decision_id": NothingDecisionEnum.NOTHING, + "target_idx": 0, + }), + NoDecisionRecord(**{ + "decision_id": NothingDecisionEnum.NOTHING, + "target_idx": 1, + }), + ] + ) == [0, 1] + + assert NoDecision.parents( + [ + NoDecisionRecord(**{ + "decision_id": NothingDecisionEnum.NOTHING, + "target_idx": 1, + }), + NoDecisionRecord(**{ + "decision_id": NothingDecisionEnum.NOTHING, + "target_idx": 0, + }), + ] + ) == [1, 0] From fcf0d95a300ab293565193f51083f49a2f7f41fc Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 12:26:07 -0500 Subject: [PATCH 077/143] add explicit DecisionRecord classes for Decision --- src/wepy/resampling/decisions/clone_merge.py | 2 ++ src/wepy/resampling/decisions/decision.py | 8 ++++++-- .../unit/test_resampling/test_decisions/test_decision.py | 9 +++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/wepy/resampling/decisions/clone_merge.py b/src/wepy/resampling/decisions/clone_merge.py index a1fed170..9a9da6db 100644 --- a/src/wepy/resampling/decisions/clone_merge.py +++ b/src/wepy/resampling/decisions/clone_merge.py @@ -71,6 +71,8 @@ class MultiCloneMergeDecision(Decision): DEFAULT_DECISION = ENUM.NOTHING + DECISION_RECORD = CloneMergeDecisionRecord + FIELDS = Decision.FIELDS + ("target_idxs",) SHAPES = Decision.SHAPES + (Ellipsis,) DTYPES = Decision.DTYPES + (int,) diff --git a/src/wepy/resampling/decisions/decision.py b/src/wepy/resampling/decisions/decision.py index 98961186..0565fdae 100644 --- a/src/wepy/resampling/decisions/decision.py +++ b/src/wepy/resampling/decisions/decision.py @@ -76,6 +76,8 @@ class Decision: DEFAULT_DECISION: int """The default decision to choose.""" + DECISION_RECORD: DecisionRecord = DecisionRecord + FIELDS: tuple[str, ...] = ("decision_id",) """The names of the fields that go into the decision record.""" @@ -214,8 +216,10 @@ def record(cls, enum_value: int, **fields: dict[str, Any]) -> DecisionRecord: ), "The field {} is not a field for that decision".format(field_key) assert field_key != "decision_id", "'decision_id' cannot be an extra field" - rec = {"decision_id": enum_value} - rec.update(fields) + rec_d = {"decision_id": enum_value} + rec_d.update(fields) + + rec = cls.DECISION_RECORD(**rec_d) return rec diff --git a/tests/unit/test_resampling/test_decisions/test_decision.py b/tests/unit/test_resampling/test_decisions/test_decision.py index b90e308e..cccca6f0 100644 --- a/tests/unit/test_resampling/test_decisions/test_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_decision.py @@ -2,7 +2,7 @@ from typing import TypedDict from enum import IntEnum import attrs -from wepy.resampling.decisions.decision import Decision +from wepy.resampling.decisions.decision import Decision, DecisionRecord from wepy.walker import Walker from wepy.runners.mock import MockState @@ -10,9 +10,6 @@ class MockDecisionEnum(IntEnum): NOTHING = 0 -class MockDecisionRecord(TypedDict): - decision_id: int - class MockDecision(Decision): ENUM = MockDecisionEnum @@ -57,7 +54,7 @@ def test_enum_by_name(self): def test_record(self): - assert MockDecision.record(0) == {"decision_id" : 0} + assert MockDecision.record(0) == DecisionRecord(decision_id=0) def test_action(self): @@ -71,7 +68,7 @@ def test_action(self): for _ in range(4) ], [ - MockDecisionRecord( + DecisionRecord( decision_id=0 ) for _ in range(4) From a570afe20631cab1a7cca3311d38344a7077ea8a Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 12:26:31 -0500 Subject: [PATCH 078/143] tests for resampler base classes --- src/wepy/resampling/resamplers/clone_merge.py | 59 ++++++-- src/wepy/resampling/resamplers/resampler.py | 102 ++++++++++++-- .../test_resamplers/test_clone_merge.py | 130 ++++++++++++++++++ .../test_resamplers/test_resampler.py | 32 +++++ 4 files changed, 298 insertions(+), 25 deletions(-) create mode 100644 tests/unit/test_resampling/test_resamplers/test_clone_merge.py create mode 100644 tests/unit/test_resampling/test_resamplers/test_resampler.py diff --git a/src/wepy/resampling/resamplers/clone_merge.py b/src/wepy/resampling/resamplers/clone_merge.py index e92dc175..54190255 100644 --- a/src/wepy/resampling/resamplers/clone_merge.py +++ b/src/wepy/resampling/resamplers/clone_merge.py @@ -1,12 +1,15 @@ +from typing import TypeVar, Generic # Third Party Library import numpy as np # First Party Library -from wepy.resampling.decisions.clone_merge import MultiCloneMergeDecision -from wepy.resampling.resamplers.resampler import Resampler, ResamplerError +from wepy.resampling.decisions.clone_merge import MultiCloneMergeDecision, CloneMergeDecisionRecord +from wepy.resampling.resamplers.resampler import Resampler, ResamplerABC, ResamplerError +from wepy.walker import WalkerState, Walker +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) -class CloneMergeResampler(Resampler): +class CloneMergeResampler(ResamplerABC, Generic[WalkerState_]): """Abstract base class for resamplers using the clone-merge decision class. @@ -23,11 +26,11 @@ class CloneMergeResampler(Resampler): DECISION = MultiCloneMergeDecision - RESAMPLING_FIELDS = DECISION.FIELDS + Resampler.CYCLE_FIELDS - RESAMPLING_SHAPES = DECISION.SHAPES + Resampler.CYCLE_SHAPES - RESAMPLING_DTYPES = DECISION.DTYPES + Resampler.CYCLE_DTYPES + RESAMPLING_FIELDS = DECISION.FIELDS + ResamplerABC.CYCLE_FIELDS + RESAMPLING_SHAPES = DECISION.SHAPES + ResamplerABC.CYCLE_SHAPES + RESAMPLING_DTYPES = DECISION.DTYPES + ResamplerABC.CYCLE_DTYPES - RESAMPLING_RECORD_FIELDS = DECISION.RECORD_FIELDS + Resampler.CYCLE_RECORD_FIELDS + RESAMPLING_RECORD_FIELDS = DECISION.RECORD_FIELDS + ResamplerABC.CYCLE_RECORD_FIELDS def __init__( self, @@ -36,7 +39,7 @@ def __init__( min_num_walkers=Ellipsis, max_num_walkers=Ellipsis, **kwargs, - ): + ) -> None: """Constructor for CloneMegerResampler class. Parameters @@ -50,21 +53,36 @@ def __init__( """ super().__init__( - min_num_walkers=min_num_walkers, max_num_walkers=max_num_walkers, **kwargs + min_num_walkers=min_num_walkers, + max_num_walkers=max_num_walkers, **kwargs ) + if pmin >= 1.0: + raise ResamplerError( + f"pmin ({pmin}) must be less 1.0" + ) + if pmax >= 1.0: + raise ResamplerError( + f"pmax ({pmax}) must be less 1.0" + ) + + if pmin > pmax: + raise ResamplerError( + f"pmin ({pmin}) must be less than pmax ({pmax})" + ) + self._pmin = pmin self._pmax = pmax @property - def pmin(self): + def pmin(self) -> float: return self._pmin @property - def pmax(self): + def pmax(self) -> float: return self._pmax - def _init_walker_actions(self, n_walkers): + def _init_walker_actions(self, n_walkers: int) -> list[CloneMergeDecisionRecord]: """Returns a list of default resampling records for a single resampling step. @@ -91,7 +109,7 @@ def _init_walker_actions(self, n_walkers): return walker_actions - def _check_resampled_walkers(self, resampled_walkers): + def _check_resampled_walkers(self, resampled_walkers: list[Walker[WalkerState_]]) -> None: """Check constraints on resampled walkers. Raises errors when constraints are violated. @@ -102,6 +120,8 @@ def _check_resampled_walkers(self, resampled_walkers): """ + # TODO: should we check that the sums are unity here? + walker_weights = np.array([walker.weight for walker in resampled_walkers]) # check that all of the weights are less than or equal to the pmax @@ -124,7 +144,11 @@ def _check_resampled_walkers(self, resampled_walkers): ) ) - def assign_clones(self, merge_groups, walker_clone_nums): + def assign_clones( + self, + merge_groups: list[list[int]], + walker_clone_nums: list[int], + ) -> list[CloneMergeDecisionRecord]: """Convert two convenient data structures to a list of almost normalized resampling records. @@ -163,6 +187,11 @@ def assign_clones(self, merge_groups, walker_clone_nums): """ + if len(merge_groups) != len(walker_clone_nums): + raise ResamplerError( + f"Size of merge_groups ({len(merge_groups)}) and walker_clone_nums ({len(walker_clone_nums)}) must be equal." + ) + n_walkers = len(walker_clone_nums) walker_actions = self._init_walker_actions(n_walkers) @@ -196,7 +225,7 @@ def assign_clones(self, merge_groups, walker_clone_nums): for walker_idx, num_clones in enumerate(walker_clone_nums): if num_clones > 0 and len(merge_groups[walker_idx]) > 0: raise ResamplerError( - "Error! cloning and merging occuring with the same walker" + f"Cloning and merging occuring with the same walker: {walker_idx}" ) # if this walker is to be cloned do so and consume the free diff --git a/src/wepy/resampling/resamplers/resampler.py b/src/wepy/resampling/resamplers/resampler.py index 48050c52..8c89419f 100644 --- a/src/wepy/resampling/resamplers/resampler.py +++ b/src/wepy/resampling/resamplers/resampler.py @@ -1,6 +1,6 @@ # Standard Library import logging -from typing import Any +from typing import Any, Protocol, TypeVar, Generic, Literal, Union # Standard Library from warnings import warn @@ -10,10 +10,12 @@ # First Party Library from wepy.resampling.decisions.decision import Decision, DecisionRecord -from wepy.walker import Walker +from wepy.walker import Walker, WalkerState logger = logging.getLogger(__name__) +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) + class ResamplerError(Exception): """Error raised when some constraint on resampling properties is @@ -22,8 +24,83 @@ class ResamplerError(Exception): pass +class Resampler(Protocol, Generic[WalkerState_]): + + DECISION: Decision + CYCLE_FIELDS: tuple[str, ...] + CYCLE_SHAPES: tuple[tuple[int, ...], ...] + CYCLE_DTYPES: tuple[int | float, ...] + CYCLE_RECORD_FIELDS: None | tuple[str, ...] + RESAMPLING_FIELDS: tuple[str, ...] + RESAMPLING_SHAPES: tuple[ + Union[ + tuple[int, ...], + Literal[Ellipsis], + None, + ], + ... + ] + RESAMPLING_DTYPES: tuple[ + Union[ + np.dtype, + None + ], + ... + ] + RESAMPLING_RECORD_FIELDS: None | tuple[str, ...] + RESAMPLER_FIELDS: tuple[str, ...] + RESAMPLER_SHAPES: tuple[ + Union[ + tuple[int, ...], + Literal[Ellipsis], + None, + ], + ... + ] + + RESAMPLER_DTYPES: tuple[ + Union[np.dtype, None], ... + ] + RESAMPLER_RECORD_FIELDS: None | tuple[str, ...] + + def resampling_fields(self) -> tuple[ + tuple[str, ...], + tuple[ + Union[ + tuple[int, ...], + Literal[Ellipsis], + None, + ], + ... + ], + tuple[ + Union[ + np.dtype, + None + ], + ... + ], + ]: + ... + + def resampling_record_field_names(self) -> None | tuple[str, ...]: + ... + + def resampler_record_field_names(self) -> None | tuple[str, ...]: + ... + + def resample( + self, + walkers: list[Walker[WalkerState_]] + ) -> tuple[ + list[Walker[WalkerState_]], + # TODO: better types for this + list[dict[str, Any]], + list[dict[str, Any]], + ]: + ... -class Resampler: +class ResamplerABC(Resampler): """Abstract base class for implementing resamplers. All subclasses of Resampler must implement the 'resample' method. @@ -308,7 +385,7 @@ def __init__( """ - # the min and max number of walkers that can be generated in + # The min and max number of walkers that can be generated in # resampling. # Ellipsis means to keep bound it by the number of @@ -323,12 +400,17 @@ def __init__( # min_num_walkers of None in practice is 1 since there must # always be at least 1 walker - if min_num_walkers not in (Ellipsis, None): + if min_num_walkers not in {Ellipsis, None}: if min_num_walkers < 1: raise ResamplerError( "The minimum number of walkers should be at least 1" ) + if max_num_walkers not in {Ellipsis, None} and min_num_walkers > max_num_walkers: + raise ResamplerError( + f"min_num_walkers ({min_num_walkers}) must be less than or equal to max_num_walkers ({max_num_walkers})" + ) + self._min_num_walkers = min_num_walkers self._max_num_walkers = max_num_walkers @@ -458,12 +540,12 @@ def debug_off(self) -> None: self.set_debug_mode(False) @property - def max_num_walkers_setting(self): + def max_num_walkers_setting(self) -> int: """The specification for the maximum number of walkers for the resampler.""" return self._max_num_walkers @property - def min_num_walkers_setting(self): + def min_num_walkers_setting(self) -> int: """The specification for the minimum number of walkers for the resampler.""" return self._min_num_walkers @@ -562,7 +644,7 @@ def _unset_resampling_num_walkers(self) -> None: def _resample_init( self, - walkers: list[Walker], + walkers: list[Walker[WalkerState_]], ) -> None: """Common initialization stuff for resamplers. @@ -589,10 +671,10 @@ def _resample_cleanup(self, **kwargs) -> None: def resample( self, - walkers: list[Walker], + walkers: list[Walker[WalkerState_]], debug_mode: bool = False, ) -> tuple[ - list[Walker], + list[Walker[WalkerState_]], list[dict[str, Any]], list[dict[str, Any]], ]: diff --git a/tests/unit/test_resampling/test_resamplers/test_clone_merge.py b/tests/unit/test_resampling/test_resamplers/test_clone_merge.py new file mode 100644 index 00000000..139c5a87 --- /dev/null +++ b/tests/unit/test_resampling/test_resamplers/test_clone_merge.py @@ -0,0 +1,130 @@ +import pytest +from wepy.resampling.resamplers.resampler import ResamplerError, Resampler, ResamplerABC +from wepy.resampling.resamplers.clone_merge import CloneMergeResampler +from wepy.resampling.decisions.clone_merge import MultiCloneMergeDecision, CloneMergeDecisionRecord, CloneMergeDecisionEnum +from wepy.walker import Walker +from wepy.runners.mock import MockState + +# class MockCloneMergeResampler(ResamplerABC): + +class Test_CloneMergeResampler: + + def test___init__(self): + + CloneMergeResampler() + + with pytest.raises(ResamplerError): + CloneMergeResampler( + pmin=0.1, + pmax=0.01, + ) + + with pytest.raises(ResamplerError): + CloneMergeResampler( + pmin=1.0, + pmax=0.01, + ) + + with pytest.raises(ResamplerError): + CloneMergeResampler( + pmin=0.1, + pmax=1.0, + ) + + def test__init_walker_actions(self): + + assert CloneMergeResampler()._init_walker_actions(4) == [ + CloneMergeDecisionRecord(decision_id=1, target_idxs=(0,)), + CloneMergeDecisionRecord(decision_id=1, target_idxs=(1,)), + CloneMergeDecisionRecord(decision_id=1, target_idxs=(2,)), + CloneMergeDecisionRecord(decision_id=1, target_idxs=(3,)), + ] + + def test__check_resampled_walkers(self): + + CloneMergeResampler( + pmin=0.1, + pmax=0.4, + )._check_resampled_walkers( + [ + Walker( + MockState(1), + weight=0.1, + ), + Walker( + MockState(1), + weight=0.4, + ), + ] + ) + + with pytest.raises(ResamplerError): + CloneMergeResampler( + pmin=0.1, + pmax=0.4, + )._check_resampled_walkers( + [ + Walker( + MockState(1), + weight=0.01, + ), + Walker( + MockState(1), + weight=0.4, + ), + ] + ) + + with pytest.raises(ResamplerError): + CloneMergeResampler( + pmin=0.1, + pmax=0.4, + )._check_resampled_walkers( + [ + Walker( + MockState(1), + weight=0.1, + ), + Walker( + MockState(1), + weight=0.5, + ), + ] + ) + + def test_assign_clones(self): + + resampler = CloneMergeResampler( + pmin=0.1, + pmax=0.4, + ) + + with pytest.raises(ResamplerError): + resampler.assign_clones( + merge_groups=[[], [],], + walker_clone_nums=[0, 0, 0], + ) + + assert resampler.assign_clones( + merge_groups=[[], []], + walker_clone_nums=[0, 0], + ) == [ + CloneMergeDecisionRecord(decision_id=1, target_idxs=(0,)), + CloneMergeDecisionRecord(decision_id=1, target_idxs=(1,)), + ] + + assert resampler.assign_clones( + merge_groups=[[], [2], [],], + walker_clone_nums=[1, 0, 0], + ) == [ + CloneMergeDecisionRecord(decision_id=2, target_idxs=(0, 2)), + CloneMergeDecisionRecord(decision_id=4, target_idxs=(1,)), + CloneMergeDecisionRecord(decision_id=3, target_idxs=(1,)), + ] + + with pytest.raises(ResamplerError): + + resampler.assign_clones( + merge_groups=[[2], [], [],], + walker_clone_nums=[1, 0, 0], + ) diff --git a/tests/unit/test_resampling/test_resamplers/test_resampler.py b/tests/unit/test_resampling/test_resamplers/test_resampler.py new file mode 100644 index 00000000..f508bbd0 --- /dev/null +++ b/tests/unit/test_resampling/test_resamplers/test_resampler.py @@ -0,0 +1,32 @@ +import pytest + +from wepy.resampling.resamplers.resampler import ResamplerABC, ResamplerError + + +class Test_ResamplerABC: + + def test___init__(self): + + ResamplerABC() + + ResamplerABC( + min_num_walkers=None, + max_num_walkers=None, + ) + + ResamplerABC( + min_num_walkers=3, + max_num_walkers=3, + ) + + with pytest.raises(ResamplerError): + ResamplerABC( + min_num_walkers=4, + max_num_walkers=3, + ) + + with pytest.raises(ResamplerError): + ResamplerABC( + min_num_walkers=0 + ) + From 20234e5c71e5532d71de0a15058a51d38a0cb1b9 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 13:03:06 -0500 Subject: [PATCH 079/143] remove randomwalk system stuff this is out of date, I don't want to maintain it and was never interesting IMO --- src/wepy/resampling/distances/randomwalk.py | 68 ---------- src/wepy/runners/randomwalk.py | 143 -------------------- 2 files changed, 211 deletions(-) delete mode 100644 src/wepy/resampling/distances/randomwalk.py delete mode 100644 src/wepy/runners/randomwalk.py diff --git a/src/wepy/resampling/distances/randomwalk.py b/src/wepy/resampling/distances/randomwalk.py deleted file mode 100644 index 382149f3..00000000 --- a/src/wepy/resampling/distances/randomwalk.py +++ /dev/null @@ -1,68 +0,0 @@ -"""This module here is part of the RandomWalk object that implements -the distance metric for the RandomWalk walk system. This distance -metric is a scaled version of the Manhattan Norm. - -""" - -# Standard Library -import logging - -logger = logging.getLogger(__name__) - -# Third Party Library -import numpy as np - -# First Party Library -from wepy.resampling.distances.distance import Distance - - -class RandomWalkDistance(Distance): - """A class to implement the RandomWalkDistance metric for measuring - differences between walker states. This is a normalized Manhattan - distance measured between the difference in positions of the walkers. - - """ - - def __init__(self): - """Construct a RandomWalkDistance metric.""" - pass - - def image(self, state): - """Transform a state into a random walk image. - - A random walk image is just the position of a walker in the - N-dimensional space. - - Parameters - ---------- - state : object implementing WalkerState - A walker state object with positions in a numpy array - of shape (N), where N is the the dimension of the random - walk system. - - Returns - ------- - randomwalk_image : array of floats of shape (N) - The positions of a walker in the N-dimensional space. - - """ - return state["positions"] - - def image_distance(self, image_a, image_b): - """Compute the distance between the image of the two walkers. - - Parameters - ---------- - image_a : array of float of shape (1, N) - Position of the first walker's state. - - image_b: array of float of shape (1, N) - Position of the second walker's state. - - Returns - ------- - distance: float - The normalized Manhattan distance. - - """ - return np.average(np.abs(image_a - image_b)) diff --git a/src/wepy/runners/randomwalk.py b/src/wepy/runners/randomwalk.py deleted file mode 100644 index a2261956..00000000 --- a/src/wepy/runners/randomwalk.py +++ /dev/null @@ -1,143 +0,0 @@ -"""The random walk dynamics runner. - -In this system, the state of the walkers is defined as an -N-dimensional vector of non-negative values. The walkers start at -position zero (in N-dimensional space) and randomly move a step either -forward or backward with the given probabilities. This is done in each -dimension at each dynamic step. All moves that result in a negative -position are rejected. - -One potentioanl use of the random walk system is to test the -performance of differnt resamplers as seen in these papers: - -"WExplore: Hierarchical Exploration of High-Dimensional Spaces -Using the Weighted Ensemble Algorithm" and -"REVO: Resampling of Ensembles by Variation Optimization". - -""" - -# Standard Library -import logging - -logger = logging.getLogger(__name__) -# Standard Library -import random as rand - -# Third Party Library -from pint import UnitRegistry - -# First Party Library -from wepy.runners.runner import Runner -from wepy.walker import Walker, WalkerState - -units = UnitRegistry() - -# the names of the units. We pass them through pint just to validate -# them -UNIT_NAMES = ( - ("positions_unit", str(units("microsecond").units)), - ("time_unit", str(units("picosecond").units)), -) - -"""Mapping of units identifiers to the corresponding pint units.""" - - -class RandomWalkRunner(Runner): - """RandomWalk runner for random walk simulations.""" - - def __init__(self, probability=0.25): - """Constructor for RandomWalkRunner. - - Parameters - ---------- - probabilty : float - "Probability" is defined here as the forward-move - probability only. The backward-move probability is - 1-probability.(Default = 0.25) - - """ - - self._probability = probability - - @property - def probability(self): - """The probability of forward-move in an N-dimensional space""" - return self._probability - - def _walk(self, positions): - """Run dynamics for the RandomWalk system for one step. - - Parameters - ---------- - positions : arraylike of shape (1, dimension) - Current position of the walker. - - Returns - ------- - new_positions : arraylike of shape (1, dimension) - The positions of the walker after one dynamic step. - - """ - - # make the deep copy of current posiotion - new_positions = positions.copy() - - # get the dimension of the random walk space - dimension = new_positions.shape[1] - - # iterates over each dimension - for dim_idx in range(dimension): - # Generates an uniform random number to choose between - # moving forward or backward. - rand_num = rand.uniform(0, 1) - - # make a forward movement - if rand_num < self.probability: - new_positions[0][dim_idx] += 1 - # make a backward movement - else: - new_positions[0][dim_idx] -= 1 - - # implement the boundary condition for movement, movements - # to -1 are rejected - if new_positions[0][dim_idx] < 0: - new_positions[0][dim_idx] = 0 - - return new_positions - - def run_segment(self, walker, segment_length, **kwargs): - """Runs a random walk simulation for the given number of steps. - - Parameters - ---------- - walker : object implementing the Walker interface - The walker for which dynamics will be propagated. - - - segment_length : int - The numerical value that specifies how much dynamical steps - are to be run. - - Returns - ------- - new_walker : object implementing the Walker interface - Walker after dynamics was run, only the state should be modified. - - """ - - # Gets the current posiotion of RandomWalk Walker - positions = walker.state["positions"] - - # Make movements for the segment_length steps - for _ in range(segment_length): - # calls walk function for one step movement - new_positions = self._walk(positions) - positions = new_positions - - # makes new state form new positions - new_state = WalkerState(positions=new_positions, time=0.0) - - # creates new_walker from new state and current weight - new_walker = Walker(new_state, walker.weight) - - return new_walker From b544d5e8a634b65b92ed7fe3516dbc4416f5a116 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 13:03:48 -0500 Subject: [PATCH 080/143] refactor and test the resampling.distances module No tests for the receptor module though --- .../distances/{distance.py => base.py} | 103 ++++-------------- src/wepy/resampling/distances/simple.py | 29 +++++ .../test_distances/test_base.py | 39 +++++++ .../test_distances/test_simple.py | 28 +++++ 4 files changed, 118 insertions(+), 81 deletions(-) rename src/wepy/resampling/distances/{distance.py => base.py} (53%) create mode 100644 src/wepy/resampling/distances/simple.py create mode 100644 tests/unit/test_resampling/test_distances/test_base.py create mode 100644 tests/unit/test_resampling/test_distances/test_simple.py diff --git a/src/wepy/resampling/distances/distance.py b/src/wepy/resampling/distances/base.py similarity index 53% rename from src/wepy/resampling/distances/distance.py rename to src/wepy/resampling/distances/base.py index c81d7eca..fbce1f23 100644 --- a/src/wepy/resampling/distances/distance.py +++ b/src/wepy/resampling/distances/base.py @@ -24,24 +24,20 @@ # Standard Library import logging - -logger = logging.getLogger(__name__) - -# Third Party Library -import numpy as np +from abc import ABC +from typing import TypeVar, Generic, Protocol # First Party Library +from wepy.walker import WalkerState from wepy.util.util import box_vectors_to_lengths_angles +logger = logging.getLogger(__name__) -class Distance: - """Abstract Base class for Distance classes.""" - - def __init__(self): - """Constructor for Distance class.""" - pass +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState, covariant=True) +DistanceImage_ = TypeVar("DistanceImage_") - def image(self, state): +class Distance(Protocol[DistanceImage_, WalkerState_]): + def image(self, state: WalkerState_) -> DistanceImage_: """Compute the 'image' of a walker state which should be some transformation of the walker state that is more convenient. E.g. for precomputation of expensive operations or @@ -62,9 +58,9 @@ def image(self, state): """ - return state + ... - def image_distance(self, image_a, image_b): + def image_distance(self, image_a: DistanceImage_, image_b: DistanceImage_) -> float: """Compute the distance between two images of walker states. Parameters @@ -83,9 +79,9 @@ def image_distance(self, image_a, image_b): NotImplementedError : always because this is abstract """ - raise NotImplementedError + ... - def distance(self, state_a, state_b): + def distance(self, state_a: WalkerState_, state_b: WalkerState_) -> float: """Compute the distance between two states. Parameters @@ -102,74 +98,19 @@ def distance(self, state_a, state_b): """ - return self.image_distance(self.image(state_a), self.image(state_b)) - - -class XYEuclideanDistance(Distance): - """2 dimensional euclidean distance between points. + ... - States have the attributes 'x' and 'y'. - """ + - def image(self, state): - return np.array([state["x"], state["y"]]) - - def image_distance(self, image_a, image_b): - return np.sqrt((image_a[0] - image_b[0]) ** 2 + (image_a[1] - image_b[1]) ** 2) - - -class AtomPairDistance(Distance): - """Constructs a vector of atomic distances for each state. - Distance is the root mean squared distance between the vectors. - """ +class DistanceABC(ABC, Generic[DistanceImage_, WalkerState_]): + """Abstract Base class for Distance classes.""" - def __init__(self, pair_list, periodic=True): - """Construct a distance metric. + def image(self, state: WalkerState_) -> DistanceImage_: + return state - Parameters - ---------- - pair_list : arraylike of tuples - The indices of the atom pairs between which to compute - distances. + def image_distance(self, image_a: DistanceImage_, image_b: DistanceImage_) -> float: + raise NotImplementedError - """ - self.pair_list = pair_list - self.periodic = periodic - - def _adjust_disp_vector(self, disp, box_lengths): - edited = True - while edited: - edited = False - for i in range(3): - if disp[i] > box_lengths[i] / 2: - disp[i] -= box_lengths[i] - edited = True - elif disp[i] < -box_lengths[i] / 2: - disp[i] += box_lengths[i] - edited = True - return disp - - def image(self, state): - if self.periodic: - # get the box lengths from the vectors - box_lengths, box_angles = box_vectors_to_lengths_angles( - state["box_vectors"] - ) - - dist_list = np.zeros((len(self.pair_list))) - for i, p in enumerate(self.pair_list): - disp_vector = state["positions"][p[0]] - state["positions"][p[1]] - if self.periodic: - dist_list[i] = np.sqrt( - np.sum( - np.square(self._adjust_disp_vector(disp_vector, box_lengths)) - ) - ) - else: - dist_list[i] = np.sqrt(np.sum(np.square(disp_vector))) - - return dist_list - - def image_distance(self, image_a, image_b): - return np.sqrt(np.mean(np.square(image_a - image_b))) + def distance(self, state_a: WalkerState_, state_b: WalkerState_) -> float: + return self.image_distance(self.image(state_a), self.image(state_b)) diff --git a/src/wepy/resampling/distances/simple.py b/src/wepy/resampling/distances/simple.py new file mode 100644 index 00000000..fec6dbdd --- /dev/null +++ b/src/wepy/resampling/distances/simple.py @@ -0,0 +1,29 @@ +# Standard Library +import logging +from abc import ABC +from typing import TypeVar, Generic, Protocol + +import numpy as np +import attrs + +# First Party Library +from wepy.walker import WalkerState +from wepy.util.util import box_vectors_to_lengths_angles +from .base import DistanceABC + +logger = logging.getLogger(__name__) + +@attrs.define +class XYDistanceState: + coord: tuple[int, int] + +@attrs.define +class XYEuclideanDistance(DistanceABC): + """2 dimensional euclidean distance between points. + + States have the attributes 'x' and 'y'. + + """ + + def image_distance(self, image_a, image_b): + return np.sqrt((image_a.coord[0] - image_b.coord[0]) ** 2 + (image_a.coord[1] - image_b.coord[1]) ** 2) diff --git a/tests/unit/test_resampling/test_distances/test_base.py b/tests/unit/test_resampling/test_distances/test_base.py new file mode 100644 index 00000000..7c3bf546 --- /dev/null +++ b/tests/unit/test_resampling/test_distances/test_base.py @@ -0,0 +1,39 @@ +import math +import attrs + +from wepy.resampling.distances.base import DistanceABC +from wepy.runners.mock import MockState + +# minimal implementation of a Distance from the ABC, in this case the +# image and state are the same + +@attrs.define +class MockDistance(DistanceABC): + + def image_distance(self, image_a: MockState, image_b: MockState) -> float: + + return math.sqrt((image_a.a - image_b.a) ** 2) + +class Test_DistanceABC: + + def test_image(self): + assert DistanceABC().image(MockState(1)) == MockState(1) + assert MockDistance().image(MockState(1)) == MockState(1) + + def test_image_distance(self): + assert math.isclose( + MockDistance().image_distance( + MockState(1), + MockState(3), + ), + 2. + ) + + def test_image_distance(self): + assert math.isclose( + MockDistance().distance( + MockState(1), + MockState(3), + ), + 2. + ) diff --git a/tests/unit/test_resampling/test_distances/test_simple.py b/tests/unit/test_resampling/test_distances/test_simple.py new file mode 100644 index 00000000..66a8a0fe --- /dev/null +++ b/tests/unit/test_resampling/test_distances/test_simple.py @@ -0,0 +1,28 @@ +import math +from wepy.resampling.distances.simple import ( + XYDistanceState, + XYEuclideanDistance, +) + + +class Test_XYEuclideanDistance: + + def test_image_distance(self): + + assert math.isclose( + XYEuclideanDistance().image_distance( + XYDistanceState((0, 0)), + XYDistanceState((0, 2)), + ), + 2. + ) + + def test_distance(self): + + assert math.isclose( + XYEuclideanDistance().distance( + XYDistanceState((0, 0)), + XYDistanceState((0, 2)), + ), + 2. + ) From 4245994bd175243df30807fd4b8a2974cb1a6b10 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 13:08:17 -0500 Subject: [PATCH 081/143] make specific Mock distance and tests --- src/wepy/resampling/distances/mock.py | 11 +++++++ .../test_distances/test_base.py | 9 +----- .../test_distances/test_mock.py | 32 +++++++++++++++++++ 3 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 src/wepy/resampling/distances/mock.py create mode 100644 tests/unit/test_resampling/test_distances/test_mock.py diff --git a/src/wepy/resampling/distances/mock.py b/src/wepy/resampling/distances/mock.py new file mode 100644 index 00000000..97055943 --- /dev/null +++ b/src/wepy/resampling/distances/mock.py @@ -0,0 +1,11 @@ +import math +import attrs +from wepy.runners.mock import MockState +from .base import DistanceABC + +@attrs.define +class MockDistance(DistanceABC): + + def image_distance(self, image_a: MockState, image_b: MockState) -> float: + + return math.sqrt((image_a.a - image_b.a) ** 2) diff --git a/tests/unit/test_resampling/test_distances/test_base.py b/tests/unit/test_resampling/test_distances/test_base.py index 7c3bf546..4d4eac09 100644 --- a/tests/unit/test_resampling/test_distances/test_base.py +++ b/tests/unit/test_resampling/test_distances/test_base.py @@ -1,24 +1,17 @@ import math -import attrs from wepy.resampling.distances.base import DistanceABC from wepy.runners.mock import MockState +from wepy.resampling.distances.mock import MockDistance # minimal implementation of a Distance from the ABC, in this case the # image and state are the same -@attrs.define -class MockDistance(DistanceABC): - - def image_distance(self, image_a: MockState, image_b: MockState) -> float: - - return math.sqrt((image_a.a - image_b.a) ** 2) class Test_DistanceABC: def test_image(self): assert DistanceABC().image(MockState(1)) == MockState(1) - assert MockDistance().image(MockState(1)) == MockState(1) def test_image_distance(self): assert math.isclose( diff --git a/tests/unit/test_resampling/test_distances/test_mock.py b/tests/unit/test_resampling/test_distances/test_mock.py new file mode 100644 index 00000000..83ee91ce --- /dev/null +++ b/tests/unit/test_resampling/test_distances/test_mock.py @@ -0,0 +1,32 @@ +import math + +from wepy.resampling.distances.base import DistanceABC +from wepy.runners.mock import MockState +from wepy.resampling.distances.mock import MockDistance +# minimal implementation of a Distance from the ABC, in this case the +# image and state are the same + + +class Test_DistanceABC: + + def test_image(self): + assert DistanceABC().image(MockState(1)) == MockState(1) + assert MockDistance().image(MockState(1)) == MockState(1) + + def test_image_distance(self): + assert math.isclose( + MockDistance().image_distance( + MockState(1), + MockState(3), + ), + 2. + ) + + def test_image_distance(self): + assert math.isclose( + MockDistance().distance( + MockState(1), + MockState(3), + ), + 2. + ) From 98e460fd2468d8a91c624151f5138e35bf37de04 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 14:47:39 -0500 Subject: [PATCH 082/143] fixup for noresampler --- src/wepy/resampling/resamplers/noresampler.py | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py index 78543adc..52b4bc5c 100644 --- a/src/wepy/resampling/resamplers/noresampler.py +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -1,7 +1,7 @@ from typing import TypedDict import numpy as np from wepy.walker import Walker -from wepy.resampling.resamplers.resampler import Resampler +from wepy.resampling.resamplers.resampler import Resampler, ResamplerABC from wepy.resampling.decisions.no_decision import ( NoDecision, NoDecisionRecord, @@ -24,11 +24,11 @@ class NoResampler(Resampler): DECISION = NoDecision # must reset these when you change the decision - RESAMPLING_FIELDS = DECISION.FIELDS + Resampler.CYCLE_FIELDS - RESAMPLING_SHAPES = DECISION.SHAPES + Resampler.CYCLE_SHAPES - RESAMPLING_DTYPES = DECISION.DTYPES + Resampler.CYCLE_DTYPES + RESAMPLING_FIELDS = DECISION.FIELDS + ResamplerABC.CYCLE_FIELDS + RESAMPLING_SHAPES = DECISION.SHAPES + ResamplerABC.CYCLE_SHAPES + RESAMPLING_DTYPES = DECISION.DTYPES + ResamplerABC.CYCLE_DTYPES - RESAMPLING_RECORD_FIELDS = DECISION.RECORD_FIELDS + Resampler.CYCLE_RECORD_FIELDS + RESAMPLING_RECORD_FIELDS = DECISION.RECORD_FIELDS + ResamplerABC.CYCLE_RECORD_FIELDS def resample( self, @@ -39,9 +39,6 @@ def resample( list[NoResamplerResamplerData], ]: - # TODO,REFACT: do we really need this - self._resample_init(walkers=walkers) - # normally decide is only for a single step and so does not # include the step_idx, so we add this to the records, and # convert the target idxs and decision_id to feature vector @@ -64,12 +61,4 @@ def resample( resampler_data: list[NoResamplerResamplerData] = [{}] # the resampled walkers are just the walkers - - # TODO,REFACT: do we really need this - self._resample_cleanup( - resampling_data=resampling_data, - resampler_data=resampler_data, - walkers=walkers, - ) - return walkers, resampling_data, resampler_data From b6340d4ed2d2c9252dc8ad7a1bf205cf1159137a Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 14:47:46 -0500 Subject: [PATCH 083/143] REVO tests, light refactoring I tried to keep it mostly as is, because it is delicate. Tests are mostly aimed at exercising the interfaces and not getting correct behaviors. --- src/wepy/resampling/resamplers/revo.py | 350 +++++++++++++----- .../test_resamplers/test_revo.py | 319 ++++++++++++++++ 2 files changed, 583 insertions(+), 86 deletions(-) create mode 100644 tests/unit/test_resampling/test_resamplers/test_revo.py diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 83d81500..329af616 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -1,21 +1,63 @@ # Standard Library import itertools as it +import time import logging +from typing import Literal, TypeVar, Generic, Callable, Any, TypedDict -logger = logging.getLogger(__name__) # Standard Library import multiprocessing as mp import random as rand # Third Party Library import numpy as np +import attrs # First Party Library -from wepy.util.multiprocessing import proc_pool_worker_setup +from wepy.util.multiprocessing import proc_pool_worker_setup, queue_listener_context from wepy.resampling.resamplers.clone_merge import CloneMergeResampler +from wepy.resampling.decisions.clone_merge import CloneMergeDecisionRecord +from wepy.resampling.distances.base import Distance +from wepy.walker import WalkerState, Walker + +logger = logging.getLogger(__name__) + +DistanceMetric_ = TypeVar("DistanceMetric_", bound=Distance) +DistanceImage_ = TypeVar("DistanceImage_") +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) + +MergeAlgorithm = Literal["pairs", "greedy"] +class REVOResamplerError(Exception): + pass -class REVOResampler(CloneMergeResampler): +class _ImageWrapper(Generic[WalkerState_, DistanceImage_]): + """Wrapper callable to inject a few log messages to image + computation. + + Useful for if the image function does not log anything and this + will guarantee some logs are generated which is useful for + troubleshooting process pool problem. + + """ + + def __init__(self, image_func: Callable[[WalkerState_], DistanceImage_]) -> None: + self.image_func = image_func + + def __call__(self, state: WalkerState_) -> DistanceImage_: + logger.info("Starting image computation") + result = self.image_func(state) + logger.info("Finished image computation") + return result + +class REVOResamplerResamplerData(TypedDict): + distance_matrix: np.typing.ArrayLike + num_walkers: int + variation: float + +class REVOResampler( + CloneMergeResampler, + Generic[DistanceMetric_, DistanceImage_, WalkerState_], +): r"""Resampler implementing the REVO algorithm. You can find more detailed information in the paper "REVO: @@ -94,15 +136,24 @@ class REVOResampler(CloneMergeResampler): """ - # fields for resampler data + distance_metric: DistanceMetric_ + merge_dist: float + char_dist: float + dist_exponent: int + weights: bool + merge_alg: MergeAlgorithm + pmin: float + pmax: float + seed: int | None + num_proc: int + lpmin: float + RESAMPLING_FIELDS = CloneMergeResampler.RESAMPLING_FIELDS - RESAMPLING_SHAPES = CloneMergeResampler.RESAMPLING_SHAPES # + (Ellipsis,) - RESAMPLING_DTYPES = CloneMergeResampler.RESAMPLING_DTYPES # + (np.int,) + RESAMPLING_SHAPES = CloneMergeResampler.RESAMPLING_SHAPES + RESAMPLING_DTYPES = CloneMergeResampler.RESAMPLING_DTYPES - # fields that can be used for a table like representation RESAMPLING_RECORD_FIELDS = CloneMergeResampler.RESAMPLING_RECORD_FIELDS - # fields for resampling data RESAMPLER_FIELDS = CloneMergeResampler.RESAMPLER_FIELDS + ( "num_walkers", "distance_matrix", @@ -126,18 +177,17 @@ class REVOResampler(CloneMergeResampler): def __init__( self, - merge_dist=None, - char_dist=None, - distance=None, - weights=True, - merge_alg="pairs", - pmin=1e-12, - pmax=0.1, - dist_exponent=4, - seed=None, - num_proc=1, - **kwargs, - ): + merge_dist: float, + char_dist: float, + distance: DistanceMetric_, + weights: bool, + merge_alg: MergeAlgorithm, + pmin: float, + pmax: float, + dist_exponent: int, + seed: int | None, + num_proc: int = 1, + ) -> None: """Constructor for the REVO Resampler. Parameters @@ -195,13 +245,8 @@ def __init__( pmax=pmax, min_num_walkers=Ellipsis, max_num_walkers=Ellipsis, - **kwargs, ) - assert merge_dist is not None, "Merge distance must be given." - assert distance is not None, "Distance object must be given." - assert char_dist is not None, "Characteristic distance value (d0) must be given" - # ln(probability_min) self.lpmin = np.log(self.pmin / 100) self.dist_exponent = dist_exponent @@ -226,7 +271,7 @@ def __init__( # setting the number of processors self.num_proc = num_proc - def _novelty(self, walker_weight, num_walker_copy): + def _novelty(self, walker_weight: float, num_walker_copy: int) -> float: """Calculates the novelty function value. Parameters @@ -244,19 +289,24 @@ def _novelty(self, walker_weight, num_walker_copy): """ - novelty = 0 + novelty = 0. if walker_weight > 0 and num_walker_copy > 0: if self.weights: novelty = np.log(walker_weight / num_walker_copy) - self.lpmin else: - novelty = 1 + novelty = 1. if novelty < 0: - novelty = 0 + novelty = 0. return novelty - def _calc_variation(self, walker_weights, num_walker_copies, distance_matrix): + def _calc_variation( + self, + walker_weights: list[float], + num_walker_copies: list[int], + distance_matrix: list[list[float]], + ) -> tuple[float, list[float]]: """Calculates the variation value. Parameters @@ -266,7 +316,7 @@ def _calc_variation(self, walker_weights, num_walker_copies, distance_matrix): num_walker_copies : list of int The number of copies of each walker. - 0 means the walker is not exists anymore. + 0 means the walker does not exist anymore. 1 means there is one of the this walker. >1 means it should be cloned to this number of walkers. @@ -293,7 +343,7 @@ def _calc_variation(self, walker_weights, num_walker_copies, distance_matrix): ) # the value to be optimized - variation = 0 + variation: float = 0. # the walker variation values (Vi values) walker_variations = np.zeros(num_walkers) @@ -322,7 +372,12 @@ def _calc_variation(self, walker_weights, num_walker_copies, distance_matrix): return variation, walker_variations - def _calc_variation_loss(self, walker_variation, weights, eligible_pairs): + def _calc_variation_loss( + self, + walker_variation: list[float], + weights: list[float], + eligible_pairs: list[tuple[int, int]], + ) -> tuple[int, int] | None: """Calculates the loss to variation through merging of eligible walkers. Parameters @@ -338,14 +393,14 @@ def _calc_variation_loss(self, walker_variation, weights, eligible_pairs): Returns ------- - variation_loss_list : tuple + variation_loss_list : tuple or None A tuple of the walker merge pair indicies that meet the criteria - for merging and minimize variation loss. + for merging and minimize variation loss. If none is found returns None """ v_loss_min = np.inf - min_loss_pair = () + min_loss_pair: tuple[int, int] | None = None for pair in eligible_pairs: walker_i = pair[0] @@ -367,8 +422,12 @@ def _calc_variation_loss(self, walker_variation, weights, eligible_pairs): return min_loss_pair def _find_eligible_merge_pairs( - self, weights, distance_matrix, max_var_idx, num_walker_copies - ): + self, + weights: list[float], + distance_matrix: list[list[float]], + max_var_idx: int, + num_walker_copies: list[int], + ) -> list[tuple[int, int]]: """Find pairs of walkers that are eligible to be merged. Parameters @@ -379,13 +438,14 @@ def _find_eligible_merge_pairs( distance_matrix : list of arraylike of shape (num_walkers) The distance between every walker according to the distance metric. - max_var_idx : float + max_var_idx : int The index of the walker that had the highest walker variance and is a candidate for cloning. - num_walker_copies : list of int The number of copies of each walker. - 0 means the walker is not exists anymore. - 1 means there is one of the this walker. >1 means it should be cloned to this number of walkers. + num_walker_copies : list of int + 0 means the walker does not exist anymore. + 1 means there is one of the this walker. + >1 means it should be cloned to this number of walkers. Returns ------- @@ -406,7 +466,15 @@ def _find_eligible_merge_pairs( return eligible_pairs - def decide(self, walker_weights, num_walker_copies, distance_matrix): + def decide( + self, + walker_weights: list[float], + num_walker_copies: list[int], + distance_matrix: list[list[float]], + ) -> tuple[ + list[CloneMergeDecisionRecord], + float, + ]: """Optimize the trajectory variation by making decisions for resampling. Parameters @@ -424,11 +492,11 @@ def decide(self, walker_weights, num_walker_copies, distance_matrix): Returns ------- - variation : float - The optimized value of the trajectory variation. resampling_data : list of dict of str: value The resampling records resulting from the decisions. + variation : float + The optimized value of the trajectory variation. """ num_walkers = len(walker_weights) @@ -448,10 +516,13 @@ def decide(self, walker_weights, num_walker_copies, distance_matrix): variations.append(variation) # maximize the variance through cloning and merging - logger.info("Starting variance optimization: {}".format(variation)) + logger.info(f"Starting variance optimization: {variation}") + _count = 1 productive = True while productive: + logger.info(f"Optimization iteration: {_count}") + _count += 1 productive = False # find min and max walker_variationss, alter new_amp @@ -481,12 +552,12 @@ def decide(self, walker_weights, num_walker_copies, distance_matrix): if len(max_tups) > 0: max_value, max_idx = max(max_tups) - merge_pair = [] + maybe_merge_pair: tuple[int, int] | None = None if self.merge_alg == "pairs": pot_merge_pairs = self._find_eligible_merge_pairs( new_walker_weights, distance_matrix, max_idx, new_num_walker_copies ) - merge_pair = self._calc_variation_loss( + maybe_merge_pair = self._calc_variation_loss( walker_variations, new_walker_weights, pot_merge_pairs ) elif self.merge_alg == "greedy": @@ -535,15 +606,15 @@ def decide(self, walker_weights, num_walker_copies, distance_matrix): # if any were found set this as the closewalk if len(closewalks_dists) > 0: closedist, closewalk = min(closewalks_dists) - merge_pair = [min_idx, closewalk] + maybe_merge_pair = (min_idx, closewalk) else: - raise ValueError("Unrecognized value for merge_alg in REVO") + raise ValueError(f"Unrecognized value for merge_alg: {self.merge_alg}") # did we find a suitable pair to merge? - if len(merge_pair) != 0: - min_idx = merge_pair[0] - closewalk = merge_pair[1] + if maybe_merge_pair is not None: + min_idx = maybe_merge_pair[0] + closewalk = maybe_merge_pair[1] # change new_amp tempsum = new_walker_weights[min_idx] + new_walker_weights[closewalk] @@ -620,21 +691,31 @@ def decide(self, walker_weights, num_walker_copies, distance_matrix): new_num_walker_copies[closewalk] = 1 new_num_walker_copies[max_idx] -= 1 - # given we know what we want to clone to specific slots - # (squashing other walkers) we need to determine where these - # squashed walkers will be merged - walker_actions = self.assign_clones(merge_groups, walker_clone_nums) + final_variation = variations[-1] + logger.info(f"Finished optimization: {final_variation}") + logger.info("Assigning clones") + walker_records = self.assign_clones(merge_groups, walker_clone_nums) + + # TOREV: this was taken out as it probably wasn't necessary, + # but this may be critical in analyses, check this + # because there is only one step in resampling here we just # add another field for the step as 0 and add the walker index # to its record as well - for walker_idx, walker_record in enumerate(walker_actions): - walker_record["step_idx"] = np.array([0]) - walker_record["walker_idx"] = np.array([walker_idx]) - - return walker_actions, variations[-1] - - def _all_to_all_distance(self, walkers): + # for walker_idx, walker_record in enumerate(walker_actions): + # walker_record["step_idx"] = np.array([0]) + # walker_record["walker_idx"] = np.array([walker_idx]) + + return walker_records, final_variation + + def _all_to_all_distance( + self, + walkers: list[Walker[WalkerState_]], + ) -> tuple[ + list[list[float]], + list[DistanceImage_], + ]: """Calculate the pairwise all-to-all distances between walkers. Parameters @@ -650,10 +731,21 @@ def _all_to_all_distance(self, walkers): """ # initialize an all-to-all matrix, with 0.0 for self distances - dist_mat = np.zeros((len(walkers), len(walkers))) + dist_mat = [ + [0. for _ in range(len(walkers))] + for _ + in range(len(walkers)) + ] + logger.info("Starting calculation of walker images") + start_time = time.time() + # make images for all the walker states for us to compute distances on if self.num_proc > 1: + logger.info(f"Multiple processes requested ({self.num_proc}) will run in Pool.") + + _distance_image = _ImageWrapper(self.distance.image) + # NOTE: Must use spawn here, otherwise there are problems # with deadlocking in the sub-processes @@ -661,25 +753,50 @@ def _all_to_all_distance(self, walkers): log_queue = mp_ctx.Queue() handlers = list(logging.getLogger().handlers) listener = logging.handlers.QueueListener(log_queue, *handlers) + logger.info("Starting log listener") listener.start() - # TODO: This should be part of some setup period - - with mp_ctx.Pool( - self.num_proc, - initializer=proc_pool_worker_setup, - initargs=(log_queue,), - # Set some upper bound so that it gets cleaned up - # in case of leaks - maxtasksperchild=4 - ) as pool: - images = pool.map(self.distance.image, [walker.state for walker in walkers]) + # TODO: This should be part of some setup phase + + logger.info("Starting multiprocessing.Pool") + with ( + queue_listener_context(log_queue), + mp_ctx.Pool( + self.num_proc, + initializer=proc_pool_worker_setup, + initargs=(log_queue,), + # Set some upper bound so that it gets cleaned up + # in case of leaks + maxtasksperchild=4 + ) as pool, + ): + + logger.info(f"Running parallel map calculation on {len(walkers)} walkers") + images = pool.map( + _distance_image, + [walker.state for walker in walkers], + ) + logger.info("Finished running parallel map calculation") + logger.info("Shutting down Pool") + + logger.info("Pool shutdown complete") + else: + logger.info("Calculating images without parallelism") images = [] for walker in walkers: image = self.distance.image(walker.state) images.append(image) + end_time = time.time() + + _image_time = end_time - start_time + + logger.info(f"Calculating walker state images took: {_image_time} s") + + logger.info("Calculating image distances") + start_time = time.time() + # get the combinations of indices for all walker pairs for i, j in it.combinations(range(len(images)), 2): # calculate the distance between the two walkers @@ -689,9 +806,22 @@ def _all_to_all_distance(self, walkers): dist_mat[i][j] = dist dist_mat[j][i] = dist - return [walker_dists for walker_dists in dist_mat], images + end_time = time.time() + + _dist_time = end_time - start_time + + logger.info(f"Calculating image distances took: {_dist_time} s") - def resample(self, walkers): + return dist_mat, images + + def resample( + self, + walkers: list[Walker[WalkerState_]], + ) -> tuple[ + list[Walker[WalkerState_]], + list[list[CloneMergeDecisionRecord]], + list[REVOResamplerResamplerData], + ]: """Resamples walkers based on REVO algorithm Parameters @@ -719,21 +849,29 @@ def resample(self, walkers): num_walker_copies = np.ones(num_walkers) # calculate distance matrix + logger.info("Calculating walker distances") distance_matrix, images = self._all_to_all_distance(walkers) + logger.info("Finished calculating distances") - logger.info("distance_matrix") + logger.info("Distance_matrix: ") logger.info("\n{}".format(str(np.array(distance_matrix)))) # determine cloning and merging actions to be performed, by # maximizing the variation, i.e. the Decider + logger.info("Making resampling decisions") resampling_data, variation = self.decide( walker_weights, num_walker_copies, distance_matrix ) + logger.info("Finished resampling decisions") + + # TOREV: need to understand the impact of this on the data + # ingestion aspect of things. Otherwise not doing this here + # would be much cleaner. - # convert the target idxs and decision_id to feature vector arrays - for record in resampling_data: - record["target_idxs"] = np.array(record["target_idxs"]) - record["decision_id"] = np.array([record["decision_id"]]) + # # convert the target idxs and decision_id to feature vector arrays + # for record in resampling_data: + # record["target_idxs"] = np.array(record["target_idxs"]) + # record["decision_id"] = np.array([record["decision_id"]]) # actually do the cloning and merging of the walkers resampled_walkers = self.DECISION.action(walkers, [resampling_data]) @@ -743,9 +881,49 @@ def resample(self, walkers): resampler_data = [ { "distance_matrix": np.ravel(np.array(distance_matrix)), - "num_walkers": np.array([len(walkers)]), - "variation": np.array([variation]), + "num_walkers": len(walkers), + "variation": variation, } ] - + + # TOREV: ditto, wrt to data interfaces + # resampler_data = [ + # { + # "distance_matrix": np.ravel(np.array(distance_matrix)), + # "num_walkers": np.array([len(walkers)]), + # "variation": np.array([variation]), + # } + # ] + return resampled_walkers, resampling_data, resampler_data + +@attrs.define +class REVOResamplerFactory(Generic[DistanceMetric_]): + + distance_metric: DistanceMetric_ + merge_dist: float + char_dist: float + dist_exponent: int = 4 + weights: bool = True + merge_alg: MergeAlgorithm = "pairs" + pmin: float = 1e-12 + pmax: float = 0.1 + seed: int | None = None + + def __call__( + self, + num_cores: int, + ) -> REVOResampler: + + return REVOResampler( + distance=self.distance_metric, + merge_dist=self.merge_dist, + char_dist=self.char_dist, + dist_exponent=self.dist_exponent, + weights=self.weights, + merge_alg=self.merge_alg, + pmin=self.pmin, + pmax=self.pmax, + seed=self.seed, + num_proc=num_cores, + ) diff --git a/tests/unit/test_resampling/test_resamplers/test_revo.py b/tests/unit/test_resampling/test_resamplers/test_revo.py new file mode 100644 index 00000000..a3586d66 --- /dev/null +++ b/tests/unit/test_resampling/test_resamplers/test_revo.py @@ -0,0 +1,319 @@ +import numpy as np +import pickle +from wepy.runners.mock import MockState +from wepy.resampling.distances.mock import MockDistance +from wepy.resampling.resamplers.revo import REVOResampler, REVOResamplerFactory, _ImageWrapper +from wepy.walker import Walker + +def test__ImageWrapper(): + + wrapped = _ImageWrapper(MockDistance().image) + + assert wrapped(MockState(1)) == MockState(1) + + # check that it is pickleable for sending to subprocesses + pickle.loads(pickle.dumps(wrapped)) + +class Test_REVOResamplerFactory: + + resampler = REVOResamplerFactory( + distance_metric=MockDistance(), + merge_dist=1, + char_dist=1, + ) + +class Test_REVOResampler: + + def test___init__(self): + + resampler = REVOResampler( + merge_dist=1.0, + char_dist=1.0, + dist_exponent=3, + distance=MockDistance(), + weights=True, + merge_alg="pairs", + pmin=0.1, + pmax=0.4, + seed=1, + num_proc=1, + ) + + assert resampler.seed == 1 + assert np.isclose(resampler.lpmin, np.log(0.1 / 100)) + + assert REVOResampler( + merge_dist=1.0, + char_dist=1.0, + dist_exponent=3, + distance=MockDistance(), + weights=True, + merge_alg="pairs", + pmin=0.1, + pmax=0.4, + seed=None, + num_proc=1, + ).seed is None + + + def test__novelty(self): + + resampler = REVOResampler( + merge_dist=1.0, + char_dist=1.0, + dist_exponent=3, + distance=MockDistance(), + weights=False, + merge_alg="pairs", + pmin=0.1, + pmax=0.4, + seed=1, + num_proc=1, + ) + + # UGLY,TOREV: there shouldn't be the possibility of negatives + # of these values but current code accepts them. + assert resampler._novelty(-1, 1) == 0. + assert resampler._novelty(0.1, -1) == 0. + assert resampler._novelty(0., 0) == 0. + assert resampler._novelty(0.1, 0) == 0. + assert resampler._novelty(0., 1) == 0. + + assert resampler._novelty(0.1, 1) == 1. + + # with weights + resampler = REVOResampler( + merge_dist=1.0, + char_dist=1.0, + dist_exponent=3, + distance=MockDistance(), + weights=True, + merge_alg="pairs", + pmin=0.1, + pmax=0.4, + seed=1, + num_proc=1, + ) + + assert resampler._novelty(.4, 1) > 0. + assert resampler._novelty(.1, 1) > 0. + + assert np.isclose(resampler._novelty(.4, 1000), 0.) + + def test__calc_variation(self): + + resampler = REVOResampler( + merge_dist=1.0, + char_dist=1.0, + dist_exponent=3, + distance=MockDistance(), + weights=False, + merge_alg="pairs", + pmin=0.1, + pmax=0.4, + seed=1, + num_proc=1, + ) + + variation, walker_variations = resampler._calc_variation( + [0.4, 0.1], + [1, 1], + [ + [0., 1.,], + [1., 0.,] + ], + ) + + def test__calc_variation_loss(self): + + resampler = REVOResampler( + merge_dist=1.0, + char_dist=1.0, + dist_exponent=3, + distance=MockDistance(), + weights=False, + merge_alg="pairs", + pmin=0.1, + pmax=0.4, + seed=1, + num_proc=1, + ) + + # only one option + assert resampler._calc_variation_loss( + [0.1, 0.4, 0.3], + [0.01, 0.02, 0.03], + [ + (0, 1), + ], + ) == (0, 1) + + resampler._calc_variation_loss( + [0.1, 0.4, 0.3], + [0.01, 0.02, 0.03], + [ + (0, 1), + (1, 2), + ], + ) + + # if no suitable pairs are found returns None + assert resampler._calc_variation_loss( + [0.1, 0.4, 0.3], + [0.01, 0.02, 0.03], + [], + ) is None + + def test__find_eligible_merge_pairs(self): + + resampler = REVOResampler( + merge_dist=2.0, + char_dist=1.0, + dist_exponent=3, + distance=MockDistance(), + weights=False, + merge_alg="pairs", + pmin=0.1, + pmax=0.9, + seed=1, + num_proc=1, + ) + + # TODO: figure out some combinations of outputs that generates + # some eligible pairs + assert resampler._find_eligible_merge_pairs( + [0.1, 0.4, 0.3], + [ + [0., 1., 2.], + [1., 0., 1.5], + [2., 1.5, 0.], + ], + 2, + [2, 2, 2], + ) == [] + + def test_decide(self): + + resampler = REVOResampler( + merge_dist=2.0, + char_dist=1.0, + dist_exponent=3, + distance=MockDistance(), + weights=False, + merge_alg="pairs", + pmin=0.1, + pmax=0.9, + seed=1, + num_proc=1, + ) + + recs, variation = resampler.decide( + [0.1, 0.4, 0.3], + [1, 1, 1], + [ + [0., 1., 2.], + [1., 0., 1.5], + [2., 1.5, 0.], + ], + ) + + def test__all_to_all_distance(self): + + resampler = REVOResampler( + merge_dist=2.0, + char_dist=1.0, + dist_exponent=3, + distance=MockDistance(), + weights=False, + merge_alg="pairs", + pmin=0.1, + pmax=0.9, + seed=1, + num_proc=1, + ) + + assert resampler._all_to_all_distance( + [ + Walker( + MockState(0), + 0.1, + ), + Walker( + MockState(2), + 0.1, + ), + ] + ) == ( + [ + [0., 2.], + [2., 0.], + ], + [ + MockState(0), + MockState(2) + ] + ) + + # test with pool + resampler = REVOResampler( + merge_dist=2.0, + char_dist=1.0, + dist_exponent=3, + distance=MockDistance(), + weights=False, + merge_alg="pairs", + pmin=0.1, + pmax=0.9, + seed=1, + num_proc=2, + ) + + assert resampler._all_to_all_distance( + [ + Walker( + MockState(0), + 0.1, + ), + Walker( + MockState(2), + 0.1, + ), + ] + ) == ( + [ + [0., 2.], + [2., 0.], + ], + [ + MockState(0), + MockState(2) + ] + ) + + def test_resample(self): + + resampler = REVOResampler( + merge_dist=1.0, + char_dist=1.0, + dist_exponent=3, + distance=MockDistance(), + weights=True, + merge_alg="pairs", + pmin=0.1, + pmax=0.4, + seed=1, + num_proc=1, + ) + + resampled_walkers, resampling_data, resampler_data = resampler.resample([ + Walker( + MockState(1), + 0.1, + ), + Walker( + MockState(2), + 0.1, + ), + ]) + + assert len(resampled_walkers) == 2 From 89bdfc99a3faa65a34eeacfe34206ee9bece54fc Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 19:19:30 -0500 Subject: [PATCH 084/143] resurrect the OpenMM Serial work mapper --- src/wepy/work_mapper/openmm/serial.py | 78 +++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/wepy/work_mapper/openmm/serial.py diff --git a/src/wepy/work_mapper/openmm/serial.py b/src/wepy/work_mapper/openmm/serial.py new file mode 100644 index 00000000..12a6c641 --- /dev/null +++ b/src/wepy/work_mapper/openmm/serial.py @@ -0,0 +1,78 @@ +import logging +from typing import Literal, Any, Callable +import time +import itertools + +import attrs + +from wepy.work_mapper.base import WorkMapper +from wepy.runners.openmm import OpenMMState, OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS + + +class OpenMMSerialWorkMapper(WorkMapper): + + def __init__( + self, + platform: OpenMMPlatformName, + global_platform_properties: dict[str, str] | None = None, + ) -> None: + self._worker_segment_times: dict[int, list[float]] = {0: []} + + self._platform = platform + self._global_platform_properties = global_platform_properties if global_platform_properties is not None else {} + + def get_worker_segment_times(self) -> dict[int, list[float]]: + """The run timings for each segment for each walker. + + Returns + ------- + worker_seg_times : Dictionary mapping worker indices to a list of times in + seconds for each segment run. + + """ + return self._worker_segment_times + + def init(self) -> None: + pass + + def cleanup(self) -> None: + pass + + def map( + self, + task: Callable[[OpenMMState, int], OpenMMState], + walker_states: list[OpenMMState], + segment_lengths: list[int], + ) -> list[OpenMMState]: + + segment_times: list[float] = [] + results: list[OpenMMState] = [] + for task_idx, task_args in enumerate(zip(walker_states, segment_lengths, strict=True)): + + tic = time.time() + result = task( + *task_args, + platform_name=self._platform, + platform_kwargs=self._global_platform_properties, + ) + toc = time.time() + + segment_times.append(toc - tic) + results.append(result) + + self._worker_segment_times[0] = segment_times + + return results + +@attrs.define +class OpenMMSerialWorkMapperFactory: + + platform: OpenMMPlatformName + global_platform_properties: dict[str, str] | None = None + + def __call__(self) -> OpenMMSerialWorkMapper: + + return OpenMMSerialWorkMapper( + platform=self.platform, + global_platform_properties=self.global_platform_properties, + ) From 82eff985d17032ef68bfb368edeabf0e43af421a Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 19:19:43 -0500 Subject: [PATCH 085/143] add pytest plugins to help with flaky tests Anything with a pool can be flaky. These plugins help with that. 1. Set a timeout on the flaky test that might hang with `pytest-timeout`, this also helps in debugging with thread dumps 2. `pytest-rerunfailures` to rerun failed tests 3. `pytest-flakefinder` to run tests many times to get a failure which is stochastic. --- pyproject.toml | 11 ++++++++++- uv.lock | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 00b14553..e23d6ef2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,9 @@ dev = [ "pytest-check", "pytest-datadir @ git+https://github.com/salotz/pytest-datadir-extras.git@41e9a1e94ba27efe28ce9d0019b1e0a73be34400", "pytest-print", + "pytest-timeout", + "pytest-rerunfailures", + "pytest-flakefinder", # for testing openmm wrappers "lxml", "psutil", @@ -121,7 +124,13 @@ verbose = 2 minversion = "9.0" strict = true -addopts = ["--import-mode=importlib"] +addopts = [ + "--import-mode=importlib", + "--timeout_method=thread", + # Global timeout just in case something goes wrong. You should set + # specific timeouts for known problematic tests + "--timeout=300", +] python_classes = ["Test*", "Test_*"] python_functions = ["test_*"] diff --git a/uv.lock b/uv.lock index fd55971d..7087adc1 100644 --- a/uv.lock +++ b/uv.lock @@ -2217,6 +2217,18 @@ dependencies = [ { name = "pytest" }, ] +[[package]] +name = "pytest-flakefinder" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/53/69c56a93ea057895b5761c5318455804873a6cd9d796d7c55d41c2358125/pytest-flakefinder-1.1.0.tar.gz", hash = "sha256:e2412a1920bdb8e7908783b20b3d57e9dad590cc39a93e8596ffdd493b403e0e", size = 6795, upload-time = "2022-10-26T18:27:54.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8b/06787150d0fd0cbd3a8054262b56f91631c7778c1bc91bf4637e47f909ad/pytest_flakefinder-1.1.0-py2.py3-none-any.whl", hash = "sha256:741e0e8eea427052f5b8c89c2b3c3019a50c39a59ce4df6a305a2c2d9ba2bd13", size = 4644, upload-time = "2022-10-26T18:27:52.128Z" }, +] + [[package]] name = "pytest-print" version = "1.2.0" @@ -2229,6 +2241,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4c/80/02c87f95140e3621dde9a599c0b5684464f16acc0786ab8c8c5e929330c2/pytest_print-1.2.0-py3-none-any.whl", hash = "sha256:12969d579e72d549b71a68cef8a2d10ec47afdbaf92e32c8777d5d93d5a1cd42", size = 7151, upload-time = "2025-10-09T19:19:54.924Z" }, ] +[[package]] +name = "pytest-rerunfailures" +version = "16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/04/71e9520551fc8fe2cf5c1a1842e4e600265b0815f2016b7c27ec85688682/pytest_rerunfailures-16.1.tar.gz", hash = "sha256:c38b266db8a808953ebd71ac25c381cb1981a78ff9340a14bcb9f1b9bff1899e", size = 30889, upload-time = "2025-10-10T07:06:01.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/54/60eabb34445e3db3d3d874dc1dfa72751bfec3265bd611cb13c8b290adea/pytest_rerunfailures-16.1-py3-none-any.whl", hash = "sha256:5d11b12c0ca9a1665b5054052fcc1084f8deadd9328962745ef6b04e26382e86", size = 14093, upload-time = "2025-10-10T07:06:00.019Z" }, +] + [[package]] name = "pytest-shutil" version = "1.8.1" @@ -2244,6 +2269,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/17/1b161657385982134b723dcd463483d498e54e286c5632d42602407e606f/pytest_shutil-1.8.1-py3-none-any.whl", hash = "sha256:0793e347e07b9296d814ce33377ed348ce376ffac76c0e57b28bf84235499f51", size = 15948, upload-time = "2024-11-29T19:33:26.349Z" }, ] +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -3041,8 +3078,11 @@ dev = [ { name = "pytest-check" }, { name = "pytest-cov" }, { name = "pytest-datadir" }, + { name = "pytest-flakefinder" }, { name = "pytest-print" }, + { name = "pytest-rerunfailures" }, { name = "pytest-shutil" }, + { name = "pytest-timeout" }, { name = "ruff" }, { name = "sphinx" }, { name = "sphinxcontrib-bibtex" }, @@ -3094,8 +3134,11 @@ dev = [ { name = "pytest-check" }, { name = "pytest-cov" }, { name = "pytest-datadir", git = "https://github.com/salotz/pytest-datadir-extras.git?rev=41e9a1e94ba27efe28ce9d0019b1e0a73be34400" }, + { name = "pytest-flakefinder" }, { name = "pytest-print" }, + { name = "pytest-rerunfailures" }, { name = "pytest-shutil" }, + { name = "pytest-timeout" }, { name = "ruff" }, { name = "sphinx" }, { name = "sphinxcontrib-bibtex" }, From 9a7dcae249c071b580688044d43e705d66461f43 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 19:21:21 -0500 Subject: [PATCH 086/143] fixups --- src/wepy/work_mapper/openmm/__init__.py | 2 ++ src/wepy_tools/systems/alanine_dipeptide.py | 2 +- src/wepy_tools/systems/lennard_jones.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wepy/work_mapper/openmm/__init__.py b/src/wepy/work_mapper/openmm/__init__.py index 7413913b..3c17b238 100644 --- a/src/wepy/work_mapper/openmm/__init__.py +++ b/src/wepy/work_mapper/openmm/__init__.py @@ -1,5 +1,7 @@ from .proc_pool import OpenMMProcPoolWorkMapperFactory +from .serial import OpenMMSerialWorkMapperFactory __all__ = [ "OpenMMProcPoolWorkMapperFactory", + "OpenMMSerialWorkMapperFactory", ] diff --git a/src/wepy_tools/systems/alanine_dipeptide.py b/src/wepy_tools/systems/alanine_dipeptide.py index 50a1e6ba..14e3b167 100644 --- a/src/wepy_tools/systems/alanine_dipeptide.py +++ b/src/wepy_tools/systems/alanine_dipeptide.py @@ -11,7 +11,7 @@ from wepy.runners.openmm import OpenMMState, OpenMMStateWrapper from wepy.walker import WalkerState from wepy.runners.openmm import OpenMMState -from wepy.resampling.distances.distance import Distance +from wepy.resampling.distances.base import Distance from wepy.util.mdtraj import traj_fields_to_mdtraj, json_to_mdtraj_topology class AlanineDipeptideExplicitSystem: diff --git a/src/wepy_tools/systems/lennard_jones.py b/src/wepy_tools/systems/lennard_jones.py index 1f61692b..516274a3 100644 --- a/src/wepy_tools/systems/lennard_jones.py +++ b/src/wepy_tools/systems/lennard_jones.py @@ -8,7 +8,7 @@ import attrs # First Party Library -from wepy.resampling.distances.distance import Distance +from wepy.resampling.distances.base import Distance from wepy.runners.openmm import OpenMMState class LennardJonesPair: From 2456b6472003bcaa04c5059c66298ec2418405b9 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 19:21:50 -0500 Subject: [PATCH 087/143] fix pool hanging issue Move most of the logging Queue functionality into the same context manager. Also clean up the Queue explicitly to avoid deadlocks and hanging pool closure. --- src/wepy/resampling/resamplers/revo.py | 8 +----- src/wepy/util/multiprocessing.py | 27 ++++++++++++++++--- src/wepy/work_mapper/openmm/proc_pool.py | 8 +----- .../test_resamplers/test_revo.py | 5 ++++ 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 329af616..30085b5e 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -750,17 +750,11 @@ def _all_to_all_distance( # NOTE: Must use spawn here, otherwise there are problems # with deadlocking in the sub-processes mp_ctx = mp.get_context(method="spawn") - log_queue = mp_ctx.Queue() - handlers = list(logging.getLogger().handlers) - listener = logging.handlers.QueueListener(log_queue, *handlers) - logger.info("Starting log listener") - listener.start() # TODO: This should be part of some setup phase - logger.info("Starting multiprocessing.Pool") with ( - queue_listener_context(log_queue), + queue_listener_context(mp_ctx) as log_queue, mp_ctx.Pool( self.num_proc, initializer=proc_pool_worker_setup, diff --git a/src/wepy/util/multiprocessing.py b/src/wepy/util/multiprocessing.py index c9d07671..61b09a70 100644 --- a/src/wepy/util/multiprocessing.py +++ b/src/wepy/util/multiprocessing.py @@ -100,12 +100,13 @@ def format(self, record): return formatted @contextlib.contextmanager -def queue_listener_context(log_queue: mp.Queue) -> Generator[None, None, None]: +def queue_listener_context(mp_ctx) -> Generator[None, None, None]: + logger.info("Setting up queue logging infrastructure") root_logger = logging.getLogger() - old_factory = logging.getLogRecordFactory() + def record_factory(*args, **kwargs): record = old_factory(*args, **kwargs) @@ -127,15 +128,35 @@ def record_factory(*args, **kwargs): new_handler.setFormatter(WorkerFormatter(handler.formatter)) listener_handlers.append(new_handler) + logger.info("Starting Queue") + log_queue = mp_ctx.Queue() listener = logging.handlers.QueueListener(log_queue, *listener_handlers) + logger.info("Starting QueueListener") listener.start() + logger.info("Listener started") try: - yield + yield log_queue finally: + logger.info("Shutting down log listener resources") + + logger.info("Stopping listener") listener.stop() + logger.info("Listener stopped") for handler, formatter in zip(root_logger.handlers, old_formatters, strict=True): handler.setFormatter(formatter) logging.setLogRecordFactory(old_factory) + + logger.info("Closing logging Queue") + try: + log_queue.close() + log_queue.join_thread() + except Exception as exc: + logger.error(f"Exception in log Queue closing, continuing: {exc}") + pass + except: + logger.info("Logger Queue shut down cleanly") + + logger.info("Finished context cleanup") diff --git a/src/wepy/work_mapper/openmm/proc_pool.py b/src/wepy/work_mapper/openmm/proc_pool.py index c0062a01..05b0cb03 100644 --- a/src/wepy/work_mapper/openmm/proc_pool.py +++ b/src/wepy/work_mapper/openmm/proc_pool.py @@ -93,14 +93,8 @@ def map( # spin up a new pool for each map logger.info(f"Starting process Pool with {self._num_procs}") - # set up the log queue and listener for getting logs from - # processes - log_queue = self._mp_ctx.Queue() - # handler = logging.StreamHandler() - - with ( - queue_listener_context(log_queue), + queue_listener_context(self._mp_ctx) as log_queue, self._mp_ctx.Pool( processes=self._num_procs, # only run one thing per task, just to make sure diff --git a/tests/unit/test_resampling/test_resamplers/test_revo.py b/tests/unit/test_resampling/test_resamplers/test_revo.py index a3586d66..7e6f01d7 100644 --- a/tests/unit/test_resampling/test_resamplers/test_revo.py +++ b/tests/unit/test_resampling/test_resamplers/test_revo.py @@ -1,3 +1,4 @@ +import pytest import numpy as np import pickle from wepy.runners.mock import MockState @@ -254,6 +255,10 @@ def test__all_to_all_distance(self): ] ) + @pytest.mark.flaky(reruns=4) + @pytest.mark.timeout(5) + def test__all_to_all_distance_pool(self): + # test with pool resampler = REVOResampler( merge_dist=2.0, From dc2dc9628e56af84d8412eeafbdb9cbd1a022a94 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 19:22:38 -0500 Subject: [PATCH 088/143] update sim_manager for resampler factory --- src/wepy/sim_manager.py | 83 ++++++++++++++++++++++------------ tests/unit/test_sim_manager.py | 16 +++---- 2 files changed, 63 insertions(+), 36 deletions(-) diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index af2d4365..439fd02c 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -49,6 +49,7 @@ import time import enum +import psutil import attrs from immutables import Map as frozenmap @@ -305,8 +306,10 @@ def send(self, event: ManagerEvent) -> ManagerStatus: self.state = new_state return self.state - - + +ResamplerFactory = Callable[[], Resampler] +WorkMapperFactory = Callable[[], WorkMapper] + State_ = TypeVar("State_") RunSegmentData_ = TypeVar("RunSegmentData_", bound=RunSegmentData, covariant=True) class Manager(Generic[State_]): @@ -345,13 +348,20 @@ class Manager(Generic[State_]): init_walkers: list[Walker[State_]] n_init_walkers: int runner_factory: RunnerFactory - resampler: Resampler + resampler_factory: ResamplerFactory boundary_conditions: BoundaryConditions | None work_mapper_factory: type[WorkMapper] - work_mapper: WorkMapper reporters: list[Reporter] monitor: Monitor | None + num_cores: int + + _runner: Runner | None + _resampler: Resampler | None + _work_mapper: WorkMapper | None + + _last_report: CycleReportDict | None + REPORT_ITEM_KEYS: Final[tuple[str, ...]] = ( "cycle_idx", "n_segment_steps", @@ -376,11 +386,12 @@ def __init__( self, init_walkers: list[Walker[State_]], runner_factory: RunnerFactory, - resampler: Resampler, - work_mapper_factory: Callable[[], WorkMapper] | None = None, + resampler_factory: ResamplerFactory, + work_mapper_factory: WorkMapperFactory | None = None, boundary_conditions: BoundaryConditions | None = None, reporters: list[Reporter] | None = None, sim_monitor: Monitor | None = None, + num_cores: int | None = None, ) -> None: """Constructor for Manager. @@ -396,8 +407,8 @@ def __init__( instantiated by simulation manager. If None will default to a serial mapper. - resampler : object implementing the Resampler interface - The resampler to be used in the simulation + resampler_factory : Callable that instantiates a stateful + Resampler used in the simulation. boundary_conditions : object implementing BoundaryCondition interface, optional The boundary conditions to apply to walkers @@ -428,7 +439,7 @@ def __init__( # the runner is the object that runs dynamics self.runner_factory = runner_factory # the resampler - self.resampler = copy.deepcopy(resampler) + self.resampler_factory = resampler_factory # object for boundary conditions self.boundary_conditions = copy.deepcopy(boundary_conditions) @@ -446,10 +457,23 @@ def __init__( ## Monitor self.monitor = sim_monitor + # figure out how many cores we have at our disposal if not + # already given + if num_cores is None: + self.num_cores = len(psutil.Process().cpu_affinity()) + + else: + self.num_cores = num_cores + # used to have a record of the last report for the simulation # monitor without breaking the API. Ugly but I don't want to # break it and no one cares about this anyhow - self._last_report: CycleReportDict | None = None + self._last_report = None + + # initialize the uncreated attributes + self._runner = None + self._resampler = None + self._work_mapper = None self.state_machine = ManagerStateMachine() @@ -502,9 +526,12 @@ def init( logger.info("Running sim_manager.init hooks") logger.info("Generating runner from factory") - self.runner = self.runner_factory() + self._runner = self.runner_factory() logger.info("Running runner.init hook") - self.runner.init() + self._runner.init() + + # initialize resampler + self._resampler = self.resampler_factory(num_cores=self.num_cores) # initialize the monitoring object @@ -518,9 +545,9 @@ def init( # mapping and the number of workers, this may include things like starting processes # etc. logger.info("Instantiating work mapper") - self.work_mapper = self.work_mapper_factory() + self._work_mapper = self.work_mapper_factory() logger.info("Running WorkMapper.init hook") - self.work_mapper.init() + self._work_mapper.init() logger.info("Finished WorkMapper.init hook") # init the reporter @@ -528,10 +555,10 @@ def init( logger.info(f"Initializing reporter: {reporter}") reporter.init( init_walkers=self.init_walkers, - runner=self.runner, - resampler=self.resampler, + runner=self._runner, + resampler=self._resampler, boundary_conditions=self.boundary_conditions, - work_mapper=self.work_mapper, + work_mapper=self._work_mapper, reporters=self.reporters, continue_run=continue_run, ) @@ -570,15 +597,15 @@ def cleanup(self) -> None: # cleanup the mapper logger.info("Cleaning up work_mapper") - self.work_mapper.cleanup() + self._work_mapper.cleanup() # cleanup things associated with the reporter for reporter in self.reporters: logger.info(f"Cleaning up reporter: {reporter}") reporter.cleanup( - runner=self.runner, - work_mapper=self.work_mapper, - resampler=self.resampler, + runner=self._runner, + work_mapper=self._work_mapper, + resampler=self._resampler, boundary_conditions=self.boundary_conditions, reporters=self.reporters, ) @@ -621,8 +648,8 @@ def run_segment( segment_lengths = [segment_length for i in range(len(states))] try: map_results = list( - self.work_mapper.map( - self.runner.run_segment, + self._work_mapper.map( + self._runner.run_segment, states, segment_lengths, ) @@ -648,12 +675,12 @@ def run_segment( def pre_segment(self) -> None: self.state_machine.send(ManagerEvent.START_PRE_SEGMENT) - self.runner.pre_cycle() + self._runner.pre_cycle() self.state_machine.send(ManagerEvent.FINISH_PRE_SEGMENT) def post_segment(self, segments_data: RunSegmentData_) -> None: self.state_machine.send(ManagerEvent.START_POST_SEGMENT) - self.runner.post_cycle(segments_data) + self._runner.post_cycle(segments_data) self.state_machine.send(ManagerEvent.FINISH_POST_SEGMENT) @@ -810,7 +837,7 @@ def run_cycle( self.state_machine.send(ManagerEvent.START_RESAMPLING) start = time.time() - resampling_results = self.resampler.resample(warped_walkers) + resampling_results = self._resampler.resample(warped_walkers) self.state_machine.send(ManagerEvent.FINISH_RESAMPLING) @@ -829,7 +856,7 @@ def run_cycle( seg_times = {} sampling_time = None - if (seg_times := self.work_mapper.get_worker_segment_times()) is not None: + if (seg_times := self._work_mapper.get_worker_segment_times()) is not None: logger.info("Segment timings provided by work mapper, recording.") # count up the total sampling time from the segments @@ -893,7 +920,7 @@ def run_cycle( self.state_machine.send(ManagerEvent.FINISH_CYCLE) - return resampled_walkers, (self.runner, self.boundary_conditions, self.resampler) + return resampled_walkers, (self._runner, self.boundary_conditions, self._resampler) def run_simulation( self, diff --git a/tests/unit/test_sim_manager.py b/tests/unit/test_sim_manager.py index 071fed21..472904f0 100644 --- a/tests/unit/test_sim_manager.py +++ b/tests/unit/test_sim_manager.py @@ -21,7 +21,7 @@ def sim_components() -> tuple[ list[Walker], MockRunnerFactory, - NoResampler, + type[NoResampler], ]: num_walkers = 4 @@ -36,7 +36,7 @@ def sim_components() -> tuple[ in range(num_walkers) ] - return init_walkers, MockRunnerFactory(fail=False), NoResampler() + return init_walkers, MockRunnerFactory(fail=False), NoResampler class Test_ManagerStateMachine: @@ -75,7 +75,7 @@ def test___init__(self, sim_components): assert manager.status == ManagerStatus.CONSTRUCTED assert len(manager.reporters) == 0 assert manager.work_mapper_factory == SerialMapper - assert not hasattr(manager, "work_mapper") + assert manager._work_mapper is None def test_init(self, sim_components): @@ -88,8 +88,8 @@ def test_init(self, sim_components): manager.init() assert manager.status == ManagerStatus.INITIALIZED - assert hasattr(manager, "work_mapper") - assert manager.runner.status == RunnerStatus.INITIALIZED + assert manager._work_mapper is not None + assert manager._runner.status == RunnerStatus.INITIALIZED with pytest.raises(ManagerStateTransitionError): manager.init() @@ -111,7 +111,7 @@ def test_pre_segment(self, sim_components): manager.pre_segment() assert manager.status == ManagerStatus.PRE_SEGMENT_FINISHED - assert manager.runner.status == RunnerStatus.PRE_CYCLE + assert manager._runner.status == RunnerStatus.PRE_CYCLE def test_post_segment(self, sim_components): @@ -141,7 +141,7 @@ def test_post_segment(self, sim_components): ]) assert manager.status == ManagerStatus.POST_SEGMENT_FINISHED - assert manager.runner.status == RunnerStatus.POST_CYCLE + assert manager._runner.status == RunnerStatus.POST_CYCLE def test_cleanup(self, sim_components): @@ -154,7 +154,7 @@ def test_cleanup(self, sim_components): manager.state_machine.send(ManagerEvent.START_PRE_SIM) manager.init() - assert manager.runner.status == RunnerStatus.INITIALIZED + assert manager._runner.status == RunnerStatus.INITIALIZED manager.cleanup() with pytest.raises(ManagerStateTransitionError): From 3f131027697bb545bc6326fffeaffbb7f7027f9c Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 19:22:49 -0500 Subject: [PATCH 089/143] update integration tests --- .../integration/test_openmm/test_realistic.py | 71 +++++++++---------- .../test_openmm/test_sim_manager.py | 15 ++-- 2 files changed, 40 insertions(+), 46 deletions(-) diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index 9b29fc32..54155fc9 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -13,33 +13,40 @@ import mdtraj # TODO: use the high-level API imports from wepy.walker import Walker -from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState, HeartBeatLoggingReporterFactory, OpenMMStateWrapper +from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState, OpenMMStateWrapper +from wepy.runners.openmm.runner import _DEFAULT_STATE_TIME_INTERVAL, _DEFAULT_HEARTBEAT_INTERVAL from wepy.sim_manager import Manager from wepy.work_mapper.openmm import OpenMMProcPoolWorkMapperFactory -from wepy.resampling.resamplers.revo import REVOResampler +from wepy.resampling.resamplers.revo import REVOResamplerFactory from wepy.util.mdtraj import mdtraj_to_json_topology -from wepy.runners.openmm.logger import HeartBeatLoggingReporter from wepy.resampling.resamplers.noresampler import NoResampler from wepy.util.mdtraj import json_to_mdtraj_topology from wepy_tools.systems.lennard_jones import LennardJonesPair, PairDistance from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideRamachandranDistance, AlanineDipeptideExplicitSystem +STEP_SIZE = 2. * openmm.unit.femtosecond +TEMPERATURE = 300. * openmm.unit.kelvin + +# minimum number of steps to hit the logging reporters, useful just +# for testing the defaults +TIME_INTERVAL_STEPS = round(_DEFAULT_STATE_TIME_INTERVAL / STEP_SIZE) +MIN_INTERVAL_STEPS = ( + TIME_INTERVAL_STEPS + if TIME_INTERVAL_STEPS > _DEFAULT_HEARTBEAT_INTERVAL + else _DEFAULT_HEARTBEAT_INTERVAL + ) + def test_lennard_jones_revo_procpool(): test_sys = LennardJonesPair() - integrator = openmm.LangevinIntegrator(300.0, 0.1, 0.002) + integrator = openmm.LangevinIntegrator(TEMPERATURE, 0.1, STEP_SIZE) runner_factory = OpenMMRunnerFactory( system=test_sys.system, topology=test_sys.topology, integrator=integrator, - # For this test we do want heart beat at shorter interval - openmm_reporter_factories=[ - # heart beat every step - HeartBeatLoggingReporterFactory(step_interval=2) - ] ) num_walkers = 4 @@ -82,43 +89,38 @@ def test_lennard_jones_revo_procpool(): distance_metric = PairDistance() - resampler = REVOResampler( + resampler_factory = REVOResamplerFactory( merge_dist=4, char_dist=0.1, - distance=distance_metric, - num_proc=num_workers, - # num_proc=1, + distance_metric=distance_metric, ) sim_manager = Manager( init_walkers=init_walkers, runner_factory=runner_factory, - # DEBUG - # resampler=resampler, - resampler=NoResampler(), + resampler_factory=resampler_factory, + # resampler_factory=NoResampler, work_mapper_factory=OpenMMProcPoolWorkMapperFactory( # DEBUG - platform="Reference", - num_procs=1, - # platform="CPU", - # num_procs=num_workers, + # platform="Reference", + # num_procs=1, + platform="CPU", + num_procs=num_workers, # global_platform_properties={"Threads" : "1"}, - # # global_platform_properties={"Threads" : str(cores_per_worker)}, + global_platform_properties={"Threads" : str(cores_per_worker)}, ), ) new_walkers, sim_components = sim_manager.run_simulation( - n_cycles=1, - segment_lengths=10, + n_cycles=2, + segment_lengths=MIN_INTERVAL_STEPS * 2 + 10, ) def test_alanine_dipeptide_revo_procpool(): - TEMPERATURE = 300. * openmm.unit.kelvin - ala_sys = AlanineDipeptideExplicitSystem() - integrator = openmm.LangevinIntegrator(TEMPERATURE, 0.1, 0.002) + integrator = openmm.LangevinIntegrator(TEMPERATURE, 0.1, STEP_SIZE) # add the pseudo forces like barostat barostat = openmm.MonteCarloBarostat( @@ -131,11 +133,6 @@ def test_alanine_dipeptide_revo_procpool(): system=ala_sys.system, topology=ala_sys.topology, integrator=integrator, - # For this test we do want heart beat at shorter interval - openmm_reporter_factories=[ - # heart beat every step - HeartBeatLoggingReporterFactory(step_interval=2) - ] ) num_walkers = 4 @@ -170,29 +167,27 @@ def test_alanine_dipeptide_revo_procpool(): distance_metric = AlanineDipeptideRamachandranDistance(ala_sys.json_top) - resampler = REVOResampler( + resampler_factory = REVOResamplerFactory( merge_dist=4, char_dist=0.1, - distance=distance_metric, - num_proc=num_workers, + distance_metric=distance_metric, ) sim_manager = Manager( init_walkers=init_walkers, runner_factory=runner_factory, # resampler=NoResampler(), - resampler=resampler, + resampler_factory=resampler_factory, work_mapper_factory=OpenMMProcPoolWorkMapperFactory( - # num_procs=2, - # platform="Reference", platform="CPU", num_procs=num_workers, global_platform_properties={"Threads" : str(cores_per_worker)}, ), ) + new_walkers, sim_components = sim_manager.run_simulation( n_cycles=2, - segment_lengths=100, + segment_lengths=MIN_INTERVAL_STEPS * 2 + 10, ) diff --git a/tests/integration/test_openmm/test_sim_manager.py b/tests/integration/test_openmm/test_sim_manager.py index 098efb48..5ba1cf4f 100644 --- a/tests/integration/test_openmm/test_sim_manager.py +++ b/tests/integration/test_openmm/test_sim_manager.py @@ -17,7 +17,6 @@ STEP_SIZE = 2 * openmm.unit.femtosecond - def test_serial_mapper(): lj_sys = LennardJonesPair() @@ -61,7 +60,7 @@ def test_serial_mapper(): sim_manager = Manager( init_walkers=init_walkers, runner_factory=runner_factory, - resampler=NoResampler(), + resampler_factory=NoResampler, work_mapper_factory=OpenMMSerialWorkMapperFactory( platform="Reference", ) @@ -75,7 +74,7 @@ def test_serial_mapper(): sim_manager = Manager( init_walkers=init_walkers, runner_factory=runner_factory, - resampler=NoResampler(), + resampler_factory=NoResampler, work_mapper_factory=OpenMMSerialWorkMapperFactory( platform="CPU", global_platform_properties={"Threads" : "1"}, @@ -90,7 +89,7 @@ def test_serial_mapper(): sim_manager = Manager( init_walkers=init_walkers, runner_factory=runner_factory, - resampler=NoResampler(), + resampler_factory=NoResampler, work_mapper_factory=OpenMMSerialWorkMapperFactory( platform="CPU", global_platform_properties={"Threads" : "4"}, @@ -147,7 +146,7 @@ def test_proc_pool_mapper(): sim_manager = Manager( init_walkers=init_walkers, runner_factory=runner_factory, - resampler=NoResampler(), + resampler_factory=NoResampler, work_mapper_factory=OpenMMProcPoolWorkMapperFactory( platform="Reference", num_procs=4, @@ -169,7 +168,7 @@ def test_proc_pool_mapper(): sim_manager = Manager( init_walkers=init_walkers, runner_factory=runner_factory, - resampler=NoResampler(), + resampler_factory=NoResampler, work_mapper_factory=OpenMMProcPoolWorkMapperFactory( platform="CPU", num_procs=len(walker_states), @@ -189,7 +188,7 @@ def test_proc_pool_mapper(): sim_manager = Manager( init_walkers=init_walkers, runner_factory=runner_factory, - resampler=NoResampler(), + resampler_factory=NoResampler, work_mapper_factory=OpenMMProcPoolWorkMapperFactory( platform="CPU", num_procs=len(walker_states), @@ -241,7 +240,7 @@ def test_proc_pool_mapper(): # sim_manager = Manager( # init_walkers=init_walkers, # runner_factory=runner_factory, -# resampler=NoResampler(), +# resampler_factory=NoResampler, # work_mapper_factory=OpenMMRayWorkMapperFactory( # platform="Reference", # num_procs=1, From 5fd0e19b0e360a74761aec957127a1f815de862d Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 19:24:14 -0500 Subject: [PATCH 090/143] formatting --- src/wepy/monitor.py | 1 + src/wepy/resampling/decisions/clone_merge.py | 9 +- src/wepy/resampling/decisions/decision.py | 8 +- src/wepy/resampling/decisions/no_decision.py | 8 +- src/wepy/resampling/distances/base.py | 6 +- src/wepy/resampling/distances/mock.py | 8 + src/wepy/resampling/distances/simple.py | 16 +- src/wepy/resampling/resamplers/clone_merge.py | 39 +- src/wepy/resampling/resamplers/noresampler.py | 11 +- src/wepy/resampling/resamplers/resampler.py | 72 ++-- src/wepy/resampling/resamplers/revo.py | 129 +++--- src/wepy/runners/mock.py | 25 +- src/wepy/runners/openmm/__init__.py | 19 +- src/wepy/runners/openmm/logger.py | 83 ++-- src/wepy/runners/openmm/reporter.py | 6 +- src/wepy/runners/openmm/runner.py | 149 ++++--- src/wepy/runners/openmm/state.py | 236 ++++++----- src/wepy/runners/runner.py | 87 ++-- src/wepy/sim_manager.py | 383 ++++++++++-------- src/wepy/util/multiprocessing.py | 33 +- src/wepy/util/openmm.py | 18 +- src/wepy/walker.py | 5 +- src/wepy/work_mapper/base.py | 16 +- src/wepy/work_mapper/openmm/__init__.py | 1 + src/wepy/work_mapper/openmm/proc_pool.py | 134 +++--- src/wepy/work_mapper/openmm/ray.py | 98 +++-- src/wepy/work_mapper/openmm/serial.py | 29 +- src/wepy/work_mapper/serial.py | 53 +-- src/wepy_tools/monitoring/prometheus.py | 4 +- src/wepy_tools/systems/alanine_dipeptide.py | 33 +- src/wepy_tools/systems/lennard_jones.py | 35 +- .../integration/test_openmm/test_realistic.py | 86 ++-- .../test_openmm/test_sim_manager.py | 79 ++-- .../test_decisions/test_clone_merge.py | 180 ++++---- .../test_decisions/test_decision.py | 42 +- .../test_decisions/test_no_decision.py | 81 ++-- .../test_distances/test_base.py | 8 +- .../test_distances/test_mock.py | 9 +- .../test_distances/test_simple.py | 7 +- .../test_resamplers/test_clone_merge.py | 37 +- .../test_resamplers/test_noresampler.py | 6 +- .../test_resamplers/test_resampler.py | 9 +- .../test_resamplers/test_revo.py | 142 ++++--- tests/unit/test_runners/test_mock.py | 19 +- .../test_runners/test_openmm/test_logger.py | 133 +++--- .../test_runners/test_openmm/test_runner.py | 91 +++-- .../test_runners/test_openmm/test_state.py | 121 +++--- tests/unit/test_runners/test_runner.py | 64 +-- tests/unit/test_sim_manager.py | 50 +-- tests/unit/test_util/test_multiprocessing.py | 14 +- tests/unit/test_util/test_openmm.py | 6 +- tests/unit/test_walker.py | 25 +- .../test_systems/test_alanine_dipeptide.py | 30 +- .../test_systems/test_lennard_jones.py | 2 + tests/unit/test_work_mapper/test_openmm.py | 37 +- tests/unit/test_work_mapper/test_serial.py | 12 +- 56 files changed, 1716 insertions(+), 1328 deletions(-) diff --git a/src/wepy/monitor.py b/src/wepy/monitor.py index 6cd5f75e..95318c2b 100644 --- a/src/wepy/monitor.py +++ b/src/wepy/monitor.py @@ -1,5 +1,6 @@ """Interface definition for simulation monitors.""" +# First Party Library from wepy.walker import Walker diff --git a/src/wepy/resampling/decisions/clone_merge.py b/src/wepy/resampling/decisions/clone_merge.py index 9a9da6db..d0aeee91 100644 --- a/src/wepy/resampling/decisions/clone_merge.py +++ b/src/wepy/resampling/decisions/clone_merge.py @@ -1,15 +1,14 @@ # Standard Library -from typing import TypedDict import logging - -# Standard Library from collections import defaultdict from enum import IntEnum + +# Third Party Library import attrs # First Party Library from wepy.resampling.decisions.decision import Decision, DecisionRecord -from wepy.walker import keep_merge, split, Walker +from wepy.walker import Walker, keep_merge, split logger = logging.getLogger(__name__) @@ -40,6 +39,7 @@ class CloneMergeDecisionEnum(IntEnum): """Do nothing with the sample value (state) but squashed walkers will donate their weight to it.""" + @attrs.define class CloneMergeDecisionRecord(DecisionRecord): decision_id: int @@ -221,4 +221,3 @@ def parents(cls, step: list[CloneMergeDecisionRecord]) -> list[int]: step_parents[child_idx] = parent_idx return step_parents - diff --git a/src/wepy/resampling/decisions/decision.py b/src/wepy/resampling/decisions/decision.py index 0565fdae..d9a45095 100644 --- a/src/wepy/resampling/decisions/decision.py +++ b/src/wepy/resampling/decisions/decision.py @@ -46,18 +46,19 @@ """ # Standard Library -from typing import TypedDict, Required, Any, Union import logging - -# Standard Library from enum import IntEnum +from typing import Any, Union +# Third Party Library import attrs +# First Party Library from wepy.walker import Walker logger = logging.getLogger(__name__) + @attrs.define class DecisionRecord: decision_id: int @@ -263,4 +264,3 @@ def action( """ raise NotImplementedError - diff --git a/src/wepy/resampling/decisions/no_decision.py b/src/wepy/resampling/decisions/no_decision.py index ba86cbf7..42affca0 100644 --- a/src/wepy/resampling/decisions/no_decision.py +++ b/src/wepy/resampling/decisions/no_decision.py @@ -1,8 +1,12 @@ -from typing import TypedDict +# Standard Library from enum import IntEnum + +# Third Party Library import attrs -from wepy.walker import Walker + +# First Party Library from wepy.resampling.decisions.decision import Decision, DecisionRecord +from wepy.walker import Walker class NothingDecisionEnum(IntEnum): diff --git a/src/wepy/resampling/distances/base.py b/src/wepy/resampling/distances/base.py index fbce1f23..7c21924f 100644 --- a/src/wepy/resampling/distances/base.py +++ b/src/wepy/resampling/distances/base.py @@ -25,17 +25,17 @@ # Standard Library import logging from abc import ABC -from typing import TypeVar, Generic, Protocol +from typing import Generic, Protocol, TypeVar # First Party Library from wepy.walker import WalkerState -from wepy.util.util import box_vectors_to_lengths_angles logger = logging.getLogger(__name__) WalkerState_ = TypeVar("WalkerState_", bound=WalkerState, covariant=True) DistanceImage_ = TypeVar("DistanceImage_") + class Distance(Protocol[DistanceImage_, WalkerState_]): def image(self, state: WalkerState_) -> DistanceImage_: """Compute the 'image' of a walker state which should be some @@ -101,8 +101,6 @@ def distance(self, state_a: WalkerState_, state_b: WalkerState_) -> float: ... - - class DistanceABC(ABC, Generic[DistanceImage_, WalkerState_]): """Abstract Base class for Distance classes.""" diff --git a/src/wepy/resampling/distances/mock.py b/src/wepy/resampling/distances/mock.py index 97055943..d3f69e12 100644 --- a/src/wepy/resampling/distances/mock.py +++ b/src/wepy/resampling/distances/mock.py @@ -1,8 +1,16 @@ +# Standard Library import math + +# Third Party Library import attrs + +# First Party Library from wepy.runners.mock import MockState + +# Local Modules from .base import DistanceABC + @attrs.define class MockDistance(DistanceABC): diff --git a/src/wepy/resampling/distances/simple.py b/src/wepy/resampling/distances/simple.py index fec6dbdd..c23fb0c3 100644 --- a/src/wepy/resampling/distances/simple.py +++ b/src/wepy/resampling/distances/simple.py @@ -1,22 +1,23 @@ # Standard Library import logging -from abc import ABC -from typing import TypeVar, Generic, Protocol -import numpy as np +# Third Party Library import attrs +import numpy as np # First Party Library -from wepy.walker import WalkerState -from wepy.util.util import box_vectors_to_lengths_angles + +# Local Modules from .base import DistanceABC logger = logging.getLogger(__name__) + @attrs.define class XYDistanceState: coord: tuple[int, int] + @attrs.define class XYEuclideanDistance(DistanceABC): """2 dimensional euclidean distance between points. @@ -26,4 +27,7 @@ class XYEuclideanDistance(DistanceABC): """ def image_distance(self, image_a, image_b): - return np.sqrt((image_a.coord[0] - image_b.coord[0]) ** 2 + (image_a.coord[1] - image_b.coord[1]) ** 2) + return np.sqrt( + (image_a.coord[0] - image_b.coord[0]) ** 2 + + (image_a.coord[1] - image_b.coord[1]) ** 2 + ) diff --git a/src/wepy/resampling/resamplers/clone_merge.py b/src/wepy/resampling/resamplers/clone_merge.py index 54190255..9eb04c98 100644 --- a/src/wepy/resampling/resamplers/clone_merge.py +++ b/src/wepy/resampling/resamplers/clone_merge.py @@ -1,14 +1,20 @@ -from typing import TypeVar, Generic +# Standard Library +from typing import Generic, TypeVar + # Third Party Library import numpy as np # First Party Library -from wepy.resampling.decisions.clone_merge import MultiCloneMergeDecision, CloneMergeDecisionRecord -from wepy.resampling.resamplers.resampler import Resampler, ResamplerABC, ResamplerError -from wepy.walker import WalkerState, Walker +from wepy.resampling.decisions.clone_merge import ( + CloneMergeDecisionRecord, + MultiCloneMergeDecision, +) +from wepy.resampling.resamplers.resampler import ResamplerABC, ResamplerError +from wepy.walker import Walker, WalkerState WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) + class CloneMergeResampler(ResamplerABC, Generic[WalkerState_]): """Abstract base class for resamplers using the clone-merge decision class. @@ -53,23 +59,16 @@ def __init__( """ super().__init__( - min_num_walkers=min_num_walkers, - max_num_walkers=max_num_walkers, **kwargs + min_num_walkers=min_num_walkers, max_num_walkers=max_num_walkers, **kwargs ) if pmin >= 1.0: - raise ResamplerError( - f"pmin ({pmin}) must be less 1.0" - ) + raise ResamplerError(f"pmin ({pmin}) must be less 1.0") if pmax >= 1.0: - raise ResamplerError( - f"pmax ({pmax}) must be less 1.0" - ) + raise ResamplerError(f"pmax ({pmax}) must be less 1.0") if pmin > pmax: - raise ResamplerError( - f"pmin ({pmin}) must be less than pmax ({pmax})" - ) + raise ResamplerError(f"pmin ({pmin}) must be less than pmax ({pmax})") self._pmin = pmin self._pmax = pmax @@ -109,7 +108,9 @@ def _init_walker_actions(self, n_walkers: int) -> list[CloneMergeDecisionRecord] return walker_actions - def _check_resampled_walkers(self, resampled_walkers: list[Walker[WalkerState_]]) -> None: + def _check_resampled_walkers( + self, resampled_walkers: list[Walker[WalkerState_]] + ) -> None: """Check constraints on resampled walkers. Raises errors when constraints are violated. @@ -145,9 +146,9 @@ def _check_resampled_walkers(self, resampled_walkers: list[Walker[WalkerState_]] ) def assign_clones( - self, - merge_groups: list[list[int]], - walker_clone_nums: list[int], + self, + merge_groups: list[list[int]], + walker_clone_nums: list[int], ) -> list[CloneMergeDecisionRecord]: """Convert two convenient data structures to a list of almost normalized resampling records. diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py index 52b4bc5c..f2973db2 100644 --- a/src/wepy/resampling/resamplers/noresampler.py +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -1,12 +1,15 @@ +# Standard Library from typing import TypedDict -import numpy as np -from wepy.walker import Walker -from wepy.resampling.resamplers.resampler import Resampler, ResamplerABC + +# Third Party Library + +# First Party Library from wepy.resampling.decisions.no_decision import ( NoDecision, - NoDecisionRecord, NothingDecisionEnum, ) +from wepy.resampling.resamplers.resampler import Resampler, ResamplerABC +from wepy.walker import Walker class NoResamplerResamplingData(TypedDict): diff --git a/src/wepy/resampling/resamplers/resampler.py b/src/wepy/resampling/resamplers/resampler.py index 8c89419f..ecb997cb 100644 --- a/src/wepy/resampling/resamplers/resampler.py +++ b/src/wepy/resampling/resamplers/resampler.py @@ -1,15 +1,13 @@ # Standard Library import logging -from typing import Any, Protocol, TypeVar, Generic, Literal, Union - -# Standard Library +from typing import Any, Generic, Literal, Protocol, TypeVar, Union from warnings import warn # Third Party Library import numpy as np # First Party Library -from wepy.resampling.decisions.decision import Decision, DecisionRecord +from wepy.resampling.decisions.decision import Decision from wepy.walker import Walker, WalkerState logger = logging.getLogger(__name__) @@ -24,6 +22,7 @@ class ResamplerError(Exception): pass + class Resampler(Protocol, Generic[WalkerState_]): DECISION: Decision @@ -38,15 +37,9 @@ class Resampler(Protocol, Generic[WalkerState_]): Literal[Ellipsis], None, ], - ... - ] - RESAMPLING_DTYPES: tuple[ - Union[ - np.dtype, - None - ], - ... + ..., ] + RESAMPLING_DTYPES: tuple[Union[np.dtype, None], ...] RESAMPLING_RECORD_FIELDS: None | tuple[str, ...] RESAMPLER_FIELDS: tuple[str, ...] RESAMPLER_SHAPES: tuple[ @@ -55,50 +48,36 @@ class Resampler(Protocol, Generic[WalkerState_]): Literal[Ellipsis], None, ], - ... + ..., ] - RESAMPLER_DTYPES: tuple[ - Union[np.dtype, None], ... - ] + RESAMPLER_DTYPES: tuple[Union[np.dtype, None], ...] RESAMPLER_RECORD_FIELDS: None | tuple[str, ...] def resampling_fields(self) -> tuple[ - tuple[str, ...], - tuple[ - Union[ - tuple[int, ...], - Literal[Ellipsis], - None, - ], - ... - ], - tuple[ - Union[ - np.dtype, - None - ], - ... + tuple[str, ...], + tuple[ + Union[ + tuple[int, ...], + Literal[Ellipsis], + None, ], - ]: - ... + ..., + ], + tuple[Union[np.dtype, None], ...], + ]: ... - def resampling_record_field_names(self) -> None | tuple[str, ...]: - ... + def resampling_record_field_names(self) -> None | tuple[str, ...]: ... - def resampler_record_field_names(self) -> None | tuple[str, ...]: - ... - - def resample( - self, - walkers: list[Walker[WalkerState_]] - ) -> tuple[ + def resampler_record_field_names(self) -> None | tuple[str, ...]: ... + + def resample(self, walkers: list[Walker[WalkerState_]]) -> tuple[ list[Walker[WalkerState_]], # TODO: better types for this list[dict[str, Any]], list[dict[str, Any]], - ]: - ... + ]: ... + class ResamplerABC(Resampler): """Abstract base class for implementing resamplers. @@ -406,7 +385,10 @@ def __init__( "The minimum number of walkers should be at least 1" ) - if max_num_walkers not in {Ellipsis, None} and min_num_walkers > max_num_walkers: + if ( + max_num_walkers not in {Ellipsis, None} + and min_num_walkers > max_num_walkers + ): raise ResamplerError( f"min_num_walkers ({min_num_walkers}) must be less than or equal to max_num_walkers ({max_num_walkers})" ) diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 30085b5e..4d7f6ae7 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -1,23 +1,21 @@ # Standard Library import itertools as it -import time import logging -from typing import Literal, TypeVar, Generic, Callable, Any, TypedDict - -# Standard Library import multiprocessing as mp import random as rand +import time +from typing import Callable, Generic, Literal, TypedDict, TypeVar # Third Party Library -import numpy as np import attrs +import numpy as np # First Party Library -from wepy.util.multiprocessing import proc_pool_worker_setup, queue_listener_context -from wepy.resampling.resamplers.clone_merge import CloneMergeResampler from wepy.resampling.decisions.clone_merge import CloneMergeDecisionRecord from wepy.resampling.distances.base import Distance -from wepy.walker import WalkerState, Walker +from wepy.resampling.resamplers.clone_merge import CloneMergeResampler +from wepy.util.multiprocessing import proc_pool_worker_setup, queue_listener_context +from wepy.walker import Walker, WalkerState logger = logging.getLogger(__name__) @@ -27,9 +25,11 @@ MergeAlgorithm = Literal["pairs", "greedy"] + class REVOResamplerError(Exception): pass + class _ImageWrapper(Generic[WalkerState_, DistanceImage_]): """Wrapper callable to inject a few log messages to image computation. @@ -49,14 +49,16 @@ def __call__(self, state: WalkerState_) -> DistanceImage_: logger.info("Finished image computation") return result + class REVOResamplerResamplerData(TypedDict): distance_matrix: np.typing.ArrayLike num_walkers: int variation: float - + + class REVOResampler( - CloneMergeResampler, - Generic[DistanceMetric_, DistanceImage_, WalkerState_], + CloneMergeResampler, + Generic[DistanceMetric_, DistanceImage_, WalkerState_], ): r"""Resampler implementing the REVO algorithm. @@ -289,23 +291,23 @@ def _novelty(self, walker_weight: float, num_walker_copy: int) -> float: """ - novelty = 0. + novelty = 0.0 if walker_weight > 0 and num_walker_copy > 0: if self.weights: novelty = np.log(walker_weight / num_walker_copy) - self.lpmin else: - novelty = 1. + novelty = 1.0 if novelty < 0: - novelty = 0. + novelty = 0.0 return novelty def _calc_variation( - self, - walker_weights: list[float], - num_walker_copies: list[int], - distance_matrix: list[list[float]], + self, + walker_weights: list[float], + num_walker_copies: list[int], + distance_matrix: list[list[float]], ) -> tuple[float, list[float]]: """Calculates the variation value. @@ -343,7 +345,7 @@ def _calc_variation( ) # the value to be optimized - variation: float = 0. + variation: float = 0.0 # the walker variation values (Vi values) walker_variations = np.zeros(num_walkers) @@ -373,10 +375,10 @@ def _calc_variation( return variation, walker_variations def _calc_variation_loss( - self, - walker_variation: list[float], - weights: list[float], - eligible_pairs: list[tuple[int, int]], + self, + walker_variation: list[float], + weights: list[float], + eligible_pairs: list[tuple[int, int]], ) -> tuple[int, int] | None: """Calculates the loss to variation through merging of eligible walkers. @@ -467,10 +469,10 @@ def _find_eligible_merge_pairs( return eligible_pairs def decide( - self, - walker_weights: list[float], - num_walker_copies: list[int], - distance_matrix: list[list[float]], + self, + walker_weights: list[float], + num_walker_copies: list[int], + distance_matrix: list[list[float]], ) -> tuple[ list[CloneMergeDecisionRecord], float, @@ -492,7 +494,6 @@ def decide( Returns ------- - resampling_data : list of dict of str: value The resampling records resulting from the decisions. variation : float @@ -699,7 +700,7 @@ def decide( # TOREV: this was taken out as it probably wasn't necessary, # but this may be critical in analyses, check this - + # because there is only one step in resampling here we just # add another field for the step as 0 and add the walker index # to its record as well @@ -710,11 +711,11 @@ def decide( return walker_records, final_variation def _all_to_all_distance( - self, - walkers: list[Walker[WalkerState_]], + self, + walkers: list[Walker[WalkerState_]], ) -> tuple[ - list[list[float]], - list[DistanceImage_], + list[list[float]], + list[DistanceImage_], ]: """Calculate the pairwise all-to-all distances between walkers. @@ -731,22 +732,19 @@ def _all_to_all_distance( """ # initialize an all-to-all matrix, with 0.0 for self distances - dist_mat = [ - [0. for _ in range(len(walkers))] - for _ - in range(len(walkers)) - ] + dist_mat = [[0.0 for _ in range(len(walkers))] for _ in range(len(walkers))] logger.info("Starting calculation of walker images") start_time = time.time() - + # make images for all the walker states for us to compute distances on if self.num_proc > 1: - logger.info(f"Multiple processes requested ({self.num_proc}) will run in Pool.") + logger.info( + f"Multiple processes requested ({self.num_proc}) will run in Pool." + ) _distance_image = _ImageWrapper(self.distance.image) - # NOTE: Must use spawn here, otherwise there are problems # with deadlocking in the sub-processes mp_ctx = mp.get_context(method="spawn") @@ -754,18 +752,20 @@ def _all_to_all_distance( # TODO: This should be part of some setup phase logger.info("Starting multiprocessing.Pool") with ( - queue_listener_context(mp_ctx) as log_queue, - mp_ctx.Pool( - self.num_proc, - initializer=proc_pool_worker_setup, - initargs=(log_queue,), - # Set some upper bound so that it gets cleaned up - # in case of leaks - maxtasksperchild=4 - ) as pool, + queue_listener_context(mp_ctx) as log_queue, + mp_ctx.Pool( + self.num_proc, + initializer=proc_pool_worker_setup, + initargs=(log_queue,), + # Set some upper bound so that it gets cleaned up + # in case of leaks + maxtasksperchild=4, + ) as pool, ): - logger.info(f"Running parallel map calculation on {len(walkers)} walkers") + logger.info( + f"Running parallel map calculation on {len(walkers)} walkers" + ) images = pool.map( _distance_image, [walker.state for walker in walkers], @@ -809,8 +809,8 @@ def _all_to_all_distance( return dist_mat, images def resample( - self, - walkers: list[Walker[WalkerState_]], + self, + walkers: list[Walker[WalkerState_]], ) -> tuple[ list[Walker[WalkerState_]], list[list[CloneMergeDecisionRecord]], @@ -879,18 +879,19 @@ def resample( "variation": variation, } ] - + # TOREV: ditto, wrt to data interfaces - # resampler_data = [ - # { - # "distance_matrix": np.ravel(np.array(distance_matrix)), - # "num_walkers": np.array([len(walkers)]), - # "variation": np.array([variation]), - # } - # ] - + # resampler_data = [ + # { + # "distance_matrix": np.ravel(np.array(distance_matrix)), + # "num_walkers": np.array([len(walkers)]), + # "variation": np.array([variation]), + # } + # ] + return resampled_walkers, resampling_data, resampler_data + @attrs.define class REVOResamplerFactory(Generic[DistanceMetric_]): @@ -905,8 +906,8 @@ class REVOResamplerFactory(Generic[DistanceMetric_]): seed: int | None = None def __call__( - self, - num_cores: int, + self, + num_cores: int, ) -> REVOResampler: return REVOResampler( diff --git a/src/wepy/runners/mock.py b/src/wepy/runners/mock.py index d4636b0e..5b9e6ef6 100644 --- a/src/wepy/runners/mock.py +++ b/src/wepy/runners/mock.py @@ -1,21 +1,35 @@ """Realistic mock runners useful mostly for testing.""" -import time + +# Standard Library import logging -from typing import Literal +import time +# Third Party Library import attrs -from wepy.walker import Walker, WalkerState -from wepy.runners.runner import Runner, RunnerStatus, RunSegmentData, RunnerStateMachine, RunnerEvent, RunnerStateError + +# First Party Library +from wepy.runners.runner import ( + Runner, + RunnerEvent, + RunnerStateError, + RunnerStateMachine, + RunnerStatus, + RunSegmentData, +) +from wepy.walker import WalkerState logger = logging.getLogger(__name__) + @attrs.define class MockState(WalkerState): a: int + class MockError(Exception): pass + @attrs.define class MockRunner(Runner): fail: bool = False @@ -73,11 +87,12 @@ def run_segment( seg_end_time = time.time() split_time = seg_end_time - seg_start_time - + segment_data = RunSegmentData(segment_split_time=split_time) return new_state, segment_data + @attrs.define class MockRunnerFactory: fail: bool = False diff --git a/src/wepy/runners/openmm/__init__.py b/src/wepy/runners/openmm/__init__.py index ace04c7d..bc251cd4 100644 --- a/src/wepy/runners/openmm/__init__.py +++ b/src/wepy/runners/openmm/__init__.py @@ -1,15 +1,24 @@ - +# Local Modules +from .logger import ( + EnergyLoggingReporterFactory, + HeartBeatLoggingReporterFactory, + UnitCellLoggingReporterFactory, +) +from .runner import ( + GPU_PLATFORMS, + OpenMMPlatformName, + OpenMMRunner, + OpenMMRunnerFactory, + PlatformKwargs, +) from .state import ( OpenMMState, - OpenMMStateWrapper, OpenMMStateValidationError, + OpenMMStateWrapper, dummy_context, get_context_state, state_to_xml, ) -from .runner import ( - OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS, OpenMMRunnerFactory) -from .logger import HeartBeatLoggingReporterFactory, UnitCellLoggingReporterFactory, EnergyLoggingReporterFactory __all__ = [ "HeartBeatLoggingReporterFactory", diff --git a/src/wepy/runners/openmm/logger.py b/src/wepy/runners/openmm/logger.py index 90529ea2..eca69fb7 100644 --- a/src/wepy/runners/openmm/logger.py +++ b/src/wepy/runners/openmm/logger.py @@ -1,16 +1,22 @@ -import time +# Standard Library import logging -from typing import Callable +import time from collections.abc import Collection -import openmm.app +from typing import Callable + +# Third Party Library +import attrs import openmm +import openmm.app import openmm.unit -import attrs + +# First Party Library from wepy.util.openmm import format_box_vectors_line +# Local Modules from .reporter import ( - OpenMMReporter, OpenMMGetStateKeys, + OpenMMReporter, OpenMMReporterNextReport, ) @@ -52,6 +58,7 @@ def report( state, ) + LoggingReporterFactory = Callable[ [ logging.Logger, @@ -61,6 +68,7 @@ def report( LoggingReporter, ] + class StepIntervalLoggingReporter(LoggingReporter): """Reporter that reports at intervals in steps.""" @@ -125,8 +133,8 @@ def __init__( self.start_time = start_time def describeNextReport( - self, - simulation: openmm.app.Simulation, + self, + simulation: openmm.app.Simulation, ) -> OpenMMReporterNextReport: _unit = openmm.unit.attosecond @@ -137,19 +145,20 @@ def describeNextReport( step_size = simulation.context.getIntegrator().getStepSize() sampling_time_left = ( - self.sampling_time_interval.value_in_unit(_unit) - ( + self.sampling_time_interval.value_in_unit(_unit) + - ( curr_sampling_time.value_in_unit(_unit) % self.sampling_time_interval.value_in_unit(_unit) ) ) * _unit - if sampling_time_left < (0. * _unit): + if sampling_time_left < (0.0 * _unit): estimated_steps_left = 0 else: - estimated_steps_left = ( - round(sampling_time_left.value_in_unit(_unit)) // round(step_size.value_in_unit(_unit)) - ) + estimated_steps_left = round( + sampling_time_left.value_in_unit(_unit) + ) // round(step_size.value_in_unit(_unit)) return OpenMMReporterNextReport( steps=estimated_steps_left, @@ -174,12 +183,12 @@ def __init__( step_interval=step_interval, start_time=start_time, ) - + def logging_callback( - self, - logger: logging.Logger, - simulation: openmm.app.Simulation, - state: openmm.State, + self, + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, ) -> None: current_time = time.time() @@ -195,15 +204,16 @@ def logging_callback( f"OpenMM simulation progress: clock_time={current_time:.4f} s, elapsed_time={elapsed_time:.4f} s, sim_time={sim_time_mag:.4f} ps, sim_steps={sim_steps}", ) + @attrs.define class HeartBeatLoggingReporterFactory: step_interval: int def __call__( - self, - logger: logging.Logger, - start_time: int, + self, + logger: logging.Logger, + start_time: int, ) -> HeartBeatLoggingReporter: return HeartBeatLoggingReporter( @@ -212,6 +222,7 @@ def __call__( start_time=start_time, ) + class EnergyLoggingReporter(SamplingTimeIntervalLoggingReporter): def __init__( @@ -230,10 +241,10 @@ def __init__( ) def logging_callback( - self, - logger: logging.Logger, - simulation: openmm.app.Simulation, - state: openmm.State + self, + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, ) -> None: sim_time = simulation.context.getTime() @@ -249,15 +260,16 @@ def logging_callback( f"kinetic={_kin_e}, potential={_pot_e}, total={_tot_e}" ) + @attrs.define class EnergyLoggingReporterFactory: sampling_time_interval: int def __call__( - self, - logger: logging.Logger, - start_time: int, + self, + logger: logging.Logger, + start_time: int, ) -> EnergyLoggingReporter: return EnergyLoggingReporter( @@ -265,7 +277,7 @@ def __call__( sampling_time_interval=self.sampling_time_interval, start_time=start_time, ) - + class UnitCellLoggingReporter(SamplingTimeIntervalLoggingReporter): @@ -285,10 +297,10 @@ def __init__( ) def logging_callback( - self, - logger: logging.Logger, - simulation: openmm.app.Simulation, - state: openmm.State + self, + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, ) -> None: sim_time = simulation.context.getTime() @@ -304,15 +316,16 @@ def logging_callback( f"volume={_box_volume}, vectors={_bvs_line}" ) + @attrs.define class UnitCellLoggingReporterFactory: sampling_time_interval: int def __call__( - self, - logger: logging.Logger, - start_time: int, + self, + logger: logging.Logger, + start_time: int, ) -> EnergyLoggingReporter: return UnitCellLoggingReporter( diff --git a/src/wepy/runners/openmm/reporter.py b/src/wepy/runners/openmm/reporter.py index ecee4060..773db6cb 100644 --- a/src/wepy/runners/openmm/reporter.py +++ b/src/wepy/runners/openmm/reporter.py @@ -3,16 +3,15 @@ # Standard Library import abc import logging -from collections.abc import Callable, Collection from typing import Literal, NotRequired, TypedDict, get_args # Third Party Library import openmm as omm import openmm.app as omma -import openmm.unit as unit logger = logging.getLogger(__name__) + class OpenMMReporterNextReport(TypedDict): steps: int @@ -34,7 +33,6 @@ class OpenMMReporterNextReport(TypedDict): ) - class OpenMMReporter(metaclass=abc.ABCMeta): """ABC for openmm.app Reporter. @@ -53,5 +51,3 @@ def report( ) -> None: raise NotImplementedError - - diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index b593a01a..a25e9dd6 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -1,22 +1,23 @@ # Standard Library -from typing import Any, Annotated, TypedDict, NotRequired, Literal, Final, Self, get_args, TypeAlias, Callable +import copy import logging -import multiprocessing as mp -import itertools import time +from typing import ( + Literal, +) from warnings import warn -import copy # Third Party Library -from immutables import Map as frozenmap import attrs import numpy as np +# Local Modules from .reporter import OpenMMReporter logger = logging.getLogger(__name__) try: + # Third Party Library import mdtraj except ModuleNotFoundError: warn("Module 'mdtraj' not found, those features will not be available.") @@ -32,12 +33,24 @@ ) # First Party Library -from wepy.runners.runner import Runner, RunnerStatus, RunSegmentData, RunnerStateMachine, RunnerEvent, RunnerStateError -from wepy.util.util import box_vectors_to_lengths_angles -from wepy.util.openmm import triclinic_volume_vec3_quantity, format_box_vectors_line -from wepy.walker import WalkerState +from wepy.runners.runner import ( + Runner, + RunnerEvent, + RunnerStateError, + RunnerStateMachine, + RunnerStatus, + RunSegmentData, +) +from wepy.util.openmm import format_box_vectors_line, triclinic_volume_vec3_quantity + +# Local Modules +from .logger import ( + EnergyLoggingReporterFactory, + HeartBeatLoggingReporterFactory, + LoggingReporterFactory, + UnitCellLoggingReporterFactory, +) from .state import OpenMMState, OpenMMStateWrapper, get_context_state -from .logger import HeartBeatLoggingReporterFactory, LoggingReporterFactory, EnergyLoggingReporterFactory, UnitCellLoggingReporterFactory PlatformKwargs = dict[str, str] @@ -45,23 +58,24 @@ GPU_PLATFORMS = frozenset({"CUDA", "OpenCL", "HIP"}) - -GET_STATE_DEFAULT_KEYS = frozenset({ - "positions", - "velocities", - "forces", - "parameters", - "parameter_derivatives", - "kinetic_energy", - "potential_energy", - "time", - "box_vectors", - "box_volume", -}) +GET_STATE_DEFAULT_KEYS = frozenset( + { + "positions", + "velocities", + "forces", + "parameters", + "parameter_derivatives", + "kinetic_energy", + "potential_energy", + "time", + "box_vectors", + "box_volume", + } +) # default heart beat every 500 steps, should be around 0.5 - 1 picoseconds _DEFAULT_HEARTBEAT_INTERVAL = 500 -_DEFAULT_STATE_TIME_INTERVAL = (10 * openmm.unit.picosecond) +_DEFAULT_STATE_TIME_INTERVAL = 10 * openmm.unit.picosecond DEFAULT_OPENMM_REPORTER_FACTORIES = [ HeartBeatLoggingReporterFactory(step_interval=_DEFAULT_HEARTBEAT_INTERVAL), @@ -69,23 +83,26 @@ EnergyLoggingReporterFactory(sampling_time_interval=_DEFAULT_STATE_TIME_INTERVAL), ] + @attrs.define class OpenMMRunnerSegmentSplitTime: gen_sim_time: float steps_time: float get_state_time: float + @attrs.define class OpenMMRunnerSegmentData(RunSegmentData): segment_split_time: float openmm_segment_split_time: OpenMMRunnerSegmentSplitTime + def _report_simulation(simulation: openmm.app.Simulation) -> tuple[ - openmm.unit.Quantity, - tuple[openmm.unit.Quantity, openmm.unit.Quantity, openmm.unit.Quantity], - openmm.unit.Quantity, - openmm.unit.Quantity, - openmm.unit.Quantity, + openmm.unit.Quantity, + tuple[openmm.unit.Quantity, openmm.unit.Quantity, openmm.unit.Quantity], + openmm.unit.Quantity, + openmm.unit.Quantity, + openmm.unit.Quantity, ]: # log some info on the constructed simulation @@ -103,8 +120,12 @@ def _report_simulation(simulation: openmm.app.Simulation) -> tuple[ for name in _platform_prop_names } _openmm_version = _platform.getOpenMMVersion() - logger.info(f"Simulation Context: current_step={_step_count}, sampling_time={_time}, num_molecules={_num_molecules}") - logger.info(f"Simulation Context Platform: openmm_version={_openmm_version}, name={_platform_name}, properties={_props}") + logger.info( + f"Simulation Context: current_step={_step_count}, sampling_time={_time}, num_molecules={_num_molecules}" + ) + logger.info( + f"Simulation Context Platform: openmm_version={_openmm_version}, name={_platform_name}, properties={_props}" + ) # report on the initial state as well _init_state = simulation.context.getState( @@ -117,7 +138,9 @@ def _report_simulation(simulation: openmm.app.Simulation) -> tuple[ _vel0 = _velocities[0] _vel0_mag = _vel0.value_in_unit(_vel0.unit) _vels_zeroed = ( - np.isclose(_vel0_mag[0], 0.) and np.isclose(_vel0_mag[1], 0.) and np.isclose(_vel0_mag[2], 0.) + np.isclose(_vel0_mag[0], 0.0) + and np.isclose(_vel0_mag[1], 0.0) + and np.isclose(_vel0_mag[2], 0.0) ) if _vels_zeroed: @@ -129,7 +152,9 @@ def _report_simulation(simulation: openmm.app.Simulation) -> tuple[ _kin_e = _init_state.getKineticEnergy() _tot_e = _pot_e + _kin_e - logger.info(f"Context state energies: kinetic={_kin_e}, potential={_pot_e}, total={_tot_e}") + logger.info( + f"Context state energies: kinetic={_kin_e}, potential={_pot_e}, total={_tot_e}" + ) _box_volume = _init_state.getPeriodicBoxVolume() _bvs = _init_state.getPeriodicBoxVectors() @@ -139,6 +164,7 @@ def _report_simulation(simulation: openmm.app.Simulation) -> tuple[ return _box_volume, _bvs, _pot_e, _kin_e, _tot_e + # the runner for the simulation which runs the actual dynamics class OpenMMRunner(Runner): """Runner for OpenMM simulations.""" @@ -174,7 +200,9 @@ def __init__( if openmm_reporter_factories is None or len(openmm_reporter_factories) == 0: logger.warning("No OpenMM reporter factories configured.") - self.openmm_reporter_factories = openmm_reporter_factories if openmm_reporter_factories is not None else [] + self.openmm_reporter_factories = ( + openmm_reporter_factories if openmm_reporter_factories is not None else [] + ) self._openmm_reporters = None self._init_time = None @@ -201,10 +229,14 @@ def _report_configuration(self) -> None: default_bvs = self.system.getDefaultPeriodicBoxVectors() default_bv_volume = triclinic_volume_vec3_quantity(default_bvs) - default_bv_line = format_box_vectors_line(default_bvs) + default_bv_line = format_box_vectors_line(default_bvs) - logger.info(f"System: num_particles={num_particles}, num_forces={num_forces}, uses_pbcs={uses_pbcs}") - logger.info(f"System default box vectors: volume={default_bv_volume}, vectors={default_bv_line}") + logger.info( + f"System: num_particles={num_particles}, num_forces={num_forces}, uses_pbcs={uses_pbcs}" + ) + logger.info( + f"System default box vectors: volume={default_bv_volume}, vectors={default_bv_line}" + ) # topology num_chains = self.topology.getNumChains() @@ -214,7 +246,6 @@ def _report_configuration(self) -> None: top_bvs = self.topology.getPeriodicBoxVectors() - logger.info( f"Topology: num_chains={num_chains}, num_residues={num_residues}, num_atoms={num_atoms}, num_bonds={num_bonds}" ) @@ -227,11 +258,7 @@ def _report_configuration(self) -> None: else: logger.info("Topology box vectors not set.") - chain_ids = [ - chain.id - for chain - in self.topology.chains() - ] + chain_ids = [chain.id for chain in self.topology.chains()] logger.info(f"Topology Chains (IDs): {','.join(chain_ids)}") for chain in self.topology.chains(): @@ -243,7 +270,9 @@ def _report_configuration(self) -> None: # integrator # UGLY: just dump the XML for simplicity - integrator_xml = openmm.XmlSerializer.serialize(self.integrator).replace("\n", " ") + integrator_xml = openmm.XmlSerializer.serialize(self.integrator).replace( + "\n", " " + ) logger.info(f"Integrator: {integrator_xml}") @property @@ -270,8 +299,6 @@ def pre_cycle( self.state_machine.send(RunnerEvent.PRE_CYCLE) - - def run_segment( self, walker_state: OpenMMState, @@ -339,7 +366,7 @@ def run_segment( # get the platform by its name to use platform = openmm.Platform.getPlatformByName(platform_name) - logger.info(f"Platform instantiated.") + logger.info("Platform instantiated.") # set properties from the kwargs if they apply to the platform for key, value in platform_kwargs.items(): @@ -371,7 +398,9 @@ def run_segment( logger.info("Generating OpenMM reporters for this segment.") openmm_reporters = [] for omm_reporter_factory in self.openmm_reporter_factories: - logger.info(f"Generating and configuring reporter for factory: {omm_reporter_factory}") + logger.info( + f"Generating and configuring reporter for factory: {omm_reporter_factory}" + ) openmm_reporters.append( omm_reporter_factory( logger, @@ -396,7 +425,9 @@ def run_segment( logger.info(f"Time to generate the system: {gen_sim_time:.4f} s") logger.info("Information on initial simulation state") - before_volume, before_bvs, before_pot_e, before_kin_e, before_tot_e = _report_simulation(simulation) + before_volume, before_bvs, before_pot_e, before_kin_e, before_tot_e = ( + _report_simulation(simulation) + ) # actually run the simulation @@ -412,7 +443,9 @@ def run_segment( logger.info(f"Time to run {segment_length} sim steps: {steps_time:.4f} s") logger.info("Information on final simulation state") - after_volume, after_bvs, after_pot_e, after_kin_e, after_tot_e = _report_simulation(simulation) + after_volume, after_bvs, after_pot_e, after_kin_e, after_tot_e = ( + _report_simulation(simulation) + ) _before_lengths = ( np.linalg.norm(before_bvs[0]), @@ -427,10 +460,13 @@ def run_segment( _delta_lengths = [ after_length - before_length - for after_length, before_length - in zip(_after_lengths, _before_lengths, strict=True) + for after_length, before_length in zip( + _after_lengths, _before_lengths, strict=True + ) ] - _delta_lengths_line = f"({_delta_lengths[0]}, {_delta_lengths[1]}, {_delta_lengths[2]})" + _delta_lengths_line = ( + f"({_delta_lengths[0]}, {_delta_lengths[1]}, {_delta_lengths[2]})" + ) # report on the change in energies and box volume _delta_volume = after_volume - before_volume @@ -459,7 +495,7 @@ def run_segment( new_state_wrapper = OpenMMStateWrapper(new_omm_state) new_state = OpenMMState.from_state_wrapper(new_state_wrapper) - + get_state_end = time.time() get_state_time = get_state_end - get_state_start logger.info(f"Getting context state time: {get_state_time:.4f} s") @@ -485,6 +521,7 @@ def post_cycle(self, segments_data: list[OpenMMRunnerSegmentData]) -> None: logger.info("Nothing to do") self.state_machine.send(RunnerEvent.POST_CYCLE) + @attrs.define class OpenMMRunnerFactory: @@ -493,7 +530,9 @@ class OpenMMRunnerFactory: integrator: openmm.Integrator enforce_box: bool = False get_state_keys: frozenset[str] = attrs.field(default=GET_STATE_DEFAULT_KEYS) - openmm_reporter_factories: list[LoggingReporterFactory] | None = attrs.field(default=DEFAULT_OPENMM_REPORTER_FACTORIES) + openmm_reporter_factories: list[LoggingReporterFactory] | None = attrs.field( + default=DEFAULT_OPENMM_REPORTER_FACTORIES + ) def __call__(self) -> OpenMMRunner: diff --git a/src/wepy/runners/openmm/state.py b/src/wepy/runners/openmm/state.py index a25aa139..f2bd7603 100644 --- a/src/wepy/runners/openmm/state.py +++ b/src/wepy/runners/openmm/state.py @@ -1,26 +1,38 @@ -import copy +# Standard Library import logging -from typing import Literal, get_args, TypeAlias, Union, Any, Self, TypedDict, NotRequired, ClassVar -from collections.abc import Mapping, Collection -import openmm -import openmm.unit -import numpy as np +from collections.abc import Collection +from typing import ( + Any, + ClassVar, + Literal, + NotRequired, + Self, + TypeAlias, + TypedDict, + get_args, +) +# Third Party Library import attrs - -from lxml import etree +import numpy as np +import openmm +import openmm.unit from immutables import Map as frozenmap +from lxml import etree -from wepy.missing import MISSING -from wepy.walker import WalkerState +# First Party Library from wepy.core import BugError +from wepy.missing import MISSING from wepy.util.openmm import array3d_to_vec3 +from wepy.walker import WalkerState logger = logging.getLogger(__name__) + class OpenMMStateValidationError(Exception): pass + PREFERRED_UNITS_LUT = frozenmap( { "length": openmm.unit.nanometer, @@ -33,7 +45,8 @@ class OpenMMStateValidationError(Exception): "energy": openmm.unit.kilojoule, "subtance": openmm.unit.mole, "velocity": openmm.unit.nanometer / openmm.unit.picosecond, - "molar_force": (openmm.unit.kilojoule / openmm.unit.nanometer) / openmm.unit.mole, + "molar_force": (openmm.unit.kilojoule / openmm.unit.nanometer) + / openmm.unit.mole, "molar_energy_density": openmm.unit.kilojoule / openmm.unit.mole, } ) @@ -82,16 +95,16 @@ class OpenMMStateValidationError(Exception): FieldDataType: TypeAlias = openmm.unit.Quantity | frozenmap[str, Any] STATE_FIELD_TYPES: frozenmap[str, type[FieldDataType]] = frozenmap( - time = openmm.unit.Quantity, - box_vectors = openmm.unit.Quantity, - box_volume = openmm.unit.Quantity, - positions = openmm.unit.Quantity, - velocities = openmm.unit.Quantity, - forces = openmm.unit.Quantity, - kinetic_energy = openmm.unit.Quantity, - potential_energy = openmm.unit.Quantity, - parameters = frozenmap[str, Any], - parameter_derivatives = frozenmap[str, Any], + time=openmm.unit.Quantity, + box_vectors=openmm.unit.Quantity, + box_volume=openmm.unit.Quantity, + positions=openmm.unit.Quantity, + velocities=openmm.unit.Quantity, + forces=openmm.unit.Quantity, + kinetic_energy=openmm.unit.Quantity, + potential_energy=openmm.unit.Quantity, + parameters=frozenmap[str, Any], + parameter_derivatives=frozenmap[str, Any], ) StateDataTypeName: TypeAlias = Literal[ @@ -107,26 +120,26 @@ class OpenMMStateValidationError(Exception): ] STATE_DATA_TYPE_ENUM_NAMES: frozenmap[str, str] = frozenmap( - positions = "Positions", - velocities = "Velocities", - forces = "Forces", - energy = "Energy", - parameters = "Parameters", - parameter_derivatives = "ParameterDerivatives", - integrator_parameters = "IntegratorParameters", + positions="Positions", + velocities="Velocities", + forces="Forces", + energy="Energy", + parameters="Parameters", + parameter_derivatives="ParameterDerivatives", + integrator_parameters="IntegratorParameters", ) FIELD_GETTER_NAMES: frozenmap[str, str] = frozenmap( - positions = "getPositions", - velocities = "getVelocities", - forces = "getForces", - kinetic_energy = "getKineticEnergy", - potential_energy = "getPotentialEnergy", - time = "getTime", - box_vectors = "getPeriodicBoxVectors", - box_volume = "getPeriodicBoxVolume", - parameters = "getParameters", - parameter_derivatives = "getEnergyParameterDerivatives", + positions="getPositions", + velocities="getVelocities", + forces="getForces", + kinetic_energy="getKineticEnergy", + potential_energy="getPotentialEnergy", + time="getTime", + box_vectors="getPeriodicBoxVectors", + box_volume="getPeriodicBoxVolume", + parameters="getParameters", + parameter_derivatives="getEnergyParameterDerivatives", ) UNREQUESTED_FIELDS: frozenset[StateFieldName] = frozenset( @@ -179,40 +192,40 @@ class OpenMMStateValidationError(Exception): ] GET_STATE_KEYWORDS: frozenmap[str, GetStateKeyWords | None] = frozenmap( - positions = "positions", - velocities = "velocities", - forces = "forces", - kinetic_energy = "energy", - potential_energy = "energy", - time = None, - box_vectors = None, - box_volume = None, - parameters = "parameters", - parameter_derivatives = "parameterDerivatives", + positions="positions", + velocities="velocities", + forces="forces", + kinetic_energy="energy", + potential_energy="energy", + time=None, + box_vectors=None, + box_volume=None, + parameters="parameters", + parameter_derivatives="parameterDerivatives", ) - GET_STATE_DEFAULT_ENFORCE_PERIODIC_BOX = False OPENMM_DEFAULT_DIMENSION_UNITS: frozenmap[str, openmm.unit.Unit] = frozenmap( - length = openmm.unit.nanometer, - time = openmm.unit.picosecond, - energy = openmm.unit.kilojoule, - substance = openmm.unit.mole, + length=openmm.unit.nanometer, + time=openmm.unit.picosecond, + energy=openmm.unit.kilojoule, + substance=openmm.unit.mole, ) OPENMM_DEFAULT_UNITS: frozenmap[str, openmm.unit.Unit] = frozenmap( - positions = openmm.unit.nanometer, - time = openmm.unit.picosecond, - box_vectors = openmm.unit.nanometer, - box_volume = openmm.unit.nanometer**3, - velocities = openmm.unit.nanometer / openmm.unit.picosecond, - forces = openmm.unit.kilojoule / openmm.unit.nanometer, - kinetic_energy = openmm.unit.kilojoule, - potential_energy = openmm.unit.kilojoule, + positions=openmm.unit.nanometer, + time=openmm.unit.picosecond, + box_vectors=openmm.unit.nanometer, + box_volume=openmm.unit.nanometer**3, + velocities=openmm.unit.nanometer / openmm.unit.picosecond, + forces=openmm.unit.kilojoule / openmm.unit.nanometer, + kinetic_energy=openmm.unit.kilojoule, + potential_energy=openmm.unit.kilojoule, ) + class StateFieldData(TypedDict): time: openmm.unit.Quantity box_vectors: openmm.unit.Quantity @@ -226,6 +239,7 @@ class StateFieldData(TypedDict): parameters: NotRequired[frozenmap[str, Any]] parameter_derivatives: NotRequired[frozenmap[str, Any]] + class StateFieldDataInput(TypedDict): time: openmm.unit.Quantity box_vectors: openmm.unit.Quantity @@ -234,10 +248,12 @@ class StateFieldDataInput(TypedDict): velocities: NotRequired[openmm.unit.Quantity] parameters: NotRequired[frozenmap[str, Any]] + STATE_REQUIRED_INPUT_FIELDS: frozenset[StateFieldName] = frozenset( {"time", "box_vectors"} ) + def dummy_context( system: openmm.System, positions: openmm.unit.Quantity, @@ -261,6 +277,7 @@ def dummy_context( return context + def get_context_state( context: openmm.Context, fields: frozenset[StateFieldName] | None = None, @@ -344,6 +361,7 @@ def get_state_core_fields_present( return frozenset(flag_fields) + def get_state_fields_present(sim_state: openmm.State) -> frozenset[StateFieldName]: """Figure out which accessible state fields are present in a State. @@ -414,6 +432,7 @@ def get_state_fields_present(sim_state: openmm.State) -> frozenset[StateFieldNam ## Wrapper for a openmm.State + class OpenMMStateWrapper(WalkerState): """Useful wrapper around an openmm.State object. @@ -467,9 +486,7 @@ def __getitem__(self, key) -> FieldDataType: getter_method = getattr(self.state, getter_name, None) if getter_method is None: - raise BugError( - f"No getter ('{getter_name}') for key '{key}'" - ) + raise BugError(f"No getter ('{getter_name}') for key '{key}'") elif key in ARRAYLIKE_FIELDS: field_val = getter_method(asNumpy=True) @@ -478,9 +495,7 @@ def __getitem__(self, key) -> FieldDataType: elif key in MAPPING_FIELDS: field_val = frozenmap(dict(getter_method())) else: - raise BugError( - f"Unhandled field key {key}, getter '{getter_name}'" - ) + raise BugError(f"Unhandled field key {key}, getter '{getter_name}'") return field_val @@ -553,8 +568,8 @@ def from_dict( missing_fields_str = ", ".join(missing_fields) raise OpenMMStateValidationError( - f"Missing required fields: {missing_fields_str}" - ) + f"Missing required fields: {missing_fields_str}" + ) # a dummy context used to generate a state only ctx = openmm.Context( @@ -566,7 +581,10 @@ def from_dict( # the fields which are always in a state dict ctx.setTime(state_dict["time"].in_units_of(OPENMM_DEFAULT_UNITS["time"])) - bvs_vec3 = tuple(v for v in array3d_to_vec3(state_dict["box_vectors"])) * state_dict["box_vectors"].unit + bvs_vec3 = ( + tuple(v for v in array3d_to_vec3(state_dict["box_vectors"])) + * state_dict["box_vectors"].unit + ) ctx.setPeriodicBoxVectors(*bvs_vec3) if "positions" in state_dict: @@ -583,15 +601,17 @@ def from_dict( ctx.setParameter(name, value) except openmm.OpenMMException: raise OpenMMStateValidationError( - f"Could not set parameter '{name}' as there is no matching parameter in the system forces." - ) + f"Could not set parameter '{name}' as there is no matching parameter in the system forces." + ) state = get_context_state(ctx, frozenset(state_dict.keys())) return cls(state) + # A plain data structure state + def _maybe_array_equal( arr0: openmm.unit.Quantity | None, arr1: openmm.unit.Quantity | None, @@ -605,13 +625,17 @@ def _maybe_array_equal( else: return np.array_equal(arr0, arr1) + def _gen_unit_cube() -> np.typing.ArrayLike: - return np.array([ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.],] + return np.array( + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] ) + @attrs.define class OpenMMState(WalkerState): """Pure data type for an OpenMM state. @@ -652,8 +676,12 @@ class OpenMMState(WalkerState): # that is somewhat arbitrary but typically the ergonomic single # way to do something DWIM_DEFAULT_TIME: ClassVar[openmm.unit.Quantity] = 0 * openmm.unit.picosecond - DWIM_DEFAULT_UNITCELL: ClassVar[openmm.unit.Quantity] = _gen_unit_cube() * openmm.unit.nanometer - DWIM_DEFAULT_BOX_VOLUME: ClassVar[openmm.unit.Quantity] = 0. * (openmm.unit.nanometer ** 3) + DWIM_DEFAULT_UNITCELL: ClassVar[openmm.unit.Quantity] = ( + _gen_unit_cube() * openmm.unit.nanometer + ) + DWIM_DEFAULT_BOX_VOLUME: ClassVar[openmm.unit.Quantity] = 0.0 * ( + openmm.unit.nanometer**3 + ) @staticmethod def _validate_array3ds( @@ -680,8 +708,8 @@ def _validate_array3ds( report = ", ".join(f"{name}={maybe_nums[name]}" for name in which_given) raise OpenMMStateValidationError( - f"The number of particles do not match: {report}" - ) + f"The number of particles do not match: {report}" + ) elif len(which_given) == 3 and ( maybe_nums[which_given[0]] != maybe_nums[which_given[1]] @@ -691,8 +719,8 @@ def _validate_array3ds( report = ", ".join(f"{name}={maybe_nums[name]}" for name in which_given) raise OpenMMStateValidationError( - f"The number of particles do not match: {report}" - ) + f"The number of particles do not match: {report}" + ) else: return True @@ -733,7 +761,7 @@ def __contains__(self, key: str) -> bool: else: return True - + def __getitem__(self, key: str) -> openmm.unit.Quantity: if key not in STATE_FIELD_NAMES: @@ -749,38 +777,29 @@ def __getitem__(self, key: str) -> openmm.unit.Quantity: def from_dict(cls, data_dict: StateFieldData) -> Self: return cls(**data_dict) - @classmethod def from_dwim( - cls, - positions: openmm.unit.Quantity, - time: openmm.unit.Quantity | None = None, - box_volume: openmm.unit.Quantity | None = None, - box_vectors: openmm.unit.Quantity | None = None, - velocities: openmm.unit.Quantity | None = None, - forces: openmm.unit.Quantity | None = None, - kinetic_energy: openmm.unit.Quantity | None = None, - potential_energy: openmm.unit.Quantity | None = None, - parameters: frozenmap[str, Any] | None = None, - parameter_derivatives: frozenmap[str, Any] | None = None, + cls, + positions: openmm.unit.Quantity, + time: openmm.unit.Quantity | None = None, + box_volume: openmm.unit.Quantity | None = None, + box_vectors: openmm.unit.Quantity | None = None, + velocities: openmm.unit.Quantity | None = None, + forces: openmm.unit.Quantity | None = None, + kinetic_energy: openmm.unit.Quantity | None = None, + potential_energy: openmm.unit.Quantity | None = None, + parameters: frozenmap[str, Any] | None = None, + parameter_derivatives: frozenmap[str, Any] | None = None, ): return cls( - time=( - cls.DWIM_DEFAULT_TIME - if time is None - else time - ), + time=(cls.DWIM_DEFAULT_TIME if time is None else time), box_volume=( - cls.DWIM_DEFAULT_BOX_VOLUME - if box_volume is None - else box_volume + cls.DWIM_DEFAULT_BOX_VOLUME if box_volume is None else box_volume ), positions=positions, box_vectors=( - cls.DWIM_DEFAULT_UNITCELL - if box_vectors is None - else box_vectors + cls.DWIM_DEFAULT_UNITCELL if box_vectors is None else box_vectors ), velocities=velocities, forces=forces, @@ -798,7 +817,6 @@ def from_state_wrapper(cls, state_wrapper: OpenMMStateWrapper) -> Self: def from_state(cls, state: openmm.State) -> Self: return cls.from_state_wrapper(OpenMMStateWrapper(state)) - def to_dict(self) -> StateFieldData: return StateFieldData( { @@ -807,7 +825,7 @@ def to_dict(self) -> StateFieldData: if v is not None } ) - + def to_state_wrapper( self, system: openmm.System | None = None, @@ -833,6 +851,7 @@ def to_state_wrapper( return wrapper + def _gen_vec3_element( name: str, parent: etree.Element, vec: tuple[int, int, int] ) -> etree.Element: @@ -978,4 +997,3 @@ def state_to_xml( ) return xml_str.decode() - diff --git a/src/wepy/runners/runner.py b/src/wepy/runners/runner.py index 09fbb8f3..8f347eef 100644 --- a/src/wepy/runners/runner.py +++ b/src/wepy/runners/runner.py @@ -19,21 +19,28 @@ # Standard Library import logging -from typing import Protocol, Any, TypedDict, TypeVar, ParamSpec, Callable, Literal from enum import IntEnum +from typing import Callable, Literal, Protocol, TypeVar + +# Third Party Library import attrs from immutables import Map as frozenmap -from wepy.walker import Walker, WalkerState + +# First Party Library +from wepy.walker import WalkerState logger = logging.getLogger(__name__) + @attrs.define class RunSegmentData: segment_split_time: float + WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) RunSegmentData_ = TypeVar("RunSegmentData_", bound=RunSegmentData) + class RunnerStatus(IntEnum): PRE_INITIALIZATION = 0 INITIALIZED = 1 @@ -41,45 +48,65 @@ class RunnerStatus(IntEnum): POST_SEGMENT = 3 POST_CYCLE = 4 + class RunnerEvent(IntEnum): INIT = 0 PRE_CYCLE = 1 POST_SEGMENT = 2 POST_CYCLE = 3 + class RunnerStateMachineError(Exception): pass + class RunnerStateTransitionError(RunnerStateMachineError): """Indicates an error with the runner state machine transition.""" + pass + class RunnerStateError(RunnerStateMachineError): """Indicates an error relating to the current state of the runner.""" + pass + # State machine table that defines what are the valid states to # transition to another state. None for the initial states RUNNER_STATE_TRANSITION_TABLE: frozenmap[ RunnerStatus, frozenmap[RunnerEvent, RunnerStatus], -] = frozenmap({ - RunnerStatus.PRE_INITIALIZATION: frozenmap({ - RunnerEvent.INIT : RunnerStatus.INITIALIZED, - }), - RunnerStatus.INITIALIZED: frozenmap({ - RunnerEvent.PRE_CYCLE : RunnerStatus.PRE_CYCLE, - }), - RunnerStatus.PRE_CYCLE: frozenmap({ - RunnerEvent.POST_SEGMENT : RunnerStatus.POST_SEGMENT, - }), - RunnerStatus.POST_SEGMENT: frozenmap({ - RunnerEvent.POST_CYCLE : RunnerStatus.POST_CYCLE, - }), - RunnerStatus.POST_CYCLE: frozenmap({ - RunnerEvent.PRE_CYCLE : RunnerStatus.PRE_CYCLE, - }), -}) +] = frozenmap( + { + RunnerStatus.PRE_INITIALIZATION: frozenmap( + { + RunnerEvent.INIT: RunnerStatus.INITIALIZED, + } + ), + RunnerStatus.INITIALIZED: frozenmap( + { + RunnerEvent.PRE_CYCLE: RunnerStatus.PRE_CYCLE, + } + ), + RunnerStatus.PRE_CYCLE: frozenmap( + { + RunnerEvent.POST_SEGMENT: RunnerStatus.POST_SEGMENT, + } + ), + RunnerStatus.POST_SEGMENT: frozenmap( + { + RunnerEvent.POST_CYCLE: RunnerStatus.POST_CYCLE, + } + ), + RunnerStatus.POST_CYCLE: frozenmap( + { + RunnerEvent.PRE_CYCLE: RunnerStatus.PRE_CYCLE, + } + ), + } +) + @attrs.define class RunnerStateMachine: @@ -102,7 +129,7 @@ def send(self, event: RunnerEvent) -> RunnerStatus: logger.info(f"Received event: {event.name}:{event.value}") self.validate_event(event) - + state_transitions = RUNNER_STATE_TRANSITION_TABLE[self.state] new_state = state_transitions[event] @@ -113,14 +140,13 @@ def send(self, event: RunnerEvent) -> RunnerStatus: self.state = new_state return self.state - + + class Runner(Protocol[WalkerState_, RunSegmentData_]): """Abstract base class for the Runner interface.""" @property - def status(self) -> RunnerStatus: - ... - + def status(self) -> RunnerStatus: ... def init(self) -> None: ... @@ -152,18 +178,19 @@ def run_segment( ... def post_cycle( - self, - segments_data: list[RunSegmentData_] | None, + self, + segments_data: list[RunSegmentData_] | None, ) -> None: """Perform post-cycle behavior.""" ... - RunnerFactory = Callable[ - [], Runner, + [], + Runner, ] + @attrs.define class NoRunner(Runner): """Stub Runner that just returns the walkers back with the same state. @@ -187,7 +214,6 @@ def init(self) -> None: def pre_cycle(self) -> None: self.state_machine.send(RunnerEvent.PRE_CYCLE) - def run_segment( self, state: WalkerState_, @@ -198,11 +224,10 @@ def run_segment( raise RunnerStateError( f"Cannot run a segment in state ({self.status.name}:{self.status.value})" ) - + return state, None def post_cycle(self, segments_data: None) -> None: self.state_machine.send(RunnerEvent.POST_SEGMENT) self.state_machine.send(RunnerEvent.POST_CYCLE) - diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index 439fd02c..2d09ca1e 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -43,28 +43,30 @@ """ # Standard Library -import logging -from typing import Final, Any, TypedDict, Generic, TypeVar, Callable, Literal import copy -import time import enum +import logging +import time +from typing import Any, Callable, Final, Generic, Literal, TypedDict, TypeVar -import psutil +# Third Party Library import attrs +import psutil from immutables import Map as frozenmap # First Party Library from wepy.boundary_conditions.boundary import BoundaryConditions +from wepy.monitor import Monitor from wepy.reporter.reporter import Reporter from wepy.resampling.resamplers.resampler import Resampler from wepy.runners.runner import Runner, RunnerFactory, RunSegmentData from wepy.walker import Walker from wepy.work_mapper.base import WorkMapper from wepy.work_mapper.serial import SerialMapper -from wepy.monitor import Monitor logger = logging.getLogger(__name__) + class CycleReportDict(TypedDict): cycle_idx: int new_walkers: list[Walker] @@ -86,6 +88,7 @@ class CycleReportDict(TypedDict): cycle_bc_time: float cycle_resampling_time: float + class ManagerStatus(enum.IntEnum): CONSTRUCTED = enum.auto() PRE_SIMULATION = enum.auto() @@ -114,6 +117,7 @@ class ManagerStatus(enum.IntEnum): CLEANUP_FINISHED = enum.auto() FINISHED = enum.auto() + class ManagerEvent(enum.IntEnum): START_PRE_SIM = enum.auto() START_INITIALIZATION = enum.auto() @@ -147,130 +151,179 @@ class ManagerEvent(enum.IntEnum): MANAGER_STATE_TRANSITION_TABLE: frozenmap[ ManagerStatus, frozenmap[ManagerEvent, ManagerStatus], -] = frozenmap({ - ManagerStatus.CONSTRUCTED: frozenmap({ - ManagerEvent.START_PRE_SIM : ManagerStatus.PRE_SIMULATION, - }), - ManagerStatus.PRE_SIMULATION: frozenmap({ - ManagerEvent.START_INITIALIZATION : ManagerStatus.INITIALIZING, - }), - ManagerStatus.INITIALIZING: frozenmap({ - ManagerEvent.FINISH_INITIALIZATION : ManagerStatus.INITIALIZED, - }), - ManagerStatus.INITIALIZED: frozenmap({ - ManagerEvent.START_SIM : ManagerStatus.SIM_STARTED, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - ManagerStatus.SIM_STARTED: frozenmap({ - ManagerEvent.START_CYCLE : ManagerStatus.RUNNING_CYCLE, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - ManagerStatus.RUNNING_CYCLE: frozenmap({ - ManagerEvent.START_PRE_SEGMENT : ManagerStatus.RUNNING_PRE_SEGMENT, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - ManagerStatus.RUNNING_PRE_SEGMENT: frozenmap({ - ManagerEvent.FINISH_PRE_SEGMENT : ManagerStatus.PRE_SEGMENT_FINISHED, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - ManagerStatus.PRE_SEGMENT_FINISHED: frozenmap({ - ManagerEvent.START_SEGMENT : ManagerStatus.RUNNING_SEGMENT, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - ManagerStatus.RUNNING_SEGMENT: frozenmap({ - ManagerEvent.FINISH_SEGMENT : ManagerStatus.SEGMENT_FINISHED, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - ManagerStatus.SEGMENT_FINISHED: frozenmap({ - ManagerEvent.START_POST_SEGMENT : ManagerStatus.RUNNING_POST_SEGMENT, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - - ManagerStatus.RUNNING_POST_SEGMENT: frozenmap({ - ManagerEvent.FINISH_POST_SEGMENT : ManagerStatus.POST_SEGMENT_FINISHED, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - - ManagerStatus.POST_SEGMENT_FINISHED: frozenmap({ - ManagerEvent.START_BC_WARPING : ManagerStatus.BC_WARPING, - ManagerEvent.START_RESAMPLING : ManagerStatus.RESAMPLING, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - ManagerStatus.BC_WARPING: frozenmap({ - ManagerEvent.FINISH_BC_WARPING : ManagerStatus.POST_BC_WARPING, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - ManagerStatus.POST_BC_WARPING: frozenmap({ - ManagerEvent.START_RESAMPLING : ManagerStatus.RESAMPLING, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - - ManagerStatus.RESAMPLING: frozenmap({ - ManagerEvent.FINISH_RESAMPLING : ManagerStatus.POST_RESAMPLING, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - ManagerStatus.POST_RESAMPLING: frozenmap({ - ManagerEvent.GENERATE_REPORT : ManagerStatus.REPORT_GENERATION, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - - - ManagerStatus.REPORT_GENERATION: frozenmap({ - ManagerEvent.START_REPORTING : ManagerStatus.REPORTING, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - - ManagerStatus.REPORTING: frozenmap({ - ManagerEvent.FINISH_REPORTING : ManagerStatus.POST_REPORTING, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - - ManagerStatus.POST_REPORTING: frozenmap({ - ManagerEvent.START_CYCLE_MONITORING : ManagerStatus.CYCLE_MONITORING, - ManagerEvent.FINISH_CYCLE : ManagerStatus.POST_CYCLE, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - ManagerStatus.CYCLE_MONITORING: frozenmap({ - ManagerEvent.FINISH_CYCLE_MONITORING : ManagerStatus.POST_CYCLE_MONITORING, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - - ManagerStatus.CYCLE_MONITORING: frozenmap({ - ManagerEvent.FINISH_CYCLE_MONITORING : ManagerStatus.POST_CYCLE_MONITORING, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - - ManagerStatus.POST_CYCLE_MONITORING: frozenmap({ - ManagerEvent.FINISH_CYCLE : ManagerStatus.POST_CYCLE, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - - ManagerStatus.POST_CYCLE: frozenmap({ - ManagerEvent.START_CYCLE : ManagerStatus.RUNNING_CYCLE, - ManagerEvent.FINISH_SIMULATION : ManagerStatus.POST_SIMULATION, - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - ManagerStatus.POST_SIMULATION: frozenmap({ - ManagerEvent.START_CLEANUP : ManagerStatus.CLEANING, - }), - ManagerStatus.CLEANING: frozenmap({ - ManagerEvent.FINISH_CLEANUP : ManagerStatus.CLEANUP_FINISHED, - }), - ManagerStatus.CLEANUP_FINISHED: frozenmap({ - ManagerEvent.SHUTDOWN : ManagerStatus.FINISHED, - }), - ManagerStatus.FINISHED: frozenmap({}), -}) +] = frozenmap( + { + ManagerStatus.CONSTRUCTED: frozenmap( + { + ManagerEvent.START_PRE_SIM: ManagerStatus.PRE_SIMULATION, + } + ), + ManagerStatus.PRE_SIMULATION: frozenmap( + { + ManagerEvent.START_INITIALIZATION: ManagerStatus.INITIALIZING, + } + ), + ManagerStatus.INITIALIZING: frozenmap( + { + ManagerEvent.FINISH_INITIALIZATION: ManagerStatus.INITIALIZED, + } + ), + ManagerStatus.INITIALIZED: frozenmap( + { + ManagerEvent.START_SIM: ManagerStatus.SIM_STARTED, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.SIM_STARTED: frozenmap( + { + ManagerEvent.START_CYCLE: ManagerStatus.RUNNING_CYCLE, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.RUNNING_CYCLE: frozenmap( + { + ManagerEvent.START_PRE_SEGMENT: ManagerStatus.RUNNING_PRE_SEGMENT, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.RUNNING_PRE_SEGMENT: frozenmap( + { + ManagerEvent.FINISH_PRE_SEGMENT: ManagerStatus.PRE_SEGMENT_FINISHED, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.PRE_SEGMENT_FINISHED: frozenmap( + { + ManagerEvent.START_SEGMENT: ManagerStatus.RUNNING_SEGMENT, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.RUNNING_SEGMENT: frozenmap( + { + ManagerEvent.FINISH_SEGMENT: ManagerStatus.SEGMENT_FINISHED, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.SEGMENT_FINISHED: frozenmap( + { + ManagerEvent.START_POST_SEGMENT: ManagerStatus.RUNNING_POST_SEGMENT, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.RUNNING_POST_SEGMENT: frozenmap( + { + ManagerEvent.FINISH_POST_SEGMENT: ManagerStatus.POST_SEGMENT_FINISHED, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.POST_SEGMENT_FINISHED: frozenmap( + { + ManagerEvent.START_BC_WARPING: ManagerStatus.BC_WARPING, + ManagerEvent.START_RESAMPLING: ManagerStatus.RESAMPLING, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.BC_WARPING: frozenmap( + { + ManagerEvent.FINISH_BC_WARPING: ManagerStatus.POST_BC_WARPING, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.POST_BC_WARPING: frozenmap( + { + ManagerEvent.START_RESAMPLING: ManagerStatus.RESAMPLING, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.RESAMPLING: frozenmap( + { + ManagerEvent.FINISH_RESAMPLING: ManagerStatus.POST_RESAMPLING, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.POST_RESAMPLING: frozenmap( + { + ManagerEvent.GENERATE_REPORT: ManagerStatus.REPORT_GENERATION, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.REPORT_GENERATION: frozenmap( + { + ManagerEvent.START_REPORTING: ManagerStatus.REPORTING, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.REPORTING: frozenmap( + { + ManagerEvent.FINISH_REPORTING: ManagerStatus.POST_REPORTING, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.POST_REPORTING: frozenmap( + { + ManagerEvent.START_CYCLE_MONITORING: ManagerStatus.CYCLE_MONITORING, + ManagerEvent.FINISH_CYCLE: ManagerStatus.POST_CYCLE, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.CYCLE_MONITORING: frozenmap( + { + ManagerEvent.FINISH_CYCLE_MONITORING: ManagerStatus.POST_CYCLE_MONITORING, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.CYCLE_MONITORING: frozenmap( + { + ManagerEvent.FINISH_CYCLE_MONITORING: ManagerStatus.POST_CYCLE_MONITORING, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.POST_CYCLE_MONITORING: frozenmap( + { + ManagerEvent.FINISH_CYCLE: ManagerStatus.POST_CYCLE, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.POST_CYCLE: frozenmap( + { + ManagerEvent.START_CYCLE: ManagerStatus.RUNNING_CYCLE, + ManagerEvent.FINISH_SIMULATION: ManagerStatus.POST_SIMULATION, + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.POST_SIMULATION: frozenmap( + { + ManagerEvent.START_CLEANUP: ManagerStatus.CLEANING, + } + ), + ManagerStatus.CLEANING: frozenmap( + { + ManagerEvent.FINISH_CLEANUP: ManagerStatus.CLEANUP_FINISHED, + } + ), + ManagerStatus.CLEANUP_FINISHED: frozenmap( + { + ManagerEvent.SHUTDOWN: ManagerStatus.FINISHED, + } + ), + ManagerStatus.FINISHED: frozenmap({}), + } +) + class ManagerStateMachineError(Exception): pass + class ManagerStateTransitionError(ManagerStateMachineError): """Indicates an error with the runner state machine transition.""" + pass + class ManagerStateError(ManagerStateMachineError): """Indicates an error relating to the current state of the runner.""" + pass @@ -295,7 +348,7 @@ def send(self, event: ManagerEvent) -> ManagerStatus: logger.info(f"Received event: {event.name}:{event.value}") self.validate_event(event) - + state_transitions = MANAGER_STATE_TRANSITION_TABLE[self.state] new_state = state_transitions[event] @@ -307,11 +360,14 @@ def send(self, event: ManagerEvent) -> ManagerStatus: return self.state + ResamplerFactory = Callable[[], Resampler] WorkMapperFactory = Callable[[], WorkMapper] State_ = TypeVar("State_") RunSegmentData_ = TypeVar("RunSegmentData_", bound=RunSegmentData, covariant=True) + + class Manager(Generic[State_]): """The class that coordinates wepy simulations. @@ -432,7 +488,6 @@ def __init__( """ - self.init_walkers = copy.deepcopy(init_walkers) self.n_init_walkers = len(init_walkers) @@ -656,7 +711,9 @@ def run_segment( ) except Exception as exception: - logger.info("Exception encountered in segment calculations. Cleaning up before raising.") + logger.info( + "Exception encountered in segment calculations. Cleaning up before raising." + ) # get the errors from the work mapper error queue self.cleanup() @@ -682,7 +739,6 @@ def post_segment(self, segments_data: RunSegmentData_) -> None: self.state_machine.send(ManagerEvent.START_POST_SEGMENT) self._runner.post_cycle(segments_data) self.state_machine.send(ManagerEvent.FINISH_POST_SEGMENT) - def run_cycle( self, @@ -762,7 +818,6 @@ def run_cycle( self.state_machine.send(ManagerEvent.START_CYCLE) - # run the runner pre-cycle hook start = time.time() self.pre_segment() @@ -780,12 +835,8 @@ def run_cycle( ) new_walkers = [ - Walker( - state=new_state, - weight=walker.weight - ) - for walker, new_state - in zip(walkers, new_states, strict=True) + Walker(state=new_state, weight=walker.weight) + for walker, new_state in zip(walkers, new_states, strict=True) ] end = time.time() @@ -814,7 +865,7 @@ def run_cycle( logger.info("Boundary conditions were provided, applying.") self.state_machine.send(ManagerEvent.START_BC_WARPING) - + # apply rules of boundary conditions and warp walkers through space start = time.time() bc_results = self.boundary_conditions.warp_walkers(new_walkers, cycle_idx) @@ -840,7 +891,7 @@ def run_cycle( resampling_results = self._resampler.resample(warped_walkers) self.state_machine.send(ManagerEvent.FINISH_RESAMPLING) - + end = time.time() resampling_time = end - start @@ -860,7 +911,7 @@ def run_cycle( logger.info("Segment timings provided by work mapper, recording.") # count up the total sampling time from the segments - sampling_time = 0. + sampling_time = 0.0 for ( worker_id, segments_times, @@ -871,35 +922,38 @@ def run_cycle( # calculate the overhead for logging sim_manager_segment_overhead_time = sim_manager_segment_time - sampling_time - logger.info(f"Simulation manager overhead time: {sim_manager_segment_overhead_time}") - + logger.info( + f"Simulation manager overhead time: {sim_manager_segment_overhead_time}" + ) else: logger.info("Worker segment times not provided") sim_manager_segment_overhead_time = 0.0 - report = CycleReportDict({ - "cycle_idx": cycle_idx, - "new_walkers": new_walkers, - "warp_data": warp_data, - "bc_data": bc_data, - "progress_data": progress_data, - "resampling_data": resampling_data, - "resampler_data": resampler_data, - "n_segment_steps": n_segment_steps, - "resampled_walkers": resampled_walkers, - # timings - "runner_precycle_time": presegment_time, - "runner_postcycle_time": post_segment_time, - "sim_manager_segment_overhead_time": sim_manager_segment_overhead_time, - # TODO: fix this - "runner_splits_time": segments_data, - "worker_segment_times": seg_times, - "cycle_sim_manager_segment_time": sim_manager_segment_time, - "cycle_runner_time": sim_manager_segment_time, - "cycle_bc_time": bc_time, - "cycle_resampling_time": resampling_time, - }) + report = CycleReportDict( + { + "cycle_idx": cycle_idx, + "new_walkers": new_walkers, + "warp_data": warp_data, + "bc_data": bc_data, + "progress_data": progress_data, + "resampling_data": resampling_data, + "resampler_data": resampler_data, + "n_segment_steps": n_segment_steps, + "resampled_walkers": resampled_walkers, + # timings + "runner_precycle_time": presegment_time, + "runner_postcycle_time": post_segment_time, + "sim_manager_segment_overhead_time": sim_manager_segment_overhead_time, + # TODO: fix this + "runner_splits_time": segments_data, + "worker_segment_times": seg_times, + "cycle_sim_manager_segment_time": sim_manager_segment_time, + "cycle_runner_time": sim_manager_segment_time, + "cycle_bc_time": bc_time, + "cycle_resampling_time": resampling_time, + } + ) self._last_report = report @@ -920,7 +974,11 @@ def run_cycle( self.state_machine.send(ManagerEvent.FINISH_CYCLE) - return resampled_walkers, (self._runner, self.boundary_conditions, self._resampler) + return resampled_walkers, ( + self._runner, + self.boundary_conditions, + self._resampler, + ) def run_simulation( self, @@ -957,9 +1015,10 @@ def run_simulation( self.state_machine.send(ManagerEvent.START_PRE_SIM) - if type(segment_lengths) == int: - logger.info("Single number of steps provided for simulation, using this for all cycles.") + logger.info( + "Single number of steps provided for simulation, using this for all cycles." + ) segment_lengths = [segment_lengths for _ in range(n_cycles)] walkers = self.init_walkers @@ -972,7 +1031,9 @@ def run_simulation( logger.info(f"Running cycle: {cycle_idx}") walkers, filters = self.run_cycle( - walkers, segment_lengths[cycle_idx], cycle_idx, + walkers, + segment_lengths[cycle_idx], + cycle_idx, ) logger.info(f"Finished running cycle: {cycle_idx}") diff --git a/src/wepy/util/multiprocessing.py b/src/wepy/util/multiprocessing.py index 61b09a70..d7d73e45 100644 --- a/src/wepy/util/multiprocessing.py +++ b/src/wepy/util/multiprocessing.py @@ -1,10 +1,11 @@ -import os +# Standard Library import contextlib import copy import logging -import logging.handlers import logging.config +import logging.handlers import multiprocessing as mp +import os from typing import Generator logger = logging.getLogger(__name__) @@ -24,6 +25,7 @@ }, } + def proc_pool_worker_setup(log_queue: mp.Queue) -> None: """Pool(initializer=) function that handles logging properly. @@ -33,7 +35,7 @@ def proc_pool_worker_setup(log_queue: mp.Queue) -> None: Works properly with 'spawn' start method. - + """ @@ -44,14 +46,10 @@ def proc_pool_worker_setup(log_queue: mp.Queue) -> None: for name, logger in parent_loggers.items(): if isinstance(logger, logging.Logger): config.setdefault("loggers", {})[name] = { - "level" : logging.getLevelName(logger.level), - "propagate" : logger.propagate, - "handlers" : [], - "filters" : [ - f.__class__.__name__ - for f - in logger.filters - ], + "level": logging.getLevelName(logger.level), + "propagate": logger.propagate, + "handlers": [], + "filters": [f.__class__.__name__ for f in logger.filters], } logging.config.dictConfig(config) @@ -73,6 +71,7 @@ def _dummy_task(foo: int) -> int: return foo + 1 + class WorkerFormatter(logging.Formatter): def __init__(self, base_formatter: logging.Formatter): self.base_formatter = base_formatter @@ -99,6 +98,7 @@ def format(self, record): record.msg = original_msg return formatted + @contextlib.contextmanager def queue_listener_context(mp_ctx) -> Generator[None, None, None]: @@ -106,7 +106,6 @@ def queue_listener_context(mp_ctx) -> Generator[None, None, None]: root_logger = logging.getLogger() old_factory = logging.getLogRecordFactory() - def record_factory(*args, **kwargs): record = old_factory(*args, **kwargs) @@ -116,11 +115,7 @@ def record_factory(*args, **kwargs): if not hasattr(record, "process"): record.process = os.getpid() - old_formatters = [ - handler.formatter - for handler - in root_logger.handlers - ] + old_formatters = [handler.formatter for handler in root_logger.handlers] listener_handlers = [] for handler in root_logger.handlers: @@ -144,7 +139,9 @@ def record_factory(*args, **kwargs): listener.stop() logger.info("Listener stopped") - for handler, formatter in zip(root_logger.handlers, old_formatters, strict=True): + for handler, formatter in zip( + root_logger.handlers, old_formatters, strict=True + ): handler.setFormatter(formatter) logging.setLogRecordFactory(old_factory) diff --git a/src/wepy/util/openmm.py b/src/wepy/util/openmm.py index d5f89a13..231322b1 100644 --- a/src/wepy/util/openmm.py +++ b/src/wepy/util/openmm.py @@ -1,13 +1,18 @@ -from typing import Generator +# Standard Library from collections.abc import Iterable +from typing import Generator + +# Third Party Library import numpy as np import openmm + def array3d_to_vec3(array: np.typing.ArrayLike) -> Generator[openmm.Vec3, None, None]: for row in array: yield openmm.Vec3(*row.tolist()) + def vec3_to_array3d(vec3s: Iterable[openmm.Vec3]) -> np.typing.ArrayLike: vs = [] @@ -21,22 +26,21 @@ def vec3_to_array3d(vec3s: Iterable[openmm.Vec3]) -> np.typing.ArrayLike: ) return np.array(vs) - -def triclinic_volume_vec3_quantity(box_vectors: list[openmm.unit.Quantity]) -> openmm.unit.Quantity: +def triclinic_volume_vec3_quantity( + box_vectors: list[openmm.unit.Quantity], +) -> openmm.unit.Quantity: return np.dot(box_vectors[0], np.cross(box_vectors[1], box_vectors[2])) + def format_box_vectors_line(box_vectors: list[openmm.unit.Quantity]) -> str: unit = box_vectors[0].unit vec_strs = [] for vec in box_vectors: - mags = [ - q.value_in_unit(q.unit) - for q in vec - ] + mags = [q.value_in_unit(q.unit) for q in vec] vec_s = f"{mags[0]:.3f}, {mags[1]:.3f}, {mags[2]:.3f}" vec_strs.append(vec_s) diff --git a/src/wepy/walker.py b/src/wepy/walker.py index 17139c19..7c53dc43 100644 --- a/src/wepy/walker.py +++ b/src/wepy/walker.py @@ -28,12 +28,11 @@ # Standard Library import logging -from typing import Protocol, Any, TypeVar, Generic import math - -# Standard Library import random as rand +from typing import Any, Generic, Protocol, TypeVar +# Third Party Library import attrs logger = logging.getLogger(__name__) diff --git a/src/wepy/work_mapper/base.py b/src/wepy/work_mapper/base.py index bdf5301f..97945f33 100644 --- a/src/wepy/work_mapper/base.py +++ b/src/wepy/work_mapper/base.py @@ -1,24 +1,26 @@ """Base classes and definitions for all work mappers.""" # Standard Library -import traceback -from typing import Callable, Literal, Generic, TypeVar, Protocol, Any, ParamSpec, Concatenate import logging +from typing import ( + Callable, + Protocol, + TypeVar, +) -# Standard Library - -from wepy.walker import WalkerState +# First Party Library from wepy.runners.runner import RunSegmentData +from wepy.walker import WalkerState logger = logging.getLogger(__name__) WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) RunSegmentData_ = TypeVar("RunSegmentData_", bound=RunSegmentData) + class WorkMapper(Protocol[WalkerState_, RunSegmentData_]): - def init(self) -> None: - ... + def init(self) -> None: ... def map( self, diff --git a/src/wepy/work_mapper/openmm/__init__.py b/src/wepy/work_mapper/openmm/__init__.py index 3c17b238..7cfe904d 100644 --- a/src/wepy/work_mapper/openmm/__init__.py +++ b/src/wepy/work_mapper/openmm/__init__.py @@ -1,3 +1,4 @@ +# Local Modules from .proc_pool import OpenMMProcPoolWorkMapperFactory from .serial import OpenMMSerialWorkMapperFactory diff --git a/src/wepy/work_mapper/openmm/proc_pool.py b/src/wepy/work_mapper/openmm/proc_pool.py index 05b0cb03..25885d24 100644 --- a/src/wepy/work_mapper/openmm/proc_pool.py +++ b/src/wepy/work_mapper/openmm/proc_pool.py @@ -1,20 +1,22 @@ """Special OpenMM mappers.""" -import os + +# Standard Library +import itertools import logging -import logging.handlers -import logging.config -from typing import Literal, Any, Callable, Generator -import contextlib -import time -import copy import multiprocessing as mp -import itertools +from typing import Callable +# Third Party Library import attrs +# First Party Library +from wepy.runners.openmm import ( + GPU_PLATFORMS, + OpenMMPlatformName, + OpenMMState, +) from wepy.util.multiprocessing import proc_pool_worker_setup, queue_listener_context from wepy.work_mapper.base import WorkMapper -from wepy.runners.openmm import OpenMMState, OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS logger = logging.getLogger(__name__) @@ -30,24 +32,24 @@ def __init__( device_platform_properties: list[dict[str, str]] | None = None, ): - if platform in {"CUDA", "HIP", "OpenCL"}: if device_ids is None: - raise ValueError(f"For accelerator platforms ({platform} requested) device_ids must be given.") + raise ValueError( + f"For accelerator platforms ({platform} requested) device_ids must be given." + ) if device_platform_properties is not None: if len(device_platform_properties) != len(device_ids): - raise ValueError(f"{len(device_ids)} devices requested, but only {len(device_platform_properties)} device platform property dicts given.") + raise ValueError( + f"{len(device_ids)} devices requested, but only {len(device_platform_properties)} device platform property dicts given." + ) else: self._device_platform_properties = { - idx : props - for idx, props - in enumerate(device_platform_properties) + idx: props for idx, props in enumerate(device_platform_properties) } - else: self._device_platform_properties = None @@ -56,61 +58,67 @@ def __init__( if device_ids is not None and num_procs != len(device_ids): - raise ValueError(f"When device_ids is given ({device_ids}) it must be the same length as the number of processes: {num_procs}") - - self._device_ids: dict[int,int] = { - idx: device_id - for idx, device_id - in enumerate(device_ids) - } if device_ids is not None else None + raise ValueError( + f"When device_ids is given ({device_ids}) it must be the same length as the number of processes: {num_procs}" + ) + + self._device_ids: dict[int, int] = ( + {idx: device_id for idx, device_id in enumerate(device_ids)} + if device_ids is not None + else None + ) self._num_procs = num_procs - def init( - self, + self, ) -> None: logger.info("Initializing ProcPoolMapper") - logger.info(f"Initializing local multiprocessing context with start method: spawn") + logger.info( + "Initializing local multiprocessing context with start method: spawn" + ) self._mp_ctx = mp.get_context(method="spawn") def cleanup(self) -> None: logger.info("Running ProcPoolMapper cleanup") logger.info("Nothing to do") - def map( - self, - task: Callable[[OpenMMState, int], OpenMMState], - walker_states: list[OpenMMState], - segment_lengths: list[int], + self, + task: Callable[[OpenMMState, int], OpenMMState], + walker_states: list[OpenMMState], + segment_lengths: list[int], ) -> list[OpenMMState]: - logger.info(f"Running map on {len(walker_states)} in batches of {self._num_procs}") + logger.info( + f"Running map on {len(walker_states)} in batches of {self._num_procs}" + ) # spin up a new pool for each map logger.info(f"Starting process Pool with {self._num_procs}") with ( - queue_listener_context(self._mp_ctx) as log_queue, - self._mp_ctx.Pool( - processes=self._num_procs, - # only run one thing per task, just to make sure - # everything is cleaned up - maxtasksperchild=1, - initializer=proc_pool_worker_setup, - initargs=(log_queue,), - ) as pool, + queue_listener_context(self._mp_ctx) as log_queue, + self._mp_ctx.Pool( + processes=self._num_procs, + # only run one thing per task, just to make sure + # everything is cleaned up + maxtasksperchild=1, + initializer=proc_pool_worker_setup, + initargs=(log_queue,), + ) as pool, ): results = [] - for batch_idx, batch in enumerate(itertools.batched( + for batch_idx, batch in enumerate( + itertools.batched( zip(walker_states, segment_lengths, strict=True), self._num_procs, strict=False, - )): + ) + ): logger.info(f"Submitting batch: {batch_idx}") @@ -123,22 +131,24 @@ def map( worker_idx = batch_task_idx if self._device_ids is not None: - logger.info("device_ids have been given, setting up special platform properties for each task.") + logger.info( + "device_ids have been given, setting up special platform properties for each task." + ) device_id = str(self._device_ids[worker_idx]) worker_platform_props = { **( - {"DeviceIndex" : device_id} + {"DeviceIndex": device_id} if self._platform in GPU_PLATFORMS else {} ), **( self._device_platform_properties[worker_idx] if ( - self._device_platform_properties is not None and - worker_idx in self._device_platform_properties + self._device_platform_properties is not None + and worker_idx in self._device_platform_properties ) else {} - ) + ), } logger.info( f"Device IDs given, resolved to using worker specific platform properties: {worker_platform_props}" @@ -155,10 +165,14 @@ def map( logger.info("Only global platform properties provided") _platform_kwargs = global_kwargs case (None, local_kwargs): - logger.info("Only device specific platform properties provided") + logger.info( + "Only device specific platform properties provided" + ) _platform_kwargs = worker_platform_props case (global_kwargs, local_kwargs): - logger.info("Both global and device specific platform properties provided") + logger.info( + "Both global and device specific platform properties provided" + ) _platform_kwargs = global_kwargs | worker_platform_props logger.info(f"Resolved 'platform_kwargs' : {_platform_kwargs}") @@ -170,7 +184,7 @@ def map( kwds=dict( platform_name=self._platform, platform_kwargs=_platform_kwargs, - ) + ), ) logger.info(f"Task {task_idx} submitted") batch_results.append(result) @@ -181,7 +195,6 @@ def map( task_idx = batch_idx + batch_task_idx logger.info(f"Awaiting task {task_idx}") - try: real_result = task_result.get() # TODO: add timeouts and retries @@ -196,10 +209,11 @@ def map( logger.info(f"Batch {batch_idx} completed") - logger.info(f"Completed all batches, terminating Pool") + logger.info("Completed all batches, terminating Pool") return results + @attrs.define class OpenMMProcPoolWorkMapperFactory: @@ -213,17 +227,22 @@ def __attrs_post_init__(self) -> None: if self.platform in {"CUDA", "HIP", "OpenCL"}: if self.device_ids is None: - raise ValueError(f"For accelerator platforms ({self.platform} requested) device_ids must be given.") + raise ValueError( + f"For accelerator platforms ({self.platform} requested) device_ids must be given." + ) if self.device_platform_properties is not None: if len(self.device_platform_properties) != len(self.device_ids): - raise ValueError(f"{len(self.device_ids)} devices requested, but only {len(self.device_platform_properties)} device platform property dicts given.") + raise ValueError( + f"{len(self.device_ids)} devices requested, but only {len(self.device_platform_properties)} device platform property dicts given." + ) if self.device_ids is not None and self.num_procs != len(self.device_ids): - raise ValueError(f"When device_ids is given ({self.device_ids}) it must be the same length as the number of processes: {self.num_procs}") - + raise ValueError( + f"When device_ids is given ({self.device_ids}) it must be the same length as the number of processes: {self.num_procs}" + ) def __call__(self) -> OpenMMProcPoolWorkMapper: @@ -234,4 +253,3 @@ def __call__(self) -> OpenMMProcPoolWorkMapper: global_platform_properties=self.global_platform_properties, device_platform_properties=self.device_platform_properties, ) - diff --git a/src/wepy/work_mapper/openmm/ray.py b/src/wepy/work_mapper/openmm/ray.py index f29b978c..93f51c50 100644 --- a/src/wepy/work_mapper/openmm/ray.py +++ b/src/wepy/work_mapper/openmm/ray.py @@ -1,72 +1,77 @@ """Special OpenMM mappers.""" -import logging -from typing import Literal, Any, Callable -import time + +# Standard Library import itertools +import logging +from typing import Any +# Third Party Library import attrs import ray import ray.util.multiprocessing -from wepy.work_mapper.base import WorkMapper -from wepy.runners.openmm import OpenMMState, OpenMMRunner, PlatformKwargs, OpenMMPlatformName +# First Party Library +from wepy.runners.openmm import ( + OpenMMState, +) logger = logging.getLogger(__name__) + @attrs.define class OpenMMRayTask: openmm_task: OpenMMTask def __call__( - self, - *args, - **kwargs, + self, + *args, + **kwargs, ) -> OpenMMState: logging.basicConfig(level=logging.INFO) - logging.getLogger("OpenMMRayTask").info("Configured logging in OpenMMRayTask process") + logging.getLogger("OpenMMRayTask").info( + "Configured logging in OpenMMRayTask process" + ) return self.openmm_task(*args, **kwargs) class OpenMMRayPoolWorkMapper: - def __init__( self, platform: str, device_ids: list[int] | None = None, device_platform_properties: list[dict[str, str]] | None = None, global_platform_properties: dict[str, str] | None = None, - ray_init_args: dict[str,Any] | None = None, + ray_init_args: dict[str, Any] | None = None, ): if platform in {"CUDA", "HIP", "OpenCL"}: if device_ids is None: - raise ValueError(f"For accelerator platforms ({platform} requested) device_ids must be given.") + raise ValueError( + f"For accelerator platforms ({platform} requested) device_ids must be given." + ) if device_platform_properties is not None: if len(device_platform_properties) != device_ids: - raise ValueError(f"{len(device_ids)} requested, but only {len(device_platform_properties)} given.") + raise ValueError( + f"{len(device_ids)} requested, but only {len(device_platform_properties)} given." + ) else: self._device_platform_properties = { - idx : props - for idx, props - in enumerate(device_platform_properties) + idx: props for idx, props in enumerate(device_platform_properties) } - else: self._device_platform_properties = None self._platform = platform - self._device_ids: dict[int,int] = { - idx: device_id - for idx, device_id - in enumerate(device_ids) + self._device_ids: dict[int, int] = { + idx: device_id for idx, device_id in enumerate(device_ids) } self._num_workers = len(device_ids) @@ -76,17 +81,17 @@ def __init__( else: self._ray_init_args = None - self._global_platform_properties = global_platform_properties def init( - self, + self, ) -> None: logger.info("Initializing RayPoolMapper") - self._ray_ctx = ray.init(**(self._ray_init_args if self._ray_init_args is not None else {})) - + self._ray_ctx = ray.init( + **(self._ray_init_args if self._ray_init_args is not None else {}) + ) def cleanup(self) -> None: @@ -95,28 +100,32 @@ def cleanup(self) -> None: ray.shutdown() def map( - self, - tasks: list[OpenMMTask], - walker_states: list[OpenMMState], + self, + tasks: list[OpenMMTask], + walker_states: list[OpenMMState], ) -> list[OpenMMState]: - logger.info(f"Running map on {len(walker_states)} in batches of {self._num_workers}") + logger.info( + f"Running map on {len(walker_states)} in batches of {self._num_workers}" + ) # spin up a new pool for each map logger.info(f"Starting ray Pool with {self._num_workers} workers") with ray.util.multiprocessing.Pool( - processes=self._num_workers, - # only run one thing per task, just to make sure - # everything is cleaned up - maxtasksperchild=1, + processes=self._num_workers, + # only run one thing per task, just to make sure + # everything is cleaned up + maxtasksperchild=1, ) as pool: results = [] - for batch_idx, batch in enumerate(itertools.batched( + for batch_idx, batch in enumerate( + itertools.batched( zip(walker_states, tasks, strict=True), self._num_workers, strict=False, - )): + ) + ): logger.info(f"Submitting batch: {batch_idx}") @@ -129,9 +138,11 @@ def map( worker_idx = batch_task_idx if self._device_ids is not None and self._platform in GPU_PLATFORMS: - logger.info("Worker platform configured and resolving platform properties.") + logger.info( + "Worker platform configured and resolving platform properties." + ) platform_kwargs = self._global_platform_properties | { - "DeviceIndex" : str(self._device_ids[worker_idx]), + "DeviceIndex": str(self._device_ids[worker_idx]), **( self._device_platform_properties[worker_idx] if self._device_platform_properties is not None @@ -144,14 +155,15 @@ def map( _task = OpenMMRayTask(task) logger.info(f"Submitting task {task_idx} to worker {worker_idx}") - logger.info(f"Injecting: platform={self._platform}, platform_kwargs={platform_kwargs}") + logger.info( + f"Injecting: platform={self._platform}, platform_kwargs={platform_kwargs}" + ) result = pool.apply_async( _task, args=(walker_state,), kwargs=dict( - platform=self._platform, - platform_kwargs=platform_kwargs - ) + platform=self._platform, platform_kwargs=platform_kwargs + ), ) logger.info(f"Task {task_idx} submitted") batch_results.append(result) @@ -162,7 +174,6 @@ def map( task_idx = batch_idx + batch_task_idx logger.info(f"Awaiting task {task_idx}") - try: real_result = task_result.get() # TODO: add timeouts and retries @@ -177,7 +188,6 @@ def map( logger.info(f"Batch {batch_idx} completed") - logger.info(f"Completed all batches, terminating Pool") - + logger.info("Completed all batches, terminating Pool") return results diff --git a/src/wepy/work_mapper/openmm/serial.py b/src/wepy/work_mapper/openmm/serial.py index 12a6c641..7022ed01 100644 --- a/src/wepy/work_mapper/openmm/serial.py +++ b/src/wepy/work_mapper/openmm/serial.py @@ -1,12 +1,16 @@ -import logging -from typing import Literal, Any, Callable +# Standard Library import time -import itertools +from typing import Callable +# Third Party Library import attrs +# First Party Library +from wepy.runners.openmm import ( + OpenMMPlatformName, + OpenMMState, +) from wepy.work_mapper.base import WorkMapper -from wepy.runners.openmm import OpenMMState, OpenMMRunner, PlatformKwargs, OpenMMPlatformName, GPU_PLATFORMS class OpenMMSerialWorkMapper(WorkMapper): @@ -19,7 +23,9 @@ def __init__( self._worker_segment_times: dict[int, list[float]] = {0: []} self._platform = platform - self._global_platform_properties = global_platform_properties if global_platform_properties is not None else {} + self._global_platform_properties = ( + global_platform_properties if global_platform_properties is not None else {} + ) def get_worker_segment_times(self) -> dict[int, list[float]]: """The run timings for each segment for each walker. @@ -39,15 +45,17 @@ def cleanup(self) -> None: pass def map( - self, - task: Callable[[OpenMMState, int], OpenMMState], - walker_states: list[OpenMMState], - segment_lengths: list[int], + self, + task: Callable[[OpenMMState, int], OpenMMState], + walker_states: list[OpenMMState], + segment_lengths: list[int], ) -> list[OpenMMState]: segment_times: list[float] = [] results: list[OpenMMState] = [] - for task_idx, task_args in enumerate(zip(walker_states, segment_lengths, strict=True)): + for task_idx, task_args in enumerate( + zip(walker_states, segment_lengths, strict=True) + ): tic = time.time() result = task( @@ -64,6 +72,7 @@ def map( return results + @attrs.define class OpenMMSerialWorkMapperFactory: diff --git a/src/wepy/work_mapper/serial.py b/src/wepy/work_mapper/serial.py index c6a9fe62..2e0035ee 100644 --- a/src/wepy/work_mapper/serial.py +++ b/src/wepy/work_mapper/serial.py @@ -1,13 +1,17 @@ """Reference implementation of a serial WorkMapper""" -import time -import sys -import traceback -from typing import Callable, Literal, Generic, TypeVar, Protocol, Any, ParamSpec, Concatenate, Sequence +# Standard Library import logging +import time +from typing import ( + Callable, + Generic, + TypeVar, +) -from wepy.walker import Walker, WalkerState +# First Party Library from wepy.runners.runner import RunSegmentData +from wepy.walker import WalkerState logger = logging.getLogger(__name__) @@ -15,11 +19,13 @@ WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) RunSegmentData_ = TypeVar("RunSegmentData_", bound=RunSegmentData) + class SerialMapper( - Generic[ - WalkerState_, - RunSegmentData_, - ]): + Generic[ + WalkerState_, + RunSegmentData_, + ] +): """Basic non-parallel reference implementation of a mapper.""" def __init__( @@ -27,7 +33,6 @@ def __init__( ) -> None: self._worker_segment_times: dict[int, list[float]] = {0: []} - def get_worker_segment_times(self) -> dict[int, list[float]]: """The run timings for each segment for each walker. @@ -46,25 +51,25 @@ def cleanup(self) -> None: pass def map( - self, - task: Callable[ - [ - WalkerState_, - int, - ], - WalkerState_ + self, + task: Callable[ + [ + WalkerState_, + int, ], - walker_states: list[WalkerState_], - segment_lengths: list[int], + WalkerState_, + ], + walker_states: list[WalkerState_], + segment_lengths: list[int], ) -> list[tuple[WalkerState_, RunSegmentData_]]: segment_times: list[float] = [] results: list[WalkerState_] = [] for task_idx, task_args in enumerate( - zip( - walker_states, - segment_lengths, - strict=True, - ) + zip( + walker_states, + segment_lengths, + strict=True, + ) ): tic = time.time() diff --git a/src/wepy_tools/monitoring/prometheus.py b/src/wepy_tools/monitoring/prometheus.py index 722e8ae3..94256a27 100644 --- a/src/wepy_tools/monitoring/prometheus.py +++ b/src/wepy_tools/monitoring/prometheus.py @@ -4,10 +4,12 @@ logger = logging.getLogger(__name__) # Third Party Library -from wepy.monitor import Monitor import prometheus_client as prom from pympler.asizeof import asizeof +# First Party Library +from wepy.monitor import Monitor + class SimMonitor(Monitor): """A simulation monitor using a prometheus http server""" diff --git a/src/wepy_tools/systems/alanine_dipeptide.py b/src/wepy_tools/systems/alanine_dipeptide.py index 14e3b167..c88f1037 100644 --- a/src/wepy_tools/systems/alanine_dipeptide.py +++ b/src/wepy_tools/systems/alanine_dipeptide.py @@ -1,18 +1,19 @@ -from typing import Any +# Standard Library import importlib.resources +# Third Party Library import attrs +import mdtraj import numpy as np import openmm import openmm.app -import openmm.unit -import mdtraj +# First Party Library +from wepy.resampling.distances.base import Distance from wepy.runners.openmm import OpenMMState, OpenMMStateWrapper +from wepy.util.mdtraj import json_to_mdtraj_topology, traj_fields_to_mdtraj from wepy.walker import WalkerState -from wepy.runners.openmm import OpenMMState -from wepy.resampling.distances.base import Distance -from wepy.util.mdtraj import traj_fields_to_mdtraj, json_to_mdtraj_topology + class AlanineDipeptideExplicitSystem: @@ -26,7 +27,9 @@ class AlanineDipeptideExplicitSystem: def __init__(self) -> None: # load the system and state information for the simulation - ala_files = importlib.resources.files("wepy_tools.systems.data.alanine_dipeptide_explicit") + ala_files = importlib.resources.files( + "wepy_tools.systems.data.alanine_dipeptide_explicit" + ) system_xml_path = ala_files / "alanine-dipeptide-explicit.system.omm.xml" state_xml_path = ala_files / "alanine-dipeptide-explicit.state.omm.xml" top_json_path = ala_files / "alanine-dipeptide-explicit.top.json" @@ -39,7 +42,7 @@ def __init__(self) -> None: self.json_top = top_json_path.read_text() self.mdj_top = json_to_mdtraj_topology(self.json_top) self.topology = self.mdj_top.to_openmm() - + @attrs.define class AlanineDipeptideRamachandranDistanceImage(WalkerState): @@ -47,6 +50,7 @@ class AlanineDipeptideRamachandranDistanceImage(WalkerState): phis: np.typing.ArrayLike psis: np.typing.ArrayLike + @attrs.define class AlanineDipeptideRamachandranDistance(Distance): # the parsed JSON topology of plain python objects @@ -54,13 +58,11 @@ class AlanineDipeptideRamachandranDistance(Distance): def image(self, state: OpenMMState) -> AlanineDipeptideRamachandranDistanceImage: - _unit = state["positions"].unit state_dict = { # traj shape to match interface requirements - key : np.array([quantity.value_in_unit(_unit)]) - for key, quantity - in state.to_dict().items() + key: np.array([quantity.value_in_unit(_unit)]) + for key, quantity in state.to_dict().items() if key in {"positions", "box_vectors"} } traj = traj_fields_to_mdtraj( @@ -86,11 +88,10 @@ def image(self, state: OpenMMState) -> AlanineDipeptideRamachandranDistanceImage psis=psis, ) - def image_distance( - self, - image_a: AlanineDipeptideRamachandranDistanceImage, - image_b: AlanineDipeptideRamachandranDistanceImage, + self, + image_a: AlanineDipeptideRamachandranDistanceImage, + image_b: AlanineDipeptideRamachandranDistanceImage, ) -> float: angles_a = np.concatenate((image_a.phis, image_a.psis)) diff --git a/src/wepy_tools/systems/lennard_jones.py b/src/wepy_tools/systems/lennard_jones.py index 516274a3..e435160b 100644 --- a/src/wepy_tools/systems/lennard_jones.py +++ b/src/wepy_tools/systems/lennard_jones.py @@ -1,18 +1,17 @@ # Third Party Library +import attrs import numpy as np import openmm import openmm.app import openmm.unit from scipy.spatial.distance import euclidean -import attrs - # First Party Library from wepy.resampling.distances.base import Distance from wepy.runners.openmm import OpenMMState -class LennardJonesPair: +class LennardJonesPair: """Create a pair of Lennard-Jones particles. Parameters @@ -26,7 +25,6 @@ class LennardJonesPair: Examples -------- - Create Lennard-Jones pair. >>> test = LennardJonesPair() @@ -50,7 +48,12 @@ class LennardJonesPair: """ - def __init__(self, mass=39.9 * openmm.unit.amu, sigma=3.350 * openmm.unit.angstrom, epsilon=10.0 * openmm.unit.kilocalories_per_mole): + def __init__( + self, + mass=39.9 * openmm.unit.amu, + sigma=3.350 * openmm.unit.angstrom, + epsilon=10.0 * openmm.unit.kilocalories_per_mole, + ): # Store parameters self.mass = mass @@ -68,9 +71,11 @@ def __init__(self, mass=39.9 * openmm.unit.amu, sigma=3.350 * openmm.unit.angstr force.setNonbondedMethod(openmm.NonbondedForce.NoCutoff) # Create positions. - positions = openmm.unit.Quantity(np.zeros([2, 3], np.float32), openmm.unit.angstrom) + positions = openmm.unit.Quantity( + np.zeros([2, 3], np.float32), openmm.unit.angstrom + ) # Move the second particle along the x axis to be at the potential minimum. - positions[1, 0] = 2.0**(1.0 / 6.0) * sigma + positions[1, 0] = 2.0 ** (1.0 / 6.0) * sigma # Create first particle. system.addParticle(mass) @@ -92,18 +97,20 @@ def __init__(self, mass=39.9 * openmm.unit.amu, sigma=3.350 * openmm.unit.angstr # Create topology. topology = openmm.app.Topology() - element = openmm.app.Element.getBySymbol('Ar') + element = openmm.app.Element.getBySymbol("Ar") chain = topology.addChain() - residue = topology.addResidue('Ar', chain) - topology.addAtom('Ar', element, residue) - residue = topology.addResidue('Ar', chain) - topology.addAtom('Ar', element, residue) + residue = topology.addResidue("Ar", chain) + topology.addAtom("Ar", element, residue) + residue = topology.addResidue("Ar", chain) + topology.addAtom("Ar", element, residue) self.topology = topology + @attrs.define class PairDistanceImage: positions: np.typing.ArrayLike + class PairDistance(Distance): def __init__(self, metric=euclidean): self.metric = metric @@ -111,7 +118,9 @@ def __init__(self, metric=euclidean): def image(self, state: OpenMMState) -> PairDistanceImage: return state.positions - def image_distance(self, image_a: PairDistanceImage, image_b: PairDistanceImage) -> float: + def image_distance( + self, image_a: PairDistanceImage, image_b: PairDistanceImage + ) -> float: dist_a = self.metric(image_a[0], image_a[1]) dist_b = self.metric(image_b[0], image_b[1]) diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index 54155fc9..4be9d427 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -5,37 +5,46 @@ Configurable platforms. """ -import importlib.resources + +# Standard Library import copy + +# Third Party Library +import mdtraj import openmm import psutil -import mdtraj -# TODO: use the high-level API imports -from wepy.walker import Walker -from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState, OpenMMStateWrapper -from wepy.runners.openmm.runner import _DEFAULT_STATE_TIME_INTERVAL, _DEFAULT_HEARTBEAT_INTERVAL -from wepy.sim_manager import Manager -from wepy.work_mapper.openmm import OpenMMProcPoolWorkMapperFactory +# First Party Library from wepy.resampling.resamplers.revo import REVOResamplerFactory +from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState +from wepy.runners.openmm.runner import ( + _DEFAULT_HEARTBEAT_INTERVAL, + _DEFAULT_STATE_TIME_INTERVAL, +) +from wepy.sim_manager import Manager from wepy.util.mdtraj import mdtraj_to_json_topology -from wepy.resampling.resamplers.noresampler import NoResampler -from wepy.util.mdtraj import json_to_mdtraj_topology +# TODO: use the high-level API imports +from wepy.walker import Walker +from wepy.work_mapper.openmm import OpenMMProcPoolWorkMapperFactory +from wepy_tools.systems.alanine_dipeptide import ( + AlanineDipeptideExplicitSystem, + AlanineDipeptideRamachandranDistance, +) from wepy_tools.systems.lennard_jones import LennardJonesPair, PairDistance -from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideRamachandranDistance, AlanineDipeptideExplicitSystem -STEP_SIZE = 2. * openmm.unit.femtosecond -TEMPERATURE = 300. * openmm.unit.kelvin +STEP_SIZE = 2.0 * openmm.unit.femtosecond +TEMPERATURE = 300.0 * openmm.unit.kelvin # minimum number of steps to hit the logging reporters, useful just # for testing the defaults TIME_INTERVAL_STEPS = round(_DEFAULT_STATE_TIME_INTERVAL / STEP_SIZE) MIN_INTERVAL_STEPS = ( - TIME_INTERVAL_STEPS - if TIME_INTERVAL_STEPS > _DEFAULT_HEARTBEAT_INTERVAL - else _DEFAULT_HEARTBEAT_INTERVAL - ) + TIME_INTERVAL_STEPS + if TIME_INTERVAL_STEPS > _DEFAULT_HEARTBEAT_INTERVAL + else _DEFAULT_HEARTBEAT_INTERVAL +) + def test_lennard_jones_revo_procpool(): @@ -52,16 +61,12 @@ def test_lennard_jones_revo_procpool(): num_walkers = 4 init_state = OpenMMState.from_dwim( - positions=test_sys.positions, - ) + positions=test_sys.positions, + ) # TODO: remove the need to deepcopy and have the components make # their own copies if necessary - walker_states = [ - copy.deepcopy(init_state) - for _ - in range(num_walkers) - ] + walker_states = [copy.deepcopy(init_state) for _ in range(num_walkers)] init_walker_weight = 1 / num_walkers init_walkers = [ @@ -69,8 +74,7 @@ def test_lennard_jones_revo_procpool(): state=walker_state, weight=init_walker_weight, ) - for walker_state - in walker_states + for walker_state in walker_states ] # number of walkers if less then total cores, otherwise the total @@ -81,11 +85,9 @@ def test_lennard_jones_revo_procpool(): cores_per_worker = 1 else: num_workers = num_walkers - cores_per_worker = (num_workers // num_walkers) + cores_per_worker = num_workers // num_walkers - json_top = mdtraj_to_json_topology( - mdtraj.Topology.from_openmm(test_sys.topology) - ) + json_top = mdtraj_to_json_topology(mdtraj.Topology.from_openmm(test_sys.topology)) distance_metric = PairDistance() @@ -107,15 +109,16 @@ def test_lennard_jones_revo_procpool(): platform="CPU", num_procs=num_workers, # global_platform_properties={"Threads" : "1"}, - global_platform_properties={"Threads" : str(cores_per_worker)}, + global_platform_properties={"Threads": str(cores_per_worker)}, ), ) - new_walkers, sim_components = sim_manager.run_simulation( + new_walkers, sim_components = sim_manager.run_simulation( n_cycles=2, segment_lengths=MIN_INTERVAL_STEPS * 2 + 10, ) + def test_alanine_dipeptide_revo_procpool(): ala_sys = AlanineDipeptideExplicitSystem() @@ -124,7 +127,7 @@ def test_alanine_dipeptide_revo_procpool(): # add the pseudo forces like barostat barostat = openmm.MonteCarloBarostat( - 1. * openmm.unit.atmosphere, + 1.0 * openmm.unit.atmosphere, TEMPERATURE, ) ala_sys.system.addForce(barostat) @@ -139,11 +142,7 @@ def test_alanine_dipeptide_revo_procpool(): # TODO: remove the need to deepcopy and have the components make # their own copies if necessary - walker_states = [ - copy.deepcopy(ala_sys.state) - for _ - in range(num_walkers) - ] + walker_states = [copy.deepcopy(ala_sys.state) for _ in range(num_walkers)] init_walker_weight = 1 / num_walkers init_walkers = [ @@ -151,8 +150,7 @@ def test_alanine_dipeptide_revo_procpool(): state=walker_state, weight=init_walker_weight, ) - for walker_state - in walker_states + for walker_state in walker_states ] # number of walkers if less then total cores, otherwise the total @@ -163,7 +161,7 @@ def test_alanine_dipeptide_revo_procpool(): cores_per_worker = 1 else: num_workers = num_walkers - cores_per_worker = (num_workers // num_walkers) + cores_per_worker = num_workers // num_walkers distance_metric = AlanineDipeptideRamachandranDistance(ala_sys.json_top) @@ -181,13 +179,11 @@ def test_alanine_dipeptide_revo_procpool(): work_mapper_factory=OpenMMProcPoolWorkMapperFactory( platform="CPU", num_procs=num_workers, - global_platform_properties={"Threads" : str(cores_per_worker)}, + global_platform_properties={"Threads": str(cores_per_worker)}, ), ) - - new_walkers, sim_components = sim_manager.run_simulation( + new_walkers, sim_components = sim_manager.run_simulation( n_cycles=2, segment_lengths=MIN_INTERVAL_STEPS * 2 + 10, ) - diff --git a/tests/integration/test_openmm/test_sim_manager.py b/tests/integration/test_openmm/test_sim_manager.py index 5ba1cf4f..a50a446a 100644 --- a/tests/integration/test_openmm/test_sim_manager.py +++ b/tests/integration/test_openmm/test_sim_manager.py @@ -1,22 +1,28 @@ -import logging -import pytest -import psutil +# Standard Library + +# Third Party Library import openmm import openmm.unit +import psutil + +# First Party Library +from wepy.resampling.resamplers.noresampler import NoResampler +from wepy.runners.openmm import ( + HeartBeatLoggingReporterFactory, + OpenMMRunnerFactory, + OpenMMState, +) +from wepy.sim_manager import Manager from wepy.walker import Walker -from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState, HeartBeatLoggingReporterFactory -from wepy.work_mapper.serial import SerialMapper from wepy.work_mapper.openmm import ( - OpenMMSerialWorkMapperFactory, OpenMMProcPoolWorkMapperFactory, + OpenMMSerialWorkMapperFactory, ) -from wepy.sim_manager import Manager -from wepy.resampling.resamplers.noresampler import NoResampler - from wepy_tools.systems.lennard_jones import LennardJonesPair STEP_SIZE = 2 * openmm.unit.femtosecond + def test_serial_mapper(): lj_sys = LennardJonesPair() @@ -34,7 +40,7 @@ def test_serial_mapper(): openmm_reporter_factories=[ # heart beat every step HeartBeatLoggingReporterFactory(step_interval=1) - ] + ], ) num_walkers = 4 @@ -43,8 +49,7 @@ def test_serial_mapper(): OpenMMState.from_dwim( positions=lj_sys.positions, ) - for _ - in range(num_walkers) + for _ in range(num_walkers) ] init_walker_weight = 1 / num_walkers @@ -53,8 +58,7 @@ def test_serial_mapper(): state=walker_state, weight=init_walker_weight, ) - for walker_state - in walker_states + for walker_state in walker_states ] sim_manager = Manager( @@ -63,10 +67,10 @@ def test_serial_mapper(): resampler_factory=NoResampler, work_mapper_factory=OpenMMSerialWorkMapperFactory( platform="Reference", - ) + ), ) - new_walkers, sim_components = sim_manager.run_simulation( + new_walkers, sim_components = sim_manager.run_simulation( n_cycles=1, segment_lengths=100, ) @@ -77,11 +81,11 @@ def test_serial_mapper(): resampler_factory=NoResampler, work_mapper_factory=OpenMMSerialWorkMapperFactory( platform="CPU", - global_platform_properties={"Threads" : "1"}, - ) + global_platform_properties={"Threads": "1"}, + ), ) - new_walkers, sim_components = sim_manager.run_simulation( + new_walkers, sim_components = sim_manager.run_simulation( n_cycles=1, segment_lengths=100, ) @@ -92,15 +96,16 @@ def test_serial_mapper(): resampler_factory=NoResampler, work_mapper_factory=OpenMMSerialWorkMapperFactory( platform="CPU", - global_platform_properties={"Threads" : "4"}, - ) + global_platform_properties={"Threads": "4"}, + ), ) - new_walkers, sim_components = sim_manager.run_simulation( + new_walkers, sim_components = sim_manager.run_simulation( n_cycles=1, segment_lengths=100, ) + def test_proc_pool_mapper(): lj_sys = LennardJonesPair() @@ -117,7 +122,7 @@ def test_proc_pool_mapper(): openmm_reporter_factories=[ # heart beat every step HeartBeatLoggingReporterFactory(step_interval=10) - ] + ], ) num_walkers = 4 @@ -126,8 +131,7 @@ def test_proc_pool_mapper(): OpenMMState.from_dwim( positions=lj_sys.positions, ) - for _ - in range(num_walkers) + for _ in range(num_walkers) ] init_walker_weight = 1 / num_walkers @@ -136,8 +140,7 @@ def test_proc_pool_mapper(): state=walker_state, weight=init_walker_weight, ) - for walker_state - in walker_states + for walker_state in walker_states ] # As an example of a useful configuration for Reference @@ -153,7 +156,7 @@ def test_proc_pool_mapper(): ), ) - new_walkers, sim_components = sim_manager.run_simulation( + new_walkers, sim_components = sim_manager.run_simulation( n_cycles=1, segment_lengths=100, ) @@ -164,7 +167,7 @@ def test_proc_pool_mapper(): # on how many CPUs you have. Here we use psutil to reliably get # the number of cores and divide that by the number of walkers. num_cores = len(psutil.Process().cpu_affinity()) - cores_per_worker = (num_cores // num_walkers) + cores_per_worker = num_cores // num_walkers sim_manager = Manager( init_walkers=init_walkers, runner_factory=runner_factory, @@ -172,11 +175,11 @@ def test_proc_pool_mapper(): work_mapper_factory=OpenMMProcPoolWorkMapperFactory( platform="CPU", num_procs=len(walker_states), - global_platform_properties={"Threads" : str(cores_per_worker)}, + global_platform_properties={"Threads": str(cores_per_worker)}, ), ) - new_walkers, sim_components = sim_manager.run_simulation( + new_walkers, sim_components = sim_manager.run_simulation( n_cycles=2, segment_lengths=100, ) @@ -192,20 +195,18 @@ def test_proc_pool_mapper(): work_mapper_factory=OpenMMProcPoolWorkMapperFactory( platform="CPU", num_procs=len(walker_states), - global_platform_properties={"Threads" : "1"}, - device_ids=[0,1,2,3], - device_platform_properties=[ - {}, {}, {}, - {"Threads" : "3"} - ] + global_platform_properties={"Threads": "1"}, + device_ids=[0, 1, 2, 3], + device_platform_properties=[{}, {}, {}, {"Threads": "3"}], ), ) - new_walkers, sim_components = sim_manager.run_simulation( + new_walkers, sim_components = sim_manager.run_simulation( n_cycles=2, segment_lengths=100, ) - + + # def test_ray_mapper(): # lj_sys = LennardJonesPair() # integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) diff --git a/tests/unit/test_resampling/test_decisions/test_clone_merge.py b/tests/unit/test_resampling/test_decisions/test_clone_merge.py index 380db07a..64f1c86b 100644 --- a/tests/unit/test_resampling/test_decisions/test_clone_merge.py +++ b/tests/unit/test_resampling/test_decisions/test_clone_merge.py @@ -1,13 +1,15 @@ -import pytest +# Third Party Library import attrs +import pytest -from wepy.walker import Walker -from wepy.runners.mock import MockState +# First Party Library from wepy.resampling.decisions.clone_merge import ( - MultiCloneMergeDecision, CloneMergeDecisionEnum, CloneMergeDecisionRecord, + MultiCloneMergeDecision, ) +from wepy.runners.mock import MockState +from wepy.walker import Walker class TestMultiCloneMergeDecision: @@ -38,14 +40,18 @@ def test_action(self): walkers, [ [ - CloneMergeDecisionRecord(**{ - "decision_id": 1000, - "target_idxs": [0], - }), - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.NOTHING, - "target_idxs": [1], - }), + CloneMergeDecisionRecord( + **{ + "decision_id": 1000, + "target_idxs": [0], + } + ), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [1], + } + ), ] ], ) @@ -55,14 +61,18 @@ def test_action(self): walkers, [ [ - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.NOTHING, - "target_idxs": [0], - }), - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.NOTHING, - "target_idxs": [1], - }), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [0], + } + ), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [1], + } + ), ] ], ) @@ -74,14 +84,18 @@ def test_action(self): walkers, [ [ - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.NOTHING, - "target_idxs": [1], - }), - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.NOTHING, - "target_idxs": [0], - }), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [1], + } + ), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [0], + } + ), ] ], ) == [walker_2, walker_1] @@ -92,14 +106,18 @@ def test_action(self): walkers, [ [ - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.NOTHING, - "target_idxs": [0], - }), - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.NOTHING, - "target_idxs": [0], - }), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [0], + } + ), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [0], + } + ), ] ], ) @@ -112,14 +130,18 @@ def test_action(self): walkers, [ [ - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.NOTHING, - "target_idxs": [0], - }), - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.SQUASH, - "target_idxs": [1], - }), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [0], + } + ), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.SQUASH, + "target_idxs": [1], + } + ), ] ], ) @@ -128,14 +150,18 @@ def test_action(self): walkers, [ [ - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.NOTHING, - "target_idxs": [0], - }), - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.SQUASH, - "target_idxs": [0], - }), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.NOTHING, + "target_idxs": [0], + } + ), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.SQUASH, + "target_idxs": [0], + } + ), ] ], ) @@ -146,14 +172,18 @@ def test_action(self): walkers, [ [ - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.KEEP_MERGE, - "target_idxs": [0], - }), - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.SQUASH, - "target_idxs": [0], - }), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.KEEP_MERGE, + "target_idxs": [0], + } + ), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.SQUASH, + "target_idxs": [0], + } + ), ] ], ) @@ -166,18 +196,24 @@ def test_action(self): ], [ [ - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.CLONE, - "target_idxs": [0, 2], - }), - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.KEEP_MERGE, - "target_idxs": [1], - }), - CloneMergeDecisionRecord(**{ - "decision_id": CloneMergeDecisionEnum.SQUASH, - "target_idxs": [1], - }), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.CLONE, + "target_idxs": [0, 2], + } + ), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.KEEP_MERGE, + "target_idxs": [1], + } + ), + CloneMergeDecisionRecord( + **{ + "decision_id": CloneMergeDecisionEnum.SQUASH, + "target_idxs": [1], + } + ), ] ], ) == [ diff --git a/tests/unit/test_resampling/test_decisions/test_decision.py b/tests/unit/test_resampling/test_decisions/test_decision.py index cccca6f0..5ffed3c4 100644 --- a/tests/unit/test_resampling/test_decisions/test_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_decision.py @@ -1,21 +1,27 @@ -import pytest -from typing import TypedDict +# Standard Library from enum import IntEnum -import attrs + +# Third Party Library +import pytest + +# First Party Library from wepy.resampling.decisions.decision import Decision, DecisionRecord -from wepy.walker import Walker from wepy.runners.mock import MockState +from wepy.walker import Walker + # minimal implementation of the ABC for testing class MockDecisionEnum(IntEnum): NOTHING = 0 + class MockDecision(Decision): ENUM = MockDecisionEnum DEFAULT_DECISION = ENUM.NOTHING ANCESTOR_DECISION_IDS = (ENUM.NOTHING.value,) + class Test_Decision: def test_default_decision(self): @@ -32,23 +38,29 @@ def test_field_dtypes(self): def test_fields(self): assert MockDecision.fields() == [ - ("decision_id", (1,), int,) + ( + "decision_id", + (1,), + int, + ) ] + def test_record_field_names(self): assert MockDecision.record_field_names() == ("decision_id",) def test_enum_dict_by_name(self): assert MockDecision.enum_dict_by_name() == { - "NOTHING" : 0, + "NOTHING": 0, } def test_enum_dict_by_value(self): assert MockDecision.enum_dict_by_value() == { - 0 : MockDecisionEnum.NOTHING, + 0: MockDecisionEnum.NOTHING, } def test_enum_by_value(self): assert MockDecision.enum_by_value(0) == MockDecisionEnum.NOTHING + def test_enum_by_name(self): assert MockDecision.enum_by_name("NOTHING") == MockDecisionEnum.NOTHING @@ -60,18 +72,6 @@ def test_action(self): with pytest.raises(NotImplementedError): MockDecision.action( - [ - Walker( - MockState(1), - 0.1 - ) - for _ in range(4) - ], - [ - DecisionRecord( - decision_id=0 - ) - for _ in range(4) - ], + [Walker(MockState(1), 0.1) for _ in range(4)], + [DecisionRecord(decision_id=0) for _ in range(4)], ) - diff --git a/tests/unit/test_resampling/test_decisions/test_no_decision.py b/tests/unit/test_resampling/test_decisions/test_no_decision.py index 056746ae..8b8ade64 100644 --- a/tests/unit/test_resampling/test_decisions/test_no_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_no_decision.py @@ -1,6 +1,11 @@ -from wepy.walker import Walker, WalkerState +# First Party Library +from wepy.resampling.decisions.no_decision import ( + NoDecision, + NoDecisionRecord, + NothingDecisionEnum, +) from wepy.runners.mock import MockState -from wepy.resampling.decisions.no_decision import NoDecision, NothingDecisionEnum, NoDecisionRecord +from wepy.walker import Walker class TestNoDecision: @@ -27,14 +32,18 @@ def test_action(self): walkers, [ [ - NoDecisionRecord(**{ - "decision_id": NothingDecisionEnum.NOTHING, - "target_idx": 0, - }), - NoDecisionRecord(**{ - "decision_id": NothingDecisionEnum.NOTHING, - "target_idx": 1, - }), + NoDecisionRecord( + **{ + "decision_id": NothingDecisionEnum.NOTHING, + "target_idx": 0, + } + ), + NoDecisionRecord( + **{ + "decision_id": NothingDecisionEnum.NOTHING, + "target_idx": 1, + } + ), ] ], ) @@ -45,14 +54,18 @@ def test_action(self): walkers, [ [ - NoDecisionRecord(**{ - "decision_id": NothingDecisionEnum.NOTHING, - "target_idx": 1, - }), - NoDecisionRecord(**{ - "decision_id": NothingDecisionEnum.NOTHING, - "target_idx": 0, - }), + NoDecisionRecord( + **{ + "decision_id": NothingDecisionEnum.NOTHING, + "target_idx": 1, + } + ), + NoDecisionRecord( + **{ + "decision_id": NothingDecisionEnum.NOTHING, + "target_idx": 0, + } + ), ] ], ) == [walker_2, walker_1] @@ -60,27 +73,35 @@ def test_action(self): def test_parents(self): assert NoDecision.parents( - [ - NoDecisionRecord(**{ + [ + NoDecisionRecord( + **{ "decision_id": NothingDecisionEnum.NOTHING, "target_idx": 0, - }), - NoDecisionRecord(**{ + } + ), + NoDecisionRecord( + **{ "decision_id": NothingDecisionEnum.NOTHING, "target_idx": 1, - }), - ] + } + ), + ] ) == [0, 1] assert NoDecision.parents( - [ - NoDecisionRecord(**{ + [ + NoDecisionRecord( + **{ "decision_id": NothingDecisionEnum.NOTHING, "target_idx": 1, - }), - NoDecisionRecord(**{ + } + ), + NoDecisionRecord( + **{ "decision_id": NothingDecisionEnum.NOTHING, "target_idx": 0, - }), - ] + } + ), + ] ) == [1, 0] diff --git a/tests/unit/test_resampling/test_distances/test_base.py b/tests/unit/test_resampling/test_distances/test_base.py index 4d4eac09..f8382d5b 100644 --- a/tests/unit/test_resampling/test_distances/test_base.py +++ b/tests/unit/test_resampling/test_distances/test_base.py @@ -1,8 +1,10 @@ +# Standard Library import math +# First Party Library from wepy.resampling.distances.base import DistanceABC -from wepy.runners.mock import MockState from wepy.resampling.distances.mock import MockDistance +from wepy.runners.mock import MockState # minimal implementation of a Distance from the ABC, in this case the # image and state are the same @@ -19,7 +21,7 @@ def test_image_distance(self): MockState(1), MockState(3), ), - 2. + 2.0, ) def test_image_distance(self): @@ -28,5 +30,5 @@ def test_image_distance(self): MockState(1), MockState(3), ), - 2. + 2.0, ) diff --git a/tests/unit/test_resampling/test_distances/test_mock.py b/tests/unit/test_resampling/test_distances/test_mock.py index 83ee91ce..bae99144 100644 --- a/tests/unit/test_resampling/test_distances/test_mock.py +++ b/tests/unit/test_resampling/test_distances/test_mock.py @@ -1,8 +1,11 @@ +# Standard Library import math +# First Party Library from wepy.resampling.distances.base import DistanceABC -from wepy.runners.mock import MockState from wepy.resampling.distances.mock import MockDistance +from wepy.runners.mock import MockState + # minimal implementation of a Distance from the ABC, in this case the # image and state are the same @@ -19,7 +22,7 @@ def test_image_distance(self): MockState(1), MockState(3), ), - 2. + 2.0, ) def test_image_distance(self): @@ -28,5 +31,5 @@ def test_image_distance(self): MockState(1), MockState(3), ), - 2. + 2.0, ) diff --git a/tests/unit/test_resampling/test_distances/test_simple.py b/tests/unit/test_resampling/test_distances/test_simple.py index 66a8a0fe..78098239 100644 --- a/tests/unit/test_resampling/test_distances/test_simple.py +++ b/tests/unit/test_resampling/test_distances/test_simple.py @@ -1,4 +1,7 @@ +# Standard Library import math + +# First Party Library from wepy.resampling.distances.simple import ( XYDistanceState, XYEuclideanDistance, @@ -14,7 +17,7 @@ def test_image_distance(self): XYDistanceState((0, 0)), XYDistanceState((0, 2)), ), - 2. + 2.0, ) def test_distance(self): @@ -24,5 +27,5 @@ def test_distance(self): XYDistanceState((0, 0)), XYDistanceState((0, 2)), ), - 2. + 2.0, ) diff --git a/tests/unit/test_resampling/test_resamplers/test_clone_merge.py b/tests/unit/test_resampling/test_resamplers/test_clone_merge.py index 139c5a87..7387e6c7 100644 --- a/tests/unit/test_resampling/test_resamplers/test_clone_merge.py +++ b/tests/unit/test_resampling/test_resamplers/test_clone_merge.py @@ -1,12 +1,18 @@ +# Third Party Library import pytest -from wepy.resampling.resamplers.resampler import ResamplerError, Resampler, ResamplerABC + +# First Party Library +from wepy.resampling.decisions.clone_merge import ( + CloneMergeDecisionRecord, +) from wepy.resampling.resamplers.clone_merge import CloneMergeResampler -from wepy.resampling.decisions.clone_merge import MultiCloneMergeDecision, CloneMergeDecisionRecord, CloneMergeDecisionEnum -from wepy.walker import Walker +from wepy.resampling.resamplers.resampler import ResamplerError from wepy.runners.mock import MockState +from wepy.walker import Walker # class MockCloneMergeResampler(ResamplerABC): + class Test_CloneMergeResampler: def test___init__(self): @@ -34,10 +40,10 @@ def test___init__(self): def test__init_walker_actions(self): assert CloneMergeResampler()._init_walker_actions(4) == [ - CloneMergeDecisionRecord(decision_id=1, target_idxs=(0,)), - CloneMergeDecisionRecord(decision_id=1, target_idxs=(1,)), - CloneMergeDecisionRecord(decision_id=1, target_idxs=(2,)), - CloneMergeDecisionRecord(decision_id=1, target_idxs=(3,)), + CloneMergeDecisionRecord(decision_id=1, target_idxs=(0,)), + CloneMergeDecisionRecord(decision_id=1, target_idxs=(1,)), + CloneMergeDecisionRecord(decision_id=1, target_idxs=(2,)), + CloneMergeDecisionRecord(decision_id=1, target_idxs=(3,)), ] def test__check_resampled_walkers(self): @@ -101,7 +107,10 @@ def test_assign_clones(self): with pytest.raises(ResamplerError): resampler.assign_clones( - merge_groups=[[], [],], + merge_groups=[ + [], + [], + ], walker_clone_nums=[0, 0, 0], ) @@ -114,7 +123,11 @@ def test_assign_clones(self): ] assert resampler.assign_clones( - merge_groups=[[], [2], [],], + merge_groups=[ + [], + [2], + [], + ], walker_clone_nums=[1, 0, 0], ) == [ CloneMergeDecisionRecord(decision_id=2, target_idxs=(0, 2)), @@ -125,6 +138,10 @@ def test_assign_clones(self): with pytest.raises(ResamplerError): resampler.assign_clones( - merge_groups=[[2], [], [],], + merge_groups=[ + [2], + [], + [], + ], walker_clone_nums=[1, 0, 0], ) diff --git a/tests/unit/test_resampling/test_resamplers/test_noresampler.py b/tests/unit/test_resampling/test_resamplers/test_noresampler.py index d6280196..ff2b6c70 100644 --- a/tests/unit/test_resampling/test_resamplers/test_noresampler.py +++ b/tests/unit/test_resampling/test_resamplers/test_noresampler.py @@ -1,8 +1,8 @@ -from wepy.resampling.resamplers.noresampler import NoResampler +# First Party Library from wepy.resampling.decisions.no_decision import NothingDecisionEnum -from wepy.walker import Walker +from wepy.resampling.resamplers.noresampler import NoResampler from wepy.runners.mock import MockState - +from wepy.walker import Walker class TestNoResampler: diff --git a/tests/unit/test_resampling/test_resamplers/test_resampler.py b/tests/unit/test_resampling/test_resamplers/test_resampler.py index f508bbd0..64bbfcb0 100644 --- a/tests/unit/test_resampling/test_resamplers/test_resampler.py +++ b/tests/unit/test_resampling/test_resamplers/test_resampler.py @@ -1,5 +1,7 @@ +# Third Party Library import pytest +# First Party Library from wepy.resampling.resamplers.resampler import ResamplerABC, ResamplerError @@ -13,7 +15,7 @@ def test___init__(self): min_num_walkers=None, max_num_walkers=None, ) - + ResamplerABC( min_num_walkers=3, max_num_walkers=3, @@ -26,7 +28,4 @@ def test___init__(self): ) with pytest.raises(ResamplerError): - ResamplerABC( - min_num_walkers=0 - ) - + ResamplerABC(min_num_walkers=0) diff --git a/tests/unit/test_resampling/test_resamplers/test_revo.py b/tests/unit/test_resampling/test_resamplers/test_revo.py index 7e6f01d7..df5a0ed7 100644 --- a/tests/unit/test_resampling/test_resamplers/test_revo.py +++ b/tests/unit/test_resampling/test_resamplers/test_revo.py @@ -1,11 +1,21 @@ -import pytest -import numpy as np +# Standard Library import pickle -from wepy.runners.mock import MockState + +# Third Party Library +import numpy as np +import pytest + +# First Party Library from wepy.resampling.distances.mock import MockDistance -from wepy.resampling.resamplers.revo import REVOResampler, REVOResamplerFactory, _ImageWrapper +from wepy.resampling.resamplers.revo import ( + REVOResampler, + REVOResamplerFactory, + _ImageWrapper, +) +from wepy.runners.mock import MockState from wepy.walker import Walker + def test__ImageWrapper(): wrapped = _ImageWrapper(MockDistance().image) @@ -15,6 +25,7 @@ def test__ImageWrapper(): # check that it is pickleable for sending to subprocesses pickle.loads(pickle.dumps(wrapped)) + class Test_REVOResamplerFactory: resampler = REVOResamplerFactory( @@ -23,6 +34,7 @@ class Test_REVOResamplerFactory: char_dist=1, ) + class Test_REVOResampler: def test___init__(self): @@ -43,19 +55,21 @@ def test___init__(self): assert resampler.seed == 1 assert np.isclose(resampler.lpmin, np.log(0.1 / 100)) - assert REVOResampler( - merge_dist=1.0, - char_dist=1.0, - dist_exponent=3, - distance=MockDistance(), - weights=True, - merge_alg="pairs", - pmin=0.1, - pmax=0.4, - seed=None, - num_proc=1, - ).seed is None - + assert ( + REVOResampler( + merge_dist=1.0, + char_dist=1.0, + dist_exponent=3, + distance=MockDistance(), + weights=True, + merge_alg="pairs", + pmin=0.1, + pmax=0.4, + seed=None, + num_proc=1, + ).seed + is None + ) def test__novelty(self): @@ -74,13 +88,13 @@ def test__novelty(self): # UGLY,TOREV: there shouldn't be the possibility of negatives # of these values but current code accepts them. - assert resampler._novelty(-1, 1) == 0. - assert resampler._novelty(0.1, -1) == 0. - assert resampler._novelty(0., 0) == 0. - assert resampler._novelty(0.1, 0) == 0. - assert resampler._novelty(0., 1) == 0. + assert resampler._novelty(-1, 1) == 0.0 + assert resampler._novelty(0.1, -1) == 0.0 + assert resampler._novelty(0.0, 0) == 0.0 + assert resampler._novelty(0.1, 0) == 0.0 + assert resampler._novelty(0.0, 1) == 0.0 - assert resampler._novelty(0.1, 1) == 1. + assert resampler._novelty(0.1, 1) == 1.0 # with weights resampler = REVOResampler( @@ -96,10 +110,10 @@ def test__novelty(self): num_proc=1, ) - assert resampler._novelty(.4, 1) > 0. - assert resampler._novelty(.1, 1) > 0. + assert resampler._novelty(0.4, 1) > 0.0 + assert resampler._novelty(0.1, 1) > 0.0 - assert np.isclose(resampler._novelty(.4, 1000), 0.) + assert np.isclose(resampler._novelty(0.4, 1000), 0.0) def test__calc_variation(self): @@ -120,8 +134,14 @@ def test__calc_variation(self): [0.4, 0.1], [1, 1], [ - [0., 1.,], - [1., 0.,] + [ + 0.0, + 1.0, + ], + [ + 1.0, + 0.0, + ], ], ) @@ -159,11 +179,14 @@ def test__calc_variation_loss(self): ) # if no suitable pairs are found returns None - assert resampler._calc_variation_loss( - [0.1, 0.4, 0.3], - [0.01, 0.02, 0.03], - [], - ) is None + assert ( + resampler._calc_variation_loss( + [0.1, 0.4, 0.3], + [0.01, 0.02, 0.03], + [], + ) + is None + ) def test__find_eligible_merge_pairs(self): @@ -182,16 +205,19 @@ def test__find_eligible_merge_pairs(self): # TODO: figure out some combinations of outputs that generates # some eligible pairs - assert resampler._find_eligible_merge_pairs( - [0.1, 0.4, 0.3], - [ - [0., 1., 2.], - [1., 0., 1.5], - [2., 1.5, 0.], - ], - 2, - [2, 2, 2], - ) == [] + assert ( + resampler._find_eligible_merge_pairs( + [0.1, 0.4, 0.3], + [ + [0.0, 1.0, 2.0], + [1.0, 0.0, 1.5], + [2.0, 1.5, 0.0], + ], + 2, + [2, 2, 2], + ) + == [] + ) def test_decide(self): @@ -212,9 +238,9 @@ def test_decide(self): [0.1, 0.4, 0.3], [1, 1, 1], [ - [0., 1., 2.], - [1., 0., 1.5], - [2., 1.5, 0.], + [0.0, 1.0, 2.0], + [1.0, 0.0, 1.5], + [2.0, 1.5, 0.0], ], ) @@ -246,13 +272,10 @@ def test__all_to_all_distance(self): ] ) == ( [ - [0., 2.], - [2., 0.], + [0.0, 2.0], + [2.0, 0.0], ], - [ - MockState(0), - MockState(2) - ] + [MockState(0), MockState(2)], ) @pytest.mark.flaky(reruns=4) @@ -286,13 +309,10 @@ def test__all_to_all_distance_pool(self): ] ) == ( [ - [0., 2.], - [2., 0.], + [0.0, 2.0], + [2.0, 0.0], ], - [ - MockState(0), - MockState(2) - ] + [MockState(0), MockState(2)], ) def test_resample(self): @@ -310,7 +330,8 @@ def test_resample(self): num_proc=1, ) - resampled_walkers, resampling_data, resampler_data = resampler.resample([ + resampled_walkers, resampling_data, resampler_data = resampler.resample( + [ Walker( MockState(1), 0.1, @@ -319,6 +340,7 @@ def test_resample(self): MockState(2), 0.1, ), - ]) + ] + ) assert len(resampled_walkers) == 2 diff --git a/tests/unit/test_runners/test_mock.py b/tests/unit/test_runners/test_mock.py index 0257cbb0..6970e088 100644 --- a/tests/unit/test_runners/test_mock.py +++ b/tests/unit/test_runners/test_mock.py @@ -1,7 +1,15 @@ +# Third Party Library import pytest -from wepy.runners.mock import MockRunner, MockState, MockError -from wepy.runners.runner import RunnerStatus, Runner, RunnerStateTransitionError, RunnerStateMachineError, RunnerEvent - + +# First Party Library +from wepy.runners.mock import MockRunner, MockState +from wepy.runners.runner import ( + RunnerStateMachineError, + RunnerStateTransitionError, + RunnerStatus, +) + + class TestMockRunner: def test_init(self): @@ -49,7 +57,6 @@ def test_post_cycle(self): runner.pre_cycle() runner.post_cycle(None) - def test_run_segment(self): runner = MockRunner() @@ -66,4 +73,6 @@ def test_run_segment(self): assert runner.run_segment( MockState(0), 10, - )[0] == MockState(10) + )[ + 0 + ] == MockState(10) diff --git a/tests/unit/test_runners/test_openmm/test_logger.py b/tests/unit/test_runners/test_openmm/test_logger.py index 7ce78caf..8f297382 100644 --- a/tests/unit/test_runners/test_openmm/test_logger.py +++ b/tests/unit/test_runners/test_openmm/test_logger.py @@ -1,43 +1,49 @@ -import time -import logging +# Standard Library import copy -import pytest +import logging +import time + +# Third Party Library import openmm import openmm.app import openmm.unit -from wepy.runners.openmm.state import OpenMMState -from wepy.runners.openmm.reporter import OpenMMReporterNextReport +import pytest + +# First Party Library from wepy.runners.openmm.logger import ( + EnergyLoggingReporter, + HeartBeatLoggingReporter, LoggingReporter, - StepIntervalLoggingReporter, SamplingTimeIntervalLoggingReporter, - HeartBeatLoggingReporter, - EnergyLoggingReporter, + StepIntervalLoggingReporter, UnitCellLoggingReporter, ) +from wepy.runners.openmm.reporter import OpenMMReporterNextReport +from wepy.runners.openmm.state import OpenMMState from wepy_tools.systems.lennard_jones import LennardJonesPair STEP_TIME = 1 * openmm.unit.femtosecond + @pytest.fixture(scope="function") def sim_components() -> tuple[ - openmm.State, - openmm.app.Topology, - openmm.System, - openmm.LangevinIntegrator, - openmm.Platform, + openmm.State, + openmm.app.Topology, + openmm.System, + openmm.LangevinIntegrator, + openmm.Platform, ]: lj_sys = LennardJonesPair() - integrator = openmm.VerletIntegrator(STEP_TIME) - omm_state = OpenMMState.from_dwim(positions=lj_sys.positions).to_state_wrapper().state - + integrator = openmm.VerletIntegrator(STEP_TIME) + omm_state = ( + OpenMMState.from_dwim(positions=lj_sys.positions).to_state_wrapper().state + ) platform = openmm.Platform.getPlatformByName("Reference") return omm_state, lj_sys.topology, lj_sys.system, integrator, platform - - + class Test_LoggingReporter: @@ -46,9 +52,9 @@ def test_report(self, caplog): logger = logging.getLogger("test-LoggingReporter") def hello_log( - logger: logging.Logger, - simulation: openmm.app.Simulation, - state: openmm.State, + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, ) -> None: logger.info("Hello") @@ -69,6 +75,7 @@ def hello_log( caplog.clear() + class Test_StepIntervalLoggingReporter: def test_describeNextReport(self, sim_components): @@ -78,9 +85,9 @@ def test_describeNextReport(self, sim_components): logger = logging.getLogger("test-StepIntervalLoggingReporter") def hello_log( - logger: logging.Logger, - simulation: openmm.app.Simulation, - state: openmm.State, + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, ) -> None: logger.info("Hello") @@ -95,33 +102,24 @@ def hello_log( start_time=time.time(), ) - - simulation = openmm.app.Simulation( - *sim_args - ) + simulation = openmm.app.Simulation(*sim_args) simulation.context.setState(omm_state) - assert step_logger.describeNextReport( - simulation - ) == OpenMMReporterNextReport( + assert step_logger.describeNextReport(simulation) == OpenMMReporterNextReport( steps=10, include=list(state_includes), periodic=False, ) simulation.step(1) - assert step_logger.describeNextReport( - simulation - ) == OpenMMReporterNextReport( + assert step_logger.describeNextReport(simulation) == OpenMMReporterNextReport( steps=9, include=list(state_includes), periodic=False, ) simulation.step(2) - assert step_logger.describeNextReport( - simulation - ) == OpenMMReporterNextReport( + assert step_logger.describeNextReport(simulation) == OpenMMReporterNextReport( steps=7, include=list(state_includes), periodic=False, @@ -129,30 +127,25 @@ def hello_log( # wraps back around at 0 simulation.step(7) - assert step_logger.describeNextReport( - simulation - ) == OpenMMReporterNextReport( + assert step_logger.describeNextReport(simulation) == OpenMMReporterNextReport( steps=10, include=list(state_includes), periodic=False, ) - def test_simulation(self, sim_components, caplog): omm_state, sim_args = sim_components[0], sim_components[1:] - simulation = openmm.app.Simulation( - *sim_args - ) + simulation = openmm.app.Simulation(*sim_args) simulation.context.setState(omm_state) logger_name = "test-StepIntervalLoggingReporter" logger = logging.getLogger(logger_name) def hello_log( - logger: logging.Logger, - simulation: openmm.app.Simulation, - state: openmm.State, + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, ) -> None: logger.info("Hello") @@ -191,13 +184,12 @@ def test_describeNextReport(self, sim_components): omm_state, sim_args = sim_components[0], sim_components[1:] topology, system, integrator, platform = sim_args - logger = logging.getLogger("test-SamplingTimeIntervalLoggingReporter") def hello_log( - logger: logging.Logger, - simulation: openmm.app.Simulation, - state: openmm.State, + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, ) -> None: logger.info("Hello") @@ -220,45 +212,35 @@ def hello_log( ) simulation.context.setState(omm_state) - assert step_logger.describeNextReport( - simulation - ) == OpenMMReporterNextReport( + assert step_logger.describeNextReport(simulation) == OpenMMReporterNextReport( steps=10, include=list(state_includes), periodic=False, ) simulation.step(1) - assert step_logger.describeNextReport( - simulation - ) == OpenMMReporterNextReport( + assert step_logger.describeNextReport(simulation) == OpenMMReporterNextReport( steps=9, include=list(state_includes), periodic=False, ) simulation.step(2) - assert step_logger.describeNextReport( - simulation - ) == OpenMMReporterNextReport( + assert step_logger.describeNextReport(simulation) == OpenMMReporterNextReport( steps=7, include=list(state_includes), periodic=False, ) simulation.step(7) - assert step_logger.describeNextReport( - simulation - ) == OpenMMReporterNextReport( + assert step_logger.describeNextReport(simulation) == OpenMMReporterNextReport( steps=10, include=list(state_includes), periodic=False, ) simulation.step(3) - assert step_logger.describeNextReport( - simulation - ) == OpenMMReporterNextReport( + assert step_logger.describeNextReport(simulation) == OpenMMReporterNextReport( steps=7, include=list(state_includes), periodic=False, @@ -273,9 +255,7 @@ def hello_log( simulation.context.setState(omm_state) simulation.step(10) - assert step_logger.describeNextReport( - simulation - ) == OpenMMReporterNextReport( + assert step_logger.describeNextReport(simulation) == OpenMMReporterNextReport( steps=10, include=list(state_includes), periodic=False, @@ -291,15 +271,12 @@ def hello_log( simulation.context.setState(omm_state) simulation.step(11) - assert step_logger.describeNextReport( - simulation - ) == OpenMMReporterNextReport( + assert step_logger.describeNextReport(simulation) == OpenMMReporterNextReport( steps=9, include=list(state_includes), periodic=False, ) - def test_simulation(self, sim_components, caplog): omm_state, sim_args = sim_components[0], sim_components[1:] @@ -309,9 +286,9 @@ def test_simulation(self, sim_components, caplog): logger = logging.getLogger(logger_name) def hello_log( - logger: logging.Logger, - simulation: openmm.app.Simulation, - state: openmm.State, + logger: logging.Logger, + simulation: openmm.app.Simulation, + state: openmm.State, ) -> None: logger.info(f"Step: {state.getStepCount()}") @@ -380,6 +357,7 @@ def hello_log( assert len(caplog.records) == 5 caplog.clear() + class Test_HeartBeatLoggingReporter: def test_logging_callback(self, sim_components, caplog): @@ -455,6 +433,7 @@ def test_simulation(self, sim_components, caplog): assert len(caplog.records) == 5 caplog.clear() + class Test_EnergyLoggingReporter: def test_logging_callback(self, sim_components, caplog): @@ -525,6 +504,7 @@ def test_simulation(self, sim_components, caplog): assert len(caplog.records) == 5 caplog.clear() + class Test_UnitCellLoggingReporter: def test_logging_callback(self, sim_components, caplog): @@ -594,4 +574,3 @@ def test_simulation(self, sim_components, caplog): assert len(caplog.records) == 5 caplog.clear() - diff --git a/tests/unit/test_runners/test_openmm/test_runner.py b/tests/unit/test_runners/test_openmm/test_runner.py index 38e19aae..be837bab 100644 --- a/tests/unit/test_runners/test_openmm/test_runner.py +++ b/tests/unit/test_runners/test_openmm/test_runner.py @@ -1,53 +1,58 @@ -import logging +# Standard Library import copy +import logging + +# Third Party Library import attrs -from wepy_tools.systems.lennard_jones import LennardJonesPair +import numpy as np +import openmm +import openmm.app +import openmm.unit +import pytest + +# First Party Library +from wepy.runners.openmm.logger import StepIntervalLoggingReporter +from wepy.runners.openmm.runner import ( + _DEFAULT_HEARTBEAT_INTERVAL, + _DEFAULT_STATE_TIME_INTERVAL, + OpenMMRunner, + OpenMMRunnerFactory, + OpenMMRunnerSegmentData, +) from wepy.runners.openmm.state import ( - dummy_context, OpenMMState, + dummy_context, ) - from wepy.runners.runner import ( - RunnerStatus, - RunnerStateTransitionError, RunnerStateError, - -) -from wepy.runners.openmm.runner import ( - OpenMMRunner, - OpenMMRunnerSegmentData, - OpenMMRunnerFactory, - _DEFAULT_HEARTBEAT_INTERVAL, - _DEFAULT_STATE_TIME_INTERVAL, + RunnerStateTransitionError, + RunnerStatus, ) - -from wepy.runners.openmm.logger import StepIntervalLoggingReporter -import pytest - -import numpy as np -import openmm -import openmm.app -import openmm.unit +from wepy_tools.systems.lennard_jones import LennardJonesPair UNIT_CUBE = np.array( - [ - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0], - ] - ) + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] +) STEP_SIZE = 2 * openmm.unit.femtoseconds + @pytest.fixture -def runner_components() -> tuple[openmm.System, openmm.app.Topology, openmm.LangevinIntegrator]: +def runner_components() -> ( + tuple[openmm.System, openmm.app.Topology, openmm.LangevinIntegrator] +): lj_sys = LennardJonesPair() - integrator = openmm.LangevinIntegrator(300.0, 0.1, STEP_SIZE) + integrator = openmm.LangevinIntegrator(300.0, 0.1, STEP_SIZE) return lj_sys.system, lj_sys.topology, integrator + @pytest.fixture def omm_context() -> openmm.Context: @@ -57,6 +62,7 @@ def omm_context() -> openmm.Context: return ctx + @attrs.define class Spy: touched: bool = False @@ -64,6 +70,7 @@ class Spy: def touch(self) -> None: self.touched = True + class TouchGlobalStepIntervalLoggingReporter(StepIntervalLoggingReporter): def __init__( @@ -87,6 +94,7 @@ def __init__( def touch(self, *args) -> None: self.spy.touch() + class Test_OpenMMRunner: def test___init__(self, runner_components): @@ -113,8 +121,13 @@ def test___init__(self, runner_components): assert runner.status == RunnerStatus.PRE_INITIALIZATION SPY = Spy() - def _mock_factory(logger: logging.Logger, start_time: int) -> TouchGlobalStepIntervalLoggingReporter: - return TouchGlobalStepIntervalLoggingReporter(logger=logger, start_time=start_time, spy=SPY) + + def _mock_factory( + logger: logging.Logger, start_time: int + ) -> TouchGlobalStepIntervalLoggingReporter: + return TouchGlobalStepIntervalLoggingReporter( + logger=logger, start_time=start_time, spy=SPY + ) runner = OpenMMRunner( system=copy.deepcopy(system), @@ -196,7 +209,9 @@ def test_run_segment(self, runner_components): system=copy.deepcopy(system), topology=copy.deepcopy(topology), integrator=copy.deepcopy(integrator), - get_state_keys={"positions",} + get_state_keys={ + "positions", + }, ) runner.init() runner.pre_cycle() @@ -212,8 +227,13 @@ def test_run_segment(self, runner_components): # test that openmm reporters are being called SPY = Spy() - def _mock_factory(logger: logging.Logger, start_time: int) -> TouchGlobalStepIntervalLoggingReporter: - return TouchGlobalStepIntervalLoggingReporter(logger=logger, start_time=start_time, spy=SPY) + + def _mock_factory( + logger: logging.Logger, start_time: int + ) -> TouchGlobalStepIntervalLoggingReporter: + return TouchGlobalStepIntervalLoggingReporter( + logger=logger, start_time=start_time, spy=SPY + ) runner = OpenMMRunner( system=copy.deepcopy(system), @@ -287,6 +307,7 @@ def test_post_cycle(self, runner_components): assert runner.status == RunnerStatus.POST_CYCLE + def test_OpenMMRunnerFactory(runner_components): system, topology, integrator = runner_components diff --git a/tests/unit/test_runners/test_openmm/test_state.py b/tests/unit/test_runners/test_openmm/test_state.py index 037df60a..7c3ba0e7 100644 --- a/tests/unit/test_runners/test_openmm/test_state.py +++ b/tests/unit/test_runners/test_openmm/test_state.py @@ -6,30 +6,29 @@ from immutables import Map as frozenmap from lxml import etree -from wepy_tools.systems.lennard_jones import LennardJonesPair - +# First Party Library from wepy.runners.openmm.state import ( + OpenMMState, + OpenMMStateValidationError, + OpenMMStateWrapper, + _gen_vec3_element, dummy_context, - resolve_state_data_type_enum_values, get_context_state, get_state_core_fields_present, get_state_fields_present, - OpenMMStateWrapper, - OpenMMStateValidationError, - OpenMMState, - _gen_vec3_element, + resolve_state_data_type_enum_values, state_to_xml, ) - -# First Party Library +from wepy_tools.systems.lennard_jones import LennardJonesPair UNIT_CUBE = np.array( - [ - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0], - ] - ) + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] +) + def test_dummy_context(): lj_sys = LennardJonesPair() @@ -42,7 +41,7 @@ def test_dummy_context(): [1.0, 0.0, 0.0], ] ) - * openmm.unit.nanometer + * openmm.unit.nanometer, ) dummy_context( @@ -57,6 +56,7 @@ def test_dummy_context(): unitcell=UNIT_CUBE * openmm.unit.nanometer, ) + def test_resolve_state_data_type_enum_values(): assert resolve_state_data_type_enum_values() == frozenmap( @@ -77,7 +77,6 @@ def omm_context() -> openmm.Context: lj_sys = LennardJonesPair() - ctx = dummy_context(lj_sys.system, lj_sys.positions) return ctx @@ -424,7 +423,6 @@ def test_from_dict(self): forces=forces, ) assert OpenMMState.from_dict(d) == OpenMMState(**d) - def test_from_xml(self, omm_context): @@ -494,6 +492,7 @@ def test_from_dict(self, omm_context): }, ) + class Test_OpenMMState: def test__validate_array3ds(self): @@ -616,38 +615,48 @@ def test___init__(self): ) def test___len__(self): - assert len(OpenMMState( - time=0.0 * openmm.unit.picosecond, - box_vectors=UNIT_CUBE * openmm.unit.nanometer, - )) == 2 - - assert len(OpenMMState( - time=0.0 * openmm.unit.picosecond, - box_vectors=UNIT_CUBE * openmm.unit.nanometer, - positions=np.array( - [ - [1.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - ] + assert ( + len( + OpenMMState( + time=0.0 * openmm.unit.picosecond, + box_vectors=UNIT_CUBE * openmm.unit.nanometer, + ) ) - * openmm.unit.nanometer, - velocities=np.array( - [ - [1.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - ] - ) - * openmm.unit.nanometer - / openmm.unit.picosecond, - forces=np.array( - [ - [1.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - ] + == 2 + ) + + assert ( + len( + OpenMMState( + time=0.0 * openmm.unit.picosecond, + box_vectors=UNIT_CUBE * openmm.unit.nanometer, + positions=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer, + velocities=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.nanometer + / openmm.unit.picosecond, + forces=np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + * openmm.unit.kilojoule + / (openmm.unit.nanometer * openmm.unit.mole), + ) ) - * openmm.unit.kilojoule - / (openmm.unit.nanometer * openmm.unit.mole), - )) == 5 + == 5 + ) def test___contains__(self): small_state = OpenMMState( @@ -704,8 +713,7 @@ def test___contains__(self): assert "potential_energy" not in large_state assert "parameters" not in large_state assert "parameter_derivatives" not in large_state - - + def test___getitem__(self): bvs = UNIT_CUBE * openmm.unit.nanometer @@ -723,7 +731,7 @@ def test___getitem__(self): with pytest.raises(ValueError): state["positions"] - + state = OpenMMState( time=0.0 * openmm.unit.picosecond, box_vectors=bvs, @@ -755,7 +763,6 @@ def test___getitem__(self): assert state["positions"] is not None assert state["velocities"] is not None assert state["forces"] is not None - def test_from_state(self, omm_context): s = OpenMMState.from_state(omm_context.getState()) @@ -820,7 +827,6 @@ def test_from_state_wrapper(self, omm_context): ) OpenMMState.from_state_wrapper(sw) - def test_from_dwim(self): positions = ( np.array( @@ -869,8 +875,8 @@ def test_from_dwim(self): bv = np.array( [ [2.0, 0.0, 0.0], - [0., 2.0, 0.0], - [0., 0.0, 2.0], + [0.0, 2.0, 0.0], + [0.0, 0.0, 2.0], ] ) @@ -935,7 +941,7 @@ def test_to_dict(self): "velocities", "forces", } - + def test_to_state_wrapper(self): time = 0.0 * openmm.unit.picosecond @@ -994,7 +1000,7 @@ def test_to_state_wrapper(self): ) os.to_state_wrapper() - + def test__gen_vec3_element(): @@ -1056,4 +1062,3 @@ def test_state_to_xml(): full_xml = state_to_xml(full_state) assert isinstance(openmm.XmlSerializer.deserialize(full_xml), openmm.State) - diff --git a/tests/unit/test_runners/test_runner.py b/tests/unit/test_runners/test_runner.py index 146389a3..c1787800 100644 --- a/tests/unit/test_runners/test_runner.py +++ b/tests/unit/test_runners/test_runner.py @@ -1,8 +1,19 @@ -import pytest +# Third Party Library import attrs -from wepy.runners.runner import NoRunner, RunnerStateMachine, RunnerStatus, RunnerEvent, RunnerStateTransitionError, RunnerStateError +import pytest + +# First Party Library +from wepy.runners.runner import ( + NoRunner, + RunnerEvent, + RunnerStateError, + RunnerStateMachine, + RunnerStateTransitionError, + RunnerStatus, +) from wepy.walker import Walker, WalkerState + class Test_RunnerStateMachine: def test_validate_event(self): @@ -13,7 +24,7 @@ def test_validate_event(self): sm = RunnerStateMachine(state=RunnerStatus.PRE_INITIALIZATION) with pytest.raises(RunnerStateTransitionError): sm.validate_event(RunnerEvent.PRE_CYCLE) - + def test_send(self): sm = RunnerStateMachine(state=RunnerStatus.PRE_INITIALIZATION) @@ -29,21 +40,26 @@ def test_send(self): sm.send(RunnerEvent.PRE_CYCLE) # test the rest of the transitions - assert RunnerStateMachine( - RunnerStatus.INITIALIZED - ).send(RunnerEvent.PRE_CYCLE) == RunnerStatus.PRE_CYCLE + assert ( + RunnerStateMachine(RunnerStatus.INITIALIZED).send(RunnerEvent.PRE_CYCLE) + == RunnerStatus.PRE_CYCLE + ) - assert RunnerStateMachine( - RunnerStatus.PRE_CYCLE - ).send(RunnerEvent.POST_SEGMENT) == RunnerStatus.POST_SEGMENT + assert ( + RunnerStateMachine(RunnerStatus.PRE_CYCLE).send(RunnerEvent.POST_SEGMENT) + == RunnerStatus.POST_SEGMENT + ) + + assert ( + RunnerStateMachine(RunnerStatus.POST_SEGMENT).send(RunnerEvent.POST_CYCLE) + == RunnerStatus.POST_CYCLE + ) - assert RunnerStateMachine( - RunnerStatus.POST_SEGMENT - ).send(RunnerEvent.POST_CYCLE) == RunnerStatus.POST_CYCLE + assert ( + RunnerStateMachine(RunnerStatus.POST_CYCLE).send(RunnerEvent.PRE_CYCLE) + == RunnerStatus.PRE_CYCLE + ) - assert RunnerStateMachine( - RunnerStatus.POST_CYCLE - ).send(RunnerEvent.PRE_CYCLE) == RunnerStatus.PRE_CYCLE class Test_NoRunner: @@ -87,7 +103,7 @@ def __getitem__(self, key: str) -> int: return self.a def dict(self) -> dict[str, int]: - return {"a" : self.a} + return {"a": self.a} runner = NoRunner() @@ -101,7 +117,7 @@ def dict(self) -> dict[str, int]: walker, 10, ) - + runner.init() with pytest.raises(RunnerStateError): @@ -109,15 +125,12 @@ def dict(self) -> dict[str, int]: walker, 10, ) - + runner.pre_cycle() - assert ( - runner.run_segment( - walker, - 10, - ) - == (walker, None) - ) + assert runner.run_segment( + walker, + 10, + ) == (walker, None) runner.post_cycle(None) with pytest.raises(RunnerStateError): @@ -125,4 +138,3 @@ def dict(self) -> dict[str, int]: walker, 10, ) - diff --git a/tests/unit/test_sim_manager.py b/tests/unit/test_sim_manager.py index 472904f0..43882597 100644 --- a/tests/unit/test_sim_manager.py +++ b/tests/unit/test_sim_manager.py @@ -1,27 +1,29 @@ +# Standard Library import copy +# Third Party Library import pytest -import attrs -from wepy.walker import Walker -from wepy.work_mapper.serial import SerialMapper -from wepy.resampling.resamplers.noresampler import NoResampler -from wepy.runners.runner import NoRunner, RunnerStatus, RunSegmentData -from wepy.runners.mock import MockRunnerFactory, MockState, MockError +# First Party Library +from wepy.resampling.resamplers.noresampler import NoResampler +from wepy.runners.mock import MockError, MockRunnerFactory, MockState +from wepy.runners.runner import RunnerStatus, RunSegmentData from wepy.sim_manager import ( Manager, + ManagerEvent, ManagerStateMachine, ManagerStateTransitionError, - ManagerStateError, ManagerStatus, - ManagerEvent, ) +from wepy.walker import Walker +from wepy.work_mapper.serial import SerialMapper + @pytest.fixture def sim_components() -> tuple[ - list[Walker], - MockRunnerFactory, - type[NoResampler], + list[Walker], + MockRunnerFactory, + type[NoResampler], ]: num_walkers = 4 @@ -32,12 +34,12 @@ def sim_components() -> tuple[ state=MockState(1), weight=init_walker_weight, ) - for walker_state - in range(num_walkers) + for walker_state in range(num_walkers) ] return init_walkers, MockRunnerFactory(fail=False), NoResampler + class Test_ManagerStateMachine: def test_validate_event(self): @@ -48,7 +50,7 @@ def test_validate_event(self): sm = ManagerStateMachine() with pytest.raises(ManagerStateTransitionError): sm.validate_event(ManagerEvent.FINISH_SIMULATION) - + def test_send(self): sm = ManagerStateMachine(state=ManagerStatus.CONSTRUCTED) @@ -65,7 +67,6 @@ def test_send(self): # sm = ManagerStateMachine(state=ManagerStatus.PRE_INITIALIZATION) - class Test_Manager: def test___init__(self, sim_components): @@ -132,17 +133,18 @@ def test_post_segment(self, sim_components): 0, ) - manager.post_segment([ - RunSegmentData( - segment_split_time=2., - ) - for _ - in range(len(sim_components[0])) - ]) + manager.post_segment( + [ + RunSegmentData( + segment_split_time=2.0, + ) + for _ in range(len(sim_components[0])) + ] + ) assert manager.status == ManagerStatus.POST_SEGMENT_FINISHED assert manager._runner.status == RunnerStatus.POST_CYCLE - + def test_cleanup(self, sim_components): manager = Manager(*sim_components) @@ -159,7 +161,7 @@ def test_cleanup(self, sim_components): with pytest.raises(ManagerStateTransitionError): manager.cleanup() - + def test_run_segment(self, sim_components): init_walkers, runner_factory, resampler = sim_components diff --git a/tests/unit/test_util/test_multiprocessing.py b/tests/unit/test_util/test_multiprocessing.py index 971df11e..16b45a5d 100644 --- a/tests/unit/test_util/test_multiprocessing.py +++ b/tests/unit/test_util/test_multiprocessing.py @@ -1,25 +1,29 @@ +# Standard Library import logging import multiprocessing as mp -from wepy.util.multiprocessing import proc_pool_worker_setup, _dummy_task +# First Party Library +from wepy.util.multiprocessing import _dummy_task, proc_pool_worker_setup + def test__dummy_task(): assert _dummy_task(1) == 2 + def test_proc_pool_worker_setup(caplog): mp_ctx = mp.get_context(method="spawn") - + log_queue = mp_ctx.Queue() handlers = list(logging.getLogger().handlers) listener = logging.handlers.QueueListener(log_queue, *handlers) listener.start() with mp_ctx.Pool( - processes=1, - initializer=proc_pool_worker_setup, - initargs=(log_queue,), + processes=1, + initializer=proc_pool_worker_setup, + initargs=(log_queue,), ) as pool: # NOTE: this is the hardcoded logger in the dummy function diff --git a/tests/unit/test_util/test_openmm.py b/tests/unit/test_util/test_openmm.py index ec4f5273..f8eaab80 100644 --- a/tests/unit/test_util/test_openmm.py +++ b/tests/unit/test_util/test_openmm.py @@ -1,7 +1,11 @@ +# Third Party Library import numpy as np import openmm + +# First Party Library from wepy.util.openmm import array3d_to_vec3, vec3_to_array3d + def test_array3d_to_vec3(): assert tuple( array3d_to_vec3( @@ -19,6 +23,7 @@ def test_array3d_to_vec3(): ] ) + def test_vec3_to_array3d(): assert np.array_equal( @@ -37,4 +42,3 @@ def test_vec3_to_array3d(): ] ), ) - diff --git a/tests/unit/test_walker.py b/tests/unit/test_walker.py index 314ea0fe..8133a96f 100644 --- a/tests/unit/test_walker.py +++ b/tests/unit/test_walker.py @@ -1,26 +1,32 @@ -import math +# Standard Library from typing import Literal, TypedDict + +# Third Party Library +import attrs + +# First Party Library from wepy.missing import MISSING from wepy.walker import ( + Walker, + WalkerState, clone, - squash, - split, keep_merge, merge, - Walker, - WalkerState, + split, + squash, ) -import attrs - # attrs provide the __eq__ method MockKeys = Literal["a", "b"] MockDataValue = int | str + + class MockData(TypedDict): a: int b: str + @attrs.define class MockWalkerState(WalkerState): a: int @@ -34,7 +40,7 @@ def __getitem__(self, key: MockKeys) -> MockDataValue: def dict(self) -> MockData: return attrs.asdict(self) - + class TestWalkerState: @@ -97,6 +103,7 @@ def test___eq__(self): weight=0.05, ) + def test_clone(): state = MockWalkerState(a=1, b="hello") walker = Walker( @@ -116,6 +123,7 @@ def test_clone(): weight=0.05, ) + def test_squash(): walker_a = Walker( @@ -138,6 +146,7 @@ def test_squash(): weight=0.2, ) + def test_split(): walker = Walker( diff --git a/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py b/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py index 2bb7dbbd..275b2389 100644 --- a/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py +++ b/tests/unit/test_wepy_tools/test_systems/test_alanine_dipeptide.py @@ -1,17 +1,21 @@ -import json +# Standard Library +# Third Party Library import numpy as np -import mdtraj -from wepy.util.mdtraj import mdtraj_to_json_topology +# First Party Library from wepy.runners.openmm import OpenMMState -from wepy_tools.systems.alanine_dipeptide import AlanineDipeptideExplicitSystem, AlanineDipeptideRamachandranDistance +from wepy_tools.systems.alanine_dipeptide import ( + AlanineDipeptideExplicitSystem, + AlanineDipeptideRamachandranDistance, +) def test_AlanineDipeptideExplicitSystem(): AlanineDipeptideExplicitSystem() + class Test_AlanineDipeptideRamachandranDistance: def test_image(self): @@ -30,15 +34,19 @@ def test_image_distance(self): image_a = distance.image(ala_sys.state) - assert np.isclose(distance.image_distance(image_a, image_a), 0.) + assert np.isclose(distance.image_distance(image_a, image_a), 0.0) # then make a jittered atom positions to get something a little # different to compare - jitter_positions = ala_sys.state.positions + np.random.uniform( - -0.01, - 0.01, - size=ala_sys.state.positions.shape, - ) * ala_sys.state.positions.unit + jitter_positions = ( + ala_sys.state.positions + + np.random.uniform( + -0.01, + 0.01, + size=ala_sys.state.positions.shape, + ) + * ala_sys.state.positions.unit + ) jitter_state = OpenMMState.from_dwim( positions=jitter_positions, @@ -52,7 +60,7 @@ def test_image_distance(self): image_a, jitter_image, ), - 0., + 0.0, ) # test it is symmetric diff --git a/tests/unit/test_wepy_tools/test_systems/test_lennard_jones.py b/tests/unit/test_wepy_tools/test_systems/test_lennard_jones.py index 16116432..259b00d3 100644 --- a/tests/unit/test_wepy_tools/test_systems/test_lennard_jones.py +++ b/tests/unit/test_wepy_tools/test_systems/test_lennard_jones.py @@ -1,5 +1,7 @@ +# First Party Library from wepy_tools.systems.lennard_jones import LennardJonesPair + def test_LennardJonesPair(): LennardJonesPair() diff --git a/tests/unit/test_work_mapper/test_openmm.py b/tests/unit/test_work_mapper/test_openmm.py index e0eca1c6..bc0fa50b 100644 --- a/tests/unit/test_work_mapper/test_openmm.py +++ b/tests/unit/test_work_mapper/test_openmm.py @@ -1,28 +1,24 @@ +# Third Party Library +import openmm +import pytest + +# First Party Library from wepy.runners.openmm import ( OpenMMRunner, OpenMMState, ) from wepy.work_mapper.openmm.proc_pool import ( OpenMMProcPoolWorkMapper, - OpenMMProcPoolWorkMapperFactory, ) - -import pytest - -import numpy as np -import openmm -import openmm.app -import openmm.unit - from wepy_tools.systems.lennard_jones import LennardJonesPair + @pytest.fixture(scope="function") def openmm_runner() -> OpenMMRunner: lj_sys = LennardJonesPair() integrator = openmm.LangevinIntegrator(300.0, 0.002, 0.1) - runner = OpenMMRunner( system=lj_sys.system, topology=lj_sys.topology, @@ -30,6 +26,7 @@ def openmm_runner() -> OpenMMRunner: ) return runner + @pytest.fixture(scope="function") def openmm_state() -> OpenMMState: @@ -46,20 +43,19 @@ class Test_OpenMMProcPoolWorkMapper: def test_all(self, openmm_runner): lj_sys = LennardJonesPair() - + init_states = [ OpenMMState.from_dwim( positions=lj_sys.positions, ) - for _ - in range(4) + for _ in range(4) ] mapper = OpenMMProcPoolWorkMapper( platform="CPU", num_procs=2, - device_ids=[0,1], - global_platform_properties={"Threads" : "1"}, + device_ids=[0, 1], + global_platform_properties={"Threads": "1"}, ) mapper.init() @@ -69,19 +65,17 @@ def test_all(self, openmm_runner): new_states = mapper.map( openmm_runner.run_segment, init_states, - [ - 10 - for _ in range(len(init_states)) - ], + [10 for _ in range(len(init_states))], ) + # class TestOpenMMRayPoolWorkMapper: # @pytest.mark.ray # def test_all(self, openmm_runner): # lj_sys = LennardJonesPair() - + # init_states = [ # OpenMMState.from_dwim( # positions=lj_sys.positions, @@ -95,7 +89,7 @@ def test_all(self, openmm_runner): # device_ids=[0,1], # global_platform_properties={"Threads" : "1"}, # # ray_init_args={ - + # # } # ) @@ -110,4 +104,3 @@ def test_all(self, openmm_runner): # ], # init_states, # ) - diff --git a/tests/unit/test_work_mapper/test_serial.py b/tests/unit/test_work_mapper/test_serial.py index 66919473..ab7d042a 100644 --- a/tests/unit/test_work_mapper/test_serial.py +++ b/tests/unit/test_work_mapper/test_serial.py @@ -1,10 +1,14 @@ -import functools +# Standard Library + +# Third Party Library import attrs -from wepy.walker import Walker, WalkerState + +# First Party Library from wepy.work_mapper.serial import SerialMapper # some minimal definitions for testing a concrete work mapper + @attrs.define class RizzWalkerState: rizz: int @@ -17,6 +21,7 @@ def rizz_run(walker_state: RizzWalkerState, segment_length: int) -> RizzWalkerSt rizz=(walker_state.rizz + segment_length), ) + @attrs.define class RizzTask: @@ -26,6 +31,7 @@ def __call__(self, state: RizzWalkerState, segment_length: int) -> RizzWalkerSta return rizz_run(state, segment_length=segment_length, multiple=self.multiple) + def test_rizz_walker(): assert rizz_run( RizzWalkerState(rizz=1), @@ -40,7 +46,7 @@ def test_map(self): mapper = SerialMapper() mapper.init() - + assert mapper.map( rizz_run, [ From e70f11ba2209359f0335476606bb8eda8aa8a0d9 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 19:27:12 -0500 Subject: [PATCH 091/143] add NoResampler factory --- src/wepy/resampling/distances/simple.py | 2 -- src/wepy/resampling/resamplers/noresampler.py | 11 +++++++++-- .../test_resamplers/test_noresampler.py | 10 ++++++++-- tests/unit/test_sim_manager.py | 6 +++--- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/wepy/resampling/distances/simple.py b/src/wepy/resampling/distances/simple.py index c23fb0c3..3a5dc8bf 100644 --- a/src/wepy/resampling/distances/simple.py +++ b/src/wepy/resampling/distances/simple.py @@ -5,8 +5,6 @@ import attrs import numpy as np -# First Party Library - # Local Modules from .base import DistanceABC diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py index f2973db2..4f4516f1 100644 --- a/src/wepy/resampling/resamplers/noresampler.py +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -1,8 +1,6 @@ # Standard Library from typing import TypedDict -# Third Party Library - # First Party Library from wepy.resampling.decisions.no_decision import ( NoDecision, @@ -65,3 +63,12 @@ def resample( # the resampled walkers are just the walkers return walkers, resampling_data, resampler_data + + +class NoResamplerFactory: + + def __init__(self) -> None: + pass + + def __call__(self, num_cores: int) -> NoResampler: + return NoResampler() diff --git a/tests/unit/test_resampling/test_resamplers/test_noresampler.py b/tests/unit/test_resampling/test_resamplers/test_noresampler.py index ff2b6c70..1e485177 100644 --- a/tests/unit/test_resampling/test_resamplers/test_noresampler.py +++ b/tests/unit/test_resampling/test_resamplers/test_noresampler.py @@ -1,11 +1,11 @@ # First Party Library from wepy.resampling.decisions.no_decision import NothingDecisionEnum -from wepy.resampling.resamplers.noresampler import NoResampler +from wepy.resampling.resamplers.noresampler import NoResampler, NoResamplerFactory from wepy.runners.mock import MockState from wepy.walker import Walker -class TestNoResampler: +class Test_NoResampler: def test_resample(self): @@ -47,3 +47,9 @@ def test_resample(self): ], [{}], ) + + +def test_NoResamplerFactory(): + + factory = NoResamplerFactory() + factory(num_cores=1) diff --git a/tests/unit/test_sim_manager.py b/tests/unit/test_sim_manager.py index 43882597..4845d438 100644 --- a/tests/unit/test_sim_manager.py +++ b/tests/unit/test_sim_manager.py @@ -5,7 +5,7 @@ import pytest # First Party Library -from wepy.resampling.resamplers.noresampler import NoResampler +from wepy.resampling.resamplers.noresampler import NoResamplerFactory from wepy.runners.mock import MockError, MockRunnerFactory, MockState from wepy.runners.runner import RunnerStatus, RunSegmentData from wepy.sim_manager import ( @@ -23,7 +23,7 @@ def sim_components() -> tuple[ list[Walker], MockRunnerFactory, - type[NoResampler], + NoResamplerFactory, ]: num_walkers = 4 @@ -37,7 +37,7 @@ def sim_components() -> tuple[ for walker_state in range(num_walkers) ] - return init_walkers, MockRunnerFactory(fail=False), NoResampler + return init_walkers, MockRunnerFactory(fail=False), NoResamplerFactory() class Test_ManagerStateMachine: From de9e23a00f1a34e376a185740a9eeaebfe2999a3 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 21:32:23 -0500 Subject: [PATCH 092/143] make e2e tests more realistic --- justfile | 5 ++- .../integration/test_openmm/test_realistic.py | 32 +++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/justfile b/justfile index 25d1b13d..625b669b 100644 --- a/justfile +++ b/justfile @@ -19,7 +19,10 @@ check: uv run mypy src test: - uv run pytest --import-mode=importlib tests/unit + uv run pytest tests/unit + +test-integration: + uv run pytest --durations=0 -s -o log_cli=true --log-cli-level=INFO tests/integration clean: find . -type d -name "__pycache__" -prune -exec rm -rf {} + diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index 4be9d427..94fb4d33 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -10,6 +10,7 @@ import copy # Third Party Library +import pytest import mdtraj import openmm import psutil @@ -45,6 +46,8 @@ else _DEFAULT_HEARTBEAT_INTERVAL ) +DEFAULT_CYCLE_TIME = 10. * openmm.unit.picosecond +DEFAULT_CYCLE_STEPS = round(DEFAULT_CYCLE_TIME / STEP_SIZE) def test_lennard_jones_revo_procpool(): @@ -58,7 +61,7 @@ def test_lennard_jones_revo_procpool(): integrator=integrator, ) - num_walkers = 4 + num_walkers = 48 init_state = OpenMMState.from_dwim( positions=test_sys.positions, @@ -114,11 +117,12 @@ def test_lennard_jones_revo_procpool(): ) new_walkers, sim_components = sim_manager.run_simulation( - n_cycles=2, - segment_lengths=MIN_INTERVAL_STEPS * 2 + 10, + n_cycles=10, + segment_lengths=DEFAULT_CYCLE_STEPS, ) - +# disable timeout for this one +@pytest.mark.timeout(timeout=0) def test_alanine_dipeptide_revo_procpool(): ala_sys = AlanineDipeptideExplicitSystem() @@ -138,7 +142,7 @@ def test_alanine_dipeptide_revo_procpool(): integrator=integrator, ) - num_walkers = 4 + num_walkers = 10 # TODO: remove the need to deepcopy and have the components make # their own copies if necessary @@ -179,11 +183,25 @@ def test_alanine_dipeptide_revo_procpool(): work_mapper_factory=OpenMMProcPoolWorkMapperFactory( platform="CPU", num_procs=num_workers, + # NOTE,TOREV: in practice not limiting this is just faster + # and gets better utilization. But for tests we don't want + # it to eat up all the CPU so we limit it and take + # longer. In CI we probably want it to use everything + # though so review this later. This is only true when + # there are very few walkers though and with more CPU + # utilization goes way up global_platform_properties={"Threads": str(cores_per_worker)}, ), ) + # new_walkers, sim_components = sim_manager.run_simulation( + # n_cycles=2, + # segment_lengths=MIN_INTERVAL_STEPS * 2 + 10, + # ) + + # short number of steps but many cycles to exercise the pools new_walkers, sim_components = sim_manager.run_simulation( - n_cycles=2, - segment_lengths=MIN_INTERVAL_STEPS * 2 + 10, + n_cycles=100, + segment_lengths=10, ) + From 50ca5d0f9662cde466959f697c681dc06a8bf621 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 10 Dec 2025 22:41:21 -0500 Subject: [PATCH 093/143] refactor reporter base classes and test --- info/users_guide.org | 8 +- src/wepy/reporter/base.py | 149 +++++ src/wepy/reporter/dashboard.py | 85 +-- src/wepy/reporter/file.py | 250 ++++++++ src/wepy/reporter/hdf5.py | 4 +- src/wepy/reporter/reporter.py | 545 ------------------ src/wepy/reporter/restree.py | 4 +- src/wepy/reporter/walker.py | 4 +- src/wepy/sim_manager.py | 27 +- .../integration/test_openmm/test_realistic.py | 19 +- tests/unit/test_reporter/test_file.py | 112 ++++ 11 files changed, 580 insertions(+), 627 deletions(-) create mode 100644 src/wepy/reporter/base.py create mode 100644 src/wepy/reporter/file.py delete mode 100644 src/wepy/reporter/reporter.py create mode 100644 tests/unit/test_reporter/test_file.py diff --git a/info/users_guide.org b/info/users_guide.org index 0d3a8fb7..4a11bea4 100644 --- a/info/users_guide.org +++ b/info/users_guide.org @@ -1478,8 +1478,8 @@ handle new ones, e.g.: #+end_src -In addition to the ABC ~Reporter~ class the ~FileReporter~ and -~ProgressiveFileReporter~ are very useful to inherit from as they +In addition to the ABC ~Reporter~ class the ~FileReporterABC~ and +~ProgressiveFileReporterABC~ are very useful to inherit from as they handle some file path and file mode logic, the latter updates modes to allow for repeated writes to the same file for each cycle of a simulation. @@ -1489,9 +1489,9 @@ specific to its own function and all the handling of filenames is done by a call to the superclass constructor: #+begin_src python - from wepy.reporter.reporter import ProgressiveFileReporter + from wepy.reporter.reporter import ProgressiveFileReporterABC - class DashboardReporter(ProgressiveFileReporter): + class DashboardReporter(ProgressiveFileReporterABC): def __init__(self, step_time=None, diff --git a/src/wepy/reporter/base.py b/src/wepy/reporter/base.py new file mode 100644 index 00000000..7361ede9 --- /dev/null +++ b/src/wepy/reporter/base.py @@ -0,0 +1,149 @@ +# Standard Library +import logging +from typing import Any, Protocol, TypedDict + +# First Party Library +from wepy.boundary_conditions.boundary import BoundaryConditions +from wepy.resampling.resamplers.resampler import Resampler +from wepy.runners.runner import Runner +from wepy.walker import Walker +from wepy.work_mapper.base import WorkMapper + +logger = logging.getLogger(__name__) + + +class ReporterError(Exception): + pass + + +class SimComponentArgs(TypedDict): + init_walkers: list[Walker] + runner: Runner + resampler: Resampler + boundary_conditions: BoundaryConditions | None + work_mapper: WorkMapper + reporters: list["Reporter"] + continue_run: int | None + + +class CycleReportDict(TypedDict): + cycle_idx: int + new_walkers: list[Walker] + # TODO: types for all the Anys + warp_data: list[Any] + bc_data: list[Any] + progress_data: dict[Any] + resampling_data: Any + resampler_data: Any + n_segment_steps: int + resampled_walkers: list[Walker] + runner_precycle_time: float + runner_postcycle_time: float + sim_manager_segment_overhead_time: float + runner_splits_time: dict[str, float] | None + worker_segment_times: dict[int, list[float]] | None + cycle_sim_manager_segment_time: float + cycle_runner_time: float + cycle_bc_time: float + cycle_resampling_time: float + + +class Reporter(Protocol): + """Abstract base class for wepy reporters. + + All reporters must customize and override minimally the 'report' + method. Optionally the 'init' and 'cleanup' can be overriden. + + """ + + def init( + self, + **kwargs: SimComponentArgs, + ) -> None: + """Initialization routines for the reporter at simulation runtime. + + Initialize I/O connections including file descriptors, + database connections, timers, stdout/stderr etc. + + Void method for reporter base class. + + Reporters can expect to have the following key word arguments + passed to them during a simulation by the sim_manager in this + call. + + + Parameters + ---------- + init_walkers : list of Walker objects + The initial walkers for the simulation. + + runner : Runner object + The runner that will be used in the simulation. + + resampler : Resampler object + The resampler that will be used in the simulation. + + boundary_conditions : BoundaryConditions object + The boundary conditions taht will be used in the simulation. + + work_mapper : WorkMapper object + The work mapper that will be used in the simulation. + + reporters : list of Reporter objects + The list of reporters that are in the simulation. + + continue_run : int or None + The index of the run that is being continued within this + same file. + + """ + ... + + def report( + self, + **kwargs: CycleReportDict, + ) -> None: + """Given data concerning the main simulation components state, perform + I/O operations to persist that data. + + Void method for reporter base class. + + Reporters can expect to have the following key word arguments + passed to them during a simulation by the sim_manager. + + """ + ... + + def cleanup( + self, + **kwargs: SimComponentArgs, + ) -> None: + """Teardown routines for the reporter at the end of the simulation. + + Use to cleanly and safely close I/O connections or other + cleanup I/O. + + Use to close file descriptors, database connections etc. + + Reporters can expect to have the following key word arguments + passed to them during a simulation by the sim_manager. + + Parameters + ---------- + runner : Runner object + The runner at the end of the simulation + + work_mapper : WorkeMapper object + The work mapper at the end of the simulation + + resampler : Resampler object + The resampler at the end of the simulation + + boundary_conditions : BoundaryConditions object + The boundary conditions at the end of the simulation + + reporters : list of Reporter objects + The list of reporters at the end of the simulation + + """ + ... diff --git a/src/wepy/reporter/dashboard.py b/src/wepy/reporter/dashboard.py index aa09933a..2386e4ce 100644 --- a/src/wepy/reporter/dashboard.py +++ b/src/wepy/reporter/dashboard.py @@ -4,9 +4,7 @@ # Standard Library import logging - -logger = logging.getLogger(__name__) -# Standard Library +import textwrap import time from copy import copy from datetime import datetime @@ -18,10 +16,12 @@ from tabulate import tabulate # First Party Library -from wepy.reporter.reporter import ProgressiveFileReporter +from wepy.reporter.file import ProgressiveFileReporterABC + +logger = logging.getLogger(__name__) -class DashboardReporter(ProgressiveFileReporter): +class DashboardReporter(ProgressiveFileReporterABC): """A text based report of the status of a wepy simulation. This serves as a container for different dashboard components to @@ -30,56 +30,61 @@ class DashboardReporter(ProgressiveFileReporter): """ FILE_ORDER = ("dashboard_path",) - SUGGESTED_EXTENSIONS = ("dash.org",) + SUGGESTED_EXTENSIONS = ("wepy_dash.org",) # TODO: add in a section for showing the number of walkers in each # cycle. This isn't relevant for our constant walker number # simulations though so I have elided it following YAGNI - SIMULATION_SECTION_TEMPLATE = """ -Init Datetime: {{ init_date_time }} -Last write Datetime: {{ curr_date_time }} -Total Run time: {{ total_run_time }} s -Last Cycle Index: {{ last_cycle_idx }} -Number of Cycles: {{ n_cycles }} - -** Walkers Summary -{{ walker_cycle_summary_table }} -""" - - PERFORMANCE_SECTION_TEMPLATE = """ -Average Cycle Time: {{ avg_cycle_time }} -{% if avg_runner_time %}Average Runner Time: {{ avg_runner_time }}{% else %}{% endif %} -{% if avg_bc_time %}Average Boundary Conditions Time: {{ avg_bc_time }}{% else %}{% endif %} -{% if avg_resampling_time %}Average Resampling Time: {{ avg_resampling_time }}{% else %}{% endif %} + SIMULATION_SECTION_TEMPLATE = textwrap.dedent( + """ + Init Datetime: {{ init_date_time }} + Last write Datetime: {{ curr_date_time }} + Total Run time: {{ total_run_time }} s + Last Cycle Index: {{ last_cycle_idx }} + Number of Cycles: {{ n_cycles }} + + ** Walkers Summary + {{ walker_cycle_summary_table }} + """ + ) -** Worker Avg. Segment Times: -{{ worker_avg_segment_time }} + PERFORMANCE_SECTION_TEMPLATE = textwrap.dedent( + """ + Average Cycle Time: {{ avg_cycle_time }} + {% if avg_runner_time %}Average Runner Time: {{ avg_runner_time }}{% else %}{% endif %} + {% if avg_bc_time %}Average Boundary Conditions Time: {{ avg_bc_time }}{% else %}{% endif %} + {% if avg_resampling_time %}Average Resampling Time: {{ avg_resampling_time }}{% else %}{% endif %} -** Cycle Performance Log -{{ cycle_log }} + ** Worker Avg. Segment Times: + {{ worker_avg_segment_time }} -** Worker Performance Log -{{ performance_log }} -""" + ** Cycle Performance Log + {{ cycle_log }} - DASHBOARD_TEMPLATE = """* Simulation -{{ simulation }} + ** Worker Performance Log + {{ performance_log }} + """ + ) + DASHBOARD_TEMPLATE = textwrap.dedent( + """* Simulation + {{ simulation }} -{% if resampler %}* Resampler{% else %}{% endif %} -{% if resampler %}{{ resampler }}{% else %}{% endif %} + {% if resampler %}* Resampler{% else %}{% endif %} + {% if resampler %}{{ resampler }}{% else %}{% endif %} -{% if boundary_condition %}* Boundary Condition{% else %}{% endif %} -{% if boundary_condition %}{{ boundary_condition }}{% else %}{% endif %} + {% if boundary_condition %}* Boundary Condition{% else %}{% endif %} + {% if boundary_condition %}{{ boundary_condition }}{% else %}{% endif %} -{% if runner %}* Runner{% else %}{% endif %} + {% if runner %}* Runner{% else %}{% endif %} -{% if runner %}{{ runner }}{% else %}{% endif %} + {% if runner %}{{ runner }}{% else %}{% endif %} -* Performance -{{ performance }} -""" + * Performance + {{ performance }} + """ + ) def __init__(self, resampler_dash=None, runner_dash=None, bc_dash=None, **kwargs): """Parameters diff --git a/src/wepy/reporter/file.py b/src/wepy/reporter/file.py new file mode 100644 index 00000000..e1817ac8 --- /dev/null +++ b/src/wepy/reporter/file.py @@ -0,0 +1,250 @@ +# Standard Library +import logging +from abc import ABC +from pathlib import Path +from typing import Literal, get_args + +# Local Modules +from .base import ReporterError, SimComponentArgs + +logger = logging.getLogger(__name__) + + +class FileReporterError(ReporterError): + pass + + +FileMode = Literal["x", "w", "w-", "r", "r+"] + + +class FileReporterABC(ABC): + """Abstract reporter that handles specifying file paths for a + reporter. + + This abstract class doesn't perform any operations that involve + actually opening file descriptors, but only the validation and + organization of file paths. + + This provides a uniform API for retrieving file paths from all + reporters inheriting from it. + + Additionally, FileReporter implements an interface for performing + a so-called reparametrization of the relevant values associated + with each file specification (i.e. file path and mode). + + A reparametrization can be performed by calling the + 'reparametrize' method, and can be customized. + + Additionally, there are some customizable class constants than can + be used in subclasses to control this process including: + DEFAULT_MODE, SUGGESTED_FILENAME_TEMPLATE, + DEFAULT_SUGGESTED_EXTENSION, FILE_ORDER, and SUGGESTED_EXTENSIONS. + + The intention is to allow the redefinition of file paths + dynamically to adapt to changing runtime requirements. Such as + execution on a separate subtree of a directory hierarchy. + + """ + + MODES = tuple(get_args(FileMode)) + """Valid modes accepted for files.""" + + DEFAULT_MODE: FileMode = "x" + """The default mode to set for opening files if none is specified + (create if doesn't exist, fail if it does.)""" + + SUGGESTED_FILENAME_TEMPLATE: str = "{config}{narration}{reporter_class}.{ext}" + """Template to use for dynamic reparametrization of file path names. + + The fields in the template are: + + config : indicator of the runtime configuration used + + narration : freeform description of the instance + + reporter_class : the name of the class that produced the + output. When no specific name is given for a file report generated + from a reporter this is used to disambiguate, along with the + extension. + + ext : The file extension, for multiple files produced from one + reporter this should be sufficient to disambiguate the files. + + The 'config' and 'narration' should be the same across all + reporters in the same simulation manager, and the 'narration' is + considered optional. + + """ + + DEFAULT_SUGGESTED_EXTENSION: str = "report" + """The default file extension used for files during dynamic + reparametrization, if none is specified""" + + FILE_ORDER: tuple[str, ...] = () + """Specify an ordering of file paths. Should be customized.""" + + SUGGESTED_EXTENSIONS: tuple[str, ...] = () + """Suggested extensions for file paths for use with the automatic + reparametrization feature. Should be customized.""" + + @classmethod + def _validate_mode(cls, mode: FileMode) -> bool: + """Check if the mode spec is a valid one. + + Parameters + ---------- + mode : str + + Returns + ------- + valid : bool + + """ + if mode in cls.MODES: + return True + else: + return False + + def __init__( + self, + file_paths: list[Path], + modes: list[FileMode] | None = None, + ) -> None: + """Constructor for FileReporter. + + This constructor allows the specification of either a list of + file names (and modes) via 'file_paths' and 'modes' key-word + arguments or a single 'file_path' and 'mode'. + + The access API though is always a list of file paths and modes + where order is important for associating other features. + + Parameters + ---------- + file_paths : list of str + The list of file paths (in order) to use. + + modes : list of str + The list of mode specs (in order) to use. + + """ + + # file paths + self._file_paths = file_paths + + # modes + + # if modes is None we make modes, from defaults if we have to + if modes is None: + # if mode is None set it to the default + if modes is None: + mode = self.DEFAULT_MODE + + # if only one mode is given copy it for each file given + modes = [mode for i in range(len(self._file_paths))] + + for mode in modes: + if not self._validate_mode(mode): + raise FileReporterError(f"Invalid file mode: {mode}") + + self._modes = modes + + @property + def file_paths(self) -> list[Path]: + """The file paths for this reporter, in order.""" + return self._file_paths + + @property + def modes(self) -> list[FileMode]: + """The modes for the files, in order.""" + return self._modes + + def set_path(self, file_idx, path): + """Set the path for a single indexed file. + + Parameters + ---------- + file_idx : int + Index in the listing of files. + path : str + The new path to set for this file + + """ + self._paths[file_idx] = path + + # TOREV: shouldn't need this. Can move to using attrs class and + # evolve if this is an issue elsewhere. + + # @modes.setter + # def modes(self, modes): + # """Setter for the modes. + + # Parameters + # ---------- + # modes : list of str + + # """ + # for i, mode in enumerate(modes): + # self.set_mode(i, mode) + + def set_mode(self, file_idx: int, mode: FileMode) -> None: + """Set the mode for a single indexed file. + + Parameters + ---------- + file_idx : int + Index in the listing of files. + mode : str + The new mode spec. + + """ + + if self._validate_mode(mode): + self._modes[file_idx] = mode + else: + raise FileReporterError(f"Incorrect mode {mode}") + + # def reparametrize(self, file_paths, modes): + # """Set the file paths and modes for all files in the reporter. + + # Parameters + # ---------- + # file_paths : list of str + # New file paths for each file, in order. + # modes : list of str + # New modes for each file, in order. + + # """ + + # self.file_paths = file_paths + # self.modes = modes + + +class ProgressiveFileReporterABC(FileReporterABC, ABC): + """Super class for a reporter that will successively overwrite the + same file over and over again. The base FileReporter really only + supports creation of file one time. + + """ + + def init(self, **kwargs: SimComponentArgs) -> None: + + # because we want to overwrite the file at every cycle we + # need to change the modes to write with truncate. This allows + # the file to first be opened in 'x' or 'w-' and check whether + # the file already exists (say from another run), and warn the + # user. However, once the file has been created for this run + # we need to overwrite it many times forcefully. + + # go thourgh each file managed by this reporter + for file_idx, mode in enumerate(self.modes): + # if the mode is 'x' or 'w-' we check to make sure the file + # doesn't exist + if mode in ["x", "w-"]: + file_path = self.file_paths[file_idx] + if file_path.exists(): + raise FileExistsError(f"File exists: {file_path}") + + # now that we have checked if the file exists we set it into + # overwrite mode + self.set_mode(file_idx, "w") diff --git a/src/wepy/reporter/hdf5.py b/src/wepy/reporter/hdf5.py index 856bd254..b1db8963 100644 --- a/src/wepy/reporter/hdf5.py +++ b/src/wepy/reporter/hdf5.py @@ -9,12 +9,12 @@ # First Party Library from wepy.hdf5 import WepyHDF5 -from wepy.reporter.reporter import FileReporter +from wepy.reporter.reporter import FileReporterABC from wepy.util.json_top import json_top_atom_count from wepy.walker import Walker, WalkerState -class WepyHDF5Reporter(FileReporter): +class WepyHDF5Reporter(FileReporterABC): """Reporter for generating an HDF5 format (WepyHDF5) data file from simulations. diff --git a/src/wepy/reporter/reporter.py b/src/wepy/reporter/reporter.py deleted file mode 100644 index 8c0a807d..00000000 --- a/src/wepy/reporter/reporter.py +++ /dev/null @@ -1,545 +0,0 @@ -# Standard Library -import logging - -logger = logging.getLogger(__name__) -# Standard Library -import os.path as osp - - -class ReporterError(Exception): - pass - - -class Reporter: - """Abstract base class for wepy reporters. - - All reporters must customize and override minimally the 'report' - method. Optionally the 'init' and 'cleanup' can be overriden. - - See Also - -------- - wepy.sim_manager : details of calls to reporter methods. - - """ - - def __init__(self, **kwargs): - """Construct a reporter. - - Void constructor for the Reporter base class. - - Parameters - ---------- - **kwargs : key-value pairs - Ignored kwargs, but accepts them from subclass calls for - compatibility. - - """ - pass - - def init(self, **kwargs): - """Initialization routines for the reporter at simulation runtime. - - Initialize I/O connections including file descriptors, - database connections, timers, stdout/stderr etc. - - Void method for reporter base class. - - Reporters can expect to have the following key word arguments - passed to them during a simulation by the sim_manager in this - call. - - - Parameters - ---------- - init_walkers : list of Walker objects - The initial walkers for the simulation. - - runner : Runner object - The runner that will be used in the simulation. - - resampler : Resampler object - The resampler that will be used in the simulation. - - boundary_conditions : BoundaryConditions object - The boundary conditions taht will be used in the simulation. - - work_mapper : WorkMapper object - The work mapper that will be used in the simulation. - - reporters : list of Reporter objects - The list of reporters that are in the simulation. - - continue_run : int - The index of the run that is being continued within this - same file. - - """ - method_name = "init" - assert not hasattr( - super(), method_name - ), f"Superclass with method {method_name} is masked" - - def report(self, **kwargs): - """Given data concerning the main simulation components state, perform - I/O operations to persist that data. - - Void method for reporter base class. - - Reporters can expect to have the following key word arguments - passed to them during a simulation by the sim_manager. - - Parameters - ---------- - cycle_idx : int - - new_walkers : list of Walker objects - List of walkers that were produced from running their - dynamics by the runner. - - warp_data : list of dict of str : value - List of dict-like records for each warping event from the - last cycle. - - bc_data : list of dict of str : value - List of dict-like records specifying the changes to the - state of the boundary conditions in the last cycle. - - progress_data : dict str : list - A record indicating the progress values for each walker in - the last cycle. - - resampling_data : list of dict of str : value - List of records specifying the resampling to occur at this - cycle. - - resampler_data : list of dict of str : value - List of records specifying the changes to the state of the - resampler in the last cycle. - - n_segment_steps : int - The number of dynamics steps that were completed in the last cycle - - worker_segment_times : dict of int : list of float - Mapping worker index to the times they took for each - segment they processed. - - cycle_runner_time : float - Total time runner took in last cycle. - - cycle_bc_time : float - Total time boundary conditions took in last cycle. - - cycle_resampling_time : float - Total time resampler took in last cycle. - - resampled_walkers : list of Walker objects - List of walkers that were produced from the new_walkers - from applying resampling and boundary conditions. - - """ - - method_name = "report" - assert not hasattr( - super(), method_name - ), "Superclass with method {} is masked".format(method_name) - - def cleanup(self, **kwargs): - """Teardown routines for the reporter at the end of the simulation. - - Use to cleanly and safely close I/O connections or other - cleanup I/O. - - Use to close file descriptors, database connections etc. - - Reporters can expect to have the following key word arguments - passed to them during a simulation by the sim_manager. - - Parameters - ---------- - runner : Runner object - The runner at the end of the simulation - - work_mapper : WorkeMapper object - The work mapper at the end of the simulation - - resampler : Resampler object - The resampler at the end of the simulation - - boundary_conditions : BoundaryConditions object - The boundary conditions at the end of the simulation - - reporters : list of Reporter objects - The list of reporters at the end of the simulation - - """ - method_name = "cleanup" - assert not hasattr( - super(), method_name - ), "Superclass with method {} is masked".format(method_name) - - -class FileReporter(Reporter): - """Abstract reporter that handles specifying file paths for a - reporter. - - This abstract class doesn't perform any operations that involve - actually opening file descriptors, but only the validation and - organization of file paths. - - This provides a uniform API for retrieving file paths from all - reporters inheriting from it. - - Additionally, FileReporter implements an interface for performing - a so-called reparametrization of the relevant values associated - with each file specification (i.e. file path and mode). - - A reparametrization can be performed by calling the - 'reparametrize' method, and can be customized. - - Additionally, there are some customizable class constants than can - be used in subclasses to control this process including: - DEFAULT_MODE, SUGGESTED_FILENAME_TEMPLATE, - DEFAULT_SUGGESTED_EXTENSION, FILE_ORDER, and SUGGESTED_EXTENSIONS. - - The intention is to allow the redefinition of file paths - dynamically to adapt to changing runtime requirements. Such as - execution on a separate subtree of a directory hierarchy. - - """ - - MODES = ( - "x", - "w", - "w-", - "r", - "r+", - ) - """Valid modes accepted for files.""" - - DEFAULT_MODE = "x" - """The default mode to set for opening files if none is specified - (create if doesn't exist, fail if it does.)""" - - SUGGESTED_FILENAME_TEMPLATE = "{config}{narration}{reporter_class}.{ext}" - """Template to use for dynamic reparametrization of file path names. - - The fields in the template are: - - config : indicator of the runtime configuration used - - narration : freeform description of the instance - - reporter_class : the name of the class that produced the - output. When no specific name is given for a file report generated - from a reporter this is used to disambiguate, along with the - extension. - - ext : The file extension, for multiple files produced from one - reporter this should be sufficient to disambiguate the files. - - The 'config' and 'narration' should be the same across all - reporters in the same simulation manager, and the 'narration' is - considered optional. - - """ - - DEFAULT_SUGGESTED_EXTENSION = "report" - """The default file extension used for files during dynamic - reparametrization, if none is specified""" - - FILE_ORDER = () - """Specify an ordering of file paths. Should be customized.""" - - SUGGESTED_EXTENSIONS = () - """Suggested extensions for file paths for use with the automatic - reparametrization feature. Should be customized.""" - - def __init__( - self, file_paths=None, modes=None, file_path=None, mode=None, **kwargs - ): - """Constructor for FileReporter. - - This constructor allows the specification of either a list of - file names (and modes) via 'file_paths' and 'modes' key-word - arguments or a single 'file_path' and 'mode'. - - The access API though is always a list of file paths and modes - where order is important for associating other features. - - Parameters - ---------- - file_paths : list of str - The list of file paths (in order) to use. - - modes : list of str - The list of mode specs (in order) to use. - - file_path : str - If 'file_paths' not specified, the single file path to use. - - mode : str - If 'file_path' option used, this is the mode for that file. - - """ - - # file paths - - assert not ( - (file_paths is not None) and (file_path is not None) - ), "only file_paths or file_path kwargs can be specified" - - # if only one file path is given then we handle it as multiple - if file_path is not None: - file_paths = [file_path] - - # if any of the explicit paths are given (from the FILE_ORDER - # constant) in the kwargs then we automatically add those to - # the file_paths being sent to the super class constructor. - - # we use a flag to condition this, initialize and fall back to - # using the 'file_paths' kwarg - use_explicit_path_kwargs = False - - # we check the kwargs for the explicit file kwargs, and if - # they are given then we check whether they are valid and if - # they are, use them to set the 'file_paths' kwarg - - # make a list of the presence of the given explicit keys - given_explicit_kwargs = [ - (True if file_key in kwargs else False) for file_key in self.FILE_ORDER - ] - - # check that all the keys are present, if they aren't all - # present then the flag will stay false and the fallback of - # using the 'file_paths' kwarg will be used - if all(given_explicit_kwargs): - # then get the values and check them - valid_explicit_kwargs = [ - (True if kwargs[file_key] is not None else False) - for file_key in self.FILE_ORDER - ] - - # if they are all valid then we can use them - if not all(valid_explicit_kwargs): - use_explicit_path_kwargs = True - - # if only some were given this is wrong - elif any(given_explicit_kwargs): - raise ValueError( - "If you explicitly pass in the paths, all must be given explicitly" - ) - - # if we use the explicit path kwargs, then we need to put them - # into the 'file_paths' for superclass initialization - if use_explicit_path_kwargs: - file_paths = [] - for file_key in self.FILE_ORDER: - # add it to the file paths for superclass initialization - file_paths.append(kwargs[file_key]) - - # otherwise we need to use the file_paths argument that should - # have been given - else: - # make sure it is in kwargs and valid - assert ( - file_paths is not None - ), "if no explicit file path is given the 'file_paths' must have a value" - - assert len(file_paths) == len( - self.FILE_ORDER - ), "you must give file_paths {} paths".format(len(self.FILE_ORDER)) - - # using the file_path paths we got above we set them as - # attributes in this object - for i, file_key in enumerate(self.FILE_ORDER): - setattr(self, file_key, file_paths[i]) - - # set the underlying file paths - self._file_paths = file_paths - - # modes - - assert not ( - (modes is not None) and (mode is not None) - ), "only modes or mode kwargs can be specified" - - # if modes is None we make modes, from defaults if we have to - if modes is None: - # if mode is None set it to the default - if modes is None and mode is None: - mode = self.DEFAULT_MODE - - # if only one mode is given copy it for each file given - modes = [mode for i in range(len(self._file_paths))] - - self._modes = modes - - super().__init__(**kwargs) - - def _validate_mode(self, mode): - """Check if the mode spec is a valid one. - - Parameters - ---------- - mode : str - - Returns - ------- - valid : bool - - """ - if mode in self.MODES: - return True - else: - return False - - @property - def mode(self): - """For single file path reporters the mode of that file.""" - if len(self._file_paths) > 1: - raise ReporterError("there are multiple files and modes defined") - - return self._modes[0] - - @property - def file_path(self): - """For single file path reporters the file path to that file spec.""" - if len(self._file_paths) > 1: - raise ReporterError("there are multiple files and modes defined") - - return self._file_paths[0] - - @property - def file_paths(self): - """The file paths for this reporter, in order.""" - return self._file_paths - - @file_paths.setter - def file_paths(self, file_paths): - """Setter for the file paths. - - Parameters - ---------- - file_paths : list of str - - """ - for i, file_path in enumerate(file_paths): - self.set_path(i, file_path) - - def set_path(self, file_idx, path): - """Set the path for a single indexed file. - - Parameters - ---------- - file_idx : int - Index in the listing of files. - path : str - The new path to set for this file - - """ - self._paths[file_idx] = path - - @property - def modes(self): - """The modes for the files, in order.""" - return self._modes - - @modes.setter - def modes(self, modes): - """Setter for the modes. - - Parameters - ---------- - modes : list of str - - """ - for i, mode in enumerate(modes): - self.set_mode(i, mode) - - def set_mode(self, file_idx, mode): - """Set the mode for a single indexed file. - - Parameters - ---------- - file_idx : int - Index in the listing of files. - mode : str - The new mode spec. - - """ - - if self._validate_mode(mode): - self._modes[file_idx] = mode - else: - raise ValueError("Incorrect mode {}".format(mode)) - - def reparametrize(self, file_paths, modes): - """Set the file paths and modes for all files in the reporter. - - Parameters - ---------- - file_paths : list of str - New file paths for each file, in order. - modes : list of str - New modes for each file, in order. - - """ - - self.file_paths = file_paths - self.modes = modes - - -class ProgressiveFileReporter(FileReporter): - """Super class for a reporter that will successively overwrite the - same file over and over again. The base FileReporter really only - supports creation of file one time. - - """ - - def init(self, **kwargs): - """Construct a ProgressiveFileReporter. - - This is exactly the same as the FileReporter. - - - Parameters - ---------- - file_paths : list of str - The list of file paths (in order) to use. - - modes : list of str - The list of mode specs (in order) to use. - - file_path : str - If 'file_paths' not specified, the single file path to use. - - mode : str - If 'file_path' option used, this is the mode for that file. - - See Also - -------- - wepy.reporter.reporter.FileReporter - - """ - - super().init(**kwargs) - - # because we want to overwrite the file at every cycle we - # need to change the modes to write with truncate. This allows - # the file to first be opened in 'x' or 'w-' and check whether - # the file already exists (say from another run), and warn the - # user. However, once the file has been created for this run - # we need to overwrite it many times forcefully. - - # go thourgh each file managed by this reporter - for file_i, mode in enumerate(self.modes): - # if the mode is 'x' or 'w-' we check to make sure the file - # doesn't exist - if mode in ["x", "w-"]: - file_path = self.file_paths[file_i] - if osp.exists(file_path): - raise FileExistsError("File exists: '{}'".format(file_path)) - - # now that we have checked if the file exists we set it into - # overwrite mode - self.set_mode(file_i, "w") diff --git a/src/wepy/reporter/restree.py b/src/wepy/reporter/restree.py index b3541f0c..f0356e5a 100644 --- a/src/wepy/reporter/restree.py +++ b/src/wepy/reporter/restree.py @@ -27,10 +27,10 @@ parent_panel, resampling_panel, ) -from wepy.reporter.reporter import ProgressiveFileReporter +from wepy.reporter.reporter import ProgressiveFileReporterABC -class ResTreeReporter(ProgressiveFileReporter): +class ResTreeReporter(ProgressiveFileReporterABC): """Reporter that generates resampling parent trees in the GEXF format. """ diff --git a/src/wepy/reporter/walker.py b/src/wepy/reporter/walker.py index c16e9ff2..c61b8266 100644 --- a/src/wepy/reporter/walker.py +++ b/src/wepy/reporter/walker.py @@ -16,7 +16,7 @@ import numpy as np # First Party Library -from wepy.reporter.reporter import ProgressiveFileReporter +from wepy.reporter.reporter import ProgressiveFileReporterABC from wepy.util.json_top import json_top_subset from wepy.util.mdtraj import json_to_mdtraj_topology from wepy.util.util import ( @@ -25,7 +25,7 @@ ) -class WalkerReporter(ProgressiveFileReporter): +class WalkerReporter(ProgressiveFileReporterABC): """Reporter for generating 3D molecular structure files of the walkers produced by a cycle. diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index 2d09ca1e..7e2953a2 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -47,7 +47,7 @@ import enum import logging import time -from typing import Any, Callable, Final, Generic, Literal, TypedDict, TypeVar +from typing import Callable, Final, Generic, Literal, TypeVar # Third Party Library import attrs @@ -57,7 +57,7 @@ # First Party Library from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.monitor import Monitor -from wepy.reporter.reporter import Reporter +from wepy.reporter.base import CycleReportDict, Reporter from wepy.resampling.resamplers.resampler import Resampler from wepy.runners.runner import Runner, RunnerFactory, RunSegmentData from wepy.walker import Walker @@ -67,28 +67,6 @@ logger = logging.getLogger(__name__) -class CycleReportDict(TypedDict): - cycle_idx: int - new_walkers: list[Walker] - # TODO: types for all the Anys - warp_data: list[Any] - bc_data: list[Any] - progress_data: dict[Any] - resampling_data: Any - resampler_data: Any - n_segment_steps: int - resampled_walkers: list[Walker] - runner_precycle_time: float - runner_postcycle_time: float - sim_manager_segment_overhead_time: float - runner_splits_time: dict[str, float] | None - worker_segment_times: dict[int, list[float]] | None - cycle_sim_manager_segment_time: float - cycle_runner_time: float - cycle_bc_time: float - cycle_resampling_time: float - - class ManagerStatus(enum.IntEnum): CONSTRUCTED = enum.auto() PRE_SIMULATION = enum.auto() @@ -658,6 +636,7 @@ def cleanup(self) -> None: for reporter in self.reporters: logger.info(f"Cleaning up reporter: {reporter}") reporter.cleanup( + init_walkers=self.init_walkers, runner=self._runner, work_mapper=self._work_mapper, resampler=self._resampler, diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index 94fb4d33..efffad45 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -10,12 +10,12 @@ import copy # Third Party Library -import pytest -import mdtraj import openmm import psutil +import pytest # First Party Library +from wepy.reporter.dashboard import DashboardReporter from wepy.resampling.resamplers.revo import REVOResamplerFactory from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState from wepy.runners.openmm.runner import ( @@ -23,7 +23,6 @@ _DEFAULT_STATE_TIME_INTERVAL, ) from wepy.sim_manager import Manager -from wepy.util.mdtraj import mdtraj_to_json_topology # TODO: use the high-level API imports from wepy.walker import Walker @@ -46,9 +45,10 @@ else _DEFAULT_HEARTBEAT_INTERVAL ) -DEFAULT_CYCLE_TIME = 10. * openmm.unit.picosecond +DEFAULT_CYCLE_TIME = 10.0 * openmm.unit.picosecond DEFAULT_CYCLE_STEPS = round(DEFAULT_CYCLE_TIME / STEP_SIZE) + def test_lennard_jones_revo_procpool(): test_sys = LennardJonesPair() @@ -90,8 +90,6 @@ def test_lennard_jones_revo_procpool(): num_workers = num_walkers cores_per_worker = num_workers // num_walkers - json_top = mdtraj_to_json_topology(mdtraj.Topology.from_openmm(test_sys.topology)) - distance_metric = PairDistance() resampler_factory = REVOResamplerFactory( @@ -100,6 +98,10 @@ def test_lennard_jones_revo_procpool(): distance_metric=distance_metric, ) + dashboard_reporter = DashboardReporter() + + reporters = [dashboard_reporter] + sim_manager = Manager( init_walkers=init_walkers, runner_factory=runner_factory, @@ -114,13 +116,15 @@ def test_lennard_jones_revo_procpool(): # global_platform_properties={"Threads" : "1"}, global_platform_properties={"Threads": str(cores_per_worker)}, ), + reporters=reporters, ) new_walkers, sim_components = sim_manager.run_simulation( - n_cycles=10, + n_cycles=2, segment_lengths=DEFAULT_CYCLE_STEPS, ) + # disable timeout for this one @pytest.mark.timeout(timeout=0) def test_alanine_dipeptide_revo_procpool(): @@ -204,4 +208,3 @@ def test_alanine_dipeptide_revo_procpool(): n_cycles=100, segment_lengths=10, ) - diff --git a/tests/unit/test_reporter/test_file.py b/tests/unit/test_reporter/test_file.py new file mode 100644 index 00000000..52655d5c --- /dev/null +++ b/tests/unit/test_reporter/test_file.py @@ -0,0 +1,112 @@ +# Standard Library +from pathlib import Path + +# Third Party Library +import pytest + +# First Party Library +from wepy.reporter.file import ( + FileReporterABC, + FileReporterError, + ProgressiveFileReporterABC, +) +from wepy.resampling.resamplers.noresampler import NoResampler +from wepy.runners.mock import MockRunner +from wepy.work_mapper.serial import SerialMapper + + +class Test_FileReporterABC: + + def test__validate_mode(self): + + assert FileReporterABC._validate_mode("x") + assert FileReporterABC._validate_mode("w") + assert FileReporterABC._validate_mode("w-") + assert FileReporterABC._validate_mode("r") + assert FileReporterABC._validate_mode("r+") + + def test___init__(self): + + assert FileReporterABC( + [Path("somewhere/else.txt")], + ).modes == ["x"] + + FileReporterABC( + [Path("somewhere/else.txt")], + ["x"], + ) + + with pytest.raises(FileReporterError): + + FileReporterABC( + [Path("somewhere/else.txt")], + ["B"], + ) + + def test_set_mode(self): + + reporter = FileReporterABC( + [Path("thing.txt")], + ["r"], + ) + assert reporter.modes[0] == "r" + reporter.set_mode(0, "w") + assert reporter.modes[0] == "w" + + +class Test_ProgressiveFileReporterABC: + + def test_init(self, tmp_path_factory): + + sim_components = { + "init_walkers": [], + "runner": MockRunner(), + "resampler": NoResampler(), + "boundary_conditions": None, + "work_mapper": SerialMapper(), + "reporters": [], + "continue_run": None, + } + + d0 = tmp_path_factory.mktemp("0") + + reporter = ProgressiveFileReporterABC( + [d0 / "a.txt"], + ["x"], + ) + + assert reporter.modes[0] == "x" + reporter.init(**sim_components) + assert reporter.modes[0] == "w" + + reporter = ProgressiveFileReporterABC( + [d0 / "a.txt"], + ["w-"], + ) + + assert reporter.modes[0] == "w-" + reporter.init(**sim_components) + assert reporter.modes[0] == "w" + + # for a file that already exists this is an error + (d0 / "a.txt").write_text("Hello") + + reporter = ProgressiveFileReporterABC( + [d0 / "a.txt"], + ["w-"], + ) + + with pytest.raises(FileExistsError): + reporter.init(**sim_components) + + # Write mode doesn't change the mode on init + d1 = tmp_path_factory.mktemp("1") + + reporter = ProgressiveFileReporterABC( + [d1 / "a.txt"], + ["w"], + ) + + assert reporter.modes[0] == "w" + reporter.init(**sim_components) + assert reporter.modes[0] == "w" From 77ff7b517490450dea2ba34ea9db00e1522ce679 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 11 Dec 2025 00:20:38 -0500 Subject: [PATCH 094/143] add tests for core of dashboard reporter --- src/wepy/reporter/base.py | 6 +- src/wepy/reporter/dashboard.py | 625 ++++++++++-------- src/wepy/reporter/file.py | 5 + .../integration/test_openmm/test_realistic.py | 44 +- tests/unit/test_reporter/test_dashboard.py | 218 ++++++ tests/unit/test_reporter/test_file.py | 14 +- 6 files changed, 640 insertions(+), 272 deletions(-) create mode 100644 tests/unit/test_reporter/test_dashboard.py diff --git a/src/wepy/reporter/base.py b/src/wepy/reporter/base.py index 7361ede9..e102e3f1 100644 --- a/src/wepy/reporter/base.py +++ b/src/wepy/reporter/base.py @@ -32,9 +32,9 @@ class CycleReportDict(TypedDict): # TODO: types for all the Anys warp_data: list[Any] bc_data: list[Any] - progress_data: dict[Any] - resampling_data: Any - resampler_data: Any + progress_data: dict[str, Any] + resampling_data: list[list[dict[str, Any]]] + resampler_data: list[dict[str, Any]] n_segment_steps: int resampled_walkers: list[Walker] runner_precycle_time: float diff --git a/src/wepy/reporter/dashboard.py b/src/wepy/reporter/dashboard.py index 2386e4ce..ce41d9cd 100644 --- a/src/wepy/reporter/dashboard.py +++ b/src/wepy/reporter/dashboard.py @@ -4,10 +4,12 @@ # Standard Library import logging +from pathlib import Path import textwrap import time from copy import copy -from datetime import datetime +import datetime +from typing import TypedDict # Third Party Library import numpy as np @@ -16,10 +18,269 @@ from tabulate import tabulate # First Party Library -from wepy.reporter.file import ProgressiveFileReporterABC +from wepy.reporter.file import ProgressiveFileReporterABC, FileMode +from wepy.reporter.base import SimComponentArgs, CycleReportDict logger = logging.getLogger(__name__) +class WalkersSummaryReport(TypedDict): + total: float + min: float + max: float + +class WorkerRecord(TypedDict): + cycle_idx: int + n_steps: int + worker_idx: int + segment_time: int + +class GenSimSectionReport(TypedDict): + + init_date_time: datetime.datetime + curr_date_time: datetime.datetime + total_run_time: int + last_cycle_idx: int + n_cycles: int + walker_cycle_summary_table: str + +class PerformanceSectionReport(TypedDict): + avg_cycle_time: int + worker_avg_segment_time: int + cycle_log: str + performance_log: str + avg_runner_time: int | None + avg_bc_time: int | None + avg_resampler_time: int | None + +class ResamplerFieldReport(TypedDict): + name: str + +class RunnerFieldReport(TypedDict): + name: str + +class BCFieldReport(TypedDict): + name: str + total_n_walker_segments: int + total_crossings: int + total_crossed_weight: float + progress_summary_table: str + warping_log: str + +class ResamplerDashboardSection: + RESAMPLER_SECTION_TEMPLATE = textwrap.dedent( + """ + Resampler: {{ name }} + """ + ) + def __init__(self, resampler=None, name=None, **kwargs): + if resampler is not None: + self.resampler_name = type(resampler).__name__ + + elif name is not None: + self.resampler_name = name + + else: + self.resampler_name = "Unknown" + + def update_values(self, **kwargs: CycleReportDict): + pass + + def gen_fields(self, **kwargs) -> ResamplerFieldReport: + fields = ResamplerFieldReport({ + "name": self.resampler_name, + }) + + return fields + + def gen_resampler_section(self, **kwargs: CycleReportDict) -> str: + section_kwargs = self.gen_fields(**kwargs) + + section_str = Template(self.RESAMPLER_SECTION_TEMPLATE).render(**section_kwargs) + + return section_str + + +class RunnerDashboardSection: + RUNNER_SECTION_TEMPLATE = textwrap.dedent( + """ + Runner: {{ name }} + """ + ) + def __init__(self, runner=None, name=None, **kwargs): + if runner is not None: + self.runner_name = type(runner).__name__ + + elif name is not None: + self.runner_name = name + + else: + self.runner_name = "Unknown" + + def update_values(self, **kwargs): + pass + + def gen_fields(self, **kwargs: CycleReportDict) -> RunnerFieldReport: + fields = {"name": self.runner_name} + + return fields + + def gen_runner_section(self, **kwargs: CycleReportDict) -> str: + section_kwargs = self.gen_fields(**kwargs) + + section_str = Template(self.RUNNER_SECTION_TEMPLATE).render(**section_kwargs) + + return section_str + + +class BCDashboardSection: + BC_SECTION_TEMPLATE = textwrap.dedent( + """ + + Boundary Condition: {{ name }} + + Total Number of Dynamics segments: {{ total_n_walker_segments }} + + Total Number of Crossings: {{ total_crossings }} + + Cumulative Boundary Crossed Weight: {{ total_crossed_weight }} + + ** Progress Log + + {{ progress_summary_table }} + + + ** Warping Log + + {{ warping_log }} + + """ + ) + WARP_RECORD_COLNAMES = ( + "cycle_idx", + "walker_idx", + "weight", + "target_idx", + "discontinuous", + ) + + def __init__(self, bc=None, discontinuities=None, name=None, **kwargs): + if bc is not None: + self.bc_name = type(bc).__name__ + + elif name is not None: + self.bc_name = name + + else: + self.bc_name = "Unknown" + + if bc is not None: + self.bc_discontinuities = copy(bc.DISCONTINUITY_TARGET_IDXS) + + else: + assert ( + discontinuities is not None + ), "If the bc is not given must give parameter: discontinuities" + self.bc_discontinuities = discontinuities + + self.warp_records = [] + self.total_n_walker_segments = 0 + self.total_crossings = 0 + self.total_crossed_weight = 0.0 + + # progress statistics + self.progress_summaries = [] + + def calc_progress_summary(self, **kwargs): + prog_data_dic = kwargs["progress_data"] + + if len(prog_data_dic) == 0: + return { + "min": np.nan, + "max": np.nan, + "mean": np.nan, + } + + else: + prog_data_key = [*prog_data_dic][0] + + prog_data = prog_data_dic[prog_data_key] + + return { + "min": np.min(prog_data), + "max": np.max(prog_data), + "mean": np.mean(prog_data), + } + + def update_values(self, **kwargs): + # keep track of exactly how many walker segments are run, this + # is useful for rate calculations via Hill's relation. + self.total_n_walker_segments += len(kwargs["new_walkers"]) + + # report on the walker progress + self.progress_summaries.append(self.calc_progress_summary(**kwargs)) + + # just create the bare warp records, since we know no more + # domain knowledge, feel free to override and add more data to + # this table + for warp_record in kwargs["warp_data"]: + # the cycle + cycle_idx = kwargs["cycle_idx"] + + # the individual values from the warp record + weight = warp_record["weight"][0] + walker_idx = warp_record["walker_idx"][0] + target_idx = warp_record["target_idx"][0] + + # determine if it was discontinuous + + # all targets are discontinuous + if self.bc_discontinuities is Ellipsis: + discont = True + # none of them are discontinuous + elif self.bc_discontinuities is None: + discont = False + # then it is a list of the discontinuous targets + else: + discont = True if target_idx in self.bc_discontinuities else False + + record = (cycle_idx, walker_idx, weight, target_idx, discont) + self.warp_records.append(record) + + self.total_crossings = len(self.warp_records) + self.total_crossed_weight = np.sum([r[2] for r in self.warp_records]) + + def gen_fields(self, **kwargs: CycleReportDict) -> BCFieldReport: + # make the table for the collected warping records + warp_table_df = pd.DataFrame( + self.warp_records, columns=self.WARP_RECORD_COLNAMES + ) + warp_table_str = tabulate( + warp_table_df, headers=warp_table_df.columns, tablefmt="orgtbl" + ) + + prog_df = pd.DataFrame(self.progress_summaries) + prog_summary_tbl_str = tabulate( + prog_df, headers=prog_df.columns, tablefmt="orgtbl" + ) + + fields = { + "name": self.bc_name, + "total_n_walker_segments": self.total_n_walker_segments, + "total_crossings": self.total_crossings, + "total_crossed_weight": self.total_crossed_weight, + "progress_summary_table": prog_summary_tbl_str, + "warping_log": warp_table_str, + } + + return fields + + def gen_bc_section(self, **kwargs: CycleReportDict) -> str: + section_kwargs = self.gen_fields(**kwargs) + + section_str = Template(self.BC_SECTION_TEMPLATE).render(**section_kwargs) + + return section_str + class DashboardReporter(ProgressiveFileReporterABC): """A text based report of the status of a wepy simulation. @@ -81,20 +342,49 @@ class DashboardReporter(ProgressiveFileReporterABC): {% if runner %}{{ runner }}{% else %}{% endif %} + * Performance + {{ performance }} """ ) - def __init__(self, resampler_dash=None, runner_dash=None, bc_dash=None, **kwargs): - """Parameters - ---------- - resampler_dash - runner_dash - bc_dash - """ - - super().__init__(**kwargs) + file_path: Path + mode: FileMode + + resampler_dash: ResamplerDashboardSection | None + runner_dash: RunnerDashboardSection | None + bc_dash: BCDashboardSection | None + + + n_cycles: int + init_date_time: datetime.datetime | None + init_sys_time: int | None + total_run_time: int | None + walker_prob_summaries: list[WalkersSummaryReport] + cycle_compute_times: list[int] + cycle_runner_times: list[int] + cycle_bc_times: list[int] + cycle_resampling_times: list[int] + worker_records: list[WorkerRecord] + worker_agg_table: pd.DataFrame | None + + avg_runner_time: int | None + avg_bc_time: int | None + avg_resampling_time: int | None + avg_cycle_time: int | None + + + + def __init__( + self, + path: Path, + resampler_dash: ResamplerDashboardSection | None = None, + runner_dash: RunnerDashboardSection | None = None, + bc_dash: BCDashboardSection | None = None, + ) -> None: + + super().__init__(file_paths=[path]) self.resampler_dash = resampler_dash self.runner_dash = runner_dash @@ -123,55 +413,57 @@ def __init__(self, resampler_dash=None, runner_dash=None, bc_dash=None, **kwargs self.cycle_bc_times = [] self.cycle_resampling_times = [] self.worker_records = [] + self.worker_agg_table = None + + self.avg_runner_time = None + self.avg_bc_time = None + self.avg_resampling_time = None + self.avg_cycle_time = None + + @property + def mode(self) -> FileMode: + return self.modes[0] + + @property + def file_path(self) -> Path: + return self.file_paths[0] + + def init(self, **kwargs: SimComponentArgs) -> None: - def init(self, **kwargs): super().init(**kwargs) - self.init_date_time = datetime.today() + logger.info(f"DashboardReporter will write to: {self.file_path}") + + self.init_date_time = datetime.datetime.today() self.total_run_time = self.init_date_time self.init_sys_time = time.time() - def calc_walker_summary(self, **kwargs): + def calc_walker_summary(self, **kwargs: CycleReportDict) -> WalkersSummaryReport: walker_weights = [walker.weight for walker in kwargs["new_walkers"]] - summary = { + summary = WalkersSummaryReport({ "total": np.sum(walker_weights), "min": np.min(walker_weights), "max": np.max(walker_weights), - } + }) return summary - def update_values(self, **kwargs): - ### simulation - - self.n_cycles += 1 - self.walker_prob_summaries.append(self.calc_walker_summary(**kwargs)) - - self.update_performance_values(**kwargs) - - # update all the sections values - if self.resampler_dash is not None: - self.resampler_dash.update_values(**kwargs) - if self.runner_dash is not None: - self.runner_dash.update_values(**kwargs) - if self.bc_dash is not None: - self.bc_dash.update_values(**kwargs) - def update_performance_values(self, **kwargs): + def update_performance_values(self, **kwargs: CycleReportDict) -> None: ## worker specific performance # only do this part if there were any workers - if len(kwargs["worker_segment_times"]) > 0: + if kwargs["worker_segment_times"] is not None and len(kwargs["worker_segment_times"]) > 0: # log of segment times for workers for worker_idx, segment_times in kwargs["worker_segment_times"].items(): for segment_time in segment_times: - record = ( - kwargs["cycle_idx"], - kwargs["n_segment_steps"], - worker_idx, - segment_time, + record = WorkerRecord( + cycle_idx=kwargs["cycle_idx"], + n_steps=kwargs["n_segment_steps"], + worker_idx=worker_idx, + segment_time=segment_time, ) self.worker_records.append(record) @@ -219,14 +511,31 @@ def update_performance_values(self, **kwargs): # average cycle time self.avg_cycle_time = np.mean(self.cycle_compute_times) - def write_dashboard(self, report_str): + def update_values(self, **kwargs: CycleReportDict) -> None: + ### simulation + + self.n_cycles += 1 + self.walker_prob_summaries.append( + self.calc_walker_summary(**kwargs) + ) + + self.update_performance_values(**kwargs) + + # update all the sections values + if self.resampler_dash is not None: + self.resampler_dash.update_values(**kwargs) + if self.runner_dash is not None: + self.runner_dash.update_values(**kwargs) + if self.bc_dash is not None: + self.bc_dash.update_values(**kwargs) + + def write_dashboard(self, report_str: str) -> None: """Write the dashboard to the file.""" with open(self.file_path, mode=self.mode) as dashboard_file: dashboard_file.write(report_str) - def gen_sim_section(self, **kwargs): - """""" + def gen_sim_section(self, **kwargs: CycleReportDict) -> str: walker_df = pd.DataFrame(self.walker_prob_summaries) walker_summary_tbl_str = tabulate( @@ -234,14 +543,14 @@ def gen_sim_section(self, **kwargs): ) # render the simulation section - sim_section_d = { + sim_section_d = GenSimSectionReport({ "init_date_time": self.init_date_time, - "curr_date_time": datetime.today().isoformat(), + "curr_date_time": datetime.datetime.today().isoformat(), "total_run_time": time.time() - self.init_sys_time, "last_cycle_idx": kwargs["cycle_idx"], "n_cycles": self.n_cycles, "walker_cycle_summary_table": walker_summary_tbl_str, - } + }) sim_section_str = Template(self.SIMULATION_SECTION_TEMPLATE).render( **sim_section_d @@ -249,7 +558,7 @@ def gen_sim_section(self, **kwargs): return sim_section_str - def gen_performance_section(self, **kwargs): + def gen_performance_section(self, **kwargs: CycleReportDict) -> str: # log of cycle times cycle_table_colnames = ( "cycle_time (s)", @@ -260,7 +569,7 @@ def gen_performance_section(self, **kwargs): cycle_table_df = pd.DataFrame( { - "cycle_times (s)": self.cycle_compute_times, + "cycle_time (s)": self.cycle_compute_times, "runner_time (s)": self.cycle_runner_times, "boundary_conditions_time (s)": self.cycle_bc_times, "resampling_time (s)": self.cycle_resampling_times, @@ -269,7 +578,7 @@ def gen_performance_section(self, **kwargs): ) cycle_table_str = tabulate( - cycle_table_df, headers=cycle_table_df.columns, tablefmt="orgtbl" + cycle_table_df, headers=cycle_table_df.columns, tablefmt="orgtbl", ) # log of workers performance @@ -277,10 +586,11 @@ def gen_performance_section(self, **kwargs): "cycle_idx", "n_steps", "worker_idx", - "segment_time (s)", + "segment_time", ) worker_table_df = pd.DataFrame( - self.worker_records, columns=worker_table_colnames + self.worker_records, + columns=worker_table_colnames, ) worker_table_str = tabulate( worker_table_df, @@ -296,7 +606,7 @@ def gen_performance_section(self, **kwargs): tablefmt="orgtbl", ) - performance_section_d = { + performance_section_d = PerformanceSectionReport({ "avg_cycle_time": self.avg_cycle_time, "worker_avg_segment_time": worker_agg_table_str, "cycle_log": cycle_table_str, @@ -305,7 +615,7 @@ def gen_performance_section(self, **kwargs): "avg_runner_time": self.avg_runner_time, "avg_bc_time": self.avg_bc_time, "avg_resampling_time": self.avg_resampling_time, - } + }) performance_section_str = Template(self.PERFORMANCE_SECTION_TEMPLATE).render( **performance_section_d @@ -313,9 +623,10 @@ def gen_performance_section(self, **kwargs): return performance_section_str - def report(self, **kwargs): + def report(self, **kwargs: CycleReportDict) -> None: # update the values that update each call to report + logger.debug("Updating values") self.update_values(**kwargs) # the two sections that are always there @@ -352,215 +663,7 @@ def report(self, **kwargs): ) # write the thing + logger.info(f"Writing dashboard at: {self.file_path}") self.write_dashboard(report_str) -class ResamplerDashboardSection: - RESAMPLER_SECTION_TEMPLATE = """ -Resampler: {{ name }} -""" - - def __init__(self, resampler=None, name=None, **kwargs): - if resampler is not None: - self.resampler_name = type(resampler).__name__ - - elif name is not None: - self.resampler_name = name - - else: - self.resampler_name = "Unknown" - - def update_values(self, **kwargs): - pass - - def gen_fields(self, **kwargs): - fields = {"name": self.resampler_name} - - return fields - - def gen_resampler_section(self, **kwargs): - section_kwargs = self.gen_fields(**kwargs) - - section_str = Template(self.RESAMPLER_SECTION_TEMPLATE).render(**section_kwargs) - - return section_str - - -class RunnerDashboardSection: - RUNNER_SECTION_TEMPLATE = """ -Runner: {{ name }} -""" - - def __init__(self, runner=None, name=None, **kwargs): - if runner is not None: - self.runner_name = type(runner).__name__ - - elif name is not None: - self.runner_name = name - - else: - self.runner_name = "Unknown" - - def update_values(self, **kwargs): - pass - - def gen_fields(self, **kwargs): - fields = {"name": self.runner_name} - - return fields - - def gen_runner_section(self, **kwargs): - section_kwargs = self.gen_fields(**kwargs) - - section_str = Template(self.RUNNER_SECTION_TEMPLATE).render(**section_kwargs) - - return section_str - - -class BCDashboardSection: - BC_SECTION_TEMPLATE = """ - -Boundary Condition: {{ name }} - -Total Number of Dynamics segments: {{ total_n_walker_segments }} - -Total Number of Crossings: {{ total_crossings }} - -Cumulative Boundary Crossed Weight: {{ total_crossed_weight }} - -** Progress Log - -{{ progress_summary_table }} - - -** Warping Log - -{{ warping_log }} - -""" - - WARP_RECORD_COLNAMES = ( - "cycle_idx", - "walker_idx", - "weight", - "target_idx", - "discontinuous", - ) - - def __init__(self, bc=None, discontinuities=None, name=None, **kwargs): - if bc is not None: - self.bc_name = type(bc).__name__ - - elif name is not None: - self.bc_name = name - - else: - self.bc_name = "Unknown" - - if bc is not None: - self.bc_discontinuities = copy(bc.DISCONTINUITY_TARGET_IDXS) - - else: - assert ( - discontinuities is not None - ), "If the bc is not given must give parameter: discontinuities" - self.bc_discontinuities = discontinuities - - self.warp_records = [] - self.total_n_walker_segments = 0 - self.total_crossings = 0 - self.total_crossed_weight = 0.0 - - # progress statistics - self.progress_summaries = [] - - def calc_progress_summary(self, **kwargs): - prog_data_dic = kwargs["progress_data"] - - if len(prog_data_dic) == 0: - return { - "min": np.nan, - "max": np.nan, - "mean": np.nan, - } - - else: - prog_data_key = [*prog_data_dic][0] - - prog_data = prog_data_dic[prog_data_key] - - return { - "min": np.min(prog_data), - "max": np.max(prog_data), - "mean": np.mean(prog_data), - } - - def update_values(self, **kwargs): - # keep track of exactly how many walker segments are run, this - # is useful for rate calculations via Hill's relation. - self.total_n_walker_segments += len(kwargs["new_walkers"]) - - # report on the walker progress - self.progress_summaries.append(self.calc_progress_summary(**kwargs)) - - # just create the bare warp records, since we know no more - # domain knowledge, feel free to override and add more data to - # this table - for warp_record in kwargs["warp_data"]: - # the cycle - cycle_idx = kwargs["cycle_idx"] - - # the individual values from the warp record - weight = warp_record["weight"][0] - walker_idx = warp_record["walker_idx"][0] - target_idx = warp_record["target_idx"][0] - - # determine if it was discontinuous - - # all targets are discontinuous - if self.bc_discontinuities is Ellipsis: - discont = True - # none of them are discontinuous - elif self.bc_discontinuities is None: - discont = False - # then it is a list of the discontinuous targets - else: - discont = True if target_idx in self.bc_discontinuities else False - - record = (cycle_idx, walker_idx, weight, target_idx, discont) - self.warp_records.append(record) - - self.total_crossings = len(self.warp_records) - self.total_crossed_weight = np.sum([r[2] for r in self.warp_records]) - - def gen_fields(self, **kwargs): - # make the table for the collected warping records - warp_table_df = pd.DataFrame( - self.warp_records, columns=self.WARP_RECORD_COLNAMES - ) - warp_table_str = tabulate( - warp_table_df, headers=warp_table_df.columns, tablefmt="orgtbl" - ) - - prog_df = pd.DataFrame(self.progress_summaries) - prog_summary_tbl_str = tabulate( - prog_df, headers=prog_df.columns, tablefmt="orgtbl" - ) - - fields = { - "name": self.bc_name, - "total_n_walker_segments": self.total_n_walker_segments, - "total_crossings": self.total_crossings, - "total_crossed_weight": self.total_crossed_weight, - "progress_summary_table": prog_summary_tbl_str, - "warping_log": warp_table_str, - } - - return fields - - def gen_bc_section(self, **kwargs): - section_kwargs = self.gen_fields(**kwargs) - - section_str = Template(self.BC_SECTION_TEMPLATE).render(**section_kwargs) - - return section_str diff --git a/src/wepy/reporter/file.py b/src/wepy/reporter/file.py index e1817ac8..b2072a77 100644 --- a/src/wepy/reporter/file.py +++ b/src/wepy/reporter/file.py @@ -235,6 +235,7 @@ def init(self, **kwargs: SimComponentArgs) -> None: # the file already exists (say from another run), and warn the # user. However, once the file has been created for this run # we need to overwrite it many times forcefully. + logger.info("Initializing ProgressiveFileReporter") # go thourgh each file managed by this reporter for file_idx, mode in enumerate(self.modes): @@ -248,3 +249,7 @@ def init(self, **kwargs: SimComponentArgs) -> None: # now that we have checked if the file exists we set it into # overwrite mode self.set_mode(file_idx, "w") + + def cleanup(self, **kwargs: SimComponentArgs) -> None: + logger.info("Nothing to do.") + pass diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index efffad45..4c6ede86 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -7,6 +7,7 @@ """ # Standard Library +import logging import copy # Third Party Library @@ -23,6 +24,7 @@ _DEFAULT_STATE_TIME_INTERVAL, ) from wepy.sim_manager import Manager +from wepy.reporter.dashboard import DashboardReporter # TODO: use the high-level API imports from wepy.walker import Walker @@ -33,6 +35,8 @@ ) from wepy_tools.systems.lennard_jones import LennardJonesPair, PairDistance +_LOGGER = logging.getLogger("tests") + STEP_SIZE = 2.0 * openmm.unit.femtosecond TEMPERATURE = 300.0 * openmm.unit.kelvin @@ -49,7 +53,9 @@ DEFAULT_CYCLE_STEPS = round(DEFAULT_CYCLE_TIME / STEP_SIZE) -def test_lennard_jones_revo_procpool(): +def test_lennard_jones_revo_procpool(tmp_path_factory): + + outputs_dir = tmp_path_factory.mktemp("outputs") test_sys = LennardJonesPair() @@ -61,7 +67,8 @@ def test_lennard_jones_revo_procpool(): integrator=integrator, ) - num_walkers = 48 + # num_walkers = 48 + num_walkers = 4 init_state = OpenMMState.from_dwim( positions=test_sys.positions, @@ -98,7 +105,8 @@ def test_lennard_jones_revo_procpool(): distance_metric=distance_metric, ) - dashboard_reporter = DashboardReporter() + dashboard_path = outputs_dir / "main.wepy_dash.org" + dashboard_reporter = DashboardReporter(dashboard_path) reporters = [dashboard_reporter] @@ -121,14 +129,24 @@ def test_lennard_jones_revo_procpool(): new_walkers, sim_components = sim_manager.run_simulation( n_cycles=2, - segment_lengths=DEFAULT_CYCLE_STEPS, + segment_lengths=10, ) + + # new_walkers, sim_components = sim_manager.run_simulation( + # n_cycles=2, + # segment_lengths=DEFAULT_CYCLE_STEPS, + # ) + + assert dashboard_path.exists() + _LOGGER.info("\n" + dashboard_path.read_text()) # disable timeout for this one @pytest.mark.timeout(timeout=0) -def test_alanine_dipeptide_revo_procpool(): +def test_alanine_dipeptide_revo_procpool(tmp_path_factory): + outputs_dir = tmp_path_factory.mktemp("outputs") + ala_sys = AlanineDipeptideExplicitSystem() integrator = openmm.LangevinIntegrator(TEMPERATURE, 0.1, STEP_SIZE) @@ -179,6 +197,11 @@ def test_alanine_dipeptide_revo_procpool(): distance_metric=distance_metric, ) + dashboard_path = outputs_dir / "main.wepy_dash.org" + dashboard_reporter = DashboardReporter(dashboard_path) + reporters = [dashboard_reporter] + + sim_manager = Manager( init_walkers=init_walkers, runner_factory=runner_factory, @@ -196,6 +219,7 @@ def test_alanine_dipeptide_revo_procpool(): # utilization goes way up global_platform_properties={"Threads": str(cores_per_worker)}, ), + reporters=reporters, ) # new_walkers, sim_components = sim_manager.run_simulation( @@ -204,7 +228,15 @@ def test_alanine_dipeptide_revo_procpool(): # ) # short number of steps but many cycles to exercise the pools + # new_walkers, sim_components = sim_manager.run_simulation( + # n_cycles=100, + # segment_lengths=10, + # ) + new_walkers, sim_components = sim_manager.run_simulation( - n_cycles=100, + n_cycles=3, segment_lengths=10, ) + + assert dashboard_path.exists() + _LOGGER.info("\n" + dashboard_path.read_text()) diff --git a/tests/unit/test_reporter/test_dashboard.py b/tests/unit/test_reporter/test_dashboard.py new file mode 100644 index 00000000..d56120fb --- /dev/null +++ b/tests/unit/test_reporter/test_dashboard.py @@ -0,0 +1,218 @@ +import logging +from wepy.walker import Walker +from wepy.runners.mock import MockRunner, MockState +from wepy.resampling.resamplers.noresampler import NoResampler +from wepy.work_mapper.serial import SerialMapper + +from wepy.reporter.dashboard import ( + WalkersSummaryReport, + WorkerRecord, + GenSimSectionReport, + PerformanceSectionReport, + ResamplerFieldReport, + ResamplerDashboardSection, + RunnerDashboardSection, + BCDashboardSection, + DashboardReporter, +) + +class Test_ResamplerDashboardReporter: + pass + +_LOGGER = logging.getLogger("tests") + +SIM_COMPONENTS = { + "init_walkers": [ + Walker( + MockState(1), + weight=0.2, + ), + Walker( + MockState(1), + weight=0.1, + ), + ], + "runner": MockRunner(), + "resampler": NoResampler(), + "boundary_conditions": None, + "work_mapper": SerialMapper(), + "reporters": [], + "continue_run": None, +} + +CYCLE_REPORT_DICT = { + "cycle_idx" : 1, + "new_walkers" : [ + Walker( + MockState(1), + weight=0.2, + ), + Walker( + MockState(1), + weight=0.1, + ), + ], + "warp_data" : [], + "bc_data" : [], + "progress_data" : {}, + "resampling_data" : [ + [ + { + "decision_id" : 1, + "target_idxs" : (0,) + }, + { + "decision_id" : 2, + "target_idxs" : (1, 2) + }, + ] + ], + "resampler_data" : [{}], + "n_segment_steps": 100, + "resampled_walkers" : [ + Walker( + MockState(1), + weight=0.1, + ), + Walker( + MockState(1), + weight=0.1, + ), + Walker( + MockState(1), + weight=0.1, + ), + ], + "runner_precycle_time" : 0.12312, + "runner_postcycle_time" : 0.234234, + "sim_manager_segment_overhead_time" : 0.89346, + "runner_splits_time" : { + "a" : .234235, + "b" : .46, + }, + "worker_segment_times" : { + 0 : [10.213], + 1 : [22.34], + }, + "cycle_sim_manager_segment_time" : 40.234, + "cycle_runner_time" : 33.45, + "cycle_bc_time" : 1.2, + "cycle_resampling_time" : 6.234, +} + + +class Test_DashboardReporter: + + def test___init__(self, tmp_path_factory): + + d0 = tmp_path_factory.mktemp("0") + dash_path = d0 / "main.wepy_dash.org" + reporter = DashboardReporter(dash_path) + + assert reporter.file_paths == [dash_path] + assert reporter.modes == ["x"] + + def test_init(self, tmp_path_factory): + + + d0 = tmp_path_factory.mktemp("0") + dash_path = d0 / "main.wepy_dash.org" + reporter = DashboardReporter(dash_path) + + assert reporter.file_paths == [dash_path] + assert reporter.modes == ["x"] + assert reporter.file_path == dash_path + assert reporter.mode == "x" + + reporter.init(**SIM_COMPONENTS) + assert reporter.modes == ["w"] + assert reporter.mode == "w" + + assert reporter.init_date_time is not None + assert reporter.total_run_time is not None + assert reporter.init_sys_time is not None + + def test_calc_walker_summary(self, tmp_path_factory): + + d0 = tmp_path_factory.mktemp("0") + dash_path = d0 / "main.wepy_dash.org" + reporter = DashboardReporter(dash_path) + + reporter.calc_walker_summary(**CYCLE_REPORT_DICT) + + def test_update_performance_values(self, tmp_path_factory): + + d0 = tmp_path_factory.mktemp("0") + dash_path = d0 / "main.wepy_dash.org" + reporter = DashboardReporter(dash_path) + + reporter.init(**SIM_COMPONENTS) + + reporter.update_performance_values(**CYCLE_REPORT_DICT) + + def test_update_values(self, tmp_path_factory): + + d0 = tmp_path_factory.mktemp("0") + dash_path = d0 / "main.wepy_dash.org" + reporter = DashboardReporter(dash_path) + + reporter.init(**SIM_COMPONENTS) + + reporter.update_values(**CYCLE_REPORT_DICT) + + def test_write_dashboard(self, tmp_path_factory): + + d0 = tmp_path_factory.mktemp("0") + dash_path = d0 / "main.wepy_dash.org" + reporter = DashboardReporter(dash_path) + + reporter.init(**SIM_COMPONENTS) + + reporter.write_dashboard("Hello, this is a fake dashboard") + assert dash_path.exists() + _LOGGER.info("\n" + dash_path.read_text()) + + reporter.write_dashboard("Hello, this is a fake dashboard") + assert dash_path.exists() + assert len(dash_path.read_text().strip().split("\n")) == 1 + _LOGGER.info("\n" + dash_path.read_text()) + + def test_gen_sim_section(self, tmp_path_factory): + + d0 = tmp_path_factory.mktemp("0") + dash_path = d0 / "main.wepy_dash.org" + reporter = DashboardReporter(dash_path) + + reporter.init(**SIM_COMPONENTS) + + reporter.update_values(**CYCLE_REPORT_DICT) + section = reporter.gen_sim_section(**CYCLE_REPORT_DICT) + + _LOGGER.info("\n" + section) + + def test_gen_performance_section(self, tmp_path_factory): + + d0 = tmp_path_factory.mktemp("0") + dash_path = d0 / "main.wepy_dash.org" + reporter = DashboardReporter(dash_path) + + reporter.init(**SIM_COMPONENTS) + + reporter.update_values(**CYCLE_REPORT_DICT) + + section = reporter.gen_performance_section(**CYCLE_REPORT_DICT) + + _LOGGER.info("\n" + section) + + def test_report(self, tmp_path_factory): + + d0 = tmp_path_factory.mktemp("0") + dash_path = d0 / "main.wepy_dash.org" + reporter = DashboardReporter(dash_path) + + reporter.init(**SIM_COMPONENTS) + reporter.report(**CYCLE_REPORT_DICT) + + assert dash_path.exists() + + _LOGGER.info("\n" + dash_path.read_text()) diff --git a/tests/unit/test_reporter/test_file.py b/tests/unit/test_reporter/test_file.py index 52655d5c..3e6788fc 100644 --- a/tests/unit/test_reporter/test_file.py +++ b/tests/unit/test_reporter/test_file.py @@ -11,7 +11,8 @@ ProgressiveFileReporterABC, ) from wepy.resampling.resamplers.noresampler import NoResampler -from wepy.runners.mock import MockRunner +from wepy.runners.mock import MockRunner, MockState +from wepy.walker import Walker from wepy.work_mapper.serial import SerialMapper @@ -59,7 +60,16 @@ class Test_ProgressiveFileReporterABC: def test_init(self, tmp_path_factory): sim_components = { - "init_walkers": [], + "init_walkers": [ + Walker( + MockState(1), + weight=0.1, + ), + Walker( + MockState(1), + weight=0.1, + ), + ], "runner": MockRunner(), "resampler": NoResampler(), "boundary_conditions": None, From f65807ccd403b2e57df01ef2a6a4200cc2055e66 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 11 Dec 2025 00:20:52 -0500 Subject: [PATCH 095/143] reduce logging in the REVO variance optimization loop --- src/wepy/resampling/resamplers/revo.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 4d7f6ae7..3710ab8d 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -522,7 +522,10 @@ def decide( _count = 1 productive = True while productive: - logger.info(f"Optimization iteration: {_count}") + _log = False + if _count == 1 or _count % 10 == 0: + _log = True + logger.info(f"Optimization iteration: {_count}") _count += 1 productive = False # find min and max walker_variationss, alter new_amp @@ -633,7 +636,8 @@ def decide( if new_variation > variation: variations.append(new_variation) - logger.info("Variance move to {} accepted".format(new_variation)) + if _log: + logger.info(f"Variance move to {new_variation} accepted") productive = True variation = new_variation @@ -684,7 +688,8 @@ def decide( ) variations.append(new_variation) - logger.info("variance after selection: {}".format(new_variation)) + if _log: + logger.info("variance after selection: {}".format(new_variation)) # if not productive else: From a8616ca90080bab47bb03dddc6d4382bf5caa15e Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 11 Dec 2025 00:28:26 -0500 Subject: [PATCH 096/143] fix ugly dashboard output --- src/wepy/reporter/dashboard.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/wepy/reporter/dashboard.py b/src/wepy/reporter/dashboard.py index ce41d9cd..b42574f1 100644 --- a/src/wepy/reporter/dashboard.py +++ b/src/wepy/reporter/dashboard.py @@ -328,21 +328,27 @@ class DashboardReporter(ProgressiveFileReporterABC): """ ) DASHBOARD_TEMPLATE = textwrap.dedent( - """* Simulation + """ + * Simulation {{ simulation }} - - - {% if resampler %}* Resampler{% else %}{% endif %} - {% if resampler %}{{ resampler }}{% else %}{% endif %} - - {% if boundary_condition %}* Boundary Condition{% else %}{% endif %} - {% if boundary_condition %}{{ boundary_condition }}{% else %}{% endif %} - - {% if runner %}* Runner{% else %}{% endif %} - - {% if runner %}{{ runner }}{% else %}{% endif %} - - + {% if resampler -%} + + * Resampler + {{ resampler }} + + {%- endif %} + {% if boundary_condition -%} + + * Boundary Condition + {{ boundary_condition }} + + {%- endif %} + {% if runner -%} + + * Runner + {{ runner }} + + {%- endif %} * Performance {{ performance }} From 1eb4ead48a9f3e3c76b1fd5d159822c1b4c74cff Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 11 Dec 2025 13:11:50 -0500 Subject: [PATCH 097/143] refacotr a few things with the decision Make the `Decision` an abstract base class. Move around some type definitions for simplicity. Make the resampler metadata classmethods rather than object. --- src/wepy/analysis/contig_tree.py | 6 +- src/wepy/reporter/base.py | 6 +- src/wepy/reporter/types.py | 11 ++ src/wepy/resampling/decisions/clone_merge.py | 12 +- src/wepy/resampling/decisions/decision.py | 2 +- src/wepy/resampling/decisions/no_decision.py | 12 +- src/wepy/resampling/resamplers/clone_merge.py | 16 +-- src/wepy/resampling/resamplers/noresampler.py | 2 +- src/wepy/resampling/resamplers/resampler.py | 117 ++++++++---------- src/wepy/resampling/resamplers/wexplore.py | 2 +- src/wepy/typing.py | 23 ++++ .../test_decisions/test_decision.py | 4 +- 12 files changed, 121 insertions(+), 92 deletions(-) create mode 100644 src/wepy/reporter/types.py create mode 100644 src/wepy/typing.py diff --git a/src/wepy/analysis/contig_tree.py b/src/wepy/analysis/contig_tree.py index 0dceb30f..8a622549 100644 --- a/src/wepy/analysis/contig_tree.py +++ b/src/wepy/analysis/contig_tree.py @@ -42,7 +42,7 @@ ) from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.hdf5 import WepyHDF5 -from wepy.resampling.decisions.decision import Decision +from wepy.resampling.decisions.decision import BaseDecisionABC # the groups of run records RESAMPLING: Final = "resampling" @@ -77,7 +77,7 @@ def __init__( continuations: type(Ellipsis) | list[tuple[int, int]] = Ellipsis, runs: type(Ellipsis) | list[int] = Ellipsis, boundary_condition_class: type[BoundaryConditions] | None = None, - decision_class: type[Decision] | None = None, + decision_class: type[BaseDecisionABC] | None = None, ): """The only required argument is an WepyHDF5 object from which to draw data. @@ -222,7 +222,7 @@ def graph(self) -> nx.DiGraph: return self._graph @property - def decision_class(self) -> type[Decision] | None: + def decision_class(self) -> type[BaseDecisionABC] | None: """The decision class used to determine parental lineages.""" return self._decision_class diff --git a/src/wepy/reporter/base.py b/src/wepy/reporter/base.py index e102e3f1..6c49e8ca 100644 --- a/src/wepy/reporter/base.py +++ b/src/wepy/reporter/base.py @@ -1,7 +1,8 @@ # Standard Library import logging -from typing import Any, Protocol, TypedDict +from typing import Any, Protocol, TypedDict, Literal, Union +import numpy as np # First Party Library from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.resampling.resamplers.resampler import Resampler @@ -9,6 +10,8 @@ from wepy.walker import Walker from wepy.work_mapper.base import WorkMapper +from .types import FieldShapeSpec, FieldDtype + logger = logging.getLogger(__name__) @@ -47,7 +50,6 @@ class CycleReportDict(TypedDict): cycle_bc_time: float cycle_resampling_time: float - class Reporter(Protocol): """Abstract base class for wepy reporters. diff --git a/src/wepy/reporter/types.py b/src/wepy/reporter/types.py new file mode 100644 index 00000000..3737b6f5 --- /dev/null +++ b/src/wepy/reporter/types.py @@ -0,0 +1,11 @@ +from typing import Union, Literal + +import numpy as np + +FieldShapeSpec = Union[ + tuple[int, ...], + Literal[Ellipsis], + None, +] + +FieldDtype = np.dtype | None diff --git a/src/wepy/resampling/decisions/clone_merge.py b/src/wepy/resampling/decisions/clone_merge.py index d0aeee91..bbc5ddbe 100644 --- a/src/wepy/resampling/decisions/clone_merge.py +++ b/src/wepy/resampling/decisions/clone_merge.py @@ -7,7 +7,7 @@ import attrs # First Party Library -from wepy.resampling.decisions.decision import Decision, DecisionRecord +from wepy.resampling.decisions.decision import BaseDecisionABC, DecisionRecord from wepy.walker import Walker, keep_merge, split logger = logging.getLogger(__name__) @@ -46,7 +46,7 @@ class CloneMergeDecisionRecord(DecisionRecord): target_idxs: list[int] -class MultiCloneMergeDecision(Decision): +class MultiCloneMergeDecision(BaseDecisionABC): """Decision encoding cloning and merging decisions for weighted ensemble. The decision records have in addition to the 'decision_id' a field @@ -73,11 +73,11 @@ class MultiCloneMergeDecision(Decision): DECISION_RECORD = CloneMergeDecisionRecord - FIELDS = Decision.FIELDS + ("target_idxs",) - SHAPES = Decision.SHAPES + (Ellipsis,) - DTYPES = Decision.DTYPES + (int,) + FIELDS = BaseDecisionABC.FIELDS + ("target_idxs",) + SHAPES = BaseDecisionABC.SHAPES + (Ellipsis,) + DTYPES = BaseDecisionABC.DTYPES + (int,) - RECORD_FIELDS = Decision.RECORD_FIELDS + ("target_idxs",) + RECORD_FIELDS = BaseDecisionABC.RECORD_FIELDS + ("target_idxs",) # the decision types that pass on their state ANCESTOR_DECISION_IDS = ( diff --git a/src/wepy/resampling/decisions/decision.py b/src/wepy/resampling/decisions/decision.py index d9a45095..f1030faa 100644 --- a/src/wepy/resampling/decisions/decision.py +++ b/src/wepy/resampling/decisions/decision.py @@ -68,7 +68,7 @@ class DecisionRecord: # ABC for the Decision class -class Decision: +class BaseDecisionABC: """Represents and provides methods for a set of decision values.""" ENUM: IntEnum diff --git a/src/wepy/resampling/decisions/no_decision.py b/src/wepy/resampling/decisions/no_decision.py index 42affca0..ab491531 100644 --- a/src/wepy/resampling/decisions/no_decision.py +++ b/src/wepy/resampling/decisions/no_decision.py @@ -5,7 +5,7 @@ import attrs # First Party Library -from wepy.resampling.decisions.decision import Decision, DecisionRecord +from wepy.resampling.decisions.decision import BaseDecisionABC, DecisionRecord from wepy.walker import Walker @@ -22,17 +22,17 @@ class NoDecisionRecord(DecisionRecord): target_idx: int -class NoDecision(Decision): +class NoDecision(BaseDecisionABC): """Decision for a resampling process that does no resampling.""" ENUM = NothingDecisionEnum DEFAULT_DECISION = ENUM.NOTHING - FIELDS = Decision.FIELDS + ("target_idxs",) - SHAPES = Decision.SHAPES + (Ellipsis,) - DTYPES = Decision.DTYPES + (int,) + FIELDS = BaseDecisionABC.FIELDS + ("target_idxs",) + SHAPES = BaseDecisionABC.SHAPES + (Ellipsis,) + DTYPES = BaseDecisionABC.DTYPES + (int,) - RECORD_FIELDS = Decision.RECORD_FIELDS + ("target_idxs",) + RECORD_FIELDS = BaseDecisionABC.RECORD_FIELDS + ("target_idxs",) ANCESTOR_DECISION_IDS = (ENUM.NOTHING.value,) diff --git a/src/wepy/resampling/resamplers/clone_merge.py b/src/wepy/resampling/resamplers/clone_merge.py index 9eb04c98..ec5e8255 100644 --- a/src/wepy/resampling/resamplers/clone_merge.py +++ b/src/wepy/resampling/resamplers/clone_merge.py @@ -100,8 +100,8 @@ def _init_walker_actions(self, n_walkers: int) -> list[CloneMergeDecisionRecord] # determine resampling actions walker_actions = [ - self.decision.record( - enum_value=self.decision.default_decision().value, target_idxs=(i,) + self.decision().record( + enum_value=self.decision().default_decision().value, target_idxs=(i,) ) for i in range(n_walkers) ] @@ -213,13 +213,13 @@ def assign_clones( # for each squashed walker write a record and save it # in the walker actions for squash_idx in merge_group: - walker_actions[squash_idx] = self.decision.record( - self.decision.ENUM.SQUASH.value, target_idxs=(walker_idx,) + walker_actions[squash_idx] = self.decision().record( + self.decision().ENUM.SQUASH.value, target_idxs=(walker_idx,) ) # make the record for the keep merge walker - walker_actions[walker_idx] = self.decision.record( - self.decision.ENUM.KEEP_MERGE.value, target_idxs=(walker_idx,) + walker_actions[walker_idx] = self.decision().record( + self.decision().ENUM.KEEP_MERGE.value, target_idxs=(walker_idx,) ) # for each walker, if it is to be cloned assign open slots for it @@ -264,8 +264,8 @@ def assign_clones( clone_targets.extend(new_slots) # make a record for this clone - walker_actions[walker_idx] = self.decision.record( - self.decision.ENUM.CLONE.value, target_idxs=tuple(clone_targets) + walker_actions[walker_idx] = self.decision().record( + self.decision().ENUM.CLONE.value, target_idxs=tuple(clone_targets) ) return walker_actions diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py index 4f4516f1..e1b402bd 100644 --- a/src/wepy/resampling/resamplers/noresampler.py +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -19,7 +19,7 @@ class NoResamplerResamplerData(TypedDict): pass -class NoResampler(Resampler): +class NoResampler(ResamplerABC): """The resampler which does nothing.""" DECISION = NoDecision diff --git a/src/wepy/resampling/resamplers/resampler.py b/src/wepy/resampling/resamplers/resampler.py index ecb997cb..1a018577 100644 --- a/src/wepy/resampling/resamplers/resampler.py +++ b/src/wepy/resampling/resamplers/resampler.py @@ -7,8 +7,9 @@ import numpy as np # First Party Library -from wepy.resampling.decisions.decision import Decision +from wepy.resampling.decisions.decision import BaseDecisionABC from wepy.walker import Walker, WalkerState +from wepy.reporter.types import FieldShapeSpec, FieldDtype logger = logging.getLogger(__name__) @@ -25,51 +26,33 @@ class ResamplerError(Exception): class Resampler(Protocol, Generic[WalkerState_]): - DECISION: Decision + DECISION: BaseDecisionABC CYCLE_FIELDS: tuple[str, ...] CYCLE_SHAPES: tuple[tuple[int, ...], ...] CYCLE_DTYPES: tuple[int | float, ...] CYCLE_RECORD_FIELDS: None | tuple[str, ...] RESAMPLING_FIELDS: tuple[str, ...] - RESAMPLING_SHAPES: tuple[ - Union[ - tuple[int, ...], - Literal[Ellipsis], - None, - ], - ..., - ] - RESAMPLING_DTYPES: tuple[Union[np.dtype, None], ...] + RESAMPLING_SHAPES: tuple[FieldShapeSpec, ...] + RESAMPLING_DTYPES: tuple[FieldDtype, ...] RESAMPLING_RECORD_FIELDS: None | tuple[str, ...] RESAMPLER_FIELDS: tuple[str, ...] - RESAMPLER_SHAPES: tuple[ - Union[ - tuple[int, ...], - Literal[Ellipsis], - None, - ], - ..., - ] - - RESAMPLER_DTYPES: tuple[Union[np.dtype, None], ...] + RESAMPLER_SHAPES: tuple[FieldShapeSpec, ...] + + RESAMPLER_DTYPES: tuple[FieldDtype, ...] RESAMPLER_RECORD_FIELDS: None | tuple[str, ...] - def resampling_fields(self) -> tuple[ + @classmethod + def resampling_fields(cls) -> tuple[ tuple[str, ...], - tuple[ - Union[ - tuple[int, ...], - Literal[Ellipsis], - None, - ], - ..., - ], - tuple[Union[np.dtype, None], ...], + tuple[FieldShapeSpec, ...], + tuple[FieldDtype, ...], ]: ... - def resampling_record_field_names(self) -> None | tuple[str, ...]: ... + @classmethod + def resampling_record_field_names(cls) -> None | tuple[str, ...]: ... - def resampler_record_field_names(self) -> None | tuple[str, ...]: ... + @classmethod + def resampler_record_field_names(cls) -> None | tuple[str, ...]: ... def resample(self, walkers: list[Walker[WalkerState_]]) -> tuple[ list[Walker[WalkerState_]], @@ -100,7 +83,7 @@ class ResamplerABC(Resampler): - RESAMPLER_RECORD_FIELDS The DECISION constant should be a - wepy.resampling.decisions.decision.Decision subclass. + wepy.resampling.decisions.decision.BaseDecisionABC subclass. This base class provides some hidden methods that are useful for various purposes. @@ -140,7 +123,7 @@ class ResamplerABC(Resampler): """ - DECISION: Decision = Decision + DECISION: BaseDecisionABC = BaseDecisionABC """The decision class for this resampler.""" CYCLE_FIELDS: tuple[str, ...] = ( @@ -406,24 +389,28 @@ def __init__( # set them to the args given self.set_debug_mode(debug_mode) - @property - def decision(self) -> Decision: + @classmethod + def decision(cls) -> BaseDecisionABC: """The decision class for this resampler.""" - return self.DECISION + return cls.DECISION - def resampling_field_names(self): + @classmethod + def resampling_field_names(cls): """Access the class level FIELDS constant for this record group.""" - return self.RESAMPLING_FIELDS + return cls.RESAMPLING_FIELDS - def resampling_field_shapes(self): + @classmethod + def resampling_field_shapes(cls): """Access the class level SHAPES constant for this record group.""" - return self.RESAMPLING_SHAPES + return cls.RESAMPLING_SHAPES - def resampling_field_dtypes(self): + @classmethod + def resampling_field_dtypes(cls): """Access the class level DTYPES constant for this record group.""" - return self.RESAMPLING_DTYPES + return cls.RESAMPLING_DTYPES - def resampling_fields(self): + @classmethod + def resampling_fields(cls): """Returns a list of zipped field specs. Returns @@ -434,29 +421,34 @@ def resampling_fields(self): """ return list( zip( - self.resampling_field_names(), - self.resampling_field_shapes(), - self.resampling_field_dtypes(), + cls.resampling_field_names(), + cls.resampling_field_shapes(), + cls.resampling_field_dtypes(), ) ) - def resampling_record_field_names(self): + @classmethod + def resampling_record_field_names(cls): """Access the class level RECORD_FIELDS constant for this record group.""" - return self.RESAMPLING_RECORD_FIELDS + return cls.RESAMPLING_RECORD_FIELDS - def resampler_field_names(self): + @classmethod + def resampler_field_names(cls): """Access the class level FIELDS constant for this record group.""" - return self.RESAMPLER_FIELDS + return cls.RESAMPLER_FIELDS - def resampler_field_shapes(self): + @classmethod + def resampler_field_shapes(cls): """Access the class level SHAPES constant for this record group.""" - return self.RESAMPLER_SHAPES + return cls.RESAMPLER_SHAPES - def resampler_field_dtypes(self): + @classmethod + def resampler_field_dtypes(cls): """Access the class level DTYPES constant for this record group.""" - return self.RESAMPLER_DTYPES + return cls.RESAMPLER_DTYPES - def resampler_fields(self): + @classmethod + def resampler_fields(cls): """Returns a list of zipped field specs. Returns @@ -467,15 +459,16 @@ def resampler_fields(self): """ return list( zip( - self.resampler_field_names(), - self.resampler_field_shapes(), - self.resampler_field_dtypes(), + cls.resampler_field_names(), + cls.resampler_field_shapes(), + cls.resampler_field_dtypes(), ) ) - def resampler_record_field_names(self): + @classmethod + def resampler_record_field_names(cls): """Access the class level RECORD_FIELDS constant for this record group.""" - return self.RESAMPLER_RECORD_FIELDS + return cls.RESAMPLER_RECORD_FIELDS @property def is_debug_on(self) -> bool: diff --git a/src/wepy/resampling/resamplers/wexplore.py b/src/wepy/resampling/resamplers/wexplore.py index d675fb3c..b6723990 100644 --- a/src/wepy/resampling/resamplers/wexplore.py +++ b/src/wepy/resampling/resamplers/wexplore.py @@ -1578,7 +1578,7 @@ def _decide_merge_leaf(self, leaf, merge_groups): ] # choose the one to keep the state of (e.g. KEEP_MERGE - # in the Decision) based on their weights + # in the BaseDecisionABC) based on their weights # normalize weights to the sum of all the chosen weights chosen_pdist = np.array(chosen_weights) / sum(chosen_weights) diff --git a/src/wepy/typing.py b/src/wepy/typing.py new file mode 100644 index 00000000..bc56fde0 --- /dev/null +++ b/src/wepy/typing.py @@ -0,0 +1,23 @@ +"""Some type helpers. + +Provides some Numpy array specifiers useful in Annotated that won't +actually be checked in a checker. + +""" +from typing import Annotated, Union, Literal +import attrs +import numpy as np +from numpy.typing import NDArray + +@attrs.define +class Shape: + dims: tuple[int | Literal[Ellipsis], ...] + +IdxArray = Annotated[ + NDArray[np.integer], + Shape((...,)), + ] +Idxs = Union[ + list[int], + IdxArray, +] diff --git a/tests/unit/test_resampling/test_decisions/test_decision.py b/tests/unit/test_resampling/test_decisions/test_decision.py index 5ffed3c4..19a20998 100644 --- a/tests/unit/test_resampling/test_decisions/test_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_decision.py @@ -5,7 +5,7 @@ import pytest # First Party Library -from wepy.resampling.decisions.decision import Decision, DecisionRecord +from wepy.resampling.decisions.decision import BaseDecisionABC, DecisionRecord from wepy.runners.mock import MockState from wepy.walker import Walker @@ -15,7 +15,7 @@ class MockDecisionEnum(IntEnum): NOTHING = 0 -class MockDecision(Decision): +class MockDecision(BaseDecisionABC): ENUM = MockDecisionEnum DEFAULT_DECISION = ENUM.NOTHING From f0ffa67452b1f343f38a274258e8ddab03986a4b Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 11 Dec 2025 13:24:04 -0500 Subject: [PATCH 098/143] add mixin for writing attrs WalkerState classes Just a helper for writing the __getitem__ and dict methods that are currently required. Could also be another class decorator, but this is fine. Just for convenience anyhow. --- src/wepy/runners/mock.py | 4 ++-- src/wepy/walker.py | 15 +++++++++++++++ tests/unit/test_walker.py | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/wepy/runners/mock.py b/src/wepy/runners/mock.py index 5b9e6ef6..b9a63d38 100644 --- a/src/wepy/runners/mock.py +++ b/src/wepy/runners/mock.py @@ -16,13 +16,13 @@ RunnerStatus, RunSegmentData, ) -from wepy.walker import WalkerState +from wepy.walker import WalkerState, AttrsWalkerStateMixin logger = logging.getLogger(__name__) @attrs.define -class MockState(WalkerState): +class MockState(AttrsWalkerStateMixin, WalkerState): a: int diff --git a/src/wepy/walker.py b/src/wepy/walker.py index 7c53dc43..f93576c0 100644 --- a/src/wepy/walker.py +++ b/src/wepy/walker.py @@ -35,6 +35,8 @@ # Third Party Library import attrs +from wepy.missing import MISSING + logger = logging.getLogger(__name__) T = TypeVar("T") @@ -48,6 +50,19 @@ def __eq__(self, other: Any) -> bool: ... def dict(self) -> dict[str, T]: ... +class AttrsWalkerStateMixin: + """A convenient mixin for implementing the WalkerState interface + for attrs classes.""" + + def __getitem__(self, key: str) -> Any: + if (value := getattr(self, key, MISSING)) is MISSING: + raise KeyError(f"'key' '{key}' not found") + else: + return value + + def dict(self) -> dict[str, Any]: + return attrs.asdict(self) + WalkerState_ = TypeVar("WalkerState_") diff --git a/tests/unit/test_walker.py b/tests/unit/test_walker.py index 8133a96f..7729c0ee 100644 --- a/tests/unit/test_walker.py +++ b/tests/unit/test_walker.py @@ -9,6 +9,7 @@ from wepy.walker import ( Walker, WalkerState, + AttrsWalkerStateMixin, clone, keep_merge, merge, @@ -21,7 +22,7 @@ MockKeys = Literal["a", "b"] MockDataValue = int | str - +# Example of writing a state from scratch class MockData(TypedDict): a: int b: str @@ -41,8 +42,14 @@ def __getitem__(self, key: MockKeys) -> MockDataValue: def dict(self) -> MockData: return attrs.asdict(self) +# example of using the Attrs mixin to write those methods for you +@attrs.define +class MockWalkerStateMixin(AttrsWalkerStateMixin, WalkerState): + a: int + b: str + -class TestWalkerState: +class Test_WalkerState: def test___init__(self): @@ -64,6 +71,28 @@ def test_dict(self): "b": "hello", } +class Test_AttrsWalkerStateMixin: + + def test___init__(self): + + MockWalkerStateMixin(a=1, b="hello") + + def test___getitem__(self): + + assert MockWalkerStateMixin(a=1, b="hello")["a"] == 1 + assert MockWalkerStateMixin(a=1, b="hello")["b"] == "hello" + + def test___eq__(self): + + assert MockWalkerStateMixin(a=1, b="hello") == MockWalkerStateMixin(a=1, b="hello") + assert MockWalkerStateMixin(a=1, b="hello") != MockWalkerStateMixin(a=100, b="hello") + + def test_dict(self): + assert MockWalkerStateMixin(a=1, b="hello").dict() == { + "a": 1, + "b": "hello", + } + class TestWalker: From 42a2066a48913e6db5800b09e2bb04076000c75c Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 11 Dec 2025 13:24:49 -0500 Subject: [PATCH 099/143] add some extra topologies to LJ pair system --- src/wepy_tools/systems/lennard_jones.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/wepy_tools/systems/lennard_jones.py b/src/wepy_tools/systems/lennard_jones.py index e435160b..ad181ef0 100644 --- a/src/wepy_tools/systems/lennard_jones.py +++ b/src/wepy_tools/systems/lennard_jones.py @@ -10,6 +10,9 @@ from wepy.resampling.distances.base import Distance from wepy.runners.openmm import OpenMMState +import mdtraj +from wepy.util.mdtraj import mdtraj_to_json_topology + class LennardJonesPair: """Create a pair of Lennard-Jones particles. @@ -105,6 +108,9 @@ def __init__( topology.addAtom("Ar", element, residue) self.topology = topology + self.mdj_top = mdtraj.Topology.from_openmm(self.topology) + self.json_top = mdtraj_to_json_topology(self.mdj_top) + @attrs.define class PairDistanceImage: From 9ecd2d3380524287355e16ce3bce804ae95b2e9f Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 11 Dec 2025 18:29:03 -0500 Subject: [PATCH 100/143] start to WepyHDF5 tests and types --- src/wepy/hdf5.py | 814 +++++++++++++++++++++------------------- tests/unit/test_hdf5.py | 222 +++++++++++ 2 files changed, 648 insertions(+), 388 deletions(-) create mode 100644 tests/unit/test_hdf5.py diff --git a/src/wepy/hdf5.py b/src/wepy/hdf5.py index c0af8187..02dfbd7d 100644 --- a/src/wepy/hdf5.py +++ b/src/wepy/hdf5.py @@ -390,12 +390,13 @@ """ # Standard Library +from pathlib import Path import gc import itertools as it import json import logging +from typing import Any -logger = logging.getLogger(__name__) # Standard Library import os.path as osp from collections import Counter, defaultdict, namedtuple @@ -413,6 +414,12 @@ traj_fields_to_mdtraj, ) from wepy.util.util import traj_box_vectors_to_lengths_angles +from wepy.reporter.file import FileMode +from wepy.reporter.types import( + FieldShapeSpec, + FieldDtype, +) +from wepy.typing import Shape, Idxs, IdxArray # optional dependencies try: @@ -427,6 +434,8 @@ except ModuleNotFoundError: warn("pandas is not installed and that functionality will not work", RuntimeWarning) +logger = logging.getLogger(__name__) + ## h5py settings # we set the libver to always be the latest (which should be 1.10) so @@ -610,7 +619,7 @@ """Weights feature vector data type.""" # Default Trajectory Field Constants -FIELD_FEATURE_SHAPES = ( +FIELD_FEATURE_SHAPES: tuple[tuple[str, tuple[int, ...]]] = ( (TIME, (1,)), (BOX_VECTORS, (3, 3)), (BOX_VOLUME, (1,)), @@ -649,7 +658,7 @@ # utility for paths -def _iter_field_paths(grp): +def _iter_field_paths(grp: h5py.Group): """Return all subgroup field name paths from a group. Useful for compound fields. For example if you have the group @@ -705,22 +714,242 @@ class WepyHDF5: WRITE_MODES = ("r+", "w", "w-", "x", "a") - #### dunder methods + ## Object attribute type declarations + _filename: Path + _swmr_mode: bool + _h5: h5py.File | None + _wepy_mode: FileMode | None + h5py_mode: FileMode | None + closed: bool + + # TODO: These are all temporary fields that should just be removed + # from object state and passed directly to static methods + _topology: str | None + _units: dict[str, str] | None + _n_dims: int | None + _n_coords: int | None + _field_feature_shapes_kwarg: Any + _field_feature_dtypes_kwarg: Any + _field_feature_dtypes: Any | None + _field_feature_shapes: Any | None + + _sparse_fields: tuple[str, Any] + _main_rep_idxs: Idxs + _alt_reps: dict[str, IdxArray] + + + ## Partial constructors/initializers + + # TODO: make these static and accept the arguments it needs to + # avoid keeping temporary state + def _set_default_init_field_attributes(self, n_dims=None): + """Sets the feature_shapes and feature_dtypes to be the default for + this module. These will be used to initialize field datasets when no + given during construction (i.e. for sparse values) + + Parameters + ---------- + n_dims : int + + """ + + # we use the module defaults for the datasets to initialize them + field_feature_shapes = dict(FIELD_FEATURE_SHAPES) + field_feature_dtypes = dict(FIELD_FEATURE_DTYPES) + + # get the number of coordinates of positions. If there is a + # main_reps then we have to set the number of atoms to that, + # if not we count the number of atoms in the topology + if self._main_rep_idxs is None: + self._n_coords = json_top_atom_count(self.topology) + self._main_rep_idxs = list(range(self._n_coords)) + else: + self._n_coords = len(self._main_rep_idxs) + + # get the number of dimensions as a default + if n_dims is None: + self._n_dims = N_DIMS + + # feature shapes for positions and positions-like fields are + # not known at the module level due to different number of + # coordinates (number of atoms) and number of dimensions + # (default 3 spatial). We set them now that we know this + # information. + # add the postitions shape + field_feature_shapes[POSITIONS] = (self._n_coords, self._n_dims) + # add the positions-like field shapes (velocities and forces) as the same + for poslike_field in POSITIONS_LIKE_FIELDS: + field_feature_shapes[poslike_field] = (self._n_coords, self._n_dims) + + # set the attributes + self._field_feature_shapes = field_feature_shapes + self._field_feature_dtypes = field_feature_dtypes + + + def _init_continuations(self): + """This will either create a dataset in the settings for the + continuations or if continuations already exist it will reinitialize + them and delete the data that exists there. + + Returns + ------- + continuation_dset : h5py.Dataset + + """ + + # if the continuations dset already exists we reinitialize the + # data + if CONTINUATIONS in self.settings_grp: + cont_dset = self.settings_grp[CONTINUATIONS] + cont_dset.resize((0, 2)) + + # otherwise we just create the data + else: + cont_dset = self.settings_grp.create_dataset( + CONTINUATIONS, shape=(0, 2), dtype=int, maxshape=(None, 2) + ) + + return cont_dset + + def _create_init(self): + """Creation mode constructor. + + Completely overwrite the data in the file. Reinitialize the values + and set with the new ones if given. + """ + + assert ( + self._topology is not None + ), "Topology must be given for a creation constructor" + + # initialize the runs group + runs_grp = self._h5.create_group(RUNS) + + # initialize the settings group + settings_grp = self._h5.create_group(SETTINGS) + + # create the topology dataset + self._h5.create_dataset(TOPOLOGY, data=self._topology) + + # sparse fields + if self._sparse_fields is not None: + # make a dataset for the sparse fields allowed. this requires + # a 'special' datatype for variable length strings. This is + # supported by HDF5 but not numpy. + vlen_str_dt = h5py.special_dtype(vlen=str) + + # create the dataset with empty values for the length of the + # sparse fields given + sparse_fields_ds = settings_grp.create_dataset( + SPARSE_FIELDS, + (len(self._sparse_fields),), + dtype=vlen_str_dt, + maxshape=(None,), + ) + + # set the flags + for i, sparse_field in enumerate(self._sparse_fields): + sparse_fields_ds[i] = sparse_field + + # field feature shapes and dtypes + + # initialize to the defaults, this gives values to + # self._n_coords, and self.field_feature_dtypes, and + # self.field_feature_shapes + self._set_default_init_field_attributes(n_dims=self._n_dims) + + # save the number of dimensions and number of atoms in settings + settings_grp.create_dataset(N_DIMS_STR, data=np.array(self._n_dims)) + settings_grp.create_dataset(N_ATOMS, data=np.array(self._n_coords)) + + # the main rep atom idxs + settings_grp.create_dataset(MAIN_REP_IDXS, data=self._main_rep_idxs, dtype=int) + + # alt_reps settings + alt_reps_idxs_grp = settings_grp.create_group(ALT_REPS_IDXS) + for alt_rep_name, idxs in self._alt_reps.items(): + alt_reps_idxs_grp.create_dataset(alt_rep_name, data=idxs, dtype=int) + + # if both feature shapes and dtypes were specified overwrite + # (or initialize if not set by defaults) the defaults + if (self._field_feature_shapes_kwarg is not None) and ( + self._field_feature_dtypes_kwarg is not None + ): + self._field_feature_shapes.update(self._field_feature_shapes_kwarg) + self._field_feature_dtypes.update(self._field_feature_dtypes_kwarg) + + # any sparse field with unspecified shape and dtype must be + # set to None so that it will be set at runtime + for sparse_field in self.sparse_fields: + if (sparse_field not in self._field_feature_shapes) or ( + sparse_field not in self._field_feature_dtypes + ): + self._field_feature_shapes[sparse_field] = None + self._field_feature_dtypes[sparse_field] = None + + # save the field feature shapes and dtypes in the settings group + shapes_grp = settings_grp.create_group(FIELD_FEATURE_SHAPES_STR) + for field_path, field_shape in self._field_feature_shapes.items(): + if field_shape is None: + # set it as a dimensionless array of NaN + field_shape = np.array(np.nan) + + shapes_grp.create_dataset(field_path, data=field_shape) + + dtypes_grp = settings_grp.create_group(FIELD_FEATURE_DTYPES_STR) + for field_path, field_dtype in self._field_feature_dtypes.items(): + if field_dtype is None: + dt_str = NONE_STR + else: + # make a json string of the datatype that can be read + # in again, we call np.dtype again because there is no + # np.float.descr attribute + dt_str = json.dumps(np.dtype(field_dtype).descr) + + dtypes_grp.create_dataset(field_path, data=dt_str) + + # initialize the units group + unit_grp = self._h5.create_group(UNITS) + + # if units were not given set them all to None + if self._units is None: + self._units = {} + for field_path in self._field_feature_shapes.keys(): + self._units[field_path] = None + + # set the units + for field_path, unit_value in self._units.items(): + # ignore the field if not given + if unit_value is None: + continue + + unit_path = "{}/{}".format(UNITS, field_path) + + unit_grp.create_dataset(unit_path, data=unit_value) + + # create the group for the run data records + records_grp = settings_grp.create_group(RECORD_FIELDS) + + # create a dataset for the continuation run tuples + # (continuation_run, base_run), where the first element + # of the new run that is continuing the run in the second + # position + self._init_continuations() def __init__( self, - filename, - mode="x", - topology=None, - units=None, - sparse_fields=None, - feature_shapes=None, - feature_dtypes=None, - n_dims=None, - alt_reps=None, - main_rep_idxs=None, - swmr_mode=False, - expert_mode=False, + filename: Path, + mode: FileMode = "x", + topology: str | None = None, + units: dict[str, str] | None = None, + sparse_fields: tuple[str, ...] = None, + feature_shapes: dict[str, FieldShapeSpec] | None = None, + feature_dtypes: dict[str, FieldDtype] | None = None, + n_dims: int | None = None, + alt_reps: dict[str, IdxArray] | None = None, + main_rep_idxs: IdxArray | None = None, + swmr_mode: bool = False, + expert_mode: bool = False, ): """Constructor for the WepyHDF5 class. @@ -791,8 +1020,7 @@ def __init__( """ - self._filename = filename - self._swmr_mode = swmr_mode + self.closed = None if expert_mode is True: self._h5 = None @@ -803,9 +1031,56 @@ def __init__( # terminate the constructor here return None - assert mode in self.MODES, "mode must be either one of: {}".format( - ", ".join(self.MODES) - ) + if mode not in self.MODES: + raise ValueError( + f"mode must be either one of: {self.MODES}" + ) + + _constructor_data = { + "topology" : topology, + "units" : units, + "sparse_fields" : sparse_fields, + "feature_shapes" : feature_shapes, + "feature_dtypes" : feature_dtypes, + "n_dims" : n_dims, + "alt_reps" : alt_reps, + "main_rep_idxs" : main_rep_idxs, + } + + # create file mode: 'w' will create a new file or overwrite, + # 'w-' and 'x' will not overwrite but will create a new file + if mode in {"w-", "x"} and filename.exists(): + raise FileExistsError( + f"WepyHDF5 file already exists and will not be overwritten in mode: {mode}" + ) + + elif mode in {"w", "w-", "x"}: + # check for required args + if topology is None: + raise ValueError( + f"In creation mode ({mode}) you must provide topology." + ) + + + elif mode in {"r", "r+"}: + + # if any data was given, warn the user + if any( + _given_data := { + key + for key, value + in _constructor_data.items() + if value is not None + } + ): + raise ValueError( + f"Data was given but opening in read mode: {_given_data}", + ) + + + + self._filename = filename + self._swmr_mode = swmr_mode # the top level mode enforced by wepy.hdf5 self._wepy_mode = mode @@ -816,6 +1091,9 @@ def __init__( # used elsewhere and could be a feature in the future. self._h5py_mode = mode + # TODO: cleanup some of these resources so they are not in + # memory if they are large, like the topology + # Temporary metadata: used to initialize the object but not # used after that @@ -836,7 +1114,7 @@ def __init__( # save the sparse fields as a private variable for use in the # create constructor if sparse_fields is None: - self._sparse_fields = [] + self._sparse_fields = () else: self._sparse_fields = sparse_fields @@ -857,302 +1135,164 @@ def __init__( # open the file and then run the different constructors based # on the mode - with h5py.File( - filename, mode=self._h5py_mode, libver=H5PY_LIBVER, swmr=self._swmr_mode - ) as h5: - self._h5 = h5 - - # set SWMR mode if asked for if we are in write mode also - if self._swmr_mode is True and mode in self.WRITE_MODES: - self._h5.swmr_mode = swmr_mode - - # create file mode: 'w' will create a new file or overwrite, - # 'w-' and 'x' will not overwrite but will create a new file - if self._wepy_mode in ["w", "w-", "x"]: - self._create_init() - - # read/write mode: in this mode we do not completely overwrite - # the old file and start again but rather write over top of - # values if requested - elif self._wepy_mode in ["r+"]: - self._read_write_init() - - # add mode: read/write create if doesn't exist - elif self._wepy_mode in ["a"]: - if osp.exists(self._filename): - self._read_write_init() - else: - self._create_init() - - # read only mode - elif self._wepy_mode == "r": - # if any data was given, warn the user - if any( - [ - kwarg is not None - for kwarg in [ - topology, - units, - sparse_fields, - feature_shapes, - feature_dtypes, - n_dims, - alt_reps, - main_rep_idxs, - ] - ] - ): - warn("Data was given but opening in read-only mode", RuntimeWarning) - - # then run the initialization process - self._read_init() - - # flush the buffers - self._h5.flush() - - # set the h5py mode to the value in the actual h5py.File - # object after creation - self._h5py_mode = self._h5.mode - - # get rid of the temporary variables - del self._topology - del self._units - del self._n_dims - del self._n_coords - del self._field_feature_shapes_kwarg - del self._field_feature_dtypes_kwarg - del self._field_feature_shapes - del self._field_feature_dtypes - del self._sparse_fields - del self._main_rep_idxs - del self._alt_reps - - # variable to reflect if it is closed or not, should be closed - # after initialization - self.closed = True - - # end of the constructor - return None - - # TODO is this right? shouldn't we actually delete the data then close - def __del__(self): - self.close() - - # context manager methods - - def __enter__(self): - self.open() - # self._h5 = h5py.File(self._filename, - # libver=H5PY_LIBVER, swmr=self._swmr_mode) - # self.closed = False - return self - - def __exit__(self, exc_type, exc_value, exc_tb): - self.close() - - @property - def swmr_mode(self): - return self._swmr_mode - - @swmr_mode.setter - def swmr_mode(self, val): - self._swmr_mode = val - - # TODO custom deepcopy to avoid copying the actual HDF5 object - - #### hidden methods (_method_name) - - ### constructors - def _create_init(self): - """Creation mode constructor. - - Completely overwrite the data in the file. Reinitialize the values - and set with the new ones if given. - """ - - assert ( - self._topology is not None - ), "Topology must be given for a creation constructor" - - # initialize the runs group - runs_grp = self._h5.create_group(RUNS) - - # initialize the settings group - settings_grp = self._h5.create_group(SETTINGS) - - # create the topology dataset - self._h5.create_dataset(TOPOLOGY, data=self._topology) - - # sparse fields - if self._sparse_fields is not None: - # make a dataset for the sparse fields allowed. this requires - # a 'special' datatype for variable length strings. This is - # supported by HDF5 but not numpy. - vlen_str_dt = h5py.special_dtype(vlen=str) - - # create the dataset with empty values for the length of the - # sparse fields given - sparse_fields_ds = settings_grp.create_dataset( - SPARSE_FIELDS, - (len(self._sparse_fields),), - dtype=vlen_str_dt, - maxshape=(None,), - ) - - # set the flags - for i, sparse_field in enumerate(self._sparse_fields): - sparse_fields_ds[i] = sparse_field - - # field feature shapes and dtypes - - # initialize to the defaults, this gives values to - # self._n_coords, and self.field_feature_dtypes, and - # self.field_feature_shapes - self._set_default_init_field_attributes(n_dims=self._n_dims) - - # save the number of dimensions and number of atoms in settings - settings_grp.create_dataset(N_DIMS_STR, data=np.array(self._n_dims)) - settings_grp.create_dataset(N_ATOMS, data=np.array(self._n_coords)) - - # the main rep atom idxs - settings_grp.create_dataset(MAIN_REP_IDXS, data=self._main_rep_idxs, dtype=int) - - # alt_reps settings - alt_reps_idxs_grp = settings_grp.create_group(ALT_REPS_IDXS) - for alt_rep_name, idxs in self._alt_reps.items(): - alt_reps_idxs_grp.create_dataset(alt_rep_name, data=idxs, dtype=int) - - # if both feature shapes and dtypes were specified overwrite - # (or initialize if not set by defaults) the defaults - if (self._field_feature_shapes_kwarg is not None) and ( - self._field_feature_dtypes_kwarg is not None - ): - self._field_feature_shapes.update(self._field_feature_shapes_kwarg) - self._field_feature_dtypes.update(self._field_feature_dtypes_kwarg) + self._h5 = h5py.File( + filename, + mode=self._h5py_mode, + libver=H5PY_LIBVER, + swmr=self._swmr_mode, + ) + self.closed = False - # any sparse field with unspecified shape and dtype must be - # set to None so that it will be set at runtime - for sparse_field in self.sparse_fields: - if (sparse_field not in self._field_feature_shapes) or ( - sparse_field not in self._field_feature_dtypes - ): - self._field_feature_shapes[sparse_field] = None - self._field_feature_dtypes[sparse_field] = None + # TOREV: do we need to set this again? + # + # set SWMR mode if asked for if we are in write mode also + if self._swmr_mode is True and mode in self.WRITE_MODES: + self._h5.swmr_mode = swmr_mode + - # save the field feature shapes and dtypes in the settings group - shapes_grp = settings_grp.create_group(FIELD_FEATURE_SHAPES_STR) - for field_path, field_shape in self._field_feature_shapes.items(): - if field_shape is None: - # set it as a dimensionless array of NaN - field_shape = np.array(np.nan) + if self._wepy_mode in {"w", "x", "w-"}: + self._create_init() - shapes_grp.create_dataset(field_path, data=field_shape) + # flush the buffers + self._h5.flush() - dtypes_grp = settings_grp.create_group(FIELD_FEATURE_DTYPES_STR) - for field_path, field_dtype in self._field_feature_dtypes.items(): - if field_dtype is None: - dt_str = NONE_STR - else: - # make a json string of the datatype that can be read - # in again, we call np.dtype again because there is no - # np.float.descr attribute - dt_str = json.dumps(np.dtype(field_dtype).descr) + # set the h5py mode to the value in the actual h5py.File + # object after creation + self._h5py_mode = self._h5.mode + + self._h5.close() - dtypes_grp.create_dataset(field_path, data=dt_str) - # initialize the units group - unit_grp = self._h5.create_group(UNITS) + # get rid of the temporary variables + del self._topology + del self._units + del self._n_dims + del self._n_coords + del self._field_feature_shapes_kwarg + del self._field_feature_dtypes_kwarg + del self._field_feature_shapes + del self._field_feature_dtypes + del self._sparse_fields + del self._main_rep_idxs + del self._alt_reps - # if units were not given set them all to None - if self._units is None: - self._units = {} - for field_path in self._field_feature_shapes.keys(): - self._units[field_path] = None + # variable to reflect if it is closed or not, should be closed + # after initialization + self.closed = True - # set the units - for field_path, unit_value in self._units.items(): - # ignore the field if not given - if unit_value is None: - continue + @property + def filename(self) -> Path: + """The path to the underlying HDF5 file.""" + return self._filename - unit_path = "{}/{}".format(UNITS, field_path) - unit_grp.create_dataset(unit_path, data=unit_value) + @property + def mode(self) -> FileMode: + """The WepyHDF5 mode this object was created with.""" + return self._wepy_mode - # create the group for the run data records - records_grp = settings_grp.create_group(RECORD_FIELDS) + @mode.setter + def mode(self, mode: FileMode) -> None: + """Set the mode for opening the file with.""" + self.set_mode(mode) - # create a dataset for the continuation run tuples - # (continuation_run, base_run), where the first element - # of the new run that is continuing the run in the second - # position - self._init_continuations() + def set_mode(self, mode: FileMode) -> None: + """Set the mode for opening the file with.""" - def _read_write_init(self): - """Read-write mode constructor.""" + if not self.closed: + raise RuntimeError("Cannot set the mode while the file is open.") - self._read_init() + self._set_h5_mode(mode) - def _add_init(self): - """The addition mode constructor. + self._wepy_mode = mode - Create the dataset if it doesn't exist and put it in r+ mode, - otherwise, just open in r+ mode. + @property + def h5_mode(self) -> FileMode: + """The h5py.File mode the HDF5 file currently has.""" + return self._h5.mode - """ + def _set_h5_mode(self, h5_mode: FileMode) -> None: + """Set the mode to open the HDF5 file with. - if not any(self._exist_flags): - self._create_init() - else: - self._read_write_init() + This really shouldn't be set without using the main wepy mode + as they need to be aligned. - def _read_init(self): - """Read mode constructor.""" + """ - pass + if not self.closed: + raise AttributeError("Cannot set the mode while the file is open.") - def _set_default_init_field_attributes(self, n_dims=None): - """Sets the feature_shapes and feature_dtypes to be the default for - this module. These will be used to initialize field datasets when no - given during construction (i.e. for sparse values) + self._h5py_mode = h5_mode + + def open(self, mode: FileMode | None = None) -> None: + """Open the underlying HDF5 file for access. Parameters ---------- - n_dims : int + mode : str + Valid mode spec. Opens the HDF5 file in this mode if given + otherwise uses the existing mode. """ - # we use the module defaults for the datasets to initialize them - field_feature_shapes = dict(FIELD_FEATURE_SHAPES) - field_feature_dtypes = dict(FIELD_FEATURE_DTYPES) + if mode is None: + mode = self.mode - # get the number of coordinates of positions. If there is a - # main_reps then we have to set the number of atoms to that, - # if not we count the number of atoms in the topology - if self._main_rep_idxs is None: - self._n_coords = json_top_atom_count(self.topology) - self._main_rep_idxs = list(range(self._n_coords)) + if self.closed: + self.set_mode(mode) + + self._h5 = h5py.File( + self._filename, mode, libver=H5PY_LIBVER, swmr=self.swmr_mode + ) + self.closed = False else: - self._n_coords = len(self._main_rep_idxs) + raise IOError("This file is already open") - # get the number of dimensions as a default - if n_dims is None: - self._n_dims = N_DIMS + def close(self) -> None: + """Close the underlying HDF5 file.""" - # feature shapes for positions and positions-like fields are - # not known at the module level due to different number of - # coordinates (number of atoms) and number of dimensions - # (default 3 spatial). We set them now that we know this - # information. - # add the postitions shape - field_feature_shapes[POSITIONS] = (self._n_coords, self._n_dims) - # add the positions-like field shapes (velocities and forces) as the same - for poslike_field in POSITIONS_LIKE_FIELDS: - field_feature_shapes[poslike_field] = (self._n_coords, self._n_dims) + # check if the HDF5 is fully initialized yet - # set the attributes - self._field_feature_shapes = field_feature_shapes - self._field_feature_dtypes = field_feature_dtypes + # not fully initialized yet + if not hasattr(self, "_h5") or self._h5 is None: + self.closed = True + + elif not self.closed: + self._h5.flush() + self._h5.close() + self.closed = True + + @property + def h5(self) -> h5py.File: + """The underlying h5py.File object.""" + return self._h5 + + def __del__(self): + self.close() + + # context manager methods + + def __enter__(self): + self.open() + # self._h5 = h5py.File(self._filename, + # libver=H5PY_LIBVER, swmr=self._swmr_mode) + # self.closed = False + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + self.close() + + @property + def swmr_mode(self): + return self._swmr_mode + + @swmr_mode.setter + def swmr_mode(self, val): + self._swmr_mode = val + + #### hidden methods (_method_name) + + ### constructors def _get_field_path_grp(self, run_idx, traj_idx, field_path): """Given a field path for the trajectory returns the group the field's @@ -1193,30 +1333,6 @@ def _get_field_path_grp(self, run_idx, traj_idx, field_path): return grp, field_name - def _init_continuations(self): - """This will either create a dataset in the settings for the - continuations or if continuations already exist it will reinitialize - them and delete the data that exists there. - - Returns - ------- - continuation_dset : h5py.Dataset - - """ - - # if the continuations dset already exists we reinitialize the - # data - if CONTINUATIONS in self.settings_grp: - cont_dset = self.settings_grp[CONTINUATIONS] - cont_dset.resize((0, 2)) - - # otherwise we just create the data - else: - cont_dset = self.settings_grp.create_dataset( - CONTINUATIONS, shape=(0, 2), dtype=int, maxshape=(None, 2) - ) - - return cont_dset def _add_run_init(self, run_idx, continue_run=None): """Routines for creating a run includes updating and setting object @@ -2476,84 +2592,6 @@ def _add_field(self, field_path, data, sparse_idxs=None, force=False): ### File Utilities - @property - def filename(self): - """The path to the underlying HDF5 file.""" - return self._filename - - def open(self, mode=None): - """Open the underlying HDF5 file for access. - - Parameters - ---------- - mode : str - Valid mode spec. Opens the HDF5 file in this mode if given - otherwise uses the existing mode. - - """ - - if mode is None: - mode = self.mode - - if self.closed: - self.set_mode(mode) - - self._h5 = h5py.File( - self._filename, mode, libver=H5PY_LIBVER, swmr=self.swmr_mode - ) - self.closed = False - else: - raise IOError("This file is already open") - - def close(self): - """Close the underlying HDF5 file.""" - if not self.closed: - self._h5.flush() - self._h5.close() - self.closed = True - - @property - def mode(self): - """The WepyHDF5 mode this object was created with.""" - return self._wepy_mode - - @mode.setter - def mode(self, mode): - """Set the mode for opening the file with.""" - self.set_mode(mode) - - def set_mode(self, mode): - """Set the mode for opening the file with.""" - - if not self.closed: - raise AttributeError("Cannot set the mode while the file is open.") - - self._set_h5_mode(mode) - - self._wepy_mode = mode - - @property - def h5_mode(self): - """The h5py.File mode the HDF5 file currently has.""" - return self._h5.mode - - def _set_h5_mode(self, h5_mode): - """Set the mode to open the HDF5 file with. - - This really shouldn't be set without using the main wepy mode - as they need to be aligned. - - """ - - if not self.closed: - raise AttributeError("Cannot set the mode while the file is open.") - - self._h5py_mode = h5_mode - - @property - def h5(self): - """The underlying h5py.File object.""" - return self._h5 ### h5py object access diff --git a/tests/unit/test_hdf5.py b/tests/unit/test_hdf5.py new file mode 100644 index 00000000..9e0e5f32 --- /dev/null +++ b/tests/unit/test_hdf5.py @@ -0,0 +1,222 @@ +from pathlib import Path +import pytest + +import h5py +from wepy.hdf5 import ( + WepyHDF5, + _iter_field_paths, +) + +from wepy_tools.systems.lennard_jones import LennardJonesPair + + +# def gen_wepy_h5(path: Path, mode: str) -> WepyHDF5: +# pass + +# @pytest.fixture(scope="session") +# def wepy_h5_file_ro(tmpdir) -> WepyHDF5: +# """Read-only WepyHDF5""" + +# return gen_wepy_h5(Path(tmpdir) / "main.wepy.h5") + +# @pytest.fixture(scope="function") +# def wepy_h5_file_rw(tmpdir): +# """Read-write WepyHDF5""" +# return gen_wepy_h5( +# Path(tmpdir) / "main.wepy.h5", +# ) + + +# # TODO: this will be easier once we have a fixture for a full WepyHDF5 +# def test__iter_field_paths(wepy_h5_file_ro): +# pass + + +class Test_WepyHDF5: + + def test___init__(self, tmp_path_factory): + + test_sys = LennardJonesPair() + d0 = tmp_path_factory.mktemp("0") + h5_path = d0 / "main.wepy.h5" + + with pytest.raises(ValueError): + WepyHDF5( + h5_path, + mode="P", + ) + + with pytest.raises(ValueError): + WepyHDF5( + h5_path, + mode="x", + ) + with pytest.raises(ValueError): + WepyHDF5( + h5_path, + mode="w", + ) + with pytest.raises(ValueError): + WepyHDF5( + h5_path, + mode="w-", + ) + + wh5 = WepyHDF5( + h5_path, + mode="x", + topology=test_sys.json_top, + ) + + assert wh5.filename.exists() + + with pytest.raises(FileExistsError): + WepyHDF5( + h5_path, + mode="x", + topology=test_sys.json_top, + ) + + WepyHDF5( + h5_path, + mode="r", + ) + + with pytest.raises(ValueError): + WepyHDF5( + h5_path, + mode="r", + topology=test_sys.json_top, + ) + + # NOTE: tests on data conformance see other tests, we first + # need to bootstrap other functionality in these unit tests + # before getting there. + + def test_set_mode(self, tmp_path_factory): + + test_sys = LennardJonesPair() + d0 = tmp_path_factory.mktemp("0") + h5_path = d0 / "main.wepy.h5" + + wh5 = WepyHDF5( + h5_path, + mode="x", + topology=test_sys.json_top, + ) + + assert wh5.mode == "x" + wh5.set_mode("r") + assert wh5.mode == "r" + + wh5.closed = False + with pytest.raises(RuntimeError): + wh5.set_mode("r+") + + def test_open(self, tmp_path_factory): + test_sys = LennardJonesPair() + d0 = tmp_path_factory.mktemp("0") + h5_path = d0 / "main.wepy.h5" + + wh5 = WepyHDF5( + h5_path, + mode="x", + topology=test_sys.json_top, + ) + + assert wh5.closed == True + assert wh5._wepy_mode == "x" + + + with pytest.raises(FileExistsError): + wh5.open() + with pytest.raises(FileExistsError): + wh5.open(mode="x") + with pytest.raises(FileExistsError): + wh5.open(mode="w-") + + wh5.set_mode('r') + wh5.open() + assert wh5.closed == False + assert wh5._wepy_mode == "r" + with pytest.raises(IOError): + wh5.open() + + h5_path = d0 / "main2.wepy.h5" + wh5 = WepyHDF5( + h5_path, + mode="x", + topology=test_sys.json_top, + ) + + wh5.open(mode='r+') + assert wh5.closed == False + assert wh5._wepy_mode == "r+" + + + def test_close(self, tmp_path_factory): + test_sys = LennardJonesPair() + d0 = tmp_path_factory.mktemp("0") + h5_path = d0 / "main.wepy.h5" + + wh5 = WepyHDF5( + h5_path, + mode="x", + topology=test_sys.json_top, + ) + + wh5.open("r") + + assert wh5.closed == False + wh5.close() + assert wh5.closed == True + + + def test___del__(self, tmp_path_factory): + test_sys = LennardJonesPair() + d0 = tmp_path_factory.mktemp("0") + + h5_path = d0 / "main.wepy.h5" + wh5 = WepyHDF5( + h5_path, + mode="x", + topology=test_sys.json_top, + ) + del wh5 + + h5_path = d0 / "2.wepy.h5" + wh5 = WepyHDF5( + h5_path, + mode="x", + topology=test_sys.json_top, + ) + + wh5.open('r+') + del wh5 + + + def test_context_manager(self, tmp_path_factory): + test_sys = LennardJonesPair() + d0 = tmp_path_factory.mktemp("0") + + h5_path = d0 / "main.wepy.h5" + # create + WepyHDF5( + h5_path, + mode="x", + topology=test_sys.json_top, + ) + + wepy_h5 = WepyHDF5( + h5_path, + mode="r", + ) + with wepy_h5: + + assert wepy_h5.closed == False + + assert wepy_h5.closed == True + + +class Test_WepyHDF5_DataConformance: + pass From af49b6f7a50aa5bb4a5f20994a1501ed13585bcc Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 11 Dec 2025 20:06:17 -0500 Subject: [PATCH 101/143] rework decision types --- src/wepy/resampling/decisions/clone_merge.py | 4 +- src/wepy/resampling/decisions/decision.py | 48 +++++++++++-------- src/wepy/resampling/decisions/no_decision.py | 4 +- .../test_decisions/test_decision.py | 6 +-- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/wepy/resampling/decisions/clone_merge.py b/src/wepy/resampling/decisions/clone_merge.py index bbc5ddbe..3723e5a4 100644 --- a/src/wepy/resampling/decisions/clone_merge.py +++ b/src/wepy/resampling/decisions/clone_merge.py @@ -7,7 +7,7 @@ import attrs # First Party Library -from wepy.resampling.decisions.decision import BaseDecisionABC, DecisionRecord +from wepy.resampling.decisions.decision import BaseDecisionABC, BaseDecisionRecord from wepy.walker import Walker, keep_merge, split logger = logging.getLogger(__name__) @@ -41,7 +41,7 @@ class CloneMergeDecisionEnum(IntEnum): @attrs.define -class CloneMergeDecisionRecord(DecisionRecord): +class CloneMergeDecisionRecord(BaseDecisionRecord): decision_id: int target_idxs: list[int] diff --git a/src/wepy/resampling/decisions/decision.py b/src/wepy/resampling/decisions/decision.py index f1030faa..e190768d 100644 --- a/src/wepy/resampling/decisions/decision.py +++ b/src/wepy/resampling/decisions/decision.py @@ -48,7 +48,7 @@ # Standard Library import logging from enum import IntEnum -from typing import Any, Union +from typing import Any, Union, Generic, TypeVar # Third Party Library import attrs @@ -60,24 +60,28 @@ @attrs.define -class DecisionRecord: +class BaseDecisionRecord: decision_id: int DecisionFieldDtype = Union[int,] +DecisionFieldShapeSpec = tuple[int | type(Ellipsis), ...] + +DecisionEnum_ = TypeVar("DecisionEnum_") +DecisionRecord_ = TypeVar("DecisionRecord", bound=BaseDecisionRecord) # ABC for the Decision class -class BaseDecisionABC: +class BaseDecisionABC(Generic[DecisionEnum_, DecisionRecord_]): """Represents and provides methods for a set of decision values.""" - ENUM: IntEnum + ENUM: type[DecisionEnum_] """The enumeration of the decision types. Maps them to integers.""" DEFAULT_DECISION: int """The default decision to choose.""" - DECISION_RECORD: DecisionRecord = DecisionRecord + DECISION_RECORD: DecisionRecord_ = BaseDecisionRecord FIELDS: tuple[str, ...] = ("decision_id",) """The names of the fields that go into the decision record.""" @@ -88,13 +92,13 @@ class BaseDecisionABC: # An Ellipsis instead of fields indicate there is a variable # number of fields. - SHAPES: tuple[tuple[int | type(Ellipsis), ...], ...] = ((1,),) + SHAPES: tuple[DecisionFieldShapeSpec, ...] = ((1,),) """Field data shapes.""" DTYPES: tuple[DecisionFieldDtype, ...] = (int,) """Field data types.""" - RECORD_FIELDS: tuple[str] = ("decision_id",) + RECORD_FIELDS: tuple[str, ...] = ("decision_id",) """The fields that could be used in a reduced table-like representation.""" ANCESTOR_DECISION_IDS: tuple[int, ...] @@ -102,26 +106,32 @@ class BaseDecisionABC: passed on in the next generation, i.e. after performing the action.""" @classmethod - def default_decision(cls): + def default_decision(cls) -> int: return cls.DEFAULT_DECISION @classmethod - def field_names(cls): + def field_names(cls) -> tuple[str, ...]: """Names of the decision record fields.""" return cls.FIELDS @classmethod - def field_shapes(cls): + def field_shapes(cls) -> tuple[DecisionFieldShapeSpec, ...]: """Field data shapes.""" return cls.SHAPES @classmethod - def field_dtypes(cls): + def field_dtypes(cls) -> tuple[DecisionFieldDtype, ...]: """Field data types.""" return cls.DTYPES @classmethod - def fields(cls): + def fields(cls) -> list[ + tuple[ + str, + DecisionFieldShapeSpec, + DecisionFieldDtype, + ] + ]: """Specs for each field. Returns @@ -133,12 +143,12 @@ def fields(cls): return list(zip(cls.field_names(), cls.field_shapes(), cls.field_dtypes())) @classmethod - def record_field_names(cls): + def record_field_names(cls) -> tuple[str, ...]: """The fields that could be used in a reduced table-like representation.""" return cls.RECORD_FIELDS @classmethod - def enum_dict_by_name(cls): + def enum_dict_by_name(cls) -> dict[str, int]: """Get the decision enumeration as a dict mapping name to integer.""" if cls.ENUM is None: raise NotImplementedError @@ -149,7 +159,7 @@ def enum_dict_by_name(cls): return d @classmethod - def enum_dict_by_value(cls): + def enum_dict_by_value(cls) -> dict[int, DecisionEnum_]: """Get the decision enumeration as a dict mapping integer to name.""" if cls.ENUM is None: @@ -161,7 +171,7 @@ def enum_dict_by_value(cls): return d @classmethod - def enum_by_value(cls, enum_value): + def enum_by_value(cls, enum_value: int) -> DecisionEnum_: """Get the enum name for an enum_value. Parameters @@ -177,7 +187,7 @@ def enum_by_value(cls, enum_value): return d[enum_value] @classmethod - def enum_by_name(cls, enum_name): + def enum_by_name(cls, enum_name: str) -> DecisionEnum_: """Get the enum name for an enum_value. Parameters @@ -194,7 +204,7 @@ def enum_by_name(cls, enum_name): return d[enum_name] @classmethod - def record(cls, enum_value: int, **fields: dict[str, Any]) -> DecisionRecord: + def record(cls, enum_value: int, **fields: dict[str, Any]) -> DecisionRecord_: """Generate a record for the enum_value and the other fields. Parameters @@ -228,7 +238,7 @@ def record(cls, enum_value: int, **fields: dict[str, Any]) -> DecisionRecord: def action( cls, walkers: list[Walker], - decisions: list[list[DecisionRecord]], + decisions: list[list[DecisionRecord_]], ) -> list[Walker]: """Perform the instructions for a set of resampling records on walkers. diff --git a/src/wepy/resampling/decisions/no_decision.py b/src/wepy/resampling/decisions/no_decision.py index ab491531..0dda3be4 100644 --- a/src/wepy/resampling/decisions/no_decision.py +++ b/src/wepy/resampling/decisions/no_decision.py @@ -5,7 +5,7 @@ import attrs # First Party Library -from wepy.resampling.decisions.decision import BaseDecisionABC, DecisionRecord +from wepy.resampling.decisions.decision import BaseDecisionABC, BaseDecisionRecord from wepy.walker import Walker @@ -17,7 +17,7 @@ class NothingDecisionEnum(IntEnum): @attrs.define -class NoDecisionRecord(DecisionRecord): +class NoDecisionRecord(BaseDecisionRecord): decision_id: int target_idx: int diff --git a/tests/unit/test_resampling/test_decisions/test_decision.py b/tests/unit/test_resampling/test_decisions/test_decision.py index 19a20998..33ebcdb7 100644 --- a/tests/unit/test_resampling/test_decisions/test_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_decision.py @@ -5,7 +5,7 @@ import pytest # First Party Library -from wepy.resampling.decisions.decision import BaseDecisionABC, DecisionRecord +from wepy.resampling.decisions.decision import BaseDecisionABC, BaseDecisionRecord from wepy.runners.mock import MockState from wepy.walker import Walker @@ -66,12 +66,12 @@ def test_enum_by_name(self): def test_record(self): - assert MockDecision.record(0) == DecisionRecord(decision_id=0) + assert MockDecision.record(0) == BaseDecisionRecord(decision_id=0) def test_action(self): with pytest.raises(NotImplementedError): MockDecision.action( [Walker(MockState(1), 0.1) for _ in range(4)], - [DecisionRecord(decision_id=0) for _ in range(4)], + [BaseDecisionRecord(decision_id=0) for _ in range(4)], ) From f6ee56d4d8445eb7ebbacf35d2238d5583dac76d Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 12 Dec 2025 00:38:04 -0500 Subject: [PATCH 102/143] progress in testing WepyHDF5 Currently tested through the initialization part of the methods. --- src/wepy/hdf5.py | 399 +++++++++++++++++++++++----------------- tests/unit/test_hdf5.py | 385 +++++++++++++++++++++++++++++++++++++- 2 files changed, 612 insertions(+), 172 deletions(-) diff --git a/src/wepy/hdf5.py b/src/wepy/hdf5.py index 02dfbd7d..a9b558da 100644 --- a/src/wepy/hdf5.py +++ b/src/wepy/hdf5.py @@ -391,11 +391,12 @@ # Standard Library from pathlib import Path +import copy import gc import itertools as it import json import logging -from typing import Any +from typing import Any, TypedDict, NotRequired, Literal # Standard Library import os.path as osp @@ -405,6 +406,7 @@ # Third Party Library import h5py import numpy as np +from numpy.typing import NDArray # First Party Library from wepy.analysis.parents import resampling_panel @@ -444,6 +446,8 @@ # in a new virtualenv if this is a problem for you H5PY_LIBVER = "latest" +STRING_ENCODING = "utf-8" + ## Header and settings keywords TOPOLOGY = "topology" @@ -628,7 +632,8 @@ ) """Default shapes for the default fields.""" -FIELD_FEATURE_DTYPES = ( +FieldFeatureDtype = float | int +FIELD_FEATURE_DTYPES: tuple[tuple[str, FieldFeatureDtype]] = ( (POSITIONS, float), (VELOCITIES, float), (FORCES, float), @@ -698,7 +703,38 @@ def _iter_field_paths(grp: h5py.Group): field_paths.append(field_name) return field_paths +class Dtype(TypedDict): + kind: Literal["simple", "structured"] + str: NotRequired[str] + descr: NotRequired[list[tuple[str, str]]] + +def numpy_dtype_to_json(dtype: np.dtype) -> str: + + payload: Dtype + if dtype.fields is None: + payload = { + "kind" : "simple", + "str" : dtype.str, + } + + else: + payload = { + "kind" : "structured", + "descr" : dtype.descr, + } + + # Warning only supports simple data types + return json.dumps(payload) + +def dtype_json_to_numpy(s: str) -> np.dtype: + payload = json.loads(s) + + if payload["kind"] == "simple": + return np.dtype(payload["str"]) + else: + return np.dtype(payload["descr"]) + class WepyHDF5: """Wrapper for h5py interface to an HDF5 file object for creation and access of WepyHDF5 data. @@ -728,10 +764,13 @@ class WepyHDF5: _units: dict[str, str] | None _n_dims: int | None _n_coords: int | None - _field_feature_shapes_kwarg: Any - _field_feature_dtypes_kwarg: Any - _field_feature_dtypes: Any | None - _field_feature_shapes: Any | None + # These are the extra fields that can be declared + _field_feature_shapes_kwarg: dict[str, tuple[int, ...]] + _field_feature_dtypes_kwarg: dict[str, FieldFeatureDtype] + # This is the consolidated field features from the defaults and + # the extra ones. + _field_feature_shapes: dict[str, tuple[int, ...]] | None + _field_feature_dtypes: dict[str, FieldFeatureDtype] | None _sparse_fields: tuple[str, Any] _main_rep_idxs: Idxs @@ -740,9 +779,18 @@ class WepyHDF5: ## Partial constructors/initializers - # TODO: make these static and accept the arguments it needs to - # avoid keeping temporary state - def _set_default_init_field_attributes(self, n_dims=None): + @staticmethod + def _gen_default_init_field_attributes( + topology: str, + main_rep_idxs: Idxs | None, + n_dims: int | None = None, + ) -> tuple[ + dict[str, tuple[int, ...]], + dict[str, FieldFeatureDtype], + int, # n_dims + int, # n_coords + NDArray[np.integer], + ]: """Sets the feature_shapes and feature_dtypes to be the default for this module. These will be used to initialize field datasets when no given during construction (i.e. for sparse values) @@ -760,15 +808,16 @@ def _set_default_init_field_attributes(self, n_dims=None): # get the number of coordinates of positions. If there is a # main_reps then we have to set the number of atoms to that, # if not we count the number of atoms in the topology - if self._main_rep_idxs is None: - self._n_coords = json_top_atom_count(self.topology) - self._main_rep_idxs = list(range(self._n_coords)) + if main_rep_idxs is None: + n_coords = json_top_atom_count(topology) + _main_rep_idxs = np.array(range(n_coords)) else: - self._n_coords = len(self._main_rep_idxs) + n_coords = len(main_rep_idxs) + _main_rep_idxs = np.array(main_rep_idxs) # get the number of dimensions as a default if n_dims is None: - self._n_dims = N_DIMS + n_dims = N_DIMS # feature shapes for positions and positions-like fields are # not known at the module level due to different number of @@ -776,17 +825,27 @@ def _set_default_init_field_attributes(self, n_dims=None): # (default 3 spatial). We set them now that we know this # information. # add the postitions shape - field_feature_shapes[POSITIONS] = (self._n_coords, self._n_dims) + field_feature_shapes[POSITIONS] = (n_coords, n_dims) # add the positions-like field shapes (velocities and forces) as the same for poslike_field in POSITIONS_LIKE_FIELDS: - field_feature_shapes[poslike_field] = (self._n_coords, self._n_dims) + field_feature_shapes[poslike_field] = (n_coords, n_dims) + + return ( + field_feature_shapes, + field_feature_dtypes, + n_dims, + n_coords, + _main_rep_idxs, + ) - # set the attributes - self._field_feature_shapes = field_feature_shapes - self._field_feature_dtypes = field_feature_dtypes - + # TODO: make these static and accept the arguments it needs to + # avoid keeping temporary state - def _init_continuations(self): + @classmethod + def _init_continuations( + cls, + h5: h5py.File, + ) -> h5py.Dataset: """This will either create a dataset in the settings for the continuations or if continuations already exist it will reinitialize them and delete the data that exists there. @@ -799,97 +858,137 @@ def _init_continuations(self): # if the continuations dset already exists we reinitialize the # data - if CONTINUATIONS in self.settings_grp: - cont_dset = self.settings_grp[CONTINUATIONS] + if CONTINUATIONS in h5[SETTINGS]: + cont_dset = h5[SETTINGS][CONTINUATIONS] cont_dset.resize((0, 2)) # otherwise we just create the data else: - cont_dset = self.settings_grp.create_dataset( + cont_dset = h5[SETTINGS].create_dataset( CONTINUATIONS, shape=(0, 2), dtype=int, maxshape=(None, 2) ) return cont_dset - - def _create_init(self): + + @classmethod + def _create_init( + cls, + h5: h5py.File, + topology: str, + alt_reps: dict[str, IdxArray] | None = None, + sparse_fields: tuple[str, ...] | None = None, + units: dict[str, str] | None = None, + n_dims: int | None = None, + main_rep_idxs: Idxs | None = None, + field_feature_shapes_overrides: dict[str, tuple[int, ...]] | None = None, + field_feature_dtypes_overrides: dict[str, FieldFeatureDtype] | None = None, + ) -> None: """Creation mode constructor. Completely overwrite the data in the file. Reinitialize the values and set with the new ones if given. """ - assert ( - self._topology is not None - ), "Topology must be given for a creation constructor" + if sparse_fields is None: + _sparse_fields = () + else: + _sparse_fields = sparse_fields + + if alt_reps is None: + _alt_reps = {} + else: + _alt_reps = alt_reps # initialize the runs group - runs_grp = self._h5.create_group(RUNS) + runs_grp = h5.create_group(RUNS) # initialize the settings group - settings_grp = self._h5.create_group(SETTINGS) + settings_grp = h5.create_group(SETTINGS) # create the topology dataset - self._h5.create_dataset(TOPOLOGY, data=self._topology) + h5.create_dataset(TOPOLOGY, data=topology) # sparse fields - if self._sparse_fields is not None: - # make a dataset for the sparse fields allowed. this requires - # a 'special' datatype for variable length strings. This is - # supported by HDF5 but not numpy. - vlen_str_dt = h5py.special_dtype(vlen=str) - - # create the dataset with empty values for the length of the - # sparse fields given - sparse_fields_ds = settings_grp.create_dataset( - SPARSE_FIELDS, - (len(self._sparse_fields),), - dtype=vlen_str_dt, - maxshape=(None,), - ) + + # make a dataset for the sparse fields allowed. this requires + # a 'special' datatype for variable length strings. This is + # supported by HDF5 but not numpy. + vlen_str_dt = h5py.string_dtype(encoding=STRING_ENCODING) + + # create the dataset with empty values for the length of the + # sparse fields given + sparse_fields_dset = settings_grp.create_dataset( + SPARSE_FIELDS, + (len(_sparse_fields),), + dtype=vlen_str_dt, + maxshape=(None,), + ) - # set the flags - for i, sparse_field in enumerate(self._sparse_fields): - sparse_fields_ds[i] = sparse_field + # set the flags + for i, sparse_field in enumerate(_sparse_fields): + sparse_fields_dset[i] = sparse_field # field feature shapes and dtypes # initialize to the defaults, this gives values to - # self._n_coords, and self.field_feature_dtypes, and - # self.field_feature_shapes - self._set_default_init_field_attributes(n_dims=self._n_dims) + # n_coords, n_dims, and field_feature_dtypes, and + # field_feature_shapes + ( + _field_feature_shapes, + _field_feature_dtypes, + _n_dims, + _n_coords, + _main_rep_idxs, + ) = cls._gen_default_init_field_attributes( + topology=topology, + main_rep_idxs=main_rep_idxs, + n_dims=n_dims, + ) # save the number of dimensions and number of atoms in settings - settings_grp.create_dataset(N_DIMS_STR, data=np.array(self._n_dims)) - settings_grp.create_dataset(N_ATOMS, data=np.array(self._n_coords)) + settings_grp.create_dataset(N_DIMS_STR, data=np.array(_n_dims)) + settings_grp.create_dataset(N_ATOMS, data=np.array(_n_coords)) # the main rep atom idxs - settings_grp.create_dataset(MAIN_REP_IDXS, data=self._main_rep_idxs, dtype=int) + settings_grp.create_dataset(MAIN_REP_IDXS, data=_main_rep_idxs, dtype=int) # alt_reps settings alt_reps_idxs_grp = settings_grp.create_group(ALT_REPS_IDXS) - for alt_rep_name, idxs in self._alt_reps.items(): + for alt_rep_name, idxs in _alt_reps.items(): alt_reps_idxs_grp.create_dataset(alt_rep_name, data=idxs, dtype=int) # if both feature shapes and dtypes were specified overwrite # (or initialize if not set by defaults) the defaults - if (self._field_feature_shapes_kwarg is not None) and ( - self._field_feature_dtypes_kwarg is not None + if (field_feature_shapes_overrides is not None) and ( + field_feature_dtypes_overrides is not None ): - self._field_feature_shapes.update(self._field_feature_shapes_kwarg) - self._field_feature_dtypes.update(self._field_feature_dtypes_kwarg) + # check that they have the same keys + if len( + mismatch_keys := ( + set(field_feature_shapes_overrides.keys()).symmetric_difference( + set(field_feature_dtypes_overrides.keys()) + ) + ) + ) > 0: + raise ValueError( + f"Mismatch in the keys for field feature overrides: {mismatch_keys}" + ) + + _field_feature_shapes.update(field_feature_shapes_overrides) + _field_feature_dtypes.update(field_feature_dtypes_overrides) # any sparse field with unspecified shape and dtype must be # set to None so that it will be set at runtime - for sparse_field in self.sparse_fields: - if (sparse_field not in self._field_feature_shapes) or ( - sparse_field not in self._field_feature_dtypes + for sparse_field in _sparse_fields: + if (sparse_field not in _field_feature_shapes) or ( + sparse_field not in _field_feature_dtypes ): - self._field_feature_shapes[sparse_field] = None - self._field_feature_dtypes[sparse_field] = None + _field_feature_shapes[sparse_field] = None + _field_feature_dtypes[sparse_field] = None # save the field feature shapes and dtypes in the settings group shapes_grp = settings_grp.create_group(FIELD_FEATURE_SHAPES_STR) - for field_path, field_shape in self._field_feature_shapes.items(): + for field_path, field_shape in _field_feature_shapes.items(): if field_shape is None: # set it as a dimensionless array of NaN field_shape = np.array(np.nan) @@ -897,35 +996,30 @@ def _create_init(self): shapes_grp.create_dataset(field_path, data=field_shape) dtypes_grp = settings_grp.create_group(FIELD_FEATURE_DTYPES_STR) - for field_path, field_dtype in self._field_feature_dtypes.items(): + for field_path, field_dtype in _field_feature_dtypes.items(): if field_dtype is None: dt_str = NONE_STR else: - # make a json string of the datatype that can be read - # in again, we call np.dtype again because there is no - # np.float.descr attribute - dt_str = json.dumps(np.dtype(field_dtype).descr) + dt_str = numpy_dtype_to_json(np.dtype(field_dtype)) dtypes_grp.create_dataset(field_path, data=dt_str) # initialize the units group - unit_grp = self._h5.create_group(UNITS) + unit_grp = h5.create_group(UNITS) # if units were not given set them all to None - if self._units is None: - self._units = {} - for field_path in self._field_feature_shapes.keys(): - self._units[field_path] = None + if units is None: + units = {} + for field_path in _field_feature_shapes.keys(): + units[field_path] = None # set the units - for field_path, unit_value in self._units.items(): + for field_path, unit_value in units.items(): # ignore the field if not given if unit_value is None: continue - unit_path = "{}/{}".format(UNITS, field_path) - - unit_grp.create_dataset(unit_path, data=unit_value) + unit_grp.create_dataset(field_path, data=unit_value) # create the group for the run data records records_grp = settings_grp.create_group(RECORD_FIELDS) @@ -934,22 +1028,23 @@ def _create_init(self): # (continuation_run, base_run), where the first element # of the new run that is continuing the run in the second # position - self._init_continuations() + cls._init_continuations(h5) + def __init__( self, filename: Path, mode: FileMode = "x", + swmr_mode: bool = False, + expert_mode: bool = False, topology: str | None = None, units: dict[str, str] | None = None, sparse_fields: tuple[str, ...] = None, - feature_shapes: dict[str, FieldShapeSpec] | None = None, - feature_dtypes: dict[str, FieldDtype] | None = None, n_dims: int | None = None, alt_reps: dict[str, IdxArray] | None = None, main_rep_idxs: IdxArray | None = None, - swmr_mode: bool = False, - expert_mode: bool = False, + feature_shapes_overrides: dict[str, FieldShapeSpec] | None = None, + feature_dtypes_overrides: dict[str, FieldDtype] | None = None, ): """Constructor for the WepyHDF5 class. @@ -983,11 +1078,13 @@ def __init__( sparse_fields : list of str, optional List of trajectory fields that should be initialized as sparse. - feature_shapes : dict of str : shape_spec, optional - Mapping of trajectory fields to their shape spec for initialization. + feature_shapes : Mapping of trajectory fields to their shape + spec for initialization. Note that the default OpenMM MD + fields will be generated automatically - feature_dtypes : dict of str : dtype_spec, optional - Mapping of trajectory fields to their shape spec for initialization. + feature_dtypes : Mapping of extra trajectory fields to their + shape spec for initialization. Note that the default + OpenMM MD fields will be generated automatically n_dims : int, default: 3 Set the number of spatial dimensions for the default @@ -1006,18 +1103,6 @@ def __init__( If True no initialization is performed other than the setting of the filename. Useful mainly for debugging. - Raises - ------ - AssertionError - If the mode is not one of the supported mode specs. - - AssertionError - If a topology is not given for a creation mode. - - Warns - ----- - If initialization data was given but the file was opened in a read mode. - """ self.closed = None @@ -1031,6 +1116,8 @@ def __init__( # terminate the constructor here return None + # Validate inputs + if mode not in self.MODES: raise ValueError( f"mode must be either one of: {self.MODES}" @@ -1040,8 +1127,8 @@ def __init__( "topology" : topology, "units" : units, "sparse_fields" : sparse_fields, - "feature_shapes" : feature_shapes, - "feature_dtypes" : feature_dtypes, + "feature_shapes" : feature_shapes_overrides, + "feature_dtypes" : feature_dtypes_overrides, "n_dims" : n_dims, "alt_reps" : alt_reps, "main_rep_idxs" : main_rep_idxs, @@ -1078,7 +1165,7 @@ def __init__( ) - + # Object attributes self._filename = filename self._swmr_mode = swmr_mode @@ -1091,47 +1178,8 @@ def __init__( # used elsewhere and could be a feature in the future. self._h5py_mode = mode - # TODO: cleanup some of these resources so they are not in - # memory if they are large, like the topology - - # Temporary metadata: used to initialize the object but not - # used after that - - self._topology = topology - self._units = units - self._n_dims = n_dims - self._n_coords = None - - # set hidden feature shapes and dtype, which are only - # referenced if needed when trajectories are created. These - # will be saved in the settings section in the actual HDF5 - # file - self._field_feature_shapes_kwarg = feature_shapes - self._field_feature_dtypes_kwarg = feature_dtypes - self._field_feature_dtypes = None - self._field_feature_shapes = None - - # save the sparse fields as a private variable for use in the - # create constructor - if sparse_fields is None: - self._sparse_fields = () - else: - self._sparse_fields = sparse_fields - - # if we specify an atom subset of the main POSITIONS field - # we must save them - self._main_rep_idxs = main_rep_idxs - - # a dictionary specifying other alt_reps to be saved - if alt_reps is not None: - self._alt_reps = alt_reps - # all alt_reps are sparse - alt_rep_keys = [ - "{}/{}".format(ALT_REPS, key) for key in self._alt_reps.keys() - ] - self._sparse_fields.extend(alt_rep_keys) - else: - self._alt_reps = {} + + ## Initialize the file # open the file and then run the different constructors based # on the mode @@ -1151,7 +1199,37 @@ def __init__( if self._wepy_mode in {"w", "x", "w-"}: - self._create_init() + + # Expand some of the inputs + + # save the sparse fields as a private variable for use in the + # create constructor + if sparse_fields is None: + _sparse_fields = () + else: + _sparse_fields = sparse_fields + + # a dictionary specifying other alt_reps to be saved + if alt_reps is not None: + _alt_reps = alt_reps + # all alt_reps are sparse + alt_rep_keys = [ + "{}/{}".format(ALT_REPS, key) for key in _alt_reps.keys() + ] + _sparse_fields.extend(alt_rep_keys) + else: + _alt_reps = {} + + self._create_init( + h5=self._h5, + topology=topology, + sparse_fields=sparse_fields, + units=units, + n_dims=n_dims, + main_rep_idxs=main_rep_idxs, + field_feature_shapes_overrides=feature_dtypes_overrides, + field_feature_dtypes_overrides=feature_dtypes_overrides, + ) # flush the buffers self._h5.flush() @@ -1162,20 +1240,6 @@ def __init__( self._h5.close() - - # get rid of the temporary variables - del self._topology - del self._units - del self._n_dims - del self._n_coords - del self._field_feature_shapes_kwarg - del self._field_feature_dtypes_kwarg - del self._field_feature_shapes - del self._field_feature_dtypes - del self._sparse_fields - del self._main_rep_idxs - del self._alt_reps - # variable to reflect if it is closed or not, should be closed # after initialization self.closed = True @@ -3040,9 +3104,11 @@ def field_feature_dtypes(self): if dtype_str == _NONE_STR: dtypes[field_path] = None else: - dtype_obj = json.loads(dtype_str.decode()) - dtype_obj = [tuple(d) for d in dtype_obj] - dtype = np.dtype(dtype_obj) + dtype_json_to_numpy(dtype_str.decode()) + # TODO: remove + # dtype_obj = json.loads(dtype_str.decode()) + # dtype_obj = [tuple(d) for d in dtype_obj] + # dtype = np.dtype(dtype_obj) dtypes[field_path] = dtype return dtypes @@ -3988,7 +4054,12 @@ def add_continuation(self, continuation_run, base_run): ] ) - def new_run(self, init_walkers, continue_run=None, **kwargs): + def new_run( + self, + init_walkers, + continue_run=None, + **kwargs, + ): """Initialize a new run. Parameters diff --git a/tests/unit/test_hdf5.py b/tests/unit/test_hdf5.py index 9e0e5f32..3cf301c7 100644 --- a/tests/unit/test_hdf5.py +++ b/tests/unit/test_hdf5.py @@ -1,8 +1,12 @@ from pathlib import Path +import json import pytest +import numpy as np import h5py from wepy.hdf5 import ( + numpy_dtype_to_json, + dtype_json_to_numpy, WepyHDF5, _iter_field_paths, ) @@ -31,7 +35,72 @@ # def test__iter_field_paths(wepy_h5_file_ro): # pass - +def test_numpy_dtype_to_json(): + assert json.loads( + numpy_dtype_to_json(np.dtype(np.int32)) + ) == { + "kind" : "simple", + "str" : " Date: Fri, 12 Dec 2025 01:23:59 -0500 Subject: [PATCH 103/143] partial tests for WepyHDF5Reporter --- src/wepy/hdf5.py | 15 +- src/wepy/reporter/hdf5.py | 624 ++++++++++++++++---------- tests/unit/test_hdf5.py | 5 +- tests/unit/test_reporter/test_hdf5.py | 410 +++++++++++++++++ 4 files changed, 819 insertions(+), 235 deletions(-) create mode 100644 tests/unit/test_reporter/test_hdf5.py diff --git a/src/wepy/hdf5.py b/src/wepy/hdf5.py index a9b558da..7b4a4654 100644 --- a/src/wepy/hdf5.py +++ b/src/wepy/hdf5.py @@ -4301,7 +4301,11 @@ def init_run_fields_resampling_decision(self, run_idx, decision_enum_dict): for name, value in decision_enum_dict.items(): decision_grp.create_dataset(name, data=value) - def init_run_fields_resampler(self, run_idx, fields): + def init_run_fields_resampler( + self, + run_idx: int, + fields: list[str], + ) -> h5py.Group: """Initialize this record group fields datasets. Parameters @@ -4377,7 +4381,12 @@ def init_run_fields_bc(self, run_idx, fields): return grp - def init_run_record_grp(self, run_idx, run_record_key, fields): + def init_run_record_grp( + self, + run_idx: int, + run_record_key: str, + fields: list[str], + ) -> h5py.Group: """Initialize a record group for a run. Parameters @@ -4396,6 +4405,8 @@ def init_run_record_grp(self, run_idx, run_record_key, fields): else: grp = self._init_run_continual_record_grp(run_idx, run_record_key, fields) + return grp + # TODO: should've been removed already just double checking things are good without it # def traj_n_frames(self, run_idx, traj_idx): # """ diff --git a/src/wepy/reporter/hdf5.py b/src/wepy/reporter/hdf5.py index b1db8963..70b121c7 100644 --- a/src/wepy/reporter/hdf5.py +++ b/src/wepy/reporter/hdf5.py @@ -1,7 +1,8 @@ # Standard Library +from pathlib import Path import logging +from typing import Self, TypedDict, Literal, Generic, TypeVar -logger = logging.getLogger(__name__) # Standard Library # Third Party Library @@ -9,12 +10,26 @@ # First Party Library from wepy.hdf5 import WepyHDF5 -from wepy.reporter.reporter import FileReporterABC +from wepy.reporter.base import ( + SimComponentArgs, + CycleReportDict, +) +from wepy.reporter.types import ( + FieldShapeSpec, + FieldDtype, +) +from wepy.reporter.file import FileReporterABC, FileMode from wepy.util.json_top import json_top_atom_count from wepy.walker import Walker, WalkerState +from wepy.resampling.resamplers.resampler import Resampler +from wepy.boundary_conditions.boundary import BoundaryConditions +from wepy.typing import Shape, Idxs, IdxArray + +logger = logging.getLogger(__name__) +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) -class WepyHDF5Reporter(FileReporterABC): +class WepyHDF5Reporter(FileReporterABC, Generic[WalkerState_]): """Reporter for generating an HDF5 format (WepyHDF5) data file from simulations. @@ -41,48 +56,82 @@ class WepyHDF5Reporter(FileReporterABC): # by this reporter, e.g. results.wepy.h5 SUGGESTED_EXTENSIONS = ("wepy.h5",) + # static attributes + swmr_mode: bool + save_fields: tuple[str, ...] | None + _sparse_fields: dict[str, int] + _feature_shapes: dict[str, FieldShapeSpec] | None + _feature_dtypes: dict[str, FieldDtype] | None + _n_dims: int + resampling_fields: tuple[str, ...] + decision_enum_dict: dict[str, int] + resampler_fields: tuple[str, ...] | None + warping_fields: tuple[str, ...] | None + progress_fields: tuple[str, ...] | None + bc_fields: tuple[str, ...] | None + resampling_records: tuple[str, ...] | None + resampler_records: tuple[str, ...] | None + bc_records: tuple[str, ...] | None + warping_records: tuple[str, ...] | None + progress_records: tuple[str, ...] | None + main_rep_idxs: IdxArray | None + alt_reps_to_save: list[str] + alt_reps_idxs: dict[str, IdxArray] + _n_atoms: int + _all_atom_idxs: IdxArray + _sparse_fields: dict[str, int] + units: dict[str, str] + + # stateful attributes + wepy_h5: WepyHDF5 | None + wepy_run_idx: int | None + + _tmp_topology: str | None + def __init__( self, - save_fields=None, - topology=None, - units=None, - sparse_fields=None, - feature_shapes=None, - feature_dtypes=None, - n_dims=None, - main_rep_idxs=None, - all_atoms_rep_freq=None, - # dictionary of alt_rep keys and a tuple of (idxs, freq) - alt_reps=None, - # pass in the resampler and boundary - # conditions classes to automatically extract the - # needed data, the objects themselves are not saves - resampler=None, - boundary_conditions=None, - # or pass the things we need from them in manually - resampling_fields=None, - decision_enum_dict=None, - resampler_fields=None, - warping_fields=None, - progress_fields=None, - bc_fields=None, - resampling_records=None, - resampler_records=None, - warping_records=None, - bc_records=None, - progress_records=None, - # other settings - swmr_mode=False, - **kwargs, + file_path: Path, + topology: str, + # Resampling features + decision_enum_dict: dict[str, int], + resampling_fields: tuple[str, ...], + swmr_mode: bool = False, + save_fields: tuple[str, ...] | None = None, + units: dict[str, str] | None = None, + sparse_fields: dict[str, int | Literal[Ellipsis]] | None = None, + n_dims: int = 3, + main_rep_idxs: Idxs | None = None, + all_atoms_rep_freq: int | None = None, + alt_reps: dict[str, tuple[Idxs, int]] | None = None, + # TOREV: are these feature fields actually needed for the main + # trajectories? I never used them. If they are useful they + # should be derived from runner metadata in the common case. I + # think in most cases they are determined dynamically, so this + # needs to be amended. + feature_shapes: dict[str, FieldShapeSpec] | None = None, + feature_dtypes: dict[str, FieldDtype] | None = None, + # Resampling optionals + resampling_records: tuple[str, ...] | None = None, + # Resampler fields are optional + resampler_fields: tuple[str, ...] | None = None, + resampler_records: tuple[str, ...] | None = None, + # BC features, optional + warping_fields: tuple[str, ...] | None = None, + progress_fields: tuple[str, ...] | None = None, + bc_fields: tuple[str, ...] | None = None, + warping_records: tuple[str, ...] | None = None, + bc_records: tuple[str, ...] | None = None, + progress_records: tuple[str, ...] | None = None, ): """Constructor for the WepyHDF5Reporter. Parameters ---------- - save_fields : tuple of str, default: None - A selection of fields from the walker states to be - stored. Allows for the ignoring of some states. If None all - fields from states will attempted to be saved. + + save_fields : A selection of fields from the walker states to + be stored. Allows for the ignoring of some states. If None + all fields from states will attempted to be saved. To not + save anything provide an empty tuple (). topology : str JSON string representing topology of system being simulated. @@ -94,11 +143,13 @@ def __init__( sparse_fields : dict of str: int, optional List of trajectory fields that should be initialized as sparse. - feature_shapes : dict of str: shape_spec, optional - Mapping of trajectory fields to their shape spec for initialization. + feature_shapes : Mapping of trajectory fields to their shape + spec for initialization. Note that these are extras and the + defaults for OpenMM MD will automatically be configured. - feature_dtypes : dict of str: dtype_spec, optional - Mapping of trajectory fields to their shape spec for initialization. + feature_dtypes : Mapping of trajectory fields to their shape + spec for initialization. Note that these are extras and the + defaults for OpenMM MD will automatically be configured. n_dims : int, default: 3 Set the number of spatial dimensions for the default @@ -122,23 +173,6 @@ def __init__( atoms in a simulation. Will be set as the field 'alt_rep/all_atoms'. - resampler : Resampler object, optional but recommended - The resampler being used for the simulation. Is used as a - convenient container for a variety of constants needed for - specifying data for the resampling records. If this is not - given then these of the Other Parameters below must be - specified manually: resampling_fields, decision_enum_dict, - resampler_fields, resampling_records, resampler_records. - - boundary_conditions : BoundaryConditions object, optional but recommended - The boundary conditions being used for the simulation. Is - used as a convenient container for a variety of constants - needed for specifying data for the warping and progress - records. If this is not given then these of the Other - Parameters below must be specified manually: - warping_fields, progress_fields, bc_fields, - warping_records, bc_records, progress_records - swmr_mode : bool Whether to write to open the HDF5 in single-writer multi-reader (SWMR) mode. @@ -188,113 +222,76 @@ def __init__( """ # initialize inherited attributes - super().__init__(**kwargs) + super().__init__( + file_paths=[file_path], + # hardcode creation mode + modes=["x"], + ) # set the preference for swmr mode, True or False, if this is # True then SWMR mode will be turned on when the file is # written to during reporting self.swmr_mode = swmr_mode - # do all the WepyHDF5 specific stuff - self.wepy_run_idx = None self._tmp_topology = topology + # which fields from the walker to save, if None then save all of them self.save_fields = save_fields - # dictionary of sparse_field_name -> int : frequency of cycles - # to save the field - # TODO: refine requirements of sparse fields. Do they need to - # be in the 'save_fields'? - - self._sparse_fields = ( - {field_name: freq for field_name, freq in sparse_fields.items()} - if sparse_fields is not None - else {} - ) - self._feature_shapes = feature_shapes - self._feature_dtypes = feature_dtypes - self._n_dims = n_dims + # check sparse fields + if sparse_fields is not None: - # get and set the record fields (naems, shapes, dtypes) for - # the resampler and the boundary conditions - if (resampling_fields is not None) and (decision_enum_dict is not None): - self.resampling_fields = resampling_fields - self.decision_enum = decision_enum_dict - elif resampler is not None: - self.resampling_fields = resampler.resampling_fields() - self.decision_enum = resampler.DECISION.enum_dict_by_name() - else: - self.resampling_fields = None - self.decision_enum = None + if self.save_fields is None: + raise ValueError( + f"The sparse fields were requested ({set(sparse_fields.keys())}) but no save fields requested." + ) + + _missing_save_fields = set( + sparse_key + for sparse_key + in sparse_fields.keys() + if sparse_key not in self.save_fields + ) - if resampler_fields is not None: - self.resampler_fields = resampler_fields() - elif resampler is not None: - self.resampler_fields = resampler.resampler_fields() - else: - self.resampler_fields = None + if len(_missing_save_fields) > 0: + raise ValueError( + f"The sparse fields were requested ({_missing_save_fields}) but are not in" + f" requested save fields ({self.save_fields})" + ) - if warping_fields is not None: - self.warping_fields = warping_fields() - elif boundary_conditions is not None: - self.warping_fields = boundary_conditions.warping_fields() - else: - self.warping_fields = None + self._sparse_fields = sparse_fields - if progress_fields is not None: - self.progress_fields = progress_fields() - elif boundary_conditions is not None: - self.progress_fields = boundary_conditions.progress_fields() else: - self.progress_fields = None + self._sparse_fields = {} - if bc_fields is not None: - self.bc_fields = bc_fields() - elif boundary_conditions is not None: - self.bc_fields = boundary_conditions.bc_fields() - else: - self.bc_fields = None + self._feature_shapes = feature_shapes + self._feature_dtypes = feature_dtypes + self._n_dims = n_dims - # the fields which are records for table like reports - if resampling_records is not None: - self.resampling_records = resampling_records - elif resampler is not None: - self.resampling_records = resampler.resampling_record_field_names() - else: - self.resampling_records = None - if resampler_records is not None: - self.resampler_records = resampler_records - elif resampler is not None: - self.resampler_records = resampler.resampler_record_field_names() - else: - self.resampler_records = None + # required resampling fields + self.resampling_fields = resampling_fields + self.decision_enum_dict = decision_enum_dict - if bc_records is not None: - self.bc_records = bc_records - elif boundary_conditions is not None: - self.bc_records = boundary_conditions.bc_record_field_names() - else: - self.bc_records = None + # optional resampler fields + self.resampler_fields = resampler_fields + self.resampling_records = resampling_records + self.resampler_records = resampler_records - if warping_records is not None: - self.warping_records = warping_records - elif boundary_conditions is not None: - self.warping_records = boundary_conditions.warping_record_field_names() - else: - self.warping_records = None + # BC fields, optional + self.warping_fields = warping_fields + self.progress_fields = progress_fields + self.bc_fields = bc_fields - if progress_records is not None: - self.progress_records = progress_records - elif boundary_conditions is not None: - self.progress_records = boundary_conditions.progress_record_field_names() - else: - self.progress_records = None + # the fields which are records for table like reports + self.bc_records = bc_records + self.warping_records = warping_records + self.progress_records = progress_records # the atom indices of the whole system that will be saved as # the main positions representation - self.main_rep_idxs = main_rep_idxs + self.main_rep_idxs = np.array(main_rep_idxs) if main_rep_idxs is not None else None # the idxs for alternate representations of the system # positions @@ -302,13 +299,21 @@ def __init__( # this is a record of which alt_reps to actually save in the simulation self.alt_reps_to_save = [] if alt_reps is not None: - self.alt_reps_idxs = {key: list(tup[0]) for key, tup in alt_reps.items()} + self.alt_reps_idxs = { + key: np.array(idxs) + for key, (idxs, frequence) + in alt_reps.items() + } # add the frequencies for these alt_reps to the # sparse_fields frequency dictionary for key, (idxs, freq) in alt_reps.items(): - self.alt_reps_to_save.append(key) + if len(idxs) == 0: + raise ValueError( + f"No indices given for sparse field: {key}" + ) + alt_rep_key = "alt_reps/{}".format(key) @@ -317,10 +322,16 @@ def __init__( # very innefficient in comparison if freq is Ellipsis or freq == 1 or freq == 0: pass - else: + elif freq > 0: self._sparse_fields[alt_rep_key] = freq - self.alt_reps_idxs[key] = list(idxs) + else: + raise ValueError( + f"Invalid frequency specifier ({freq}) for sparse field '{key}'" + ) + + self.alt_reps_to_save.append(key) + self.alt_reps_idxs[key] = np.array(idxs) else: self.alt_reps_idxs = {} @@ -363,29 +374,206 @@ def __init__( else: self.units = units - def init(self, continue_run=None, init_walkers=None, **kwargs): - # do the inherited stuff - super().init(**kwargs) + @classmethod + def from_components( + self, + file_path: Path, + topology: str, + resampler_class: type[Resampler], + feature_shapes: dict[str, FieldShapeSpec] | None = None, + feature_dtypes: dict[str, FieldDtype] | None = None, + boundary_conditions_class: type[BoundaryConditions] | None = None, + swmr_mode: bool = False, + save_fields: tuple[str, ...] | None = None, + units: dict[str, str] | None = None, + sparse_fields: dict[str, int] | None = None, + n_dims: int = 3, + main_rep_idxs: Idxs | None = None, + all_atoms_rep_freq: int | None = None, + alt_reps: dict[str, tuple[Idxs, int]]=None, + ) -> Self: + """Construct reporter from simulation components. + + Does introspection on components to get information. Does not + save these objects as state. + + Parameters + ---------- - # open and initialize the HDF5 file - logger.info("Initializing HDF5 file at {}".format(self.file_path)) + resampler : Resampler object, optional but recommended + The resampler being used for the simulation. Is used as a + convenient container for a variety of constants needed for + specifying data for the resampling records. If this is not + given then these of the Other Parameters below must be + specified manually: resampling_fields, decision_enum_dict, + resampler_fields, resampling_records, resampler_records. - self.wepy_h5 = WepyHDF5( - self.file_path, - mode=self.mode, - topology=self._tmp_topology, - units=self.units, - sparse_fields=list(self._sparse_fields.keys()), - feature_shapes=self._feature_shapes, - feature_dtypes=self._feature_dtypes, - n_dims=self._n_dims, - main_rep_idxs=self.main_rep_idxs, - alt_reps=self.alt_reps_idxs, + boundary_conditions : BoundaryConditions object, optional but recommended + The boundary conditions being used for the simulation. Is + used as a convenient container for a variety of constants + needed for specifying data for the warping and progress + records. If this is not given then these of the Other + Parameters below must be specified manually: + warping_fields, progress_fields, bc_fields, + warping_records, bc_records, progress_records + + """ + + if boundary_conditions_class is not None: + warping_fields = boundary_conditions_class.warping_fields() + progress_fields = boundary_conditions_class.progress_fields() + bc_fields = boundary_conditions_class.bc_fields() + bc_records = boundary_conditions_class.bc_record_field_names() + warping_records = boundary_conditions_class.warping_record_field_names() + progress_records = boundary_conditions_class.progress_record_field_names() + + else: + warping_fields = None + progress_fields = None + bc_fields = None + bc_records = None + warping_records = None + progress_records = None + + + return WepyHDF5Reporter( + file_path=file_path, + topology=topology, + feature_shapes=feature_shapes, + feature_dtypes=feature_dtypes, + swmr_mode=swmr_mode, + save_fields=save_fields, + units=units, + sparse_fields=sparse_fields, + n_dims=n_dims, + main_rep_idxs=main_rep_idxs, + all_atoms_rep_freq=all_atoms_rep_freq, + alt_reps=alt_reps, + # components + resampling_fields=resampler_class.resampling_fields(), + decision_enum_dict=resampler_class.DECISION.enum_dict_by_name(), + resampler_fields = resampler_class.resampler_fields(), + resampling_records = resampler_class.resampling_record_field_names(), + resampler_records = resampler_class.resampler_record_field_names(), + warping_fields=warping_fields, + progress_fields=progress_fields, + bc_fields=bc_fields, + bc_records=bc_records, + warping_records=warping_records, + progress_records=progress_records, ) + @property + def file_path(self) -> Path: + return self.file_paths[0] + + @property + def mode(self) -> FileMode: + return self.modes[0] + + @staticmethod + def _initialize_h5_run( + wepy_h5: WepyHDF5, + init_walkers: list[Walker[WalkerState_]], + resampling_fields: tuple[str, ...], + decision_enum_dict: dict[str, int], + continue_run: int | None = None, + resampler_fields: tuple[str, ...] | None = None, + warping_fields: tuple[str, ...] | None = None, + progress_fields: tuple[str, ...] | None = None, + bc_fields: tuple[str, ...] | None = None, + resampling_records: tuple[str, ...] | None = None, + resampler_records: tuple[str, ...] | None = None, + bc_records: tuple[str, ...] | None = None, + warping_records: tuple[str, ...] | None = None, + progress_records: tuple[str, ...] | None = None, + ) -> int: + """Initialize the WepyHDF5 data structures.""" + + if wepy_h5.mode != "r+": + raise IOError(f"wepy_h5 must be in non-creation read-write mode (r+), in '{wepy_h5.mode}'") + + if not wepy_h5.closed: + raise IOError("WepyHDF5 is already open, must be closed.") + + with wepy_h5: + # if this is a continuation run of another run we want to + # initialize it as such + + # initialize a new run, we don't know which run it will be + # until it is created. + run_grp = wepy_h5.new_run( + init_walkers, + continue_run=continue_run, + ) + wepy_run_idx = run_grp.attrs["run_idx"] + + # initialize the run record groups using their fields + wepy_h5.init_run_fields_resampling( + wepy_run_idx, + resampling_fields, + ) + # the enumeration for the values of resampling + wepy_h5.init_run_fields_resampling_decision( + wepy_run_idx, + decision_enum_dict, + ) + + if resampler_fields is not None: + wepy_h5.init_run_fields_resampler( + wepy_run_idx, + resampler_fields, + ) + # set the fields that are records for tables etc. unless + # they are already set + if resampling_records is not None and "resampling" not in wepy_h5.record_fields: + wepy_h5.init_record_fields( + "resampling", + resampling_records, + ) + if resampler_records is not None and "resampler" not in wepy_h5.record_fields: + wepy_h5.init_record_fields( + "resampler", + resampler_records, + ) + + # if there were no warping fields set there is no boundary + # conditions and we don't initialize them + if warping_fields is not None: + wepy_h5.init_run_fields_warping( + wepy_run_idx, + warping_fields, + ) + wepy_h5.init_run_fields_progress( + wepy_run_idx, + progress_fields, + ) + wepy_h5.init_run_fields_bc( + wepy_run_idx, + bc_fields, + ) + # table records + if "warping" not in wepy_h5.record_fields: + wepy_h5.init_record_fields("warping", warping_records) + if "boundary_conditions" not in wepy_h5.record_fields: + wepy_h5.init_record_fields( + "boundary_conditions", bc_records + ) + if "progress" not in wepy_h5.record_fields: + wepy_h5.init_record_fields("progress", progress_records) + + return wepy_run_idx + + def init(self, **kwargs: SimComponentArgs) -> None: + + # TODO: remove dynamic configuration. Instead replace with + # static configuration from the Runner for good defaults. + + ## Do checks on the inputs and figure out runtime field metadata + # if we specify save fields only save these for the initial walkers if self.save_fields is not None: - state_fields = list(init_walkers[0].state.dict().keys()) + state_fields = list(kwargs["init_walkers"][0].state.dict().keys()) # make sure all the save_fields are present in the state assert all( @@ -396,7 +584,7 @@ def init(self, continue_run=None, init_walkers=None, **kwargs): ), "Not all specified save_fields present in walker states" filtered_init_walkers = [] - for walker in init_walkers: + for walker in kwargs["init_walkers"]: # make a new state by filtering the attributes of the old ones state_d = { k: v @@ -432,77 +620,53 @@ def init(self, continue_run=None, init_walkers=None, **kwargs): filtered_init_walkers.append(Walker(new_state, walker.weight)) # otherwise save the full state else: - filtered_init_walkers = init_walkers + filtered_init_walkers = kwargs["init_walkers"] - self.wepy_h5.set_mode(mode="r+") - with self.wepy_h5: - # if this is a continuation run of another run we want to - # initialize it as such + # Run the constructor intialization + logger.info(f"Initializing HDF5 file at {self.file_path}") - # initialize a new run - run_grp = self.wepy_h5.new_run( - filtered_init_walkers, - continue_run=continue_run, - ) - self.wepy_run_idx = run_grp.attrs["run_idx"] + init_wepy_h5 = WepyHDF5( + self.file_path, + mode="x", + topology=self._tmp_topology, + units=self.units, + sparse_fields=list(self._sparse_fields.keys()), + feature_shapes_overrides=self._feature_shapes, + feature_dtypes_overrides=self._feature_dtypes, + n_dims=self._n_dims, + main_rep_idxs=self.main_rep_idxs, + alt_reps=self.alt_reps_idxs, + ) - # initialize the run record groups using their fields - self.wepy_h5.init_run_fields_resampling( - self.wepy_run_idx, - self.resampling_fields, - ) - # the enumeration for the values of resampling - self.wepy_h5.init_run_fields_resampling_decision( - self.wepy_run_idx, - self.decision_enum, - ) - self.wepy_h5.init_run_fields_resampler( - self.wepy_run_idx, - self.resampler_fields, - ) - # set the fields that are records for tables etc. unless - # they are already set - if "resampling" not in self.wepy_h5.record_fields: - self.wepy_h5.init_record_fields( - "resampling", - self.resampling_records, - ) - if "resampler" not in self.wepy_h5.record_fields: - self.wepy_h5.init_record_fields( - "resampler", - self.resampler_records, - ) + # delete the topology as it isn't needed anymore and we can + # get it from the HDF5. This will alleviate some memory + # pressure for large topologies + del self._tmp_topology + self._tmp_topology = None - # if there were no warping fields set there is no boundary - # conditions and we don't initialize them - if self.warping_fields is not None: - self.wepy_h5.init_run_fields_warping( - self.wepy_run_idx, - self.warping_fields, - ) - self.wepy_h5.init_run_fields_progress( - self.wepy_run_idx, - self.progress_fields, - ) - self.wepy_h5.init_run_fields_bc( - self.wepy_run_idx, - self.bc_fields, - ) - # table records - if "warping" not in self.wepy_h5.record_fields: - self.wepy_h5.init_record_fields("warping", self.warping_records) - if "boundary_conditions" not in self.wepy_h5.record_fields: - self.wepy_h5.init_record_fields( - "boundary_conditions", self.bc_records - ) - if "progress" not in self.wepy_h5.record_fields: - self.wepy_h5.init_record_fields("progress", self.progress_records) - - # if this was opened in a truncation mode, we don't want to - # overwrite old runs with future calls to init(). so we - # change the mode to read/write 'r+' - if self.mode == "w": - self.set_mode(0, "r+") + # then the file that is the actual attribute is opened in + # read-write non-create mode + self.wepy_h5 = WepyHDF5( + self.file_path, + mode='r+', + ) + + self._initialize_h5_run( + self.wepy_h5, + init_walkers=filtered_init_walkers, + continue_run=kwargs["continue_run"], + resampling_fields=self.resampling_fields, + decision_enum_dict=self.decision_enum_dict, + resampler_fields=self.resampler_fields, + warping_fields=self.warping_fields, + progress_fields=self.progress_fields, + bc_fields=self.bc_fields, + resampling_records=self.resampling_records, + resampler_records=self.resampler_records, + bc_records=self.bc_records, + warping_records=self.warping_records, + progress_records=self.progress_records, + ) def cleanup(self, **kwargs): # it should be already closed at this point but just in case diff --git a/tests/unit/test_hdf5.py b/tests/unit/test_hdf5.py index 3cf301c7..161084d7 100644 --- a/tests/unit/test_hdf5.py +++ b/tests/unit/test_hdf5.py @@ -561,7 +561,6 @@ def test__create_init(self, tmp_path_factory): assert "positions" in h5["units"] assert len(h5["units"]) == 1 assert h5["units/positions"][()].decode() == "nanometer" - # def test_new_run(self): # pass @@ -572,8 +571,8 @@ def test__create_init(self, tmp_path_factory): # def test_init_run_fields_resampling_decision(self): # pass - # def test_init_run_fields_resampler(self): - # pass + def test_init_run_fields_resampler(self): + pass # def test_init_record_fields(self): # pass diff --git a/tests/unit/test_reporter/test_hdf5.py b/tests/unit/test_reporter/test_hdf5.py new file mode 100644 index 00000000..dca6d228 --- /dev/null +++ b/tests/unit/test_reporter/test_hdf5.py @@ -0,0 +1,410 @@ +import pytest +import numpy as np +from wepy.reporter.hdf5 import WepyHDF5Reporter +from wepy_tools.systems.lennard_jones import LennardJonesPair +from wepy.resampling.resamplers.noresampler import NoResampler +from wepy.walker import Walker +from wepy.runners.mock import MockState, MockRunner +from wepy.work_mapper.serial import SerialMapper +from wepy.hdf5 import WepyHDF5 +from wepy.resampling.decisions.no_decision import ( + NoDecision, + NothingDecisionEnum, +) + +SIM_COMPONENTS = { + "init_walkers": [ + Walker( + MockState(1), + weight=0.2, + ), + Walker( + MockState(1), + weight=0.1, + ), + ], + "runner": MockRunner(), + "resampler": NoResampler(), + "boundary_conditions": None, + "work_mapper": SerialMapper(), + "reporters": [], + "continue_run": None, +} + +RESAMPLER_REPORTER_ARGS = { + "resampling_fields" : NoResampler.resampling_fields(), + "decision_enum_dict" : NoDecision.enum_dict_by_name(), +} + +class Test_WepyHDF5Reporter: + + def test___init__(self, tmp_path_factory): + + # TODO: should just use LJ pair for speed, save this for e2e + # tests + test_sys = LennardJonesPair() + n_atoms = test_sys.mdj_top.n_atoms + + # NOTE: we don't need multiple of these because in this test + # we don't actually write to a file yet + d0 = tmp_path_factory.mktemp("0") + + h5_path = d0 / "main.wepy.h5" + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + ) + + assert reporter.file_path == h5_path + assert reporter.mode == "x" + assert reporter.file_paths == [h5_path] + assert reporter.modes == ["x"] + + assert not reporter.swmr_mode + assert reporter.wepy_run_idx is None + + assert reporter._sparse_fields == {} + assert reporter.alt_reps_to_save == [] + assert "all_atoms" in reporter.alt_reps_idxs + assert np.array_equal( + reporter.alt_reps_idxs["all_atoms"], + np.array(range(n_atoms)), + ) + assert reporter.main_rep_idxs is None + + assert reporter.units == {} + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + save_fields=("positions", "box_vectors",) + ) + + assert reporter.save_fields == ("positions", "box_vectors",) + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + units={ + "positions" : "nanometer", + } + ) + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + main_rep_idxs=[0,1,2,3], + ) + assert np.array_equal( + reporter.main_rep_idxs, + np.array([0,1,2,3]) + ) + assert set(reporter.alt_reps_idxs.keys()) == {"all_atoms"} + assert "alt_reps/all_atoms" not in reporter.alt_reps_to_save + assert "alt_reps/all_atoms" not in reporter._sparse_fields + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + all_atoms_rep_freq=5, + ) + assert set(reporter.alt_reps_idxs.keys()) == {"all_atoms"} + assert "all_atoms" in reporter.alt_reps_to_save + assert reporter._sparse_fields == { + "alt_reps/all_atoms" : 5 + } + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + save_fields=("positions", "velocities",), + sparse_fields={ + "velocities" : 5, + }, + ) + + assert "velocities" in reporter._sparse_fields + assert reporter._sparse_fields["velocities"] == 5 + + with pytest.raises(ValueError): + WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + save_fields=("positions",), + sparse_fields={ + "velocities" : 5, + }, + ) + + with pytest.raises(ValueError): + WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + save_fields=None, + sparse_fields={ + "velocities" : 5, + }, + ) + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + save_fields=("positions",), + alt_reps={ + "a" : ( + [0,1,2,3], + 5, + ), + } + ) + + assert "a" in reporter.alt_reps_to_save + assert "a" in reporter.alt_reps_idxs + assert np.array_equal( + reporter.alt_reps_idxs["a"], + np.array([0,1,2,3]), + ) + assert "alt_reps/a" in reporter._sparse_fields + assert reporter._sparse_fields["alt_reps/a"] == 5 + + with pytest.raises(ValueError): + WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + save_fields=("positions",), + alt_reps={ + "a" : ( + [], + 5, + ), + } + ) + + with pytest.raises(ValueError): + WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + save_fields=("positions",), + alt_reps={ + "a" : ( + [0,1,2,3], + -1, + ), + } + ) + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + save_fields=("positions",), + alt_reps={ + "a" : ( + [0,1,2,3], + 1, + ), + } + ) + assert "a" in reporter.alt_reps_to_save + assert "a" in reporter.alt_reps_idxs + assert "alt_reps/a" not in reporter._sparse_fields + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + save_fields=("positions",), + alt_reps={ + "a" : ( + [0,1,2,3], + Ellipsis, + ), + } + ) + assert "a" in reporter.alt_reps_to_save + assert "a" in reporter.alt_reps_idxs + assert "alt_reps/a" not in reporter._sparse_fields + + def test_from_components(self, tmp_path_factory): + + test_sys = LennardJonesPair() + + d0 = tmp_path_factory.mktemp("0") + h5_path = d0 / "main.wepy.h5" + + WepyHDF5Reporter.from_components( + file_path=h5_path, + topology=test_sys.json_top, + resampler_class=NoResampler, + ) + + def test__initialize_h5_run(self, tmp_path_factory): + test_sys = LennardJonesPair() + + d0 = tmp_path_factory.mktemp("0") + h5_path = d0 / "main.wepy.h5" + + # create the starting file + WepyHDF5( + h5_path, + mode="x", + topology=test_sys.json_top, + ) + + wepy_h5 = WepyHDF5( + h5_path, + mode="r+", + ) + + common_kwargs = dict( + init_walkers=[ + Walker( + MockState(1), + 0.1, + ), + Walker( + MockState(2), + 0.2, + ), + ], + continue_run=None, + ) + + run_idx = WepyHDF5Reporter._initialize_h5_run( + wepy_h5=wepy_h5, + **RESAMPLER_REPORTER_ARGS, + **common_kwargs, + ) + assert run_idx == 0 + + wepy_h5_ro = WepyHDF5( + h5_path, + mode="r", + ) + + with pytest.raises(IOError): + WepyHDF5Reporter._initialize_h5_run( + wepy_h5=wepy_h5_ro, + **RESAMPLER_REPORTER_ARGS, + **common_kwargs, + ) + + # test that what we want is created + + wepy_h5_ro.open() + assert str(run_idx) in wepy_h5_ro.h5["runs"] + assert set(wepy_h5_ro.h5["runs/0"].keys()) == { + "decision", + "init_walkers", + "resampling", + "trajectories" + } + + # decision + assert "NOTHING" in wepy_h5_ro.h5["runs/0/decision"] + assert wepy_h5_ro.h5["runs/0/decision/NOTHING"][()] == np.int64(0) + + # resampling + assert set(wepy_h5_ro.h5["runs/0/resampling"].keys()) == { + "_cycle_idxs", + "decision_id", + "step_idx", + "target_idxs", + "walker_idx", + } + + # trajectories + assert len(wepy_h5_ro.h5["runs/0/trajectories"].keys()) == 0 + + # resampling records + assert wepy_h5_ro.h5["runs/0/resampling/_cycle_idxs"].shape == (0,) + assert wepy_h5_ro.h5["runs/0/resampling/decision_id"].shape == (0,1) + assert wepy_h5_ro.h5["runs/0/resampling/step_idx"].shape == (0,1) + assert wepy_h5_ro.h5["runs/0/resampling/walker_idx"].shape == (0,1) + + # init walkers + assert len(wepy_h5_ro.h5["runs/0/init_walkers"]) == 2 + assert set(wepy_h5_ro.h5["runs/0/init_walkers"].keys()) == {"0", "1"} + assert set(wepy_h5_ro.h5["runs/0/init_walkers/0"].keys()) == {"a", "weights"} + + # TOREV: weights are shape (1,1) don't see a good reason why, + # but I think there was something about this. Perhaps instead + # the 'a' field should be nested this way + assert np.array_equal( + wepy_h5_ro.h5["runs/0/init_walkers/0/a"][:], + np.array([1]) + ) + assert np.array_equal( + wepy_h5_ro.h5["runs/0/init_walkers/1/a"][:], + np.array([2]) + ) + + assert np.array_equal( + wepy_h5_ro.h5["runs/0/init_walkers/0/weights"][:], + np.array([[0.1]]) + ) + assert np.array_equal( + wepy_h5_ro.h5["runs/0/init_walkers/1/weights"][:], + np.array([[0.2]]) + ) + + wepy_h5_ro.close() + + common_kwargs = dict( + init_walkers=[ + Walker( + MockState(1), + 0.1, + ), + Walker( + MockState(2), + 0.2, + ), + ], + continue_run=0, + ) + run_idx = WepyHDF5Reporter._initialize_h5_run( + wepy_h5=wepy_h5, + **RESAMPLER_REPORTER_ARGS, + **common_kwargs, + ) + + assert run_idx == 1 + wepy_h5_ro.open() + assert "1" in wepy_h5_ro.h5["runs"] + assert wepy_h5_ro.h5["_settings/continuations"].shape == (1, 2) + assert np.array_equal( + wepy_h5_ro.h5["_settings/continuations"][:], + np.array([ + [1, 0] + ]) + ) + + + + def test_init(self, tmp_path_factory): + + test_sys = LennardJonesPair() + n_atoms = test_sys.mdj_top.n_atoms + + d0 = tmp_path_factory.mktemp("0") + h5_path = d0 / "main.wepy.h5" + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + ) + + reporter.init(**SIM_COMPONENTS) From 0a08c888ce595e7558775d9e3c316b3cb2dc5278 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 12 Dec 2025 13:08:55 -0500 Subject: [PATCH 104/143] change OpenMMState.to_dict to .dict This is to satisfy the WalkerState interface. --- src/wepy/runners/openmm/state.py | 4 ++-- src/wepy_tools/systems/alanine_dipeptide.py | 2 +- tests/unit/test_runners/test_openmm/test_state.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/wepy/runners/openmm/state.py b/src/wepy/runners/openmm/state.py index f2bd7603..87149085 100644 --- a/src/wepy/runners/openmm/state.py +++ b/src/wepy/runners/openmm/state.py @@ -817,7 +817,7 @@ def from_state_wrapper(cls, state_wrapper: OpenMMStateWrapper) -> Self: def from_state(cls, state: openmm.State) -> Self: return cls.from_state_wrapper(OpenMMStateWrapper(state)) - def to_dict(self) -> StateFieldData: + def dict(self) -> StateFieldData: return StateFieldData( { k: v @@ -841,7 +841,7 @@ def to_state_wrapper( """ if system is not None: - wrapper = OpenMMStateWrapper.from_dict(system, self.to_dict()) + wrapper = OpenMMStateWrapper.from_dict(system, self.dict()) else: diff --git a/src/wepy_tools/systems/alanine_dipeptide.py b/src/wepy_tools/systems/alanine_dipeptide.py index c88f1037..ec96cd95 100644 --- a/src/wepy_tools/systems/alanine_dipeptide.py +++ b/src/wepy_tools/systems/alanine_dipeptide.py @@ -62,7 +62,7 @@ def image(self, state: OpenMMState) -> AlanineDipeptideRamachandranDistanceImage state_dict = { # traj shape to match interface requirements key: np.array([quantity.value_in_unit(_unit)]) - for key, quantity in state.to_dict().items() + for key, quantity in state.dict().items() if key in {"positions", "box_vectors"} } traj = traj_fields_to_mdtraj( diff --git a/tests/unit/test_runners/test_openmm/test_state.py b/tests/unit/test_runners/test_openmm/test_state.py index 7c3ba0e7..68182004 100644 --- a/tests/unit/test_runners/test_openmm/test_state.py +++ b/tests/unit/test_runners/test_openmm/test_state.py @@ -337,7 +337,7 @@ def test___eq__(self, omm_context): assert state_wrapper1 == state_wrapper2 - def test_to_dict(self, omm_context): + def test_dict(self, omm_context): state = omm_context.getState() @@ -890,7 +890,7 @@ def test_from_dwim(self): positions=positions, ) - def test_to_dict(self): + def test_dict(self): time = 0.0 * openmm.unit.picosecond bvs = UNIT_CUBE * openmm.unit.nanometer @@ -933,7 +933,7 @@ def test_to_dict(self): forces=forces, ) - osd = os.to_dict() + osd = os.dict() assert set(osd.keys()) == { "time", "box_vectors", From 1cf5023936744b03cefa1d7a3328ebb422ea740e Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 12 Dec 2025 13:09:18 -0500 Subject: [PATCH 105/143] add WalkerStateBox class An opaque wrapper for a WalkerState that doesn't have specific types. Used for testing and for data processing in reporters --- src/wepy/walker.py | 29 ++++++++++++++++++++++++++++- tests/unit/test_walker.py | 23 ++++++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/wepy/walker.py b/src/wepy/walker.py index f93576c0..b40b397f 100644 --- a/src/wepy/walker.py +++ b/src/wepy/walker.py @@ -31,6 +31,7 @@ import math import random as rand from typing import Any, Generic, Protocol, TypeVar +import copy # Third Party Library import attrs @@ -62,7 +63,33 @@ def __getitem__(self, key: str) -> Any: def dict(self) -> dict[str, Any]: return attrs.asdict(self) - + +class WalkerStateBox: + """A type black box walker state, useful in reporting when you + need polymorphism in communicating walker data.""" + + def __init__(self, **kwargs: dict[str, Any]) -> None: + """Constructor for WalkerState. + + All key-word arguments passed in will be set as the key-value + pairs for the state. + + """ + self._data = copy.deepcopy(kwargs) + + def __getitem__(self, key: str) -> Any: + return self._data[key] + + def dict(self) -> dict[str, Any]: + """Return all key-value pairs as a dictionary.""" + return self._data + + def __eq__(self, other: Any) -> bool: + + if not isinstance(other, WalkerStateBox): + return False + else: + return self.dict() == other.dict() WalkerState_ = TypeVar("WalkerState_") diff --git a/tests/unit/test_walker.py b/tests/unit/test_walker.py index 7729c0ee..ab764577 100644 --- a/tests/unit/test_walker.py +++ b/tests/unit/test_walker.py @@ -9,6 +9,7 @@ from wepy.walker import ( Walker, WalkerState, + WalkerStateBox, AttrsWalkerStateMixin, clone, keep_merge, @@ -92,8 +93,28 @@ def test_dict(self): "a": 1, "b": "hello", } - +class Test_WalkerStateBox: + + def test___init__(self): + + assert WalkerStateBox(a=1)._data == {"a" : 1} + + def test___getitem__(self): + + assert WalkerStateBox(a=1)["a"] == 1 + + def test_dict(self): + + assert WalkerStateBox(a=1).dict() == {"a" : 1} + + def test___eq__(self): + + s = WalkerStateBox(a=1) + assert s == s + + assert WalkerStateBox(a=1) == WalkerStateBox(a=1) + class TestWalker: def test___init__(self): From 42687dabeb86519553a8c3c41535a05905adca13 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 12 Dec 2025 13:55:58 -0500 Subject: [PATCH 106/143] redo how WepyHDF5Reporter deals with units and quantities --- src/wepy/reporter/file.py | 3 +- src/wepy/reporter/hdf5.py | 95 +++++++++++++++-- tests/unit/test_reporter/test_hdf5.py | 144 +++++++++++++++++++++++--- 3 files changed, 216 insertions(+), 26 deletions(-) diff --git a/src/wepy/reporter/file.py b/src/wepy/reporter/file.py index b2072a77..c748364f 100644 --- a/src/wepy/reporter/file.py +++ b/src/wepy/reporter/file.py @@ -251,5 +251,4 @@ def init(self, **kwargs: SimComponentArgs) -> None: self.set_mode(file_idx, "w") def cleanup(self, **kwargs: SimComponentArgs) -> None: - logger.info("Nothing to do.") - pass + logger.info("Nothing to do for ProgressiveFileReporterABC.cleanup.") diff --git a/src/wepy/reporter/hdf5.py b/src/wepy/reporter/hdf5.py index 70b121c7..436f5a26 100644 --- a/src/wepy/reporter/hdf5.py +++ b/src/wepy/reporter/hdf5.py @@ -7,6 +7,7 @@ # Third Party Library import numpy as np +import openmm.unit # First Party Library from wepy.hdf5 import WepyHDF5 @@ -20,7 +21,7 @@ ) from wepy.reporter.file import FileReporterABC, FileMode from wepy.util.json_top import json_top_atom_count -from wepy.walker import Walker, WalkerState +from wepy.walker import Walker, WalkerState, WalkerStateBox from wepy.resampling.resamplers.resampler import Resampler from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.typing import Shape, Idxs, IdxArray @@ -29,6 +30,12 @@ WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) +class UnitError(Exception): + pass + +# TODO: support for pint +Quantity = openmm.unit.Quantity + class WepyHDF5Reporter(FileReporterABC, Generic[WalkerState_]): """Reporter for generating an HDF5 format (WepyHDF5) data file from simulations. @@ -80,7 +87,7 @@ class WepyHDF5Reporter(FileReporterABC, Generic[WalkerState_]): _n_atoms: int _all_atom_idxs: IdxArray _sparse_fields: dict[str, int] - units: dict[str, str] + units: dict[str, openmm.unit.Unit] # stateful attributes wepy_h5: WepyHDF5 | None @@ -97,7 +104,7 @@ def __init__( resampling_fields: tuple[str, ...], swmr_mode: bool = False, save_fields: tuple[str, ...] | None = None, - units: dict[str, str] | None = None, + units: dict[str, openmm.unit.Unit] | None = None, sparse_fields: dict[str, int | Literal[Ellipsis]] | None = None, n_dims: int = 3, main_rep_idxs: Idxs | None = None, @@ -136,9 +143,7 @@ def __init__( topology : str JSON string representing topology of system being simulated. - units : dict of str: str, optional - Mapping of trajectory field names to string specs - for units. + units : Mapping of trajectory field names to Unit objects. sparse_fields : dict of str: int, optional List of trajectory fields that should be initialized as sparse. @@ -385,7 +390,7 @@ def from_components( boundary_conditions_class: type[BoundaryConditions] | None = None, swmr_mode: bool = False, save_fields: tuple[str, ...] | None = None, - units: dict[str, str] | None = None, + units: dict[str, openmm.unit.Unit] | None = None, sparse_fields: dict[str, int] | None = None, n_dims: int = 3, main_rep_idxs: Idxs | None = None, @@ -563,6 +568,46 @@ def _initialize_h5_run( wepy_h5.init_record_fields("progress", progress_records) return wepy_run_idx + + @staticmethod + def _resolve_state_units( + units: dict[str, openmm.unit.Unit], + state: WalkerStateBox, + ) -> tuple[WalkerStateBox, dict[str, openmm.unit.Unit]]: + """For walker states convert all quantity field values to plain values. + + Currently only supports openmm.unit. + + Returns the units used. If these were dynamically discovered + from the quantity it will be that, otherwise it will be the + unit that was passed in. + + """ + + units_used = {} + new_walker_fields = {} + for field_key, field_value in state.dict().items(): + if not isinstance(field_value, Quantity): + new_walker_fields[field_key] = field_value + elif isinstance(field_value, openmm.unit.Quantity): + + # if there is a configured unit, convert to that + if field_key in units: + + unit = units[field_key] + + units_used[field_key] = unit + + new_walker_fields[field_key] = field_value.value_in_unit(unit) + # If there is no unit for it, just get the + # magnitude in the current units + else: + new_walker_fields[field_key] = field_value.value_in_unit(field_value.unit) + + units_used[field_key] = field_value.unit + + return WalkerStateBox(**new_walker_fields), units_used + def init(self, **kwargs: SimComponentArgs) -> None: @@ -614,22 +659,50 @@ def init(self, **kwargs: SimComponentArgs) -> None: if self.main_rep_idxs is not None: state_d["positions"] = state_d["positions"][self.main_rep_idxs] + # TODO: reusing the state infrastructure here is not + # the right thing. Currently just using a Box type to + # get around this but it really should just be it's + # own standalone type. + # then making the new state - new_state = WalkerState(**state_d) + new_state = WalkerStateBox(**state_d) filtered_init_walkers.append(Walker(new_state, walker.weight)) # otherwise save the full state else: filtered_init_walkers = kwargs["init_walkers"] + # If the state field values are quantities convert them to + # plain values. + converted_filtered_init_walkers = [] + for walker_idx, init_walker in enumerate(filtered_init_walkers): + _state, units_used = self._resolve_state_units(self.units, init_walker.state) + + # If no self.units were given, use the first + # init walker to determine the units for a field overall, set + # this and use for the rest of the walkers + if walker_idx == 0: + self.units.update(units_used) + + converted_filtered_init_walkers.append( + Walker(state=_state, weight=init_walker.weight) + ) + # Run the constructor intialization logger.info(f"Initializing HDF5 file at {self.file_path}") + # convert units to strings + _str_units = { + key : str(unit) + for key, unit + in self.units.items() + } + init_wepy_h5 = WepyHDF5( self.file_path, mode="x", topology=self._tmp_topology, - units=self.units, + units=_str_units, sparse_fields=list(self._sparse_fields.keys()), feature_shapes_overrides=self._feature_shapes, feature_dtypes_overrides=self._feature_dtypes, @@ -651,9 +724,9 @@ def init(self, **kwargs: SimComponentArgs) -> None: mode='r+', ) - self._initialize_h5_run( + self.wepy_run_idx = self._initialize_h5_run( self.wepy_h5, - init_walkers=filtered_init_walkers, + init_walkers=converted_filtered_init_walkers, continue_run=kwargs["continue_run"], resampling_fields=self.resampling_fields, decision_enum_dict=self.decision_enum_dict, diff --git a/tests/unit/test_reporter/test_hdf5.py b/tests/unit/test_reporter/test_hdf5.py index dca6d228..92ae54a8 100644 --- a/tests/unit/test_reporter/test_hdf5.py +++ b/tests/unit/test_reporter/test_hdf5.py @@ -1,10 +1,12 @@ import pytest import numpy as np +import openmm.unit from wepy.reporter.hdf5 import WepyHDF5Reporter from wepy_tools.systems.lennard_jones import LennardJonesPair from wepy.resampling.resamplers.noresampler import NoResampler -from wepy.walker import Walker +from wepy.walker import Walker, WalkerStateBox from wepy.runners.mock import MockState, MockRunner +from wepy.runners.openmm import OpenMMState from wepy.work_mapper.serial import SerialMapper from wepy.hdf5 import WepyHDF5 from wepy.resampling.decisions.no_decision import ( @@ -12,17 +14,27 @@ NothingDecisionEnum, ) -SIM_COMPONENTS = { +LJ_OPENMM_SIM_COMPONENTS = { "init_walkers": [ - Walker( - MockState(1), - weight=0.2, - ), - Walker( - MockState(1), - weight=0.1, - ), - ], + Walker( + OpenMMState.from_dwim( + positions=np.array([ + [0., 0., 0.,], + [0., 0., 0.,], + ]) * openmm.unit.nanometer, + ), + 0.5, + ), + Walker( + OpenMMState.from_dwim( + positions=np.array([ + [0., 0., 0.,], + [0., 0., 0.,], + ]) * openmm.unit.nanometer, + ), + 0.5, + ), + ], "runner": MockRunner(), "resampler": NoResampler(), "boundary_conditions": None, @@ -36,6 +48,25 @@ "decision_enum_dict" : NoDecision.enum_dict_by_name(), } +CYCLE_REPORT_DICT_COMMON = { + "runner_precycle_time" : 1., + "runner_postcycle_time" : 1., + "sim_manager_segment_overhead_time" : 1., + "cycle_sim_manager_segment_time" : 1., + "cycle_runner_time" : 1., + "cycle_bc_time" : 1., + "cycle_resampling_time" : 1., +} + +CYCLE_REPORT_DICT_EMPTY_OPTIONALS = { + "warp_data" : [], + "bc_data" : [], + "progress_data" : [], + "resampler_data" : [], + "runner_splits_time" : None, + "worker_segment_times" : None, +} + class Test_WepyHDF5Reporter: def test___init__(self, tmp_path_factory): @@ -391,12 +422,58 @@ def test__initialize_h5_run(self, tmp_path_factory): ]) ) + def test__resolve_state_units(self): + + state_nm = WalkerStateBox( + positions=np.array([ + [1., 1., 1.,], + [1., 1., 1.,], + ]) * openmm.unit.nanometer + ) + + state_nm_mags, units_used = WepyHDF5Reporter._resolve_state_units( + units={ + "positions" : openmm.unit.nanometer, + }, + state=state_nm, + ) + assert units_used == {"positions" : openmm.unit.nanometer} + assert isinstance(state_nm_mags["positions"], np.ndarray) + assert np.array_equal( + state_nm_mags["positions"], + np.array([ + [1., 1., 1.,], + [1., 1., 1.,], + ]), + ) + + state_nm_mags, units_used = WepyHDF5Reporter._resolve_state_units( + units={ + "positions" : openmm.unit.angstrom, + }, + state=state_nm, + ) + assert units_used == {"positions" : openmm.unit.angstrom} + assert isinstance(state_nm_mags["positions"], np.ndarray) + assert np.array_equal( + state_nm_mags["positions"], + np.array([ + [10., 10., 10.,], + [10., 10., 10.,], + ]), + ) + # if no units specified the ones from the state + state_nm_mags, units_used = WepyHDF5Reporter._resolve_state_units( + units={}, + state=state_nm, + ) + assert units_used == {"positions" : openmm.unit.nanometer} + def test_init(self, tmp_path_factory): test_sys = LennardJonesPair() - n_atoms = test_sys.mdj_top.n_atoms d0 = tmp_path_factory.mktemp("0") h5_path = d0 / "main.wepy.h5" @@ -404,7 +481,48 @@ def test_init(self, tmp_path_factory): reporter = WepyHDF5Reporter( file_path=h5_path, topology=test_sys.json_top, + units={"positions" : openmm.unit.angstrom}, **RESAMPLER_REPORTER_ARGS, ) - reporter.init(**SIM_COMPONENTS) + reporter.init(**LJ_OPENMM_SIM_COMPONENTS) + + assert reporter.units == { + "positions" : openmm.unit.angstrom, + "time" : openmm.unit.picosecond, + "box_vectors" : openmm.unit.nanometer, + "box_volume" : (openmm.unit.nanometer ** 3), + } + assert reporter.wepy_run_idx == 0 + assert reporter._tmp_topology is None + assert reporter.file_path == h5_path + assert h5_path.exists() + assert reporter.wepy_h5.mode == "r+" + + # minimal tests, see _initialize_h5_run for more in depth tests + with reporter.wepy_h5 as wepy_h5: + assert "0" in wepy_h5.h5["runs"] + assert "init_walkers" in wepy_h5.h5["runs/0"] + assert len(wepy_h5.h5["runs/0/init_walkers"]) == 2 + + # if no units are given, derive them dynamically from + # quantities + d0 = tmp_path_factory.mktemp("0") + h5_path = d0 / "main.wepy.h5" + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + units=None, + **RESAMPLER_REPORTER_ARGS, + ) + + reporter.init(**LJ_OPENMM_SIM_COMPONENTS) + + assert reporter.units == { + "positions" : openmm.unit.nanometer, + "time" : openmm.unit.picosecond, + "box_vectors" : openmm.unit.nanometer, + "box_volume" : (openmm.unit.nanometer ** 3), + } + From 3e79a3b91259faf56b7c633925615e0dfc91fdf8 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 12 Dec 2025 13:56:40 -0500 Subject: [PATCH 107/143] tests for WepyHDF5 new_run and add_traj --- src/wepy/hdf5.py | 203 +++++++++++++---------- tests/unit/test_hdf5.py | 357 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 470 insertions(+), 90 deletions(-) diff --git a/src/wepy/hdf5.py b/src/wepy/hdf5.py index 7b4a4654..cd6c6f72 100644 --- a/src/wepy/hdf5.py +++ b/src/wepy/hdf5.py @@ -404,6 +404,7 @@ from warnings import warn # Third Party Library +import attrs import h5py import numpy as np from numpy.typing import NDArray @@ -422,11 +423,12 @@ FieldDtype, ) from wepy.typing import Shape, Idxs, IdxArray +from wepy.walker import WalkerStateBox, Walker # optional dependencies try: # Third Party Library - import mdtraj as mdj + import mdtraj except ModuleNotFoundError: warn("mdtraj is not installed and that functionality will not work", RuntimeWarning) @@ -437,6 +439,10 @@ warn("pandas is not installed and that functionality will not work", RuntimeWarning) logger = logging.getLogger(__name__) + +H5AttrDtype = str | int | float + +H5Attrs = dict[str, H5AttrDtype] ## h5py settings @@ -614,6 +620,12 @@ OBSERVABLES = "observables" """The field name for the default compound field observables.""" +RESERVED_TRAJ_FIELDS = frozenset({ + WEIGHTS, + ALT_REPS, + OBSERVABLES, +}) + ## Trajectory Field Constants WEIGHT_SHAPE = (1,) @@ -1419,7 +1431,7 @@ def _add_run_init(self, run_idx, continue_run=None): if continue_run is not None: self.add_continuation(run_idx, continue_run) - def _add_init_walkers(self, init_walkers_grp, init_walkers): + def _add_init_walkers(self, init_walkers_grp: h5py.Group, init_walkers: list[Walker[WalkerStateBox]]) -> None: """Adds the run field group for the initial walkers. Parameters @@ -2048,6 +2060,7 @@ def _extend_run_record_data_field( records_grp = self.h5["{}/{}/{}".format(RUNS, run_idx, run_record_key)] field = records_grp[field_name] + breakpoint() # make sure this is a feature vector assert ( len(field_data.shape) > 1 @@ -2059,7 +2072,7 @@ def _extend_run_record_data_field( # check whether it is a variable length record, by getting the # record dataset dtype and using the checker to see if it is # the vlen special type in h5py - if h5py.check_dtype(vlen=field.dtype) is not None: + if h5py.check_vlen_dtype(field.dtype) is not None: # if it is we have to treat it differently, since it # cannot be multidimensional @@ -2659,7 +2672,7 @@ def _add_field(self, field_path, data, sparse_idxs=None, force=False): ### h5py object access - def run(self, run_idx): + def run(self, run_idx: int) -> h5py.Group: """Get the h5py.Group for a run. Parameters @@ -2673,7 +2686,7 @@ def run(self, run_idx): """ return self._h5["{}/{}".format(RUNS, int(run_idx))] - def traj(self, run_idx, traj_idx): + def traj(self, run_idx: int, traj_idx: int) -> h5py.Group: """Get an h5py.Group trajectory group. Parameters @@ -2688,7 +2701,7 @@ def traj(self, run_idx, traj_idx): """ return self._h5["{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx)] - def run_trajs(self, run_idx): + def run_trajs(self, run_idx: int) -> h5py.Group: """Get the trajectories group for a run. Parameters @@ -2703,14 +2716,17 @@ def run_trajs(self, run_idx): return self._h5["{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES)] @property - def runs(self): + def runs(self) -> h5py.Group: """The runs group.""" return self.h5[RUNS] - def run_grp(self, run_idx): + def run_grp(self, run_idx: int) -> h5py.Group: """A group for a single run.""" return self.runs["{}".format(run_idx)] + # TOREV: probably can get rid of these since we are deprecating + # the Orchestrator and snapshots as implemented + def run_start_snapshot_hash(self, run_idx): """Hash identifier for the starting snapshot of a run from orchestration. @@ -2747,12 +2763,12 @@ def set_run_end_snapshot_hash(self, run_idx, snaphash): raise AttributeError("The snapshot has already been set.") @property - def settings_grp(self): + def settings_grp(self) -> h5py.Group: """The header settings group.""" settings_grp = self.h5[SETTINGS] return settings_grp - def decision_grp(self, run_idx): + def decision_grp(self, run_idx: int) -> h5py.Group: """Get the decision enumeration group for a run. Parameters @@ -2766,7 +2782,7 @@ def decision_grp(self, run_idx): """ return self.run(run_idx)[DECISION] - def init_walkers_grp(self, run_idx): + def init_walkers_grp(self, run_idx: int) -> h5py.Group: """Get the group for the initial walkers for a run. Parameters @@ -2781,7 +2797,7 @@ def init_walkers_grp(self, run_idx): return self.run(run_idx)[INIT_WALKERS] - def records_grp(self, run_idx, run_record_key): + def records_grp(self, run_idx: int, run_record_key: str) -> h5py.Group: """Get a record group h5py.Group for a run. Parameters @@ -2798,7 +2814,7 @@ def records_grp(self, run_idx, run_record_key): path = "{}/{}/{}".format(RUNS, run_idx, run_record_key) return self.h5[path] - def resampling_grp(self, run_idx): + def resampling_grp(self, run_idx: int) -> h5py.Group: """Get this record group for a run. Parameters @@ -2812,7 +2828,7 @@ def resampling_grp(self, run_idx): """ return self.records_grp(run_idx, RESAMPLING) - def resampler_grp(self, run_idx): + def resampler_grp(self, run_idx: int) -> h5py.Group: """Get this record group for a run. Parameters @@ -2826,7 +2842,7 @@ def resampler_grp(self, run_idx): """ return self.records_grp(run_idx, RESAMPLER) - def warping_grp(self, run_idx): + def warping_grp(self, run_idx: int) -> h5py.Group: """Get this record group for a run. Parameters @@ -2840,7 +2856,7 @@ def warping_grp(self, run_idx): """ return self.records_grp(run_idx, WARPING) - def bc_grp(self, run_idx): + def bc_grp(self, run_idx: int) -> h5py.Group: """Get this record group for a run. Parameters @@ -2854,7 +2870,7 @@ def bc_grp(self, run_idx): """ return self.records_grp(run_idx, BC) - def progress_grp(self, run_idx): + def progress_grp(self, run_idx: int) -> h5py.Group: """Get this record group for a run. Parameters @@ -2868,7 +2884,7 @@ def progress_grp(self, run_idx): """ return self.records_grp(run_idx, PROGRESS) - def iter_runs(self, idxs=False, run_sel=None): + def iter_runs(self, idxs: bool = False, run_sel: list[int] | None = None): """Generator for iterating through the runs of a file. Parameters @@ -2899,7 +2915,7 @@ def iter_runs(self, idxs=False, run_sel=None): else: yield run - def iter_trajs(self, idxs=False, traj_sel=None): + def iter_trajs(self, idxs: bool = False, traj_sel: list[int] | None = None): """Generator for iterating over trajectories in a file. Parameters @@ -2933,7 +2949,7 @@ def iter_trajs(self, idxs=False, traj_sel=None): else: yield traj - def iter_run_trajs(self, run_idx, idxs=False): + def iter_run_trajs(self, run_idx: int, idxs: bool = False): """Iterate over the trajectories of a run. Parameters @@ -2955,13 +2971,13 @@ def iter_run_trajs(self, run_idx, idxs=False): ### Settings @property - def defined_traj_field_names(self): + def defined_traj_field_names(self) -> list[str]: """A list of the settings defined field names all trajectories have in the file.""" return list(self.field_feature_shapes.keys()) @property - def observable_field_names(self): + def observable_field_names(self) -> list[str]: """Returns a list of the names of the observables that all trajectories have. If this encounters observable fields that don't occur in all @@ -2984,7 +3000,7 @@ def observable_field_names(self): # otherwise return the field names for the observables return list(field_names.keys()) - def _check_traj_field_consistency(self, field_names): + def _check_traj_field_consistency(self, field_names: str) -> bool: """Checks that every trajectory has the given fields across the entire dataset. @@ -3016,7 +3032,7 @@ def _check_traj_field_consistency(self, field_names): return True @property - def record_fields(self): + def record_fields(self) -> dict[str, list[str]]: """The record fields for each record group which are selected for inclusion in the truncated records. These are the fields which are considered to be table-ified. @@ -3036,12 +3052,12 @@ def record_fields(self): return record_fields_dict @property - def sparse_fields(self): + def sparse_fields(self) -> NDArray: """The trajectory fields that are sparse.""" return self.h5["{}/{}".format(SETTINGS, SPARSE_FIELDS)].asstr()[:] @property - def main_rep_idxs(self): + def main_rep_idxs(self) -> NDArray | None: """The indices of the atoms included from the full topology in the default 'positions' trajectory""" if "{}/{}".format(SETTINGS, MAIN_REP_IDXS) in self.h5: @@ -3050,7 +3066,7 @@ def main_rep_idxs(self): return None @property - def alt_reps_idxs(self): + def alt_reps_idxs(self) -> dict[str, NDArray]: """Mapping of the names of the alt reps to the indices of the atoms from the topology that they include in their datasets. """ @@ -3059,14 +3075,14 @@ def alt_reps_idxs(self): return {name: ds[:] for name, ds in idxs_grp.items()} @property - def alt_reps(self): + def alt_reps(self) -> set[str]: """Names of the alt reps.""" idxs_grp = self.h5["{}/{}".format(SETTINGS, ALT_REPS_IDXS)] return {name for name in idxs_grp.keys()} @property - def field_feature_shapes(self): + def field_feature_shapes(self) -> dict[str, NDArray | None]: """Mapping of the names of the trajectory fields to their feature vector shapes. """ @@ -3086,7 +3102,7 @@ def field_feature_shapes(self): return shapes @property - def field_feature_dtypes(self): + def field_feature_dtypes(self) -> dict[str, np.dtype]: """Mapping of the names of the trajectory fields to their feature vector numpy dtypes. """ @@ -3104,26 +3120,22 @@ def field_feature_dtypes(self): if dtype_str == _NONE_STR: dtypes[field_path] = None else: - dtype_json_to_numpy(dtype_str.decode()) - # TODO: remove - # dtype_obj = json.loads(dtype_str.decode()) - # dtype_obj = [tuple(d) for d in dtype_obj] - # dtype = np.dtype(dtype_obj) + dtype = dtype_json_to_numpy(dtype_str.decode()) dtypes[field_path] = dtype return dtypes @property - def continuations(self): + def continuations(self) -> NDArray: """The continuation relationships in this file.""" return self.settings_grp[CONTINUATIONS][:] @property - def metadata(self): + def metadata(self) -> H5Attrs: """File metadata (h5py.attrs).""" return dict(self._h5.attrs) - def decision_enum(self, run_idx): + def decision_enum(self, run_idx: int) -> dict[str, int]: """Mapping of decision enumerated names to their integer representations. Parameters @@ -3148,7 +3160,7 @@ def decision_enum(self, run_idx): return enum - def decision_value_names(self, run_idx): + def decision_value_names(self, run_idx: int) -> dict[str, int]: """Mapping of the integer values for decisions to the decision ID strings. Parameters @@ -3175,7 +3187,7 @@ def decision_value_names(self, run_idx): ### Topology - def get_topology(self, alt_rep=POSITIONS): + def get_topology(self, alt_rep: str = POSITIONS) -> str: """Get the JSON topology for a particular represenation of the positions. By default gives the topology for the main 'positions' field @@ -3222,7 +3234,7 @@ def get_topology(self, alt_rep=POSITIONS): return top @property - def topology(self): + def topology(self) -> str: """The topology for the full simulated system. May not be the main representation in the POSITIONS field; for @@ -3236,7 +3248,7 @@ def topology(self): """ return self._h5[TOPOLOGY][()] - def get_mdtraj_topology(self, alt_rep=POSITIONS): + def get_mdtraj_topology(self, alt_rep: str = POSITIONS) -> mdtraj.Topology: """Get an mdtraj.Topology object for a system representation. By default gives the topology for the main 'positions' field @@ -3263,7 +3275,12 @@ def get_mdtraj_topology(self, alt_rep=POSITIONS): ## Initial walkers - def initial_walker_fields(self, run_idx, fields, walker_idxs=None): + def initial_walker_fields( + self, + run_idx: int, + fields: list[str], + walker_idxs: list[int] | None = None + ) -> dict[str, NDArray]: """Get fields from the initial walkers of the simulation. Parameters @@ -3309,7 +3326,12 @@ def initial_walker_fields(self, run_idx, fields, walker_idxs=None): return init_walker_fields - def initial_walkers_to_mdtraj(self, run_idx, walker_idxs=None, alt_rep=POSITIONS): + def initial_walkers_to_mdtraj( + self, + run_idx: int, + walker_idxs: list[int] | None = None, + alt_rep: str = POSITIONS, + ) -> mdtraj.Trajectory: """Generate an mdtraj Trajectory from a trace of frames from the runs. Uses the default fields for positions (unless an alternate @@ -3356,26 +3378,26 @@ def initial_walkers_to_mdtraj(self, run_idx, walker_idxs=None, alt_rep=POSITIONS ### Counts and Indexing @property - def num_atoms(self): + def num_atoms(self) -> int: """The number of atoms in the full topology representation.""" return self.h5["{}/{}".format(SETTINGS, N_ATOMS)][()] @property - def num_dims(self): + def num_dims(self) -> int: """The number of spatial dimensions in the positions and alt_reps trajectory fields.""" return self.h5["{}/{}".format(SETTINGS, N_DIMS_STR)][()] @property - def num_runs(self): + def num_runs(self) -> int: """The number of runs in the file.""" return len(self._h5[RUNS]) @property - def num_trajs(self): + def num_trajs(self) -> int: """The total number of trajectories in the entire file.""" return len(list(self.run_traj_idx_tuples())) - def num_init_walkers(self, run_idx): + def num_init_walkers(self, run_idx: int) -> int: """The number of initial walkers for a run. Parameters @@ -3390,7 +3412,7 @@ def num_init_walkers(self, run_idx): return len(self.init_walkers_grp(run_idx)) - def num_walkers(self, run_idx, cycle_idx): + def num_walkers(self, run_idx: int, cycle_idx: int) -> int: """Get the number of walkers at a given cycle in a run. Parameters @@ -3536,7 +3558,7 @@ def get_traj_field_cycle_idxs(self, run_idx, traj_idx, field_path): return cycle_idxs - def next_run_idx(self): + def next_run_idx(self) -> int: """The index of the next run if it were to be added. Because runs are named as the integer value of the order they @@ -3550,7 +3572,7 @@ def next_run_idx(self): """ return self.num_runs - def next_run_traj_idx(self, run_idx): + def next_run_traj_idx(self, run_idx: int) -> int: """The index of the next trajectory for this run. Parameters @@ -4056,10 +4078,10 @@ def add_continuation(self, continuation_run, base_run): def new_run( self, - init_walkers, - continue_run=None, - **kwargs, - ): + init_walkers: Walker[WalkerStateBox], + continue_run: int | None = None, + **kwargs: H5Attrs, + ) -> h5py.Group: """Initialize a new run. Parameters @@ -4083,11 +4105,12 @@ def new_run( if continue_run is not None: if continue_run not in self.run_idxs: raise ValueError( - "The continue_run idx given, {}, is not present in this file".format( - continue_run - ) + f"The continue_run idx given, {continue_run}, is not present in this file." ) + if len(init_walkers) == 0: + raise ValueError("No init_walkers provided.") + # get the index for this run new_run_idx = self.next_run_idx() @@ -4110,8 +4133,8 @@ def new_run( if key != RUN_IDX: run_grp.attrs[key] = val else: - warn( - "run_idx metadata is set by wepy and cannot be used", RuntimeWarning + raise ValueError( + f"'{RUN_IDX}' given as metadata but is reserved and cannot be used" ) return run_grp @@ -4267,7 +4290,7 @@ def init_run_bc(self, run_idx, bc): # application level methods for initializing the run records # groups with just the fields and without the objects - def init_run_fields_resampling(self, run_idx, fields): + def init_run_fields_resampling(self, run_idx: int, fields: list[str]) -> h5py.Group: """Initialize this record group fields datasets. Parameters @@ -4423,7 +4446,14 @@ def init_run_record_grp( # """ # return self.traj(run_idx, traj_idx)[POSITIONS].shape[0] - def add_traj(self, run_idx, data, weights=None, sparse_idxs=None, metadata=None): + def add_traj( + self, + run_idx: int, + data: dict[str, NDArray], + weights: NDArray[np.float64] | None = None, + sparse_idxs: dict[str, list[int]] | None = None, + metadata: H5Attrs | None = None, + ) -> h5py.Group: """Add a full trajectory to a run. Parameters @@ -4434,8 +4464,8 @@ def add_traj(self, run_idx, data, weights=None, sparse_idxs=None, metadata=None) weights : 1-D arraylike of float The weights of each frame. If None defaults all frames to 1.0. - sparse_idxs : list of int - Cycle indices the data corresponds to. + sparse_idxs : Cycle indices the data corresponds to for each + field. metadata : dict of str : value Metadata for the trajectory. @@ -4456,27 +4486,37 @@ def add_traj(self, run_idx, data, weights=None, sparse_idxs=None, metadata=None) if metadata is None: metadata = {} + if len(_wrong_data_fields := [ + reserved_field + for reserved_field + in RESERVED_TRAJ_FIELDS + if reserved_field in data + ]) > 0: + raise ValueError(f"{_wrong_data_fields} are reserved field names and cannot be given in data") + # positions are mandatory - assert POSITIONS in traj_data, "positions must be given to create a trajectory" - assert isinstance(traj_data[POSITIONS], np.ndarray) + if POSITIONS not in traj_data: + raise ValueError(f"{POSITIONS} field must be given to create a trajectory") + + if not isinstance(traj_data[POSITIONS], np.ndarray): + raise TypeError(f"{POSITIONS} field must be a numpy array") n_frames = traj_data[POSITIONS].shape[0] # if weights are None then we assume they are 1.0 if weights is None: weights = np.ones((n_frames, 1), dtype=float) - else: - assert isinstance(weights, np.ndarray), "weights must be a numpy.ndarray" - assert ( - weights.shape[0] == n_frames - ), "weights and the number of frames must be the same length" + elif not isinstance(weights, np.ndarray): + raise TypeError("weights must be a numpy.ndarray") + elif weights.shape[0] != n_frames: + raise ValueError("weights and the number of frames must be the same length") # current traj_idx traj_idx = self.next_run_traj_idx(run_idx) # make a group for this trajectory, with the current traj_idx # for this run traj_grp = self._h5.create_group( - "{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx) + f"{RUNS}/{run_idx}/{TRAJECTORIES}/{traj_idx}" ) # add the run_idx as metadata @@ -4489,10 +4529,7 @@ def add_traj(self, run_idx, data, weights=None, sparse_idxs=None, metadata=None) if key not in [RUN_IDX, TRAJ_IDX]: traj_grp.attrs[key] = val else: - warn( - "run_idx and traj_idx are used by wepy and cannot be set", - RuntimeWarning, - ) + raise ValueError(f"'{RUN_IDX}' and '{TRAJ_IDX}' metadata keys are reserved and cannot be set") # check to make sure the positions are the right shape assert ( @@ -4820,8 +4857,8 @@ def extend_cycle_run_group_records( ) # then add all the data for the field - for record_dict in fields_data: - for field_name, field_data in record_dict.items(): + for record in fields_data: + for field_name, field_data in attrs.asdict(record).items(): self._extend_run_record_data_field( run_idx, run_record_key, field_name, np.array([field_data]) ) @@ -5894,7 +5931,7 @@ def to_mdtraj(self, run_idx, traj_idx, frames=None, alt_rep=None): ) if (box_vectors is not None) and (time is not None): - traj = mdj.Trajectory( + traj = mdtraj.Trajectory( positions, topology, time=time, @@ -5902,16 +5939,16 @@ def to_mdtraj(self, run_idx, traj_idx, frames=None, alt_rep=None): unitcell_angles=unitcell_angles, ) elif box_vectors is not None: - traj = mdj.Trajectory( + traj = mdtraj.Trajectory( positions, topology, unitcell_lengths=unitcell_lengths, unitcell_angles=unitcell_angles, ) elif time is not None: - traj = mdj.Trajectory(positions, topology, time=time) + traj = mdtraj.Trajectory(positions, topology, time=time) else: - traj = mdj.Trajectory(positions, topology) + traj = mdtraj.Trajectory(positions, topology) return traj diff --git a/tests/unit/test_hdf5.py b/tests/unit/test_hdf5.py index 161084d7..2ca336f0 100644 --- a/tests/unit/test_hdf5.py +++ b/tests/unit/test_hdf5.py @@ -1,7 +1,10 @@ from pathlib import Path import json +from typing import Callable import pytest import numpy as np +from unittest.mock import patch, PropertyMock + import h5py from wepy.hdf5 import ( @@ -10,12 +13,45 @@ WepyHDF5, _iter_field_paths, ) +from wepy.walker import Walker, WalkerStateBox from wepy_tools.systems.lennard_jones import LennardJonesPair +@pytest.fixture(scope="session") +def wepy_h5_factory() -> Callable[[Path], WepyHDF5]: -# def gen_wepy_h5(path: Path, mode: str) -> WepyHDF5: -# pass + test_sys = LennardJonesPair() + + def _factory(path: Path) -> Path: + + # create the file + WepyHDF5( + path, + mode="x", + topology=test_sys.json_top, + ) + + return path + + return _factory + +_INIT_WALKERS = [ + Walker( + WalkerStateBox( + positions=np.array([ + [1., 1., 1.], + [2., 2., 2.], + ]), + box_vectors=np.array([ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]), + kinetic_energy=3.455, + ), + 0.1, + ), + ] # @pytest.fixture(scope="session") # def wepy_h5_file_ro(tmpdir) -> WepyHDF5: @@ -377,7 +413,7 @@ def test__init_continuations(self, tmp_path_factory): def test__create_init(self, tmp_path_factory): test_sys = LennardJonesPair() - + d0 = tmp_path_factory.mktemp("0") h5_path = d0 / "test1.h5" @@ -562,17 +598,140 @@ def test__create_init(self, tmp_path_factory): assert len(h5["units"]) == 1 assert h5["units/positions"][()].decode() == "nanometer" - # def test_new_run(self): + def test_num_runs(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + assert wepy_h5.num_runs == 0 + + wepy_h5.h5["runs"].create_group("0") + assert wepy_h5.num_runs == 1 + + + def test_next_run_idx(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + assert wepy_h5.next_run_idx() == 0 + wepy_h5.h5["runs"].create_group("0") + assert wepy_h5.next_run_idx() == 1 + + # TODO: see new_run test for the same effect + # def test__add_init_walkers(self, wepy_h5_factory, tmpdir): + # pass + + # def test__add_run_init(self, wepy_h5_factory, tmpdir): # pass - # def test_init_run_fields_resampling(self): + def test_new_run(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=[ + Walker( + WalkerStateBox( + positions=np.array([ + [1., 1., 1.], + [2., 2., 2.], + ]), + box_vectors=np.array([ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]), + kinetic_energy=3.455, + ), + 0.1, + ), + ], + ) + + assert "0" in wepy_h5.h5["runs"] + assert "init_walkers" in run_grp + + assert len(run_grp["init_walkers"]) == 1 + assert "0" in run_grp["init_walkers"] + assert set(run_grp["init_walkers/0"].keys()) == { + "weights", + "box_vectors", + "kinetic_energy", + "positions", + } + + assert run_grp["init_walkers/0/weights"].shape == (1,1) + assert run_grp["init_walkers/0/box_vectors"].shape == (1,3,3) + assert run_grp["init_walkers/0/positions"].shape == (1,2,3) + + # TOREV: this should probably be (1,1) shape, but waiting + # to see how things shake out later + assert run_grp["init_walkers/0/kinetic_energy"].shape == (1,) + + assert "trajectories" in run_grp + + assert "run_idx" in run_grp.attrs + assert run_grp.attrs["run_idx"] == 0 + + run_grp = wepy_h5.new_run( + init_walkers=[ + Walker( + WalkerStateBox(), + 0.1, + ), + ], + continue_run=0, + # extra attrs + foo="hello", + ) + + assert len(wepy_h5.h5["runs"]) == 2 + assert "1" in wepy_h5.h5["runs"] + + assert np.array_equal( + wepy_h5.h5["_settings/continuations"][:], + np.array([ + [1, 0] + ]) + ) + + assert len(run_grp["init_walkers/0"].keys()) == 1 + assert "weights" in run_grp["init_walkers/0"] + + assert run_grp.attrs["run_idx"] == 1 + assert "foo" in run_grp.attrs + assert run_grp.attrs["foo"] == "hello" + + with pytest.raises(ValueError): + wepy_h5.new_run( + init_walkers=[ + Walker( + WalkerStateBox(), + 0.1, + ), + ], + run_idx=1, + ) + + with pytest.raises(ValueError): + wepy_h5.new_run( + init_walkers=[], + ) + + + # TODO: I know these are working from the WepyHDF5Reporter tests, + # but these should be tested individually as time permits + + # def test_init_run_fields_resampling(self, wepy_h5_factory, tmpdir): # pass # def test_init_run_fields_resampling_decision(self): # pass - def test_init_run_fields_resampler(self): - pass + # def test_init_run_fields_resampler(self): + # pass # def test_init_record_fields(self): # pass @@ -586,5 +745,189 @@ def test_init_run_fields_resampler(self): # def test_init_run_fields_bc(self): # pass + def test_add_traj(self, wepy_h5_factory, tmpdir, monkeypatch): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + traj_grp = wepy_h5.add_traj( + 0, + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + weights=np.array([[0.2]]), + metadata={"foo" : "hello"}, + ) + + assert "0" in wepy_h5.h5["runs/0/trajectories"] + assert set(traj_grp.attrs.keys()) == {"run_idx", "traj_idx", "foo"} + assert traj_grp.attrs["run_idx"] == 0 + assert traj_grp.attrs["traj_idx"] == 0 + assert traj_grp.attrs["foo"] == "hello" + + assert set(traj_grp.keys()) == { + "weights", + "positions", + "box_vectors", + "kinetic_energy", + } + + assert traj_grp["weights"].shape == (1,1) + assert np.array_equal( + traj_grp["weights"][:], + np.array([[0.2]]), + ) + assert traj_grp["positions"].shape == (1,2,3) + assert traj_grp["box_vectors"].shape == (1,3,3) + assert traj_grp["kinetic_energy"].shape == (1,1) + + # TOREV: this is an old requirement and should be + # reviewed. Should use fields defined earlier in + # initialization. But currently just testing existing + # behavior + + # must have positions + with pytest.raises(ValueError): + + wepy_h5.add_traj( + 0, + data={ + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + ) + + # default weights + traj_grp = wepy_h5.add_traj( + 0, + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + weights=None, + ) + + assert "1" in wepy_h5.h5["runs/0/trajectories"] + assert traj_grp.attrs["run_idx"] == 0 + assert traj_grp.attrs["traj_idx"] == 1 + + assert "weights" in traj_grp + assert traj_grp["weights"].shape == (1,1) + assert np.array_equal( + traj_grp["weights"][:], + np.array([[1.]]), + ) + + # TOREV: this can lead to data inconsistency + + # local sparse_idxs + traj_grp = wepy_h5.add_traj( + 0, + data={ + "positions" : np.array([ + [ + [2., 2., 2.,], + [1., 1., 1.,], + ], + [ + [2., 2., 2.,], + [1., 1., 1.,], + ], + ]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + sparse_idxs={"kinetic_energy" : [1,]}, + ) + assert "2" in wepy_h5.h5["runs/0/trajectories"] + assert traj_grp.attrs["run_idx"] == 0 + assert traj_grp.attrs["traj_idx"] == 2 + + # NOTE: no box_vectors + assert set(traj_grp.keys()) == { + "weights", + "positions", + "kinetic_energy", + } + + assert traj_grp["positions"].shape == (2, 2, 3) + + assert set(traj_grp["kinetic_energy"].keys()) == {"_sparse_idxs", "data"} + assert traj_grp["kinetic_energy/_sparse_idxs"].shape == (1,) + assert np.array_equal( + traj_grp["kinetic_energy/_sparse_idxs"][:], + np.array([1]), + ) + + assert np.array_equal( + traj_grp["kinetic_energy/data"][:], + np.array([[4.87]]), + ) + + # unitialized sparse fields declared in initialization of + # file + + # HACK: patch in the sparse_fields instead of doing it from scratch + with patch("wepy.hdf5.WepyHDF5.sparse_fields", new_callable=PropertyMock) as sparse_fields_mock: + sparse_fields_mock.return_value = np.array(["box_vectors"]) + + traj_grp = wepy_h5.add_traj( + 0, + data={ + "positions" : np.array([ + [ + [2., 2., 2.,], + [1., 1., 1.,], + ], + [ + [2., 2., 2.,], + [1., 1., 1.,], + ], + ]), + }, + ) + assert "3" in wepy_h5.h5["runs/0/trajectories"] + assert traj_grp.attrs["run_idx"] == 0 + assert traj_grp.attrs["traj_idx"] == 3 + + assert "box_vectors" in traj_grp + assert set(traj_grp["box_vectors"].keys()) == {"_sparse_idxs", "data"} + assert traj_grp["box_vectors/_sparse_idxs"].shape == (0,) + assert traj_grp["box_vectors/data"].shape == (0,0,0) + + + def test_extend_traj(self, wepy_h5_factory, tmpdir): + pass # class Test_WepyHDF5_DataConformance: # pass From 42b63adb1667f5942066a310fb7a9c19cbc24d75 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 12 Dec 2025 23:35:12 -0500 Subject: [PATCH 108/143] add AttrsMappingMixin For implementing a Mapping like interface on an attrs class --- src/wepy/util/attrs.py | 25 +++++++++++ src/wepy/walker.py | 11 ++--- tests/unit/test_util/test_attrs.py | 67 ++++++++++++++++++++++++++++++ tests/unit/test_walker.py | 2 +- 4 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 src/wepy/util/attrs.py create mode 100644 tests/unit/test_util/test_attrs.py diff --git a/src/wepy/util/attrs.py b/src/wepy/util/attrs.py new file mode 100644 index 00000000..509430e2 --- /dev/null +++ b/src/wepy/util/attrs.py @@ -0,0 +1,25 @@ +from typing import Any, Iterator +from collections.abc import Mapping + +import attrs + +from wepy.missing import MISSING + +class AttrsMappingMixin(Mapping[str, object]): + """A convenient mixin for implementing the WalkerState interface + for attrs classes.""" + + def __len__(self) -> int: + + return len(attrs.fields(type(self))) + + def __getitem__(self, key: str) -> Any: + if (value := getattr(self, key, MISSING)) is MISSING: + raise KeyError(f"'key' '{key}' not found") + else: + return value + + def __iter__(self) -> Iterator[str]: + + for field in attrs.fields(type(self)): + yield field.name diff --git a/src/wepy/walker.py b/src/wepy/walker.py index b40b397f..52fc6b0c 100644 --- a/src/wepy/walker.py +++ b/src/wepy/walker.py @@ -36,6 +36,8 @@ # Third Party Library import attrs +from wepy.util.attrs import AttrsMappingMixin + from wepy.missing import MISSING logger = logging.getLogger(__name__) @@ -51,16 +53,11 @@ def __eq__(self, other: Any) -> bool: ... def dict(self) -> dict[str, T]: ... -class AttrsWalkerStateMixin: +# TODO: merge with the AttrsMappingMixin +class AttrsWalkerStateMixin(AttrsMappingMixin): """A convenient mixin for implementing the WalkerState interface for attrs classes.""" - def __getitem__(self, key: str) -> Any: - if (value := getattr(self, key, MISSING)) is MISSING: - raise KeyError(f"'key' '{key}' not found") - else: - return value - def dict(self) -> dict[str, Any]: return attrs.asdict(self) diff --git a/tests/unit/test_util/test_attrs.py b/tests/unit/test_util/test_attrs.py new file mode 100644 index 00000000..c2909059 --- /dev/null +++ b/tests/unit/test_util/test_attrs.py @@ -0,0 +1,67 @@ +import attrs +from wepy.util.attrs import AttrsMappingMixin + +@attrs.define +class Thing(AttrsMappingMixin): + a: int + b: str + +class Test_AttrsMappingMixin: + + def test___init__(self): + + Thing(a=1, b="hello") + + def test___len__(self): + + assert len(Thing(a=1, b="hello")) == 2 + + def test___getitem__(self): + + assert Thing(a=1, b="hello")["a"] == 1 + assert Thing(a=1, b="hello")["b"] == "hello" + + def test___eq__(self): + + assert Thing(a=1, b="hello") == Thing(a=1, b="hello") + + def test__ne__(self): + assert Thing(a=1, b="hello") != Thing(a=100, b="hello") + assert Thing(a=1, b="hello") != Thing(a=1, b="goodbye") + + def test___contains__(self): + + t = Thing(a=1, b="hello") + assert "a" in t + assert "b" in t + + def test___iter__(self): + t = Thing(a=1, b="hello") + t_it = iter(t) + _vs = set() + _vs.add(next(t_it)) + _vs.add(next(t_it)) + assert _vs == {"a", "b"} + + + def test_keys(self): + t = Thing(a=1, b="hello") + assert set(t.keys()) == {"a", "b"} + + + def test_values(self): + t = Thing(a=1, b="hello") + assert set(t.values()) == {1, "hello"} + + + def test_items(self): + t = Thing(a=1, b="hello") + assert set(t.items()) == { + ("a", 1), + ("b", "hello"), + } + + def test_get(self): + t = Thing(a=1, b="hello") + assert t.get("a") == 1 + assert t.get("b") == "hello" diff --git a/tests/unit/test_walker.py b/tests/unit/test_walker.py index ab764577..9f91ff76 100644 --- a/tests/unit/test_walker.py +++ b/tests/unit/test_walker.py @@ -9,8 +9,8 @@ from wepy.walker import ( Walker, WalkerState, + AttrsWalkerStateMixin, WalkerStateBox, - AttrsWalkerStateMixin, clone, keep_merge, merge, From 0db1dd9ebb07e14b359bb009e02ee370deb158c8 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sat, 13 Dec 2025 00:13:40 -0500 Subject: [PATCH 109/143] add clear types for resampl{ing,er} records Convert to attrs classes, add validation, and distinguish between decision records and resampler records. Add an AttrsMappingMixin so that the Records can be treated like dicts downstream. Create an explicit storage protocol which defines the kinds of types that can be generated for different things. In this case the records. --- src/wepy/resampling/decisions/clone_merge.py | 48 +++++++++- src/wepy/resampling/decisions/decision.py | 18 +++- src/wepy/resampling/decisions/no_decision.py | 17 +++- src/wepy/resampling/resamplers/clone_merge.py | 20 +++- src/wepy/resampling/resamplers/noresampler.py | 28 +++--- src/wepy/resampling/resamplers/resampler.py | 21 +++-- src/wepy/resampling/resamplers/revo.py | 91 +++++++++++-------- src/wepy/storage/__init__.py | 1 + src/wepy/storage/protocol.py | 12 +++ .../test_decisions/test_clone_merge.py | 80 +++++++++++++++- .../test_decisions/test_decision.py | 4 + .../test_decisions/test_no_decision.py | 33 ++++++- .../test_resamplers/test_noresampler.py | 15 +-- .../test_resamplers/test_revo.py | 4 +- 14 files changed, 311 insertions(+), 81 deletions(-) create mode 100644 src/wepy/storage/__init__.py create mode 100644 src/wepy/storage/protocol.py diff --git a/src/wepy/resampling/decisions/clone_merge.py b/src/wepy/resampling/decisions/clone_merge.py index 3723e5a4..4bd43767 100644 --- a/src/wepy/resampling/decisions/clone_merge.py +++ b/src/wepy/resampling/decisions/clone_merge.py @@ -2,6 +2,7 @@ import logging from collections import defaultdict from enum import IntEnum +from typing import TypedDict # Third Party Library import attrs @@ -12,6 +13,8 @@ logger = logging.getLogger(__name__) +class CloneMergeDecisionError(Exception): + pass # the possible types of decisions that can be made enumerated for # storage, these each correspond to specific instruction type @@ -39,11 +42,50 @@ class CloneMergeDecisionEnum(IntEnum): """Do nothing with the sample value (state) but squashed walkers will donate their weight to it.""" +# TODO: get this automatically +CLONE_MERGE_DECISION_ENUM_VALUES = {1, 2, 3, 4} + +class CloneMergeDecisionRecordDict(TypedDict): + decision_id: int + target_idxs: tuple[int] @attrs.define class CloneMergeDecisionRecord(BaseDecisionRecord): - decision_id: int - target_idxs: list[int] + decision_id: int = attrs.field() + target_idxs: tuple[int] = attrs.field() + + @decision_id.validator + def _check_decision_id(self, attribute, value) -> None: + if value not in CLONE_MERGE_DECISION_ENUM_VALUES: + raise ValueError(f"Invalid decision_id ({value}) must be one of {CLONE_MERGE_DECISION_ENUM_VALUES}") + + @target_idxs.validator + def _check_decision_id(self, attribute, value) -> None: + + if len(value) == 0: + raise ValueError("Must provide at least one target index in target_idxs.") + + if any(idx < 0 for idx in value): + raise ValueError("All target_idx values must be >= 0") + + def __attrs_post_init__(self) -> None: + + if self.decision_id in {1, 3, 4}: + if len(self.target_idxs) != 1: + raise CloneMergeDecisionError( + f"For decision_id ({CloneMergeDecisionEnum(self.decision_id).name}:{self.decision_id}) " + f"only a single target_idx is allowed." + ) + + else: + if len(self.target_idxs) < 2: + raise CloneMergeDecisionError( + f"For decision_id ({CloneMergeDecisionEnum(self.decision_id).name}:{self.decision_id}) " + f"more than one target_idx must be given." + ) + + def to_dict(self) -> CloneMergeDecisionRecordDict: + return attrs.asdict(self) class MultiCloneMergeDecision(BaseDecisionABC): @@ -90,7 +132,7 @@ class MultiCloneMergeDecision(BaseDecisionABC): def action( cls, walkers: list[Walker], - decisions: list[CloneMergeDecisionRecord], + decisions: list[list[CloneMergeDecisionRecord]], ) -> list[Walker]: # list for the modified walkers mod_walkers = [None for i in range(len(walkers))] diff --git a/src/wepy/resampling/decisions/decision.py b/src/wepy/resampling/decisions/decision.py index e190768d..d5d245fe 100644 --- a/src/wepy/resampling/decisions/decision.py +++ b/src/wepy/resampling/decisions/decision.py @@ -48,7 +48,7 @@ # Standard Library import logging from enum import IntEnum -from typing import Any, Union, Generic, TypeVar +from typing import Any, Union, Generic, TypeVar, Protocol, TypedDict # Third Party Library import attrs @@ -58,17 +58,25 @@ logger = logging.getLogger(__name__) +DecisionFieldDtype = Union[int,] +DecisionFieldShapeSpec = tuple[int | type(Ellipsis), ...] + +class DecisionRecord(Protocol): + + def to_dict(self) -> dict[str, DecisionFieldDtype]: ... + +class BaseDecisionRecordDict(TypedDict): + decision_id: int @attrs.define class BaseDecisionRecord: decision_id: int - -DecisionFieldDtype = Union[int,] -DecisionFieldShapeSpec = tuple[int | type(Ellipsis), ...] + def to_dict(self) -> BaseDecisionRecordDict: + return attrs.asdict(self) DecisionEnum_ = TypeVar("DecisionEnum_") -DecisionRecord_ = TypeVar("DecisionRecord", bound=BaseDecisionRecord) +DecisionRecord_ = TypeVar("DecisionRecord", bound=DecisionRecord) # ABC for the Decision class diff --git a/src/wepy/resampling/decisions/no_decision.py b/src/wepy/resampling/decisions/no_decision.py index 0dda3be4..7687648b 100644 --- a/src/wepy/resampling/decisions/no_decision.py +++ b/src/wepy/resampling/decisions/no_decision.py @@ -1,5 +1,6 @@ # Standard Library from enum import IntEnum +from typing import TypedDict # Third Party Library import attrs @@ -15,11 +16,23 @@ class NothingDecisionEnum(IntEnum): NOTHING = 0 """Do nothing with the walker.""" +class NoDecisionRecordDict(TypedDict): + decision_id: int + target_idx: int @attrs.define class NoDecisionRecord(BaseDecisionRecord): - decision_id: int - target_idx: int + decision_id: int = attrs.field() + target_idx: int = attrs.field(validator=attrs.validators.ge(0)) + + @decision_id.validator + def _check_decision_id(self, attribute, value) -> None: + + if value != NothingDecisionEnum.NOTHING.value: + raise ValueError(f"Invalid decision_id ({value}) must be {NothingDecisionEnum.NOTHING.value}") + + def to_dict(self) -> NoDecisionRecordDict: + return attrs.asdict(self) class NoDecision(BaseDecisionABC): diff --git a/src/wepy/resampling/resamplers/clone_merge.py b/src/wepy/resampling/resamplers/clone_merge.py index ec5e8255..70dade61 100644 --- a/src/wepy/resampling/resamplers/clone_merge.py +++ b/src/wepy/resampling/resamplers/clone_merge.py @@ -3,17 +3,30 @@ # Third Party Library import numpy as np +import attrs # First Party Library from wepy.resampling.decisions.clone_merge import ( CloneMergeDecisionRecord, MultiCloneMergeDecision, ) -from wepy.resampling.resamplers.resampler import ResamplerABC, ResamplerError +from wepy.resampling.resamplers.resampler import ( + ResamplerABC, + ResamplerError, +) from wepy.walker import Walker, WalkerState +from wepy.util.attrs import AttrsMappingMixin -WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) +@attrs.define +class CloneMergeResamplerRecord(AttrsMappingMixin): + # from the Decision + decision_id: int + target_idxs: tuple[int] + # extra for the resampler + step_idx: int + walker_idx: int +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) class CloneMergeResampler(ResamplerABC, Generic[WalkerState_]): """Abstract base class for resamplers using the clone-merge decision @@ -265,7 +278,8 @@ def assign_clones( # make a record for this clone walker_actions[walker_idx] = self.decision().record( - self.decision().ENUM.CLONE.value, target_idxs=tuple(clone_targets) + self.decision().ENUM.CLONE.value, + target_idxs=tuple(clone_targets,) ) return walker_actions diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py index e1b402bd..52b3ebd2 100644 --- a/src/wepy/resampling/resamplers/noresampler.py +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -1,6 +1,8 @@ # Standard Library from typing import TypedDict +import attrs + # First Party Library from wepy.resampling.decisions.no_decision import ( NoDecision, @@ -8,18 +10,22 @@ ) from wepy.resampling.resamplers.resampler import Resampler, ResamplerABC from wepy.walker import Walker +from wepy.util.attrs import AttrsMappingMixin - -class NoResamplerResamplingData(TypedDict): +@attrs.define +class NoResamplerResamplingRecord(AttrsMappingMixin): decision_id: int - target_idxs: tuple[int, ...] + target_idxs: tuple[int, ...] = attrs.field( + converter=(lambda v: tuple(v)) + ) -class NoResamplerResamplerData(TypedDict): +@attrs.define +class NoResamplerResamplerRecord(AttrsMappingMixin): pass -class NoResampler(ResamplerABC): +class NoResampler(Resampler): """The resampler which does nothing.""" DECISION = NoDecision @@ -36,18 +42,18 @@ def resample( walkers: list[Walker], ) -> tuple[ list[Walker], - list[list[NoResamplerResamplingData]], - list[NoResamplerResamplerData], + list[NoResamplerResamplingRecord], + list[NoResamplerResamplerRecord], ]: # normally decide is only for a single step and so does not # include the step_idx, so we add this to the records, and # convert the target idxs and decision_id to feature vector # arrays - _resampling_data: list[NoResamplerResamplingData] = [] + _resampling_data = [] for walker_idx in range(len(walkers)): - walker_record = NoResamplerResamplingData( + walker_record = NoResamplerResamplingRecord( decision_id=NothingDecisionEnum.NOTHING.value, target_idxs=[walker_idx], ) @@ -55,11 +61,11 @@ def resample( _resampling_data.append(walker_record) # only a single step of decisions - resampling_data = [_resampling_data] + resampling_data = _resampling_data # there is no change in state in the resampler so there are no # resampler records - resampler_data: list[NoResamplerResamplerData] = [{}] + resampler_data = [NoResamplerResamplerRecord()] # the resampled walkers are just the walkers return walkers, resampling_data, resampler_data diff --git a/src/wepy/resampling/resamplers/resampler.py b/src/wepy/resampling/resamplers/resampler.py index 1a018577..154c6678 100644 --- a/src/wepy/resampling/resamplers/resampler.py +++ b/src/wepy/resampling/resamplers/resampler.py @@ -1,6 +1,6 @@ # Standard Library import logging -from typing import Any, Generic, Literal, Protocol, TypeVar, Union +from typing import Generic, Protocol, TypeVar, Union from warnings import warn # Third Party Library @@ -10,6 +10,7 @@ from wepy.resampling.decisions.decision import BaseDecisionABC from wepy.walker import Walker, WalkerState from wepy.reporter.types import FieldShapeSpec, FieldDtype +from wepy.storage.protocol import Record logger = logging.getLogger(__name__) @@ -24,7 +25,10 @@ class ResamplerError(Exception): pass -class Resampler(Protocol, Generic[WalkerState_]): +ResamplingRecord_ = TypeVar("ResamplingRecord_", bound=Record) +ResamplerRecord_ = TypeVar("ResamplerRecord_", bound=Record) + +class Resampler(Protocol, Generic[WalkerState_, ResamplingRecord_, ResamplerRecord_]): DECISION: BaseDecisionABC CYCLE_FIELDS: tuple[str, ...] @@ -56,13 +60,12 @@ def resampler_record_field_names(cls) -> None | tuple[str, ...]: ... def resample(self, walkers: list[Walker[WalkerState_]]) -> tuple[ list[Walker[WalkerState_]], - # TODO: better types for this - list[dict[str, Any]], - list[dict[str, Any]], + list[ResamplingRecord_], + list[ResamplerRecord_], ]: ... -class ResamplerABC(Resampler): +class ResamplerABC(Resampler, Generic[WalkerState_, ResamplingRecord_, ResamplerRecord_]): """Abstract base class for implementing resamplers. All subclasses of Resampler must implement the 'resample' method. @@ -650,8 +653,8 @@ def resample( debug_mode: bool = False, ) -> tuple[ list[Walker[WalkerState_]], - list[dict[str, Any]], - list[dict[str, Any]], + list[ResamplingRecord_], + list[ResamplerRecord_], ]: """Perform resampling on the set of walkers. @@ -680,3 +683,5 @@ def resample( """ raise NotImplementedError + + diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 3710ab8d..5f05c044 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -13,9 +13,10 @@ # First Party Library from wepy.resampling.decisions.clone_merge import CloneMergeDecisionRecord from wepy.resampling.distances.base import Distance -from wepy.resampling.resamplers.clone_merge import CloneMergeResampler +from wepy.resampling.resamplers.clone_merge import CloneMergeResampler, CloneMergeResamplerRecord from wepy.util.multiprocessing import proc_pool_worker_setup, queue_listener_context from wepy.walker import Walker, WalkerState +from wepy.util.attrs import AttrsMappingMixin logger = logging.getLogger(__name__) @@ -49,8 +50,8 @@ def __call__(self, state: WalkerState_) -> DistanceImage_: logger.info("Finished image computation") return result - -class REVOResamplerResamplerData(TypedDict): +@attrs.define +class REVOResamplerResamplerRecord(AttrsMappingMixin): distance_matrix: np.typing.ArrayLike num_walkers: int variation: float @@ -156,21 +157,26 @@ class REVOResampler( RESAMPLING_RECORD_FIELDS = CloneMergeResampler.RESAMPLING_RECORD_FIELDS - RESAMPLER_FIELDS = CloneMergeResampler.RESAMPLER_FIELDS + ( - "num_walkers", - "distance_matrix", - "variation", - ) - RESAMPLER_SHAPES = CloneMergeResampler.RESAMPLER_SHAPES + ( - (1,), - Ellipsis, - (1,), - ) - RESAMPLER_DTYPES = CloneMergeResampler.RESAMPLER_DTYPES + ( - int, - float, - float, - ) + RESAMPLER_FIELDS = CloneMergeResampler.RESAMPLER_FIELDS + ("variation",) + + # TOREV: not using these anymore + # + ( + # "num_walkers", + # "distance_matrix", + # "variation", + # ) + RESAMPLER_SHAPES = CloneMergeResampler.RESAMPLER_SHAPES + (1,) + # + ( + # (1,), + # Ellipsis, + # (1,), + # ) + RESAMPLER_DTYPES = CloneMergeResampler.RESAMPLER_DTYPES + (float,) + # + ( + # int, + # float, + # float, + # ) # fields that can be used for a table like representation RESAMPLER_RECORD_FIELDS = CloneMergeResampler.RESAMPLER_RECORD_FIELDS + ( @@ -701,19 +707,10 @@ def decide( logger.info(f"Finished optimization: {final_variation}") logger.info("Assigning clones") - walker_records = self.assign_clones(merge_groups, walker_clone_nums) + decision_records = self.assign_clones(merge_groups, walker_clone_nums) - # TOREV: this was taken out as it probably wasn't necessary, - # but this may be critical in analyses, check this - # because there is only one step in resampling here we just - # add another field for the step as 0 and add the walker index - # to its record as well - # for walker_idx, walker_record in enumerate(walker_actions): - # walker_record["step_idx"] = np.array([0]) - # walker_record["walker_idx"] = np.array([walker_idx]) - - return walker_records, final_variation + return decision_records, final_variation def _all_to_all_distance( self, @@ -818,8 +815,8 @@ def resample( walkers: list[Walker[WalkerState_]], ) -> tuple[ list[Walker[WalkerState_]], - list[list[CloneMergeDecisionRecord]], - list[REVOResamplerResamplerData], + list[CloneMergeResamplerRecord], + list[REVOResamplerResamplerRecord], ]: """Resamples walkers based on REVO algorithm @@ -858,11 +855,31 @@ def resample( # determine cloning and merging actions to be performed, by # maximizing the variation, i.e. the Decider logger.info("Making resampling decisions") - resampling_data, variation = self.decide( + decision_records, variation = self.decide( walker_weights, num_walker_copies, distance_matrix ) logger.info("Finished resampling decisions") + # actually do the cloning and merging of the walkers + resampled_walkers = self.DECISION.action(walkers, [decision_records]) + + ## Generate the full resampling records + + # because there is only one step in resampling here we just + # add another field for the step as 0 and add the walker index + # to its record as well + resampling_records = [] + for walker_idx, decision_record in enumerate(decision_records): + resampling_record = CloneMergeResamplerRecord( + **attrs.asdict(decision_record), + step_idx=0, + walker_idx=walker_idx + ) + resampling_records.append(resampling_record) + # TODO: handle the 2D or 1D issue + # resampling_record["step_idx"] = np.array([0]) + # walker_record["walker_idx"] = np.array([walker_idx]) + # TOREV: need to understand the impact of this on the data # ingestion aspect of things. Otherwise not doing this here # would be much cleaner. @@ -872,17 +889,15 @@ def resample( # record["target_idxs"] = np.array(record["target_idxs"]) # record["decision_id"] = np.array([record["decision_id"]]) - # actually do the cloning and merging of the walkers - resampled_walkers = self.DECISION.action(walkers, [resampling_data]) # flatten the distance matrix and give the number of walkers # as well for the resampler data, there is just one per cycle - resampler_data = [ - { + resampler_records = [ + REVOResamplerResamplerRecord(**{ "distance_matrix": np.ravel(np.array(distance_matrix)), "num_walkers": len(walkers), "variation": variation, - } + }) ] # TOREV: ditto, wrt to data interfaces @@ -894,7 +909,7 @@ def resample( # } # ] - return resampled_walkers, resampling_data, resampler_data + return resampled_walkers, resampling_records, resampler_records @attrs.define diff --git a/src/wepy/storage/__init__.py b/src/wepy/storage/__init__.py new file mode 100644 index 00000000..509d9132 --- /dev/null +++ b/src/wepy/storage/__init__.py @@ -0,0 +1 @@ +"""Storage protocol and methods.""" diff --git a/src/wepy/storage/protocol.py b/src/wepy/storage/protocol.py new file mode 100644 index 00000000..c747c8b6 --- /dev/null +++ b/src/wepy/storage/protocol.py @@ -0,0 +1,12 @@ +"""Defines a generic protocol for storage of data. + +This should only define the interfaces between the generating +components (resamplers, runner, boundary conditions, sim_manager) and +the reporting and storage backends should utilize. + +""" +from collections.abc import Mapping +from numpy.typing import NDArray + +RecordValueDtype = int | float | NDArray +Record = Mapping[str, RecordValueDtype] diff --git a/tests/unit/test_resampling/test_decisions/test_clone_merge.py b/tests/unit/test_resampling/test_decisions/test_clone_merge.py index 64f1c86b..852f44f0 100644 --- a/tests/unit/test_resampling/test_decisions/test_clone_merge.py +++ b/tests/unit/test_resampling/test_decisions/test_clone_merge.py @@ -7,11 +7,87 @@ CloneMergeDecisionEnum, CloneMergeDecisionRecord, MultiCloneMergeDecision, + CloneMergeDecisionError, ) from wepy.runners.mock import MockState from wepy.walker import Walker +class Test_CloneMergeDecisionRecord: + + def test___init__(self): + + # single target records + CloneMergeDecisionRecord( + decision_id=1, + target_idxs=(0,), + ) + CloneMergeDecisionRecord( + decision_id=3, + target_idxs=(0,), + ) + CloneMergeDecisionRecord( + decision_id=4, + target_idxs=(0,), + ) + + # clone + CloneMergeDecisionRecord( + decision_id=2, + target_idxs=(0,1), + ) + + with pytest.raises(ValueError): + CloneMergeDecisionRecord( + decision_id=7, + target_idxs=(0,), + ) + + with pytest.raises(ValueError): + CloneMergeDecisionRecord( + decision_id=1, + target_idxs=(), + ) + + with pytest.raises(ValueError): + CloneMergeDecisionRecord( + decision_id=1, + target_idxs=(-1,), + ) + + with pytest.raises(CloneMergeDecisionError): + CloneMergeDecisionRecord( + decision_id=1, + target_idxs=(0,1,), + ) + + with pytest.raises(CloneMergeDecisionError): + CloneMergeDecisionRecord( + decision_id=3, + target_idxs=(0,1,), + ) + with pytest.raises(CloneMergeDecisionError): + CloneMergeDecisionRecord( + decision_id=4, + target_idxs=(0,1,), + ) + + with pytest.raises(CloneMergeDecisionError): + CloneMergeDecisionRecord( + decision_id=2, + target_idxs=(0,), + ) + + def test_to_dict(self): + + assert CloneMergeDecisionRecord( + decision_id=1, + target_idxs=(0,) + ).to_dict() == { + "decision_id" : 1, + "target_idxs" : (0,), + } + class TestMultiCloneMergeDecision: def test_action(self): @@ -230,8 +306,8 @@ def test_parents(self): MultiCloneMergeDecision.parents( [ CloneMergeDecisionRecord( - decision_id=0, - target_idxs=idx, + decision_id=1, + target_idxs=(idx,), ) for idx in range(4) ], diff --git a/tests/unit/test_resampling/test_decisions/test_decision.py b/tests/unit/test_resampling/test_decisions/test_decision.py index 33ebcdb7..89c1412c 100644 --- a/tests/unit/test_resampling/test_decisions/test_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_decision.py @@ -21,6 +21,10 @@ class MockDecision(BaseDecisionABC): DEFAULT_DECISION = ENUM.NOTHING ANCESTOR_DECISION_IDS = (ENUM.NOTHING.value,) +class Test_BaseDecisionRecord: + + def test_to_dict(self): + assert BaseDecisionRecord(decision_id=1).to_dict() == {"decision_id" : 1} class Test_Decision: diff --git a/tests/unit/test_resampling/test_decisions/test_no_decision.py b/tests/unit/test_resampling/test_decisions/test_no_decision.py index 8b8ade64..f6d65285 100644 --- a/tests/unit/test_resampling/test_decisions/test_no_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_no_decision.py @@ -1,3 +1,4 @@ +import pytest # First Party Library from wepy.resampling.decisions.no_decision import ( NoDecision, @@ -7,8 +8,38 @@ from wepy.runners.mock import MockState from wepy.walker import Walker +class Test_NoDecisionRecord: -class TestNoDecision: + def test___init__(self): + + NoDecisionRecord( + decision_id=0, + target_idx=1, + ) + + with pytest.raises(ValueError): + NoDecisionRecord( + decision_id=1, + target_idx=1, + ) + + with pytest.raises(ValueError): + NoDecisionRecord( + decision_id=0, + target_idx=-1, + ) + + def test_to_dict(self): + + assert NoDecisionRecord( + decision_id=0, + target_idx=0 + ).to_dict() == { + "decision_id" : 0, + "target_idx" : 0, + } + +class Test_NoDecision: def test_action(self): diff --git a/tests/unit/test_resampling/test_resamplers/test_noresampler.py b/tests/unit/test_resampling/test_resamplers/test_noresampler.py index 1e485177..b35cee78 100644 --- a/tests/unit/test_resampling/test_resamplers/test_noresampler.py +++ b/tests/unit/test_resampling/test_resamplers/test_noresampler.py @@ -1,9 +1,12 @@ # First Party Library from wepy.resampling.decisions.no_decision import NothingDecisionEnum -from wepy.resampling.resamplers.noresampler import NoResampler, NoResamplerFactory +from wepy.resampling.resamplers.noresampler import NoResampler, NoResamplerFactory, NoResamplerResamplingRecord, NoResamplerResamplerRecord from wepy.runners.mock import MockState from wepy.walker import Walker +def test_NoResamplerResamplingRecord(): + + assert NoResamplerResamplingRecord(0, (1,)) == NoResamplerResamplingRecord(0, (1,)) class Test_NoResampler: @@ -30,22 +33,20 @@ def test_resample(self): assert resampler.resample(walkers) == ( walkers, [ - [ - dict( + NoResamplerResamplingRecord( decision_id=NothingDecisionEnum.NOTHING.value, target_idxs=[0], ), - dict( + NoResamplerResamplingRecord( decision_id=NothingDecisionEnum.NOTHING.value, target_idxs=[1], ), - dict( + NoResamplerResamplingRecord( decision_id=NothingDecisionEnum.NOTHING.value, target_idxs=[2], ), - ] ], - [{}], + [NoResamplerResamplerRecord()], ) diff --git a/tests/unit/test_resampling/test_resamplers/test_revo.py b/tests/unit/test_resampling/test_resamplers/test_revo.py index df5a0ed7..d932eba3 100644 --- a/tests/unit/test_resampling/test_resamplers/test_revo.py +++ b/tests/unit/test_resampling/test_resamplers/test_revo.py @@ -330,7 +330,7 @@ def test_resample(self): num_proc=1, ) - resampled_walkers, resampling_data, resampler_data = resampler.resample( + resampled_walkers, resampling_records, resampler_records = resampler.resample( [ Walker( MockState(1), @@ -344,3 +344,5 @@ def test_resample(self): ) assert len(resampled_walkers) == 2 + assert len(resampling_records) == 2 + assert len(resampler_records) == 1 From e838163c77fcd1c3dfe25e3d68cfb7b38b690f39 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sat, 13 Dec 2025 01:13:55 -0500 Subject: [PATCH 110/143] add record fields to storage protocol --- src/wepy/reporter/base.py | 2 - src/wepy/reporter/types.py | 11 ---- src/wepy/resampling/resamplers/noresampler.py | 2 +- src/wepy/resampling/resamplers/resampler.py | 25 ++++----- src/wepy/storage/protocol.py | 55 +++++++++++++++++++ 5 files changed, 67 insertions(+), 28 deletions(-) delete mode 100644 src/wepy/reporter/types.py diff --git a/src/wepy/reporter/base.py b/src/wepy/reporter/base.py index 6c49e8ca..6bd8fe54 100644 --- a/src/wepy/reporter/base.py +++ b/src/wepy/reporter/base.py @@ -10,8 +10,6 @@ from wepy.walker import Walker from wepy.work_mapper.base import WorkMapper -from .types import FieldShapeSpec, FieldDtype - logger = logging.getLogger(__name__) diff --git a/src/wepy/reporter/types.py b/src/wepy/reporter/types.py deleted file mode 100644 index 3737b6f5..00000000 --- a/src/wepy/reporter/types.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Union, Literal - -import numpy as np - -FieldShapeSpec = Union[ - tuple[int, ...], - Literal[Ellipsis], - None, -] - -FieldDtype = np.dtype | None diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py index 52b3ebd2..d14a2894 100644 --- a/src/wepy/resampling/resamplers/noresampler.py +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -25,7 +25,7 @@ class NoResamplerResamplerRecord(AttrsMappingMixin): pass -class NoResampler(Resampler): +class NoResampler(ResamplerABC): """The resampler which does nothing.""" DECISION = NoDecision diff --git a/src/wepy/resampling/resamplers/resampler.py b/src/wepy/resampling/resamplers/resampler.py index 154c6678..d0d17c31 100644 --- a/src/wepy/resampling/resamplers/resampler.py +++ b/src/wepy/resampling/resamplers/resampler.py @@ -9,14 +9,12 @@ # First Party Library from wepy.resampling.decisions.decision import BaseDecisionABC from wepy.walker import Walker, WalkerState -from wepy.reporter.types import FieldShapeSpec, FieldDtype -from wepy.storage.protocol import Record +from wepy.storage.protocol import Record, RecordFieldShapeSpec, RecordFieldDtype logger = logging.getLogger(__name__) WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) - class ResamplerError(Exception): """Error raised when some constraint on resampling properties is violated. @@ -24,7 +22,6 @@ class ResamplerError(Exception): pass - ResamplingRecord_ = TypeVar("ResamplingRecord_", bound=Record) ResamplerRecord_ = TypeVar("ResamplerRecord_", bound=Record) @@ -32,24 +29,24 @@ class Resampler(Protocol, Generic[WalkerState_, ResamplingRecord_, ResamplerReco DECISION: BaseDecisionABC CYCLE_FIELDS: tuple[str, ...] - CYCLE_SHAPES: tuple[tuple[int, ...], ...] - CYCLE_DTYPES: tuple[int | float, ...] - CYCLE_RECORD_FIELDS: None | tuple[str, ...] + CYCLE_SHAPES: tuple[RecordFieldShapeSpec, ...] + CYCLE_DTYPES: tuple[RecordFieldDtype, ...] + CYCLE_RECORD_FIELDS: tuple[str, ...] | None RESAMPLING_FIELDS: tuple[str, ...] - RESAMPLING_SHAPES: tuple[FieldShapeSpec, ...] - RESAMPLING_DTYPES: tuple[FieldDtype, ...] + RESAMPLING_SHAPES: tuple[RecordFieldShapeSpec | None, ...] + RESAMPLING_DTYPES: tuple[RecordFieldDtype | None, ...] RESAMPLING_RECORD_FIELDS: None | tuple[str, ...] RESAMPLER_FIELDS: tuple[str, ...] - RESAMPLER_SHAPES: tuple[FieldShapeSpec, ...] + RESAMPLER_SHAPES: tuple[RecordFieldShapeSpec | None, ...] - RESAMPLER_DTYPES: tuple[FieldDtype, ...] - RESAMPLER_RECORD_FIELDS: None | tuple[str, ...] + RESAMPLER_DTYPES: tuple[RecordFieldDtype | None, ...] + RESAMPLER_RECORD_FIELDS: tuple[str, ...] | None @classmethod def resampling_fields(cls) -> tuple[ tuple[str, ...], - tuple[FieldShapeSpec, ...], - tuple[FieldDtype, ...], + tuple[RecordFieldShapeSpec, ...], + tuple[RecordFieldDtype, ...], ]: ... @classmethod diff --git a/src/wepy/storage/protocol.py b/src/wepy/storage/protocol.py index c747c8b6..52c1feb3 100644 --- a/src/wepy/storage/protocol.py +++ b/src/wepy/storage/protocol.py @@ -7,6 +7,61 @@ """ from collections.abc import Mapping from numpy.typing import NDArray +from typing import Union, Literal + +import numpy as np RecordValueDtype = int | float | NDArray Record = Mapping[str, RecordValueDtype] + +# Numpy-style shapes of all fields produced in records. +# +# There should be the same number of elements as there are in the +# corresponding 'FIELDS' class constant. +# +# Each entry should either be: +# +# A. A tuple of ints that specify the shape of the field element +# array. +# +# B. Ellipsis, indicating that the field is variable length and +# limited to being a rank one array (e.g. (3,) or (1,)). +# Note that the shapes must be tuple and not simple integers for rank-1 +# arrays. +# +# Option B will result in the special h5py datatype 'vlen' and +# should not be used for large datasets for efficiency reasons. +RecordFieldShapeSpec = Union[ + tuple[int, ...], + Literal[Ellipsis], +] + + + +# There should be the same number of elements as there are in the +# corresponding 'FIELDS' class constant. +# +# Each entry should either be: +# +# A. A `numpy.dtype` object. + +RecordFieldDtype = Union[ + np.int16, + np.int32, + np.int64, + np.uint8, + np.uint16, + np.uint32, + np.uint64, + np.float16, + np.float32, + np.float64, + np.bool, +] + + +RecordFieldSpec = tuple[ + str, # name + RecordFieldShapeSpec, # shape + RecordFieldDtype, # dtype +] From cc1b7e76d06b1825e95fc8a03ec4083df64c7dfe Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sat, 13 Dec 2025 02:16:26 -0500 Subject: [PATCH 111/143] finalized record data interfaces and working HDF5 Realistic example is running again. Still needs to add e2e data conformance tests for the rest of the HDF5 and for the realistic tests --- src/wepy/hdf5.py | 96 ++++-- src/wepy/reporter/hdf5.py | 148 +++++---- src/wepy/resampling/decisions/clone_merge.py | 4 +- src/wepy/resampling/resamplers/clone_merge.py | 27 +- src/wepy/resampling/resamplers/revo.py | 90 ++---- src/wepy/runners/openmm/runner.py | 6 +- .../integration/test_openmm/test_realistic.py | 49 ++- tests/unit/test_hdf5.py | 304 +++++++++++++++++- tests/unit/test_reporter/test_hdf5.py | 81 +++++ 9 files changed, 633 insertions(+), 172 deletions(-) diff --git a/src/wepy/hdf5.py b/src/wepy/hdf5.py index cd6c6f72..9cbf2986 100644 --- a/src/wepy/hdf5.py +++ b/src/wepy/hdf5.py @@ -404,7 +404,6 @@ from warnings import warn # Third Party Library -import attrs import h5py import numpy as np from numpy.typing import NDArray @@ -418,12 +417,17 @@ ) from wepy.util.util import traj_box_vectors_to_lengths_angles from wepy.reporter.file import FileMode -from wepy.reporter.types import( - FieldShapeSpec, - FieldDtype, -) from wepy.typing import Shape, Idxs, IdxArray from wepy.walker import WalkerStateBox, Walker +from wepy.resampling.decisions.decision import DecisionRecord + +from wepy.storage.protocol import ( + RecordValueDtype, + Record, + RecordFieldShapeSpec, + RecordFieldDtype, + RecordFieldSpec, +) # optional dependencies try: @@ -443,7 +447,7 @@ H5AttrDtype = str | int | float H5Attrs = dict[str, H5AttrDtype] - + ## h5py settings # we set the libver to always be the latest (which should be 1.10) so @@ -1055,8 +1059,8 @@ def __init__( n_dims: int | None = None, alt_reps: dict[str, IdxArray] | None = None, main_rep_idxs: IdxArray | None = None, - feature_shapes_overrides: dict[str, FieldShapeSpec] | None = None, - feature_dtypes_overrides: dict[str, FieldDtype] | None = None, + feature_shapes_overrides: dict[str, RecordFieldShapeSpec] | None = None, + feature_dtypes_overrides: dict[str, RecordFieldDtype] | None = None, ): """Constructor for the WepyHDF5 class. @@ -1464,7 +1468,12 @@ def _add_init_walkers(self, init_walkers_grp: h5py.Group, init_walkers: list[Wal # (wrapping it in another list) walker_grp.create_dataset(field_key, data=np.array([field_value])) - def _init_run_sporadic_record_grp(self, run_idx, run_record_key, fields): + def _init_run_sporadic_record_grp( + self, + run_idx: int, + run_record_key: str, + fields: list[RecordFieldSpec], + ) -> h5py.Group: """Initialize a sporadic record group for a run. Parameters @@ -1489,7 +1498,7 @@ def _init_run_sporadic_record_grp(self, run_idx, run_record_key, fields): # initialize the cycles dataset that maps when the records # were recorded - record_grp.create_dataset(CYCLE_IDXS, (0,), dtype=int, maxshape=(None,)) + record_grp.create_dataset(CYCLE_IDXS, (0,), dtype=np.int64, maxshape=(None,)) # for each field simply create the dataset for field_name, field_shape, field_dtype in fields: @@ -1532,8 +1541,13 @@ def _init_run_continual_record_grp(self, run_idx, run_record_key, fields): return record_grp def _init_run_records_field( - self, run_idx, run_record_key, field_name, field_shape, field_dtype - ): + self, + run_idx: int, + run_record_key: str, + field_name: str, + field_shape: RecordFieldShapeSpec, + field_dtype: RecordFieldDtype, + ) -> h5py.Dataset: """Initialize a single field for a run record group. Parameters @@ -1561,7 +1575,7 @@ def _init_run_records_field( if field_shape is Ellipsis: # make a special dtype that allows it to be # variable length - vlen_dt = h5py.special_dtype(vlen=field_dtype) + vlen_dt = h5py.vlen_dtype(field_dtype) # this is only allowed to be a single dimension # since no real shape was given @@ -1581,7 +1595,10 @@ def _init_run_records_field( return dset - def _is_sporadic_records(self, run_record_key): + @staticmethod + def _is_sporadic_records( + run_record_key: str + ) -> bool: """Tests whether a record group is sporadic or not. Parameters @@ -2037,8 +2054,12 @@ def _set_field_feature_dtype(self, field_path, field_feature_dtype): self._add_field_feature_dtype(field_path, field_feature_dtype) def _extend_run_record_data_field( - self, run_idx, run_record_key, field_name, field_data - ): + self, + run_idx: int, + run_record_key: str, + field_name: str, + field_data: NDArray, + ) -> None: """Primitive record append method. Adds data for a single field dataset in a run records group. This @@ -2060,7 +2081,6 @@ def _extend_run_record_data_field( records_grp = self.h5["{}/{}/{}".format(RUNS, run_idx, run_record_key)] field = records_grp[field_name] - breakpoint() # make sure this is a feature vector assert ( len(field_data.shape) > 1 @@ -4290,7 +4310,11 @@ def init_run_bc(self, run_idx, bc): # application level methods for initializing the run records # groups with just the fields and without the objects - def init_run_fields_resampling(self, run_idx: int, fields: list[str]) -> h5py.Group: + def init_run_fields_resampling( + self, + run_idx: int, + fields: list[RecordFieldSpec], + ) -> h5py.Group: """Initialize this record group fields datasets. Parameters @@ -4309,7 +4333,11 @@ def init_run_fields_resampling(self, run_idx: int, fields: list[str]) -> h5py.Gr return grp - def init_run_fields_resampling_decision(self, run_idx, decision_enum_dict): + def init_run_fields_resampling_decision( + self, + run_idx: int, + decision_enum_dict: dict[str,str] + ) -> None: """Initialize the decision group for this run. Parameters @@ -4408,7 +4436,7 @@ def init_run_record_grp( self, run_idx: int, run_record_key: str, - fields: list[str], + fields: list[RecordFieldSpec], ) -> h5py.Group: """Initialize a record group for a run. @@ -4782,7 +4810,12 @@ def extend_cycle_progress_records(self, run_idx, cycle_idx, progress_data): """ self.extend_cycle_run_group_records(run_idx, PROGRESS, cycle_idx, progress_data) - def extend_cycle_resampling_records(self, run_idx, cycle_idx, resampling_data): + def extend_cycle_resampling_records( + self, + run_idx: int, + cycle_idx: int, + resampling_data: list[Record], + ) -> None: """Add records for each field for this record group. Parameters @@ -4790,14 +4823,21 @@ def extend_cycle_resampling_records(self, run_idx, cycle_idx, resampling_data): run_idx : int cycle_idx : int The cycle index these records correspond to. - resampling_data : dict of str : arraylike + resampling_data : list[dict of str : arraylike] Mapping of the record group fields to a collection of values for each field. """ + # TODO: we should probably expand the data arrays to the + # feature arrays if that is required so the data types from + # the outside stay cleaner. + self.extend_cycle_run_group_records( - run_idx, RESAMPLING, cycle_idx, resampling_data + run_idx, + RESAMPLING, + cycle_idx, + resampling_data, ) def extend_cycle_resampler_records(self, run_idx, cycle_idx, resampler_data): @@ -4818,8 +4858,12 @@ def extend_cycle_resampler_records(self, run_idx, cycle_idx, resampler_data): ) def extend_cycle_run_group_records( - self, run_idx, run_record_key, cycle_idx, fields_data - ): + self, + run_idx: int, + run_record_key: str, + cycle_idx: int, + fields_data: list[Record], + ) -> None: """Extend data for a whole records group. This must have the cycle index for the data it is appending as @@ -4858,7 +4902,7 @@ def extend_cycle_run_group_records( # then add all the data for the field for record in fields_data: - for field_name, field_data in attrs.asdict(record).items(): + for field_name, field_data in record.items(): self._extend_run_record_data_field( run_idx, run_record_key, field_name, np.array([field_data]) ) diff --git a/src/wepy/reporter/hdf5.py b/src/wepy/reporter/hdf5.py index 436f5a26..75e502e5 100644 --- a/src/wepy/reporter/hdf5.py +++ b/src/wepy/reporter/hdf5.py @@ -1,7 +1,7 @@ # Standard Library from pathlib import Path import logging -from typing import Self, TypedDict, Literal, Generic, TypeVar +from typing import Self, TypedDict, Literal, Generic, TypeVar, Any # Standard Library @@ -15,9 +15,9 @@ SimComponentArgs, CycleReportDict, ) -from wepy.reporter.types import ( - FieldShapeSpec, - FieldDtype, +from wepy.storage.protocol import ( + RecordFieldShapeSpec, + RecordFieldDtype, ) from wepy.reporter.file import FileReporterABC, FileMode from wepy.util.json_top import json_top_atom_count @@ -25,10 +25,17 @@ from wepy.resampling.resamplers.resampler import Resampler from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.typing import Shape, Idxs, IdxArray +from wepy.storage.protocol import Record logger = logging.getLogger(__name__) WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) +ResamplingRecord_ = TypeVar("ResamplingRecord_", bound=Record) +ResamplerRecord_ = TypeVar("ResamplerRecord_", bound=Record) + +WarpingRecord_ = TypeVar("WarpingRecord_", bound=Record) +BCRecord_ = TypeVar("BCRecord_", bound=Record) +ProgressRecord_ = TypeVar("ProgressRecord_", bound=Record) class UnitError(Exception): pass @@ -36,7 +43,17 @@ class UnitError(Exception): # TODO: support for pint Quantity = openmm.unit.Quantity -class WepyHDF5Reporter(FileReporterABC, Generic[WalkerState_]): +class WepyHDF5Reporter( + FileReporterABC, + Generic[ + WalkerState_, + ResamplingRecord_, + ResamplerRecord_, + WarpingRecord_, + BCRecord_, + ProgressRecord_, + ], +): """Reporter for generating an HDF5 format (WepyHDF5) data file from simulations. @@ -67,8 +84,8 @@ class WepyHDF5Reporter(FileReporterABC, Generic[WalkerState_]): swmr_mode: bool save_fields: tuple[str, ...] | None _sparse_fields: dict[str, int] - _feature_shapes: dict[str, FieldShapeSpec] | None - _feature_dtypes: dict[str, FieldDtype] | None + _feature_shapes: dict[str, RecordFieldShapeSpec] | None + _feature_dtypes: dict[str, RecordFieldDtype] | None _n_dims: int resampling_fields: tuple[str, ...] decision_enum_dict: dict[str, int] @@ -115,8 +132,8 @@ def __init__( # should be derived from runner metadata in the common case. I # think in most cases they are determined dynamically, so this # needs to be amended. - feature_shapes: dict[str, FieldShapeSpec] | None = None, - feature_dtypes: dict[str, FieldDtype] | None = None, + feature_shapes: dict[str, RecordFieldShapeSpec] | None = None, + feature_dtypes: dict[str, RecordFieldDtype] | None = None, # Resampling optionals resampling_records: tuple[str, ...] | None = None, # Resampler fields are optional @@ -385,8 +402,8 @@ def from_components( file_path: Path, topology: str, resampler_class: type[Resampler], - feature_shapes: dict[str, FieldShapeSpec] | None = None, - feature_dtypes: dict[str, FieldDtype] | None = None, + feature_shapes: dict[str, RecordFieldShapeSpec] | None = None, + feature_dtypes: dict[str, RecordFieldDtype] | None = None, boundary_conditions_class: type[BoundaryConditions] | None = None, swmr_mode: bool = False, save_fields: tuple[str, ...] | None = None, @@ -741,33 +758,16 @@ def init(self, **kwargs: SimComponentArgs) -> None: progress_records=self.progress_records, ) - def cleanup(self, **kwargs): - # it should be already closed at this point but just in case - if not self.wepy_h5.closed: - self.wepy_h5.close() - - # remove reference to the WepyHDF5 file so we can serialize this object - del self.wepy_h5 - - super().cleanup(**kwargs) - def report( self, - new_walkers=None, - cycle_idx=None, - warp_data=None, - bc_data=None, - progress_data=None, - resampling_data=None, - resampler_data=None, - **kwargs, - ): - n_walkers = len(new_walkers) + **kwargs: CycleReportDict, + ) -> None: + n_walkers = len(kwargs["new_walkers"]) # determine which fields to save. If there were none specified # save all of them if self.save_fields is None: - save_fields = list(new_walkers[0].state.dict().keys()) + save_fields = list(kwargs["new_walkers"][0].state.dict().keys()) else: save_fields = self.save_fields @@ -777,7 +777,7 @@ def report( self.wepy_h5.swmr_mode = True # add trajectory data for the walkers - for walker_idx, walker in enumerate(new_walkers): + for walker_idx, walker in enumerate(kwargs["new_walkers"]): walker_weight = walker.weight walker_data = walker.state.dict() @@ -797,12 +797,24 @@ def report( # if this is a sparse field we decide # whether it is a valid cycle to save on if field_path in self._sparse_fields: - if cycle_idx % self._sparse_fields[field_path] != 0: + if kwargs["cycle_idx"] % self._sparse_fields[field_path] != 0: # this is not a valid cycle so we # remove from the walker_data walker_data.pop(field_path) continue + # Convert the walker data to plain non-quantity values. Only + # do this for the save fields to avoid expensive conversions + # for unused fields (e.g. forces) + # + # NOTE: do this before creating derived fields so the + # Quantities don't propagate to them + _state_noq, _units_used = self._resolve_state_units( + units=self.units, + state=WalkerStateBox(**walker_data), + ) + _walker_data_noq = _state_noq.dict() + # Add the alt_reps fields by slicing the positions for alt_rep_key in self.alt_reps_to_save: @@ -814,30 +826,32 @@ def report( # check to make sure this is a cycle this is # to be saved to, if it is not continue on to # the next field without saving this one - if cycle_idx % self._sparse_fields[alt_rep_path] != 0: + if kwargs["cycle_idx"] % self._sparse_fields[alt_rep_path] != 0: continue # slice them and save them # if the idxs are None we want all of the atoms if alt_rep_idxs is None: - alt_rep_data = walker_data["positions"][:] + alt_rep_data = _walker_data_noq["positions"][:] # otherwise get only th atoms we want else: - alt_rep_data = walker_data["positions"][alt_rep_idxs] - walker_data[alt_rep_path] = alt_rep_data + alt_rep_data = _walker_data_noq["positions"][alt_rep_idxs] + + _walker_data_noq[alt_rep_path] = alt_rep_data # lastly reduce the atoms for the main representation # if this option was given if self.main_rep_idxs is not None: - walker_data["positions"] = walker_data["positions"][ + _walker_data_noq["positions"] = _walker_data_noq["positions"][ self.main_rep_idxs ] + # for all of these fields we wrap them in another # dimension to make them feature vectors for field_path in list(walker_data.keys()): - walker_data[field_path] = np.array([walker_data[field_path]]) + _walker_data_noq[field_path] = np.array([_walker_data_noq[field_path]]) # save the data to the HDF5 file for this walker @@ -848,37 +862,47 @@ def report( self.wepy_run_idx, walker_idx, weights=np.array([[walker_weight]]), - data=walker_data, + data=_walker_data_noq, ) # start a new trajectory else: # add the traj for the walker with the data - traj_grp = self.wepy_h5.add_traj( self.wepy_run_idx, weights=np.array([[walker_weight]]), - data=walker_data, + data=_walker_data_noq, ) # add as metadata the cycle idx where this walker started - traj_grp.attrs["cycle_idx"] = cycle_idx + traj_grp.attrs["cycle_idx"] = kwargs["cycle_idx"] # report the boundary conditions records data, if boundary # conditions were initialized if self.warping_fields is not None: - self._report_warping(cycle_idx, warp_data) - self._report_bc(cycle_idx, bc_data) - self._report_progress(cycle_idx, progress_data) + self._report_warping(kwargs["cycle_idx"], kwargs["warp_data"]) + self._report_bc(kwargs["cycle_idx"], kwargs["bc_data"]) + self._report_progress(kwargs["cycle_idx"], kwargs["progress_data"]) # report the resampling records data - self._report_resampling(cycle_idx, resampling_data) + self._report_resampling( + kwargs["cycle_idx"], + kwargs["resampling_data"], + ) + + self._report_resampler(kwargs["cycle_idx"], kwargs["resampler_data"]) - self._report_resampler(cycle_idx, resampler_data) - super().report(**kwargs) + def cleanup(self, **kwargs: SimComponentArgs) -> None: + # # it should be already closed at this point but just in case + # if not self.wepy_h5.closed: + # self.wepy_h5.close() + # remove reference to the WepyHDF5 file so we can serialize this object + del self.wepy_h5 + + # sporadic - def _report_warping(self, cycle_idx, warping_data): + def _report_warping(self, cycle_idx: int, warping_data: list[WarpingRecord_]) -> None: """Method to write warping specific information. Parameters @@ -896,7 +920,7 @@ def _report_warping(self, cycle_idx, warping_data): self.wepy_run_idx, cycle_idx, warping_data ) - def _report_bc(self, cycle_idx, bc_data): + def _report_bc(self, cycle_idx: int, bc_data: list[BCRecord_]) -> None: """Method to write boundary condition update specific information. Parameters @@ -912,7 +936,11 @@ def _report_bc(self, cycle_idx, bc_data): if len(bc_data) > 0: self.wepy_h5.extend_cycle_bc_records(self.wepy_run_idx, cycle_idx, bc_data) - def _report_resampler(self, cycle_idx, resampler_data): + def _report_resampler( + self, + cycle_idx: int, + resampler_data: list[ResamplerRecord_], + ) -> None: """Method to write resampler update specific information. Parameters @@ -927,12 +955,18 @@ def _report_resampler(self, cycle_idx, resampler_data): if len(resampler_data) > 0: self.wepy_h5.extend_cycle_resampler_records( - self.wepy_run_idx, cycle_idx, resampler_data + self.wepy_run_idx, + cycle_idx, + resampler_data, ) # the resampling records are provided every cycle but they need to # be saved as sporadic because of the variable number of walkers - def _report_resampling(self, cycle_idx, resampling_data): + def _report_resampling( + self, + cycle_idx: int, + resampling_records: list[ResamplingRecord_], + ) -> None: """Method to write resampling specific information. Parameters @@ -946,11 +980,11 @@ def _report_resampling(self, cycle_idx, resampling_data): """ self.wepy_h5.extend_cycle_resampling_records( - self.wepy_run_idx, cycle_idx, resampling_data + self.wepy_run_idx, cycle_idx, resampling_records ) # continual - def _report_progress(self, cycle_idx, progress_data): + def _report_progress(self, cycle_idx: int, progress_data: ProgressRecord_) -> None: """Method to write progress specific information. Parameters diff --git a/src/wepy/resampling/decisions/clone_merge.py b/src/wepy/resampling/decisions/clone_merge.py index 4bd43767..6619be93 100644 --- a/src/wepy/resampling/decisions/clone_merge.py +++ b/src/wepy/resampling/decisions/clone_merge.py @@ -47,12 +47,12 @@ class CloneMergeDecisionEnum(IntEnum): class CloneMergeDecisionRecordDict(TypedDict): decision_id: int - target_idxs: tuple[int] + target_idxs: tuple[int, ...] @attrs.define class CloneMergeDecisionRecord(BaseDecisionRecord): decision_id: int = attrs.field() - target_idxs: tuple[int] = attrs.field() + target_idxs: tuple[int, ...] = attrs.field() @decision_id.validator def _check_decision_id(self, attribute, value) -> None: diff --git a/src/wepy/resampling/resamplers/clone_merge.py b/src/wepy/resampling/resamplers/clone_merge.py index 70dade61..2123e2ae 100644 --- a/src/wepy/resampling/resamplers/clone_merge.py +++ b/src/wepy/resampling/resamplers/clone_merge.py @@ -1,8 +1,9 @@ # Standard Library -from typing import Generic, TypeVar +from typing import Generic, TypeVar, Annotated # Third Party Library import numpy as np +from numpy.typing import NDArray import attrs # First Party Library @@ -16,15 +17,29 @@ ) from wepy.walker import Walker, WalkerState from wepy.util.attrs import AttrsMappingMixin +from wepy.typing import Shape @attrs.define -class CloneMergeResamplerRecord(AttrsMappingMixin): +class CloneMergeResamplingRecord(AttrsMappingMixin): # from the Decision - decision_id: int - target_idxs: tuple[int] + decision_id: Annotated[ + NDArray[np.int64], + Shape((1,)), + ] + target_idxs: Annotated[ + NDArray[np.int64], + Shape((Ellipsis,)), + ] + # extra for the resampler - step_idx: int - walker_idx: int + step_idx: Annotated[ + NDArray[np.int64], + Shape((1,)), + ] + walker_idx: Annotated[ + NDArray[np.int64], + Shape((1,)), + ] WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 5f05c044..3e4d118b 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -4,16 +4,18 @@ import multiprocessing as mp import random as rand import time -from typing import Callable, Generic, Literal, TypedDict, TypeVar +from typing import Callable, Generic, Literal, TypedDict, TypeVar, Annotated # Third Party Library import attrs import numpy as np +from numpy.typing import NDArray # First Party Library +from wepy.typing import Shape from wepy.resampling.decisions.clone_merge import CloneMergeDecisionRecord from wepy.resampling.distances.base import Distance -from wepy.resampling.resamplers.clone_merge import CloneMergeResampler, CloneMergeResamplerRecord +from wepy.resampling.resamplers.clone_merge import CloneMergeResampler, CloneMergeResamplingRecord from wepy.util.multiprocessing import proc_pool_worker_setup, queue_listener_context from wepy.walker import Walker, WalkerState from wepy.util.attrs import AttrsMappingMixin @@ -52,9 +54,14 @@ def __call__(self, state: WalkerState_) -> DistanceImage_: @attrs.define class REVOResamplerResamplerRecord(AttrsMappingMixin): - distance_matrix: np.typing.ArrayLike - num_walkers: int - variation: float + distance_matrix: Annotated[ + NDArray[np.float32], + Shape((Ellipsis, Ellipsis,)) + ] + variation: Annotated[ + NDArray[np.float32], + Shape((1,)), + ] class REVOResampler( @@ -157,26 +164,10 @@ class REVOResampler( RESAMPLING_RECORD_FIELDS = CloneMergeResampler.RESAMPLING_RECORD_FIELDS - RESAMPLER_FIELDS = CloneMergeResampler.RESAMPLER_FIELDS + ("variation",) - - # TOREV: not using these anymore - # + ( - # "num_walkers", - # "distance_matrix", - # "variation", - # ) - RESAMPLER_SHAPES = CloneMergeResampler.RESAMPLER_SHAPES + (1,) - # + ( - # (1,), - # Ellipsis, - # (1,), - # ) - RESAMPLER_DTYPES = CloneMergeResampler.RESAMPLER_DTYPES + (float,) - # + ( - # int, - # float, - # float, - # ) + RESAMPLER_FIELDS = CloneMergeResampler.RESAMPLER_FIELDS + ("distance_matrix", "variation",) + + RESAMPLER_SHAPES = CloneMergeResampler.RESAMPLER_SHAPES + (Ellipsis, (1,)) + RESAMPLER_DTYPES = CloneMergeResampler.RESAMPLER_DTYPES + (float, float,) # fields that can be used for a table like representation RESAMPLER_RECORD_FIELDS = CloneMergeResampler.RESAMPLER_RECORD_FIELDS + ( @@ -815,7 +806,7 @@ def resample( walkers: list[Walker[WalkerState_]], ) -> tuple[ list[Walker[WalkerState_]], - list[CloneMergeResamplerRecord], + list[CloneMergeResamplingRecord], list[REVOResamplerResamplerRecord], ]: """Resamples walkers based on REVO algorithm @@ -870,45 +861,30 @@ def resample( # to its record as well resampling_records = [] for walker_idx, decision_record in enumerate(decision_records): - resampling_record = CloneMergeResamplerRecord( - **attrs.asdict(decision_record), - step_idx=0, - walker_idx=walker_idx + # UGLY: we need to wrap the field data into the shape + # declared in the CloneMergeResampler, see other notes on + # why + resampling_record = CloneMergeResamplingRecord( + # The decision record fields are simple, so we wrap + # them here as well + decision_id=np.array([[decision_record.decision_id]]), + target_idxs=np.array([[ + np.array(decision_record.target_idxs), + ]]), + step_idx=np.array([[0]]), + walker_idx=np.array([[walker_idx]]) ) resampling_records.append(resampling_record) - # TODO: handle the 2D or 1D issue - # resampling_record["step_idx"] = np.array([0]) - # walker_record["walker_idx"] = np.array([walker_idx]) - - # TOREV: need to understand the impact of this on the data - # ingestion aspect of things. Otherwise not doing this here - # would be much cleaner. - - # # convert the target idxs and decision_id to feature vector arrays - # for record in resampling_data: - # record["target_idxs"] = np.array(record["target_idxs"]) - # record["decision_id"] = np.array([record["decision_id"]]) - # flatten the distance matrix and give the number of walkers # as well for the resampler data, there is just one per cycle resampler_records = [ - REVOResamplerResamplerRecord(**{ - "distance_matrix": np.ravel(np.array(distance_matrix)), - "num_walkers": len(walkers), - "variation": variation, - }) + REVOResamplerResamplerRecord( + distance_matrix=np.ravel(np.array(distance_matrix)), + variation=np.array([[variation]]), + ) ] - # TOREV: ditto, wrt to data interfaces - # resampler_data = [ - # { - # "distance_matrix": np.ravel(np.array(distance_matrix)), - # "num_walkers": np.array([len(walkers)]), - # "variation": np.array([variation]), - # } - # ] - return resampled_walkers, resampling_records, resampler_records diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index a25e9dd6..6d0db92f 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -73,9 +73,9 @@ } ) -# default heart beat every 500 steps, should be around 0.5 - 1 picoseconds -_DEFAULT_HEARTBEAT_INTERVAL = 500 -_DEFAULT_STATE_TIME_INTERVAL = 10 * openmm.unit.picosecond +# default heart beat every 50 steps +_DEFAULT_HEARTBEAT_INTERVAL = 50 +_DEFAULT_STATE_TIME_INTERVAL = 1 * openmm.unit.picosecond DEFAULT_OPENMM_REPORTER_FACTORIES = [ HeartBeatLoggingReporterFactory(step_interval=_DEFAULT_HEARTBEAT_INTERVAL), diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index 4c6ede86..e01298df 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -17,7 +17,7 @@ # First Party Library from wepy.reporter.dashboard import DashboardReporter -from wepy.resampling.resamplers.revo import REVOResamplerFactory +from wepy.resampling.resamplers.revo import REVOResamplerFactory, REVOResampler from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState from wepy.runners.openmm.runner import ( _DEFAULT_HEARTBEAT_INTERVAL, @@ -25,6 +25,7 @@ ) from wepy.sim_manager import Manager from wepy.reporter.dashboard import DashboardReporter +from wepy.reporter.hdf5 import WepyHDF5Reporter # TODO: use the high-level API imports from wepy.walker import Walker @@ -108,7 +109,15 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): dashboard_path = outputs_dir / "main.wepy_dash.org" dashboard_reporter = DashboardReporter(dashboard_path) - reporters = [dashboard_reporter] + hdf5_path = outputs_dir / "main.wepy.h5" + hdf5_reporter = WepyHDF5Reporter.from_components( + file_path=hdf5_path, + save_fields=("positions",), + topology=test_sys.json_top, + resampler_class=REVOResampler, + ) + + reporters = [dashboard_reporter, hdf5_reporter] sim_manager = Manager( init_walkers=init_walkers, @@ -116,29 +125,26 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): resampler_factory=resampler_factory, # resampler_factory=NoResampler, work_mapper_factory=OpenMMProcPoolWorkMapperFactory( - # DEBUG - # platform="Reference", - # num_procs=1, platform="CPU", num_procs=num_workers, - # global_platform_properties={"Threads" : "1"}, global_platform_properties={"Threads": str(cores_per_worker)}, ), reporters=reporters, ) - new_walkers, sim_components = sim_manager.run_simulation( - n_cycles=2, - segment_lengths=10, - ) - # new_walkers, sim_components = sim_manager.run_simulation( # n_cycles=2, - # segment_lengths=DEFAULT_CYCLE_STEPS, + # segment_lengths=10, # ) + new_walkers, sim_components = sim_manager.run_simulation( + n_cycles=2, + segment_lengths=DEFAULT_CYCLE_STEPS, + ) assert dashboard_path.exists() _LOGGER.info("\n" + dashboard_path.read_text()) + assert hdf5_path.exists() + # TODO: add some tests for HDF5 data # disable timeout for this one @@ -146,7 +152,7 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): def test_alanine_dipeptide_revo_procpool(tmp_path_factory): outputs_dir = tmp_path_factory.mktemp("outputs") - + ala_sys = AlanineDipeptideExplicitSystem() integrator = openmm.LangevinIntegrator(TEMPERATURE, 0.1, STEP_SIZE) @@ -199,8 +205,16 @@ def test_alanine_dipeptide_revo_procpool(tmp_path_factory): dashboard_path = outputs_dir / "main.wepy_dash.org" dashboard_reporter = DashboardReporter(dashboard_path) - reporters = [dashboard_reporter] - + + hdf5_path = outputs_dir / "main.wepy.h5" + hdf5_reporter = WepyHDF5Reporter.from_components( + file_path=hdf5_path, + save_fields=("positions",), + topology=ala_sys.json_top, + resampler_class=REVOResampler, + ) + + reporters = [dashboard_reporter, hdf5_reporter] sim_manager = Manager( init_walkers=init_walkers, @@ -235,8 +249,9 @@ def test_alanine_dipeptide_revo_procpool(tmp_path_factory): new_walkers, sim_components = sim_manager.run_simulation( n_cycles=3, - segment_lengths=10, + segment_lengths=DEFAULT_CYCLE_STEPS, ) - + assert dashboard_path.exists() _LOGGER.info("\n" + dashboard_path.read_text()) + assert hdf5_path.exists() diff --git a/tests/unit/test_hdf5.py b/tests/unit/test_hdf5.py index 2ca336f0..3d7d310c 100644 --- a/tests/unit/test_hdf5.py +++ b/tests/unit/test_hdf5.py @@ -720,13 +720,160 @@ def test_new_run(self, wepy_h5_factory, tmpdir): init_walkers=[], ) + def test__init_run_records_field(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + record_grp = run_grp.create_group("example") + + a_dset = wepy_h5._init_run_records_field( + 0, + "example", + field_name="a", + field_shape=(1,), + field_dtype=np.int64, + ) + + assert "a" in record_grp + assert a_dset.shape == (0,1) + assert a_dset.dtype == np.int64 + assert a_dset.maxshape == (None, 1) + + b_dset = wepy_h5._init_run_records_field( + 0, + "example", + field_name="b", + field_shape=(3,3), + field_dtype=np.float64, + ) + + assert "b" in record_grp + assert b_dset.shape == (0,3,3) + assert b_dset.dtype == np.float64 + assert b_dset.maxshape == (None, 3,3) + + c_dset = wepy_h5._init_run_records_field( + 0, + "example", + field_name="c", + field_shape=Ellipsis, + field_dtype=np.bool, + ) + + assert "c" in record_grp + assert c_dset.shape == (0,) + assert h5py.check_vlen_dtype(c_dset.dtype) == np.bool + assert c_dset.maxshape == (None,) + + + def test__init_run_sporadic_record_grp(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + record_grp = wepy_h5._init_run_sporadic_record_grp( + 0, + "example", + [ + ("a", (1,), np.float64), + ("b", (3, 3), np.int32), + ], + ) + + assert "runs/0/example" in wepy_h5.h5 + + assert "_cycle_idxs" in record_grp + assert record_grp["_cycle_idxs"].shape == (0,) + assert record_grp["_cycle_idxs"].dtype == np.int64 + + assert "a" in record_grp + assert "b" in record_grp + + # TODO: + # def test__init_run_continual_record_grp(self, wepy_h5_factory, tmpdir): + # path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + # with WepyHDF5(path, mode="r+") as wepy_h5: + + # run_grp = wepy_h5.new_run( + # init_walkers=_INIT_WALKERS + # ) + + # wepy_h5.init_run_continual_record_grp(0, "example", ("a", "b"),) + + def test__is_sporadic_records(self): + + assert WepyHDF5._is_sporadic_records("resampler") + assert WepyHDF5._is_sporadic_records("warping") + assert WepyHDF5._is_sporadic_records("resampling") + assert WepyHDF5._is_sporadic_records("boundary_conditions") + + # everything else... + assert not WepyHDF5._is_sporadic_records("example") + + def test_init_run_record_grp(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + # continual + example_grp = wepy_h5.init_run_record_grp( + 0, + "example", + [ + ("a", (1,), np.float64), + ("b", (3, 3), np.int32), + ], + ) + + assert "runs/0/example" in wepy_h5.h5 + assert "_cycle_idxs" not in example_grp + + # sporadic + resampling_grp = wepy_h5.init_run_record_grp( + 0, + "resampling", + [ + ("a", (1,), np.float64), + ("b", (3, 3), np.int32), + ], + ) + + assert "runs/0/resampling" in wepy_h5.h5 + assert "_cycle_idxs" in resampling_grp + + def test_init_run_fields_resampling(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + resampling_grp = wepy_h5.init_run_fields_resampling( + 0, + [ + ("decision_id", (1,), np.uint32), + ("target_idxs", Ellipsis, np.uint32), + ], + ) + + assert "resampling" in wepy_h5.h5["runs/0"] + assert "_cycle_idxs" in resampling_grp + # TODO: I know these are working from the WepyHDF5Reporter tests, # but these should be tested individually as time permits - # def test_init_run_fields_resampling(self, wepy_h5_factory, tmpdir): - # pass - # def test_init_run_fields_resampling_decision(self): # pass @@ -745,6 +892,155 @@ def test_new_run(self, wepy_h5_factory, tmpdir): # def test_init_run_fields_bc(self): # pass + def test__extend_run_record_data_field(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + resampling_grp = wepy_h5.init_run_fields_resampling( + 0, + [ + ("decision_id", (1,), np.uint32), + ("target_idxs", Ellipsis, np.uint32), + ("step_idx", (1,), np.uint32), + ("walker_idx", (1,), np.uint32), + ], + ) + + assert resampling_grp["decision_id"].shape == (0,1) + wepy_h5._extend_run_record_data_field( + 0, + "resampling", + "decision_id", + np.array([[0]]), + ) + + assert resampling_grp["decision_id"].shape == (1,1) + assert np.array_equal( + resampling_grp["decision_id"][:], + np.array([ + [0] + ]), + ) + + wepy_h5._extend_run_record_data_field( + 0, + "resampling", + "decision_id", + np.array([[0]]), + ) + + assert resampling_grp["decision_id"].shape == (2,1) + assert np.array_equal( + resampling_grp["decision_id"][:], + np.array([ + [0], + [0], + ]), + ) + + + def test_extend_cycle_run_group_records(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + resampling_grp = wepy_h5.init_run_fields_resampling( + 0, + [ + ("decision_id", (1,), np.uint32), + ("target_idxs", Ellipsis, np.uint32), + ("step_idx", (1,), np.uint32), + ("walker_idx", (1,), np.uint32), + ], + ) + + assert resampling_grp["_cycle_idxs"].shape[0] == 0 + assert resampling_grp["decision_id"].shape[0] == 0 + assert resampling_grp["target_idxs"].shape[0] == 0 + + wepy_h5.extend_cycle_run_group_records( + 0, + "resampling", + 0, + [ + { + "decision_id" : np.array([[0]]), + "target_idxs" : np.array([[[0]]]), + "step_idx" : np.array([[0]]), + "walker_idx" : np.array([[0]]), + }, + { + "decision_id" : np.array([[0]]), + "target_idxs" : np.array([[[0]]]), + "step_idx" : np.array([[0]]), + "walker_idx" : np.array([[1]]), + }, + ] + ) + + assert resampling_grp["decision_id"].shape == (2,1) + assert resampling_grp["target_idxs"].shape == (2,) + assert resampling_grp["step_idx"].shape == (2,1) + assert resampling_grp["walker_idx"].shape == (2,1) + + def test_extend_cycle_resampling_records(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + resampling_grp = wepy_h5.init_run_fields_resampling( + 0, + [ + ("decision_id", (1,), np.uint32), + ("target_idxs", Ellipsis, np.uint32), + ("step_idx", (1,), np.uint32), + ("walker_idx", (1,), np.uint32), + ], + ) + + # UGLY,TOREV: This is ugly because the resampler then + # needs to handle processing the records into these deeply + # bracketed arrays. However, this is the interface and + # promised shapes given their interfaces in the + # e.g. Resampler components and all the downstream tools + # will rely on this structure so it must stay. + wepy_h5.extend_cycle_resampling_records( + 0, + 0, + [ + { + "decision_id" : np.array([[0]]), + "target_idxs" : np.array([[[0]]]), + "step_idx" : np.array([[0]]), + "walker_idx" : np.array([[0]]), + }, + { + "decision_id" : np.array([[0]]), + "target_idxs" : np.array([[[0]]]), + "step_idx" : np.array([[0]]), + "walker_idx" : np.array([[1]]), + }, + ] + ) + + assert resampling_grp["decision_id"].shape == (2,1) + assert resampling_grp["target_idxs"].shape == (2,) + assert resampling_grp["step_idx"].shape == (2,1) + assert resampling_grp["walker_idx"].shape == (2,1) + + + def test_add_traj(self, wepy_h5_factory, tmpdir, monkeypatch): path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") with WepyHDF5(path, mode="r+") as wepy_h5: @@ -926,8 +1222,8 @@ def test_add_traj(self, wepy_h5_factory, tmpdir, monkeypatch): assert traj_grp["box_vectors/_sparse_idxs"].shape == (0,) assert traj_grp["box_vectors/data"].shape == (0,0,0) - def test_extend_traj(self, wepy_h5_factory, tmpdir): pass + # class Test_WepyHDF5_DataConformance: # pass diff --git a/tests/unit/test_reporter/test_hdf5.py b/tests/unit/test_reporter/test_hdf5.py index 92ae54a8..b7fbd8da 100644 --- a/tests/unit/test_reporter/test_hdf5.py +++ b/tests/unit/test_reporter/test_hdf5.py @@ -526,3 +526,84 @@ def test_init(self, tmp_path_factory): "box_volume" : (openmm.unit.nanometer ** 3), } + def test_report(self, tmp_path_factory): + + # TODO: using the OpenMM Runner OpenMMState here because the + # HDF5 requires a 'positions' field, but this could be another + # stripped down kind of state for testing. + test_sys = LennardJonesPair() + + d0 = tmp_path_factory.mktemp("0") + h5_path = d0 / "main.wepy.h5" + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + **RESAMPLER_REPORTER_ARGS, + ) + reporter.init(**LJ_OPENMM_SIM_COMPONENTS) + + reporter.report( + **{ + "cycle_idx" : 0, + "new_walkers" : [ + Walker( + OpenMMState.from_dwim( + positions=np.array([ + [0., 0., 0.,], + [1., 1., 1.,], + ]) * openmm.unit.nanometer, + ), + 0.5, + ), + Walker( + OpenMMState.from_dwim( + positions=np.array([ + [1., 1., 1.,], + [0., 0., 0.,], + ]) * openmm.unit.nanometer, + ), + 0.5, + ), + ], + "n_segment_steps" : 100, + # Instead of cloning and merging we just swap their + # positions to add a little more reality to the test + "resampled_walkers" : [ + Walker( + OpenMMState.from_dwim( + positions=np.array([ + [1., 1., 1.,], + [0., 0., 0.,], + ]) * openmm.unit.nanometer, + ), + 0.5, + ), + Walker( + OpenMMState.from_dwim( + positions=np.array([ + [0., 0., 0.,], + [1., 1., 1.,], + ]) * openmm.unit.nanometer, + ), + 0.5, + ), + ], + "resampling_data" : [ + { + "decision_id" : np.array([[0]]), + "target_idxs" : np.array([[[1]]]), + "step_idx" : np.array([[0]]), + "walker_idx" : np.array([[0]]), + }, + { + "decision_id" : np.array([[0]]), + "target_idxs" : np.array([[[0]]]), + "step_idx" : np.array([[0]]), + "walker_idx" : np.array([[1]]), + }, + ], + }, + **CYCLE_REPORT_DICT_COMMON, + **CYCLE_REPORT_DICT_EMPTY_OPTIONALS, + ) From 016f41133890e18364349d8bf3eb51673609d976 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 6 Jan 2026 12:58:09 -0500 Subject: [PATCH 112/143] relax python version requirement --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e23d6ef2..534cc9fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ name = "wepy" description = "Weighted Ensemble Framework" readme = { "file" = "README.md", "content-type" = "text/plain" } license = "MIT" -requires-python = ">=3.12" +requires-python = ">=3.11" authors = [ { name = "Samuel Lotz", email = "samuel.lotz@salotz.info" }, From a5cda21cb4dd1fb1e5e8ee6842d3acf499e1556c Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 6 Jan 2026 13:34:09 -0500 Subject: [PATCH 113/143] replace itertools.batched Use more-itertools so that we can support more versions of Python where this isn't available. --- pyproject.toml | 1 + src/wepy/work_mapper/openmm/proc_pool.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 534cc9fe..cd0c51f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dependencies = [ "tabulate", "jinja2", "pint", + "more-itertools", ] [project.optional-dependencies] diff --git a/src/wepy/work_mapper/openmm/proc_pool.py b/src/wepy/work_mapper/openmm/proc_pool.py index 25885d24..e405a071 100644 --- a/src/wepy/work_mapper/openmm/proc_pool.py +++ b/src/wepy/work_mapper/openmm/proc_pool.py @@ -10,6 +10,7 @@ import attrs # First Party Library +import more_itertools from wepy.runners.openmm import ( GPU_PLATFORMS, OpenMMPlatformName, @@ -113,7 +114,7 @@ def map( results = [] for batch_idx, batch in enumerate( - itertools.batched( + more_itertools.chunked( zip(walker_states, segment_lengths, strict=True), self._num_procs, strict=False, From 7b8199e149a109c09daace07771db7032781e4c0 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 6 Jan 2026 13:34:36 -0500 Subject: [PATCH 114/143] add support for more versions of python Remove Ray support (for now) which doesn't support newer Python versions --- CONTRIBUTING.md | 9 + justfile | 9 +- pyproject.toml | 7 +- src/wepy/work_mapper/openmm/ray.py | 193 ------------ uv.lock | 460 ++++++++++++++++++++++------- 5 files changed, 366 insertions(+), 312 deletions(-) delete mode 100644 src/wepy/work_mapper/openmm/ray.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e826f981..b7723b8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,15 @@ uv sync --all-extras ``` +To test against other python versions run this first for each version +you are interested in: + +``` +uv sync --python 3.11 --all-extras +``` + + + diff --git a/justfile b/justfile index 625b669b..1271d259 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,8 @@ #!/usr/bin/env just --justfile +default_python := "3.14" + + fmt-check: uv run black --check src tests sphinx/conf.py @@ -14,12 +17,12 @@ fix: uv run isort src tests sphinx/conf.py uv run ruff check --fix src tests sphinx/conf.py - check: uv run mypy src -test: - uv run pytest tests/unit +test python=default_python: + uv sync --python {{python}} --all-extras + uv run --python {{python}} pytest tests/unit test-integration: uv run pytest --durations=0 -s -o log_cli=true --log-cli-level=INFO tests/integration diff --git a/pyproject.toml b/pyproject.toml index cd0c51f2..7a6c8bba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ name = "wepy" description = "Weighted Ensemble Framework" readme = { "file" = "README.md", "content-type" = "text/plain" } license = "MIT" -requires-python = ">=3.11" +requires-python = ">=3.11,<3.15" authors = [ { name = "Samuel Lotz", email = "samuel.lotz@salotz.info" }, @@ -25,7 +25,6 @@ dependencies = [ "attrs", "immutables", "numpy>=2", - "nptyping", "h5py>=3", "networkx", "pandas", @@ -50,10 +49,6 @@ md = [ "lxml", ] -distributed = [ - "ray", -] - prometheus = [ "prometheus_client", "pympler", diff --git a/src/wepy/work_mapper/openmm/ray.py b/src/wepy/work_mapper/openmm/ray.py deleted file mode 100644 index 93f51c50..00000000 --- a/src/wepy/work_mapper/openmm/ray.py +++ /dev/null @@ -1,193 +0,0 @@ -"""Special OpenMM mappers.""" - -# Standard Library -import itertools -import logging -from typing import Any - -# Third Party Library -import attrs -import ray -import ray.util.multiprocessing - -# First Party Library -from wepy.runners.openmm import ( - OpenMMState, -) - -logger = logging.getLogger(__name__) - - -@attrs.define -class OpenMMRayTask: - - openmm_task: OpenMMTask - - def __call__( - self, - *args, - **kwargs, - ) -> OpenMMState: - - logging.basicConfig(level=logging.INFO) - logging.getLogger("OpenMMRayTask").info( - "Configured logging in OpenMMRayTask process" - ) - - return self.openmm_task(*args, **kwargs) - - -class OpenMMRayPoolWorkMapper: - - def __init__( - self, - platform: str, - device_ids: list[int] | None = None, - device_platform_properties: list[dict[str, str]] | None = None, - global_platform_properties: dict[str, str] | None = None, - ray_init_args: dict[str, Any] | None = None, - ): - - if platform in {"CUDA", "HIP", "OpenCL"}: - if device_ids is None: - raise ValueError( - f"For accelerator platforms ({platform} requested) device_ids must be given." - ) - - if device_platform_properties is not None: - - if len(device_platform_properties) != device_ids: - raise ValueError( - f"{len(device_ids)} requested, but only {len(device_platform_properties)} given." - ) - - else: - self._device_platform_properties = { - idx: props for idx, props in enumerate(device_platform_properties) - } - - else: - self._device_platform_properties = None - - self._platform = platform - self._device_ids: dict[int, int] = { - idx: device_id for idx, device_id in enumerate(device_ids) - } - self._num_workers = len(device_ids) - - if ray_init_args is not None: - self._ray_init_args = ray_init_args - - else: - self._ray_init_args = None - - self._global_platform_properties = global_platform_properties - - def init( - self, - ) -> None: - - logger.info("Initializing RayPoolMapper") - - self._ray_ctx = ray.init( - **(self._ray_init_args if self._ray_init_args is not None else {}) - ) - - def cleanup(self) -> None: - - logger.info("Running RayPoolMapper cleanup") - - ray.shutdown() - - def map( - self, - tasks: list[OpenMMTask], - walker_states: list[OpenMMState], - ) -> list[OpenMMState]: - - logger.info( - f"Running map on {len(walker_states)} in batches of {self._num_workers}" - ) - - # spin up a new pool for each map - logger.info(f"Starting ray Pool with {self._num_workers} workers") - with ray.util.multiprocessing.Pool( - processes=self._num_workers, - # only run one thing per task, just to make sure - # everything is cleaned up - maxtasksperchild=1, - ) as pool: - - results = [] - for batch_idx, batch in enumerate( - itertools.batched( - zip(walker_states, tasks, strict=True), - self._num_workers, - strict=False, - ) - ): - - logger.info(f"Submitting batch: {batch_idx}") - - batch_results = [] - for batch_task_idx, (walker_state, task) in enumerate(batch): - - task_idx = batch_idx + batch_task_idx - # for our purposes each element in this batch - # should be associated with a worker. - worker_idx = batch_task_idx - - if self._device_ids is not None and self._platform in GPU_PLATFORMS: - logger.info( - "Worker platform configured and resolving platform properties." - ) - platform_kwargs = self._global_platform_properties | { - "DeviceIndex": str(self._device_ids[worker_idx]), - **( - self._device_platform_properties[worker_idx] - if self._device_platform_properties is not None - else {} - ), - } - else: - logger.info("Non-worker platform, only using global properties") - platform_kwargs = self._global_platform_properties - - _task = OpenMMRayTask(task) - logger.info(f"Submitting task {task_idx} to worker {worker_idx}") - logger.info( - f"Injecting: platform={self._platform}, platform_kwargs={platform_kwargs}" - ) - result = pool.apply_async( - _task, - args=(walker_state,), - kwargs=dict( - platform=self._platform, platform_kwargs=platform_kwargs - ), - ) - logger.info(f"Task {task_idx} submitted") - batch_results.append(result) - - logger.info(f"Batch {batch_idx} submitted, awaiting results.") - for batch_task_idx, task_result in enumerate(batch_results): - - task_idx = batch_idx + batch_task_idx - logger.info(f"Awaiting task {task_idx}") - - try: - real_result = task_result.get() - # TODO: add timeouts and retries - except TimeoutError as exc: - raise exc - except Exception as exc: - raise exc - - results.append(real_result) - - logger.info(f"Retrieved completed results for task: {task_idx}") - - logger.info(f"Batch {batch_idx} completed") - - logger.info("Completed all batches, terminating Pool") - - return results diff --git a/uv.lock b/uv.lock index 7087adc1..a14289a6 100644 --- a/uv.lock +++ b/uv.lock @@ -1,9 +1,10 @@ version = 1 revision = 3 -requires-python = ">=3.12" +requires-python = ">=3.11, <3.15" resolution-markers = [ "python_full_version >= '3.14'", - "python_full_version < '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", + "python_full_version < '3.12'", ] [[package]] @@ -157,6 +158,10 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, @@ -207,6 +212,19 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, @@ -261,6 +279,22 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, @@ -351,6 +385,17 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, @@ -406,6 +451,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] [[package]] @@ -414,6 +464,19 @@ version = "7.12.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, + { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, + { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, + { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, @@ -482,6 +545,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, ] +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -497,6 +565,10 @@ version = "1.8.17" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129, upload-time = "2025-09-17T16:33:20.633Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/53/3af72b5c159278c4a0cf4cffa518675a0e73bdb7d1cac0239b815502d2ce/debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840", size = 2207154, upload-time = "2025-09-17T16:33:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6d/204f407df45600e2245b4a39860ed4ba32552330a0b3f5f160ae4cc30072/debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f", size = 3170322, upload-time = "2025-09-17T16:33:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/f2/13/1b8f87d39cf83c6b713de2620c31205299e6065622e7dd37aff4808dd410/debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da", size = 5155078, upload-time = "2025-09-17T16:33:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c5/c012c60a2922cc91caa9675d0ddfbb14ba59e1e36228355f41cab6483469/debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4", size = 5179011, upload-time = "2025-09-17T16:33:35.711Z" }, { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" }, { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" }, { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" }, @@ -579,15 +651,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, ] -[[package]] -name = "filelock" -version = "3.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, -] - [[package]] name = "flexcache" version = "0.3" @@ -618,6 +681,14 @@ version = "4.60.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" }, + { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" }, + { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" }, { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" }, { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" }, { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" }, @@ -694,6 +765,14 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/4d/6a/0d79de0b025aa85dc8864de8e97659c94cf3d23148394a954dc5ca52f8c8/h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69", size = 426236, upload-time = "2025-10-16T10:35:27.404Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243", size = 3434135, upload-time = "2025-10-16T10:33:47.954Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509", size = 2870958, upload-time = "2025-10-16T10:33:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e3/c255cafc9b85e6ea04e2ad1bba1416baa1d7f57fc98a214be1144087690c/h5py-3.15.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80e5bb5b9508d5d9da09f81fd00abbb3f85da8143e56b1585d59bc8ceb1dba8b", size = 4504770, upload-time = "2025-10-16T10:33:54.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf", size = 4700329, upload-time = "2025-10-16T10:33:57.616Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e4/932a3a8516e4e475b90969bf250b1924dbe3612a02b897e426613aed68f4/h5py-3.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f6c841efd4e6e5b7e82222eaf90819927b6d256ab0f3aca29675601f654f3c", size = 4152456, upload-time = "2025-10-16T10:34:00.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/f74d589883b13737021b2049ac796328f188dbb60c2ed35b101f5b95a3fc/h5py-3.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8a3a22458956ee7b40d8e39c9a9dc01f82933e4c030c964f8b875592f4d831", size = 4617295, upload-time = "2025-10-16T10:34:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878", size = 2882129, upload-time = "2025-10-16T10:34:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/cfcc70b8a42222ba3ad4478bcef1791181ea908e2adbd7d53c66395edad5/h5py-3.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:b39239947cb36a819147fc19e86b618dcb0953d1cd969f5ed71fc0de60392427", size = 2477121, upload-time = "2025-10-16T10:34:09.579Z" }, { url = "https://files.pythonhosted.org/packages/62/b8/c0d9aa013ecfa8b7057946c080c0c07f6fa41e231d2e9bd306a2f8110bdc/h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c", size = 3399089, upload-time = "2025-10-16T10:34:12.135Z" }, { url = "https://files.pythonhosted.org/packages/a4/5e/3c6f6e0430813c7aefe784d00c6711166f46225f5d229546eb53032c3707/h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76", size = 2847803, upload-time = "2025-10-16T10:34:14.564Z" }, { url = "https://files.pythonhosted.org/packages/00/69/ba36273b888a4a48d78f9268d2aee05787e4438557450a8442946ab8f3ec/h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e", size = 4914884, upload-time = "2025-10-16T10:34:18.452Z" }, @@ -772,6 +851,14 @@ version = "0.21" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/69/41/0ccaa6ef9943c0609ec5aa663a3b3e681c1712c1007147b84590cec706a0/immutables-0.21.tar.gz", hash = "sha256:b55ffaf0449790242feb4c56ab799ea7af92801a0a43f9e2f4f8af2ab24dfc4a", size = 89008, upload-time = "2024-10-10T00:55:01.434Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/85/39/2d7d54f6cf33f6fcb3f78e985142166d2ec6ff93be761a17dfee31a372cd/immutables-0.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5d780c38067047911a2e06a86ba063ba0055618ab5573c8198ef3f368e321303", size = 31227, upload-time = "2024-10-10T00:54:09.884Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4b/260f632d0fb83b8cdabb0c1fc14657a9bdead456f3399c4eb5a0c0697989/immutables-0.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9aab9d0f0016f6e0bfe7e4a4cb831ef20063da6468b1bbc71d06ef285781ee9e", size = 31081, upload-time = "2024-10-10T00:54:11.325Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8d/0f4a1d10fac2700874680dad4d1d7b0d0bf31225cb21f3a804d085bfb603/immutables-0.21-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ff83390b05d3372acb9a0c928f6cc20c78e74ca20ed88eb941f84a63b65e444", size = 99334, upload-time = "2024-10-10T00:54:12.229Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fc/20f4a5d5fcd4d17b46027d7d542190f5084f3d74abc6bdc2c0fab4d3deb3/immutables-0.21-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01497713e71509c4481ffccdbe3a47b94969345f4e92f814d6626f7c0a4c304", size = 99511, upload-time = "2024-10-10T00:54:13.202Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/f929eaee4b247b1b71823d784734e51761685d6e69d7ad000459c3f2b84d/immutables-0.21-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc7844c9fbb5bece5bfdf2bf8ea74d308f42f40b0665fd25c58abf56d7db024a", size = 97474, upload-time = "2024-10-10T00:54:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/85/b1/690aeaa4acc1db13e8356888bc929c8d0c9363c1409ffd8ab74d65d8c49c/immutables-0.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:984106fa4345efd9f96de22e9949fc97bac8598bdebee03c20b2497a88bff3b7", size = 97940, upload-time = "2024-10-10T00:54:15.957Z" }, + { url = "https://files.pythonhosted.org/packages/dd/de/78bd53b69ee99e034c2b69f46063abf600f81417f28de6120d430f523c98/immutables-0.21-cp311-cp311-win32.whl", hash = "sha256:1bdb5200518518601377e4877d5034e7c535e9ea8a9d601ed8b0eedef0c7becd", size = 30503, upload-time = "2024-10-10T00:54:16.891Z" }, + { url = "https://files.pythonhosted.org/packages/1f/20/999ecf398412c9ad174da083c6ffea56d027ef62cf892f695b43210c4721/immutables-0.21-cp311-cp311-win_amd64.whl", hash = "sha256:dd00c34f431c54c95e7b84bfdbdeacb4f039a6a24eb0c1f7aa4b168bb9a6ad0a", size = 34395, upload-time = "2024-10-10T00:54:18.481Z" }, { url = "https://files.pythonhosted.org/packages/4d/f9/0c46f600702b815182212453f5514c0070ee168b817cdf7c3767554c8489/immutables-0.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1ed262094b755903122c3c3a83ad0e0d5c3ab7887cda12b2fe878769d1ee0d", size = 31885, upload-time = "2024-10-10T00:54:19.406Z" }, { url = "https://files.pythonhosted.org/packages/29/34/7608d2eab6179aa47e8f59ab0fbd5b3eeb2333d78c9dc2da0de8de4ed322/immutables-0.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce604f81d9d8f26e60b52ebcb56bb5c0462c8ea50fb17868487d15f048a2f13e", size = 31537, upload-time = "2024-10-10T00:54:20.998Z" }, { url = "https://files.pythonhosted.org/packages/f7/52/cb9e2bb7a69338155ffabbd2f993c968c750dd2d5c6c6eaa6ebb7bfcbdfa/immutables-0.21-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b48b116aaca4500398058b5a87814857a60c4cb09417fecc12d7da0f5639b73d", size = 104270, upload-time = "2024-10-10T00:54:21.912Z" }, @@ -854,6 +941,7 @@ dependencies = [ { name = "pygments" }, { name = "stack-data" }, { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/e6/48c74d54039241a456add616464ea28c6ebf782e4110d419411b83dae06f/ipython-9.7.0.tar.gz", hash = "sha256:5f6de88c905a566c6a9d6c400a8fed54a638e1f7543d17aae2551133216b1e4e", size = 4422115, upload-time = "2025-11-05T12:18:54.646Z" } wheels = [ @@ -1049,6 +1137,7 @@ dependencies = [ { name = "jupyter-server-terminals" }, { name = "nbconvert" }, { name = "nbformat" }, + { name = "overrides", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "prometheus-client" }, { name = "pywinpty", marker = "os_name == 'nt'" }, @@ -1134,6 +1223,19 @@ version = "1.4.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, @@ -1198,6 +1300,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] [[package]] @@ -1224,6 +1331,22 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, @@ -1296,6 +1419,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, ] [[package]] @@ -1304,6 +1433,17 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, @@ -1378,6 +1518,13 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" }, + { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" }, { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" }, { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" }, { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" }, @@ -1413,6 +1560,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" }, { url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" }, { url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" }, + { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" }, ] [[package]] @@ -1439,6 +1589,9 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/49/87/04f21b321e7bb6f3e09b5339df51699b8caae4846796175845bbc44f298b/mdtraj-1.11.0.tar.gz", hash = "sha256:2cf0ed2ee9a603dc4599743b1806e1387c655e2a1e9d013841b99fb137ad852e", size = 2860049, upload-time = "2025-06-25T02:06:19.716Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/bc/1a6d273467a24dad5048bf527288a7bbe997ecee8342f6e8fdd7465c9132/mdtraj-1.11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a6bbf2bc03a2ed23fd1fe696f6f6f970f495e843f3baaa71a0e99da086420de1", size = 2463123, upload-time = "2025-06-25T02:03:06.602Z" }, + { url = "https://files.pythonhosted.org/packages/09/dc/37bd8a22c66d05a293979d8d8cf1de5e6c03d21ebd6950b6a839c8d8a16b/mdtraj-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c34002c49282d208fdd34cd1b2578859bc7377a22ebd66a194efa1fc17395e5", size = 8023237, upload-time = "2025-06-25T02:03:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/4c/48/6fc195907c056df9b6ff6442bb39c5dd433b971b9deaecf6d18a0761efa8/mdtraj-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:4c794aac3db13937ed7e4f8c2b773564a89b2b539c15f43bae9dab6e1ffa5de1", size = 1355146, upload-time = "2025-06-25T02:03:31.524Z" }, { url = "https://files.pythonhosted.org/packages/21/71/56adb24577052f2cd6f9b678da908e8e2d7963db3a88faff39792be43903/mdtraj-1.11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:878fb8e62962b881ba75d4200637280b518e891e4aa5f213052ddb1905704558", size = 2448664, upload-time = "2025-06-25T02:03:43.052Z" }, { url = "https://files.pythonhosted.org/packages/50/af/7d65be36619befb29b51b6118a97121de6685720af7f4cfd6ff1902ca7eb/mdtraj-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01eb00d9b62f22056303fb8e4be0f85257a2ce1003f1e6274393bbcc977badaf", size = 8028112, upload-time = "2025-06-25T02:04:12.949Z" }, { url = "https://files.pythonhosted.org/packages/d2/8f/fd05aa3613fad29b3a779e3384797d2b64aa329bcfd18d25f8fb9c0d725e/mdtraj-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:f87117f7e6742b11a3a39e55fe5c9b4fc013d27691ac671bcc6dfb8193beae0f", size = 1341985, upload-time = "2025-06-25T02:04:19.133Z" }, @@ -1461,47 +1614,12 @@ wheels = [ ] [[package]] -name = "msgpack" -version = "1.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, - { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, - { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, - { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, - { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, - { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, - { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, - { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, - { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, - { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, - { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, - { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, - { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, - { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, - { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, - { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, - { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, - { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, - { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, - { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, - { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, - { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] [[package]] @@ -1515,6 +1633,12 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, @@ -1663,23 +1787,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, ] -[[package]] -name = "nptyping" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/03/3f5af42a8858c223f1a4b1d5a773f3a4f7cb9ae9dcbd1ad61bdb8539e633/nptyping-2.0.1-py3-none-any.whl", hash = "sha256:0fc5c4d76c65e12a77e750b9e2701dab6468d00926c8c4f383867bd70598a532", size = 19435, upload-time = "2022-04-28T21:02:07.815Z" }, -] - [[package]] name = "numpy" version = "2.3.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, @@ -1735,6 +1859,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, ] [[package]] @@ -1745,6 +1876,10 @@ dependencies = [ { name = "numpy" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/35/9a51d07fc60fc267a241a2199deb31502baa2e438801d4223a256b898c8a/openmm-8.4.0.post1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:eb5d3da43b7ac4b3e41e3a29005982b1f87de1879d2d7a97b982d20022321383", size = 13228166, upload-time = "2025-11-07T18:03:58.349Z" }, + { url = "https://files.pythonhosted.org/packages/95/50/772848b1b81c7609cbe119c5456996488ce2efd4863e782c257e42829be4/openmm-8.4.0.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef172c5ab504f99a7346c3e4b35e29703be803a610ab72e6b2d16e39f754ebea", size = 12741362, upload-time = "2025-11-07T18:04:04.236Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b2/8acf418560b6fe29635f4f992f3a1a71d9f017cca19ebe4ccff9f86fae90/openmm-8.4.0.post1-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:2b5dcbc789d9d16fbe4fcc7326532499f45143a2bec05c869fa98af0c23c3d80", size = 14248283, upload-time = "2025-11-07T18:04:10.516Z" }, + { url = "https://files.pythonhosted.org/packages/38/1f/976e1fae88a6ff4ba74310dc663faf80e58e9f740ebfc12c890309706032/openmm-8.4.0.post1-cp311-cp311-win_amd64.whl", hash = "sha256:a49c498564bdaeb3bad936440ed402dca4b0718a6578147142f2c3ffe4580559", size = 13096103, upload-time = "2025-11-07T18:04:16.787Z" }, { url = "https://files.pythonhosted.org/packages/6d/c1/63de7b93c6b57323cb3524f1038fbb75749d44f087a9c443de4a5d35be33/openmm-8.4.0.post1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:117165ce53a465389f05249cae9feb90276874dc7ac5bf0cc8b9151031b06fa6", size = 13224758, upload-time = "2025-11-07T18:04:22.948Z" }, { url = "https://files.pythonhosted.org/packages/dc/17/ddae4b246950b68a45a5521f925a90db332e0761c28194a3b7533e2e4495/openmm-8.4.0.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:226d462ea3e1b36daa5276b931979d36db72c9daf0e3502f7cfd5528ade8861f", size = 12739396, upload-time = "2025-11-07T18:04:29.885Z" }, { url = "https://files.pythonhosted.org/packages/3b/ca/2063be344cefbeaec480d9d44662551f41f27097c0df825cc2bcbea221cc/openmm-8.4.0.post1-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:a503c2b860e21364f1c49bf23c5bb8b6fa04e1a4227bc577d543d4137e99eaf5", size = 14253643, upload-time = "2025-11-07T18:04:37.888Z" }, @@ -1773,6 +1908,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/e1/02debe3a6f49490d2fc40c3fc5a96ab902ad96a66e5b8173254d3ef900b5/openmm_systems-0.0.0-py2.py3-none-any.whl", hash = "sha256:9e9ce3157a1034baf9fae1f652dc5187f6b36e6d66b2871521a4ef98f3c4b64b", size = 11327692, upload-time = "2020-04-30T19:51:18.139Z" }, ] +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -1794,6 +1938,13 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, @@ -1887,6 +2038,17 @@ version = "12.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, @@ -1948,6 +2110,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, + { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, ] [[package]] @@ -2016,21 +2185,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] -[[package]] -name = "protobuf" -version = "6.33.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432, upload-time = "2025-11-13T16:44:18.895Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593, upload-time = "2025-11-13T16:44:06.275Z" }, - { url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883, upload-time = "2025-11-13T16:44:09.222Z" }, - { url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522, upload-time = "2025-11-13T16:44:10.475Z" }, - { url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445, upload-time = "2025-11-13T16:44:11.869Z" }, - { url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161, upload-time = "2025-11-13T16:44:12.778Z" }, - { url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171, upload-time = "2025-11-13T16:44:14.035Z" }, - { url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" }, -] - [[package]] name = "psutil" version = "7.1.3" @@ -2200,7 +2354,7 @@ name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage" }, + { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] @@ -2325,6 +2479,9 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, @@ -2342,6 +2499,7 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/bb/a7cc2967c5c4eceb6cc49cfe39447d4bfc56e6c865e7c2249b6eb978935f/pywinpty-3.0.2.tar.gz", hash = "sha256:1505cc4cb248af42cb6285a65c9c2086ee9e7e574078ee60933d5d7fa86fb004", size = 30669, upload-time = "2025-10-03T21:16:29.205Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a1/409c1651c9f874d598c10f51ff586c416625601df4bca315d08baec4c3e3/pywinpty-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:327790d70e4c841ebd9d0f295a780177149aeb405bca44c7115a3de5c2054b23", size = 2050304, upload-time = "2025-10-03T21:19:29.466Z" }, { url = "https://files.pythonhosted.org/packages/02/4e/1098484e042c9485f56f16eb2b69b43b874bd526044ee401512234cf9e04/pywinpty-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:99fdd9b455f0ad6419aba6731a7a0d2f88ced83c3c94a80ff9533d95fa8d8a9e", size = 2050391, upload-time = "2025-10-03T21:19:01.642Z" }, { url = "https://files.pythonhosted.org/packages/fc/19/b757fe28008236a4a713e813283721b8a40aa60cd7d3f83549f2e25a3155/pywinpty-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:18f78b81e4cfee6aabe7ea8688441d30247b73e52cd9657138015c5f4ee13a51", size = 2050057, upload-time = "2025-10-03T21:19:26.732Z" }, { url = "https://files.pythonhosted.org/packages/cb/44/cbae12ecf6f4fa4129c36871fd09c6bef4f98d5f625ecefb5e2449765508/pywinpty-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:663383ecfab7fc382cc97ea5c4f7f0bb32c2f889259855df6ea34e5df42d305b", size = 2049874, upload-time = "2025-10-03T21:18:53.923Z" }, @@ -2355,6 +2513,15 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, @@ -2404,6 +2571,16 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, @@ -2436,30 +2613,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, -] - -[[package]] -name = "ray" -version = "2.51.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "filelock" }, - { name = "jsonschema" }, - { name = "msgpack" }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "pyyaml" }, - { name = "requests" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/54/66fcfebd26c9747d908e2ac24f3a8a5502e84f19ea1e7a9b7f4d4a12bc34/ray-2.51.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:461b0e711f73cebc68128bca7202bef8db2c0e14dc6d49140f96549e5e752eb1", size = 68030141, upload-time = "2025-11-29T00:29:08.67Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9e/7add3c78a5a3d05f9c702d247da83a8a3e30d57eae153985f48ec3309c82/ray-2.51.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:5c97f29574072e3568a2714a84e6948fb457ce09eefd251c919221584b2d458d", size = 70506728, upload-time = "2025-11-29T00:29:14.051Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8e/5d1325619399d7eb9563e2f883f8e782fb26b39a122d6d629e54c8989a5a/ray-2.51.2-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:7b2a842744a1d4b47af8f3c0665a319736139518dd2e26fb9e18114281d8f9ea", size = 71359570, upload-time = "2025-11-29T00:29:19.508Z" }, - { url = "https://files.pythonhosted.org/packages/ba/96/ec1ee03fb1731d9e09d94d7ba6d9e47fce886d7cc79aac47e8422fe9c528/ray-2.51.2-cp312-cp312-win_amd64.whl", hash = "sha256:6b04ca7dccf540da2ab07fd7073009dfe04d9d084d705e337572272fa3e56485", size = 26675734, upload-time = "2025-11-29T00:29:24.27Z" }, - { url = "https://files.pythonhosted.org/packages/70/89/255ac2a70928a1d439c98fca9f3437cabbbebd3ac767523df608cce39197/ray-2.51.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c9ed290667868c809eb467ad8830d887fdce10dac2c674b3d43d3b3b5f9c7b07", size = 67975149, upload-time = "2025-11-29T00:29:28.995Z" }, - { url = "https://files.pythonhosted.org/packages/d3/05/1e3bb04e263a2bc1eacd762b37a0013d18f76341de0a7199d84a5a00b372/ray-2.51.2-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:554bd393e97bed9dfa5f73f47e4fbf42aa35d81b1228081aa93ccb7cdd5d4b34", size = 70414911, upload-time = "2025-11-29T00:29:34.286Z" }, - { url = "https://files.pythonhosted.org/packages/c4/85/f6994a74cf5e6fa6ebc959c27ff6f1f5352b78e71b947b4b302c6bb0a203/ray-2.51.2-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:e3bf004ed23971ec5d324ed9748aed23f6645d56696a44cdbe35d331f66c4619", size = 71275062, upload-time = "2025-11-29T00:29:39.379Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] [[package]] @@ -2530,6 +2688,21 @@ version = "0.29.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/36/ab/7fb95163a53ab122c74a7c42d2d2f012819af2cf3deb43fb0d5acf45cc1a/rpds_py-0.29.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b9c764a11fd637e0322a488560533112837f5334ffeb48b1be20f6d98a7b437", size = 372344, upload-time = "2025-11-16T14:47:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/b3/45/f3c30084c03b0d0f918cb4c5ae2c20b0a148b51ba2b3f6456765b629bedd/rpds_py-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fd2164d73812026ce970d44c3ebd51e019d2a26a4425a5dcbdfa93a34abc383", size = 363041, upload-time = "2025-11-16T14:47:58.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e9/4d044a1662608c47a87cbb37b999d4d5af54c6d6ebdda93a4d8bbf8b2a10/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a097b7f7f7274164566ae90a221fd725363c0e9d243e2e9ed43d195ccc5495c", size = 391775, upload-time = "2025-11-16T14:48:00.197Z" }, + { url = "https://files.pythonhosted.org/packages/50/c9/7616d3ace4e6731aeb6e3cd85123e03aec58e439044e214b9c5c60fd8eb1/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cdc0490374e31cedefefaa1520d5fe38e82fde8748cbc926e7284574c714d6b", size = 405624, upload-time = "2025-11-16T14:48:01.496Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/6d7d6941ca0843609fd2d72c966a438d6f22617baf22d46c3d2156c31350/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89ca2e673ddd5bde9b386da9a0aac0cab0e76f40c8f0aaf0d6311b6bbf2aa311", size = 527894, upload-time = "2025-11-16T14:48:03.167Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f7/aee14dc2db61bb2ae1e3068f134ca9da5f28c586120889a70ff504bb026f/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5d9da3ff5af1ca1249b1adb8ef0573b94c76e6ae880ba1852f033bf429d4588", size = 412720, upload-time = "2025-11-16T14:48:04.413Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e2/2293f236e887c0360c2723d90c00d48dee296406994d6271faf1712e94ec/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8238d1d310283e87376c12f658b61e1ee23a14c0e54c7c0ce953efdbdc72deed", size = 392945, upload-time = "2025-11-16T14:48:06.252Z" }, + { url = "https://files.pythonhosted.org/packages/14/cd/ceea6147acd3bd1fd028d1975228f08ff19d62098078d5ec3eed49703797/rpds_py-0.29.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2d6fb2ad1c36f91c4646989811e84b1ea5e0c3cf9690b826b6e32b7965853a63", size = 406385, upload-time = "2025-11-16T14:48:07.575Z" }, + { url = "https://files.pythonhosted.org/packages/52/36/fe4dead19e45eb77a0524acfdbf51e6cda597b26fc5b6dddbff55fbbb1a5/rpds_py-0.29.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:534dc9df211387547267ccdb42253aa30527482acb38dd9b21c5c115d66a96d2", size = 423943, upload-time = "2025-11-16T14:48:10.175Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7b/4551510803b582fa4abbc8645441a2d15aa0c962c3b21ebb380b7e74f6a1/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d456e64724a075441e4ed648d7f154dc62e9aabff29bcdf723d0c00e9e1d352f", size = 574204, upload-time = "2025-11-16T14:48:11.499Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/071ccdd7b171e727a6ae079f02c26f75790b41555f12ca8f1151336d2124/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a738f2da2f565989401bd6fd0b15990a4d1523c6d7fe83f300b7e7d17212feca", size = 600587, upload-time = "2025-11-16T14:48:12.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/09/96983d48c8cf5a1e03c7d9cc1f4b48266adfb858ae48c7c2ce978dbba349/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a110e14508fd26fd2e472bb541f37c209409876ba601cf57e739e87d8a53cf95", size = 562287, upload-time = "2025-11-16T14:48:14.108Z" }, + { url = "https://files.pythonhosted.org/packages/40/f0/8c01aaedc0fa92156f0391f39ea93b5952bc0ec56b897763858f95da8168/rpds_py-0.29.0-cp311-cp311-win32.whl", hash = "sha256:923248a56dd8d158389a28934f6f69ebf89f218ef96a6b216a9be6861804d3f4", size = 221394, upload-time = "2025-11-16T14:48:15.374Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/a8b21c54c7d234efdc83dc034a4d7cd9668e3613b6316876a29b49dece71/rpds_py-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:539eb77eb043afcc45314d1be09ea6d6cafb3addc73e0547c171c6d636957f60", size = 235713, upload-time = "2025-11-16T14:48:16.636Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1f/df3c56219523947b1be402fa12e6323fe6d61d883cf35d6cb5d5bb6db9d9/rpds_py-0.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:bdb67151ea81fcf02d8f494703fb728d4d34d24556cbff5f417d74f6f5792e7c", size = 229157, upload-time = "2025-11-16T14:48:17.891Z" }, { url = "https://files.pythonhosted.org/packages/3c/50/bc0e6e736d94e420df79be4deb5c9476b63165c87bb8f19ef75d100d21b3/rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954", size = 376000, upload-time = "2025-11-16T14:48:19.141Z" }, { url = "https://files.pythonhosted.org/packages/3e/3a/46676277160f014ae95f24de53bed0e3b7ea66c235e7de0b9df7bd5d68ba/rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c", size = 360575, upload-time = "2025-11-16T14:48:20.443Z" }, { url = "https://files.pythonhosted.org/packages/75/ba/411d414ed99ea1afdd185bbabeeaac00624bd1e4b22840b5e9967ade6337/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d", size = 392159, upload-time = "2025-11-16T14:48:22.12Z" }, @@ -2603,6 +2776,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/3d/eb820f95dce4306f07a495ede02fb61bef36ea201d9137d4fcd5ab94ec1e/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a", size = 557288, upload-time = "2025-11-16T14:50:10.73Z" }, { url = "https://files.pythonhosted.org/packages/e9/f8/b8ff786f40470462a252918e0836e0db903c28e88e3eec66bc4a7856ee5d/rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5", size = 211382, upload-time = "2025-11-16T14:50:12.827Z" }, { url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919, upload-time = "2025-11-16T14:50:14.734Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ac/b97e80bf107159e5b9ba9c91df1ab95f69e5e41b435f27bdd737f0d583ac/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:acd82a9e39082dc5f4492d15a6b6c8599aa21db5c35aaf7d6889aea16502c07d", size = 373963, upload-time = "2025-11-16T14:50:16.205Z" }, + { url = "https://files.pythonhosted.org/packages/40/5a/55e72962d5d29bd912f40c594e68880d3c7a52774b0f75542775f9250712/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:715b67eac317bf1c7657508170a3e011a1ea6ccb1c9d5f296e20ba14196be6b3", size = 364644, upload-time = "2025-11-16T14:50:18.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/6b6524d0191b7fc1351c3c0840baac42250515afb48ae40c7ed15499a6a2/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b1b87a237cb2dba4db18bcfaaa44ba4cd5936b91121b62292ff21df577fc43", size = 393847, upload-time = "2025-11-16T14:50:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b8/c5692a7df577b3c0c7faed7ac01ee3c608b81750fc5d89f84529229b6873/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c3c3e8101bb06e337c88eb0c0ede3187131f19d97d43ea0e1c5407ea74c0cbf", size = 407281, upload-time = "2025-11-16T14:50:21.64Z" }, + { url = "https://files.pythonhosted.org/packages/f0/57/0546c6f84031b7ea08b76646a8e33e45607cc6bd879ff1917dc077bb881e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8e54d6e61f3ecd3abe032065ce83ea63417a24f437e4a3d73d2f85ce7b7cfe", size = 529213, upload-time = "2025-11-16T14:50:23.219Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c1/01dd5f444233605555bc11fe5fed6a5c18f379f02013870c176c8e630a23/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fbd4e9aebf110473a420dea85a238b254cf8a15acb04b22a5a6b5ce8925b760", size = 413808, upload-time = "2025-11-16T14:50:25.262Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0a/60f98b06156ea2a7af849fb148e00fbcfdb540909a5174a5ed10c93745c7/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fdf53d36e6c72819993e35d1ebeeb8e8fc688d0c6c2b391b55e335b3afba5a", size = 394600, upload-time = "2025-11-16T14:50:26.956Z" }, + { url = "https://files.pythonhosted.org/packages/37/f1/dc9312fc9bec040ece08396429f2bd9e0977924ba7a11c5ad7056428465e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:ea7173df5d86f625f8dde6d5929629ad811ed8decda3b60ae603903839ac9ac0", size = 408634, upload-time = "2025-11-16T14:50:28.989Z" }, + { url = "https://files.pythonhosted.org/packages/ed/41/65024c9fd40c89bb7d604cf73beda4cbdbcebe92d8765345dd65855b6449/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:76054d540061eda273274f3d13a21a4abdde90e13eaefdc205db37c05230efce", size = 426064, upload-time = "2025-11-16T14:50:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e0/cf95478881fc88ca2fdbf56381d7df36567cccc39a05394beac72182cd62/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9f84c549746a5be3bc7415830747a3a0312573afc9f95785eb35228bb17742ec", size = 575871, upload-time = "2025-11-16T14:50:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c0/df88097e64339a0218b57bd5f9ca49898e4c394db756c67fccc64add850a/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:0ea962671af5cb9a260489e311fa22b2e97103e3f9f0caaea6f81390af96a9ed", size = 601702, upload-time = "2025-11-16T14:50:36.051Z" }, + { url = "https://files.pythonhosted.org/packages/87/f4/09ffb3ebd0cbb9e2c7c9b84d252557ecf434cd71584ee1e32f66013824df/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f7728653900035fb7b8d06e1e5900545d8088efc9d5d4545782da7df03ec803f", size = 564054, upload-time = "2025-11-16T14:50:37.733Z" }, ] [[package]] @@ -2640,6 +2825,16 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, + { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, @@ -2922,6 +3117,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, ] +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + [[package]] name = "tornado" version = "6.5.2" @@ -3033,8 +3277,8 @@ dependencies = [ { name = "h5py" }, { name = "immutables" }, { name = "jinja2" }, + { name = "more-itertools" }, { name = "networkx" }, - { name = "nptyping" }, { name = "numpy" }, { name = "pandas" }, { name = "pint" }, @@ -3043,9 +3287,6 @@ dependencies = [ ] [package.optional-dependencies] -distributed = [ - { name = "ray" }, -] graphics = [ { name = "matplotlib" }, { name = "pillow" }, @@ -3101,8 +3342,8 @@ requires-dist = [ { name = "lxml", marker = "extra == 'md'" }, { name = "matplotlib", marker = "extra == 'graphics'" }, { name = "mdtraj", marker = "extra == 'md'" }, + { name = "more-itertools" }, { name = "networkx" }, - { name = "nptyping" }, { name = "numpy", specifier = ">=2" }, { name = "openmm", marker = "extra == 'md'" }, { name = "openmm-systems", marker = "extra == 'md'", specifier = "==0.0.0" }, @@ -3111,11 +3352,10 @@ requires-dist = [ { name = "pint" }, { name = "prometheus-client", marker = "extra == 'prometheus'" }, { name = "pympler", marker = "extra == 'prometheus'" }, - { name = "ray", marker = "extra == 'distributed'" }, { name = "scipy" }, { name = "tabulate" }, ] -provides-extras = ["md", "distributed", "prometheus", "graphics"] +provides-extras = ["md", "prometheus", "graphics"] [package.metadata.requires-dev] dev = [ From eb96d9746b4b0abf5bebfb9fef79f1d72aee0e69 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Tue, 13 Jan 2026 14:54:29 -0500 Subject: [PATCH 115/143] bump version number --- pyproject.toml | 2 +- src/wepy/__about__.py | 2 +- src/wepy/__main__.py | 26 -------------------------- 3 files changed, 2 insertions(+), 28 deletions(-) delete mode 100644 src/wepy/__main__.py diff --git a/pyproject.toml b/pyproject.toml index 7a6c8bba..fcd2428a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ authors = [ { name = "Robert Hall" }, { name = "Nicole Roussey" }, ] -version = "1.1.0" +version = "2.0.0b0" classifiers = [ "Topic :: Utilities", diff --git a/src/wepy/__about__.py b/src/wepy/__about__.py index 6849410a..ef2ec23a 100644 --- a/src/wepy/__about__.py +++ b/src/wepy/__about__.py @@ -1 +1 @@ -__version__ = "1.1.0" +__version__ = "2.0.0b0" diff --git a/src/wepy/__main__.py b/src/wepy/__main__.py deleted file mode 100644 index f9c8391c..00000000 --- a/src/wepy/__main__.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Glue all the CLIs together into one interface.""" - -# First Party Library -from wepy.orchestration.cli import cli as orch_cli - -cli = orch_cli - - -# SNIPPET: I was intending to aggregate multiple command lines other -# than the orchestration, but this never materialized or was -# needed. In the future though this can be the place for that. - -# @click.group() -# def cli(): -# """ """ -# pass - -# # add in the sub-clis -# cli.add_command(orch_cli) - -# # the orchestrator stuff we keep in the top-level still though -# for subgroup in orch_subgroups: -# cli.add_command(subgroup) - -if __name__ == "__main__": - cli() From 3e4f6b2f6fb9ed2d7af83435b5e1af2c5673ba08 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 15 Jan 2026 11:56:10 -0500 Subject: [PATCH 116/143] add support for numpy 1 --- CONTRIBUTING.md | 18 ++++ justfile | 23 ++-- pyproject.toml | 2 +- src/wepy/runners/openmm/state.py | 3 +- src/wepy/storage/protocol.py | 2 +- src/wepy/util/openmm.py | 5 +- src/wepy_tools/systems/alanine_dipeptide.py | 14 ++- src/wepy_tools/systems/lennard_jones.py | 3 +- tests/unit/test_hdf5.py | 4 +- .../test_proc_pool.py} | 40 +------ uv.lock | 101 ++++-------------- 11 files changed, 82 insertions(+), 133 deletions(-) rename tests/unit/test_work_mapper/{test_openmm.py => test_openmm/test_proc_pool.py} (63%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7723b8a..c576c278 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,25 @@ uv sync --python 3.11 --all-extras ``` +## Tests +The standard test run like: + +``` +uv run --all-extras pytest tests/unit +``` + +The flake finder flag will enable running certain tests multiple +times. Typically for parallel routines which intermittently fail. + +You can test on other python versions like: + +``` +uv run --python 3.11 --all-extras pytest --flake-finder tests/unit +``` + +The `--flake-finder` flag will run things multiple times to see if +they are flaky. diff --git a/justfile b/justfile index 1271d259..d8ec323c 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,6 @@ #!/usr/bin/env just --justfile -default_python := "3.14" +default_python := "3.13" fmt-check: @@ -21,11 +21,22 @@ check: uv run mypy src test python=default_python: - uv sync --python {{python}} --all-extras - uv run --python {{python}} pytest tests/unit - -test-integration: - uv run pytest --durations=0 -s -o log_cli=true --log-cli-level=INFO tests/integration + uv run --all-extras --python {{python}} pytest tests/unit + +test-comprehensive: + uv run --all-extras --python 3.11 pytest tests/unit + uv run --all-extras --python 3.12 pytest tests/unit + uv run --all-extras --python 3.13 pytest tests/unit + uv run --all-extras --python 3.14 pytest tests/unit + + +test-integration python=default_python: + uv run --all-extras --python {{ python }} \ + pytest \ + --durations=0 \ + -s \ + -o log_cli=true --log-cli-level=INFO \ + tests/integration clean: find . -type d -name "__pycache__" -prune -exec rm -rf {} + diff --git a/pyproject.toml b/pyproject.toml index fcd2428a..431da49b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ dependencies = [ "attrs", "immutables", - "numpy>=2", + "numpy", "h5py>=3", "networkx", "pandas", diff --git a/src/wepy/runners/openmm/state.py b/src/wepy/runners/openmm/state.py index 87149085..c1981635 100644 --- a/src/wepy/runners/openmm/state.py +++ b/src/wepy/runners/openmm/state.py @@ -15,6 +15,7 @@ # Third Party Library import attrs import numpy as np +import numpy.typing import openmm import openmm.unit from immutables import Map as frozenmap @@ -626,7 +627,7 @@ def _maybe_array_equal( return np.array_equal(arr0, arr1) -def _gen_unit_cube() -> np.typing.ArrayLike: +def _gen_unit_cube() -> numpy.typing.ArrayLike: return np.array( [ [1.0, 0.0, 0.0], diff --git a/src/wepy/storage/protocol.py b/src/wepy/storage/protocol.py index 52c1feb3..427a4647 100644 --- a/src/wepy/storage/protocol.py +++ b/src/wepy/storage/protocol.py @@ -56,7 +56,7 @@ np.float16, np.float32, np.float64, - np.bool, + bool, ] diff --git a/src/wepy/util/openmm.py b/src/wepy/util/openmm.py index 231322b1..e0353480 100644 --- a/src/wepy/util/openmm.py +++ b/src/wepy/util/openmm.py @@ -4,16 +4,17 @@ # Third Party Library import numpy as np +import numpy.typing import openmm -def array3d_to_vec3(array: np.typing.ArrayLike) -> Generator[openmm.Vec3, None, None]: +def array3d_to_vec3(array: numpy.typing.ArrayLike) -> Generator[openmm.Vec3, None, None]: for row in array: yield openmm.Vec3(*row.tolist()) -def vec3_to_array3d(vec3s: Iterable[openmm.Vec3]) -> np.typing.ArrayLike: +def vec3_to_array3d(vec3s: Iterable[openmm.Vec3]) -> numpy.typing.ArrayLike: vs = [] for vec3 in vec3s: diff --git a/src/wepy_tools/systems/alanine_dipeptide.py b/src/wepy_tools/systems/alanine_dipeptide.py index ec96cd95..e480feb3 100644 --- a/src/wepy_tools/systems/alanine_dipeptide.py +++ b/src/wepy_tools/systems/alanine_dipeptide.py @@ -5,6 +5,7 @@ import attrs import mdtraj import numpy as np +import numpy.typing import openmm import openmm.app @@ -47,8 +48,8 @@ def __init__(self) -> None: @attrs.define class AlanineDipeptideRamachandranDistanceImage(WalkerState): - phis: np.typing.ArrayLike - psis: np.typing.ArrayLike + phis: numpy.typing.ArrayLike + psis: numpy.typing.ArrayLike @attrs.define @@ -97,10 +98,17 @@ def image_distance( angles_a = np.concatenate((image_a.phis, image_a.psis)) angles_b = np.concatenate((image_b.phis, image_b.psis)) + # TODO: which one to use? + # compute the circular difference - deltas = np.atan2( + deltas = np.arctan2( np.sin(angles_a - angles_b), np.cos(angles_a - angles_b), ) + # deltas = np.atan2( + # np.sin(angles_a - angles_b), + # np.cos(angles_a - angles_b), + # ) + return np.sqrt(np.sum(deltas**2)) diff --git a/src/wepy_tools/systems/lennard_jones.py b/src/wepy_tools/systems/lennard_jones.py index ad181ef0..f1779495 100644 --- a/src/wepy_tools/systems/lennard_jones.py +++ b/src/wepy_tools/systems/lennard_jones.py @@ -1,6 +1,7 @@ # Third Party Library import attrs import numpy as np +import numpy.typing import openmm import openmm.app import openmm.unit @@ -114,7 +115,7 @@ def __init__( @attrs.define class PairDistanceImage: - positions: np.typing.ArrayLike + positions: numpy.typing.ArrayLike class PairDistance(Distance): diff --git a/tests/unit/test_hdf5.py b/tests/unit/test_hdf5.py index 3d7d310c..efb49529 100644 --- a/tests/unit/test_hdf5.py +++ b/tests/unit/test_hdf5.py @@ -761,12 +761,12 @@ def test__init_run_records_field(self, wepy_h5_factory, tmpdir): "example", field_name="c", field_shape=Ellipsis, - field_dtype=np.bool, + field_dtype=bool, ) assert "c" in record_grp assert c_dset.shape == (0,) - assert h5py.check_vlen_dtype(c_dset.dtype) == np.bool + assert h5py.check_vlen_dtype(c_dset.dtype) == bool assert c_dset.maxshape == (None,) diff --git a/tests/unit/test_work_mapper/test_openmm.py b/tests/unit/test_work_mapper/test_openmm/test_proc_pool.py similarity index 63% rename from tests/unit/test_work_mapper/test_openmm.py rename to tests/unit/test_work_mapper/test_openmm/test_proc_pool.py index bc0fa50b..587edbed 100644 --- a/tests/unit/test_work_mapper/test_openmm.py +++ b/tests/unit/test_work_mapper/test_openmm/test_proc_pool.py @@ -40,6 +40,9 @@ def openmm_state() -> OpenMMState: class Test_OpenMMProcPoolWorkMapper: + + @pytest.mark.timeout(5) + @pytest.mark.flaky(reruns=20) def test_all(self, openmm_runner): lj_sys = LennardJonesPair() @@ -67,40 +70,3 @@ def test_all(self, openmm_runner): init_states, [10 for _ in range(len(init_states))], ) - - -# class TestOpenMMRayPoolWorkMapper: - -# @pytest.mark.ray -# def test_all(self, openmm_runner): - -# lj_sys = LennardJonesPair() - -# init_states = [ -# OpenMMState.from_dwim( -# positions=lj_sys.positions, -# ) -# for _ -# in range(4) -# ] - -# mapper = OpenMMRayPoolWorkMapper( -# platform="CPU", -# device_ids=[0,1], -# global_platform_properties={"Threads" : "1"}, -# # ray_init_args={ - -# # } -# ) - -# mapper.init() -# new_states = mapper.map( -# [ -# OpenMMTask( -# runner=openmm_runner, -# segment_length=100000, -# ) -# for _ in range(len(init_states)) -# ], -# init_states, -# ) diff --git a/uv.lock b/uv.lock index a14289a6..af193e4d 100644 --- a/uv.lock +++ b/uv.lock @@ -1789,83 +1789,26 @@ wheels = [ [[package]] name = "numpy" -version = "2.3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, - { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, - { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, - { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, - { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, - { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, - { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, - { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, - { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, - { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, - { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, - { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, - { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, - { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, - { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, - { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, - { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, - { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, - { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, - { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, - { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, - { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, - { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, - { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, - { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, - { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, - { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, - { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, - { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, - { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, - { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, - { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, - { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, - { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, - { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, - { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, - { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, ] [[package]] @@ -3268,7 +3211,7 @@ wheels = [ [[package]] name = "wepy" -version = "1.1.0" +version = "2.0.0b0" source = { editable = "." } dependencies = [ { name = "attrs" }, @@ -3344,7 +3287,7 @@ requires-dist = [ { name = "mdtraj", marker = "extra == 'md'" }, { name = "more-itertools" }, { name = "networkx" }, - { name = "numpy", specifier = ">=2" }, + { name = "numpy" }, { name = "openmm", marker = "extra == 'md'" }, { name = "openmm-systems", marker = "extra == 'md'", specifier = "==0.0.0" }, { name = "pandas" }, From 5842579d33efd6fcf8d6e9a1c2e0dbebe7afc4be Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Thu, 15 Jan 2026 11:56:37 -0500 Subject: [PATCH 117/143] add top level wepy exports --- src/wepy/__init__.py | 182 +++++++++++++++++++++++++++++++++++ src/wepy/reporter/restree.py | 4 +- 2 files changed, 184 insertions(+), 2 deletions(-) diff --git a/src/wepy/__init__.py b/src/wepy/__init__.py index 19536bb5..a23ce096 100644 --- a/src/wepy/__init__.py +++ b/src/wepy/__init__.py @@ -2,6 +2,188 @@ # Local Modules from .__about__ import __version__ as __version__ +from .hdf5 import WepyHDF5 +from .monitor import Monitor +from .sim_manager import Manager +from .walker import ( + Walker, + WalkerState, + WalkerStateBox, +) +from .reporter.base import Reporter +from .reporter.dashboard import ( + ResamplerDashboardSection, + RunnerDashboardSection, + BCDashboardSection, + DashboardReporter, +) +from .reporter.hdf5 import WepyHDF5Reporter +from .reporter.openmm import OpenMMRunnerDashboardSection +from .reporter.restree import ResTreeReporter +from .reporter.revo.dashboard import REVODashboardSection + +from .resampling.decisions.decision import BaseDecisionRecord, BaseDecisionABC +from .resampling.decisions.no_decision import NoDecision +from .resampling.decisions.clone_merge import MultiCloneMergeDecision + +from .resampling.resamplers.noresampler import NoResampler, NoResamplerFactory +from .resampling.resamplers.resampler import Resampler +from .resampling.resamplers.revo import REVOResampler +from .resampling.resamplers.wexplore import WExploreResampler +from .resampling.distances.base import Distance +from .resampling.distances.mock import MockDistance +from .resampling.distances.simple import XYDistanceState, XYEuclideanDistance + +from .runners.runner import Runner, NoRunner, RunnerFactory +from .runners.mock import ( + MockState, + MockRunner, + MockRunnerFactory, +) + +from .runners.openmm.reporter import OpenMMReporter, OpenMMReporterNextReport +from .runners.openmm.logger import ( + StepIntervalLoggingReporter, + SamplingTimeIntervalLoggingReporter, + HeartBeatLoggingReporter, + HeartBeatLoggingReporterFactory, + EnergyLoggingReporter, + EnergyLoggingReporterFactory, + UnitCellLoggingReporter, + UnitCellLoggingReporterFactory, +) +from .runners.openmm.state import ( + OpenMMStateWrapper, + OpenMMState, +) +from .runners.openmm.runner import OpenMMRunner, OpenMMRunnerFactory + +from .util.json_top import ( + json_top_atom_df, + json_top_residue_df, + json_top_chain_df, + json_top_atom_count, + json_top_subset, +) +from .util.mdtraj import ( + mdtraj_to_json_topology, + json_to_mdtraj_topology, + traj_fields_to_mdtraj, +) + +# TODO: Boundary conditions + + +from .analysis.contig_tree import ( + BaseContigTree, + ContigTree, + Contig, +) +from .analysis.network import ( + BaseMacroStateNetwork, + MacroStateNetwork, +) +from .analysis.parents import ( + resampling_panel, + parent_panel, + net_parent_table, + parent_table_discontinuities, + sliding_window, + ParentForest, + ancestors, + parent_cycle_discontinuities, +) +from .analysis.profiles import ( + cumulative_partitions, + free_energy_profile, + contigtrees_bin_edges, + ContigTreeProfiler, +) +from .analysis.rates import ( + calc_warp_rate, + contig_warp_rates, +) + __author__ = "Samuel D. Lotz" __email__ = "samuel.lotz@salotz.info" + +__all__ = [ + "__version__", + "WepyHDF5", + "Monitor", + "Manager", + "Reporter", + "ResamplerDashboardSection", + "RunnerDashboardSection", + "BCDashboardSection", + "OpenMMRunnerDashboardSection", + "DashboardReporter", + "WepyHDF5Reporter", + "OpenMMReporterDashboardSection", + "ResTreeReporter", + "REVODashboardSection", + "BaseDecisionRecord", + "BaseDecisionABC", + "Walker", + "WalkerState", + "WalkerStateBox", + "NoDecision", + "MultiCloneMergeDecision", + "NoResampler", + "NoResamplerFactory", + "Resampler", + "REVOResampler", + "WExploreResampler", + "Distance", + "MockDistance", + "XYDistanceState", + "XYEuclideanDistance", + "Runner", + "NoRunner", + "RunnerFactory", + "MockState", + "MockRunner", + "MockRunnerFactory", + "OpenMMReporter", + "OpenMMReporterNextReport", + "StepIntervalLoggingReporter", + "SamplingTimeIntervalLoggingReporter", + "HeartBeatLoggingReporter", + "HeartBeatLoggingReporterFactory", + "EnergyLoggingReporter", + "EnergyLoggingReporterFactory", + "UnitCellLoggingReporter", + "UnitCellLoggingReporterFactory", + "OpenMMStateWrapper", + "OpenMMState", + "OpenMMRunner", + "OpenMMRunnerFactory", + "json_top_atom_df", + "json_top_residue_df", + "json_top_chain_df", + "json_top_atom_count", + "json_top_subset", + "mdtraj_to_json_topology", + "json_to_mdtraj_topology", + "traj_fields_to_mdtraj", + "BaseContigTree", + "ContigTree", + "Contig", + "BaseMacroStateNetwork", + "MacroStateNetwork", + "resampling_panel", + "parent_panel", + "net_parent_table", + "parent_table_discontinuities", + "sliding_window", + "ParentForest", + "ancestors", + "parent_cycle_discontinuities", + "cumulative_partitions", + "free_energy_profile", + "contigtrees_bin_edges", + "ContigTreeProfiler", + "calc_warp_rate", + "contig_warp_rates", +] diff --git a/src/wepy/reporter/restree.py b/src/wepy/reporter/restree.py index f0356e5a..3dd932ec 100644 --- a/src/wepy/reporter/restree.py +++ b/src/wepy/reporter/restree.py @@ -4,7 +4,7 @@ # Standard Library from collections import namedtuple - +import warnings # Third Party Library import networkx as nx import numpy as np @@ -27,7 +27,7 @@ parent_panel, resampling_panel, ) -from wepy.reporter.reporter import ProgressiveFileReporterABC +from wepy.reporter.file import ProgressiveFileReporterABC class ResTreeReporter(ProgressiveFileReporterABC): From a8d5c5bea0ab1f5929d2c271f06f7c0f807ae059 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 16 Jan 2026 16:43:09 -0500 Subject: [PATCH 118/143] implement new Factory interface for all factories You need to be able to report the type of the object you would produce as a factory. So add that to an explicit interface and implement in all factories. --- src/wepy/__init__.py | 41 ++++++++++++++++--- src/wepy/factory.py | 10 +++++ src/wepy/resampling/resamplers/noresampler.py | 6 ++- src/wepy/resampling/resamplers/revo.py | 4 ++ src/wepy/runners/mock.py | 4 ++ src/wepy/runners/openmm/runner.py | 4 ++ src/wepy/runners/runner.py | 15 +++++-- src/wepy/sim_manager.py | 8 ++-- src/wepy/work_mapper/base.py | 1 + src/wepy/work_mapper/openmm/proc_pool.py | 5 +++ src/wepy/work_mapper/openmm/serial.py | 4 ++ src/wepy/work_mapper/serial.py | 10 +++++ 12 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 src/wepy/factory.py diff --git a/src/wepy/__init__.py b/src/wepy/__init__.py index a23ce096..4c8b2d7e 100644 --- a/src/wepy/__init__.py +++ b/src/wepy/__init__.py @@ -4,7 +4,12 @@ from .__about__ import __version__ as __version__ from .hdf5 import WepyHDF5 from .monitor import Monitor -from .sim_manager import Manager +from .sim_manager import ( + Manager, + ResamplerFactory, + WorkMapperFactory, + RunnerFactory, +) from .walker import ( Walker, WalkerState, @@ -28,13 +33,13 @@ from .resampling.resamplers.noresampler import NoResampler, NoResamplerFactory from .resampling.resamplers.resampler import Resampler -from .resampling.resamplers.revo import REVOResampler +from .resampling.resamplers.revo import REVOResampler, REVOResamplerFactory from .resampling.resamplers.wexplore import WExploreResampler from .resampling.distances.base import Distance from .resampling.distances.mock import MockDistance from .resampling.distances.simple import XYDistanceState, XYEuclideanDistance -from .runners.runner import Runner, NoRunner, RunnerFactory +from .runners.runner import Runner, NoRunner, NoRunnerFactory from .runners.mock import ( MockState, MockRunner, @@ -56,7 +61,11 @@ OpenMMStateWrapper, OpenMMState, ) -from .runners.openmm.runner import OpenMMRunner, OpenMMRunnerFactory +from .runners.openmm.runner import ( + OpenMMRunner, + OpenMMRunnerFactory, +) + from .util.json_top import ( json_top_atom_df, @@ -71,7 +80,12 @@ traj_fields_to_mdtraj, ) -# TODO: Boundary conditions +from .work_mapper.base import WorkMapper +from .work_mapper.serial import SerialMapper, SerialMapperFactory +from .work_mapper.openmm.serial import OpenMMSerialWorkMapper, OpenMMSerialWorkMapperFactory +from .work_mapper.openmm.proc_pool import OpenMMProcPoolWorkMapper, OpenMMProcPoolWorkMapperFactory + +from .boundary_conditions.boundary import BoundaryConditions from .analysis.contig_tree import ( @@ -103,12 +117,14 @@ calc_warp_rate, contig_warp_rates, ) +from .analysis.network_layouts.layout_graph import LayoutGraph __author__ = "Samuel D. Lotz" __email__ = "samuel.lotz@salotz.info" __all__ = [ + "LayoutGraph", "__version__", "WepyHDF5", "Monitor", @@ -141,7 +157,6 @@ "XYEuclideanDistance", "Runner", "NoRunner", - "RunnerFactory", "MockState", "MockRunner", "MockRunnerFactory", @@ -186,4 +201,18 @@ "ContigTreeProfiler", "calc_warp_rate", "contig_warp_rates", + "BoundaryConditions", + "WorkMapper", + "SerialMapper", + "OpenMMSerialWorkMapper", + "OpenMMSerialWorkMapperFactory", + "OpenMMProcPoolWorkMapper", + "OpenMMProcPoolWorkMapperFactory", + "ResamplerFactory", + "WorkMapperFactory", + "RunnerFactory", + "REVOResamplerFactory", + "NoRunnerFactory", + "SerialMapperFactory", + "OPENMM_DEFAULT_UNITS", ] diff --git a/src/wepy/factory.py b/src/wepy/factory.py new file mode 100644 index 00000000..99871e46 --- /dev/null +++ b/src/wepy/factory.py @@ -0,0 +1,10 @@ +from typing import Protocol, Generic, TypeVar + +GeneratedType_ = TypeVar("GeneratedType_") + +class Factory(Protocol, Generic[GeneratedType_]): + + @classmethod + def type(cls) -> type[GeneratedType_]: ... + + def __call__(self) -> GeneratedType_: ... diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py index d14a2894..36b50fcd 100644 --- a/src/wepy/resampling/resamplers/noresampler.py +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -71,10 +71,12 @@ def resample( return walkers, resampling_data, resampler_data +@attrs.define class NoResamplerFactory: - def __init__(self) -> None: - pass + @classmethod + def type(cls) -> type[NoResampler]: + return NoResampler def __call__(self, num_cores: int) -> NoResampler: return NoResampler() diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 3e4d118b..7a21dbf9 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -901,6 +901,10 @@ class REVOResamplerFactory(Generic[DistanceMetric_]): pmax: float = 0.1 seed: int | None = None + @classmethod + def type(cls) -> type[REVOResampler]: + return REVOResampler + def __call__( self, num_cores: int, diff --git a/src/wepy/runners/mock.py b/src/wepy/runners/mock.py index b9a63d38..224f9874 100644 --- a/src/wepy/runners/mock.py +++ b/src/wepy/runners/mock.py @@ -97,5 +97,9 @@ def run_segment( class MockRunnerFactory: fail: bool = False + @classmethod + def type(cls) -> type[MockRunner]: + return MockRunner + def __call__(self) -> MockRunner: return MockRunner(fail=self.fail) diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index 6d0db92f..204f8329 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -534,6 +534,10 @@ class OpenMMRunnerFactory: default=DEFAULT_OPENMM_REPORTER_FACTORIES ) + @classmethod + def type(cls) -> type[OpenMMRunner]: + return OpenMMRunner + def __call__(self) -> OpenMMRunner: return OpenMMRunner( diff --git a/src/wepy/runners/runner.py b/src/wepy/runners/runner.py index 8f347eef..84a26824 100644 --- a/src/wepy/runners/runner.py +++ b/src/wepy/runners/runner.py @@ -185,10 +185,6 @@ def post_cycle( ... -RunnerFactory = Callable[ - [], - Runner, -] @attrs.define @@ -231,3 +227,14 @@ def post_cycle(self, segments_data: None) -> None: self.state_machine.send(RunnerEvent.POST_SEGMENT) self.state_machine.send(RunnerEvent.POST_CYCLE) + +@attrs.define +class NoRunnerFactory: + + @classmethod + def type(cls) -> type[NoRunner]: + return NoRunner + + def __call__(self) -> NoRunner: + return NoRunner() + diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index 7e2953a2..943db07a 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -55,11 +55,12 @@ from immutables import Map as frozenmap # First Party Library +from wepy.factory import Factory from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.monitor import Monitor from wepy.reporter.base import CycleReportDict, Reporter from wepy.resampling.resamplers.resampler import Resampler -from wepy.runners.runner import Runner, RunnerFactory, RunSegmentData +from wepy.runners.runner import Runner, RunSegmentData from wepy.walker import Walker from wepy.work_mapper.base import WorkMapper from wepy.work_mapper.serial import SerialMapper @@ -339,8 +340,9 @@ def send(self, event: ManagerEvent) -> ManagerStatus: return self.state -ResamplerFactory = Callable[[], Resampler] -WorkMapperFactory = Callable[[], WorkMapper] +ResamplerFactory = Factory[Resampler] +WorkMapperFactory = Factory[WorkMapper] +RunnerFactory = Factory[Runner] State_ = TypeVar("State_") RunSegmentData_ = TypeVar("RunSegmentData_", bound=RunSegmentData, covariant=True) diff --git a/src/wepy/work_mapper/base.py b/src/wepy/work_mapper/base.py index 97945f33..3f63b474 100644 --- a/src/wepy/work_mapper/base.py +++ b/src/wepy/work_mapper/base.py @@ -37,3 +37,4 @@ def map( def get_worker_segment_times(self) -> dict[int, list[float]] | None: ... def cleanup(self) -> None: ... + diff --git a/src/wepy/work_mapper/openmm/proc_pool.py b/src/wepy/work_mapper/openmm/proc_pool.py index e405a071..06da9f5a 100644 --- a/src/wepy/work_mapper/openmm/proc_pool.py +++ b/src/wepy/work_mapper/openmm/proc_pool.py @@ -245,6 +245,11 @@ def __attrs_post_init__(self) -> None: f"When device_ids is given ({self.device_ids}) it must be the same length as the number of processes: {self.num_procs}" ) + @classmethod + def type(cls) -> type[OpenMMProcPoolWorkMapper]: + return OpenMMProcPoolWorkMapper + + def __call__(self) -> OpenMMProcPoolWorkMapper: return OpenMMProcPoolWorkMapper( diff --git a/src/wepy/work_mapper/openmm/serial.py b/src/wepy/work_mapper/openmm/serial.py index 7022ed01..5d9fad8f 100644 --- a/src/wepy/work_mapper/openmm/serial.py +++ b/src/wepy/work_mapper/openmm/serial.py @@ -79,6 +79,10 @@ class OpenMMSerialWorkMapperFactory: platform: OpenMMPlatformName global_platform_properties: dict[str, str] | None = None + @classmethod + def type(cls) -> type[OpenMMSerialWorkMapper]: + return OpenMMSerialWorkMapper + def __call__(self) -> OpenMMSerialWorkMapper: return OpenMMSerialWorkMapper( diff --git a/src/wepy/work_mapper/serial.py b/src/wepy/work_mapper/serial.py index 2e0035ee..351c3232 100644 --- a/src/wepy/work_mapper/serial.py +++ b/src/wepy/work_mapper/serial.py @@ -12,6 +12,7 @@ # First Party Library from wepy.runners.runner import RunSegmentData from wepy.walker import WalkerState +from wepy.factory import Factory logger = logging.getLogger(__name__) @@ -82,3 +83,12 @@ def map( self._worker_segment_times[0] = segment_times return results + +class SerialMapperFactory(Factory[SerialMapper]): + + @classmethod + def type(cls) -> type[SerialMapper]: + return SerialMapper + + def __call__(self) -> SerialMapper: + return SerialMapper() From 26313f75d6163481dbb5bb8ed8835aa9496f5cd4 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 16 Jan 2026 16:44:09 -0500 Subject: [PATCH 119/143] wip! partial updates to dashboard reporters --- src/wepy/reporter/dashboard.py | 6 ++-- src/wepy/reporter/openmm.py | 36 ++++++++++++---------- tests/unit/test_reporter/test_openmm.py | 41 +++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 20 deletions(-) create mode 100644 tests/unit/test_reporter/test_openmm.py diff --git a/src/wepy/reporter/dashboard.py b/src/wepy/reporter/dashboard.py index b42574f1..8150af8f 100644 --- a/src/wepy/reporter/dashboard.py +++ b/src/wepy/reporter/dashboard.py @@ -106,9 +106,9 @@ class RunnerDashboardSection: Runner: {{ name }} """ ) - def __init__(self, runner=None, name=None, **kwargs): - if runner is not None: - self.runner_name = type(runner).__name__ + def __init__(self, runner_factory = None, name=None): + if runner_factory is not None: + self.runner_name = runner_factory.type().__name__ elif name is not None: self.runner_name = name diff --git a/src/wepy/reporter/openmm.py b/src/wepy/reporter/openmm.py index 2ef19e42..8dde27e2 100644 --- a/src/wepy/reporter/openmm.py +++ b/src/wepy/reporter/openmm.py @@ -1,12 +1,8 @@ -# Third Party Library -from pint import UnitRegistry +import openmm.unit # First Party Library from wepy.reporter.dashboard import RunnerDashboardSection -# initialize the unit registry -units = UnitRegistry() - class OpenMMRunnerDashboardSection(RunnerDashboardSection): RUNNER_SECTION_TEMPLATE = """ @@ -20,13 +16,14 @@ class OpenMMRunnerDashboardSection(RunnerDashboardSection): Total Sampling Time: {{ total_sampling_time }} """ - def __init__(self, runner=None, step_time=None, **kwargs): - if "name" not in kwargs: - kwargs["name"] = "OpenMMRunner" + def __init__(self, runner_factory=None, step_time=None): - super().__init__(runner=runner, step_time=step_time, **kwargs) + super().__init__( + runner_factory=runner_factory, + name="OpenMMRunner", + ) - if runner is None: + if runner_factory is None: assert ( step_time is not None ), "If no complete runner is given must give parameters: step_time" @@ -35,23 +32,28 @@ def __init__(self, runner=None, step_time=None, **kwargs): self.step_time = step_time else: - simtk_step_time = runner.integrator.getStepSize() - simtk_val = simtk_step_time.value_in_unit(simtk_step_time.unit) - + self.step_time = runner_factory.integrator.getStepSize() + + # HACK,TODO: this conversion would likely not work in + # general so I'm just removing it and using the plain + # openmm on until there is a better conversion system + # between them. + # + # simtk_val = simtk_step_time.value_in_unit(simtk_step_time.unit) + # # convert to a more general purpose pint unit, which will be # used for the dashboards so we don't have the simtk # dependency - self.step_time = simtk_val * units(simtk_step_time.unit.get_name()) + # self.step_time = simtk_val * units(simtk_step_time.unit.get_name()) # TODO - # integrator and params # FF and params # updatables - self.walker_total_sampling_time = 0.0 * units("microsecond") - self.total_sampling_time = 0.0 * units("microsecond") + self.walker_total_sampling_time = 0.0 * openmm.unit.microsecond + self.total_sampling_time = 0.0 * openmm.unit.microsecond def update_values(self, **kwargs): super().update_values(**kwargs) diff --git a/tests/unit/test_reporter/test_openmm.py b/tests/unit/test_reporter/test_openmm.py new file mode 100644 index 00000000..449f089a --- /dev/null +++ b/tests/unit/test_reporter/test_openmm.py @@ -0,0 +1,41 @@ +import pytest + +import openmm +import openmm.app + +from wepy.reporter.openmm import OpenMMRunnerDashboardSection +from wepy.runners.openmm import OpenMMRunnerFactory + +from wepy_tools.systems.lennard_jones import LennardJonesPair + +STEP_SIZE = 2 * openmm.unit.femtoseconds + +@pytest.fixture +def runner_components() -> ( + tuple[openmm.System, openmm.app.Topology, openmm.LangevinIntegrator] +): + + lj_sys = LennardJonesPair() + + integrator = openmm.LangevinIntegrator(300.0, 0.1, STEP_SIZE) + + return lj_sys.system, lj_sys.topology, integrator + +class Test_OpenMMRunnerDashboardSection: + + def test___init__(self, runner_components): + + system, topology, integrator = runner_components + + section = OpenMMRunnerDashboardSection( + runner_factory=OpenMMRunnerFactory( + system=system, + topology=topology, + integrator=integrator, + ) + ) + + assert section.runner_name == "OpenMMRunner" + assert section.step_time == STEP_SIZE + assert section.walker_total_sampling_time == 0.0 * openmm.unit.microsecond + assert section.total_sampling_time == 0.0 * openmm.unit.microsecond From 25eb33f2cbccc19b88a4d843ed0d6dfee4a324f2 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 16 Jan 2026 16:45:12 -0500 Subject: [PATCH 120/143] update and export the openmm default units --- src/wepy/__init__.py | 1 + src/wepy/runners/openmm/__init__.py | 2 ++ src/wepy/runners/openmm/state.py | 24 +++--------------------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/wepy/__init__.py b/src/wepy/__init__.py index 4c8b2d7e..76007975 100644 --- a/src/wepy/__init__.py +++ b/src/wepy/__init__.py @@ -60,6 +60,7 @@ from .runners.openmm.state import ( OpenMMStateWrapper, OpenMMState, + OPENMM_DEFAULT_UNITS, ) from .runners.openmm.runner import ( OpenMMRunner, diff --git a/src/wepy/runners/openmm/__init__.py b/src/wepy/runners/openmm/__init__.py index bc251cd4..ddffba94 100644 --- a/src/wepy/runners/openmm/__init__.py +++ b/src/wepy/runners/openmm/__init__.py @@ -18,6 +18,7 @@ dummy_context, get_context_state, state_to_xml, + OPENMM_DEFAULT_UNITS, ) __all__ = [ @@ -35,4 +36,5 @@ "PlatformKwargs", "UnitCellLoggingReporterFactory", "EnergyLoggingReporterFactory", + "OPENMM_DEFAULT_UNITS", ] diff --git a/src/wepy/runners/openmm/state.py b/src/wepy/runners/openmm/state.py index c1981635..a5510f34 100644 --- a/src/wepy/runners/openmm/state.py +++ b/src/wepy/runners/openmm/state.py @@ -1,6 +1,6 @@ # Standard Library import logging -from collections.abc import Collection +from collections.abc import Collection, Mapping from typing import ( Any, ClassVar, @@ -222,8 +222,8 @@ class OpenMMStateValidationError(Exception): box_volume=openmm.unit.nanometer**3, velocities=openmm.unit.nanometer / openmm.unit.picosecond, forces=openmm.unit.kilojoule / openmm.unit.nanometer, - kinetic_energy=openmm.unit.kilojoule, - potential_energy=openmm.unit.kilojoule, + kinetic_energy=(openmm.unit.kilojoule / openmm.unit.mole), + potential_energy=(openmm.unit.kilojoule / openmm.unit.mole), ) @@ -413,24 +413,6 @@ def get_state_fields_present(sim_state: openmm.State) -> frozenset[StateFieldNam return frozenset(present_fields) -# the names of the units from the units objects above. This is used -# for saving them to files -UNIT_NAMES: tuple[tuple[str, str], ...] = ( - ("positions_unit", openmm.unit.nanometer.get_name()), - ("time_unit", openmm.unit.picosecond.get_name()), - ("box_vectors_unit", openmm.unit.nanometer.get_name()), - ("velocities_unit", (openmm.unit.nanometer / openmm.unit.picosecond).get_name()), - ( - "forces_unit", - (openmm.unit.kilojoule / (openmm.unit.nanometer * openmm.unit.mole)).get_name(), - ), - ("box_volume_unit", openmm.unit.nanometer.get_name()), - ("kinetic_energy_unit", (openmm.unit.kilojoule / openmm.unit.mole).get_name()), - ("potential_energy_unit", (openmm.unit.kilojoule / openmm.unit.mole).get_name()), -) -"""Mapping of unit identifier strings to the serialized string spec of the unit.""" - - ## Wrapper for a openmm.State From 62333c27701c86b7904584feadfe9dfbb413a35d Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 16 Jan 2026 16:54:29 -0500 Subject: [PATCH 121/143] fixes saving of scalar values from states The scalar values like energy and time were not being wrapped in the appropriate number of dimensions and were giving errors when added to the HDF5. Fix this in the reporter and add tests explicitly to exercise this. Adds some more fields to the realistic test to show doing this as well. --- src/wepy/hdf5.py | 2 +- src/wepy/reporter/hdf5.py | 15 ++++++++++--- .../integration/test_openmm/test_realistic.py | 12 ++++++++-- tests/unit/test_reporter/test_hdf5.py | 22 +++++++++++++++++++ 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/wepy/hdf5.py b/src/wepy/hdf5.py index 9cbf2986..cb8d70ee 100644 --- a/src/wepy/hdf5.py +++ b/src/wepy/hdf5.py @@ -1823,7 +1823,7 @@ def _extend_contiguous_traj_field(self, run_idx, traj_idx, field_path, field_dat # make sure this is a feature vector assert ( len(field_data.shape) > 1 - ), "field_data must be a feature vector with the same number of dimensions as the number" + ), f"field_data (path={field_path}, shape={field_data.shape}) must be a feature vector with the same number of dimensions as the number." # of datase new frames n_new_frames = field_data.shape[0] diff --git a/src/wepy/reporter/hdf5.py b/src/wepy/reporter/hdf5.py index 75e502e5..c7562e4f 100644 --- a/src/wepy/reporter/hdf5.py +++ b/src/wepy/reporter/hdf5.py @@ -848,10 +848,19 @@ def report( ] - # for all of these fields we wrap them in another - # dimension to make them feature vectors + # for all of these fields we wrap them in additional + # dimensions to make them feature vectors for field_path in list(walker_data.keys()): - _walker_data_noq[field_path] = np.array([_walker_data_noq[field_path]]) + + # first if its a scalar wrap in the first layer + _val = _walker_data_noq[field_path] + if np.isscalar(_val): + _val = np.array([_val]) + + # then reshape to feature vector + _val = _val.reshape((1, *_val.shape)) + + _walker_data_noq[field_path] = _val # save the data to the HDF5 file for this walker diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index e01298df..5fe96789 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -53,6 +53,14 @@ DEFAULT_CYCLE_TIME = 10.0 * openmm.unit.picosecond DEFAULT_CYCLE_STEPS = round(DEFAULT_CYCLE_TIME / STEP_SIZE) +DEFAULT_SAVE_FIELDS = ( + "positions", + "box_vectors", + "box_volume", + "potential_energy", + "kinetic_energy", +) + def test_lennard_jones_revo_procpool(tmp_path_factory): @@ -112,7 +120,7 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): hdf5_path = outputs_dir / "main.wepy.h5" hdf5_reporter = WepyHDF5Reporter.from_components( file_path=hdf5_path, - save_fields=("positions",), + save_fields=DEFAULT_SAVE_FIELDS, topology=test_sys.json_top, resampler_class=REVOResampler, ) @@ -209,7 +217,7 @@ def test_alanine_dipeptide_revo_procpool(tmp_path_factory): hdf5_path = outputs_dir / "main.wepy.h5" hdf5_reporter = WepyHDF5Reporter.from_components( file_path=hdf5_path, - save_fields=("positions",), + save_fields=DEFAULT_SAVE_FIELDS, topology=ala_sys.json_top, resampler_class=REVOResampler, ) diff --git a/tests/unit/test_reporter/test_hdf5.py b/tests/unit/test_reporter/test_hdf5.py index b7fbd8da..60638682 100644 --- a/tests/unit/test_reporter/test_hdf5.py +++ b/tests/unit/test_reporter/test_hdf5.py @@ -22,6 +22,7 @@ [0., 0., 0.,], [0., 0., 0.,], ]) * openmm.unit.nanometer, + time=(1.3 * openmm.unit.nanosecond), ), 0.5, ), @@ -31,6 +32,7 @@ [0., 0., 0.,], [0., 0., 0.,], ]) * openmm.unit.nanometer, + time=(1.3 * openmm.unit.nanosecond), ), 0.5, ), @@ -540,6 +542,8 @@ def test_report(self, tmp_path_factory): file_path=h5_path, topology=test_sys.json_top, **RESAMPLER_REPORTER_ARGS, + # test output of both an array and a scalar + save_fields=("positions", "time"), ) reporter.init(**LJ_OPENMM_SIM_COMPONENTS) @@ -553,6 +557,7 @@ def test_report(self, tmp_path_factory): [0., 0., 0.,], [1., 1., 1.,], ]) * openmm.unit.nanometer, + time=(1.0 * openmm.unit.picosecond), ), 0.5, ), @@ -562,6 +567,7 @@ def test_report(self, tmp_path_factory): [1., 1., 1.,], [0., 0., 0.,], ]) * openmm.unit.nanometer, + time=(1.0 * openmm.unit.picosecond), ), 0.5, ), @@ -576,6 +582,7 @@ def test_report(self, tmp_path_factory): [1., 1., 1.,], [0., 0., 0.,], ]) * openmm.unit.nanometer, + time=(1.0 * openmm.unit.picosecond), ), 0.5, ), @@ -585,6 +592,7 @@ def test_report(self, tmp_path_factory): [0., 0., 0.,], [1., 1., 1.,], ]) * openmm.unit.nanometer, + time=(1.0 * openmm.unit.picosecond), ), 0.5, ), @@ -607,3 +615,17 @@ def test_report(self, tmp_path_factory): **CYCLE_REPORT_DICT_COMMON, **CYCLE_REPORT_DICT_EMPTY_OPTIONALS, ) + + with reporter.wepy_h5 as wepy_h5: + + assert len(wepy_h5.h5['runs/0/trajectories']) == 2 + + assert "0" in wepy_h5.h5['runs/0/trajectories'] + assert "1" in wepy_h5.h5['runs/0/trajectories'] + + assert "weights" in wepy_h5.h5['runs/0/trajectories/0'] + assert "positions" in wepy_h5.h5['runs/0/trajectories/0'] + assert "time" in wepy_h5.h5['runs/0/trajectories/0'] + + assert wepy_h5.h5['runs/0/trajectories/0/positions'].shape == (1,2,3) + assert wepy_h5.h5['runs/0/trajectories/0/time'].shape == (1,1) From ef73a1f97bf129d2f80dcade3571ba8d1d5a4dbc Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 16 Jan 2026 16:58:10 -0500 Subject: [PATCH 122/143] add default units to HDF5 reporter --- src/wepy/reporter/hdf5.py | 14 ++++-- tests/unit/test_reporter/test_hdf5.py | 66 +++++++++++++++++++-------- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/src/wepy/reporter/hdf5.py b/src/wepy/reporter/hdf5.py index c7562e4f..d377621e 100644 --- a/src/wepy/reporter/hdf5.py +++ b/src/wepy/reporter/hdf5.py @@ -26,6 +26,7 @@ from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.typing import Shape, Idxs, IdxArray from wepy.storage.protocol import Record +from wepy.runners.openmm import OPENMM_DEFAULT_UNITS logger = logging.getLogger(__name__) @@ -160,7 +161,9 @@ def __init__( topology : str JSON string representing topology of system being simulated. - units : Mapping of trajectory field names to Unit objects. + units : Mapping of trajectory field names to Unit objects. If + None the defaults will be used. If not all units are defined + then the missing ones will be filled in by the defaults. sparse_fields : dict of str: int, optional List of trajectory fields that should be initialized as sparse. @@ -392,9 +395,9 @@ def __init__( # if units were given add them otherwise set as an empty dictionary if units is None: - self.units = {} + self.units = OPENMM_DEFAULT_UNITS else: - self.units = units + self.units = dict(OPENMM_DEFAULT_UNITS) | units @classmethod def from_components( @@ -698,8 +701,11 @@ def init(self, **kwargs: SimComponentArgs) -> None: # If no self.units were given, use the first # init walker to determine the units for a field overall, set # this and use for the rest of the walkers + if walker_idx == 0: - self.units.update(units_used) + for unit_name, unit in units_used.items(): + if unit_name not in self.units: + self.units.update(units_used) converted_filtered_init_walkers.append( Walker(state=_state, weight=init_walker.weight) diff --git a/tests/unit/test_reporter/test_hdf5.py b/tests/unit/test_reporter/test_hdf5.py index 60638682..99cf5d6b 100644 --- a/tests/unit/test_reporter/test_hdf5.py +++ b/tests/unit/test_reporter/test_hdf5.py @@ -6,7 +6,7 @@ from wepy.resampling.resamplers.noresampler import NoResampler from wepy.walker import Walker, WalkerStateBox from wepy.runners.mock import MockState, MockRunner -from wepy.runners.openmm import OpenMMState +from wepy.runners.openmm import OpenMMState, OPENMM_DEFAULT_UNITS from wepy.work_mapper.serial import SerialMapper from wepy.hdf5 import WepyHDF5 from wepy.resampling.decisions.no_decision import ( @@ -106,7 +106,7 @@ def test___init__(self, tmp_path_factory): ) assert reporter.main_rep_idxs is None - assert reporter.units == {} + assert reporter.units == OPENMM_DEFAULT_UNITS reporter = WepyHDF5Reporter( file_path=h5_path, @@ -122,10 +122,12 @@ def test___init__(self, tmp_path_factory): topology=test_sys.json_top, **RESAMPLER_REPORTER_ARGS, units={ - "positions" : "nanometer", + "positions" : openmm.unit.angstrom, } ) + assert reporter.units == dict(OPENMM_DEFAULT_UNITS) | {"positions" : openmm.unit.angstrom} + reporter = WepyHDF5Reporter( file_path=h5_path, topology=test_sys.json_top, @@ -483,18 +485,14 @@ def test_init(self, tmp_path_factory): reporter = WepyHDF5Reporter( file_path=h5_path, topology=test_sys.json_top, - units={"positions" : openmm.unit.angstrom}, + units=None, **RESAMPLER_REPORTER_ARGS, ) reporter.init(**LJ_OPENMM_SIM_COMPONENTS) - assert reporter.units == { - "positions" : openmm.unit.angstrom, - "time" : openmm.unit.picosecond, - "box_vectors" : openmm.unit.nanometer, - "box_volume" : (openmm.unit.nanometer ** 3), - } + assert reporter.units == OPENMM_DEFAULT_UNITS + assert reporter.wepy_run_idx == 0 assert reporter._tmp_topology is None assert reporter.file_path == h5_path @@ -503,31 +501,63 @@ def test_init(self, tmp_path_factory): # minimal tests, see _initialize_h5_run for more in depth tests with reporter.wepy_h5 as wepy_h5: + # should be defaults + assert wepy_h5.h5["units/positions"][()].decode() == "nanometer" + assert wepy_h5.h5["units/box_vectors"][()].decode() == "nanometer" + assert wepy_h5.h5["units/box_volume"][()].decode() == "nanometer**3" + assert wepy_h5.h5["units/time"][()].decode() == "picosecond" assert "0" in wepy_h5.h5["runs"] assert "init_walkers" in wepy_h5.h5["runs/0"] assert len(wepy_h5.h5["runs/0/init_walkers"]) == 2 # if no units are given, derive them dynamically from # quantities - d0 = tmp_path_factory.mktemp("0") - h5_path = d0 / "main.wepy.h5" + d1 = tmp_path_factory.mktemp("1") + h5_path = d1 / "main.wepy.h5" reporter = WepyHDF5Reporter( file_path=h5_path, topology=test_sys.json_top, - units=None, + units={ + # provide explicit units for all the encountered + # fields. These should be the reporter units + "box_vectors" : openmm.unit.angstrom, + "time" : openmm.unit.nanosecond, + }, **RESAMPLER_REPORTER_ARGS, ) reporter.init(**LJ_OPENMM_SIM_COMPONENTS) - assert reporter.units == { - "positions" : openmm.unit.nanometer, - "time" : openmm.unit.picosecond, - "box_vectors" : openmm.unit.nanometer, - "box_volume" : (openmm.unit.nanometer ** 3), + assert { + unit_name : unit + for unit_name, unit + in reporter.units.items() + if unit_name in {"box_vectors", "time"} + } == { + "box_vectors" : openmm.unit.angstrom, + "time" : openmm.unit.nanosecond, } + assert { + unit_name : unit + for unit_name, unit + in reporter.units.items() + if unit_name not in {"box_vectors", "time"} + } == { + unit_name : unit + for unit_name, unit + in OPENMM_DEFAULT_UNITS.items() + if unit_name not in {"box_vectors", "time"} + } + + with reporter.wepy_h5 as wepy_h5: + # the overridden ones + assert wepy_h5.h5["units/box_vectors"][()].decode() == "angstrom" + assert wepy_h5.h5["units/time"][()].decode() == "nanosecond" + # some of the defaults + assert wepy_h5.h5["units/positions"][()].decode() == "nanometer" + assert wepy_h5.h5["units/box_volume"][()].decode() == "nanometer**3" def test_report(self, tmp_path_factory): # TODO: using the OpenMM Runner OpenMMState here because the From 607d30d100bc71561138f25496f80852c94f7b5e Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 16 Jan 2026 16:58:22 -0500 Subject: [PATCH 123/143] implement separate `init_walker_save_fields` for HDF5 reporter There is a problem when running realistic simulations in which you want to save something like energy. The problem is that previously when saving the initial walkers to the HDF5 it would enforce that they also have energy. This is burdensome and not necessary to the application user and would likely cause more problems. So to work around this this change adds an additional option to the `WepyHDF5Reporter` `init_walker_save_fields` which lets you customize this. In default usage it does default to having the same fields as the `save_fields` (what will show up in the trajectory data). But you can then provide only the fields you want, e.g. just positions and box_vectors as most initial states have. --- src/wepy/reporter/hdf5.py | 80 ++++++++++-- .../integration/test_openmm/test_realistic.py | 8 ++ tests/unit/test_reporter/test_hdf5.py | 114 +++++++++++++++++- 3 files changed, 188 insertions(+), 14 deletions(-) diff --git a/src/wepy/reporter/hdf5.py b/src/wepy/reporter/hdf5.py index d377621e..01783da1 100644 --- a/src/wepy/reporter/hdf5.py +++ b/src/wepy/reporter/hdf5.py @@ -1,5 +1,6 @@ # Standard Library from pathlib import Path +import builtins import logging from typing import Self, TypedDict, Literal, Generic, TypeVar, Any @@ -84,6 +85,7 @@ class WepyHDF5Reporter( # static attributes swmr_mode: bool save_fields: tuple[str, ...] | None + init_walker_save_fields: tuple[str, ...] | None | Literal[Ellipsis] _sparse_fields: dict[str, int] _feature_shapes: dict[str, RecordFieldShapeSpec] | None _feature_dtypes: dict[str, RecordFieldDtype] | None @@ -122,6 +124,7 @@ def __init__( resampling_fields: tuple[str, ...], swmr_mode: bool = False, save_fields: tuple[str, ...] | None = None, + init_walker_save_fields: tuple[str, ...] | None | Literal[Ellipsis] = None, units: dict[str, openmm.unit.Unit] | None = None, sparse_fields: dict[str, int | Literal[Ellipsis]] | None = None, n_dims: int = 3, @@ -158,6 +161,11 @@ def __init__( all fields from states will attempted to be saved. To not save anything provide an empty tuple (). + init_walker_save_fields : A selection of fields to require for + the initial walkers. If None this will require the same as + the 'save_fields' argument. If Ellipsis this will accept + whatever fields the init_walkers have without error. + topology : str JSON string representing topology of system being simulated. @@ -263,6 +271,7 @@ def __init__( # which fields from the walker to save, if None then save all of them self.save_fields = save_fields + self.init_walker_save_fields = init_walker_save_fields # check sparse fields if sparse_fields is not None: @@ -410,6 +419,7 @@ def from_components( boundary_conditions_class: type[BoundaryConditions] | None = None, swmr_mode: bool = False, save_fields: tuple[str, ...] | None = None, + init_walker_save_fields: tuple[str, ...] | None | Literal[Ellipsis] = None, units: dict[str, openmm.unit.Unit] | None = None, sparse_fields: dict[str, int] | None = None, n_dims: int = 3, @@ -468,6 +478,7 @@ def from_components( feature_dtypes=feature_dtypes, swmr_mode=swmr_mode, save_fields=save_fields, + init_walker_save_fields=init_walker_save_fields, units=units, sparse_fields=sparse_fields, n_dims=n_dims, @@ -637,16 +648,60 @@ def init(self, **kwargs: SimComponentArgs) -> None: ## Do checks on the inputs and figure out runtime field metadata # if we specify save fields only save these for the initial walkers - if self.save_fields is not None: - state_fields = list(kwargs["init_walkers"][0].state.dict().keys()) + state_fields = set(kwargs["init_walkers"][0].state.dict().keys()) + match (self.save_fields, self.init_walker_save_fields): + + # NOTE: the builtins.Ellipsis is needed to avoid matching + # anything + case (_, builtins.Ellipsis): + _save_fields = state_fields + logger.info( + f"Accepting and saving all fields found in init_walkers: {state_fields}" + ) + logger.warning("To ensure all required data is in a simulation these fields should be explicit.") + + + case (None, None): + _save_fields = state_fields + logger.info( + f"Accepting and saving all fields found in init_walkers: {state_fields}" + ) + logger.warning( + "'save_fields' is None and 'init_walker_save_fields' is None. " + "Any found fields will be saved. This is inadvisable and fields should be " + "declared to avoid spurious data outputs." + ) + case (fields, None): + _save_fields = set(fields) + logger.info( + f"Initial walker fields being saved determined from 'save_fields': {_save_fields}" + ) + + case (_, fields): + _save_fields = set(fields) + logger.info( + f"Initial walker fields being saved determined from 'init_walker_save_fields': {_save_fields}" + ) + + + if _save_fields == state_fields: + filtered_init_walkers = kwargs["init_walkers"] - # make sure all the save_fields are present in the state - assert all( + elif not all( [ True if save_field in state_fields else False - for save_field in self.save_fields + for save_field + in _save_fields ] - ), "Not all specified save_fields present in walker states" + ): + + # make sure all the save_fields are present in the state + raise ValueError( + f"init_walkers should have all fields as required: {_save_fields}. " + f"Found: {state_fields}" + ) + + else: filtered_init_walkers = [] for walker in kwargs["init_walkers"]: @@ -654,7 +709,7 @@ def init(self, **kwargs: SimComponentArgs) -> None: state_d = { k: v for k, v in walker.state.dict().items() - if k in self.save_fields + if k in _save_fields } # and saving alternate representations as we would @@ -688,12 +743,9 @@ def init(self, **kwargs: SimComponentArgs) -> None: new_state = WalkerStateBox(**state_d) filtered_init_walkers.append(Walker(new_state, walker.weight)) - # otherwise save the full state - else: - filtered_init_walkers = kwargs["init_walkers"] # If the state field values are quantities convert them to - # plain values. + # plain values. converted_filtered_init_walkers = [] for walker_idx, init_walker in enumerate(filtered_init_walkers): _state, units_used = self._resolve_state_units(self.units, init_walker.state) @@ -711,8 +763,6 @@ def init(self, **kwargs: SimComponentArgs) -> None: Walker(state=_state, weight=init_walker.weight) ) - # Run the constructor intialization - logger.info(f"Initializing HDF5 file at {self.file_path}") # convert units to strings _str_units = { @@ -720,6 +770,10 @@ def init(self, **kwargs: SimComponentArgs) -> None: for key, unit in self.units.items() } + logger.info(f"Serialized units: {_str_units}") + + # Run the constructor intialization + logger.info(f"Initializing HDF5 file at {self.file_path}") init_wepy_h5 = WepyHDF5( self.file_path, diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index 5fe96789..f3314e87 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -16,6 +16,7 @@ import pytest # First Party Library +import wepy from wepy.reporter.dashboard import DashboardReporter from wepy.resampling.resamplers.revo import REVOResamplerFactory, REVOResampler from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState @@ -121,6 +122,8 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): hdf5_reporter = WepyHDF5Reporter.from_components( file_path=hdf5_path, save_fields=DEFAULT_SAVE_FIELDS, + # only require these fields for the initial walkers + init_walker_save_fields=("positions", "box_vectors",), topology=test_sys.json_top, resampler_class=REVOResampler, ) @@ -211,13 +214,18 @@ def test_alanine_dipeptide_revo_procpool(tmp_path_factory): distance_metric=distance_metric, ) + # TODO: + # openmm_dashboard_section = wepy.OpenMMRunnerDashboardSection(runner_factory) dashboard_path = outputs_dir / "main.wepy_dash.org" dashboard_reporter = DashboardReporter(dashboard_path) + hdf5_path = outputs_dir / "main.wepy.h5" hdf5_reporter = WepyHDF5Reporter.from_components( file_path=hdf5_path, save_fields=DEFAULT_SAVE_FIELDS, + # only require these fields for the initial walkers + init_walker_save_fields=("positions", "box_vectors",), topology=ala_sys.json_top, resampler_class=REVOResampler, ) diff --git a/tests/unit/test_reporter/test_hdf5.py b/tests/unit/test_reporter/test_hdf5.py index 99cf5d6b..cc8f7f1f 100644 --- a/tests/unit/test_reporter/test_hdf5.py +++ b/tests/unit/test_reporter/test_hdf5.py @@ -474,7 +474,6 @@ def test__resolve_state_units(self): ) assert units_used == {"positions" : openmm.unit.nanometer} - def test_init(self, tmp_path_factory): test_sys = LennardJonesPair() @@ -501,14 +500,25 @@ def test_init(self, tmp_path_factory): # minimal tests, see _initialize_h5_run for more in depth tests with reporter.wepy_h5 as wepy_h5: + # should be defaults assert wepy_h5.h5["units/positions"][()].decode() == "nanometer" assert wepy_h5.h5["units/box_vectors"][()].decode() == "nanometer" assert wepy_h5.h5["units/box_volume"][()].decode() == "nanometer**3" assert wepy_h5.h5["units/time"][()].decode() == "picosecond" + assert "0" in wepy_h5.h5["runs"] assert "init_walkers" in wepy_h5.h5["runs/0"] assert len(wepy_h5.h5["runs/0/init_walkers"]) == 2 + assert "0" in wepy_h5.h5["runs/0/init_walkers"] + # all the fields in the state will be saved, since no save_fields given + assert "positions" in wepy_h5.h5["runs/0/init_walkers/0"] + assert "box_vectors" in wepy_h5.h5["runs/0/init_walkers/0"] + assert "box_volume" in wepy_h5.h5["runs/0/init_walkers/0"] + assert "time" in wepy_h5.h5["runs/0/init_walkers/0"] + + # compare to the different unit output later + nanometer_bvs = wepy_h5.h5["runs/0/init_walkers/0/box_vectors"][:] # if no units are given, derive them dynamically from # quantities @@ -558,6 +568,108 @@ def test_init(self, tmp_path_factory): # some of the defaults assert wepy_h5.h5["units/positions"][()].decode() == "nanometer" assert wepy_h5.h5["units/box_volume"][()].decode() == "nanometer**3" + + # compare the numbers from each to make sure they have the same magnitude + angstrom_bvs = wepy_h5.h5["runs/0/init_walkers/0/box_vectors"][:] + assert np.array_equal( + nanometer_bvs * 10, + angstrom_bvs, + ) + + # test the init_walker_save_fields behavior + d2 = tmp_path_factory.mktemp("2") + h5_path = d2 / "main.wepy.h5" + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + units=None, + **RESAMPLER_REPORTER_ARGS, + save_fields=("positions",), + init_walker_save_fields=None, + ) + + reporter.init(**LJ_OPENMM_SIM_COMPONENTS) + + with reporter.wepy_h5 as wepy_h5: + assert "0" in wepy_h5.h5["runs"] + assert "init_walkers" in wepy_h5.h5["runs/0"] + assert len(wepy_h5.h5["runs/0/init_walkers"]) == 2 + assert "0" in wepy_h5.h5["runs/0/init_walkers"] + assert "positions" in wepy_h5.h5["runs/0/init_walkers/0"] + + # the remainder of the fields that were in the state should not be saved + assert not "box_vectors" in wepy_h5.h5["runs/0/init_walkers/0"] + assert not "box_volume" in wepy_h5.h5["runs/0/init_walkers/0"] + assert not "time" in wepy_h5.h5["runs/0/init_walkers/0"] + + + ## Test the save fields family of arguments + d3 = tmp_path_factory.mktemp("3") + h5_path = d3 / "main.wepy.h5" + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + units=None, + **RESAMPLER_REPORTER_ARGS, + save_fields=("positions",), + init_walker_save_fields=("positions",), + ) + + reporter.init(**LJ_OPENMM_SIM_COMPONENTS) + + with reporter.wepy_h5 as wepy_h5: + assert "positions" in wepy_h5.h5["runs/0/init_walkers/0"] + assert not "box_vectors" in wepy_h5.h5["runs/0/init_walkers/0"] + assert not "box_volume" in wepy_h5.h5["runs/0/init_walkers/0"] + assert not "time" in wepy_h5.h5["runs/0/init_walkers/0"] + + # special cases for the init walkers, save all fields that + # were given to it + d4 = tmp_path_factory.mktemp("4") + h5_path = d4 / "main.wepy.h5" + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + units=None, + **RESAMPLER_REPORTER_ARGS, + save_fields=("positions",), + init_walker_save_fields=Ellipsis, + ) + + reporter.init(**LJ_OPENMM_SIM_COMPONENTS) + + with reporter.wepy_h5 as wepy_h5: + assert "positions" in wepy_h5.h5["runs/0/init_walkers/0"] + assert "box_vectors" in wepy_h5.h5["runs/0/init_walkers/0"] + assert "box_volume" in wepy_h5.h5["runs/0/init_walkers/0"] + assert "time" in wepy_h5.h5["runs/0/init_walkers/0"] + + # Match a different set of fields for init_walkers than the + # save_fields + d5 = tmp_path_factory.mktemp("5") + h5_path = d5 / "main.wepy.h5" + + reporter = WepyHDF5Reporter( + file_path=h5_path, + topology=test_sys.json_top, + units=None, + **RESAMPLER_REPORTER_ARGS, + save_fields=("positions", "box_vectors", "time",), + # only take positions and box_vectors + init_walker_save_fields=("positions", "box_vectors",), + ) + + reporter.init(**LJ_OPENMM_SIM_COMPONENTS) + + with reporter.wepy_h5 as wepy_h5: + assert "positions" in wepy_h5.h5["runs/0/init_walkers/0"] + assert "box_vectors" in wepy_h5.h5["runs/0/init_walkers/0"] + assert not "box_volume" in wepy_h5.h5["runs/0/init_walkers/0"] + assert not "time" in wepy_h5.h5["runs/0/init_walkers/0"] + def test_report(self, tmp_path_factory): # TODO: using the OpenMM Runner OpenMMState here because the From d1e7e37af005324a529077c20c4b0762fb9fc606 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 16 Jan 2026 22:19:26 -0500 Subject: [PATCH 124/143] fixes dimension wrapping mismatch There were too many dimensions on the REVO resampling records and not enough for the NoResampler. Straighten that out. --- src/wepy/hdf5.py | 10 ++++-- src/wepy/resampling/decisions/clone_merge.py | 1 + src/wepy/resampling/resamplers/noresampler.py | 32 ++++++++++++++----- src/wepy/resampling/resamplers/revo.py | 10 +++--- tests/unit/test_reporter/test_hdf5.py | 16 +++++----- .../test_resamplers/test_noresampler.py | 13 ++++---- 6 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/wepy/hdf5.py b/src/wepy/hdf5.py index cb8d70ee..997bb1fd 100644 --- a/src/wepy/hdf5.py +++ b/src/wepy/hdf5.py @@ -2081,10 +2081,9 @@ def _extend_run_record_data_field( records_grp = self.h5["{}/{}/{}".format(RUNS, run_idx, run_record_key)] field = records_grp[field_name] - # make sure this is a feature vector assert ( len(field_data.shape) > 1 - ), "field_data must be a feature vector with the same number of dimensions as the number" + ), f"field_data (record={run_record_key}, name={field_name}, shape={field_data.shape}) must be a feature vector with the same number of dimensions as the number" # of datase new frames n_new_frames = field_data.shape[0] @@ -4904,7 +4903,12 @@ def extend_cycle_run_group_records( for record in fields_data: for field_name, field_data in record.items(): self._extend_run_record_data_field( - run_idx, run_record_key, field_name, np.array([field_data]) + run_idx, + run_record_key, + field_name, + # wrap in an extra dimension to keep them as + # feature vectors + np.array([field_data]), ) ### Analysis Routines diff --git a/src/wepy/resampling/decisions/clone_merge.py b/src/wepy/resampling/decisions/clone_merge.py index 6619be93..06f7c1c0 100644 --- a/src/wepy/resampling/decisions/clone_merge.py +++ b/src/wepy/resampling/decisions/clone_merge.py @@ -51,6 +51,7 @@ class CloneMergeDecisionRecordDict(TypedDict): @attrs.define class CloneMergeDecisionRecord(BaseDecisionRecord): + # TODO: get types correct decision_id: int = attrs.field() target_idxs: tuple[int, ...] = attrs.field() diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py index 36b50fcd..6a1c6200 100644 --- a/src/wepy/resampling/resamplers/noresampler.py +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -1,9 +1,12 @@ # Standard Library -from typing import TypedDict +from typing import TypedDict, Annotated +import numpy as np +from numpy.typing import NDArray import attrs # First Party Library +from wepy.typing import Shape from wepy.resampling.decisions.no_decision import ( NoDecision, NothingDecisionEnum, @@ -11,13 +14,24 @@ from wepy.resampling.resamplers.resampler import Resampler, ResamplerABC from wepy.walker import Walker from wepy.util.attrs import AttrsMappingMixin +from wepy.resampling.decisions.no_decision import NoDecisionRecord @attrs.define class NoResamplerResamplingRecord(AttrsMappingMixin): - decision_id: int - target_idxs: tuple[int, ...] = attrs.field( - converter=(lambda v: tuple(v)) - ) + decision_id: Annotated[ + NDArray[np.int64], + Shape((1,)), + ] + # NOTE,UGLY: It isn't strictly necessary to have multiple target + # indices for this type of resampling record to have all the + # information, but all of the downstream infrastucture for + # interpreting them relies on there being multiple indices so we + # don't want to break this for this record that is only used for + # troubleshooting really. + target_idxs: Annotated[ + NDArray[np.int64], + Shape((1,1,)), + ] @attrs.define @@ -42,7 +56,7 @@ def resample( walkers: list[Walker], ) -> tuple[ list[Walker], - list[NoResamplerResamplingRecord], + list[NoDecision], list[NoResamplerResamplerRecord], ]: @@ -53,9 +67,11 @@ def resample( _resampling_data = [] for walker_idx in range(len(walkers)): + # UGLY: we need to wrap the field data into the shape walker_record = NoResamplerResamplingRecord( - decision_id=NothingDecisionEnum.NOTHING.value, - target_idxs=[walker_idx], + decision_id=np.array([NothingDecisionEnum.NOTHING.value]), + # NOTE: two dimensions to match the target_idxs shape + target_idxs=np.array([[walker_idx]]), ) _resampling_data.append(walker_record) diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 7a21dbf9..72364f58 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -867,12 +867,12 @@ def resample( resampling_record = CloneMergeResamplingRecord( # The decision record fields are simple, so we wrap # them here as well - decision_id=np.array([[decision_record.decision_id]]), - target_idxs=np.array([[ + decision_id=np.array([decision_record.decision_id]), + target_idxs=np.array([ np.array(decision_record.target_idxs), - ]]), - step_idx=np.array([[0]]), - walker_idx=np.array([[walker_idx]]) + ]), + step_idx=np.array([0]), + walker_idx=np.array([walker_idx]) ) resampling_records.append(resampling_record) diff --git a/tests/unit/test_reporter/test_hdf5.py b/tests/unit/test_reporter/test_hdf5.py index cc8f7f1f..c03e3c6d 100644 --- a/tests/unit/test_reporter/test_hdf5.py +++ b/tests/unit/test_reporter/test_hdf5.py @@ -741,16 +741,16 @@ def test_report(self, tmp_path_factory): ], "resampling_data" : [ { - "decision_id" : np.array([[0]]), - "target_idxs" : np.array([[[1]]]), - "step_idx" : np.array([[0]]), - "walker_idx" : np.array([[0]]), + "decision_id" : np.array([0]), + "target_idxs" : np.array([[1]]), + "step_idx" : np.array([0]), + "walker_idx" : np.array([0]), }, { - "decision_id" : np.array([[0]]), - "target_idxs" : np.array([[[0]]]), - "step_idx" : np.array([[0]]), - "walker_idx" : np.array([[1]]), + "decision_id" : np.array([0]), + "target_idxs" : np.array([[0]]), + "step_idx" : np.array([0]), + "walker_idx" : np.array([1]), }, ], }, diff --git a/tests/unit/test_resampling/test_resamplers/test_noresampler.py b/tests/unit/test_resampling/test_resamplers/test_noresampler.py index b35cee78..35dcc406 100644 --- a/tests/unit/test_resampling/test_resamplers/test_noresampler.py +++ b/tests/unit/test_resampling/test_resamplers/test_noresampler.py @@ -1,3 +1,4 @@ +import numpy as np # First Party Library from wepy.resampling.decisions.no_decision import NothingDecisionEnum from wepy.resampling.resamplers.noresampler import NoResampler, NoResamplerFactory, NoResamplerResamplingRecord, NoResamplerResamplerRecord @@ -34,16 +35,16 @@ def test_resample(self): walkers, [ NoResamplerResamplingRecord( - decision_id=NothingDecisionEnum.NOTHING.value, - target_idxs=[0], + decision_id=np.array([NothingDecisionEnum.NOTHING.value]), + target_idxs=np.array([[0]]), ), NoResamplerResamplingRecord( - decision_id=NothingDecisionEnum.NOTHING.value, - target_idxs=[1], + decision_id=np.array([NothingDecisionEnum.NOTHING.value]), + target_idxs=np.array([[1]]), ), NoResamplerResamplingRecord( - decision_id=NothingDecisionEnum.NOTHING.value, - target_idxs=[2], + decision_id=np.array([NothingDecisionEnum.NOTHING.value]), + target_idxs=np.array([[2]]), ), ], [NoResamplerResamplerRecord()], From ccafa875c360109de04e002c0752c0c59d65bd72 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 16 Jan 2026 22:21:36 -0500 Subject: [PATCH 125/143] minor fix to realistic_test --- tests/integration/test_openmm/test_realistic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index f3314e87..c60d714b 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -123,9 +123,9 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): file_path=hdf5_path, save_fields=DEFAULT_SAVE_FIELDS, # only require these fields for the initial walkers - init_walker_save_fields=("positions", "box_vectors",), + init_walker_save_fields=("positions",), topology=test_sys.json_top, - resampler_class=REVOResampler, + resampler_class=REVOResamplerFactory.type(), ) reporters = [dashboard_reporter, hdf5_reporter] From 05c15f636d9197026c4f6450c65dd6c44de650ae Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sat, 17 Jan 2026 13:33:32 -0500 Subject: [PATCH 126/143] fix minor typos --- src/wepy/reporter/hdf5.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wepy/reporter/hdf5.py b/src/wepy/reporter/hdf5.py index 01783da1..0513701d 100644 --- a/src/wepy/reporter/hdf5.py +++ b/src/wepy/reporter/hdf5.py @@ -335,7 +335,7 @@ def __init__( if alt_reps is not None: self.alt_reps_idxs = { key: np.array(idxs) - for key, (idxs, frequence) + for key, (idxs, _) in alt_reps.items() } @@ -425,7 +425,7 @@ def from_components( n_dims: int = 3, main_rep_idxs: Idxs | None = None, all_atoms_rep_freq: int | None = None, - alt_reps: dict[str, tuple[Idxs, int]]=None, + alt_reps: dict[str, tuple[Idxs, int]] = None, ) -> Self: """Construct reporter from simulation components. From 2c2486518ecc808d1ac68d3c3fc987d916261cb6 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sat, 17 Jan 2026 13:33:45 -0500 Subject: [PATCH 127/143] adds some more complex HDF5 reporting to realistic_example Use sparse fields, main reps, all atom save freqs, and alt reps in examples to exercise these parts of the interface. --- .../integration/test_openmm/test_realistic.py | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index c60d714b..b2345996 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -16,6 +16,7 @@ import pytest # First Party Library +import mdtraj import wepy from wepy.reporter.dashboard import DashboardReporter from wepy.resampling.resamplers.revo import REVOResamplerFactory, REVOResampler @@ -121,11 +122,14 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): hdf5_path = outputs_dir / "main.wepy.h5" hdf5_reporter = WepyHDF5Reporter.from_components( file_path=hdf5_path, + topology=test_sys.json_top, + resampler_class=REVOResamplerFactory.type(), save_fields=DEFAULT_SAVE_FIELDS, # only require these fields for the initial walkers init_walker_save_fields=("positions",), - topology=test_sys.json_top, - resampler_class=REVOResamplerFactory.type(), + sparse_fields={ + "velocities" : 2, + }, ) reporters = [dashboard_reporter, hdf5_reporter] @@ -219,15 +223,26 @@ def test_alanine_dipeptide_revo_procpool(tmp_path_factory): dashboard_path = outputs_dir / "main.wepy_dash.org" dashboard_reporter = DashboardReporter(dashboard_path) + mdj_top = mdtraj.Topology.from_openmm(ala_sys.topology) + protein_idxs = mdj_top.select("protein") + water_idxs = mdj_top.select("water") hdf5_path = outputs_dir / "main.wepy.h5" hdf5_reporter = WepyHDF5Reporter.from_components( file_path=hdf5_path, - save_fields=DEFAULT_SAVE_FIELDS, - # only require these fields for the initial walkers - init_walker_save_fields=("positions", "box_vectors",), topology=ala_sys.json_top, resampler_class=REVOResampler, + save_fields=DEFAULT_SAVE_FIELDS + ("velocities",), + # only require these fields for the initial walkers + init_walker_save_fields=("positions", "box_vectors",), + sparse_fields={ + "velocities" : 2, + }, + main_rep_idxs=protein_idxs, + all_atoms_rep_freq=2, + alt_reps={ + "water" : (water_idxs, 2), + } ) reporters = [dashboard_reporter, hdf5_reporter] From 8eec3e491c0f98c6d11611619500944457156289 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sat, 17 Jan 2026 13:34:32 -0500 Subject: [PATCH 128/143] minimal addition of WExploreResamplerFactory --- src/wepy/__init__.py | 3 +- src/wepy/resampling/resamplers/wexplore.py | 72 ++++++++++++++++++---- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/wepy/__init__.py b/src/wepy/__init__.py index 76007975..228c0060 100644 --- a/src/wepy/__init__.py +++ b/src/wepy/__init__.py @@ -34,7 +34,7 @@ from .resampling.resamplers.noresampler import NoResampler, NoResamplerFactory from .resampling.resamplers.resampler import Resampler from .resampling.resamplers.revo import REVOResampler, REVOResamplerFactory -from .resampling.resamplers.wexplore import WExploreResampler +from .resampling.resamplers.wexplore import WExploreResampler, WExploreResamplerFactory from .resampling.distances.base import Distance from .resampling.distances.mock import MockDistance from .resampling.distances.simple import XYDistanceState, XYEuclideanDistance @@ -216,4 +216,5 @@ "NoRunnerFactory", "SerialMapperFactory", "OPENMM_DEFAULT_UNITS", + "WExploreResamplerFactory", ] diff --git a/src/wepy/resampling/resamplers/wexplore.py b/src/wepy/resampling/resamplers/wexplore.py index b6723990..193b8e97 100644 --- a/src/wepy/resampling/resamplers/wexplore.py +++ b/src/wepy/resampling/resamplers/wexplore.py @@ -1,9 +1,7 @@ # Standard Library import itertools as it import logging - -logger = logging.getLogger(__name__) -# Standard Library +from typing import Generic, TypeVar import math import random as rand from collections import defaultdict @@ -12,10 +10,18 @@ # Third Party Library import networkx as nx import numpy as np +import attrs # First Party Library from wepy.resampling.resamplers.clone_merge import CloneMergeResampler from wepy.resampling.resamplers.resampler import ResamplerError +from wepy.resampling.distances.base import Distance +from wepy.walker import WalkerState + +logger = logging.getLogger(__name__) + +DistanceMetric_ = TypeVar("DistanceMetric_", bound=Distance) +WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) class RegionTreeError(Exception): @@ -2077,7 +2083,10 @@ def balance_tree(self, delta_walkers=0): return merge_groups, walkers_num_clones -class WExploreResampler(CloneMergeResampler): +class WExploreResampler( + CloneMergeResampler, + Generic[DistanceMetric_, WalkerState_], +): """Resampler implementing the WExplore algorithm. See the paper for a full description of the algorithm, but @@ -2288,13 +2297,13 @@ class WExploreResampler(CloneMergeResampler): def __init__( self, - seed=None, - distance=None, - max_region_sizes=None, - init_state=None, - pmin=1e-12, - pmax=0.1, - max_n_regions=(10, 10, 10, 10), + distance: Distance, + max_region_sizes: tuple[float, ...], + init_state: WalkerState_, + pmin: float = 1e-12, + pmax: float = 0.1, + max_n_regions: tuple[int, ...] = (10, 10, 10, 10), + seed: int | None = None, **kwargs, ): """Constructor for the WExploreResampler. @@ -2648,3 +2657,44 @@ def resample(self, walkers): ) return resampled_walkers, resampling_data, resampler_data + +@attrs.define +class WExploreResamplerFactory(Generic[DistanceMetric_, WalkerState_]): + + distance_metric: DistanceMetric_ + init_state: WalkerState_ + max_region_sizes: tuple[float, ...] + max_n_regions: tuple[int, ...] + pmin: float = 1e-12 + pmax: float = 0.1 + seed: int | None = None + + def __attrs_post_init__(self) -> None: + + if len(self.max_region_sizes) != len(self.max_n_regions): + + raise ValueError( + "The number of levels must be the same. Received: " + f"max_region_sizes={len(self.max_region_sizes)} " + f"max_n_regions={len(self.max_n_regions)}" + ) + + @classmethod + def type(cls) -> type[WExploreResampler]: + return WExploreResampler + + def __call__( + self, + num_cores: int, + ) -> WExploreResampler: + + return WExploreResampler( + distance=self.distance_metric, + init_state=self.init_state, + max_n_regions=self.max_n_regions, + max_region_sizes=self.max_region_sizes, + pmin=self.pmin, + pmax=self.pmax, + seed=self.seed, + ) + From 2c1fd0deb3c031b71b441c3af08e86f53f2585da Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sat, 17 Jan 2026 13:42:22 -0500 Subject: [PATCH 129/143] make the num_cores argument to ResamplerFactory optional Write down an idea for a general resource allocation system between components that has a start in the ResamplerFactory --- src/wepy/resampling/resamplers/revo.py | 2 +- src/wepy/resampling/resamplers/wexplore.py | 2 +- src/wepy/sim_manager.py | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 72364f58..dbfc00ec 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -907,7 +907,7 @@ def type(cls) -> type[REVOResampler]: def __call__( self, - num_cores: int, + num_cores: int | None = None, ) -> REVOResampler: return REVOResampler( diff --git a/src/wepy/resampling/resamplers/wexplore.py b/src/wepy/resampling/resamplers/wexplore.py index 193b8e97..baae9be7 100644 --- a/src/wepy/resampling/resamplers/wexplore.py +++ b/src/wepy/resampling/resamplers/wexplore.py @@ -2685,7 +2685,7 @@ def type(cls) -> type[WExploreResampler]: def __call__( self, - num_cores: int, + num_cores: int | None = None, ) -> WExploreResampler: return WExploreResampler( diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index 943db07a..8933a96e 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -492,6 +492,14 @@ def __init__( ## Monitor self.monitor = sim_monitor + # OPT,IDEA: figure out a general resource allocation scheme + # that lets you customize more than just the number of cores + # and lets you configure per component how many resources they + # can have during the simulation. Currently we just tell each + # component what we have. This isn't too bad as nothing else + # is CPU bound and they run sequentially so they wouldn't be + # competing for them at runtime. + # figure out how many cores we have at our disposal if not # already given if num_cores is None: From 06635bf546278f91d9dcd0a7c5aee3022e021690 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 21 Jan 2026 14:22:53 -0500 Subject: [PATCH 130/143] configure dvc in repo --- .dvc/.gitignore | 3 +++ .dvc/config | 4 ++++ .dvcignore | 3 +++ 3 files changed, 10 insertions(+) create mode 100644 .dvc/.gitignore create mode 100644 .dvc/config create mode 100644 .dvcignore diff --git a/.dvc/.gitignore b/.dvc/.gitignore new file mode 100644 index 00000000..528f30c7 --- /dev/null +++ b/.dvc/.gitignore @@ -0,0 +1,3 @@ +/config.local +/tmp +/cache diff --git a/.dvc/config b/.dvc/config new file mode 100644 index 00000000..201f2de0 --- /dev/null +++ b/.dvc/config @@ -0,0 +1,4 @@ +[core] + remote = idp-dvc-gs +['remote "idp-dvc-gs"'] + url = gs://examol-idp-prod-dvc/dvcstore-wepy2 diff --git a/.dvcignore b/.dvcignore new file mode 100644 index 00000000..51973055 --- /dev/null +++ b/.dvcignore @@ -0,0 +1,3 @@ +# Add patterns of files dvc should ignore, which could improve +# the performance. Learn more at +# https://dvc.org/doc/user-guide/dvcignore From 620bb54dafeda4d12e42b9c15329edd9b3a471bd Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 21 Jan 2026 14:42:15 -0500 Subject: [PATCH 131/143] fix realistic integration test --- .../integration/test_openmm/test_realistic.py | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index b2345996..26b4f20a 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -18,20 +18,10 @@ # First Party Library import mdtraj import wepy -from wepy.reporter.dashboard import DashboardReporter -from wepy.resampling.resamplers.revo import REVOResamplerFactory, REVOResampler -from wepy.runners.openmm import OpenMMRunnerFactory, OpenMMState from wepy.runners.openmm.runner import ( _DEFAULT_HEARTBEAT_INTERVAL, _DEFAULT_STATE_TIME_INTERVAL, ) -from wepy.sim_manager import Manager -from wepy.reporter.dashboard import DashboardReporter -from wepy.reporter.hdf5 import WepyHDF5Reporter - -# TODO: use the high-level API imports -from wepy.walker import Walker -from wepy.work_mapper.openmm import OpenMMProcPoolWorkMapperFactory from wepy_tools.systems.alanine_dipeptide import ( AlanineDipeptideExplicitSystem, AlanineDipeptideRamachandranDistance, @@ -72,7 +62,7 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): integrator = openmm.LangevinIntegrator(TEMPERATURE, 0.1, STEP_SIZE) - runner_factory = OpenMMRunnerFactory( + runner_factory = wepy.OpenMMRunnerFactory( system=test_sys.system, topology=test_sys.topology, integrator=integrator, @@ -81,7 +71,7 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): # num_walkers = 48 num_walkers = 4 - init_state = OpenMMState.from_dwim( + init_state = wepy.OpenMMState.from_dwim( positions=test_sys.positions, ) @@ -91,7 +81,7 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): init_walker_weight = 1 / num_walkers init_walkers = [ - Walker( + wepy.Walker( state=walker_state, weight=init_walker_weight, ) @@ -110,21 +100,21 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): distance_metric = PairDistance() - resampler_factory = REVOResamplerFactory( + resampler_factory = wepy.REVOResamplerFactory( merge_dist=4, char_dist=0.1, distance_metric=distance_metric, ) dashboard_path = outputs_dir / "main.wepy_dash.org" - dashboard_reporter = DashboardReporter(dashboard_path) + dashboard_reporter = wepy.DashboardReporter(dashboard_path) hdf5_path = outputs_dir / "main.wepy.h5" - hdf5_reporter = WepyHDF5Reporter.from_components( + hdf5_reporter = wepy.WepyHDF5Reporter.from_components( file_path=hdf5_path, topology=test_sys.json_top, - resampler_class=REVOResamplerFactory.type(), - save_fields=DEFAULT_SAVE_FIELDS, + resampler_class=wepy.REVOResamplerFactory.type(), + save_fields=DEFAULT_SAVE_FIELDS + ("velocities",), # only require these fields for the initial walkers init_walker_save_fields=("positions",), sparse_fields={ @@ -134,12 +124,12 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): reporters = [dashboard_reporter, hdf5_reporter] - sim_manager = Manager( + sim_manager = wepy.Manager( init_walkers=init_walkers, runner_factory=runner_factory, resampler_factory=resampler_factory, # resampler_factory=NoResampler, - work_mapper_factory=OpenMMProcPoolWorkMapperFactory( + work_mapper_factory=wepy.OpenMMProcPoolWorkMapperFactory( platform="CPU", num_procs=num_workers, global_platform_properties={"Threads": str(cores_per_worker)}, @@ -179,7 +169,7 @@ def test_alanine_dipeptide_revo_procpool(tmp_path_factory): ) ala_sys.system.addForce(barostat) - runner_factory = OpenMMRunnerFactory( + runner_factory = wepy.OpenMMRunnerFactory( system=ala_sys.system, topology=ala_sys.topology, integrator=integrator, @@ -193,7 +183,7 @@ def test_alanine_dipeptide_revo_procpool(tmp_path_factory): init_walker_weight = 1 / num_walkers init_walkers = [ - Walker( + wepy.Walker( state=walker_state, weight=init_walker_weight, ) @@ -212,7 +202,7 @@ def test_alanine_dipeptide_revo_procpool(tmp_path_factory): distance_metric = AlanineDipeptideRamachandranDistance(ala_sys.json_top) - resampler_factory = REVOResamplerFactory( + resampler_factory = wepy.REVOResamplerFactory( merge_dist=4, char_dist=0.1, distance_metric=distance_metric, @@ -221,17 +211,17 @@ def test_alanine_dipeptide_revo_procpool(tmp_path_factory): # TODO: # openmm_dashboard_section = wepy.OpenMMRunnerDashboardSection(runner_factory) dashboard_path = outputs_dir / "main.wepy_dash.org" - dashboard_reporter = DashboardReporter(dashboard_path) + dashboard_reporter = wepy.DashboardReporter(dashboard_path) mdj_top = mdtraj.Topology.from_openmm(ala_sys.topology) protein_idxs = mdj_top.select("protein") water_idxs = mdj_top.select("water") hdf5_path = outputs_dir / "main.wepy.h5" - hdf5_reporter = WepyHDF5Reporter.from_components( + hdf5_reporter = wepy.WepyHDF5Reporter.from_components( file_path=hdf5_path, topology=ala_sys.json_top, - resampler_class=REVOResampler, + resampler_class=wepy.REVOResampler, save_fields=DEFAULT_SAVE_FIELDS + ("velocities",), # only require these fields for the initial walkers init_walker_save_fields=("positions", "box_vectors",), @@ -247,12 +237,12 @@ def test_alanine_dipeptide_revo_procpool(tmp_path_factory): reporters = [dashboard_reporter, hdf5_reporter] - sim_manager = Manager( + sim_manager = wepy.Manager( init_walkers=init_walkers, runner_factory=runner_factory, # resampler=NoResampler(), resampler_factory=resampler_factory, - work_mapper_factory=OpenMMProcPoolWorkMapperFactory( + work_mapper_factory=wepy.OpenMMProcPoolWorkMapperFactory( platform="CPU", num_procs=num_workers, # NOTE,TOREV: in practice not limiting this is just faster From fc1825321afae179f1705c900419e2b8161e4ad5 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 21 Jan 2026 14:53:57 -0500 Subject: [PATCH 132/143] add test data synthesis and populate with hdf5 file --- pyproject.toml | 2 + tests/unit/_test_data/.gitignore | 1 + .../alanine_dipeptide_revo.wepy.hdf5.dvc | 5 + tests/unit/_test_data/synthesize.py | 134 ++++++++++++++++++ uv.lock | 73 ++++++++++ 5 files changed, 215 insertions(+) create mode 100644 tests/unit/_test_data/.gitignore create mode 100644 tests/unit/_test_data/alanine_dipeptide_revo.wepy.hdf5.dvc create mode 100644 tests/unit/_test_data/synthesize.py diff --git a/pyproject.toml b/pyproject.toml index 431da49b..6f758eb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,8 @@ build-backend = "uv_build" [dependency-groups] dev = [ + # for ad hoc executables like for data generation + "cyclopts", # testing "pytest", "coverage", diff --git a/tests/unit/_test_data/.gitignore b/tests/unit/_test_data/.gitignore new file mode 100644 index 00000000..9a9d0d27 --- /dev/null +++ b/tests/unit/_test_data/.gitignore @@ -0,0 +1 @@ +/alanine_dipeptide_revo.wepy.hdf5 diff --git a/tests/unit/_test_data/alanine_dipeptide_revo.wepy.hdf5.dvc b/tests/unit/_test_data/alanine_dipeptide_revo.wepy.hdf5.dvc new file mode 100644 index 00000000..1f46ee16 --- /dev/null +++ b/tests/unit/_test_data/alanine_dipeptide_revo.wepy.hdf5.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 09ee4a41e14774d554f487c5f6396121 + size: 6626397 + hash: md5 + path: alanine_dipeptide_revo.wepy.hdf5 diff --git a/tests/unit/_test_data/synthesize.py b/tests/unit/_test_data/synthesize.py new file mode 100644 index 00000000..562390c9 --- /dev/null +++ b/tests/unit/_test_data/synthesize.py @@ -0,0 +1,134 @@ +from pathlib import Path + +import cyclopts +import logging +import copy + +# Third Party Library +import openmm +import psutil + +# First Party Library +import mdtraj +import wepy +from wepy_tools.systems.alanine_dipeptide import ( + AlanineDipeptideExplicitSystem, + AlanineDipeptideRamachandranDistance, +) + +logging.basicConfig(level=logging.INFO) + +_logger = logging.getLogger("tests") + + +DEFAULT_SAVE_FIELDS = ( + "positions", + "box_vectors", + "box_volume", + "potential_energy", + "kinetic_energy", +) + + +app = cyclopts.App() + +@app.command +def realistic_hdf5_dialanine_explicit(out_path: Path): + + STEP_SIZE = 2.0 * openmm.unit.femtosecond + TEMPERATURE = 300.0 * openmm.unit.kelvin + + ala_sys = AlanineDipeptideExplicitSystem() + + integrator = openmm.LangevinIntegrator(TEMPERATURE, 0.1, STEP_SIZE) + + # add the pseudo forces like barostat + barostat = openmm.MonteCarloBarostat( + 1.0 * openmm.unit.atmosphere, + TEMPERATURE, + ) + ala_sys.system.addForce(barostat) + + runner_factory = wepy.OpenMMRunnerFactory( + system=ala_sys.system, + topology=ala_sys.topology, + integrator=integrator, + ) + + num_walkers = 10 + + # TODO: remove the need to deepcopy and have the components make + # their own copies if necessary + walker_states = [copy.deepcopy(ala_sys.state) for _ in range(num_walkers)] + + init_walker_weight = 1 / num_walkers + init_walkers = [ + wepy.Walker( + state=walker_state, + weight=init_walker_weight, + ) + for walker_state in walker_states + ] + + # number of walkers if less then total cores, otherwise the total + # number of cores + num_cores = len(psutil.Process().cpu_affinity()) + if num_cores < num_walkers: + num_workers = num_cores + cores_per_worker = 1 + else: + num_workers = num_walkers + cores_per_worker = num_workers // num_walkers + + distance_metric = AlanineDipeptideRamachandranDistance(ala_sys.json_top) + + resampler_factory = wepy.REVOResamplerFactory( + merge_dist=4, + char_dist=0.1, + distance_metric=distance_metric, + ) + + mdj_top = mdtraj.Topology.from_openmm(ala_sys.topology) + protein_idxs = mdj_top.select("protein") + water_idxs = mdj_top.select("water") + + hdf5_reporter = wepy.WepyHDF5Reporter.from_components( + file_path=out_path, + topology=ala_sys.json_top, + resampler_class=wepy.REVOResampler, + save_fields=DEFAULT_SAVE_FIELDS + ("velocities",), + # only require these fields for the initial walkers + init_walker_save_fields=("positions", "box_vectors",), + sparse_fields={ + "velocities" : 2, + }, + main_rep_idxs=protein_idxs, + all_atoms_rep_freq=2, + alt_reps={ + "water" : (water_idxs, 2), + } + ) + + sim_manager = wepy.Manager( + init_walkers=init_walkers, + runner_factory=runner_factory, + # resampler=NoResampler(), + resampler_factory=resampler_factory, + work_mapper_factory=wepy.OpenMMProcPoolWorkMapperFactory( + platform="CPU", + num_procs=num_workers, + global_platform_properties={"Threads": str(cores_per_worker)}, + ), + reporters=[hdf5_reporter], + ) + + cycle_time = 10.0 * openmm.unit.picosecond + cycle_steps = round(cycle_time / STEP_SIZE) + + new_walkers, sim_components = sim_manager.run_simulation( + n_cycles=3, + segment_lengths=cycle_steps, + ) + +if __name__ == "__main__": + app() diff --git a/uv.lock b/uv.lock index af193e4d..8dc64da9 100644 --- a/uv.lock +++ b/uv.lock @@ -559,6 +559,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "cyclopts" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7b/663f3285c1ac0e5d0854bd9db2c87caa6fa3d1a063185e3394a6cdca9151/cyclopts-4.5.0.tar.gz", hash = "sha256:717ac4235548b58d500baf7e688aa4d024caf0ee68f61a012ffd5e29db3099f9", size = 161980, upload-time = "2026-01-16T02:07:16.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/2e00fececc34a99ae3a5d5702a5dd29c5371e4ed016647301a2b9bcc1976/cyclopts-4.5.0-py3-none-any.whl", hash = "sha256:305b9aa90a9cd0916f0a450b43e50ad5df9c252680731a0719edfb9b20381bf5", size = 199772, upload-time = "2026-01-16T02:07:14.707Z" }, +] + [[package]] name = "debugpy" version = "1.8.17" @@ -602,6 +617,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + [[package]] name = "docutils" version = "0.21.2" @@ -1427,6 +1451,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1604,6 +1640,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/a5/82af73a397434488042fa52ad8922028e1221d06ec31535ed13fff63cb57/mdtraj-1.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4d682faf2dd2eaf1d9cbba53f8e04504e80f0710580d6cd95f05286bdbeb755", size = 7914822, upload-time = "2025-10-22T20:21:00.39Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mistune" version = "3.1.4" @@ -2625,6 +2670,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, ] +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + [[package]] name = "rpds-py" version = "0.29.0" @@ -3249,6 +3320,7 @@ prometheus = [ dev = [ { name = "black" }, { name = "coverage" }, + { name = "cyclopts" }, { name = "interrogate" }, { name = "ipython" }, { name = "isort" }, @@ -3304,6 +3376,7 @@ provides-extras = ["md", "prometheus", "graphics"] dev = [ { name = "black" }, { name = "coverage" }, + { name = "cyclopts" }, { name = "interrogate" }, { name = "ipython" }, { name = "isort" }, From 3b18796762b2aab5b5e747c4fbe060b845a25dc0 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Wed, 21 Jan 2026 15:03:11 -0500 Subject: [PATCH 133/143] add fixture for loading the hdf5 data in tests move the data folder --- tests/{unit/_test_data => data}/.gitignore | 0 .../alanine_dipeptide_revo.wepy.hdf5.dvc | 0 tests/{unit/_test_data => data}/synthesize.py | 0 tests/unit/conftest.py | 46 +++++++++++++++++++ tests/unit/test_analysis/test_contig_tree.py | 22 +++++++++ 5 files changed, 68 insertions(+) rename tests/{unit/_test_data => data}/.gitignore (100%) rename tests/{unit/_test_data => data}/alanine_dipeptide_revo.wepy.hdf5.dvc (100%) rename tests/{unit/_test_data => data}/synthesize.py (100%) create mode 100644 tests/unit/conftest.py create mode 100644 tests/unit/test_analysis/test_contig_tree.py diff --git a/tests/unit/_test_data/.gitignore b/tests/data/.gitignore similarity index 100% rename from tests/unit/_test_data/.gitignore rename to tests/data/.gitignore diff --git a/tests/unit/_test_data/alanine_dipeptide_revo.wepy.hdf5.dvc b/tests/data/alanine_dipeptide_revo.wepy.hdf5.dvc similarity index 100% rename from tests/unit/_test_data/alanine_dipeptide_revo.wepy.hdf5.dvc rename to tests/data/alanine_dipeptide_revo.wepy.hdf5.dvc diff --git a/tests/unit/_test_data/synthesize.py b/tests/data/synthesize.py similarity index 100% rename from tests/unit/_test_data/synthesize.py rename to tests/data/synthesize.py diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 00000000..938e998d --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,46 @@ +import shutil +import subprocess +import sys + +from pathlib import Path + +import pytest + + + +def reflink_or_copy(src: Path, dst: Path) -> None: + """ + Create a copy-on-write reflink if supported. + Fall back to a full copy otherwise. + """ + try: + if sys.platform.startswith("linux"): + subprocess.run( + ["cp", "--reflink=auto", src, dst], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + elif sys.platform == "darwin": + subprocess.run( + ["cp", "-c", src, dst], # APFS clone + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + else: + raise RuntimeError("No reflink support") + except Exception: + shutil.copy2(src, dst) + +DATA_DIR = Path(__file__).parent.parent / "data" + + +@pytest.fixture(scope="function") +def alanine_dipeptide_revo_wepy_hdf5(tmp_path: Path) -> Path: + + src = DATA_DIR / "alanine_dipeptide_revo.wepy.hdf5" + dst = tmp_path / "data.wepy.hdf5" + + reflink_or_copy(src, dst) + return dst diff --git a/tests/unit/test_analysis/test_contig_tree.py b/tests/unit/test_analysis/test_contig_tree.py new file mode 100644 index 00000000..1af634ae --- /dev/null +++ b/tests/unit/test_analysis/test_contig_tree.py @@ -0,0 +1,22 @@ +from pathlib import Path +import wepy +from wepy.analysis.contig_tree import ( + BaseContigTree, + ContigTree, + Contig, +) + + +class Test_BaseContigTree: + + def test___init__(self, alanine_dipeptide_revo_wepy_hdf5: Path): + + wepy_h5 = wepy.WepyHDF5(alanine_dipeptide_revo_wepy_hdf5, mode='r') + wepy_h5.open() + wepy_h5.close() + +class Test_ContigTree: + pass + +class Test_Contig: + pass From 4cf64c01e48f89e0a1e6cfd150d0b05485b70958 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 23 Jan 2026 16:36:46 -0500 Subject: [PATCH 134/143] wip! add a lot of tests and fixes to analysis and hdf5 modules - Reworked the records passed around for the resampling and decision records used in the parent tables etc. - Relaxed the requirements in the incoming records to be wrapped in so many layers. HDF5 now does this for you to maintain the data compatibility. THe outgoing data works the same. --- src/wepy/analysis/parents.py | 103 +- src/wepy/hdf5.py | 760 ++++---- src/wepy/reporter/hdf5.py | 4 +- src/wepy/resampling/decisions/decision.py | 61 +- src/wepy/resampling/decisions/no_decision.py | 45 +- src/wepy/resampling/resamplers/clone_merge.py | 4 +- src/wepy/resampling/resamplers/noresampler.py | 47 +- src/wepy/resampling/resamplers/revo.py | 12 +- src/wepy/storage/protocol.py | 62 +- tests/unit/test_analysis/test_contig_tree.py | 9 +- tests/unit/test_analysis/test_parents.py | 312 ++++ tests/unit/test_hdf5.py | 1523 +++++++++++++++-- .../test_decisions/test_decision.py | 30 +- .../test_decisions/test_no_decision.py | 30 +- .../test_resamplers/test_noresampler.py | 26 +- 15 files changed, 2432 insertions(+), 596 deletions(-) create mode 100644 tests/unit/test_analysis/test_parents.py diff --git a/src/wepy/analysis/parents.py b/src/wepy/analysis/parents.py index bddf8259..883afeed 100644 --- a/src/wepy/analysis/parents.py +++ b/src/wepy/analysis/parents.py @@ -58,11 +58,19 @@ # Standard Library import itertools as it -from copy import copy +import copy # Third Party Library import networkx as nx +from wepy.resampling.decisions.decision import BaseDecisionABC +from wepy.boundary_conditions.boundary import BoundaryConditions + +from wepy.storage.protocol import ( + ResamplingRecordUnstruct, + DecisionRecordUnstruct, +) + DISCONTINUITY_VALUE = -1 """Special value used to determine if a parent-child relationship has discontinuous dynamical continuity. Functions in this module uses this @@ -70,8 +78,18 @@ """ +DecisionPanel = list[list[list[DecisionRecordUnstruct]]] + +ParentPanel = list[list[list[int]]] +ParentTable = list[list[int]] -def resampling_panel(resampling_records, is_sorted=False): +# (traj_idx, cycle_idx) +Trace = list[tuple[int, int]] + +def resampling_panel( + resampling_records: list[ResamplingRecordUnstruct], + is_sorted: bool = False, +) -> DecisionPanel: """Converts an unordered collection of resampling records into a structured array (lists) corresponding to cycles and resampling steps within cycles. @@ -99,12 +117,21 @@ def resampling_panel(resampling_records, is_sorted=False): res_panel = [] + _resampling_records = [ + (run_record.cycle_idx, run_record.record) + for run_record + in resampling_records + ] # if the records are not sorted this must be done: if not is_sorted: - resampling_records.sort() + _resampling_records.sort(key=lambda tup: tup[0]) + + # otherwise just unpack them + else: + _resampling_records = copy.copy(resampling_records) # iterate through the resampling records - rec_it = iter(resampling_records) + rec_it = iter(_resampling_records) last_cycle_idx = None cycle_recs = [] stop = False @@ -114,7 +141,7 @@ def resampling_panel(resampling_records, is_sorted=False): cycle_stop = False while not cycle_stop: try: - rec = next(rec_it) + cycle_idx, record = next(rec_it) except StopIteration: # this is the last record of all the records stop = True @@ -128,11 +155,11 @@ def resampling_panel(resampling_records, is_sorted=False): # cycle_idx so we know when in the records we have # gotten to the next cycle of records if last_cycle_idx is None: - last_cycle_idx = rec.cycle_idx + last_cycle_idx = cycle_idx # if the resampling record retrieved is from the next # cycle we finish the last cycle - if rec.cycle_idx > last_cycle_idx: + if cycle_idx > last_cycle_idx: cycle_stop = True # save the current cycle as a special # list which we will iterate through @@ -142,11 +169,11 @@ def resampling_panel(resampling_records, is_sorted=False): # start a new cycle_recs for the record # we just got - cycle_recs = [rec] + cycle_recs = [record] last_cycle_idx += 1 if not cycle_stop: - cycle_recs.append(rec) + cycle_recs.append(record) else: # we need to break up the records in the cycle into steps @@ -168,7 +195,7 @@ def resampling_panel(resampling_records, is_sorted=False): # or if the next stop index has been obtained else: - if cycle_rec.step_idx > step_idx: + if cycle_rec["step_idx"] > step_idx: step_stop = True # save the current step as a special # list which we will iterate through @@ -188,12 +215,15 @@ def resampling_panel(resampling_records, is_sorted=False): step_row = [None for _ in range(len(curr_step_recs))] for walker_rec in curr_step_recs: # collect data from the record - walker_idx = walker_rec.walker_idx - decision_id = walker_rec.decision_id - instruction = walker_rec.target_idxs + walker_idx = walker_rec["walker_idx"] + decision_id = walker_rec["decision_id"] + target_idxs = walker_rec["target_idxs"] # set the resampling record for the walker in the step records - step_row[walker_idx] = (decision_id, instruction) + step_row[walker_idx] = DecisionRecordUnstruct({ + "decision_id" : decision_id, + "target_idxs" : target_idxs, + }) # add the records for this step to the cycle table cycle_table.append(step_row) @@ -204,7 +234,10 @@ def resampling_panel(resampling_records, is_sorted=False): return res_panel -def parent_panel(decision_class, resampling_panel): +def parent_panel( + decision_class: type[BaseDecisionABC], + resampling_panel: DecisionPanel, +) -> ParentPanel: """Using the parental interpretation of resampling records given by the decision_class, convert resampling records in a resampling panel to parent indices. @@ -231,9 +264,16 @@ def parent_panel(decision_class, resampling_panel): parent_table = [] # now iterate through the rest of the stages - for step in cycle: + for step_recs in cycle: + + # cast the unstructured record to decision records + decision_recs = [ + decision_class.DECISION_RECORD(**step_rec) + for step_rec + in step_recs + ] # get the parents idxs for the children of this step - step_parents = decision_class.parents(step) + step_parents = decision_class.parents(decision_recs) # for the full stage table save all the intermediate parents parent_table.append(step_parents) @@ -244,7 +284,7 @@ def parent_panel(decision_class, resampling_panel): return parent_panel_in -def net_parent_table(parent_panel): +def net_parent_table(parent_panel: ParentPanel) -> ParentTable: """Reduces a full parent panel to get parent indices on a cycle basis. The full parent panel has parent indices for every step in each @@ -295,8 +335,10 @@ def net_parent_table(parent_panel): def parent_table_discontinuities( - boundary_condition_class, parent_table, warping_records -): + boundary_condition_class: type[BoundaryConditions], + parent_table: ParentTable, + warping_records, +) -> ParentTable: """Given a parent table and warping records returns a new parent table with the discontinuous warping events for parents set to a special value (-1). @@ -320,7 +362,7 @@ def parent_table_discontinuities( """ # Make a copy of the parent table - new_parent_table = copy(parent_table) + new_parent_table = copy.copy(parent_table) for warp_record in warping_records: cycle_idx = warp_record[0] @@ -343,8 +385,11 @@ def parent_table_discontinuities( return new_parent_table -def parent_cycle_discontinuities(parent_idxs, discontinuities): - parent_row = copy(parent_idxs) +def parent_cycle_discontinuities( + parent_idxs: list[int], + discontinuities: list[bool], +) -> list[int]: + parent_row = copy.copy(parent_idxs) for walker_idx, disc in enumerate(discontinuities): # if there was a discontinuity in this walker, we need to # check for which children it had and apply the discontinuity @@ -359,7 +404,12 @@ def parent_cycle_discontinuities(parent_idxs, discontinuities): return parent_row -def ancestors(parent_table, cycle_idx, walker_idx, ancestor_cycle=0): +def ancestors( + parent_table: ParentTable, + cycle_idx: int, + walker_idx: int, + ancestor_cycle: int = 0, +) -> Trace: """Returns the lineage of ancestors as walker indices leading up to the given walker. @@ -402,7 +452,10 @@ def ancestors(parent_table, cycle_idx, walker_idx, ancestor_cycle=0): return lineage -def sliding_window(parent_table, window_length): +def sliding_window( + parent_table: ParentTable, + window_length: int, +) -> list[Trace]: """Return contig walker traces of sliding windows of given length over the parent forest imposed over the contig given by the parent table. diff --git a/src/wepy/hdf5.py b/src/wepy/hdf5.py index 997bb1fd..413a7ea6 100644 --- a/src/wepy/hdf5.py +++ b/src/wepy/hdf5.py @@ -396,7 +396,7 @@ import itertools as it import json import logging -from typing import Any, TypedDict, NotRequired, Literal +from typing import Any, TypedDict, NotRequired, Literal, Union, Required # Standard Library import os.path as osp @@ -409,7 +409,7 @@ from numpy.typing import NDArray # First Party Library -from wepy.analysis.parents import resampling_panel +from wepy.analysis.parents import resampling_panel, DecisionPanel from wepy.util.json_top import json_top_atom_count, json_top_subset from wepy.util.mdtraj import ( json_to_mdtraj_topology, @@ -424,9 +424,16 @@ from wepy.storage.protocol import ( RecordValueDtype, Record, + RunRecord, + RecordFieldShape, RecordFieldShapeSpec, RecordFieldDtype, RecordFieldSpec, + ResamplingRecord, + ResamplingRecordUnstruct, + RESAMPLING_RECORD_FIELDS, + WARPING_RECORD_FIELDS, + WarpRecordUnstruct, ) # optional dependencies @@ -448,6 +455,13 @@ H5Attrs = dict[str, H5AttrDtype] +H5FieldDtype = Union[ + np.dtype, + type[np.generic], + str, + h5py.Datatype, +] + ## h5py settings # we set the libver to always be the latest (which should be 1.10) so @@ -553,6 +567,32 @@ BC = "boundary_conditions" """Record group run field name for the boundary conditions records """ +RunRecordKey = Literal[ + "trajectories", + "init_walkers", + "decision", + "resampling", + "resampler", + "warping", + "progress", + "boundary_conditions", +] +RUN_RECORD_KEYS = frozenset({ + RESAMPLING, + RESAMPLER, + WARPING, + PROGRESS, + BC, +}) + +class RunRecordColumns(TypedDict, total=False): + cycle_idx: Required[list[int]] + step_idx: Required[list[int]] + walker_idx: Required[list[int]] + decision_id: Required[list[int]] + target_idxs: Required[list[tuple[int, ...]]] + + ## Record groups constants # special datatypes strings @@ -678,6 +718,25 @@ """Name of the dataset that indexes sparse trajectory fields.""" +# TOREV: this is potentially an interface which should be in the +# storage protocol +# +## Data types for specific data objects + +FieldsData = dict[str, NDArray] +WeightsTrajArray = NDArray[np.float64] + +SparseIdxs = dict[str, list[int]] + +class WepyHDF5Error(Exception): + pass + +class WepyHDF5WriteError(WepyHDF5Error): + pass + +class WepyHDF5ReadError(WepyHDF5Error): + pass + # utility for paths def _iter_field_paths(grp: h5py.Group): """Return all subgroup field name paths from a group. @@ -1374,7 +1433,10 @@ def swmr_mode(self, val): ### constructors - def _get_field_path_grp(self, run_idx, traj_idx, field_path): + def _get_field_path_grp(self, run_idx: int, traj_idx: int, field_path: str) -> tuple[ + h5py.Group, + str, + ]: """Given a field path for the trajectory returns the group the field's dataset goes in and the key for the field name in that group. @@ -1403,9 +1465,15 @@ def _get_field_path_grp(self, run_idx, traj_idx, field_path): # split it grp_name, field_name = field_path.split("/") # get the hdf5 group - grp = self.h5[ - "{}/{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx, grp_name) - ] + traj_grp = self.traj(run_idx, traj_idx) + if grp_name not in traj_grp: + raise WepyHDF5ReadError( + f"Field group '{field_path}' not in run={run_idx}, traj={traj_idx}" + ) + + else: + grp = traj_grp[grp_name] + # its simple so just return the root group and the original path else: grp = self.h5 @@ -1414,7 +1482,7 @@ def _get_field_path_grp(self, run_idx, traj_idx, field_path): return grp, field_name - def _add_run_init(self, run_idx, continue_run=None): + def _add_run_init(self, run_idx: int, continue_run: int | None = None) -> None: """Routines for creating a run includes updating and setting object global variables, increasing the counter for the number of runs. @@ -1427,8 +1495,10 @@ def _add_run_init(self, run_idx, continue_run=None): """ + run_grp = self.run(run_idx) + # add the run idx as metadata in the run group - self._h5["{}/{}".format(RUNS, run_idx)].attrs[RUN_IDX] = run_idx + run_grp.attrs[RUN_IDX] = run_idx # if this is continuing another run add the tuple (this_run, # continues_run) to the continutations settings @@ -1509,7 +1579,12 @@ def _init_run_sporadic_record_grp( return record_grp - def _init_run_continual_record_grp(self, run_idx, run_record_key, fields): + def _init_run_continual_record_grp( + self, + run_idx: int, + run_record_key: RunRecordKey, + fields: list[RecordFieldSpec], + ) -> h5py.Group: """Initialize a continual record group for a run. Parameters @@ -1619,39 +1694,14 @@ def _is_sporadic_records( else: return False - def _init_traj_field(self, run_idx, traj_idx, field_path, feature_shape, dtype): - """Initialize a trajectory field. - - Initialize a data field in the trajectory to be empty but - resizeable. - - Parameters - ---------- - run_idx : int - traj_idx : int - field_path : str - Field name specification. - feature_shape : shape_spec - Specification of shape of a feature vector of the field. - dtype : dtype_spec - Specification of the feature vector datatype. - - """ - - # check whether this is a sparse field and create it - # appropriately - if field_path in self.sparse_fields: - # it is a sparse field - self._init_sparse_traj_field( - run_idx, traj_idx, field_path, feature_shape, dtype - ) - else: - # it is not a sparse field (AKA simple) - self._init_contiguous_traj_field( - run_idx, traj_idx, field_path, feature_shape, dtype - ) - - def _init_contiguous_traj_field(self, run_idx, traj_idx, field_path, shape, dtype): + def _init_contiguous_traj_field( + self, + run_idx: int, + traj_idx: int, + field_path: str, + shape: RecordFieldShape, + dtype: H5FieldDtype, + ) -> None: """Initialize a contiguous (non-sparse) trajectory field. Parameters @@ -1667,7 +1717,7 @@ def _init_contiguous_traj_field(self, run_idx, traj_idx, field_path, shape, dtyp """ - traj_grp = self._h5["{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx)] + traj_grp = self.traj(run_idx, traj_idx) # create the empty dataset in the correct group, setting # maxshape so it can be resized for new feature vectors to be added @@ -1675,7 +1725,14 @@ def _init_contiguous_traj_field(self, run_idx, traj_idx, field_path, shape, dtyp field_path, (0, *[0 for i in shape]), dtype=dtype, maxshape=(None, *shape) ) - def _init_sparse_traj_field(self, run_idx, traj_idx, field_path, shape, dtype): + def _init_sparse_traj_field( + self, + run_idx: int, + traj_idx: int, + field_path: str, + shape: RecordFieldShape, + dtype: H5FieldDtype, + ) -> None: """Parameters ---------- run_idx : int @@ -1689,7 +1746,7 @@ def _init_sparse_traj_field(self, run_idx, traj_idx, field_path, shape, dtype): """ - traj_grp = self._h5["{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx)] + traj_grp = self.traj(run_idx, traj_idx) # check to see that neither the shape and dtype are # None which indicates it is a runtime defined value and @@ -1710,9 +1767,54 @@ def _init_sparse_traj_field(self, run_idx, traj_idx, field_path, shape, dtype): # create the dataset for the sparse indices sparse_grp.create_dataset(SPARSE_IDXS, (0,), dtype=int, maxshape=(None,)) + + def _init_traj_field( + self, + run_idx: int, + traj_idx: int, + field_path: str, + feature_shape: RecordFieldShape, + dtype: H5FieldDtype, + ) -> None: + """Initialize a trajectory field. + + Initialize a data field in the trajectory to be empty but + resizeable. + + Parameters + ---------- + run_idx : int + traj_idx : int + field_path : str + Field name specification. + feature_shape : shape_spec + Specification of shape of a feature vector of the field. + dtype : dtype_spec + Specification of the feature vector datatype. + + """ + + # check whether this is a sparse field and create it + # appropriately + if field_path in self.sparse_fields: + # it is a sparse field + self._init_sparse_traj_field( + run_idx, traj_idx, field_path, feature_shape, dtype + ) + else: + # it is not a sparse field (AKA simple) + self._init_contiguous_traj_field( + run_idx, traj_idx, field_path, feature_shape, dtype + ) + def _init_traj_fields( - self, run_idx, traj_idx, field_paths, field_feature_shapes, field_feature_dtypes + self, + run_idx: int, + traj_idx: int, + field_paths: list[str], + field_feature_shapes: list[RecordFieldShape], + field_feature_dtypes: list[H5FieldDtype], ): """Initialize a number of fields for a trajectory. @@ -1737,12 +1839,12 @@ def _init_traj_fields( def _add_traj_field_data( self, - run_idx, - traj_idx, - field_path, - field_data, - sparse_idxs=None, - ): + run_idx: int, + traj_idx: int, + field_path: str, + field_data: NDArray, + sparse_idxs: SparseIdxs | None = None, + ) -> None: """Add a trajectory field to a trajectory. If the sparse indices are given the field will be created as a @@ -1766,7 +1868,7 @@ def _add_traj_field_data( """ # get the traj group - traj_grp = self._h5["{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx)] + traj_grp = self.traj(run_idx, traj_idx) # if it is a sparse dataset we need to add the data and add # the idxs in a group @@ -1817,7 +1919,8 @@ def _extend_contiguous_traj_field(self, run_idx, traj_idx, field_path, field_dat """ - traj_grp = self.h5["{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx)] + traj_grp = self.traj(run_idx, traj_idx) + field = traj_grp[field_path] # make sure this is a feature vector @@ -1878,9 +1981,7 @@ def _extend_sparse_traj_field( """ - field = self.h5[ - "{}/{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx, field_path) - ] + field = self.traj_field_entity(run_idx, traj_idx, field_path) field_data = field[DATA] field_sparse_idxs = field[SPARSE_IDXS] @@ -1944,8 +2045,7 @@ def _add_sparse_field_flag(self, field_path): Name of the trajectory field you want to flag as sparse """ - - sparse_fields_ds = self._h5["{}/{}".format(SETTINGS, SPARSE_FIELDS)] + sparse_fields_ds = self.settings_grp[SPARSE_FIELDS] # make sure it isn't already in the sparse_fields if field_path in sparse_fields_ds[:]: @@ -1965,7 +2065,7 @@ def _add_field_feature_shape(self, field_path, field_feature_shape): The shape spec to serialize as a dataset. """ - shapes_grp = self._h5["{}/{}".format(SETTINGS, FIELD_FEATURE_SHAPES_STR)] + shapes_grp = self.settings_grp[FIELD_FEATURE_SHAPES_STR] shapes_grp.create_dataset(field_path, data=np.array(field_feature_shape)) def _add_field_feature_dtype(self, field_path, field_feature_dtype): @@ -1980,7 +2080,7 @@ def _add_field_feature_dtype(self, field_path, field_feature_dtype): """ feature_dtype_str = json.dumps(field_feature_dtype.descr) - dtypes_grp = self._h5["{}/{}".format(SETTINGS, FIELD_FEATURE_DTYPES_STR)] + dtypes_grp = self.settings_grp[FIELD_FEATURE_DTYPES_STR] dtypes_grp.create_dataset(field_path, data=feature_dtype_str) def _set_field_feature_shape(self, field_path, field_feature_shape): @@ -1999,6 +2099,7 @@ def _set_field_feature_shape(self, field_path, field_feature_shape): # check that the shape was previously saved as "None" as we # won't overwrite anything else if self.field_feature_shapes[field_path] is None: + full_path = "{}/{}/{}".format( SETTINGS, FIELD_FEATURE_SHAPES_STR, field_path ) @@ -2078,13 +2179,9 @@ def _extend_run_record_data_field( """ - records_grp = self.h5["{}/{}/{}".format(RUNS, run_idx, run_record_key)] + records_grp = self.records_grp(run_idx, run_record_key) field = records_grp[field_name] - assert ( - len(field_data.shape) > 1 - ), f"field_data (record={run_record_key}, name={field_name}, shape={field_data.shape}) must be a feature vector with the same number of dimensions as the number" - # of datase new frames n_new_frames = field_data.shape[0] @@ -2144,9 +2241,9 @@ def _extend_run_record_data_field( # must exist field.resize((field.shape[0] + n_new_frames, *field.shape[1:])) # add the new data - field[-n_new_frames:, ...] = field_data + field[-n_new_frames:, ...] = np.array([field_data]) - def _run_record_namedtuple(self, run_record_key): + def _run_record_namedtuple(self, run_record_key: str): """Generate a namedtuple record type for a record group. The class name will be formatted like '{}_Record' where the {} @@ -2171,9 +2268,10 @@ def _run_record_namedtuple(self, run_record_key): return Record + # TODO: get the tablified value types recorded somewhere def _convert_record_field_to_table_column( - self, run_idx, run_record_key, record_field - ): + self, run_idx: int, run_record_key: RunRecordKey, record_field: str, + ) -> list[RecordValueDtype]: """Converts a dataset of feature vectors to more palatable values for use in external datasets. @@ -2235,7 +2333,7 @@ def _convert_record_field_to_table_column( return rec_dset - def _convert_record_fields_to_table_columns(self, run_idx, run_record_key): + def _convert_record_fields_to_table_columns(self, run_idx: int, run_record_key: RunRecordKey) -> RunRecordColumns: """Convert record group data to truncated namedtuple records. This uses the specified record fields from the header settings @@ -2265,41 +2363,81 @@ def _convert_record_fields_to_table_columns(self, run_idx, run_record_key): return fields - def _make_records(self, run_record_key, cycle_idxs, fields): - """Generate a list of proper (nametuple) records for a record group. + # def _make_records( + # self, + # run_record_key: RunRecordKey, + # cycle_idxs: list[int], + # table_fields: dict[str, RecordValueDtype], + # ) -> list[Record]: + # """Generate a list of record dicts for a record group. - Parameters - ---------- - run_record_key : str - Name of the record group - cycle_idxs : list of int - The cycle indices you want to get records for. - fields : list of str - The fields to make record entries for. + # Parameters + # ---------- + # run_record_key : str + # Name of the record group + # cycle_idxs : list of int + # The cycle indices you want to get records for. + # fields - Returns - ------- - records : list of namedtuple objects + # Returns + # ------- + # records : list of Records - """ - Record = self._run_record_namedtuple(run_record_key) + # """ + # Record = self._run_record_namedtuple(run_record_key) - # for each record we make a tuple and yield it - records = [] - for record_idx in range(len(cycle_idxs)): - # make a record for this cycle - record_d = {CYCLE_IDX: cycle_idxs[record_idx]} - for record_field, column in fields.items(): - datum = column[record_idx] - record_d[record_field] = datum + # # for each record we make a tuple and yield it + # records = [] + # for record_idx in range(len(cycle_idxs)): + # # make a record for this cycle + # record_d = {CYCLE_IDX: cycle_idxs[record_idx]} + # for record_field, column in table_fields.items(): + # datum = column[record_idx] + # record_d[record_field] = datum + + # record = Record(*(record_d[key] for key in Record._fields)) + + # records.append(record) + + # return records + + def _table_to_run_records( + self, + run_record_key: RunRecordKey, + table_fields: RunRecordColumns, + ) -> list[RunRecord]: + + it_fields = set(table_fields.keys()) + it_fields.remove("cycle_idx") - record = Record(*(record_d[key] for key in Record._fields)) + field_its = { + field_name : iter(table_fields[field_name]) + for field_name + in it_fields + } + + records = [] + for cycle_idx in table_fields["cycle_idx"]: + + # get the next value from each iterator + record_d = { + field_name : next(field_its[field_name]) + for field_name + in it_fields + } + + record = RunRecord( + cycle_idx=cycle_idx, + record=record_d, + ) records.append(record) + return records + - def _run_records_sporadic(self, run_idxs, run_record_key): + def _run_records_sporadic(self, run_idxs: list[int], run_record_key: RunRecordKey) -> Record: """Generate records for a sporadic record group for a multi-run contig. @@ -2329,8 +2467,7 @@ def _run_records_sporadic(self, run_idxs, run_record_key): # we loop over the run_idxs in the contig and get the fields # and cycle idxs for the whole contig - fields = None - cycle_idxs = np.array([], dtype=int) + table_fields = defaultdict(list) # keep a cumulative total of the runs cycle idxs prev_run_cycle_total = 0 for run_idx in run_idxs: @@ -2340,19 +2477,6 @@ def _run_records_sporadic(self, run_idxs, run_record_key): run_idx, run_record_key ) - # we need to concatenate each field to the end of the - # field in the master dictionary, first we need to - # initialize it if it isn't already made - if fields is None: - # if it isn't initialized we just set it as this first - # run fields dictionary - fields = run_fields - else: - # if it is already initialized we need to go through - # each field and concatenate - for field_name, field_data in run_fields.items(): - # just add it to the list of fields that will be concatenated later - fields[field_name].extend(field_data) # get the cycle idxs for this run rec_grp = self.records_grp(run_idx, run_record_key) @@ -2363,19 +2487,22 @@ def _run_records_sporadic(self, run_idxs, run_record_key): # of the full contig run_contig_cycle_idxs = run_cycle_idxs + prev_run_cycle_total - # add these cycle indices to the records for the whole contig - cycle_idxs = np.hstack((cycle_idxs, run_contig_cycle_idxs)) + for field_name, field_data in run_fields.items(): + # just add it to the list of fields that will be concatenated later + table_fields[field_name].extend(field_data) + + table_fields["cycle_idx"].extend(list(run_contig_cycle_idxs)) # add the total number of cycle_idxs from this run to the # running total prev_run_cycle_total += self.num_run_cycles(run_idx) - # then make the records from the fields - records = self._make_records(run_record_key, cycle_idxs, fields) + # then make the records from the tablified fields + records = self._table_to_run_records(run_record_key, table_fields) return records - def _run_records_continual(self, run_idxs, run_record_key): + def _run_records_continual(self, run_idxs: list[int], run_record_key: RunRecordKey) -> list[Record]: """Generate records for a continual record group for a multi-run contig. @@ -2403,8 +2530,9 @@ def _run_records_continual(self, run_idxs, run_record_key): """ - cycle_idxs = np.array([], dtype=int) - fields = None + # TODO: revert changes here + + fields = defaultdict(list) prev_run_cycle_total = 0 for run_idx in run_idxs: # get all the value columns from the datasets, and convert @@ -2412,43 +2540,36 @@ def _run_records_continual(self, run_idxs, run_record_key): run_fields = self._convert_record_fields_to_table_columns( run_idx, run_record_key ) - - # we need to concatenate each field to the end of the - # field in the master dictionary, first we need to - # initialize it if it isn't already made - if fields is None: - # if it isn't initialized we just set it as this first - # run fields dictionary - fields = run_fields - else: - # if it is already initialized we need to go through - # each field and concatenate - for field_name, field_data in run_fields.items(): - # just add it to the list of fields that will be concatenated later - fields[field_name].extend(field_data) - - # get one of the fields (if any to iterate over) - record_fields = self.record_fields[run_record_key] - main_record_field = record_fields[0] + # if it is already initialized we need to go through + # each field and concatenate + for field_name, field_data in run_fields.items(): + # just add it to the list of fields that will be concatenated later + fields[field_name].extend(field_data) + + + # use one of the fields as the lead to find how many + # cycles there are since they are not tracked explicitly + # in the data file + lead_record_field = self.record_fields[run_record_key][0] # make the cycle idxs from that - run_rec_grp = self.records_grp(run_idx, run_record_key) - run_cycle_idxs = np.array(range(run_rec_grp[main_record_field].shape[0])) - - # add the total number of cycles that came before this run - # to each of the cycle idxs to get the cycle_idxs in terms - # of the full contig - run_contig_cycle_idxs = run_cycle_idxs + prev_run_cycle_total + run_cycle_idxs = list(range( + self.records_grp( + run_idx, + run_record_key, + )[lead_record_field].shape[0])) - # add these cycle indices to the records for the whole contig - cycle_idxs = np.hstack((cycle_idxs, run_contig_cycle_idxs)) + reindexed_cycle_idxs = [idx + prev_run_cycle_total for idx in run_cycle_idxs] # add the total number of cycle_idxs from this run to the # running total prev_run_cycle_total += self.num_run_cycles(run_idx) + # then update the cycle_idxs with the reindexed ones + fields["cycle_idx"] = reindexed_cycle_idxs + # then make the records from the fields - records = self._make_records(run_record_key, cycle_idxs, fields) + records = self._table_to_run_records(run_record_key, fields) return records @@ -2471,14 +2592,12 @@ def _get_contiguous_traj_field(self, run_idx, traj_idx, field_path, frames=None) """ - full_path = "{}/{}/{}/{}/{}".format( - RUNS, run_idx, TRAJECTORIES, traj_idx, field_path - ) + field_thing = self.get_traj_field(run_idx, traj_idx, field_path) if frames is None: - field = self._h5[full_path][:] + field = field_thing[:] else: - field = self._h5[full_path][list(frames)] + field = field_thing[list(frames)] return field @@ -2508,8 +2627,8 @@ def _get_sparse_traj_field( """ - traj_path = "{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx) - traj_grp = self.h5[traj_path] + traj_grp = self.traj(run_idx, traj_idx) + field = traj_grp[field_path] n_frames = traj_grp[POSITIONS].shape[0] @@ -2691,6 +2810,17 @@ def _add_field(self, field_path, data, sparse_idxs=None, force=False): ### h5py object access + @property + def runs(self) -> h5py.Group: + """The runs group.""" + + if RUNS not in self.h5: + raise WepyHDF5ReadError(f"The '{RUNS}' is not initialized.") + + else: + return self.h5[RUNS] + + def run(self, run_idx: int) -> h5py.Group: """Get the h5py.Group for a run. @@ -2703,89 +2833,79 @@ def run(self, run_idx: int) -> h5py.Group: run_group : h5py.Group """ - return self._h5["{}/{}".format(RUNS, int(run_idx))] - def traj(self, run_idx: int, traj_idx: int) -> h5py.Group: - """Get an h5py.Group trajectory group. + run_id = str(run_idx) + if run_id not in self.runs: + raise WepyHDF5ReadError(f"Run '{run_idx}' is not initialized.") + else: + return self.runs[run_id] + + def run_trajs(self, run_idx: int) -> h5py.Group: + """Get the trajectories group for a run. Parameters ---------- run_idx : int - traj_idx : int Returns ------- - traj_group : h5py.Group + trajectories_grp : h5py.Group """ - return self._h5["{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx)] - def run_trajs(self, run_idx: int) -> h5py.Group: - """Get the trajectories group for a run. + run_grp = self.run(run_idx) + + if TRAJECTORIES not in run_grp: + raise WepyHDF5ReadError(f"The '{TRAJECTORIES}' group not initialized for run {run_idx}") + else: + return run_grp[TRAJECTORIES] + + + def traj(self, run_idx: int, traj_idx: int) -> h5py.Group: + """Get an h5py.Group trajectory group. Parameters ---------- run_idx : int + traj_idx : int Returns ------- - trajectories_grp : h5py.Group + traj_group : h5py.Group """ - return self._h5["{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES)] - - @property - def runs(self) -> h5py.Group: - """The runs group.""" - return self.h5[RUNS] - def run_grp(self, run_idx: int) -> h5py.Group: - """A group for a single run.""" - return self.runs["{}".format(run_idx)] + trajs_grp = self.run_trajs(run_idx) - # TOREV: probably can get rid of these since we are deprecating - # the Orchestrator and snapshots as implemented + traj_id = str(traj_idx) - def run_start_snapshot_hash(self, run_idx): - """Hash identifier for the starting snapshot of a run from - orchestration. - - """ - return self.run_grp(run_idx).attrs[RUN_START_SNAPSHOT_HASH] - - def run_end_snapshot_hash(self, run_idx): - """Hash identifier for the ending snapshot of a run from - orchestration. - - """ - return self.run_grp(run_idx).attrs[RUN_END_SNAPSHOT_HASH] + if traj_id not in trajs_grp: + raise WepyHDF5ReadError( + f"Trajectory {traj_idx} not in trajectories of run {run_idx}" + ) - def set_run_start_snapshot_hash(self, run_idx, snaphash): - """Set the starting snapshot hash identifier for a run from - orchestration. + else: - """ + return trajs_grp[traj_id] - if RUN_START_SNAPSHOT_HASH not in self.run_grp(run_idx).attrs: - self.run_grp(run_idx).attrs[RUN_START_SNAPSHOT_HASH] = snaphash - else: - raise AttributeError("The snapshot has already been set.") + def traj_field_entity(self, run_idx: int, traj_idx: int, field_path: str) -> h5py.Dataset | h5py.Group: - def set_run_end_snapshot_hash(self, run_idx, snaphash): - """Set the ending snapshot hash identifier for a run from - orchestration. + traj_grp = self.traj(run_idx, traj_idx) - """ - if RUN_END_SNAPSHOT_HASH not in self.run_grp(run_idx).attrs: - self.run_grp(run_idx).attrs[RUN_END_SNAPSHOT_HASH] = snaphash + if field_path not in traj_grp: + raise WepyHDF5ReadError( + f"The field path '{field_path}' was not found in run={run_idx}, traj={traj_idx}." + ) else: - raise AttributeError("The snapshot has already been set.") + return traj_grp[field_path] @property def settings_grp(self) -> h5py.Group: """The header settings group.""" - settings_grp = self.h5[SETTINGS] - return settings_grp + if SETTINGS not in self.h5: + raise WepyHDF5ReadError(f"The settings group ({SETTINGS}) has not been initialized") + else: + return self.h5[SETTINGS] def decision_grp(self, run_idx: int) -> h5py.Group: """Get the decision enumeration group for a run. @@ -2799,7 +2919,15 @@ def decision_grp(self, run_idx: int) -> h5py.Group: decision_grp : h5py.Group """ - return self.run(run_idx)[DECISION] + + run_grp = self.run(run_idx) + if DECISION not in run_grp: + raise WepyHDF5ReadError( + f"Decision group not initialized in run {run_idx}" + ) + else: + + return run_grp[DECISION] def init_walkers_grp(self, run_idx: int) -> h5py.Group: """Get the group for the initial walkers for a run. @@ -2816,7 +2944,7 @@ def init_walkers_grp(self, run_idx: int) -> h5py.Group: return self.run(run_idx)[INIT_WALKERS] - def records_grp(self, run_idx: int, run_record_key: str) -> h5py.Group: + def records_grp(self, run_idx: int, run_record_key: RunRecordKey) -> h5py.Group: """Get a record group h5py.Group for a run. Parameters @@ -2830,8 +2958,21 @@ def records_grp(self, run_idx: int, run_record_key: str) -> h5py.Group: run_record_group : h5py.Group """ - path = "{}/{}/{}".format(RUNS, run_idx, run_record_key) - return self.h5[path] + + if run_record_key not in RUN_RECORD_KEYS: + raise KeyError( + f"'{run_record_key}' is not valid. Choose from: {RUN_RECORD_KEYS}" + ) + + run_grp = self.run(run_idx) + + if run_record_key not in run_grp: + raise WepyHDF5ReadError( + f"Run record key '{run_record_key}' not in run {run_idx}" + ) + + else: + return run_grp[run_record_key] def resampling_grp(self, run_idx: int) -> h5py.Group: """Get this record group for a run. @@ -3073,14 +3214,15 @@ def record_fields(self) -> dict[str, list[str]]: @property def sparse_fields(self) -> NDArray: """The trajectory fields that are sparse.""" - return self.h5["{}/{}".format(SETTINGS, SPARSE_FIELDS)].asstr()[:] + + return self.settings_grp[SPARSE_FIELDS].asstr()[:] @property def main_rep_idxs(self) -> NDArray | None: """The indices of the atoms included from the full topology in the default 'positions' trajectory""" - if "{}/{}".format(SETTINGS, MAIN_REP_IDXS) in self.h5: - return self.h5["{}/{}".format(SETTINGS, MAIN_REP_IDXS)][:] + if MAIN_REP_IDXS in self.settings_grp: + return self.settings_grp[MAIN_REP_IDXS][:] else: return None @@ -3090,14 +3232,14 @@ def alt_reps_idxs(self) -> dict[str, NDArray]: from the topology that they include in their datasets. """ - idxs_grp = self.h5["{}/{}".format(SETTINGS, ALT_REPS_IDXS)] + idxs_grp = self.settings_grp[ALT_REPS_IDXS] return {name: ds[:] for name, ds in idxs_grp.items()} @property def alt_reps(self) -> set[str]: """Names of the alt reps.""" - idxs_grp = self.h5["{}/{}".format(SETTINGS, ALT_REPS_IDXS)] + idxs_grp = self.settings_grp[ALT_REPS_IDXS] return {name for name in idxs_grp.keys()} @property @@ -3106,7 +3248,7 @@ def field_feature_shapes(self) -> dict[str, NDArray | None]: vector shapes. """ - shapes_grp = self.h5["{}/{}".format(SETTINGS, FIELD_FEATURE_SHAPES_STR)] + shapes_grp = self.settings_grp[FIELD_FEATURE_SHAPES_STR] field_paths = _iter_field_paths(shapes_grp) @@ -3126,7 +3268,7 @@ def field_feature_dtypes(self) -> dict[str, np.dtype]: vector numpy dtypes. """ - dtypes_grp = self.h5["{}/{}".format(SETTINGS, FIELD_FEATURE_DTYPES_STR)] + dtypes_grp = self.settings_grp[FIELD_FEATURE_DTYPES_STR] field_paths = _iter_field_paths(dtypes_grp) @@ -3399,17 +3541,17 @@ def initial_walkers_to_mdtraj( @property def num_atoms(self) -> int: """The number of atoms in the full topology representation.""" - return self.h5["{}/{}".format(SETTINGS, N_ATOMS)][()] + return self.settings_grp[N_ATOMS][()] @property def num_dims(self) -> int: """The number of spatial dimensions in the positions and alt_reps trajectory fields.""" - return self.h5["{}/{}".format(SETTINGS, N_DIMS_STR)][()] + return self.settings_grp[N_DIMS_STR][()] @property def num_runs(self) -> int: """The number of runs in the file.""" - return len(self._h5[RUNS]) + return len(self.runs) @property def num_trajs(self) -> int: @@ -3456,7 +3598,7 @@ def num_walkers(self, run_idx: int, cycle_idx: int) -> int: # trajectory data so just return the number of trajectories return self.num_run_trajs(run_idx) - def num_run_trajs(self, run_idx): + def num_run_trajs(self, run_idx: int) -> int: """The number of trajectories in a run. Parameters @@ -3468,9 +3610,11 @@ def num_run_trajs(self, run_idx): n_trajs : int """ - return len(self._h5["{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES)]) - def num_run_cycles(self, run_idx): + trajs_grp = self.run_trajs(0) + return len(trajs_grp) + + def num_run_cycles(self, run_idx: int) -> int: """The number of cycles in a run. Parameters @@ -3484,7 +3628,7 @@ def num_run_cycles(self, run_idx): """ return self.num_traj_frames(run_idx, 0) - def num_traj_frames(self, run_idx, traj_idx): + def num_traj_frames(self, run_idx: int, traj_idx: int) -> int: """The number of frames in a given trajectory. Parameters @@ -3500,11 +3644,11 @@ def num_traj_frames(self, run_idx, traj_idx): return self.traj(run_idx, traj_idx)[POSITIONS].shape[0] @property - def run_idxs(self): + def run_idxs(self) -> list[int]: """The indices of the runs in the file.""" return list(range(len(self._h5[RUNS]))) - def run_traj_idxs(self, run_idx): + def run_traj_idxs(self, run_idx: int) -> list[int]: """The indices of trajectories in a run. Parameters @@ -3517,10 +3661,10 @@ def run_traj_idxs(self, run_idx): """ return list( - range(len(self._h5["{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES)])) + range(len(self.run_trajs(run_idx))) ) - def run_traj_idx_tuples(self, runs=None): + def run_traj_idx_tuples(self, runs: list[int] | None = None) -> list[tuple[int, int]]: """Get identifier tuples (run_idx, traj_idx) for all trajectories in all runs. @@ -3547,7 +3691,7 @@ def run_traj_idx_tuples(self, runs=None): return tups - def get_traj_field_cycle_idxs(self, run_idx, traj_idx, field_path): + def get_traj_field_cycle_idxs(self, run_idx: int, traj_idx: int, field_path: str) -> NDArray[np.integer]: """Returns the cycle indices for a sparse trajectory field. Parameters @@ -3563,17 +3707,14 @@ def get_traj_field_cycle_idxs(self, run_idx, traj_idx, field_path): """ - traj_path = "{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx) - - if field_path not in self._h5[traj_path]: - raise KeyError("key for field {} not found".format(field_path)) - + field = self.traj_field_entity(run_idx, traj_idx, field_path) + # if the field is not sparse just return the cycle indices for # that run if field_path not in self.sparse_fields: cycle_idxs = np.array(range(self.num_run_cycles(run_idx))) else: - cycle_idxs = self._h5[traj_path][field_path][SPARSE_IDXS][:] + cycle_idxs = field[SPARSE_IDXS][:] return cycle_idxs @@ -3607,7 +3748,7 @@ def next_run_traj_idx(self, run_idx: int) -> int: ### Aggregation - def is_run_contig(self, run_idxs): + def is_run_contig(self, run_idxs: list[int]) -> bool: """This method checks that if a given list of run indices is a valid contig or not. @@ -3979,7 +4120,7 @@ def add_metadata(self, key, value): """ self._h5.attrs[key] = value - def init_record_fields(self, run_record_key, record_fields): + def init_record_fields(self, run_record_key: RunRecordKey, record_fields: list[str]) -> None: """Initialize the settings record fields for a record group in the settings group. @@ -4476,9 +4617,9 @@ def init_run_record_grp( def add_traj( self, run_idx: int, - data: dict[str, NDArray], - weights: NDArray[np.float64] | None = None, - sparse_idxs: dict[str, list[int]] | None = None, + data: FieldsData, + weights: WeightsTrajArray | None = None, + sparse_idxs: SparseIdxs | None = None, metadata: H5Attrs | None = None, ) -> h5py.Group: """Add a full trajectory to a run. @@ -4542,9 +4683,8 @@ def add_traj( traj_idx = self.next_run_traj_idx(run_idx) # make a group for this trajectory, with the current traj_idx # for this run - traj_grp = self._h5.create_group( - f"{RUNS}/{run_idx}/{TRAJECTORIES}/{traj_idx}" - ) + trajs_grp = self.run_trajs(run_idx) + traj_grp = trajs_grp.create_group(str(traj_idx)) # add the run_idx as metadata traj_grp.attrs[RUN_IDX] = run_idx @@ -4622,7 +4762,13 @@ def add_traj( return traj_grp - def extend_traj(self, run_idx, traj_idx, data, weights=None): + def extend_traj( + self, + run_idx: int, + traj_idx: int, + data: FieldsData, + weights: WeightsTrajArray | None = None, + ) -> None: """Extend a trajectory with data for all fields. Parameters @@ -4655,7 +4801,7 @@ def extend_traj(self, run_idx, traj_idx, data, weights=None): sparse_idxs = np.array(range(n_frames, n_frames + n_new_frames)) # get the trajectory group - traj_grp = self._h5["{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx)] + traj_grp = self.traj(run_idx, traj_idx) ## weights @@ -4763,7 +4909,7 @@ def extend_traj(self, run_idx, traj_idx, data, weights=None): ## application level append methods for run records groups - def extend_cycle_warping_records(self, run_idx, cycle_idx, warping_data): + def extend_cycle_warping_records(self, run_idx: int, cycle_idx: int, warping_data: WarpRecordUnstruct) -> None: """Add records for each field for this record group. Parameters @@ -4778,7 +4924,7 @@ def extend_cycle_warping_records(self, run_idx, cycle_idx, warping_data): """ self.extend_cycle_run_group_records(run_idx, WARPING, cycle_idx, warping_data) - def extend_cycle_bc_records(self, run_idx, cycle_idx, bc_data): + def extend_cycle_bc_records(self, run_idx: int, cycle_idx: int, bc_data: Record) -> None: """Add records for each field for this record group. Parameters @@ -4794,7 +4940,7 @@ def extend_cycle_bc_records(self, run_idx, cycle_idx, bc_data): self.extend_cycle_run_group_records(run_idx, BC, cycle_idx, bc_data) - def extend_cycle_progress_records(self, run_idx, cycle_idx, progress_data): + def extend_cycle_progress_records(self, run_idx: int, cycle_idx: int, progress_data: Record) -> None: """Add records for each field for this record group. Parameters @@ -4813,7 +4959,7 @@ def extend_cycle_resampling_records( self, run_idx: int, cycle_idx: int, - resampling_data: list[Record], + resampling_data: list[ResamplingRecordUnstruct], ) -> None: """Add records for each field for this record group. @@ -4839,7 +4985,7 @@ def extend_cycle_resampling_records( resampling_data, ) - def extend_cycle_resampler_records(self, run_idx, cycle_idx, resampler_data): + def extend_cycle_resampler_records(self, run_idx: int, cycle_idx: int, resampler_data: Record) -> None: """Add records for each field for this record group. Parameters @@ -4915,29 +5061,7 @@ def extend_cycle_run_group_records( ## Record Getters - def run_records(self, run_idx, run_record_key): - """Get the records for a record group for a single run. - - Parameters - ---------- - run_idx : int - run_record_key : str - The name of the record group. - - Returns - ------- - records : list of namedtuple objects - The list of records for the run's record group. - - """ - - # wrap this in a list since the underlying functions accept a - # list of records - run_idxs = [run_idx] - - return self.run_contig_records(run_idxs, run_record_key) - - def run_contig_records(self, run_idxs, run_record_key): + def run_contig_records(self, run_idxs: list[int], run_record_key: RunRecordKey) -> list[Record]: """Get the records for a record group for the contig that is formed by the run indices. @@ -4975,7 +5099,30 @@ def run_contig_records(self, run_idxs, run_record_key): return records - def run_records_dataframe(self, run_idx, run_record_key): + def run_records(self, run_idx: int, run_record_key: RunRecordKey) -> list[RunRecord]: + """Get the records for a record group for a single run. + + Parameters + ---------- + run_idx : int + run_record_key : str + The name of the record group. + + Returns + ------- + records : list of namedtuple objects + The list of records for the run's record group. + + """ + + # wrap this in a list since the underlying functions accept a + # list of records + run_idxs = [run_idx] + + return self.run_contig_records(run_idxs, run_record_key) + + + def run_records_dataframe(self, run_idx: int, run_record_key: RunRecordKey) -> pd.DataFrame: """Get the records for a record group for a single run in the form of a pandas DataFrame. @@ -4992,7 +5139,7 @@ def run_records_dataframe(self, run_idx, run_record_key): records = self.run_records(run_idx, run_record_key) return pd.DataFrame(records) - def run_contig_records_dataframe(self, run_idxs, run_record_key): + def run_contig_records_dataframe(self, run_idxs: list[int], run_record_key: RunRecordKey) -> pd.DataFrame: """Get the records for a record group for a contig of runs in the form of a pandas DataFrame. @@ -5015,7 +5162,7 @@ def run_contig_records_dataframe(self, run_idxs, run_record_key): # application level specific methods for each main group # resampling - def resampling_records(self, run_idxs): + def resampling_records(self, run_idxs: list[int]) -> list[RunRecord]: """Get the records this record group for the contig that is formed by the run indices. @@ -5038,7 +5185,7 @@ def resampling_records(self, run_idxs): return self.run_contig_records(run_idxs, RESAMPLING) - def resampling_records_dataframe(self, run_idxs): + def resampling_records_dataframe(self, run_idxs: list[int]) -> pd.DataFrame: """Get the records for this record group for a contig of runs in the form of a pandas DataFrame. @@ -5057,7 +5204,7 @@ def resampling_records_dataframe(self, run_idxs): return pd.DataFrame(self.resampling_records(run_idxs)) # resampler records - def resampler_records(self, run_idxs): + def resampler_records(self, run_idxs: list[int]) -> list[RunRecord]: """Get the records this record group for the contig that is formed by the run indices. @@ -5080,7 +5227,7 @@ def resampler_records(self, run_idxs): return self.run_contig_records(run_idxs, RESAMPLER) - def resampler_records_dataframe(self, run_idxs): + def resampler_records_dataframe(self, run_idxs: list[int]) -> pd.DataFrame: """Get the records for this record group for a contig of runs in the form of a pandas DataFrame. @@ -5099,7 +5246,7 @@ def resampler_records_dataframe(self, run_idxs): return pd.DataFrame(self.resampler_records(run_idxs)) # warping - def warping_records(self, run_idxs): + def warping_records(self, run_idxs: list[int]) -> list[RunRecord]: """Get the records this record group for the contig that is formed by the run indices. @@ -5122,7 +5269,7 @@ def warping_records(self, run_idxs): return self.run_contig_records(run_idxs, WARPING) - def warping_records_dataframe(self, run_idxs): + def warping_records_dataframe(self, run_idxs: list[int]) -> pd.DataFrame: """Get the records for this record group for a contig of runs in the form of a pandas DataFrame. @@ -5141,7 +5288,7 @@ def warping_records_dataframe(self, run_idxs): return pd.DataFrame(self.warping_records(run_idxs)) # boundary conditions - def bc_records(self, run_idxs): + def bc_records(self, run_idxs: list[int]) -> list[RunRecord]: """Get the records this record group for the contig that is formed by the run indices. @@ -5164,7 +5311,7 @@ def bc_records(self, run_idxs): return self.run_contig_records(run_idxs, BC) - def bc_records_dataframe(self, run_idxs): + def bc_records_dataframe(self, run_idxs: list[int]) -> pd.DataFrame: """Get the records for this record group for a contig of runs in the form of a pandas DataFrame. @@ -5183,7 +5330,7 @@ def bc_records_dataframe(self, run_idxs): return pd.DataFrame(self.bc_records(run_idxs)) # progress - def progress_records(self, run_idxs): + def progress_records(self, run_idxs: list[int]) -> list[RunRecord]: """Get the records this record group for the contig that is formed by the run indices. @@ -5206,7 +5353,7 @@ def progress_records(self, run_idxs): return self.run_contig_records(run_idxs, PROGRESS) - def progress_records_dataframe(self, run_idxs): + def progress_records_dataframe(self, run_idxs: list[int]) -> pd.DataFrame: """Get the records for this record group for a contig of runs in the form of a pandas DataFrame. @@ -5224,23 +5371,7 @@ def progress_records_dataframe(self, run_idxs): return pd.DataFrame(self.progress_records(run_idxs)) - def run_resampling_panel(self, run_idx): - """Generate a resampling panel from the resampling records of a run. - - Parameters - ---------- - run_idx : int - - Returns - ------- - resampling_panel : list of list of list of namedtuple records - The panel (list of tables) of resampling records in order - (cycle, step, walker) - - """ - return self.run_contig_resampling_panel([run_idx]) - - def run_contig_resampling_panel(self, run_idxs): + def run_contig_resampling_panel(self, run_idxs: list[int]) -> DecisionPanel: """Generate a resampling panel from the resampling records of a contig, which is a series of runs. @@ -5252,7 +5383,7 @@ def run_contig_resampling_panel(self, run_idxs): Returns ------- - resampling_panel : list of list of list of namedtuple records + resampling_panel : list of list of list of tuples The panel (list of tables) of resampling records in order (cycle, step, walker) @@ -5265,11 +5396,30 @@ def run_contig_resampling_panel(self, run_idxs): # make the resampling panel from the resampling records for the contig contig_resampling_panel = resampling_panel( - self.resampling_records(run_idxs), is_sorted=False + self.resampling_records(run_idxs), + is_sorted=False, ) return contig_resampling_panel + + def run_resampling_panel(self, run_idx: int) -> DecisionPanel: + """Generate a resampling panel from the resampling records of a run. + + Parameters + ---------- + run_idx : int + + Returns + ------- + resampling_panel : list of list of list of namedtuple records + The panel (list of tables) of resampling records in order + (cycle, step, walker) + + """ + return self.run_contig_resampling_panel([run_idx]) + + # Trajectory Field Setters def add_run_observable(self, run_idx, observable_name, data, sparse_idxs=None): @@ -5292,6 +5442,7 @@ def add_run_observable(self, run_idx, observable_name, data, sparse_idxs=None): self._add_run_field(run_idx, obs_path, data, sparse_idxs=sparse_idxs) + def add_traj_observable(self, observable_name, data, sparse_idxs=None): """Add a trajectory sub-field in the compound field "observables" for an entire file, on a trajectory basis. @@ -5523,12 +5674,7 @@ def get_traj_field(self, run_idx, traj_idx, field_path, frames=None, masked=True """ - traj_path = "{}/{}/{}/{}".format(RUNS, run_idx, TRAJECTORIES, traj_idx) - - # if the field doesn't exist return None - if field_path not in self._h5[traj_path]: - raise KeyError("key for field {} not found".format(field_path)) - # return None + field_thing = self.traj_field_entity(run_idx, traj_idx, field_path) # get the field depending on whether it is sparse or not if field_path in self.sparse_fields: diff --git a/src/wepy/reporter/hdf5.py b/src/wepy/reporter/hdf5.py index 0513701d..93217157 100644 --- a/src/wepy/reporter/hdf5.py +++ b/src/wepy/reporter/hdf5.py @@ -26,13 +26,13 @@ from wepy.resampling.resamplers.resampler import Resampler from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.typing import Shape, Idxs, IdxArray -from wepy.storage.protocol import Record +from wepy.storage.protocol import Record, ResamplingRecord from wepy.runners.openmm import OPENMM_DEFAULT_UNITS logger = logging.getLogger(__name__) WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) -ResamplingRecord_ = TypeVar("ResamplingRecord_", bound=Record) +ResamplingRecord_ = TypeVar("ResamplingRecord_", bound=ResamplingRecord) ResamplerRecord_ = TypeVar("ResamplerRecord_", bound=Record) WarpingRecord_ = TypeVar("WarpingRecord_", bound=Record) diff --git a/src/wepy/resampling/decisions/decision.py b/src/wepy/resampling/decisions/decision.py index d5d245fe..04094c9d 100644 --- a/src/wepy/resampling/decisions/decision.py +++ b/src/wepy/resampling/decisions/decision.py @@ -67,10 +67,12 @@ def to_dict(self) -> dict[str, DecisionFieldDtype]: ... class BaseDecisionRecordDict(TypedDict): decision_id: int + target_idxs: tuple[int, ...] @attrs.define class BaseDecisionRecord: decision_id: int + target_idxs: tuple[int, ...] def to_dict(self) -> BaseDecisionRecordDict: return attrs.asdict(self) @@ -91,22 +93,21 @@ class BaseDecisionABC(Generic[DecisionEnum_, DecisionRecord_]): DECISION_RECORD: DecisionRecord_ = BaseDecisionRecord - FIELDS: tuple[str, ...] = ("decision_id",) + FIELDS: tuple[str, ...] = ("decision_id", "target_idxs",) """The names of the fields that go into the decision record.""" - # suggestion for subclassing, FIELDS and others - # FIELDS = super().FIELDS + ('target_idxs',) - # etc. - # An Ellipsis instead of fields indicate there is a variable # number of fields. - SHAPES: tuple[DecisionFieldShapeSpec, ...] = ((1,),) + SHAPES: tuple[DecisionFieldShapeSpec, ...] = ( + (1,), + Ellipsis, + ) """Field data shapes.""" - DTYPES: tuple[DecisionFieldDtype, ...] = (int,) + DTYPES: tuple[DecisionFieldDtype, ...] = (int, int,) """Field data types.""" - RECORD_FIELDS: tuple[str, ...] = ("decision_id",) + RECORD_FIELDS: tuple[str, ...] = ("decision_id", "target_idxs",) """The fields that could be used in a reduced table-like representation.""" ANCESTOR_DECISION_IDS: tuple[int, ...] @@ -211,36 +212,36 @@ def enum_by_name(cls, enum_name: str) -> DecisionEnum_: d = cls.enum_dict_by_name() return d[enum_name] - @classmethod - def record(cls, enum_value: int, **fields: dict[str, Any]) -> DecisionRecord_: - """Generate a record for the enum_value and the other fields. + # @classmethod + # def record(cls, enum_value: int, **fields: dict[str, Any]) -> DecisionRecord_: + # """Generate a record for the enum_value and the other fields. - Parameters - ---------- - enum_value : int + # Parameters + # ---------- + # enum_value : int - Returns - ------- - rec : dict of str: value + # Returns + # ------- + # rec : dict of str: value - """ + # """ - assert ( - enum_value in cls.enum_dict_by_value() - ), "value is not a valid Enumerated value" + # assert ( + # enum_value in cls.enum_dict_by_value() + # ), "value is not a valid Enumerated value" - for field_key in fields.keys(): - assert ( - field_key in cls.FIELDS - ), "The field {} is not a field for that decision".format(field_key) - assert field_key != "decision_id", "'decision_id' cannot be an extra field" + # for field_key in fields.keys(): + # assert ( + # field_key in cls.FIELDS + # ), "The field {} is not a field for that decision".format(field_key) + # assert field_key != "decision_id", "'decision_id' cannot be an extra field" - rec_d = {"decision_id": enum_value} - rec_d.update(fields) + # rec_d = {"decision_id": enum_value} + # rec_d.update(fields) - rec = cls.DECISION_RECORD(**rec_d) + # rec = cls.DECISION_RECORD(**rec_d) - return rec + # return rec @classmethod def action( diff --git a/src/wepy/resampling/decisions/no_decision.py b/src/wepy/resampling/decisions/no_decision.py index 7687648b..2a4a3e81 100644 --- a/src/wepy/resampling/decisions/no_decision.py +++ b/src/wepy/resampling/decisions/no_decision.py @@ -18,12 +18,12 @@ class NothingDecisionEnum(IntEnum): class NoDecisionRecordDict(TypedDict): decision_id: int - target_idx: int + target_idx: tuple[int, ...] @attrs.define class NoDecisionRecord(BaseDecisionRecord): decision_id: int = attrs.field() - target_idx: int = attrs.field(validator=attrs.validators.ge(0)) + target_idxs: tuple[int, ...] = attrs.field() @decision_id.validator def _check_decision_id(self, attribute, value) -> None: @@ -31,6 +31,21 @@ def _check_decision_id(self, attribute, value) -> None: if value != NothingDecisionEnum.NOTHING.value: raise ValueError(f"Invalid decision_id ({value}) must be {NothingDecisionEnum.NOTHING.value}") + @target_idxs.validator + def _check_target_idxs(self, attribute, value) -> None: + + if len(value) < 1: + raise ValueError( + f"'target_idxs' must have at least one entry." + ) + + if any(idx < 0 for idx in value): + + raise ValueError( + f"'target_idxs' values must be non-negative, received: {value}" + ) + + def to_dict(self) -> NoDecisionRecordDict: return attrs.asdict(self) @@ -40,12 +55,13 @@ class NoDecision(BaseDecisionABC): ENUM = NothingDecisionEnum DEFAULT_DECISION = ENUM.NOTHING + DECISION_RECORD = NoDecisionRecord - FIELDS = BaseDecisionABC.FIELDS + ("target_idxs",) - SHAPES = BaseDecisionABC.SHAPES + (Ellipsis,) - DTYPES = BaseDecisionABC.DTYPES + (int,) + FIELDS = BaseDecisionABC.FIELDS + SHAPES = BaseDecisionABC.SHAPES + DTYPES = BaseDecisionABC.DTYPES - RECORD_FIELDS = BaseDecisionABC.RECORD_FIELDS + ("target_idxs",) + RECORD_FIELDS = BaseDecisionABC.RECORD_FIELDS ANCESTOR_DECISION_IDS = (ENUM.NOTHING.value,) @@ -60,21 +76,22 @@ def action( # go through each decision and perform the decision # instructions for step_idx, step_recs in enumerate(decisions): - for walker_idx, decision in enumerate(step_recs): + for walker_idx, decision_record in enumerate(step_recs): + + if decision_record.decision_id == cls.ENUM.NOTHING.value: - if decision.decision_id == cls.ENUM.NOTHING.value: + target_idx = decision_record.target_idxs[0] + # check to make sure a walker doesn't already exist # where you are going to put it - if mod_walkers[decision.target_idx] is not None: + if mod_walkers[target_idx] is not None: raise ValueError( - "Multiple walkers assigned to position {}".format( - decision.target_idx - ) + f"Multiple walkers assigned to position {target_idx}" ) # put the walker in the position specified by the # instruction - mod_walkers[decision.target_idx] = walkers[walker_idx] + mod_walkers[target_idx] = walkers[walker_idx] return mod_walkers @@ -84,6 +101,6 @@ def parents(cls, step: list[NoDecisionRecord]) -> list[int]: step_parents = [None for i in range(len(step))] for parent_idx, parent_rec in enumerate(step): - step_parents[parent_rec.target_idx] = parent_idx + step_parents[parent_rec.target_idxs[0]] = parent_idx return step_parents diff --git a/src/wepy/resampling/resamplers/clone_merge.py b/src/wepy/resampling/resamplers/clone_merge.py index 2123e2ae..ec253611 100644 --- a/src/wepy/resampling/resamplers/clone_merge.py +++ b/src/wepy/resampling/resamplers/clone_merge.py @@ -19,8 +19,10 @@ from wepy.util.attrs import AttrsMappingMixin from wepy.typing import Shape +from wepy.storage.protocol import ResamplingRecord + @attrs.define -class CloneMergeResamplingRecord(AttrsMappingMixin): +class CloneMergeResamplingRecord(AttrsMappingMixin, ResamplingRecord): # from the Decision decision_id: Annotated[ NDArray[np.int64], diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py index 6a1c6200..e2ed24d2 100644 --- a/src/wepy/resampling/resamplers/noresampler.py +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -15,23 +15,34 @@ from wepy.walker import Walker from wepy.util.attrs import AttrsMappingMixin from wepy.resampling.decisions.no_decision import NoDecisionRecord +from wepy.storage.protocol import ResamplingRecord @attrs.define -class NoResamplerResamplingRecord(AttrsMappingMixin): - decision_id: Annotated[ - NDArray[np.int64], - Shape((1,)), - ] - # NOTE,UGLY: It isn't strictly necessary to have multiple target - # indices for this type of resampling record to have all the - # information, but all of the downstream infrastucture for - # interpreting them relies on there being multiple indices so we - # don't want to break this for this record that is only used for - # troubleshooting really. - target_idxs: Annotated[ - NDArray[np.int64], - Shape((1,1,)), - ] +class NoResamplerResamplingRecord(AttrsMappingMixin, ResamplingRecord): + + # TODO: remove this once new interface is stable + # + # decision_id: Annotated[ + # NDArray[np.int64], + # Shape((1,)), + # ] + # target_idxs: Annotated[ + # NDArray[np.int64], + # Shape((1,1,)), + # ] + # step_idx: Annotated[ + # NDArray[np.int64], + # Shape((1,)), + # ] + # walker_idx: Annotated[ + # NDArray[np.int64], + # Shape((1,)), + # ] + + decision_id: int + target_idxs: tuple[int, ...] + step_idx: int + walker_idx: int @attrs.define @@ -69,9 +80,11 @@ def resample( # UGLY: we need to wrap the field data into the shape walker_record = NoResamplerResamplingRecord( - decision_id=np.array([NothingDecisionEnum.NOTHING.value]), + decision_id=NothingDecisionEnum.NOTHING.value, # NOTE: two dimensions to match the target_idxs shape - target_idxs=np.array([[walker_idx]]), + target_idxs=(walker_idx,), + walker_idx=walker_idx, + step_idx=0, ) _resampling_data.append(walker_record) diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index dbfc00ec..2f3064b8 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -867,12 +867,10 @@ def resample( resampling_record = CloneMergeResamplingRecord( # The decision record fields are simple, so we wrap # them here as well - decision_id=np.array([decision_record.decision_id]), - target_idxs=np.array([ - np.array(decision_record.target_idxs), - ]), - step_idx=np.array([0]), - walker_idx=np.array([walker_idx]) + decision_id=decision_record.decision_id, + target_idxs=decision_record.target_idxs, + step_idx=0, + walker_idx=walker_idx ) resampling_records.append(resampling_record) @@ -881,7 +879,7 @@ def resample( resampler_records = [ REVOResamplerResamplerRecord( distance_matrix=np.ravel(np.array(distance_matrix)), - variation=np.array([[variation]]), + variation=variation, ) ] diff --git a/src/wepy/storage/protocol.py b/src/wepy/storage/protocol.py index 427a4647..c0d5fd20 100644 --- a/src/wepy/storage/protocol.py +++ b/src/wepy/storage/protocol.py @@ -7,12 +7,18 @@ """ from collections.abc import Mapping from numpy.typing import NDArray -from typing import Union, Literal +from typing import Union, Literal, TypedDict, Required import numpy as np +import attrs RecordValueDtype = int | float | NDArray + Record = Mapping[str, RecordValueDtype] +@attrs.define +class RunRecord: + cycle_idx: Required[int] + record: Record # Numpy-style shapes of all fields produced in records. # @@ -31,8 +37,9 @@ # # Option B will result in the special h5py datatype 'vlen' and # should not be used for large datasets for efficiency reasons. +RecordFieldShape = tuple[int, ...] RecordFieldShapeSpec = Union[ - tuple[int, ...], + RecordFieldShape, Literal[Ellipsis], ] @@ -65,3 +72,54 @@ RecordFieldShapeSpec, # shape RecordFieldDtype, # dtype ] + + +# Specific record types guaranteed + +DECISION_RECORD_FIELDS = frozenset({ + "decision_id", + "target_idxs", +}) + +class DecisionRecordUnstruct(TypedDict, total=False): + decision_id: Required[int] + target_idxs: Required[tuple[int, ...]] + + +RESAMPLING_RECORD_FIELDS = frozenset({ + "step_idx", + "walker_idx", + "decision_id", + "target_idxs", +}) + +@attrs.define +class ResamplingRecord: + step_idx: int + walker_idx: int + decision_id: int + target_idxs: tuple[int, ...] + +class ResamplingRecordUnstruct(TypedDict, total=False): + step_idx: Required[int] + walker_idx: Required[int] + decision_id: Required[int] + target_idxs: Required[tuple[int, ...]] + +WARPING_RECORD_FIELDS = frozenset({ + "walker_idx", + "target_idx", + "weight", +}) + +@attrs.define +class WarpRecord: + walker_idx: int + target_idx: int + weight: float + +class WarpRecordUnstruct(TypedDict, total=False): + walker_idx: Required[int] + target_idx: Required[int] + weight: Required[float] + diff --git a/tests/unit/test_analysis/test_contig_tree.py b/tests/unit/test_analysis/test_contig_tree.py index 1af634ae..29d98a81 100644 --- a/tests/unit/test_analysis/test_contig_tree.py +++ b/tests/unit/test_analysis/test_contig_tree.py @@ -12,8 +12,13 @@ class Test_BaseContigTree: def test___init__(self, alanine_dipeptide_revo_wepy_hdf5: Path): wepy_h5 = wepy.WepyHDF5(alanine_dipeptide_revo_wepy_hdf5, mode='r') - wepy_h5.open() - wepy_h5.close() + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.MultiCloneMergeDecision, + ) + + # TODO: test multiple runs/files class Test_ContigTree: pass diff --git a/tests/unit/test_analysis/test_parents.py b/tests/unit/test_analysis/test_parents.py new file mode 100644 index 00000000..e1d4883a --- /dev/null +++ b/tests/unit/test_analysis/test_parents.py @@ -0,0 +1,312 @@ +from typing import NamedTuple +from wepy.analysis.parents import ( + resampling_panel, + parent_panel, + net_parent_table, + parent_table_discontinuities, + parent_cycle_discontinuities, + ancestors, + sliding_window, + ParentForest, +) +from wepy.storage.protocol import RunRecord +from wepy.resampling.decisions.no_decision import NoDecision + +def test_resampling_panel(): + + + # simple case + assert resampling_panel( + [ + RunRecord( + cycle_idx=0, + record=dict( + step_idx=0, + walker_idx=0, + decision_id=0, + target_idxs=(0,), + ) + ), + RunRecord( + cycle_idx=0, + record=dict( + step_idx=0, + walker_idx=1, + decision_id=0, + target_idxs=(1,), + ) + ), + ] + ) == [ + # cycle 0 + [ + # step 0 + [ + # walker 0 + { + "decision_id" : 0, + "target_idxs" : (0,) + }, + # walker 1 + { + "decision_id" : 0, + "target_idxs" : (1,) + }, + ] + ] + ] + + # TODO: failing + # with multiple steps + assert resampling_panel( + [ + # step 0 + RunRecord( + cycle_idx=0, + record=dict( + step_idx=0, + walker_idx=0, + decision_id=0, + target_idxs=(0,),) + ), + RunRecord( + cycle_idx=0, + record=dict( + step_idx=0, + walker_idx=1, + decision_id=0, + target_idxs=(1,),) + ), + + # step 1 + RunRecord( + cycle_idx=0, + record=dict( + step_idx=1, + walker_idx=0, + decision_id=0, + target_idxs=(1,),) + ), + RunRecord( + cycle_idx=0, + record=dict( + step_idx=1, + walker_idx=1, + decision_id=0, + target_idxs=(0,),) + ), + ] + ) == [ + # cycle 0 + [ + # step 0 + [ + # walker 0 + { + "decision_id" : 0, + "target_idxs" : (0,) + }, + # walker 1 + { + "decision_id" : 0, + "target_idxs" : (1,) + }, + ], + # step 1 + [ + # walker 0 + { + "decision_id" : 0, + "target_idxs" : (1,) + }, + # walker 1 + { + "decision_id" : 0, + "target_idxs" : (0,) + }, + ], + ] + ] + +def test_parent_panel(): + + assert parent_panel( + NoDecision, + [ + # cycle 0 + [ + # step 0 + [ + # walker 0 + { + "decision_id" : 0, + "target_idxs" : (0,) + }, + # walker 1 + { + "decision_id" : 0, + "target_idxs" : (1,) + } + ], + ], + ] + ) == [ + [ + [0, 1], + ] + ] + + assert parent_panel( + NoDecision, + [ + # cycle 0 + [ + # step 0 + [ + # walker 0 + { + "decision_id" : 0, + "target_idxs" : (0,) + }, + # walker 1 + { + "decision_id" : 0, + "target_idxs" : (1,) + } + ], + # step 1 + [ + # walker 0 + { + "decision_id" : 0, + "target_idxs" : (1,) + }, + # walker 1 + { + "decision_id" : 0, + "target_idxs" : (0,) + } + ], + ], + ] + ) == [ + [ + [0, 1], + [1, 0], + ] + ] + +def test_net_parent_table(): + + net_parent_table([ + [ + [0, 1], + ] + ]) == [ + # cycle 0 + [ + 0, 1 + ] + ] + + + net_parent_table([ + [ + [0, 1], + [1, 0], + ] + ]) == [ + # cycle 0 + [ + 1, 0 + ] + ] + +# TODO: tests for discontinuities +# +# def test_parent_table_discontinuities(): +# pass + +# def test_parent_cycle_discontinuities(): +# pass + +def test_ancestors(): + + assert ancestors( + [ + [0, 1], + ], + cycle_idx=0, + walker_idx=0, + ancestor_cycle=0, + ) == [ + (0, 0), + ] + + assert ancestors( + [ + [0, 1], + [0, 1], + ], + cycle_idx=1, + walker_idx=0, + ancestor_cycle=0, + ) == [ + (0, 0), + (0, 1), + ] + + assert ancestors( + [ + [0, 1], + [0, 1], + ], + cycle_idx=1, + walker_idx=0, + ancestor_cycle=1, + ) == [ + (0, 1), + ] + + assert ancestors( + [ + [0, 1], + [0, 1], + [1, 0], + [1, 0], + ], + cycle_idx=3, + walker_idx=0, + ancestor_cycle=0, + ) == [ + (1, 0), + (1, 1), + (1, 2), + (0, 3), + ] + + assert ancestors( + [ + [0, 1], + [0, 1], + [1, 0], + [1, 0], + ], + cycle_idx=3, + walker_idx=1, + ancestor_cycle=0, + ) == [ + (0, 0), + (0, 1), + (0, 2), + (1, 3), + ] + +# TODO: test this +# +# def test_sliding_window(): +# pass + +# TODO: need Contig for this to work +class Test_ParentForest: + pass diff --git a/tests/unit/test_hdf5.py b/tests/unit/test_hdf5.py index efb49529..938529f0 100644 --- a/tests/unit/test_hdf5.py +++ b/tests/unit/test_hdf5.py @@ -1,5 +1,8 @@ from pathlib import Path import json +import sys +import shutil +import subprocess from typing import Callable import pytest import numpy as np @@ -7,28 +10,71 @@ import h5py +from wepy.typing import IdxArray from wepy.hdf5 import ( numpy_dtype_to_json, dtype_json_to_numpy, WepyHDF5, _iter_field_paths, + WepyHDF5Error, + WepyHDF5WriteError, + WepyHDF5ReadError, ) +from wepy.storage.protocol import RunRecord from wepy.walker import Walker, WalkerStateBox +from wepy.resampling.decisions.no_decision import ( + NoDecision, +) +from wepy.resampling.resamplers.noresampler import NoResampler, NoResamplerResamplingRecord from wepy_tools.systems.lennard_jones import LennardJonesPair +def reflink_or_copy(src: Path, dst: Path) -> None: + """ + Create a copy-on-write reflink if supported. + Fall back to a full copy otherwise. + """ + try: + if sys.platform.startswith("linux"): + subprocess.run( + ["cp", "--reflink=auto", src, dst], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + elif sys.platform == "darwin": + subprocess.run( + ["cp", "-c", src, dst], # APFS clone + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + else: + raise RuntimeError("No reflink support") + except Exception: + shutil.copy2(src, dst) + + @pytest.fixture(scope="session") -def wepy_h5_factory() -> Callable[[Path], WepyHDF5]: +def wepy_h5_factory() -> Callable[[Path], Path]: test_sys = LennardJonesPair() - def _factory(path: Path) -> Path: + def _factory( + path: Path, + sparse_fields: tuple[str, ...] | None = None, + alt_reps: dict[str, IdxArray] | None = None, + main_rep_idxs: IdxArray | None = None, + ) -> Path: # create the file WepyHDF5( path, mode="x", topology=test_sys.json_top, + sparse_fields=sparse_fields, + alt_reps=alt_reps, + main_rep_idxs=main_rep_idxs, ) return path @@ -53,18 +99,290 @@ def _factory(path: Path) -> Path: ), ] -# @pytest.fixture(scope="session") -# def wepy_h5_file_ro(tmpdir) -> WepyHDF5: -# """Read-only WepyHDF5""" +@pytest.fixture(scope="session") +def _wepy_h5_run_init(wepy_h5_factory, tmp_path_factory) -> Path: + """Data generation fixture, should not be used by individual tests + as it is slow. Instead use the fixture that makes a copy for + read/write in each test function. + + """ + + d = tmp_path_factory.mktemp("wepy_h5_run_init") + + # initialize a file + path = wepy_h5_factory( + d / "main.wepy.h5", + sparse_fields={"velocities"}, + ) + + # initialize the file + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + wepy_h5.init_run_fields_resampling_decision( + 0, + NoDecision.enum_dict_by_name(), + ) + wepy_h5.init_run_fields_resampling( + 0, + NoResampler.resampling_fields(), + ) + wepy_h5.init_record_fields( + "resampling", + [name for name, _, _ in NoResampler.resampling_fields()], + ) + + # UGLY: synthetic examples of warping without a real class to use + + # warping as it has hardcoded behavior that should be tested + warp_fields = [ + ("walker_idx", (1,), int), + ("target_idx", (1,), int), + ("weight", (1,), float), + ] + wepy_h5.init_run_fields_warping( + 0, + warp_fields, + ) + wepy_h5.init_record_fields( + "warping", + [name for name, _, _ in warp_fields], + ) + + # minimal progress fields for testing continual records + progress_fields = [ + # single number for a cycle + ("ensemble_average", (1,), float), + # per-walker data + ("walker_distances", Ellipsis, float), + ] + wepy_h5.init_run_fields_progress( + 0, + progress_fields, + ) + wepy_h5.init_record_fields( + "progress", + [name for name, _, _ in progress_fields], + ) + + + # TODO: more fields for resampler records and BC + # records. These are always optional and strictly accessory so + # holding off on writing more test cases on these. + + return path + +def test__wepy_h5_run_init(_wepy_h5_run_init): + + with WepyHDF5(_wepy_h5_run_init, mode='r') as wepy_h5: + + wepy_h5.run(0) + + assert wepy_h5.num_run_trajs(0) == 0 + + wepy_h5.resampling_grp(0) + wepy_h5.decision_grp(0) + wepy_h5.warping_grp(0) + wepy_h5.progress_grp(0) + + assert "resampling" in wepy_h5.record_fields + assert wepy_h5.record_fields["resampling"] == [ + "decision_id", + "target_idxs", + "step_idx", + "walker_idx", + ] + + assert "warping" in wepy_h5.record_fields + assert wepy_h5.record_fields["warping"] == [ + "walker_idx", + "target_idx", + "weight" + ] + + assert "progress" in wepy_h5.record_fields + assert wepy_h5.record_fields["progress"] == [ + "ensemble_average", + "walker_distances", + ] + +@pytest.fixture(scope="function") +def wepy_h5_run_init(_wepy_h5_run_init, tmpdir) -> Path: + + path = tmpdir / "main.wepy.h5" + + reflink_or_copy(_wepy_h5_run_init, path) + + return path + +# test that each test gets its own copy +def test_wepy_h5_run_init_1(wepy_h5_run_init): + + with WepyHDF5(wepy_h5_run_init, mode='r') as wepy_h5: + assert "mutation_flag" not in wepy_h5.h5 + + # mutate + with WepyHDF5(wepy_h5_run_init, mode='r+') as wepy_h5: + wepy_h5.h5["mutation_flag"] = np.array([0]) + +def test_wepy_h5_run_init_2(wepy_h5_run_init): + + with WepyHDF5(wepy_h5_run_init, mode='r') as wepy_h5: + assert "mutation_flag" not in wepy_h5.h5 + + # mutate + with WepyHDF5(wepy_h5_run_init, mode='r+') as wepy_h5: + wepy_h5.h5["mutation_flag"] = np.array([0]) + + +@pytest.fixture(scope="session") +def _wepy_h5_traj_init(_wepy_h5_run_init, tmp_path_factory) -> Path: + """Data generation fixture, should not be used by individual tests + as it is slow. Instead use the fixture that makes a copy for + read/write in each test function. + + """ + + # make a copy of the run init H5 file and then mutate to add stuff + d = tmp_path_factory.mktemp("wepy_h5_traj_init") + + path = d / "main.wepy.h5" + shutil.copy( + _wepy_h5_run_init, + path, + ) + + # Add the new data + with WepyHDF5(path, mode="r+") as wepy_h5: + + traj0_grp = wepy_h5.add_traj( + 0, + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + weights=np.array([[0.2]]), + metadata={"foo" : "hello"}, + ) + + traj1_grp = wepy_h5.add_traj( + 0, + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + weights=np.array([[0.2]]), + metadata={"foo" : "hello"}, + ) + + wepy_h5.extend_cycle_resampling_records( + 0, + 0, + [ + # NOTE: that this function requires mappings, and + # these record types double as mappings via the mixin, + # so we just use them + NoResamplerResamplingRecord( + decision_id=0, + target_idxs=[0], + walker_idx=0, + step_idx=0, + ), + NoResamplerResamplingRecord( + decision_id=0, + target_idxs=[1], + walker_idx=1, + step_idx=0, + ), + ] + ) + + wepy_h5.extend_cycle_progress_records( + 0, + 0, + [ + # only a single record for the cycle + { + "ensemble_average" : 1.2, + "walker_distances" : [ + 1., 1., + ], + }, + ] + ) + + # TODO: the other record groups + + return path + +def test__wepy_h5_traj_init(_wepy_h5_traj_init): + + with WepyHDF5(_wepy_h5_traj_init, mode='r') as wepy_h5: + + assert "velocities" in wepy_h5.sparse_fields + + wepy_h5.run(0) + + assert len(wepy_h5.resampling_records([0])) == 2 + assert len(wepy_h5.progress_records([0])) == 1 + + assert wepy_h5.num_run_trajs(0) == 2 + + for traj_idx in (0, 1): + assert "weights" in wepy_h5.traj(0, traj_idx) + assert "positions" in wepy_h5.traj(0, traj_idx) + assert "box_vectors" in wepy_h5.traj(0, traj_idx) + assert "kinetic_energy" in wepy_h5.traj(0, traj_idx) + assert "velocities" in wepy_h5.traj(0, traj_idx) + + assert wepy_h5.traj_field_entity(0, traj_idx, "weights").shape == (1, 1) + assert wepy_h5.traj_field_entity(0, traj_idx, "positions").shape == (1, 2, 3) + assert wepy_h5.traj_field_entity(0, traj_idx, "box_vectors").shape == (1, 3, 3) + assert wepy_h5.traj_field_entity(0, traj_idx, "kinetic_energy").shape == (1, 1) + + assert "data" in wepy_h5.traj_field_entity(0, traj_idx, "velocities") + assert "_sparse_idxs" in wepy_h5.traj_field_entity(0, traj_idx, "velocities") + + assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")["data"].shape == (0, 0, 0) + assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")["data"].maxshape == (None, 2, 3) + + assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")["_sparse_idxs"].shape == (0,) + + + +@pytest.fixture(scope="function") +def wepy_h5_traj_init(_wepy_h5_traj_init, tmpdir) -> Path: + + path = tmpdir / "main.wepy.h5" -# return gen_wepy_h5(Path(tmpdir) / "main.wepy.h5") + reflink_or_copy(_wepy_h5_traj_init, path) -# @pytest.fixture(scope="function") -# def wepy_h5_file_rw(tmpdir): -# """Read-write WepyHDF5""" -# return gen_wepy_h5( -# Path(tmpdir) / "main.wepy.h5", -# ) + return path + + # # TODO: this will be easier once we have a fixture for a full WepyHDF5 @@ -598,32 +916,20 @@ def test__create_init(self, tmp_path_factory): assert len(h5["units"]) == 1 assert h5["units/positions"][()].decode() == "nanometer" - def test_num_runs(self, wepy_h5_factory, tmpdir): + def test_settings_grp(self, wepy_h5_factory, tmpdir): path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") with WepyHDF5(path, mode="r+") as wepy_h5: - assert wepy_h5.num_runs == 0 - - wepy_h5.h5["runs"].create_group("0") - assert wepy_h5.num_runs == 1 - - - def test_next_run_idx(self, wepy_h5_factory, tmpdir): + assert wepy_h5.settings_grp == wepy_h5.h5["_settings"] - path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") - with WepyHDF5(path, mode="r+") as wepy_h5: - assert wepy_h5.next_run_idx() == 0 - wepy_h5.h5["runs"].create_group("0") - assert wepy_h5.next_run_idx() == 1 + def test__add_init_walkers(self, wepy_h5_factory, tmpdir): + assert False - # TODO: see new_run test for the same effect - # def test__add_init_walkers(self, wepy_h5_factory, tmpdir): - # pass + def test__add_run_init(self, wepy_h5_factory, tmpdir): + pass - # def test__add_run_init(self, wepy_h5_factory, tmpdir): - # pass def test_new_run(self, wepy_h5_factory, tmpdir): @@ -791,22 +1097,43 @@ def test__init_run_sporadic_record_grp(self, wepy_h5_factory, tmpdir): assert "_cycle_idxs" in record_grp assert record_grp["_cycle_idxs"].shape == (0,) + assert record_grp["_cycle_idxs"].maxshape == (None,) assert record_grp["_cycle_idxs"].dtype == np.int64 assert "a" in record_grp assert "b" in record_grp - # TODO: - # def test__init_run_continual_record_grp(self, wepy_h5_factory, tmpdir): - # path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") - # with WepyHDF5(path, mode="r+") as wepy_h5: + def test__init_run_continual_record_grp(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + record_grp = wepy_h5._init_run_continual_record_grp( + 0, + "example", + ( + ("a", (3, 3,), np.float32), + ("b", Ellipsis, np.int32), + ), + ) + + assert "example" in run_grp - # run_grp = wepy_h5.new_run( - # init_walkers=_INIT_WALKERS - # ) + assert "_cycle_idxs" not in record_grp + assert "a" in record_grp + assert "b" in record_grp - # wepy_h5.init_run_continual_record_grp(0, "example", ("a", "b"),) + assert record_grp["a"].shape == (0, 3, 3) + assert record_grp["a"].maxshape == (None, 3, 3) + assert record_grp["a"].dtype == np.float32 + assert record_grp["b"].shape == (0,) + assert record_grp["b"].maxshape == (None,) + assert h5py.check_vlen_dtype(record_grp["b"].dtype) == np.int32 + def test__is_sporadic_records(self): assert WepyHDF5._is_sporadic_records("resampler") @@ -892,171 +1219,640 @@ def test_init_run_fields_resampling(self, wepy_h5_factory, tmpdir): # def test_init_run_fields_bc(self): # pass - def test__extend_run_record_data_field(self, wepy_h5_factory, tmpdir): + + + def test_runs(self, wepy_h5_factory, tmpdir): path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") with WepyHDF5(path, mode="r+") as wepy_h5: + assert wepy_h5.runs == wepy_h5.h5["runs"] + + def test_run(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.run(0) run_grp = wepy_h5.new_run( init_walkers=_INIT_WALKERS ) - resampling_grp = wepy_h5.init_run_fields_resampling( - 0, - [ - ("decision_id", (1,), np.uint32), - ("target_idxs", Ellipsis, np.uint32), - ("step_idx", (1,), np.uint32), - ("walker_idx", (1,), np.uint32), - ], - ) + assert wepy_h5.run(0) == run_grp - assert resampling_grp["decision_id"].shape == (0,1) - wepy_h5._extend_run_record_data_field( - 0, - "resampling", - "decision_id", - np.array([[0]]), - ) + def test_decision_grp(self, wepy_h5_factory, tmpdir): - assert resampling_grp["decision_id"].shape == (1,1) - assert np.array_equal( - resampling_grp["decision_id"][:], - np.array([ - [0] - ]), + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS ) + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.decision_grp(0) - wepy_h5._extend_run_record_data_field( + decision_grp = wepy_h5.init_run_fields_resampling_decision( 0, - "resampling", - "decision_id", - np.array([[0]]), + NoDecision.enum_dict_by_name(), ) - assert resampling_grp["decision_id"].shape == (2,1) - assert np.array_equal( - resampling_grp["decision_id"][:], - np.array([ - [0], - [0], - ]), - ) - - - def test_extend_cycle_run_group_records(self, wepy_h5_factory, tmpdir): + assert wepy_h5.decision_grp(0) == run_grp["decision"] + + def test_init_walkers_grp(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") with WepyHDF5(path, mode="r+") as wepy_h5: + with pytest.raises(WepyHDF5ReadError): + wepy_h5.init_walkers_grp(0) + run_grp = wepy_h5.new_run( init_walkers=_INIT_WALKERS ) - resampling_grp = wepy_h5.init_run_fields_resampling( - 0, - [ - ("decision_id", (1,), np.uint32), - ("target_idxs", Ellipsis, np.uint32), - ("step_idx", (1,), np.uint32), - ("walker_idx", (1,), np.uint32), - ], - ) + assert wepy_h5.init_walkers_grp(0) == run_grp["init_walkers"] - assert resampling_grp["_cycle_idxs"].shape[0] == 0 - assert resampling_grp["decision_id"].shape[0] == 0 - assert resampling_grp["target_idxs"].shape[0] == 0 + def test_records_grp(self, wepy_h5_factory, tmpdir): - wepy_h5.extend_cycle_run_group_records( - 0, - "resampling", - 0, - [ - { - "decision_id" : np.array([[0]]), - "target_idxs" : np.array([[[0]]]), - "step_idx" : np.array([[0]]), - "walker_idx" : np.array([[0]]), - }, - { - "decision_id" : np.array([[0]]), - "target_idxs" : np.array([[[0]]]), - "step_idx" : np.array([[0]]), - "walker_idx" : np.array([[1]]), - }, - ] + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + with pytest.raises(KeyError): + wepy_h5.records_grp(0, "notarecordgroup") + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.records_grp(0, "resampling") + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS ) - assert resampling_grp["decision_id"].shape == (2,1) - assert resampling_grp["target_idxs"].shape == (2,) - assert resampling_grp["step_idx"].shape == (2,1) - assert resampling_grp["walker_idx"].shape == (2,1) + with pytest.raises(KeyError): + wepy_h5.records_grp(0, "trajectories") + + run_grp.create_group("resampling") + run_grp.create_group("resampler") + run_grp.create_group("warping") + run_grp.create_group("boundary_conditions") + run_grp.create_group("progress") + assert wepy_h5.records_grp(0, "resampling") == run_grp["resampling"] + assert wepy_h5.records_grp(0, "resampler") == run_grp["resampler"] + assert wepy_h5.records_grp(0, "warping") == run_grp["warping"] + assert wepy_h5.records_grp(0, "boundary_conditions") == run_grp["boundary_conditions"] + assert wepy_h5.records_grp(0, "progress") == run_grp["progress"] + + def test_resampling_grp(self, wepy_h5_factory, tmpdir): - def test_extend_cycle_resampling_records(self, wepy_h5_factory, tmpdir): path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") with WepyHDF5(path, mode="r+") as wepy_h5: + with pytest.raises(WepyHDF5ReadError): + wepy_h5.resampling_grp(0) + run_grp = wepy_h5.new_run( init_walkers=_INIT_WALKERS ) - resampling_grp = wepy_h5.init_run_fields_resampling( - 0, - [ - ("decision_id", (1,), np.uint32), - ("target_idxs", Ellipsis, np.uint32), - ("step_idx", (1,), np.uint32), - ("walker_idx", (1,), np.uint32), - ], - ) + run_grp.create_group("resampling") + assert wepy_h5.resampling_grp(0) == run_grp["resampling"] - # UGLY,TOREV: This is ugly because the resampler then - # needs to handle processing the records into these deeply - # bracketed arrays. However, this is the interface and - # promised shapes given their interfaces in the - # e.g. Resampler components and all the downstream tools - # will rely on this structure so it must stay. - wepy_h5.extend_cycle_resampling_records( - 0, - 0, - [ - { - "decision_id" : np.array([[0]]), - "target_idxs" : np.array([[[0]]]), - "step_idx" : np.array([[0]]), - "walker_idx" : np.array([[0]]), - }, - { - "decision_id" : np.array([[0]]), - "target_idxs" : np.array([[[0]]]), - "step_idx" : np.array([[0]]), - "walker_idx" : np.array([[1]]), - }, - ] + def test_resampler_grp(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.resampler_grp(0) + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS ) - assert resampling_grp["decision_id"].shape == (2,1) - assert resampling_grp["target_idxs"].shape == (2,) - assert resampling_grp["step_idx"].shape == (2,1) - assert resampling_grp["walker_idx"].shape == (2,1) + run_grp.create_group("resampler") - + assert wepy_h5.records_grp(0, "resampler") == run_grp["resampler"] + def test_warping_grp(self, wepy_h5_factory, tmpdir): - def test_add_traj(self, wepy_h5_factory, tmpdir, monkeypatch): path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") with WepyHDF5(path, mode="r+") as wepy_h5: + with pytest.raises(WepyHDF5ReadError): + wepy_h5.warping_grp(0) + run_grp = wepy_h5.new_run( init_walkers=_INIT_WALKERS ) + run_grp.create_group("warping") + assert wepy_h5.records_grp(0, "warping") == run_grp["warping"] + def test_bc_grp(self, wepy_h5_factory, tmpdir): - traj_grp = wepy_h5.add_traj( - 0, - data={ - "positions" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), - "box_vectors" : np.array([[ + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.bc_grp(0) + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + run_grp.create_group("boundary_conditions") + assert wepy_h5.records_grp(0, "boundary_conditions") == run_grp["boundary_conditions"] + def test_progress_grp(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.progress_grp(0) + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + run_grp.create_group("progress") + assert wepy_h5.records_grp(0, "progress") == run_grp["progress"] + + def test_run_trajs(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.run_trajs(0) + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + assert wepy_h5.run_trajs(0) == run_grp["trajectories"] + + def test_traj(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.traj(0, 0) + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.traj(0, 0) + + traj_grp = run_grp["trajectories"].create_group("0") + + assert wepy_h5.traj(0, 0) == traj_grp + + def test_traj_field_entity(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.traj_field_entity(0, 0, "something") + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.traj_field_entity(0, 0, "something") + + traj_grp = run_grp["trajectories"].create_group("0") + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.traj_field_entity(0, 0, "something") + + field_grp = traj_grp.create_group("something") + + assert wepy_h5.traj_field_entity(0, 0, "something") == field_grp + + field_dset = traj_grp.create_dataset("dset", dtype=np.float32, shape=(0, 0)) + assert wepy_h5.traj_field_entity(0, 0, "dset") == field_dset + + + def test_num_runs(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + assert wepy_h5.num_runs == 0 + + wepy_h5.h5["runs"].create_group("0") + assert wepy_h5.num_runs == 1 + + def test_num_run_trajs(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.num_run_trajs(0) + + run_grp = wepy_h5.h5["runs"].create_group("0") + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.num_run_trajs(0) + + run_grp.create_group("trajectories/0") + + assert wepy_h5.num_run_trajs(0) == 1 + + run_grp.create_group("trajectories/1") + assert wepy_h5.num_run_trajs(0) == 2 + + + def test_next_run_idx(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + assert wepy_h5.next_run_idx() == 0 + wepy_h5.h5["runs"].create_group("0") + assert wepy_h5.next_run_idx() == 1 + + def test_next_run_traj_idx(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + with pytest.raises(WepyHDF5ReadError): + wepy_h5.next_run_traj_idx(0) + + run_grp = wepy_h5.h5["runs"].create_group("0") + run_grp.create_group("trajectories") + + assert wepy_h5.next_run_traj_idx(0) == 0 + + run_grp.create_group("trajectories/0") + + assert wepy_h5.next_run_traj_idx(0) == 1 + + run_grp.create_group("trajectories/1") + assert wepy_h5.next_run_traj_idx(0) == 2 + + def test__extend_run_record_data_field(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + resampling_grp = wepy_h5.init_run_fields_resampling( + 0, + [ + ("decision_id", (1,), np.uint32), + ("target_idxs", Ellipsis, np.uint32), + ("step_idx", (1,), np.uint32), + ("walker_idx", (1,), np.uint32), + ], + ) + + assert resampling_grp["decision_id"].shape == (0,1) + wepy_h5._extend_run_record_data_field( + 0, + "resampling", + "decision_id", + np.array([[0]]), + ) + + assert resampling_grp["decision_id"].shape == (1,1) + assert np.array_equal( + resampling_grp["decision_id"][:], + np.array([ + [0] + ]), + ) + + wepy_h5._extend_run_record_data_field( + 0, + "resampling", + "decision_id", + np.array([[0]]), + ) + + assert resampling_grp["decision_id"].shape == (2,1) + assert np.array_equal( + resampling_grp["decision_id"][:], + np.array([ + [0], + [0], + ]), + ) + + + def test_extend_cycle_run_group_records(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + resampling_grp = wepy_h5.init_run_fields_resampling( + 0, + [ + ("decision_id", (1,), np.uint32), + ("target_idxs", Ellipsis, np.uint32), + ("step_idx", (1,), np.uint32), + ("walker_idx", (1,), np.uint32), + ], + ) + + assert resampling_grp["_cycle_idxs"].shape[0] == 0 + assert resampling_grp["decision_id"].shape[0] == 0 + assert resampling_grp["target_idxs"].shape[0] == 0 + + wepy_h5.extend_cycle_run_group_records( + 0, + "resampling", + 0, + [ + { + "decision_id" : np.array([[0]]), + "target_idxs" : np.array([[[0]]]), + "step_idx" : np.array([[0]]), + "walker_idx" : np.array([[0]]), + }, + { + "decision_id" : np.array([[0]]), + "target_idxs" : np.array([[[0]]]), + "step_idx" : np.array([[0]]), + "walker_idx" : np.array([[1]]), + }, + ] + ) + + assert resampling_grp["decision_id"].shape == (2,1) + assert resampling_grp["target_idxs"].shape == (2,) + assert resampling_grp["step_idx"].shape == (2,1) + assert resampling_grp["walker_idx"].shape == (2,1) + + def test_extend_cycle_resampling_records(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + resampling_grp = wepy_h5.init_run_fields_resampling( + 0, + [ + ("decision_id", (1,), np.uint32), + ("target_idxs", Ellipsis, np.uint32), + ("step_idx", (1,), np.uint32), + ("walker_idx", (1,), np.uint32), + ], + ) + + # UGLY,TOREV: This is ugly because the resampler then + # needs to handle processing the records into these deeply + # bracketed arrays. However, this is the interface and + # promised shapes given their interfaces in the + # e.g. Resampler components and all the downstream tools + # will rely on this structure so it must stay. + wepy_h5.extend_cycle_resampling_records( + 0, + 0, + [ + { + "decision_id" : 0, + "target_idxs" : [0], + "step_idx" : 0, + "walker_idx" : 0, + }, + { + "decision_id" : 0, + "target_idxs" : [0], + "step_idx" : 0, + "walker_idx" : 1, + }, + ] + ) + + assert resampling_grp["decision_id"].shape == (2,1) + assert resampling_grp["target_idxs"].shape == (2,) + assert resampling_grp["step_idx"].shape == (2,1) + assert resampling_grp["walker_idx"].shape == (2,1) + + def test_sparse_fields(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory( + Path(tmpdir) / "nosparse.wepy.h5", + ) + with WepyHDF5(path, mode="r+") as wepy_h5: + + assert len(wepy_h5.sparse_fields) == 0 + + path = wepy_h5_factory( + Path(tmpdir) / "sparse.wepy.h5", + sparse_fields=("sparse_thing",), + ) + with WepyHDF5(path, mode="r+") as wepy_h5: + + assert len(wepy_h5.sparse_fields) == 1 + assert set(wepy_h5.sparse_fields) == {"sparse_thing"} + + def test__init_contiguous_traj_field(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + traj_grp = run_grp.create_group("trajectories/0") + + wepy_h5._init_contiguous_traj_field( + 0, + 0, + "something", + (2, 3), + np.float32, + ) + assert "something" in traj_grp + assert traj_grp["something"].maxshape == (None, 2, 3) + assert traj_grp["something"].shape == (0, 0, 0) + + def test__init_sparse_traj_field(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + traj_grp = run_grp.create_group("trajectories/0") + + wepy_h5._init_sparse_traj_field( + 0, + 0, + "something", + (2, 3), + np.float32, + ) + assert "something" in traj_grp + assert "data" in traj_grp["something"] + assert "_sparse_idxs" in traj_grp["something"] + + assert traj_grp["something/data"].maxshape == (None, 2, 3) + assert traj_grp["something/data"].shape == (0, 0, 0) + + assert traj_grp["something/_sparse_idxs"].maxshape == (None,) + assert traj_grp["something/_sparse_idxs"].shape == (0,) + assert traj_grp["something/_sparse_idxs"].dtype == int + + def test__init_traj_field(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory( + Path(tmpdir) / "0.wepy.h5", + sparse_fields=("sparse_thing",), + ) + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + traj_grp = run_grp.create_group("trajectories/0") + + wepy_h5._init_traj_field( + 0, 0, + "thing", + (2, 2), + np.float32, + ) + + assert "thing" in traj_grp + assert traj_grp["thing"].maxshape == (None, 2, 2) + assert traj_grp["thing"].shape == (0, 0, 0) + + wepy_h5._init_traj_field( + 0, 0, + "sparse_thing", + (2, 2), + np.float32, + ) + assert "sparse_thing" in traj_grp + assert "data" in traj_grp["sparse_thing"] + assert "_sparse_idxs" in traj_grp["sparse_thing"] + + assert traj_grp["sparse_thing/data"].maxshape == (None, 2, 2) + assert traj_grp["sparse_thing/data"].shape == (0, 0, 0) + + assert traj_grp["sparse_thing/_sparse_idxs"].maxshape == (None,) + assert traj_grp["sparse_thing/_sparse_idxs"].shape == (0,) + assert traj_grp["sparse_thing/_sparse_idxs"].dtype == int + + def test__init_traj_fields(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory( + Path(tmpdir) / "0.wepy.h5", + sparse_fields=("sparse_thing",), + ) + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + traj_grp = run_grp.create_group("trajectories/0") + + wepy_h5._init_traj_fields( + 0, 0, + field_paths=["thing", "sparse_thing"], + field_feature_shapes=[(2, 2), (2, 2)], + field_feature_dtypes=[np.float32, np.float32], + ) + + assert "thing" in traj_grp + assert traj_grp["thing"].maxshape == (None, 2, 2) + assert traj_grp["thing"].shape == (0, 0, 0) + + assert "sparse_thing" in traj_grp + assert "data" in traj_grp["sparse_thing"] + assert "_sparse_idxs" in traj_grp["sparse_thing"] + + assert traj_grp["sparse_thing/data"].maxshape == (None, 2, 2) + assert traj_grp["sparse_thing/data"].shape == (0, 0, 0) + + assert traj_grp["sparse_thing/_sparse_idxs"].maxshape == (None,) + assert traj_grp["sparse_thing/_sparse_idxs"].shape == (0,) + assert traj_grp["sparse_thing/_sparse_idxs"].dtype == int + + def test__add_traj_field_data(self, wepy_h5_factory, tmpdir): + + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + with pytest.raises(WepyHDF5ReadError): + wepy_h5._add_traj_field_data( + 0, 0, "something", + np.array([1, 2, 3]), + ) + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + with pytest.raises(WepyHDF5ReadError): + wepy_h5._add_traj_field_data( + 0, 0, "something", + np.array([1, 2, 3]), + ) + + traj_grp = run_grp["trajectories"].create_group("0") + + wepy_h5._add_traj_field_data( + 0, 0, "something", + np.array([1, 2, 3]), + ) + + assert "something" in traj_grp + assert list(traj_grp["something"][:]) == [1, 2, 3] + + # overwrite should work if it is the same shape + wepy_h5._add_traj_field_data( + 0, 0, "something", + np.array([3, 2, 1]), + ) + assert list(traj_grp["something"][:]) == [3, 2, 1] + + # but not for different sizes + with pytest.raises(TypeError): + wepy_h5._add_traj_field_data( + 0, 0, "something", + np.array([3, 2, 1, 0]), + ) + + # sparse field + wepy_h5._add_traj_field_data( + 0, 0, "sparse_thing", + np.array([3, 2, 1]), + sparse_idxs=np.array([5, 10, 15]), + ) + + assert "sparse_thing" in traj_grp + assert "data" in traj_grp["sparse_thing"] + assert "_sparse_idxs" in traj_grp["sparse_thing"] + + assert list(traj_grp["sparse_thing/data"][:]) == [3, 2, 1] + assert list(traj_grp["sparse_thing/_sparse_idxs"][:]) == [5, 10, 15] + + def test_add_traj(self, wepy_h5_factory, tmpdir): + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + traj_grp = wepy_h5.add_traj( + 0, + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ [1., 0., 0.], [0., 1., 0.], [0., 0., 1.], @@ -1223,7 +2019,418 @@ def test_add_traj(self, wepy_h5_factory, tmpdir, monkeypatch): assert traj_grp["box_vectors/data"].shape == (0,0,0) def test_extend_traj(self, wepy_h5_factory, tmpdir): - pass + path = wepy_h5_factory(Path(tmpdir) / "0.wepy.h5") + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + traj_grp = wepy_h5.add_traj( + 0, + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + weights=np.array([[0.2]]), + metadata={"foo" : "hello"}, + ) + + assert traj_grp["weights"].shape == (1,1) + assert traj_grp["positions"].shape == (1,2,3) + assert traj_grp["box_vectors"].shape == (1,3,3) + assert traj_grp["kinetic_energy"].shape == (1,1) + + # then extend this trajectory with all values + wepy_h5.extend_traj( + 0, + 0, + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + weights=np.array([[0.2]]), + ) + + assert traj_grp["weights"].shape == (2,1) + assert traj_grp["positions"].shape == (2,2,3) + assert traj_grp["box_vectors"].shape == (2,3,3) + assert traj_grp["kinetic_energy"].shape == (2,1) + + # sparse fields + + traj_grp = wepy_h5.add_traj( + 0, + data={ + "positions" : np.array([ + [ + [2., 2., 2.,], + [1., 1., 1.,], + ], + [ + [2., 2., 2.,], + [1., 1., 1.,], + ], + ]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + sparse_idxs={"kinetic_energy" : [1,]}, + ) + + def test_record_fields(self, wepy_h5_traj_init): + + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + + assert "resampling" in wepy_h5.record_fields + assert wepy_h5.record_fields["resampling"] == [ + "decision_id", + "target_idxs", + "step_idx", + "walker_idx", + ] + + + + def test__convert_record_field_to_table_column(self, wepy_h5_traj_init): + + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + + assert wepy_h5._convert_record_field_to_table_column( + 0, "resampling", "walker_idx" + ) == [0, 1] + + assert wepy_h5._convert_record_field_to_table_column( + 0, "resampling", "step_idx" + ) == [0, 0] + + assert wepy_h5._convert_record_field_to_table_column( + 0, "resampling", "target_idxs" + ) == [(0,), (1,)] + + def test__convert_record_fields_to_table_columns(self, wepy_h5_traj_init): + + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + + assert wepy_h5._convert_record_fields_to_table_columns( + 0, "resampling" + ) == { + "walker_idx" : [0, 1], + "step_idx" : [0, 0], + "target_idxs" : [(0,), (1,)], + "decision_id" : [0, 0], + } + + progress_cols = wepy_h5._convert_record_fields_to_table_columns( + 0, "progress" + ) + assert len(progress_cols) == 2 + assert set(progress_cols) == {"ensemble_average", "walker_distances"} + assert len(progress_cols["ensemble_average"]) == 1 + assert progress_cols["ensemble_average"][0] == 1.2 + assert len(progress_cols["walker_distances"]) == 1 + assert list(progress_cols["walker_distances"][0]) == [1.,1.,] + + def test__table_to_run_records(self, wepy_h5_traj_init): + + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + + assert wepy_h5._table_to_run_records( + "resampling", + { + "cycle_idx": [0, 0, 1, 1], + "walker_idx" : [0, 1, 0, 1], + "step_idx": [0, 0, 0, 0], + "target_idxs" : [(0,), (1,), (0,), (1,)], + "decision_id": [0, 0, 0, 0], + }, + ) == [ + # cycle 0 + RunRecord( + cycle_idx=0, + record={ + "walker_idx" : 0, + "step_idx" : 0, + "target_idxs" : (0,), + "decision_id" : 0, + }, + ), + RunRecord( + cycle_idx=0, + record={ + "walker_idx" : 1, + "step_idx" : 0, + "target_idxs" : (1,), + "decision_id" : 0, + }, + ), + # cycle 1 + RunRecord( + cycle_idx=1, + record={ + "walker_idx" : 0, + "step_idx" : 0, + "target_idxs" : (0,), + "decision_id" : 0, + }, + ), + RunRecord( + cycle_idx=1, + record={ + "walker_idx" : 1, + "step_idx" : 0, + "target_idxs" : (1,), + "decision_id" : 0, + }, + ), + ] + + + def test__run_records_sporadic(self, wepy_h5_traj_init): + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + + assert wepy_h5._run_records_sporadic( + [0], + "resampling", + ) == [ + RunRecord( + cycle_idx=0, + record={ + "walker_idx" : 0, + "step_idx" : 0, + "target_idxs" : (0,), + "decision_id" : 0, + }, + ), + RunRecord( + cycle_idx=0, + record={ + "walker_idx" : 1, + "step_idx" : 0, + "target_idxs" : (1,), + "decision_id" : 0, + }, + ), + ] + + + def test__run_records_continual(self, wepy_h5_traj_init): + + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + + recs = wepy_h5._run_records_continual( + [0], + "progress", + ) + + assert len(recs) == 1 + + assert recs[0].cycle_idx == 0 + + assert set(recs[0].record.keys()) == {"ensemble_average", "walker_distances"} + assert recs[0].record["ensemble_average"] == 1.2 + assert list(recs[0].record["walker_distances"]) == [1., 1.] + + + def test_run_contig_records(self, wepy_h5_traj_init): + + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + + # Sporadic example + assert wepy_h5.run_contig_records( + [0], + "resampling", + ) == [ + RunRecord( + cycle_idx=0, + record={ + "walker_idx" : 0, + "step_idx" : 0, + "target_idxs" : (0,), + "decision_id" : 0, + }, + ), + RunRecord( + cycle_idx=0, + record={ + "walker_idx" : 1, + "step_idx" : 0, + "target_idxs" : (1,), + "decision_id" : 0, + }, + ), + ] + + # continual + progress_recs = wepy_h5.run_contig_records( + [0], + "progress", + ) + + assert len(progress_recs) == 1 + assert progress_recs[0].cycle_idx == 0 + + # TODO: for multiple runs + + def test_run_records(self, wepy_h5_traj_init): + + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + + assert wepy_h5.run_records(0, "resampling") == [ + RunRecord( + cycle_idx=0, + record={ + "walker_idx" : 0, + "step_idx" : 0, + "target_idxs" : (0,), + "decision_id" : 0, + }, + ), + RunRecord( + cycle_idx=0, + record={ + "walker_idx" : 1, + "step_idx" : 0, + "target_idxs" : (1,), + "decision_id" : 0, + }, + ), + ] + + def test_run_records_dataframe(self, wepy_h5_traj_init): + + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + + df = wepy_h5.run_records_dataframe(0, "resampling") + + assert set(df.columns) == {"cycle_idx", "walker_idx", "step_idx", "target_idxs", "decision_id"} + + def test_resampling_records(self, wepy_h5_traj_init): + + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + + assert wepy_h5.resampling_records([0]) == [ + RunRecord( + cycle_idx=0, + record={ + "walker_idx" : 0, + "step_idx" : 0, + "target_idxs" : (0,), + "decision_id" : 0, + }, + ), + RunRecord( + cycle_idx=0, + record={ + "walker_idx" : 1, + "step_idx" : 0, + "target_idxs" : (1,), + "decision_id" : 0, + }, + ), + ] + + + # TODO + # def test_resampling_records_dataframe(self, wepy_h5_factory, tmpdir): + # pass + # + # def test_resampler_records(self, wepy_h5_factory, tmpdir): + # pass + # + # def test_resampler_records_dataframe(self, wepy_h5_factory, tmpdir): + # pass + + + def test_is_run_contig(self, wepy_h5_traj_init): + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + assert wepy_h5.is_run_contig([0]) + + # TODO: more complex scenarios + + def test_run_contig_resampling_panel(self, wepy_h5_traj_init): + + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + + resampling_panel = wepy_h5.run_contig_resampling_panel([0]) + assert resampling_panel == [ + # cycle 0 + [ + # step 0 + [ + # walker 0 + { + "decision_id": 0, + "target_idxs": (0,), + }, + # walker 1 + { + "decision_id": 0, + "target_idxs": (1,), + }, + ], + ], + ] + + def test_run_resampling_panel(self, wepy_h5_traj_init): + with WepyHDF5(wepy_h5_traj_init, mode='r') as wepy_h5: + + resampling_panel = wepy_h5.run_resampling_panel(0) + assert resampling_panel == [ + # cycle 0 + [ + # step 0 + [ + # walker 0 + { + "decision_id": 0, + "target_idxs": (0,), + }, + # walker 1 + { + "decision_id": 0, + "target_idxs": (1,), + }, + ], + ], + ] + + + # TODO: for observables + # + # def test__add_run_field(self, wepy_h5_factory, tmpdir): + # assert False + # def test__add_field(self, wepy_h5_factory, tmpdir): + # assert False + # + # def test_add_observable(self, wepy_h5_factory, tmpdir): + # assert False + +# TODO: add some high level acceptance tests for data. Perhaps move to +# another file +# # class Test_WepyHDF5_DataConformance: # pass + diff --git a/tests/unit/test_resampling/test_decisions/test_decision.py b/tests/unit/test_resampling/test_decisions/test_decision.py index 89c1412c..ca591630 100644 --- a/tests/unit/test_resampling/test_decisions/test_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_decision.py @@ -24,7 +24,10 @@ class MockDecision(BaseDecisionABC): class Test_BaseDecisionRecord: def test_to_dict(self): - assert BaseDecisionRecord(decision_id=1).to_dict() == {"decision_id" : 1} + assert BaseDecisionRecord(decision_id=1, target_idxs=(0,)).to_dict() == { + "decision_id" : 1, + "target_idxs" : (0,) + } class Test_Decision: @@ -32,13 +35,13 @@ def test_default_decision(self): assert MockDecision.default_decision() == MockDecisionEnum.NOTHING def test_field_names(self): - assert MockDecision.field_names() == ("decision_id",) + assert MockDecision.field_names() == ("decision_id", "target_idxs",) def test_field_shapes(self): - assert MockDecision.field_shapes() == ((1,),) + assert MockDecision.field_shapes() == ((1,), Ellipsis) def test_field_dtypes(self): - assert MockDecision.field_dtypes() == (int,) + assert MockDecision.field_dtypes() == (int, int) def test_fields(self): assert MockDecision.fields() == [ @@ -46,11 +49,16 @@ def test_fields(self): "decision_id", (1,), int, + ), + ( + "target_idxs", + Ellipsis, + int, ) ] def test_record_field_names(self): - assert MockDecision.record_field_names() == ("decision_id",) + assert MockDecision.record_field_names() == ("decision_id", "target_idxs",) def test_enum_dict_by_name(self): assert MockDecision.enum_dict_by_name() == { @@ -68,14 +76,20 @@ def test_enum_by_value(self): def test_enum_by_name(self): assert MockDecision.enum_by_name("NOTHING") == MockDecisionEnum.NOTHING - def test_record(self): + # def test_record(self): - assert MockDecision.record(0) == BaseDecisionRecord(decision_id=0) + # assert MockDecision.record(0, (0,)) == BaseDecisionRecord( + # decision_id=0, + # target_idxs=(0,), + # ) def test_action(self): with pytest.raises(NotImplementedError): MockDecision.action( [Walker(MockState(1), 0.1) for _ in range(4)], - [BaseDecisionRecord(decision_id=0) for _ in range(4)], + [BaseDecisionRecord( + decision_id=0, + target_idxs=(idx,) + ) for idx in range(4)], ) diff --git a/tests/unit/test_resampling/test_decisions/test_no_decision.py b/tests/unit/test_resampling/test_decisions/test_no_decision.py index f6d65285..e1417f8f 100644 --- a/tests/unit/test_resampling/test_decisions/test_no_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_no_decision.py @@ -14,29 +14,29 @@ def test___init__(self): NoDecisionRecord( decision_id=0, - target_idx=1, + target_idxs=(1,), ) with pytest.raises(ValueError): NoDecisionRecord( decision_id=1, - target_idx=1, + target_idxs=(1,), ) with pytest.raises(ValueError): NoDecisionRecord( decision_id=0, - target_idx=-1, + target_idxs=(-1,), ) def test_to_dict(self): assert NoDecisionRecord( decision_id=0, - target_idx=0 + target_idxs=(0,) ).to_dict() == { "decision_id" : 0, - "target_idx" : 0, + "target_idxs" : (0,), } class Test_NoDecision: @@ -66,13 +66,13 @@ def test_action(self): NoDecisionRecord( **{ "decision_id": NothingDecisionEnum.NOTHING, - "target_idx": 0, + "target_idxs": (0,), } ), NoDecisionRecord( **{ "decision_id": NothingDecisionEnum.NOTHING, - "target_idx": 1, + "target_idxs": (1,), } ), ] @@ -88,13 +88,13 @@ def test_action(self): NoDecisionRecord( **{ "decision_id": NothingDecisionEnum.NOTHING, - "target_idx": 1, + "target_idxs": (1,), } ), NoDecisionRecord( **{ "decision_id": NothingDecisionEnum.NOTHING, - "target_idx": 0, + "target_idxs": (0,), } ), ] @@ -107,14 +107,14 @@ def test_parents(self): [ NoDecisionRecord( **{ - "decision_id": NothingDecisionEnum.NOTHING, - "target_idx": 0, + "decision_id": NothingDecisionEnum.NOTHING.value, + "target_idxs": (0,), } ), NoDecisionRecord( **{ - "decision_id": NothingDecisionEnum.NOTHING, - "target_idx": 1, + "decision_id": NothingDecisionEnum.NOTHING.value, + "target_idxs": (1,), } ), ] @@ -125,13 +125,13 @@ def test_parents(self): NoDecisionRecord( **{ "decision_id": NothingDecisionEnum.NOTHING, - "target_idx": 1, + "target_idxs": (1,), } ), NoDecisionRecord( **{ "decision_id": NothingDecisionEnum.NOTHING, - "target_idx": 0, + "target_idxs": (0,), } ), ] diff --git a/tests/unit/test_resampling/test_resamplers/test_noresampler.py b/tests/unit/test_resampling/test_resamplers/test_noresampler.py index 35dcc406..1cd978e1 100644 --- a/tests/unit/test_resampling/test_resamplers/test_noresampler.py +++ b/tests/unit/test_resampling/test_resamplers/test_noresampler.py @@ -7,8 +7,12 @@ def test_NoResamplerResamplingRecord(): - assert NoResamplerResamplingRecord(0, (1,)) == NoResamplerResamplingRecord(0, (1,)) - + NoResamplerResamplingRecord( + decision_id=0, + target_idxs=(1,), + walker_idx=1, + step_idx=0 + ) class Test_NoResampler: def test_resample(self): @@ -35,16 +39,22 @@ def test_resample(self): walkers, [ NoResamplerResamplingRecord( - decision_id=np.array([NothingDecisionEnum.NOTHING.value]), - target_idxs=np.array([[0]]), + decision_id=NothingDecisionEnum.NOTHING.value, + target_idxs=(0,), + walker_idx=0, + step_idx=0, ), NoResamplerResamplingRecord( - decision_id=np.array([NothingDecisionEnum.NOTHING.value]), - target_idxs=np.array([[1]]), + decision_id=NothingDecisionEnum.NOTHING.value, + target_idxs=(1,), + walker_idx=1, + step_idx=0, ), NoResamplerResamplingRecord( - decision_id=np.array([NothingDecisionEnum.NOTHING.value]), - target_idxs=np.array([[2]]), + decision_id=NothingDecisionEnum.NOTHING.value, + target_idxs=(2,), + walker_idx=2, + step_idx=0, ), ], [NoResamplerResamplerRecord()], From bdf90218a9820ede47266027bce384b8277e9669 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sat, 24 Jan 2026 18:04:58 -0500 Subject: [PATCH 135/143] fix mistake hdf5 --- src/wepy/hdf5.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wepy/hdf5.py b/src/wepy/hdf5.py index 413a7ea6..b4fcd1d5 100644 --- a/src/wepy/hdf5.py +++ b/src/wepy/hdf5.py @@ -3611,7 +3611,7 @@ def num_run_trajs(self, run_idx: int) -> int: """ - trajs_grp = self.run_trajs(0) + trajs_grp = self.run_trajs(run_idx) return len(trajs_grp) def num_run_cycles(self, run_idx: int) -> int: From 3fcbaad8e4c2631e472e0c8002594de549c52113 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sat, 24 Jan 2026 18:05:18 -0500 Subject: [PATCH 136/143] move hdf5 fixtures to general conftest --- tests/unit/conftest.py | 578 ++++++++++++++++++++++++++++++++++++ tests/unit/test_fixtures.py | 112 +++++++ tests/unit/test_hdf5.py | 326 -------------------- 3 files changed, 690 insertions(+), 326 deletions(-) create mode 100644 tests/unit/test_fixtures.py diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 938e998d..fbde12b4 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,11 +1,23 @@ import shutil import subprocess import sys +from typing import Callable from pathlib import Path import pytest +import numpy as np + +from wepy.walker import Walker, WalkerStateBox +from wepy_tools.systems.lennard_jones import LennardJonesPair +from wepy.typing import IdxArray +from wepy.hdf5 import WepyHDF5 +from wepy.resampling.resamplers.noresampler import ( + NoResamplerResamplingRecord, + NoResampler, +) +from wepy.resampling.decisions.no_decision import NoDecision def reflink_or_copy(src: Path, dst: Path) -> None: @@ -44,3 +56,569 @@ def alanine_dipeptide_revo_wepy_hdf5(tmp_path: Path) -> Path: reflink_or_copy(src, dst) return dst + + +@pytest.fixture(scope="session") +def wepy_h5_factory() -> Callable[[Path], Path]: + + test_sys = LennardJonesPair() + + def _factory( + path: Path, + sparse_fields: tuple[str, ...] | None = None, + alt_reps: dict[str, IdxArray] | None = None, + main_rep_idxs: IdxArray | None = None, + ) -> Path: + + # create the file + WepyHDF5( + path, + mode="x", + topology=test_sys.json_top, + sparse_fields=sparse_fields, + alt_reps=alt_reps, + main_rep_idxs=main_rep_idxs, + ) + + return path + + return _factory + +_INIT_WALKERS = [ + Walker( + WalkerStateBox( + positions=np.array([ + [1., 1., 1.], + [2., 2., 2.], + ]), + box_vectors=np.array([ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]), + kinetic_energy=3.455, + ), + 0.1, + ), + ] + +@pytest.fixture(scope="session") +def _wepy_h5_run_init(wepy_h5_factory, tmp_path_factory) -> Path: + """Data generation fixture, should not be used by individual tests + as it is slow. Instead use the fixture that makes a copy for + read/write in each test function. + + """ + + d = tmp_path_factory.mktemp("wepy_h5_run_init") + + # initialize a file + path = wepy_h5_factory( + d / "main.wepy.h5", + sparse_fields={"velocities"}, + ) + + # initialize the file + with WepyHDF5(path, mode="r+") as wepy_h5: + + run_grp = wepy_h5.new_run( + init_walkers=_INIT_WALKERS + ) + + wepy_h5.init_run_fields_resampling_decision( + 0, + NoDecision.enum_dict_by_name(), + ) + wepy_h5.init_run_fields_resampling( + 0, + NoResampler.resampling_fields(), + ) + wepy_h5.init_record_fields( + "resampling", + [name for name, _, _ in NoResampler.resampling_fields()], + ) + + # UGLY: synthetic examples of warping without a real class to use + + # warping as it has hardcoded behavior that should be tested + warp_fields = [ + ("walker_idx", (1,), int), + ("target_idx", (1,), int), + ("weight", (1,), float), + ] + wepy_h5.init_run_fields_warping( + 0, + warp_fields, + ) + wepy_h5.init_record_fields( + "warping", + [name for name, _, _ in warp_fields], + ) + + # minimal progress fields for testing continual records + progress_fields = [ + # single number for a cycle + ("ensemble_average", (1,), float), + # per-walker data + ("walker_distances", Ellipsis, float), + ] + wepy_h5.init_run_fields_progress( + 0, + progress_fields, + ) + wepy_h5.init_record_fields( + "progress", + [name for name, _, _ in progress_fields], + ) + + + # TODO: more fields for resampler records and BC + # records. These are always optional and strictly accessory so + # holding off on writing more test cases on these. + + return path + +@pytest.fixture(scope="function") +def wepy_h5_run_init(_wepy_h5_run_init, tmpdir) -> Path: + + path = tmpdir / "main.wepy.h5" + + reflink_or_copy(_wepy_h5_run_init, path) + + return path + + +@pytest.fixture(scope="session") +def _wepy_h5_traj_init(_wepy_h5_run_init, tmp_path_factory) -> Path: + """Data generation fixture, should not be used by individual tests + as it is slow. Instead use the fixture that makes a copy for + read/write in each test function. + + This generates an HDF5 with everything in the run init fixture as + well as trajectories in the run with a single cycle's worth of + data. + + """ + + # make a copy of the run init H5 file and then mutate to add stuff + d = tmp_path_factory.mktemp("wepy_h5_traj_init") + + path = d / "main.wepy.h5" + shutil.copy( + _wepy_h5_run_init, + path, + ) + + # Add the new data + with WepyHDF5(path, mode="r+") as wepy_h5: + + traj0_grp = wepy_h5.add_traj( + 0, + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + weights=np.array([[0.2]]), + metadata={"foo" : "hello"}, + ) + + traj1_grp = wepy_h5.add_traj( + 0, + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + weights=np.array([[0.2]]), + metadata={"foo" : "hello"}, + ) + + wepy_h5.extend_cycle_resampling_records( + 0, + 0, + [ + # NOTE: that this function requires mappings, and + # these record types double as mappings via the mixin, + # so we just use them + NoResamplerResamplingRecord( + decision_id=0, + target_idxs=[0], + walker_idx=0, + step_idx=0, + ), + NoResamplerResamplingRecord( + decision_id=0, + target_idxs=[1], + walker_idx=1, + step_idx=0, + ), + ] + ) + + wepy_h5.extend_cycle_progress_records( + 0, + 0, + [ + # only a single record for the cycle + { + "ensemble_average" : 1.2, + "walker_distances" : [ + 1., 1., + ], + }, + ] + ) + + # TODO: the other record groups + + return path + + + + +@pytest.fixture(scope="function") +def wepy_h5_traj_init(_wepy_h5_traj_init, tmpdir) -> Path: + + path = tmpdir / "main.wepy.h5" + + reflink_or_copy(_wepy_h5_traj_init, path) + + return path + +@pytest.fixture(scope="session") +def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: + """Data generation fixture, should not be used by individual tests + as it is slow. Instead use the fixture that makes a copy for + read/write in each test function. + + This generates an HDF5 with everything in the traj init fixture as + well as extending those trajectories for a few cycles and adding + another 1 run. This run is a continuation of the first. + + For each trajectory it also includes sparse data and alternate reps. + + This should be sufficient for testing of all HDF5 related methods + and analysis. + + """ + + # make a copy of the run init H5 file and then mutate to add stuff + d = tmp_path_factory.mktemp("wepy_h5_full") + + path = d / "main.wepy.h5" + shutil.copy( + _wepy_h5_traj_init, + path, + ) + + # Add the new data + with WepyHDF5(path, mode="r+") as wepy_h5: + + + # extend run 0 + wepy_h5.extend_traj( + 0, + 0, + weights=np.array([[0.2]]), + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + "velocities" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + }, + ) + + wepy_h5.extend_traj( + 0, + 1, + weights=np.array([[0.2]]), + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + "velocities" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + }, + ) + + wepy_h5.extend_cycle_resampling_records( + 0, + 1, + [ + # NOTE: that this function requires mappings, and + # these record types double as mappings via the mixin, + # so we just use them + NoResamplerResamplingRecord( + decision_id=0, + target_idxs=[0], + walker_idx=0, + step_idx=0, + ), + NoResamplerResamplingRecord( + decision_id=0, + target_idxs=[1], + walker_idx=1, + step_idx=0, + ), + ] + ) + + wepy_h5.extend_cycle_progress_records( + 0, + 1, + [ + # only a single record for the cycle + { + "ensemble_average" : 1.2, + "walker_distances" : [ + 1., 1., + ], + }, + ] + ) + + # run 1, a continuation of run 0 + wepy_h5.new_run( + init_walkers=_INIT_WALKERS, + continue_run=0, + ) + + wepy_h5.init_run_fields_resampling_decision( + 1, + NoDecision.enum_dict_by_name(), + ) + wepy_h5.init_run_fields_resampling( + 1, + NoResampler.resampling_fields(), + ) + + # UGLY: synthetic examples of warping without a real class to use + + # warping as it has hardcoded behavior that should be tested + warp_fields = [ + ("walker_idx", (1,), int), + ("target_idx", (1,), int), + ("weight", (1,), float), + ] + wepy_h5.init_run_fields_warping( + 1, + warp_fields, + ) + + # minimal progress fields for testing continual records + progress_fields = [ + # single number for a cycle + ("ensemble_average", (1,), float), + # per-walker data + ("walker_distances", Ellipsis, float), + ] + wepy_h5.init_run_fields_progress( + 1, + progress_fields, + ) + + wepy_h5.add_traj( + 1, + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + weights=np.array([[0.2]]), + metadata={"foo" : "hello"}, + ) + + wepy_h5.add_traj( + 1, + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + }, + weights=np.array([[0.2]]), + metadata={"foo" : "hello"}, + ) + + wepy_h5.extend_cycle_resampling_records( + 1, + 0, + [ + # NOTE: that this function requires mappings, and + # these record types double as mappings via the mixin, + # so we just use them + NoResamplerResamplingRecord( + decision_id=0, + target_idxs=[0], + walker_idx=0, + step_idx=0, + ), + NoResamplerResamplingRecord( + decision_id=0, + target_idxs=[1], + walker_idx=1, + step_idx=0, + ), + ] + ) + + wepy_h5.extend_cycle_progress_records( + 1, + 0, + [ + # only a single record for the cycle + { + "ensemble_average" : 1.2, + "walker_distances" : [ + 1., 1., + ], + }, + ] + ) + + # extend run 1 + wepy_h5.extend_traj( + 1, + 0, + weights=np.array([[0.2]]), + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + "velocities" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + }, + ) + + wepy_h5.extend_traj( + 1, + 1, + weights=np.array([[0.2]]), + data={ + "positions" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + "box_vectors" : np.array([[ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]]), + "kinetic_energy" : np.array([ + [4.87], + ]), + "velocities" : np.array([[ + [2., 2., 2.,], + [1., 1., 1.,], + ]]), + }, + ) + + wepy_h5.extend_cycle_resampling_records( + 1, + 1, + [ + # NOTE: that this function requires mappings, and + # these record types double as mappings via the mixin, + # so we just use them + NoResamplerResamplingRecord( + decision_id=0, + target_idxs=[0], + walker_idx=0, + step_idx=0, + ), + NoResamplerResamplingRecord( + decision_id=0, + target_idxs=[1], + walker_idx=1, + step_idx=0, + ), + ] + ) + + wepy_h5.extend_cycle_progress_records( + 1, + 1, + [ + # only a single record for the cycle + { + "ensemble_average" : 1.2, + "walker_distances" : [ + 1., 1., + ], + }, + ] + ) + + + return path diff --git a/tests/unit/test_fixtures.py b/tests/unit/test_fixtures.py new file mode 100644 index 00000000..e70a6d7e --- /dev/null +++ b/tests/unit/test_fixtures.py @@ -0,0 +1,112 @@ +from wepy.hdf5 import WepyHDF5 +import numpy as np + + +def test__wepy_h5_run_init(_wepy_h5_run_init): + + with WepyHDF5(_wepy_h5_run_init, mode='r') as wepy_h5: + + wepy_h5.run(0) + + assert wepy_h5.num_run_trajs(0) == 0 + + wepy_h5.resampling_grp(0) + wepy_h5.decision_grp(0) + wepy_h5.warping_grp(0) + wepy_h5.progress_grp(0) + + assert "resampling" in wepy_h5.record_fields + assert wepy_h5.record_fields["resampling"] == [ + "decision_id", + "target_idxs", + "step_idx", + "walker_idx", + ] + + assert "warping" in wepy_h5.record_fields + assert wepy_h5.record_fields["warping"] == [ + "walker_idx", + "target_idx", + "weight" + ] + + assert "progress" in wepy_h5.record_fields + assert wepy_h5.record_fields["progress"] == [ + "ensemble_average", + "walker_distances", + ] + +# test that each test gets its own copy +def test_wepy_h5_run_init_1(wepy_h5_run_init): + + with WepyHDF5(wepy_h5_run_init, mode='r') as wepy_h5: + assert "mutation_flag" not in wepy_h5.h5 + + # mutate + with WepyHDF5(wepy_h5_run_init, mode='r+') as wepy_h5: + wepy_h5.h5["mutation_flag"] = np.array([0]) + +def test_wepy_h5_run_init_2(wepy_h5_run_init): + + with WepyHDF5(wepy_h5_run_init, mode='r') as wepy_h5: + assert "mutation_flag" not in wepy_h5.h5 + + # mutate + with WepyHDF5(wepy_h5_run_init, mode='r+') as wepy_h5: + wepy_h5.h5["mutation_flag"] = np.array([0]) + +def test__wepy_h5_traj_init(_wepy_h5_traj_init): + + with WepyHDF5(_wepy_h5_traj_init, mode='r') as wepy_h5: + + assert "velocities" in wepy_h5.sparse_fields + + wepy_h5.run(0) + + assert len(wepy_h5.resampling_records([0])) == 2 + assert len(wepy_h5.progress_records([0])) == 1 + + assert wepy_h5.num_run_trajs(0) == 2 + + for traj_idx in (0, 1): + assert "weights" in wepy_h5.traj(0, traj_idx) + assert "positions" in wepy_h5.traj(0, traj_idx) + assert "box_vectors" in wepy_h5.traj(0, traj_idx) + assert "kinetic_energy" in wepy_h5.traj(0, traj_idx) + assert "velocities" in wepy_h5.traj(0, traj_idx) + + assert wepy_h5.traj_field_entity(0, traj_idx, "weights").shape == (1, 1) + assert wepy_h5.traj_field_entity(0, traj_idx, "positions").shape == (1, 2, 3) + assert wepy_h5.traj_field_entity(0, traj_idx, "box_vectors").shape == (1, 3, 3) + assert wepy_h5.traj_field_entity(0, traj_idx, "kinetic_energy").shape == (1, 1) + + assert "data" in wepy_h5.traj_field_entity(0, traj_idx, "velocities") + assert "_sparse_idxs" in wepy_h5.traj_field_entity(0, traj_idx, "velocities") + + assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")["data"].shape == (0, 0, 0) + assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")["data"].maxshape == (None, 2, 3) + + assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")["_sparse_idxs"].shape == (0,) + +def test__wepy_h5_full_init(_wepy_h5_full_init): + + with WepyHDF5(_wepy_h5_full_init, mode='r') as wepy_h5: + + assert wepy_h5.num_runs == 2 + + # run 0 + assert wepy_h5.num_traj_frames(0, 0) == 2 + assert wepy_h5.num_traj_frames(0, 1) == 2 + + assert len(wepy_h5.resampling_records([0])) == 4 + assert len(wepy_h5.progress_records([0])) == 2 + + # run 1 + assert len(wepy_h5.continuations) == 1 + assert tuple(wepy_h5.continuations[0]) == (1, 0) + assert wepy_h5.num_traj_frames(1, 0) == 2 + assert wepy_h5.num_traj_frames(1, 1) == 2 + + assert len(wepy_h5.resampling_records([1])) == 4 + assert len(wepy_h5.progress_records([1])) == 2 + diff --git a/tests/unit/test_hdf5.py b/tests/unit/test_hdf5.py index 938529f0..65f08de4 100644 --- a/tests/unit/test_hdf5.py +++ b/tests/unit/test_hdf5.py @@ -55,332 +55,6 @@ def reflink_or_copy(src: Path, dst: Path) -> None: shutil.copy2(src, dst) -@pytest.fixture(scope="session") -def wepy_h5_factory() -> Callable[[Path], Path]: - - test_sys = LennardJonesPair() - - def _factory( - path: Path, - sparse_fields: tuple[str, ...] | None = None, - alt_reps: dict[str, IdxArray] | None = None, - main_rep_idxs: IdxArray | None = None, - ) -> Path: - - # create the file - WepyHDF5( - path, - mode="x", - topology=test_sys.json_top, - sparse_fields=sparse_fields, - alt_reps=alt_reps, - main_rep_idxs=main_rep_idxs, - ) - - return path - - return _factory - -_INIT_WALKERS = [ - Walker( - WalkerStateBox( - positions=np.array([ - [1., 1., 1.], - [2., 2., 2.], - ]), - box_vectors=np.array([ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]), - kinetic_energy=3.455, - ), - 0.1, - ), - ] - -@pytest.fixture(scope="session") -def _wepy_h5_run_init(wepy_h5_factory, tmp_path_factory) -> Path: - """Data generation fixture, should not be used by individual tests - as it is slow. Instead use the fixture that makes a copy for - read/write in each test function. - - """ - - d = tmp_path_factory.mktemp("wepy_h5_run_init") - - # initialize a file - path = wepy_h5_factory( - d / "main.wepy.h5", - sparse_fields={"velocities"}, - ) - - # initialize the file - with WepyHDF5(path, mode="r+") as wepy_h5: - - run_grp = wepy_h5.new_run( - init_walkers=_INIT_WALKERS - ) - - wepy_h5.init_run_fields_resampling_decision( - 0, - NoDecision.enum_dict_by_name(), - ) - wepy_h5.init_run_fields_resampling( - 0, - NoResampler.resampling_fields(), - ) - wepy_h5.init_record_fields( - "resampling", - [name for name, _, _ in NoResampler.resampling_fields()], - ) - - # UGLY: synthetic examples of warping without a real class to use - - # warping as it has hardcoded behavior that should be tested - warp_fields = [ - ("walker_idx", (1,), int), - ("target_idx", (1,), int), - ("weight", (1,), float), - ] - wepy_h5.init_run_fields_warping( - 0, - warp_fields, - ) - wepy_h5.init_record_fields( - "warping", - [name for name, _, _ in warp_fields], - ) - - # minimal progress fields for testing continual records - progress_fields = [ - # single number for a cycle - ("ensemble_average", (1,), float), - # per-walker data - ("walker_distances", Ellipsis, float), - ] - wepy_h5.init_run_fields_progress( - 0, - progress_fields, - ) - wepy_h5.init_record_fields( - "progress", - [name for name, _, _ in progress_fields], - ) - - - # TODO: more fields for resampler records and BC - # records. These are always optional and strictly accessory so - # holding off on writing more test cases on these. - - return path - -def test__wepy_h5_run_init(_wepy_h5_run_init): - - with WepyHDF5(_wepy_h5_run_init, mode='r') as wepy_h5: - - wepy_h5.run(0) - - assert wepy_h5.num_run_trajs(0) == 0 - - wepy_h5.resampling_grp(0) - wepy_h5.decision_grp(0) - wepy_h5.warping_grp(0) - wepy_h5.progress_grp(0) - - assert "resampling" in wepy_h5.record_fields - assert wepy_h5.record_fields["resampling"] == [ - "decision_id", - "target_idxs", - "step_idx", - "walker_idx", - ] - - assert "warping" in wepy_h5.record_fields - assert wepy_h5.record_fields["warping"] == [ - "walker_idx", - "target_idx", - "weight" - ] - - assert "progress" in wepy_h5.record_fields - assert wepy_h5.record_fields["progress"] == [ - "ensemble_average", - "walker_distances", - ] - -@pytest.fixture(scope="function") -def wepy_h5_run_init(_wepy_h5_run_init, tmpdir) -> Path: - - path = tmpdir / "main.wepy.h5" - - reflink_or_copy(_wepy_h5_run_init, path) - - return path - -# test that each test gets its own copy -def test_wepy_h5_run_init_1(wepy_h5_run_init): - - with WepyHDF5(wepy_h5_run_init, mode='r') as wepy_h5: - assert "mutation_flag" not in wepy_h5.h5 - - # mutate - with WepyHDF5(wepy_h5_run_init, mode='r+') as wepy_h5: - wepy_h5.h5["mutation_flag"] = np.array([0]) - -def test_wepy_h5_run_init_2(wepy_h5_run_init): - - with WepyHDF5(wepy_h5_run_init, mode='r') as wepy_h5: - assert "mutation_flag" not in wepy_h5.h5 - - # mutate - with WepyHDF5(wepy_h5_run_init, mode='r+') as wepy_h5: - wepy_h5.h5["mutation_flag"] = np.array([0]) - - -@pytest.fixture(scope="session") -def _wepy_h5_traj_init(_wepy_h5_run_init, tmp_path_factory) -> Path: - """Data generation fixture, should not be used by individual tests - as it is slow. Instead use the fixture that makes a copy for - read/write in each test function. - - """ - - # make a copy of the run init H5 file and then mutate to add stuff - d = tmp_path_factory.mktemp("wepy_h5_traj_init") - - path = d / "main.wepy.h5" - shutil.copy( - _wepy_h5_run_init, - path, - ) - - # Add the new data - with WepyHDF5(path, mode="r+") as wepy_h5: - - traj0_grp = wepy_h5.add_traj( - 0, - data={ - "positions" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), - "box_vectors" : np.array([[ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]]), - "kinetic_energy" : np.array([ - [4.87], - ]), - }, - weights=np.array([[0.2]]), - metadata={"foo" : "hello"}, - ) - - traj1_grp = wepy_h5.add_traj( - 0, - data={ - "positions" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), - "box_vectors" : np.array([[ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]]), - "kinetic_energy" : np.array([ - [4.87], - ]), - }, - weights=np.array([[0.2]]), - metadata={"foo" : "hello"}, - ) - - wepy_h5.extend_cycle_resampling_records( - 0, - 0, - [ - # NOTE: that this function requires mappings, and - # these record types double as mappings via the mixin, - # so we just use them - NoResamplerResamplingRecord( - decision_id=0, - target_idxs=[0], - walker_idx=0, - step_idx=0, - ), - NoResamplerResamplingRecord( - decision_id=0, - target_idxs=[1], - walker_idx=1, - step_idx=0, - ), - ] - ) - - wepy_h5.extend_cycle_progress_records( - 0, - 0, - [ - # only a single record for the cycle - { - "ensemble_average" : 1.2, - "walker_distances" : [ - 1., 1., - ], - }, - ] - ) - - # TODO: the other record groups - - return path - -def test__wepy_h5_traj_init(_wepy_h5_traj_init): - - with WepyHDF5(_wepy_h5_traj_init, mode='r') as wepy_h5: - - assert "velocities" in wepy_h5.sparse_fields - - wepy_h5.run(0) - - assert len(wepy_h5.resampling_records([0])) == 2 - assert len(wepy_h5.progress_records([0])) == 1 - - assert wepy_h5.num_run_trajs(0) == 2 - - for traj_idx in (0, 1): - assert "weights" in wepy_h5.traj(0, traj_idx) - assert "positions" in wepy_h5.traj(0, traj_idx) - assert "box_vectors" in wepy_h5.traj(0, traj_idx) - assert "kinetic_energy" in wepy_h5.traj(0, traj_idx) - assert "velocities" in wepy_h5.traj(0, traj_idx) - - assert wepy_h5.traj_field_entity(0, traj_idx, "weights").shape == (1, 1) - assert wepy_h5.traj_field_entity(0, traj_idx, "positions").shape == (1, 2, 3) - assert wepy_h5.traj_field_entity(0, traj_idx, "box_vectors").shape == (1, 3, 3) - assert wepy_h5.traj_field_entity(0, traj_idx, "kinetic_energy").shape == (1, 1) - - assert "data" in wepy_h5.traj_field_entity(0, traj_idx, "velocities") - assert "_sparse_idxs" in wepy_h5.traj_field_entity(0, traj_idx, "velocities") - - assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")["data"].shape == (0, 0, 0) - assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")["data"].maxshape == (None, 2, 3) - - assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")["_sparse_idxs"].shape == (0,) - - - -@pytest.fixture(scope="function") -def wepy_h5_traj_init(_wepy_h5_traj_init, tmpdir) -> Path: - - path = tmpdir / "main.wepy.h5" - - reflink_or_copy(_wepy_h5_traj_init, path) - - return path From 4c1285fc858914ff3a2c9b66d3bf6a7cd443cc67 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sat, 24 Jan 2026 20:44:07 -0500 Subject: [PATCH 137/143] adds lots of tests and types for contig_tree module --- src/wepy/analysis/contig_tree.py | 419 ++++++++++------ tests/unit/conftest.py | 9 + tests/unit/test_analysis/test_contig_tree.py | 497 ++++++++++++++++++- 3 files changed, 759 insertions(+), 166 deletions(-) diff --git a/src/wepy/analysis/contig_tree.py b/src/wepy/analysis/contig_tree.py index 8a622549..93941149 100644 --- a/src/wepy/analysis/contig_tree.py +++ b/src/wepy/analysis/contig_tree.py @@ -13,11 +13,12 @@ from collections import deque from copy import copy from operator import attrgetter -from typing import Final +from typing import Final, Self # Third Party Library import networkx as nx import numpy as np +import numpy.typing try: # Third Party Library @@ -33,6 +34,7 @@ from wepy.analysis.network_layouts.layout_graph import LayoutGraph from wepy.analysis.network_layouts.tree import ResamplingTreeLayout from wepy.analysis.parents import ( + ParentTable, ParentForest, ancestors, net_parent_table, @@ -43,6 +45,7 @@ from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.hdf5 import WepyHDF5 from wepy.resampling.decisions.decision import BaseDecisionABC +from wepy.reporter.file import FileMode # the groups of run records RESAMPLING: Final = "resampling" @@ -56,6 +59,22 @@ BC: Final = "boundary_conditions" """Record key for boundary condition records.""" +# (traj_idx, cycle_idx) +ContigWalkerTrace = list[tuple[int, int], ...] + +# (run_idx, traj_idx, cycle_idx) +RunTrace = list[tuple[int, int, int], ...] + +# (run_idx, cycle_idx) +ContigTrace = list[tuple[int, int], ...] + +# (run_idx, cycle_idx) +NodeId = tuple[int, int] +Edge = tuple[NodeId, NodeId] + +# (run_idx, run_idx) +ContinuationsTable = list[tuple[int, int]] + class BaseContigTree: """A base class for the contigtree which doesn't contain a WepyHDF5 @@ -71,10 +90,17 @@ class BaseContigTree: DISCONTINUITY_KEY: Final = "discontinuities" """Key for discontinuity node attributes in the tree graph.""" + _graph: nx.DiGraph + _boundary_condition_class: type[BoundaryConditions] | None + _decision_class: type[BaseDecisionABC] | None + _continuations: set[tuple[int, int]] + _run_idxs: set[int] + _spans: dict[int, ContigTrace] + def __init__( self, wepy_h5: WepyHDF5, - continuations: type(Ellipsis) | list[tuple[int, int]] = Ellipsis, + continuations: type(Ellipsis) | ContinuationsTable = Ellipsis, runs: type(Ellipsis) | list[int] = Ellipsis, boundary_condition_class: type[BoundaryConditions] | None = None, decision_class: type[BaseDecisionABC] | None = None, @@ -232,23 +258,23 @@ def boundary_condition_class(self) -> type[BoundaryConditions] | None: return self._boundary_condition_class @property - def span_traces(self): - """Dictionary mapping the spand indices to their run traces.""" + def span_traces(self) -> dict[int, ContigTrace]: + """Dictionary mapping the span indices to their run traces.""" return self._spans - def make_contig(self, span_trace): + def make_contig(self, contig_trace: ContigTrace) -> "Contig": raise NotImplementedError( f"'make_contig' is not implemented in '{self.__class__.__name__}'" ) - def span_contig(self, span_idx): + def span_contig(self, span_idx: int) -> "Contig": """Generates a contig object for the specified spanning contig.""" contig = self.make_contig(self.span_traces[span_idx]) return contig - def _create_tree(self, wepy_h5): + def _create_tree(self, wepy_h5: WepyHDF5) -> None: """Generate the tree of cycles from the WepyHDF5 object/file.""" # first go through each run without continuations @@ -285,7 +311,7 @@ def _create_tree(self, wepy_h5): # add this connector edge to the network self.graph.add_edge(*edge) - def _set_resampling_panels(self, wepy_h5): + def _set_resampling_panels(self, wepy_h5: WepyHDF5) -> None: """Generates resampling panels for each cycle and sets them as node attributes.""" # then get the resampling tables for each cycle and put them @@ -299,7 +325,7 @@ def _set_resampling_panels(self, wepy_h5): node = (run_idx, step_idx) self.graph.nodes[node][self.RESAMPLING_PANEL_KEY] = step - def _initialize_discontinuities(self, wepy_h5): + def _initialize_discontinuities(self, wepy_h5: WepyHDF5) -> None: """Initialize the nodes with discontinuities attributes but set to 0s indicating no discontinuities. """ @@ -312,7 +338,11 @@ def _initialize_discontinuities(self, wepy_h5): 0 for i in range(n_walkers) ] - def _set_discontinuities(self, wepy_h5, boundary_conditions_class): + def _set_discontinuities( + self, + wepy_h5: WepyHDF5, + boundary_conditions_class: type[BoundaryConditions], + ) -> None: """Given the boundary condition class sets node attributes for where there are discontinuities in the parental lineages. @@ -361,7 +391,7 @@ def _set_discontinuities(self, wepy_h5, boundary_conditions_class): rec_traj_idx ] = -1 - def _set_parents(self, decision_class): + def _set_parents(self, decision_class: BaseDecisionABC) -> None: """Determines the net parents for each cycle and sets them in-place to the cycle tree given. @@ -387,17 +417,20 @@ def _set_parents(self, decision_class): self.graph.nodes[node][self.PARENTS_KEY] = node_parents @property - def run_idxs(self): + def run_idxs(self) -> set[int]: """Indices of runs in WepyHDF5 used in this contig tree.""" return self._run_idxs @property - def continuations(self): + def continuations(self) -> set[tuple[int, int]]: """The continuations that are used in this contig tree over the runs.""" return self._continuations @staticmethod - def contig_trace_to_run_trace(contig_trace, contig_walker_trace): + def contig_trace_to_run_trace( + contig_trace: ContigTrace, + contig_walker_trace: ContigWalkerTrace, + ) -> RunTrace: """Combine a contig trace and a walker trace to get the equivalent run trace. The contig_walker_trace cycle_idxs must be a subset of the @@ -431,30 +464,8 @@ def contig_trace_to_run_trace(contig_trace, contig_walker_trace): return trace - def walker_trace_to_run_trace(self, contig_walker_trace): - """Combine a walker trace to get the equivalent run trace for this contig. - - The contig_walker_trace cycle_idxs must be a subset of the - frame indices given by the contig_trace. - - - Parameters - ---------- - contig_walker_trace : list of tuples of ints (traj_idx, cycle_idx) - - Returns - ------- - run_trace : list of tuples of ints (run_idx, traj_idx, cycle_idx) - - See Also - -------- - Contig.contig_trace_to_run_trace : calls this static method - - """ - - return self.contig_trace_to_run_trace(self.contig_trace, contig_walker_trace) - def run_trace_to_contig_trace(self, run_trace): + def run_trace_to_contig_trace(self, run_trace: RunTrace) -> ContigWalkerTrace: """Assumes that the run trace goes along a valid contig. Parameters @@ -477,7 +488,7 @@ def run_trace_to_contig_trace(self, run_trace): return contig_walker_trace - def contig_cycle_idx(self, run_idx, cycle_idx): + def contig_cycle_idx(self, run_idx: int, cycle_idx: int) -> int: """Convert an in-run cycle index to an in-contig cyle_idx. Parameters @@ -501,7 +512,12 @@ def contig_cycle_idx(self, run_idx, cycle_idx): # get the length and subtract one for the index return len(contig_trace) - 1 - def get_branch_trace(self, run_idx, cycle_idx, start_contig_idx=0): + def get_branch_trace( + self, + run_idx: int, + cycle_idx: int, + start_contig_idx: int = 0, + ) -> ContigTrace: """Get a contig trace for a branch of the contig tree from an end point back to a set point (defaults to root of contig tree). @@ -555,7 +571,11 @@ def get_branch_trace(self, run_idx, cycle_idx, start_contig_idx=0): return contig_trace - def trace_parent_table(self, contig_trace, discontinuities=True): + def trace_parent_table( + self, + contig_trace: ContigTrace, + discontinuities: bool = True, + ) -> ParentTable: """Given a contig trace returns a parent table for that contig. Parameters @@ -585,7 +605,7 @@ def trace_parent_table(self, contig_trace, discontinuities=True): return parent_table @classmethod - def _tree_leaves(cls, root, tree): + def _tree_leaves(cls, root: NodeId, tree: nx.DiGraph) -> list[NodeId]: """Given the root node ID and the tree as a networkX DiGraph returns the leaves of the tree. @@ -644,7 +664,7 @@ def _tree_leaves(cls, root, tree): return leaves - def _subtree_leaves(self, root): + def _subtree_leaves(self, root: NodeId) -> list[NodeId]: """Given a root defining a subtree on the full tree returns the leaves of that subtree. @@ -672,7 +692,7 @@ def _subtree_leaves(self, root): return leaves - def leaves(self): + def leaves(self) -> list[NodeId]: """All of the leaves of this contig tree. Returns @@ -688,7 +708,7 @@ def leaves(self): return leaves - def root_leaves(self): + def root_leaves(self) -> dict[NodeId, NodeId]: """Return a dictionary mapping the roots to their leaves.""" root_leaves = {} @@ -697,7 +717,7 @@ def root_leaves(self): return root_leaves - def _subtree_root(self, node): + def _subtree_root(self, node: NodeId) -> NodeId: """Given a node find the root of the tree it is on Parameters @@ -737,7 +757,7 @@ def _subtree_root(self, node): return curr_node - def roots(self): + def roots(self) -> list[NodeId]: """Returns all of the roots in this contig tree (which is technically a forest and can have multiple roots). @@ -756,7 +776,7 @@ def roots(self): return subtree_roots - def subtrees(self): + def subtrees(self) -> list[nx.DiGraph]: """Returns all of the subtrees (with unique roots) in this contig tree (which is technically a forest and can have multiple roots). @@ -776,7 +796,7 @@ def subtrees(self): return subtree_nxs - def get_subtree(self, node): + def get_subtree(self, node: NodeId) -> nx.DiGraph: """Given a node defining a subtree root return that subtree. Parameters @@ -792,6 +812,8 @@ def get_subtree(self, node): # get all the subtrees subtrees = self.subtrees() + # TODO: This is ambiguous if it is in multiple subtrees... + # see which tree the node is in for subtree in subtrees: # if the node is in it this is the subtree it is in so @@ -799,7 +821,11 @@ def get_subtree(self, node): if node in subtree: return subtree - def contig_sliding_windows(self, contig_trace, window_length): + def contig_sliding_windows( + self, + contig_trace: ContigTrace, + window_length: int, + ) -> list[ContigWalkerTrace]: """Given a contig trace get the sliding windows of length 'window_length' as contig walker traces. @@ -825,7 +851,7 @@ def contig_sliding_windows(self, contig_trace, window_length): return windows - def sliding_contig_windows(self, window_length): + def sliding_contig_windows(self, window_length: int) -> list[ContigTrace]: """Given a 'window_length' return all the windows over the contig tree as contig traces. @@ -855,7 +881,11 @@ def sliding_contig_windows(self, window_length): return contig_windows - def _subtree_sliding_contig_windows(self, subtree_root, window_length): + def _subtree_sliding_contig_windows( + self, + subtree_root: NodeId, + window_length: int, + ) -> list[ContigTrace]: """Get all the sliding windows of length 'window_length' from the subtree defined by the subtree root as run traces. @@ -951,7 +981,7 @@ def _subtree_sliding_contig_windows(self, subtree_root, window_length): return contig_windows - def sliding_windows(self, window_length): + def sliding_windows(self, window_length: int) -> list[RunTrace]: """Returns all the sliding windows over walker trajectories as run traces for a given window length. @@ -986,62 +1016,64 @@ def sliding_windows(self, window_length): return windows - @classmethod - def _rec_spanning_paths(cls, edges, root): - """Given a set of directed edges (source, target) and a root node id - of a tree imposed over the edges, returns all the paths over - that tree which span from the root to a leaf. + # TODO: not used anywhere should be removed. Also doesn't work - This is a recursive function and has pretty bad performance - for nontrivial simulations. + # @classmethod + # def _rec_spanning_paths(cls, edges: Edge, root: NodeId) -> list[list[Edge]]: + # """Given a set of directed edges (source, target) and a root node id + # of a tree imposed over the edges, returns all the paths over + # that tree which span from the root to a leaf. - Parameters - ---------- - edges : (node_id, node_id) + # This is a recursive function and has pretty bad performance + # for nontrivial simulations. - root : node_id + # Parameters + # ---------- + # edges : (node_id, node_id) - Returns - ------- - spanning_paths : list of edges + # root : node_id - """ + # Returns + # ------- + # spanning_paths : list of edges - # nodes targetting this root - root_sources = [] - - # go through all the edges and find those with this - # node as their target - for edge_source, edge_target in edges: - # check if the target_node we are looking for matches - # the edge target node - if root == edge_target: - # if this root is a target of the source add it to the - # list of edges targetting this root - root_sources.append(edge_source) - - # from the list of source nodes targetting this root we choose - # the lowest index one, so we sort them and iterate through - # finding the paths starting from it recursively - root_paths = [] - root_sources.sort() - for new_root in root_sources: - # add these paths for this new root to the paths for the - # current root - root_paths.extend(cls._rec_spanning_paths(edges, new_root)) - - # if there are no more sources to this root it is a leaf node and - # we terminate recursion, by not entering the loop above, however - # we manually generate an empty list for a path so that we return - # this "root" node as a leaf, for default. - if len(root_paths) < 1: - root_paths = [[]] - - final_root_paths = [] - for root_path in root_paths: - final_root_paths.append([root] + root_path) - - return final_root_paths + # """ + + # # nodes targetting this root + # root_sources = [] + + # # go through all the edges and find those with this + # # node as their target + # for edge_source, edge_target in edges: + # # check if the target_node we are looking for matches + # # the edge target node + # if root == edge_target: + # # if this root is a target of the source add it to the + # # list of edges targetting this root + # root_sources.append(edge_source) + + # # from the list of source nodes targetting this root we choose + # # the lowest index one, so we sort them and iterate through + # # finding the paths starting from it recursively + # root_paths = [] + # root_sources.sort() + # for new_root in root_sources: + # # add these paths for this new root to the paths for the + # # current root + # root_paths.extend(cls._rec_spanning_paths(edges, new_root)) + + # # if there are no more sources to this root it is a leaf node and + # # we terminate recursion, by not entering the loop above, however + # # we manually generate an empty list for a path so that we return + # # this "root" node as a leaf, for default. + # if len(root_paths) < 1: + # root_paths = [[]] + + # final_root_paths = [] + # for root_path in root_paths: + # final_root_paths.append([root] + root_path) + + # return final_root_paths # @classmethod # def _find_root_sources(cls, edges, root): @@ -1063,14 +1095,14 @@ def _rec_spanning_paths(cls, edges, root): # return root_sources - def _spanning_paths(self, root): + def _spanning_paths(self, root: NodeId) -> dict[NodeId, list[NodeId]]: """Parameters ---------- root : node_id Returns ------- - spanning_paths : list of list of edges + spanning_paths """ @@ -1104,7 +1136,7 @@ def _spanning_paths(self, root): return leaf_paths - def spanning_contig_traces(self): + def spanning_contig_traces(self) -> list[ContigTrace]: """Returns a list of all possible spanning contigs given the continuations present in this file. Spanning contigs are paths through a tree that must start from a root node and end at a @@ -1128,7 +1160,7 @@ def spanning_contig_traces(self): return spanning_contig_traces - def _root_spanning_contig_traces(self): + def _root_spanning_contig_traces(self) -> dict[NodeId, list[ContigTrace]]: """Returns a list of all possible spanning contigs given the continuations present in this file. Spanning contigs are paths through a tree that must start from a root node and end at a @@ -1140,7 +1172,7 @@ def _root_spanning_contig_traces(self): Returns ------- - spanning_contig_traces : dict of root_id to list of tuples of ints (run_idx, cycle_idx) + spanning_contig_traces: Dictionary mapping the root ids to all spanning contigs for it which are contig traces. @@ -1160,7 +1192,7 @@ def _root_spanning_contig_traces(self): return spanning_contig_traces @classmethod - def _contig_trace_to_contig_runs(cls, contig_trace): + def _contig_trace_to_contig_runs(cls, contig_trace: ContigTrace) -> list[int]: """Convert a contig trace to a list of runs. Parameters @@ -1183,7 +1215,7 @@ def _contig_trace_to_contig_runs(cls, contig_trace): return contig_runs @classmethod - def _contig_runs_to_continuations(cls, contig_runs): + def _contig_runs_to_continuations(cls, contig_runs: list[int]) -> ContinuationsTable: """Helper function to convert a list of run indices defining a contig to continuations. @@ -1204,7 +1236,7 @@ def _contig_runs_to_continuations(cls, contig_runs): return continuations @classmethod - def _continuations_to_contig_runs(cls, continuations): + def _continuations_to_contig_runs(cls, continuations: ContinuationsTable) -> list[int]: """Helper function that converts a list of continuations to a list of the runs in the order of the contigs defined by the continuations. @@ -1300,15 +1332,25 @@ class ContigTree(BaseContigTree): """ + closed: bool + _wepy_h5: WepyHDF5 + _base_contigtree: BaseContigTree + _graph: nx.DiGraph + _boundary_condition_class: type[BoundaryConditions] | None + _decision_class: type[BaseDecisionABC] | None + _continuations: set[tuple[int, int]] + _run_idxs: set[int] + _spans: dict[int, ContigTrace] + def __init__( self, - wepy_h5, - base_contigtree=None, - continuations=Ellipsis, - runs=Ellipsis, - boundary_condition_class=None, - decision_class=None, - ): + wepy_h5: WepyHDF5, + base_contigtree: BaseContigTree | None = None, + continuations: type(Ellipsis) | ContinuationsTable = Ellipsis, + runs: type(Ellipsis) | list[int] = Ellipsis, + boundary_condition_class: type[BoundaryConditions] | None = None, + decision_class: type[BaseDecisionABC] | None = None, + ) -> None: self.closed = True # if we pass a base contigtree use that one instead of building one manually @@ -1330,7 +1372,7 @@ def __init__( self._wepy_h5 = wepy_h5 - def _set_base_contigtree_to_self(self, base_contigtree): + def _set_base_contigtree_to_self(self, base_contigtree: BaseContigTree) -> None: self._base_contigtree = base_contigtree # then make references to this for the attributes we need @@ -1341,32 +1383,32 @@ def _set_base_contigtree_to_self(self, base_contigtree): self._run_idxs = self._base_contigtree._run_idxs self._spans = self._base_contigtree._spans - def open(self, mode=None): + def open(self, mode: FileMode | None = None) -> None: if self.closed: self.wepy_h5.open(mode=mode) self.closed = False else: raise IOError("This file is already open") - def close(self): + def close(self) -> None: self.wepy_h5.close() self.closed = True - def __enter__(self): + def __enter__(self) -> Self: self.wepy_h5.__enter__() self.closed = False return self - def __exit__(self, exc_type, exc_value, exc_tb): + def __exit__(self, exc_type, exc_value, exc_tb) -> None: self.wepy_h5.__exit__(exc_type, exc_value, exc_tb) self.close() @property - def base_contigtree(self): + def base_contigtree(self) -> BaseContigTree: return self._base_contigtree @property - def wepy_h5(self): + def wepy_h5(self) -> WepyHDF5: """The WepyHDF5 source object for which the contig tree is being constructed.""" return self._wepy_h5 @@ -1418,7 +1460,7 @@ def wepy_h5(self): # TODO: optimize this, we don't need to recalculate everything # each time to implement this - def make_contig(self, contig_trace): + def make_contig(self, contig_trace: ContigTrace) -> "Contig": """Create a Contig object given a contig trace. Parameters @@ -1447,7 +1489,7 @@ def make_contig(self, contig_trace): decision_class=self.decision_class, ) - def warp_trace(self): + def warp_trace(self) -> RunTrace: """Get the trace for all unique warping events from all contigs.""" with self: @@ -1460,7 +1502,7 @@ def warp_trace(self): # then cast to a set to get the unique ones return list(set(big_trace)) - def resampling_trace(self, decision_id): + def resampling_trace(self, decision_id: int) -> RunTrace: """Return full run traces for every specified type of resampling event. @@ -1495,7 +1537,7 @@ def final_trace(self): # then cast to a set to get the unique ones return list(set(big_trace)) - def lineages(self, trace, discontinuities=True): + def lineages(self, trace: RunTrace, discontinuities: bool = True) -> list[RunTrace]: """Get the ancestry lineage for each element of the trace as a run trace. """ @@ -1537,12 +1579,41 @@ class Contig(ContigTree): """ - def __init__(self, wepy_h5, **kwargs): + closed: bool + _wepy_h5: WepyHDF5 + _base_contigtree: BaseContigTree + _graph: nx.DiGraph + _boundary_condition_class: type[BoundaryConditions] | None + _decision_class: type[BaseDecisionABC] | None + _continuations: set[tuple[int, int]] + _run_idxs: set[int] + _spans: dict[int, ContigTrace] + + _contig_trace: ContigTrace + _contig_run_idxs: list[int] + + + def __init__( + self, + wepy_h5: WepyHDF5, + base_contigtree: BaseContigTree | None = None, + continuations: type(Ellipsis) | ContinuationsTable = Ellipsis, + runs: type(Ellipsis) | list[int] = Ellipsis, + boundary_condition_class: type[BoundaryConditions] | None = None, + decision_class: type[BaseDecisionABC] | None = None, + ): # uses superclass docstring exactly, this constructor just # generates some extra attributes # use the superclass initialization - super().__init__(wepy_h5, **kwargs) + super().__init__( + wepy_h5, + base_contigtree=base_contigtree, + continuations=continuations, + runs=runs, + boundary_condition_class=boundary_condition_class, + decision_class=decision_class, + ) # check that the result is a single contig spanning_contig_traces = self.spanning_contig_traces() @@ -1576,7 +1647,7 @@ def __init__(self, wepy_h5, **kwargs): else: self._contig_run_idxs = list(self.run_idxs) - def contig_fields(self, fields): + def contig_fields(self, fields: list[str]) -> dict[str, numpy.typing.ArrayLike]: """Returns trajectory field data for the specified fields. Parameters @@ -1593,7 +1664,7 @@ def contig_fields(self, fields): return self.wepy_h5.get_contig_trace_fields(self.contig_trace, fields) @property - def contig_trace(self): + def contig_trace(self) -> ContigTrace: """Returns the contig trace corresponding to this contig. Returns @@ -1605,7 +1676,7 @@ def contig_trace(self): return self._contig_trace @property - def num_cycles(self): + def num_cycles(self) -> int: """The number of cycles in this contig. Returns @@ -1615,8 +1686,32 @@ def num_cycles(self): """ return len(self.contig_trace) + def walker_trace_to_run_trace(self, contig_walker_trace: ContigWalkerTrace) -> RunTrace: + """Combine a walker trace to get the equivalent run trace for this contig. + + The contig_walker_trace cycle_idxs must be a subset of the + frame indices given by the contig_trace. + + + Parameters + ---------- + contig_walker_trace : list of tuples of ints (traj_idx, cycle_idx) + + Returns + ------- + run_trace : list of tuples of ints (run_idx, traj_idx, cycle_idx) + + See Also + -------- + Contig.contig_trace_to_run_trace : calls this static method + + """ + + return self.contig_trace_to_run_trace(self.contig_trace, contig_walker_trace) + + # TODO: may need to be implemented without using the wepy_h5 in the BaseContigTree - def num_walkers(self, cycle_idx): + def num_walkers(self, cycle_idx: int) -> int: """Get the number of walkers at a given cycle in the contig. Parameters @@ -1637,7 +1732,7 @@ def num_walkers(self, cycle_idx): return n_walkers - def records(self, record_key): + def records(self, record_key: str): """Returns the records for the given key. Parameters @@ -1651,7 +1746,7 @@ def records(self, record_key): """ return self.wepy_h5.run_contig_records(self._contig_run_idxs, record_key) - def records_dataframe(self, record_key): + def records_dataframe(self, record_key: str) -> pd.DataFrame: """Returns the records as a pandas.DataFrame for the given key. Parameters @@ -1679,7 +1774,7 @@ def resampling_records(self): return self.records(RESAMPLING) - def resampling_records_dataframe(self): + def resampling_records_dataframe(self) -> pd.DataFrame: """Returns the resampling records as a pandas.DataFrame. Returns @@ -1702,7 +1797,7 @@ def resampler_records(self): return self.records(RESAMPLER) - def resampler_records_dataframe(self): + def resampler_records_dataframe(self) -> pd.DataFrame: """Returns the resampler records as a pandas.DataFrame. Returns @@ -1725,7 +1820,7 @@ def warping_records(self): return self.records(WARPING) - def warping_records_dataframe(self): + def warping_records_dataframe(self) -> pd.DataFrame: """Returns the warping records as a pandas.DataFrame. Returns @@ -1748,7 +1843,7 @@ def bc_records(self): return self.records(BC) - def bc_records_dataframe(self): + def bc_records_dataframe(self) -> pd.DataFrame: """Returns the boundary conditions records as a pandas.DataFrame. Returns @@ -1771,7 +1866,7 @@ def progress_records(self): return self.records(PROGRESS) - def progress_records_dataframe(self): + def progress_records_dataframe(self) -> pd.DataFrame: """Returns the progress records as a pandas.DataFrame. Returns @@ -1794,7 +1889,7 @@ def resampling_panel(self): return self.wepy_h5.run_contig_resampling_panel(self._contig_run_idxs) - def parent_table(self, discontinuities=True): + def parent_table(self, discontinuities: bool = True) -> ParentTable: """Returns the full parent table for this contig. Notes @@ -1820,7 +1915,11 @@ def parent_table(self, discontinuities=True): self.contig_trace, discontinuities=discontinuities ) - def lineages_contig(self, contig_trace, discontinuities=True): + def lineages_contig( + self, + contig_trace: ContigTrace, + discontinuities: bool = True, + ): # get the parent table for this contig parent_table = self.parent_table(discontinuities=discontinuities) @@ -1832,7 +1931,7 @@ def lineages_contig(self, contig_trace, discontinuities=True): return lineages - def lineages(self, contig_trace, discontinuities=True): + def lineages(self, contig_trace: ContigTrace, discontinuities: bool = True): """Get the ancestry lineage for each element of the trace as a run trace. """ @@ -1844,7 +1943,7 @@ def lineages(self, contig_trace, discontinuities=True): ) ] - def warp_contig_trace(self): + def warp_contig_trace(self) -> ContigWalkerTrace: """Return a trace that gives all of the walkers that were warped.""" trace = [] @@ -1858,7 +1957,7 @@ def warp_contig_trace(self): return trace - def resampling_contig_trace(self, decision_id): + def resampling_contig_trace(self, decision_id: int) -> ContigWalkerTrace: """Return full run traces for every specified type of resampling event. @@ -1881,7 +1980,7 @@ def resampling_contig_trace(self, decision_id): return trace - def final_contig_trace(self): + def final_contig_trace(self) -> ContigWalkerTrace: # this is just the last cycle index last_cycle_idx = self.num_cycles - 1 @@ -1893,7 +1992,7 @@ def final_contig_trace(self): return trace - def warp_trace(self): + def warp_trace(self) -> RunTrace: """Return a run trace that gives all of the walkers that were warped.""" trace = self.warp_contig_trace() @@ -1902,7 +2001,7 @@ def warp_trace(self): return run_trace - def resampling_trace(self, decision_id): + def resampling_trace(self, decision_id: int) -> RunTrace: """Return full run traces for every specified type of resampling event. @@ -1920,7 +2019,7 @@ def resampling_trace(self, decision_id): return run_trace - def final_trace(self): + def final_trace(self) -> RunTrace: """Return a trace of all the walkers at the end of the contig.""" trace = self.final_contig_trace() @@ -1931,16 +2030,16 @@ def final_trace(self): def resampling_tree_layout_graph( self, - bc_class=None, - progress_key=None, - node_shape="disc", - discontinuous_node_shape="square", - colormap_name="plasma", - node_radius=None, - row_spacing=None, - step_spacing=None, - central_axis=None, - ): + bc_class: type[BoundaryConditions] | None = None, + progress_key: str | None = None, + node_shape: str = "disc", + discontinuous_node_shape: str = "square", + colormap_name: str = "plasma", + node_radius: float | None = None, + row_spacing: float | None = None, + step_spacing: float | None = None, + central_axis: float | None = None, + ) -> LayoutGraph: ### The data we need for making the resampling tree ## parent table, don't include discontinuities, we will handle diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index fbde12b4..d8da2fce 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -621,4 +621,13 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: ) + return path + +@pytest.fixture(scope="function") +def wepy_h5_full_init(_wepy_h5_full_init, tmpdir) -> Path: + + path = tmpdir / "main.wepy.h5" + + reflink_or_copy(_wepy_h5_full_init, path) + return path diff --git a/tests/unit/test_analysis/test_contig_tree.py b/tests/unit/test_analysis/test_contig_tree.py index 29d98a81..6f93a1ea 100644 --- a/tests/unit/test_analysis/test_contig_tree.py +++ b/tests/unit/test_analysis/test_contig_tree.py @@ -1,5 +1,6 @@ from pathlib import Path import wepy +import networkx as nx from wepy.analysis.contig_tree import ( BaseContigTree, ContigTree, @@ -9,19 +10,503 @@ class Test_BaseContigTree: - def test___init__(self, alanine_dipeptide_revo_wepy_hdf5: Path): + def test___init__(self, wepy_h5_full_init: Path): - wepy_h5 = wepy.WepyHDF5(alanine_dipeptide_revo_wepy_hdf5, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') bct = BaseContigTree( wepy_h5, - decision_class=wepy.MultiCloneMergeDecision, + decision_class=wepy.NoDecision, ) - # TODO: test multiple runs/files + assert bct.continuations == {(1, 0)} + assert bct.run_idxs == {0, 1} + + assert set(bct.graph.nodes) == { + (0, 0), + (0, 1), + (1, 0), + (1, 1), + } + + assert set(bct.graph.edges) == { + ((0, 1), (0, 0)), + ((1, 0), (0, 1)), + ((1, 1), (1, 0)), + } + + for node_id in bct.graph.nodes: + node = bct.graph.nodes[node_id] + assert set(node.keys()) == { + "resampling_steps", "parent_idxs", "discontinuities", + } + + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + runs=[0], + ) + + assert bct.continuations == set() + assert bct.run_idxs == {0} + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + runs=[0, 1], + ) + + assert bct.continuations == {(1, 0)} + assert bct.run_idxs == {0, 1} + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + runs=[0, 1], + continuations=[(1, 0)], + ) + + assert bct.continuations == {(1, 0)} + assert bct.run_idxs == {0, 1} + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + runs=[0, 1], + # NOTE: manually overriding, even if incorrect + continuations=[(0, 1)], + ) + + assert bct.continuations == {(0, 1)} + assert bct.run_idxs == {0, 1} + + def test_contig_to_run_trace(self): + + assert BaseContigTree.contig_trace_to_run_trace( + [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), + ], + [ + (0, 0), + (0, 1), + (0, 2), + (0, 3), + ] + ) == [ + (0, 0, 0), + (0, 0, 1), + (1, 0, 0), + (1, 0, 1), + ] + + def test_run_trace_to_contig_trace(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert bct.run_trace_to_contig_trace( + [ + (0, 0, 0), + (0, 0, 1), + (1, 0, 0), + (1, 0, 1), + ] + ) == [ + (0, 0), + (0, 1), + (0, 2), + (0, 3), + ] + + def test_contig_cycle_idx(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert bct.contig_cycle_idx(0, 0) == 0 + assert bct.contig_cycle_idx(0, 1) == 1 + assert bct.contig_cycle_idx(1, 0) == 2 + assert bct.contig_cycle_idx(1, 1) == 3 + + def test_get_branch_trace(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert bct.get_branch_trace( + run_idx=0, + cycle_idx=0, + start_contig_idx=0, + ) == [ + (0, 0) + ] + + assert bct.get_branch_trace( + run_idx=0, + cycle_idx=3, + start_contig_idx=0, + ) == [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), + ] + + def test_trace_parent_table(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert bct.trace_parent_table( + [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), + ], + discontinuities=False, + ) == [ + [0, 1], + [0, 1], + [0, 1], + [0, 1], + ] + + # TODO: discontinuities + + def test__tree_leaves(self): + + assert set(BaseContigTree._tree_leaves( + (0, 0), + # NOTE: the tree is reversed from what is in the BaseContigTree + nx.DiGraph([ + ((0, 0), (0, 1),), + ((0, 1), (1, 0),), + # leaves + ((1, 0), (1, 1),), + ((1, 0), (2, 0),), + ]) + )) == { + (1, 1), + (2, 0), + } + + def test__subtree_leaves(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert set(bct._subtree_leaves((0, 0))) == {(1, 1)} + + def test_leaves(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert set(bct.leaves()) == {(1,1)} + + def test_root_leaves(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + root_leaves = bct.root_leaves() + assert (0, 0) in root_leaves + assert set(root_leaves[(0, 0)]) == { + (1,1), + } + + def test_subtrees(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + # for one tree its the same + assert len(bct.subtrees()) == 1 + + def test_get_subtree(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + # TODO: this method is fallacious and need reworked, so just a + # minimal test + bct.get_subtree((0, 0)).nodes + + def test__subtree_root(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert bct._subtree_root((0,0)) == (0,0) + assert bct._subtree_root((0,1)) == (0,0) + assert bct._subtree_root((1,0)) == (0,0) + assert bct._subtree_root((1,1)) == (0,0) + + def test_roots(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert bct.roots() == [(0, 0)] + + def test_span_traces(self, wepy_h5_full_init): + + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert bct.span_traces == { + 0 : [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), + ]} + + + def test__root_spanning_contig_traces(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert bct._root_spanning_contig_traces() == { + (0, 0) : [ + [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), + ], + ], + } + + + def test_spanning_contig_traces(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert bct.spanning_contig_traces() == [ + [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), + ], + ] + + def test__spanning_paths(self, wepy_h5_full_init): + + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert bct._spanning_paths((0,0)) == { + (1, 1) : [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), + ] + } + + def test__contig_trace_to_contig_runs(self): + + assert BaseContigTree._contig_trace_to_contig_runs( + [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), + ], + ) == [0, 1] + + def test__contig_runs_to_continuations(self): + + assert BaseContigTree._contig_runs_to_continuations( + [0, 1] + ) == [[1, 0]] + + def test__continuations_to_contig_runs(self): + + assert BaseContigTree._continuations_to_contig_runs( + [[1, 0]] + ) == [0, 1] + + # TODO: requires Contig + def test_span_contig(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = BaseContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert False + + assert bct.span_traces == { + 0 : [] + } class Test_ContigTree: - pass + def test___init__(self, wepy_h5_full_init): + + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + ct = ContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert ct.wepy_h5.closed + + assert ct.base_contigtree is not None + + + def test_make_contig(self, wepy_h5_full_init): + + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + ct = ContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + contig = ct.make_contig([(0, 0), (0, 1), (1, 0), (1, 1)]) + + # TODO: + # def test_resampling_trace(self, wepy_h5_full_init): + # wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + # ct = ContigTree( + # wepy_h5, + # decision_class=wepy.NoDecision, + # ) + + def test_final_trace(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + ct = ContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert ct.final_trace() == [ + (1, 0, 1), + (1, 1, 1), + ] + + def test_lineages(self, wepy_h5_full_init): + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + ct = ContigTree( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert ct.lineages( + [(1, 0, 1),],# (1, 1, 1)], + discontinuities=False, + ) == [ + [ + (0, 0, 0), + (0, 0, 1), + (1, 0, 0), + (1, 0, 1), + ], + ] + class Test_Contig: - pass + + def test___init__(self, wepy_h5_full_init): + + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + contig = Contig( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert contig.contig_trace == [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), + ] + + assert contig.num_cycles == 4 + + def test_walker_trace_to_run_trace(self, wepy_h5_full_init): + + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + + bct = Contig( + wepy_h5, + decision_class=wepy.NoDecision, + ) + + assert bct.walker_trace_to_run_trace( + [ + (0, 0), + (0, 1), + (0, 2), + (0, 3), + ], + ) == [ + (0, 0, 0), + (0, 0, 1), + (1, 0, 0), + (1, 0, 1), + ] + From 4aca796caedb161febbacc085ee41bc84985cc17 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sun, 25 Jan 2026 13:57:27 -0500 Subject: [PATCH 138/143] !wip more fixes and tests --- src/wepy/analysis/contig_tree.py | 9 +- src/wepy/hdf5.py | 143 ++++++++++++++---- src/wepy/resampling/decisions/decision.py | 44 +++--- src/wepy/storage/protocol.py | 12 ++ tests/unit/test_analysis/test_parents.py | 80 +++++----- tests/unit/test_hdf5.py | 113 ++++++++++---- .../test_decisions/test_decision.py | 10 +- 7 files changed, 281 insertions(+), 130 deletions(-) diff --git a/src/wepy/analysis/contig_tree.py b/src/wepy/analysis/contig_tree.py index 93941149..71617f5c 100644 --- a/src/wepy/analysis/contig_tree.py +++ b/src/wepy/analysis/contig_tree.py @@ -31,6 +31,7 @@ from geomm.free_energy import free_energy as calc_free_energy # First Party Library +from wepy.storage.protocol import ContigWalkerTrace, RunTrace, ContigTrace from wepy.analysis.network_layouts.layout_graph import LayoutGraph from wepy.analysis.network_layouts.tree import ResamplingTreeLayout from wepy.analysis.parents import ( @@ -59,14 +60,6 @@ BC: Final = "boundary_conditions" """Record key for boundary condition records.""" -# (traj_idx, cycle_idx) -ContigWalkerTrace = list[tuple[int, int], ...] - -# (run_idx, traj_idx, cycle_idx) -RunTrace = list[tuple[int, int, int], ...] - -# (run_idx, cycle_idx) -ContigTrace = list[tuple[int, int], ...] # (run_idx, cycle_idx) NodeId = tuple[int, int] diff --git a/src/wepy/hdf5.py b/src/wepy/hdf5.py index b4fcd1d5..e35076e0 100644 --- a/src/wepy/hdf5.py +++ b/src/wepy/hdf5.py @@ -396,7 +396,7 @@ import itertools as it import json import logging -from typing import Any, TypedDict, NotRequired, Literal, Union, Required +from typing import Any, TypedDict, NotRequired, Literal, Union, Required, Generator # Standard Library import os.path as osp @@ -422,6 +422,7 @@ from wepy.resampling.decisions.decision import DecisionRecord from wepy.storage.protocol import ( + ContigWalkerTrace, RunTrace, ContigTrace, RecordValueDtype, Record, RunRecord, @@ -738,7 +739,7 @@ class WepyHDF5ReadError(WepyHDF5Error): pass # utility for paths -def _iter_field_paths(grp: h5py.Group): +def _iter_field_paths(grp: h5py.Group) -> list[str]: """Return all subgroup field name paths from a group. Useful for compound fields. For example if you have the group @@ -2573,7 +2574,13 @@ def _run_records_continual(self, run_idxs: list[int], run_record_key: RunRecordK return records - def _get_contiguous_traj_field(self, run_idx, traj_idx, field_path, frames=None): + def _get_contiguous_traj_field( + self, + run_idx: int, + traj_idx: int, + field_path: str, + frames: list[int] | None = None, + ) -> FieldsData: """Access actual data for a trajectory field. Parameters @@ -2592,7 +2599,7 @@ def _get_contiguous_traj_field(self, run_idx, traj_idx, field_path, frames=None) """ - field_thing = self.get_traj_field(run_idx, traj_idx, field_path) + field_thing = self.traj_field_entity(run_idx, traj_idx, field_path) if frames is None: field = field_thing[:] @@ -2602,8 +2609,13 @@ def _get_contiguous_traj_field(self, run_idx, traj_idx, field_path, frames=None) return field def _get_sparse_traj_field( - self, run_idx, traj_idx, field_path, frames=None, masked=True - ): + self, + run_idx: int, + traj_idx: int, + field_path: str, + frames: list[int] | None = None, + masked: bool = True, + ) -> FieldsData: """Access actual data for a trajectory field. Parameters @@ -2677,7 +2689,14 @@ def _get_sparse_traj_field( return data - def _add_run_field(self, run_idx, field_path, data, sparse_idxs=None, force=False): + def _add_run_field( + self, + run_idx: int, + field_path: str, + data: NDArray, + sparse_idxs: list[int] | None = None, + force: bool = False, + ) -> None: """Add a trajectory field to all trajectories in a run. By enforcing adding it to all trajectories at one time we @@ -2773,7 +2792,13 @@ def _add_run_field(self, run_idx, field_path, data, sparse_idxs=None, force=Fals *idx_tup, field_path, data[i], sparse_idxs=sparse_idxs[i] ) - def _add_field(self, field_path, data, sparse_idxs=None, force=False): + def _add_field( + self, + field_path: str, + data: list[NDArray], + sparse_idxs: list[int] | None = None, + force: bool = False + ) -> None: """Add a trajectory field to all runs in a file. Parameters @@ -3044,7 +3069,15 @@ def progress_grp(self, run_idx: int) -> h5py.Group: """ return self.records_grp(run_idx, PROGRESS) - def iter_runs(self, idxs: bool = False, run_sel: list[int] | None = None): + def iter_runs( + self, + idxs: bool = False, + run_sel: list[int] | None = None, + ) -> Generator[ + tuple[int, h5py.Group] | h5py.Group, + None, + None, + ]: """Generator for iterating through the runs of a file. Parameters @@ -3075,7 +3108,15 @@ def iter_runs(self, idxs: bool = False, run_sel: list[int] | None = None): else: yield run - def iter_trajs(self, idxs: bool = False, traj_sel: list[int] | None = None): + def iter_trajs( + self, + idxs: bool = False, + traj_sel: list[int] | None = None, + ) -> Generator[ + tuple[tuple[int, int], h5py.Group] | h5py.Group, + None, + None, + ]: """Generator for iterating over trajectories in a file. Parameters @@ -3109,7 +3150,15 @@ def iter_trajs(self, idxs: bool = False, traj_sel: list[int] | None = None): else: yield traj - def iter_run_trajs(self, run_idx: int, idxs: bool = False): + def iter_run_trajs( + self, + run_idx: int, + idxs: bool = False, + ) -> Generator[ + tuple[tuple[int, int], h5py.Group], + None, + None, + ]: """Iterate over the trajectories of a run. Parameters @@ -4210,7 +4259,7 @@ def init_progress_record_fields(self, bc): """ self.init_record_fields(PROGRESS, bc.progress_record_field_names()) - def add_continuation(self, continuation_run, base_run): + def add_continuation(self, continuation_run: int, base_run: int) -> None: """Add a continuation between runs. Parameters @@ -5643,7 +5692,14 @@ def compute_observable( ## Trajectory Getters - def get_traj_field(self, run_idx, traj_idx, field_path, frames=None, masked=True): + def get_traj_field( + self, + run_idx: int, + traj_idx: int, + field_path: str, + frames: list[int] | None = None, + masked: bool = True + ) -> FieldsData: """Returns a numpy array for the given trajectory field. You can control how sparse fields are returned using the @@ -5688,10 +5744,10 @@ def get_traj_field(self, run_idx, traj_idx, field_path, frames=None, masked=True def get_trace_fields( self, - frame_tups, - fields, - same_order=True, - ): + frame_tups: RunTrace, + fields: list[str], + same_order: bool = True, + ) -> FieldsData: """Get trajectory field data for the frames specified by the trace. Parameters @@ -5796,7 +5852,12 @@ def apply_argsorted(shuffled_seq, sorted_idxs): return frame_fields - def get_run_trace_fields(self, run_idx, frame_tups, fields): + def get_run_trace_fields( + self, + run_idx: int, + frame_tups: ContigWalkerTrace, + fields: str, + ) -> FieldsData: """Get trajectory field data for the frames specified by the trace within a single run. @@ -5834,7 +5895,11 @@ def get_run_trace_fields(self, run_idx, frame_tups, fields): return frame_fields - def get_contig_trace_fields(self, contig_trace, fields): + def get_contig_trace_fields( + self, + contig_trace: ContigTrace, + fields: list[str], + ) -> FieldsData: """Get field data for all trajectories of a contig for the frames specified by the contig trace. @@ -5919,7 +5984,16 @@ def get_contig_trace_fields(self, contig_trace, fields): return field_values - def iter_trajs_fields(self, fields, idxs=False, traj_sel=None): + def iter_trajs_fields( + self, + fields: list[str], + idxs: bool = False, + traj_sel: list[tuple[int, int]] | None = None, + ) -> Generator[ + tuple[tuple[int, int], FieldsData] | FieldsData, + None, + None, + ]: """Generator for iterating over fields trajectories in a file. Parameters @@ -5978,7 +6052,13 @@ def iter_trajs_fields(self, fields, idxs=False, traj_sel=None): yield dsets def traj_fields_map( - self, func, fields, args, map_func=map, idxs=False, traj_sel=None + self, + func, + fields: list[str], + args: None | tuple[Any, ...], + map_func=map, + idxs: bool = False, + traj_sel: list[tuple[int, int]] | None = None, ): """Function for mapping work onto field of trajectories. @@ -6051,7 +6131,13 @@ def traj_fields_map( else: return results - def to_mdtraj(self, run_idx, traj_idx, frames=None, alt_rep=None): + def to_mdtraj( + self, + run_idx: int, + traj_idx: int, + frames: list[int] | None = None, + alt_rep: str | None = None, + ) -> mdtraj.Trajectory: """Convert a trajectory to an mdtraj Trajectory object. Works if the right trajectory fields are defined. Minimally @@ -6146,7 +6232,7 @@ def to_mdtraj(self, run_idx, traj_idx, frames=None, alt_rep=None): return traj - def trace_to_mdtraj(self, trace, alt_rep=None): + def trace_to_mdtraj(self, trace: RunTrace, alt_rep: str | None = None) -> mdtraj.Trajectory: """Generate an mdtraj Trajectory from a trace of frames from the runs. Uses the default fields for positions (unless an alternate @@ -6182,7 +6268,12 @@ def trace_to_mdtraj(self, trace, alt_rep=None): return self.traj_fields_to_mdtraj(trace_fields, alt_rep=alt_rep) - def run_trace_to_mdtraj(self, run_idx, trace, alt_rep=None): + def run_trace_to_mdtraj( + self, + run_idx: int, + trace: ContigWalkerTrace, + alt_rep: str | None = None, + ) -> mdtraj.Trajectory: """Generate an mdtraj Trajectory from a trace of frames from the runs. Uses the default fields for positions (unless an alternate @@ -6223,7 +6314,7 @@ def run_trace_to_mdtraj(self, run_idx, trace, alt_rep=None): return self.traj_fields_to_mdtraj(trace_fields, alt_rep=alt_rep) - def _choose_rep_path(self, alt_rep): + def _choose_rep_path(self, alt_rep: str | None) -> str: """Given a positions specification string, gets the field name/path for it. @@ -6267,7 +6358,7 @@ def _choose_rep_path(self, alt_rep): return rep_path - def traj_fields_to_mdtraj(self, traj_fields, alt_rep=POSITIONS): + def traj_fields_to_mdtraj(self, traj_fields: FieldsData, alt_rep: str = POSITIONS) -> mdtraj.Trajectory: """Create an mdtraj.Trajectory from a traj_fields dictionary. Parameters diff --git a/src/wepy/resampling/decisions/decision.py b/src/wepy/resampling/decisions/decision.py index 04094c9d..df5b7813 100644 --- a/src/wepy/resampling/decisions/decision.py +++ b/src/wepy/resampling/decisions/decision.py @@ -212,36 +212,36 @@ def enum_by_name(cls, enum_name: str) -> DecisionEnum_: d = cls.enum_dict_by_name() return d[enum_name] - # @classmethod - # def record(cls, enum_value: int, **fields: dict[str, Any]) -> DecisionRecord_: - # """Generate a record for the enum_value and the other fields. + @classmethod + def record(cls, enum_value: int, **fields: dict[str, Any]) -> DecisionRecord_: + """Generate a record for the enum_value and the other fields. - # Parameters - # ---------- - # enum_value : int + Parameters + ---------- + enum_value : int - # Returns - # ------- - # rec : dict of str: value + Returns + ------- + rec : dict of str: value - # """ + """ - # assert ( - # enum_value in cls.enum_dict_by_value() - # ), "value is not a valid Enumerated value" + assert ( + enum_value in cls.enum_dict_by_value() + ), "value is not a valid Enumerated value" - # for field_key in fields.keys(): - # assert ( - # field_key in cls.FIELDS - # ), "The field {} is not a field for that decision".format(field_key) - # assert field_key != "decision_id", "'decision_id' cannot be an extra field" + for field_key in fields.keys(): + assert ( + field_key in cls.FIELDS + ), "The field {} is not a field for that decision".format(field_key) + assert field_key != "decision_id", "'decision_id' cannot be an extra field" - # rec_d = {"decision_id": enum_value} - # rec_d.update(fields) + rec_d = {"decision_id": enum_value} + rec_d.update(fields) - # rec = cls.DECISION_RECORD(**rec_d) + rec = cls.DECISION_RECORD(**rec_d) - # return rec + return rec @classmethod def action( diff --git a/src/wepy/storage/protocol.py b/src/wepy/storage/protocol.py index c0d5fd20..5bc31e38 100644 --- a/src/wepy/storage/protocol.py +++ b/src/wepy/storage/protocol.py @@ -123,3 +123,15 @@ class WarpRecordUnstruct(TypedDict, total=False): target_idx: Required[int] weight: Required[float] + + +# Trace types used in data access +# (traj_idx, cycle_idx) +ContigWalkerTrace = list[tuple[int, int], ...] + +# (run_idx, traj_idx, cycle_idx) +RunTrace = list[tuple[int, int, int], ...] + +# (run_idx, cycle_idx) +ContigTrace = list[tuple[int, int], ...] + diff --git a/tests/unit/test_analysis/test_parents.py b/tests/unit/test_analysis/test_parents.py index e1d4883a..58f5761f 100644 --- a/tests/unit/test_analysis/test_parents.py +++ b/tests/unit/test_analysis/test_parents.py @@ -15,46 +15,46 @@ def test_resampling_panel(): - # simple case - assert resampling_panel( - [ - RunRecord( - cycle_idx=0, - record=dict( - step_idx=0, - walker_idx=0, - decision_id=0, - target_idxs=(0,), - ) - ), - RunRecord( - cycle_idx=0, - record=dict( - step_idx=0, - walker_idx=1, - decision_id=0, - target_idxs=(1,), - ) - ), - ] - ) == [ - # cycle 0 - [ - # step 0 - [ - # walker 0 - { - "decision_id" : 0, - "target_idxs" : (0,) - }, - # walker 1 - { - "decision_id" : 0, - "target_idxs" : (1,) - }, - ] - ] - ] + # # simple case + # assert resampling_panel( + # [ + # RunRecord( + # cycle_idx=0, + # record=dict( + # step_idx=0, + # walker_idx=0, + # decision_id=0, + # target_idxs=(0,), + # ) + # ), + # RunRecord( + # cycle_idx=0, + # record=dict( + # step_idx=0, + # walker_idx=1, + # decision_id=0, + # target_idxs=(1,), + # ) + # ), + # ] + # ) == [ + # # cycle 0 + # [ + # # step 0 + # [ + # # walker 0 + # { + # "decision_id" : 0, + # "target_idxs" : (0,) + # }, + # # walker 1 + # { + # "decision_id" : 0, + # "target_idxs" : (1,) + # }, + # ] + # ] + # ] # TODO: failing # with multiple steps diff --git a/tests/unit/test_hdf5.py b/tests/unit/test_hdf5.py index 65f08de4..e0c92d1f 100644 --- a/tests/unit/test_hdf5.py +++ b/tests/unit/test_hdf5.py @@ -29,35 +29,23 @@ from wepy_tools.systems.lennard_jones import LennardJonesPair -def reflink_or_copy(src: Path, dst: Path) -> None: - """ - Create a copy-on-write reflink if supported. - Fall back to a full copy otherwise. - """ - try: - if sys.platform.startswith("linux"): - subprocess.run( - ["cp", "--reflink=auto", src, dst], - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - elif sys.platform == "darwin": - subprocess.run( - ["cp", "-c", src, dst], # APFS clone - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - else: - raise RuntimeError("No reflink support") - except Exception: - shutil.copy2(src, dst) - - - - - +_INIT_WALKERS = [ + Walker( + WalkerStateBox( + positions=np.array([ + [1., 1., 1.], + [2., 2., 2.], + ]), + box_vectors=np.array([ + [1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.], + ]), + kinetic_energy=3.455, + ), + 0.1, + ), + ] # # TODO: this will be easier once we have a fixture for a full WepyHDF5 # def test__iter_field_paths(wepy_h5_file_ro): @@ -2091,6 +2079,73 @@ def test_run_resampling_panel(self, wepy_h5_traj_init): ], ] + def test__get_sparse_traj_field(self, wepy_h5_full_init): + with WepyHDF5(wepy_h5_full_init, mode='r') as wepy_h5: + + v_masked = wepy_h5._get_sparse_traj_field(0, 0, "velocities", masked=True) + + assert np.ma.is_masked(v_masked) + assert v_masked.shape == (2, 2, 3) + + assert np.all(v_masked[0].mask) + assert np.all(np.isnan(v_masked[0].data)) + + assert not np.any(v_masked[1].mask) + assert not np.any(np.isnan(v_masked[1].data)) + + v_unmasked = wepy_h5._get_sparse_traj_field(0, 0, "velocities", masked=False) + + assert v_unmasked.shape == (1, 2, 3) + assert not np.ma.is_masked(v_unmasked) + + # TODO: + # v_unmasked_sel = wepy_h5._get_sparse_traj_field( + # 0, 0, "velocities", masked=False, + # frames=[0], + # ) + + # assert v_unmasked_sel.shape == (1, 2, 3) + # assert not np.ma.is_masked(v_unmasked_sel) + + + def test__get_contiguous_traj_field(self, wepy_h5_full_init): + with WepyHDF5(wepy_h5_full_init, mode='r') as wepy_h5: + + p = wepy_h5._get_contiguous_traj_field(0, 0, "positions", frames=None) + + assert p.shape == (2, 2, 3) + + p = wepy_h5._get_contiguous_traj_field(0, 0, "positions", frames=[0]) + assert p.shape == (1, 2, 3) + + p = wepy_h5._get_contiguous_traj_field(0, 0, "positions", frames=[1]) + assert p.shape == (1, 2, 3) + + p = wepy_h5._get_contiguous_traj_field(0, 0, "positions", frames=[0,1]) + assert p.shape == (2, 2, 3) + + # TODO: better error here + # p = wepy_h5._get_contiguous_traj_field(0, 0, "positions", frames=[0,1,2]) + + + def test_get_traj_field(self, wepy_h5_full_init): + with WepyHDF5(wepy_h5_full_init, mode='r') as wepy_h5: + p = wepy_h5.get_traj_field(0, 0, "positions") + + assert p.shape == (2, 2, 3) + assert not np.ma.is_masked(p) + + v_masked = wepy_h5.get_traj_field(0, 0, "velocities") + + assert v_masked.shape == (2, 2, 3) + assert np.ma.is_masked(v_masked) + + v_unmasked = wepy_h5.get_traj_field(0, 0, "velocities", masked=False) + + assert v_unmasked.shape == (1, 2, 3) + assert not np.ma.is_masked(v_unmasked) + + # TODO: for observables # diff --git a/tests/unit/test_resampling/test_decisions/test_decision.py b/tests/unit/test_resampling/test_decisions/test_decision.py index ca591630..aa85307b 100644 --- a/tests/unit/test_resampling/test_decisions/test_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_decision.py @@ -76,12 +76,12 @@ def test_enum_by_value(self): def test_enum_by_name(self): assert MockDecision.enum_by_name("NOTHING") == MockDecisionEnum.NOTHING - # def test_record(self): + def test_record(self): - # assert MockDecision.record(0, (0,)) == BaseDecisionRecord( - # decision_id=0, - # target_idxs=(0,), - # ) + assert MockDecision.record(0, target_idxs=(0,)) == BaseDecisionRecord( + decision_id=0, + target_idxs=(0,), + ) def test_action(self): From ebcf1f75d5e056845338230ef5bda4265480c78b Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Sun, 25 Jan 2026 14:16:12 -0500 Subject: [PATCH 139/143] fixes duplicate field definition --- src/wepy/resampling/decisions/clone_merge.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wepy/resampling/decisions/clone_merge.py b/src/wepy/resampling/decisions/clone_merge.py index 06f7c1c0..7a263550 100644 --- a/src/wepy/resampling/decisions/clone_merge.py +++ b/src/wepy/resampling/decisions/clone_merge.py @@ -116,11 +116,11 @@ class MultiCloneMergeDecision(BaseDecisionABC): DECISION_RECORD = CloneMergeDecisionRecord - FIELDS = BaseDecisionABC.FIELDS + ("target_idxs",) - SHAPES = BaseDecisionABC.SHAPES + (Ellipsis,) - DTYPES = BaseDecisionABC.DTYPES + (int,) + FIELDS = BaseDecisionABC.FIELDS + SHAPES = BaseDecisionABC.SHAPES + DTYPES = BaseDecisionABC.DTYPES - RECORD_FIELDS = BaseDecisionABC.RECORD_FIELDS + ("target_idxs",) + RECORD_FIELDS = BaseDecisionABC.RECORD_FIELDS # the decision types that pass on their state ANCESTOR_DECISION_IDS = ( From ca4b985230c6ea300c7aa05e7fb5ade1935241a3 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 26 Jan 2026 12:44:19 -0500 Subject: [PATCH 140/143] formatting --- src/wepy/__init__.py | 147 +- src/wepy/analysis/contig_tree.py | 79 +- src/wepy/analysis/parents.py | 51 +- src/wepy/factory.py | 6 +- src/wepy/hdf5.py | 586 ++++---- src/wepy/reporter/base.py | 6 +- src/wepy/reporter/dashboard.py | 111 +- src/wepy/reporter/hdf5.py | 166 ++- src/wepy/reporter/openmm.py | 1 + src/wepy/reporter/restree.py | 3 +- src/wepy/resampling/decisions/clone_merge.py | 9 +- src/wepy/resampling/decisions/decision.py | 32 +- src/wepy/resampling/decisions/no_decision.py | 13 +- src/wepy/resampling/resamplers/clone_merge.py | 16 +- src/wepy/resampling/resamplers/noresampler.py | 13 +- src/wepy/resampling/resamplers/resampler.py | 14 +- src/wepy/resampling/resamplers/revo.py | 38 +- src/wepy/resampling/resamplers/wexplore.py | 14 +- src/wepy/runners/mock.py | 2 +- src/wepy/runners/openmm/__init__.py | 2 +- src/wepy/runners/openmm/runner.py | 2 +- src/wepy/runners/openmm/state.py | 2 +- src/wepy/runners/runner.py | 6 +- src/wepy/sim_manager.py | 4 +- src/wepy/storage/protocol.py | 67 +- src/wepy/typing.py | 14 +- src/wepy/util/attrs.py | 9 +- src/wepy/util/openmm.py | 4 +- src/wepy/walker.py | 14 +- src/wepy/work_mapper/base.py | 1 - src/wepy/work_mapper/openmm/proc_pool.py | 4 +- src/wepy/work_mapper/serial.py | 3 +- src/wepy_tools/systems/alanine_dipeptide.py | 4 +- src/wepy_tools/systems/lennard_jones.py | 3 +- tests/data/synthesize.py | 25 +- .../integration/test_openmm/test_realistic.py | 17 +- tests/unit/conftest.py | 555 +++++--- tests/unit/test_analysis/test_contig_tree.py | 185 +-- tests/unit/test_analysis/test_parents.py | 148 +- tests/unit/test_fixtures.py | 89 +- tests/unit/test_hdf5.py | 1223 +++++++++-------- tests/unit/test_reporter/test_dashboard.py | 77 +- tests/unit/test_reporter/test_hdf5.py | 454 +++--- tests/unit/test_reporter/test_openmm.py | 10 +- .../test_decisions/test_clone_merge.py | 29 +- .../test_decisions/test_decision.py | 26 +- .../test_decisions/test_no_decision.py | 13 +- .../test_resamplers/test_noresampler.py | 54 +- tests/unit/test_util/test_attrs.py | 8 +- tests/unit/test_walker.py | 23 +- 50 files changed, 2487 insertions(+), 1895 deletions(-) diff --git a/src/wepy/__init__.py b/src/wepy/__init__.py index 228c0060..1e1390df 100644 --- a/src/wepy/__init__.py +++ b/src/wepy/__init__.py @@ -2,124 +2,119 @@ # Local Modules from .__about__ import __version__ as __version__ -from .hdf5 import WepyHDF5 -from .monitor import Monitor -from .sim_manager import ( - Manager, - ResamplerFactory, - WorkMapperFactory, - RunnerFactory, +from .analysis.contig_tree import ( + BaseContigTree, + Contig, + ContigTree, ) -from .walker import ( - Walker, - WalkerState, - WalkerStateBox, +from .analysis.network import ( + BaseMacroStateNetwork, + MacroStateNetwork, +) +from .analysis.network_layouts.layout_graph import LayoutGraph +from .analysis.parents import ( + ParentForest, + ancestors, + net_parent_table, + parent_cycle_discontinuities, + parent_panel, + parent_table_discontinuities, + resampling_panel, + sliding_window, +) +from .analysis.profiles import ( + ContigTreeProfiler, + contigtrees_bin_edges, + cumulative_partitions, + free_energy_profile, +) +from .analysis.rates import ( + calc_warp_rate, + contig_warp_rates, ) +from .boundary_conditions.boundary import BoundaryConditions +from .hdf5 import WepyHDF5 +from .monitor import Monitor from .reporter.base import Reporter from .reporter.dashboard import ( - ResamplerDashboardSection, - RunnerDashboardSection, BCDashboardSection, DashboardReporter, + ResamplerDashboardSection, + RunnerDashboardSection, ) from .reporter.hdf5 import WepyHDF5Reporter from .reporter.openmm import OpenMMRunnerDashboardSection from .reporter.restree import ResTreeReporter from .reporter.revo.dashboard import REVODashboardSection - -from .resampling.decisions.decision import BaseDecisionRecord, BaseDecisionABC -from .resampling.decisions.no_decision import NoDecision from .resampling.decisions.clone_merge import MultiCloneMergeDecision - +from .resampling.decisions.decision import BaseDecisionABC, BaseDecisionRecord +from .resampling.decisions.no_decision import NoDecision +from .resampling.distances.base import Distance +from .resampling.distances.mock import MockDistance +from .resampling.distances.simple import XYDistanceState, XYEuclideanDistance from .resampling.resamplers.noresampler import NoResampler, NoResamplerFactory from .resampling.resamplers.resampler import Resampler from .resampling.resamplers.revo import REVOResampler, REVOResamplerFactory from .resampling.resamplers.wexplore import WExploreResampler, WExploreResamplerFactory -from .resampling.distances.base import Distance -from .resampling.distances.mock import MockDistance -from .resampling.distances.simple import XYDistanceState, XYEuclideanDistance - -from .runners.runner import Runner, NoRunner, NoRunnerFactory from .runners.mock import ( - MockState, MockRunner, MockRunnerFactory, + MockState, ) - -from .runners.openmm.reporter import OpenMMReporter, OpenMMReporterNextReport from .runners.openmm.logger import ( - StepIntervalLoggingReporter, - SamplingTimeIntervalLoggingReporter, - HeartBeatLoggingReporter, - HeartBeatLoggingReporterFactory, EnergyLoggingReporter, EnergyLoggingReporterFactory, + HeartBeatLoggingReporter, + HeartBeatLoggingReporterFactory, + SamplingTimeIntervalLoggingReporter, + StepIntervalLoggingReporter, UnitCellLoggingReporter, UnitCellLoggingReporterFactory, ) -from .runners.openmm.state import ( - OpenMMStateWrapper, - OpenMMState, - OPENMM_DEFAULT_UNITS, -) +from .runners.openmm.reporter import OpenMMReporter, OpenMMReporterNextReport from .runners.openmm.runner import ( OpenMMRunner, OpenMMRunnerFactory, ) - - +from .runners.openmm.state import ( + OPENMM_DEFAULT_UNITS, + OpenMMState, + OpenMMStateWrapper, +) +from .runners.runner import NoRunner, NoRunnerFactory, Runner +from .sim_manager import ( + Manager, + ResamplerFactory, + RunnerFactory, + WorkMapperFactory, +) from .util.json_top import ( + json_top_atom_count, json_top_atom_df, - json_top_residue_df, json_top_chain_df, - json_top_atom_count, + json_top_residue_df, json_top_subset, ) from .util.mdtraj import ( - mdtraj_to_json_topology, json_to_mdtraj_topology, + mdtraj_to_json_topology, traj_fields_to_mdtraj, ) - -from .work_mapper.base import WorkMapper -from .work_mapper.serial import SerialMapper, SerialMapperFactory -from .work_mapper.openmm.serial import OpenMMSerialWorkMapper, OpenMMSerialWorkMapperFactory -from .work_mapper.openmm.proc_pool import OpenMMProcPoolWorkMapper, OpenMMProcPoolWorkMapperFactory - -from .boundary_conditions.boundary import BoundaryConditions - - -from .analysis.contig_tree import ( - BaseContigTree, - ContigTree, - Contig, -) -from .analysis.network import ( - BaseMacroStateNetwork, - MacroStateNetwork, -) -from .analysis.parents import ( - resampling_panel, - parent_panel, - net_parent_table, - parent_table_discontinuities, - sliding_window, - ParentForest, - ancestors, - parent_cycle_discontinuities, +from .walker import ( + Walker, + WalkerState, + WalkerStateBox, ) -from .analysis.profiles import ( - cumulative_partitions, - free_energy_profile, - contigtrees_bin_edges, - ContigTreeProfiler, +from .work_mapper.base import WorkMapper +from .work_mapper.openmm.proc_pool import ( + OpenMMProcPoolWorkMapper, + OpenMMProcPoolWorkMapperFactory, ) -from .analysis.rates import ( - calc_warp_rate, - contig_warp_rates, +from .work_mapper.openmm.serial import ( + OpenMMSerialWorkMapper, + OpenMMSerialWorkMapperFactory, ) -from .analysis.network_layouts.layout_graph import LayoutGraph - +from .work_mapper.serial import SerialMapper, SerialMapperFactory __author__ = "Samuel D. Lotz" __email__ = "samuel.lotz@salotz.info" diff --git a/src/wepy/analysis/contig_tree.py b/src/wepy/analysis/contig_tree.py index 71617f5c..2fa1db36 100644 --- a/src/wepy/analysis/contig_tree.py +++ b/src/wepy/analysis/contig_tree.py @@ -31,12 +31,11 @@ from geomm.free_energy import free_energy as calc_free_energy # First Party Library -from wepy.storage.protocol import ContigWalkerTrace, RunTrace, ContigTrace from wepy.analysis.network_layouts.layout_graph import LayoutGraph from wepy.analysis.network_layouts.tree import ResamplingTreeLayout from wepy.analysis.parents import ( - ParentTable, ParentForest, + ParentTable, ancestors, net_parent_table, parent_cycle_discontinuities, @@ -45,8 +44,9 @@ ) from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.hdf5 import WepyHDF5 -from wepy.resampling.decisions.decision import BaseDecisionABC from wepy.reporter.file import FileMode +from wepy.resampling.decisions.decision import BaseDecisionABC +from wepy.storage.protocol import ContigTrace, ContigWalkerTrace, RunTrace # the groups of run records RESAMPLING: Final = "resampling" @@ -332,9 +332,9 @@ def _initialize_discontinuities(self, wepy_h5: WepyHDF5) -> None: ] def _set_discontinuities( - self, - wepy_h5: WepyHDF5, - boundary_conditions_class: type[BoundaryConditions], + self, + wepy_h5: WepyHDF5, + boundary_conditions_class: type[BoundaryConditions], ) -> None: """Given the boundary condition class sets node attributes for where there are discontinuities in the parental lineages. @@ -421,8 +421,8 @@ def continuations(self) -> set[tuple[int, int]]: @staticmethod def contig_trace_to_run_trace( - contig_trace: ContigTrace, - contig_walker_trace: ContigWalkerTrace, + contig_trace: ContigTrace, + contig_walker_trace: ContigWalkerTrace, ) -> RunTrace: """Combine a contig trace and a walker trace to get the equivalent run trace. @@ -457,7 +457,6 @@ def contig_trace_to_run_trace( return trace - def run_trace_to_contig_trace(self, run_trace: RunTrace) -> ContigWalkerTrace: """Assumes that the run trace goes along a valid contig. @@ -506,10 +505,10 @@ def contig_cycle_idx(self, run_idx: int, cycle_idx: int) -> int: return len(contig_trace) - 1 def get_branch_trace( - self, - run_idx: int, - cycle_idx: int, - start_contig_idx: int = 0, + self, + run_idx: int, + cycle_idx: int, + start_contig_idx: int = 0, ) -> ContigTrace: """Get a contig trace for a branch of the contig tree from an end point back to a set point (defaults to root of contig tree). @@ -565,9 +564,9 @@ def get_branch_trace( return contig_trace def trace_parent_table( - self, - contig_trace: ContigTrace, - discontinuities: bool = True, + self, + contig_trace: ContigTrace, + discontinuities: bool = True, ) -> ParentTable: """Given a contig trace returns a parent table for that contig. @@ -806,7 +805,7 @@ def get_subtree(self, node: NodeId) -> nx.DiGraph: subtrees = self.subtrees() # TODO: This is ambiguous if it is in multiple subtrees... - + # see which tree the node is in for subtree in subtrees: # if the node is in it this is the subtree it is in so @@ -815,9 +814,9 @@ def get_subtree(self, node: NodeId) -> nx.DiGraph: return subtree def contig_sliding_windows( - self, - contig_trace: ContigTrace, - window_length: int, + self, + contig_trace: ContigTrace, + window_length: int, ) -> list[ContigWalkerTrace]: """Given a contig trace get the sliding windows of length 'window_length' as contig walker traces. @@ -875,9 +874,9 @@ def sliding_contig_windows(self, window_length: int) -> list[ContigTrace]: return contig_windows def _subtree_sliding_contig_windows( - self, - subtree_root: NodeId, - window_length: int, + self, + subtree_root: NodeId, + window_length: int, ) -> list[ContigTrace]: """Get all the sliding windows of length 'window_length' from the subtree defined by the subtree root as run traces. @@ -1208,7 +1207,9 @@ def _contig_trace_to_contig_runs(cls, contig_trace: ContigTrace) -> list[int]: return contig_runs @classmethod - def _contig_runs_to_continuations(cls, contig_runs: list[int]) -> ContinuationsTable: + def _contig_runs_to_continuations( + cls, contig_runs: list[int] + ) -> ContinuationsTable: """Helper function to convert a list of run indices defining a contig to continuations. @@ -1229,7 +1230,9 @@ def _contig_runs_to_continuations(cls, contig_runs: list[int]) -> ContinuationsT return continuations @classmethod - def _continuations_to_contig_runs(cls, continuations: ContinuationsTable) -> list[int]: + def _continuations_to_contig_runs( + cls, continuations: ContinuationsTable + ) -> list[int]: """Helper function that converts a list of continuations to a list of the runs in the order of the contigs defined by the continuations. @@ -1585,15 +1588,14 @@ class Contig(ContigTree): _contig_trace: ContigTrace _contig_run_idxs: list[int] - def __init__( - self, - wepy_h5: WepyHDF5, - base_contigtree: BaseContigTree | None = None, - continuations: type(Ellipsis) | ContinuationsTable = Ellipsis, - runs: type(Ellipsis) | list[int] = Ellipsis, - boundary_condition_class: type[BoundaryConditions] | None = None, - decision_class: type[BaseDecisionABC] | None = None, + self, + wepy_h5: WepyHDF5, + base_contigtree: BaseContigTree | None = None, + continuations: type(Ellipsis) | ContinuationsTable = Ellipsis, + runs: type(Ellipsis) | list[int] = Ellipsis, + boundary_condition_class: type[BoundaryConditions] | None = None, + decision_class: type[BaseDecisionABC] | None = None, ): # uses superclass docstring exactly, this constructor just # generates some extra attributes @@ -1679,7 +1681,9 @@ def num_cycles(self) -> int: """ return len(self.contig_trace) - def walker_trace_to_run_trace(self, contig_walker_trace: ContigWalkerTrace) -> RunTrace: + def walker_trace_to_run_trace( + self, contig_walker_trace: ContigWalkerTrace + ) -> RunTrace: """Combine a walker trace to get the equivalent run trace for this contig. The contig_walker_trace cycle_idxs must be a subset of the @@ -1701,7 +1705,6 @@ def walker_trace_to_run_trace(self, contig_walker_trace: ContigWalkerTrace) -> R """ return self.contig_trace_to_run_trace(self.contig_trace, contig_walker_trace) - # TODO: may need to be implemented without using the wepy_h5 in the BaseContigTree def num_walkers(self, cycle_idx: int) -> int: @@ -1909,9 +1912,9 @@ def parent_table(self, discontinuities: bool = True) -> ParentTable: ) def lineages_contig( - self, - contig_trace: ContigTrace, - discontinuities: bool = True, + self, + contig_trace: ContigTrace, + discontinuities: bool = True, ): # get the parent table for this contig parent_table = self.parent_table(discontinuities=discontinuities) diff --git a/src/wepy/analysis/parents.py b/src/wepy/analysis/parents.py index 883afeed..78b44d3a 100644 --- a/src/wepy/analysis/parents.py +++ b/src/wepy/analysis/parents.py @@ -57,18 +57,18 @@ """ # Standard Library -import itertools as it import copy +import itertools as it # Third Party Library import networkx as nx -from wepy.resampling.decisions.decision import BaseDecisionABC +# First Party Library from wepy.boundary_conditions.boundary import BoundaryConditions - +from wepy.resampling.decisions.decision import BaseDecisionABC from wepy.storage.protocol import ( - ResamplingRecordUnstruct, DecisionRecordUnstruct, + ResamplingRecordUnstruct, ) DISCONTINUITY_VALUE = -1 @@ -86,9 +86,10 @@ # (traj_idx, cycle_idx) Trace = list[tuple[int, int]] + def resampling_panel( - resampling_records: list[ResamplingRecordUnstruct], - is_sorted: bool = False, + resampling_records: list[ResamplingRecordUnstruct], + is_sorted: bool = False, ) -> DecisionPanel: """Converts an unordered collection of resampling records into a structured array (lists) corresponding to cycles and resampling @@ -118,9 +119,7 @@ def resampling_panel( res_panel = [] _resampling_records = [ - (run_record.cycle_idx, run_record.record) - for run_record - in resampling_records + (run_record.cycle_idx, run_record.record) for run_record in resampling_records ] # if the records are not sorted this must be done: if not is_sorted: @@ -220,10 +219,12 @@ def resampling_panel( target_idxs = walker_rec["target_idxs"] # set the resampling record for the walker in the step records - step_row[walker_idx] = DecisionRecordUnstruct({ - "decision_id" : decision_id, - "target_idxs" : target_idxs, - }) + step_row[walker_idx] = DecisionRecordUnstruct( + { + "decision_id": decision_id, + "target_idxs": target_idxs, + } + ) # add the records for this step to the cycle table cycle_table.append(step_row) @@ -235,8 +236,8 @@ def resampling_panel( def parent_panel( - decision_class: type[BaseDecisionABC], - resampling_panel: DecisionPanel, + decision_class: type[BaseDecisionABC], + resampling_panel: DecisionPanel, ) -> ParentPanel: """Using the parental interpretation of resampling records given by the decision_class, convert resampling records in a resampling @@ -268,9 +269,7 @@ def parent_panel( # cast the unstructured record to decision records decision_recs = [ - decision_class.DECISION_RECORD(**step_rec) - for step_rec - in step_recs + decision_class.DECISION_RECORD(**step_rec) for step_rec in step_recs ] # get the parents idxs for the children of this step step_parents = decision_class.parents(decision_recs) @@ -386,8 +385,8 @@ def parent_table_discontinuities( def parent_cycle_discontinuities( - parent_idxs: list[int], - discontinuities: list[bool], + parent_idxs: list[int], + discontinuities: list[bool], ) -> list[int]: parent_row = copy.copy(parent_idxs) for walker_idx, disc in enumerate(discontinuities): @@ -405,10 +404,10 @@ def parent_cycle_discontinuities( def ancestors( - parent_table: ParentTable, - cycle_idx: int, - walker_idx: int, - ancestor_cycle: int = 0, + parent_table: ParentTable, + cycle_idx: int, + walker_idx: int, + ancestor_cycle: int = 0, ) -> Trace: """Returns the lineage of ancestors as walker indices leading up to the given walker. @@ -453,8 +452,8 @@ def ancestors( def sliding_window( - parent_table: ParentTable, - window_length: int, + parent_table: ParentTable, + window_length: int, ) -> list[Trace]: """Return contig walker traces of sliding windows of given length over the parent forest imposed over the contig given by the parent table. diff --git a/src/wepy/factory.py b/src/wepy/factory.py index 99871e46..a1dacdd0 100644 --- a/src/wepy/factory.py +++ b/src/wepy/factory.py @@ -1,10 +1,12 @@ -from typing import Protocol, Generic, TypeVar +# Standard Library +from typing import Generic, Protocol, TypeVar GeneratedType_ = TypeVar("GeneratedType_") + class Factory(Protocol, Generic[GeneratedType_]): @classmethod def type(cls) -> type[GeneratedType_]: ... - + def __call__(self) -> GeneratedType_: ... diff --git a/src/wepy/hdf5.py b/src/wepy/hdf5.py index e35076e0..a69f99e5 100644 --- a/src/wepy/hdf5.py +++ b/src/wepy/hdf5.py @@ -390,17 +390,14 @@ """ # Standard Library -from pathlib import Path -import copy import gc import itertools as it import json import logging -from typing import Any, TypedDict, NotRequired, Literal, Union, Required, Generator - -# Standard Library import os.path as osp from collections import Counter, defaultdict, namedtuple +from pathlib import Path +from typing import Any, Generator, Literal, NotRequired, Required, TypedDict, Union from warnings import warn # Third Party Library @@ -409,33 +406,30 @@ from numpy.typing import NDArray # First Party Library -from wepy.analysis.parents import resampling_panel, DecisionPanel -from wepy.util.json_top import json_top_atom_count, json_top_subset -from wepy.util.mdtraj import ( - json_to_mdtraj_topology, - traj_fields_to_mdtraj, -) -from wepy.util.util import traj_box_vectors_to_lengths_angles +from wepy.analysis.parents import DecisionPanel, resampling_panel from wepy.reporter.file import FileMode -from wepy.typing import Shape, Idxs, IdxArray -from wepy.walker import WalkerStateBox, Walker -from wepy.resampling.decisions.decision import DecisionRecord - from wepy.storage.protocol import ( - ContigWalkerTrace, RunTrace, ContigTrace, - RecordValueDtype, + ContigTrace, + ContigWalkerTrace, Record, - RunRecord, + RecordFieldDtype, RecordFieldShape, RecordFieldShapeSpec, - RecordFieldDtype, RecordFieldSpec, - ResamplingRecord, + RecordValueDtype, ResamplingRecordUnstruct, - RESAMPLING_RECORD_FIELDS, - WARPING_RECORD_FIELDS, + RunRecord, + RunTrace, WarpRecordUnstruct, ) +from wepy.typing import IdxArray, Idxs +from wepy.util.json_top import json_top_atom_count, json_top_subset +from wepy.util.mdtraj import ( + json_to_mdtraj_topology, + traj_fields_to_mdtraj, +) +from wepy.util.util import traj_box_vectors_to_lengths_angles +from wepy.walker import Walker, WalkerStateBox # optional dependencies try: @@ -578,13 +572,16 @@ "progress", "boundary_conditions", ] -RUN_RECORD_KEYS = frozenset({ - RESAMPLING, - RESAMPLER, - WARPING, - PROGRESS, - BC, -}) +RUN_RECORD_KEYS = frozenset( + { + RESAMPLING, + RESAMPLER, + WARPING, + PROGRESS, + BC, + } +) + class RunRecordColumns(TypedDict, total=False): cycle_idx: Required[list[int]] @@ -592,7 +589,7 @@ class RunRecordColumns(TypedDict, total=False): walker_idx: Required[list[int]] decision_id: Required[list[int]] target_idxs: Required[list[tuple[int, ...]]] - + ## Record groups constants @@ -665,11 +662,13 @@ class RunRecordColumns(TypedDict, total=False): OBSERVABLES = "observables" """The field name for the default compound field observables.""" -RESERVED_TRAJ_FIELDS = frozenset({ - WEIGHTS, - ALT_REPS, - OBSERVABLES, -}) +RESERVED_TRAJ_FIELDS = frozenset( + { + WEIGHTS, + ALT_REPS, + OBSERVABLES, + } +) ## Trajectory Field Constants @@ -729,15 +728,19 @@ class RunRecordColumns(TypedDict, total=False): SparseIdxs = dict[str, list[int]] + class WepyHDF5Error(Exception): pass + class WepyHDF5WriteError(WepyHDF5Error): pass + class WepyHDF5ReadError(WepyHDF5Error): pass + # utility for paths def _iter_field_paths(grp: h5py.Group) -> list[str]: """Return all subgroup field name paths from a group. @@ -779,29 +782,32 @@ def _iter_field_paths(grp: h5py.Group) -> list[str]: field_paths.append(field_name) return field_paths + class Dtype(TypedDict): kind: Literal["simple", "structured"] str: NotRequired[str] descr: NotRequired[list[tuple[str, str]]] + def numpy_dtype_to_json(dtype: np.dtype) -> str: payload: Dtype if dtype.fields is None: payload = { - "kind" : "simple", - "str" : dtype.str, + "kind": "simple", + "str": dtype.str, } else: payload = { - "kind" : "structured", - "descr" : dtype.descr, + "kind": "structured", + "descr": dtype.descr, } # Warning only supports simple data types return json.dumps(payload) + def dtype_json_to_numpy(s: str) -> np.dtype: payload = json.loads(s) @@ -810,7 +816,8 @@ def dtype_json_to_numpy(s: str) -> np.dtype: return np.dtype(payload["str"]) else: return np.dtype(payload["descr"]) - + + class WepyHDF5: """Wrapper for h5py interface to an HDF5 file object for creation and access of WepyHDF5 data. @@ -851,20 +858,19 @@ class WepyHDF5: _sparse_fields: tuple[str, Any] _main_rep_idxs: Idxs _alt_reps: dict[str, IdxArray] - ## Partial constructors/initializers @staticmethod def _gen_default_init_field_attributes( - topology: str, - main_rep_idxs: Idxs | None, - n_dims: int | None = None, + topology: str, + main_rep_idxs: Idxs | None, + n_dims: int | None = None, ) -> tuple[ dict[str, tuple[int, ...]], dict[str, FieldFeatureDtype], - int, # n_dims - int, # n_coords + int, # n_dims + int, # n_coords NDArray[np.integer], ]: """Sets the feature_shapes and feature_dtypes to be the default for @@ -919,8 +925,8 @@ def _gen_default_init_field_attributes( @classmethod def _init_continuations( - cls, - h5: h5py.File, + cls, + h5: h5py.File, ) -> h5py.Dataset: """This will either create a dataset in the settings for the continuations or if continuations already exist it will reinitialize @@ -948,16 +954,16 @@ def _init_continuations( @classmethod def _create_init( - cls, - h5: h5py.File, - topology: str, - alt_reps: dict[str, IdxArray] | None = None, - sparse_fields: tuple[str, ...] | None = None, - units: dict[str, str] | None = None, - n_dims: int | None = None, - main_rep_idxs: Idxs | None = None, - field_feature_shapes_overrides: dict[str, tuple[int, ...]] | None = None, - field_feature_dtypes_overrides: dict[str, FieldFeatureDtype] | None = None, + cls, + h5: h5py.File, + topology: str, + alt_reps: dict[str, IdxArray] | None = None, + sparse_fields: tuple[str, ...] | None = None, + units: dict[str, str] | None = None, + n_dims: int | None = None, + main_rep_idxs: Idxs | None = None, + field_feature_shapes_overrides: dict[str, tuple[int, ...]] | None = None, + field_feature_dtypes_overrides: dict[str, FieldFeatureDtype] | None = None, ) -> None: """Creation mode constructor. @@ -985,7 +991,7 @@ def _create_init( h5.create_dataset(TOPOLOGY, data=topology) # sparse fields - + # make a dataset for the sparse fields allowed. this requires # a 'special' datatype for variable length strings. This is # supported by HDF5 but not numpy. @@ -1039,17 +1045,20 @@ def _create_init( field_feature_dtypes_overrides is not None ): # check that they have the same keys - if len( + if ( + len( mismatch_keys := ( set(field_feature_shapes_overrides.keys()).symmetric_difference( set(field_feature_dtypes_overrides.keys()) ) ) - ) > 0: + ) + > 0 + ): raise ValueError( f"Mismatch in the keys for field feature overrides: {mismatch_keys}" ) - + _field_feature_shapes.update(field_feature_shapes_overrides) _field_feature_dtypes.update(field_feature_dtypes_overrides) @@ -1106,7 +1115,6 @@ def _create_init( # position cls._init_continuations(h5) - def __init__( self, filename: Path, @@ -1195,19 +1203,17 @@ def __init__( # Validate inputs if mode not in self.MODES: - raise ValueError( - f"mode must be either one of: {self.MODES}" - ) + raise ValueError(f"mode must be either one of: {self.MODES}") _constructor_data = { - "topology" : topology, - "units" : units, - "sparse_fields" : sparse_fields, - "feature_shapes" : feature_shapes_overrides, - "feature_dtypes" : feature_dtypes_overrides, - "n_dims" : n_dims, - "alt_reps" : alt_reps, - "main_rep_idxs" : main_rep_idxs, + "topology": topology, + "units": units, + "sparse_fields": sparse_fields, + "feature_shapes": feature_shapes_overrides, + "feature_dtypes": feature_dtypes_overrides, + "n_dims": n_dims, + "alt_reps": alt_reps, + "main_rep_idxs": main_rep_idxs, } # create file mode: 'w' will create a new file or overwrite, @@ -1224,23 +1230,18 @@ def __init__( f"In creation mode ({mode}) you must provide topology." ) - elif mode in {"r", "r+"}: # if any data was given, warn the user if any( _given_data := { - key - for key, value - in _constructor_data.items() - if value is not None + key for key, value in _constructor_data.items() if value is not None } ): raise ValueError( f"Data was given but opening in read mode: {_given_data}", ) - # Object attributes self._filename = filename self._swmr_mode = swmr_mode @@ -1254,7 +1255,6 @@ def __init__( # used elsewhere and could be a feature in the future. self._h5py_mode = mode - ## Initialize the file # open the file and then run the different constructors based @@ -1272,7 +1272,6 @@ def __init__( # set SWMR mode if asked for if we are in write mode also if self._swmr_mode is True and mode in self.WRITE_MODES: self._h5.swmr_mode = swmr_mode - if self._wepy_mode in {"w", "x", "w-"}: @@ -1295,7 +1294,7 @@ def __init__( _sparse_fields.extend(alt_rep_keys) else: _alt_reps = {} - + self._create_init( h5=self._h5, topology=topology, @@ -1313,7 +1312,7 @@ def __init__( # set the h5py mode to the value in the actual h5py.File # object after creation self._h5py_mode = self._h5.mode - + self._h5.close() # variable to reflect if it is closed or not, should be closed @@ -1325,7 +1324,6 @@ def filename(self) -> Path: """The path to the underlying HDF5 file.""" return self._filename - @property def mode(self) -> FileMode: """The WepyHDF5 mode this object was created with.""" @@ -1434,9 +1432,11 @@ def swmr_mode(self, val): ### constructors - def _get_field_path_grp(self, run_idx: int, traj_idx: int, field_path: str) -> tuple[ - h5py.Group, - str, + def _get_field_path_grp( + self, run_idx: int, traj_idx: int, field_path: str + ) -> tuple[ + h5py.Group, + str, ]: """Given a field path for the trajectory returns the group the field's dataset goes in and the key for the field name in that group. @@ -1482,7 +1482,6 @@ def _get_field_path_grp(self, run_idx: int, traj_idx: int, field_path: str) -> t return grp, field_name - def _add_run_init(self, run_idx: int, continue_run: int | None = None) -> None: """Routines for creating a run includes updating and setting object global variables, increasing the counter for the number of runs. @@ -1506,7 +1505,9 @@ def _add_run_init(self, run_idx: int, continue_run: int | None = None) -> None: if continue_run is not None: self.add_continuation(run_idx, continue_run) - def _add_init_walkers(self, init_walkers_grp: h5py.Group, init_walkers: list[Walker[WalkerStateBox]]) -> None: + def _add_init_walkers( + self, init_walkers_grp: h5py.Group, init_walkers: list[Walker[WalkerStateBox]] + ) -> None: """Adds the run field group for the initial walkers. Parameters @@ -1540,10 +1541,10 @@ def _add_init_walkers(self, init_walkers_grp: h5py.Group, init_walkers: list[Wal walker_grp.create_dataset(field_key, data=np.array([field_value])) def _init_run_sporadic_record_grp( - self, - run_idx: int, - run_record_key: str, - fields: list[RecordFieldSpec], + self, + run_idx: int, + run_record_key: str, + fields: list[RecordFieldSpec], ) -> h5py.Group: """Initialize a sporadic record group for a run. @@ -1581,10 +1582,10 @@ def _init_run_sporadic_record_grp( return record_grp def _init_run_continual_record_grp( - self, - run_idx: int, - run_record_key: RunRecordKey, - fields: list[RecordFieldSpec], + self, + run_idx: int, + run_record_key: RunRecordKey, + fields: list[RecordFieldSpec], ) -> h5py.Group: """Initialize a continual record group for a run. @@ -1672,9 +1673,7 @@ def _init_run_records_field( return dset @staticmethod - def _is_sporadic_records( - run_record_key: str - ) -> bool: + def _is_sporadic_records(run_record_key: str) -> bool: """Tests whether a record group is sporadic or not. Parameters @@ -1696,12 +1695,12 @@ def _is_sporadic_records( return False def _init_contiguous_traj_field( - self, - run_idx: int, - traj_idx: int, - field_path: str, - shape: RecordFieldShape, - dtype: H5FieldDtype, + self, + run_idx: int, + traj_idx: int, + field_path: str, + shape: RecordFieldShape, + dtype: H5FieldDtype, ) -> None: """Initialize a contiguous (non-sparse) trajectory field. @@ -1727,12 +1726,12 @@ def _init_contiguous_traj_field( ) def _init_sparse_traj_field( - self, - run_idx: int, - traj_idx: int, - field_path: str, - shape: RecordFieldShape, - dtype: H5FieldDtype, + self, + run_idx: int, + traj_idx: int, + field_path: str, + shape: RecordFieldShape, + dtype: H5FieldDtype, ) -> None: """Parameters ---------- @@ -1768,14 +1767,14 @@ def _init_sparse_traj_field( # create the dataset for the sparse indices sparse_grp.create_dataset(SPARSE_IDXS, (0,), dtype=int, maxshape=(None,)) - + def _init_traj_field( - self, - run_idx: int, - traj_idx: int, - field_path: str, - feature_shape: RecordFieldShape, - dtype: H5FieldDtype, + self, + run_idx: int, + traj_idx: int, + field_path: str, + feature_shape: RecordFieldShape, + dtype: H5FieldDtype, ) -> None: """Initialize a trajectory field. @@ -1808,7 +1807,6 @@ def _init_traj_field( run_idx, traj_idx, field_path, feature_shape, dtype ) - def _init_traj_fields( self, run_idx: int, @@ -1921,7 +1919,7 @@ def _extend_contiguous_traj_field(self, run_idx, traj_idx, field_path, field_dat """ traj_grp = self.traj(run_idx, traj_idx) - + field = traj_grp[field_path] # make sure this is a feature vector @@ -2271,7 +2269,10 @@ def _run_record_namedtuple(self, run_record_key: str): # TODO: get the tablified value types recorded somewhere def _convert_record_field_to_table_column( - self, run_idx: int, run_record_key: RunRecordKey, record_field: str, + self, + run_idx: int, + run_record_key: RunRecordKey, + record_field: str, ) -> list[RecordValueDtype]: """Converts a dataset of feature vectors to more palatable values for use in external datasets. @@ -2334,7 +2335,9 @@ def _convert_record_field_to_table_column( return rec_dset - def _convert_record_fields_to_table_columns(self, run_idx: int, run_record_key: RunRecordKey) -> RunRecordColumns: + def _convert_record_fields_to_table_columns( + self, run_idx: int, run_record_key: RunRecordKey + ) -> RunRecordColumns: """Convert record group data to truncated namedtuple records. This uses the specified record fields from the header settings @@ -2403,18 +2406,16 @@ def _convert_record_fields_to_table_columns(self, run_idx: int, run_record_key: # return records def _table_to_run_records( - self, - run_record_key: RunRecordKey, - table_fields: RunRecordColumns, + self, + run_record_key: RunRecordKey, + table_fields: RunRecordColumns, ) -> list[RunRecord]: it_fields = set(table_fields.keys()) it_fields.remove("cycle_idx") field_its = { - field_name : iter(table_fields[field_name]) - for field_name - in it_fields + field_name: iter(table_fields[field_name]) for field_name in it_fields } records = [] @@ -2422,9 +2423,7 @@ def _table_to_run_records( # get the next value from each iterator record_d = { - field_name : next(field_its[field_name]) - for field_name - in it_fields + field_name: next(field_its[field_name]) for field_name in it_fields } record = RunRecord( @@ -2434,11 +2433,11 @@ def _table_to_run_records( records.append(record) - return records - - def _run_records_sporadic(self, run_idxs: list[int], run_record_key: RunRecordKey) -> Record: + def _run_records_sporadic( + self, run_idxs: list[int], run_record_key: RunRecordKey + ) -> Record: """Generate records for a sporadic record group for a multi-run contig. @@ -2478,7 +2477,6 @@ def _run_records_sporadic(self, run_idxs: list[int], run_record_key: RunRecordKe run_idx, run_record_key ) - # get the cycle idxs for this run rec_grp = self.records_grp(run_idx, run_record_key) run_cycle_idxs = rec_grp[CYCLE_IDXS][:] @@ -2503,7 +2501,9 @@ def _run_records_sporadic(self, run_idxs: list[int], run_record_key: RunRecordKe return records - def _run_records_continual(self, run_idxs: list[int], run_record_key: RunRecordKey) -> list[Record]: + def _run_records_continual( + self, run_idxs: list[int], run_record_key: RunRecordKey + ) -> list[Record]: """Generate records for a continual record group for a multi-run contig. @@ -2547,20 +2547,26 @@ def _run_records_continual(self, run_idxs: list[int], run_record_key: RunRecordK # just add it to the list of fields that will be concatenated later fields[field_name].extend(field_data) - # use one of the fields as the lead to find how many # cycles there are since they are not tracked explicitly # in the data file lead_record_field = self.record_fields[run_record_key][0] # make the cycle idxs from that - run_cycle_idxs = list(range( - self.records_grp( - run_idx, - run_record_key, - )[lead_record_field].shape[0])) + run_cycle_idxs = list( + range( + self.records_grp( + run_idx, + run_record_key, + )[ + lead_record_field + ].shape[0] + ) + ) - reindexed_cycle_idxs = [idx + prev_run_cycle_total for idx in run_cycle_idxs] + reindexed_cycle_idxs = [ + idx + prev_run_cycle_total for idx in run_cycle_idxs + ] # add the total number of cycle_idxs from this run to the # running total @@ -2575,11 +2581,11 @@ def _run_records_continual(self, run_idxs: list[int], run_record_key: RunRecordK return records def _get_contiguous_traj_field( - self, - run_idx: int, - traj_idx: int, - field_path: str, - frames: list[int] | None = None, + self, + run_idx: int, + traj_idx: int, + field_path: str, + frames: list[int] | None = None, ) -> FieldsData: """Access actual data for a trajectory field. @@ -2690,12 +2696,12 @@ def _get_sparse_traj_field( return data def _add_run_field( - self, - run_idx: int, - field_path: str, - data: NDArray, - sparse_idxs: list[int] | None = None, - force: bool = False, + self, + run_idx: int, + field_path: str, + data: NDArray, + sparse_idxs: list[int] | None = None, + force: bool = False, ) -> None: """Add a trajectory field to all trajectories in a run. @@ -2793,11 +2799,11 @@ def _add_run_field( ) def _add_field( - self, - field_path: str, - data: list[NDArray], - sparse_idxs: list[int] | None = None, - force: bool = False + self, + field_path: str, + data: list[NDArray], + sparse_idxs: list[int] | None = None, + force: bool = False, ) -> None: """Add a trajectory field to all runs in a file. @@ -2832,7 +2838,6 @@ def _add_field( ### File Utilities - ### h5py object access @property @@ -2841,10 +2846,9 @@ def runs(self) -> h5py.Group: if RUNS not in self.h5: raise WepyHDF5ReadError(f"The '{RUNS}' is not initialized.") - + else: return self.h5[RUNS] - def run(self, run_idx: int) -> h5py.Group: """Get the h5py.Group for a run. @@ -2881,11 +2885,12 @@ def run_trajs(self, run_idx: int) -> h5py.Group: run_grp = self.run(run_idx) if TRAJECTORIES not in run_grp: - raise WepyHDF5ReadError(f"The '{TRAJECTORIES}' group not initialized for run {run_idx}") + raise WepyHDF5ReadError( + f"The '{TRAJECTORIES}' group not initialized for run {run_idx}" + ) else: return run_grp[TRAJECTORIES] - def traj(self, run_idx: int, traj_idx: int) -> h5py.Group: """Get an h5py.Group trajectory group. @@ -2913,7 +2918,9 @@ def traj(self, run_idx: int, traj_idx: int) -> h5py.Group: return trajs_grp[traj_id] - def traj_field_entity(self, run_idx: int, traj_idx: int, field_path: str) -> h5py.Dataset | h5py.Group: + def traj_field_entity( + self, run_idx: int, traj_idx: int, field_path: str + ) -> h5py.Dataset | h5py.Group: traj_grp = self.traj(run_idx, traj_idx) @@ -2928,7 +2935,9 @@ def traj_field_entity(self, run_idx: int, traj_idx: int, field_path: str) -> h5p def settings_grp(self) -> h5py.Group: """The header settings group.""" if SETTINGS not in self.h5: - raise WepyHDF5ReadError(f"The settings group ({SETTINGS}) has not been initialized") + raise WepyHDF5ReadError( + f"The settings group ({SETTINGS}) has not been initialized" + ) else: return self.h5[SETTINGS] @@ -2947,11 +2956,9 @@ def decision_grp(self, run_idx: int) -> h5py.Group: run_grp = self.run(run_idx) if DECISION not in run_grp: - raise WepyHDF5ReadError( - f"Decision group not initialized in run {run_idx}" - ) + raise WepyHDF5ReadError(f"Decision group not initialized in run {run_idx}") else: - + return run_grp[DECISION] def init_walkers_grp(self, run_idx: int) -> h5py.Group: @@ -3070,9 +3077,9 @@ def progress_grp(self, run_idx: int) -> h5py.Group: return self.records_grp(run_idx, PROGRESS) def iter_runs( - self, - idxs: bool = False, - run_sel: list[int] | None = None, + self, + idxs: bool = False, + run_sel: list[int] | None = None, ) -> Generator[ tuple[int, h5py.Group] | h5py.Group, None, @@ -3109,9 +3116,9 @@ def iter_runs( yield run def iter_trajs( - self, - idxs: bool = False, - traj_sel: list[int] | None = None, + self, + idxs: bool = False, + traj_sel: list[int] | None = None, ) -> Generator[ tuple[tuple[int, int], h5py.Group] | h5py.Group, None, @@ -3151,9 +3158,9 @@ def iter_trajs( yield traj def iter_run_trajs( - self, - run_idx: int, - idxs: bool = False, + self, + run_idx: int, + idxs: bool = False, ) -> Generator[ tuple[tuple[int, int], h5py.Group], None, @@ -3263,7 +3270,7 @@ def record_fields(self) -> dict[str, list[str]]: @property def sparse_fields(self) -> NDArray: """The trajectory fields that are sparse.""" - + return self.settings_grp[SPARSE_FIELDS].asstr()[:] @property @@ -3486,10 +3493,7 @@ def get_mdtraj_topology(self, alt_rep: str = POSITIONS) -> mdtraj.Topology: ## Initial walkers def initial_walker_fields( - self, - run_idx: int, - fields: list[str], - walker_idxs: list[int] | None = None + self, run_idx: int, fields: list[str], walker_idxs: list[int] | None = None ) -> dict[str, NDArray]: """Get fields from the initial walkers of the simulation. @@ -3537,10 +3541,10 @@ def initial_walker_fields( return init_walker_fields def initial_walkers_to_mdtraj( - self, - run_idx: int, - walker_idxs: list[int] | None = None, - alt_rep: str = POSITIONS, + self, + run_idx: int, + walker_idxs: list[int] | None = None, + alt_rep: str = POSITIONS, ) -> mdtraj.Trajectory: """Generate an mdtraj Trajectory from a trace of frames from the runs. @@ -3709,11 +3713,11 @@ def run_traj_idxs(self, run_idx: int) -> list[int]: traj_idxs : list of int """ - return list( - range(len(self.run_trajs(run_idx))) - ) + return list(range(len(self.run_trajs(run_idx)))) - def run_traj_idx_tuples(self, runs: list[int] | None = None) -> list[tuple[int, int]]: + def run_traj_idx_tuples( + self, runs: list[int] | None = None + ) -> list[tuple[int, int]]: """Get identifier tuples (run_idx, traj_idx) for all trajectories in all runs. @@ -3740,7 +3744,9 @@ def run_traj_idx_tuples(self, runs: list[int] | None = None) -> list[tuple[int, return tups - def get_traj_field_cycle_idxs(self, run_idx: int, traj_idx: int, field_path: str) -> NDArray[np.integer]: + def get_traj_field_cycle_idxs( + self, run_idx: int, traj_idx: int, field_path: str + ) -> NDArray[np.integer]: """Returns the cycle indices for a sparse trajectory field. Parameters @@ -3757,7 +3763,7 @@ def get_traj_field_cycle_idxs(self, run_idx: int, traj_idx: int, field_path: str """ field = self.traj_field_entity(run_idx, traj_idx, field_path) - + # if the field is not sparse just return the cycle indices for # that run if field_path not in self.sparse_fields: @@ -4169,7 +4175,9 @@ def add_metadata(self, key, value): """ self._h5.attrs[key] = value - def init_record_fields(self, run_record_key: RunRecordKey, record_fields: list[str]) -> None: + def init_record_fields( + self, run_record_key: RunRecordKey, record_fields: list[str] + ) -> None: """Initialize the settings record fields for a record group in the settings group. @@ -4286,10 +4294,10 @@ def add_continuation(self, continuation_run: int, base_run: int) -> None: ) def new_run( - self, - init_walkers: Walker[WalkerStateBox], - continue_run: int | None = None, - **kwargs: H5Attrs, + self, + init_walkers: Walker[WalkerStateBox], + continue_run: int | None = None, + **kwargs: H5Attrs, ) -> h5py.Group: """Initialize a new run. @@ -4500,9 +4508,9 @@ def init_run_bc(self, run_idx, bc): # application level methods for initializing the run records # groups with just the fields and without the objects def init_run_fields_resampling( - self, - run_idx: int, - fields: list[RecordFieldSpec], + self, + run_idx: int, + fields: list[RecordFieldSpec], ) -> h5py.Group: """Initialize this record group fields datasets. @@ -4523,9 +4531,7 @@ def init_run_fields_resampling( return grp def init_run_fields_resampling_decision( - self, - run_idx: int, - decision_enum_dict: dict[str,str] + self, run_idx: int, decision_enum_dict: dict[str, str] ) -> None: """Initialize the decision group for this run. @@ -4542,9 +4548,9 @@ def init_run_fields_resampling_decision( decision_grp.create_dataset(name, data=value) def init_run_fields_resampler( - self, - run_idx: int, - fields: list[str], + self, + run_idx: int, + fields: list[str], ) -> h5py.Group: """Initialize this record group fields datasets. @@ -4622,10 +4628,10 @@ def init_run_fields_bc(self, run_idx, fields): return grp def init_run_record_grp( - self, - run_idx: int, - run_record_key: str, - fields: list[RecordFieldSpec], + self, + run_idx: int, + run_record_key: str, + fields: list[RecordFieldSpec], ) -> h5py.Group: """Initialize a record group for a run. @@ -4664,12 +4670,12 @@ def init_run_record_grp( # return self.traj(run_idx, traj_idx)[POSITIONS].shape[0] def add_traj( - self, - run_idx: int, - data: FieldsData, - weights: WeightsTrajArray | None = None, - sparse_idxs: SparseIdxs | None = None, - metadata: H5Attrs | None = None, + self, + run_idx: int, + data: FieldsData, + weights: WeightsTrajArray | None = None, + sparse_idxs: SparseIdxs | None = None, + metadata: H5Attrs | None = None, ) -> h5py.Group: """Add a full trajectory to a run. @@ -4703,13 +4709,19 @@ def add_traj( if metadata is None: metadata = {} - if len(_wrong_data_fields := [ - reserved_field - for reserved_field - in RESERVED_TRAJ_FIELDS - if reserved_field in data - ]) > 0: - raise ValueError(f"{_wrong_data_fields} are reserved field names and cannot be given in data") + if ( + len( + _wrong_data_fields := [ + reserved_field + for reserved_field in RESERVED_TRAJ_FIELDS + if reserved_field in data + ] + ) + > 0 + ): + raise ValueError( + f"{_wrong_data_fields} are reserved field names and cannot be given in data" + ) # positions are mandatory if POSITIONS not in traj_data: @@ -4745,7 +4757,9 @@ def add_traj( if key not in [RUN_IDX, TRAJ_IDX]: traj_grp.attrs[key] = val else: - raise ValueError(f"'{RUN_IDX}' and '{TRAJ_IDX}' metadata keys are reserved and cannot be set") + raise ValueError( + f"'{RUN_IDX}' and '{TRAJ_IDX}' metadata keys are reserved and cannot be set" + ) # check to make sure the positions are the right shape assert ( @@ -4812,11 +4826,11 @@ def add_traj( return traj_grp def extend_traj( - self, - run_idx: int, - traj_idx: int, - data: FieldsData, - weights: WeightsTrajArray | None = None, + self, + run_idx: int, + traj_idx: int, + data: FieldsData, + weights: WeightsTrajArray | None = None, ) -> None: """Extend a trajectory with data for all fields. @@ -4958,7 +4972,9 @@ def extend_traj( ## application level append methods for run records groups - def extend_cycle_warping_records(self, run_idx: int, cycle_idx: int, warping_data: WarpRecordUnstruct) -> None: + def extend_cycle_warping_records( + self, run_idx: int, cycle_idx: int, warping_data: WarpRecordUnstruct + ) -> None: """Add records for each field for this record group. Parameters @@ -4973,7 +4989,9 @@ def extend_cycle_warping_records(self, run_idx: int, cycle_idx: int, warping_dat """ self.extend_cycle_run_group_records(run_idx, WARPING, cycle_idx, warping_data) - def extend_cycle_bc_records(self, run_idx: int, cycle_idx: int, bc_data: Record) -> None: + def extend_cycle_bc_records( + self, run_idx: int, cycle_idx: int, bc_data: Record + ) -> None: """Add records for each field for this record group. Parameters @@ -4989,7 +5007,9 @@ def extend_cycle_bc_records(self, run_idx: int, cycle_idx: int, bc_data: Record) self.extend_cycle_run_group_records(run_idx, BC, cycle_idx, bc_data) - def extend_cycle_progress_records(self, run_idx: int, cycle_idx: int, progress_data: Record) -> None: + def extend_cycle_progress_records( + self, run_idx: int, cycle_idx: int, progress_data: Record + ) -> None: """Add records for each field for this record group. Parameters @@ -5005,10 +5025,10 @@ def extend_cycle_progress_records(self, run_idx: int, cycle_idx: int, progress_d self.extend_cycle_run_group_records(run_idx, PROGRESS, cycle_idx, progress_data) def extend_cycle_resampling_records( - self, - run_idx: int, - cycle_idx: int, - resampling_data: list[ResamplingRecordUnstruct], + self, + run_idx: int, + cycle_idx: int, + resampling_data: list[ResamplingRecordUnstruct], ) -> None: """Add records for each field for this record group. @@ -5034,7 +5054,9 @@ def extend_cycle_resampling_records( resampling_data, ) - def extend_cycle_resampler_records(self, run_idx: int, cycle_idx: int, resampler_data: Record) -> None: + def extend_cycle_resampler_records( + self, run_idx: int, cycle_idx: int, resampler_data: Record + ) -> None: """Add records for each field for this record group. Parameters @@ -5110,7 +5132,9 @@ def extend_cycle_run_group_records( ## Record Getters - def run_contig_records(self, run_idxs: list[int], run_record_key: RunRecordKey) -> list[Record]: + def run_contig_records( + self, run_idxs: list[int], run_record_key: RunRecordKey + ) -> list[Record]: """Get the records for a record group for the contig that is formed by the run indices. @@ -5148,7 +5172,9 @@ def run_contig_records(self, run_idxs: list[int], run_record_key: RunRecordKey) return records - def run_records(self, run_idx: int, run_record_key: RunRecordKey) -> list[RunRecord]: + def run_records( + self, run_idx: int, run_record_key: RunRecordKey + ) -> list[RunRecord]: """Get the records for a record group for a single run. Parameters @@ -5170,8 +5196,9 @@ def run_records(self, run_idx: int, run_record_key: RunRecordKey) -> list[RunRec return self.run_contig_records(run_idxs, run_record_key) - - def run_records_dataframe(self, run_idx: int, run_record_key: RunRecordKey) -> pd.DataFrame: + def run_records_dataframe( + self, run_idx: int, run_record_key: RunRecordKey + ) -> pd.DataFrame: """Get the records for a record group for a single run in the form of a pandas DataFrame. @@ -5188,7 +5215,9 @@ def run_records_dataframe(self, run_idx: int, run_record_key: RunRecordKey) -> p records = self.run_records(run_idx, run_record_key) return pd.DataFrame(records) - def run_contig_records_dataframe(self, run_idxs: list[int], run_record_key: RunRecordKey) -> pd.DataFrame: + def run_contig_records_dataframe( + self, run_idxs: list[int], run_record_key: RunRecordKey + ) -> pd.DataFrame: """Get the records for a record group for a contig of runs in the form of a pandas DataFrame. @@ -5451,7 +5480,6 @@ def run_contig_resampling_panel(self, run_idxs: list[int]) -> DecisionPanel: return contig_resampling_panel - def run_resampling_panel(self, run_idx: int) -> DecisionPanel: """Generate a resampling panel from the resampling records of a run. @@ -5467,7 +5495,6 @@ def run_resampling_panel(self, run_idx: int) -> DecisionPanel: """ return self.run_contig_resampling_panel([run_idx]) - # Trajectory Field Setters @@ -5491,7 +5518,6 @@ def add_run_observable(self, run_idx, observable_name, data, sparse_idxs=None): self._add_run_field(run_idx, obs_path, data, sparse_idxs=sparse_idxs) - def add_traj_observable(self, observable_name, data, sparse_idxs=None): """Add a trajectory sub-field in the compound field "observables" for an entire file, on a trajectory basis. @@ -5693,12 +5719,12 @@ def compute_observable( ## Trajectory Getters def get_traj_field( - self, - run_idx: int, - traj_idx: int, - field_path: str, - frames: list[int] | None = None, - masked: bool = True + self, + run_idx: int, + traj_idx: int, + field_path: str, + frames: list[int] | None = None, + masked: bool = True, ) -> FieldsData: """Returns a numpy array for the given trajectory field. @@ -5853,10 +5879,10 @@ def apply_argsorted(shuffled_seq, sorted_idxs): return frame_fields def get_run_trace_fields( - self, - run_idx: int, - frame_tups: ContigWalkerTrace, - fields: str, + self, + run_idx: int, + frame_tups: ContigWalkerTrace, + fields: str, ) -> FieldsData: """Get trajectory field data for the frames specified by the trace within a single run. @@ -5896,9 +5922,9 @@ def get_run_trace_fields( return frame_fields def get_contig_trace_fields( - self, - contig_trace: ContigTrace, - fields: list[str], + self, + contig_trace: ContigTrace, + fields: list[str], ) -> FieldsData: """Get field data for all trajectories of a contig for the frames specified by the contig trace. @@ -5985,10 +6011,10 @@ def get_contig_trace_fields( return field_values def iter_trajs_fields( - self, - fields: list[str], - idxs: bool = False, - traj_sel: list[tuple[int, int]] | None = None, + self, + fields: list[str], + idxs: bool = False, + traj_sel: list[tuple[int, int]] | None = None, ) -> Generator[ tuple[tuple[int, int], FieldsData] | FieldsData, None, @@ -6132,11 +6158,11 @@ def traj_fields_map( return results def to_mdtraj( - self, - run_idx: int, - traj_idx: int, - frames: list[int] | None = None, - alt_rep: str | None = None, + self, + run_idx: int, + traj_idx: int, + frames: list[int] | None = None, + alt_rep: str | None = None, ) -> mdtraj.Trajectory: """Convert a trajectory to an mdtraj Trajectory object. @@ -6232,7 +6258,9 @@ def to_mdtraj( return traj - def trace_to_mdtraj(self, trace: RunTrace, alt_rep: str | None = None) -> mdtraj.Trajectory: + def trace_to_mdtraj( + self, trace: RunTrace, alt_rep: str | None = None + ) -> mdtraj.Trajectory: """Generate an mdtraj Trajectory from a trace of frames from the runs. Uses the default fields for positions (unless an alternate @@ -6269,10 +6297,10 @@ def trace_to_mdtraj(self, trace: RunTrace, alt_rep: str | None = None) -> mdtraj return self.traj_fields_to_mdtraj(trace_fields, alt_rep=alt_rep) def run_trace_to_mdtraj( - self, - run_idx: int, - trace: ContigWalkerTrace, - alt_rep: str | None = None, + self, + run_idx: int, + trace: ContigWalkerTrace, + alt_rep: str | None = None, ) -> mdtraj.Trajectory: """Generate an mdtraj Trajectory from a trace of frames from the runs. @@ -6358,7 +6386,9 @@ def _choose_rep_path(self, alt_rep: str | None) -> str: return rep_path - def traj_fields_to_mdtraj(self, traj_fields: FieldsData, alt_rep: str = POSITIONS) -> mdtraj.Trajectory: + def traj_fields_to_mdtraj( + self, traj_fields: FieldsData, alt_rep: str = POSITIONS + ) -> mdtraj.Trajectory: """Create an mdtraj.Trajectory from a traj_fields dictionary. Parameters diff --git a/src/wepy/reporter/base.py b/src/wepy/reporter/base.py index 6bd8fe54..0d8a41a0 100644 --- a/src/wepy/reporter/base.py +++ b/src/wepy/reporter/base.py @@ -1,8 +1,9 @@ # Standard Library import logging -from typing import Any, Protocol, TypedDict, Literal, Union +from typing import Any, Protocol, TypedDict + +# Third Party Library -import numpy as np # First Party Library from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.resampling.resamplers.resampler import Resampler @@ -48,6 +49,7 @@ class CycleReportDict(TypedDict): cycle_bc_time: float cycle_resampling_time: float + class Reporter(Protocol): """Abstract base class for wepy reporters. diff --git a/src/wepy/reporter/dashboard.py b/src/wepy/reporter/dashboard.py index 8150af8f..3d98904d 100644 --- a/src/wepy/reporter/dashboard.py +++ b/src/wepy/reporter/dashboard.py @@ -3,12 +3,12 @@ """ # Standard Library +import datetime import logging -from pathlib import Path import textwrap import time from copy import copy -import datetime +from pathlib import Path from typing import TypedDict # Third Party Library @@ -18,22 +18,25 @@ from tabulate import tabulate # First Party Library -from wepy.reporter.file import ProgressiveFileReporterABC, FileMode -from wepy.reporter.base import SimComponentArgs, CycleReportDict +from wepy.reporter.base import CycleReportDict, SimComponentArgs +from wepy.reporter.file import FileMode, ProgressiveFileReporterABC logger = logging.getLogger(__name__) + class WalkersSummaryReport(TypedDict): total: float min: float max: float + class WorkerRecord(TypedDict): cycle_idx: int n_steps: int worker_idx: int segment_time: int + class GenSimSectionReport(TypedDict): init_date_time: datetime.datetime @@ -43,6 +46,7 @@ class GenSimSectionReport(TypedDict): n_cycles: int walker_cycle_summary_table: str + class PerformanceSectionReport(TypedDict): avg_cycle_time: int worker_avg_segment_time: int @@ -52,12 +56,15 @@ class PerformanceSectionReport(TypedDict): avg_bc_time: int | None avg_resampler_time: int | None + class ResamplerFieldReport(TypedDict): name: str + class RunnerFieldReport(TypedDict): name: str + class BCFieldReport(TypedDict): name: str total_n_walker_segments: int @@ -66,12 +73,14 @@ class BCFieldReport(TypedDict): progress_summary_table: str warping_log: str + class ResamplerDashboardSection: RESAMPLER_SECTION_TEMPLATE = textwrap.dedent( """ Resampler: {{ name }} """ ) + def __init__(self, resampler=None, name=None, **kwargs): if resampler is not None: self.resampler_name = type(resampler).__name__ @@ -86,9 +95,11 @@ def update_values(self, **kwargs: CycleReportDict): pass def gen_fields(self, **kwargs) -> ResamplerFieldReport: - fields = ResamplerFieldReport({ - "name": self.resampler_name, - }) + fields = ResamplerFieldReport( + { + "name": self.resampler_name, + } + ) return fields @@ -106,7 +117,8 @@ class RunnerDashboardSection: Runner: {{ name }} """ ) - def __init__(self, runner_factory = None, name=None): + + def __init__(self, runner_factory=None, name=None): if runner_factory is not None: self.runner_name = runner_factory.type().__name__ @@ -362,7 +374,6 @@ class DashboardReporter(ProgressiveFileReporterABC): runner_dash: RunnerDashboardSection | None bc_dash: BCDashboardSection | None - n_cycles: int init_date_time: datetime.datetime | None init_sys_time: int | None @@ -379,15 +390,13 @@ class DashboardReporter(ProgressiveFileReporterABC): avg_bc_time: int | None avg_resampling_time: int | None avg_cycle_time: int | None - - def __init__( - self, - path: Path, - resampler_dash: ResamplerDashboardSection | None = None, - runner_dash: RunnerDashboardSection | None = None, - bc_dash: BCDashboardSection | None = None, + self, + path: Path, + resampler_dash: ResamplerDashboardSection | None = None, + runner_dash: RunnerDashboardSection | None = None, + bc_dash: BCDashboardSection | None = None, ) -> None: super().__init__(file_paths=[path]) @@ -448,20 +457,24 @@ def init(self, **kwargs: SimComponentArgs) -> None: def calc_walker_summary(self, **kwargs: CycleReportDict) -> WalkersSummaryReport: walker_weights = [walker.weight for walker in kwargs["new_walkers"]] - summary = WalkersSummaryReport({ - "total": np.sum(walker_weights), - "min": np.min(walker_weights), - "max": np.max(walker_weights), - }) + summary = WalkersSummaryReport( + { + "total": np.sum(walker_weights), + "min": np.min(walker_weights), + "max": np.max(walker_weights), + } + ) return summary - def update_performance_values(self, **kwargs: CycleReportDict) -> None: ## worker specific performance # only do this part if there were any workers - if kwargs["worker_segment_times"] is not None and len(kwargs["worker_segment_times"]) > 0: + if ( + kwargs["worker_segment_times"] is not None + and len(kwargs["worker_segment_times"]) > 0 + ): # log of segment times for workers for worker_idx, segment_times in kwargs["worker_segment_times"].items(): for segment_time in segment_times: @@ -521,9 +534,7 @@ def update_values(self, **kwargs: CycleReportDict) -> None: ### simulation self.n_cycles += 1 - self.walker_prob_summaries.append( - self.calc_walker_summary(**kwargs) - ) + self.walker_prob_summaries.append(self.calc_walker_summary(**kwargs)) self.update_performance_values(**kwargs) @@ -534,7 +545,7 @@ def update_values(self, **kwargs: CycleReportDict) -> None: self.runner_dash.update_values(**kwargs) if self.bc_dash is not None: self.bc_dash.update_values(**kwargs) - + def write_dashboard(self, report_str: str) -> None: """Write the dashboard to the file.""" @@ -549,14 +560,16 @@ def gen_sim_section(self, **kwargs: CycleReportDict) -> str: ) # render the simulation section - sim_section_d = GenSimSectionReport({ - "init_date_time": self.init_date_time, - "curr_date_time": datetime.datetime.today().isoformat(), - "total_run_time": time.time() - self.init_sys_time, - "last_cycle_idx": kwargs["cycle_idx"], - "n_cycles": self.n_cycles, - "walker_cycle_summary_table": walker_summary_tbl_str, - }) + sim_section_d = GenSimSectionReport( + { + "init_date_time": self.init_date_time, + "curr_date_time": datetime.datetime.today().isoformat(), + "total_run_time": time.time() - self.init_sys_time, + "last_cycle_idx": kwargs["cycle_idx"], + "n_cycles": self.n_cycles, + "walker_cycle_summary_table": walker_summary_tbl_str, + } + ) sim_section_str = Template(self.SIMULATION_SECTION_TEMPLATE).render( **sim_section_d @@ -584,7 +597,9 @@ def gen_performance_section(self, **kwargs: CycleReportDict) -> str: ) cycle_table_str = tabulate( - cycle_table_df, headers=cycle_table_df.columns, tablefmt="orgtbl", + cycle_table_df, + headers=cycle_table_df.columns, + tablefmt="orgtbl", ) # log of workers performance @@ -612,16 +627,18 @@ def gen_performance_section(self, **kwargs: CycleReportDict) -> str: tablefmt="orgtbl", ) - performance_section_d = PerformanceSectionReport({ - "avg_cycle_time": self.avg_cycle_time, - "worker_avg_segment_time": worker_agg_table_str, - "cycle_log": cycle_table_str, - "performance_log": worker_table_str, - # optionals - "avg_runner_time": self.avg_runner_time, - "avg_bc_time": self.avg_bc_time, - "avg_resampling_time": self.avg_resampling_time, - }) + performance_section_d = PerformanceSectionReport( + { + "avg_cycle_time": self.avg_cycle_time, + "worker_avg_segment_time": worker_agg_table_str, + "cycle_log": cycle_table_str, + "performance_log": worker_table_str, + # optionals + "avg_runner_time": self.avg_runner_time, + "avg_bc_time": self.avg_bc_time, + "avg_resampling_time": self.avg_resampling_time, + } + ) performance_section_str = Template(self.PERFORMANCE_SECTION_TEMPLATE).render( **performance_section_d @@ -671,5 +688,3 @@ def report(self, **kwargs: CycleReportDict) -> None: # write the thing logger.info(f"Writing dashboard at: {self.file_path}") self.write_dashboard(report_str) - - diff --git a/src/wepy/reporter/hdf5.py b/src/wepy/reporter/hdf5.py index 93217157..e1a63483 100644 --- a/src/wepy/reporter/hdf5.py +++ b/src/wepy/reporter/hdf5.py @@ -1,33 +1,32 @@ # Standard Library -from pathlib import Path import builtins import logging -from typing import Self, TypedDict, Literal, Generic, TypeVar, Any - -# Standard Library +from pathlib import Path +from typing import Generic, Literal, Self, TypeVar # Third Party Library import numpy as np import openmm.unit # First Party Library +from wepy.boundary_conditions.boundary import BoundaryConditions from wepy.hdf5 import WepyHDF5 from wepy.reporter.base import ( - SimComponentArgs, CycleReportDict, + SimComponentArgs, ) +from wepy.reporter.file import FileMode, FileReporterABC +from wepy.resampling.resamplers.resampler import Resampler +from wepy.runners.openmm import OPENMM_DEFAULT_UNITS from wepy.storage.protocol import ( - RecordFieldShapeSpec, + Record, RecordFieldDtype, + RecordFieldShapeSpec, + ResamplingRecord, ) -from wepy.reporter.file import FileReporterABC, FileMode +from wepy.typing import IdxArray, Idxs from wepy.util.json_top import json_top_atom_count from wepy.walker import Walker, WalkerState, WalkerStateBox -from wepy.resampling.resamplers.resampler import Resampler -from wepy.boundary_conditions.boundary import BoundaryConditions -from wepy.typing import Shape, Idxs, IdxArray -from wepy.storage.protocol import Record, ResamplingRecord -from wepy.runners.openmm import OPENMM_DEFAULT_UNITS logger = logging.getLogger(__name__) @@ -39,22 +38,25 @@ BCRecord_ = TypeVar("BCRecord_", bound=Record) ProgressRecord_ = TypeVar("ProgressRecord_", bound=Record) + class UnitError(Exception): pass + # TODO: support for pint Quantity = openmm.unit.Quantity + class WepyHDF5Reporter( - FileReporterABC, - Generic[ - WalkerState_, - ResamplingRecord_, - ResamplerRecord_, - WarpingRecord_, - BCRecord_, - ProgressRecord_, - ], + FileReporterABC, + Generic[ + WalkerState_, + ResamplingRecord_, + ResamplerRecord_, + WarpingRecord_, + BCRecord_, + ProgressRecord_, + ], ): """Reporter for generating an HDF5 format (WepyHDF5) data file from simulations. @@ -112,7 +114,7 @@ class WepyHDF5Reporter( # stateful attributes wepy_h5: WepyHDF5 | None wepy_run_idx: int | None - + _tmp_topology: str | None def __init__( @@ -155,7 +157,6 @@ def __init__( Parameters ---------- - save_fields : A selection of fields from the walker states to be stored. Allows for the ignoring of some states. If None all fields from states will attempted to be saved. To not @@ -280,11 +281,10 @@ def __init__( raise ValueError( f"The sparse fields were requested ({set(sparse_fields.keys())}) but no save fields requested." ) - + _missing_save_fields = set( sparse_key - for sparse_key - in sparse_fields.keys() + for sparse_key in sparse_fields.keys() if sparse_key not in self.save_fields ) @@ -303,7 +303,6 @@ def __init__( self._feature_dtypes = feature_dtypes self._n_dims = n_dims - # required resampling fields self.resampling_fields = resampling_fields self.decision_enum_dict = decision_enum_dict @@ -325,7 +324,9 @@ def __init__( # the atom indices of the whole system that will be saved as # the main positions representation - self.main_rep_idxs = np.array(main_rep_idxs) if main_rep_idxs is not None else None + self.main_rep_idxs = ( + np.array(main_rep_idxs) if main_rep_idxs is not None else None + ) # the idxs for alternate representations of the system # positions @@ -334,9 +335,7 @@ def __init__( self.alt_reps_to_save = [] if alt_reps is not None: self.alt_reps_idxs = { - key: np.array(idxs) - for key, (idxs, _) - in alt_reps.items() + key: np.array(idxs) for key, (idxs, _) in alt_reps.items() } # add the frequencies for these alt_reps to the @@ -344,10 +343,7 @@ def __init__( for key, (idxs, freq) in alt_reps.items(): if len(idxs) == 0: - raise ValueError( - f"No indices given for sparse field: {key}" - ) - + raise ValueError(f"No indices given for sparse field: {key}") alt_rep_key = "alt_reps/{}".format(key) @@ -434,7 +430,6 @@ def from_components( Parameters ---------- - resampler : Resampler object, optional but recommended The resampler being used for the simulation. Is used as a convenient container for a variety of constants needed for @@ -469,7 +464,6 @@ def from_components( bc_records = None warping_records = None progress_records = None - return WepyHDF5Reporter( file_path=file_path, @@ -488,9 +482,9 @@ def from_components( # components resampling_fields=resampler_class.resampling_fields(), decision_enum_dict=resampler_class.DECISION.enum_dict_by_name(), - resampler_fields = resampler_class.resampler_fields(), - resampling_records = resampler_class.resampling_record_field_names(), - resampler_records = resampler_class.resampler_record_field_names(), + resampler_fields=resampler_class.resampler_fields(), + resampling_records=resampler_class.resampling_record_field_names(), + resampler_records=resampler_class.resampler_record_field_names(), warping_fields=warping_fields, progress_fields=progress_fields, bc_fields=bc_fields, @@ -527,7 +521,9 @@ def _initialize_h5_run( """Initialize the WepyHDF5 data structures.""" if wepy_h5.mode != "r+": - raise IOError(f"wepy_h5 must be in non-creation read-write mode (r+), in '{wepy_h5.mode}'") + raise IOError( + f"wepy_h5 must be in non-creation read-write mode (r+), in '{wepy_h5.mode}'" + ) if not wepy_h5.closed: raise IOError("WepyHDF5 is already open, must be closed.") @@ -562,12 +558,18 @@ def _initialize_h5_run( ) # set the fields that are records for tables etc. unless # they are already set - if resampling_records is not None and "resampling" not in wepy_h5.record_fields: + if ( + resampling_records is not None + and "resampling" not in wepy_h5.record_fields + ): wepy_h5.init_record_fields( "resampling", resampling_records, ) - if resampler_records is not None and "resampler" not in wepy_h5.record_fields: + if ( + resampler_records is not None + and "resampler" not in wepy_h5.record_fields + ): wepy_h5.init_record_fields( "resampler", resampler_records, @@ -592,9 +594,7 @@ def _initialize_h5_run( if "warping" not in wepy_h5.record_fields: wepy_h5.init_record_fields("warping", warping_records) if "boundary_conditions" not in wepy_h5.record_fields: - wepy_h5.init_record_fields( - "boundary_conditions", bc_records - ) + wepy_h5.init_record_fields("boundary_conditions", bc_records) if "progress" not in wepy_h5.record_fields: wepy_h5.init_record_fields("progress", progress_records) @@ -602,8 +602,8 @@ def _initialize_h5_run( @staticmethod def _resolve_state_units( - units: dict[str, openmm.unit.Unit], - state: WalkerStateBox, + units: dict[str, openmm.unit.Unit], + state: WalkerStateBox, ) -> tuple[WalkerStateBox, dict[str, openmm.unit.Unit]]: """For walker states convert all quantity field values to plain values. @@ -633,18 +633,19 @@ def _resolve_state_units( # If there is no unit for it, just get the # magnitude in the current units else: - new_walker_fields[field_key] = field_value.value_in_unit(field_value.unit) + new_walker_fields[field_key] = field_value.value_in_unit( + field_value.unit + ) units_used[field_key] = field_value.unit return WalkerStateBox(**new_walker_fields), units_used - - + def init(self, **kwargs: SimComponentArgs) -> None: # TODO: remove dynamic configuration. Instead replace with # static configuration from the Runner for good defaults. - + ## Do checks on the inputs and figure out runtime field metadata # if we specify save fields only save these for the initial walkers @@ -658,8 +659,9 @@ def init(self, **kwargs: SimComponentArgs) -> None: logger.info( f"Accepting and saving all fields found in init_walkers: {state_fields}" ) - logger.warning("To ensure all required data is in a simulation these fields should be explicit.") - + logger.warning( + "To ensure all required data is in a simulation these fields should be explicit." + ) case (None, None): _save_fields = state_fields @@ -683,23 +685,21 @@ def init(self, **kwargs: SimComponentArgs) -> None: f"Initial walker fields being saved determined from 'init_walker_save_fields': {_save_fields}" ) - if _save_fields == state_fields: filtered_init_walkers = kwargs["init_walkers"] elif not all( - [ - True if save_field in state_fields else False - for save_field - in _save_fields - ] + [ + True if save_field in state_fields else False + for save_field in _save_fields + ] ): - # make sure all the save_fields are present in the state - raise ValueError( - f"init_walkers should have all fields as required: {_save_fields}. " - f"Found: {state_fields}" - ) + # make sure all the save_fields are present in the state + raise ValueError( + f"init_walkers should have all fields as required: {_save_fields}. " + f"Found: {state_fields}" + ) else: @@ -707,9 +707,7 @@ def init(self, **kwargs: SimComponentArgs) -> None: for walker in kwargs["init_walkers"]: # make a new state by filtering the attributes of the old ones state_d = { - k: v - for k, v in walker.state.dict().items() - if k in _save_fields + k: v for k, v in walker.state.dict().items() if k in _save_fields } # and saving alternate representations as we would @@ -748,7 +746,9 @@ def init(self, **kwargs: SimComponentArgs) -> None: # plain values. converted_filtered_init_walkers = [] for walker_idx, init_walker in enumerate(filtered_init_walkers): - _state, units_used = self._resolve_state_units(self.units, init_walker.state) + _state, units_used = self._resolve_state_units( + self.units, init_walker.state + ) # If no self.units were given, use the first # init walker to determine the units for a field overall, set @@ -763,13 +763,8 @@ def init(self, **kwargs: SimComponentArgs) -> None: Walker(state=_state, weight=init_walker.weight) ) - # convert units to strings - _str_units = { - key : str(unit) - for key, unit - in self.units.items() - } + _str_units = {key: str(unit) for key, unit in self.units.items()} logger.info(f"Serialized units: {_str_units}") # Run the constructor intialization @@ -798,7 +793,7 @@ def init(self, **kwargs: SimComponentArgs) -> None: # read-write non-create mode self.wepy_h5 = WepyHDF5( self.file_path, - mode='r+', + mode="r+", ) self.wepy_run_idx = self._initialize_h5_run( @@ -907,7 +902,6 @@ def report( self.main_rep_idxs ] - # for all of these fields we wrap them in additional # dimensions to make them feature vectors for field_path in list(walker_data.keys()): @@ -960,7 +954,6 @@ def report( self._report_resampler(kwargs["cycle_idx"], kwargs["resampler_data"]) - def cleanup(self, **kwargs: SimComponentArgs) -> None: # # it should be already closed at this point but just in case # if not self.wepy_h5.closed: @@ -969,9 +962,10 @@ def cleanup(self, **kwargs: SimComponentArgs) -> None: # remove reference to the WepyHDF5 file so we can serialize this object del self.wepy_h5 - # sporadic - def _report_warping(self, cycle_idx: int, warping_data: list[WarpingRecord_]) -> None: + def _report_warping( + self, cycle_idx: int, warping_data: list[WarpingRecord_] + ) -> None: """Method to write warping specific information. Parameters @@ -1006,9 +1000,9 @@ def _report_bc(self, cycle_idx: int, bc_data: list[BCRecord_]) -> None: self.wepy_h5.extend_cycle_bc_records(self.wepy_run_idx, cycle_idx, bc_data) def _report_resampler( - self, - cycle_idx: int, - resampler_data: list[ResamplerRecord_], + self, + cycle_idx: int, + resampler_data: list[ResamplerRecord_], ) -> None: """Method to write resampler update specific information. @@ -1032,9 +1026,9 @@ def _report_resampler( # the resampling records are provided every cycle but they need to # be saved as sporadic because of the variable number of walkers def _report_resampling( - self, - cycle_idx: int, - resampling_records: list[ResamplingRecord_], + self, + cycle_idx: int, + resampling_records: list[ResamplingRecord_], ) -> None: """Method to write resampling specific information. diff --git a/src/wepy/reporter/openmm.py b/src/wepy/reporter/openmm.py index 8dde27e2..a848cb2f 100644 --- a/src/wepy/reporter/openmm.py +++ b/src/wepy/reporter/openmm.py @@ -1,3 +1,4 @@ +# Third Party Library import openmm.unit # First Party Library diff --git a/src/wepy/reporter/restree.py b/src/wepy/reporter/restree.py index 3dd932ec..774c2b93 100644 --- a/src/wepy/reporter/restree.py +++ b/src/wepy/reporter/restree.py @@ -3,8 +3,9 @@ """ # Standard Library -from collections import namedtuple import warnings +from collections import namedtuple + # Third Party Library import networkx as nx import numpy as np diff --git a/src/wepy/resampling/decisions/clone_merge.py b/src/wepy/resampling/decisions/clone_merge.py index 7a263550..3c459196 100644 --- a/src/wepy/resampling/decisions/clone_merge.py +++ b/src/wepy/resampling/decisions/clone_merge.py @@ -13,9 +13,11 @@ logger = logging.getLogger(__name__) + class CloneMergeDecisionError(Exception): pass + # the possible types of decisions that can be made enumerated for # storage, these each correspond to specific instruction type class CloneMergeDecisionEnum(IntEnum): @@ -42,13 +44,16 @@ class CloneMergeDecisionEnum(IntEnum): """Do nothing with the sample value (state) but squashed walkers will donate their weight to it.""" + # TODO: get this automatically CLONE_MERGE_DECISION_ENUM_VALUES = {1, 2, 3, 4} + class CloneMergeDecisionRecordDict(TypedDict): decision_id: int target_idxs: tuple[int, ...] + @attrs.define class CloneMergeDecisionRecord(BaseDecisionRecord): # TODO: get types correct @@ -58,7 +63,9 @@ class CloneMergeDecisionRecord(BaseDecisionRecord): @decision_id.validator def _check_decision_id(self, attribute, value) -> None: if value not in CLONE_MERGE_DECISION_ENUM_VALUES: - raise ValueError(f"Invalid decision_id ({value}) must be one of {CLONE_MERGE_DECISION_ENUM_VALUES}") + raise ValueError( + f"Invalid decision_id ({value}) must be one of {CLONE_MERGE_DECISION_ENUM_VALUES}" + ) @target_idxs.validator def _check_decision_id(self, attribute, value) -> None: diff --git a/src/wepy/resampling/decisions/decision.py b/src/wepy/resampling/decisions/decision.py index df5b7813..e665395f 100644 --- a/src/wepy/resampling/decisions/decision.py +++ b/src/wepy/resampling/decisions/decision.py @@ -47,8 +47,7 @@ # Standard Library import logging -from enum import IntEnum -from typing import Any, Union, Generic, TypeVar, Protocol, TypedDict +from typing import Any, Generic, Protocol, TypedDict, TypeVar, Union # Third Party Library import attrs @@ -61,14 +60,17 @@ DecisionFieldDtype = Union[int,] DecisionFieldShapeSpec = tuple[int | type(Ellipsis), ...] + class DecisionRecord(Protocol): def to_dict(self) -> dict[str, DecisionFieldDtype]: ... + class BaseDecisionRecordDict(TypedDict): decision_id: int target_idxs: tuple[int, ...] + @attrs.define class BaseDecisionRecord: decision_id: int @@ -77,6 +79,7 @@ class BaseDecisionRecord: def to_dict(self) -> BaseDecisionRecordDict: return attrs.asdict(self) + DecisionEnum_ = TypeVar("DecisionEnum_") DecisionRecord_ = TypeVar("DecisionRecord", bound=DecisionRecord) @@ -93,7 +96,10 @@ class BaseDecisionABC(Generic[DecisionEnum_, DecisionRecord_]): DECISION_RECORD: DecisionRecord_ = BaseDecisionRecord - FIELDS: tuple[str, ...] = ("decision_id", "target_idxs",) + FIELDS: tuple[str, ...] = ( + "decision_id", + "target_idxs", + ) """The names of the fields that go into the decision record.""" # An Ellipsis instead of fields indicate there is a variable @@ -104,10 +110,16 @@ class BaseDecisionABC(Generic[DecisionEnum_, DecisionRecord_]): ) """Field data shapes.""" - DTYPES: tuple[DecisionFieldDtype, ...] = (int, int,) + DTYPES: tuple[DecisionFieldDtype, ...] = ( + int, + int, + ) """Field data types.""" - RECORD_FIELDS: tuple[str, ...] = ("decision_id", "target_idxs",) + RECORD_FIELDS: tuple[str, ...] = ( + "decision_id", + "target_idxs", + ) """The fields that could be used in a reduced table-like representation.""" ANCESTOR_DECISION_IDS: tuple[int, ...] @@ -135,11 +147,11 @@ def field_dtypes(cls) -> tuple[DecisionFieldDtype, ...]: @classmethod def fields(cls) -> list[ - tuple[ - str, - DecisionFieldShapeSpec, - DecisionFieldDtype, - ] + tuple[ + str, + DecisionFieldShapeSpec, + DecisionFieldDtype, + ] ]: """Specs for each field. diff --git a/src/wepy/resampling/decisions/no_decision.py b/src/wepy/resampling/decisions/no_decision.py index 2a4a3e81..7f547b37 100644 --- a/src/wepy/resampling/decisions/no_decision.py +++ b/src/wepy/resampling/decisions/no_decision.py @@ -16,10 +16,12 @@ class NothingDecisionEnum(IntEnum): NOTHING = 0 """Do nothing with the walker.""" + class NoDecisionRecordDict(TypedDict): decision_id: int target_idx: tuple[int, ...] + @attrs.define class NoDecisionRecord(BaseDecisionRecord): decision_id: int = attrs.field() @@ -29,22 +31,21 @@ class NoDecisionRecord(BaseDecisionRecord): def _check_decision_id(self, attribute, value) -> None: if value != NothingDecisionEnum.NOTHING.value: - raise ValueError(f"Invalid decision_id ({value}) must be {NothingDecisionEnum.NOTHING.value}") + raise ValueError( + f"Invalid decision_id ({value}) must be {NothingDecisionEnum.NOTHING.value}" + ) @target_idxs.validator def _check_target_idxs(self, attribute, value) -> None: if len(value) < 1: - raise ValueError( - f"'target_idxs' must have at least one entry." - ) + raise ValueError("'target_idxs' must have at least one entry.") if any(idx < 0 for idx in value): raise ValueError( f"'target_idxs' values must be non-negative, received: {value}" ) - def to_dict(self) -> NoDecisionRecordDict: return attrs.asdict(self) @@ -81,7 +82,7 @@ def action( if decision_record.decision_id == cls.ENUM.NOTHING.value: target_idx = decision_record.target_idxs[0] - + # check to make sure a walker doesn't already exist # where you are going to put it if mod_walkers[target_idx] is not None: diff --git a/src/wepy/resampling/resamplers/clone_merge.py b/src/wepy/resampling/resamplers/clone_merge.py index ec253611..5c636587 100644 --- a/src/wepy/resampling/resamplers/clone_merge.py +++ b/src/wepy/resampling/resamplers/clone_merge.py @@ -1,10 +1,10 @@ # Standard Library -from typing import Generic, TypeVar, Annotated +from typing import Annotated, Generic, TypeVar # Third Party Library +import attrs import numpy as np from numpy.typing import NDArray -import attrs # First Party Library from wepy.resampling.decisions.clone_merge import ( @@ -15,11 +15,11 @@ ResamplerABC, ResamplerError, ) -from wepy.walker import Walker, WalkerState -from wepy.util.attrs import AttrsMappingMixin +from wepy.storage.protocol import ResamplingRecord from wepy.typing import Shape +from wepy.util.attrs import AttrsMappingMixin +from wepy.walker import Walker, WalkerState -from wepy.storage.protocol import ResamplingRecord @attrs.define class CloneMergeResamplingRecord(AttrsMappingMixin, ResamplingRecord): @@ -43,8 +43,10 @@ class CloneMergeResamplingRecord(AttrsMappingMixin, ResamplingRecord): Shape((1,)), ] + WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) + class CloneMergeResampler(ResamplerABC, Generic[WalkerState_]): """Abstract base class for resamplers using the clone-merge decision class. @@ -296,7 +298,9 @@ def assign_clones( # make a record for this clone walker_actions[walker_idx] = self.decision().record( self.decision().ENUM.CLONE.value, - target_idxs=tuple(clone_targets,) + target_idxs=tuple( + clone_targets, + ), ) return walker_actions diff --git a/src/wepy/resampling/resamplers/noresampler.py b/src/wepy/resampling/resamplers/noresampler.py index e2ed24d2..66598163 100644 --- a/src/wepy/resampling/resamplers/noresampler.py +++ b/src/wepy/resampling/resamplers/noresampler.py @@ -1,21 +1,18 @@ # Standard Library -from typing import TypedDict, Annotated -import numpy as np -from numpy.typing import NDArray +# Third Party Library import attrs # First Party Library -from wepy.typing import Shape from wepy.resampling.decisions.no_decision import ( NoDecision, NothingDecisionEnum, ) -from wepy.resampling.resamplers.resampler import Resampler, ResamplerABC -from wepy.walker import Walker -from wepy.util.attrs import AttrsMappingMixin -from wepy.resampling.decisions.no_decision import NoDecisionRecord +from wepy.resampling.resamplers.resampler import ResamplerABC from wepy.storage.protocol import ResamplingRecord +from wepy.util.attrs import AttrsMappingMixin +from wepy.walker import Walker + @attrs.define class NoResamplerResamplingRecord(AttrsMappingMixin, ResamplingRecord): diff --git a/src/wepy/resampling/resamplers/resampler.py b/src/wepy/resampling/resamplers/resampler.py index d0d17c31..4e2acf86 100644 --- a/src/wepy/resampling/resamplers/resampler.py +++ b/src/wepy/resampling/resamplers/resampler.py @@ -1,20 +1,20 @@ # Standard Library import logging -from typing import Generic, Protocol, TypeVar, Union +from typing import Generic, Protocol, TypeVar from warnings import warn # Third Party Library -import numpy as np # First Party Library from wepy.resampling.decisions.decision import BaseDecisionABC +from wepy.storage.protocol import Record, RecordFieldDtype, RecordFieldShapeSpec from wepy.walker import Walker, WalkerState -from wepy.storage.protocol import Record, RecordFieldShapeSpec, RecordFieldDtype logger = logging.getLogger(__name__) WalkerState_ = TypeVar("WalkerState_", bound=WalkerState) + class ResamplerError(Exception): """Error raised when some constraint on resampling properties is violated. @@ -22,9 +22,11 @@ class ResamplerError(Exception): pass + ResamplingRecord_ = TypeVar("ResamplingRecord_", bound=Record) ResamplerRecord_ = TypeVar("ResamplerRecord_", bound=Record) + class Resampler(Protocol, Generic[WalkerState_, ResamplingRecord_, ResamplerRecord_]): DECISION: BaseDecisionABC @@ -62,7 +64,9 @@ def resample(self, walkers: list[Walker[WalkerState_]]) -> tuple[ ]: ... -class ResamplerABC(Resampler, Generic[WalkerState_, ResamplingRecord_, ResamplerRecord_]): +class ResamplerABC( + Resampler, Generic[WalkerState_, ResamplingRecord_, ResamplerRecord_] +): """Abstract base class for implementing resamplers. All subclasses of Resampler must implement the 'resample' method. @@ -680,5 +684,3 @@ def resample( """ raise NotImplementedError - - diff --git a/src/wepy/resampling/resamplers/revo.py b/src/wepy/resampling/resamplers/revo.py index 2f3064b8..5696d710 100644 --- a/src/wepy/resampling/resamplers/revo.py +++ b/src/wepy/resampling/resamplers/revo.py @@ -4,7 +4,7 @@ import multiprocessing as mp import random as rand import time -from typing import Callable, Generic, Literal, TypedDict, TypeVar, Annotated +from typing import Annotated, Callable, Generic, Literal, TypeVar # Third Party Library import attrs @@ -12,13 +12,16 @@ from numpy.typing import NDArray # First Party Library -from wepy.typing import Shape from wepy.resampling.decisions.clone_merge import CloneMergeDecisionRecord from wepy.resampling.distances.base import Distance -from wepy.resampling.resamplers.clone_merge import CloneMergeResampler, CloneMergeResamplingRecord +from wepy.resampling.resamplers.clone_merge import ( + CloneMergeResampler, + CloneMergeResamplingRecord, +) +from wepy.typing import Shape +from wepy.util.attrs import AttrsMappingMixin from wepy.util.multiprocessing import proc_pool_worker_setup, queue_listener_context from wepy.walker import Walker, WalkerState -from wepy.util.attrs import AttrsMappingMixin logger = logging.getLogger(__name__) @@ -52,11 +55,17 @@ def __call__(self, state: WalkerState_) -> DistanceImage_: logger.info("Finished image computation") return result + @attrs.define class REVOResamplerResamplerRecord(AttrsMappingMixin): distance_matrix: Annotated[ NDArray[np.float32], - Shape((Ellipsis, Ellipsis,)) + Shape( + ( + Ellipsis, + Ellipsis, + ) + ), ] variation: Annotated[ NDArray[np.float32], @@ -164,10 +173,16 @@ class REVOResampler( RESAMPLING_RECORD_FIELDS = CloneMergeResampler.RESAMPLING_RECORD_FIELDS - RESAMPLER_FIELDS = CloneMergeResampler.RESAMPLER_FIELDS + ("distance_matrix", "variation",) + RESAMPLER_FIELDS = CloneMergeResampler.RESAMPLER_FIELDS + ( + "distance_matrix", + "variation", + ) RESAMPLER_SHAPES = CloneMergeResampler.RESAMPLER_SHAPES + (Ellipsis, (1,)) - RESAMPLER_DTYPES = CloneMergeResampler.RESAMPLER_DTYPES + (float, float,) + RESAMPLER_DTYPES = CloneMergeResampler.RESAMPLER_DTYPES + ( + float, + float, + ) # fields that can be used for a table like representation RESAMPLER_RECORD_FIELDS = CloneMergeResampler.RESAMPLER_RECORD_FIELDS + ( @@ -686,7 +701,9 @@ def decide( variations.append(new_variation) if _log: - logger.info("variance after selection: {}".format(new_variation)) + logger.info( + "variance after selection: {}".format(new_variation) + ) # if not productive else: @@ -700,7 +717,6 @@ def decide( logger.info("Assigning clones") decision_records = self.assign_clones(merge_groups, walker_clone_nums) - return decision_records, final_variation def _all_to_all_distance( @@ -870,7 +886,7 @@ def resample( decision_id=decision_record.decision_id, target_idxs=decision_record.target_idxs, step_idx=0, - walker_idx=walker_idx + walker_idx=walker_idx, ) resampling_records.append(resampling_record) @@ -902,7 +918,7 @@ class REVOResamplerFactory(Generic[DistanceMetric_]): @classmethod def type(cls) -> type[REVOResampler]: return REVOResampler - + def __call__( self, num_cores: int | None = None, diff --git a/src/wepy/resampling/resamplers/wexplore.py b/src/wepy/resampling/resamplers/wexplore.py index baae9be7..28f4365d 100644 --- a/src/wepy/resampling/resamplers/wexplore.py +++ b/src/wepy/resampling/resamplers/wexplore.py @@ -1,21 +1,21 @@ # Standard Library import itertools as it import logging -from typing import Generic, TypeVar import math import random as rand from collections import defaultdict from copy import copy, deepcopy +from typing import Generic, TypeVar # Third Party Library +import attrs import networkx as nx import numpy as np -import attrs # First Party Library +from wepy.resampling.distances.base import Distance from wepy.resampling.resamplers.clone_merge import CloneMergeResampler from wepy.resampling.resamplers.resampler import ResamplerError -from wepy.resampling.distances.base import Distance from wepy.walker import WalkerState logger = logging.getLogger(__name__) @@ -2084,8 +2084,8 @@ def balance_tree(self, delta_walkers=0): class WExploreResampler( - CloneMergeResampler, - Generic[DistanceMetric_, WalkerState_], + CloneMergeResampler, + Generic[DistanceMetric_, WalkerState_], ): """Resampler implementing the WExplore algorithm. @@ -2658,6 +2658,7 @@ def resample(self, walkers): return resampled_walkers, resampling_data, resampler_data + @attrs.define class WExploreResamplerFactory(Generic[DistanceMetric_, WalkerState_]): @@ -2682,7 +2683,7 @@ def __attrs_post_init__(self) -> None: @classmethod def type(cls) -> type[WExploreResampler]: return WExploreResampler - + def __call__( self, num_cores: int | None = None, @@ -2697,4 +2698,3 @@ def __call__( pmax=self.pmax, seed=self.seed, ) - diff --git a/src/wepy/runners/mock.py b/src/wepy/runners/mock.py index 224f9874..fd3c4099 100644 --- a/src/wepy/runners/mock.py +++ b/src/wepy/runners/mock.py @@ -16,7 +16,7 @@ RunnerStatus, RunSegmentData, ) -from wepy.walker import WalkerState, AttrsWalkerStateMixin +from wepy.walker import AttrsWalkerStateMixin, WalkerState logger = logging.getLogger(__name__) diff --git a/src/wepy/runners/openmm/__init__.py b/src/wepy/runners/openmm/__init__.py index ddffba94..846d53dd 100644 --- a/src/wepy/runners/openmm/__init__.py +++ b/src/wepy/runners/openmm/__init__.py @@ -12,13 +12,13 @@ PlatformKwargs, ) from .state import ( + OPENMM_DEFAULT_UNITS, OpenMMState, OpenMMStateValidationError, OpenMMStateWrapper, dummy_context, get_context_state, state_to_xml, - OPENMM_DEFAULT_UNITS, ) __all__ = [ diff --git a/src/wepy/runners/openmm/runner.py b/src/wepy/runners/openmm/runner.py index 204f8329..c6dc6b73 100644 --- a/src/wepy/runners/openmm/runner.py +++ b/src/wepy/runners/openmm/runner.py @@ -537,7 +537,7 @@ class OpenMMRunnerFactory: @classmethod def type(cls) -> type[OpenMMRunner]: return OpenMMRunner - + def __call__(self) -> OpenMMRunner: return OpenMMRunner( diff --git a/src/wepy/runners/openmm/state.py b/src/wepy/runners/openmm/state.py index a5510f34..fbb7a123 100644 --- a/src/wepy/runners/openmm/state.py +++ b/src/wepy/runners/openmm/state.py @@ -1,6 +1,6 @@ # Standard Library import logging -from collections.abc import Collection, Mapping +from collections.abc import Collection from typing import ( Any, ClassVar, diff --git a/src/wepy/runners/runner.py b/src/wepy/runners/runner.py index 84a26824..34dccd8e 100644 --- a/src/wepy/runners/runner.py +++ b/src/wepy/runners/runner.py @@ -20,7 +20,7 @@ # Standard Library import logging from enum import IntEnum -from typing import Callable, Literal, Protocol, TypeVar +from typing import Literal, Protocol, TypeVar # Third Party Library import attrs @@ -185,8 +185,6 @@ def post_cycle( ... - - @attrs.define class NoRunner(Runner): """Stub Runner that just returns the walkers back with the same state. @@ -228,6 +226,7 @@ def post_cycle(self, segments_data: None) -> None: self.state_machine.send(RunnerEvent.POST_SEGMENT) self.state_machine.send(RunnerEvent.POST_CYCLE) + @attrs.define class NoRunnerFactory: @@ -237,4 +236,3 @@ def type(cls) -> type[NoRunner]: def __call__(self) -> NoRunner: return NoRunner() - diff --git a/src/wepy/sim_manager.py b/src/wepy/sim_manager.py index 8933a96e..eeb252eb 100644 --- a/src/wepy/sim_manager.py +++ b/src/wepy/sim_manager.py @@ -47,7 +47,7 @@ import enum import logging import time -from typing import Callable, Final, Generic, Literal, TypeVar +from typing import Final, Generic, Literal, TypeVar # Third Party Library import attrs @@ -55,8 +55,8 @@ from immutables import Map as frozenmap # First Party Library -from wepy.factory import Factory from wepy.boundary_conditions.boundary import BoundaryConditions +from wepy.factory import Factory from wepy.monitor import Monitor from wepy.reporter.base import CycleReportDict, Reporter from wepy.resampling.resamplers.resampler import Resampler diff --git a/src/wepy/storage/protocol.py b/src/wepy/storage/protocol.py index 5bc31e38..30e41e17 100644 --- a/src/wepy/storage/protocol.py +++ b/src/wepy/storage/protocol.py @@ -5,21 +5,27 @@ the reporting and storage backends should utilize. """ + +# Standard Library from collections.abc import Mapping -from numpy.typing import NDArray -from typing import Union, Literal, TypedDict, Required +from typing import Literal, Required, TypedDict, Union -import numpy as np +# Third Party Library import attrs +import numpy as np +from numpy.typing import NDArray RecordValueDtype = int | float | NDArray Record = Mapping[str, RecordValueDtype] + + @attrs.define class RunRecord: cycle_idx: Required[int] record: Record + # Numpy-style shapes of all fields produced in records. # # There should be the same number of elements as there are in the @@ -44,7 +50,6 @@ class RunRecord: ] - # There should be the same number of elements as there are in the # corresponding 'FIELDS' class constant. # @@ -64,34 +69,40 @@ class RunRecord: np.float32, np.float64, bool, -] +] RecordFieldSpec = tuple[ - str, # name - RecordFieldShapeSpec, # shape - RecordFieldDtype, # dtype + str, # name + RecordFieldShapeSpec, # shape + RecordFieldDtype, # dtype ] # Specific record types guaranteed -DECISION_RECORD_FIELDS = frozenset({ - "decision_id", - "target_idxs", -}) +DECISION_RECORD_FIELDS = frozenset( + { + "decision_id", + "target_idxs", + } +) + class DecisionRecordUnstruct(TypedDict, total=False): decision_id: Required[int] target_idxs: Required[tuple[int, ...]] - -RESAMPLING_RECORD_FIELDS = frozenset({ - "step_idx", - "walker_idx", - "decision_id", - "target_idxs", -}) + +RESAMPLING_RECORD_FIELDS = frozenset( + { + "step_idx", + "walker_idx", + "decision_id", + "target_idxs", + } +) + @attrs.define class ResamplingRecord: @@ -100,17 +111,22 @@ class ResamplingRecord: decision_id: int target_idxs: tuple[int, ...] + class ResamplingRecordUnstruct(TypedDict, total=False): step_idx: Required[int] walker_idx: Required[int] decision_id: Required[int] target_idxs: Required[tuple[int, ...]] -WARPING_RECORD_FIELDS = frozenset({ - "walker_idx", - "target_idx", - "weight", -}) + +WARPING_RECORD_FIELDS = frozenset( + { + "walker_idx", + "target_idx", + "weight", + } +) + @attrs.define class WarpRecord: @@ -118,13 +134,13 @@ class WarpRecord: target_idx: int weight: float + class WarpRecordUnstruct(TypedDict, total=False): walker_idx: Required[int] target_idx: Required[int] weight: Required[float] - # Trace types used in data access # (traj_idx, cycle_idx) ContigWalkerTrace = list[tuple[int, int], ...] @@ -134,4 +150,3 @@ class WarpRecordUnstruct(TypedDict, total=False): # (run_idx, cycle_idx) ContigTrace = list[tuple[int, int], ...] - diff --git a/src/wepy/typing.py b/src/wepy/typing.py index bc56fde0..b9526483 100644 --- a/src/wepy/typing.py +++ b/src/wepy/typing.py @@ -4,19 +4,25 @@ actually be checked in a checker. """ -from typing import Annotated, Union, Literal + +# Standard Library +from typing import Annotated, Literal, Union + +# Third Party Library import attrs import numpy as np from numpy.typing import NDArray + @attrs.define class Shape: dims: tuple[int | Literal[Ellipsis], ...] + IdxArray = Annotated[ - NDArray[np.integer], - Shape((...,)), - ] + NDArray[np.integer], + Shape((...,)), +] Idxs = Union[ list[int], IdxArray, diff --git a/src/wepy/util/attrs.py b/src/wepy/util/attrs.py index 509430e2..c3f315e9 100644 --- a/src/wepy/util/attrs.py +++ b/src/wepy/util/attrs.py @@ -1,13 +1,18 @@ -from typing import Any, Iterator +# Standard Library from collections.abc import Mapping +from typing import Any, Iterator +# Third Party Library import attrs +# First Party Library from wepy.missing import MISSING + class AttrsMappingMixin(Mapping[str, object]): """A convenient mixin for implementing the WalkerState interface - for attrs classes.""" + for attrs classes. + """ def __len__(self) -> int: diff --git a/src/wepy/util/openmm.py b/src/wepy/util/openmm.py index e0353480..6712042d 100644 --- a/src/wepy/util/openmm.py +++ b/src/wepy/util/openmm.py @@ -8,7 +8,9 @@ import openmm -def array3d_to_vec3(array: numpy.typing.ArrayLike) -> Generator[openmm.Vec3, None, None]: +def array3d_to_vec3( + array: numpy.typing.ArrayLike, +) -> Generator[openmm.Vec3, None, None]: for row in array: yield openmm.Vec3(*row.tolist()) diff --git a/src/wepy/walker.py b/src/wepy/walker.py index 52fc6b0c..0a24248a 100644 --- a/src/wepy/walker.py +++ b/src/wepy/walker.py @@ -27,19 +27,18 @@ """ # Standard Library +import copy import logging import math import random as rand from typing import Any, Generic, Protocol, TypeVar -import copy # Third Party Library import attrs +# First Party Library from wepy.util.attrs import AttrsMappingMixin -from wepy.missing import MISSING - logger = logging.getLogger(__name__) T = TypeVar("T") @@ -53,17 +52,21 @@ def __eq__(self, other: Any) -> bool: ... def dict(self) -> dict[str, T]: ... + # TODO: merge with the AttrsMappingMixin class AttrsWalkerStateMixin(AttrsMappingMixin): """A convenient mixin for implementing the WalkerState interface - for attrs classes.""" + for attrs classes. + """ def dict(self) -> dict[str, Any]: return attrs.asdict(self) + class WalkerStateBox: """A type black box walker state, useful in reporting when you - need polymorphism in communicating walker data.""" + need polymorphism in communicating walker data. + """ def __init__(self, **kwargs: dict[str, Any]) -> None: """Constructor for WalkerState. @@ -88,6 +91,7 @@ def __eq__(self, other: Any) -> bool: else: return self.dict() == other.dict() + WalkerState_ = TypeVar("WalkerState_") diff --git a/src/wepy/work_mapper/base.py b/src/wepy/work_mapper/base.py index 3f63b474..97945f33 100644 --- a/src/wepy/work_mapper/base.py +++ b/src/wepy/work_mapper/base.py @@ -37,4 +37,3 @@ def map( def get_worker_segment_times(self) -> dict[int, list[float]] | None: ... def cleanup(self) -> None: ... - diff --git a/src/wepy/work_mapper/openmm/proc_pool.py b/src/wepy/work_mapper/openmm/proc_pool.py index 06da9f5a..dd1cc044 100644 --- a/src/wepy/work_mapper/openmm/proc_pool.py +++ b/src/wepy/work_mapper/openmm/proc_pool.py @@ -1,16 +1,15 @@ """Special OpenMM mappers.""" # Standard Library -import itertools import logging import multiprocessing as mp from typing import Callable # Third Party Library import attrs +import more_itertools # First Party Library -import more_itertools from wepy.runners.openmm import ( GPU_PLATFORMS, OpenMMPlatformName, @@ -248,7 +247,6 @@ def __attrs_post_init__(self) -> None: @classmethod def type(cls) -> type[OpenMMProcPoolWorkMapper]: return OpenMMProcPoolWorkMapper - def __call__(self) -> OpenMMProcPoolWorkMapper: diff --git a/src/wepy/work_mapper/serial.py b/src/wepy/work_mapper/serial.py index 351c3232..56fe896b 100644 --- a/src/wepy/work_mapper/serial.py +++ b/src/wepy/work_mapper/serial.py @@ -10,9 +10,9 @@ ) # First Party Library +from wepy.factory import Factory from wepy.runners.runner import RunSegmentData from wepy.walker import WalkerState -from wepy.factory import Factory logger = logging.getLogger(__name__) @@ -84,6 +84,7 @@ def map( return results + class SerialMapperFactory(Factory[SerialMapper]): @classmethod diff --git a/src/wepy_tools/systems/alanine_dipeptide.py b/src/wepy_tools/systems/alanine_dipeptide.py index e480feb3..be72573b 100644 --- a/src/wepy_tools/systems/alanine_dipeptide.py +++ b/src/wepy_tools/systems/alanine_dipeptide.py @@ -99,7 +99,7 @@ def image_distance( angles_b = np.concatenate((image_b.phis, image_b.psis)) # TODO: which one to use? - + # compute the circular difference deltas = np.arctan2( np.sin(angles_a - angles_b), @@ -110,5 +110,5 @@ def image_distance( # np.sin(angles_a - angles_b), # np.cos(angles_a - angles_b), # ) - + return np.sqrt(np.sum(deltas**2)) diff --git a/src/wepy_tools/systems/lennard_jones.py b/src/wepy_tools/systems/lennard_jones.py index f1779495..5645ccc7 100644 --- a/src/wepy_tools/systems/lennard_jones.py +++ b/src/wepy_tools/systems/lennard_jones.py @@ -1,5 +1,6 @@ # Third Party Library import attrs +import mdtraj import numpy as np import numpy.typing import openmm @@ -10,8 +11,6 @@ # First Party Library from wepy.resampling.distances.base import Distance from wepy.runners.openmm import OpenMMState - -import mdtraj from wepy.util.mdtraj import mdtraj_to_json_topology diff --git a/tests/data/synthesize.py b/tests/data/synthesize.py index 562390c9..085cce6f 100644 --- a/tests/data/synthesize.py +++ b/tests/data/synthesize.py @@ -1,15 +1,15 @@ -from pathlib import Path - -import cyclopts -import logging +# Standard Library import copy +import logging +from pathlib import Path # Third Party Library +import cyclopts +import mdtraj import openmm import psutil # First Party Library -import mdtraj import wepy from wepy_tools.systems.alanine_dipeptide import ( AlanineDipeptideExplicitSystem, @@ -32,12 +32,13 @@ app = cyclopts.App() + @app.command def realistic_hdf5_dialanine_explicit(out_path: Path): STEP_SIZE = 2.0 * openmm.unit.femtosecond TEMPERATURE = 300.0 * openmm.unit.kelvin - + ala_sys = AlanineDipeptideExplicitSystem() integrator = openmm.LangevinIntegrator(TEMPERATURE, 0.1, STEP_SIZE) @@ -98,15 +99,18 @@ def realistic_hdf5_dialanine_explicit(out_path: Path): resampler_class=wepy.REVOResampler, save_fields=DEFAULT_SAVE_FIELDS + ("velocities",), # only require these fields for the initial walkers - init_walker_save_fields=("positions", "box_vectors",), + init_walker_save_fields=( + "positions", + "box_vectors", + ), sparse_fields={ - "velocities" : 2, + "velocities": 2, }, main_rep_idxs=protein_idxs, all_atoms_rep_freq=2, alt_reps={ - "water" : (water_idxs, 2), - } + "water": (water_idxs, 2), + }, ) sim_manager = wepy.Manager( @@ -130,5 +134,6 @@ def realistic_hdf5_dialanine_explicit(out_path: Path): segment_lengths=cycle_steps, ) + if __name__ == "__main__": app() diff --git a/tests/integration/test_openmm/test_realistic.py b/tests/integration/test_openmm/test_realistic.py index 26b4f20a..ea418783 100644 --- a/tests/integration/test_openmm/test_realistic.py +++ b/tests/integration/test_openmm/test_realistic.py @@ -7,16 +7,16 @@ """ # Standard Library -import logging import copy +import logging # Third Party Library +import mdtraj import openmm import psutil import pytest # First Party Library -import mdtraj import wepy from wepy.runners.openmm.runner import ( _DEFAULT_HEARTBEAT_INTERVAL, @@ -118,7 +118,7 @@ def test_lennard_jones_revo_procpool(tmp_path_factory): # only require these fields for the initial walkers init_walker_save_fields=("positions",), sparse_fields={ - "velocities" : 2, + "velocities": 2, }, ) @@ -224,15 +224,18 @@ def test_alanine_dipeptide_revo_procpool(tmp_path_factory): resampler_class=wepy.REVOResampler, save_fields=DEFAULT_SAVE_FIELDS + ("velocities",), # only require these fields for the initial walkers - init_walker_save_fields=("positions", "box_vectors",), + init_walker_save_fields=( + "positions", + "box_vectors", + ), sparse_fields={ - "velocities" : 2, + "velocities": 2, }, main_rep_idxs=protein_idxs, all_atoms_rep_freq=2, alt_reps={ - "water" : (water_idxs, 2), - } + "water": (water_idxs, 2), + }, ) reporters = [dashboard_reporter, hdf5_reporter] diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index d8da2fce..77bcfd51 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,28 +1,28 @@ +# Standard Library import shutil import subprocess import sys -from typing import Callable - from pathlib import Path +from typing import Callable -import pytest - +# Third Party Library import numpy as np +import pytest -from wepy.walker import Walker, WalkerStateBox -from wepy_tools.systems.lennard_jones import LennardJonesPair -from wepy.typing import IdxArray +# First Party Library from wepy.hdf5 import WepyHDF5 +from wepy.resampling.decisions.no_decision import NoDecision from wepy.resampling.resamplers.noresampler import ( - NoResamplerResamplingRecord, NoResampler, + NoResamplerResamplingRecord, ) -from wepy.resampling.decisions.no_decision import NoDecision +from wepy.typing import IdxArray +from wepy.walker import Walker, WalkerStateBox +from wepy_tools.systems.lennard_jones import LennardJonesPair def reflink_or_copy(src: Path, dst: Path) -> None: - """ - Create a copy-on-write reflink if supported. + """Create a copy-on-write reflink if supported. Fall back to a full copy otherwise. """ try: @@ -45,6 +45,7 @@ def reflink_or_copy(src: Path, dst: Path) -> None: except Exception: shutil.copy2(src, dst) + DATA_DIR = Path(__file__).parent.parent / "data" @@ -64,10 +65,10 @@ def wepy_h5_factory() -> Callable[[Path], Path]: test_sys = LennardJonesPair() def _factory( - path: Path, - sparse_fields: tuple[str, ...] | None = None, - alt_reps: dict[str, IdxArray] | None = None, - main_rep_idxs: IdxArray | None = None, + path: Path, + sparse_fields: tuple[str, ...] | None = None, + alt_reps: dict[str, IdxArray] | None = None, + main_rep_idxs: IdxArray | None = None, ) -> Path: # create the file @@ -84,23 +85,29 @@ def _factory( return _factory + _INIT_WALKERS = [ - Walker( - WalkerStateBox( - positions=np.array([ - [1., 1., 1.], - [2., 2., 2.], - ]), - box_vectors=np.array([ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]), - kinetic_energy=3.455, - ), - 0.1, - ), + Walker( + WalkerStateBox( + positions=np.array( + [ + [1.0, 1.0, 1.0], + [2.0, 2.0, 2.0], ] + ), + box_vectors=np.array( + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + ), + kinetic_energy=3.455, + ), + 0.1, + ), +] + @pytest.fixture(scope="session") def _wepy_h5_run_init(wepy_h5_factory, tmp_path_factory) -> Path: @@ -121,9 +128,7 @@ def _wepy_h5_run_init(wepy_h5_factory, tmp_path_factory) -> Path: # initialize the file with WepyHDF5(path, mode="r+") as wepy_h5: - run_grp = wepy_h5.new_run( - init_walkers=_INIT_WALKERS - ) + run_grp = wepy_h5.new_run(init_walkers=_INIT_WALKERS) wepy_h5.init_run_fields_resampling_decision( 0, @@ -170,7 +175,6 @@ def _wepy_h5_run_init(wepy_h5_factory, tmp_path_factory) -> Path: "progress", [name for name, _, _ in progress_fields], ) - # TODO: more fields for resampler records and BC # records. These are always optional and strictly accessory so @@ -178,6 +182,7 @@ def _wepy_h5_run_init(wepy_h5_factory, tmp_path_factory) -> Path: return path + @pytest.fixture(scope="function") def wepy_h5_run_init(_wepy_h5_run_init, tmpdir) -> Path: @@ -215,41 +220,77 @@ def _wepy_h5_traj_init(_wepy_h5_run_init, tmp_path_factory) -> Path: traj0_grp = wepy_h5.add_traj( 0, data={ - "positions" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), - "box_vectors" : np.array([[ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]]), - "kinetic_energy" : np.array([ - [4.87], - ]), + "positions": np.array( + [ + [ + [ + 2.0, + 2.0, + 2.0, + ], + [ + 1.0, + 1.0, + 1.0, + ], + ] + ] + ), + "box_vectors": np.array( + [ + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + ] + ), + "kinetic_energy": np.array( + [ + [4.87], + ] + ), }, weights=np.array([[0.2]]), - metadata={"foo" : "hello"}, + metadata={"foo": "hello"}, ) - + traj1_grp = wepy_h5.add_traj( 0, data={ - "positions" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), - "box_vectors" : np.array([[ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]]), - "kinetic_energy" : np.array([ - [4.87], - ]), + "positions": np.array( + [ + [ + [ + 2.0, + 2.0, + 2.0, + ], + [ + 1.0, + 1.0, + 1.0, + ], + ] + ] + ), + "box_vectors": np.array( + [ + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + ] + ), + "kinetic_energy": np.array( + [ + [4.87], + ] + ), }, weights=np.array([[0.2]]), - metadata={"foo" : "hello"}, + metadata={"foo": "hello"}, ) wepy_h5.extend_cycle_resampling_records( @@ -271,7 +312,7 @@ def _wepy_h5_traj_init(_wepy_h5_run_init, tmp_path_factory) -> Path: walker_idx=1, step_idx=0, ), - ] + ], ) wepy_h5.extend_cycle_progress_records( @@ -280,21 +321,20 @@ def _wepy_h5_traj_init(_wepy_h5_run_init, tmp_path_factory) -> Path: [ # only a single record for the cycle { - "ensemble_average" : 1.2, - "walker_distances" : [ - 1., 1., + "ensemble_average": 1.2, + "walker_distances": [ + 1.0, + 1.0, ], }, - ] + ], ) # TODO: the other record groups - - return path + return path - @pytest.fixture(scope="function") def wepy_h5_traj_init(_wepy_h5_traj_init, tmpdir) -> Path: @@ -304,6 +344,7 @@ def wepy_h5_traj_init(_wepy_h5_traj_init, tmpdir) -> Path: return path + @pytest.fixture(scope="session") def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: """Data generation fixture, should not be used by individual tests @@ -332,7 +373,6 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: # Add the new data with WepyHDF5(path, mode="r+") as wepy_h5: - # extend run 0 wepy_h5.extend_traj( @@ -340,22 +380,52 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: 0, weights=np.array([[0.2]]), data={ - "positions" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), - "box_vectors" : np.array([[ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]]), - "kinetic_energy" : np.array([ - [4.87], - ]), - "velocities" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), + "positions": np.array( + [ + [ + [ + 2.0, + 2.0, + 2.0, + ], + [ + 1.0, + 1.0, + 1.0, + ], + ] + ] + ), + "box_vectors": np.array( + [ + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + ] + ), + "kinetic_energy": np.array( + [ + [4.87], + ] + ), + "velocities": np.array( + [ + [ + [ + 2.0, + 2.0, + 2.0, + ], + [ + 1.0, + 1.0, + 1.0, + ], + ] + ] + ), }, ) @@ -364,22 +434,52 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: 1, weights=np.array([[0.2]]), data={ - "positions" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), - "box_vectors" : np.array([[ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]]), - "kinetic_energy" : np.array([ - [4.87], - ]), - "velocities" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), + "positions": np.array( + [ + [ + [ + 2.0, + 2.0, + 2.0, + ], + [ + 1.0, + 1.0, + 1.0, + ], + ] + ] + ), + "box_vectors": np.array( + [ + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + ] + ), + "kinetic_energy": np.array( + [ + [4.87], + ] + ), + "velocities": np.array( + [ + [ + [ + 2.0, + 2.0, + 2.0, + ], + [ + 1.0, + 1.0, + 1.0, + ], + ] + ] + ), }, ) @@ -402,7 +502,7 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: walker_idx=1, step_idx=0, ), - ] + ], ) wepy_h5.extend_cycle_progress_records( @@ -411,12 +511,13 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: [ # only a single record for the cycle { - "ensemble_average" : 1.2, - "walker_distances" : [ - 1., 1., + "ensemble_average": 1.2, + "walker_distances": [ + 1.0, + 1.0, ], }, - ] + ], ) # run 1, a continuation of run 0 @@ -462,41 +563,77 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: wepy_h5.add_traj( 1, data={ - "positions" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), - "box_vectors" : np.array([[ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]]), - "kinetic_energy" : np.array([ - [4.87], - ]), + "positions": np.array( + [ + [ + [ + 2.0, + 2.0, + 2.0, + ], + [ + 1.0, + 1.0, + 1.0, + ], + ] + ] + ), + "box_vectors": np.array( + [ + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + ] + ), + "kinetic_energy": np.array( + [ + [4.87], + ] + ), }, weights=np.array([[0.2]]), - metadata={"foo" : "hello"}, + metadata={"foo": "hello"}, ) - + wepy_h5.add_traj( 1, data={ - "positions" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), - "box_vectors" : np.array([[ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]]), - "kinetic_energy" : np.array([ - [4.87], - ]), + "positions": np.array( + [ + [ + [ + 2.0, + 2.0, + 2.0, + ], + [ + 1.0, + 1.0, + 1.0, + ], + ] + ] + ), + "box_vectors": np.array( + [ + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + ] + ), + "kinetic_energy": np.array( + [ + [4.87], + ] + ), }, weights=np.array([[0.2]]), - metadata={"foo" : "hello"}, + metadata={"foo": "hello"}, ) wepy_h5.extend_cycle_resampling_records( @@ -518,7 +655,7 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: walker_idx=1, step_idx=0, ), - ] + ], ) wepy_h5.extend_cycle_progress_records( @@ -527,12 +664,13 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: [ # only a single record for the cycle { - "ensemble_average" : 1.2, - "walker_distances" : [ - 1., 1., + "ensemble_average": 1.2, + "walker_distances": [ + 1.0, + 1.0, ], }, - ] + ], ) # extend run 1 @@ -541,22 +679,52 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: 0, weights=np.array([[0.2]]), data={ - "positions" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), - "box_vectors" : np.array([[ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]]), - "kinetic_energy" : np.array([ - [4.87], - ]), - "velocities" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), + "positions": np.array( + [ + [ + [ + 2.0, + 2.0, + 2.0, + ], + [ + 1.0, + 1.0, + 1.0, + ], + ] + ] + ), + "box_vectors": np.array( + [ + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + ] + ), + "kinetic_energy": np.array( + [ + [4.87], + ] + ), + "velocities": np.array( + [ + [ + [ + 2.0, + 2.0, + 2.0, + ], + [ + 1.0, + 1.0, + 1.0, + ], + ] + ] + ), }, ) @@ -565,22 +733,52 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: 1, weights=np.array([[0.2]]), data={ - "positions" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), - "box_vectors" : np.array([[ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]]), - "kinetic_energy" : np.array([ - [4.87], - ]), - "velocities" : np.array([[ - [2., 2., 2.,], - [1., 1., 1.,], - ]]), + "positions": np.array( + [ + [ + [ + 2.0, + 2.0, + 2.0, + ], + [ + 1.0, + 1.0, + 1.0, + ], + ] + ] + ), + "box_vectors": np.array( + [ + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + ] + ), + "kinetic_energy": np.array( + [ + [4.87], + ] + ), + "velocities": np.array( + [ + [ + [ + 2.0, + 2.0, + 2.0, + ], + [ + 1.0, + 1.0, + 1.0, + ], + ] + ] + ), }, ) @@ -603,7 +801,7 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: walker_idx=1, step_idx=0, ), - ] + ], ) wepy_h5.extend_cycle_progress_records( @@ -612,17 +810,18 @@ def _wepy_h5_full_init(_wepy_h5_traj_init, tmp_path_factory) -> Path: [ # only a single record for the cycle { - "ensemble_average" : 1.2, - "walker_distances" : [ - 1., 1., + "ensemble_average": 1.2, + "walker_distances": [ + 1.0, + 1.0, ], }, - ] + ], ) - - + return path + @pytest.fixture(scope="function") def wepy_h5_full_init(_wepy_h5_full_init, tmpdir) -> Path: diff --git a/tests/unit/test_analysis/test_contig_tree.py b/tests/unit/test_analysis/test_contig_tree.py index 6f93a1ea..9f1903a9 100644 --- a/tests/unit/test_analysis/test_contig_tree.py +++ b/tests/unit/test_analysis/test_contig_tree.py @@ -1,10 +1,15 @@ +# Standard Library from pathlib import Path -import wepy + +# Third Party Library import networkx as nx + +# First Party Library +import wepy from wepy.analysis.contig_tree import ( BaseContigTree, - ContigTree, Contig, + ContigTree, ) @@ -12,7 +17,7 @@ class Test_BaseContigTree: def test___init__(self, wepy_h5_full_init: Path): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -38,9 +43,10 @@ def test___init__(self, wepy_h5_full_init: Path): for node_id in bct.graph.nodes: node = bct.graph.nodes[node_id] assert set(node.keys()) == { - "resampling_steps", "parent_idxs", "discontinuities", + "resampling_steps", + "parent_idxs", + "discontinuities", } - bct = BaseContigTree( wepy_h5, @@ -95,7 +101,7 @@ def test_contig_to_run_trace(self): (0, 1), (0, 2), (0, 3), - ] + ], ) == [ (0, 0, 0), (0, 0, 1), @@ -104,7 +110,7 @@ def test_contig_to_run_trace(self): ] def test_run_trace_to_contig_trace(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -126,7 +132,7 @@ def test_run_trace_to_contig_trace(self, wepy_h5_full_init): ] def test_contig_cycle_idx(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -139,7 +145,7 @@ def test_contig_cycle_idx(self, wepy_h5_full_init): assert bct.contig_cycle_idx(1, 1) == 3 def test_get_branch_trace(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -150,9 +156,7 @@ def test_get_branch_trace(self, wepy_h5_full_init): run_idx=0, cycle_idx=0, start_contig_idx=0, - ) == [ - (0, 0) - ] + ) == [(0, 0)] assert bct.get_branch_trace( run_idx=0, @@ -166,7 +170,7 @@ def test_get_branch_trace(self, wepy_h5_full_init): ] def test_trace_parent_table(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -192,23 +196,39 @@ def test_trace_parent_table(self, wepy_h5_full_init): def test__tree_leaves(self): - assert set(BaseContigTree._tree_leaves( - (0, 0), - # NOTE: the tree is reversed from what is in the BaseContigTree - nx.DiGraph([ - ((0, 0), (0, 1),), - ((0, 1), (1, 0),), - # leaves - ((1, 0), (1, 1),), - ((1, 0), (2, 0),), - ]) - )) == { + assert set( + BaseContigTree._tree_leaves( + (0, 0), + # NOTE: the tree is reversed from what is in the BaseContigTree + nx.DiGraph( + [ + ( + (0, 0), + (0, 1), + ), + ( + (0, 1), + (1, 0), + ), + # leaves + ( + (1, 0), + (1, 1), + ), + ( + (1, 0), + (2, 0), + ), + ] + ), + ) + ) == { (1, 1), (2, 0), } def test__subtree_leaves(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -218,17 +238,17 @@ def test__subtree_leaves(self, wepy_h5_full_init): assert set(bct._subtree_leaves((0, 0))) == {(1, 1)} def test_leaves(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, decision_class=wepy.NoDecision, ) - assert set(bct.leaves()) == {(1,1)} + assert set(bct.leaves()) == {(1, 1)} def test_root_leaves(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -238,11 +258,11 @@ def test_root_leaves(self, wepy_h5_full_init): root_leaves = bct.root_leaves() assert (0, 0) in root_leaves assert set(root_leaves[(0, 0)]) == { - (1,1), + (1, 1), } - + def test_subtrees(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -253,7 +273,7 @@ def test_subtrees(self, wepy_h5_full_init): assert len(bct.subtrees()) == 1 def test_get_subtree(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -263,22 +283,22 @@ def test_get_subtree(self, wepy_h5_full_init): # TODO: this method is fallacious and need reworked, so just a # minimal test bct.get_subtree((0, 0)).nodes - + def test__subtree_root(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, decision_class=wepy.NoDecision, ) - assert bct._subtree_root((0,0)) == (0,0) - assert bct._subtree_root((0,1)) == (0,0) - assert bct._subtree_root((1,0)) == (0,0) - assert bct._subtree_root((1,1)) == (0,0) - + assert bct._subtree_root((0, 0)) == (0, 0) + assert bct._subtree_root((0, 1)) == (0, 0) + assert bct._subtree_root((1, 0)) == (0, 0) + assert bct._subtree_root((1, 1)) == (0, 0) + def test_roots(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -289,7 +309,7 @@ def test_roots(self, wepy_h5_full_init): def test_span_traces(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -297,16 +317,16 @@ def test_span_traces(self, wepy_h5_full_init): ) assert bct.span_traces == { - 0 : [ - (0, 0), - (0, 1), - (1, 0), - (1, 1), - ]} + 0: [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), + ] + } - def test__root_spanning_contig_traces(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -314,7 +334,7 @@ def test__root_spanning_contig_traces(self, wepy_h5_full_init): ) assert bct._root_spanning_contig_traces() == { - (0, 0) : [ + (0, 0): [ [ (0, 0), (0, 1), @@ -323,10 +343,9 @@ def test__root_spanning_contig_traces(self, wepy_h5_full_init): ], ], } - def test_spanning_contig_traces(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -334,29 +353,29 @@ def test_spanning_contig_traces(self, wepy_h5_full_init): ) assert bct.spanning_contig_traces() == [ - [ - (0, 0), - (0, 1), - (1, 0), - (1, 1), - ], + [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), + ], ] def test__spanning_paths(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, decision_class=wepy.NoDecision, ) - assert bct._spanning_paths((0,0)) == { - (1, 1) : [ - (0, 0), - (0, 1), - (1, 0), - (1, 1), + assert bct._spanning_paths((0, 0)) == { + (1, 1): [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), ] } @@ -373,19 +392,15 @@ def test__contig_trace_to_contig_runs(self): def test__contig_runs_to_continuations(self): - assert BaseContigTree._contig_runs_to_continuations( - [0, 1] - ) == [[1, 0]] + assert BaseContigTree._contig_runs_to_continuations([0, 1]) == [[1, 0]] def test__continuations_to_contig_runs(self): - assert BaseContigTree._continuations_to_contig_runs( - [[1, 0]] - ) == [0, 1] + assert BaseContigTree._continuations_to_contig_runs([[1, 0]]) == [0, 1] # TODO: requires Contig def test_span_contig(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = BaseContigTree( wepy_h5, @@ -394,15 +409,14 @@ def test_span_contig(self, wepy_h5_full_init): assert False - assert bct.span_traces == { - 0 : [] - } + assert bct.span_traces == {0: []} + class Test_ContigTree: def test___init__(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") ct = ContigTree( wepy_h5, @@ -413,10 +427,9 @@ def test___init__(self, wepy_h5_full_init): assert ct.base_contigtree is not None - def test_make_contig(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") ct = ContigTree( wepy_h5, @@ -435,7 +448,7 @@ def test_make_contig(self, wepy_h5_full_init): # ) def test_final_trace(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") ct = ContigTree( wepy_h5, @@ -448,7 +461,7 @@ def test_final_trace(self, wepy_h5_full_init): ] def test_lineages(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") ct = ContigTree( wepy_h5, @@ -456,7 +469,9 @@ def test_lineages(self, wepy_h5_full_init): ) assert ct.lineages( - [(1, 0, 1),],# (1, 1, 1)], + [ + (1, 0, 1), + ], # (1, 1, 1)], discontinuities=False, ) == [ [ @@ -466,12 +481,13 @@ def test_lineages(self, wepy_h5_full_init): (1, 0, 1), ], ] - + + class Test_Contig: def test___init__(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") contig = Contig( wepy_h5, @@ -489,7 +505,7 @@ def test___init__(self, wepy_h5_full_init): def test_walker_trace_to_run_trace(self, wepy_h5_full_init): - wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode='r') + wepy_h5 = wepy.WepyHDF5(wepy_h5_full_init, mode="r") bct = Contig( wepy_h5, @@ -509,4 +525,3 @@ def test_walker_trace_to_run_trace(self, wepy_h5_full_init): (1, 0, 0), (1, 0, 1), ] - diff --git a/tests/unit/test_analysis/test_parents.py b/tests/unit/test_analysis/test_parents.py index 58f5761f..18cc69ce 100644 --- a/tests/unit/test_analysis/test_parents.py +++ b/tests/unit/test_analysis/test_parents.py @@ -1,19 +1,17 @@ -from typing import NamedTuple +# Standard Library + +# First Party Library from wepy.analysis.parents import ( - resampling_panel, - parent_panel, - net_parent_table, - parent_table_discontinuities, - parent_cycle_discontinuities, ancestors, - sliding_window, - ParentForest, + net_parent_table, + parent_panel, + resampling_panel, ) -from wepy.storage.protocol import RunRecord from wepy.resampling.decisions.no_decision import NoDecision +from wepy.storage.protocol import RunRecord -def test_resampling_panel(): +def test_resampling_panel(): # # simple case # assert resampling_panel( @@ -64,36 +62,39 @@ def test_resampling_panel(): RunRecord( cycle_idx=0, record=dict( - step_idx=0, - walker_idx=0, - decision_id=0, - target_idxs=(0,),) + step_idx=0, + walker_idx=0, + decision_id=0, + target_idxs=(0,), + ), ), RunRecord( cycle_idx=0, record=dict( - step_idx=0, - walker_idx=1, - decision_id=0, - target_idxs=(1,),) + step_idx=0, + walker_idx=1, + decision_id=0, + target_idxs=(1,), + ), ), - # step 1 RunRecord( cycle_idx=0, record=dict( - step_idx=1, - walker_idx=0, - decision_id=0, - target_idxs=(1,),) + step_idx=1, + walker_idx=0, + decision_id=0, + target_idxs=(1,), + ), ), RunRecord( cycle_idx=0, record=dict( - step_idx=1, - walker_idx=1, - decision_id=0, - target_idxs=(0,),) + step_idx=1, + walker_idx=1, + decision_id=0, + target_idxs=(0,), + ), ), ] ) == [ @@ -102,32 +103,21 @@ def test_resampling_panel(): # step 0 [ # walker 0 - { - "decision_id" : 0, - "target_idxs" : (0,) - }, + {"decision_id": 0, "target_idxs": (0,)}, # walker 1 - { - "decision_id" : 0, - "target_idxs" : (1,) - }, + {"decision_id": 0, "target_idxs": (1,)}, ], # step 1 [ # walker 0 - { - "decision_id" : 0, - "target_idxs" : (1,) - }, + {"decision_id": 0, "target_idxs": (1,)}, # walker 1 - { - "decision_id" : 0, - "target_idxs" : (0,) - }, + {"decision_id": 0, "target_idxs": (0,)}, ], ] ] - + + def test_parent_panel(): assert parent_panel( @@ -138,18 +128,12 @@ def test_parent_panel(): # step 0 [ # walker 0 - { - "decision_id" : 0, - "target_idxs" : (0,) - }, + {"decision_id": 0, "target_idxs": (0,)}, # walker 1 - { - "decision_id" : 0, - "target_idxs" : (1,) - } + {"decision_id": 0, "target_idxs": (1,)}, ], ], - ] + ], ) == [ [ [0, 1], @@ -164,64 +148,53 @@ def test_parent_panel(): # step 0 [ # walker 0 - { - "decision_id" : 0, - "target_idxs" : (0,) - }, + {"decision_id": 0, "target_idxs": (0,)}, # walker 1 - { - "decision_id" : 0, - "target_idxs" : (1,) - } + {"decision_id": 0, "target_idxs": (1,)}, ], # step 1 [ # walker 0 - { - "decision_id" : 0, - "target_idxs" : (1,) - }, + {"decision_id": 0, "target_idxs": (1,)}, # walker 1 - { - "decision_id" : 0, - "target_idxs" : (0,) - } + {"decision_id": 0, "target_idxs": (0,)}, ], ], - ] + ], ) == [ [ [0, 1], [1, 0], ] ] - + + def test_net_parent_table(): - net_parent_table([ + net_parent_table( [ - [0, 1], + [ + [0, 1], + ] ] - ]) == [ + ) == [ # cycle 0 - [ - 0, 1 - ] + [0, 1] ] - - net_parent_table([ + net_parent_table( [ - [0, 1], - [1, 0], + [ + [0, 1], + [1, 0], + ] ] - ]) == [ + ) == [ # cycle 0 - [ - 1, 0 - ] + [1, 0] ] + # TODO: tests for discontinuities # # def test_parent_table_discontinuities(): @@ -230,6 +203,7 @@ def test_net_parent_table(): # def test_parent_cycle_discontinuities(): # pass + def test_ancestors(): assert ancestors( @@ -301,12 +275,14 @@ def test_ancestors(): (0, 2), (1, 3), ] - + + # TODO: test this # # def test_sliding_window(): # pass + # TODO: need Contig for this to work class Test_ParentForest: pass diff --git a/tests/unit/test_fixtures.py b/tests/unit/test_fixtures.py index e70a6d7e..096c3d85 100644 --- a/tests/unit/test_fixtures.py +++ b/tests/unit/test_fixtures.py @@ -1,10 +1,13 @@ -from wepy.hdf5 import WepyHDF5 +# Third Party Library import numpy as np +# First Party Library +from wepy.hdf5 import WepyHDF5 + def test__wepy_h5_run_init(_wepy_h5_run_init): - with WepyHDF5(_wepy_h5_run_init, mode='r') as wepy_h5: + with WepyHDF5(_wepy_h5_run_init, mode="r") as wepy_h5: wepy_h5.run(0) @@ -17,57 +20,60 @@ def test__wepy_h5_run_init(_wepy_h5_run_init): assert "resampling" in wepy_h5.record_fields assert wepy_h5.record_fields["resampling"] == [ - "decision_id", - "target_idxs", - "step_idx", - "walker_idx", - ] + "decision_id", + "target_idxs", + "step_idx", + "walker_idx", + ] assert "warping" in wepy_h5.record_fields assert wepy_h5.record_fields["warping"] == [ - "walker_idx", - "target_idx", - "weight" - ] + "walker_idx", + "target_idx", + "weight", + ] assert "progress" in wepy_h5.record_fields assert wepy_h5.record_fields["progress"] == [ - "ensemble_average", - "walker_distances", - ] - + "ensemble_average", + "walker_distances", + ] + + # test that each test gets its own copy def test_wepy_h5_run_init_1(wepy_h5_run_init): - with WepyHDF5(wepy_h5_run_init, mode='r') as wepy_h5: + with WepyHDF5(wepy_h5_run_init, mode="r") as wepy_h5: assert "mutation_flag" not in wepy_h5.h5 # mutate - with WepyHDF5(wepy_h5_run_init, mode='r+') as wepy_h5: + with WepyHDF5(wepy_h5_run_init, mode="r+") as wepy_h5: wepy_h5.h5["mutation_flag"] = np.array([0]) + def test_wepy_h5_run_init_2(wepy_h5_run_init): - with WepyHDF5(wepy_h5_run_init, mode='r') as wepy_h5: + with WepyHDF5(wepy_h5_run_init, mode="r") as wepy_h5: assert "mutation_flag" not in wepy_h5.h5 # mutate - with WepyHDF5(wepy_h5_run_init, mode='r+') as wepy_h5: + with WepyHDF5(wepy_h5_run_init, mode="r+") as wepy_h5: wepy_h5.h5["mutation_flag"] = np.array([0]) + def test__wepy_h5_traj_init(_wepy_h5_traj_init): - with WepyHDF5(_wepy_h5_traj_init, mode='r') as wepy_h5: + with WepyHDF5(_wepy_h5_traj_init, mode="r") as wepy_h5: assert "velocities" in wepy_h5.sparse_fields - + wepy_h5.run(0) assert len(wepy_h5.resampling_records([0])) == 2 assert len(wepy_h5.progress_records([0])) == 1 - + assert wepy_h5.num_run_trajs(0) == 2 - + for traj_idx in (0, 1): assert "weights" in wepy_h5.traj(0, traj_idx) assert "positions" in wepy_h5.traj(0, traj_idx) @@ -76,21 +82,41 @@ def test__wepy_h5_traj_init(_wepy_h5_traj_init): assert "velocities" in wepy_h5.traj(0, traj_idx) assert wepy_h5.traj_field_entity(0, traj_idx, "weights").shape == (1, 1) - assert wepy_h5.traj_field_entity(0, traj_idx, "positions").shape == (1, 2, 3) - assert wepy_h5.traj_field_entity(0, traj_idx, "box_vectors").shape == (1, 3, 3) - assert wepy_h5.traj_field_entity(0, traj_idx, "kinetic_energy").shape == (1, 1) + assert wepy_h5.traj_field_entity(0, traj_idx, "positions").shape == ( + 1, + 2, + 3, + ) + assert wepy_h5.traj_field_entity(0, traj_idx, "box_vectors").shape == ( + 1, + 3, + 3, + ) + assert wepy_h5.traj_field_entity(0, traj_idx, "kinetic_energy").shape == ( + 1, + 1, + ) assert "data" in wepy_h5.traj_field_entity(0, traj_idx, "velocities") - assert "_sparse_idxs" in wepy_h5.traj_field_entity(0, traj_idx, "velocities") + assert "_sparse_idxs" in wepy_h5.traj_field_entity( + 0, traj_idx, "velocities" + ) + + assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")[ + "data" + ].shape == (0, 0, 0) + assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")[ + "data" + ].maxshape == (None, 2, 3) - assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")["data"].shape == (0, 0, 0) - assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")["data"].maxshape == (None, 2, 3) + assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")[ + "_sparse_idxs" + ].shape == (0,) - assert wepy_h5.traj_field_entity(0, traj_idx, "velocities")["_sparse_idxs"].shape == (0,) def test__wepy_h5_full_init(_wepy_h5_full_init): - with WepyHDF5(_wepy_h5_full_init, mode='r') as wepy_h5: + with WepyHDF5(_wepy_h5_full_init, mode="r") as wepy_h5: assert wepy_h5.num_runs == 2 @@ -109,4 +135,3 @@ def test__wepy_h5_full_init(_wepy_h5_full_init): assert len(wepy_h5.resampling_records([1])) == 4 assert len(wepy_h5.progress_records([1])) == 2 - diff --git a/tests/unit/test_hdf5.py b/tests/unit/test_hdf5.py index e0c92d1f..8ae4e1db 100644 --- a/tests/unit/test_hdf5.py +++ b/tests/unit/test_hdf5.py @@ -1,91 +1,82 @@ -from pathlib import Path +# Standard Library import json -import sys -import shutil -import subprocess -from typing import Callable -import pytest -import numpy as np -from unittest.mock import patch, PropertyMock - +from pathlib import Path +from unittest.mock import PropertyMock, patch +# Third Party Library import h5py -from wepy.typing import IdxArray +import numpy as np +import pytest + +# First Party Library from wepy.hdf5 import ( - numpy_dtype_to_json, - dtype_json_to_numpy, WepyHDF5, - _iter_field_paths, - WepyHDF5Error, - WepyHDF5WriteError, WepyHDF5ReadError, + dtype_json_to_numpy, + numpy_dtype_to_json, ) -from wepy.storage.protocol import RunRecord -from wepy.walker import Walker, WalkerStateBox from wepy.resampling.decisions.no_decision import ( NoDecision, ) -from wepy.resampling.resamplers.noresampler import NoResampler, NoResamplerResamplingRecord - +from wepy.storage.protocol import RunRecord +from wepy.walker import Walker, WalkerStateBox from wepy_tools.systems.lennard_jones import LennardJonesPair _INIT_WALKERS = [ - Walker( - WalkerStateBox( - positions=np.array([ - [1., 1., 1.], - [2., 2., 2.], - ]), - box_vectors=np.array([ - [1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.], - ]), - kinetic_energy=3.455, - ), - 0.1, - ), + Walker( + WalkerStateBox( + positions=np.array( + [ + [1.0, 1.0, 1.0], + [2.0, 2.0, 2.0], + ] + ), + box_vectors=np.array( + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], ] + ), + kinetic_energy=3.455, + ), + 0.1, + ), +] # # TODO: this will be easier once we have a fixture for a full WepyHDF5 # def test__iter_field_paths(wepy_h5_file_ro): # pass + def test_numpy_dtype_to_json(): - assert json.loads( - numpy_dtype_to_json(np.dtype(np.int32)) - ) == { - "kind" : "simple", - "str" : " ( tuple[openmm.System, openmm.app.Topology, openmm.LangevinIntegrator] @@ -21,12 +22,13 @@ def runner_components() -> ( return lj_sys.system, lj_sys.topology, integrator + class Test_OpenMMRunnerDashboardSection: def test___init__(self, runner_components): system, topology, integrator = runner_components - + section = OpenMMRunnerDashboardSection( runner_factory=OpenMMRunnerFactory( system=system, diff --git a/tests/unit/test_resampling/test_decisions/test_clone_merge.py b/tests/unit/test_resampling/test_decisions/test_clone_merge.py index 852f44f0..8a0558d8 100644 --- a/tests/unit/test_resampling/test_decisions/test_clone_merge.py +++ b/tests/unit/test_resampling/test_decisions/test_clone_merge.py @@ -5,9 +5,9 @@ # First Party Library from wepy.resampling.decisions.clone_merge import ( CloneMergeDecisionEnum, + CloneMergeDecisionError, CloneMergeDecisionRecord, MultiCloneMergeDecision, - CloneMergeDecisionError, ) from wepy.runners.mock import MockState from wepy.walker import Walker @@ -34,7 +34,7 @@ def test___init__(self): # clone CloneMergeDecisionRecord( decision_id=2, - target_idxs=(0,1), + target_idxs=(0, 1), ) with pytest.raises(ValueError): @@ -58,18 +58,27 @@ def test___init__(self): with pytest.raises(CloneMergeDecisionError): CloneMergeDecisionRecord( decision_id=1, - target_idxs=(0,1,), + target_idxs=( + 0, + 1, + ), ) with pytest.raises(CloneMergeDecisionError): CloneMergeDecisionRecord( decision_id=3, - target_idxs=(0,1,), + target_idxs=( + 0, + 1, + ), ) with pytest.raises(CloneMergeDecisionError): CloneMergeDecisionRecord( decision_id=4, - target_idxs=(0,1,), + target_idxs=( + 0, + 1, + ), ) with pytest.raises(CloneMergeDecisionError): @@ -80,14 +89,12 @@ def test___init__(self): def test_to_dict(self): - assert CloneMergeDecisionRecord( - decision_id=1, - target_idxs=(0,) - ).to_dict() == { - "decision_id" : 1, - "target_idxs" : (0,), + assert CloneMergeDecisionRecord(decision_id=1, target_idxs=(0,)).to_dict() == { + "decision_id": 1, + "target_idxs": (0,), } + class TestMultiCloneMergeDecision: def test_action(self): diff --git a/tests/unit/test_resampling/test_decisions/test_decision.py b/tests/unit/test_resampling/test_decisions/test_decision.py index aa85307b..29121657 100644 --- a/tests/unit/test_resampling/test_decisions/test_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_decision.py @@ -21,21 +21,26 @@ class MockDecision(BaseDecisionABC): DEFAULT_DECISION = ENUM.NOTHING ANCESTOR_DECISION_IDS = (ENUM.NOTHING.value,) + class Test_BaseDecisionRecord: def test_to_dict(self): assert BaseDecisionRecord(decision_id=1, target_idxs=(0,)).to_dict() == { - "decision_id" : 1, - "target_idxs" : (0,) + "decision_id": 1, + "target_idxs": (0,), } + class Test_Decision: def test_default_decision(self): assert MockDecision.default_decision() == MockDecisionEnum.NOTHING def test_field_names(self): - assert MockDecision.field_names() == ("decision_id", "target_idxs",) + assert MockDecision.field_names() == ( + "decision_id", + "target_idxs", + ) def test_field_shapes(self): assert MockDecision.field_shapes() == ((1,), Ellipsis) @@ -54,11 +59,14 @@ def test_fields(self): "target_idxs", Ellipsis, int, - ) + ), ] def test_record_field_names(self): - assert MockDecision.record_field_names() == ("decision_id", "target_idxs",) + assert MockDecision.record_field_names() == ( + "decision_id", + "target_idxs", + ) def test_enum_dict_by_name(self): assert MockDecision.enum_dict_by_name() == { @@ -88,8 +96,8 @@ def test_action(self): with pytest.raises(NotImplementedError): MockDecision.action( [Walker(MockState(1), 0.1) for _ in range(4)], - [BaseDecisionRecord( - decision_id=0, - target_idxs=(idx,) - ) for idx in range(4)], + [ + BaseDecisionRecord(decision_id=0, target_idxs=(idx,)) + for idx in range(4) + ], ) diff --git a/tests/unit/test_resampling/test_decisions/test_no_decision.py b/tests/unit/test_resampling/test_decisions/test_no_decision.py index e1417f8f..1c99eb06 100644 --- a/tests/unit/test_resampling/test_decisions/test_no_decision.py +++ b/tests/unit/test_resampling/test_decisions/test_no_decision.py @@ -1,4 +1,6 @@ +# Third Party Library import pytest + # First Party Library from wepy.resampling.decisions.no_decision import ( NoDecision, @@ -8,6 +10,7 @@ from wepy.runners.mock import MockState from wepy.walker import Walker + class Test_NoDecisionRecord: def test___init__(self): @@ -31,14 +34,12 @@ def test___init__(self): def test_to_dict(self): - assert NoDecisionRecord( - decision_id=0, - target_idxs=(0,) - ).to_dict() == { - "decision_id" : 0, - "target_idxs" : (0,), + assert NoDecisionRecord(decision_id=0, target_idxs=(0,)).to_dict() == { + "decision_id": 0, + "target_idxs": (0,), } + class Test_NoDecision: def test_action(self): diff --git a/tests/unit/test_resampling/test_resamplers/test_noresampler.py b/tests/unit/test_resampling/test_resamplers/test_noresampler.py index 1cd978e1..90807dcc 100644 --- a/tests/unit/test_resampling/test_resamplers/test_noresampler.py +++ b/tests/unit/test_resampling/test_resamplers/test_noresampler.py @@ -1,18 +1,24 @@ -import numpy as np +# Third Party Library + # First Party Library from wepy.resampling.decisions.no_decision import NothingDecisionEnum -from wepy.resampling.resamplers.noresampler import NoResampler, NoResamplerFactory, NoResamplerResamplingRecord, NoResamplerResamplerRecord +from wepy.resampling.resamplers.noresampler import ( + NoResampler, + NoResamplerFactory, + NoResamplerResamplerRecord, + NoResamplerResamplingRecord, +) from wepy.runners.mock import MockState from wepy.walker import Walker + def test_NoResamplerResamplingRecord(): NoResamplerResamplingRecord( - decision_id=0, - target_idxs=(1,), - walker_idx=1, - step_idx=0 + decision_id=0, target_idxs=(1,), walker_idx=1, step_idx=0 ) + + class Test_NoResampler: def test_resample(self): @@ -38,24 +44,24 @@ def test_resample(self): assert resampler.resample(walkers) == ( walkers, [ - NoResamplerResamplingRecord( - decision_id=NothingDecisionEnum.NOTHING.value, - target_idxs=(0,), - walker_idx=0, - step_idx=0, - ), - NoResamplerResamplingRecord( - decision_id=NothingDecisionEnum.NOTHING.value, - target_idxs=(1,), - walker_idx=1, - step_idx=0, - ), - NoResamplerResamplingRecord( - decision_id=NothingDecisionEnum.NOTHING.value, - target_idxs=(2,), - walker_idx=2, - step_idx=0, - ), + NoResamplerResamplingRecord( + decision_id=NothingDecisionEnum.NOTHING.value, + target_idxs=(0,), + walker_idx=0, + step_idx=0, + ), + NoResamplerResamplingRecord( + decision_id=NothingDecisionEnum.NOTHING.value, + target_idxs=(1,), + walker_idx=1, + step_idx=0, + ), + NoResamplerResamplingRecord( + decision_id=NothingDecisionEnum.NOTHING.value, + target_idxs=(2,), + walker_idx=2, + step_idx=0, + ), ], [NoResamplerResamplerRecord()], ) diff --git a/tests/unit/test_util/test_attrs.py b/tests/unit/test_util/test_attrs.py index c2909059..237915f2 100644 --- a/tests/unit/test_util/test_attrs.py +++ b/tests/unit/test_util/test_attrs.py @@ -1,11 +1,16 @@ +# Third Party Library import attrs + +# First Party Library from wepy.util.attrs import AttrsMappingMixin + @attrs.define class Thing(AttrsMappingMixin): a: int b: str + class Test_AttrsMappingMixin: def test___init__(self): @@ -43,17 +48,14 @@ def test___iter__(self): _vs.add(next(t_it)) assert _vs == {"a", "b"} - def test_keys(self): t = Thing(a=1, b="hello") assert set(t.keys()) == {"a", "b"} - def test_values(self): t = Thing(a=1, b="hello") assert set(t.values()) == {1, "hello"} - def test_items(self): t = Thing(a=1, b="hello") assert set(t.items()) == { diff --git a/tests/unit/test_walker.py b/tests/unit/test_walker.py index 9f91ff76..d984a37c 100644 --- a/tests/unit/test_walker.py +++ b/tests/unit/test_walker.py @@ -7,9 +7,9 @@ # First Party Library from wepy.missing import MISSING from wepy.walker import ( + AttrsWalkerStateMixin, Walker, WalkerState, - AttrsWalkerStateMixin, WalkerStateBox, clone, keep_merge, @@ -23,6 +23,7 @@ MockKeys = Literal["a", "b"] MockDataValue = int | str + # Example of writing a state from scratch class MockData(TypedDict): a: int @@ -43,6 +44,7 @@ def __getitem__(self, key: MockKeys) -> MockDataValue: def dict(self) -> MockData: return attrs.asdict(self) + # example of using the Attrs mixin to write those methods for you @attrs.define class MockWalkerStateMixin(AttrsWalkerStateMixin, WalkerState): @@ -72,6 +74,7 @@ def test_dict(self): "b": "hello", } + class Test_AttrsWalkerStateMixin: def test___init__(self): @@ -85,8 +88,12 @@ def test___getitem__(self): def test___eq__(self): - assert MockWalkerStateMixin(a=1, b="hello") == MockWalkerStateMixin(a=1, b="hello") - assert MockWalkerStateMixin(a=1, b="hello") != MockWalkerStateMixin(a=100, b="hello") + assert MockWalkerStateMixin(a=1, b="hello") == MockWalkerStateMixin( + a=1, b="hello" + ) + assert MockWalkerStateMixin(a=1, b="hello") != MockWalkerStateMixin( + a=100, b="hello" + ) def test_dict(self): assert MockWalkerStateMixin(a=1, b="hello").dict() == { @@ -94,19 +101,20 @@ def test_dict(self): "b": "hello", } + class Test_WalkerStateBox: def test___init__(self): - assert WalkerStateBox(a=1)._data == {"a" : 1} + assert WalkerStateBox(a=1)._data == {"a": 1} def test___getitem__(self): assert WalkerStateBox(a=1)["a"] == 1 def test_dict(self): - - assert WalkerStateBox(a=1).dict() == {"a" : 1} + + assert WalkerStateBox(a=1).dict() == {"a": 1} def test___eq__(self): @@ -114,7 +122,8 @@ def test___eq__(self): assert s == s assert WalkerStateBox(a=1) == WalkerStateBox(a=1) - + + class TestWalker: def test___init__(self): From 728cfd5aaa3334b4745173678f44a6cf7bdf0aec Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 26 Jan 2026 13:43:35 -0500 Subject: [PATCH 141/143] Include wepy_tools in wepy package modules --- pyproject.toml | 6 ++++++ src/wepy/__init__.py | 1 + 2 files changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6f758eb5..1ee6b5e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,12 @@ Issues = "https://github.com/ADicksonLab/wepy/issues" requires = ["uv_build>=0.9.11,<0.10.0"] build-backend = "uv_build" +[tool.uv.build] +packages = [ + "src/wepy", + "src/wepy_tools", +] + [dependency-groups] dev = [ diff --git a/src/wepy/__init__.py b/src/wepy/__init__.py index 1e1390df..31587686 100644 --- a/src/wepy/__init__.py +++ b/src/wepy/__init__.py @@ -75,6 +75,7 @@ from .runners.openmm.runner import ( OpenMMRunner, OpenMMRunnerFactory, + DEFAULT_OPENMM_REPORTER_FACTORIES, ) from .runners.openmm.state import ( OPENMM_DEFAULT_UNITS, From 22bb20b8523298378b9829ef2215270344f14bd2 Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Mon, 26 Jan 2026 14:22:42 -0500 Subject: [PATCH 142/143] fixup! fix module inclusion --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1ee6b5e6..4b2aaee3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,10 +72,10 @@ Issues = "https://github.com/ADicksonLab/wepy/issues" requires = ["uv_build>=0.9.11,<0.10.0"] build-backend = "uv_build" -[tool.uv.build] -packages = [ - "src/wepy", - "src/wepy_tools", +[tool.uv.build-backend] +module-name = [ + "wepy", + "wepy_tools", ] [dependency-groups] From c44070ab5cde00552533675b73b09f84cce8109c Mon Sep 17 00:00:00 2001 From: Samuel Lotz Date: Fri, 17 Apr 2026 09:57:30 -0400 Subject: [PATCH 143/143] proper type signature for alt_reps in reporter --- src/wepy/reporter/hdf5.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wepy/reporter/hdf5.py b/src/wepy/reporter/hdf5.py index e1a63483..558980ab 100644 --- a/src/wepy/reporter/hdf5.py +++ b/src/wepy/reporter/hdf5.py @@ -132,7 +132,7 @@ def __init__( n_dims: int = 3, main_rep_idxs: Idxs | None = None, all_atoms_rep_freq: int | None = None, - alt_reps: dict[str, tuple[Idxs, int]] | None = None, + alt_reps: dict[str, tuple[Idxs, int | Literal[Ellipsis]]] | None = None, # TOREV: are these feature fields actually needed for the main # trajectories? I never used them. If they are useful they # should be derived from runner metadata in the common case. I @@ -194,8 +194,9 @@ def __init__( named by the keys of this mapping and containing the indices in each value list as the first value of the tuple and the second value being the frequency at which this - field gets saved. Setting `all_atoms_rep_freq` is the - equivalent of setting an entry {'all_atoms' : ([...], + field gets saved. If frequency is 0, 1, or Ellipsis then + all frames will be saved. Setting `all_atoms_rep_freq` is + the equivalent of setting an entry {'all_atoms' : ([...], `all_atoms_rep_freq`)}. main_rep_idxs : list of int, optional