From 46282daa2d734f81e879f250613bfdaf5e0ad879 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 11 Oct 2023 21:16:27 -0700 Subject: [PATCH 001/143] Use `pydantic` > 1 Try un-pinning `pydantic` and allowing it to be pinned by other packages as needed. --- devtools/conda-envs/alchemiscale-client.yml | 2 +- devtools/conda-envs/alchemiscale-compute.yml | 2 +- devtools/conda-envs/alchemiscale-server.yml | 2 +- devtools/conda-envs/test.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/devtools/conda-envs/alchemiscale-client.yml b/devtools/conda-envs/alchemiscale-client.yml index c3abc819..fd8fa25a 100644 --- a/devtools/conda-envs/alchemiscale-client.yml +++ b/devtools/conda-envs/alchemiscale-client.yml @@ -13,7 +13,7 @@ dependencies: - requests - click - httpx - - pydantic<2.0 + - pydantic >1 ## user client printing - rich diff --git a/devtools/conda-envs/alchemiscale-compute.yml b/devtools/conda-envs/alchemiscale-compute.yml index d24f4e1c..38667b3a 100644 --- a/devtools/conda-envs/alchemiscale-compute.yml +++ b/devtools/conda-envs/alchemiscale-compute.yml @@ -13,7 +13,7 @@ dependencies: - requests - click - httpx - - pydantic<2.0 + - pydantic >1 # perses dependencies - openeye-toolkits diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index b0bd1534..9ae5bb02 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -12,7 +12,7 @@ dependencies: - openfe=0.13.0 - requests - click - - pydantic<2.0 + - pydantic >1 ## state store - py2neo diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index 8d09243e..0b1b3a9f 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -10,7 +10,7 @@ dependencies: - numpy<1.24 # https://github.com/numba/numba/issues/8615#issuecomment-1360792615 - networkx - rdkit - - pydantic<2.0 + - pydantic >1 - openff-toolkit - openff-units >=0.2.0 - openff-models >=0.0.4 From 83c97f5042934a39354abcf070123c5c98067b43 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 18 Oct 2023 21:24:05 -0700 Subject: [PATCH 002/143] Updates to deprecated pydantic decorators --- alchemiscale/models.py | 12 ++++++------ alchemiscale/security/models.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index ebbe1dbe..ea3da70d 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -4,7 +4,7 @@ """ from typing import Optional, Union -from pydantic import BaseModel, Field, validator, root_validator +from pydantic import BaseModel, Field, field_validator, model_validator from gufe.tokenization import GufeKey from re import fullmatch @@ -60,19 +60,19 @@ def _validate_component(v, component): return v - @validator("org") + @field_validator("org") def valid_org(cls, v): return cls._validate_component(v, "org") - @validator("campaign") + @field_validator("campaign") def valid_campaign(cls, v): return cls._validate_component(v, "campaign") - @validator("project") + @field_validator("project") def valid_project(cls, v): return cls._validate_component(v, "project") - @root_validator + @model_validator def check_scope_hierarchy(cls, values): if not _hierarchy_valid(values): raise InvalidScopeError( @@ -129,7 +129,7 @@ class ScopedKey(BaseModel): class Config: frozen = True - @validator("gufe_key") + @field_validator("gufe_key") def cast_gufe_key(cls, v): return GufeKey(v) diff --git a/alchemiscale/security/models.py b/alchemiscale/security/models.py index 4f8322d1..f5ab910a 100644 --- a/alchemiscale/security/models.py +++ b/alchemiscale/security/models.py @@ -7,7 +7,7 @@ from datetime import datetime, timedelta from typing import List, Union, Optional -from pydantic import BaseModel, validator +from pydantic import BaseModel, field_validator from ..models import Scope @@ -32,7 +32,7 @@ class ScopedIdentity(BaseModel): disabled: bool = False scopes: List[str] = [] - @validator("scopes", pre=True, each_item=True) + @field_validator("scopes", pre=True, each_item=True) def cast_scopes_to_str(cls, scope): """Ensure that each scope object is correctly cast to its str representation""" if isinstance(scope, Scope): From 9b7670cae9a7f5c2364eb2161cb4ba2e167d36b7 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 19 Oct 2023 16:46:16 -0700 Subject: [PATCH 003/143] Think this fixes model_validator usage --- alchemiscale/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index ea3da70d..fb1870b2 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -72,7 +72,8 @@ def valid_campaign(cls, v): def valid_project(cls, v): return cls._validate_component(v, "project") - @model_validator + @model_validator(mode='before') + @classmethod def check_scope_hierarchy(cls, values): if not _hierarchy_valid(values): raise InvalidScopeError( From f60979c2bf3980db3567f2432d7b018f10e3af27 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 19 Oct 2023 17:13:49 -0700 Subject: [PATCH 004/143] More pydantic 2 updates --- alchemiscale/models.py | 13 ++++++++----- alchemiscale/settings.py | 8 ++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index fb1870b2..81d384b1 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -4,7 +4,7 @@ """ from typing import Optional, Union -from pydantic import BaseModel, Field, field_validator, model_validator +from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict from gufe.tokenization import GufeKey from re import fullmatch @@ -33,8 +33,9 @@ def __eq__(self, other): return str(self) == str(other) - class Config: - frozen = True + model_config: ConfigDict( + frozen = True, + ) @staticmethod def _validate_component(v, component): @@ -127,8 +128,10 @@ class ScopedKey(BaseModel): campaign: str project: str - class Config: - frozen = True + model_config: ConfigDict( + frozen = True, + arbitrary_types_allowed = True + ) @field_validator("gufe_key") def cast_gufe_key(cls, v): diff --git a/alchemiscale/settings.py b/alchemiscale/settings.py index c670a0ba..132767b3 100644 --- a/alchemiscale/settings.py +++ b/alchemiscale/settings.py @@ -7,13 +7,13 @@ from functools import lru_cache from typing import Optional -from pydantic import BaseSettings +from pydantic import BaseSettings, ConfigDict class FrozenSettings(BaseSettings): - class Config: - frozen = True - + model_config: ConfigDict( + frozen = True, + ) class Neo4jStoreSettings(FrozenSettings): """Automatically populates settings from environment variables where they From 6eb5af9d1a694cedd395382afc26255b8a7b6b92 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 19 Oct 2023 17:16:55 -0700 Subject: [PATCH 005/143] Black! --- alchemiscale/models.py | 11 ++++------- alchemiscale/settings.py | 5 +++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index 81d384b1..3e256c92 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -34,8 +34,8 @@ def __eq__(self, other): return str(self) == str(other) model_config: ConfigDict( - frozen = True, - ) + frozen=True, + ) @staticmethod def _validate_component(v, component): @@ -73,7 +73,7 @@ def valid_campaign(cls, v): def valid_project(cls, v): return cls._validate_component(v, "project") - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def check_scope_hierarchy(cls, values): if not _hierarchy_valid(values): @@ -128,10 +128,7 @@ class ScopedKey(BaseModel): campaign: str project: str - model_config: ConfigDict( - frozen = True, - arbitrary_types_allowed = True - ) + model_config: ConfigDict(frozen=True, arbitrary_types_allowed=True) @field_validator("gufe_key") def cast_gufe_key(cls, v): diff --git a/alchemiscale/settings.py b/alchemiscale/settings.py index 132767b3..296fe9ea 100644 --- a/alchemiscale/settings.py +++ b/alchemiscale/settings.py @@ -12,8 +12,9 @@ class FrozenSettings(BaseSettings): model_config: ConfigDict( - frozen = True, - ) + frozen=True, + ) + class Neo4jStoreSettings(FrozenSettings): """Automatically populates settings from environment variables where they From c725fb3ec81c838e4a7ea2ecebec9cffdb111fb7 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 19 Oct 2023 21:29:39 -0700 Subject: [PATCH 006/143] More pydantic updates... --- alchemiscale/models.py | 4 ++-- alchemiscale/settings.py | 2 +- alchemiscale/storage/models.py | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index 3e256c92..871c4d2e 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -33,7 +33,7 @@ def __eq__(self, other): return str(self) == str(other) - model_config: ConfigDict( + model_config = ConfigDict( frozen=True, ) @@ -128,7 +128,7 @@ class ScopedKey(BaseModel): campaign: str project: str - model_config: ConfigDict(frozen=True, arbitrary_types_allowed=True) + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) @field_validator("gufe_key") def cast_gufe_key(cls, v): diff --git a/alchemiscale/settings.py b/alchemiscale/settings.py index 296fe9ea..325e0011 100644 --- a/alchemiscale/settings.py +++ b/alchemiscale/settings.py @@ -11,7 +11,7 @@ class FrozenSettings(BaseSettings): - model_config: ConfigDict( + model_config = ConfigDict( frozen=True, ) diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index 40c7af1f..fdbba768 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -12,7 +12,7 @@ import hashlib -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict from gufe.tokenization import GufeTokenizable, GufeKey from ..models import ScopedKey, Scope @@ -29,6 +29,8 @@ class ComputeServiceRegistration(BaseModel): registered: datetime heartbeat: datetime + model_config = ConfigDict(arbitrary_types_allowed=True) + def __repr__(self): # pragma: no cover return f"" @@ -59,6 +61,8 @@ class TaskProvenance(BaseModel): datetime_start: datetime datetime_end: datetime + model_config = ConfigDict(arbitrary_types_allowed=True) + # this should include versions of various libraries From a7801b891a301e108dbdd54bfd8df3f1f424a973 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 19 Oct 2023 22:06:03 -0700 Subject: [PATCH 007/143] Think I finally have tests working again --- alchemiscale/models.py | 1 - alchemiscale/security/models.py | 24 +++++++++++--------- alchemiscale/settings.py | 4 ++-- devtools/conda-envs/alchemiscale-client.yml | 1 + devtools/conda-envs/alchemiscale-compute.yml | 1 + devtools/conda-envs/alchemiscale-server.yml | 1 + devtools/conda-envs/test.yml | 1 + 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index 871c4d2e..6619f824 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -74,7 +74,6 @@ def valid_project(cls, v): return cls._validate_component(v, "project") @model_validator(mode="before") - @classmethod def check_scope_hierarchy(cls, values): if not _hierarchy_valid(values): raise InvalidScopeError( diff --git a/alchemiscale/security/models.py b/alchemiscale/security/models.py index f5ab910a..6b2fbdc4 100644 --- a/alchemiscale/security/models.py +++ b/alchemiscale/security/models.py @@ -32,20 +32,22 @@ class ScopedIdentity(BaseModel): disabled: bool = False scopes: List[str] = [] - @field_validator("scopes", pre=True, each_item=True) - def cast_scopes_to_str(cls, scope): + @field_validator("scopes") + def cast_scopes_to_str(cls, scopes): """Ensure that each scope object is correctly cast to its str representation""" - if isinstance(scope, Scope): - scope = str(scope) - elif isinstance(scope, str): - try: - Scope.from_str(scope) - except: + scopes_ = [] + for scope in scopes: + if isinstance(scope, Scope): + scopes_.append(str(scope)) + elif isinstance(scope, str): + try: + scopes_.append(Scope.from_str(scope)) + except: + raise ValueError(f"Invalid scope `{scope}` set for `{cls}`") + else: raise ValueError(f"Invalid scope `{scope}` set for `{cls}`") - else: - raise ValueError(f"Invalid scope `{scope}` set for `{cls}`") - return scope + return scopes_ class UserIdentity(ScopedIdentity): diff --git a/alchemiscale/settings.py b/alchemiscale/settings.py index 325e0011..f0974d19 100644 --- a/alchemiscale/settings.py +++ b/alchemiscale/settings.py @@ -7,11 +7,11 @@ from functools import lru_cache from typing import Optional -from pydantic import BaseSettings, ConfigDict +from pydantic_settings import BaseSettings, SettingsConfigDict class FrozenSettings(BaseSettings): - model_config = ConfigDict( + model_config = SettingsConfigDict( frozen=True, ) diff --git a/devtools/conda-envs/alchemiscale-client.yml b/devtools/conda-envs/alchemiscale-client.yml index 799fb643..8decd8e0 100644 --- a/devtools/conda-envs/alchemiscale-client.yml +++ b/devtools/conda-envs/alchemiscale-client.yml @@ -15,6 +15,7 @@ dependencies: - click - httpx - pydantic >1 + - pydantic-settings ## user client printing - rich diff --git a/devtools/conda-envs/alchemiscale-compute.yml b/devtools/conda-envs/alchemiscale-compute.yml index 974af429..0b245d1e 100644 --- a/devtools/conda-envs/alchemiscale-compute.yml +++ b/devtools/conda-envs/alchemiscale-compute.yml @@ -15,6 +15,7 @@ dependencies: - click - httpx - pydantic >1 + - pydantic-settings # perses dependencies - openeye-toolkits diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index c6992805..ab03c7f5 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -14,6 +14,7 @@ dependencies: - requests - click - pydantic >1 + - pydantic-settings ## state store - py2neo diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index 5fe19797..163e230a 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -11,6 +11,7 @@ dependencies: - networkx - rdkit - pydantic >1 + - pydantic-settings - openff-toolkit - openff-units >=0.2.0 - openff-models >=0.0.4 From 435cc448692e830d73f9deb98c91303f786f77e3 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 19 Oct 2023 22:11:20 -0700 Subject: [PATCH 008/143] Fix broken docs --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 5a7c3348..a5268ba9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,6 +46,7 @@ "passlib", "py2neo", "pydantic", + "pydantic_settings", "starlette", "yaml", ] From 8bac219ad6cc5a68e75ed947ba0f5d9a3d5f4c0b Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 8 Nov 2023 22:27:26 -0700 Subject: [PATCH 009/143] More pydantic fixes; added docstring to authenticate function --- alchemiscale/compute/api.py | 2 +- alchemiscale/models.py | 2 +- alchemiscale/security/auth.py | 25 ++++++++++++++++++++++--- alchemiscale/security/models.py | 4 ++-- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/alchemiscale/compute/api.py b/alchemiscale/compute/api.py index 8679c3a1..50cb1189 100644 --- a/alchemiscale/compute/api.py +++ b/alchemiscale/compute/api.py @@ -107,7 +107,7 @@ def register_computeservice( ): now = datetime.utcnow() csreg = ComputeServiceRegistration( - identifier=compute_service_id, registered=now, heartbeat=now + identifier=ComputeServiceID(compute_service_id), registered=now, heartbeat=now ) compute_service_id_ = n4js.register_computeservice(csreg) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index 6619f824..1427b1a9 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -122,7 +122,7 @@ class ScopedKey(BaseModel): """ - gufe_key: GufeKey + gufe_key: Union[GufeKey, str] org: str campaign: str project: str diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index 16626529..a4dae9c1 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -25,12 +25,31 @@ def generate_secret_key(): return secrets.token_hex(32) -def authenticate(db, cls, identifier: str, key: str) -> CredentialedEntity: +def authenticate(db, cls, identifier: str, key: str) -> Optional[CredentialedEntity]: + """Authenticate the given identity+key against the db instance. + + Parameters + ---------- + db + State store instance featuring a `get_credentialed_entity` method. + cls + The `CredentialedEntity` subclass the identity corresponds to. + identity + String identifier for the the identity. + key + Secret key string for the identity. + + Returns + ------- + If successfully authenticated, returns the `CredentialedEntity` subclass instance. + If not, returns `None`. + + """ entity: CredentialedEntity = db.get_credentialed_entity(identifier, cls) if entity is None: - return False + return None if not pwd_context.verify(key, entity.hashed_key): - return False + return None return entity diff --git a/alchemiscale/security/models.py b/alchemiscale/security/models.py index 6b2fbdc4..85b0feaf 100644 --- a/alchemiscale/security/models.py +++ b/alchemiscale/security/models.py @@ -30,7 +30,7 @@ class CredentialedEntity(BaseModel): class ScopedIdentity(BaseModel): identifier: str disabled: bool = False - scopes: List[str] = [] + scopes: List[Union[Scope, str]] = [] @field_validator("scopes") def cast_scopes_to_str(cls, scopes): @@ -41,7 +41,7 @@ def cast_scopes_to_str(cls, scopes): scopes_.append(str(scope)) elif isinstance(scope, str): try: - scopes_.append(Scope.from_str(scope)) + scopes_.append(str(Scope.from_str(scope))) except: raise ValueError(f"Invalid scope `{scope}` set for `{cls}`") else: From e3d11e6bd27ed293c9fb80ce2a4b29fe0d5e21be Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 8 Nov 2023 22:36:14 -0700 Subject: [PATCH 010/143] Force newer `openff-models` --- devtools/conda-envs/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index e7698593..c6558659 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -14,7 +14,7 @@ dependencies: - pydantic-settings - openff-toolkit - openff-units >=0.2.0 - - openff-models >=0.0.4 + - openff-models >=0.1.1 - openeye-toolkits - typing-extensions From 23f8b759a41bfc7f03d4662d88c2a76c34db0f82 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 8 Nov 2023 22:39:59 -0700 Subject: [PATCH 011/143] Update CI to use latest best practices for micromamba Getting weird solves; trying to address this. --- .github/workflows/ci-integration.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-integration.yml b/.github/workflows/ci-integration.yml index e9c4cacb..e5598fbb 100644 --- a/.github/workflows/ci-integration.yml +++ b/.github/workflows/ci-integration.yml @@ -35,14 +35,10 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: conda-incubator/setup-miniconda@v2 + - name: Install environment + uses: mamba-org/setup-micromamba@v1 with: - auto-update-conda: true - use-mamba: true - python-version: ${{ matrix.python-version }} - miniforge-variant: Mambaforge - environment-file: devtools/conda-envs/test.yml - activate-environment: alchemiscale-test + environment-file: devtools/conda-envs/test.yml - name: Decrypt OpenEye license if: ${{ !github.event.pull_request.head.repo.fork }} From fb32353f5e3c09878a2efb31a1797a5c704ec1d4 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Fri, 17 Nov 2023 12:02:31 -0700 Subject: [PATCH 012/143] Fix CI --- .github/workflows/ci-integration.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-integration.yml b/.github/workflows/ci-integration.yml index e5598fbb..6aa47c09 100644 --- a/.github/workflows/ci-integration.yml +++ b/.github/workflows/ci-integration.yml @@ -39,6 +39,9 @@ jobs: uses: mamba-org/setup-micromamba@v1 with: environment-file: devtools/conda-envs/test.yml + create-args: >- + python=${{ matrix.python-version }} + cache-environment: true - name: Decrypt OpenEye license if: ${{ !github.event.pull_request.head.repo.fork }} @@ -53,8 +56,8 @@ jobs: - name: "Environment Information" run: | - mamba info -a - mamba list + conda info -a + conda list - name: "Run tests" run: | From 0a4ccd8a796e18ef63840e74d9eb58c6a5b1cd07 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 20 Nov 2023 22:54:20 -0700 Subject: [PATCH 013/143] Add pydantic-settings to test env --- devtools/conda-envs/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index e145b187..b49de8df 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -10,6 +10,7 @@ dependencies: - openfe>=0.14.0 - openmmforcefields>=0.12.0 - pydantic >1 + - pydantic-settings ## state store - neo4j-python-driver From 7f752b32bc2fd5b3f9aaa93128a91b30202e3c44 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 16 Jul 2024 08:15:23 -0700 Subject: [PATCH 014/143] Added placeholder tests for proposed methods * Test: test_add_task_restart_policy_patterns * Test: test_get_task_restart_policy_patterns * Test: test_remove_task_restart_policy_patterns * Test: test_clear_task_restart_policy_patterns * Test: test_task_resolve_restarts --- .../interface/client/test_client.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index ceb968f4..c39ce4f8 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -2123,3 +2123,31 @@ def test_get_task_failures( # TODO: can we mix in a success in here somewhere? # not possible with current BrokenProtocol, unfortunately + + # TaskRestartPolicy client methods + + @pytest.mark.xfail(raises=NotImplementedError) + def test_add_task_restart_policy_patterns(self): + raise NotImplementedError + + @pytest.mark.xfail(raises=NotImplementedError) + def test_get_task_restart_policy_patterns(self): + raise NotImplementedError + + @pytest.mark.xfail(raises=NotImplementedError) + def test_remove_task_restart_policy_patterns(self): + raise NotImplementedError + + @pytest.mark.xfail(raises=NotImplementedError) + def test_clear_task_restart_policy_patterns(self): + raise NotImplementedError + + @pytest.mark.xfail(raises=NotImplementedError) + def test_task_resolve_restarts( + self, + scope_test, + n4js_preloaded, + user_client: client.AlchemiscaleClient, + network_tyk2_failure, + ): + raise NotImplementedError From dd8f0e967ebfd313f2a815fd5a8576b8a1e86552 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 16 Jul 2024 08:19:30 -0700 Subject: [PATCH 015/143] Added models for new node types * TaskRestartPattern * TaskRestartPolicy * TaskHistory --- alchemiscale/storage/models.py | 80 +++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index c9b000b8..25a4c3d3 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -8,12 +8,12 @@ from copy import copy from datetime import datetime from enum import Enum -from typing import Union, Dict, Optional +from typing import Union, Optional, List from uuid import uuid4 import hashlib -from pydantic import BaseModel, Field +from pydantic import BaseModel from gufe.tokenization import GufeTokenizable, GufeKey from ..models import ScopedKey, Scope @@ -143,6 +143,82 @@ def _defaults(cls): return super()._defaults() +# TODO: fill in docstrings +class TaskRestartPattern(GufeTokenizable): + """A pattern to compare returned Task tracebacks to. + + Attributes + ---------- + pattern: str + A regular expression pattern that can match to returned tracebacks of errored Tasks. + retry_count: int + The number of times the pattern can trigger a restart for a Task. + """ + + pattern: str + retry_count: int + + def __init__(self, pattern: str): + self.pattern = pattern + + def _gufe_tokenize(self): + return hashlib.md5(self.pattern).hexdigest() + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + return self.pattern == other.pattern + + +# TODO: fill in docstrings +class TaskRestartPolicy(GufeTokenizable): + """Restart policy that enforces a TaskHub. + + Attributes + ---------- + taskhub: str + ScopedKey of the TaskHub this TaskRestartPolicy enforces. + """ + + taskhub: str + + def __init__(self, taskhub: ScopedKey): + self.taskhub = taskhub + + def _gufe_tokenize(self): + return hashlib.md5( + self.__class__.__qualname__ + str(self.taskhub), usedforsecurity=False + ).hexdigest() + + +# TODO: fill in docstrings +class TaskHistory(GufeTokenizable): + """History attached to a `Task`. + + Attributes + ---------- + task: str + ScopedKey of the Task this TaskHistory corresponds to. + tracebacks: List[str] + The history of tracebacks returned with the newest entries appearing at the end of the list. + times_restarted: int + The number of times the task has bee + """ + + task: str + tracebacks: list + times_restarted: int + + def __init__(self, task: ScopedKey, tracebacks: List[str]): + self.task = task + self.tracebacks = tracebacks + + def _gufe_tokenize(self): + return hashlib.md5( + self.__class__.__qualname__ + str(self.task), usedforsecurity=False + ).hexdigest() + + class TaskHub(GufeTokenizable): """ From da17e45913e3bc55498012323cebbe6d4ee2ebd4 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Wed, 17 Jul 2024 14:59:31 -0700 Subject: [PATCH 016/143] Updated new GufeTokenizable models in statestore * Removed TaskRestartPolicy and TaskHistory * Added Traceback --- alchemiscale/storage/models.py | 77 +++++++++++++----------------- alchemiscale/storage/statestore.py | 16 ++++++- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index 25a4c3d3..b9090160 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -151,18 +151,34 @@ class TaskRestartPattern(GufeTokenizable): ---------- pattern: str A regular expression pattern that can match to returned tracebacks of errored Tasks. - retry_count: int + max_retries: int The number of times the pattern can trigger a restart for a Task. """ pattern: str - retry_count: int + max_retries: int - def __init__(self, pattern: str): + def __init__(self, pattern: str, max_retries: int): self.pattern = pattern + if not isinstance(max_retries, int) or max_retries <= 0: + raise ValueError("`max_retries` must have a positive integer value.") + self.max_retries = max_retries + + # TODO: these hashes can overlap across TaskHubs def _gufe_tokenize(self): - return hashlib.md5(self.pattern).hexdigest() + return hashlib.md5(self.pattern.encode()).hexdigest() + + @classmethod + def _defaults(cls): + raise NotImplementedError + + @classmethod + def _from_dict(cls, dct): + return cls(**dct) + + def _to_dict(self): + return {"pattern": self.pattern, "max_retries": self.max_retries} def __eq__(self, other): if not isinstance(other, self.__class__): @@ -170,53 +186,24 @@ def __eq__(self, other): return self.pattern == other.pattern -# TODO: fill in docstrings -class TaskRestartPolicy(GufeTokenizable): - """Restart policy that enforces a TaskHub. - - Attributes - ---------- - taskhub: str - ScopedKey of the TaskHub this TaskRestartPolicy enforces. - """ - - taskhub: str +class Traceback(GufeTokenizable): - def __init__(self, taskhub: ScopedKey): - self.taskhub = taskhub + def __init__(self, tracebacks: List[str]): + self.tracebacks = tracebacks def _gufe_tokenize(self): - return hashlib.md5( - self.__class__.__qualname__ + str(self.taskhub), usedforsecurity=False - ).hexdigest() - + return hashlib.md5(str(self.tracebacks).encode()).hexdigest() -# TODO: fill in docstrings -class TaskHistory(GufeTokenizable): - """History attached to a `Task`. - - Attributes - ---------- - task: str - ScopedKey of the Task this TaskHistory corresponds to. - tracebacks: List[str] - The history of tracebacks returned with the newest entries appearing at the end of the list. - times_restarted: int - The number of times the task has bee - """ - - task: str - tracebacks: list - times_restarted: int + @classmethod + def _defaults(cls): + raise NotImplementedError - def __init__(self, task: ScopedKey, tracebacks: List[str]): - self.task = task - self.tracebacks = tracebacks + @classmethod + def _from_dict(cls, dct): + return Traceback(**dct) - def _gufe_tokenize(self): - return hashlib.md5( - self.__class__.__qualname__ + str(self.task), usedforsecurity=False - ).hexdigest() + def _to_dict(self): + return {"tracebacks": self.tracebacks} class TaskHub(GufeTokenizable): diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 1ffd4f4a..3ec0aa5e 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -24,10 +24,10 @@ ComputeServiceRegistration, NetworkMark, NetworkStateEnum, + ProtocolDAGResultRef, Task, TaskHub, TaskStatusEnum, - ProtocolDAGResultRef, ) from ..strategies import Strategy from ..models import Scope, ScopedKey @@ -2703,6 +2703,20 @@ def err_msg(t, status): return self._set_task_status(tasks, q, err_msg, raise_error=raise_error) + ## task restart policy + + # TODO: fill in docstring + def add_task_restart_policy_patterns( + self, taskhub: ScopedKey, patterns: List[str], number_of_retries: int + ): + """Add a list of restart policy patterns to a `TaskHub` along with the number of retries allowed. + + Parameters + ---------- + + """ + raise NotImplementedError + ## authentication def create_credentialed_entity(self, entity: CredentialedEntity): From b7f63d4909e9e5ee3852a0dc7efa1e29e6327566 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Wed, 17 Jul 2024 15:41:08 -0700 Subject: [PATCH 017/143] Added placeholder unit tests for new models --- .../tests/unit/test_storage_models.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/alchemiscale/tests/unit/test_storage_models.py b/alchemiscale/tests/unit/test_storage_models.py index 36678b9a..68c9b8c7 100644 --- a/alchemiscale/tests/unit/test_storage_models.py +++ b/alchemiscale/tests/unit/test_storage_models.py @@ -38,3 +38,37 @@ def test_suggested_states_message(self): assert len(suggested_states) == len(NetworkStateEnum) for state in suggested_states: NetworkStateEnum(state) + + +class TestTaskRestartPattern(object): + + @pytest.mark.xfail(raises=NotImplementedError) + def test_empty_pattern(self): + raise NotImplementedError + + @pytest.mark.xfail(raises=NotImplementedError) + def test_negative_max_retries(self): + raise NotImplementedError + + @pytest.mark.xfail(raises=NotImplementedError) + def test_non_int_max_retries(self): + raise NotImplementedError + + @pytest.mark.xfail(raises=NotImplementedError) + def test_to_dict(self): + raise NotImplementedError + + @pytest.mark.xfail(raises=NotImplementedError) + def test_from_dict(self): + raise NotImplementedError + + +class TestTraceback(object): + + @pytest.mark.xfail(raises=NotImplementedError) + def test_to_dict(self): + raise NotImplementedError + + @pytest.mark.xfail(raises=NotImplementedError) + def test_from_dict(self): + raise NotImplementedError From 6a167f13cd532b51c0f41a2741a947b5c073946d Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Thu, 18 Jul 2024 13:40:28 -0700 Subject: [PATCH 018/143] Added validation and unit tests for storage models * TaskReturnPattern: Confirm that the input pattern is a string type and that it is not empty. * Traceback: Confirm that the input is a list of strings and that none of them are empty. --- alchemiscale/storage/models.py | 15 +++ .../tests/unit/test_storage_models.py | 117 +++++++++++++++--- 2 files changed, 116 insertions(+), 16 deletions(-) diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index b9090160..fae7af93 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -159,6 +159,10 @@ class TaskRestartPattern(GufeTokenizable): max_retries: int def __init__(self, pattern: str, max_retries: int): + + if not isinstance(pattern, str) or pattern == "": + raise ValueError("`pattern` must be a non-empty string") + self.pattern = pattern if not isinstance(max_retries, int) or max_retries <= 0: @@ -189,6 +193,17 @@ def __eq__(self, other): class Traceback(GufeTokenizable): def __init__(self, tracebacks: List[str]): + value_error = ValueError( + "`tracebacks` must be a non-empty list of string values" + ) + if not isinstance(tracebacks, list) or tracebacks == []: + raise value_error + else: + # in the case where tracebacks is not an iterable, this will raise a TypeError + all_string_values = all([isinstance(value, str) for value in tracebacks]) + if not all_string_values or "" in tracebacks: + raise value_error + self.tracebacks = tracebacks def _gufe_tokenize(self): diff --git a/alchemiscale/tests/unit/test_storage_models.py b/alchemiscale/tests/unit/test_storage_models.py index 68c9b8c7..02fe188e 100644 --- a/alchemiscale/tests/unit/test_storage_models.py +++ b/alchemiscale/tests/unit/test_storage_models.py @@ -1,6 +1,11 @@ import pytest -from alchemiscale.storage.models import NetworkStateEnum, NetworkMark +from alchemiscale.storage.models import ( + NetworkStateEnum, + NetworkMark, + TaskRestartPattern, + Traceback, +) from alchemiscale import ScopedKey @@ -42,33 +47,113 @@ def test_suggested_states_message(self): class TestTaskRestartPattern(object): - @pytest.mark.xfail(raises=NotImplementedError) + pattern_value_error = "`pattern` must be a non-empty string" + max_retries_value_error = "`max_retries` must have a positive integer value." + def test_empty_pattern(self): - raise NotImplementedError + with pytest.raises(ValueError, match=self.pattern_value_error): + _ = TaskRestartPattern("", 3) + + def test_non_string_pattern(self): + with pytest.raises(ValueError, match=self.pattern_value_error): + _ = TaskRestartPattern(None, 3) + + with pytest.raises(ValueError, match=self.pattern_value_error): + _ = TaskRestartPattern([], 3) + + def test_non_positive_max_retries(self): - @pytest.mark.xfail(raises=NotImplementedError) - def test_negative_max_retries(self): - raise NotImplementedError + with pytest.raises(ValueError, match=self.max_retries_value_error): + TaskRestartPattern("Example pattern", 0) + + with pytest.raises(ValueError, match=self.max_retries_value_error): + TaskRestartPattern("Example pattern", -1) - @pytest.mark.xfail(raises=NotImplementedError) def test_non_int_max_retries(self): - raise NotImplementedError + with pytest.raises(ValueError, match=self.max_retries_value_error): + TaskRestartPattern("Example pattern", 4.0) - @pytest.mark.xfail(raises=NotImplementedError) def test_to_dict(self): - raise NotImplementedError + trp = TaskRestartPattern("Example pattern", 3) + dict_trp = trp.to_dict() + + assert len(dict_trp.keys()) == 5 + + assert dict_trp.pop("__qualname__") == "TaskRestartPattern" + assert dict_trp.pop("__module__") == "alchemiscale.storage.models" + + # light test of the version key + try: + dict_trp.pop(":version:") + except KeyError: + raise AssertionError("expected to find :version:") + + expected = {"pattern": "Example pattern", "max_retries": 3} + + assert expected == dict_trp - @pytest.mark.xfail(raises=NotImplementedError) def test_from_dict(self): - raise NotImplementedError + + original_pattern = "Example pattern" + original_max_retries = 3 + + trp_orig = TaskRestartPattern(original_pattern, original_max_retries) + trp_dict = trp_orig.to_dict() + trp_reconstructed: TaskRestartPattern = TaskRestartPattern.from_dict(trp_dict) + + assert trp_reconstructed.pattern == original_pattern + assert trp_reconstructed.max_retries == original_max_retries class TestTraceback(object): - @pytest.mark.xfail(raises=NotImplementedError) + valid_entry = ["traceback1", "traceback2", "traceback3"] + tracebacks_value_error = "`tracebacks` must be a non-empty list of string values" + + def test_empty_string_element(self): + with pytest.raises(ValueError, match=self.tracebacks_value_error): + Traceback(self.valid_entry + [""]) + + def test_non_list_parameter(self): + with pytest.raises(ValueError, match=self.tracebacks_value_error): + Traceback(None) + + with pytest.raises(ValueError, match=self.tracebacks_value_error): + Traceback(100) + + with pytest.raises(ValueError, match=self.tracebacks_value_error): + Traceback("not a list, but still an iterable that yields strings") + + def test_list_non_string_elements(self): + with pytest.raises(ValueError, match=self.tracebacks_value_error): + Traceback(self.valid_entry + [None]) + + def test_empty_list(self): + with pytest.raises(ValueError, match=self.tracebacks_value_error): + Traceback([]) + def test_to_dict(self): - raise NotImplementedError + tb = Traceback(self.valid_entry) + tb_dict = tb.to_dict() + + assert len(tb_dict) == 4 + + assert tb_dict.pop("__qualname__") == "Traceback" + assert tb_dict.pop("__module__") == "alchemiscale.storage.models" + + # light test of the version key + try: + tb_dict.pop(":version:") + except KeyError: + raise AssertionError("expected to find :version:") + + expected = {"tracebacks": self.valid_entry} + + assert expected == tb_dict - @pytest.mark.xfail(raises=NotImplementedError) def test_from_dict(self): - raise NotImplementedError + tb_orig = Traceback(self.valid_entry) + tb_dict = tb_orig.to_dict() + tb_reconstructed: TaskRestartPattern = TaskRestartPattern.from_dict(tb_dict) + + assert tb_reconstructed.tracebacks == self.valid_entry From a10e2355196debf7ba3ebc466040c35193374541 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 22 Jul 2024 14:16:17 -0700 Subject: [PATCH 019/143] Added `taskhub_sk` to `TaskRestartPattern` Similar to `TaskHub`s, the `TaskRestartPattern` needs additonal hashed data to uniquely identify it as a Neo4j node (via the gufe key). The unit tests have been updated to reflect this change. --- alchemiscale/storage/models.py | 20 ++++++-- .../tests/unit/test_storage_models.py | 50 +++++++++++++++---- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index fae7af93..3dc69e0d 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -153,12 +153,17 @@ class TaskRestartPattern(GufeTokenizable): A regular expression pattern that can match to returned tracebacks of errored Tasks. max_retries: int The number of times the pattern can trigger a restart for a Task. + taskhub_sk: str + The TaskHub the pattern is bound to. This is needed to properly set a unique Gufe key. """ pattern: str max_retries: int + taskhub_sk: str - def __init__(self, pattern: str, max_retries: int): + def __init__( + self, pattern: str, max_retries: int, taskhub_scoped_key: Union[str, ScopedKey] + ): if not isinstance(pattern, str) or pattern == "": raise ValueError("`pattern` must be a non-empty string") @@ -169,9 +174,11 @@ def __init__(self, pattern: str, max_retries: int): raise ValueError("`max_retries` must have a positive integer value.") self.max_retries = max_retries - # TODO: these hashes can overlap across TaskHubs + self.taskhub_scoped_key = str(taskhub_scoped_key) + def _gufe_tokenize(self): - return hashlib.md5(self.pattern.encode()).hexdigest() + key_string = self.pattern + self.taskhub_scoped_key + return hashlib.md5(key_string.encode()).hexdigest() @classmethod def _defaults(cls): @@ -182,8 +189,13 @@ def _from_dict(cls, dct): return cls(**dct) def _to_dict(self): - return {"pattern": self.pattern, "max_retries": self.max_retries} + return { + "pattern": self.pattern, + "max_retries": self.max_retries, + "taskhub_scoped_key": self.taskhub_scoped_key, + } + # TODO: should this also compare taskhub scoped keys? def __eq__(self, other): if not isinstance(other, self.__class__): return False diff --git a/alchemiscale/tests/unit/test_storage_models.py b/alchemiscale/tests/unit/test_storage_models.py index 02fe188e..55dc872f 100644 --- a/alchemiscale/tests/unit/test_storage_models.py +++ b/alchemiscale/tests/unit/test_storage_models.py @@ -52,35 +52,61 @@ class TestTaskRestartPattern(object): def test_empty_pattern(self): with pytest.raises(ValueError, match=self.pattern_value_error): - _ = TaskRestartPattern("", 3) + _ = TaskRestartPattern( + "", 3, "FakeScopedKey-1234-fake_org-fake_campaign-fake_project" + ) def test_non_string_pattern(self): with pytest.raises(ValueError, match=self.pattern_value_error): - _ = TaskRestartPattern(None, 3) + _ = TaskRestartPattern( + None, 3, "FakeScopedKey-1234-fake_org-fake_campaign-fake_project" + ) with pytest.raises(ValueError, match=self.pattern_value_error): - _ = TaskRestartPattern([], 3) + _ = TaskRestartPattern( + [], 3, "FakeScopedKey-1234-fake_org-fake_campaign-fake_project" + ) def test_non_positive_max_retries(self): with pytest.raises(ValueError, match=self.max_retries_value_error): - TaskRestartPattern("Example pattern", 0) + TaskRestartPattern( + "Example pattern", + 0, + "FakeScopedKey-1234-fake_org-fake_campaign-fake_project", + ) with pytest.raises(ValueError, match=self.max_retries_value_error): - TaskRestartPattern("Example pattern", -1) + TaskRestartPattern( + "Example pattern", + -1, + "FakeScopedKey-1234-fake_org-fake_campaign-fake_project", + ) def test_non_int_max_retries(self): with pytest.raises(ValueError, match=self.max_retries_value_error): - TaskRestartPattern("Example pattern", 4.0) + TaskRestartPattern( + "Example pattern", + 4.0, + "FakeScopedKey-1234-fake_org-fake_campaign-fake_project", + ) def test_to_dict(self): - trp = TaskRestartPattern("Example pattern", 3) + trp = TaskRestartPattern( + "Example pattern", + 3, + "FakeScopedKey-1234-fake_org-fake_campaign-fake_project", + ) dict_trp = trp.to_dict() - assert len(dict_trp.keys()) == 5 + assert len(dict_trp.keys()) == 6 assert dict_trp.pop("__qualname__") == "TaskRestartPattern" assert dict_trp.pop("__module__") == "alchemiscale.storage.models" + assert ( + dict_trp.pop("taskhub_scoped_key") + == "FakeScopedKey-1234-fake_org-fake_campaign-fake_project" + ) # light test of the version key try: @@ -96,13 +122,19 @@ def test_from_dict(self): original_pattern = "Example pattern" original_max_retries = 3 + original_taskhub_scoped_key = ( + "FakeScopedKey-1234-fake_org-fake_campaign-fake_project" + ) - trp_orig = TaskRestartPattern(original_pattern, original_max_retries) + trp_orig = TaskRestartPattern( + original_pattern, original_max_retries, original_taskhub_scoped_key + ) trp_dict = trp_orig.to_dict() trp_reconstructed: TaskRestartPattern = TaskRestartPattern.from_dict(trp_dict) assert trp_reconstructed.pattern == original_pattern assert trp_reconstructed.max_retries == original_max_retries + assert trp_reconstructed.taskhub_scoped_key == original_taskhub_scoped_key class TestTraceback(object): From b99d8ef3a2f83fd68b3e756d59f6fd6b2db92345 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 22 Jul 2024 14:30:07 -0700 Subject: [PATCH 020/143] Added `statestore` methods for restart patterns `statestore` methods have been added to modify the database state: * add_task_restart_patterns * remove_task_restart_patterns * get_task_restart_patterns Tests were added for each method in the integration tests for the statestore. --- alchemiscale/storage/statestore.py | 84 +++++++++- .../integration/storage/test_statestore.py | 158 ++++++++++++++++++ 2 files changed, 240 insertions(+), 2 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 3ec0aa5e..74390bdf 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -27,6 +27,7 @@ ProtocolDAGResultRef, Task, TaskHub, + TaskRestartPattern, TaskStatusEnum, ) from ..strategies import Strategy @@ -2706,7 +2707,7 @@ def err_msg(t, status): ## task restart policy # TODO: fill in docstring - def add_task_restart_policy_patterns( + def add_task_restart_patterns( self, taskhub: ScopedKey, patterns: List[str], number_of_retries: int ): """Add a list of restart policy patterns to a `TaskHub` along with the number of retries allowed. @@ -2714,8 +2715,87 @@ def add_task_restart_policy_patterns( Parameters ---------- + + Raises + ------ """ - raise NotImplementedError + + # get taskhub node + q = """ + MATCH (th:TaskHub {`_scoped_key`: $taskhub}) + RETURN th + """ + results = self.execute_query(q, taskhub=str(taskhub)) + ## raise error if taskhub not found + + if not results.records: + raise KeyError("No such TaskHub in the database") + + record_data = results.records[0]["th"] + taskhub_node = record_data_to_node(record_data) + scope = taskhub.scope + + subgraph = Subgraph() + + for pattern in patterns: + task_restart_pattern = TaskRestartPattern( + pattern, + max_retries=number_of_retries, + taskhub_scoped_key=str(taskhub), + ) + + _, task_restart_policy_node, scoped_key = self._gufe_to_subgraph( + task_restart_pattern.to_shallow_dict(), + labels=["GufeTokenizable", task_restart_pattern.__class__.__name__], + gufe_key=task_restart_pattern.key, + scope=scope, + ) + + subgraph |= Relationship.type("ENFORCES")( + task_restart_policy_node, + taskhub_node, + _org=scope.org, + _campaign=scope.campaign, + _project=scope.project, + ) + + with self.transaction() as tx: + merge_subgraph(tx, subgraph, "GufeTokenizable", "_scoped_key") + + # TODO: fill in docstring + def remove_task_restart_patterns(self, taskhub: ScopedKey, patterns: List[str]): + q = """ + UNWIND $patterns AS pattern + + MATCH (trp: TaskRestartPattern {pattern: pattern, taskhub_scoped_key: $taskhub_scoped_key}) + + DETACH DELETE trp + """ + + self.execute_query(q, patterns=patterns, taskhub_scoped_key=str(taskhub)) + + # TODO: fill in docstring + def get_task_restart_patterns(self, taskhubs: List[ScopedKey]): + + q = """ + UNWIND $taskhub_scoped_keys as taskhub_scoped_key + MATCH (trp: TaskRestartPattern)-[ENFORCES]->(th: TaskHub {`_scoped_key`: taskhub_scoped_key}) + RETURN th, trp + """ + + records = self.execute_query( + q, taskhub_scoped_keys=list(map(str, taskhubs)) + ).records + + data = {taskhub: set() for taskhub in taskhubs} + + for record in records: + pattern = record["trp"]["pattern"] + max_retries = record["trp"]["max_retries"] + taskhub_sk = ScopedKey.from_str(record["th"]["_scoped_key"]) + data[taskhub_sk].add((pattern, max_retries)) + + return dict(data) ## authentication diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 2632524b..1b96dfda 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -3,6 +3,7 @@ from typing import List, Dict from pathlib import Path from itertools import chain +from collections import defaultdict import pytest from gufe import AlchemicalNetwork @@ -1851,6 +1852,163 @@ def test_get_task_failures( assert pdr_ref_sk in failure_pdr_ref_sks assert pdr_ref2_sk in failure_pdr_ref_sks + ### task restart policies + + def test_add_task_restart_patterns(self, n4js, network_tyk2, scope_test): + # create three new alchemical networks (and taskhubs) + taskhub_sks = [] + for network_index in range(3): + an = network_tyk2.copy_with_replacements( + name=network_tyk2.name + + f"_test_add_task_restart_patterns_{network_index}" + ) + _, taskhub_scoped_key, _ = n4js.assemble_network(an, scope_test) + taskhub_sks.append(taskhub_scoped_key) + + # test a shared pattern with and without shared number of restarts + # this will create 6 unique patterns + for network_index in range(3): + taskhub_scoped_key = taskhub_sks[network_index] + n4js.add_task_restart_patterns( + taskhub_scoped_key, ["shared_pattern_and_restarts.+"], 5 + ) + n4js.add_task_restart_patterns( + taskhub_scoped_key, + ["shared_pattern_and_different_restarts.+"], + network_index + 1, + ) + + q = """UNWIND $taskhub_sks AS taskhub_sk + MATCH (trp: TaskRestartPattern)-[ENFORCES]->(th: TaskHub {`_scoped_key`: taskhub_sk}) RETURN trp, th + """ + + taskhub_sks = list(map(str, taskhub_sks)) + records = n4js.execute_query(q, taskhub_sks=taskhub_sks).records + + assert len(records) == 6 + + taskhub_scoped_key_set = set() + taskrestartpattern_scoped_key_set = set() + + for record in records: + taskhub_scoped_key = ScopedKey.from_str(record["th"]["_scoped_key"]) + taskrestartpattern_scoped_key = ScopedKey.from_str( + record["trp"]["_scoped_key"] + ) + + taskhub_scoped_key_set.add(taskhub_scoped_key) + taskrestartpattern_scoped_key_set.add(taskrestartpattern_scoped_key) + + assert len(taskhub_scoped_key_set) == 3 + assert len(taskrestartpattern_scoped_key_set) == 6 + + def test_remove_task_restart_patterns(self, n4js, network_tyk2, scope_test): + + # collect what we expect `get_task_restart_patterns` to return + expected_results = defaultdict(set) + + # create three new alchemical networks (and taskhubs) + taskhub_sks = [] + for network_index in range(3): + an = network_tyk2.copy_with_replacements( + name=network_tyk2.name + + f"_test_remove_task_restart_patterns_{network_index}" + ) + _, taskhub_scoped_key, _ = n4js.assemble_network(an, scope_test) + taskhub_sks.append(taskhub_scoped_key) + + # test a shared pattern with and without shared number of restarts + # this will create 6 unique patterns + for network_index in range(3): + taskhub_scoped_key = taskhub_sks[network_index] + n4js.add_task_restart_patterns( + taskhub_scoped_key, ["shared_pattern_and_restarts.+"], 5 + ) + expected_results[taskhub_scoped_key].add( + ("shared_pattern_and_restarts.+", 5) + ) + + n4js.add_task_restart_patterns( + taskhub_scoped_key, + ["shared_pattern_and_different_restarts.+"], + network_index + 1, + ) + expected_results[taskhub_scoped_key].add( + ("shared_pattern_and_different_restarts.+", network_index + 1) + ) + + # remove both patterns enforcing the first taskhub at the same time, two patterns + target_taskhub = taskhub_sks[0] + target_patterns = [] + + for pattern, _ in expected_results[target_taskhub]: + target_patterns.append(pattern) + + expected_results[target_taskhub].clear() + + n4js.remove_task_restart_patterns(target_taskhub, target_patterns) + assert expected_results == n4js.get_task_restart_patterns(taskhub_sks) + + # remove both patterns enforcing the second taskhub one at a time, two patterns + target_taskhub = taskhub_sks[1] + # pointer to underlying set, pops will update comparison data structure + target_patterns = expected_results[target_taskhub] + + pattern, _ = target_patterns.pop() + n4js.remove_task_restart_patterns(target_taskhub, [pattern]) + assert expected_results == n4js.get_task_restart_patterns(taskhub_sks) + + pattern, _ = target_patterns.pop() + n4js.remove_task_restart_patterns(target_taskhub, [pattern]) + assert expected_results == n4js.get_task_restart_patterns(taskhub_sks) + + def test_get_task_restart_patterns(self, n4js, network_tyk2, scope_test): + # create three new alchemical networks (and taskhubs) + taskhub_sks = [] + for network_index in range(3): + an = network_tyk2.copy_with_replacements( + name=network_tyk2.name + + f"_test_add_task_restart_patterns_{network_index}" + ) + _, taskhub_scoped_key, _ = n4js.assemble_network(an, scope_test) + taskhub_sks.append(taskhub_scoped_key) + + expected_results = defaultdict(set) + # test a shared pattern with and without shared number of restarts + # this will create 6 unique patterns + for network_index in range(3): + taskhub_scoped_key = taskhub_sks[network_index] + n4js.add_task_restart_patterns( + taskhub_scoped_key, ["shared_pattern_and_restarts.+"], 5 + ) + expected_results[taskhub_scoped_key].add( + ("shared_pattern_and_restarts.+", 5) + ) + n4js.add_task_restart_patterns( + taskhub_scoped_key, + ["shared_pattern_and_different_restarts.+"], + network_index + 1, + ) + expected_results[taskhub_scoped_key].add( + ("shared_pattern_and_different_restarts.+", network_index + 1) + ) + + taskhub_grouped_patterns = n4js.get_task_restart_patterns(taskhub_sks) + + assert taskhub_grouped_patterns == expected_results + + @pytest.mark.xfail(raises=NotImplementedError) + def test_task_actioning_applies_relationship(self): + raise NotImplementedError + + @pytest.mark.xfail(raises=NotImplementedError) + def test_add_restart_applies_relationship(self): + raise NotImplementedError + + @pytest.mark.xfail(raises=NotImplementedError) + def test_task_deaction_applies_relationship(self): + raise NotImplementedError + ### authentication @pytest.mark.parametrize( From 39f986888909d19c9dd0c999a72650ae6a8b01fd Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Wed, 24 Jul 2024 17:06:42 -0700 Subject: [PATCH 021/143] Added APPLIES relationship when adding pattern The `add_task_restart_patterns` method now establishes the APPLIES relationship between the each new pattern and all Tasks ACTIONED on the corresponding TaskHub. Added testing for creation of the APPLIES relationship, asserting the number of created connections over multiple TaskHubs and Tasks. Further subdivided the test classes. Additionally added a `set_task_restart_patterns_max_retries` method for updating the max_retries of a TaskRestartPattern. --- alchemiscale/storage/statestore.py | 95 ++++-- .../integration/storage/test_statestore.py | 320 +++++++++++------- 2 files changed, 272 insertions(+), 143 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 74390bdf..598bf2e6 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -9,7 +9,7 @@ from contextlib import contextmanager import json from functools import lru_cache -from typing import Dict, List, Optional, Union, Tuple +from typing import Dict, List, Optional, Union, Tuple, Set import weakref import numpy as np @@ -2735,31 +2735,56 @@ def add_task_restart_patterns( taskhub_node = record_data_to_node(record_data) scope = taskhub.scope - subgraph = Subgraph() + with self.transaction() as tx: + actioned_tasks_query = """ + MATCH (taskhub: TaskHub {`_scoped_key`: $taskhub_scoped_key})-[:ACTIONS]->(task: Task) + RETURN task + """ - for pattern in patterns: - task_restart_pattern = TaskRestartPattern( - pattern, - max_retries=number_of_retries, - taskhub_scoped_key=str(taskhub), - ) + subgraph = Subgraph() - _, task_restart_policy_node, scoped_key = self._gufe_to_subgraph( - task_restart_pattern.to_shallow_dict(), - labels=["GufeTokenizable", task_restart_pattern.__class__.__name__], - gufe_key=task_restart_pattern.key, - scope=scope, - ) + actioned_task_nodes = [] - subgraph |= Relationship.type("ENFORCES")( - task_restart_policy_node, - taskhub_node, - _org=scope.org, - _campaign=scope.campaign, - _project=scope.project, - ) + for actioned_tasks_record in ( + tx.run(actioned_tasks_query, taskhub_scoped_key=str(taskhub)) + .to_eager_result() + .records + ): + actioned_task_nodes.append( + record_data_to_node(actioned_tasks_record["task"]) + ) - with self.transaction() as tx: + for pattern in patterns: + task_restart_pattern = TaskRestartPattern( + pattern, + max_retries=number_of_retries, + taskhub_scoped_key=str(taskhub), + ) + + _, task_restart_pattern_node, scoped_key = self._gufe_to_subgraph( + task_restart_pattern.to_shallow_dict(), + labels=["GufeTokenizable", task_restart_pattern.__class__.__name__], + gufe_key=task_restart_pattern.key, + scope=scope, + ) + + subgraph |= Relationship.type("ENFORCES")( + task_restart_pattern_node, + taskhub_node, + _org=scope.org, + _campaign=scope.campaign, + _project=scope.project, + ) + + for actioned_task_node in actioned_task_nodes: + subgraph |= Relationship.type("APPLIES")( + task_restart_pattern_node, + actioned_task_node, + num_retries=0, + _org=scope.org, + _campaign=scope.campaign, + _project=scope.project, + ) merge_subgraph(tx, subgraph, "GufeTokenizable", "_scoped_key") # TODO: fill in docstring @@ -2775,7 +2800,29 @@ def remove_task_restart_patterns(self, taskhub: ScopedKey, patterns: List[str]): self.execute_query(q, patterns=patterns, taskhub_scoped_key=str(taskhub)) # TODO: fill in docstring - def get_task_restart_patterns(self, taskhubs: List[ScopedKey]): + def set_task_restart_patterns_max_retries( + self, + taskhub_scoped_key: Union[ScopedKey, str], + patterns: List[str], + max_retries: int, + ): + query = """ + UNWIND $patterns AS pattern + MATCH (trp: TaskRestartPattern {pattern: pattern, taskhub_scoped_key: $taskhub_scoped_key}) + SET trp.max_retries = $max_retries + """ + + self.execute_query( + query, + patterns=patterns, + taskhub_scoped_key=str(taskhub_scoped_key), + max_retries=max_retries, + ) + + # TODO: fill in docstring + def get_task_restart_patterns( + self, taskhubs: List[ScopedKey] + ) -> Dict[ScopedKey, Set[Tuple[str, int]]]: q = """ UNWIND $taskhub_scoped_keys as taskhub_scoped_key @@ -2795,7 +2842,7 @@ def get_task_restart_patterns(self, taskhubs: List[ScopedKey]): taskhub_sk = ScopedKey.from_str(record["th"]["_scoped_key"]) data[taskhub_sk].add((pattern, max_retries)) - return dict(data) + return data ## authentication diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 1b96dfda..fa8f7e0d 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -1854,160 +1854,242 @@ def test_get_task_failures( ### task restart policies - def test_add_task_restart_patterns(self, n4js, network_tyk2, scope_test): - # create three new alchemical networks (and taskhubs) - taskhub_sks = [] - for network_index in range(3): - an = network_tyk2.copy_with_replacements( - name=network_tyk2.name - + f"_test_add_task_restart_patterns_{network_index}" - ) - _, taskhub_scoped_key, _ = n4js.assemble_network(an, scope_test) - taskhub_sks.append(taskhub_scoped_key) + class TestTaskRestartPolicy: + + def test_add_task_restart_patterns(self, n4js, network_tyk2, scope_test): + # create three new alchemical networks (and taskhubs) + taskhub_sks = [] + for network_index in range(3): + an = network_tyk2.copy_with_replacements( + name=network_tyk2.name + + f"_test_add_task_restart_patterns_{network_index}" + ) + _, taskhub_scoped_key, _ = n4js.assemble_network(an, scope_test) + + # don't action tasks on every network, take every other + if network_index % 2 == 0: + transformation = list(an.edges)[0] + transformation_sk = n4js.get_scoped_key(transformation, scope_test) + task_sks = n4js.create_tasks([transformation_sk] * 3) + n4js.action_tasks(task_sks, taskhub_scoped_key) + + taskhub_sks.append(taskhub_scoped_key) + # test a shared pattern with and without shared number of restarts + # this will create 6 unique patterns + for network_index in range(3): + taskhub_scoped_key = taskhub_sks[network_index] + n4js.add_task_restart_patterns( + taskhub_scoped_key, ["shared_pattern_and_restarts.+"], 5 + ) + n4js.add_task_restart_patterns( + taskhub_scoped_key, + ["shared_pattern_and_different_restarts.+"], + network_index + 1, + ) - # test a shared pattern with and without shared number of restarts - # this will create 6 unique patterns - for network_index in range(3): - taskhub_scoped_key = taskhub_sks[network_index] - n4js.add_task_restart_patterns( - taskhub_scoped_key, ["shared_pattern_and_restarts.+"], 5 - ) - n4js.add_task_restart_patterns( - taskhub_scoped_key, - ["shared_pattern_and_different_restarts.+"], - network_index + 1, - ) + q = """UNWIND $taskhub_sks AS taskhub_sk + MATCH (trp: TaskRestartPattern)-[:ENFORCES]->(th: TaskHub {`_scoped_key`: taskhub_sk}) RETURN trp, th + """ - q = """UNWIND $taskhub_sks AS taskhub_sk - MATCH (trp: TaskRestartPattern)-[ENFORCES]->(th: TaskHub {`_scoped_key`: taskhub_sk}) RETURN trp, th - """ + taskhub_sks = list(map(str, taskhub_sks)) + records = n4js.execute_query(q, taskhub_sks=taskhub_sks).records - taskhub_sks = list(map(str, taskhub_sks)) - records = n4js.execute_query(q, taskhub_sks=taskhub_sks).records + assert len(records) == 6 - assert len(records) == 6 + taskhub_scoped_key_set = set() + taskrestartpattern_scoped_key_set = set() - taskhub_scoped_key_set = set() - taskrestartpattern_scoped_key_set = set() + for record in records: + taskhub_scoped_key = ScopedKey.from_str(record["th"]["_scoped_key"]) + taskrestartpattern_scoped_key = ScopedKey.from_str( + record["trp"]["_scoped_key"] + ) - for record in records: - taskhub_scoped_key = ScopedKey.from_str(record["th"]["_scoped_key"]) - taskrestartpattern_scoped_key = ScopedKey.from_str( - record["trp"]["_scoped_key"] - ) + taskhub_scoped_key_set.add(taskhub_scoped_key) + taskrestartpattern_scoped_key_set.add(taskrestartpattern_scoped_key) - taskhub_scoped_key_set.add(taskhub_scoped_key) - taskrestartpattern_scoped_key_set.add(taskrestartpattern_scoped_key) + assert len(taskhub_scoped_key_set) == 3 + assert len(taskrestartpattern_scoped_key_set) == 6 - assert len(taskhub_scoped_key_set) == 3 - assert len(taskrestartpattern_scoped_key_set) == 6 + # check that the applies relationships were correctly added - def test_remove_task_restart_patterns(self, n4js, network_tyk2, scope_test): + ## first check that the number of applies relationships is correct and + ## that the number of retries is zero + applies_query = """ + MATCH (trp: TaskRestartPattern)-[app:APPLIES {num_retries: 0}]->(task: Task)<-[:ACTIONS]-(th: TaskHub) + RETURN th, count(app) AS num_applied + """ - # collect what we expect `get_task_restart_patterns` to return - expected_results = defaultdict(set) + records = n4js.execute_query(applies_query).records - # create three new alchemical networks (and taskhubs) - taskhub_sks = [] - for network_index in range(3): - an = network_tyk2.copy_with_replacements( - name=network_tyk2.name - + f"_test_remove_task_restart_patterns_{network_index}" + ### one record per taskhub, each with six num_applied + assert len(records) == 2 + assert records[0]["num_applied"] == records[1]["num_applied"] == 6 + + applies_nonzero_retries = """ + MATCH (trp: TaskRestartPattern)-[app:APPLIES]->(task: Task)<-[:ACTIONS]-(th: TaskHub) + WHERE app.num_retries <> 0 + RETURN th, count(app) AS num_applied + """ + assert len(n4js.execute_query(applies_nonzero_retries).records) == 0 + + def test_remove_task_restart_patterns(self, n4js, network_tyk2, scope_test): + + # collect what we expect `get_task_restart_patterns` to return + expected_results = defaultdict(set) + + # create three new alchemical networks (and taskhubs) + taskhub_sks = [] + for network_index in range(3): + an = network_tyk2.copy_with_replacements( + name=network_tyk2.name + + f"_test_remove_task_restart_patterns_{network_index}" + ) + _, taskhub_scoped_key, _ = n4js.assemble_network(an, scope_test) + taskhub_sks.append(taskhub_scoped_key) + + # test a shared pattern with and without shared number of restarts + # this will create 6 unique patterns + for network_index in range(3): + taskhub_scoped_key = taskhub_sks[network_index] + n4js.add_task_restart_patterns( + taskhub_scoped_key, ["shared_pattern_and_restarts.+"], 5 + ) + expected_results[taskhub_scoped_key].add( + ("shared_pattern_and_restarts.+", 5) + ) + + n4js.add_task_restart_patterns( + taskhub_scoped_key, + ["shared_pattern_and_different_restarts.+"], + network_index + 1, + ) + expected_results[taskhub_scoped_key].add( + ("shared_pattern_and_different_restarts.+", network_index + 1) + ) + + # remove both patterns enforcing the first taskhub at the same time, two patterns + target_taskhub = taskhub_sks[0] + target_patterns = [] + + for pattern, _ in expected_results[target_taskhub]: + target_patterns.append(pattern) + + expected_results[target_taskhub].clear() + + n4js.remove_task_restart_patterns(target_taskhub, target_patterns) + assert expected_results == n4js.get_task_restart_patterns(taskhub_sks) + + # remove both patterns enforcing the second taskhub one at a time, two patterns + target_taskhub = taskhub_sks[1] + # pointer to underlying set, pops will update comparison data structure + target_patterns = expected_results[target_taskhub] + + pattern, _ = target_patterns.pop() + n4js.remove_task_restart_patterns(target_taskhub, [pattern]) + assert expected_results == n4js.get_task_restart_patterns(taskhub_sks) + + pattern, _ = target_patterns.pop() + n4js.remove_task_restart_patterns(target_taskhub, [pattern]) + assert expected_results == n4js.get_task_restart_patterns(taskhub_sks) + + def test_set_task_restart_patterns_max_retries( + self, n4js, network_tyk2, scope_test + ): + network_name = ( + network_tyk2.name + "_test_set_task_restart_patterns_max_retries" ) + an = network_tyk2.copy_with_replacements(name=network_name) _, taskhub_scoped_key, _ = n4js.assemble_network(an, scope_test) - taskhub_sks.append(taskhub_scoped_key) - # test a shared pattern with and without shared number of restarts - # this will create 6 unique patterns - for network_index in range(3): - taskhub_scoped_key = taskhub_sks[network_index] - n4js.add_task_restart_patterns( - taskhub_scoped_key, ["shared_pattern_and_restarts.+"], 5 - ) - expected_results[taskhub_scoped_key].add( - ("shared_pattern_and_restarts.+", 5) - ) + pattern_data = [("pattern_1", 5), ("pattern_2", 5), ("pattern_3", 5)] n4js.add_task_restart_patterns( taskhub_scoped_key, - ["shared_pattern_and_different_restarts.+"], - network_index + 1, + patterns=[data[0] for data in pattern_data], + number_of_retries=5, ) - expected_results[taskhub_scoped_key].add( - ("shared_pattern_and_different_restarts.+", network_index + 1) + + expected_results = {taskhub_scoped_key: set(pattern_data)} + + assert expected_results == n4js.get_task_restart_patterns( + [taskhub_scoped_key] ) - # remove both patterns enforcing the first taskhub at the same time, two patterns - target_taskhub = taskhub_sks[0] - target_patterns = [] + # reflect changing just one max_retry + new_pattern_1_tuple = ("pattern_1", 1) - for pattern, _ in expected_results[target_taskhub]: - target_patterns.append(pattern) + expected_results[taskhub_scoped_key].remove(pattern_data[0]) + expected_results[taskhub_scoped_key].add(new_pattern_1_tuple) - expected_results[target_taskhub].clear() + n4js.set_task_restart_patterns_max_retries( + taskhub_scoped_key, new_pattern_1_tuple[0], new_pattern_1_tuple[1] + ) - n4js.remove_task_restart_patterns(target_taskhub, target_patterns) - assert expected_results == n4js.get_task_restart_patterns(taskhub_sks) + assert expected_results == n4js.get_task_restart_patterns( + [taskhub_scoped_key] + ) - # remove both patterns enforcing the second taskhub one at a time, two patterns - target_taskhub = taskhub_sks[1] - # pointer to underlying set, pops will update comparison data structure - target_patterns = expected_results[target_taskhub] + # reflect changing more than one at a time + new_pattern_2_tuple = ("pattern_2", 2) + new_pattern_3_tuple = ("pattern_3", 2) - pattern, _ = target_patterns.pop() - n4js.remove_task_restart_patterns(target_taskhub, [pattern]) - assert expected_results == n4js.get_task_restart_patterns(taskhub_sks) + expected_results[taskhub_scoped_key].remove(pattern_data[1]) + expected_results[taskhub_scoped_key].add(new_pattern_2_tuple) - pattern, _ = target_patterns.pop() - n4js.remove_task_restart_patterns(target_taskhub, [pattern]) - assert expected_results == n4js.get_task_restart_patterns(taskhub_sks) + expected_results[taskhub_scoped_key].remove(pattern_data[2]) + expected_results[taskhub_scoped_key].add(new_pattern_3_tuple) - def test_get_task_restart_patterns(self, n4js, network_tyk2, scope_test): - # create three new alchemical networks (and taskhubs) - taskhub_sks = [] - for network_index in range(3): - an = network_tyk2.copy_with_replacements( - name=network_tyk2.name - + f"_test_add_task_restart_patterns_{network_index}" + n4js.set_task_restart_patterns_max_retries( + taskhub_scoped_key, [new_pattern_2_tuple[0], new_pattern_3_tuple[0]], 2 ) - _, taskhub_scoped_key, _ = n4js.assemble_network(an, scope_test) - taskhub_sks.append(taskhub_scoped_key) - expected_results = defaultdict(set) - # test a shared pattern with and without shared number of restarts - # this will create 6 unique patterns - for network_index in range(3): - taskhub_scoped_key = taskhub_sks[network_index] - n4js.add_task_restart_patterns( - taskhub_scoped_key, ["shared_pattern_and_restarts.+"], 5 - ) - expected_results[taskhub_scoped_key].add( - ("shared_pattern_and_restarts.+", 5) - ) - n4js.add_task_restart_patterns( - taskhub_scoped_key, - ["shared_pattern_and_different_restarts.+"], - network_index + 1, - ) - expected_results[taskhub_scoped_key].add( - ("shared_pattern_and_different_restarts.+", network_index + 1) + assert expected_results == n4js.get_task_restart_patterns( + [taskhub_scoped_key] ) - taskhub_grouped_patterns = n4js.get_task_restart_patterns(taskhub_sks) + def test_get_task_restart_patterns(self, n4js, network_tyk2, scope_test): + # create three new alchemical networks (and taskhubs) + taskhub_sks = [] + for network_index in range(3): + an = network_tyk2.copy_with_replacements( + name=network_tyk2.name + + f"_test_add_task_restart_patterns_{network_index}" + ) + _, taskhub_scoped_key, _ = n4js.assemble_network(an, scope_test) + taskhub_sks.append(taskhub_scoped_key) + + expected_results = defaultdict(set) + # test a shared pattern with and without shared number of restarts + # this will create 6 unique patterns + for network_index in range(3): + taskhub_scoped_key = taskhub_sks[network_index] + n4js.add_task_restart_patterns( + taskhub_scoped_key, ["shared_pattern_and_restarts.+"], 5 + ) + expected_results[taskhub_scoped_key].add( + ("shared_pattern_and_restarts.+", 5) + ) + n4js.add_task_restart_patterns( + taskhub_scoped_key, + ["shared_pattern_and_different_restarts.+"], + network_index + 1, + ) + expected_results[taskhub_scoped_key].add( + ("shared_pattern_and_different_restarts.+", network_index + 1) + ) - assert taskhub_grouped_patterns == expected_results + taskhub_grouped_patterns = n4js.get_task_restart_patterns(taskhub_sks) - @pytest.mark.xfail(raises=NotImplementedError) - def test_task_actioning_applies_relationship(self): - raise NotImplementedError + assert taskhub_grouped_patterns == expected_results - @pytest.mark.xfail(raises=NotImplementedError) - def test_add_restart_applies_relationship(self): - raise NotImplementedError + @pytest.mark.xfail(raises=NotImplementedError) + def test_task_actioning_applies_relationship(self): + raise NotImplementedError - @pytest.mark.xfail(raises=NotImplementedError) - def test_task_deaction_applies_relationship(self): - raise NotImplementedError + @pytest.mark.xfail(raises=NotImplementedError) + def test_task_deaction_applies_relationship(self): + raise NotImplementedError ### authentication From 988155f36c227912a615ee9b2241ee172319ef35 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Fri, 26 Jul 2024 12:19:16 -0700 Subject: [PATCH 022/143] Establish APPLIES when actioning a Task "actioning" a Task on a TaskHub with preexisting TaskRestartPatterns created the APPLIES relationship between them with a num_retries value of 0. This behavior is tested in the test_action_task function in the statestore. --- alchemiscale/storage/statestore.py | 39 +++++++++--- .../integration/storage/test_statestore.py | 59 +++++++++++++++++++ 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 598bf2e6..3689b8fd 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -1406,30 +1406,54 @@ def action_tasks( # so we can properly return `None` if needed task_map = {str(task): None for task in tasks} - q = f""" + query_safe_task_list = [str(task) for task in tasks if task] + + q = """ // get our TaskHub - UNWIND {cypher_list_from_scoped_keys(tasks)} AS task_sk - MATCH (th:TaskHub {{_scoped_key: "{taskhub}"}})-[:PERFORMS]->(an:AlchemicalNetwork) + UNWIND $query_safe_task_list AS task_sk + MATCH (th:TaskHub {_scoped_key: $taskhub_scoped_key})-[:PERFORMS]->(an:AlchemicalNetwork) // get the task we want to add to the hub; check that it connects to same network - MATCH (task:Task {{_scoped_key: task_sk}})-[:PERFORMS]->(tf:Transformation|NonTransformation)<-[:DEPENDS_ON]-(an) + MATCH (task:Task {_scoped_key: task_sk})-[:PERFORMS]->(:Transformation|NonTransformation)<-[:DEPENDS_ON]-(an) // only proceed for cases where task is not already actioned on hub // and where the task is either in 'waiting', 'running', or 'error' status WITH th, an, task WHERE NOT (th)-[:ACTIONS]->(task) - AND task.status IN ['{TaskStatusEnum.waiting.value}', '{TaskStatusEnum.running.value}', '{TaskStatusEnum.error.value}'] + AND task.status IN [$waiting, $running, $error] // create the connection - CREATE (th)-[ar:ACTIONS {{weight: 0.5}}]->(task) + CREATE (th)-[ar:ACTIONS {weight: 0.5}]->(task) // set the task property to the scoped key of the Task // this is a convenience for when we have to loop over relationships in Python SET ar.task = task._scoped_key + // we want to preserve the list of tasks for the return, so we need to make a subquery + // since the subsequent WHERE clause could reduce the records in task + WITH task, th + CALL { + WITH task, th + MATCH (trp: TaskRestartPattern)-[:ENFORCES]->(th) + WHERE NOT (trp)-[:APPLIES]->(task) + + CREATE (trp)-[:APPLIES {num_retries: 0, `_campaign`: $campaign, `_org`: $org, `_project`: $project}]->(task) + } + RETURN task """ - results = self.execute_query(q) + + results = self.execute_query( + q, + query_safe_task_list=query_safe_task_list, + waiting=TaskStatusEnum.waiting.value, + running=TaskStatusEnum.running.value, + error=TaskStatusEnum.error.value, + taskhub_scoped_key=str(taskhub), + campaign=taskhub.campaign, + org=taskhub.org, + project=taskhub.project, + ) # update our map with the results, leaving None for tasks that aren't found for task_record in results.records: @@ -1587,6 +1611,7 @@ def cancel_tasks( // get our task hub, as well as the task :ACTIONS relationship we want to remove MATCH (th:TaskHub {{_scoped_key: '{taskhub}'}})-[ar:ACTIONS]->(task:Task {{_scoped_key: '{t}'}}) DELETE ar + RETURN task """ _task = tx.run(q).to_eager_result() diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index fa8f7e0d..f79a1c4a 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -1096,6 +1096,65 @@ def test_action_task(self, n4js: Neo4jStore, network_tyk2, scope_test): task_sks_fail = n4js.action_tasks(task_sks, taskhub_sk2) assert all([i is None for i in task_sks_fail]) + # test for APPLIES relationship between an ACTIONED task and a TaskRestartPattern + + ## create a restart pattern, should already create APPLIES relationships with those + ## already actioned + n4js.add_task_restart_patterns(taskhub_sk, ["test_pattern"], 5) + + query = """ + MATCH (:TaskRestartPattern)-[applies:APPLIES]->(Task)<-[:ACTIONS]-(:TaskHub {`_scoped_key`: $taskhub_scoped_key}) + // change this so that later tests can show the value was not overwritten + SET applies.num_retries = 1 + RETURN count(applies) AS applies_count + """ + + ## sanity check that this number makes sense + applies_count = n4js.execute_query( + query, taskhub_scoped_key=str(taskhub_sk) + ).records[0]["applies_count"] + + assert applies_count == 10 + + # create 10 more tasks and action them + task_sks = n4js.create_tasks([transformation_sk] * 10) + n4js.action_tasks(task_sks, taskhub_sk) + + assert len(n4js.get_taskhub_actioned_tasks([taskhub_sk])[0]) == 20 + + # same as above query without the set num_retries = 1 + query = """ + MATCH (:TaskRestartPattern)-[applies:APPLIES]->(:Task)<-[:ACTIONS]-(:TaskHub {`_scoped_key`: $taskhub_scoped_key}) + RETURN count(applies) AS applies_count + """ + + applies_count = n4js.execute_query( + query, taskhub_scoped_key=str(taskhub_sk) + ).records[0]["applies_count"] + + query = """ + MATCH (:TaskRestartPattern)-[applies:APPLIES]->(:Task) + RETURN applies + """ + + results = n4js.execute_query(query) + + count_0, count_1 = 0, 0 + for count in map( + lambda record: record["applies"]["num_retries"], results.records + ): + match count: + case 0: + count_0 += 1 + case 1: + count_1 += 1 + case _: + raise AssertionError( + "Unexpected count value found in num_retries field" + ) + + assert count_0 == count_1 == 10 + def test_action_task_other_statuses( self, n4js: Neo4jStore, network_tyk2, scope_test ): From d3f25f885cc1e13c5ff0f16328ba5cdb7128450e Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Fri, 26 Jul 2024 13:52:21 -0700 Subject: [PATCH 023/143] Canceling a Task removes the APPLIES relationship When an actioned Task is canceled and also has an APPLIES relationship with a TaskRestartPattern, APPLIES is removed between the two nodes. Removed org, project, and campaign fields since they are not necessary for the APPLIES relationship. --- alchemiscale/storage/statestore.py | 25 +++++++------- .../integration/storage/test_statestore.py | 33 +++++++++++++++++++ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 3689b8fd..3f9a11d0 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -1437,7 +1437,7 @@ def action_tasks( MATCH (trp: TaskRestartPattern)-[:ENFORCES]->(th) WHERE NOT (trp)-[:APPLIES]->(task) - CREATE (trp)-[:APPLIES {num_retries: 0, `_campaign`: $campaign, `_org`: $org, `_project`: $project}]->(task) + CREATE (trp)-[:APPLIES {num_retries: 0}]->(task) } RETURN task @@ -1450,9 +1450,6 @@ def action_tasks( running=TaskStatusEnum.running.value, error=TaskStatusEnum.error.value, taskhub_scoped_key=str(taskhub), - campaign=taskhub.campaign, - org=taskhub.org, - project=taskhub.project, ) # update our map with the results, leaving None for tasks that aren't found @@ -1606,15 +1603,24 @@ def cancel_tasks( """ canceled_sks = [] with self.transaction() as tx: - for t in tasks: - q = f""" + for task in tasks: + query = """ // get our task hub, as well as the task :ACTIONS relationship we want to remove - MATCH (th:TaskHub {{_scoped_key: '{taskhub}'}})-[ar:ACTIONS]->(task:Task {{_scoped_key: '{t}'}}) + MATCH (th:TaskHub {_scoped_key: $taskhub_scoped_key})-[ar:ACTIONS]->(task:Task {_scoped_key: $task_scoped_key}) DELETE ar + WITH task + CALL { + WITH task + MATCH (task)<-[applies:APPLIES]-(:TaskRestartPattern) + DELETE applies + } + RETURN task """ - _task = tx.run(q).to_eager_result() + _task = tx.run( + query, taskhub_scoped_key=str(taskhub), task_scoped_key=str(task) + ).to_eager_result() if _task.records: sk = _task.records[0].data()["task"]["_scoped_key"] @@ -2806,9 +2812,6 @@ def add_task_restart_patterns( task_restart_pattern_node, actioned_task_node, num_retries=0, - _org=scope.org, - _campaign=scope.campaign, - _project=scope.project, ) merge_subgraph(tx, subgraph, "GufeTokenizable", "_scoped_key") diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index f79a1c4a..2f5acf03 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -1270,6 +1270,14 @@ def test_cancel_task(self, n4js, network_tyk2, scope_test): # cancel the second and third task we created canceled = n4js.cancel_tasks(task_sks[1:3], taskhub_sk) + # cancel a fake task + fake_canceled = n4js.cancel_tasks( + [ScopedKey.from_str("Task-FAKE-test_org-test_campaign-test_project")], + taskhub_sk, + ) + + assert fake_canceled[0] is None + # check that the hub has the contents we expect q = f"""MATCH (tq:TaskHub {{_scoped_key: '{taskhub_sk}'}})-[:ACTIONS]->(task:Task) return task @@ -1283,6 +1291,31 @@ def test_cancel_task(self, n4js, network_tyk2, scope_test): actioned ) - set(canceled) + # create a TaskRestartPattern + n4js.add_task_restart_patterns(taskhub_sk, ["Test pattern"], 1) + + query = """ + MATCH (:TaskHub {`_scoped_key`: $taskhub_scoped_key})<-[:ENFORCES]-(:TaskRestartPattern)-[applies:APPLIES]->(:Task) + RETURN count(applies) AS applies_count + """ + + assert ( + n4js.execute_query(query, taskhub_scoped_key=str(taskhub_sk)).records[0][ + "applies_count" + ] + == 8 + ) + + # cancel the fourth and fifth task we created + canceled = n4js.cancel_tasks(task_sks[3:5], taskhub_sk) + + assert ( + n4js.execute_query(query, taskhub_scoped_key=str(taskhub_sk)).records[0][ + "applies_count" + ] + == 6 + ) + def test_get_taskhub_tasks(self, n4js, network_tyk2, scope_test): an = network_tyk2 network_sk, taskhub_sk, _ = n4js.assemble_network(an, scope_test) From 510ae664d243c0e736412a557222e089e7e231ae Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Thu, 1 Aug 2024 09:13:29 -0700 Subject: [PATCH 024/143] Task status changes affect APPLIES relationship Setting an actioned Task status to the following statuses now removes the APPLIES relationship from attached TaskRestartPatterns: * complete * invalid * deleted NOTE: tests have not been added for this yet --- alchemiscale/storage/statestore.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 3f9a11d0..07d05d02 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -2584,7 +2584,9 @@ def set_task_complete( // if we changed the status to complete, // drop all ACTIONS relationships OPTIONAL MATCH (t_)<-[ar:ACTIONS]-(th:TaskHub) + OPTIONAL MATCH (t_)<-[applies:APPLIES]-(:TaskRestartPattern) DELETE ar + DELETE applies WITH scoped_key, t, t_ @@ -2667,9 +2669,11 @@ def set_task_invalid( OPTIONAL MATCH (t_)<-[ar:ACTIONS]-(th:TaskHub) OPTIONAL MATCH (extends_task)<-[are:ACTIONS]-(th:TaskHub) + OPTIONAL MATCH (t_)<-[applies:APPLIES]-(:TaskRestartPattern) DELETE ar DELETE are + DELETE applies WITH scoped_key, t, t_ @@ -2717,9 +2721,11 @@ def set_task_deleted( OPTIONAL MATCH (t_)<-[ar:ACTIONS]-(th:TaskHub) OPTIONAL MATCH (extends_task)<-[are:ACTIONS]-(th:TaskHub) + OPTIONAL MATCH (t_)<-[applies:APPLIES]-(:TaskRestartPattern) DELETE ar DELETE are + DELETE applies WITH scoped_key, t, t_ From 2310fd575ce560d34408affecdfff3afd3519322 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Sun, 4 Aug 2024 14:06:43 -0700 Subject: [PATCH 025/143] Tests for Task status change on APPLIES Confirming that changing the status of an actioned Task to any of the following removes the APPLIES relationship: * complete * invalid * deleted --- .../integration/storage/test_statestore.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 2f5acf03..c7901840 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -1948,6 +1948,56 @@ def test_get_task_failures( class TestTaskRestartPolicy: + @pytest.mark.parametrize("status", ("complete", "invalid", "deleted")) + def test_task_status_change(self, n4js, network_tyk2, scope_test, status): + an = network_tyk2.copy_with_replacements( + name=network_tyk2.name + f"_test_task_status_change" + ) + _, taskhub_scoped_key, _ = n4js.assemble_network(an, scope_test) + transformation = list(an.edges)[0] + transformation_scoped_key = n4js.get_scoped_key(transformation, scope_test) + task_scoped_keys = n4js.create_tasks([transformation_scoped_key]) + n4js.action_tasks(task_scoped_keys, taskhub_scoped_key) + + n4js.add_task_restart_patterns(taskhub_scoped_key, ["Test pattern"], 10) + + query = """ + MATCH (:TaskRestartPattern)-[:APPLIES]->(task:Task {`_scoped_key`: $task_scoped_key})<-[:ACTIONS]-(:TaskHub {`_scoped_key`: $taskhub_scoped_key}) + RETURN task + """ + + results = n4js.execute_query( + query, + task_scoped_key=str(task_scoped_keys[0]), + taskhub_scoped_key=str(taskhub_scoped_key), + ) + + assert len(results.records) == 1 + + target_method = { + "complete": n4js.set_task_complete, + "invalid": n4js.set_task_invalid, + "deleted": n4js.set_task_deleted, + } + + if status == "complete": + n4js.set_task_running(task_scoped_keys) + + assert target_method[status](task_scoped_keys)[0] is not None + + query = """ + MATCH (:TaskRestartPattern)-[:APPLIES]->(task:Task) + RETURN task + """ + + results = n4js.execute_query( + query, + task_scoped_key=str(task_scoped_keys[0]), + taskhub_scoped_key=str(taskhub_scoped_key), + ) + + assert len(results.records) == 0 + def test_add_task_restart_patterns(self, n4js, network_tyk2, scope_test): # create three new alchemical networks (and taskhubs) taskhub_sks = [] From ea2851f799ca5ad9389c3fde6eac48ae7f411f17 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Sun, 4 Aug 2024 15:23:38 -0700 Subject: [PATCH 026/143] Added method (unimplemented) calls for restarts New statestore method placeholders: - add_task_traceback - resolve_task_restarts The compute api will add a Task Traceback and resolve restarts for returned failed Tasks. When a list of restart patterns are added, restarts are resolved. --- alchemiscale/compute/api.py | 5 ++++- alchemiscale/storage/statestore.py | 33 +++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/alchemiscale/compute/api.py b/alchemiscale/compute/api.py index db21d5b8..df4844c8 100644 --- a/alchemiscale/compute/api.py +++ b/alchemiscale/compute/api.py @@ -12,6 +12,7 @@ from fastapi import FastAPI, APIRouter, Body, Depends from fastapi.middleware.gzip import GZipMiddleware from gufe.tokenization import GufeTokenizable, JSON_HANDLER +from gufe.protocols import ProtocolDAGResult from ..base.api import ( QueryGUFEHandler, @@ -248,7 +249,7 @@ def set_task_result( validate_scopes(task_sk.scope, token) pdr = json.loads(protocoldagresult, cls=JSON_HANDLER.decoder) - pdr = GufeTokenizable.from_dict(pdr) + pdr: ProtocolDAGResult = GufeTokenizable.from_dict(pdr) tf_sk, _ = n4js.get_task_transformation( task=task_scoped_key, @@ -270,7 +271,9 @@ def set_task_result( if protocoldagresultref.ok: n4js.set_task_complete(tasks=[task_sk]) else: + n4js.add_task_traceback(task_sk, pdr.protocol_unit_failures, result_sk) n4js.set_task_error(tasks=[task_sk]) + n4js.resolve_task_restarts(tasks=[task_sk]) return result_sk diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 07d05d02..bf1bf6fc 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -16,6 +16,7 @@ import networkx as nx from gufe import AlchemicalNetwork, Transformation, NonTransformation, Settings from gufe.tokenization import GufeTokenizable, GufeKey, JSON_HANDLER +from gufe.protocols import ProtocolUnitFailure from neo4j import Transaction, GraphDatabase, Driver @@ -2416,6 +2417,14 @@ def get_task_failures(self, task: ScopedKey) -> List[ProtocolDAGResultRef]: """ return self._get_protocoldagresultrefs(q, task) + def add_task_traceback( + self, + task_scoped_key: ScopedKey, + protocol_unit_failures: List[ProtocolUnitFailure], + protocol_dag_result_ref_scoped_key: ScopedKey, + ): + raise NotImplementedError + def set_task_status( self, tasks: List[ScopedKey], status: TaskStatusEnum, raise_error: bool = False ) -> List[Optional[ScopedKey]]: @@ -2778,15 +2787,17 @@ def add_task_restart_patterns( RETURN task """ + actioned_task_records = ( + tx.run(actioned_tasks_query, taskhub_scoped_key=str(taskhub)) + .to_eager_result() + .records + ) + subgraph = Subgraph() actioned_task_nodes = [] - for actioned_tasks_record in ( - tx.run(actioned_tasks_query, taskhub_scoped_key=str(taskhub)) - .to_eager_result() - .records - ): + for actioned_tasks_record in actioned_task_records: actioned_task_nodes.append( record_data_to_node(actioned_tasks_record["task"]) ) @@ -2821,6 +2832,15 @@ def add_task_restart_patterns( ) merge_subgraph(tx, subgraph, "GufeTokenizable", "_scoped_key") + actioned_task_scoped_keys: List[ScopedKey] = [] + + for actioned_task_record in actioned_task_records: + actioned_task_scoped_keys.append( + ScopedKey(actioned_task_record["task"]["_scoped_key"]) + ) + + self.resolve_task_restarts(actioned_task_scoped_keys) + # TODO: fill in docstring def remove_task_restart_patterns(self, taskhub: ScopedKey, patterns: List[str]): q = """ @@ -2878,6 +2898,9 @@ def get_task_restart_patterns( return data + def resolve_task_restarts(self, task_scoped_keys: List[ScopedKey]): + raise NotImplementedError + ## authentication def create_credentialed_entity(self, entity: CredentialedEntity): From 8e011beccce70dc4fcac041c36dc673d087990b0 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 5 Aug 2024 10:49:57 -0700 Subject: [PATCH 027/143] Implemented add_protocol_dag_result_ref_traceback * Renamed add_task_traceback to add_protocol_dag_result_ref_traceback * Added tests for add_protocol_dag_result_ref_traceback --- alchemiscale/compute/api.py | 4 +- alchemiscale/storage/statestore.py | 44 +++++++++++++- .../integration/storage/test_statestore.py | 58 +++++++++++++++++++ 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/alchemiscale/compute/api.py b/alchemiscale/compute/api.py index df4844c8..a50f6d93 100644 --- a/alchemiscale/compute/api.py +++ b/alchemiscale/compute/api.py @@ -271,7 +271,9 @@ def set_task_result( if protocoldagresultref.ok: n4js.set_task_complete(tasks=[task_sk]) else: - n4js.add_task_traceback(task_sk, pdr.protocol_unit_failures, result_sk) + n4js.add_protocol_dag_result_ref_traceback( + pdr.protocol_unit_failures, result_sk + ) n4js.set_task_error(tasks=[task_sk]) n4js.resolve_task_restarts(tasks=[task_sk]) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index bf1bf6fc..f1902421 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -30,6 +30,7 @@ TaskHub, TaskRestartPattern, TaskStatusEnum, + Traceback, ) from ..strategies import Strategy from ..models import Scope, ScopedKey @@ -2417,13 +2418,50 @@ def get_task_failures(self, task: ScopedKey) -> List[ProtocolDAGResultRef]: """ return self._get_protocoldagresultrefs(q, task) - def add_task_traceback( + def add_protocol_dag_result_ref_traceback( self, - task_scoped_key: ScopedKey, protocol_unit_failures: List[ProtocolUnitFailure], protocol_dag_result_ref_scoped_key: ScopedKey, ): - raise NotImplementedError + subgraph = Subgraph() + + with self.transaction() as tx: + + query = """ + MATCH (pdrr:ProtocolDAGResultRef {`_scoped_key`: $protocol_dag_result_ref_scoped_key}) + RETURN pdrr + """ + + pdrr_result = tx.run( + query, + protocol_dag_result_ref_scoped_key=str( + protocol_dag_result_ref_scoped_key + ), + ).to_eager_result() + + try: + protocol_dag_result_ref_node = record_data_to_node( + pdrr_result.records[0]["pdrr"] + ) + except IndexError: + raise KeyError("Could not find ProtocolDAGResultRef in database.") + + tracebacks = list(map(lambda puf: puf.traceback, protocol_unit_failures)) + traceback = Traceback(tracebacks) + + _, traceback_node, _ = self._gufe_to_subgraph( + traceback.to_shallow_dict(), + labels=["GufeTokenizable", traceback.__class__.__name__], + gufe_key=traceback.key, + scope=protocol_dag_result_ref_scoped_key.scope, + ) + + subgraph |= Relationship.type("DETAILS")( + traceback_node, + protocol_dag_result_ref_node, + ) + + merge_subgraph(tx, subgraph, "GufeTokenizable", "_scoped_key") def set_task_status( self, tasks: List[ScopedKey], status: TaskStatusEnum, raise_error: bool = False diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index c7901840..94d6e110 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -1944,6 +1944,64 @@ def test_get_task_failures( assert pdr_ref_sk in failure_pdr_ref_sks assert pdr_ref2_sk in failure_pdr_ref_sks + @pytest.mark.parametrize("failure_count", (1, 2, 3, 4)) + def test_add_protocol_dag_result_ref_traceback( + self, + network_tyk2_failure, + n4js, + scope_test, + transformation_failure, + protocoldagresults_failure, + failure_count: int, + ): + + an = network_tyk2_failure.copy_with_replacements( + name=network_tyk2_failure.name + + "_test_add_protocol_dag_result_ref_traceback" + ) + n4js.assemble_network(an, scope_test) + transformation_scoped_key = n4js.get_scoped_key( + transformation_failure, scope_test + ) + + # create a task; pretend we computed it, submit reference for pre-baked + # result + task_scoped_key = n4js.create_task(transformation_scoped_key) + + protocol_unit_failure = protocoldagresults_failure[0].protocol_unit_failures[0] + + pdrr = ProtocolDAGResultRef( + scope=task_scoped_key.scope, + obj_key=protocoldagresults_failure[0].key, + ok=protocoldagresults_failure[0].ok(), + ) + + # push the result + pdrr_scoped_key = n4js.set_task_result(task_scoped_key, pdrr) + + protocol_unit_failures = [] + for failure_index in range(failure_count): + protocol_unit_failures.append( + protocol_unit_failure.copy_with_replacements( + traceback=protocol_unit_failure.traceback + "_" + str(failure_index) + ) + ) + + n4js.add_protocol_dag_result_ref_traceback( + protocol_unit_failures, pdrr_scoped_key + ) + + query = """ + MATCH (traceback:Traceback)-[:DETAILS]->(:ProtocolDAGResultRef {`_scoped_key`: $pdrr_scoped_key}) + RETURN traceback + """ + + results = n4js.execute_query(query, pdrr_scoped_key=str(pdrr_scoped_key)) + + returned_tracebacks = results.records[0]["traceback"]["tracebacks"] + + assert returned_tracebacks == [puf.traceback for puf in protocol_unit_failures] + ### task restart policies class TestTaskRestartPolicy: From 4f07dde8303b334c797a6c68d12e31fa445b2197 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 6 Aug 2024 07:17:37 -0700 Subject: [PATCH 028/143] Started implementation of restart resolution --- alchemiscale/storage/statestore.py | 15 +++++++ .../integration/storage/test_statestore.py | 41 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index f1902421..2b847539 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -2937,6 +2937,21 @@ def get_task_restart_patterns( return data def resolve_task_restarts(self, task_scoped_keys: List[ScopedKey]): + + query = """ + UNWIND $task_scoped_keys AS task_scoped_key + MATCH (task:Task {status: $error, `_scoped_key`: task_scoped_key})<-[app:APPLIES]-(trp:TaskRestartPattern) + CALL { + WITH task + OPTIONAL MATCH (task:Task)-[:RESULTS_IN]->(pdrr:ProtocolDAGResultRef)<-[:DETAILS]-(traceback:Traceback) + RETURN traceback + ORDER BY pdrr.date DESCENDING + LIMIT 1 + } + WITH traceback + RETURN task, app, trp, traceback + """ + raise NotImplementedError ## authentication diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 94d6e110..c5e90fe4 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -2283,6 +2283,47 @@ def test_get_task_restart_patterns(self, n4js, network_tyk2, scope_test): assert taskhub_grouped_patterns == expected_results + @pytest.mark.xfail(raises=NotImplementedError) + def test_resolve_task_restarts( + self, + n4js, + network_tyk2_failure, + scope_test, + transformation_failure, + protocoldagresults_failure, + ): + + an = network_tyk2_failure.copy_with_replacements( + name=network_tyk2_failure.name + + "_test_add_protocol_dag_result_ref_traceback" + ) + n4js.assemble_network(an, scope_test) + transformation_scoped_key = n4js.get_scoped_key( + transformation_failure, scope_test + ) + + # create a task; pretend we computed it, submit reference for pre-baked + # result + task_scoped_key = n4js.create_task(transformation_scoped_key) + + protocol_unit_failure = protocoldagresults_failure[ + 0 + ].protocol_unit_failures[0] + + from datetime import datetime + + for index in range(5): + pdrr = ProtocolDAGResultRef( + scope=task_scoped_key.scope, + obj_key=protocoldagresults_failure[0].key, + ok=protocoldagresults_failure[0].ok(), + datetime_created=datetime.utcnow(), + ) + + pdrr_scoped_key = n4js.set_task_result(task_scoped_key, pdrr) + + raise NotImplementedError + @pytest.mark.xfail(raises=NotImplementedError) def test_task_actioning_applies_relationship(self): raise NotImplementedError From 78c45518293fd542ac6e17601be9a4a955ac4535 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Wed, 7 Aug 2024 15:14:30 -0700 Subject: [PATCH 029/143] Tracebacks now include key data from its source units --- alchemiscale/storage/models.py | 13 +++++++++++-- alchemiscale/storage/statestore.py | 12 ++++++++---- .../tests/integration/storage/test_statestore.py | 1 + 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index 3dc69e0d..7fee8156 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -204,7 +204,9 @@ def __eq__(self, other): class Traceback(GufeTokenizable): - def __init__(self, tracebacks: List[str]): + def __init__( + self, tracebacks: List[str], source_keys: List[str], failure_keys: List[str] + ): value_error = ValueError( "`tracebacks` must be a non-empty list of string values" ) @@ -216,7 +218,10 @@ def __init__(self, tracebacks: List[str]): if not all_string_values or "" in tracebacks: raise value_error + # TODO: validate self.tracebacks = tracebacks + self.source_keys = source_keys + self.failure_keys = failure_keys def _gufe_tokenize(self): return hashlib.md5(str(self.tracebacks).encode()).hexdigest() @@ -230,7 +235,11 @@ def _from_dict(cls, dct): return Traceback(**dct) def _to_dict(self): - return {"tracebacks": self.tracebacks} + return { + "tracebacks": self.tracebacks, + "source_keys": self.source_keys, + "failure_keys": self.failure_keys, + } class TaskHub(GufeTokenizable): diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 2b847539..5d3da98e 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -2447,7 +2447,9 @@ def add_protocol_dag_result_ref_traceback( raise KeyError("Could not find ProtocolDAGResultRef in database.") tracebacks = list(map(lambda puf: puf.traceback, protocol_unit_failures)) - traceback = Traceback(tracebacks) + source_keys = list(map(lambda puf: puf.source_key, protocol_unit_failures)) + failure_keys = list(map(lambda puf: puf.key, protocol_unit_failures)) + traceback = Traceback(tracebacks, source_keys, failure_keys) _, traceback_node, _ = self._gufe_to_subgraph( traceback.to_shallow_dict(), @@ -2877,7 +2879,7 @@ def add_task_restart_patterns( ScopedKey(actioned_task_record["task"]["_scoped_key"]) ) - self.resolve_task_restarts(actioned_task_scoped_keys) + self.resolve_task_restarts(actioned_task_scoped_keys, transaction=tx) # TODO: fill in docstring def remove_task_restart_patterns(self, taskhub: ScopedKey, patterns: List[str]): @@ -2936,7 +2938,9 @@ def get_task_restart_patterns( return data - def resolve_task_restarts(self, task_scoped_keys: List[ScopedKey]): + def resolve_task_restarts( + self, task_scoped_keys: List[ScopedKey], transaction=None + ): query = """ UNWIND $task_scoped_keys AS task_scoped_key @@ -2945,7 +2949,7 @@ def resolve_task_restarts(self, task_scoped_keys: List[ScopedKey]): WITH task OPTIONAL MATCH (task:Task)-[:RESULTS_IN]->(pdrr:ProtocolDAGResultRef)<-[:DETAILS]-(traceback:Traceback) RETURN traceback - ORDER BY pdrr.date DESCENDING + ORDER BY pdrr.datetime_created DESCENDING LIMIT 1 } WITH traceback diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index c5e90fe4..bc5b0f50 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -1979,6 +1979,7 @@ def test_add_protocol_dag_result_ref_traceback( # push the result pdrr_scoped_key = n4js.set_task_result(task_scoped_key, pdrr) + # simulating many failures protocol_unit_failures = [] for failure_index in range(failure_count): protocol_unit_failures.append( From 7acc0036039324c86d2348908d990c8e121775f8 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 13 Aug 2024 08:57:16 -0700 Subject: [PATCH 030/143] Built out custom fixture for testing restart policies Implemented half of the resolve_task_restarts test --- alchemiscale/storage/statestore.py | 8 +- alchemiscale/tests/integration/conftest.py | 40 ++++++ .../tests/integration/interface/conftest.py | 34 +++++ .../integration/storage/test_statestore.py | 134 ++++++++++++++---- 4 files changed, 189 insertions(+), 27 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 5d3da98e..9255c988 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -2876,10 +2876,13 @@ def add_task_restart_patterns( for actioned_task_record in actioned_task_records: actioned_task_scoped_keys.append( - ScopedKey(actioned_task_record["task"]["_scoped_key"]) + ScopedKey.from_str(actioned_task_record["task"]["_scoped_key"]) ) - self.resolve_task_restarts(actioned_task_scoped_keys, transaction=tx) + try: + self.resolve_task_restarts(actioned_task_scoped_keys, transaction=tx) + except NotImplementedError: + pass # TODO: fill in docstring def remove_task_restart_patterns(self, taskhub: ScopedKey, patterns: List[str]): @@ -2914,6 +2917,7 @@ def set_task_restart_patterns_max_retries( ) # TODO: fill in docstring + # TODO: validation of taskhubs variable, will fail in weird ways if not enforced def get_task_restart_patterns( self, taskhubs: List[ScopedKey] ) -> Dict[ScopedKey, Set[Tuple[str, int]]]: diff --git a/alchemiscale/tests/integration/conftest.py b/alchemiscale/tests/integration/conftest.py index 1875981e..026f5866 100644 --- a/alchemiscale/tests/integration/conftest.py +++ b/alchemiscale/tests/integration/conftest.py @@ -167,6 +167,46 @@ def n4js(graph): return Neo4jStore(graph) +@fixture +def n4js_task_restart_policy( + n4js_fresh: Neo4jStore, network_tyk2: AlchemicalNetwork, scope_test +): + + n4js = n4js_fresh + + _, taskhub_scoped_key_with_policy, _ = n4js.assemble_network( + network_tyk2, scope_test + ) + + _, taskhub_scoped_key_no_policy, _ = n4js.assemble_network( + network_tyk2.copy_with_replacements(name=network_tyk2.name + "_no_policy"), + scope_test, + ) + + transformation_1_scoped_key, transformation_2_scoped_key = map( + lambda transformation: n4js.get_scoped_key(transformation, scope_test), + list(network_tyk2.edges)[:2], + ) + + task_scoped_keys = n4js.create_tasks( + [transformation_1_scoped_key] * 4 + [transformation_2_scoped_key] * 4 + ) + + assert all(n4js.action_tasks(task_scoped_keys[:4], taskhub_scoped_key_no_policy)) + assert all(n4js.action_tasks(task_scoped_keys, taskhub_scoped_key_with_policy)) + + patterns = [ + "This is an example pattern that will be used as a restart string. 1", + "This is an example pattern that will be used as a restart string. 2", + ] + + n4js.add_task_restart_patterns( + taskhub_scoped_key_with_policy, patterns=patterns, number_of_retries=2 + ) + + return n4js + + @fixture def n4js_fresh(graph): n4js = Neo4jStore(graph) diff --git a/alchemiscale/tests/integration/interface/conftest.py b/alchemiscale/tests/integration/interface/conftest.py index 2eb2c996..b24332e4 100644 --- a/alchemiscale/tests/integration/interface/conftest.py +++ b/alchemiscale/tests/integration/interface/conftest.py @@ -89,6 +89,40 @@ def n4js_preloaded( return n4js +from alchemiscale.storage.statestore import Neo4jStore + + +@pytest.fixture +def n4js_task_restart_policy( + n4js_fresh: Neo4jStore, network_tyk2: AlchemicalNetwork, scope_test +): + + n4js = n4js_fresh + + _, taskhub_scoped_key_with_policy, _ = n4js.assemble_network( + network_tyk2, scope_test + ) + + _, taskhub_scoped_key_no_policy, _ = n4js.assemble_network( + network_tyk2.copy_with_replacements(name=network_tyk2.name + "_no_policy"), + scope_test, + ) + + transformation_1_scoped_key, transformation_2_scoped_key = map( + lambda transformation: n4js.get_scoped_key(transformation, scope_test), + network_tyk2.edges[:2], + ) + + task_scoped_keys = n4js.create_tasks( + [transformation_1_scoped_key] * 4 + [transformation_2_scoped_key] * 4 + ) + + assert all(n4js.action_tasks(task_scoped_keys, taskhub_scoped_key_no_policy)) + assert all(n4js.action_tasks(task_scoped_keys, taskhub_scoped_key_with_policy)) + + breakpoint() + + @pytest.fixture(scope="module") def scope_consistent_token_data_depends_override(scope_test): """Make a consistent helper to provide an override to the api.app while still accessing fixtures""" diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index bc5b0f50..0e08dc02 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -2,12 +2,15 @@ import random from typing import List, Dict from pathlib import Path +from functools import reduce from itertools import chain +from operator import and_ from collections import defaultdict import pytest from gufe import AlchemicalNetwork from gufe.tokenization import TOKENIZABLE_REGISTRY +from gufe.protocols import ProtocolUnitFailure from gufe.protocols.protocoldag import execute_DAG from alchemiscale.storage.statestore import Neo4jStore @@ -2287,43 +2290,124 @@ def test_get_task_restart_patterns(self, n4js, network_tyk2, scope_test): @pytest.mark.xfail(raises=NotImplementedError) def test_resolve_task_restarts( self, - n4js, - network_tyk2_failure, - scope_test, - transformation_failure, - protocoldagresults_failure, + scope_test: Scope, + n4js_task_restart_policy: Neo4jStore, ): - an = network_tyk2_failure.copy_with_replacements( - name=network_tyk2_failure.name - + "_test_add_protocol_dag_result_ref_traceback" + def spoof_failure(): + raise NotImplementedError + + # get the actioned tasks for each taskhub + taskhub_actioned_tasks = {} + for taskhub_scoped_key in n4js_task_restart_policy.query_taskhubs(): + taskhub_actioned_tasks[taskhub_scoped_key] = set( + n4js_task_restart_policy.get_taskhub_actioned_tasks( + [taskhub_scoped_key] + )[0] + ) + + restart_patterns = n4js_task_restart_policy.get_task_restart_patterns( + list(taskhub_actioned_tasks.keys()) ) - n4js.assemble_network(an, scope_test) - transformation_scoped_key = n4js.get_scoped_key( - transformation_failure, scope_test + + transformation_tasks = defaultdict(list) + for task in n4js_task_restart_policy.query_tasks( + status=TaskStatusEnum.waiting.value + ): + transformation_scoped_key, _ = ( + n4js_task_restart_policy.get_task_transformation( + task, return_gufe=False + ) + ) + transformation_tasks[transformation_scoped_key].append(task) + + # get a list of all tasks for more convient calls of the resolve method + all_tasks = [] + for task_group in transformation_tasks.values(): + all_tasks.extend(task_group) + + taskhub_scoped_key_no_policy = None + taskhub_scoped_key_with_policy = None + + for taskhub_scoped_key, patterns in restart_patterns.items(): + if not patterns: + taskhub_scoped_key_no_policy = taskhub_scoped_key + continue + else: + taskhub_scoped_key_with_policy = taskhub_scoped_key + continue + + if patterns and taskhub_scoped_key_with_policy: + raise AssertionError("More than one TaskHub has restart patterns") + + assert ( + taskhub_scoped_key_no_policy + and taskhub_scoped_key_with_policy + and (taskhub_scoped_key_no_policy != taskhub_scoped_key_with_policy) ) - # create a task; pretend we computed it, submit reference for pre-baked - # result - task_scoped_key = n4js.create_task(transformation_scoped_key) + # we first check the behavior involving tasks that are actioned by both taskhubs + # this involves confirming: + # + # 1. Completed Tasks do not have an actions relationship with either TaskHub + # 2. A Task entering the error state is switched back to waiting if any restart patterns apply + # 3. A Task entering the error state is left in the error state if no patterns apply and only the TaskHub with + # an enforcing task restart policy exists + # + # Tasks will be set to the error state with a spoofing method, which will create a fake ProtocolDAGResultRef + # and Traceback. This is done since making a protocol fail systematically in the testing environment is not + # obvious at this time. + + # reduce down all tasks until only the common elements between taskhubs exist + tasks_actioned_by_all_taskhubs: List[ScopedKey] = list( + reduce(and_, taskhub_actioned_tasks.values(), set(all_tasks)) + ) - protocol_unit_failure = protocoldagresults_failure[ - 0 - ].protocol_unit_failures[0] + assert len(tasks_actioned_by_all_taskhubs) == 4 - from datetime import datetime + # we're going to just pass the first 2 and fail the second 2 + tasks_to_complete = tasks_actioned_by_all_taskhubs[:2] + tasks_to_fail = tasks_actioned_by_all_taskhubs[3:] - for index in range(5): - pdrr = ProtocolDAGResultRef( - scope=task_scoped_key.scope, - obj_key=protocoldagresults_failure[0].key, - ok=protocoldagresults_failure[0].ok(), + # TODO: either check the results after the loop or within it, whichever makes more sense + for task in tasks_to_complete: + n4js_task_restart_policy.set_task_running([task]) + ok_pdrr = ProtocolDAGResultRef( + ok=True, datetime_created=datetime.utcnow(), + obj_key=task.gufe_key, + scope=task.scope, ) - pdrr_scoped_key = n4js.set_task_result(task_scoped_key, pdrr) + _ = n4js_task_restart_policy.set_task_result(task, ok_pdrr) - raise NotImplementedError + # this should do nothing to the database state since all + # relationships are removed in the previous method call + # TODO: perhaps counts of the connections will be a good test + n4js_task_restart_policy.set_task_complete([task]) + + # TODO: it's unclear the best way to fake a systematic error here + for i, task in enumerate(tasks_to_fail): + n4js_task_restart_policy.set_task_running([task]) + + not_ok_pdrr = ProtocolDAGResultRef( + ok=False, + datetime_created=datetime.utcnow(), + obj_key=task.gufe_key, + scope=task.scope, + ) + + error_messages = ( + "Error message 1", + "Error message 2", + "Error message 3", + ) + + n4js_task_restart_policy.add_protocol_dag_result_ref_traceback() + n4js_task_restart_policy.set_task_error([task]) + + # always feed in all tasks to test for side effects + n4js_task_restart_policy.resolve_task_restarts(all_tasks) @pytest.mark.xfail(raises=NotImplementedError) def test_task_actioning_applies_relationship(self): From 02d427d8dd99255031bf323763d1a7c1caba2c27 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 14 Aug 2024 13:04:10 -0700 Subject: [PATCH 031/143] Switch pip installs to conda packages where possible, add restart policies to docker-compose containers A small set of adjustments from deploying v0.5.0. --- devtools/conda-envs/alchemiscale-client.yml | 10 +++++----- devtools/conda-envs/alchemiscale-compute.yml | 2 +- devtools/conda-envs/alchemiscale-server.yml | 3 +-- docker/alchemiscale-server/docker-compose.yml | 4 ++++ docs/deployment.rst | 4 ++-- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/devtools/conda-envs/alchemiscale-client.yml b/devtools/conda-envs/alchemiscale-client.yml index a027bde5..b64ec483 100644 --- a/devtools/conda-envs/alchemiscale-client.yml +++ b/devtools/conda-envs/alchemiscale-client.yml @@ -14,13 +14,15 @@ dependencies: - click - httpx - pydantic<2.0 + - async-lru + + ## user client + - rich + - nest-asyncio # openmm protocols - feflow=0.1.0 - ## user client printing - - rich - # additional pins - openmm=8.1.2 - openmmforcefields>=0.14.1 @@ -30,6 +32,4 @@ dependencies: - plyvel - pip: - - nest_asyncio - - async_lru - git+https://github.com/openforcefield/alchemiscale.git@v0.5.0 diff --git a/devtools/conda-envs/alchemiscale-compute.yml b/devtools/conda-envs/alchemiscale-compute.yml index a44788d4..9d563bd4 100644 --- a/devtools/conda-envs/alchemiscale-compute.yml +++ b/devtools/conda-envs/alchemiscale-compute.yml @@ -14,6 +14,7 @@ dependencies: - click - httpx - pydantic<2.0 + - async-lru # openmm protocols - feflow=0.1.0 @@ -23,5 +24,4 @@ dependencies: - openmmforcefields>=0.14.1 - pip: - - async_lru - git+https://github.com/openforcefield/alchemiscale.git@v0.5.0 diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index 7d46147c..a175762f 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -14,6 +14,7 @@ dependencies: - requests - click - pydantic<2.0 + - async-lru ## state store - neo4j-python-driver @@ -46,9 +47,7 @@ dependencies: - curl # used in healthchecks for API services # alchemiscale-fah dependencies - - cryptography - plyvel - pip: - - async_lru - git+https://github.com/openforcefield/alchemiscale.git@v0.5.0 diff --git a/docker/alchemiscale-server/docker-compose.yml b/docker/alchemiscale-server/docker-compose.yml index af56d8e8..ee1c3111 100644 --- a/docker/alchemiscale-server/docker-compose.yml +++ b/docker/alchemiscale-server/docker-compose.yml @@ -20,6 +20,7 @@ services: ports: - 7687:7687 - 7474:7474 + restart: unless-stopped # Uncomment the volumes to be mounted to make them accessible from outside the container. volumes: #- ./neo4j.conf:/conf/neo4j.conf # This is the main configuration file. @@ -75,6 +76,7 @@ services: depends_on: - neo4j - alchemiscale-db-init + restart: unless-stopped command: "api --host 0.0.0.0 --port 1840 --workers 2" labels: - "traefik.enable=true" @@ -121,6 +123,7 @@ services: condition: service_healthy alchemiscale-db-init: condition: service_completed_successfully + restart: unless-stopped command: "compute api --host 0.0.0.0 --port 1841 --workers 2" labels: - "traefik.enable=true" @@ -179,6 +182,7 @@ services: depends_on: - alchemiscale-client-API - alchemiscale-compute-API + restart: unless-stopped command: - "--log.level=DEBUG" - "--providers.docker" diff --git a/docs/deployment.rst b/docs/deployment.rst index c6efb46b..fafa9abb 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -65,9 +65,9 @@ For example, using the location set in ``.env.testing``:: Now start the service with:: - $ USER_ID=$(id -u) GROUP_ID=$(id -g) docker-compose up -d + $ USER_ID=$(id -u) GROUP_ID=$(id -g) docker compose up -d -We set ``USER_ID`` and ``GROUP_ID`` to be the same as the user running the ``docker-compose up -d`` command. +We set ``USER_ID`` and ``GROUP_ID`` to be the same as the user running the ``docker compose up -d`` command. Setting up a host on AWS EC2 From 13ef1c79ccd676942542285a8155f7d8b5fa8676 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Fri, 16 Aug 2024 10:44:55 -0700 Subject: [PATCH 032/143] Set default `claim_limit` back to 1 It appears we accidentally changed the default `claim_limit` for compute services to 1000, which massively changes the behavior for deployed compute services if they don't explicitly set this option in their configuration files. This changes this default back to 1. --- alchemiscale/compute/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/compute/settings.py b/alchemiscale/compute/settings.py index 87c80f97..4e97adba 100644 --- a/alchemiscale/compute/settings.py +++ b/alchemiscale/compute/settings.py @@ -61,7 +61,7 @@ class Config: description="Names of Protocols to run with this service; `None` means no restriction.", ) claim_limit: int = Field( - 1000, description="Maximum number of Tasks to claim at a time from a TaskHub." + 1, description="Maximum number of Tasks to claim at a time from a TaskHub." ) loglevel: str = Field( "WARN", From 03d9fa1c0218676558f4aa4fd998a6d7a5447f96 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 19 Aug 2024 10:29:43 -0700 Subject: [PATCH 033/143] Added the `chainable` decorator to Neo4jStore With this decorator, if a transaction isn't passed as a keyword arg, one is automatically created (and closed). This allows a chaining behavior where many method calls share a single transaction object. --- alchemiscale/storage/statestore.py | 119 +++++++++++++----- .../tests/integration/interface/conftest.py | 2 - 2 files changed, 87 insertions(+), 34 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 9255c988..7a38e882 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -8,6 +8,7 @@ from datetime import datetime from contextlib import contextmanager import json +import re from functools import lru_cache from typing import Dict, List, Optional, Union, Tuple, Set import weakref @@ -175,6 +176,17 @@ def transaction(self, ignore_exceptions=False) -> Transaction: else: tx.commit() + def chainable(func): + def inner(self, *args, **kwargs): + if kwargs.get("tx") is not None: + return func(self, *args, **kwargs) + + with self.transaction() as tx: + kwargs.update(tx=tx) + return func(self, *args, **kwargs) + + return inner + def execute_query(self, *args, **kwargs): kwargs.update({"database_": self.db_name}) return self.graph.execute_query(*args, **kwargs) @@ -1590,10 +1602,12 @@ def get_task_weights( return weights + @chainable def cancel_tasks( self, tasks: List[ScopedKey], taskhub: ScopedKey, + tx=None, ) -> List[Union[ScopedKey, None]]: """Remove Tasks from the TaskHub for a given AlchemicalNetwork. @@ -1604,31 +1618,30 @@ def cancel_tasks( """ canceled_sks = [] - with self.transaction() as tx: - for task in tasks: - query = """ - // get our task hub, as well as the task :ACTIONS relationship we want to remove - MATCH (th:TaskHub {_scoped_key: $taskhub_scoped_key})-[ar:ACTIONS]->(task:Task {_scoped_key: $task_scoped_key}) - DELETE ar + for task in tasks: + query = """ + // get our task hub, as well as the task :ACTIONS relationship we want to remove + MATCH (th:TaskHub {_scoped_key: $taskhub_scoped_key})-[ar:ACTIONS]->(task:Task {_scoped_key: $task_scoped_key}) + DELETE ar + WITH task + CALL { WITH task - CALL { - WITH task - MATCH (task)<-[applies:APPLIES]-(:TaskRestartPattern) - DELETE applies - } + MATCH (task)<-[applies:APPLIES]-(:TaskRestartPattern) + DELETE applies + } - RETURN task - """ - _task = tx.run( - query, taskhub_scoped_key=str(taskhub), task_scoped_key=str(task) - ).to_eager_result() + RETURN task + """ + _task = tx.run( + query, taskhub_scoped_key=str(taskhub), task_scoped_key=str(task) + ).to_eager_result() - if _task.records: - sk = _task.records[0].data()["task"]["_scoped_key"] - canceled_sks.append(ScopedKey.from_str(sk)) - else: - canceled_sks.append(None) + if _task.records: + sk = _task.records[0].data()["task"]["_scoped_key"] + canceled_sks.append(ScopedKey.from_str(sk)) + else: + canceled_sks.append(None) return canceled_sks @@ -2879,10 +2892,7 @@ def add_task_restart_patterns( ScopedKey.from_str(actioned_task_record["task"]["_scoped_key"]) ) - try: - self.resolve_task_restarts(actioned_task_scoped_keys, transaction=tx) - except NotImplementedError: - pass + self.resolve_task_restarts(actioned_task_scoped_keys, tx=tx) # TODO: fill in docstring def remove_task_restart_patterns(self, taskhub: ScopedKey, patterns: List[str]): @@ -2942,13 +2952,12 @@ def get_task_restart_patterns( return data - def resolve_task_restarts( - self, task_scoped_keys: List[ScopedKey], transaction=None - ): + @chainable + def resolve_task_restarts(self, task_scoped_keys: List[ScopedKey], *, tx=None): query = """ UNWIND $task_scoped_keys AS task_scoped_key - MATCH (task:Task {status: $error, `_scoped_key`: task_scoped_key})<-[app:APPLIES]-(trp:TaskRestartPattern) + MATCH (task:Task {status: $error, `_scoped_key`: task_scoped_key})<-[app:APPLIES]-(trp:TaskRestartPattern)-[:ENFORCES]->(taskhub:TaskHub) CALL { WITH task OPTIONAL MATCH (task:Task)-[:RESULTS_IN]->(pdrr:ProtocolDAGResultRef)<-[:DETAILS]-(traceback:Traceback) @@ -2956,11 +2965,57 @@ def resolve_task_restarts( ORDER BY pdrr.datetime_created DESCENDING LIMIT 1 } - WITH traceback - RETURN task, app, trp, traceback + WITH task, traceback, trp, app, taskhub + RETURN task, traceback, trp, app, taskhub """ - raise NotImplementedError + results = tx.run( + query, + task_scoped_keys=list(map(str, task_scoped_keys)), + error=TaskStatusEnum.error.value, + ).to_eager_result() + + if not results: + return + + to_increment: List[Tuple[str, str]] = [] + to_cancel: List[Tuple[str, str]] = [] + for record in results.records: + task_restart_pattern = record["trp"] + applies_relationship = record["app"] + task = record["task"] + taskhub = record["taskhub"] + # TODO: what happens if there is no traceback? i.e. older errored tasks + traceback = record["traceback"] + + num_retries = applies_relationship["num_retries"] + max_retries = task_restart_pattern["max_retries"] + pattern = task_restart_pattern["pattern"] + tracebacks: List[str] = traceback["tracebacks"] + + # exit early if we already know a task is being canceled on a TaskHub + if (task["_scoped_key"], taskhub["_scoped_key"]) in to_cancel: + continue + + # we will always increment (even above the max_retries) and + # cancel later + to_increment.append( + (task["_scoped_key"], task_restart_pattern["_scoped_key"]) + ) + if any([re.search(pattern, message) for message in tracebacks]): + if num_retries + 1 > max_retries: + to_cancel.append((task["_scoped_key"], taskhub["_scoped_key"])) + + increment_query = """ + UNWIND $trp_and_task_pairs as pairs + WITH pairs[0] as task_scoped_key, pairs[1] as task_restart_pattern_scoped_key + MATCH (:Task {`_scoped_key`: task_scoped_key})<-[app:APPLIES]-(:TaskRestartPattern {`_scoped_key`: task_restart_pattern_scoped_key}) + SET app.num_retries = app.num_retries + 1 + """ + + tx.run(increment_query, trp_and_task_pairs=to_increment) + for task, taskhub in to_cancel: + self.cancel_tasks([task], taskhub, tx=tx) ## authentication diff --git a/alchemiscale/tests/integration/interface/conftest.py b/alchemiscale/tests/integration/interface/conftest.py index b24332e4..d7c5da6a 100644 --- a/alchemiscale/tests/integration/interface/conftest.py +++ b/alchemiscale/tests/integration/interface/conftest.py @@ -120,8 +120,6 @@ def n4js_task_restart_policy( assert all(n4js.action_tasks(task_scoped_keys, taskhub_scoped_key_no_policy)) assert all(n4js.action_tasks(task_scoped_keys, taskhub_scoped_key_with_policy)) - breakpoint() - @pytest.fixture(scope="module") def scope_consistent_token_data_depends_override(scope_test): From aad97e3c0c6438d1dff3fd1b21623a5351b888e4 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 19 Aug 2024 11:57:13 -0700 Subject: [PATCH 034/143] Resolve task restarts now sets all remaining tasks to waiting --- alchemiscale/storage/statestore.py | 25 +++++++++- .../integration/storage/test_statestore.py | 50 +++++++++++++++---- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 7a38e882..b6c05966 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -11,6 +11,7 @@ import re from functools import lru_cache from typing import Dict, List, Optional, Union, Tuple, Set +from collections.abc import Iterable import weakref import numpy as np @@ -2952,9 +2953,13 @@ def get_task_restart_patterns( return data + # TODO: docstrings @chainable - def resolve_task_restarts(self, task_scoped_keys: List[ScopedKey], *, tx=None): + def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=None): + # Given the scoped keys of a list of Tasks, find all tasks that have an + # error status and have a TaskRestartPattern applied. A subquery is executed + # to optionally get the latest traceback associated with the task query = """ UNWIND $task_scoped_keys AS task_scoped_key MATCH (task:Task {status: $error, `_scoped_key`: task_scoped_key})<-[app:APPLIES]-(trp:TaskRestartPattern)-[:ENFORCES]->(taskhub:TaskHub) @@ -2978,6 +2983,9 @@ def resolve_task_restarts(self, task_scoped_keys: List[ScopedKey], *, tx=None): if not results: return + # iterate over all of the results to determine if an applied pattern needs + # to be iterated or if the task needs to be cancelled outright + to_increment: List[Tuple[str, str]] = [] to_cancel: List[Tuple[str, str]] = [] for record in results.records: @@ -3017,6 +3025,21 @@ def resolve_task_restarts(self, task_scoped_keys: List[ScopedKey], *, tx=None): for task, taskhub in to_cancel: self.cancel_tasks([task], taskhub, tx=tx) + # any remaining tasks must then be okay to switch to waiting + + renew_waiting_status_query = """ + UNWIND $task_scoped_keys AS task_scoped_key + MATCH (task:Task {status: $error, `_scoped_key`: task_scoped_key})<-[app:APPLIES]-(trp:TaskRestartPattern)-[:ENFORCES]->(taskhub:TaskHub) + SET task.status = $waiting + """ + + tx.run( + renew_waiting_status_query, + task_scoped_keys=list(map(str, task_scoped_keys)), + waiting=TaskStatusEnum.waiting.value, + error=TaskStatusEnum.error.value, + ) + ## authentication def create_credentialed_entity(self, entity: CredentialedEntity): diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 0e08dc02..64993239 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -2294,9 +2294,6 @@ def test_resolve_task_restarts( n4js_task_restart_policy: Neo4jStore, ): - def spoof_failure(): - raise NotImplementedError - # get the actioned tasks for each taskhub taskhub_actioned_tasks = {} for taskhub_scoped_key in n4js_task_restart_policy.query_taskhubs(): @@ -2367,7 +2364,7 @@ def spoof_failure(): # we're going to just pass the first 2 and fail the second 2 tasks_to_complete = tasks_actioned_by_all_taskhubs[:2] - tasks_to_fail = tasks_actioned_by_all_taskhubs[3:] + tasks_to_fail = tasks_actioned_by_all_taskhubs[2:] # TODO: either check the results after the loop or within it, whichever makes more sense for task in tasks_to_complete: @@ -2386,7 +2383,6 @@ def spoof_failure(): # TODO: perhaps counts of the connections will be a good test n4js_task_restart_policy.set_task_complete([task]) - # TODO: it's unclear the best way to fake a systematic error here for i, task in enumerate(tasks_to_fail): n4js_task_restart_policy.set_task_running([task]) @@ -2397,18 +2393,50 @@ def spoof_failure(): scope=task.scope, ) - error_messages = ( - "Error message 1", - "Error message 2", - "Error message 3", - ) + error_messages = [ + f"Error message {repeat}, round {i}" for repeat in range(3) + ] + + protocol_unit_failures = [] + for j, message in enumerate(error_messages): + puf = ProtocolUnitFailure( + source_key=f"FakeProtocolUnitKey-123{j}", + inputs={}, + outputs={}, + exception=RuntimeError, + traceback=message, + ) + protocol_unit_failures.append(puf) - n4js_task_restart_policy.add_protocol_dag_result_ref_traceback() + pdrr_scoped_key = n4js_task_restart_policy.set_task_result( + task, not_ok_pdrr + ) + # the following mimics what the compute API would do for a failed task + n4js_task_restart_policy.add_protocol_dag_result_ref_traceback( + protocol_unit_failures, pdrr_scoped_key + ) n4js_task_restart_policy.set_task_error([task]) # always feed in all tasks to test for side effects n4js_task_restart_policy.resolve_task_restarts(all_tasks) + # both tasks should have the waiting status and the APPLIES + # relationship num_retries should have incremented by 1 + + query = """ + UNWIND $task_scoped_keys as task_scoped_key + MATCH (task:Task {`_scoped_key`: task_scoped_key, status: $waiting})<-[:APPLIES {num_retries: 1}]-(:TaskRestartPattern {max_retries: 2}) + RETURN count(DISTINCT task) as renewed_waiting_tasks + """ + + renewed_waiting = n4js_task_restart_policy.execute_query( + query, + task_scoped_keys=list(map(str, tasks_to_fail)), + waiting=TaskStatusEnum.waiting.value, + ).records[0]["renewed_waiting_tasks"] + + assert renewed_waiting == 2 + @pytest.mark.xfail(raises=NotImplementedError) def test_task_actioning_applies_relationship(self): raise NotImplementedError From a655dc7bb50c90a86bfbd3168d8690e6b5fe288b Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 19 Aug 2024 16:33:48 -0700 Subject: [PATCH 035/143] Corrected resolution logic --- alchemiscale/storage/statestore.py | 41 +++++++++++++------ alchemiscale/tests/integration/conftest.py | 4 +- .../integration/storage/test_statestore.py | 35 +++++++++++++++- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index b6c05966..e08ec1c8 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -11,6 +11,7 @@ import re from functools import lru_cache from typing import Dict, List, Optional, Union, Tuple, Set +from collections import defaultdict from collections.abc import Iterable import weakref import numpy as np @@ -2986,8 +2987,14 @@ def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=Non # iterate over all of the results to determine if an applied pattern needs # to be iterated or if the task needs to be cancelled outright + # Keep track of which task/taskhub pairs would need to be canceled + # None => the pair never had a matching restart pattern + # True => at least one patterns max_retries was exceeded + # False => at least one regex matched, but no pattern max_retries were exceeded + cancel_map: defaultdict[Tuple[str, str], Optional[bool]] = defaultdict( + lambda: None + ) to_increment: List[Tuple[str, str]] = [] - to_cancel: List[Tuple[str, str]] = [] for record in results.records: task_restart_pattern = record["trp"] applies_relationship = record["app"] @@ -2996,23 +3003,27 @@ def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=Non # TODO: what happens if there is no traceback? i.e. older errored tasks traceback = record["traceback"] + task_taskhub_tuple = (task["_scoped_key"], taskhub["_scoped_key"]) + + # we have already determined that the task is to be canceled + # is only ever truthy when we say a task needs to be canceled + if cancel_map[task_taskhub_tuple]: + continue + num_retries = applies_relationship["num_retries"] max_retries = task_restart_pattern["max_retries"] pattern = task_restart_pattern["pattern"] tracebacks: List[str] = traceback["tracebacks"] - # exit early if we already know a task is being canceled on a TaskHub - if (task["_scoped_key"], taskhub["_scoped_key"]) in to_cancel: - continue - - # we will always increment (even above the max_retries) and - # cancel later - to_increment.append( - (task["_scoped_key"], task_restart_pattern["_scoped_key"]) - ) if any([re.search(pattern, message) for message in tracebacks]): if num_retries + 1 > max_retries: - to_cancel.append((task["_scoped_key"], taskhub["_scoped_key"])) + cancel_map[task_taskhub_tuple] = True + else: + # to_increment.append(task_taskhub_tuple) + to_increment.append( + (task["_scoped_key"], task_restart_pattern["_scoped_key"]) + ) + cancel_map[task_taskhub_tuple] = False increment_query = """ UNWIND $trp_and_task_pairs as pairs @@ -3022,11 +3033,15 @@ def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=Non """ tx.run(increment_query, trp_and_task_pairs=to_increment) - for task, taskhub in to_cancel: + + # cancel all tasks that didn't trigger any restart patterns (None) + # or exceeded a patterns max_retries value (True) + for (task, taskhub), _ in filter( + lambda values: values[1] is True or values[1] is None, cancel_map.items() + ): self.cancel_tasks([task], taskhub, tx=tx) # any remaining tasks must then be okay to switch to waiting - renew_waiting_status_query = """ UNWIND $task_scoped_keys AS task_scoped_key MATCH (task:Task {status: $error, `_scoped_key`: task_scoped_key})<-[app:APPLIES]-(trp:TaskRestartPattern)-[:ENFORCES]->(taskhub:TaskHub) diff --git a/alchemiscale/tests/integration/conftest.py b/alchemiscale/tests/integration/conftest.py index 026f5866..1a415156 100644 --- a/alchemiscale/tests/integration/conftest.py +++ b/alchemiscale/tests/integration/conftest.py @@ -196,8 +196,8 @@ def n4js_task_restart_policy( assert all(n4js.action_tasks(task_scoped_keys, taskhub_scoped_key_with_policy)) patterns = [ - "This is an example pattern that will be used as a restart string. 1", - "This is an example pattern that will be used as a restart string. 2", + r"Error message \d, round \d", + "This is an example pattern that will be used as a restart string.", ] n4js.add_task_restart_patterns( diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 64993239..334c1999 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -2396,7 +2396,6 @@ def test_resolve_task_restarts( error_messages = [ f"Error message {repeat}, round {i}" for repeat in range(3) ] - protocol_unit_failures = [] for j, message in enumerate(error_messages): puf = ProtocolUnitFailure( @@ -2437,6 +2436,40 @@ def test_resolve_task_restarts( assert renewed_waiting == 2 + # we want the resolve restarts to cancel a task. + # deconstruct the tasks to fail, where the first + # one will be cancelled and the second will once again be continued + # but with an additional traceback + task_to_cancel, task_to_wait = tasks_to_fail + + query = """ + MATCH (task:Task {`_scoped_key`: $task_scoped_key_fail})<-[app:APPLIES]-(:TaskRestartPattern) + SET app.num_retries = 2 + SET task.status = $error + """ + + n4js_task_restart_policy.execute_query( + query, + task_scoped_key_fail=str(task_to_cancel), + task_scoped_key_wait=str(task_to_wait), + error=TaskStatusEnum.error.value, + ) + + n4js_task_restart_policy.resolve_task_restarts(tasks_to_fail) + + query = """ + MATCH (task:Task {_scoped_key: $task_scoped_key})<-[:ACTIONS]-(:TaskHub {_scoped_key: $taskhub_scoped_key}) + RETURN task + """ + + results = n4js_task_restart_policy.execute_query( + query, + task_scoped_key=str(task_to_cancel), + taskhub_scoped_key=str(taskhub_scoped_key_with_policy), + ) + + assert len(results.records) == 0 + @pytest.mark.xfail(raises=NotImplementedError) def test_task_actioning_applies_relationship(self): raise NotImplementedError From 5bb67001e08e105871bc0ee4608a8c7c21e8c11e Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Fri, 23 Aug 2024 12:04:14 -0700 Subject: [PATCH 036/143] Extracted complexity out of test_resolve_task_restarts --- .../integration/storage/test_statestore.py | 137 +++++++----------- .../tests/integration/storage/utils.py | 90 ++++++++++++ 2 files changed, 144 insertions(+), 83 deletions(-) create mode 100644 alchemiscale/tests/integration/storage/utils.py diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 334c1999..5f5a1eb1 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -4,7 +4,7 @@ from pathlib import Path from functools import reduce from itertools import chain -from operator import and_ +import operator from collections import defaultdict import pytest @@ -31,6 +31,13 @@ ) from alchemiscale.security.auth import hash_key +from alchemiscale.tests.integration.storage.utils import ( + complete_tasks, + fail_task, + tasks_are_errored, + tasks_are_not_actioned_on_taskhub, +) + class TestStateStore: ... @@ -2013,7 +2020,7 @@ class TestTaskRestartPolicy: @pytest.mark.parametrize("status", ("complete", "invalid", "deleted")) def test_task_status_change(self, n4js, network_tyk2, scope_test, status): an = network_tyk2.copy_with_replacements( - name=network_tyk2.name + f"_test_task_status_change" + name=network_tyk2.name + "_test_task_status_change" ) _, taskhub_scoped_key, _ = n4js.assemble_network(an, scope_test) transformation = list(an.edges)[0] @@ -2287,34 +2294,29 @@ def test_get_task_restart_patterns(self, n4js, network_tyk2, scope_test): assert taskhub_grouped_patterns == expected_results - @pytest.mark.xfail(raises=NotImplementedError) def test_resolve_task_restarts( self, scope_test: Scope, n4js_task_restart_policy: Neo4jStore, ): + n4js = n4js_task_restart_policy # get the actioned tasks for each taskhub taskhub_actioned_tasks = {} - for taskhub_scoped_key in n4js_task_restart_policy.query_taskhubs(): + for taskhub_scoped_key in n4js.query_taskhubs(): taskhub_actioned_tasks[taskhub_scoped_key] = set( - n4js_task_restart_policy.get_taskhub_actioned_tasks( - [taskhub_scoped_key] - )[0] + n4js.get_taskhub_actioned_tasks([taskhub_scoped_key])[0] ) - restart_patterns = n4js_task_restart_policy.get_task_restart_patterns( + restart_patterns = n4js.get_task_restart_patterns( list(taskhub_actioned_tasks.keys()) ) - transformation_tasks = defaultdict(list) - for task in n4js_task_restart_policy.query_tasks( - status=TaskStatusEnum.waiting.value - ): - transformation_scoped_key, _ = ( - n4js_task_restart_policy.get_task_transformation( - task, return_gufe=False - ) + # create a map of the transformations and all of the tasks that perform them + transformation_tasks: dict[ScopedKey, list[ScopedKey]] = defaultdict(list) + for task in n4js.query_tasks(status=TaskStatusEnum.waiting.value): + transformation_scoped_key, _ = n4js.get_task_transformation( + task, return_gufe=False ) transformation_tasks[transformation_scoped_key].append(task) @@ -2326,6 +2328,7 @@ def test_resolve_task_restarts( taskhub_scoped_key_no_policy = None taskhub_scoped_key_with_policy = None + # bind taskhub scoped keys to variables for convenience later for taskhub_scoped_key, patterns in restart_patterns.items(): if not patterns: taskhub_scoped_key_no_policy = taskhub_scoped_key @@ -2357,7 +2360,7 @@ def test_resolve_task_restarts( # reduce down all tasks until only the common elements between taskhubs exist tasks_actioned_by_all_taskhubs: List[ScopedKey] = list( - reduce(and_, taskhub_actioned_tasks.values(), set(all_tasks)) + reduce(operator.and_, taskhub_actioned_tasks.values(), set(all_tasks)) ) assert len(tasks_actioned_by_all_taskhubs) == 4 @@ -2366,69 +2369,43 @@ def test_resolve_task_restarts( tasks_to_complete = tasks_actioned_by_all_taskhubs[:2] tasks_to_fail = tasks_actioned_by_all_taskhubs[2:] - # TODO: either check the results after the loop or within it, whichever makes more sense - for task in tasks_to_complete: - n4js_task_restart_policy.set_task_running([task]) - ok_pdrr = ProtocolDAGResultRef( - ok=True, - datetime_created=datetime.utcnow(), - obj_key=task.gufe_key, - scope=task.scope, - ) + complete_tasks(n4js, tasks_to_complete) - _ = n4js_task_restart_policy.set_task_result(task, ok_pdrr) + records = n4js.execute_query( + """ + UNWIND $task_scoped_keys as task_scoped_key + MATCH (task:Task {_scoped_key: task_scoped_key})-[:RESULTS_IN]->(:ProtocolDAGResultRef) + RETURN count(task) as task_count + """, + task_scoped_keys=list(map(str, tasks_to_complete)), + ).records - # this should do nothing to the database state since all - # relationships are removed in the previous method call - # TODO: perhaps counts of the connections will be a good test - n4js_task_restart_policy.set_task_complete([task]) + assert records[0]["task_count"] == 2 + # test the behavior of the compute API for i, task in enumerate(tasks_to_fail): - n4js_task_restart_policy.set_task_running([task]) - - not_ok_pdrr = ProtocolDAGResultRef( - ok=False, - datetime_created=datetime.utcnow(), - obj_key=task.gufe_key, - scope=task.scope, - ) - error_messages = [ f"Error message {repeat}, round {i}" for repeat in range(3) ] - protocol_unit_failures = [] - for j, message in enumerate(error_messages): - puf = ProtocolUnitFailure( - source_key=f"FakeProtocolUnitKey-123{j}", - inputs={}, - outputs={}, - exception=RuntimeError, - traceback=message, - ) - protocol_unit_failures.append(puf) - pdrr_scoped_key = n4js_task_restart_policy.set_task_result( - task, not_ok_pdrr + fail_task( + n4js, + task, + resolve=False, + error_messages=error_messages, ) - # the following mimics what the compute API would do for a failed task - n4js_task_restart_policy.add_protocol_dag_result_ref_traceback( - protocol_unit_failures, pdrr_scoped_key - ) - n4js_task_restart_policy.set_task_error([task]) - # always feed in all tasks to test for side effects - n4js_task_restart_policy.resolve_task_restarts(all_tasks) + n4js.resolve_task_restarts(all_tasks) # both tasks should have the waiting status and the APPLIES # relationship num_retries should have incremented by 1 - query = """ UNWIND $task_scoped_keys as task_scoped_key MATCH (task:Task {`_scoped_key`: task_scoped_key, status: $waiting})<-[:APPLIES {num_retries: 1}]-(:TaskRestartPattern {max_retries: 2}) RETURN count(DISTINCT task) as renewed_waiting_tasks """ - renewed_waiting = n4js_task_restart_policy.execute_query( + renewed_waiting = n4js.execute_query( query, task_scoped_keys=list(map(str, tasks_to_fail)), waiting=TaskStatusEnum.waiting.value, @@ -2442,33 +2419,27 @@ def test_resolve_task_restarts( # but with an additional traceback task_to_cancel, task_to_wait = tasks_to_fail - query = """ - MATCH (task:Task {`_scoped_key`: $task_scoped_key_fail})<-[app:APPLIES]-(:TaskRestartPattern) - SET app.num_retries = 2 - SET task.status = $error - """ - - n4js_task_restart_policy.execute_query( - query, - task_scoped_key_fail=str(task_to_cancel), - task_scoped_key_wait=str(task_to_wait), - error=TaskStatusEnum.error.value, - ) + for _ in range(2): + error_messages = [ + f"Error message {repeat}, round {i}" for repeat in range(3) + ] - n4js_task_restart_policy.resolve_task_restarts(tasks_to_fail) + fail_task( + n4js, + task_to_cancel, + resolve=False, + error_messages=error_messages, + ) - query = """ - MATCH (task:Task {_scoped_key: $task_scoped_key})<-[:ACTIONS]-(:TaskHub {_scoped_key: $taskhub_scoped_key}) - RETURN task - """ + n4js.resolve_task_restarts(tasks_to_fail) - results = n4js_task_restart_policy.execute_query( - query, - task_scoped_key=str(task_to_cancel), - taskhub_scoped_key=str(taskhub_scoped_key_with_policy), + assert tasks_are_not_actioned_on_taskhub( + n4js, + [task_to_cancel], + taskhub_scoped_key_with_policy, ) - assert len(results.records) == 0 + assert tasks_are_errored(n4js, [task_to_cancel]) @pytest.mark.xfail(raises=NotImplementedError) def test_task_actioning_applies_relationship(self): diff --git a/alchemiscale/tests/integration/storage/utils.py b/alchemiscale/tests/integration/storage/utils.py new file mode 100644 index 00000000..91e4a268 --- /dev/null +++ b/alchemiscale/tests/integration/storage/utils.py @@ -0,0 +1,90 @@ +from datetime import datetime + +from gufe.protocols import ProtocolUnitFailure + +from alchemiscale.storage.statestore import Neo4jStore +from alchemiscale import ScopedKey +from alchemiscale.storage.models import TaskStatusEnum, ProtocolDAGResultRef + + +def tasks_are_not_actioned_on_taskhub( + n4js: Neo4jStore, + task_scoped_keys: list[ScopedKey], + taskhub_scoped_key: ScopedKey, +) -> bool: + + actioned_tasks = n4js.get_taskhub_actioned_tasks([taskhub_scoped_key]) + + for task in task_scoped_keys: + if task in actioned_tasks[0].keys(): + return False + return True + + +def tasks_are_errored(n4js: Neo4jStore, task_scoped_keys: list[ScopedKey]) -> bool: + query = """ + UNWIND $task_scoped_keys as task_scoped_key + MATCH (task:Task {_scoped_key: task_scoped_key, status: $error}) + RETURN task + """ + + results = n4js.execute_query( + query, + task_scoped_keys=list(map(str, task_scoped_keys)), + error=TaskStatusEnum.error.value, + ) + + return len(results.records) == len(task_scoped_keys) + + +def complete_tasks( + n4js: Neo4jStore, + tasks: list[ScopedKey], +): + n4js.set_task_running(tasks) + for task in tasks: + ok_pdrr = ProtocolDAGResultRef( + ok=True, + datetime_created=datetime.utcnow(), + obj_key=task.gufe_key, + scope=task.scope, + ) + + _ = n4js.set_task_result(task, ok_pdrr) + + n4js.set_task_complete(tasks) + + +def fail_task( + n4js: Neo4jStore, + task: ScopedKey, + resolve: bool = False, + error_messages: list[str] = [], +) -> None: + n4js.set_task_running([task]) + + not_ok_pdrr = ProtocolDAGResultRef( + ok=False, + datetime_created=datetime.utcnow(), + obj_key=task.gufe_key, + scope=task.scope, + ) + + protocol_unit_failures = [] + for j, message in enumerate(error_messages): + puf = ProtocolUnitFailure( + source_key=f"FakeProtocolUnitKey-123{j}", + inputs={}, + outputs={}, + exception=RuntimeError, + traceback=message, + ) + protocol_unit_failures.append(puf) + + pdrr_scoped_key = n4js.set_task_result(task, not_ok_pdrr) + + n4js.add_protocol_dag_result_ref_traceback(protocol_unit_failures, pdrr_scoped_key) + n4js.set_task_error([task]) + + if resolve: + n4js.resolve_task_restarts([task]) From fe4b87be49d61d90f9b8d943cfd57e2a56db1fa4 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Fri, 23 Aug 2024 12:56:11 -0700 Subject: [PATCH 037/143] resolve restart of tasks with no tracebacks --- alchemiscale/storage/statestore.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index e08ec1c8..a2a3b4a1 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -2944,7 +2944,9 @@ def get_task_restart_patterns( q, taskhub_scoped_keys=list(map(str, taskhubs)) ).records - data = {taskhub: set() for taskhub in taskhubs} + data: dict[ScopedKey, set[tuple[str, int]]] = { + taskhub: set() for taskhub in taskhubs + } for record in records: pattern = record["trp"]["pattern"] @@ -3000,11 +3002,15 @@ def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=Non applies_relationship = record["app"] task = record["task"] taskhub = record["taskhub"] - # TODO: what happens if there is no traceback? i.e. older errored tasks traceback = record["traceback"] task_taskhub_tuple = (task["_scoped_key"], taskhub["_scoped_key"]) + # TODO: remove in v1.0.0 + # tasks that errored, prior to the indtroduction of task restart policies will have no tracebacks in the database + if traceback is None: + cancel_map[task_taskhub_tuple] = True + # we have already determined that the task is to be canceled # is only ever truthy when we say a task needs to be canceled if cancel_map[task_taskhub_tuple]: From 8a6f98041388804f02cf530df1956474457402c5 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Fri, 23 Aug 2024 13:17:31 -0700 Subject: [PATCH 038/143] Replaced many maps with a for loop --- alchemiscale/storage/statestore.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index a2a3b4a1..446aef26 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -2461,9 +2461,15 @@ def add_protocol_dag_result_ref_traceback( except IndexError: raise KeyError("Could not find ProtocolDAGResultRef in database.") - tracebacks = list(map(lambda puf: puf.traceback, protocol_unit_failures)) - source_keys = list(map(lambda puf: puf.source_key, protocol_unit_failures)) - failure_keys = list(map(lambda puf: puf.key, protocol_unit_failures)) + failure_keys = [] + source_keys = [] + tracebacks = [] + + for puf in protocol_unit_failures: + failure_keys.append(puf.key) + source_keys.append(puf.source_key) + tracebacks.append(puf.traceback) + traceback = Traceback(tracebacks, source_keys, failure_keys) _, traceback_node, _ = self._gufe_to_subgraph( From 7c34d9c48a164e18e81ec3108c11ec393dbaed17 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 3 Sep 2024 10:24:05 -0700 Subject: [PATCH 039/143] Added UNWIND to cancel tasks query Added the UNWIND clause to the cancel tasks query. Additionally, I expanded the tests to explicitly test for returned `None`. --- alchemiscale/storage/statestore.py | 32 +++++++++---------- .../integration/storage/test_statestore.py | 21 +++++++----- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index fc8b38b2..87c00097 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -1585,24 +1585,24 @@ def cancel_tasks( none at all. """ - canceled_sks = [] - with self.transaction() as tx: - for t in tasks: - q = f""" - // get our task hub, as well as the task :ACTIONS relationship we want to remove - MATCH (th:TaskHub {{_scoped_key: '{taskhub}'}})-[ar:ACTIONS]->(task:Task {{_scoped_key: '{t}'}}) - DELETE ar - RETURN task - """ - _task = tx.run(q).to_eager_result() + query = """ + UNWIND $task_scoped_keys AS task_scoped_key + MATCH (:TaskHub {_scoped_key: $taskhub_scoped_key})-[ar:ACTIONS]->(task:Task {_scoped_key: task_scoped_key}) + DELETE ar + RETURN task._scoped_key as task_scoped_key + """ + results = self.execute_query( + query, + task_scoped_keys=list(map(str, tasks)), + taskhub_scoped_key=str(taskhub), + ) - if _task.records: - sk = _task.records[0].data()["task"]["_scoped_key"] - canceled_sks.append(ScopedKey.from_str(sk)) - else: - canceled_sks.append(None) + returned_keys = {record["task_scoped_key"] for record in results.records} + filtered_tasks = [ + task if str(task) in returned_keys else None for task in tasks + ] - return canceled_sks + return filtered_tasks def get_taskhub_tasks( self, taskhub: ScopedKey, return_gufe=False diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 802eab94..3ec702a6 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -1214,17 +1214,22 @@ def test_cancel_task(self, n4js, network_tyk2, scope_test): canceled = n4js.cancel_tasks(task_sks[1:3], taskhub_sk) # check that the hub has the contents we expect - q = f"""MATCH (tq:TaskHub {{_scoped_key: '{taskhub_sk}'}})-[:ACTIONS]->(task:Task) - return task - """ + q = """ + MATCH (:TaskHub {_scoped_key: $taskhub_scoped_key})-[:ACTIONS]->(task:Task) + RETURN task._scoped_key AS task_scoped_key + """ - tasks = n4js.execute_query(q) - tasks = [record["task"] for record in tasks.records] + tasks = n4js.execute_query(q, taskhub_scoped_key=str(taskhub_sk)) + tasks = [ + ScopedKey.from_str(record["task_scoped_key"]) for record in tasks.records + ] assert len(tasks) == 8 - assert set([ScopedKey.from_str(t["_scoped_key"]) for t in tasks]) == set( - actioned - ) - set(canceled) + assert set(tasks) == set(actioned) - set(canceled) + + # cancel the remaining tasks and check for Nones + canceled = n4js.cancel_tasks(task_sks, taskhub_sk) + assert canceled == [task_sks[0]] + [None, None] + task_sks[3:] def test_get_taskhub_tasks(self, n4js, network_tyk2, scope_test): an = network_tyk2 From 93eb5f5e9fc8eff6a802658fd2cd11be1384003b Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 4 Sep 2024 10:53:18 -0700 Subject: [PATCH 040/143] Small changes from review --- alchemiscale/storage/statestore.py | 1 - alchemiscale/tests/integration/conftest.py | 23 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 446aef26..ffe4bbc4 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -3031,7 +3031,6 @@ def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=Non if num_retries + 1 > max_retries: cancel_map[task_taskhub_tuple] = True else: - # to_increment.append(task_taskhub_tuple) to_increment.append( (task["_scoped_key"], task_restart_pattern["_scoped_key"]) ) diff --git a/alchemiscale/tests/integration/conftest.py b/alchemiscale/tests/integration/conftest.py index 1a415156..ed9e2b31 100644 --- a/alchemiscale/tests/integration/conftest.py +++ b/alchemiscale/tests/integration/conftest.py @@ -167,6 +167,16 @@ def n4js(graph): return Neo4jStore(graph) +@fixture +def n4js_fresh(graph): + n4js = Neo4jStore(graph) + + n4js.reset() + n4js.initialize() + + return n4js + + @fixture def n4js_task_restart_policy( n4js_fresh: Neo4jStore, network_tyk2: AlchemicalNetwork, scope_test @@ -188,10 +198,13 @@ def n4js_task_restart_policy( list(network_tyk2.edges)[:2], ) + # create 4 tasks for each of the 2 selected transformations task_scoped_keys = n4js.create_tasks( [transformation_1_scoped_key] * 4 + [transformation_2_scoped_key] * 4 ) + # action the tasks for transformation 1 on the taskhub with no policy + # action the tasks for both transformations on the taskhub with a policy assert all(n4js.action_tasks(task_scoped_keys[:4], taskhub_scoped_key_no_policy)) assert all(n4js.action_tasks(task_scoped_keys, taskhub_scoped_key_with_policy)) @@ -207,16 +220,6 @@ def n4js_task_restart_policy( return n4js -@fixture -def n4js_fresh(graph): - n4js = Neo4jStore(graph) - - n4js.reset() - n4js.initialize() - - return n4js - - @fixture(scope="module") def s3objectstore_settings(): os.environ["AWS_ACCESS_KEY_ID"] = "test-key-id" From 0900f392e9811fd7bbe0f44d04f9d05594c32287 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 9 Sep 2024 09:50:09 -0700 Subject: [PATCH 041/143] Chainable now uses the update_wrapper function --- alchemiscale/storage/statestore.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index ffe4bbc4..4af43441 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -9,7 +9,7 @@ from contextlib import contextmanager import json import re -from functools import lru_cache +from functools import lru_cache, update_wrapper from typing import Dict, List, Optional, Union, Tuple, Set from collections import defaultdict from collections.abc import Iterable @@ -187,6 +187,8 @@ def inner(self, *args, **kwargs): kwargs.update(tx=tx) return func(self, *args, **kwargs) + update_wrapper(inner, func) + return inner def execute_query(self, *args, **kwargs): From c8ddafc6e773cdcfbf7daa57b1c83ae0b2982218 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 9 Sep 2024 09:51:09 -0700 Subject: [PATCH 042/143] Updated Traceback class * Removed custom tokenization * Implemented _defaults to allow default tokenization to work --- alchemiscale/storage/models.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index 7fee8156..f9c20e9c 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -223,12 +223,9 @@ def __init__( self.source_keys = source_keys self.failure_keys = failure_keys - def _gufe_tokenize(self): - return hashlib.md5(str(self.tracebacks).encode()).hexdigest() - @classmethod def _defaults(cls): - raise NotImplementedError + return {"tracebacks": [], "source_keys": [], "failure_keys": []} @classmethod def _from_dict(cls, dct): From 2a59499acdb1b9d9f082ae4ad02d90b0e905c105 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 9 Sep 2024 10:18:44 -0700 Subject: [PATCH 043/143] Renamed Traceback to Tracebacks --- alchemiscale/compute/api.py | 2 +- alchemiscale/storage/models.py | 4 ++-- alchemiscale/storage/statestore.py | 24 ++++++++++--------- .../integration/storage/test_statestore.py | 6 ++--- .../tests/integration/storage/utils.py | 2 +- .../tests/unit/test_storage_models.py | 22 ++++++++--------- 6 files changed, 31 insertions(+), 29 deletions(-) diff --git a/alchemiscale/compute/api.py b/alchemiscale/compute/api.py index a50f6d93..f3bff55c 100644 --- a/alchemiscale/compute/api.py +++ b/alchemiscale/compute/api.py @@ -271,7 +271,7 @@ def set_task_result( if protocoldagresultref.ok: n4js.set_task_complete(tasks=[task_sk]) else: - n4js.add_protocol_dag_result_ref_traceback( + n4js.add_protocol_dag_result_ref_tracebacks( pdr.protocol_unit_failures, result_sk ) n4js.set_task_error(tasks=[task_sk]) diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index f9c20e9c..618467ce 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -202,7 +202,7 @@ def __eq__(self, other): return self.pattern == other.pattern -class Traceback(GufeTokenizable): +class Tracebacks(GufeTokenizable): def __init__( self, tracebacks: List[str], source_keys: List[str], failure_keys: List[str] @@ -229,7 +229,7 @@ def _defaults(cls): @classmethod def _from_dict(cls, dct): - return Traceback(**dct) + return Tracebacks(**dct) def _to_dict(self): return { diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 4af43441..81ecd40b 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -33,7 +33,7 @@ TaskHub, TaskRestartPattern, TaskStatusEnum, - Traceback, + Tracebacks, ) from ..strategies import Strategy from ..models import Scope, ScopedKey @@ -2435,7 +2435,7 @@ def get_task_failures(self, task: ScopedKey) -> List[ProtocolDAGResultRef]: """ return self._get_protocoldagresultrefs(q, task) - def add_protocol_dag_result_ref_traceback( + def add_protocol_dag_result_ref_tracebacks( self, protocol_unit_failures: List[ProtocolUnitFailure], protocol_dag_result_ref_scoped_key: ScopedKey, @@ -2472,7 +2472,7 @@ def add_protocol_dag_result_ref_traceback( source_keys.append(puf.source_key) tracebacks.append(puf.traceback) - traceback = Traceback(tracebacks, source_keys, failure_keys) + traceback = Tracebacks(tracebacks, source_keys, failure_keys) _, traceback_node, _ = self._gufe_to_subgraph( traceback.to_shallow_dict(), @@ -2976,13 +2976,13 @@ def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=Non MATCH (task:Task {status: $error, `_scoped_key`: task_scoped_key})<-[app:APPLIES]-(trp:TaskRestartPattern)-[:ENFORCES]->(taskhub:TaskHub) CALL { WITH task - OPTIONAL MATCH (task:Task)-[:RESULTS_IN]->(pdrr:ProtocolDAGResultRef)<-[:DETAILS]-(traceback:Traceback) - RETURN traceback + OPTIONAL MATCH (task:Task)-[:RESULTS_IN]->(pdrr:ProtocolDAGResultRef)<-[:DETAILS]-(tracebacks:Tracebacks) + RETURN tracebacks ORDER BY pdrr.datetime_created DESCENDING LIMIT 1 } - WITH task, traceback, trp, app, taskhub - RETURN task, traceback, trp, app, taskhub + WITH task, tracebacks, trp, app, taskhub + RETURN task, tracebacks, trp, app, taskhub """ results = tx.run( @@ -3010,13 +3010,13 @@ def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=Non applies_relationship = record["app"] task = record["task"] taskhub = record["taskhub"] - traceback = record["traceback"] + _tracebacks = record["tracebacks"] task_taskhub_tuple = (task["_scoped_key"], taskhub["_scoped_key"]) # TODO: remove in v1.0.0 # tasks that errored, prior to the indtroduction of task restart policies will have no tracebacks in the database - if traceback is None: + if _tracebacks is None: cancel_map[task_taskhub_tuple] = True # we have already determined that the task is to be canceled @@ -3027,9 +3027,11 @@ def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=Non num_retries = applies_relationship["num_retries"] max_retries = task_restart_pattern["max_retries"] pattern = task_restart_pattern["pattern"] - tracebacks: List[str] = traceback["tracebacks"] + tracebacks: List[str] = _tracebacks["tracebacks"] - if any([re.search(pattern, message) for message in tracebacks]): + compiled_pattern = re.compile(pattern) + + if any([compiled_pattern.search(message) for message in tracebacks]): if num_retries + 1 > max_retries: cancel_map[task_taskhub_tuple] = True else: diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 5f5a1eb1..c85d97ae 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -1998,12 +1998,12 @@ def test_add_protocol_dag_result_ref_traceback( ) ) - n4js.add_protocol_dag_result_ref_traceback( + n4js.add_protocol_dag_result_ref_tracebacks( protocol_unit_failures, pdrr_scoped_key ) query = """ - MATCH (traceback:Traceback)-[:DETAILS]->(:ProtocolDAGResultRef {`_scoped_key`: $pdrr_scoped_key}) + MATCH (traceback:Tracebacks)-[:DETAILS]->(:ProtocolDAGResultRef {`_scoped_key`: $pdrr_scoped_key}) RETURN traceback """ @@ -2355,7 +2355,7 @@ def test_resolve_task_restarts( # an enforcing task restart policy exists # # Tasks will be set to the error state with a spoofing method, which will create a fake ProtocolDAGResultRef - # and Traceback. This is done since making a protocol fail systematically in the testing environment is not + # and Tracebacks. This is done since making a protocol fail systematically in the testing environment is not # obvious at this time. # reduce down all tasks until only the common elements between taskhubs exist diff --git a/alchemiscale/tests/integration/storage/utils.py b/alchemiscale/tests/integration/storage/utils.py index 91e4a268..43ec3979 100644 --- a/alchemiscale/tests/integration/storage/utils.py +++ b/alchemiscale/tests/integration/storage/utils.py @@ -83,7 +83,7 @@ def fail_task( pdrr_scoped_key = n4js.set_task_result(task, not_ok_pdrr) - n4js.add_protocol_dag_result_ref_traceback(protocol_unit_failures, pdrr_scoped_key) + n4js.add_protocol_dag_result_ref_tracebacks(protocol_unit_failures, pdrr_scoped_key) n4js.set_task_error([task]) if resolve: diff --git a/alchemiscale/tests/unit/test_storage_models.py b/alchemiscale/tests/unit/test_storage_models.py index 55dc872f..391a1063 100644 --- a/alchemiscale/tests/unit/test_storage_models.py +++ b/alchemiscale/tests/unit/test_storage_models.py @@ -4,7 +4,7 @@ NetworkStateEnum, NetworkMark, TaskRestartPattern, - Traceback, + Tracebacks, ) from alchemiscale import ScopedKey @@ -137,40 +137,40 @@ def test_from_dict(self): assert trp_reconstructed.taskhub_scoped_key == original_taskhub_scoped_key -class TestTraceback(object): +class TestTracebacks(object): valid_entry = ["traceback1", "traceback2", "traceback3"] tracebacks_value_error = "`tracebacks` must be a non-empty list of string values" def test_empty_string_element(self): with pytest.raises(ValueError, match=self.tracebacks_value_error): - Traceback(self.valid_entry + [""]) + Tracebacks(self.valid_entry + [""]) def test_non_list_parameter(self): with pytest.raises(ValueError, match=self.tracebacks_value_error): - Traceback(None) + Tracebacks(None) with pytest.raises(ValueError, match=self.tracebacks_value_error): - Traceback(100) + Tracebacks(100) with pytest.raises(ValueError, match=self.tracebacks_value_error): - Traceback("not a list, but still an iterable that yields strings") + Tracebacks("not a list, but still an iterable that yields strings") def test_list_non_string_elements(self): with pytest.raises(ValueError, match=self.tracebacks_value_error): - Traceback(self.valid_entry + [None]) + Tracebacks(self.valid_entry + [None]) def test_empty_list(self): with pytest.raises(ValueError, match=self.tracebacks_value_error): - Traceback([]) + Tracebacks([]) def test_to_dict(self): - tb = Traceback(self.valid_entry) + tb = Tracebacks(self.valid_entry) tb_dict = tb.to_dict() assert len(tb_dict) == 4 - assert tb_dict.pop("__qualname__") == "Traceback" + assert tb_dict.pop("__qualname__") == "Tracebacks" assert tb_dict.pop("__module__") == "alchemiscale.storage.models" # light test of the version key @@ -184,7 +184,7 @@ def test_to_dict(self): assert expected == tb_dict def test_from_dict(self): - tb_orig = Traceback(self.valid_entry) + tb_orig = Tracebacks(self.valid_entry) tb_dict = tb_orig.to_dict() tb_reconstructed: TaskRestartPattern = TaskRestartPattern.from_dict(tb_dict) From 148d048510c142474a4b720a94cc107f87b337c2 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 9 Sep 2024 11:04:19 -0700 Subject: [PATCH 044/143] Updated cancel and increment logic cancel_map has been changed from a defaultdict to a base dict and instead using the dict.get method to return None. Additionally added a set of all task/taskhub pairs that is later used to determine what should be canceled. I've also added grouping on taskhubs so the number of calls to cancel_tasks is minimized. --- alchemiscale/storage/statestore.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 81ecd40b..f88234e7 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -3001,10 +3001,9 @@ def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=Non # None => the pair never had a matching restart pattern # True => at least one patterns max_retries was exceeded # False => at least one regex matched, but no pattern max_retries were exceeded - cancel_map: defaultdict[Tuple[str, str], Optional[bool]] = defaultdict( - lambda: None - ) + cancel_map: dict[Tuple[str, str], Optional[bool]] = {} to_increment: List[Tuple[str, str]] = [] + all_task_taskhub_pairs: set[Tuple[str, str]] = set() for record in results.records: task_restart_pattern = record["trp"] applies_relationship = record["app"] @@ -3014,14 +3013,16 @@ def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=Non task_taskhub_tuple = (task["_scoped_key"], taskhub["_scoped_key"]) + all_task_taskhub_pairs.add(task_taskhub_tuple) + # TODO: remove in v1.0.0 # tasks that errored, prior to the indtroduction of task restart policies will have no tracebacks in the database if _tracebacks is None: cancel_map[task_taskhub_tuple] = True - # we have already determined that the task is to be canceled - # is only ever truthy when we say a task needs to be canceled - if cancel_map[task_taskhub_tuple]: + # we have already determined that the task is to be canceled. + # this is only ever truthy when we say a task needs to be canceled. + if cancel_map.get(task_taskhub_tuple): continue num_retries = applies_relationship["num_retries"] @@ -3051,10 +3052,14 @@ def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=Non # cancel all tasks that didn't trigger any restart patterns (None) # or exceeded a patterns max_retries value (True) - for (task, taskhub), _ in filter( - lambda values: values[1] is True or values[1] is None, cancel_map.items() - ): - self.cancel_tasks([task], taskhub, tx=tx) + cancel_groups: defaultdict[str, list[str]] = defaultdict(list) + for task_taskhub_pair in all_task_taskhub_pairs: + cancel_result = cancel_map.get(task_taskhub_pair) + if cancel_result is True or cancel_result is None: + cancel_groups[task_taskhub_pair[1]].append(task_taskhub_pair[0]) + + for taskhub, tasks in cancel_groups.items(): + self.cancel_tasks(tasks, taskhub, tx=tx) # any remaining tasks must then be okay to switch to waiting renew_waiting_status_query = """ From 645b2e47a9dc1f8a4eb0bf8190899bbb2ff8ad4d Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 9 Sep 2024 11:12:06 -0700 Subject: [PATCH 045/143] Fixed query for deleting the APPLIES relationship --- alchemiscale/storage/statestore.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index f88234e7..03c8f6d5 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -1628,10 +1628,10 @@ def cancel_tasks( MATCH (th:TaskHub {_scoped_key: $taskhub_scoped_key})-[ar:ACTIONS]->(task:Task {_scoped_key: $task_scoped_key}) DELETE ar - WITH task + WITH task, th CALL { - WITH task - MATCH (task)<-[applies:APPLIES]-(:TaskRestartPattern) + WITH task, th + MATCH (task)<-[applies:APPLIES]-(:TaskRestartPattern)-[:ENFORCES]->(th) DELETE applies } From 3a8eeca158f07e6f3c0de5783af72b86018184ed Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 9 Sep 2024 11:17:07 -0700 Subject: [PATCH 046/143] Removed unused testing fixture --- .../tests/integration/interface/conftest.py | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/alchemiscale/tests/integration/interface/conftest.py b/alchemiscale/tests/integration/interface/conftest.py index d7c5da6a..2eb2c996 100644 --- a/alchemiscale/tests/integration/interface/conftest.py +++ b/alchemiscale/tests/integration/interface/conftest.py @@ -89,38 +89,6 @@ def n4js_preloaded( return n4js -from alchemiscale.storage.statestore import Neo4jStore - - -@pytest.fixture -def n4js_task_restart_policy( - n4js_fresh: Neo4jStore, network_tyk2: AlchemicalNetwork, scope_test -): - - n4js = n4js_fresh - - _, taskhub_scoped_key_with_policy, _ = n4js.assemble_network( - network_tyk2, scope_test - ) - - _, taskhub_scoped_key_no_policy, _ = n4js.assemble_network( - network_tyk2.copy_with_replacements(name=network_tyk2.name + "_no_policy"), - scope_test, - ) - - transformation_1_scoped_key, transformation_2_scoped_key = map( - lambda transformation: n4js.get_scoped_key(transformation, scope_test), - network_tyk2.edges[:2], - ) - - task_scoped_keys = n4js.create_tasks( - [transformation_1_scoped_key] * 4 + [transformation_2_scoped_key] * 4 - ) - - assert all(n4js.action_tasks(task_scoped_keys, taskhub_scoped_key_no_policy)) - assert all(n4js.action_tasks(task_scoped_keys, taskhub_scoped_key_with_policy)) - - @pytest.fixture(scope="module") def scope_consistent_token_data_depends_override(scope_test): """Make a consistent helper to provide an override to the api.app while still accessing fixtures""" From ea6e66f4daa087934fffe6c0133e80911bc8a3fb Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 9 Sep 2024 11:45:05 -0700 Subject: [PATCH 047/143] Clarified comment and added complimentary assertion Also expanded test to check behavior of the task that was meant to be waiting. --- .../integration/storage/test_statestore.py | 50 +++++++++++++++++-- .../tests/integration/storage/utils.py | 16 ++++++ 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index c85d97ae..9a5e71ef 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -36,6 +36,7 @@ fail_task, tasks_are_errored, tasks_are_not_actioned_on_taskhub, + tasks_are_waiting, ) @@ -2351,8 +2352,8 @@ def test_resolve_task_restarts( # # 1. Completed Tasks do not have an actions relationship with either TaskHub # 2. A Task entering the error state is switched back to waiting if any restart patterns apply - # 3. A Task entering the error state is left in the error state if no patterns apply and only the TaskHub with - # an enforcing task restart policy exists + # 3. A Task entering the error state is left in the error state if no patterns apply and only the TaskHub without + # an enforcing task restart policy actions the Task # # Tasks will be set to the error state with a spoofing method, which will create a fake ProtocolDAGResultRef # and Tracebacks. This is done since making a protocol fail systematically in the testing environment is not @@ -2360,7 +2361,7 @@ def test_resolve_task_restarts( # reduce down all tasks until only the common elements between taskhubs exist tasks_actioned_by_all_taskhubs: List[ScopedKey] = list( - reduce(operator.and_, taskhub_actioned_tasks.values(), set(all_tasks)) + reduce(operator.and_, taskhub_actioned_tasks.values()) ) assert len(tasks_actioned_by_all_taskhubs) == 4 @@ -2415,10 +2416,10 @@ def test_resolve_task_restarts( # we want the resolve restarts to cancel a task. # deconstruct the tasks to fail, where the first - # one will be cancelled and the second will once again be continued - # but with an additional traceback + # one will be cancelled and the second will continue to wait task_to_cancel, task_to_wait = tasks_to_fail + # error out the first task for _ in range(2): error_messages = [ f"Error message {repeat}, round {i}" for repeat in range(3) @@ -2433,14 +2434,53 @@ def test_resolve_task_restarts( n4js.resolve_task_restarts(tasks_to_fail) + # check that it is no longer actioned on the enforced taskhub assert tasks_are_not_actioned_on_taskhub( n4js, [task_to_cancel], taskhub_scoped_key_with_policy, ) + # check that it is still actioned on the unenforced taskhub + assert not tasks_are_not_actioned_on_taskhub( + n4js, + [task_to_cancel], + taskhub_scoped_key_no_policy, + ) + + # it should still be errored though! assert tasks_are_errored(n4js, [task_to_cancel]) + # fail the second task one time + error_messages = [ + f"Error message {repeat}, round {i}" for repeat in range(3) + ] + + fail_task( + n4js, + task_to_wait, + resolve=False, + error_messages=error_messages, + ) + + n4js.resolve_task_restarts(tasks_to_fail) + + # check that the waiting task is actioned on both taskhubs + assert not tasks_are_not_actioned_on_taskhub( + n4js, + [task_to_wait], + taskhub_scoped_key_with_policy, + ) + + assert not tasks_are_not_actioned_on_taskhub( + n4js, + [task_to_wait], + taskhub_scoped_key_no_policy, + ) + + # it should be waiting + assert tasks_are_waiting(n4js, [task_to_wait]) + @pytest.mark.xfail(raises=NotImplementedError) def test_task_actioning_applies_relationship(self): raise NotImplementedError diff --git a/alchemiscale/tests/integration/storage/utils.py b/alchemiscale/tests/integration/storage/utils.py index 43ec3979..40514a53 100644 --- a/alchemiscale/tests/integration/storage/utils.py +++ b/alchemiscale/tests/integration/storage/utils.py @@ -37,6 +37,22 @@ def tasks_are_errored(n4js: Neo4jStore, task_scoped_keys: list[ScopedKey]) -> bo return len(results.records) == len(task_scoped_keys) +def tasks_are_waiting(n4js: Neo4jStore, task_scoped_keys: list[ScopedKey]) -> bool: + query = """ + UNWIND $task_scoped_keys as task_scoped_key + MATCH (task:Task {_scoped_key: task_scoped_key, status: $waiting}) + RETURN task + """ + + results = n4js.execute_query( + query, + task_scoped_keys=list(map(str, task_scoped_keys)), + waiting=TaskStatusEnum.waiting.value, + ) + + return len(results.records) == len(task_scoped_keys) + + def complete_tasks( n4js: Neo4jStore, tasks: list[ScopedKey], From 7a4b1149f63468c30f2c38404e91944317598f52 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 12 Sep 2024 18:27:12 -0700 Subject: [PATCH 048/143] Small changes to Tracebacks We don't want to change `_defaults()` from what's done in the base class unless we have real default values to leave out of the hash. --- alchemiscale/storage/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index 618467ce..1d8e1679 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -225,11 +225,11 @@ def __init__( @classmethod def _defaults(cls): - return {"tracebacks": [], "source_keys": [], "failure_keys": []} + return super()._defaults() @classmethod def _from_dict(cls, dct): - return Tracebacks(**dct) + return cls(**dct) def _to_dict(self): return { From 4ac3e46e1565355a464417d443077ed4cb933233 Mon Sep 17 00:00:00 2001 From: LilDojd Date: Wed, 18 Sep 2024 16:13:46 +0400 Subject: [PATCH 049/143] refactor(auth): replace passlib CryptContext with bcrypt-based handler --- alchemiscale/security/auth.py | 62 ++++++++++++++++++++++-- alchemiscale/tests/unit/test_security.py | 7 +++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index f4782ea1..a0a8403b 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -4,21 +4,73 @@ """ -from datetime import datetime, timedelta -from typing import Union, Optional import secrets +from datetime import datetime, timedelta +from typing import Optional, Union +import bcrypt from fastapi import HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt -from passlib.context import CryptContext from pydantic import BaseModel -from .models import Token, TokenData, CredentialedEntity +from .models import CredentialedEntity, Token, TokenData + +MAX_PASSWORD_SIZE = 4096 +_dummy_secret = "dummy" + + +class BcryptPasswordHandler(object): + rounds: int = 12 + ident: str = "$2b$" + salt: str = "" + checksum: str = "" + + def __init__(self, rounds: int = 12, ident: str = "$2b$"): + self.rounds = rounds + self.ident = ident + + def _get_config(self) -> bytes: + config = bcrypt.gensalt( + self.rounds, prefix=self.ident.strip("$").encode("ascii") + ) + self.salt = config.decode("ascii")[len(self.ident) + 3 :] + return config + + def to_string(self) -> str: + return "%s%02d$%s%s" % (self.ident, self.rounds, self.salt, self.checksum) + + def hash(self, key: str) -> str: + validate_secret(key) + config = self._get_config() + hash_ = bcrypt.hashpw(key.encode("utf-8"), config) + if not hash_.startswith(config) or len(hash_) != len(config) + 31: + raise ValueError("bcrypt.hashpw returned an invalid hash") + self.checksum = hash_[-31:].decode("ascii") + return self.to_string() + + def verify(self, key: str, hash: str) -> bool: + validate_secret(key) + + if hash is None: + self.hash(_dummy_secret) + return False + + return bcrypt.checkpw(key.encode("utf-8"), hash.encode("utf-8")) -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") +pwd_context = BcryptPasswordHandler() + + +def validate_secret(secret): + """ensure secret has correct type & size""" + if not isinstance(secret, (str, bytes)): + raise TypeError("secret must be a string or bytes") + if len(secret) > MAX_PASSWORD_SIZE: + raise ValueError( + f"secret is too long, maximum length is {MAX_PASSWORD_SIZE} characters" + ) def generate_secret_key(): diff --git a/alchemiscale/tests/unit/test_security.py b/alchemiscale/tests/unit/test_security.py index fca015e3..0f120414 100644 --- a/alchemiscale/tests/unit/test_security.py +++ b/alchemiscale/tests/unit/test_security.py @@ -30,3 +30,10 @@ def test_token_data(secret_key): token_data = auth.get_token_data(token=token, secret_key=secret_key) assert token_data.scopes == ["*-*-*"] + + +def test_bcrypt_password_handler(): + handler = auth.BcryptPasswordHandler() + hash_ = handler.hash("test") + assert handler.verify("test", hash_) + assert not handler.verify("deadbeef", hash_) From 71e0efb19c31e5e4337294f2aaa46525623a5017 Mon Sep 17 00:00:00 2001 From: LilDojd Date: Wed, 18 Sep 2024 16:15:32 +0400 Subject: [PATCH 050/143] :recycle:(deps): remove passlib dependency fixes OpenFreeEnergy/alchemiscale/#304 --- devtools/conda-envs/alchemiscale-server.yml | 1 - devtools/conda-envs/test.yml | 3 +-- docs/conf.py | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index a175762f..2b764e8c 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -29,7 +29,6 @@ dependencies: - uvicorn - gunicorn - python-jose - - passlib - bcrypt - python-multipart - starlette diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index a23320c2..144051cb 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -17,7 +17,7 @@ dependencies: - monotonic - docker-py # for grolt - # user client printing + # user client printing - rich ## object store @@ -28,7 +28,6 @@ dependencies: - uvicorn - gunicorn - python-jose - - passlib - bcrypt - python-multipart - starlette diff --git a/docs/conf.py b/docs/conf.py index 895c4f15..fdf43e2b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,6 @@ "jose", "networkx", "numpy", - "passlib", "py2neo", "pydantic", "starlette", From 5701fca00640cfdb492ac7d73663abcdb9e43a76 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 19 Sep 2024 23:40:39 -0700 Subject: [PATCH 051/143] Added link in README to landing page --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c83ade3d..9a20fee3 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ **alchemiscale**: a high-throughput alchemical free energy execution system for use with HPC, cloud, bare metal, and Folding@Home +Learn more about the project, including how to get involved at: https://alchemiscale.org + ---

alchemiscale logo by Jenke Scheen is marked with CC0 1.0

From 6066796898683b19c8b4e5e73e0b456466cc2866 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 24 Sep 2024 15:54:01 -0700 Subject: [PATCH 052/143] Fix for Tracebacks unit tests The addition of source_keys and failure_keys was not included in the unit tests so all initializations of Tracebacks failed. I've added default values for the test class. --- .../tests/unit/test_storage_models.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/alchemiscale/tests/unit/test_storage_models.py b/alchemiscale/tests/unit/test_storage_models.py index 391a1063..f6916430 100644 --- a/alchemiscale/tests/unit/test_storage_models.py +++ b/alchemiscale/tests/unit/test_storage_models.py @@ -140,35 +140,45 @@ def test_from_dict(self): class TestTracebacks(object): valid_entry = ["traceback1", "traceback2", "traceback3"] + source_keys = ["ProtocolUnit-ABC123", "ProtocolUnit-DEF456", "ProtocolUnit-GHI789"] + failure_keys = [ + "ProtocolUnitFailure-ABC123", + "ProtocolUnitFailure-DEF456", + "ProtocolUnitFailure-GHI789", + ] tracebacks_value_error = "`tracebacks` must be a non-empty list of string values" def test_empty_string_element(self): with pytest.raises(ValueError, match=self.tracebacks_value_error): - Tracebacks(self.valid_entry + [""]) + Tracebacks(self.valid_entry + [""], self.source_keys, self.failure_keys) def test_non_list_parameter(self): with pytest.raises(ValueError, match=self.tracebacks_value_error): - Tracebacks(None) + Tracebacks(None, self.source_keys, self.failure_keys) with pytest.raises(ValueError, match=self.tracebacks_value_error): - Tracebacks(100) + Tracebacks(100, self.source_keys, self.failure_keys) with pytest.raises(ValueError, match=self.tracebacks_value_error): - Tracebacks("not a list, but still an iterable that yields strings") + Tracebacks( + "not a list, but still an iterable that yields strings", + self.source_keys, + self.failure_keys, + ) def test_list_non_string_elements(self): with pytest.raises(ValueError, match=self.tracebacks_value_error): - Tracebacks(self.valid_entry + [None]) + Tracebacks(self.valid_entry + [None], self.source_keys, self.failure_keys) def test_empty_list(self): with pytest.raises(ValueError, match=self.tracebacks_value_error): - Tracebacks([]) + Tracebacks([], self.source_keys, self.failure_keys) def test_to_dict(self): - tb = Tracebacks(self.valid_entry) + tb = Tracebacks(self.valid_entry, self.source_keys, self.failure_keys) tb_dict = tb.to_dict() - assert len(tb_dict) == 4 + assert len(tb_dict) == 6 assert tb_dict.pop("__qualname__") == "Tracebacks" assert tb_dict.pop("__module__") == "alchemiscale.storage.models" @@ -179,12 +189,16 @@ def test_to_dict(self): except KeyError: raise AssertionError("expected to find :version:") - expected = {"tracebacks": self.valid_entry} + expected = { + "tracebacks": self.valid_entry, + "source_keys": self.source_keys, + "failure_keys": self.failure_keys, + } assert expected == tb_dict def test_from_dict(self): - tb_orig = Tracebacks(self.valid_entry) + tb_orig = Tracebacks(self.valid_entry, self.source_keys, self.failure_keys) tb_dict = tb_orig.to_dict() tb_reconstructed: TaskRestartPattern = TaskRestartPattern.from_dict(tb_dict) From fcf77a097949734938c79d48d978277ed522fecf Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Wed, 25 Sep 2024 08:19:28 -0700 Subject: [PATCH 053/143] Added API endpoints for managing restart policies * add_task_restart_patterns * remove_task_restart_patterns * get_task_restart_patterns * set_task_restart_patterns_max_retries Additionally, I added the get_taskhubs method to Neo4jStore since get_taskhub will only get the taskhub for a single network at a time. It might make sense to replace the old method with this new one. --- alchemiscale/interface/api.py | 60 ++++++++++++++++++++++++++++++ alchemiscale/storage/models.py | 1 + alchemiscale/storage/statestore.py | 40 ++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/alchemiscale/interface/api.py b/alchemiscale/interface/api.py index 0784ea49..ba9a92f7 100644 --- a/alchemiscale/interface/api.py +++ b/alchemiscale/interface/api.py @@ -947,6 +947,66 @@ def get_task_status( return status[0].value +# TODO docstring +@router.post("/networks/{network_scoped_key}/restartpolicy/add") +def add_task_restart_patterns( + network_scoped_key: str, + *, + patterns: list[str], + number_of_retries: int, + request: Request, + n4js: Neo4jStore = Depends(get_n4js_depends), + token: TokenData = Depends(get_token_data_depends), +): + + taskhub_scoped_key = n4js.get_taskhub(ScopedKey.from_str(network_scoped_key)) + n4js.add_task_restart_patterns(taskhub_scoped_key, patterns, number_of_retries) + + +# TODO docstring +@router.post("/networks/{network_scoped_key}/restartpolicy/remove") +def remove_task_restart_patterns( + network_scoped_key: str, + *, + patterns: list[str], + n4js: Neo4jStore = Depends(get_n4js_depends), + token: TokenData = Depends(get_token_data_depends), +): + taskhub_scoped_key = n4js.get_taskhub(ScopedKey.from_str(network_scoped_key)) + n4js.remove_task_restart_patterns(taskhub_scoped_key, patterns) + + +# TODO docstring +@router.get("/bulk/networks/restartpolicy/get") +def get_task_restart_patterns( + *, + networks: list[str], + n4js: Neo4jStore = Depends(get_n4js_depends), + token: TokenData = Depends(get_token_data_depends), +) -> dict[str, set[tuple[str, int]]]: + + network_scoped_keys = [ScopedKey.from_str(network) for network in networks] + taskhubs_scoped_keys = n4js.get_taskhubs(network_scoped_keys) + restart_patterns = n4js.get_task_restart_patterns(network_scoped_key) + + return restart_patterns + + +@router.post("/networks/{network_scoped_key}/restartpolicy/maxretries") +def set_task_restart_patterns_max_retries( + network_scoped_key: str, + *, + patterns: list[str], + max_retries: int, + n4js: Neo4jStore = Depends(get_n4js_depends), + token: TokenData = Depends(get_token_data_depends), +): + taskhub_scoped_key = n4js.get_taskhub(ScopedKey.from_str(network_scoped_key)) + n4js.set_task_restart_patterns_max_retries( + taskhub_scoped_key, patterns, max_retries + ) + + @router.get("/tasks/{task_scoped_key}/transformation") def get_task_transformation( task_scoped_key, diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index 1d8e1679..59c6659f 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -202,6 +202,7 @@ def __eq__(self, other): return self.pattern == other.pattern +# TODO: docstrings class Tracebacks(GufeTokenizable): def __init__( diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 03c8f6d5..28af9a45 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -1249,6 +1249,46 @@ def get_taskhub( else: return ScopedKey.from_str(node["_scoped_key"]) + # TODO: write docstring + # TODO: can we replace the above method with this one? + def get_taskhubs( + self, network_scoped_keys: list[ScopedKey], return_gufe: bool = False + ) -> list[Union[ScopedKey, TaskHub]]: + # TODO: this could fail better, report all instances rather than first + for network_scoped_key in network_scoped_keys: + if network.qualname != "AlchemicalNetwork": + raise ValueError( + "`network` ScopedKey does not correspond to an `AlchemicalNetwork`" + ) + + query = """ + UNWIND $network_scoped_keys AS network_scoped_key + MATCH (th:TaskHub {network: network_scoped_key})-[:PERFORMS]->(an:AlchemicalNetwork) + RETURN th, an + """ + + query_results = self.execute_query( + query, network_scoped_keys=list(map(str, network_scoped_key)) + ) + + def _node_to_gufe(node): + return self._subgraph_to_gufe([node], node)[node] + + def _node_to_scoped_key(node): + return ScopedKey.from_str(node["_scoped_key"]) + + transform_function = _node_to_gufe if return_gufe else _node_to_scoped_key + transform_results = defaultdict(None) + for record in query_results.records: + node = record_data_to_node(record["th"]) + network_scoped_key = record["an"]["_scoped_key"] + transform_results[network_scoped_key] = transform_function(node) + + return [ + transform_results[str(network_scoped_key)] + for network_scoped_key in network_scoped_keys + ] + def delete_taskhub( self, network: ScopedKey, From 7249b1181a9a6c8c714cccc4d4d1a00e5f636b2a Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 25 Sep 2024 23:00:00 -0700 Subject: [PATCH 054/143] Deploy openfe 1.1.0; update deployment action for OpenFreeEnergy namespace --- .github/workflows/deploy-docker.yml | 6 +++--- devtools/conda-envs/alchemiscale-client.yml | 2 +- devtools/conda-envs/alchemiscale-compute.yml | 2 +- devtools/conda-envs/alchemiscale-server.yml | 2 +- devtools/conda-envs/test.yml | 2 +- docker/alchemiscale-compute/Dockerfile | 6 +++--- docker/alchemiscale-server/Dockerfile | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy-docker.yml b/.github/workflows/deploy-docker.yml index f79e29cb..a060d8a5 100644 --- a/.github/workflows/deploy-docker.yml +++ b/.github/workflows/deploy-docker.yml @@ -16,7 +16,7 @@ on: env: REGISTRY: ghcr.io - NAMESPACE: openforcefield + NAMESPACE: OpenFreeEnergy jobs: build-and-push-image: @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@v3 - name: Log in to the Container registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -47,7 +47,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ matrix.image }} tags: | diff --git a/devtools/conda-envs/alchemiscale-client.yml b/devtools/conda-envs/alchemiscale-client.yml index b64ec483..ba7b942e 100644 --- a/devtools/conda-envs/alchemiscale-client.yml +++ b/devtools/conda-envs/alchemiscale-client.yml @@ -9,7 +9,7 @@ dependencies: # alchemiscale dependencies - gufe=1.0.0 - - openfe=1.0.1 + - openfe=1.1.0 - requests - click - httpx diff --git a/devtools/conda-envs/alchemiscale-compute.yml b/devtools/conda-envs/alchemiscale-compute.yml index 9d563bd4..a1241c89 100644 --- a/devtools/conda-envs/alchemiscale-compute.yml +++ b/devtools/conda-envs/alchemiscale-compute.yml @@ -9,7 +9,7 @@ dependencies: # alchemiscale dependencies - gufe=1.0.0 - - openfe=1.0.1 + - openfe=1.1.0 - requests - click - httpx diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index a175762f..02ad1ea7 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -9,7 +9,7 @@ dependencies: # alchemiscale dependencies - gufe=1.0.0 - - openfe=1.0.1 + - openfe=1.1.0 - requests - click diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index a23320c2..ec9a6955 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -8,7 +8,7 @@ dependencies: # alchemiscale dependencies - gufe>=1.0.0 - - openfe>=1.0.1 + - openfe>=1.1.0 - pydantic<2.0 ## state store diff --git a/docker/alchemiscale-compute/Dockerfile b/docker/alchemiscale-compute/Dockerfile index e42e29f9..cd56ffc9 100644 --- a/docker/alchemiscale-compute/Dockerfile +++ b/docker/alchemiscale-compute/Dockerfile @@ -1,7 +1,7 @@ -FROM mambaorg/micromamba:1.4.1 +FROM mambaorg/micromamba:1.5.10 -LABEL org.opencontainers.image.source=https://github.com/openforcefield/alchemiscale -LABEL org.opencontainers.image.description="deployable compute services for an alchemiscale server" +LABEL org.opencontainers.image.source=https://github.com/OpenFreeEnergy/alchemiscale +LABEL org.opencontainers.image.description="deployable compute services for alchemiscale" LABEL org.opencontainers.image.licenses=MIT # Don't buffer stdout & stderr streams, so if there is a crash no partial buffer output is lost diff --git a/docker/alchemiscale-server/Dockerfile b/docker/alchemiscale-server/Dockerfile index 5882240f..0ca1b2c5 100644 --- a/docker/alchemiscale-server/Dockerfile +++ b/docker/alchemiscale-server/Dockerfile @@ -1,6 +1,6 @@ -FROM mambaorg/micromamba:1.4.1 +FROM mambaorg/micromamba:1.5.10 -LABEL org.opencontainers.image.source=https://github.com/openforcefield/alchemiscale +LABEL org.opencontainers.image.source=https://github.com/OpenFreeEnergy/alchemiscale LABEL org.opencontainers.image.description="a high-throughput alchemical free energy execution system for use with HPC, cloud, bare metal, and Folding@Home" LABEL org.opencontainers.image.licenses=MIT From 438440962affb8b164e164aa2d28873e3aceff1c Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 25 Sep 2024 23:16:39 -0700 Subject: [PATCH 055/143] openforcefield -> OpenFreeEnergy where appropriate --- .github/workflows/ci-integration.yml | 2 +- README.md | 4 ++-- devtools/conda-envs/alchemiscale-client.yml | 2 +- devtools/conda-envs/alchemiscale-compute.yml | 2 +- devtools/conda-envs/alchemiscale-server.yml | 2 +- docker/alchemiscale-server/.env.testing | 2 +- docs/compute.rst | 8 ++++---- docs/deployment.rst | 2 +- docs/development.rst | 14 +++++++------- docs/user_guide.rst | 2 +- pyproject.toml | 2 +- 11 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci-integration.yml b/.github/workflows/ci-integration.yml index 9a961fe8..4817e2c9 100644 --- a/.github/workflows/ci-integration.yml +++ b/.github/workflows/ci-integration.yml @@ -53,7 +53,7 @@ jobs: pytest -v --cov=alchemiscale --cov-report=xml alchemiscale/tests - name: codecov - if: ${{ github.repository == 'openforcefield/alchemiscale' + if: ${{ github.repository == 'OpenFreeEnergy/alchemiscale' && github.event != 'schedule' }} uses: codecov/codecov-action@v2 with: diff --git a/README.md b/README.md index 9a20fee3..fef337aa 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ --- -[![build](https://github.com/openforcefield/alchemiscale/actions/workflows/ci-integration.yml/badge.svg)](https://github.com/openforcefield/alchemiscale/actions/workflows/ci-integration.yml) -[![coverage](https://codecov.io/gh/openforcefield/alchemiscale/branch/main/graph/badge.svg)](https://codecov.io/gh/openforcefield/alchemiscale) +[![build](https://github.com/OpenFreeEnergy/alchemiscale/actions/workflows/ci-integration.yml/badge.svg)](https://github.com/OpenFreeEnergy/alchemiscale/actions/workflows/ci-integration.yml) +[![coverage](https://codecov.io/gh/OpenFreeEnergy/alchemiscale/branch/main/graph/badge.svg)](https://codecov.io/gh/OpenFreeEnergy/alchemiscale) [![Documentation Status](https://readthedocs.org/projects/alchemiscale/badge/?version=latest)](https://alchemiscale.readthedocs.io/en/latest/?badge=latest) diff --git a/devtools/conda-envs/alchemiscale-client.yml b/devtools/conda-envs/alchemiscale-client.yml index ba7b942e..a3cdf3fe 100644 --- a/devtools/conda-envs/alchemiscale-client.yml +++ b/devtools/conda-envs/alchemiscale-client.yml @@ -32,4 +32,4 @@ dependencies: - plyvel - pip: - - git+https://github.com/openforcefield/alchemiscale.git@v0.5.0 + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.0 diff --git a/devtools/conda-envs/alchemiscale-compute.yml b/devtools/conda-envs/alchemiscale-compute.yml index a1241c89..7823841f 100644 --- a/devtools/conda-envs/alchemiscale-compute.yml +++ b/devtools/conda-envs/alchemiscale-compute.yml @@ -24,4 +24,4 @@ dependencies: - openmmforcefields>=0.14.1 - pip: - - git+https://github.com/openforcefield/alchemiscale.git@v0.5.0 + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.0 diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index 02ad1ea7..6d0d0564 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -50,4 +50,4 @@ dependencies: - plyvel - pip: - - git+https://github.com/openforcefield/alchemiscale.git@v0.5.0 + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.0 diff --git a/docker/alchemiscale-server/.env.testing b/docker/alchemiscale-server/.env.testing index 67fe7fbf..2cf76a75 100644 --- a/docker/alchemiscale-server/.env.testing +++ b/docker/alchemiscale-server/.env.testing @@ -24,4 +24,4 @@ ACME_EMAIL=foo@bar.com HOST_DOMAIN=localhost # alchemiscale -ALCHEMISCALE_DOCKER_IMAGE=ghcr.io/openforcefield/alchemiscale:feat-add_docker_compose +ALCHEMISCALE_DOCKER_IMAGE=ghcr.io/OpenFreeEnergy/alchemiscale:latest diff --git a/docs/compute.rst b/docs/compute.rst index d93bca58..29724a41 100644 --- a/docs/compute.rst +++ b/docs/compute.rst @@ -14,7 +14,7 @@ This documentation will expand over time as these variants become available; for In all cases, you will need to define a configuration file for your compute services to consume on startup. A template for this file can be found here; replace ``$ALCHEMISCALE_VERSION`` with the version tag, e.g. ``v0.1.4``, you have deployed for your server:: - https://raw.githubusercontent.com/openforcefield/alchemiscale/$ALCHEMISCALE_VERSION/devtools/configs/synchronous-compute-settings.yaml + https://raw.githubusercontent.com/OpenFreeEnergy/alchemiscale/$ALCHEMISCALE_VERSION/devtools/configs/synchronous-compute-settings.yaml *********** @@ -35,7 +35,7 @@ Deploying with conda/mamba To deploy via ``conda``/``mamba``, first create an environment (we recommend ``mamba`` for its performance):: mamba env create -n alchemiscale-compute-$ALCHEMISCALE_VERSION \ - -f https://raw.githubusercontent.com/openforcefield/alchemiscale/$ALCHEMISCALE_VERSION/devtools/conda-envs/alchemiscale-compute.yml + -f https://raw.githubusercontent.com/OpenFreeEnergy/alchemiscale/$ALCHEMISCALE_VERSION/devtools/conda-envs/alchemiscale-compute.yml Once created, activate the environment in your current shell:: @@ -55,7 +55,7 @@ Assuming your configuration file is in the current working directory, to deploy docker run --gpus all \ --rm \ - -v $(pwd):/mnt ghcr.io/openforcefield/alchemiscale-compute:$ALCHEMISCALE_VERSION \ + -v $(pwd):/mnt ghcr.io/OpenFreeEnergy/alchemiscale-compute:$ALCHEMISCALE_VERSION \ compute synchronous -c /mnt/synchronous-compute-settings.yaml @@ -157,7 +157,7 @@ We define a k8s `Deployment`_ featuring a single container spec as the file ``co spec: containers: - name: alchemiscale-synchronous-container - image: ghcr.io/openforcefield/alchemiscale-compute:$ALCHEMISCALE_VERSION + image: ghcr.io/OpenFreeEnergy/alchemiscale-compute:$ALCHEMISCALE_VERSION args: ["compute", "synchronous", "-c", "/mnt/settings/synchronous-compute-settings.yaml"] resources: limits: diff --git a/docs/deployment.rst b/docs/deployment.rst index fafa9abb..9c6766cb 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -36,7 +36,7 @@ First install the `docker engine Date: Fri, 27 Sep 2024 15:47:56 -0700 Subject: [PATCH 056/143] Small doc typo fix --- docs/operations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operations.rst b/docs/operations.rst index 0a14610e..7c18b2b6 100644 --- a/docs/operations.rst +++ b/docs/operations.rst @@ -92,7 +92,7 @@ To later restore from a database dump, navigate to the directory containing your **With the Neo4j service shut down**, choose ``$DUMP_DATE`` and set ``$NEO4J_VERSION`` to the version of Neo4j you are using, then run:: # create a copy of the timestamped dump to `neo4j.dump` - cp ${BACKUPS_DIR}/neo4j-$(date -I).dump ${BACKUPS_DIR}/neo4j.dump + cp ${BACKUPS_DIR}/neo4j-${DUMP_DATE}.dump ${BACKUPS_DIR}/neo4j.dump # load the dump `neo4j.dump` docker run --rm \ From 3bcdb88b88f0b5b3a49248ee119a4e53cbcb0338 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Fri, 27 Sep 2024 17:02:48 -0700 Subject: [PATCH 057/143] Prepping prod environments for v0.5.1 release --- devtools/conda-envs/alchemiscale-client.yml | 2 +- devtools/conda-envs/alchemiscale-compute.yml | 2 +- devtools/conda-envs/alchemiscale-server.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/devtools/conda-envs/alchemiscale-client.yml b/devtools/conda-envs/alchemiscale-client.yml index a3cdf3fe..fc462e9c 100644 --- a/devtools/conda-envs/alchemiscale-client.yml +++ b/devtools/conda-envs/alchemiscale-client.yml @@ -32,4 +32,4 @@ dependencies: - plyvel - pip: - - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.0 + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.1 diff --git a/devtools/conda-envs/alchemiscale-compute.yml b/devtools/conda-envs/alchemiscale-compute.yml index 7823841f..eb4dc7b7 100644 --- a/devtools/conda-envs/alchemiscale-compute.yml +++ b/devtools/conda-envs/alchemiscale-compute.yml @@ -24,4 +24,4 @@ dependencies: - openmmforcefields>=0.14.1 - pip: - - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.0 + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.1 diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index 6d0d0564..e7205497 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -50,4 +50,4 @@ dependencies: - plyvel - pip: - - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.0 + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.1 From cea16bc46346428f222d5d3b0fd604aae70ab298 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 30 Sep 2024 20:34:48 -0700 Subject: [PATCH 058/143] Added untested client method for task restart policies --- alchemiscale/interface/api.py | 14 ++++++++++- alchemiscale/interface/client.py | 40 ++++++++++++++++++++++++++++++ alchemiscale/storage/statestore.py | 7 ++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/alchemiscale/interface/api.py b/alchemiscale/interface/api.py index ba9a92f7..f3ba0548 100644 --- a/alchemiscale/interface/api.py +++ b/alchemiscale/interface/api.py @@ -976,8 +976,20 @@ def remove_task_restart_patterns( n4js.remove_task_restart_patterns(taskhub_scoped_key, patterns) +# TODO: docstring +@router.get("/networks/{network_scoped_key}/restartpolicy/clear") +def clear_task_restart_patterns( + network_scoped_key: str, + *, + n4js: Neo4jStore = Depends(get_n4js_depends), + token: TokenData = Depends(get_token_data_depends), +): + taskhub_scoped_key = n4js.get_taskhub(ScopedKey.from_str(network_scoped_key)) + n4js.clear_task_restart_patterns(taskhub_scoped_key) + + # TODO docstring -@router.get("/bulk/networks/restartpolicy/get") +@router.post("/bulk/networks/restartpolicy/get") def get_task_restart_patterns( *, networks: list[str], diff --git a/alchemiscale/interface/client.py b/alchemiscale/interface/client.py index 2699ce4b..d5f3672e 100644 --- a/alchemiscale/interface/client.py +++ b/alchemiscale/interface/client.py @@ -1740,3 +1740,43 @@ def get_task_failures( ) return pdrs + + def add_task_restart_patterns( + self, + network_scoped_key: ScopedKey, + patterns: list[str], + num_allowed_restarts: int, + ) -> ScopedKey: + data = {"patterns": patterns, "number_of_retries": num_allowed_restarts} + self._post_resource("/networks/{network_scoped_key}/restartpolicy/add", data) + + def get_task_restart_patterns( + self, network_scoped_key: ScopedKey + ) -> dict[str, int]: + data = {network: str(network_scoped_key)} + mapped_patterns = self._post_resource( + "/bulk/networks/restartpolicy/get", data=data + ) + network_patterns = mapped_patterns[str(network_scoped_key)] + patterns_with_retries = {pattern: retry for pattern, retry in network_patterns} + return patterns_with_retries + + def set_task_restart_patterns_allowed_restarts( + self, + network_scoped_key: ScopedKey, + patterns: list[str], + num_allowed_restarts: Union[int, list[int]], + ): + data = {"patterns": patterns, "max_retries": num_allowed_restarts} + self._post_resource( + f"/networks/{network_scoped_key}/restartpolicy/maxretries", data + ) + + def remove_task_restart_patterns( + self, network_scoped_key: ScopedKey, patterns: list[str] + ): + data = {"patterns": patterns} + self._post_resource(f"/networks/{network_scoped_key}/restartpolicy/remove") + + def clear_task_restart_patterns(self, network_scoped_key: ScopedKey): + self._query_resource(f"/networks/{network_scoped_key}/restartpolicy/clear") diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 28af9a45..d9dc0bd9 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -2956,6 +2956,13 @@ def remove_task_restart_patterns(self, taskhub: ScopedKey, patterns: List[str]): self.execute_query(q, patterns=patterns, taskhub_scoped_key=str(taskhub)) + def clear_task_restart_patterns(self, taskhub: ScopedKey): + q = """ + MATCH (trp: TaskRestartpattern {taskhub_scoped_key: $taskhub_scoped_key}) + DETACH DELETE trp + """ + self.execute_query(q, taskhub_scoped_key=str(taskhub)) + # TODO: fill in docstring def set_task_restart_patterns_max_retries( self, From a4da776f79743c0c14b1f25f04e7d68d11e89965 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 1 Oct 2024 15:22:26 -0700 Subject: [PATCH 059/143] Added testing for client methods dealing with restart policies --- alchemiscale/interface/api.py | 34 +++-- alchemiscale/interface/client.py | 11 +- alchemiscale/storage/statestore.py | 6 +- .../interface/client/test_client.py | 144 +++++++++++++++--- 4 files changed, 160 insertions(+), 35 deletions(-) diff --git a/alchemiscale/interface/api.py b/alchemiscale/interface/api.py index f3ba0548..66ce6a31 100644 --- a/alchemiscale/interface/api.py +++ b/alchemiscale/interface/api.py @@ -952,13 +952,11 @@ def get_task_status( def add_task_restart_patterns( network_scoped_key: str, *, - patterns: list[str], - number_of_retries: int, - request: Request, + patterns: list[str] = Body(embed=True), + number_of_retries: int = Body(embed=True), n4js: Neo4jStore = Depends(get_n4js_depends), token: TokenData = Depends(get_token_data_depends), ): - taskhub_scoped_key = n4js.get_taskhub(ScopedKey.from_str(network_scoped_key)) n4js.add_task_restart_patterns(taskhub_scoped_key, patterns, number_of_retries) @@ -968,7 +966,7 @@ def add_task_restart_patterns( def remove_task_restart_patterns( network_scoped_key: str, *, - patterns: list[str], + patterns: list[str] = Body(embed=True), n4js: Neo4jStore = Depends(get_n4js_depends), token: TokenData = Depends(get_token_data_depends), ): @@ -986,30 +984,44 @@ def clear_task_restart_patterns( ): taskhub_scoped_key = n4js.get_taskhub(ScopedKey.from_str(network_scoped_key)) n4js.clear_task_restart_patterns(taskhub_scoped_key) + return [network_scoped_key] # TODO docstring @router.post("/bulk/networks/restartpolicy/get") def get_task_restart_patterns( *, - networks: list[str], + networks: list[str] = Body(embed=True), n4js: Neo4jStore = Depends(get_n4js_depends), token: TokenData = Depends(get_token_data_depends), ) -> dict[str, set[tuple[str, int]]]: network_scoped_keys = [ScopedKey.from_str(network) for network in networks] - taskhubs_scoped_keys = n4js.get_taskhubs(network_scoped_keys) - restart_patterns = n4js.get_task_restart_patterns(network_scoped_key) + taskhub_scoped_keys = n4js.get_taskhubs(network_scoped_keys) + + taskhub_network_map = { + taskhub_scoped_key: network_scoped_key + for taskhub_scoped_key, network_scoped_key in zip( + taskhub_scoped_keys, network_scoped_keys + ) + } + + restart_patterns = n4js.get_task_restart_patterns(taskhub_scoped_keys) + + as_str = {} + for key, value in restart_patterns.items(): + network_scoped_key = taskhub_network_map[key] + as_str[str(network_scoped_key)] = value - return restart_patterns + return as_str @router.post("/networks/{network_scoped_key}/restartpolicy/maxretries") def set_task_restart_patterns_max_retries( network_scoped_key: str, *, - patterns: list[str], - max_retries: int, + patterns: list[str] = Body(embed=True), + max_retries: int = Body(embed=True), n4js: Neo4jStore = Depends(get_n4js_depends), token: TokenData = Depends(get_token_data_depends), ): diff --git a/alchemiscale/interface/client.py b/alchemiscale/interface/client.py index d5f3672e..8510cc05 100644 --- a/alchemiscale/interface/client.py +++ b/alchemiscale/interface/client.py @@ -1748,12 +1748,13 @@ def add_task_restart_patterns( num_allowed_restarts: int, ) -> ScopedKey: data = {"patterns": patterns, "number_of_retries": num_allowed_restarts} - self._post_resource("/networks/{network_scoped_key}/restartpolicy/add", data) + self._post_resource(f"/networks/{network_scoped_key}/restartpolicy/add", data) + return network_scoped_key def get_task_restart_patterns( self, network_scoped_key: ScopedKey ) -> dict[str, int]: - data = {network: str(network_scoped_key)} + data = {"networks": [str(network_scoped_key)]} mapped_patterns = self._post_resource( "/bulk/networks/restartpolicy/get", data=data ) @@ -1765,7 +1766,7 @@ def set_task_restart_patterns_allowed_restarts( self, network_scoped_key: ScopedKey, patterns: list[str], - num_allowed_restarts: Union[int, list[int]], + num_allowed_restarts: int, ): data = {"patterns": patterns, "max_retries": num_allowed_restarts} self._post_resource( @@ -1776,7 +1777,9 @@ def remove_task_restart_patterns( self, network_scoped_key: ScopedKey, patterns: list[str] ): data = {"patterns": patterns} - self._post_resource(f"/networks/{network_scoped_key}/restartpolicy/remove") + self._post_resource( + f"/networks/{network_scoped_key}/restartpolicy/remove", data + ) def clear_task_restart_patterns(self, network_scoped_key: ScopedKey): self._query_resource(f"/networks/{network_scoped_key}/restartpolicy/clear") diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index d9dc0bd9..7a339895 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -1256,7 +1256,7 @@ def get_taskhubs( ) -> list[Union[ScopedKey, TaskHub]]: # TODO: this could fail better, report all instances rather than first for network_scoped_key in network_scoped_keys: - if network.qualname != "AlchemicalNetwork": + if network_scoped_key.qualname != "AlchemicalNetwork": raise ValueError( "`network` ScopedKey does not correspond to an `AlchemicalNetwork`" ) @@ -1268,7 +1268,7 @@ def get_taskhubs( """ query_results = self.execute_query( - query, network_scoped_keys=list(map(str, network_scoped_key)) + query, network_scoped_keys=list(map(str, network_scoped_keys)) ) def _node_to_gufe(node): @@ -2958,7 +2958,7 @@ def remove_task_restart_patterns(self, taskhub: ScopedKey, patterns: List[str]): def clear_task_restart_patterns(self, taskhub: ScopedKey): q = """ - MATCH (trp: TaskRestartpattern {taskhub_scoped_key: $taskhub_scoped_key}) + MATCH (trp: TaskRestartPattern {taskhub_scoped_key: $taskhub_scoped_key}) DETACH DELETE trp """ self.execute_query(q, taskhub_scoped_key=str(taskhub)) diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index c39ce4f8..3f9db9ba 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -2124,30 +2124,140 @@ def test_get_task_failures( # TODO: can we mix in a success in here somewhere? # not possible with current BrokenProtocol, unfortunately - # TaskRestartPolicy client methods - @pytest.mark.xfail(raises=NotImplementedError) - def test_add_task_restart_policy_patterns(self): - raise NotImplementedError +class TestTaskRestartPolicy: - @pytest.mark.xfail(raises=NotImplementedError) - def test_get_task_restart_policy_patterns(self): - raise NotImplementedError + default_max_retries = 3 + default_patterns = ["Pattern 1", "Pattern 2", "Pattern 3"] - @pytest.mark.xfail(raises=NotImplementedError) - def test_remove_task_restart_policy_patterns(self): - raise NotImplementedError + def create_default_network(self, network, client, scope): + network_scoped_key = client.create_network(network, scope) + client.add_task_restart_patterns( + network_scoped_key, self.default_patterns, self.default_max_retries + ) + return network_scoped_key + + def test_add_task_restart_patterns( + self, user_client, network_tyk2, scope_test, n4js_preloaded + ): + + network_scoped_key = self.create_default_network( + network_tyk2, user_client, scope_test + ) + + query = """ + MATCH (trp: TaskRestartPattern)-[:ENFORCES]->(:TaskHub)-[:PERFORMS]->(:AlchemicalNetwork {`_scoped_key`: $network_scoped_key}) + RETURN trp + """ + + results = n4js_preloaded.execute_query( + query, network_scoped_key=str(network_scoped_key) + ) + + assert len(results.records) == 3 - @pytest.mark.xfail(raises=NotImplementedError) - def test_clear_task_restart_policy_patterns(self): - raise NotImplementedError + patterns_list = self.default_patterns[:] + for record in results.records: + trp = record["trp"] + assert trp["pattern"] in patterns_list + patterns_list.remove(trp["pattern"]) - @pytest.mark.xfail(raises=NotImplementedError) - def test_task_resolve_restarts( + def test_get_task_restart_patterns( self, + user_client: client.AlchemiscaleClient, + network_tyk2, scope_test, n4js_preloaded, + ): + network_scoped_key = self.create_default_network( + network_tyk2, user_client, scope_test + ) + taskrestartpatterns = user_client.get_task_restart_patterns(network_scoped_key) + expected = { + pattern: self.default_max_retries for pattern in self.default_patterns + } + assert taskrestartpatterns == expected + + def test_remove_task_restart_patterns( + self, user_client: client.AlchemiscaleClient, - network_tyk2_failure, + network_tyk2, + scope_test, + n4js_preloaded, ): - raise NotImplementedError + network_scoped_key = self.create_default_network( + network_tyk2, user_client, scope_test + ) + expected = { + pattern: self.default_max_retries for pattern in self.default_patterns + } + + # check that we have the expected 3 restart patterns + assert user_client.get_task_restart_patterns(network_scoped_key) == expected + + pattern_to_remove = next(expected.__iter__()) + user_client.remove_task_restart_patterns( + network_scoped_key, [pattern_to_remove] + ) + del expected[pattern_to_remove] + + # check that one was removed + assert user_client.get_task_restart_patterns(network_scoped_key) == expected + + patterns_to_remove = [pattern for pattern in expected] + user_client.remove_task_restart_patterns(network_scoped_key, patterns_to_remove) + + # check the remaining patterns are removed + assert user_client.get_task_restart_patterns(network_scoped_key) == {} + + def test_clear_task_restart_patterns( + self, + user_client: client.AlchemiscaleClient, + network_tyk2, + scope_test, + n4js_preloaded, + ): + network_scoped_key = self.create_default_network( + network_tyk2, user_client, scope_test + ) + + query = """ + MATCH (trp:TaskRestartPattern)-[:ENFORCES]->(:TaskHub)-[:PERFORMS]->(:AlchemicalNetwork {`_scoped_key`: $network_scoped_key}) + RETURN trp + """ + + assert ( + len( + n4js_preloaded.execute_query( + query, network_scoped_key=str(network_scoped_key) + ).records + ) + == 3 + ) + user_client.clear_task_restart_patterns(network_scoped_key) + assert ( + len( + n4js_preloaded.execute_query( + query, network_scoped_key=str(network_scoped_key) + ).records + ) + == 0 + ) + + def test_set_task_restart_patterns_allowed_restarts( + self, + user_client: client.AlchemiscaleClient, + network_tyk2, + scope_test, + n4js_preloaded, + ): + network_scoped_key = self.create_default_network( + network_tyk2, user_client, scope_test + ) + user_client.set_task_restart_patterns_allowed_restarts( + network_scoped_key, self.default_patterns[:2], 1 + ) + + expected = {pattern: 1 for pattern in self.default_patterns[:2]} + expected[self.default_patterns[-1]] = self.default_max_retries + assert user_client.get_task_restart_patterns(network_scoped_key) == expected From 44ca5d5438951e427ce54938c49768be16a55cf8 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 3 Oct 2024 02:19:59 +0200 Subject: [PATCH 060/143] Update tutorial notebook for openfe 1.0, latest cinnabar --- docs/tutorials/demo/Alchemiscale Demo.ipynb | 3185 +++++++++++++++---- 1 file changed, 2510 insertions(+), 675 deletions(-) diff --git a/docs/tutorials/demo/Alchemiscale Demo.ipynb b/docs/tutorials/demo/Alchemiscale Demo.ipynb index 7030fb6d..45eecea8 100644 --- a/docs/tutorials/demo/Alchemiscale Demo.ipynb +++ b/docs/tutorials/demo/Alchemiscale Demo.ipynb @@ -57,22 +57,7 @@ }, { "cell_type": "code", - "execution_count": 32, - "id": "27b4aa61-ca82-44fa-be84-c802f1083a29", - "metadata": {}, - "outputs": [], - "source": [ - "# suppress `numba` warnings, if present\n", - "from numba.core.errors import NumbaWarning\n", - "import warnings\n", - "\n", - "warnings.simplefilter('ignore', category=NumbaWarning)\n", - "warnings.filterwarnings('ignore')" - ] - }, - { - "cell_type": "code", - "execution_count": 5, + "execution_count": 1, "id": "6bb3f4a4-2135-494a-8365-9ef229dd124d", "metadata": {}, "outputs": [], @@ -85,7 +70,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 2, "id": "7d1fca4b-9750-4dcf-816a-b5a0782003af", "metadata": {}, "outputs": [ @@ -93,8 +78,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "gufe version: 0.9.1\n", - "openfe version: 0.11.0\n" + "gufe version: 1.0.0\n", + "openfe version: 1.0.1+0.g48dcbb26.dirty\n" ] } ], @@ -105,7 +90,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 3, "id": "4517e70f-ed72-46c8-946a-00cac4207882", "metadata": {}, "outputs": [], @@ -140,7 +125,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 4, "id": "16527065", "metadata": {}, "outputs": [ @@ -162,7 +147,7 @@ " SmallMoleculeComponent(name=lig_ejm_48)]" ] }, - "execution_count": 8, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -176,7 +161,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 5, "id": "16e4b2b9-14ab-4d07-8ec7-2926f41c9426", "metadata": {}, "outputs": [], @@ -203,7 +188,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 6, "id": "a31c89a4", "metadata": {}, "outputs": [ @@ -213,7 +198,7 @@ "ProteinComponent(name=tyk2)" ] }, - "execution_count": 10, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -239,7 +224,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 7, "id": "3c47166d-143b-41f0-9dce-7805e56d7191", "metadata": {}, "outputs": [ @@ -249,7 +234,7 @@ "SolventComponent(name=O, Na+, Cl-)" ] }, - "execution_count": 11, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -277,7 +262,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 8, "id": "10edc8b3-5f45-4e37-b1d5-bd508601e352", "metadata": {}, "outputs": [ @@ -299,7 +284,7 @@ " 'lig_ejm_48': ChemicalSystem(name=lig_ejm_48_complex, components={'ligand': SmallMoleculeComponent(name=lig_ejm_48), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)})}" ] }, - "execution_count": 12, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -315,7 +300,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 9, "id": "c8a0b60d-bfae-4f99-9f4d-656c982433b9", "metadata": {}, "outputs": [ @@ -337,7 +322,7 @@ " 'lig_ejm_48': ChemicalSystem(name=lig_ejm_48_solvent, components={'ligand': SmallMoleculeComponent(name=lig_ejm_48), 'solvent': SolventComponent(name=O, Na+, Cl-)})}" ] }, - "execution_count": 13, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -388,7 +373,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 10, "id": "183b23a6-1839-4586-bbbe-60b2defd2f01", "metadata": {}, "outputs": [], @@ -406,7 +391,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 11, "id": "d7bac455-ddc1-4735-96d7-5c6681ab3cbc", "metadata": { "scrolled": true, @@ -418,60 +403,61 @@ "text/plain": [ "{'forcefield_settings': {'constraints': 'hbonds',\n", " 'rigid_water': True,\n", - " 'remove_com': False,\n", " 'hydrogen_mass': 3.0,\n", " 'forcefields': ['amber/ff14SB.xml',\n", " 'amber/tip3p_standard.xml',\n", " 'amber/tip3p_HFE_multivalent.xml',\n", " 'amber/phosaa10.xml'],\n", - " 'small_molecule_forcefield': 'openff-2.0.0'},\n", + " 'small_molecule_forcefield': 'openff-2.1.1',\n", + " 'nonbonded_cutoff': 1.0 ,\n", + " 'nonbonded_method': 'PME'},\n", " 'thermo_settings': {'temperature': 298.15 ,\n", " 'pressure': 0.9869232667160129 ,\n", " 'ph': None,\n", " 'redox_potential': None},\n", - " 'system_settings': {'nonbonded_method': 'PME',\n", - " 'nonbonded_cutoff': 1.0 },\n", + " 'protocol_repeats': 3,\n", " 'solvation_settings': {'solvent_model': 'tip3p',\n", " 'solvent_padding': 1.2 },\n", - " 'alchemical_settings': {'lambda_functions': 'default',\n", - " 'lambda_windows': 11,\n", - " 'unsampled_endstates': False,\n", + " 'partial_charge_settings': {'partial_charge_method': 'am1bcc',\n", + " 'off_toolkit_backend': 'ambertools',\n", + " 'number_of_conformers': None,\n", + " 'nagl_model': None},\n", + " 'lambda_settings': {'lambda_functions': 'default', 'lambda_windows': 11},\n", + " 'alchemical_settings': {'softcore_LJ': 'gapsys',\n", + " 'explicit_charge_correction_cutoff': 0.8 ,\n", + " 'endstate_dispersion_correction': False,\n", " 'use_dispersion_correction': False,\n", - " 'softcore_LJ_v2': True,\n", - " 'softcore_electrostatics': True,\n", " 'softcore_alpha': 0.85,\n", - " 'softcore_electrostatics_alpha': 0.3,\n", - " 'softcore_sigma_Q': 1.0,\n", - " 'interpolate_old_and_new_14s': False,\n", - " 'flatten_torsions': False},\n", - " 'alchemical_sampler_settings': {'online_analysis_interval': 250,\n", - " 'n_repeats': 3,\n", + " 'turn_off_core_unique_exceptions': False,\n", + " 'explicit_charge_correction': False},\n", + " 'simulation_settings': {'equilibration_length': 1.0 ,\n", + " 'production_length': 5.0 ,\n", + " 'minimization_steps': 5000,\n", + " 'time_per_iteration': 1 ,\n", + " 'real_time_analysis_interval': 250 ,\n", + " 'early_termination_target_error': 0.0 ,\n", + " 'real_time_analysis_minimum_time': 500 ,\n", " 'sampler_method': 'repex',\n", - " 'online_analysis_target_error': 0.0 ,\n", - " 'online_analysis_minimum_iterations': 500,\n", - " 'flatness_criteria': 'logZ-flatness',\n", - " 'gamma0': 1.0,\n", + " 'sams_flatness_criteria': 'logZ-flatness',\n", + " 'sams_gamma0': 1.0,\n", " 'n_replicas': 11},\n", " 'engine_settings': {'compute_platform': None},\n", " 'integrator_settings': {'timestep': 4 ,\n", - " 'collision_rate': 1.0 ,\n", - " 'n_steps': 250 ,\n", + " 'langevin_collision_rate': 1.0 ,\n", + " 'barostat_frequency': 25 ,\n", + " 'remove_com': False,\n", " 'reassign_velocities': False,\n", " 'n_restart_attempts': 20,\n", - " 'constraint_tolerance': 1e-06,\n", - " 'barostat_frequency': 25 },\n", - " 'simulation_settings': {'equilibration_length': 1.0 ,\n", - " 'production_length': 5.0 ,\n", + " 'constraint_tolerance': 1e-06},\n", + " 'output_settings': {'checkpoint_interval': 250 ,\n", " 'forcefield_cache': 'db.json',\n", - " 'minimization_steps': 5000,\n", - " 'output_filename': 'simulation.nc',\n", - " 'output_structure': 'hybrid_system.pdb',\n", " 'output_indices': 'not water',\n", - " 'checkpoint_interval': 250 ,\n", - " 'checkpoint_storage': 'checkpoint.nc'}}" + " 'checkpoint_storage_filename': 'checkpoint.chk',\n", + " 'output_filename': 'simulation.nc',\n", + " 'output_structure': 'hybrid_system.pdb'}}" ] }, - "execution_count": 15, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -483,21 +469,85 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 18, + "id": "54d04e3c-7602-4b70-8b34-8a47ec5abb29", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'alchemical_settings': {'endstate_dispersion_correction': False,\n", + " 'explicit_charge_correction': False,\n", + " 'explicit_charge_correction_cutoff': ,\n", + " 'softcore_LJ': 'gapsys',\n", + " 'softcore_alpha': 0.85,\n", + " 'turn_off_core_unique_exceptions': False,\n", + " 'use_dispersion_correction': False},\n", + " 'engine_settings': {'compute_platform': None},\n", + " 'forcefield_settings': {'constraints': 'hbonds',\n", + " 'forcefields': ['amber/ff14SB.xml',\n", + " 'amber/tip3p_standard.xml',\n", + " 'amber/tip3p_HFE_multivalent.xml',\n", + " 'amber/phosaa10.xml'],\n", + " 'hydrogen_mass': 3.0,\n", + " 'nonbonded_cutoff': ,\n", + " 'nonbonded_method': 'PME',\n", + " 'rigid_water': True,\n", + " 'small_molecule_forcefield': 'openff-2.1.1'},\n", + " 'integrator_settings': {'barostat_frequency': ,\n", + " 'constraint_tolerance': 1e-06,\n", + " 'langevin_collision_rate': ,\n", + " 'n_restart_attempts': 20,\n", + " 'reassign_velocities': False,\n", + " 'remove_com': False,\n", + " 'timestep': },\n", + " 'lambda_settings': {'lambda_functions': 'default', 'lambda_windows': 11},\n", + " 'output_settings': {'checkpoint_interval': ,\n", + " 'checkpoint_storage_filename': 'checkpoint.chk',\n", + " 'forcefield_cache': 'db.json',\n", + " 'output_filename': 'simulation.nc',\n", + " 'output_indices': 'not water',\n", + " 'output_structure': 'hybrid_system.pdb'},\n", + " 'partial_charge_settings': {'nagl_model': None,\n", + " 'number_of_conformers': None,\n", + " 'off_toolkit_backend': 'ambertools',\n", + " 'partial_charge_method': 'am1bcc'},\n", + " 'protocol_repeats': 3,\n", + " 'simulation_settings': {'early_termination_target_error': ,\n", + " 'equilibration_length': ,\n", + " 'minimization_steps': 5000,\n", + " 'n_replicas': 11,\n", + " 'production_length': ,\n", + " 'real_time_analysis_interval': ,\n", + " 'real_time_analysis_minimum_time': ,\n", + " 'sampler_method': 'repex',\n", + " 'sams_flatness_criteria': 'logZ-flatness',\n", + " 'sams_gamma0': 1.0,\n", + " 'time_per_iteration': },\n", + " 'solvation_settings': {'solvent_model': 'tip3p',\n", + " 'solvent_padding': },\n", + " 'thermo_settings': {'ph': None,\n", + " 'pressure': ,\n", + " 'redox_potential': None,\n", + " 'temperature': }}\n" + ] + } + ], + "source": [ + "protocol_settings" + ] + }, + { + "cell_type": "code", + "execution_count": 20, "id": "c38305b0-879f-43b3-ba13-b010e478d2eb", "metadata": {}, "outputs": [], "source": [ - "# Here we make it so that each simulation only encompasses a single repeat\n", - "# We then do multiple repeats by running each simulation multiple time\n", - "protocol_settings.alchemical_sampler_settings.n_repeats = 1\n", - "\n", - "# We enforce the compute platform to be CUDA. This ensures that a bad GPU node\n", - "# on alchemiscale will fail automatically rather than trying to default to the CPU kernels\n", - "protocol_settings.engine_settings.compute_platform = \"CUDA\"\n", - "\n", - "# We set the protocol to auto terminate once the MBAR error of the estimate drops below 0.2 kT\n", - "protocol_settings.alchemical_sampler_settings.online_analysis_target_error = 0.2 * unit.boltzmann_constant * unit.kelvin" + "# Here we make it so that each simulation only features a single repeat\n", + "# We then do multiple repeats by running each simulation multiple times\n", + "protocol_settings.protocol_repeats = 1" ] }, { @@ -510,7 +560,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 21, "id": "bd1d0a0d-00c9-44a1-820c-186b4ee05334", "metadata": {}, "outputs": [], @@ -538,7 +588,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 22, "id": "a5bcca4e", "metadata": {}, "outputs": [], @@ -550,19 +600,19 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 23, "id": "cad6ecf6", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "362fce2a1dd548519f4c8e4c7e271e11", + "model_id": "81b932ea9a8d4333951fe37ecc0339fd", "version_major": 2, "version_minor": 0 }, "text/plain": [ - " 29%|##9 | 23/78 [00:01<00:03, 14.34it/s]" + " 88%|########8 | 69/78 [00:01<00:00, 45.56it/s]" ] }, "metadata": {}, @@ -587,7 +637,7 @@ "outputs": [ { "data": { - "image/png": "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XY6IiEi20Chgkb+tWrWKkX8vFLxmzRoKFChgc0UiIiLZQy2AIsC5c+cIDQ0FICwsjObNm9tckYiISPZRABSPZ1kWvXr14vTp01SvXp3XXnvN7pJERESylQKgeLylS5fy0Ucf4ePjw+LFi/Hz87O7JBERkWylACge7dixY/Tr1w+AUaNGUa9ePZsrEhERyX4KgOKxnE4nXbp0ISoqioYNGzJ8+HC7SxIREckRCoDisWbMmMG6devw9/dn8eLF+PhoULyIiHgGBUDxSHv37mXo0KEATJkyhcqVK9tckYiISM5RABSPc+XKFTp16kRcXBxt2rShV69edpckIiKSoxQAxeOMGzeOH374gcDAQObNm4fD4bC7JBERkRylACgeZdu2bYSHhwPwzjvvULp0aZsrEhERyXkKgOIxLl68SKdOnXA6nXTo0IF//etfdpckIiJiCwVA8RiDBg0iMjKSMmXKMHPmTLvLERERsY0CoHiEVatW8c477wCwYMECChUqZG9BIiIiNlIAlFzv3LlzhIaGAtC/f3+aN29uc0UiIiL2UgCUXM2yLHr37s3p06epVq0aEydOtLskERER2ykASq62bNkyVqxYgY+PD0uWLMHPz8/ukkRERGynACi51rFjx+jbty8Ao0aNol69ejZXJCIi4hoUACVXcjqddOnShaioKBo2bMjw4cPtLklERMRlKABKrjRjxgzWrVuHn58fixYtwsfHx+6SREREXIYCoOQ6e/fuZdiwYQC88cYbVKlSxeaKREREXIsCoOQqV65coVOnTly+fJk2bdrQq1cvu0sSERFxOQqAkquMGzeOH374gcDAQObNm4fD4bC7JBEREZejACi5xrZt2wgPDwdg9uzZlC5d2uaKREREXJMCoOQKFy9epFOnTjidTjp06EDbtm3tLklERMRlKQBKrjB48GAiIyMpU6YMM2fOtLscERERl6YAKG5v1apVzJ49G4AFCxZQqFAhewsSERFxcQqA4tbOnTtHaGgoAP3796d58+Y2VyQiIuL6FADFbVmWRe/evTl9+jTVqlVj4sSJdpckIiLiFhQAxW0tW7aMFStW4OPjw+LFi/Hz87O7JBEREbegAChu6dixY/Tt2xeAUaNGUb9+fZsrEhERcR8KgOJ2nE4nXbp0ISoqioYNGzJ8+HC7SxIREXErCoDidmbMmMG6devw8/Nj0aJF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", + "image/png": "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iIhIpaQAKJXGr371K9avX09wcDD/+Mc/6NevHz///LOnyxIREal0FAClUunfvz+bN28mPDycTz/9lLvuuosffvjB02WJiIhUKgqAUunccccd7Ny5k/r16/PFF1/Qo0cPjh496umyREREKg0FQKmU2rZtS1paGjfffDNHjx6lR48efPnll54uS0REpFJQAJRK6+abbyY1NZU2bdrwww8/cOedd7J7925PlyUiIuL1FAClUmvQoAE7d+7kjjvu4Oeff+buu+9m06ZNni5LRETEqykASqVXq1YtNm3aRL9+/Thz5gyDBg1izZo1ni5LRETEaykASpVwww03sH79en71q19x8eJFRo4cyeLFiz1dloiIiFdSAJQqIyAggL/97W8kJCRgtVpJSEhgzpw5ni5LRETE6ygASpXi5+fHwoULSUpKAuDpp5/mqaeewmazebgyERER76EAKFWOxWIhOTmZF198EYC5c+eSkJDA5cuXPVyZiIiId1AAlCprypQpLFmyhGrVqrFkyRIeeOABzp8/7+myREREPE4BUKq0Rx55hPfee4+AgADWrFnDfffdR0FBgafLEhER8SgFQKnyhg0bxoYNG7jhhhvYtGkT99xzDzk5OZ4uS0RExGMUAMUn3HPPPWzZsoWIiAjS09O58847ycrKcqhtr14waZL5vmlTmD/fRUUW47nnoH17991PRER8gwKg+IwuXbqwc+dOGjZsyFdffUX37t3JzMws0zUyMiAhwUUFFmPKFNiyxT33On/ehE2LBfbvt58/cAAefBCaNIEaNaBlS3j1VffUJCIirqEAKD6ldevWpKWlccstt3Ds2DF69OjBN99843D7unUhONiFBV4lJARq13bPvZ58Eho2vPb83r3mc//1r/DVVzB9OiQlwf/8j3vqEhGRiqcAKD6nadOmpKam0q5dO7Kzs4mPjy9D26JDwIcPQ48eEBQErVrB5s2mB23dOseud/IkjBwJtWqZoDdkCBw7Zn/96iHgsWNh6FBITobISAgPh5kz4dIlmDoVIiKgcWNYutThjwTAxo2QkgIvvXTta3FxsGAB3HUX3HwzjB4Nv/kNfPBB2e4hIiLeQwFQfFJkZCTbt2+ne/fu5OfnO3UNq9WEseBgSE+HRYtM75ijzpyB3r1NL9/OnZCaar4fMAAuXCi53datkJVl2sybZ0JibKwJkenpkJho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", "text/plain": [ "
" ] @@ -620,18 +670,18 @@ "name": "stdout", "output_type": "stream", "text": [ - "lig_ejm_46 lig_jmc_28\n", - "lig_ejm_31 lig_ejm_48\n", - "lig_ejm_31 lig_ejm_45\n", "lig_ejm_46 lig_ejm_31\n", - "lig_ejm_42 lig_ejm_43\n", + "lig_ejm_55 lig_ejm_43\n", + "lig_ejm_31 lig_ejm_50\n", + "lig_ejm_31 lig_ejm_48\n", + "lig_ejm_47 lig_ejm_31\n", "lig_ejm_42 lig_ejm_50\n", + "lig_jmc_23 lig_ejm_46\n", + "lig_ejm_42 lig_ejm_43\n", "lig_jmc_23 lig_jmc_28\n", "lig_jmc_27 lig_jmc_28\n", "lig_ejm_54 lig_ejm_55\n", - "lig_ejm_47 lig_ejm_31\n", - "lig_ejm_31 lig_ejm_50\n", - "lig_ejm_55 lig_ejm_43\n" + "lig_ejm_31 lig_ejm_45\n" ] } ], @@ -700,7 +750,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 28, @@ -741,7 +791,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 29, "id": "4471ecb9-5e58-40b2-b959-9c1a04cbda72", "metadata": {}, "outputs": [ @@ -749,7 +799,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "alchemiscale version: 0.1.3.post3\n" + "alchemiscale version: 0.5.0\n" ] } ], @@ -760,7 +810,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 30, "id": "c09991ea-bb8f-4907-9c99-94d5fe9c3714", "metadata": {}, "outputs": [], @@ -771,7 +821,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 31, "id": "03b860de-8cf7-4ec3-909f-708ab2165a7a", "metadata": {}, "outputs": [ @@ -789,7 +839,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 32, "id": "544ba62a-68c2-42e4-98b2-a9067ab643ef", "metadata": {}, "outputs": [ @@ -799,7 +849,7 @@ "AlchemiscaleClient('https://api.alchemiscale.org')" ] }, - "execution_count": 24, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -811,7 +861,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 33, "id": "a124d7ec-fb70-4194-b26c-ba57293c27ae", "metadata": {}, "outputs": [ @@ -821,7 +871,7 @@ "[]" ] }, - "execution_count": 25, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } @@ -832,17 +882,17 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 34, "id": "8a44648f-fc45-4bd1-8ce0-b12445f1f453", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 28, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } @@ -863,7 +913,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 35, "id": "194b7484-2db0-4255-84e6-dabe220ff455", "metadata": {}, "outputs": [], @@ -881,14 +931,14 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 36, "id": "503a72fb-7d6a-4206-a34b-4f696880b148", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "b038af755384457385bd3ada27e98326", + "model_id": "6dbc2f0a1b134571bae417ab2cef51d2", "version_major": 2, "version_minor": 0 }, @@ -908,19 +958,6 @@ }, "metadata": {}, "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n",
-       "
\n" - ], - "text/plain": [ - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" } ], "source": [ @@ -937,17 +974,17 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 37, "id": "57928257-eea8-43dd-8899-bde7550eac46", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 30, + "execution_count": 37, "metadata": {}, "output_type": "execute_result" } @@ -966,18 +1003,17 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 38, "id": "89a4302b-1f3e-4aef-b20d-09ea763d9c80", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[,\n", - " ]" + "[]" ] }, - "execution_count": 34, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } @@ -996,14 +1032,14 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 39, "id": "0ee299eb-c5a4-4802-90ed-4096baf6ecd3", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "afdb692e202b464188177d12232f8e56", + "model_id": "ff0bf3a744b9489faa1794cc93dbe3f0", "version_major": 2, "version_minor": 0 }, @@ -1023,19 +1059,6 @@ }, "metadata": {}, "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n",
-       "
\n" - ], - "text/plain": [ - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" } ], "source": [ @@ -1063,49 +1086,56 @@ }, { "cell_type": "code", - "execution_count": 36, - "id": "35400b4b-1232-4097-888a-5fda6d15b19b", + "execution_count": null, + "id": "1e3f0956-5dc2-411b-8a57-1d73507f26fb", + "metadata": {}, + "outputs": [], + "source": [ + "tf_sks = asc.get_network_transformations(an_sk)\n", + "tasks = asc.create_transformations_tasks(tf_sks)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "e2033823-6d61-4d04-839a-79b2cbd1e11e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ]" - ] - }, - "execution_count": 36, + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "tasks = []\n", - "for tf_sk in asc.get_network_transformations(an_sk):\n", - " tasks.extend(asc.create_tasks(tf_sk, count=1))\n", - "\n", "tasks" ] }, @@ -1119,14 +1149,14 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 45, "id": "728c5157-ff6c-4fb7-9925-418b00a7823a", "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
AlchemicalNetwork-391c0eb68025cf4b83e4d706f189f745-ddotson-tyk2-demo                                               \n",
+       "
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo                                               \n",
        "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
        "┃ status                                                                                                   count ┃\n",
        "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
@@ -1140,7 +1170,7 @@
        "
\n" ], "text/plain": [ - "\u001b[3mAlchemicalNetwork-391c0eb68025cf4b83e4d706f189f745-ddotson-tyk2-demo \u001b[0m\n", + "\u001b[3mAlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo \u001b[0m\n", "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", "┃\u001b[1m \u001b[0m\u001b[1mstatus \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m count\u001b[0m\u001b[1m \u001b[0m┃\n", "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", @@ -1162,7 +1192,7 @@ "{'waiting': 24}" ] }, - "execution_count": 37, + "execution_count": 45, "metadata": {}, "output_type": "execute_result" } @@ -1181,40 +1211,40 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 46, "id": "9c849250-8f08-4cc2-b32d-213392efb2e4", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ]" + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" ] }, - "execution_count": 38, + "execution_count": 46, "metadata": {}, "output_type": "execute_result" } @@ -1241,20 +1271,20 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 47, "id": "d1a27b3f-e2e3-4501-b1cc-e475ccc6a24b", "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
AlchemicalNetwork-391c0eb68025cf4b83e4d706f189f745-ddotson-tyk2-demo                                               \n",
+       "
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo                                               \n",
        "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
        "┃ status                                                                                                   count ┃\n",
        "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
        "│ complete                                                                                                     0 │\n",
-       "│ running                                                                                                     15 │\n",
-       "│ waiting                                                                                                      9 │\n",
+       "│ running                                                                                                      0 │\n",
+       "│ waiting                                                                                                     24 │\n",
        "│ error                                                                                                        0 │\n",
        "│ invalid                                                                                                      0 │\n",
        "│ deleted                                                                                                      0 │\n",
@@ -1262,13 +1292,13 @@
        "
\n" ], "text/plain": [ - "\u001b[3mAlchemicalNetwork-391c0eb68025cf4b83e4d706f189f745-ddotson-tyk2-demo \u001b[0m\n", + "\u001b[3mAlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo \u001b[0m\n", "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", "┃\u001b[1m \u001b[0m\u001b[1mstatus \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m count\u001b[0m\u001b[1m \u001b[0m┃\n", "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", "│\u001b[32m \u001b[0m\u001b[32mcomplete \u001b[0m\u001b[32m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m 0\u001b[0m\u001b[32m \u001b[0m│\n", - "│\u001b[38;5;172m \u001b[0m\u001b[38;5;172mrunning \u001b[0m\u001b[38;5;172m \u001b[0m│\u001b[38;5;172m \u001b[0m\u001b[38;5;172m 15\u001b[0m\u001b[38;5;172m \u001b[0m│\n", - "│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208mwaiting \u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208m 9\u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\n", + "│\u001b[38;5;172m \u001b[0m\u001b[38;5;172mrunning \u001b[0m\u001b[38;5;172m \u001b[0m│\u001b[38;5;172m \u001b[0m\u001b[38;5;172m 0\u001b[0m\u001b[38;5;172m \u001b[0m│\n", + "│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208mwaiting \u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208m 24\u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\n", "│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58merror \u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58m 0\u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\n", "│\u001b[38;5;201m \u001b[0m\u001b[38;5;201minvalid \u001b[0m\u001b[38;5;201m \u001b[0m│\u001b[38;5;201m \u001b[0m\u001b[38;5;201m 0\u001b[0m\u001b[38;5;201m \u001b[0m│\n", "│\u001b[38;5;129m \u001b[0m\u001b[38;5;129mdeleted \u001b[0m\u001b[38;5;129m \u001b[0m│\u001b[38;5;129m \u001b[0m\u001b[38;5;129m 0\u001b[0m\u001b[38;5;129m \u001b[0m│\n", @@ -1281,10 +1311,10 @@ { "data": { "text/plain": [ - "{'waiting': 9, 'running': 15}" + "{'waiting': 24}" ] }, - "execution_count": 43, + "execution_count": 47, "metadata": {}, "output_type": "execute_result" } @@ -1303,20 +1333,20 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 53, "id": "67f5edd1-9775-45e1-a161-85cf12c67265", "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
AlchemicalNetwork-391c0eb68025cf4b83e4d706f189f745-ddotson-tyk2-demo                                               \n",
+       "
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo                                               \n",
        "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
        "┃ status                                                                                                   count ┃\n",
        "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
-       "│ complete                                                                                                     0 │\n",
-       "│ running                                                                                                     24 │\n",
-       "│ waiting                                                                                                      0 │\n",
+       "│ complete                                                                                                     4 │\n",
+       "│ running                                                                                                     12 │\n",
+       "│ waiting                                                                                                      8 │\n",
        "│ error                                                                                                        0 │\n",
        "│ invalid                                                                                                      0 │\n",
        "│ deleted                                                                                                      0 │\n",
@@ -1324,13 +1354,13 @@
        "
\n" ], "text/plain": [ - "\u001b[3mAlchemicalNetwork-391c0eb68025cf4b83e4d706f189f745-ddotson-tyk2-demo \u001b[0m\n", + "\u001b[3mAlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo \u001b[0m\n", "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", "┃\u001b[1m \u001b[0m\u001b[1mstatus \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m count\u001b[0m\u001b[1m \u001b[0m┃\n", "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", - "│\u001b[32m \u001b[0m\u001b[32mcomplete \u001b[0m\u001b[32m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m 0\u001b[0m\u001b[32m \u001b[0m│\n", - "│\u001b[38;5;172m \u001b[0m\u001b[38;5;172mrunning \u001b[0m\u001b[38;5;172m \u001b[0m│\u001b[38;5;172m \u001b[0m\u001b[38;5;172m 24\u001b[0m\u001b[38;5;172m \u001b[0m│\n", - "│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208mwaiting \u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208m 0\u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\n", + "│\u001b[32m \u001b[0m\u001b[32mcomplete \u001b[0m\u001b[32m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m 4\u001b[0m\u001b[32m \u001b[0m│\n", + "│\u001b[38;5;172m \u001b[0m\u001b[38;5;172mrunning \u001b[0m\u001b[38;5;172m \u001b[0m│\u001b[38;5;172m \u001b[0m\u001b[38;5;172m 12\u001b[0m\u001b[38;5;172m \u001b[0m│\n", + "│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208mwaiting \u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208m 8\u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\n", "│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58merror \u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58m 0\u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\n", "│\u001b[38;5;201m \u001b[0m\u001b[38;5;201minvalid \u001b[0m\u001b[38;5;201m \u001b[0m│\u001b[38;5;201m \u001b[0m\u001b[38;5;201m 0\u001b[0m\u001b[38;5;201m \u001b[0m│\n", "│\u001b[38;5;129m \u001b[0m\u001b[38;5;129mdeleted \u001b[0m\u001b[38;5;129m \u001b[0m│\u001b[38;5;129m \u001b[0m\u001b[38;5;129m 0\u001b[0m\u001b[38;5;129m \u001b[0m│\n", @@ -1343,10 +1373,10 @@ { "data": { "text/plain": [ - "{'running': 24}" + "{'waiting': 8, 'complete': 4, 'running': 12}" ] }, - "execution_count": 52, + "execution_count": 53, "metadata": {}, "output_type": "execute_result" } @@ -1355,37 +1385,45 @@ "asc.get_network_status(an_sk)" ] }, + { + "cell_type": "markdown", + "id": "a30d40aa-ee2a-4811-a21e-baa143b15686", + "metadata": {}, + "source": [ + "...and some might have errored:" + ] + }, { "cell_type": "code", - "execution_count": 171, + "execution_count": 54, "id": "d78652a8-ecee-491e-aedc-c9312682c418", "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
AlchemicalNetwork-391c0eb68025cf4b83e4d706f189f745-ddotson-tyk2-demo                                               \n",
+       "
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo                                               \n",
        "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
        "┃ status                                                                                                   count ┃\n",
        "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
-       "│ complete                                                                                                    12 │\n",
-       "│ running                                                                                                     12 │\n",
+       "│ complete                                                                                                    14 │\n",
+       "│ running                                                                                                      9 │\n",
        "│ waiting                                                                                                      0 │\n",
-       "│ error                                                                                                        0 │\n",
+       "│ error                                                                                                        1 │\n",
        "│ invalid                                                                                                      0 │\n",
        "│ deleted                                                                                                      0 │\n",
        "└──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘\n",
        "
\n" ], "text/plain": [ - "\u001b[3mAlchemicalNetwork-391c0eb68025cf4b83e4d706f189f745-ddotson-tyk2-demo \u001b[0m\n", + "\u001b[3mAlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo \u001b[0m\n", "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", "┃\u001b[1m \u001b[0m\u001b[1mstatus \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m count\u001b[0m\u001b[1m \u001b[0m┃\n", "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", - "│\u001b[32m \u001b[0m\u001b[32mcomplete \u001b[0m\u001b[32m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m 12\u001b[0m\u001b[32m \u001b[0m│\n", - "│\u001b[38;5;172m \u001b[0m\u001b[38;5;172mrunning \u001b[0m\u001b[38;5;172m \u001b[0m│\u001b[38;5;172m \u001b[0m\u001b[38;5;172m 12\u001b[0m\u001b[38;5;172m \u001b[0m│\n", + "│\u001b[32m \u001b[0m\u001b[32mcomplete \u001b[0m\u001b[32m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m 14\u001b[0m\u001b[32m \u001b[0m│\n", + "│\u001b[38;5;172m \u001b[0m\u001b[38;5;172mrunning \u001b[0m\u001b[38;5;172m \u001b[0m│\u001b[38;5;172m \u001b[0m\u001b[38;5;172m 9\u001b[0m\u001b[38;5;172m \u001b[0m│\n", "│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208mwaiting \u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208m 0\u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\n", - "│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58merror \u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58m 0\u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\n", + "│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58merror \u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58m 1\u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\n", "│\u001b[38;5;201m \u001b[0m\u001b[38;5;201minvalid \u001b[0m\u001b[38;5;201m \u001b[0m│\u001b[38;5;201m \u001b[0m\u001b[38;5;201m 0\u001b[0m\u001b[38;5;201m \u001b[0m│\n", "│\u001b[38;5;129m \u001b[0m\u001b[38;5;129mdeleted \u001b[0m\u001b[38;5;129m \u001b[0m│\u001b[38;5;129m \u001b[0m\u001b[38;5;129m 0\u001b[0m\u001b[38;5;129m \u001b[0m│\n", "└──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘\n" @@ -1397,10 +1435,10 @@ { "data": { "text/plain": [ - "{'complete': 12, 'running': 12}" + "{'error': 1, 'running': 9, 'complete': 14}" ] }, - "execution_count": 171, + "execution_count": 54, "metadata": {}, "output_type": "execute_result" } @@ -1411,50 +1449,80 @@ }, { "cell_type": "markdown", - "id": "9dd5fce4-fc5d-4bc4-add2-1ecd6d0c893f", - "metadata": {}, - "source": [ - "### Dealing with errors" - ] - }, - { - "cell_type": "markdown", - "id": "b0901744-073e-4ea4-9b5f-b9a49eeb3186", - "metadata": {}, - "source": [ - "Inevitably, some of your `Task`s will encounter problems in execution, either random or systematic errors. When this happens, the `Task` status will be set to `'error'`. To illustrate this, we'll look at an `AlchemicalNetwork` with some failures:" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "id": "2ae49ee5-05ff-4834-9dce-59ff2255b9e2", + "id": "ce6a0d3c-7276-47c4-a7a5-949e25a49fd9", "metadata": {}, - "outputs": [], "source": [ - "failed_tasks = asc.query_tasks(scope=Scope('ddotson'), status='error')" + "...and at some point, none are `waiting` or `running`; they are either `complete` or `errored`:" ] }, { "cell_type": "code", - "execution_count": 41, - "id": "0f8670fb-efd6-414b-858c-be30d3af742e", + "execution_count": 67, + "id": "033a8832-e89b-42c7-85ce-e691be138c26", "metadata": {}, "outputs": [ + { + "data": { + "text/html": [ + "
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo                                               \n",
+       "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
+       "┃ status                                                                                                   count ┃\n",
+       "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
+       "│ complete                                                                                                    21 │\n",
+       "│ running                                                                                                      0 │\n",
+       "│ waiting                                                                                                      0 │\n",
+       "│ error                                                                                                        3 │\n",
+       "│ invalid                                                                                                      0 │\n",
+       "│ deleted                                                                                                      0 │\n",
+       "└──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3mAlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo \u001b[0m\n", + "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mstatus \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m count\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", + "│\u001b[32m \u001b[0m\u001b[32mcomplete \u001b[0m\u001b[32m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m 21\u001b[0m\u001b[32m \u001b[0m│\n", + "│\u001b[38;5;172m \u001b[0m\u001b[38;5;172mrunning \u001b[0m\u001b[38;5;172m \u001b[0m│\u001b[38;5;172m \u001b[0m\u001b[38;5;172m 0\u001b[0m\u001b[38;5;172m \u001b[0m│\n", + "│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208mwaiting \u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208m 0\u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\n", + "│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58merror \u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58m 3\u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\n", + "│\u001b[38;5;201m \u001b[0m\u001b[38;5;201minvalid \u001b[0m\u001b[38;5;201m \u001b[0m│\u001b[38;5;201m \u001b[0m\u001b[38;5;201m 0\u001b[0m\u001b[38;5;201m \u001b[0m│\n", + "│\u001b[38;5;129m \u001b[0m\u001b[38;5;129mdeleted \u001b[0m\u001b[38;5;129m \u001b[0m│\u001b[38;5;129m \u001b[0m\u001b[38;5;129m 0\u001b[0m\u001b[38;5;129m \u001b[0m│\n", + "└──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, { "data": { "text/plain": [ - "" + "{'error': 3, 'complete': 21}" ] }, - "execution_count": 41, + "execution_count": 67, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "an2_sk = asc.get_task_networks(failed_tasks[0])[0]\n", - "an2_sk" + "asc.get_network_status(an_sk)" + ] + }, + { + "cell_type": "markdown", + "id": "9dd5fce4-fc5d-4bc4-add2-1ecd6d0c893f", + "metadata": {}, + "source": [ + "### Dealing with errors" + ] + }, + { + "cell_type": "markdown", + "id": "b0901744-073e-4ea4-9b5f-b9a49eeb3186", + "metadata": {}, + "source": [ + "Inevitably, some of your `Task`s will encounter problems in execution, either random or systematic errors. When this happens, the `Task` status will be set to `'error'`." ] }, { @@ -1467,27 +1535,25 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 68, "id": "2a75797f-4289-4b49-9165-dd7252d055fd", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ,\n", - " ]" + "[,\n", + " ,\n", + " ]" ] }, - "execution_count": 42, + "execution_count": 68, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "failed_tasks = asc.get_network_tasks(an2_sk, status='error')\n", + "failed_tasks = asc.get_network_tasks(an_sk, status='error')\n", "failed_tasks" ] }, @@ -1501,14 +1567,14 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 69, "id": "10bb091c-99f4-4dc6-9e57-bd2e6a780761", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "16504c50b6444cceb90ddd1514b3caad", + "model_id": "8bb66e693c9d492186fa5869476610ab", "version_major": 2, "version_minor": 0 }, @@ -1520,24 +1586,18 @@ "output_type": "display_data" }, { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-15fdd9ff3f68cde67e706ffa87f6a9b7-ddotson-tyk2-demo/failures/ProtocolDAGResultRef-2bc722e46be751f9ec1a770028f393f7-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n"
+     ]
     },
     {
      "data": {
       "text/html": [
-       "
\n",
-       "
\n" + "
\n"
       ],
-      "text/plain": [
-       "\n"
-      ]
+      "text/plain": []
      },
      "metadata": {},
      "output_type": "display_data"
@@ -1545,10 +1605,10 @@
     {
      "data": {
       "text/plain": [
-       "[]"
+       "[]"
       ]
      },
-     "execution_count": 43,
+     "execution_count": 69,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1568,17 +1628,17 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 44,
+   "execution_count": 70,
    "id": "b37bc64d-8fc1-42c5-8b88-4383c53f91da",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/plain": [
-       ""
+       ""
       ]
      },
-     "execution_count": 44,
+     "execution_count": 70,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1589,17 +1649,20 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 45,
+   "execution_count": 71,
    "id": "4ffe5d77-f42f-4157-b8d1-d8c85c241bbf",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/plain": [
-       "[ProtocolUnitFailure(lig_ejm_55 to lig_ejm_43 repeat 0 generation 0)]"
+       "[ProtocolUnitFailure(lig_jmc_27 to lig_jmc_28 repeat 0 generation 0),\n",
+       " ProtocolUnitFailure(lig_jmc_27 to lig_jmc_28 repeat 0 generation 0),\n",
+       " ProtocolUnitFailure(lig_jmc_27 to lig_jmc_28 repeat 0 generation 0),\n",
+       " ProtocolUnitFailure(lig_jmc_27 to lig_jmc_28 repeat 0 generation 0)]"
       ]
      },
-     "execution_count": 45,
+     "execution_count": 71,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1618,7 +1681,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 46,
+   "execution_count": 72,
    "id": "8bba1453-7a1b-4f6d-8f20-05009d22ba76",
    "metadata": {},
    "outputs": [
@@ -1627,23 +1690,29 @@
      "output_type": "stream",
      "text": [
       "Traceback (most recent call last):\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/gufe/protocols/protocolunit.py\", line 319, in execute\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/gufe/protocols/protocolunit.py\", line 320, in execute\n",
       "    outputs = self._execute(context, **inputs)\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openfe/protocols/openmm_rfe/equil_rfe_methods.py\", line 684, in _execute\n",
+      "              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/equil_rfe_methods.py\", line 1129, in _execute\n",
       "    outputs = self.run(scratch_basepath=ctx.scratch,\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openfe/protocols/openmm_rfe/equil_rfe_methods.py\", line 593, in run\n",
+      "              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/equil_rfe_methods.py\", line 998, in run\n",
       "    sampler.setup(\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py\", line 121, in setup\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py\", line 121, in setup\n",
       "    minimize(compound_thermostate_copy, sampler_state,\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py\", line 295, in minimize\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py\", line 295, in minimize\n",
       "    context, integrator = dummy_cache.get_context(\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openmmtools/cache.py\", line 770, in get_context\n",
+      "                          ^^^^^^^^^^^^^^^^^^^^^^^^\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openmmtools/cache.py\", line 770, in get_context\n",
       "    context = thermodynamic_state.create_context(integrator, self.platform)\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openmmtools/states.py\", line 1179, in create_context\n",
+      "              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openmmtools/states.py\", line 1179, in create_context\n",
       "    return openmm.Context(system, integrator, platform)\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openmm/openmm.py\", line 3749, in __init__\n",
+      "           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openmm/openmm.py\", line 8037, in __init__\n",
       "    _openmm.Context_swiginit(self, _openmm.new_Context(*args))\n",
-      "openmm.OpenMMException: Error initializing CUDA: CUDA_ERROR_UNKNOWN (999) at /home/conda/feedstock_root/build_artifacts/openmm_1682500546897/work/platforms/cuda/src/CudaContext.cpp:140\n",
+      "                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+      "openmm.OpenMMException: Error initializing CUDA: CUDA_ERROR_MPS_CONNECTION_FAILED (805) at /home/conda/feedstock_root/build_artifacts/openmm_1721257909416/work/platforms/cuda/src/CudaContext.cpp:91\n",
       "\n"
      ]
     }
@@ -1670,17 +1739,24 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 58,
+   "execution_count": 73,
    "id": "4c661612-7cfa-4b55-95d0-713d8fcbee22",
    "metadata": {},
    "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "INFO:\tHTTP Request: POST https://api.alchemiscale.org/bulk/tasks/status/set \"HTTP/1.1 200 OK\"\n"
+     ]
+    },
     {
      "data": {
       "text/plain": [
-       "[]"
+       "[]"
       ]
      },
-     "execution_count": 58,
+     "execution_count": 73,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1707,52 +1783,80 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 63,
+   "execution_count": 74,
    "id": "704c9ddd-be00-43c3-9bc1-2ce99fb3fe02",
    "metadata": {},
-   "outputs": [],
-   "source": [
-    "results = dict()\n",
-    "for tf_sk in asc.get_network_transformations(an_sk):\n",
-    "    results[str(tf_sk)] = asc.get_transformation_results(tf_sk, visualize=False)"
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-82e90d15b483a813a6501073e3964930-ddotson-tyk2-demo/results/ProtocolDAGResultRef-01a2e46b4c33a9c52aab71b8c01ba5cd-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-73164716e2ad3c2e410fce413fa41be4-ddotson-tyk2-demo/results/ProtocolDAGResultRef-caabc7fbbe8e9bae4ad0da4f5db7cb61-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-e3022bfd607e772136f5c262875a8e78-ddotson-tyk2-demo/results/ProtocolDAGResultRef-b6b00c75b4f66e546623a98d42b165c8-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-6064e55fed6e90b90b727b299462cbad-ddotson-tyk2-demo/results/ProtocolDAGResultRef-f87f2f38715e0e03c30b574fe3c0e01d-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-28453f83bb03486a08afc096a8dc5298-ddotson-tyk2-demo/results/ProtocolDAGResultRef-d206475f1ae25c4818964579eabca7b0-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-eebbe29feefc097086749081844d8adc-ddotson-tyk2-demo/results/ProtocolDAGResultRef-2a9f4953c83460388614a4cb872bc601-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-2a345646fa5b3900aba8b74935fc4ec6-ddotson-tyk2-demo/results/ProtocolDAGResultRef-821c1a7ccc7740a28f4dd3d553b2f035-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-f8fb4a09881bce886dcb55a88be49016-ddotson-tyk2-demo/results/ProtocolDAGResultRef-8295104d5c32f10bdd86b69675bb7494-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-874c39cca17041ceb7a91f142104e8eb-ddotson-tyk2-demo/results/ProtocolDAGResultRef-2d7d1eed2ee1b96e67c95385c14b220c-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-b4857c06e9564cf3c2451f436f7af410-ddotson-tyk2-demo/results/ProtocolDAGResultRef-fdea668674894a4ebc21cd51a9ca8e02-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-958a3d55d581ec276b72394435df4bd0-ddotson-tyk2-demo/results/ProtocolDAGResultRef-19825c22d09bf2a8d70e47e5cac34829-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-22a2847c2a28584956e90d549fc6bcc6-ddotson-tyk2-demo/results/ProtocolDAGResultRef-83f986401353591db65c9ed401af260b-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-d5cdd9f4f4edd380361129082a01360b-ddotson-tyk2-demo/results/ProtocolDAGResultRef-57a8c3097273d30ff5b0350943883824-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-20fd18a057e4affd5c78cad41a81d3b4-ddotson-tyk2-demo/results/ProtocolDAGResultRef-2798447525f7139b158919d08faadb95-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-44cf523e9d0e53674fadc59c9b9110b7-ddotson-tyk2-demo/results/ProtocolDAGResultRef-f56c970e363dbe075f2dda74386055a8-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-856f85d23d9b8b84916d5b83ebfa1773-ddotson-tyk2-demo/results/ProtocolDAGResultRef-f94dec1b5cc13aff45aba30eb1647c3c-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-ef57a778259ba88b9fb4c23cc2bd78f4-ddotson-tyk2-demo/results/ProtocolDAGResultRef-474df4f73b5bdb270a43361d8651dfa4-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-c33480c9f0dfd80852541205f46914a7-ddotson-tyk2-demo/results/ProtocolDAGResultRef-943e801e0c7774f3c962ca8127150ace-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-4bfeb3edd7f0d47ec52f533a5cf435a0-ddotson-tyk2-demo/results/ProtocolDAGResultRef-0d56400cc2c06fe557f170b872c9a465-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-06eac733376ab1ff566248040fe37e01-ddotson-tyk2-demo/results/ProtocolDAGResultRef-cc1df82df45efd8fba44f029a28094b6-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n",
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-76de739d87d53981abfff4ab9ce071e4-ddotson-tyk2-demo/results/ProtocolDAGResultRef-810f1f48475da9a8cbb7ed364848cdf2-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n"
+     ]
+    }
+   ],
+   "source": [
+    "results = dict()\n",
+    "for tf_sk in asc.get_network_transformations(an_sk):\n",
+    "    results[str(tf_sk)] = asc.get_transformation_results(tf_sk, visualize=False)"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 64,
+   "execution_count": 75,
    "id": "816d46a2-178b-459a-80a5-357b5438462a",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/plain": [
-       "{'Transformation-3bde3c547da7c4db25af92e94f34efce-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-3d1c7ab3c70341bc88fc91112689a648-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-333a2629c325b4f257b416c2431d9132-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-086a259a37b0f93979cc23dc31344aa8-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-553107db824ee8a99f30fb42cbe5798c-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-08f4c68a8e178d698e59cc1a366e3415-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-c244dffbafa7e4e95ff4c0a03a54888b-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-7080ba2b294fbb443f7c8fd0432523b8-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-2168cd5e6257debd9e31a7dea8f865db-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-6fedaff17af81e8f8cd903ff54f4fa43-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-058b41226978833e875145eca60fa44d-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-a34291c782001508c20b0f8fbd8e8768-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-be3971fa5275fae3d0767ef387e4f673-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-c824272678b20bb10b0b3ed82a75cba7-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-72f58e6ef2cc85e52de765b797fb935b-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-d5634110bde446e3c0135a213ca126be-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-cfc64db96341e6c6fe78b31bc45086fe-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-f549eb13fa5c5aa43a4b0581509ec17b-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-22f055e336144e1192a54fc36b008761-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-954a54426ba12cd7f97b163692de84b3-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-43fcc3ea77628d04acffbfafaf7289f3-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-1ed12e0907b854bd0810ebb8bce91bbd-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-e85c3cfe5386d9e53c4593acf52df560-ddotson-tyk2-demo': ,\n",
-       " 'Transformation-1d104d98ba12a95e13d2e086f1839b51-ddotson-tyk2-demo': }"
-      ]
-     },
-     "execution_count": 64,
+       "{'Transformation-15fdd9ff3f68cde67e706ffa87f6a9b7-ddotson-tyk2-demo': None,\n",
+       " 'Transformation-152928d4d69298dcedf2dd622073a95b-ddotson-tyk2-demo': None,\n",
+       " 'Transformation-82e90d15b483a813a6501073e3964930-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-73164716e2ad3c2e410fce413fa41be4-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-e3022bfd607e772136f5c262875a8e78-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-6064e55fed6e90b90b727b299462cbad-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-28453f83bb03486a08afc096a8dc5298-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-eebbe29feefc097086749081844d8adc-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-2a345646fa5b3900aba8b74935fc4ec6-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-f8fb4a09881bce886dcb55a88be49016-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-874c39cca17041ceb7a91f142104e8eb-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-b4857c06e9564cf3c2451f436f7af410-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-958a3d55d581ec276b72394435df4bd0-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-22a2847c2a28584956e90d549fc6bcc6-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-d5cdd9f4f4edd380361129082a01360b-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-20fd18a057e4affd5c78cad41a81d3b4-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-44cf523e9d0e53674fadc59c9b9110b7-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-856f85d23d9b8b84916d5b83ebfa1773-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-ef57a778259ba88b9fb4c23cc2bd78f4-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-c33480c9f0dfd80852541205f46914a7-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-4bfeb3edd7f0d47ec52f533a5cf435a0-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-8e74110792d4c68bb5a772733d490401-ddotson-tyk2-demo': None,\n",
+       " 'Transformation-06eac733376ab1ff566248040fe37e01-ddotson-tyk2-demo': ,\n",
+       " 'Transformation-76de739d87d53981abfff4ab9ce071e4-ddotson-tyk2-demo': }"
+      ]
+     },
+     "execution_count": 75,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1763,56 +1867,56 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 68,
+   "execution_count": 76,
    "id": "35d41826-c6fa-44fe-aec9-45e85bd5d30c",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/html": [
-       "39.7580051742956 kilocalorie/mole"
+       "20.937595533249475 kilocalorie_per_mole"
       ],
       "text/latex": [
-       "$39.7580051742956\\ \\frac{\\mathrm{kilocalorie}}{\\mathrm{mole}}$"
+       "$20.937595533249475\\ \\mathrm{kilocalorie\\_per\\_mole}$"
       ],
       "text/plain": [
-       "39.7580051742956 "
+       "20.937595533249475 "
       ]
      },
-     "execution_count": 68,
+     "execution_count": 76,
      "metadata": {},
      "output_type": "execute_result"
     }
    ],
    "source": [
-    "results['Transformation-08f4c68a8e178d698e59cc1a366e3415-ddotson-tyk2-demo'].get_estimate()"
+    "results['Transformation-82e90d15b483a813a6501073e3964930-ddotson-tyk2-demo'].get_estimate()"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 69,
+   "execution_count": 77,
    "id": "88efaebb-a47b-4a53-b6b9-382bcfc2b1ec",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/html": [
-       "0.0 kilocalorie/mole"
+       "0.0 kilocalorie_per_mole"
       ],
       "text/latex": [
-       "$0.0\\ \\frac{\\mathrm{kilocalorie}}{\\mathrm{mole}}$"
+       "$0.0\\ \\mathrm{kilocalorie\\_per\\_mole}$"
       ],
       "text/plain": [
-       "0.0 "
+       "0.0 "
       ]
      },
-     "execution_count": 69,
+     "execution_count": 77,
      "metadata": {},
      "output_type": "execute_result"
     }
    ],
    "source": [
-    "results['Transformation-08f4c68a8e178d698e59cc1a366e3415-ddotson-tyk2-demo'].get_uncertainty()"
+    "results['Transformation-82e90d15b483a813a6501073e3964930-ddotson-tyk2-demo'].get_uncertainty()"
    ]
   },
   {
@@ -1822,7 +1926,7 @@
    "source": [
     "In this case, we have only a single `ProtocolDAGResult` for each `Transformation` (since we created and actioned only 1 `Task` for each), and so the uncertainty (standard deviation between replicate simulations) given for this `ProtocolResult` is 0.0. The `RelativeHybridTopologyProtocol` combines `ProtocolDAGResult` values statistically, reducing the uncertainty but not increasing convergence with additional `Task`s\n",
     "\n",
-    "By contrast other `Protocol`s, such as the `perses` `NonEquilibriumCyclingProtocol`, will improve convergence with more `Task`s on a given `Transformation`; in the case of the `NonEquilibriumCyclingProtocol`, the non-equilibrium work values for each `ProtocolDAGResult` are combined together and fed to `BAR` to produce a single estimate with its own uncertainty."
+    "By contrast other `Protocol`s, such as the `feflow` `NonEquilibriumCyclingProtocol`, will improve convergence with more `Task`s on a given `Transformation`; in the case of the `NonEquilibriumCyclingProtocol`, the non-equilibrium work values for each `ProtocolDAGResult` are combined together and fed to `BAR` to produce a single estimate with its own uncertainty."
    ]
   },
   {
@@ -1835,64 +1939,64 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 58,
+   "execution_count": 78,
    "id": "78a17d4c-da4d-4879-a7d1-ae81f158fc00",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/plain": [
-       "[,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ,\n",
-       " ]"
-      ]
-     },
-     "execution_count": 58,
+       "[,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ,\n",
+       " ]"
+      ]
+     },
+     "execution_count": 78,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1907,35 +2011,35 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 75,
+   "execution_count": 81,
    "id": "7416399a-a06c-461f-a1ae-739bd46abcdb",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/html": [
-       "
AlchemicalNetwork-391c0eb68025cf4b83e4d706f189f745-ddotson-tyk2-demo                                               \n",
+       "
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo                                               \n",
        "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
        "┃ status                                                                                                   count ┃\n",
        "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
-       "│ complete                                                                                                    19 │\n",
-       "│ running                                                                                                     48 │\n",
-       "│ waiting                                                                                                      0 │\n",
-       "│ error                                                                                                        5 │\n",
+       "│ complete                                                                                                    21 │\n",
+       "│ running                                                                                                      0 │\n",
+       "│ waiting                                                                                                     49 │\n",
+       "│ error                                                                                                        2 │\n",
        "│ invalid                                                                                                      0 │\n",
        "│ deleted                                                                                                      0 │\n",
        "└──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘\n",
        "
\n" ], "text/plain": [ - "\u001b[3mAlchemicalNetwork-391c0eb68025cf4b83e4d706f189f745-ddotson-tyk2-demo \u001b[0m\n", + "\u001b[3mAlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo \u001b[0m\n", "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", "┃\u001b[1m \u001b[0m\u001b[1mstatus \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m count\u001b[0m\u001b[1m \u001b[0m┃\n", "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", - "│\u001b[32m \u001b[0m\u001b[32mcomplete \u001b[0m\u001b[32m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m 19\u001b[0m\u001b[32m \u001b[0m│\n", - "│\u001b[38;5;172m \u001b[0m\u001b[38;5;172mrunning \u001b[0m\u001b[38;5;172m \u001b[0m│\u001b[38;5;172m \u001b[0m\u001b[38;5;172m 48\u001b[0m\u001b[38;5;172m \u001b[0m│\n", - "│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208mwaiting \u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208m 0\u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\n", - "│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58merror \u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58m 5\u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\n", + "│\u001b[32m \u001b[0m\u001b[32mcomplete \u001b[0m\u001b[32m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m 21\u001b[0m\u001b[32m \u001b[0m│\n", + "│\u001b[38;5;172m \u001b[0m\u001b[38;5;172mrunning \u001b[0m\u001b[38;5;172m \u001b[0m│\u001b[38;5;172m \u001b[0m\u001b[38;5;172m 0\u001b[0m\u001b[38;5;172m \u001b[0m│\n", + "│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208mwaiting \u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208m 49\u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\n", + "│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58merror \u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58m 2\u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\n", "│\u001b[38;5;201m \u001b[0m\u001b[38;5;201minvalid \u001b[0m\u001b[38;5;201m \u001b[0m│\u001b[38;5;201m \u001b[0m\u001b[38;5;201m 0\u001b[0m\u001b[38;5;201m \u001b[0m│\n", "│\u001b[38;5;129m \u001b[0m\u001b[38;5;129mdeleted \u001b[0m\u001b[38;5;129m \u001b[0m│\u001b[38;5;129m \u001b[0m\u001b[38;5;129m 0\u001b[0m\u001b[38;5;129m \u001b[0m│\n", "└──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘\n" @@ -1947,10 +2051,10 @@ { "data": { "text/plain": [ - "{'complete': 19, 'running': 48, 'error': 5}" + "{'waiting': 49, 'error': 2, 'complete': 21}" ] }, - "execution_count": 75, + "execution_count": 81, "metadata": {}, "output_type": "execute_result" } @@ -1969,21 +2073,72 @@ }, { "cell_type": "code", - "execution_count": 76, + "execution_count": 84, + "id": "9c0f6683-603d-4a82-8255-104e473cb4a6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo                                               \n",
+       "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
+       "┃ status                                                                                                   count ┃\n",
+       "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
+       "│ complete                                                                                                    46 │\n",
+       "│ running                                                                                                     24 │\n",
+       "│ waiting                                                                                                      0 │\n",
+       "│ error                                                                                                        2 │\n",
+       "│ invalid                                                                                                      0 │\n",
+       "│ deleted                                                                                                      0 │\n",
+       "└──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3mAlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo \u001b[0m\n", + "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mstatus \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m count\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", + "│\u001b[32m \u001b[0m\u001b[32mcomplete \u001b[0m\u001b[32m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m 46\u001b[0m\u001b[32m \u001b[0m│\n", + "│\u001b[38;5;172m \u001b[0m\u001b[38;5;172mrunning \u001b[0m\u001b[38;5;172m \u001b[0m│\u001b[38;5;172m \u001b[0m\u001b[38;5;172m 24\u001b[0m\u001b[38;5;172m \u001b[0m│\n", + "│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208mwaiting \u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208m 0\u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\n", + "│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58merror \u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58m 2\u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\n", + "│\u001b[38;5;201m \u001b[0m\u001b[38;5;201minvalid \u001b[0m\u001b[38;5;201m \u001b[0m│\u001b[38;5;201m \u001b[0m\u001b[38;5;201m 0\u001b[0m\u001b[38;5;201m \u001b[0m│\n", + "│\u001b[38;5;129m \u001b[0m\u001b[38;5;129mdeleted \u001b[0m\u001b[38;5;129m \u001b[0m│\u001b[38;5;129m \u001b[0m\u001b[38;5;129m 0\u001b[0m\u001b[38;5;129m \u001b[0m│\n", + "└──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "{'complete': 46, 'running': 24, 'error': 2}" + ] + }, + "execution_count": 84, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "asc.get_network_status(an_sk)" + ] + }, + { + "cell_type": "code", + "execution_count": 85, "id": "892122f7-95c9-47e0-8931-f15ab9f2efae", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ,\n", - " ]" + "[,\n", + " ]" ] }, - "execution_count": 76, + "execution_count": 85, "metadata": {}, "output_type": "execute_result" } @@ -1994,14 +2149,14 @@ }, { "cell_type": "code", - "execution_count": 78, + "execution_count": 86, "id": "cbfe1d37-0934-4121-abbb-09d7c1f19396", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "51c7eaf8708b4a99ba637c22d4c621a3", + "model_id": "a679f2867bb946d9963cdb603c80bf52", "version_major": 2, "version_minor": 0 }, @@ -2013,24 +2168,18 @@ "output_type": "display_data" }, { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "INFO:\tHTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-152928d4d69298dcedf2dd622073a95b-ddotson-tyk2-demo/failures/ProtocolDAGResultRef-08d10b9130b46d11e70cdf91343e686e-ddotson-tyk2-demo \"HTTP/1.1 200 OK\"\n"
+     ]
     },
     {
      "data": {
       "text/html": [
-       "
\n",
-       "
\n" + "
\n"
       ],
-      "text/plain": [
-       "\n"
-      ]
+      "text/plain": []
      },
      "metadata": {},
      "output_type": "display_data"
@@ -2040,23 +2189,29 @@
      "output_type": "stream",
      "text": [
       "Traceback (most recent call last):\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/gufe/protocols/protocolunit.py\", line 319, in execute\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/gufe/protocols/protocolunit.py\", line 320, in execute\n",
       "    outputs = self._execute(context, **inputs)\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openfe/protocols/openmm_rfe/equil_rfe_methods.py\", line 684, in _execute\n",
+      "              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/equil_rfe_methods.py\", line 1129, in _execute\n",
       "    outputs = self.run(scratch_basepath=ctx.scratch,\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openfe/protocols/openmm_rfe/equil_rfe_methods.py\", line 593, in run\n",
+      "              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/equil_rfe_methods.py\", line 998, in run\n",
       "    sampler.setup(\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py\", line 121, in setup\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py\", line 121, in setup\n",
       "    minimize(compound_thermostate_copy, sampler_state,\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py\", line 295, in minimize\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py\", line 295, in minimize\n",
       "    context, integrator = dummy_cache.get_context(\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openmmtools/cache.py\", line 770, in get_context\n",
+      "                          ^^^^^^^^^^^^^^^^^^^^^^^^\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openmmtools/cache.py\", line 770, in get_context\n",
       "    context = thermodynamic_state.create_context(integrator, self.platform)\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openmmtools/states.py\", line 1179, in create_context\n",
+      "              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openmmtools/states.py\", line 1179, in create_context\n",
       "    return openmm.Context(system, integrator, platform)\n",
-      "  File \"/opt/conda/lib/python3.10/site-packages/openmm/openmm.py\", line 3749, in __init__\n",
+      "           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+      "  File \"/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openmm/openmm.py\", line 8037, in __init__\n",
       "    _openmm.Context_swiginit(self, _openmm.new_Context(*args))\n",
-      "openmm.OpenMMException: Error initializing CUDA: CUDA_ERROR_UNKNOWN (999) at /home/conda/feedstock_root/build_artifacts/openmm_1682500546897/work/platforms/cuda/src/CudaContext.cpp:140\n",
+      "                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
+      "openmm.OpenMMException: Error initializing CUDA: CUDA_ERROR_MPS_CONNECTION_FAILED (805) at /home/conda/feedstock_root/build_artifacts/openmm_1721257909416/work/platforms/cuda/src/CudaContext.cpp:91\n",
       "\n"
      ]
     }
@@ -2067,7 +2222,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 222,
+   "execution_count": 87,
    "id": "42b373ed-2ec3-4e10-a551-034fdd2b5ed6",
    "metadata": {},
    "outputs": [
@@ -2075,17 +2230,17 @@
      "name": "stderr",
      "output_type": "stream",
      "text": [
-      "INFO:\tHTTP Request: POST http://api.alchemiscale.internal/bulk/tasks/status/set \"HTTP/1.1 200 OK\"\n"
+      "INFO:\tHTTP Request: POST https://api.alchemiscale.org/bulk/tasks/status/set \"HTTP/1.1 200 OK\"\n"
      ]
     },
     {
      "data": {
       "text/plain": [
-       "[,\n",
-       " ]"
+       "[,\n",
+       " ]"
       ]
      },
-     "execution_count": 222,
+     "execution_count": 87,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2096,35 +2251,35 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 86,
+   "execution_count": 98,
    "id": "55c733aa-3e41-481c-8f63-df7b2c8a79de",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "text/html": [
-       "
AlchemicalNetwork-391c0eb68025cf4b83e4d706f189f745-ddotson-tyk2-demo                                               \n",
+       "
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo                                               \n",
        "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n",
        "┃ status                                                                                                   count ┃\n",
        "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n",
-       "│ complete                                                                                                    66 │\n",
-       "│ running                                                                                                      1 │\n",
+       "│ complete                                                                                                    72 │\n",
+       "│ running                                                                                                      0 │\n",
        "│ waiting                                                                                                      0 │\n",
-       "│ error                                                                                                        5 │\n",
+       "│ error                                                                                                        0 │\n",
        "│ invalid                                                                                                      0 │\n",
        "│ deleted                                                                                                      0 │\n",
        "└──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘\n",
        "
\n" ], "text/plain": [ - "\u001b[3mAlchemicalNetwork-391c0eb68025cf4b83e4d706f189f745-ddotson-tyk2-demo \u001b[0m\n", + "\u001b[3mAlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo \u001b[0m\n", "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n", "┃\u001b[1m \u001b[0m\u001b[1mstatus \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m count\u001b[0m\u001b[1m \u001b[0m┃\n", "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩\n", - "│\u001b[32m \u001b[0m\u001b[32mcomplete \u001b[0m\u001b[32m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m 66\u001b[0m\u001b[32m \u001b[0m│\n", - "│\u001b[38;5;172m \u001b[0m\u001b[38;5;172mrunning \u001b[0m\u001b[38;5;172m \u001b[0m│\u001b[38;5;172m \u001b[0m\u001b[38;5;172m 1\u001b[0m\u001b[38;5;172m \u001b[0m│\n", + "│\u001b[32m \u001b[0m\u001b[32mcomplete \u001b[0m\u001b[32m \u001b[0m│\u001b[32m \u001b[0m\u001b[32m 72\u001b[0m\u001b[32m \u001b[0m│\n", + "│\u001b[38;5;172m \u001b[0m\u001b[38;5;172mrunning \u001b[0m\u001b[38;5;172m \u001b[0m│\u001b[38;5;172m \u001b[0m\u001b[38;5;172m 0\u001b[0m\u001b[38;5;172m \u001b[0m│\n", "│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208mwaiting \u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\u001b[38;2;23;147;208m \u001b[0m\u001b[38;2;23;147;208m 0\u001b[0m\u001b[38;2;23;147;208m \u001b[0m│\n", - "│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58merror \u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58m 5\u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\n", + "│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58merror \u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\u001b[38;2;255;7;58m \u001b[0m\u001b[38;2;255;7;58m 0\u001b[0m\u001b[38;2;255;7;58m \u001b[0m│\n", "│\u001b[38;5;201m \u001b[0m\u001b[38;5;201minvalid \u001b[0m\u001b[38;5;201m \u001b[0m│\u001b[38;5;201m \u001b[0m\u001b[38;5;201m 0\u001b[0m\u001b[38;5;201m \u001b[0m│\n", "│\u001b[38;5;129m \u001b[0m\u001b[38;5;129mdeleted \u001b[0m\u001b[38;5;129m \u001b[0m│\u001b[38;5;129m \u001b[0m\u001b[38;5;129m 0\u001b[0m\u001b[38;5;129m \u001b[0m│\n", "└──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘\n" @@ -2136,10 +2291,10 @@ { "data": { "text/plain": [ - "{'complete': 66, 'error': 5, 'running': 1}" + "{'complete': 72}" ] }, - "execution_count": 86, + "execution_count": 98, "metadata": {}, "output_type": "execute_result" } @@ -2158,7 +2313,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 102, "id": "43aa0be9-4306-48f8-a3c6-e6851b05d553", "metadata": {}, "outputs": [], @@ -2170,40 +2325,40 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 103, "id": "bd6409b5-1ec9-469e-b7e7-5d0dbb2c29b3", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'Transformation-3bde3c547da7c4db25af92e94f34efce-ddotson-tyk2-demo': ,\n", - " 'Transformation-3d1c7ab3c70341bc88fc91112689a648-ddotson-tyk2-demo': ,\n", - " 'Transformation-333a2629c325b4f257b416c2431d9132-ddotson-tyk2-demo': ,\n", - " 'Transformation-086a259a37b0f93979cc23dc31344aa8-ddotson-tyk2-demo': ,\n", - " 'Transformation-553107db824ee8a99f30fb42cbe5798c-ddotson-tyk2-demo': ,\n", - " 'Transformation-08f4c68a8e178d698e59cc1a366e3415-ddotson-tyk2-demo': ,\n", - " 'Transformation-c244dffbafa7e4e95ff4c0a03a54888b-ddotson-tyk2-demo': ,\n", - " 'Transformation-7080ba2b294fbb443f7c8fd0432523b8-ddotson-tyk2-demo': ,\n", - " 'Transformation-2168cd5e6257debd9e31a7dea8f865db-ddotson-tyk2-demo': ,\n", - " 'Transformation-6fedaff17af81e8f8cd903ff54f4fa43-ddotson-tyk2-demo': ,\n", - " 'Transformation-058b41226978833e875145eca60fa44d-ddotson-tyk2-demo': ,\n", - " 'Transformation-a34291c782001508c20b0f8fbd8e8768-ddotson-tyk2-demo': ,\n", - " 'Transformation-be3971fa5275fae3d0767ef387e4f673-ddotson-tyk2-demo': ,\n", - " 'Transformation-c824272678b20bb10b0b3ed82a75cba7-ddotson-tyk2-demo': ,\n", - " 'Transformation-72f58e6ef2cc85e52de765b797fb935b-ddotson-tyk2-demo': ,\n", - " 'Transformation-d5634110bde446e3c0135a213ca126be-ddotson-tyk2-demo': ,\n", - " 'Transformation-cfc64db96341e6c6fe78b31bc45086fe-ddotson-tyk2-demo': ,\n", - " 'Transformation-f549eb13fa5c5aa43a4b0581509ec17b-ddotson-tyk2-demo': ,\n", - " 'Transformation-22f055e336144e1192a54fc36b008761-ddotson-tyk2-demo': ,\n", - " 'Transformation-954a54426ba12cd7f97b163692de84b3-ddotson-tyk2-demo': ,\n", - " 'Transformation-43fcc3ea77628d04acffbfafaf7289f3-ddotson-tyk2-demo': ,\n", - " 'Transformation-1ed12e0907b854bd0810ebb8bce91bbd-ddotson-tyk2-demo': ,\n", - " 'Transformation-e85c3cfe5386d9e53c4593acf52df560-ddotson-tyk2-demo': ,\n", - " 'Transformation-1d104d98ba12a95e13d2e086f1839b51-ddotson-tyk2-demo': }" - ] - }, - "execution_count": 35, + "{'Transformation-15fdd9ff3f68cde67e706ffa87f6a9b7-ddotson-tyk2-demo': ,\n", + " 'Transformation-152928d4d69298dcedf2dd622073a95b-ddotson-tyk2-demo': ,\n", + " 'Transformation-82e90d15b483a813a6501073e3964930-ddotson-tyk2-demo': ,\n", + " 'Transformation-73164716e2ad3c2e410fce413fa41be4-ddotson-tyk2-demo': ,\n", + " 'Transformation-e3022bfd607e772136f5c262875a8e78-ddotson-tyk2-demo': ,\n", + " 'Transformation-6064e55fed6e90b90b727b299462cbad-ddotson-tyk2-demo': ,\n", + " 'Transformation-28453f83bb03486a08afc096a8dc5298-ddotson-tyk2-demo': ,\n", + " 'Transformation-eebbe29feefc097086749081844d8adc-ddotson-tyk2-demo': ,\n", + " 'Transformation-2a345646fa5b3900aba8b74935fc4ec6-ddotson-tyk2-demo': ,\n", + " 'Transformation-f8fb4a09881bce886dcb55a88be49016-ddotson-tyk2-demo': ,\n", + " 'Transformation-874c39cca17041ceb7a91f142104e8eb-ddotson-tyk2-demo': ,\n", + " 'Transformation-b4857c06e9564cf3c2451f436f7af410-ddotson-tyk2-demo': ,\n", + " 'Transformation-958a3d55d581ec276b72394435df4bd0-ddotson-tyk2-demo': ,\n", + " 'Transformation-22a2847c2a28584956e90d549fc6bcc6-ddotson-tyk2-demo': ,\n", + " 'Transformation-d5cdd9f4f4edd380361129082a01360b-ddotson-tyk2-demo': ,\n", + " 'Transformation-20fd18a057e4affd5c78cad41a81d3b4-ddotson-tyk2-demo': ,\n", + " 'Transformation-44cf523e9d0e53674fadc59c9b9110b7-ddotson-tyk2-demo': ,\n", + " 'Transformation-856f85d23d9b8b84916d5b83ebfa1773-ddotson-tyk2-demo': ,\n", + " 'Transformation-ef57a778259ba88b9fb4c23cc2bd78f4-ddotson-tyk2-demo': ,\n", + " 'Transformation-c33480c9f0dfd80852541205f46914a7-ddotson-tyk2-demo': ,\n", + " 'Transformation-4bfeb3edd7f0d47ec52f533a5cf435a0-ddotson-tyk2-demo': ,\n", + " 'Transformation-8e74110792d4c68bb5a772733d490401-ddotson-tyk2-demo': ,\n", + " 'Transformation-06eac733376ab1ff566248040fe37e01-ddotson-tyk2-demo': ,\n", + " 'Transformation-76de739d87d53981abfff4ab9ce071e4-ddotson-tyk2-demo': }" + ] + }, + "execution_count": 103, "metadata": {}, "output_type": "execute_result" } @@ -2238,7 +2393,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 104, "id": "0717730d-dc4a-4ee5-a7e9-6af423c3335c", "metadata": {}, "outputs": [ @@ -2386,7 +2541,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 105, "id": "7076019c-aa12-4be3-9b87-f864b5a5ffd8", "metadata": {}, "outputs": [], @@ -2396,7 +2551,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 106, "id": "f688cd6c", "metadata": {}, "outputs": [], @@ -2414,164 +2569,1843 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 109, "id": "7af574ec", "metadata": { "scrolled": true }, - "outputs": [], - "source": [ - "# Next we create a results dictionary and scan our network edges to accumulate all\n", - "# the free energy results and their uncertainty\n", - "results_dir = Path('results')\n", - "results_dir.mkdir(parents=True, exist_ok=True)\n", - "results = dict()\n", - "for tf_sk in asc.get_network_transformations(an_sk):\n", - " transformation = asc.get_transformation(tf_sk)\n", - " result = asc.get_transformation_results(tf_sk)\n", - " if result is None:\n", - " continue\n", - " runtype = _scan_components(transformation.stateA)\n", - " mapping = transformation.mapping['ligand']\n", - " nameA = mapping.componentA.name\n", - " nameB = mapping.componentB.name\n", - "\n", - " # store in accumulator\n", - " if f\"{nameA}_{nameB}\" in results.keys():\n", - " results[f\"{nameA}_{nameB}\"][runtype] = result\n", - " else:\n", - " results[f\"{nameA}_{nameB}\"] = {runtype: result, 'molA': nameA, 'molB': nameB}\n", - "\n", - " # output individual results to a separate `.dat` file for future use\n", - " filename = results_dir / f\"{nameA}_{nameB}.{runtype}.results.dat\"\n", - " output = f\"{result.get_estimate()},{result.get_uncertainty()}\"\n", - "\n", - " with open(filename, 'w') as f:\n", - " f.write(output)" - ] - }, - { - "cell_type": "markdown", - "id": "7a185273-3bea-4ee0-b69b-64a859bff35c", - "metadata": {}, - "source": [ - "### Writing out a `cinnabar` input CSV file\n", - "\n", - "Since this is a known benchmark system, we have experimental values for each of our ligands. We can combine them with our results to create a `cinnabar` input CSV file.\n", - "\n", - "**Note: this will change very soon. We are in the process of refactoring the cinnabar API.**" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "id": "e61661ee", - "metadata": {}, - "outputs": [], - "source": [ - "# Here we create a dictionary of experimental values from an input protein-ligand benchmark ligands.yml\n", - "\n", - "# First load the yaml data\n", - "import yaml\n", - "\n", - "with open('ligands.yml') as stream:\n", - " exp_data = yaml.safe_load(stream)" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "id": "8d693308", - "metadata": {}, - "outputs": [], - "source": [ - "# Define a method for converting between Ki to estimated DG\n", - "from openff.units import unit\n", - "import math\n", - "\n", - "def ki_to_dg(\n", - " ki: unit.Quantity, uncertainty: unit.Quantity,\n", - " temperature: unit.Quantity = 298.15 * unit.kelvin\n", - ") -> tuple[unit.Quantity, unit.Quantity]:\n", - " \"\"\"\n", - " Convenience method to convert a Ki w/ a given uncertainty to an\n", - " experimental estimate of the binding free energy.\n", - " \n", - " Parameters\n", - " ----------\n", - " ki : unit.Quantity\n", - " Experimental Ki value (e.g. 5 * unit.nanomolar)\n", - " uncertainty : unit.Quantity\n", - " Experimental error. Note: returns 0 if =< 0 * unit.nanomolar.\n", - " temperature : unit.Quantity\n", - " Experimental temperature. Default: 298.15 * unit.kelvin.\n", - " \n", - " Returns\n", - " -------\n", - " DG : unit.Quantity\n", - " Gibbs binding free energy.\n", - " dDG : unit.Quantity\n", - " Error in binding free energy.\n", - " \"\"\"\n", - " if ki > 1e-15 * unit.nanomolar:\n", - " DG = (unit.molar_gas_constant * temperature.to(unit.kelvin)\n", - " * math.log(ki / unit.molar)).to(unit.kilocalorie_per_mole)\n", - " else:\n", - " raise ValueError(\"negative Ki values are not supported\")\n", - " if uncertainty > 0 * unit.molar:\n", - " dDG = (unit.molar_gas_constant * temperature.to(unit.kelvin)\n", - " * uncertainty / ki).to(unit.kilocalorie_per_mole)\n", - " else:\n", - " dDG = 0 * unit.kilocalorie_per_mole\n", - " \n", - " return DG, dDG" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "id": "bbc447a8", - "metadata": {}, - "outputs": [], - "source": [ - "from openff.units import unit\n", - "\n", - "exp_values = {}\n", - "for lig in exp_data:\n", - " exp_units = unit(exp_data[lig]['measurement']['unit'])\n", - " exp_values[lig] = {}\n", - " DG, dDG = ki_to_dg(exp_data[lig]['measurement']['value'] * exp_units,\n", - " exp_data[lig]['measurement']['error'] * exp_units)\n", - " exp_values[lig]['value'] = DG\n", - " exp_values[lig]['error'] = dDG" - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "id": "03439752", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "# write out the cinnabar input file\n", - "with open('cinnabar_input.csv', 'w') as f:\n", - " f.write(\"# Experimental block\\n\")\n", - " f.write(\"# Ligand, expt_DDG, expt_dDDG\\n\")\n", - " for entry in exp_values:\n", - " f.write(f\"{entry},{exp_values[entry]['value'].m:.2f},{exp_values[entry]['error'].m:.2f}\\n\")\n", - " f.write('\\n')\n", - " \n", - " f.write('# Calculated block\\n')\n", - " f.write('# Ligand1,Ligand2,calc_DDG,calc_dDDG(MBAR),calc_dDDG(additional)\\n')\n", - " for entry in results:\n", - " estimate = (results[entry]['complex'].get_estimate()\n", - " - results[entry]['solvent'].get_estimate())\n", - " err = np.sqrt(results[entry]['complex'].get_uncertainty()**2\n", - " + results[entry]['solvent'].get_uncertainty()**2)\n", - " molA = results[entry]['molA']\n", - " molB = results[entry]['molB']\n", + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5640f16b208b45b2bed156a7c1cd2abb", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "845047b7a9cf4c85ac99743a3ecc518e",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "47d1c9fb6abb40c69bebf036eda37a9a",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "c53caabbffe2450abf5a5bf7d79b2971",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "ad925a7d7ac140c1a4f8c5afb8478ebb",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "5ef90d51ad2046f289db987ff1b4628a",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "838201f53e1d4149a9c1536443aee3ce",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "d50442f24ec94b24a8d68d020d93cee6",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "674d814a72ba4fb2b636162ac63fb280",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "383e725ae62d4df2ae55b14e084af261",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "1fb3baae68dd4fb08ff1030ced2b6d11",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "bf1110fdffb9434c9592b9b1fcb8b2de",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "4f822a027b4c46b5bc1ac9d2eb08a3ef",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "b9698a65e59240799d4b01943b45b1a8",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "24b046c1c06b4822be16f35b329bbb80",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "42932d87f6764a7bac5f55f07ae87ebb",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "79ba040445ac48c8b4a023921860635d",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "76d1dfe808aa48ecb5fb51d28a651260",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "7c5b48f995964c688cd4d4616db9a98b",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "ae39c441c65f458c89733f8999647ff6",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "142d2b7e001649bc92c1f67dcd68aa61",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "d9edac5adf1b4b23895a97f732085228",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "e6430eb94a95449e8cf05b40dd35c53c",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "705fc471c9934c73ab154770dfa260fa",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "a824882f0bcd4aee8837198ff622ab98",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "263ceb1b35844f688257019aab56abdf",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "bd7e31f3b1ab43e491fd8d9971cfd816",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "dc2a347f4b5c4505bbab03b8a0282265",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "583bf82b76cd45369508e44eee099745",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "3d68642ae3b74b7d9ed59c8bac3d6183",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "811f3559740f457396013e40808136b6",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "e8d49748a3a54b6bb02ba88534338eba",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "cc5410edf0f54836bda93c10ddf338a7",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "f53b79d2deb44be78d72ce65994faefa",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "a42280ce12b74e409c0750c577fdae5d",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "a4dc7fa912944302b857ae7f850e4f90",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "e8962d615e9a4e27ba7d8c5054102a2d",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "383eb5aebf724c72acb6ad6b0441e12b",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "8d2fddee302348ec8d84e9ac59074882",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "a5edea747d104f6b8c446502ad7018b2",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "c1f70064159645b38c3c77cdf96c1092",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "bc2f90eccdf743d0b27c13d70f2a0530",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "f3bfab0988684822ae3827397bb21747",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "f7dff08088554f3689630d034311ad15",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "720a037fd3634ad390d57a41e624c52d",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "afc64bb900fe4ea7a386995529725151",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "53bc5bb6d8ec40e884b4a2c9c4305b1c",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "a75e01b4007b4dfc981d0a7180dc91ce",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "5db61c39491b4c5788065e9144912f99",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "1a5fc3c12fa14995ae9c8da66f978ddd",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "70a986f7ce2c43dbaa0ea6ead8342e59",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "e61f3b4367034945b3265f576dfad535",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "ee2922bba40c4bb1840d35a806d217b8",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "c1e2d7ea12984bfead52f1ee8f07f643",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "8a8902fc7888428b8717f700d044c94f",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "a9b270285b394c48bb5da6e7978be104",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "2e208d5417e34d38bfc7d2b5b9a1a245",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "f4d94b60391f4557800763de8c0b7d0f",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "cb6fb55d42a545bbad0feae5e94e8fc7",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "9427b4c604b3464f8761b5f5610bf48b",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "30f8344c2ab34b14a828dd5dc299319b",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "b968ae8260b248e98fb3327273c14660",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "3279db9a348a4bc9a1b72fb2a54cb8a8",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "177311c4b4784caa8d966a84eb2b6e32",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "147ca9dba234457b958db4d29287152b",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "17817e9a32f943e7aa4c65dcac85e1ff",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "16379ac2e83648a4b0383e44004d5f07",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "6558a1acb84c4c41a66122255c078a3c",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "a809e3b4d67e45a8964cb201bb6625f5",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "972ec7796a16447f94371ffbdcfbf36d",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "# Next we create a results dictionary and scan our network edges to accumulate all\n",
+    "# the free energy results and their uncertainty\n",
+    "results_dir = Path('results')\n",
+    "results_dir.mkdir(parents=True, exist_ok=True)\n",
+    "results = dict()\n",
+    "for tf_sk in asc.get_network_transformations(an_sk):\n",
+    "    transformation = asc.get_transformation(tf_sk)\n",
+    "    result = asc.get_transformation_results(tf_sk)\n",
+    "    if result is None:\n",
+    "        continue\n",
+    "    runtype = _scan_components(transformation.stateA)\n",
+    "    mapping = transformation.mapping[0]\n",
+    "    nameA = mapping.componentA.name\n",
+    "    nameB = mapping.componentB.name\n",
+    "\n",
+    "    # store in accumulator\n",
+    "    if f\"{nameA}_{nameB}\" in results.keys():\n",
+    "        results[f\"{nameA}_{nameB}\"][runtype] = result\n",
+    "    else:\n",
+    "        results[f\"{nameA}_{nameB}\"] = {runtype: result, 'molA': nameA, 'molB': nameB}\n",
+    "\n",
+    "    # output individual results to a separate `.dat` file for future use\n",
+    "    filename = results_dir / f\"{nameA}_{nameB}.{runtype}.results.dat\"\n",
+    "    output = f\"{result.get_estimate()},{result.get_uncertainty()}\"\n",
+    "\n",
+    "    with open(filename, 'w') as f:\n",
+    "        f.write(output)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7a185273-3bea-4ee0-b69b-64a859bff35c",
+   "metadata": {},
+   "source": [
+    "### Writing out a `cinnabar` input CSV file\n",
+    "\n",
+    "Since this is a known benchmark system, we have experimental values for each of our ligands. We can combine them with our results to create a `cinnabar` input CSV file."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 110,
+   "id": "e61661ee",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Here we create a dictionary of experimental values from an input protein-ligand benchmark ligands.yml\n",
+    "\n",
+    "# First load the yaml data\n",
+    "import yaml\n",
+    "\n",
+    "with open('ligands.yml') as stream:\n",
+    "    exp_data = yaml.safe_load(stream)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 111,
+   "id": "8d693308",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Define a method for converting between Ki to estimated DG\n",
+    "from openff.units import unit\n",
+    "import math\n",
+    "\n",
+    "def ki_to_dg(\n",
+    "    ki: unit.Quantity, uncertainty: unit.Quantity,\n",
+    "    temperature: unit.Quantity = 298.15 * unit.kelvin\n",
+    ") -> tuple[unit.Quantity, unit.Quantity]:\n",
+    "    \"\"\"\n",
+    "    Convenience method to convert a Ki w/ a given uncertainty to an\n",
+    "    experimental estimate of the binding free energy.\n",
+    "    \n",
+    "    Parameters\n",
+    "    ----------\n",
+    "    ki : unit.Quantity\n",
+    "        Experimental Ki value (e.g. 5 * unit.nanomolar)\n",
+    "    uncertainty : unit.Quantity\n",
+    "        Experimental error. Note: returns 0 if =< 0 * unit.nanomolar.\n",
+    "    temperature : unit.Quantity\n",
+    "        Experimental temperature. Default: 298.15 * unit.kelvin.\n",
+    "        \n",
+    "    Returns\n",
+    "    -------\n",
+    "    DG : unit.Quantity\n",
+    "        Gibbs binding free energy.\n",
+    "    dDG : unit.Quantity\n",
+    "        Error in binding free energy.\n",
+    "    \"\"\"\n",
+    "    if ki > 1e-15 * unit.nanomolar:\n",
+    "        DG = (unit.molar_gas_constant * temperature.to(unit.kelvin)\n",
+    "              * math.log(ki / unit.molar)).to(unit.kilocalorie_per_mole)\n",
+    "    else:\n",
+    "        raise ValueError(\"negative Ki values are not supported\")\n",
+    "    if uncertainty > 0 * unit.molar:\n",
+    "        dDG = (unit.molar_gas_constant * temperature.to(unit.kelvin)\n",
+    "               * uncertainty / ki).to(unit.kilocalorie_per_mole)\n",
+    "    else:\n",
+    "        dDG = 0 * unit.kilocalorie_per_mole\n",
+    "        \n",
+    "    return DG, dDG"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 112,
+   "id": "bbc447a8",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from openff.units import unit\n",
+    "\n",
+    "exp_values = {}\n",
+    "for lig in exp_data:\n",
+    "    exp_units = unit(exp_data[lig]['measurement']['unit'])\n",
+    "    exp_values[lig] = {}\n",
+    "    DG, dDG = ki_to_dg(exp_data[lig]['measurement']['value'] * exp_units,\n",
+    "                       exp_data[lig]['measurement']['error'] * exp_units)\n",
+    "    exp_values[lig]['value'] = DG\n",
+    "    exp_values[lig]['error'] = dDG"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 113,
+   "id": "03439752",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "\n",
+    "# write out the cinnabar input file\n",
+    "with open('cinnabar_input.csv', 'w') as f:\n",
+    "    f.write(\"# Experimental block\\n\")\n",
+    "    f.write(\"# Ligand, expt_DDG, expt_dDDG\\n\")\n",
+    "    for entry in exp_values:\n",
+    "        f.write(f\"{entry},{exp_values[entry]['value'].m:.2f},{exp_values[entry]['error'].m:.2f}\\n\")\n",
+    "    f.write('\\n')\n",
+    "    \n",
+    "    f.write('# Calculated block\\n')\n",
+    "    f.write('# Ligand1,Ligand2,calc_DDG,calc_dDDG(MBAR),calc_dDDG(additional)\\n')\n",
+    "    for entry in results:\n",
+    "        estimate = (results[entry]['complex'].get_estimate()\n",
+    "                    - results[entry]['solvent'].get_estimate())\n",
+    "        err = np.sqrt(results[entry]['complex'].get_uncertainty()**2\n",
+    "                      + results[entry]['solvent'].get_uncertainty()**2)\n",
+    "        molA = results[entry]['molA']\n",
+    "        molB = results[entry]['molB']\n",
     "        f.write(f\"{molA},{molB},{estimate.m:.2f},0,{err.m:.2f}\\n\")"
    ]
   },
@@ -2587,14 +4421,14 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 45,
+   "execution_count": 124,
    "id": "60413e9e",
    "metadata": {},
    "outputs": [],
    "source": [
     "import cinnabar\n",
     "from cinnabar import plotting as cinnabar_plotting\n",
-    "from cinnabar.wrangle import FEMap\n",
+    "from cinnabar import FEMap, femap\n",
     "%matplotlib inline"
    ]
   },
@@ -2616,13 +4450,13 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 47,
+   "execution_count": 118,
    "id": "56de38d7-fbde-4610-a1ff-428eaaa70648",
    "metadata": {},
    "outputs": [
     {
      "data": {
-      "image/png": "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b/ny5UhISMDIkSOV2vzvf/9Dx44d0aJFCxw4cICP2iMiIvqCSPHlQkMiIiLKEBkZiQoVKmDUqFHo1asX5HI5Pnz4gGvXrmHjxo148uQJ5s+fj/Hjx2dc8/LlSzRt2hR6enoYPXo0qlSpgqSkJERGRuLo0aNYs2YNrK2tkZCQgLp16yImJga+vr6oXLkygoKCsH//fkRGRmLz5s3o27cvgLRH7+3ZswcfP35UypeamgpXV1dcuHABY8aMQaNGjaClpYWoqCicPn0aHTp0QKdOnQAAfn5+mDlzJgYMGAAnJyeYmpoiOjoaFy9ehIGBAWbPnp2nn9WbN29Qp04dlCxZEt7e3tDR0cG8efNw7do1XLx4EVWrVs1oW7lyZQDAgwcPMo6tW7cOAFCpUiXExsYiKCgIGzZswNy5czF16tSMdufOnUPr1q1haWmJjRs3Qk9PTylH9erVMy13ICIiKm44sk9ERPQdVqxYgRUrVkBDQwPGxsaoWLEiXF1dMWjQoIyd5NOVKVMGly9fho+PD/z9/REVFQUjIyNUqFAho8gGAAMDA5w6dQpjx47F5MmTIRKJ0Lp1a6xatQouLi4wMTH5Zi4NDQ0cOnQIy5Ytw9atW+Hn5wdNTU1YW1tDIpGgVq1aGW09PDxQvXp1LFu2DAEBAfj8+TNKly6Nhg0bYujQoXn+GZUqVQpnz57FxIkT0a9fP8hkMjRt2hQhISFKhT4ApU0C0ykUCixduhRPnjyBWCxGvXr1sH//fnTo0EGp3YkTJ5CYmIjIyEj8+OOPmfo5ffo0pFJpnl8PERFRUcaRfSIiokJm7ty5mDFjBp4+fQpra2uh4xAREVERxJF9IiIiAa1cuRIAULVqVaSkpODUqVNYvnw5fvnlFxb6RERElGss9omIiASkr6+PJUuWIDIyEp8/f0b58uUxZcoUzJgxQ+hoREREVIRxGj8RERERERGRmhELHYCIiIiIiIiIVIvFPhEREREREZGaYbFPREREREREpGZY7BMRERERERGpGRb7RERERERERGqGxT4RERERERGRmmGxT0RERERERKRmWOwTERERERERqRkW+0RERERERERqhsU+ERERERERkZphsU9ERERERESkZljsExEREREREakZFvtEREREREREaobFPhEREREREZGaYbFPREREREREpGZY7BMRERERERGpGRb7RERERERERGqGxT4RERERERGRmmGxT0RERERERKRmWOwTERERERERqRkW+0RERERERERqhsU+ERERERERkZphsU9ERERERESkZljsExEREREREakZFvtEREREREREaobFPhEREREREZGaYbFPREREREREpGZY7BMRERERERGpGRb7RERERERERGqGxT4RERERERGRmmGxT0RERERERKRmWOwTERERERERqRkW+0RERERERERqhsU+ERERERERkZphsU9ERERERESkZljsExEREREREakZFvtEREREREREaobFPhEREREREZGaYbFPREREREREpGZY7BMRERERERGpGRb7RERERERERGqGxT4RERERERGRmmGxT0RERERERKRmWOwTERERERERqRkW+0RERERERERqhsU+ERERERERkZphsU9ERERERESkZljsExEREREREakZFvtEREREREREaobFPhEREREREZGaYbFPREREREREpGZY7BMRERERERGpGRb7RERERERERGqGxT4RERERERGRmmGxT0RERERERKRmWOwTERERERERqRkW+0RERERERERqhsU+ERERERERkZphsU9ERERERESkZljsExEREREREakZFvtEREREREREaobFPhEREREREZGaYbFPREREREREpGZY7BMRERERERGpGRb7RERERERERGqGxT4RERERERGRmmGxT0RERERERKRmWOwTERERERERqRkW+0RERERERERqhsU+ERERERERkZphsU9ERERERESkZljsExEREREREakZFvtEREREREREaobFPhEREREREZGaYbFPREREREREpGZY7BMRERERERGpGRb7RERERERERGqGxT4RERERERGRmmGxT0RERERERKRmNIUOQERERJRJqhx4nwQkpwLaGoCpLqDBMQoiIqLvxWKfiIiICoeEZOB8FHDlJRAVB8jk/57TFAPWxoB9GaCJNWCgLVxOIiKiIkCkUCgUQocgIiKiYixVDhx7CATdB+QKILt3JiIAYhHgbAe0qcTRfiIioq9gsU9ERETCiUkEVl0CXsTn/ForI2B4Q8BMT/W5iIiIijh+HE5ERFTMbNq0CSKRCJGRkQAAd3d32NraFnyQmETAPwx49REAID04FdKDU7//+lcf066PScx1hJCQEIhEoiy/zp8/r9TW3d09y3ZVq1bN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bICGti75jlehcFfn//VBwKVC0T5bAAkJCThx4gQAYMyYMc+8P2jQIAQHB5fY+YmIiApLpdVqtaJDEBERke5bv349+vfvj8jISLi7v8bV+PQsYOJvQE7+H0Ga/vIRXC3tsaXD5MKvaaACvmgPlDMqei4iIiI9wiv7RERE9IwNGzYgNjYWDRs2hFqtRlRUFBYuXAgvL6/XK/oAUN4Y8KkF7LkCALiSEosj8X/hwv1oDKglF21Nn1os+kRERP/Csk9ERETPsLCwwMaNGzFnzhykp6fDwcEBAQEBmDNnTt4x2dnZL11DrVa/eL6/gytwOh64k4b5p3/B7n+OY2CdNzGyQcfCBVWrAHvz3PX+RaPRQKN5+U0ADQ35YxAREekvbuMnIiKiQouOjkb16tVfesz06dMxY8aMFx+QlAEsjAQeZgGaIvw4olYBlibAeHfAJv/N+WbMmIGZM2e+9OM3b96Ei4tL4c9LRERUBrDsExERUaFlZWXh/PnzLz3G0dERjo6OL18oKQNYegKIe1j4EI4WwMgWzxR9AIiLi0NcXNxLP96oUSMYGxsX/rxERERlAMs+ERERiZWjAfZfB0Ku5l7hf9lPJirkXtH3qZW7dd+g4I8BJCIiUhKWfSIiItIN6VlA1G3gVDxwOxXI/tfMvaEacLIEmjsAbaryZnxERESvwLJPREREukejzd3in5UDGBvkbtVXq0SnIiIiKjNY9omIiIiIiIj0DAfdiIiIiIiIiPQMyz4RERERERGRnmHZJyIiIiIiItIzLPtEREREREREeoZln4iIiIiIiEjPsOwTERERERER6RmWfSIiIiIiIiI9w7JPREREREREpGdY9omIiIiIiIj0DMs+ERERERERkZ5h2SciIiIiIiLSMyz7RERERERERHqGZZ+IiIiIiIhIz7DsExEREREREekZln0iIiIiIiIiPcOyT0RERERERKRnWPaJiIiIiIiI9AzLPhEREREREZGeYdknIiIiIiIi0jMs+0RERERERER6hmWfiIiIiIiISM+w7BMRERERERHpGZZ9IiIiIiIiIj3Dsk9ERERERESkZ1j2iYiIiIiIiPQMyz4RERERERGRnmHZJyIiIiIiItIzLPtEREREREREeoZln4iIiIiIiEjPsOwTERERERER6RmWfSIiIiIiIiI9w7JPREREREREpGdY9omIiIiIiIj0DMs+ERERERERkZ5h2SciIiIiIiLSMyz7RERERERERHqGZZ+IiIiIiIhIz7DsExEREREREekZln0iIiIiIiIiPcOyT0RERERERKRnWPaJiIiIiIiI9AzLPhEREREREZGeYdknIiIiIiIi0jMs+0RERERERER6hmWfiIiIiIiISM+w7BMRERERERHpGZZ9IiIiIiIiIj3Dsk9ERERERESkZ1j2iYiIiIiIiPQMyz4RERERERGRnmHZJyIiIiIiItIzLPtEREREREREeoZln4iIiIiIiEjPsOwTERERERER6RmWfSIiIiIiIiI9w7JPREREREREpGdY9omIiIiIiIj0DMs+ERERERERkZ5h2SciIiIiIiLSMyz7RERERERERHqGZZ+IiIiIiIhIz7DsExEREREREemZ/2u/DmQAAAAABvlb3+Mri2QfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZmQfAAAAZgIh0VO0/y/zZgAAAABJRU5ErkJggg==",
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAA/sAAARECAYAAAAqQwEUAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd1zVZf/H8dcBxIkhmAou3DY09wB3qThwpeYWR+7UtHIrjpxpqaWZA9yj3AMsBzjA0jRzVK5AQdziQJFxzu8PfnFHLvYBfD8fDx53fM/1va7P4S7lfa7xNZhMJhMiIiIiIiIikmlYmLsAEREREREREUlZCvsiIiIiIiIimYzCvoiIiIiIiEgmo7AvIiIiIiIiksko7IuIiIiIiIhkMgr7IiIiIiIiIpmMwr6IiIiIiIhIJqOwLyIiIiIiIpLJKOyLiIiIiIiIZDIK+yIiIiIiIiKZjMK+iIiIiIiISCajsC8iIiIiIiKSySjsi4iIiIiIiGQyCvsiIiIiIiIimYzCvoiIiIiIiEgmo7AvIiIiIiIiksko7IuIiIiIiIhkMgr7IiIiIiIiIpmMwr6IiIiIiIhIJqOwLyIiIiIiIpLJKOyLiIiIiIiIZDIK+yIiIiIiIiKZjMK+iIiIiIiISCajsC8iIiIiIiKSySjsi4iIiIiIiGQyCvsiIiIiIiIimYzCvoiISCIEBgZiMBgwGAysW7fuqdc9PDwwGAzcunUrxcb8p8+MatOmTXTs2JGSJUuSPXt2nJyc6Ny5M+fPn39m+/DwcMaPH0/p0qXJmjUr9vb21K9f/7ntRURE5GlW5i5AREQkoxozZgzvv/8+WbJkMXcp6dqMGTMoUKAAY8aMoXjx4ly5coWpU6dSqVIljhw5wltvvRXX9uHDh9SvX5+rV68ycuRIypcvz7179/D39+fRo0dmfBciIiIZi8K+iIhIEjRp0gRvb2++/fZbPvroI3OXk65t376dfPnyxbvWoEEDnJyc+PLLL1myZEnc9bFjx/LHH3/w+++/U7x48bjrLVq0SLN6RUREMgMt4xcREUmCBg0a0LhxYyZPnsyDBw9e2n7Pnj28++675M6dmxw5cuDi4sLevXufardz504qVKhA1qxZKVasGF988cUz+wsLC6NXr17Y2dmRK1cumjVrxqVLlzAYDHh4eMRre/78eTp16kS+fPnImjUrb7zxBt988028NkajkSlTplCmTBmyZ8+Ora0t5cuXZ+7cuQn/oTzHf4M+gKOjI4UKFeLKlStx1x49esSSJUto165dvKAvIiIiiaewLyIikkQzZszg1q1bzJo164XtVq1aRaNGjcidOzfLly9nw4YN2NnZ0bhx43iBf+/evbRs2RIbGxvWrVvHrFmz2LBhA56envH6MxqNuLm5sWbNGkaMGMHmzZupXr06rq6uT4199uxZqlatyunTp5k9ezY7duygWbNmDB48mIkTJ8a1mzlzJh4eHnTs2JGdO3eyfv16evXqRVhYWFwbk8lEdHR0gr5e5tKlSwQFBcVbwv/rr78SHh5OqVKl6N+/P3ny5MHa2poqVaqwc+fOl/YpIiIi/2ISERGRBPv7779NgGnWrFkmk8lk6ty5sylnzpym0NBQk8lkMk2YMMEEmG7evGkymUym8PBwk52dncnNzS1ePzExMaZ33nnHVK1atbhr1atXNzk6OpoeP34cd+3+/fsmOzs707//yt65c6cJMC1cuDBen9OmTTMBpgkTJsRda9y4salQoUKme/fuxWs7aNAgU7Zs2Ux37twxmUwmU/PmzU0VKlR44Xvfv3+/CUjQ199///3cfqKiokz16tUz5c6d23T58uW462vXrjUBpty5c5tcXFxM27ZtM+3YscNUv359k8FgMPn4+LywPhEREfkfzeyLiIgkw5QpU4iKioo3S/5v/v7+3Llzh+7du8eb+TYajbi6unL06FHCw8MJDw/n6NGjtGnThmzZssXdb2Njg5ubW7w+/fz8AGjfvn286x07doz3fUREBHv37qV169bkyJEj3vhNmzYlIiKCI0eOAFCtWjVOnjzJgAED2L17N/fv33/qvVSuXJmjR48m6MvR0fGZPw+TyUSvXr04ePAgK1asoHDhwnGvGY1GAKytrfH29sbNzY1mzZqxY8cOHBwcmDx58jP7FBERkafpgD4REZFkcHJyYsCAAXz99dcMGzbsqdevX78OQNu2bZ/bx507dzAYDBiNRgoUKPDU6/+9dvv2baysrLCzs4t3PX/+/E+1i46OZv78+cyfP/+ZY//ziMBRo0aRM2dOVq1axbfffoulpSV16tRhxowZVKlSBYBcuXJRoUKF576Pf7OyevpXDJPJRO/evVm1ahXLly+nZcuW8V63t7cHwNnZGRsbm7jrOXLkoG7dumzZ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fREREREREZFMRmFfREREREREJJNR2BcRERERERHJZBT2RURERERERDIZhX0RERERERGRTEZhX0RERERERCSTUdgXERERERERyWQU9kVEREREREQyGYV9ERERERERkUxGYV9EREREREQkk1HYFxEREREREclkFPZFREREREREMhmFfREREREREZFMRmFfREREREREJJNR2BcRERERERHJZBT2RURERERERDIZhX0RERERERGRTEZhX0RERERERCSTUdgXERERERERyWQU9kVEREREREQyGYV9ERERERERkUxGYV9EREREREQkk1HYFxEREREREclkFPZFREREREREMhmFfREREREREZFMRmFfREREREREJJNR2BcRERERERHJZBT2RURERERERDIZhX0RERERERGRTEZhX0RERERERCSTUdgXERERERERyWQU9kVEREREREQyGYV9ERERERERkUxGYV9EREREREQkk1HYFxEREREREclkFPZFREREREREMhmFfREREREREZFMRmFfREREREREJJNR2BcRERERERHJZBT2RURERERERDIZhX0RERERERGRTEZhX0RERERERCSTUdgXERERERERyWQU9kVEREREREQyGYV9ERERERERkUxGYV9EREREREQkk1HYFxEREREREclkFPZFREREREREMhmFfREREREREZFMRmFfREREREREJJNR2BcRERERERHJZBT2RURERERERDIZhX0RERERERGRTEZhX0RERERERCSTUdgXERERERERyWQU9kVEREREREQyGYV9ERERERERkUxGYV9EREREREQkk1HYFxEREREREclkFPZFREREREREMhmFfREREREREZFMRmFfREREREREJJNR2BcRERERERHJZBT2RURERERERDIZhX0RERERERGRTEZhX0RERERERCSTUdgXERERERERyWQU9kVEREREREQyGYV9ERERERERkUxGYV9EREREREQkk1HYFxEREREREclkFPZFREREREREMhmFfREREREREZFMRmFfREREREREJJNR2BcRERERERHJZKzMXYCIiIjIU2KMcDcCImPA2hLyZANLzVGIiIgklMK+iIiIpA/hkXAkGH4NheD7EG3832tWFlAoN1R2gBqFIKe1+eoUERHJAAwmk8lk7iJERETkFRZjhN0Xwfs8GE3w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qZKknTt36v/+7/8Mp4M/8M9WA/xXQkKCunTpopo1a+qLL75QcHCw6UhG7Ny5U++++66ee+65Ev+ourPz+h07djQdBQAA+Lmrr75aS5cu1ebNm3XfffdpzZo1atGihbp3766UlBTT8eDjbB6Px2M6BGBCUlKSOnbsqKSkJMXExKhGjRqmIxlz//3369tvv9Uff/yhMmXKmI5TIA899JA2bdqkH3/80XQUAAAASdKSJUt06623ymazSZLcbreWLFmi7t27G04GX8bKPvzSmTNndNtttyk+Pl7Lly/366K/detWzZ07V2PGjCnxRV9iXh8AAHifhIQESdkl3+12KzAwUF9//bXhVPB1lH34HbfbrQcffFAxMTH68ssv1ahRI9ORjBo5cqTq1q2rAQMGmI5SYHv37tWuXbuY1wcAAF5j9uzZGjRokM7dUJ2ZmamlS5caTAV/QNmH33n66af1n//8R3PnztWNN95oOo5R69at05IlSzRx4kSVKlXKdJwCc7lcksS8PgAA8Bp169ZVgwYNJEmBgYE5r//xxx+Kj4+/8Juy3NLR09L+k9n/meUujqjwMczsw6+88sorevrpp/XWW2/pscceMx3HKI/HI4fDoeTkZG3evNknnkIwcOBAfffdd/rpp59MRwEAAMjh8XgUGxurd955R/Pnz9eZM2ckSRMmTNDo0aOzLzqVLsXGS5sOSPHJUuY5BT/QLkWFSS2qS22ipLJBBj4FSpqS/2/3wBWaN2+enn76aQ0fPtzvi74kLV++XGvWrNHzzz/vE0VfkizLYl4fAAB4HZvNprZt22rmzJk6ePCgXn31VVWoUEGnT/931f6rHdKwldJnv0p7knIXfSn793uSsr8+bGX29az24zJY2Ydf+Oabb9S1a1f17dtXM2fOzDkJ1V+53W61aNFCoaGhWr16tU/8ecTHx6tmzZpasGCB+vTpYzoOAADA5SWmStM2Zm/Xz6vIctKjLaWI0oWfCz7BN5bzgEv48ccfddttt+mmm27S+++/7xPFtqA+/fRT/fDDD5oyZYrP/Hkwrw8AAK7U2cWfPXv2SJL69eun6OjoIvt+NptN48aNy/1iYqr00jrpYEr+bnowJfv9ian5zvXZZ5+pb9++uuqqq1S6dGlFR0frvvvu044dO8679syZM3rppZd07bXXqmzZsqpataq6du2q9evX5/v7o2ixsg+ftmfPHrVr106RkZGyLEuhoaGmIxmXkZGhRo0a6eqrr/apU2AffvhhrV+/Xj///LPpKAAAwMvNnDlT/fv31+7duxUdHa2dO3cqOTlZzZo1K5LvFxsbq6ioKEVFRWW/kOWWpqzNLuzuAtQxu02qFioNv1EKyPs6buvWrVWtWjX17t1bdevW1b59+zR58mTt27dPsbGxaty4cc61Dz74oD7++GMNHz5cN910kxITEzV16lT9+OOPWrdunVq1apX/z4EiEXj5S4CS6dixY+rSpYtCQkK0dOlSiv5/ffjhh/rjjz+0YMEC01EKlWVZ6ty5s+kYAACgBKpXr16R3r9Nmza5X1ixM39b9//M7cm+z4qdUrf6eX774sWLVaVKlVyv3XTTTYqOjtZrr72m999/X1L2qv7cuXN17733atKkSTnXtm/fXpGRkfr4448p+16IbfzwSampqbr11lt17NgxrVixQlWrVjUdySukpqZq/Pjx6tu3r66//nrTcQrN/v37tWPHDg7nAwAA+XKhbfxJSUkaMGCAIiIiFBoaqu7du2vXrl0X3pJ/GbnecypdM1+dJtv0Hvo2/kf93fqXKn7QV2Hv36kHv3lFpzLSdPD0cd31f1MVPuNuVZ/1gJ5eP0MZWZm57nkmK0MTvp+nhvMGKeTWxqpYsaI6deqUp231fy76khQZGamoqCjt27cv5zW73S673a7y5cvnujYsLEx2u10hISFX/oeBYsPKPnxOVlaW7r33Xv3www9atWqV6tfP+085fdVbb72lw4cPa8KECaajFCrm9QEAQGFyu93q2bOnvv/+e40bN07NmzdXTEyMunTpUvCbx8ZL/925P9D6l26v207zOz+rLUd3asR3s5Xpduu3pHjdXredHm7URSvjf9ALWxYosmyE/nn9bZKkTHeWui4ZqzUHf9FTTXrppqjrlNkmUrGpe7R37161a9cu3/F27dqluLg49e7dO+e1UqVK6dFHH9WMGTN0880352zjHzFihMqXL6+///3vBfkTQRGh7MOneDwePfHEE1q8eLEWLVrEdqJzJCUlacqUKRo4cKCuuuoq03EKlWVZatiwITs4AABAoVi+fLnWrl2r6dOna9CgQZKkzp07KygoSMOHDy/YzTcdyCn7PaJb6eV2A7LvX7OZYg5t17w/XHq13UANub63JOnmqKZasW+zPv7dyin7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mjJlih588EHTcXzC+PHjFRoaqn/+85+moxizbt06ZWVlMa8PAABwAU6nU9u2bdPhw4dNR4EXouyjwBYvXqxHHnlEjz32mJ577jnTcXzC77//rg8//FAjR45UWFiY6TjGuFwuVa1aVVdffbXpKAAAAF7n7O7H1atXG04Cb0TZR4HExsbq7rvvVu/evfXGG28wV11IRo8ercjISD3yyCOmoxjFvD4AAMDF1ahRQ1dddRVz+7ggyj7y7bffflOPHj3UokULzZkzRwEBAaYj+YTNmzfr008/1dixYxUSEmI6jjEpKSn6/vvv2cIPAABwCU6nk7l9XBBlH/ly4MABdenSRVWrVtWXX36p0qVLm47kM0aMGKFrrrlGf/vb30xHMWr9+vXKzMzkcD4AAIBLcDgc+vnnn3XkyBHTUeBlKPvIs+TkZHXr1k3p6elatmyZKlSoYDqSz7AsSytWrNCkSZMUGBhoOo5RLpdLVapUUYMGDUxHAQAA8FrM7eNiKPvIk/T0dPXp00e7du3S8uXLVatWLdORfIbH49Hw4cPVokUL9enTx3Qc4yzLksPhYF4fAADgEmrWrKm6deuylR/n8e+lQ+SJ2+3WgAEDtHr1ai1fvlxNmjQxHcmnLF68WLGxsfq///s/vy+4p06d0oYNG/TGG2+YjgIAAOD1nE4nh/ThPKzs44qNGDFCc+bM0ezZs9WpUyfTcXxKVlaWRowYoU6dOunmm282Hce4mJgY5vUBAACukMPh0E8//aRjx46ZjgIvQtnHFXnzzTf1wgsv6LXXXtPdd99tOo7PmTt3rn755RdNnjzZ71f1pewt/JUqVVKjRo1MRwEAAPB6zO3jQij7uKwFCxZo8ODBGjp0qJ566inTcXxOenq6xowZo969e6tNmzam43gFl8slp9PJDz4AAACuQO3atVWnTh3m9pELZR+XtHr1at1///2655579OKLL5qO45PeffddxcXFadKkSaajeIXTp0/ru+++Yws/AABAHjgcDub2kQtlHxf1888/69Zbb1X79u314Ycfym7nb5fCdurUKU2aNEkPPPCAGjdubDqOV4iNjVVGRoacTqfpKAAAACWG0+nU1q1blZiYaDoKvATtDRe0b98+de3aVdHR0fr8888VHBxsOpJPeuONN5SYmKjx48ebjuI1LMtSxYoVmdcHAADIA4fDIY/HozVr1piO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ayRJCQkJ6tKli6KiovT5558rODjYcDr/tmLFCq1evVqTJ0/mhy5XyLIslSlTRjfccIPpKAAAAD7P4XAoIyNDMTExpqOgGNk8Ho/HdAhc2v79+1WjRg0FBgbq3Xff1WuvvaakpCTFxMSoRo0apuP5NbfbrRtuuEFlypTRmjVrGKW4Qt27d1dmZqZWrFhhOgoAAIDPc7vdqlq1qh555BFNmDDBdBwUk0DTAXB5X3/9taTsOeeHHnpIISEh2rRpE0XfC/znP//Rli1btHr1aor+FcrMzNSaNWs0bNgw01EAAAD8gt1uV8eOHZnb9zPsOS4B/u///k+Bgf/7uUxaWppmzJght9ttMBUyMjI0evRodevWTR06dDAdp8T44YcfdPLkSQ7nAwAAKEZOp1PfffedUlNTTUdBMaHsm5Tllo6elvafzP7PrPPLu9vt1vLly5WZmZnr9VdffVXTpk0rrqS4gJkzZ2rHjh16/vnnTUcpUSzLUunSpZnXBwAAKEYOh0Pp6emKjY01HQXFhG38xe1UuhQbL206IMUnS5nnFPxAuxQVJrWoLrWJksoGaevWrUpMTMy5xGazKTAwUHfeead69uxp4ANAklJTUzVu3Djdc889atq0qek4JYplWWrfvr2CgoJMRwEAAPAb1157rSIiImRZljp16mQ6DooBZb+4ZLmlFTulZTskt0e60LGImW5pT5IUlyR9sV3qWl/TP3s758tXX321Hn30UT3wwAOKiIgoruS4gLfffluHDx/WxIkTTUcpUbKysrRmzRo988wzpqMAAAD4FbvdLofDIZfLZToKigllvzgkpkrTNmZv178SHklZHmnJ75oU2kUJ7XZr2Avj1L59ew6B8wInTpzQlClTNGDAAF111VWm45QoP/zwg5KTk5nXBwAAMMDhcOi5555TWlqaQkJCTMdBEWNmv6glpkovrZMOpuTr7ZUzS2tJx2G6sVELir6XePnll3X69GmNHj3adJQSx+VyKSQkRC1btjQdBQAAwO84nU6dOXNG3333nekoKAY+W/Znzpwpm82mPXv2SJL69eun6Ojo4g2R5c5e0T+Znr11X5Jz0TA5F+XhkWNuT/b7p2284AF++fH+++/LZrMpNDT0vK/169dPNpvtvF8NGjQolO9d0h06dEivvfaannzySR59mA+WZaldu3YKDg42HQUAAMDvNGnSRBUqVOARfH7Cb7bxjx49WoMHDy7eb7pi53lb96d1eDTv93F7su+zYqfUrX6BIiUkJOjpp59WZGSkTpw4ccFrSpcurW+//fa81yBNnjxZgYGBeu6550xHKXHOzuv/85//NB0FAADAL9ntdnXs2FGWZWns2LGm46CI+U3Zr1evXvF+w1Pp2Yfx/UmjiFr5v+eyHZKjtlQ2/6eYDxo0SB07dlRERIQWLFhwwWvsdrvatGmT7+/hq/bs2aPp06dr7NixHJCYD1u3blVSUpIcDofpKAAAAH7L4XBoxIgRzO37AZ/dxv9nF9rGn5SUpAEDBigiIkKhoaHq3r27du3aJZvNpnHjxuXp/unp6Zo0aZIaNGig4OBgVa5RXf1XvqYjqblXz/+8jX9P8iHZpvfQS1sW6oUtCxQ95yGVfvd2ORcN0+9JCcrIytSw2JmKnPWgyr97p267pYcOHz6crz+DOXPmyOVyadq0afl6v78bN26cIiIiin+HiI84O6/fqlUr01EAAAD8ltPpVFpamjZs2GA6CoqY35T9P3O73erZs6fmzp2r5557Tp9//rlat26tLl265OtevXr10tSpU3Xvvfdq6dKlmnrzw/p63xY5Fw1XauaZy97j7V+Wat2BbXq7wyN63/mEth+PV8+vJmiA9YaOpJ7QB50G68U2/bVy41oNHDgwzxkPHz6sp556SlOnTlVUVNQlr01NTVW1atUUEBCgqKgoPf7440pMTMzz9/Qlv/zyiz766CONGjXqgmcd4PIsy1KbNm34CTIAAIBB1113ncLDw3kEnx/wm238f7Z8+XKtXbtW06dP16BBgyRJnTt3VlBQkIYPH56ne3366adavny5Fi5cqNtvvz37IL0vM3R9l6pquXCIZm7/Ro9c2+2S9wgPKqsvuo6S3Zb985ejacl6at17alAhSou6/u/U9+3JCXp98RdKTk5WWFjYFWd89NFHdc011+iRRx655HXXX3+9rr/+el177bWSsldjX3vtNX3zzTfauHGj3xbdUaNGqVatWnr44YdNRymR3G63Vq9eza4IAAAAwwICAtShQwdZlsXTpXyc367sn/1J1l133ZXr9b59++b5XkuWLFF4eLh69uypzMxMZR5JUWZ6hppWqqtqZSrI2v/TZe/RrdYNOUVfkhpWqClJ6l479yPKGpbPXpXfu3fvFedbuHChFi9erPfee++y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OCSmStM2SvtPXtn1HklZHmnJ79LmA9KjLaWI0kUaEd7P5XLplltuMR0DAAAAQAngs0vGM2fOlM1m0549eyRJ/fr1U3R0dPEHSUyVXlonHUyRJDkXDZNz0bArf//BlOz3J6YWWqT3339fNptNoaGh533N4/HovffeU4sWLRQWFqaKFSvK4XBo6dKlhfb9kXcJCQn6448/mNcHAAAAcEV8tuz/2ejRo/X5558X7zfNcmev6J9Mz966L2lah0c1rcOjV34Ptyf7/dM2Zt+vgBISEvT0008rMjLygl8fO3asHn74YbVq1UoLFy7UzJkzFRwcrB49euizzz4r8PdH/jCvDwAAACAvbB6P50LT4yXezJkz1b9/f+3evdvMir4kfbUjeyt+YelxtdStfoFu0bNnT9lsNkVERGjBggVKSUnJ9fWoqCjVqVNHa9asyXktLS1N1apVk8Ph0KJFiwr0/ZE/Dz/8sNatW6dffvnFdBQAAAAAJYDfrOxfaBt/UlKSBgwYoIiICIWGhqp79+7atWuXbDabxo0bl6f7p6ena9KkSWrQoIGCg4NVuVJl9R/6qI6knsh13Z+38e9JPiTb9B56actCvbBlgaLnPKTS794u56Jh+j0pQRlZmRoWO1ORsx5U+dua6raevXT48OF8/RnMmTNHLpdL06ZNu+g1pUqVUvny5XO9FhISkvMLZrhcLrbwAwAAALhiflP2/8ztdqtnz56aO3eunnvuOX3++edq3bq1unTpkq979erVS1OnTtW9996rpUuXamq/ofp63xY5Fw1XauaZy97j7V+Wat2BbXq7wyN63/mEth+PV8+vJmiA9YaOpJ7QB50G68W2/bXym280cODAPGc8fPiwnnrqKU2dOlVRUVEXvW7w4MFavny5ZsyYoePHj+vAgQP65z//qRMnTujJJ5/M8/dFwe3fv1+///67HA6H6SgAAAAASgi/PY1/+fLlWrt2raZPn65BgwZJkjp37qygoCANHz48T/f69NNPtXz5ci1cuFC333579oubS+v6LiFquXCIZm7/Ro9c2+2S9wgPKqsvuo6S3Zb985ejacl6at17alAhSou6js65bnvWEb2++FMlJycrLCzsijM++uijuuaaa/TII49c8rqnnnpKpUuX1mOPPZbzQ4WIiAgtXrxY7du3v+Lvh8Jzdl6fsg8AAADgSvntyv7ZAnXXXXfler1v3755vteSJUsUHh6unj17KjMzU5ln0pW597iaVqqramUqyNr/02Xv0a3WDTlFX5IaVqgpSepeu2Wu6xqWqipJ2rt37xXnW7hwoRYvXqz33ntPNpvtktd++OGHGjx4sB5//HGtXLlSX331lf7617+qV69eWrFixRV/TxQel8ulBg0aqGrVqqajAAAAACgh/HZl/9ixYwoMDFRERESu1/NTqA4dOqSkpCQFBQVd8OtH005c8PVzRYSUy/X7IHv2X5qI4NyPxwtSgKTsQ/OuREpKih577DE98cQTioyMVFJSkqTsMwak7HMLSpUqpbJly+r48eM5K/ovv/xyzj26du0qp9OpQYMGaffu3Vf0fVF4LMtSp06dTMcAAAAAUIL4bdmvWLGiMjMzlZiYmKvwHzx4MM/3qlSpkipWrKjly5dnv3DklDRjS87Xy5UqXeC8+XX06FEdOnRIr7zyil555ZXzvl6hQgX16tVLX3zxhX777TelpqaqZcuW5113ww03yOVyKSUlRaGhoed9HUXj4MGD+u233zR+/HjTUQAAAACUIH5b9h0Oh1588UV98sknuebY58+fn+d79ejRQ/Pnz1dWVpZat24tHT0tLU65/BuLQbVq1bRq1arzXp86dapcLpeWLVumSpUqSZIiIyMlSbGxsfrb3/6Wc63H41FsbKwqVKigsmXLFk9wSGJeHwAAAED++G3Z79Kli9q3b6+hQ4cqOTlZLVq0UExMjGbPni1Jstuv/DiDe+65Rx9//LG6deumwYMHq1WLG1TqwFbFnziiVfu3qld0a91Wt13hBA+49Mz9n4WEhFzwkW0zZ85UQEBArq/VqlVLt99+u959910FBwerW7duOnPmjGbNmqV169Zp4sSJl535R+FyuVy65pprVK1aNdNRAAAAAJQgflv27Xa7Fi9erKFDh2rq1KlKT09X+/btNWfOHLVp00bh4eFXfK+AgAB9+eWXeuONN/TRRx9pypQpCnTbFFWmohyR16pJxehc19tUgMJcoWhHAj7++GO99dZb+uijj/TBBx+oVKlSuvrqqzVnzhzde++9Rfq9cT7LsljVBwAAAJBnNo/H4zEdwpvMnTtX9913n9atW6d27QqwGv/NLumzX6U//ek2+8+TqhdWTQtuGZH3e9ok3d5Q+kvd/OdCiXHo0CFVq1ZNc+fOzddTIgAAAAD4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vaJb67a67QonfIW8LTyFhIRccAx15syZCggIyPW11NRU3XLLLdqyZYtef/11ZWZmKjY2NufrlStXVr169fKbHEWEsg8UwC+//KJjx45xOB8AAEAJkJWVpcTEREnSokWLdPr0acXExOjQoUNq1KiR9u7dq1OnTuVcv2TJEtlsNkVERKhLly5atGiR7r//fv3973/PKfOBgZeuVF9++aXeeOMNffTRR5oyZYoCbQGKCq4gR+S1alIxOte1NuVzxd8mKbp8/t57BQ4dOqSNGzdKkgYPHnze1//2t79p5syZRfb9kT82j8fjMR0CKKneeustDR06VMePH1eZMmVMxwEAAPBraWlpOdvpz91af/ZXfHy8MjMzc66vUKFCrpX42rVrq1atWjn/vXLlyjmn1M+dO1f33Xef1q1bp3btCrAafypdGrZSyspdw5r950nVC6umBbeMyPs9A2zSC52lMqXynws+h5V9oAAsy1KrVq0o+gAAAMUgKSnporPycXFxOnToUM61NptN1atXzynubdq0Oa/QlytX7oLfZ968eXK5XGrSpInsdrtiY2P10ksvqWPHjgUr+pJUNkjqWl9a8rsk6fekBK058It+OrZH99d35u+eXetT9HEeyj6QTx6PRy6XS4MGDTIdBQAAoMRzu906dOjQeWX+3EKfnJycc31QUJBq1qyp2rVrq3HjxurWrVuuFfqoqKiLPvLucsqVK6f58+dr0qRJOnXqlKpXr65+/fpp0qRJOdecu0PgQux2+8WfP39LPWnzAXkOpmjK5v9ocdwGPXjNTXr02u55C2q3SdVCs+93Drfbfd4hgX92ufEDlHxs4wfy6ZdfftG1116rr7/+WjfffLPpOAAAAF4tPT1d8fHxFyzzcXFx2rdvX84z3iUpLCzsvG315/6qWrXqxct0EduzZ4/q1KlzyWvGjh2rcePGXfTrx3bEK2vqalUKDpM9P7P6dpsUFiw93U6KyH0437hx4zR+/PhLvn337t2Kjo7O+/dFiUHZB/Lp7bff1pAhQ3T8+HGVLVvWdBwAAACjTp48ecE5+bO/Dhw4oHOrR9WqVS86K1+7dm2Fh4eb+zCXkZ6erq1bt17ymsjISEVGRl7wa6dOndJNN92kjEPJinnwTQUfPZP3EJHlpEdbnlf0JWn//v3av3//Jd9+3XXX5XvnA0oGyj6QT3fddZcSEhK0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AAAAAAAXQ9gHAAAAAMDFEPYBAAAAAHAxhH0AAAAAAFwMYR8AAAAAABdD2AcAAAAAwMUQ9gEAAAAAcDGEfQAAAAAAXAxhHwAAAAAAF0PYBwAAAADAxRD2AQAAAABwMYR9AAAAAABcDGEfAAAAAAAXQ9gHAAAAAMDFEPYBAAAAAHAxhH0AAAAAAFwMYR8AAAAAABdD2AcAAAAAwMUQ9gEAAAAAcDGEfQAAAAAAXAxhHwAAAAAAF0PYBwAAAADAxRD2AQAAAABwMYR9AAAAAABcDGEfAAAAAAAXQ9gHAAAAAMDFEPYBAAAAAHAxhH0AAAAAAFwMYR8AAAAAABdD2AcAAAAAwMUQ9gEAAAAAcDGEfQAAAAAAXAxhHwAAAAAAF0PYBwAAAADAxRD2AQAAAABwMYR9AAAAAABcDGEfAAAAAAAXQ9gHAAAAAMDFEPYBAAAAAHAxhH0AAAAAAFwMYR8AAAAAABdD2AcAAAAAwMUQ9gEAAAAAcDGEfQAAAAAAXAxhHwAAAAAAF0PYBwAAAADAxRD2AQAAAABwMYR9AAAAAABcDGEfAAAAAAAXQ9gHAAAAAMDFEPYBAAAAAHAxhH0AAAAAAFwMYR8AAAAAABdD2AcAAAAAwMUQ9gEAAAAAcDGEfQAAAAAAXAxhHwAAAAAAF0PYBwAAAADAxRD2AQAAAABwMYR9AAAAAABcDGEfAAAAAAAXQ9gHAAAAAMDFEPYBAAAAAHAxhH0AAAAAAFwMYR8AAAAAABdD2AcAAAAAwMUQ9gEAAAAAcDGEfQAAAAAAXAxhHwAAAAAAF0PYBwAAAADAxRD2AQAAAABwMYR9AAAAAABcDGEfAAAAAAAXQ9gHAAAAAMDFEPYBAAAAAHAxhH0AAAAAAFwMYR8AAAAAABdD2AcAAAAAwMUQ9gEAAAAAcDGEfQAAAAAAXAxhHwAAAAAAF/P/ALg0hBL8oUvsAAAAAElFTkSuQmCC",
       "text/plain": [
        "
" ] @@ -2632,7 +4466,7 @@ } ], "source": [ - "fe = cinnabar.wrangle.FEMap('cinnabar_input.csv')\n", + "fe = FEMap.from_csv('cinnabar_input.csv')\n", "fe.generate_absolute_values() # Get MLE generated estimates of the absolute values\n", "fe.draw_graph()" ] @@ -2655,13 +4489,13 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 120, "id": "1a747b7f-a06c-4027-9556-433242fb50ce", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -2672,7 +4506,7 @@ ], "source": [ "# note you can pass the filename argument to write things out to file\n", - "cinnabar_plotting.plot_DDGs(fe.graph, figsize=5, xy_lim=[5, -5])" + "cinnabar_plotting.plot_DDGs(fe.to_legacy_graph(), figsize=5, xy_lim=[5, -5])" ] }, { @@ -2693,13 +4527,13 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 121, "id": "f9e652a4-3657-43c9-a2ed-7b074a03578e", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -2710,12 +4544,12 @@ ], "source": [ "# note you can pass the filename argument to write to file\n", - "cinnabar_plotting.plot_DGs(fe.graph, figsize=5, xy_lim=[5, -5])" + "cinnabar_plotting.plot_DGs(fe.to_legacy_graph(), figsize=5, xy_lim=[5, -5])" ] }, { "cell_type": "markdown", - "id": "18619f57-fa05-4cfd-a970-8fb66db8b871", + "id": "999ee6ed-b2b2-4d79-aca4-de45d06bdaa5", "metadata": {}, "source": [ "We can also shift our free energies by the average experimental value to have DGs on the same scale as experiment:" @@ -2723,24 +4557,21 @@ }, { "cell_type": "code", - "execution_count": 50, - "id": "8dfc0f1f-e928-4d7a-8c13-9895ab8f37cb", - "metadata": {}, - "outputs": [], - "source": [ - "exp_DG_sum = sum([fe.results['Experimental'][i].DG for i in fe.results['Experimental'].keys()])\n", - "shift = exp_DG_sum / len(fe.results['Experimental'].keys())" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "id": "80a46f11-8e99-414b-b9ee-8cfe120fbe3c", + "execution_count": 125, + "id": "873fb9e5-5035-4c2d-9d16-620acdc6f94a", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/david/micromamba/envs/alchemiscale-client-user-guide-2024.09.26/lib/python3.12/site-packages/cinnabar/femap.py:35: UserWarning: Assuming kcal/mol units on measurements\n", + " warnings.warn(\"Assuming kcal/mol units on measurements\")\n" + ] + }, { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYgAAAHxCAYAAAB3bisvAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAB79ElEQVR4nO3dd1xTV/8H8E8CIWwQAUFRUFFx4cStuFHUPm5ttQ5crVr7qNTV1oJardvWqnVrW2vddVurgjgQ956ICxHLUpB1E3J+f/BLHgIJJOFmQL7v1yuvlpube84lMR/uPfeer4AxxkAIIYQUIjR2BwghhJgmCghCCCEqUUAQQghRiQKCEEKIShQQhBBCVKKAIIQQohIFBCGEEJUoIAghhKhEAUEIIUQlCghCCCEqUUAQQghRiQKCEEKIShQQhBBCVKKAIIQQohIFBCGEEJUoIAghhKhEAUEIIUQlCghCCCEqUUAQQghRiQKCEEKIShQQhBBCVKKAIIQQohIFBCGEEJUoIAghhKhEAUEIIUQlCghCCCEqUUAQQghRiQKCEGLS/vjjD7i6uiIjI0NpuY+PDwQCAX755Rel5b1794aPj48Be6jeokWL0KNHD7i4uEAgEGDbtm1F1jl06BA6d+4MDw8PiMViVK9eHePGjUN8fHyRdSUSCWrWrImVK1caoPcUEIQQE5aVlYWZM2di5syZcHBwULnO2rVrDdwrza1evRpZWVno2bOn2nXevHmDhg0bYtWqVfj7778RFhaGU6dOoWXLlkhLS1NaVyQS4bvvvsO8efOQnJys7+4DjBBCTNS6deuYWCxmqampRZ7z9vZmtWvXZgKBgEVFRSmW9+rVi3l7exuwl+pJpVLGGGNXrlxhANjWrVs1et0///zDALDNmzcXeS43N5e5uLiw8PBwPruqEh1BEEJM1rp16xAcHIwKFSqofL569eoICgoy2aMICwsLnV7n5OQEIP+UUmFWVlYYPHgwNmzYAJlMVqr+lYQCghBikuLj43H79m107Nix2PUmTpyI/fv34+3btzq3xRiDVCot9sEY03n7msjLy0NOTg5u3ryJqVOnolKlSujfv7/KdTt27IjXr1/j1q1beu0TBQQhxCRdvHgRANCsWbNi1+vVqxc8PT2xadMmnds6e/YsRCJRsY/t27frvH1NtG3bFjY2NmjSpAlycnJw5coVuLm5qVxX/ju5cOGCXvtkqdetE0KIjhISEgAA7u7uxa4nFAoxfvx4/PLLL5g1a5ZObTVr1gxXrlwpdp3q1avrtG1Nbdu2De/evcPDhw+xcOFCfPTRRzh16hQqVqxYZF357+T169d67RMFBCHEJGVnZwMArK2tS1x37NixCA8Px5EjR3Rqy97eHo0bNy52HV3HEzTl5+cHAGjVqhW6dOmCGjVqYNmyZVi0aFGRdeW/E/nvSF/oFBMhxCS5uroCAFJTU0tc193dHQMGDMCaNWt0assUTjEVVLVqVVSpUgU3b95U+bz8dyL/HekLHUEQQkxS3bp1AQBPnz5Fo0aNSlz/888/R2BgIGrXrq11W8Y8xcQYg0AgUFr28uVLxMfHo0uXLipf8/TpUwBAvXr19NInOQoIQohJatGiBWxtbXHp0iW1V/MU1L59ezRo0AB37tyBt7e3Vm05ODigefPmunZVrbNnzyIpKQlxcXEAgKtXr8Le3h4AMHDgQABAu3bt0KFDBzRv3hwVK1bEkydPsGTJEojFYkyZMkXldqOjoyEUCku8wqu0BEzf124RQoiORo0ahYsXL+Lx48dFnvPx8YGfnx9OnDihWLZu3TpMnDgR3t7eeP78uQF7qlrHjh1x9uxZlc/Jv3rDw8Nx/PhxxMXF4f379/D09ETr1q0xd+5cxVFUYW3btkWFChV0HnPRFAUEIcRk3bhxA02bNsX58+fRtm1bY3fHJDx58gS1a9fGiRMnEBQUpNe2KCAIISZt2LBhSE1NxfHjx43dFZMwcuRIvH79GqdOndJ7W3QVEyHEpC1duhStWrUqMpurOZJKpahVq5bBphahIwhCCCEq0REEIYQQlSggNCAvTCIQCCASiVC9enUMHz4cT548UawTGRmpWOfevXuK5WlpabCysipSLCQmJgbBwcHw8PCAnZ0d6tatizFjxuDVq1eKdbZt26bYZuFHq1atDLLvfElMTMTHH38MZ2dnODg44D//+Q9vV5lMmjQJAoEAn332WZHnNCnYoq2C77VAIMDVq1eLrLN37140adIE1tbWcHV1xSeffFJkWgR1768+LrfUFZ/vmzaFcfT5eSkOn+3q+v4W93m+evWq0rYiIyN16pum6D4IDbVq1QorV66ERCLB9evXERYWhiNHjuDWrVtK11w7OTlh9+7dCA8PBwAcOHAAdnZ2ePfunWKdf/75Bz179kTXrl2xbt062Nvb4969e9i+fTuePn2KqlWrKrW9adMm1K9fX2mZuuIppkgqlSIoKAipqanYuHEjxGIxZs6cic6dO+POnTuws7PTedsxMTHYs2eP2udXr14NX19f9OzZE3/88YfO7aiyZs0aNG3atMiliGfOnMGgQYMwcOBALFq0CK9evcKsWbNw/fp13Lp1C2KxWGn9/fv3w9PTU/Gz/Dp5IH+656lTp+L333+Ho6MjwsPDMXr0aF73Qx2+3zd5YZzx48fDw8MDL168QFhYGI4dO4a7d+8qpvTW5+elOPpqt7j3t7CSPs9169ZFdHQ0rl+/jkmTJunUH63oveJEOeDt7c2CgoKUlm3ZsoUBUBTtiIiIYADYp59+yvz8/BTrBQUFsZEjRyoVC+nSpQurXr06k0gkRdrKzc1V/P/WrVsZABYdHa2HvTKcP/74gwFgJ06cUCy7ceMGA8B+/PFHnbcrkUiYv78/W7duHQPAJkyYUGQdXQu2FEf+XkdERKh8vmPHjszb21vRNmOM7dy5kwFg27ZtUyyTv7/Pnj1T29bixYuZr68vO3r0KNuyZQuztbVlV69eLfU+aEJf71tBqgrjGKJdVfhuV5P3tyBNPs9yJX0G+UKnmHTUsmVLAPl/FRXUrVs3vH37Frdv30ZKSgoiIyMVd0zKvX79Gq6urrC0LHoAZ2VlVap+PXz4EA8fPizVNvh2/PhxODo6olu3bopljRs3hq+vL44dO6bzdpcvXw4LCwuMHz9e7Tr6nmBNlbt376JVq1ZKbcv3/eTJk1pt69y5c5gyZQqCg4MxevRo9OjRQ+2NVwXx8TnQ1/tWkKrCOIZoVxVjtSunyefZ0CggdCQ/n1x4LhSRSIT//Oc/2L17N/bv349WrVrBw8NDaZ2mTZviypUrmDt3Ll68eFFiW3l5eUWKl6irJFW3bl21d18ay8OHD1G7dm0IhUJwHIecnBwAQJ06dfDgwQOdtvns2TPMmzcPP//8M4RC0/oYcxwHkUiktEwe/AXHreRat24NCwsLVKpUCePHj1eanK5OnTrYv38/EhMTcfPmTZw/f14x6ycA3L9/H4mJiUW2ycfnQB/vG1ByYRx9tVsSfbVb3PsrZ6qfZ9PpiYlj/19xKicnBzdu3MDMmTPRvn17jB07tsi6gwYNwu7du7Fr1y4MHjy4yPOLFy9G06ZNMX/+fPj4+MDHxwdffvmlyi8PIH+ulsIzS86ZM4f3fdSXtLQ0ODs7A8gPxxo1aiArKwvOzs5FirJr6vPPP8eAAQPQpk0bHnvKD19fX9y+fVtp2eXLlwFAaSzK09MT8+fPx7Zt23Dy5El89tln+P3339GxY0fk5uYCAObMmYPc3Fx4enqiSZMmGDRoEJo3b44ff/wRLVq0wMSJE5UubOCTPt43oOTCOPpqtyR8t6vJ+ytnqp9nGqTW0MmTJ5X+KuzevTsOHDgAGxubIut269YNycnJePbsGXbs2FHkH7CXlxcuX76Ms2fP4siRIzh79ix++uknbNq0CceOHUNgYKDS+lu3bkWDBg2UlhUc9CqImfhtLRUrVoRMJoOFhYXKWSw18ccff+DixYt49OiRHnpYehMmTMCECRPw/fff47PPPsPLly8xZcoUODo6Kv11GBQUpDRVQpcuXVCzZk2MHDkSO3fuxKhRo+Di4oILFy7gwYMHOH36NP7++290794dQ4YMwd69e1GtWjWVfeD7c8DH+yanTWEcPtvVBh/tavL+Aqb9eaYjCA21bt0aV65cwblz5zB16lScPHlS7UyLIpEIoaGhmDhxIipVqqRyHQsLC3Tu3BkrVqzAtWvXcPnyZQiFQpVHBn5+fmjevLnSo0qVKrzunz45Ozsr/nI+e/Ys7t+/D7FYjPfv3yv+YtNUZmYmpk2bhtDQUDg4OODDhw/48OEDgPyrUD58+KD3Qu4lGTNmDCZPnoywsDC4urqiRYsW+Oijj+Dv71/i/P0DBgyAQCBQTD2dmJiI8ePHY+DAgXj+/Dnmz5+PcePGYfXq1fD398eXX34JqVSql/3g830ryM/PD61atcKoUaNw+vRp3L17F8uWLdN7uyUxRLuF319T/zxTQGjI0dERzZs3R7t27bBixQoMHz4cW7ZsUTuH/Jw5c/Djjz9qvP2AgAB0795dr+dYjaVu3bp48uRJkb9qHz16pPV58qSkJLx9+xbfffcdHBwcFA8A2Lx5MxwcHHD9+nXe+q4LCwsLrF69GsnJybh9+zYSExMxf/583Lt3D02aNCn2tfK/VOX/tbe3x+DBg3H37l0sX74cHz58wOzZs7F48WLs3LkTJ0+exIoVK/SyH3y+b+qoKoxjiHZVMUS7hd9fU/88U0DoaPHixRCLxZg7d67Wr1U1qCiTyfD06dMS6++WxBSvYurRowfev3+vNLnY7du3ERsbi+DgYJWvUbcfnp6eOHfuXJEHAPznP//BuXPnlAZxjcnJyQkNGzZExYoVsX37dqSlpeHjjz8u9jV//fUXGGOKGyHt7e3RrVs3xampmJgYdOvWDZ9++il69uyJzz//HOfPny+yHT4+B3y+b4Dq017ywjiVK1cuVbvym1lLczMd3/urSuH319Q/zzQGoaPKlStjwoQJ+PHHHxETE6PVa0NCQiAUCjFw4EDUqFEDqamp2LJlC27duqWyZGLBO7PlLC0tVd6RKf9Lx5TGIgYPHozFixdj7NixWL58OaysrDBr1ixUr14dY8aMUfkadfshFovRrl07la/x8PAo8pwmBVv49vbtW/z0009o3749xGIxTp8+jaVLlyIkJERpELJLly7o3LkzGjVqBGtra1y8eBFLlixBQEAAhgwZonLbLVu2xLx587B9+3a4urpi3bp1+PTTT4usx8fngM/3DdC8MI4u7cpPw6i6dNxY+6vJ+6vt59ng9HqXRTmh6kY5xhhLTExktra2rEePHoobV3bu3FlkvcI3af3111+sX79+zNvbm4nFYubu7s46dOjADhw4oPQ6+Y02qh5OTk4q+yp/3tS8efOGDRkyhDk6OjJ7e3vWp08fFhcXp3Z9bfcDam4sCgwMVPs71FVJNymlpKSwTp06MWdnZyYWi1m9evXY8uXLlW6cY4yxL774gtWsWZPZ2dkxS0tLVr16dRYaGsoyMjKKbf/nn39mVapUYS4uLmzixIlKN1fK8fU54PN9CwsLYy1btmRubm7MysqKeXt7s6FDh7L79++Xqt2UlBQmEAhYu3btdNtJHdtlrPj91fX9lW/XFG6Uo9lcCdFSZGQkOnXqhFOnTiEwMLBUf7WS0jtw4AD69++P48ePo0ePHsbujt5JpVKcPXsWXbt2RUREhF7LjtIYBCE66tq1K0QikcrJ+ojhnD17Fk2bNjWLcLh69SpEIhG6du1qkPboCIIQLWVkZChds16vXj3Y2toasUfEXGRlZeH+/fuKn+vUqaPXiTspIAghhKhEp5gIIYSoRAHBE20KBnXs2FHltc29e/eGj4+P4mcqGKSaNoVnpFIpli1bhvr168Pa2hru7u7o0aNHqeobU8Gg5zpt68yZMxg6dCiqV68OGxsb1KxZE1OmTEFKSkqRdfXxvmnCWIWtoqOj0b17d7i6usLZ2Rlt2rTB4cOHi6xHBYPKOE0KBmmLCgYp07TwDJA/7cXOnTsRGhqKbt26IT09HREREUrTS+uKCgZp975t3LgR7969w9y5c1GjRg08evQI3377Lf7++2/cuHFDaRxHn++bOsYqbPXw4UN07doVTZo0wdatW2FlZYW1a9fiP//5D06cOIHu3bsr1qWCQWWUNgWDAgMDWZ06dYpso1evXszb21vxMxUM0pyqwjNRUVEMAFu9ejUvbchRwSDd3rd///23yLLdu3czAGz79u2KZfp630pirMJWCxYsYADYixcvFMuys7OZtbU1GzlypMptUsGgMkqTgkH6ZIpTbRir8Myff/4JOzs7jBs3jpc2NEUFg1QrOKW3XMOGDQEACQkJimXGet+MVdhKPtliwbMC1tbWRY40jYECgmeaFAzSFhUMUq2kwjM3b95EvXr18Oeff8LHxweWlpaoU6cOtm/fXup9Kg4VDNJcdHQ0AChNYmis981Yha0+/fRTODk5YcaMGUhMTERKSgrmzp0LqVSqdsZoQ6GA0IOSCgZpiwoGqVZS4Znk5GS8evUKM2fORFhYGE6cOIEmTZpg1KhR+Pvvv0u7W2pRwSDNpKSk4Ntvv0WrVq2UzrMb630zVmGrGjVq4OLFizh37hw8PT3h6uqKDRs24O+//0bTpk11apcvNEitByUVDNIWFQxSraTCMzKZDImJidi3b5/iyKJjx46KSdMKFnPhExUMKplEIsHgwYORm5uLnTt3Km3PWO9bQYYsbPX8+XP85z//QY0aNbBixQqIRCJs27YNwcHBOHbsGNq2bavrbpQaBYQeyAsGvX37FpUqVSoSEMV92FQdhsoLBpVVhQuxyPFReAYAWrVqhS5duqBGjRpYtmwZFi1aBOB/4xKdO3dWvEY+C+6lS5d0brckY8aMwZ07dxAWFoZvvvkGlpaWCA0Nxfnz50v84h4wYABGjRqFK1euYNSoUUhMTMS3336LCxcuoGfPnpg/fz4uXryI77//HosXL8bIkSOxfPlyvcwHpa/3jTGGUaNG4fLlyzh9+rTSpd2A8d43PvdXVSEgOXkhIFtbWwiFQnz99dfIyMjAwYMHYW1tDSB/GpfmzZtj2rRpWs8WzSc6xaQnxRUMsrW1VTmGIJPJyuWUDcYqPFO7dm2V6zLG9FoYngoGFW/KlCnYv38/Dh48iBYtWhR53ljvm7EKW924cQN16tRRhAOQ//43atQId+7cKeVelQ4FhBFUr14diYmJSoXLGWOIi4tD9erVS7VtU7yKyViFZ+RXo5w5c0axTCKR4OrVq0VO2ekDFQwqas6cOVi/fj327NmjdIRQkC7vm6kVDNKmEFCFChXw8OFDZGdnK17PGMPNmzdVXvllUHq9iNaMaFMP4saNG8zKyop17tyZ7d27lx06dIgNGDCACQQCduzYMcXr5NfJb9q0iUVHRys9rly5orIfMMF6EPLrwKtVq8b27NnDDh48yOrWrcuqV6/OPnz4oPI1xe1HmzZt2KxZs9jevXtZREQE27BhA/P19WW2trbs5s2bivVycnJY7dq1mZubG9u0aRM7ceIEGzBgAAPAzp07p/P+lHQNemJiIpszZw47fvw4O3PmDPv666+ZlZUVCwkJUVqvc+fObMGCBezw4cPsn3/+YeHh4czOzo4FBAQwjuNUbjsqKoo5ODiwbdu2sSNHjjA/Pz/2/fffF1mPj88B3+/bkiVLGAA2ffr0Ip/n2NhYxXq6vG9Vq1ZlANirV69MZn/VrV/4Pgj5PTJdunRhBw8eZMePH2cff/wxA8CWL1+ucjuGug/CtL5JyjBtAkK+LDg4mLm5uTFnZ2cWGBjI/vnnH6XXUcGg0heeefXqFRsyZAirUKECs7KyYk2aNGH79u0r1b5QwSDd3rfiijcVviFMm/fNVAsGqVtfVSGgo0ePsvbt2zMXFxfm5OTEWrRowXbs2KF2O1QwiBATRQWDTAsVDOqot7ZoDIIQHVHBINNABYP0h44gCNESFQwixkIFgwghhJgEOsVECCFEJQoIQgghKlFAGIH8ph6BQACRSITq1atj+PDhRWb61KYCl6nju1KXphW4Ciquopc2qKLcc523p2mlOH1WdisO3+3y/TkwdEU507tg3gx4e3uzVq1asejoaBYVFcVWrVrFnJ2dmZOTE3v+/LlivaFDh7KgoCC2ZcsWFhkZydavX8/c3d1Z7dq1WWZmphH3QDvyG5C8vLzY7t272cGDB5mfn1+xNyAV58GDB8zW1pa1bduWHTp0iJ04cYJ99NFHTCAQsL///lvlay5dusTc3NzUXoeuDfk16GvWrGHR0dFF9uH06dMMABs4cCA7fvw427BhA3NxcWF16tRhOTk5ivXk97ns379f6aaxO3fuKNbhOI5NmjSJOTk5sapVq7ItW7aUqu/a4Pt9Y4yxESNGMJFIxGbPns3OnDnD/vrrL/bll1+ylJQUvbarCb7b5fNzIPfhwwcWHR3N1qxZQzfKlVfe3t4sKChIadmWLVsYABYeHq5YpmkFLlPHd6UubStwlVTRS1tUUU63903TSnGGqEBoiHb5/BwURhXlzEzLli0B5NdbltO0AldBpjgXE9+VurStwFVSRS++UUU51TStFGeICoSGaJfPz4GxUECYCPl5yXr16hW7nqoKXAWZQ0U5bSpwaVLRi29UUU41TSvFGaKSnSp8t8vn58BYaI4AI2GMKcqHPnjwADNnzkT79u0xduxYta9RV4HL1KWlpSnm/G/atClSU1MRGxurc6UueQWu/v37KwonVapUSWUFLk0qevFN24pyAQEBsLS0RFRUFJYuXYpLly7hypUrEIvFmDNnDnr37q3Yz0mTJikqyu3YsQO2trZYunRpqcvaqsL3+5acnIz09HTMnDkTP/zwA7y8vLBp0yaMGjUKHh4eikJAfLerKb7b5fNzYCwUEEZy8uRJpb8uunfvjgMHDsDGxkbl+sVV4CqImfh9j3xU6tK0ApemFb34RhXlVNOlUhzflew0xUe7fH4OjEavIxxEJW9vb9a6dWt25coVdu7cOTZ16lQGgI0ZM0bl+jKZjH3yySfM3t6excTEGLi3pde8eXPWvHnzIst79erFqlWrpvX2PvnkE1apUiWWnZ2tWCaTyVjTpk1ZixYtGGP5V3tUqlSJhYeHs4yMDMVD/nvOyMhgeXl5Ou1PSQOEUqmUTZ48mVlaWjIAzNLSks2aNYu1a9eOtW3btthtf/jwgQkEAjZx4kTGWP7somPHjmV169Zl06ZNY9evX2c///wz8/T0ZE5OTmzKlClMIpHotB8l4ft9a9asGQPA0tLSlJb369ePeXp66q1dTfHdLp+fg8Jouu9yTNVVTMOHD2cCgYBdvny5yPqTJ09m1tbW7PTp04bqIq8+/fRT5uTkxGQymdJyX1/fIr8HTdStW5d16NChyPLRo0czGxsbxhhjz549Uzu1tPyhrqZGSTT9x/nu3Tt2+/ZtlpyczCQSCatQoQKbPHlysa/JzMxkAoGATZo0iTHGWEZGBjt58qQizOT1IH799Vd27Ngx5ufnxxYvXqzTfpSE7/dNXuOgcED07duXValSRW/takpf7fLxOSiMrmIyM4sXL4ZYLMbcuXOVlmtSgasgU7yKie/KZJpU4NKmope+UEU5ZZpWitOlXVOrKFcQH58Do9Fr/BCVVB1BMMbYl19+yQCwS5cuMcY0r8BVEEywYBDflbp0qcBVcLv6vg+CKsqpblfTSnG6tGuKFeX0+TmgU0zlmLqASExMZLa2tqxHjx6MMe0qcMmZYkAwxn+lLm0rcBXcrr4DgirKqW9X00px2rRrqhXl9Pk5oIpyhJgoqihnWqiiXEe9tUVjEIToiCrKmQaqKKc/dARBiJaoohwxFqooRwghxCTQKSZCCCEqUUAYgbxAiCEKoJgKYxRi0VfBJSoY9FynbRUslFX4UXj8QJeCUHwwxucU0Hx/qWCQGdBl/veyzFiFWPRVcIkKBun2vl2/fr3I/Tzr169nANjatWsV6+lSEIoPxvqcarO/VDDIDJhbQBirEIu+Ci5RwSD+CvdMnDiRWVtbK02/oW1BKL4Y63Oqy/7SVBtlmEAgQFhYGLZu3Qo/Pz+IxWJUq1YNhw4dUlrvxYsX6NWrF+zt7eHt7Y2VK1cW2da1a9fQvXt3ODg4wNbWFh06dEBUVJTatk1xqg1jFWLRpeASH6hgkGays7Pxxx9/oF+/fnB2dlYs17YgFF+M9Tk11v5qggJCT44ePYoffvgBs2bNwrFjxzBlyhR8+PBBaZ1x48ahZ8+eOHDgAFq2bIlp06bh0qVLiudjY2MRGBiIxMREbN26FTt37oRUKkXXrl1x7do1le2aQ8EgbQuxFFRSwSU+UMEgzezduxfv3r3D6NGjlZZrUxCKT8b6nBprfzWi1+MTMwWAOTk5seTkZJXPy08t/PTTT4plGRkZTCQSsW+//VaxbNKkSczCwkLp0DM1NZXZ2tqyQYMGqW3b1N5WX19f1rVrV8YYY/Xr12eenp4sMzOTDRs2jDk4OGi9vaZNmzJ/f3+lZadOnWIAWK1atdS+Ljk5mVWpUoW1atWqyIyd2ijp8F7T/p04cYLNnz+fnThxgp06dYrNnTuX2djYsIYNGyrOUaekpLDWrVsr3tdJkyaxt2/fslWrVrGAgAAWGBiocgZgPj4HfL9vhXXo0IFVq1ZN5bTr9+7dY3Xq1FHsR6VKldj58+dL3WZxjPk51XZ/6RRTGdenTx9UrFix2HU6dOig+H97e3tUqlRJqSb1pUuXUL9+faWiMBUqVECbNm2UjjQKYvnjSqXsvf5UrFgRzs7OpS7Ecvv2bXz//fdISUnBjRs3VBZiKUjTgkt80LR/QUFB+OabbxAUFIQuXbogPDwcv/zyC+7cuYOdO3cCgKJg0L179/DTTz/h+fPn6N69O7KysrB3715ERkYiICCgSB/4/hzw8b4V9PjxY0RFRWHkyJFF3jN5QShvb28cPXoUJ0+eRJcuXRAcHIwLFy6Uql1NGfJzagr7q5Ze48dMAWBff/212ufVDU56e3srDUrVrFmTderUqcjrhw4dyuzt7fnqrt4ZuxAL3wWXqGBQ6Qv3fPXVV0wgELCnT58WeU6TglD6YKzPqS77S0cQZRwfE7g5OzsjOTm5yPLk5GSlQT1TV7duXTx58qTIX7SPHj3S6Ty5hYUFVq9ejeTkZNy+fRuJiYmYP38+7t27p3JsYcqUKdi/fz8OHjyIFi1a6Lwf+upfQfK/VOX/tbe3x+DBg3H37l0sX74cHz58wOzZs7F48WLs3LkTJ0+exIoVK/SyH3y/b3ISiQS//vorOnTogBo1ahR5/saNG6hTpw6sra0VywQCARo1aoQ7d+7o3G5JjPU5Ndb+aoICwoS1bNkS9+/fx8uXLxXL0tLScPHiRbWFREzxKiZjFmLRtuASn6hgkGqHDx/G27dvERISovJ5TQpCFVaWCwbpsr8Go9fjEzMFgH333Xdqn9f0FNOjR4+Yra0ta9iwIduzZw/766+/WOvWrZmlpaXKgUl526b2thqrEIsuBZc0QQWDdHvf5Hr27MkcHBzUbkOXglBluWCQLvtLBYPKML4CgjHGYmJiWJcuXZidnR2zsbFhbdu2ZWfOnCm2bVMLCMaMU4hFl4JLmqCCQaUrGCQUCtmYMWOKbVebglBlvWAQY9oXwKKCQYSYKCoYZFqoYFBHvbVFYxCE6IgKBpkGKhikP3QEQYiWqGAQMRYqGEQIIcQk0CkmQgghKlFAEEIIUYkCghBCiEoUEIQQQlSigCCEEKISBQQhhBCVKCAIIYSoRAFBCCFEJQoIQgghKlFAEEIIUYkCghBCiEoUEIQQQlSigCCEEKISBQQhhBCVKCAIIYSoRAFBCCFEJQoIQgghKlFAEEIIUYkCghBCiEoUEIQQQlSyNHYHygOZTIaEhAQ4ODhAIBAYuzuEEKIWYwwZGRmoXLkyhMLijxEoIHiQkJCAqlWrGrsbhBCisVevXsHLy6vYdSggeODg4AAg/xfu6Oho5N4QQkqSl5eH5ORkvHv3DnZ2drCwsDBY21KpFGlpacjKyoKtra1BzjpwHIdvvvkGzZo1Q+/evREUFKT43ioOBQQP5G+wo6MjBQQhJi4vLw9JSUmQSqXw8PAweDikpqZCIBDA3d3dYKekjx49iosXL+L69evo1KkTAGjUNgUEIcRsyMMhLS0N9vb2RgmHzMxM2NnZGXS8Mjg4GPHx8WjcuDHc3d01fh0FBCHELJhbOEgkElhYWEAoFEIgEGDChAkAgA8fPmi8DbrMlRBS7plbOHAch2nTpiE8PBwymUzn7VBAEELKNXMLBwC4desWLly4gBMnTuDp06c6b4dOMRFCyi1zDAcACAgIwKJFi+Ds7IxatWrpvB0KCEJIuWRu4cBxHCQSCezs7AAAQUFBpd4mnWIihJQ75hgOoaGhmDhxIjIzM3nbLgUEIaRcMbdwAID4+HjcuHEDDx8+RGxsLG/bpVNMhJBywxzDAQBq1KiB9evXIyMjA40aNeJtuxQQhJBywdzCgeM4pKamwsPDAwBQr1493tugU0yEkDLPHMMhNDQUo0aNQnx8vN7aoYAghJRp5hYOQP7d0K9evUJaWhrevHmjt3boFBMhpMwyx3AAABcXF2zYsAEvX75Es2bN9NYOHUEQQsokcwsHjuPw8OFDxc9ubm56DQeAAoIQUgaZYzjIxxyuXr2q9/bkKCAIIWWKuYUDkF8mVCqVgjGGvLw8g7QJ0BgEIaQMMcdwAACxWIyVK1fiyZMnaNCggcHapSMIQkiZYG7hwHEczp07p/hZLBYbNBwACghCSBlgbuEgkUgQGhqKL774Avv27dN7e+pQQBBCTJq5hQMAWFpawtvbG2KxGF5eXgZpUxUBY4wZrfVyIj09HU5OTnj//j0cHR2N3R1Cyg1jhMPt27cV5TorV65slDEHIH9g+uXLl/D29uZ1ux8+fEC7du00+r6iIwhCiEky1pHD3bt3cf36ddy5c8fgYw579uxRlAgVCAS8h4O26ComQojJMeZpJflJFcaYwcKBMYYZM2YgMjISz58/x1dffaX3NjVBRxCEEJNi7DGHgn/BG+q0kkAgQOfOnWFjY4MOHToYpE1N0BEEIcRkGDscUlJSIYU98izswZBlsLYB4KOPPkK7du3g4uJi0HaLQ0cQhBCTYOxwSE5JReQDV6Q6jEKmyyik2H2K91kivbXJcRzWrVuH7OxsxTJTCgeAAoIQYgKMHQ6pqam49dwO8e+cFctlAjtEPvTQW7tz587F+vXrMWPGDL21UVp0iokQYlSGCofbt2/j7t27SssYY5DJZGCM4b1NcP43onzYQSDE+2wxjp84BQGkarfr6+sLX19frfszdOhQXL58GcOHD9f6tYZCAUEIMRpDHjlIJBJkZmaqfZ6JMgBLBkVCMAZAgpycDyhuqFoikejUn8aNG+Po0aOwsbHR6fW64jhO43XpFBMhxCgMfVpJJBLBzs4OdnZ2sLW1hbW1NcRiMaytrWFtbQ3HvFsQsFyAyQCWBwgEcMg5B5v/f17dQyTSbJyC4zgsXLgQiYmJimXGCAdtZoM1+yOIGzduIDw8HJcvX8a7d+9QrVo1fPLJJwgNDYWtra2xu0dIuWSMMQd/f3/4+/sXO31GFpeI4+cTIckTwhav8VE3fwD83Ky2bNky7N69G9evX8euXbsMOs4C5IeDVCrVaiDcrAPi/v37aNOmDerUqYNVq1bB1dUVUVFRmDdvHq5du4aDBw8au4uElDumMCCt7g5pW6s82HHXkJOTAytrawD+vLUdEhKC69ev46uvvjJaOFSsWBHazK5k1gHxxx9/ICcnB/v27UPNmjUBAJ07d8abN2+wYcMGpKWloUKFCkbuJSHlhymHg755eHgY9cihYsWKsLe3R0ZGhsavNesxCPm5QycnJ6Xlzs7OEAqFsLKyMka3CCmXzC0cOI7D7Nmzcfv2bcUyY4eDtsw6IEaOHAlnZ2d8/vnniIuLQ0ZGBo4cOYL169dj0qRJsLOzU/m63NxcpKenKz0IIeqZWzgAwKZNm3D8+HFMnTpV6WY4QyltOAB6OMUkk8mQk5NTJgZ4fXx8EB0djX79+ilOMQHAlClTsGrVKrWvW7RoEcLDww3QQ0LKvrIWDr6+vpBIJBpfnaTO6NGjcf/+fXz66adGuVqptOEA8FAPIicnB3/++SeOHj2KCxcu4N9//wVjDGKxGPXq1UPnzp0xbNgwNGrUqDTNlCgyMhKdOnXSaN0bN26gcePGeP78Obp164ZKlSph6tSpcHNzQ0xMDBYsWICBAwdi8+bNKl+fm5uL3Nxcxc/p6emoWrUq1YMgpJCyFg6lxRhTaqfwz4ZQUjhkZGTAz89Po+8rnQMiOzsbS5YswY8//oj379/Dz88PTZs2hbu7O6ytrZGamoq4uDhcvnwZ6enpaNOmDZYsWYLWrVvr0lyJ3rx5g6NHj2q0bv/+/eHi4oKhQ4ciIiICcXFxSqeTtm7dipCQEERGRiIwMLDE7VHBIEKKMrdw4DgOM2bMQJcuXdCnTx+9t6euDyUdOWgTEDqfYqpVqxbs7OzwzTffYNiwYahUqZLK9RhjiIiIwNatW9GpUyf8/PPPGDt2rK7NquXp6an1dm/evIl69eoVGWsICAgAkF84RJOAIIQoM7dwAIBDhw4hMjISMTExaNu2rcEn3uPrtFJBOgfEvHnzMHLkyBLfePk85507d0Z4eDhevnypa5O8q1y5Mu7evYsPHz4o/UKjo6MBwKi1YAkpq8wxHID8MxPPnj1Dhw4dykU4AGZek/rQoUPo27cvWrZsialTp8LV1RWXLl3CokWLUK1aNdy4cUOjS13pFBMh+cwtHCQSCSwtLQ0+zlCQtuGgzSkms77M9aOPPsLp06fh6OiIL7/8Er1798b27dsxYcIEREVF0X0QhGjB3MKB4zhMmzYNK1as0OruZL77oI8jBzneLnOVyWT4/fffMWLECL42aRCdOnXS+OonQohq5hYOAHDp0iWcO3cOly9fxsCBA+Htzc+cTZrSdzgAPJ5iys3Nha2trVYzBZYXdIqJmDNzDAe5vXv3omrVqmjZsqVB2y1NOOjtKqZ58+apfU7XOdEJIWWXuYUDx3GK+7wAYODAgXpvU1Uf9H3kIKdVQCxYsAD9+vVTmTrmeORAiDkzx3AIDQ1FXl4eVqxYoQgJQzJkOABaBkTDhg0xYsQI9OrVq8hzOTk52LZtG1/9IoSYMHMLBwCKG38ZY4iNjUX9+vUN0q6cocMB0DIgxo0bp/ZIQSQS4bvvvuOlU4QQ02WO4QAAfn5+WL16NWQymVmEA2Dm90HwhQapibkwt3DgOA4fPnww+I1vhfvAZzjQfRCEEN6ZYziEhoZizJgxSEpK0nt76vpgjCMHuVIHBJ1WIqT8M7dwAIDU1FQ8fvwYCQkJRpkiyNjhAPBwisnW1hZZWVl89adMolNMpDwzx3CQi4+Px5s3bxQTeBqKPsPBoKeYaAiDkPLL3MKB4zg8e/ZM8bOXl1e5CgdtlTogjDlJFSFEf8wxHKZPn46RI0fiwYMHem9PXR9MJRwAGqQmhKhgbuEA5M8G8f79e0XNeUMztXAA9FCTmhBStpljOACAnZ0d1q5di6dPn+q9RHJhphgOAI1BEEIKMLdw4DgOV69eVfxsb29P4VBAqQOibdu2fPSDEGJk5hYOEokEoaGhmDBhAk6ePKn39lQx5XAAeAiIU6dO8dEPQogRmVs4AIBQKISzszMsLS3h5ORkkDYLMvVwAGiqDV7QfRCkLDPHcJDLy8tDXFwcatWqZdB2jRkOb9++RdOmTfmvB1GQtncWVqtWTdemCCF6Ym7hwHEc/vnnHwQHB0MgEMDCwsKswiEzM1Or37POAeHj46NVQ1QvghDTYm7hIJPJEBoaiqioKMTHx2PChAl6b7MwY4eDUCiEu7u7xq/ROSC2bNlCN8kRUkaZWzgA+WMOLVq0wOXLl9G4cWODtFmQKYRDpUqVIJPJNH4djUHwgMYgSFlijuFQ0Nu3b1GpUiWDtmkq4WBvb6/V9xXvd1I/fvwY0dHRePLkCd+bJoSUkrmFA8dx2Lp1KyQSiWKZOYeDtngLiD179sDb2xt169ZFu3bt4OfnB29vb+zdu5evJgghpWBu4QAAc+bMwY8//oi5c+capL3CynI4ADwFxLFjxzB06FA4OTnhhx9+wK+//opFixbByckJQ4cOxfHjx/lohhCiI3MMBwDo168fHB0d0bdvX4O1KVfWwwHgaQyibdu2cHR0xNGjRyEU/i9zGGPo2bMnMjIycOHChdI2Y7JoDIKYMnMNB7mMjAw4ODgYtE1TDgeDj0HcvHkTEydOVAoHIH8q8IkTJ+LWrVt8NEMI0ZK5hQPHcVi2bBnS0tIUyygcdMdLQFhYWIDjOJXPSSSSIsFBCNE/cwsHAFi4cCF+//13TJkyxSgTiZancAB4CoiAgAAsWbIE2dnZSstzc3OxbNkytGzZko9mCCEaMsdwAIBRo0bBy8sLkydPNvjprPIWDgBPYxDnz59Hly5d4OLigkGDBsHDwwNv3rzB/v37kZKSgjNnzqBNmzZ89Nck0RgEMSXmGg5yEokEIpHIoG2WpXDQ5vuKl4JB7dq1w8mTJzFr1iysWbMGjDEIhUK0bNkSO3fuLNfhQIgpMbdw4DgO8+fPx4gRIxRzKlE48If3O6mzsrKQlpaGChUqwNbWls9Nmyw6giCmwNzCAQBWrlyJ7du3w9PTEwcPHoSVlZVB2pUri+Fg8COIgmxtbc0mGAgxFeYYDgAwZswY3LlzBxMmTKBw0APeAuKvv/7Cjh078OLFC+Tk5Cg9JxAI6FJXQvTEXMMBABwdHbF582YakNYTXq5iWrp0Kfr374+oqCiIRCJUrFhR6eHi4sJHM4SQQswtHDiOw/Tp0xEZGalYRuGgP7wcQaxduxYhISFYv369QT+ghJgzcwsHANi1axdOnz6NmJgYHDt2zOBjfuYUDgBPAZGSkoJPPvmEwoEQAzHHcACAjz/+GE+ePEGvXr0oHAyAl1NMbdu2xYMHD/jYFCGkBOYWDgWrUVpaWmLevHkGv/nWHMMB4CkgVq1ahTVr1uDQoUNqp9wghJSeuYUDx3GYOnUqNm3apPe2iuuDOYYDwNMpJl9fX3Tt2hX9+vWDQCAocpmrQCDA+/fv+WiKELNlbuEAABEREYiKikJMTAyCg4NRuXJlg7QrZ87hAPAUEDNmzMDPP/+Mxo0bo27duga/HpmQ8s4cwwEAgoKCkJCQgHr16lE4GAEvd1JXrFgR48ePx6JFi/joU5lDd1ITfTK3cOA4DkKhEJaWvN/Hq1Ufyms4GLweRF5eHrp168bHpgghBZhjOEyfPh3ffPMNpFKp3ttT14fyGg7a4iUgunfvjkuXLvGxKULI/zO3cACAe/fuITo6GhEREYiNjTVImwVROCjj5Rju22+/xZAhQ2BnZ4devXqpvHOa7qYmRHPmGA4A0KRJEyxduhS2trbw8/MzWLsAhYMqvIxByCvGFfdBKngtc3lDYxCET+YWDhzHIScnx6j/dswpHAw+m+vcuXONUhiEkPLGHMMhNDQUycnJ+OWXX4wSEuYUDtriJSDCwsL42AwhZs3cwgEAEhMTcefOHWRlZeHp06do0qSJQdqVo3AonvGuIyOEKJhjOABAtWrVsGHDBqSlpVE4mCAKCEKMzNzCgeM4JCUloUqVKgCgKBVqSBQOmuHlMtey7PLlywgKCoKDgwPs7e3RqVMnXLhwwdjdImbCHMMhNDQUI0eOxPPnz/Xenro+UDhoRucjiJcvX2q1frVq1XRtSm+uXLmCDh06oEWLFvjtt9/AGMOSJUvQpUsXREREoHXr1sbuIinHzC0cgPya9W/evEFGRgaSkpLg4+NjkHblKBy0o/NlrkKhUKsPlSle5tqjRw/cvHkTcXFxigkGMzIyUKNGDdSuXVvjIwm6zJVoyxzDQS4tLQ3Pnz+nMQcjMchlrlu2bCnzl7ZeuHABvXr1Upp91sHBAR06dMD+/fvx5s0beHp6GrGHpDwyt3DgOA6PHz9GgwYNAAAVKlRAhQoV9N5u4T5QOGhP54AYNWoUj90wDo7jIBaLiyyXL7tz547KgMjNzUVubq7i5/T0dP11kpQr5hgOoaGhuHTpElatWoU2bdrovU1VfaBw0I1ZD1LXq1cPly5dgkwmUyyTSqWIiYkBkF9KVZVFixbByclJ8ahatapB+kvKNnMLByB/dgX56WhjlCSmcCidchMQkZGREAgEGj1u3rwJAPjiiy/w+PFjTJ48Ga9fv8arV6/w2Wef4cWLFwD+N4VIYbNnz8b79+8Vj1evXhlqN0kZZY7hAAAikQhLly7Ftm3bqExoGcTbfRBPnjzB+vXr8eDBA2RnZys9JxAIcPr0ab6aUqlOnTrYuHGjRuvKr6gKCQlBUlISFixYgHXr1gEAWrdujdDQUCxevFhxnXZhYrFY5akpQlQxt3DgOA7nz59H586dAeSHRN26dfXebuE+UDiUHi+T9d29exetWrVClSpVEBsbC39/fyQnJ+P169eoWrUqatasiTNnzvDRX73Izc3FkydP4ODgAG9vb0yYMAE7duxAUlISbGxsSnw9XcVE1DG3cJBKpZg2bRqioqIwc+ZMfPzxx3pvszAKh+IZvGDQnDlzEBQUhHv37oExhs2bN+PVq1c4fPgwcnJysGDBAj6a0RuxWIwGDRrA29sbL1++xK5duzBu3DiNwoEQdcwtHADAwsICderUgVgsRo0aNQzSZkEUDvzi5QjCy8sLa9euRe/evWFpaYnLly+jefPmAIClS5fiyJEjOHv2bKk7y7e7d+9i3759aN68OcRiMW7duoUffvgBPj4+iIiI0PhNpiMIUpg5hoMcYwzx8fEGv3iDwkEzBj+CSEtLg4uLC4RCIUQiEdLS0hTPNW/eHNevX+ejGd5ZWVnhzJkzGDFiBHr16oVffvkFn332GSIjI03+TSamy9zCgeM47Ny5U3EzrEAgoHAoJ3gZpK5SpQqSk5MBAL6+voiKilLUqL59+7bJ/tJq165tkkc2pOwyt3BgjGHGjBmIjIxEXFwcvv76a723WRiFg/7wEhDt2rXDxYsX0bdvXwwbNgzfffcd3rx5AysrK2zbtg3Dhw/noxlCTJq5hQOQf7TQs2dPXL58GV27djVImwVROOgXL2MQT58+RUJCAtq3b4+8vDxMnToVO3bsgEAgQO/evfHTTz+V63PzNAZBzDEcCkpLS6PpM8oIbb6veAkIc0cBYd7MLRw4jsP69esREhICOzs7vbenrg8UDrox+CC1RCJBZmamyucyMzMhkUj4aIYQk2Nu4QAA4eHh2Lx5M6ZNmwZj/H1J4WA4vATEuHHjMHbsWJXPjR8/Hp9//jkfzRBiUswxHABg2LBhcHNzQ0hIiMFPZ1E4GBYvAREREYGPPvpI5XN9+vTR+zQbhBiauYYDkD/J5ZEjR2huJTPAS0C8fftWbd0EDw8PJCYm8tEMISbB3MKB4zjMnz8f8fHximWGnouMwsE4eAkIZ2dnxMbGqnwuNjYWDg4OfDRDiNGZWzgAwMqVK7Fv3z5MnjwZUqnUIG0WROFgPLwERKdOnbBo0SKkpqYqLU9NTcUPP/ygmNWRkLLMHMMByJ/12M/PD7Nnz4alJW8TQGuEwsG4eLnM9dGjRwgICIBIJMKQIUNQpUoVxMfHY8+ePZBIJLh8+TLq1KnDR39NEl3mWv6ZazjIyWQytfVR9IXCQT8MfplrnTp1cO7cOTRu3BgbN27Et99+i02bNqFx48Y4d+5cuQ4HUv6ZWzhwHIcZM2bgypUrimUUDuaJl+PF5ORkNGrUCKdPn0Z2drZi8j5ra2sAwPXr19G0aVM+miLEoMwtHABg27ZtOHnyJK5cuYKjR4/C1tZW523FxsZCIpFAJBLB19dXo9dQOJgOXgKiT58+iIiIgLW1NWxsbJTqKNy7dw9BQUFISkrioylCDMYcwwEARo4ciYcPH2LIkCGlCgcgPyBycnIUv7uSQoLCwbTwdpnrsGHDiiyPjY1Ft27dDF5ukJDSMrdwKDgUKRaLsWLFCl7vc8jLy1N7paMchYPp4SUgjh07hsjISEybNk2x7OXLl+jSpQsqV66Mo0eP8tEMIQZR1sMhNjYWDx48KPELWY7jOPz3v//F3r17tW6rOHkyIEPUEhmOH+ODQ19wQvdi+0DhYHp4OcXk5+eH/fv3IygoCD4+PhgyZAi6dOkCBwcHnDx5ku6DIGWGKYXD06dPtT5/D/zvtI61tbVGrzt27BjOnj2LmJgYBAYGws3NrTS7oXD+sTsyRTUAgQBgMqSKfJCWmYAKdpzSehQOpou3i5oDAwOxadMmjB49GitXroSlpSVOnToFFxcXvpogRK9MKRwEAoHWX/S6+s9//oOXL1+iZcuWWoVDbGys2qMUTuiBVJsBgPwASCAEWB7+iUmFI3deaV3GGIRCoeJKqQYNGsDf31+nfdEGhUPJdA6IwjfFAUBwcDC++OIL7NixAydOnICVlZViPQoKYspMLRz0TSKRwMLCAkKhEAKBAFOmTNFpGzk5OUWWywTW+OASrOIVAkjzmMrXFN6uvlE4aEbngHB1dVX7QWaMoXnz5krL5PVqCTE1phgOMhnwgdVAjoUYjL3ntU2O4xAaGopKlSph9uzZWt/jID9yUDftRp6oKpjABih8C65ACBH3pMj6Bb9HLC0tIRKJtOqPtigcNKdzQMydO9cod3QSwidTDIc8GXDkqhv+RVXAQoZ0CHHlyXsE1Ernpd1r167h3LlzsLKywtChQ1GzZk2tXq/uyOF/ZP+/LwwF52kQ59yASPKyyNoFr6Bq2LChXk8vUThoR+eACAsL47EbhBiescLh9u3buHPnDmQyGRhjRf7QymC+SIJX/g+C/L/ur8Q6ITH2OESCDLXb9fX1ha+vLxgDiptAp3Xr1pg/fz7c3Ny0DgcAEIlEiptgVbHCv8iVpUMmsIdAKASYDALZB1hnnYe1tXWRMYfC29YXCgftGXbmLUJMhDGPHHJzc5GVlaX2+RwLa8CC5V/9U0AWJ4KYqf/LPTtHil3HpLh6pyMAhqqur9FdxiAUCsBxHPLy8hQ3sfbu3Vvn/suDqDhZuSm48lyA+LccBJJkWGdGwkYsRIcOHehqpTKEAoKUa9HR0cjNzYVYLEbr1q0BGP+0kkQigVgsVnuKNo+lI6PgLUqMAQIZ7KyyYSlQ/5f71QduuBfHwFj+/rxIqobT0TIEBuQhNDQU2dnZ+Omnn5RmOtAXW3EeAuu8xYlnJxR3UlerVpPCoYzROSAaNGiA+fPno1+/fhqt/+bNGyxatAiVK1fGrFmzdG2WEK1ER0cjIyMDDg4OaN26tdHDITU1FZ6envD19S3mIg/gwoMM3H7x//cPCRi6NXoHQVaVYm9+e/zCutCpJQHOX3mP108OISYmBnl5efjtt99QrVo1pddpckRQWpaWlmjWrBmFQxmjc0AMHjwYI0aMQIUKFTBs2DB07NgRTZs2VVzdlJ2djadPn+LSpUs4ePAg/v77bwQEBOCzzz7js/+EaMwUwkGTS1kFAqBdvXdIfXECWZwI9laZqFW5Ax48KH5wWAApAKsCSxgEkMLZ2RnDhg2DVCqFu7t7kW1oelmpLhPvyQmFQgqHMqhUVzGNGzcOq1atwqZNm7B48WIIBAIIBAKIRCJwXP7dkowxtG/fHn/++Sf69+/PW8cJ0VZZCIeCrATvIGM5itNKJQ0OV/d4iYfxfvjf9aUCVPeIh7W1NWrXrq32dZoODGt74x7HcYorlAw9XTiFAz9KNQbh6emJxYsXY8GCBYiJiUF0dDQSEhKQnZ0NV1dX+Pn5oWPHjvDy8uKrv4ToRCaTlalwUEWTU0FX78pwIuItEhJeISHuL3w1ago8PAw/1b58+gxDBwNA4cAnXgapRSIR2rVrh3bt2vGxOUJ4J5PJynQ4aKp5AyHi7p/D6f3rkJGRgfj4/vDw8NB7uwUVnFvJ399fcVrKECgc+EVXMZFyKy8vDzKZDED+3bplLRx8fX11+nJ1dHTEqFGjkJmZWWRGA30rPPGeIeZUkqNw4B8FBCmX5APS1vausKlQDZCqv++Ab3wdOWgzEMxxHF68eIFatWoBAJycnFCpUiWd2tUVzcpa/lBAkHJHHg7PkwTwa/OxYnlSVhbcbLP12raxakhPnz4d169fx5o1a/jfvlSIO/EV8d62LyBKhZX0qso+UDiUPxQQpFyRh0NiShY+sCpKzyVl2cLeioONpX4mjjRWmdC8vDzk5ORAKpUiNzeX323LgFP3vfAuUwwmqgBYeuE9qwFO+i+sLPNP31E4lF8UEKRMio6ORnR0dJHlMpkMMpkMFb3qw7tBlSLPR0RdwrvEojOKFqRLPQJj1pC2sbHBTz/9hNjYWDRs2BAnTpzQ+LXF1XQAAImFF9LtC1wiKxBCJnDE8bOXIJY8BFC0noOcvus6UDjoHwUEKZNyc3ORkaF+4rr01ESVy9+nJCIzM7PYbWtbj8BYp5WuXbummD7ExsYGDRs2BKDd4HZJM7NKrWRFFzIGTsrAjFjXgcLBMEoVEM+ePYONjY3SZXQrVqxQWsfR0RFjx44tTTOEFCEWi5VK2cqPHOQ3a+blpCHpxS24eTdSrPP22TUwSQbs7OyK3bY2Vw0Zq9hPaGgozp07h/nz5xeZeE+bwe2Sbr6TCVLByXLABFb/XxVOBkAGW0EihGKx2llZ5dvWBwoHw9E5IK5du4YWLVpg9+7dGDBgAID8c6GhoaFK6wkEAvj6+qJjx46l6ighBbVu3VqjuZUOHtsHWNqASTLRt1dnIMCHtz4Y67SShYUFKlWqBCsrq1LXj9bk5ruUD29x4YknMrJFEMg+wCH7FDq1a0RjDmZA59scN27ciDZt2ijCoaDDhw/j2bNniIuLQ//+/bF9+/ZSdZIQVTSZWynzXQL+fXEbWe9Vn3LSlTHHHIRCIWbPno0//vgDLVu21Ht7Fe1z8VGT53BJXw3b5F9gKX1F4WAmdA6IM2fO4JNPPlH5nKenJ7y9veHj44MBAwbg4sWLOneQEFXKysR7fOE4Dn/99ZfS3Ea6FPspDfle0sR75kPnU0zx8fGoW7eu0jKBQIBGjRrB1tZWsczT0xPx8fG695CQQswtHBhj+Oqrr3D27Fm8fPkSU6ZM0XubhdHEe+apVO80K1TXUCgU4saNG/Dz81Msk5dVJIQP5hYOQP4fXu3bt4e1tbVBTikVRhPvmS+d3/HKlSvj3r17Ja537949VK5cWddmCFEwx3CQGzhwIA4fPmzwgCh4ExwdOZgfnd/xwMBAbNiwAVKpVO06UqkUGzZsoCuYSKmZWzhwHIeNGzcq3Rld2iuWdOkD3SFt3nQOiC+//BIPHz7EoEGD8O+//xZ5/u3btxg0aBAePXqEL7/8slSdJObN3MIBAL755husWbMGc+bMMUh7hakKhwYNGqBp06Zo0KCBXtumcDAdOg9S+/v7Y/Xq1Zg0aRKOHz+O5s2bw9vbGwDw4sULXL16FVKpFGvWrFHc4UmItkobDg0aNNC5HoExTysNGjQIly9fxuDBgw3Wppy6IwdDTN1N4WBaBKyUI8gXLlzAwoULERkZiezs/JkybWxs0LlzZ8yePRtt2rThpaOmLD09HU5OTnj//j0cHR2N3Z1ywxyPHArKyspSuiLQEOi0UvmnzfdVqQNCTiaTITk5GQDg6upqlCsejIUCgn/mFg4cx2HlypUICQkx+FhDwT5QOJR/2nxf8fYtLhQK4e7uDnd3d7MKB8I/cwsHAFi8eDF27tyJL774QlEFz5AoHIgq9E1OTIo5hgMAjB49Gj4+Ppg6darB/8CicCDq0HTfxGSYazgAgJeXF/bu3QtLy+L/ScbGxioG3bWZtVUdCgdSnHJ3BJGRkYEZM2age/fucHNzg0AgQFhYmNr1r1+/jq5du8Le3h7Ozs7o378/4uLiDNdhAsD8woHjOHz99de4f/++YllJ4QDkB8SjR4+KLfKjTR8oHEhxyl1ApKSkYMOGDcjNzUXfvn2LXffhw4fo2LEjOI7D7t27sWXLFjx+/Bjt27dHUlKSYTpMzC4cAGD9+vU4evQovvzyS97LhGqCwoFowiCnmJKSkgx2ZYa3tzfS0tIgEAiQnJyMTZs2qV137ty5EIvFOHLkiGI0v1mzZqhVqxaWLVuGxYsXG6TP5swcwwEAQkJCcPfuXYSEhEAsFhusXYDCgWhOb0cQjDEcO3YMAwYMQNWqVfXVTBHyimIlkUqlOHLkCAYMGKB0qZe3tzc6deqEAwcO6LObBOYXDgWvKLezs8Mvv/yit7mVYmNj8eDBgyKnoigciDZ4P4J4+vQptmzZgu3bt+PNmzdgjBnlJqOSPH36FNnZ2SrvDvX398c///yDnJwcleUYc3NzlU4LpKen67Wv5ZG5hQPHcZg5cyZ69OiBoKAgANBbu4wBj55/QK4UsBEkKgazKRyItngJiJycHOzZswebN2/GuXPnwBhDy5YtFadwQkJC+GiGVykpKQAAFxeXIs+5uLiAMYa0tDR4enoWeX7RokUIDw/Xex/LK3MLBwDYt28fIiIicPnyZbRs2RLOzs46bYcBkBXzzzZPJsD5p17Iqphfq+WDLAcpmW/gIEqncCBaK9UppitXruCzzz6Dh4cHRo4cicePHyM0NBT3799HdHQ0xo8fr/M/BACIjIxUnDIq6XHz5k2d2ijuC0Ldc7Nnz8b79+8Vj1evXunUtjkyx3AAgMGDB2PQoEFYsWKFzv8m7r+ywwuMxlvriYjHIKR9KBoUj95WxL8f7P63QGCFi3FVIJFQOBDtlWqyvnv37kEkEqF3794YPXo0evbsyetNPnXq1MHGjRs1WrdatWpabbtixYoA/nckUVBqaioEAoHaf8hisdjgA4vlgbmFg1QqhYWFBQQCASwsLPD111/rvK34FDHO3quYf/5IAEiYE/act0RV7IIA/7vzOtupNyCuCMj3TyBEjtQK5y7EwEKgfmr+Bg0a8D4ZH4VD2adzQNy9exdCoRBTp07F1KlT4e7uzme/AOSXKx07dizv2wWAmjVrwsbGBnfu3Cny3J07d+Dr66ty/IHoxtzCgeM4hIaGok6dOpg4cWKJbcbGxhZ7b0MqWgBwBQT//3sTCJEHB3zItYaIpSrWk9mkAQX/dmEMYLnIznqP4nogkUhK3iktUDiUDzr/ub9q1So0bNgQixcvhpeXF/r27YuDBw8WW0DIlFhaWqJPnz7Yv38/MjIyFMtfvnyJiIgI9O/f34i9K1/MLRwA4Pz584iKisKvv/6qUU12iUSCnJwctY88STbAivZdyDjln99fBKTv/z8YZAAYbD+cgb2dHeyKeegyHbo6FA7lR6lnc7169So2b96MP//8E+np6XB1dcXw4cMxatQoNGzYEAcPHkT//v2Rl5fHV59LdPz4cWRmZiIjIwMhISEYNGiQYl794OBgxRTKDx8+REBAAJo2bYpZs2YhJycHc+fORWpqKm7evKnxvRs0m6t65hgOcjt37kSNGjU0upS1pCMIKWzxGgMgY2Lg/48F7ASxcEdEkXWZwArZQh8woRWspK8RMry3zvugLQoH02eU6b4LX8kEAE2aNEHdunXxxx9/GDQgfHx88OLFC5XPPXv2DD4+Poqfr127hpkzZyI6OhqWlpbo3Lkzli1bhpo1a2rcHgWEauYWDhzHQSAQ8PrXeEEZ2RY4ePY9JHlWsLFIxuDulVF4yE9+Keu5c+eQnZ0NOzs7fPLJJ3rpT2EUDmWDUQKioKdPn2Lz5s349ddfkZCQAIFAYNCAMDQKiKLMMRymT58OCwsLLF26VG8hceLECcX9OT169CjSB/mlrHFxcYpJ/agSHClIm+8rvUy1UbNmTSxcuBALFizAsWPHsGXLFn00Q0yUuYUDADx69AgxMTEQCASIjY1F3bp1DdKuXOGb4AwRCnIUDuWXXudiEgqF6N27N3r3Ntw5UGJc5hgOANCwYUOsWrUKFhYWRg8HQ6JwKN+oHgThjbmFA8dxyMrKUtwvY4z66xQORJ/K3XTfxDjMMRxCQ0Mxbtw4pKWl6b09dX2gcCD6RAFBSs3cwgHIn8L+/v37ePnyJZ4/f26QNgujcCD6RqeYSKmYYzgAQJUqVbBp0yYkJSWhSZMmBmtXjjFG4UD0jgKC6MzcwoHjOCQmJirm/fLx8VG6p8YQfHx8wHEcnJycKByI3lFAEJ2YYziEhobizp072LBhA2rVqqX3NlX1oVq1anTkQAxG54AQCoVa/cMszzfKmRtzCwcgf6aA5ORkZGVlGWVQmgakiTHoHBBz585V+se5detWfPjwAX369IGHhwfevHmDI0eOwM7OziQLBhHdmGM4AICjoyN++eUXPH361OBjDhQOxFh0DoiwsDDF/y9fvhweHh44deqU0ocoIyMDXbt2VUyOR8o2cwsHjuNw7949RSA4OjpSOBCzwstlrmvXrsWMGTOKfIgcHBwwY8YMrF27lo9miBGZYzhMnz4d48aNQ2RkpN7bU9cHCgdiTLwExOvXr2FpqfpgxNLSEomJiXw0Q4zE3MIByB9js7W1hYWFBWxsbAzSZkEUDsQU8DKba5MmTeDk5IR//vlHaRZLjuPQtWtXZGRk4MaNG6VtxmSV59lczTEcCrYfGxsLPz8/g7ZL4UD0yeCzuS5YsAB9+/ZFjRo10L9/f3h4eCAxMRH79+9HYmIi/vrrLz6aIQZmbuHAcRwiIiIQFBQEIP/ol8KBmDNeAqJXr144ceIEvv76a6xZswYymQwCgQAtWrTA1q1b0bVrVz6aIQZkbuGQl5eH0NBQREVFISEhAaNHj9Z7m4VROBBTw9uNcl26dEGXLl0U14lXqFCBrl4qo8wtHADAwsIC/v7+iImJQb169QzSZkEUDsQU8VpR7v3797h06RKSk5MRHByMChUq8LVpk1aexiDMMRwKSkhIQOXKlQ3aJoUDMSRtvq94m811/vz5qFy5Mnr27IkRI0bg2bNnAPKPLH744Qe+miF6ZG7hwHEcfvvtN0ilUsUyCgdC/oe3+yDCw8MxZswYHD16FAUPSnr37o2jR4/y0QzRI3MLBwCYPXs2li9fjnnz5hmkvcIoHIip42UM4ueff8a0adOwZMmSInMu1apVC0+ePOGjGaIn5hgOANCnTx/ExMSgV69eBmtTjsKBlAW8BERcXJzi0sDCHBwc8O7dOz6aIXpgruEAAB07dsSxY8cMPm5E4UDKCl5OMTk5OeHt27cqn3v+/Dnc3d35aIbwzNzCgeM4rFq1Cunp6YplFA6EqMdLQHTp0gVLlixBZmamYplAIIBUKsW6devUHl0Q4zG3cADyL6TYtm0bpk6dCh4v3tMYhQMpa3g5xTRv3jwEBASgXr166NevHwQCAX7++WfcuHEDL1++xO7du/lohvDEHMMBAEaMGIGrV69i/PjxBj+dReFAyiLe7oN48OABpk6dijNnzkAqlcLCwgKdOnXCjz/+iLp16/LRhMkqS/dBmGs4yHEcBysrK4O3SeFATIXB74OIiopC1apVceLECWRkZCA+Ph7p6ek4efIkqlatiqioKD6aIaVkbuHAcRzCwsLw/PlzxTIKB0I0x0tAdOrUCffv3wcAiMViVK5cWTFF8qNHj9CpUyc+miGlYG7hAAA//fQT/vrrL0yePBkSicQgbRZE4UDKOl7GIIo7SyWRSCAU8nbDNtGBOYYDAIwZMwa3bt3C5MmTlaahNwQKB1Ie6BwQ6enpSvc3JCYm4uXLl0rrZGdnY/v27fDw8NC5g6R0zDUcAKBChQr49ddfaUCaEB3pHBArV65UTFEgEAjQr18/lesxxjBnzhxdmyGlYG7hwHEc5syZg379+qFt27YAQOFASCnoHBDdu3eHvb09GGOYMWMGvvjiC1SrVk1pHbFYjIYNGyIwMLDUHSXaMbdwAIAdO3bg1KlTuHz5Mo4ePQoHBweDtCtH4UDKG50DonXr1mjdujWA/A/nuHHjDD4TJlHNHMMBAIYPH45Hjx6hX79+FA6E8IDXehDmypTugzC3cJDJZEa/CILCgZQlRqkHQYzP3MKB4zj897//xe+//673torrA4UDKa94Kzn65MkTrF+/Hg8ePEB2drbScwKBAKdPn+arKaKCuYUDAPzzzz+IiorC5cuX0a1bN1SqVMkg7cpROJDyjpeAuHv3Llq1aoUqVaogNjYW/v7+SE5OxuvXr1G1alXUrFmTj2aIGuYYDgAQHByM+Ph4NG7cmMKBED3gZQzio48+gkgkwq5du2BlZYWrV6+iadOmOHr0KEJCQnDgwAG0adOGj/6aJGOOQZhbOMhvvDTkfhZG4UDKMoOPQVy/fh0jR45UDBbKZDIAQK9evRAaGorZs2fz0QwpxNzCgeM4TJ8+HeHh4UUqFxoKhQMxJ7wERFpaGlxcXCAUCiESiZCWlqZ4rnnz5rh+/TofzZACzC0cAOD27du4cOEC/v77b8TFxRmkzYIoHIi54WUMokqVKkhOTgYA+Pr6IioqCt26dQOQ/4+aPtD8MsdwAPL/2Fi0aBGcnJxQq1Ytg7ULUDgQ88RLQLRr1w4XL15E3759MWzYMHz33Xd48+YNrKyssG3bNgwfPpyPZgjMLxw4joNEIoGdnR2A/Dv4DY3CgZgrXgLi66+/RkJCAgBg5syZSExMxI4dOyAQCDB48GAsXbqUj2bMnjmGw/Tp05Geno41a9YY5QuSwoGYM7qTmgeGuIrJ3MIBAOLi4jBy5EhwHIcNGzagUaNGBmlXjsKBlEfafF/pfARReGrvkhSeyI9ozhzDAQBq1KiB9evXIyMjg8KBECPQOSB8fHy0+rIw1mWJZV15DIfY2FhIJBKIRCL4+voqPcdxHFJTUxU1ROrVq8dLm9qgcCAkn84BsWXLFqMUnTcn5TEcgPyAyMnJgbW1tVJAcByH0NBQPH78GJs2bYKXlxdvbWqKwoGQ/9E5IEaNGsVjN0hh5TUcivPhwwe8evUKaWlpePPmjcEDgsKBEGXlbjbXjIwMzJgxA927d4ebmxsEAgHCwsJUrnv+/HmMHTsWzZo1g1gshkAgwPPnzw3aX1XMMRwAwMXFBRs2bMDatWsREBBgsHYBCgdCVCl3AZGSkoINGzYgNzcXffv2LXbd06dP49SpU6hWrZrJzBVV3sPh8bM8nLvTBOfudcC1x3WRkpaLhw8fKp53c3NDs2bNeG+3OBQOhKjGS0DIJ08r7mEo3t7eSEtLw9mzZ7Fo0aJi1/3222/x/PlzHDhwAL169TJQD9Ur7+GQ8FaGdb/nICPLDhKpGP+muSB8xQuMGhWCK1eu8N6eJigcCFGPlxvl5s6dW+QLJSkpCSdPnkReXh5GjBjBRzMa0eaLzdiVyAoqD+EQGxuL2NhYtc8/jq8Gmawq5H+XMAgByyoQWXsiJiYGKSkpKl/n6+tb5GonPlA4EFI8XgJC3Tl+juMQFBQEd3d3PpoxGbm5ucjNzVX8nJ6eXqrtlYdwAPKn4s7JyVH7fJ5UqnJ5v3794OVlr/a1EolE5z6pQ+FASMn0+ie0lZUVvvjiC6xYsUKfzRicfMI4+aNq1ao6b8vUwyE2NhYPHjwo9shATiQSwdraWu2jaqU0CAQMgOz/X8HgbJ+OurVdi32dSCTidb8pHAjRDG8lR9WxsbHBmzdvdHptZGQkOnXqpNG6N27cQOPGjXVqR1uzZ8/GtGnTFD+np6frFBKmHg6A+nsWVCnpVJBEIkHUuZV4x7WGi6sPPN04fDGmBmytPUu1L9qgcCBEc3oNiKSkJCxduhR16tTR6fV16tTBxo0bNVrXkFN5iMViiMXiUm2jLIQD3ywtLeFdxQIXd07FJ598gno168HW2nDlaCkcCNEOLwFRvXr1Il8yubm5+PfffyEUCnHo0CGdtuvp6YmxY8fy0UWTYo7hAORfQDBt2jS4u7vDwcHBYO0CFA6E6IKXgAgMDCzyRWNtbQ0fHx8MGTIEPj4+fDRTLpSlcHidbIknGUGQ5NnANi8FWTkC2FprN/kvx3E4dOgQ+vfvD6FQCIFAADc3t2IHs/lG4UCIbngJiG3btvGxGd4cP34cmZmZyMjIAADcv38fe/fuBQAEBwfD1tYWQP4psLNnzwIA7ty5o3itm5sb3NzcEBgYyGu/ylI4pKRb4M8zTsiTMQBCpEtssTtShhHd30HTq4MZY5gxYwYiIyPx7NkzfPXVV6XfES1ROBCiO17qQUgkEnAcp6j6VVBmZiasrKx4vxKlOD4+Pnjx4oXK5549e6Y4oiluEDwwMBCRkZEatafJ/OqmGA7F3bfwNqcB3ub4o/CFbrUcjsDG4p3atgoPVB8+fBgLFy7EqlWr0LJlS0W76mZz5ROFAyFFGaQeREFjx44Fx3HYuXNnkefGjx8PGxsbbNq0iY+mNKLpfEodO3aEIeolmWI4AMXftyBVc88Cl8tBIFR/eqjwPQt9+vRB27Zt4eLiolimz1CQo3AgpPR4uQ8iMjISH330kcrn+vTpg9OnT/PRTJlkquEAFH/fQkWbBAhQ8J4FGayFKXC0yS72ngUAWLt2LbKzsxXtFAwHQ6BwIIQfvBxBvH37Fp6eqq9l9/DwQGJiIh/NlDmmHA5AyfctJCRnYH8EBy7PBnaiFHza2wG24qBi2501axZOnDiB+/fvY/Xq1QavGULhQAh/eDmCcHZ2VnsuOzY21uCXNJoCUw8HTVR2lcLX4SRqWO+Ct9152IpLPh03dOhQuLi44NNPP6VwIKSM4yUgOnXqhEWLFiE1NVVpeWpqKn744Qd07tyZj2bKjPIQDrpq3Lgxjh49qhiQNhQKB0L4x9tkfQEBAahVqxaGDBmCKlWqID4+Hnv27IFEIkF4eDgfzZQJ5hYOHMdh2bJlCAkJUdSRtrGx0Xu7hftA4UAI/3gJiDp16uDcuXOYNm0aNm7ciLy8PFhYWCAwMBArVqzQeaqNssbcwgEAli1bht27d+P69evYtWuXQfcZoHAgRJ94m4upUaNGOH36NLKzs5GWlgYXFxfFVS3mIjk5GVKptFyFg6+vr+KeBVVCQkJw/fp1fPXVVxQOhJQzvNwoZ+7kN55cvnwZHh4e5SYcNCU/YjQkCgdCdGOQG+Vevnyp1fqGnG3VWOSlVw3FWGMOc+fOxdChQxXTq1M4EFI+6RwQPj4+Wn0h5eXl6doUUcFYRw6bN2/GiRMncPnyZRw9epQGpAkpx3QOiC1bthjldAZRDoc3b95AKpXqfV4juVGjRuH+/fsYPnw4hQMh5ZzOATFq1Cgeu0E0VfjI4enTpxpXfNMVY0zxx4CNjQ1Wr16tl3aKQ+FAiOHptSY14ZexxhymTp2Kw4cP672t4vpA4UCI4fF2meuTJ0+wfv16PHjwQGmiNiC/kpg5T9jHB2ONORw6dAiRkZGIiYkpMiurIVA4EGI8vATE3bt30apVK1SpUgWxsbHw9/dHcnIyXr9+japVq6JmTcPVHS6P1IWDjAFZrApyhEJYsPd6abt///549uwZOnToQOFAiJnh5T6Ijz76CCKRCLt27YKVlRWuXr2Kpk2b4ujRowgJCcGBAwfQpk0bPvprkuTXFV+9elXtrLa6UhcOeTLg6FU3xKfIb0aUoXvjVPh6ZqvfmIYkEgksLS2NehEChQMh+qHNfRC8jEFcv34dI0eOhPD/a1HKZPk1BHr16oXQ0FDMnj2bj2bMTnGnle69tEd8irjA2gKcvu0CSV7pvtQ5jsP06dOxYsUKgxRTUtcHCgdCjI+XU0zyqTWEQiFEIhHS0tIUzzVv3hzz5s3joxmzcvPmTdy5c0fpCqKCklkbAI4A5DepCZAnE+D4yYuwEqg/3VRSDYiYmBhERUVBLBZj4MCB8Pb2Lt2OaInCgRDTwUtAVKlSBcnJyQDyv4CioqLQrVs3AMDt27fpH5uWpFIp0tPT1ZYDBQBYpAAWQkCeHYwByIOUS4EMqsuFAkVLghbWvn17fPvtt/Dy8qJwIMTM8RIQ7dq1w8WLF9G3b18MGzYM3333Hd68eQMrKyts27YNw4cP56MZsyA/rSSTyYqd7FDMnoCDL3JQ+f+XMLgLzsLW2hLFva2qJt3jOA6MMYjF+aesBgwYUJpd0AmFAyGmh5eA+Prrr5GQkAAAmDlzJhITE7Fjxw4IBAIMHjwYS5cu5aOZcq/gmEO9evVQv379YteXyfLw19/HkSOxhL0oDR/1bA2gtlZtyscc8vLysHLlSkVIGBKFAyGmiZeAqFmzpuJSVgsLC/z000/46aef+Ni02dDlPgehELAVxEMoy4GVQLep1ePi4nDlyhUwxvDkyRM0aNBA7bqxsbGKqb/5umubwoEQ06XzVUxpaWkYMGAAjhw5onadI0eOYMCAAUhJSdG1GbNgzCm7/fz8sHr1avz444/FhgOQHxCPHj1SW39cWxQOhJg2nQNi06ZNuHXrFnr06KF2nR49euDOnTtYs2aNrs2Ue8aaPqNg/fCAgAC0atVK7+0W7gOFAyGmTeeA+PPPPzFu3DhYWqo/S2VpaYlx48bh0KFDujZTrhkrHEJDQzFmzBgkJSXpvT11faBwIMT06RwQjx8/RvPmzUtcr2nTpnj8+LGuzZRbfIWDr68v6tSpo/GYQGpqKh4/foyEhAStij5xEgGy8tyRyyqiNPfPUTgQUnboPEgtr0FQEpFIVOK19+aGzyMHbQeLPTw8sGnTJrx58wbNmjXT6DVv31niwIUKyOaCAQDvuXhI8wBLLQvJUTgQUrbofATh6emJ+/fvl7jevXv34OHhoWsz5Y6xTivFxcUpfvby8kJAQIDGrz8S44wc7n8flUxZFVx7Yqd1HygcCClbdD6CCAwMxNq1azFmzBi1RxISiQTr1q1Dp06ddO5geWLMMYcbN25g/fr1qFevXpF1YmNj1V6ZJGOWSM8peqPjnUepSHseobbdglN6UDgQUjbpfAQxdepUPHz4EP369VPcJFdQQkIC+vbti0ePHmHq1Kml6mR5YKxLWSUSCd6/fw+O45CRkaF2nZycHJWP3JwPEIADUHDggUEgS1f7mpycHMVpRQoHQsounY8g/P39sWbNGkycOBHVq1dHs2bNUL16dQDAs2fPcO3aNchkMqxbtw4NGzbkrcNlkTHvc7Czs8PatWsRGxuLRo0aqVxHJBIVO62HpzQaCZIOAGQABLBAFipZ34dlMTfniUQiCgdCyrhS14OIjo7GwoULERERgaysLACAra0tunTpgtmzZxv8+npjKK4ehLFOK926dUurcYaSJKZZ4tT555BJs1HR+hV69excYh8oHAgxPdrUgyj1VButW7fG4cOHIZPJFDO6urq6KmpDmDNjhINEIsH06dNx8eJFLFy4EEFBQbxs16OCFK6iu8jJy4FFCdN6UDgQUj7wVpNaKBTC3d2dr82VecY6rSQUClGhQgVYWlrC2dnZIG0WROFASPnBW0CQ/zHmmIOFhQW+++47fPrpp6hVq5bB2gUoHAgpb+g8EM+MNeZw9OhRRYlQCwsLCgdCSKnREQSPjBEOMpkMoaGhiIqKQnx8PCZMmKC3tnx9fRXTfRdE4UBI+UQBwaP09HTY2toafMyhRYsWuHz5Mho3bqzXtlRN60HhQEj5RQHBo6ysLLi5uRl0zAEAhg8fjm7duqFSpUoGbZfCgZDyjcYgeCQUCg025rB161alSRApHAghfKMjiDLo66+/xj///INHjx7hhx9+MHj7FA6EmAc6giiD+vXrB0dHR/Tr18/gbVM4EGI+6AiiDGrTpg2OHTtm8C9JCgdCzAsdQZQBHMdh2bJlSEtLUyyjcCCE6BsFRBmwcOFC/P7775gyZQpKObeiTigcCDFPFBBlwKhRo+Dl5YXJkycb/BJaCgdCzBeNQZQBPj4+OHDggEY1wPlE4UCIeaMjCBPEcRy+/fZbPHnyRLGMwoEQYmgUECZo7dq1OHz4MKZMmQKO4wzePoUDIQQohwGRkZGBGTNmoHv37oppL8LCwoqsl5eXhxUrVqBHjx7w8vKCra0t6tati1mzZuHdu3cG73dBISEhaNasGcLCwmBlZWXQtikcCCFy5S4gUlJSsGHDBuTm5qJv375q18vOzkZYWBi8vb2xatUqHDt2DOPGjcOGDRvQtm1bZGdnG67TgNLVSY6Ojti0aRNatmxp0D5QOBBCCip3g9Te3t5IS0uDQCBAcnIyNm3apHI9GxsbPHv2DBUrVlQs69ixI6pVq4ZBgwZh3759GD58uEH6zHEcZs2ahT59+qBTp04AQFcrEUKMrtwdQQgEAo2+XC0sLJTCQa5FixYAgFevXvHeN3V27dqFM2fO4Ntvv0V6errB2pWjcCCEqFLujiBK68yZMwCA+vXrq10nNzcXubm5ip9L+6X+8ccfIzY2FsHBwXB0dCzVtrRF4UAIUafcHUGUxuvXrzFr1iw0b94cvXv3VrveokWL4OTkpHhUrVpV67by8vIU/29paYnw8HAacyCEmBSTDojIyEjFKaOSHjdv3ixVW6mpqQgODgZjDLt27YJQqP5XM3v2bLx//17x0PZ0FMdxmDp1qtrxEUOgcCCElMSkTzHVqVMHGzdu1GjdatWq6dxOWloaunXrhtevX+PMmTOoUaNGseuLxWKIxWKd24uIiEBUVBRiYmIQHByMypUr67wtXVA4EEI0YdIB4enpibFjx+q1jbS0NHTt2hXPnj3D6dOn4e/vr9f2ACAoKAgJCQmoV68ehQMhxGSZdEDomzwc4uLi8M8//6BJkyZ6a4vjOAiFQlha5v/KR48erbe2iusDhQMhRFPlMiCOHz+OzMxMZGRkAADu37+PvXv3AgCCg4Nha2uL7OxsBAUF4caNG1i1ahWkUikuXbqk2Iabmxtq1qzJS384jkNoaChsbGzw/fffK0LCkCgcCCHaEjBjFBjQMx8fH7x48ULlc8+ePYOPjw+eP3+O6tWrq93GyJEjsW3bNo3aS09Ph5OTEw4dOqRyLOTmzZsYO3YsLCwssH37dvj5+Wm0Xb5QOBBC5OTfV+/fvy/xsvpyeQTx/PnzEtfx8fExWPGdxo0bY9myZbCxsaFwIISUGeUyIEwBx3HIyclRJHTHjh2N0gcKB0KIrkz6PoiyiuM4TJ8+HZ999plRps6Q94HCgRBSGhQQepCYmIi7d+/i6dOnePr0qcHbp3AghPCBTjHpQbVq1bBhwwakpaXp9dJZVSgcCCF8oYDg0du3bxVXMdWqVcvg7VM4EEL4RKeYeDRjxgyNrqDSBwoHQgjf6AiCB/LLZTMyMvDy5Uu4uroatH2O45CXlwcXFxcwxhQ3CBpCVlYWBAIB3N3dIZPJjDYoTwjRjPzfqCaX+ZfLG+UMLT4+XqcpvwkhxFhevXoFLy+vYtehgOCBTCZDQkICHBwctC4Vmp6ejqpVq+LVq1cGLxZkTLTf5rPf5rjPgOnut/wsQ+XKlYstawDQKSZeCIXCEpO4JI6Ojib1ITIU2m/zYY77DJjmfjs5OWm0Hg1SE0IIUYkCghBCiEoUEEYmFovx3XfflapCXVlE+20++22O+wyUj/2mQWpCCCEq0REEIYQQlSggCCGEqEQBQQghRCUKCCPJyMjAjBkz0L17d7i5uUEgECAsLKzIenl5eVixYgV69OgBLy8v2Nraom7dupg1axbevXtn8H6Xlqb7DQDnz5/H2LFj0axZM4jFYggEAqPNdVVa2uw3AFy/fh1du3aFvb09nJ2d0b9/f8TFxRmuw3p2+fJlBAUFwcHBAfb29ujUqRMuXLhg7G7p3Y0bN9C3b19UrlwZtra28PPzw7x585CVlWXsrqlEAWEkKSkp2LBhA3Jzc9G3b1+162VnZyMsLAze3t5YtWoVjh07hnHjxmHDhg1o27YtsrOzDddpHmi63wBw+vRpnDp1CtWqVUObNm0M00E90Wa/Hz58iI4dO4LjOOzevRtbtmzB48eP0b59eyQlJRmmw3p05coVdOjQAdnZ2fjtt9/w22+/IScnB126dEF0dLSxu6c39+/fR5s2bfD8+XOsWrUKR44cwdChQzFv3jx8/PHHxu6eaowYhUwmYzKZjDHGWFJSEgPAvvvuuyLrSaVSlpycXGT5nj17GAD222+/6burvNJ0vxljLC8vT/H/S5cuZQDYs2fPDNBL/mmz34MGDWKurq7s/fv3imXPnz9nIpGIzZgxwxDd1augoCBWqVIllpmZqViWnp7OXF1dWZs2bYzYM/36+uuvGQAWGxurtHz8+PEMAEtNTTVSz9SjIwgjEQgEGs3bZGFhgYoVKxZZ3qJFCwD5E26VJZruN4AS54kpSzTdb6lUiiNHjmDAgAFK0zN4e3ujU6dOOHDggD67aRAXLlxAx44dYWtrq1jm4OCADh064OLFi3jz5o0Re6c/IpEIQNFpLpydnSEUCmFlZWWMbhWr/PwLNDNnzpwBANSvX9/IPSF8evr0KbKzs+Hv71/kOX9/f8TGxiInJ8cIPeMPx3Eqbx6TL7tz546hu2QQI0eOhLOzMz7//HPExcUhIyMDR44cwfr16zFp0iTY2dkZu4tF0GR9ZdDr168xa9YsNG/eHL179zZ2dwiPUlJSAAAuLi5FnpPX+0hLS4Onp6ehu8abevXq4dKlS5DJZIqjRKlUipiYGAD/+x2UNz4+PoiOjka/fv1Qs2ZNxfIpU6Zg1apVxutYMegIggeRkZGKUwglPW7evFmqtlJTUxEcHAzGGHbt2mXU0zCG3G9TYoj9Lu50lLZTyuuTLr+LL774Ao8fP8bkyZPx+vVrvHr1Cp999hlevHgBoGycWtRlv58/f44+ffqgYsWK2Lt3L86ePYslS5Zg27ZtGDt2rHF3SA06guBBnTp1sHHjRo3Wldes1kVaWhq6deuG169f48yZM6hRo4bO2+KDofbb1Ohzv+XjTar+ik5NTYVAIICzs7NW29QnXX4XISEhSEpKwoIFC7Bu3ToAQOvWrREaGorFixejSpUqeusvX3TZ71mzZiE9PR03b95UnE7q0KEDXF1dERISghEjRiAwMFBvfdYFBQQPPD099f4XQFpaGrp27Ypnz57h9OnTKs9RG5oh9tsU6XO/a9asCRsbG5Xn4e/cuQNfX19YW1vrpW1d6Pq7mDlzJv773//iyZMncHBwgLe3NyZMmAA7Ozs0a9ZMDz3lly77ffPmTdSrV6/IWENAQAAA4O7duyYXEKZ/LEcU4RAXF4eTJ0+iSZMmxu4S0RNLS0v06dMH+/fvV6ot/vLlS0RERKB///5G7B2/xGIxGjRoAG9vb7x8+RK7du3CuHHjYGNjY+yu6UXlypVx7949fPjwQWm5/N6P0hYd0wc6gjCi48ePIzMzU/FFcP/+fezduxcAEBwcDFtbW2RnZyMoKAg3btzAqlWrIJVKcenSJcU23NzclAa8ygJN9hsAkpKScPbsWQD/u7Ll+PHjcHNzg5ubm8n9tVUSTfc7PDwcAQEB6N27N2bNmoWcnBzMnTsXrq6umD59utH6z5e7d+9i3759aN68OcRiMW7duoUffvgBtWrVwvz5843dPb3573//i759+6Jbt26YOnUqXF1dcenSJSxatAj16tVDz549jd3Foox9I4Y58/b2ZgBUPuQ3hD179kztOgDYyJEjjboPutBkvxljLCIiQu16gYGBRuu/rjTdb8YYu3r1KuvSpQuztbVljo6OrG/fvkVusCqrHj16xDp06MBcXFyYlZUV8/X1Zd988w378OGDsbumd2fOnGHdu3dnHh4ezMbGhtWuXZtNnz5d5c2wpoDqQRBCCFGJxiAIIYSoRAFBCCFEJQoIQgghKlFAEEIIUYkCghBCiEoUEIQQQlSigCCEEKISBQQhhBCVKCAIIYSoRAFBCCFEJQoIQgjRk3Xr1qFp06YQiUQICwszdne0RgFBCCF64unpifDwcPTt29fYXdEJTfdNCCF6Ig+GgwcPGrcjOqIjiDJo27ZtxdbAjYyMNHYXiyXv//Pnz43dFbUuXryIsLAwvHv3Tudt6LKfP/30EwQCARo0aKBTm7dv38aYMWMUlelsbGxQq1YtTJgwAVevXtV4O/PmzUO9evUgk8kAAGFhYRAIBEhOTtapX7owlc+JPvuxefNmVKlSBZmZmbxvmw8UEGXY1q1bER0dXeTRtGlTY3etWL169UJ0dDQ8PT2N3RW1Ll68iPDw8FIFhC62bNkCgUCAe/fuISYmRqvXrl+/Hs2aNUNMTAy+/PJLHDlyBEePHsV///tf3Lt3DwEBAXj69GmJ20lISMCSJUswb948CIX0FaFPI0eOhJ2dHZYsWWLsrqhEp5jKsAYNGqB58+bG7obGsrKyYGtrq6gIR5RdvXoVt27dwowZM7Bq1Sps3rwZLVu21Oi1Fy5cwMSJE9GrVy/s3bsXVlZWiuc6d+6MSZMmYc+ePRqV8/zxxx/h7Oxcrsqb8q1Lly64cOGCyue++uorjSvjWVpaYsKECZg/fz5mzpypqCpoKujPg3IqJycHTZo0ga+vL96/f69YnpiYCA8PD3Ts2BF5eXkA/nf64MaNG+jfvz8cHR3h5OSE4cOHIykpSWm7T548wSeffAJ3d3eIxWLUrVsXa9asKdK+fJvXr1/HwIEDUaFCBUVp1MKH7PJ1b9++jUGDBsHJyQkuLi6YNm0apFIpHj16hB49esDBwQE+Pj4q/9rStl/37t3Dxx9/DCcnJ1SqVAkhISGK31NYWBi++uorAED16tWLnLqLjY3F6NGjUatWLdja2qJKlSro06ePoiyqrjZv3gwLCwtMnToVvXv3xp9//omsrCyNXrtw4UJYWFhg/fr1SuFQ0KBBg1C5cuVit8NxHDZv3oxPPvmkxKOHhw8fokaNGmjZsiX+/fdfpeUff/wxKlWqBLFYjGrVqmHEiBHIzc0FwN/vj4/Pzfnz59GlSxc4ODjA1tYWbdq0wdGjR0ts+/Tp08jJyVH50LZs6rBhw5Ceno4///xTq9cZAgVEGZaXlwepVKr0kH/pW1tbY/fu3fj3338REhICAJDJZBg2bBgYY9i5cycsLCyUttevXz/4+vpi7969CAsLw19//YWgoCBIJBIA+TWUAwICcPfuXSxfvhxHjhxBr169MGXKFISHh6vsY//+/eHr64s9e/bgl19+KXZ/Bg8ejEaNGmHfvn0YN24cVq5cialTp6Jv377o1asXDhw4gM6dO2PmzJnYv3+/4nW69GvAgAGoXbs29u3bh1mzZuGPP/7A1KlTAQBjx47FF198AQDYv39/kVN3CQkJqFixIn744QecOHECa9asgaWlJVq2bIlHjx4Vu4/qZGdnY+fOnejZsyc8PDwwevRoZGRkYM+ePSW+Ni8vDxEREWjevHmpT9vFxMQgJSUFnTp1Kna9s2fPok2bNvD390dERATc3d0BALdu3UJAQAAuXbqEefPm4fjx41i0aBFyc3PBcRwA/n9/un5uzp49i86dO+P9+/fYvHkzdu7cCQcHB/Tp0we7du3Suh+qSKVS5OTkKP6tyv+/IA8PD/j5+WkUTAZn5JKnRAdbt25VW9vYwsJCad1du3YxAGzVqlVs7ty5TCgUspMnTyqt89133zEAbOrUqUrLd+zYwQCw33//nTHGWFBQEPPy8mLv379XWm/y5MnM2tqapaamFtnm3Llz1fZfXodZvu7y5cuV1mvcuDEDwPbv369YJpFImJubG+vfv79imS79WrJkidK6EydOZNbW1kwmkzHGGFu6dKnKWtGqSKVSxnEcq1WrltLvsPB+FufXX39lANi+ffsU2/Tw8GDt27cv8bWJiYkMABs6dKjKvkkkEsVDvn/qLF68mAFgiYmJSsvlv7ekpCT222+/MSsrKzZlyhSWl5entF7nzp2Zs7Mz+/fff0vsd8E+6vL7K+3nplWrVszd3Z1lZGQo9aVBgwbMy8tL8bvS5n1U18eCj61btxZZb9iwYaxSpUpab1/f6AiiDPv1119x5coVpUfhgc3Bgwfj888/x1dffYUFCxZgzpw56Natm8rtDRs2rMhrLS0tERERgZycHJw+fRr9+vWDra2t0lFLcHAwcnJycOnSpSLbHDBggMb707t3b6Wf69atC4FAgJ49eyqWWVpawtfXFy9evAAAnfv10UcfKf3s7++PnJwcpVMl6kilUixcuBD16tWDlZUVLC0tYWVlhSdPnuDBgwca729Bmzdvhqurq+J3YGFhgU8//RTnzp3DkydPdNomADRr1gwikUjxWL58ebHrJyQkQCAQwNXVVeXz33//PUaNGoUffvgBP/74o9JpqKysLJw9exaDBw8udoyJ79+fLp+bzMxMxMTEYODAgbC3t1esJ/+9x8fH63w0WFBYWBgYY0qPUaNGFVnP3d0d//77L6RSaanb5BMFRBlWt25dNG/eXOnRrFmzIuuFhIRAIpHA0tISU6ZMUbs9Dw8PpZ8tLS1RsWJFpKSkICUlBVKpFKtXr1b6whGJRAgODgYAlZdAanPKw8XFRelnKysr2NrawtrausjynJwcANC5XxUrVlT6WSwWA8g/1VOSadOm4dtvv0Xfvn1x+PBhxMTE4MqVK2jUqJFGry8sNjYWUVFRGDZsmNL4wejRowHkX9lUHFdXV9jY2Ci+/Ar6448/cOXKFRw6dEijvmRnZ0MkEhU5/Sj3+++/o0qVKhg6dGiR59LS0pCXlwcvL69i2+D796fL5yYtLQ2MMZWfT/k4TUpKitZ90ZW1tTUYY4r+mQq6iqmcy8zMxKefforatWvj7du3GDt2rNqbdhITE1GlShXFz1KpFCkpKahYsSIqVKig+Otq0qRJKl9fvXr1IssEAgE/O6KGrv0qjd9//x0jRozAwoULlZYnJyfD2dlZ6+1t2bJF5V+WdevWRcuWLbF9+3YsWLBA7Ze2hYUFOnfujJMnT+LNmzdKX3r16tUDAI2v4Xd1dQXHccjMzISdnV2R50+cOIEhQ4agffv2OH36NLy9vRXPubi4wMLCAvHx8cW2wffvTxcVKlSAUCjEmzdvijyXkJAAAGqPovQhNTUVYrFY6WjGFNARRDn32Wef4eXLl9i/fz82b96MQ4cOYeXKlSrX3bFjh9LPu3fvhlQqRceOHWFra4tOnTrhxo0b8Pf3L3Lk0rx58yJ/lRuCvvpV3BGFQCBQPC939OhRvH79Wut28vLysH37djRp0gSNGzcu8vzo0aPx5s0bHD9+vNjtzJ49G3l5efjss88UFxXows/PDwDU3i/h7e2Nc+fOQSwWo3379kqnv2xsbBAYGIg9e/YUe0Mdn78/XdnZ2aFly5bYv3+/0nssk8nw+++/w8vLC7Vr1zZYf+Li4hRhbkroCKIMu3v3rspzljVr1oSbmxs2bdqE33//HVu3bkX9+vVRv359TJ48GTNnzkTbtm3RokULpdft378flpaW6NatG+7du4dvv/0WjRo1wuDBgwHkXx/frl07tG/fHp9//jl8fHyQkZGB2NhYHD58GGfOnDHIfhemj341bNhQse2RI0dCJBKhTp06cHBwQO/evbFt2zb4+fnB398f165dw9KlS0s8taLK8ePHkZCQgI4dO+Kvv/4q8rz8NMnmzZuLnGsvqG3btlizZg2++OILNG3aFOPHj0f9+vUVfyXv27cPAODo6Fhsfzp27AgAuHTpEvz9/VWu4+npibNnzyIoKAgdOnTAP//8o7jze8WKFWjXrh1atmyJWbNmwdfXF2/fvsWhQ4ewfv163n9/pbFo0SJ069YNnTp1QmhoKKysrLB27VrcvXsXO3fu1PvRr5xMJsPly5cxZswYg7SnFSMOkBMdFXcVEwC2ceNGdvv2bWZjY8NGjhyp9NqcnBzWrFkz5uPjw9LS0hhj/7vS4tq1a6xPnz7M3t6eOTg4sI8//pi9fftW6fXPnj1jISEhrEqVKkwkEjE3NzfWpk0btmDBAqX1Cl71oq7/ha9iKrzuyJEjmZ2dXZHXBwYGsvr16/PaL1VXqsyePZtVrlyZCYVCBoBFREQwxhhLS0tjY8aMYe7u7szW1pa1a9eOnTt3jgUGBrLAwMBit1lY3759i30v5Q9LS8siVxapcvPmTTZ69GhWvXp1JhaLmbW1NfP19WUjRoxgp0+fLvH1jDHWvn17FhwcrLRM1e/t3bt3rG3btszFxYVduXJFsfz+/fts0KBBrGLFiszKyopVq1aNjRo1iuXk5PD6++Pjc3Pu3DnWuXNnZmdnx2xsbFirVq3Y4cOHldYpzVVMmjh9+rTi35+pETDGmEGSiJissLAwhIeHIykpyaDnXYlp2rdvH4YMGYIXL14ojUkR/fj0008RFxen9s5sY6IxCEKIkv79+yMgIACLFi0ydlfKvadPn2LXrl1YvHixsbuiEgUEIUSJQCDAxo0bUblyZcVsrkQ/Xr58iZ9//hnt2rUzdldUolNMhBBCVKIjCEIIISpRQBBCCFGJAoIQQohKFBCEEEJUooAghBCiEgUEIYQQlSggCCGEqEQBQQghRCUKCEIIISpRQBBCCFGJAoIQQohK/wcWgQNbGDNHYgAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -2750,7 +4581,11 @@ } ], "source": [ - "cinnabar_plotting.plot_DGs(fe.graph, figsize=5, shift=shift)" + "data = femap.read_csv('cinnabar_input.csv')\n", + "exp_DG_sum = sum([data['Experimental'][i].DG for i in data['Experimental'].keys()])\n", + "shift = exp_DG_sum / len(data['Experimental'].keys())\n", + "\n", + "cinnabar_plotting.plot_DGs(fe.to_legacy_graph(), figsize=5, shift=shift.m)" ] } ], @@ -2770,7 +4605,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.12.6" } }, "nbformat": 4, From 12ea0439497868deafe786a0755213fb29f63df2 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Sat, 5 Oct 2024 19:22:10 +0200 Subject: [PATCH 061/143] Release version update to user guide --- docs/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 140cb12a..81554dbf 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -16,7 +16,7 @@ Clone alchemiscale from Github, and switch to the latest release tag:: $ git clone https://github.com/OpenFreeEnergy/alchemiscale.git $ cd alchemiscale - $ git checkout v0.5.0 + $ git checkout v0.5.1 Create a conda environment using, e.g. `micromamba`_:: From 65b4c4eddacf0bb7ce48864bd298b569e522ac9b Mon Sep 17 00:00:00 2001 From: David Dotson Date: Sun, 6 Oct 2024 00:31:57 +0200 Subject: [PATCH 062/143] Update codecov in CI to use token, update action from v2 to v4 --- .github/workflows/ci-integration.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-integration.yml b/.github/workflows/ci-integration.yml index 4817e2c9..a91229af 100644 --- a/.github/workflows/ci-integration.yml +++ b/.github/workflows/ci-integration.yml @@ -55,8 +55,9 @@ jobs: - name: codecov if: ${{ github.repository == 'OpenFreeEnergy/alchemiscale' && github.event != 'schedule' }} - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v4 with: + token: ${{ secrets.CODECOV_TOKEN }} file: coverage.xml fail_ci_if_error: False verbose: True From fdc25a73fd1f2c75371394ea89318645d1fce26a Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Sun, 6 Oct 2024 18:47:50 -0700 Subject: [PATCH 063/143] `get_taskhub` calls `get_taskhubs` --- alchemiscale/storage/statestore.py | 51 ++++++++++++------------------ 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 7a339895..ad88dd7f 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -1217,43 +1217,19 @@ def query_taskhubs( """ return self._query(qualname="TaskHub", scope=scope, return_gufe=return_gufe) - def get_taskhub( - self, network: ScopedKey, return_gufe: bool = False - ) -> Union[ScopedKey, TaskHub]: - """Get the TaskHub for the given AlchemicalNetwork. + def get_taskhubs( + self, network_scoped_keys: list[ScopedKey], return_gufe: bool = False + ) -> list[Union[ScopedKey, TaskHub]]: + """Get the TaskHubs for the given AlchemicalNetworks. Parameters ---------- return_gufe - If True, return a `TaskHub` instance. - Otherwise, return a `ScopedKey`. + If True, return `TaskHub` instances. + Otherwise, return `ScopedKey`s. """ - if network.qualname != "AlchemicalNetwork": - raise ValueError( - "`network` ScopedKey does not correspond to an `AlchemicalNetwork`" - ) - - q = f""" - match (th:TaskHub {{network: "{network}"}})-[:PERFORMS]->(an:AlchemicalNetwork) - return th - """ - try: - node = record_data_to_node(self.execute_query(q).records[0]["th"]) - except IndexError: - raise KeyError("No such object in database") - - if return_gufe: - return self._subgraph_to_gufe([node], node)[node] - else: - return ScopedKey.from_str(node["_scoped_key"]) - - # TODO: write docstring - # TODO: can we replace the above method with this one? - def get_taskhubs( - self, network_scoped_keys: list[ScopedKey], return_gufe: bool = False - ) -> list[Union[ScopedKey, TaskHub]]: # TODO: this could fail better, report all instances rather than first for network_scoped_key in network_scoped_keys: if network_scoped_key.qualname != "AlchemicalNetwork": @@ -1289,6 +1265,21 @@ def _node_to_scoped_key(node): for network_scoped_key in network_scoped_keys ] + def get_taskhub( + self, network: ScopedKey, return_gufe: bool = False + ) -> Union[ScopedKey, TaskHub]: + """Get the TaskHub for the given AlchemicalNetwork. + + Parameters + ---------- + return_gufe + If True, return a `TaskHub` instance. + Otherwise, return a `ScopedKey`. + + """ + + return self.get_taskhubs([network], return_gufe)[0] + def delete_taskhub( self, network: ScopedKey, From 51194ff8c46c4e3c49eb851d3a29753539ddcf58 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 7 Oct 2024 15:53:10 -0700 Subject: [PATCH 064/143] Updated docstrings --- alchemiscale/interface/api.py | 4 -- alchemiscale/storage/models.py | 12 ++++- alchemiscale/storage/statestore.py | 80 ++++++++++++++++++++++++------ 3 files changed, 76 insertions(+), 20 deletions(-) diff --git a/alchemiscale/interface/api.py b/alchemiscale/interface/api.py index 66ce6a31..f1109b35 100644 --- a/alchemiscale/interface/api.py +++ b/alchemiscale/interface/api.py @@ -947,7 +947,6 @@ def get_task_status( return status[0].value -# TODO docstring @router.post("/networks/{network_scoped_key}/restartpolicy/add") def add_task_restart_patterns( network_scoped_key: str, @@ -961,7 +960,6 @@ def add_task_restart_patterns( n4js.add_task_restart_patterns(taskhub_scoped_key, patterns, number_of_retries) -# TODO docstring @router.post("/networks/{network_scoped_key}/restartpolicy/remove") def remove_task_restart_patterns( network_scoped_key: str, @@ -974,7 +972,6 @@ def remove_task_restart_patterns( n4js.remove_task_restart_patterns(taskhub_scoped_key, patterns) -# TODO: docstring @router.get("/networks/{network_scoped_key}/restartpolicy/clear") def clear_task_restart_patterns( network_scoped_key: str, @@ -987,7 +984,6 @@ def clear_task_restart_patterns( return [network_scoped_key] -# TODO docstring @router.post("/bulk/networks/restartpolicy/get") def get_task_restart_patterns( *, diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index 59c6659f..96920a78 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -143,7 +143,6 @@ def _defaults(cls): return super()._defaults() -# TODO: fill in docstrings class TaskRestartPattern(GufeTokenizable): """A pattern to compare returned Task tracebacks to. @@ -202,8 +201,17 @@ def __eq__(self, other): return self.pattern == other.pattern -# TODO: docstrings class Tracebacks(GufeTokenizable): + """ + Attributes + ---------- + tracebacks: list[str] + The tracebacks returned with the ProtocolUnitFailures. + source_keys:list[ScopedKey] + The ScopedKeys of the Protocols that failed. + failure_keys: list[ScopedKey] + The ScopedKeys of the ProtocolUnitFailures. + """ def __init__( self, tracebacks: List[str], source_keys: List[str], failure_keys: List[str] diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index ad88dd7f..a85d718d 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -2846,18 +2846,25 @@ def err_msg(t, status): ## task restart policy - # TODO: fill in docstring def add_task_restart_patterns( - self, taskhub: ScopedKey, patterns: List[str], number_of_retries: int + self, taskhub: ScopedKey, patterns: list[str], number_of_retries: int ): """Add a list of restart policy patterns to a `TaskHub` along with the number of retries allowed. Parameters ---------- - + taskhub : ScopedKey + TaskHub for the restart patterns to enforce. + patterns: list[str] + Regular expression patterns that will be compared to tracebacks returned by ProtocolUnitFailures. + number_of_retries: int + The number of times the given patterns will apply to a single Task, attempts to restart beyond + this value will result in a canceled Task with an error status. Raises ------ + KeyError + Raised when the provided TaskHub ScopedKey cannot be associated with a TaskHub in the database. """ # get taskhub node @@ -2866,8 +2873,8 @@ def add_task_restart_patterns( RETURN th """ results = self.execute_query(q, taskhub=str(taskhub)) - ## raise error if taskhub not found - + + # raise error if taskhub not found if not results.records: raise KeyError("No such TaskHub in the database") @@ -2935,8 +2942,16 @@ def add_task_restart_patterns( self.resolve_task_restarts(actioned_task_scoped_keys, tx=tx) - # TODO: fill in docstring - def remove_task_restart_patterns(self, taskhub: ScopedKey, patterns: List[str]): + def remove_task_restart_patterns(self, taskhub: ScopedKey, patterns: list[str]): + """Remove a list of restart patterns enforcing a TaskHub from the database. + + Parameters + ---------- + taskhub: ScopedKey + The ScopedKey of the TaskHub that the patterns enforce. + patterns: list[str] + The patterns to remove. Patterns not enforcing the TaskHub are ignored. + """ q = """ UNWIND $patterns AS pattern @@ -2948,19 +2963,36 @@ def remove_task_restart_patterns(self, taskhub: ScopedKey, patterns: List[str]): self.execute_query(q, patterns=patterns, taskhub_scoped_key=str(taskhub)) def clear_task_restart_patterns(self, taskhub: ScopedKey): + """Clear all restart patterns from a TaskHub. + + Parameters + ---------- + taskhub: ScopedKey + The ScopedKey of the TaskHub to clear of restart patterns. + """ q = """ MATCH (trp: TaskRestartPattern {taskhub_scoped_key: $taskhub_scoped_key}) DETACH DELETE trp """ self.execute_query(q, taskhub_scoped_key=str(taskhub)) - # TODO: fill in docstring def set_task_restart_patterns_max_retries( self, - taskhub_scoped_key: Union[ScopedKey, str], - patterns: List[str], + taskhub_scoped_key: ScopedKey, + patterns: list[str], max_retries: int, ): + """Set the maximum number of retries of a pattern enforcing a TaskHub. + + Parameters + ---------- + taskhub_scoped_key: ScopedKey + The ScopedKey of the TaskHub that the patterns enforce. + patterns: list[str] + The patterns to change the maximum retries value for. + max_retries: int + The new maximum retries value. + """ query = """ UNWIND $patterns AS pattern MATCH (trp: TaskRestartPattern {pattern: pattern, taskhub_scoped_key: $taskhub_scoped_key}) @@ -2974,11 +3006,24 @@ def set_task_restart_patterns_max_retries( max_retries=max_retries, ) - # TODO: fill in docstring # TODO: validation of taskhubs variable, will fail in weird ways if not enforced def get_task_restart_patterns( - self, taskhubs: List[ScopedKey] - ) -> Dict[ScopedKey, Set[Tuple[str, int]]]: + self, taskhubs: list[ScopedKey] + ) -> dict[ScopedKey, set[tuple[str, int]]]: + """For a list of TaskHub ScopedKeys, get the associated restart patterns along with the maximum number of retries for each pattern. + + Parameters + ---------- + taskhubs: list[ScopedKey] + The ScopedKeys of the TaskHubs to get the restart patterns of. + + Returns + ------- + dict[ScopedKey, set[tuple[str, int]]] + A dictionary containing whose keys are the ScopedKeys of the TaskHubs provided and whose + values are a set of tuples containing the patterns enforcing each TaskHub along with their + associated maximum number of retries. + """ q = """ UNWIND $taskhub_scoped_keys as taskhub_scoped_key @@ -3002,9 +3047,16 @@ def get_task_restart_patterns( return data - # TODO: docstrings @chainable def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=None): + """Determine whether or not Tasks need to be restarted or canceled and perform that action. + + Parameters + ---------- + task_scoped_keys: Iterable[ScopedKey] + An iterable of Task ScopedKeys that need to be resolved. Tasks without the error status + are filtered out and ignored. + """ # Given the scoped keys of a list of Tasks, find all tasks that have an # error status and have a TaskRestartPattern applied. A subquery is executed From 977c89680e74dbcd7943aca81b57834b4dc2d7f1 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 21 Oct 2024 16:09:37 -0700 Subject: [PATCH 065/143] Added docstrings to client methods --- alchemiscale/interface/client.py | 65 ++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/alchemiscale/interface/client.py b/alchemiscale/interface/client.py index 8510cc05..265ed835 100644 --- a/alchemiscale/interface/client.py +++ b/alchemiscale/interface/client.py @@ -1747,6 +1747,25 @@ def add_task_restart_patterns( patterns: list[str], num_allowed_restarts: int, ) -> ScopedKey: + """Add a list of restart patterns to an `AlchemicalNetwork`. + + Parameters + ---------- + network_scoped_key: ScopedKey + The ScopedKey for the AlchemicalNetwork to add the patterns to. + patterns: list[str] + The regular expression strings to compare to ProtocolUnitFailure tracebacks. + Matching patterns will set the Task status back to 'waiting'. + num_allowed_restarts: int + The number of times each pattern will be able to restart each `Task`. When + this number is exceeded, the `Task` is canceled from the `AlchemicalNetwork` + and left with the `error` status. + + Returns + ------- + network_scoped_key: ScopedKey + The ScopedKey of the AlchemicalNetwork the patterns were added to. + """ data = {"patterns": patterns, "number_of_retries": num_allowed_restarts} self._post_resource(f"/networks/{network_scoped_key}/restartpolicy/add", data) return network_scoped_key @@ -1754,6 +1773,19 @@ def add_task_restart_patterns( def get_task_restart_patterns( self, network_scoped_key: ScopedKey ) -> dict[str, int]: + """Get the Task restart patterns enforcing an AlchemicalNetwork along with the number of retries allowed for each pattern. + + Parameters + ---------- + network_scoped_key: ScopedKey + The ScopedKey of the AlchemicalNetwork to query. + + Returns + ------- + patterns : dict[str, int] + A dictionary whose keys are all of the patterns enforcing the `AlchemicalNetwork` and whose + values are the number of retries each pattern will allow. + """ data = {"networks": [str(network_scoped_key)]} mapped_patterns = self._post_resource( "/bulk/networks/restartpolicy/get", data=data @@ -1767,7 +1799,18 @@ def set_task_restart_patterns_allowed_restarts( network_scoped_key: ScopedKey, patterns: list[str], num_allowed_restarts: int, - ): + ) -> None: + """Set the number of allowed restarts that patterns allowed to perform within an AlchemicalNetwork. + + Parameters + ---------- + network_scoped_key : ScopedKey + The ScopedKey of the `AlchemicalNetwork` enforced by `patterns`. + patterns: list[str] + The patterns to set the number of allowed restarts for. + num_allowed_restarts : int + The new number of allowed restarts. + """ data = {"patterns": patterns, "max_retries": num_allowed_restarts} self._post_resource( f"/networks/{network_scoped_key}/restartpolicy/maxretries", data @@ -1775,11 +1818,27 @@ def set_task_restart_patterns_allowed_restarts( def remove_task_restart_patterns( self, network_scoped_key: ScopedKey, patterns: list[str] - ): + ) -> None: + """Remove specific patterns from an `AlchemicalNetwork`. + + Parameters + ---------- + network_scoped_key : ScopedKey + The ScopedKey of the `AlchemicalNetwork` enforced by `patterns`. + patterns: list[str] + The patterns to remove from the `AlchemicalNetwork`. + """ data = {"patterns": patterns} self._post_resource( f"/networks/{network_scoped_key}/restartpolicy/remove", data ) - def clear_task_restart_patterns(self, network_scoped_key: ScopedKey): + def clear_task_restart_patterns(self, network_scoped_key: ScopedKey) -> None: + """Clear all restart patterns from an `AlchemicalNetwork`. + + Parameters + ---------- + network_scoped_key : ScopedKey + The ScopeKey of the `AlchemicalNetwork` to be cleared of restart patterns. + """ self._query_resource(f"/networks/{network_scoped_key}/restartpolicy/clear") From 2d2d8f6807777136558f415ae9a87d228b4d63f1 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 21 Oct 2024 16:10:12 -0700 Subject: [PATCH 066/143] Added Task restart patterns to user guide --- docs/user_guide.rst | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 81554dbf..caf026b1 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -510,6 +510,48 @@ If you’re feeling confident, you could set all errored :py:class:`~alchemiscal , ...] +*************************************************** +Re-running Errored Tasks with Task Restart Patterns +*************************************************** + +Re-running errored :py:class`~alchemiscale.storage.models.Task`\s manually for known failure modes (such as those described in the previous section) quickly becomes tedious, especially for large networks. +Alternatively, you can add `regular expression `_ strings as Task restart patterns to an :external+gufe:py:class`~gufe.network.AlchemicalNetwork`. +These patterns _enforce_ the `AlchemicalNetwork` and there is no limit to the number of patterns that can enforce an `AlchemicalNetwork`. +As a result, `Task`\s actioned on that `AlchemicalNetwork` now support automatic restarts if the `Task` fails during any part of its execution, provided that an enforcing pattern matches a traceback returned by any of the `Task`\'s returned `ProtocolUnitFailure`\s. +The number of restarts is controlled by the ``num_allowed_restarts`` parameter of the `AlchemiscaleClient.add_task_restart_patterns` method. +If a `Task` is restarted more than ``num_allowed_restarts`` times, the `Task` is canceled and left with an ``error`` status. +As an example, if you wanted to rerun any `Task` that failed with a ``RuntimeError`` _or_ a ``MemoryError`` and attempt it at least 5 times, you could add the following patterns::: + + >>> asc.add_task_restart_patterns(network_scoped_key, [r"RuntimeError: .+", r"MemoryError: Unable to allocate \d+ GiB"], 5) + +Providing too general a pattern, such as the example above, you may consume compute resources on failures that are unavoidable. +On the other hand, an overly strict pattern (such as specifying explicit Gufe keys) will likely do nothing. +Therefore, it is best to find a balance in your patterns that matches your use-case. + +Restart patterns _enforcing_ an `AlchemicalNetwork` can be retrieved with:: + + >>> asc.get_task_restart_patterns(network_scoped_key) + {"RuntimeError: .+": 5, "MemoryError: Unable to allocate \d+ GiB": 5} + +The number of allowed restarts can be modified:: + + >>> asc.set_task_restart_patterns_allowed_restarts(network_scoped_key, ["RuntimeError: .+"], 3) + >>> asc.set_task_restart_patterns_allowed_restarts(network_scoped_key, ["MemoryError: Unable to allocate \d+ GiB"], 2) + >>> asc.get_task_restart_patterns(network_scoped_key) + {"RuntimeError: .+": 3, "MemoryError: Unable to allocate \d+ GiB": 2} + +Patterns can be removed by specifying the patterns in a list:: + + >>> asc.remove_task_restart_patterns(network_scoped_key, ["MemoryError: Unable to allocate \d+ GiB"]) + >>> asc.get_task_restart_patterns(network_scoped_key) + {"RuntimeError: .+": 3} + +Or by clearing all enforcing patterns:: + + >>> asc.clear_task_restart_patterns(network_scoped_key) + >>> asc.get_task_restart_patterns(network_scoped_key) + {} + *********************************** Marking Tasks as deleted or invalid From d7dcd5c488e6f89448d452201744db0876311d86 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 21 Oct 2024 16:30:24 -0700 Subject: [PATCH 067/143] Link to python classes and methods in restart pattern section --- docs/user_guide.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/user_guide.rst b/docs/user_guide.rst index caf026b1..6815dbaa 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -516,11 +516,11 @@ Re-running Errored Tasks with Task Restart Patterns Re-running errored :py:class`~alchemiscale.storage.models.Task`\s manually for known failure modes (such as those described in the previous section) quickly becomes tedious, especially for large networks. Alternatively, you can add `regular expression `_ strings as Task restart patterns to an :external+gufe:py:class`~gufe.network.AlchemicalNetwork`. -These patterns _enforce_ the `AlchemicalNetwork` and there is no limit to the number of patterns that can enforce an `AlchemicalNetwork`. -As a result, `Task`\s actioned on that `AlchemicalNetwork` now support automatic restarts if the `Task` fails during any part of its execution, provided that an enforcing pattern matches a traceback returned by any of the `Task`\'s returned `ProtocolUnitFailure`\s. -The number of restarts is controlled by the ``num_allowed_restarts`` parameter of the `AlchemiscaleClient.add_task_restart_patterns` method. -If a `Task` is restarted more than ``num_allowed_restarts`` times, the `Task` is canceled and left with an ``error`` status. -As an example, if you wanted to rerun any `Task` that failed with a ``RuntimeError`` _or_ a ``MemoryError`` and attempt it at least 5 times, you could add the following patterns::: +These patterns _enforce_ the :external+gufe:py:class`~gufe.network.AlchemicalNetwork` and there is no limit to the number of patterns that can enforce an :external+gufe:py:class`~gufe.network.AlchemicalNetwork`. +As a result, :py:class`~alchemiscale.storage.models.Task`\s actioned on that :external+gufe:py:class`~gufe.network.AlchemicalNetwork` now support automatic restarts if the :py:class`~alchemiscale.storage.models.Task` fails during any part of its execution, provided that an enforcing pattern matches a traceback returned by any of the :py:class`~alchemiscale.storage.models.Task`\'s returned :external+gufe:py:class`~gufe.protocols.ProtocolUnitFailure`\s. +The number of restarts is controlled by the ``num_allowed_restarts`` parameter of the :py:meth:`~alchemiscale.interface.client.AlchemiscaleClient.add_task_restart_patterns` method. +If a :py:class`~alchemiscale.storage.models.Task` is restarted more than ``num_allowed_restarts`` times, the :py:class`~alchemiscale.storage.models.Task` is canceled and left with an ``error`` status. +As an example, if you wanted to rerun any :py:class`~alchemiscale.storage.models.Task` that failed with a ``RuntimeError`` _or_ a ``MemoryError`` and attempt it at least 5 times, you could add the following patterns::: >>> asc.add_task_restart_patterns(network_scoped_key, [r"RuntimeError: .+", r"MemoryError: Unable to allocate \d+ GiB"], 5) @@ -528,7 +528,7 @@ Providing too general a pattern, such as the example above, you may consume comp On the other hand, an overly strict pattern (such as specifying explicit Gufe keys) will likely do nothing. Therefore, it is best to find a balance in your patterns that matches your use-case. -Restart patterns _enforcing_ an `AlchemicalNetwork` can be retrieved with:: +Restart patterns enforcing an :external+gufe:py:class`~gufe.network.AlchemicalNetwork` can be retrieved with:: >>> asc.get_task_restart_patterns(network_scoped_key) {"RuntimeError: .+": 5, "MemoryError: Unable to allocate \d+ GiB": 5} From 91f14d828d819fb184e2a7bf2c89d97456cc5f77 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Tue, 22 Oct 2024 19:19:23 +0200 Subject: [PATCH 068/143] Switch to matching against `SettingsBaseModel` instead of `Settings` in `gufe_to_subgraph` --- alchemiscale/storage/statestore.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 87c00097..4e6b9c3c 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -18,9 +18,9 @@ AlchemicalNetwork, Transformation, NonTransformation, - Settings, Protocol, ) +from gufe.settings import SettingsBaseModel from gufe.tokenization import GufeTokenizable, GufeKey, JSON_HANDLER from neo4j import Transaction, GraphDatabase, Driver @@ -342,7 +342,7 @@ def _gufe_to_subgraph( ): node[key] = json.dumps(value, cls=JSON_HANDLER.encoder) node["_json_props"].append(key) - elif isinstance(value, Settings): + elif isinstance(value, SettingsBaseModel): node[key] = json.dumps(value, cls=JSON_HANDLER.encoder, sort_keys=True) node["_json_props"].append(key) elif isinstance(value, GufeTokenizable): From cbef297cd68d7d8a140c9082f0f742873fc24d33 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Fri, 25 Oct 2024 08:06:29 -0700 Subject: [PATCH 069/143] Bump checkout action version to v4 --- .github/workflows/ci-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-integration.yml b/.github/workflows/ci-integration.yml index a91229af..d559b1bd 100644 --- a/.github/workflows/ci-integration.yml +++ b/.github/workflows/ci-integration.yml @@ -32,7 +32,7 @@ jobs: - "3.12" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: mamba-org/setup-micromamba@v1 with: From bdf8b16787e547a1c8fb94342fefd1985694c0d1 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Fri, 25 Oct 2024 08:18:37 -0700 Subject: [PATCH 070/143] Pinning python-multipart to v0.0.12 CI is attempting to install 0.0.14 but versions 0.0.13 and 0.0.14 have been yanked from PyPI. --- devtools/conda-envs/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index ec9a6955..7d117978 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -30,7 +30,7 @@ dependencies: - python-jose - passlib - bcrypt - - python-multipart + - python-multipart=0.0.12 - starlette - httpx - cryptography From 2d34bdd8a225a10cecd05652618f26a4eba7fd3c Mon Sep 17 00:00:00 2001 From: David Dotson Date: Fri, 25 Oct 2024 14:23:59 -0700 Subject: [PATCH 071/143] Add rationale for pinning python-multipart --- devtools/conda-envs/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index 7d117978..038d6acc 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -30,7 +30,7 @@ dependencies: - python-jose - passlib - bcrypt - - python-multipart=0.0.12 + - python-multipart=0.0.12 # temporarily pinned due to broken 0.14 release on conda-forge - starlette - httpx - cryptography From 8a5a28be7419985eada7672d5118160363c8cf62 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Fri, 25 Oct 2024 14:25:01 -0700 Subject: [PATCH 072/143] Pin python-multipart in server environment --- devtools/conda-envs/alchemiscale-server.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index e7205497..2a68451b 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -31,7 +31,7 @@ dependencies: - python-jose - passlib - bcrypt - - python-multipart + - python-multipart=0.0.12 # temporarily pinned due to broken 0.14 release on conda-forge - starlette - httpx - cryptography From 529d4627379ba5441ed78b732617aefa5435b0ac Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 6 Nov 2024 17:56:26 -0700 Subject: [PATCH 073/143] Changes to address #292 Ups neo4j testing to use 5.25, updates test environment --- alchemiscale/tests/integration/conftest.py | 2 +- devtools/conda-envs/test.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/alchemiscale/tests/integration/conftest.py b/alchemiscale/tests/integration/conftest.py index c05a7374..054c7d48 100644 --- a/alchemiscale/tests/integration/conftest.py +++ b/alchemiscale/tests/integration/conftest.py @@ -103,7 +103,7 @@ def generate_uri(self, service_name=None): # TODO: test with full certificates neo4j_deployment_profiles = [ - DeploymentProfile(release=(5, 16), topology="CE", schemes=["bolt"]), + DeploymentProfile(release=(5, 25), topology="CE", schemes=["bolt"]), ] if NEO4J_VERSION == "LATEST": diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index 038d6acc..4b81670b 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -10,6 +10,7 @@ dependencies: - gufe>=1.0.0 - openfe>=1.1.0 - pydantic<2.0 + - async-lru ## state store - neo4j-python-driver @@ -53,6 +54,5 @@ dependencies: - openmmforcefields>=0.14.1 - pip: - - async_lru - - git+https://github.com/datryllic/grolt@neo4j-5.x # neo4j test server deployment + - git+https://github.com/datryllic/grolt@neo4j-5.23-fix # neo4j test server deployment - git+https://github.com/OpenFreeEnergy/openfe-benchmarks From 9fc01ade1192dd1b95b46c977ea1979391824c70 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 6 Nov 2024 18:00:13 -0700 Subject: [PATCH 074/143] Attempting to drop python-multipart pin from test env This pin was placed due to a broken package on conda-forge; appears resolved now. --- devtools/conda-envs/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index 4b81670b..995194d4 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -31,7 +31,6 @@ dependencies: - python-jose - passlib - bcrypt - - python-multipart=0.0.12 # temporarily pinned due to broken 0.14 release on conda-forge - starlette - httpx - cryptography From 926db2e2ef22694e146f5d79af8d7a7fb8f9e76d Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 6 Nov 2024 19:49:36 -0700 Subject: [PATCH 075/143] Switch to using grolt master/main in test env --- devtools/conda-envs/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index 995194d4..fb61a3a6 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -53,5 +53,5 @@ dependencies: - openmmforcefields>=0.14.1 - pip: - - git+https://github.com/datryllic/grolt@neo4j-5.23-fix # neo4j test server deployment + - git+https://github.com/datryllic/grolt # neo4j test server deployment - git+https://github.com/OpenFreeEnergy/openfe-benchmarks From defa1b6dad8d249c26f0238a90ed75760fa370fe Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 6 Nov 2024 19:52:51 -0700 Subject: [PATCH 076/143] Pinning openff-units to 0.2.2 temporarily --- devtools/conda-envs/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index fb61a3a6..b4853df6 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -51,6 +51,7 @@ dependencies: # additional pins - openmm=8.1.2 - openmmforcefields>=0.14.1 + - openff-units=0.2.2 - pip: - git+https://github.com/datryllic/grolt # neo4j test server deployment From 43474acbaa54548b8815b3e52cfcb15278731cad Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 6 Nov 2024 20:48:50 -0700 Subject: [PATCH 077/143] Closes #297 --- alchemiscale/interface/api.py | 10 +++++++- alchemiscale/interface/client.py | 2 +- alchemiscale/models.py | 6 ++++- .../interface/client/test_client.py | 25 +++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/alchemiscale/interface/api.py b/alchemiscale/interface/api.py index 0784ea49..0ad4fe6c 100644 --- a/alchemiscale/interface/api.py +++ b/alchemiscale/interface/api.py @@ -96,7 +96,15 @@ def check_existence( n4js: Neo4jStore = Depends(get_n4js_depends), token: TokenData = Depends(get_token_data_depends), ): - sk = ScopedKey.from_str(scoped_key) + try: + sk = ScopedKey.from_str(scoped_key) + except ValueError as e: + raise HTTPException( + status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=e.args[0], + ) + + validate_scopes(sk.scope, token) return n4js.check_existence(scoped_key=sk) diff --git a/alchemiscale/interface/client.py b/alchemiscale/interface/client.py index 2699ce4b..be9414b2 100644 --- a/alchemiscale/interface/client.py +++ b/alchemiscale/interface/client.py @@ -88,7 +88,7 @@ def get_scoped_key(self, obj: GufeTokenizable, scope: Scope) -> ScopedKey: "Scope for a ScopedKey must be specific; it cannot contain wildcards." ) - def check_exists(self, scoped_key: ScopedKey) -> bool: + def check_exists(self, scoped_key: Union[ScopedKey, str]) -> bool: """Returns ``True`` if the given ScopedKey represents an object in the database.""" return self._get_resource(f"/exists/{scoped_key}") diff --git a/alchemiscale/models.py b/alchemiscale/models.py index 75edd1d0..69fc1288 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -151,7 +151,11 @@ def __eq__(self, other): @classmethod def from_str(cls, string): - prefix, token, org, campaign, project = string.split("-") + try: + prefix, token, org, campaign, project = string.split("-") + except ValueError: + raise ValueError("input does not appear to be a `ScopedKey`") + gufe_key = GufeKey(f"{prefix}-{token}") return cls(gufe_key=gufe_key, org=org, campaign=campaign, project=project) diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index ceb968f4..dec0bfdf 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -86,10 +86,35 @@ def test_create_network( network_sks = user_client.query_networks() assert an_sk in network_sks + assert user_client.check_exists(an_sk) + # TODO: make a network in a scope that doesn't have any components in # common with an existing network # user_client.create_network( + def test_check_exists( + self, + scope_test, + n4js_preloaded, + user_client: client.AlchemiscaleClient, + network_tyk2, + ): + an_sks = user_client.query_networks() + + # check that a known existing AlchemicalNetwork exists + assert user_client.check_exists(an_sks[0]) + + # check that an AlchemicalNetwork that doesn't exist shows as not existing + an_sk = an_sks[0] + an_sk_nonexistent = ScopedKey(gufe_key=GufeKey('AlchemicalNetwork-lol'), **scope_test.dict()) + assert not user_client.check_exists(an_sk_nonexistent) + + # check that we get an exception when we try a malformed key + with pytest.raises(AlchemiscaleClientError, + match="Status Code 422 : Unprocessable Entity : input does not appear to be a `ScopedKey`", + ): + user_client.check_exists('lol') + @pytest.mark.parametrize(("state",), [[state.value] for state in NetworkStateEnum]) def test_set_network_state( self, From df0e355dc08f9d330eb0cc2e90e085c2c76916a6 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 6 Nov 2024 20:50:51 -0700 Subject: [PATCH 078/143] Black! --- alchemiscale/interface/api.py | 1 - .../tests/integration/interface/client/test_client.py | 11 +++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/alchemiscale/interface/api.py b/alchemiscale/interface/api.py index 0ad4fe6c..fef0b2c3 100644 --- a/alchemiscale/interface/api.py +++ b/alchemiscale/interface/api.py @@ -104,7 +104,6 @@ def check_existence( detail=e.args[0], ) - validate_scopes(sk.scope, token) return n4js.check_existence(scoped_key=sk) diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index dec0bfdf..7146c50f 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -106,14 +106,17 @@ def test_check_exists( # check that an AlchemicalNetwork that doesn't exist shows as not existing an_sk = an_sks[0] - an_sk_nonexistent = ScopedKey(gufe_key=GufeKey('AlchemicalNetwork-lol'), **scope_test.dict()) + an_sk_nonexistent = ScopedKey( + gufe_key=GufeKey("AlchemicalNetwork-lol"), **scope_test.dict() + ) assert not user_client.check_exists(an_sk_nonexistent) # check that we get an exception when we try a malformed key - with pytest.raises(AlchemiscaleClientError, + with pytest.raises( + AlchemiscaleClientError, match="Status Code 422 : Unprocessable Entity : input does not appear to be a `ScopedKey`", - ): - user_client.check_exists('lol') + ): + user_client.check_exists("lol") @pytest.mark.parametrize(("state",), [[state.value] for state in NetworkStateEnum]) def test_set_network_state( From 2cf0ab5be99603409856646a58e386bc6a4c9c2e Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Thu, 7 Nov 2024 12:23:12 -0700 Subject: [PATCH 079/143] Adopt code from py2neo to remove the use of the `id` function in cypher queries (#302) We are adopting the query generators from within py2neo and modifying them to satisfy our needs in alchemiscale. This is needed because the `id` function is deprecated, flooding outputs with deprecation warnings. Code included from py2neo is under the Apache v2.0 license and therefore we need to include a copy of Apache v2.0 along with a NOTICE with information on our changes to the code. --- LICENSE | 211 ++++++++++++++++++++++++++++++- NOTICE | 38 ++++++ alchemiscale/storage/cypher.py | 135 ++++++++++++++++++++ alchemiscale/storage/subgraph.py | 20 ++- 4 files changed, 398 insertions(+), 6 deletions(-) create mode 100644 NOTICE diff --git a/LICENSE b/LICENSE index edc5657e..186b41d8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -MIT License +## MIT License Copyright (c) 2022 Open Force Field Initiative @@ -19,3 +19,212 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## Apache 2.0 Licensed Code + +This project includes code from py2neo, which is licensed under the Apache License 2.0. The Apache License 2.0 text is included below for reference. + +## Apache 2.0 License + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..aa389fd0 --- /dev/null +++ b/NOTICE @@ -0,0 +1,38 @@ +# NOTICE + +This project includes software developed by py2neo (https://github.com/neo4j-contrib/py2neo/tree/master). + +## Original NOTICE + + py2neo + Copyright (c) "Neo4j" + Neo4j Sweden AB [https://neo4j.com] + + This product includes software developed by + Neo4j, Inc. . + + This product includes software developed by + Nigel Small . + + This product includes software developed by + Simon Harrison . + + This product includes software developed by + Marcel Hellkamp . + +## Modifications + +- alchemiscale/storage/cypher.py + - Functions modified from the py2neo.cypher.queries module: + - _match_clause + - Modifications made: + - switched all usage of the id function to elementId + +- alchemiscale/storage/subgraph.py + - Functions modified from the py2neo.data module: + - Subgraph.__db_merge__ (moved to merge_subgraph) + - Subgraph.__db_create__ (moved to create_subgraph) + - Modifications made: + - switched all usage of the id function to elementId + - removed the usage of py2neo database connections + and replaced them with the official neo4j driver diff --git a/alchemiscale/storage/cypher.py b/alchemiscale/storage/cypher.py index 91d91152..3eb99277 100644 --- a/alchemiscale/storage/cypher.py +++ b/alchemiscale/storage/cypher.py @@ -1,6 +1,18 @@ from alchemiscale import ScopedKey from typing import List, Optional +from py2neo.cypher.queries import ( + _create_clause, + _merge_clause, + _on_create_set_properties_clause, + _relationship_data, + _set_labels_clause, + _set_properties_clause, + cypher_escape, + cypher_join, +) +from py2neo.cypher.queries import NodeKey + def cypher_list_from_scoped_keys(scoped_keys: List[Optional[ScopedKey]]) -> str: """Generate a Cypher list structure from a list of ScopedKeys, ignoring NoneType entries. @@ -28,3 +40,126 @@ def cypher_list_from_scoped_keys(scoped_keys: List[Optional[ScopedKey]]) -> str: def cypher_or(items): return "|".join(items) + + +# Original code from py2neo, licensed under the Apache License 2.0. +# Modifications by alchemiscale: +# - switched id function to use elementId +def _match_clause(name, node_key, value, prefix="(", suffix=")"): + if node_key is None: + # ... add MATCH by id clause + return "MATCH %s%s%s WHERE elementId(%s) = %s" % ( + prefix, + name, + suffix, + name, + value, + ) + else: + # ... add MATCH by label/property clause + nk = NodeKey(node_key) + n_pk = len(nk.keys()) + if n_pk == 0: + return "MATCH %s%s%s%s" % (prefix, name, nk.label_string(), suffix) + elif n_pk == 1: + return "MATCH %s%s%s {%s:%s}%s" % ( + prefix, + name, + nk.label_string(), + cypher_escape(nk.keys()[0]), + value, + suffix, + ) + else: + return "MATCH %s%s%s {%s}%s" % ( + prefix, + name, + nk.label_string(), + nk.key_value_string(value, list(range(n_pk))), + suffix, + ) + + +# Original code from py2neo, licensed under the Apache License 2.0. +def unwind_merge_nodes_query(data, merge_key, labels=None, keys=None, preserve=None): + """Generate a parameterised ``UNWIND...MERGE`` query for bulk + loading nodes into Neo4j. + + Parameters + ---------- + data + merge_key + labels + keys + preserve + Collection of key names for values that should be protected + should the node already exist. + + Returns + ------- + (query, parameters) tuple + """ + return cypher_join( + "UNWIND $data AS r", + _merge_clause("_", merge_key, "r", keys), + _on_create_set_properties_clause("r", keys, preserve), + _set_properties_clause("r", keys, exclude_keys=preserve), + _set_labels_clause(labels), + data=list(data), + ) + + +# Original code from py2neo, licensed under the Apache License 2.0. +def unwind_merge_relationships_query( + data, merge_key, start_node_key=None, end_node_key=None, keys=None, preserve=None +): + """Generate a parameterised ``UNWIND...MERGE`` query for bulk + loading relationships into Neo4j. + + Parameters + ---------- + data + merge_key : tuple[str, ...] + start_node_key + end_node_key + keys + preserve + Collection of key names for values that should be protected + should the relationship already exist. + + Returns + ------- + (query, parameters) : tuple + """ + return cypher_join( + "UNWIND $data AS r", + _match_clause("a", start_node_key, "r[0]"), + _match_clause("b", end_node_key, "r[2]"), + _merge_clause("_", merge_key, "r[1]", keys, "(a)-[", "]->(b)"), + _on_create_set_properties_clause("r[1]", keys, preserve), + _set_properties_clause("r[1]", keys, exclude_keys=preserve), + data=_relationship_data(data), + ) + + +# Original code from py2neo, licensed under the Apache License 2.0. +def unwind_create_nodes_query(data, labels=None, keys=None): + """Generate a parameterised ``UNWIND...CREATE`` query for bulk + loading nodes into Neo4j. + + Parameters + ---------- + data + labels + keys + + Returns + ------- + (query, parameters) : tuple + """ + return cypher_join( + "UNWIND $data AS r", + _create_clause("_", (tuple(labels or ()),)), + _set_properties_clause("r", keys), + data=list(data), + ) diff --git a/alchemiscale/storage/subgraph.py b/alchemiscale/storage/subgraph.py index 3ca2c58b..fd225ff6 100644 --- a/alchemiscale/storage/subgraph.py +++ b/alchemiscale/storage/subgraph.py @@ -1,7 +1,7 @@ from py2neo import Node, Subgraph, Relationship, UniquenessError from py2neo.cypher import cypher_join -from py2neo.cypher.queries import ( +from alchemiscale.storage.cypher import ( unwind_create_nodes_query, unwind_merge_nodes_query, unwind_merge_relationships_query, @@ -54,6 +54,11 @@ def subgraph_from_path_record(path_record): return Subgraph(path_nodes, path_rels) +# Original code from py2neo, licensed under the Apache License 2.0. +# Modifications: +# - Removed usage of py2neo database connections to instead use +# the official neo4j driver +# - Switched all usage of the id function to elementId def merge_subgraph( transaction: Transaction, subgraph: Subgraph, @@ -90,7 +95,7 @@ def merge_subgraph( "Primary label and primary key are required for MERGE operation" ) pq = unwind_merge_nodes_query(map(dict, nodes), (pl, pk), labels) - pq = cypher_join(pq, "RETURN id(_)") + pq = cypher_join(pq, "RETURN elementId(_)") identities = [record[0] for record in transaction.run(*pq)] if len(identities) > len(nodes): raise UniquenessError( @@ -110,13 +115,18 @@ def merge_subgraph( relationships, ) pq = unwind_merge_relationships_query(data, r_type) - pq = cypher_join(pq, "RETURN id(_)") + pq = cypher_join(pq, "RETURN elementId(_)") for i, record in enumerate(transaction.run(*pq)): relationship = relationships[i] relationship.identity = record[0] +# Original code from py2neo, licensed under the Apache License 2.0. +# Modifications: +# - Removed usage of py2neo database connections to instead use +# the official neo4j driver +# - Switched all usage of the id function to elementId def create_subgraph(transaction, subgraph): """Code adapted from the py2neo Subgraph.__db_create__ method.""" node_dict = {} @@ -131,7 +141,7 @@ def create_subgraph(transaction, subgraph): for labels, nodes in node_dict.items(): pq = unwind_create_nodes_query(list(map(dict, nodes)), labels=labels) - pq = cypher_join(pq, "RETURN id(_)") + pq = cypher_join(pq, "RETURN elementId(_)") records = transaction.run(*pq) for i, record in enumerate(records): node = nodes[i] @@ -143,7 +153,7 @@ def create_subgraph(transaction, subgraph): relationships, ) pq = unwind_merge_relationships_query(data, r_type) - pq = cypher_join(pq, "RETURN id(_)") + pq = cypher_join(pq, "RETURN elementId(_)") for i, record in enumerate(transaction.run(*pq)): relationship = relationships[i] relationship.identity = record[0] From cea758f1a924cff63191ee49184bb00a6fe2c615 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Thu, 7 Nov 2024 14:47:34 -0700 Subject: [PATCH 080/143] Use python < 3.13 for building docs Python v3.13 removed imghdr, which is used by the versions sphinx we use for building our docs. Since we don't officially support 3.13 yet, we can get the docs working by adding this constraint. A better option would be to use newer versions of Sphinx, but that can be done in a different commit. --- devtools/conda-envs/docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/devtools/conda-envs/docs.yml b/devtools/conda-envs/docs.yml index b0718227..2bf1ff51 100644 --- a/devtools/conda-envs/docs.yml +++ b/devtools/conda-envs/docs.yml @@ -3,6 +3,7 @@ channels: - conda-forge dependencies: + - python<3.13 - sphinx>=5.0,<6 - sphinx_rtd_theme - async-lru From c1b9fadbc64a5daaa18b264fa29d1d88dc74ab2b Mon Sep 17 00:00:00 2001 From: "David L. Dotson" Date: Mon, 11 Nov 2024 16:20:33 -0700 Subject: [PATCH 081/143] Update alchemiscale/interface/client.py @ianmkenney suggestion Co-authored-by: Ian Kenney --- alchemiscale/interface/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/interface/client.py b/alchemiscale/interface/client.py index be9414b2..eaba27b7 100644 --- a/alchemiscale/interface/client.py +++ b/alchemiscale/interface/client.py @@ -88,7 +88,7 @@ def get_scoped_key(self, obj: GufeTokenizable, scope: Scope) -> ScopedKey: "Scope for a ScopedKey must be specific; it cannot contain wildcards." ) - def check_exists(self, scoped_key: Union[ScopedKey, str]) -> bool: + def check_exists(self, scoped_key: ScopedKey | str) -> bool: """Returns ``True`` if the given ScopedKey represents an object in the database.""" return self._get_resource(f"/exists/{scoped_key}") From e8a346cc365d19cff23363477541cad0c48fed6a Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Wed, 13 Nov 2024 11:44:15 -0700 Subject: [PATCH 082/143] Replace the alchemiscale KeyedChain with new gufe KeyedChain --- alchemiscale/base/api.py | 3 +- alchemiscale/interface/api.py | 3 +- alchemiscale/interface/client.py | 3 +- alchemiscale/keyedchain.py | 82 ------------------- .../tests/integration/interface/test_api.py | 3 +- alchemiscale/tests/unit/test_keyedchain.py | 45 ---------- 6 files changed, 4 insertions(+), 135 deletions(-) delete mode 100644 alchemiscale/keyedchain.py delete mode 100644 alchemiscale/tests/unit/test_keyedchain.py diff --git a/alchemiscale/base/api.py b/alchemiscale/base/api.py index a44865e3..b8e49417 100644 --- a/alchemiscale/base/api.py +++ b/alchemiscale/base/api.py @@ -14,7 +14,7 @@ from fastapi import status as http_status from fastapi.routing import APIRoute from fastapi.security import OAuth2PasswordRequestForm -from gufe.tokenization import JSON_HANDLER, GufeTokenizable +from gufe.tokenization import JSON_HANDLER, GufeTokenizable, KeyedChain from ..settings import ( JWTSettings, @@ -32,7 +32,6 @@ oauth2_scheme, ) from ..security.models import Token, TokenData, CredentialedEntity -from ..keyedchain import KeyedChain def validate_scopes(scope: Scope, token: TokenData) -> None: diff --git a/alchemiscale/interface/api.py b/alchemiscale/interface/api.py index fef0b2c3..5b6aeb1e 100644 --- a/alchemiscale/interface/api.py +++ b/alchemiscale/interface/api.py @@ -12,7 +12,7 @@ from fastapi.middleware.gzip import GZipMiddleware import json -from gufe.tokenization import JSON_HANDLER +from gufe.tokenization import JSON_HANDLER, KeyedChain from ..base.api import ( GufeJSONResponse, @@ -34,7 +34,6 @@ from ..storage.models import TaskStatusEnum from ..models import Scope, ScopedKey from ..security.models import TokenData, CredentialedUserIdentity -from ..keyedchain import KeyedChain app = FastAPI(title="AlchemiscaleAPI") diff --git a/alchemiscale/interface/client.py b/alchemiscale/interface/client.py index eaba27b7..7bd1311f 100644 --- a/alchemiscale/interface/client.py +++ b/alchemiscale/interface/client.py @@ -13,7 +13,7 @@ from async_lru import alru_cache import networkx as nx from gufe import AlchemicalNetwork, Transformation, ChemicalSystem -from gufe.tokenization import GufeTokenizable, JSON_HANDLER +from gufe.tokenization import GufeTokenizable, JSON_HANDLER, KeyedChain from gufe.protocols import ProtocolResult, ProtocolDAGResult @@ -29,7 +29,6 @@ ) from ..strategies import Strategy from ..validators import validate_network_nonself -from ..keyedchain import KeyedChain from warnings import warn diff --git a/alchemiscale/keyedchain.py b/alchemiscale/keyedchain.py deleted file mode 100644 index e2cf187c..00000000 --- a/alchemiscale/keyedchain.py +++ /dev/null @@ -1,82 +0,0 @@ -from gufe.tokenization import GufeTokenizable, key_decode_dependencies -import networkx as nx -from alchemiscale.utils import gufe_to_digraph - -from typing import List, Tuple, Dict, Generator - - -class KeyedChain(object): - """Keyed chain representation of a GufeTokenizable. - - The keyed chain representation of a GufeTokenizable provides a - topologically sorted list of gufe keys and GufeTokenizable keyed dicts - that can be used to fully recreate a GufeTokenizable without the need for a - populated TOKENIZATION_REGISTRY. - - The class wraps around a list of tuples containing the gufe key and the - keyed dict form of the GufeTokenizable. - - """ - - def __init__(self, keyed_chain): - self._keyed_chain = keyed_chain - - @classmethod - def from_gufe(cls, gufe_object: GufeTokenizable) -> super: - """Initialize a KeyedChain from a GufeTokenizable.""" - return cls(cls.gufe_to_keyed_chain_rep(gufe_object)) - - def to_gufe(self) -> GufeTokenizable: - """Initialize a GufeTokenizable.""" - gts = {} - for gufe_key, keyed_dict in self: - gt = key_decode_dependencies(keyed_dict, registry=gts) - gts[gufe_key] = gt - return gt - - @staticmethod - def gufe_to_keyed_chain_rep( - gufe_object: GufeTokenizable, - ) -> List[Tuple[str, Dict]]: - """Create the keyed chain represenation of a GufeTokenizable. - - This represents the GufeTokenizable as a list of two-element tuples - containing, as their first and second elements, the gufe key and keyed - dict form of the GufeTokenizable, respectively, and provides the - underlying structure used in the KeyedChain class. - - Parameters - ---------- - gufe_object - The GufeTokenizable for which the KeyedChain is generated. - - Returns - ------- - key_and_keyed_dicts - The keyed chain represenation of a GufeTokenizable. - - """ - key_and_keyed_dicts = [ - (str(gt.key), gt.to_keyed_dict()) - for gt in nx.topological_sort(gufe_to_digraph(gufe_object)) - ][::-1] - return key_and_keyed_dicts - - def gufe_keys(self) -> Generator[str, None, None]: - """Create a generator that iterates over the gufe keys in the KeyedChain.""" - for key, _ in self: - yield key - - def keyed_dicts(self) -> Generator[Dict, None, None]: - """Create a generator that iterates over the keyed dicts in the KeyedChain.""" - for _, _dict in self: - yield _dict - - def __len__(self): - return len(self._keyed_chain) - - def __iter__(self): - return self._keyed_chain.__iter__() - - def __getitem__(self, index): - return self._keyed_chain[index] diff --git a/alchemiscale/tests/integration/interface/test_api.py b/alchemiscale/tests/integration/interface/test_api.py index 7814b8c7..6702a99e 100644 --- a/alchemiscale/tests/integration/interface/test_api.py +++ b/alchemiscale/tests/integration/interface/test_api.py @@ -2,10 +2,9 @@ import json from gufe import AlchemicalNetwork, ChemicalSystem, Transformation -from gufe.tokenization import JSON_HANDLER, GufeTokenizable +from gufe.tokenization import JSON_HANDLER, GufeTokenizable, KeyedChain from alchemiscale.models import ScopedKey -from alchemiscale.keyedchain import KeyedChain def pre_load_payload(network, scope, name="incomplete 2"): diff --git a/alchemiscale/tests/unit/test_keyedchain.py b/alchemiscale/tests/unit/test_keyedchain.py deleted file mode 100644 index d272d5b6..00000000 --- a/alchemiscale/tests/unit/test_keyedchain.py +++ /dev/null @@ -1,45 +0,0 @@ -from alchemiscale.keyedchain import KeyedChain -from alchemiscale.utils import RegistryBackup - -from gufe.tokenization import get_all_gufe_objs, GufeTokenizable, is_gufe_key_dict - - -def test_keyedchain_full_network(network): - objects = get_all_gufe_objs(network) - - for o in objects: - with RegistryBackup(): - kc = KeyedChain.from_gufe(o) - _o = kc.to_gufe() - assert o == _o - - -def test_keyedchain_len(network): - objects = get_all_gufe_objs(network) - expect_len = len(objects) - - keyedchain = KeyedChain.from_gufe(network) - - assert len(keyedchain) == expect_len - - -def test_keyedchain_get_keys(network): - keyedchain = KeyedChain.from_gufe(network) - keys = list(map(lambda x: x.key, get_all_gufe_objs(network))) - - assert set(keyedchain.gufe_keys()) == set(keys) - - -def test_keyedchain_get_keyed_dicts(network): - keyedchain = KeyedChain.from_gufe(network) - - for keyed_dict in keyedchain.keyed_dicts(): - assert isinstance(keyed_dict, dict) - - -def test_keyedchain_iteration(network): - keyedchain = KeyedChain.from_gufe(network) - - for key, keyed_dict in keyedchain: - gt = GufeTokenizable.from_keyed_dict(keyed_dict) - assert gt.key == key From 37095e677d7ed117c0b7d44c51650b9b5989f2ff Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 18 Nov 2024 22:32:43 -0700 Subject: [PATCH 083/143] Simplified bcrypt-based password handling --- alchemiscale/security/auth.py | 48 +++++++++++++++-------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index a0a8403b..cf442bb7 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -5,6 +5,8 @@ """ import secrets +import base64 +import hashlib from datetime import datetime, timedelta from typing import Optional, Union @@ -17,46 +19,36 @@ from .models import CredentialedEntity, Token, TokenData MAX_PASSWORD_SIZE = 4096 -_dummy_secret = "dummy" class BcryptPasswordHandler(object): - rounds: int = 12 - ident: str = "$2b$" - salt: str = "" - checksum: str = "" - def __init__(self, rounds: int = 12, ident: str = "$2b$"): + def __init__(self, rounds: int = 12, ident: str = "2b"): self.rounds = rounds self.ident = ident - def _get_config(self) -> bytes: - config = bcrypt.gensalt( - self.rounds, prefix=self.ident.strip("$").encode("ascii") - ) - self.salt = config.decode("ascii")[len(self.ident) + 3 :] - return config - - def to_string(self) -> str: - return "%s%02d$%s%s" % (self.ident, self.rounds, self.salt, self.checksum) - def hash(self, key: str) -> str: validate_secret(key) - config = self._get_config() - hash_ = bcrypt.hashpw(key.encode("utf-8"), config) - if not hash_.startswith(config) or len(hash_) != len(config) + 31: - raise ValueError("bcrypt.hashpw returned an invalid hash") - self.checksum = hash_[-31:].decode("ascii") - return self.to_string() - - def verify(self, key: str, hash: str) -> bool: + + # generate a salt unique to this key + salt = bcrypt.gensalt(rounds=self.rounds, prefix=self.ident.encode("ascii")) + + # bcrypt can handle up to 72 characters + # to go beyond this, we first perform sha256 hashing, + # then base64 encode to avoid NULL byte problems + # details: https://github.com/pyca/bcrypt/?tab=readme-ov-file#maximum-password-length + hashed = base64.b64encode(hashlib.sha256(key.encode("utf-8")).digest()) + hashed_salted = bcrypt.hashpw(hashed, salt) + + return hashed_salted + + def verify(self, key: str, hashed_salted: str) -> bool: validate_secret(key) - if hash is None: - self.hash(_dummy_secret) - return False + # see note above on why we perform sha256 hashing first + key_hashed = base64.b64encode(hashlib.sha256(key.encode("utf-8")).digest()) - return bcrypt.checkpw(key.encode("utf-8"), hash.encode("utf-8")) + return bcrypt.checkpw(key_hashed, hashed_salted.encode("utf-8")) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") From d86eac5784bb7246fbedd934a64af2827dc1e6bb Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 18 Nov 2024 22:41:03 -0700 Subject: [PATCH 084/143] Added note on max password size --- alchemiscale/security/auth.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index cf442bb7..c0b94ade 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -18,7 +18,13 @@ from .models import CredentialedEntity, Token, TokenData -MAX_PASSWORD_SIZE = 4096 + +# we set a max size to avoid denial-of-service attacks +# since an extremely large secret attempted by an attacker can take +# increasing amounts of time or memory to validate; +# this is deliberately higher than any reasonable key length +# this is the same max size that `passlib` defaults to +MAX_SECRET_SIZE = 4096 class BcryptPasswordHandler(object): @@ -59,9 +65,9 @@ def validate_secret(secret): """ensure secret has correct type & size""" if not isinstance(secret, (str, bytes)): raise TypeError("secret must be a string or bytes") - if len(secret) > MAX_PASSWORD_SIZE: + if len(secret) > MAX_SECRET_SIZE: raise ValueError( - f"secret is too long, maximum length is {MAX_PASSWORD_SIZE} characters" + f"secret is too long, maximum length is {MAX_SECRET_SIZE} characters" ) From 994fdafbe63ba88edbd848b361c6318ae982c89e Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 18 Nov 2024 23:02:19 -0700 Subject: [PATCH 085/143] Make sure we return a string from BcryptPasswordHandler.hash --- alchemiscale/security/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index c0b94ade..fdfc7872 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -46,7 +46,7 @@ def hash(self, key: str) -> str: hashed = base64.b64encode(hashlib.sha256(key.encode("utf-8")).digest()) hashed_salted = bcrypt.hashpw(hashed, salt) - return hashed_salted + return hashed_salted.decode('utf-8') def verify(self, key: str, hashed_salted: str) -> bool: validate_secret(key) From 82d8c4bd3c905f19d86c4c5b220525a8cfed92dc Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 18 Nov 2024 23:06:21 -0700 Subject: [PATCH 086/143] Black! --- alchemiscale/security/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index fdfc7872..5e1f8c3b 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -46,7 +46,7 @@ def hash(self, key: str) -> str: hashed = base64.b64encode(hashlib.sha256(key.encode("utf-8")).digest()) hashed_salted = bcrypt.hashpw(hashed, salt) - return hashed_salted.decode('utf-8') + return hashed_salted.decode("utf-8") def verify(self, key: str, hashed_salted: str) -> bool: validate_secret(key) From e2bd4fc86d1ee5644dd977766fae7722cc943e7b Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 18 Nov 2024 23:29:51 -0700 Subject: [PATCH 087/143] Only apply sha256 to passwords longer than 64 characters This reproduces passlib behavior. --- alchemiscale/security/auth.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index 5e1f8c3b..81f8b226 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -43,8 +43,14 @@ def hash(self, key: str) -> str: # to go beyond this, we first perform sha256 hashing, # then base64 encode to avoid NULL byte problems # details: https://github.com/pyca/bcrypt/?tab=readme-ov-file#maximum-password-length - hashed = base64.b64encode(hashlib.sha256(key.encode("utf-8")).digest()) - hashed_salted = bcrypt.hashpw(hashed, salt) + + # to reproduce `passlib` behavior, we only perform sha256 hashing if + # key is longer than the sha256 default block size of 64 + key_ = key.encode("utf-8") + if len(key_) > 64: + key_ = base64.b64encode(hashlib.sha256(key_).digest()) + + hashed_salted = bcrypt.hashpw(key_, salt) return hashed_salted.decode("utf-8") @@ -52,9 +58,13 @@ def verify(self, key: str, hashed_salted: str) -> bool: validate_secret(key) # see note above on why we perform sha256 hashing first - key_hashed = base64.b64encode(hashlib.sha256(key.encode("utf-8")).digest()) + # to reproduce `passlib` behavior, we only perform sha256 hashing if + # key is longer than the sha256 default block size of 64 + key_ = key.encode("utf-8") + if len(key_) > 64: + key_ = base64.b64encode(hashlib.sha256(key_).digest()) - return bcrypt.checkpw(key_hashed, hashed_salted.encode("utf-8")) + return bcrypt.checkpw(key_, hashed_salted.encode("utf-8")) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") From 28791da7f89b4767f20fde4ce9bf9e5a859d8866 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 18 Nov 2024 23:50:18 -0700 Subject: [PATCH 088/143] Realized on closer look that passlib *doesn't* do sha256 under our usage Instead, truncation is what will happen for passwords beyond 72 characters. This is current behavior. --- alchemiscale/security/auth.py | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index 81f8b226..67a16e07 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -38,33 +38,14 @@ def hash(self, key: str) -> str: # generate a salt unique to this key salt = bcrypt.gensalt(rounds=self.rounds, prefix=self.ident.encode("ascii")) - - # bcrypt can handle up to 72 characters - # to go beyond this, we first perform sha256 hashing, - # then base64 encode to avoid NULL byte problems - # details: https://github.com/pyca/bcrypt/?tab=readme-ov-file#maximum-password-length - - # to reproduce `passlib` behavior, we only perform sha256 hashing if - # key is longer than the sha256 default block size of 64 - key_ = key.encode("utf-8") - if len(key_) > 64: - key_ = base64.b64encode(hashlib.sha256(key_).digest()) - - hashed_salted = bcrypt.hashpw(key_, salt) + hashed_salted = bcrypt.hashpw(key.encode("utf-8"), salt) return hashed_salted.decode("utf-8") def verify(self, key: str, hashed_salted: str) -> bool: validate_secret(key) - # see note above on why we perform sha256 hashing first - # to reproduce `passlib` behavior, we only perform sha256 hashing if - # key is longer than the sha256 default block size of 64 - key_ = key.encode("utf-8") - if len(key_) > 64: - key_ = base64.b64encode(hashlib.sha256(key_).digest()) - - return bcrypt.checkpw(key_, hashed_salted.encode("utf-8")) + return bcrypt.checkpw(key.encode("utf-8"), hashed_salted.encode("utf-8")) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") From 0056efa8e730f933a73d6ba0bc0561063a67113b Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 18 Nov 2024 23:51:34 -0700 Subject: [PATCH 089/143] Set max secret size to reflect our use of bcrypt directly, 72 character limit --- alchemiscale/security/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index 67a16e07..cad8db4f 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -24,7 +24,7 @@ # increasing amounts of time or memory to validate; # this is deliberately higher than any reasonable key length # this is the same max size that `passlib` defaults to -MAX_SECRET_SIZE = 4096 +MAX_SECRET_SIZE = 72 class BcryptPasswordHandler(object): From 718a44d4b81d2baf69051ddc0702d4f925a6a024 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Tue, 19 Nov 2024 00:01:14 -0700 Subject: [PATCH 090/143] Put max password size back to avoid surprises for users; added explicit test comparing to passlib behavior If it was truncating passwords before, leaving the same limit in place does the least harm. --- alchemiscale/security/auth.py | 2 +- alchemiscale/tests/unit/test_security.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index cad8db4f..67a16e07 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -24,7 +24,7 @@ # increasing amounts of time or memory to validate; # this is deliberately higher than any reasonable key length # this is the same max size that `passlib` defaults to -MAX_SECRET_SIZE = 72 +MAX_SECRET_SIZE = 4096 class BcryptPasswordHandler(object): diff --git a/alchemiscale/tests/unit/test_security.py b/alchemiscale/tests/unit/test_security.py index 0f120414..11559ed8 100644 --- a/alchemiscale/tests/unit/test_security.py +++ b/alchemiscale/tests/unit/test_security.py @@ -37,3 +37,16 @@ def test_bcrypt_password_handler(): hash_ = handler.hash("test") assert handler.verify("test", hash_) assert not handler.verify("deadbeef", hash_) + + +def test_bcrypt_against_passlib(): + """Test the our bcrypt handler has the same behavior as passlib did""" + + # pre-generated hash from + # `passlib.context.CryptContext(schemes=["bcrypt"], deprecated="auto").hash()` + test_password = "the quick brown fox jumps over the lazy dog" + test_hash = "$2b$12$QZTnjdx/sJS7jnEnCqhM3uS8mZ4mhLV5dDfM7ZBUT6LwDiNZ2p7S." + + # test that we get the same thing back from our bcrypt handler + handler = auth.BcryptPasswordHandler() + assert handler.verify(test_password, test_hash) From 1c106acf1034bdc08e9ec68cbf11aa0a3b92ad03 Mon Sep 17 00:00:00 2001 From: LilDojd Date: Tue, 19 Nov 2024 15:40:16 +0400 Subject: [PATCH 091/143] Forbid NULL byte in secret --- alchemiscale/security/auth.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index 67a16e07..78c4ac0e 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -25,6 +25,10 @@ # this is deliberately higher than any reasonable key length # this is the same max size that `passlib` defaults to MAX_SECRET_SIZE = 4096 +# Bcrypt truncates the secret at first NULL it encounters. For this reason, +# passlib forbids NULL bytes in the secret. This is not necessary for backwards +# compatibility, but we follow passlib's lead. +_BNULL = b"\x00" class BcryptPasswordHandler(object): @@ -52,14 +56,16 @@ def verify(self, key: str, hashed_salted: str) -> bool: pwd_context = BcryptPasswordHandler() -def validate_secret(secret): +def validate_secret(secret: str): """ensure secret has correct type & size""" - if not isinstance(secret, (str, bytes)): - raise TypeError("secret must be a string or bytes") + if not isinstance(secret, str): + raise TypeError("secret must be a string") if len(secret) > MAX_SECRET_SIZE: raise ValueError( f"secret is too long, maximum length is {MAX_SECRET_SIZE} characters" ) + if _BNULL in secret.encode("utf-8"): + raise ValueError("secret contains NULL byte") def generate_secret_key(): From 49182dc38f5ce3d6eea85ee5f63f2e2b243ee2f6 Mon Sep 17 00:00:00 2001 From: George <37330594+LilDojd@users.noreply.github.com> Date: Tue, 19 Nov 2024 22:39:08 +0400 Subject: [PATCH 092/143] Use query parameters wherever possible in Neo4jStore (#330) * Add validation for GufeKey format and characters to prevent Cypher injection Introduce a validator for GufeKeys to ensure they follow the - format. The validator restricts characters to ASCII letters (A-Za-z), digits (0-9), underscores (_), and hyphens (-). * Add tests for GufeKey validation Add tests to verify that GufeKeys are restricted to allowed characters. * Refactor _query method to use Cypher parameters Update `_query()` method in Neo4jStore to use Cypher parameters instead of f-strings, reducing the risk of injection attacks. Also add a test demonstrating how previous versions were vulnerable. --------- Co-authored-by: Ian Kenney --- alchemiscale/models.py | 27 +- alchemiscale/storage/statestore.py | 431 +++++++++--------- .../integration/storage/test_statestore.py | 53 +++ alchemiscale/tests/unit/test_models.py | 34 +- 4 files changed, 332 insertions(+), 213 deletions(-) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index 69fc1288..ed7a6cfb 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -8,6 +8,8 @@ from pydantic import BaseModel, Field, validator, root_validator from gufe.tokenization import GufeKey from re import fullmatch +import unicodedata +import string class Scope(BaseModel): @@ -114,6 +116,9 @@ def specific(self) -> bool: return all(self.to_tuple()) +class InvalidGufeKeyError(ValueError): ... + + class ScopedKey(BaseModel): """Unique identifier for GufeTokenizables in state store. @@ -131,8 +136,26 @@ class Config: frozen = True @validator("gufe_key") - def cast_gufe_key(cls, v): - return GufeKey(v) + def gufe_key_validator(cls, v): + v = str(v) + + # GufeKey is of form - + try: + _prefix, _token = v.split("-") + except ValueError: + raise InvalidGufeKeyError("gufe_key must be of the form '-'") + + # Normalize the input to NFC form + v_normalized = unicodedata.normalize("NFC", v) + + # Allowed characters: letters, numbers, underscores, hyphens + allowed_chars = set(string.ascii_letters + string.digits + "_-") + + if not set(v_normalized).issubset(allowed_chars): + raise InvalidGufeKeyError("gufe_key contains invalid characters") + + # Cast to GufeKey + return GufeKey(v_normalized) def __repr__(self): # pragma: no cover return f"" diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 4e6b9c3c..801b6600 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -9,6 +9,7 @@ from contextlib import contextmanager import json from functools import lru_cache +from operator import ne from typing import Dict, List, Optional, Union, Tuple import weakref import numpy as np @@ -99,43 +100,22 @@ def _select_tasks_from_taskpool(taskpool: List[Tuple[str, float]], count) -> Lis return list(np.random.choice(tasks, count, replace=False, p=prob)) -def _generate_claim_query( - task_sks: List[ScopedKey], compute_service_id: ComputeServiceID -) -> str: - """Generate a query to claim a list of Tasks. - - Parameters - ---------- - task_sks - A list of ScopedKeys of Tasks to claim. - compute_service_id - ComputeServiceID of the claiming service. - - Returns - ------- - query: str - The Cypher query to claim the Task. - """ - - task_data = cypher_list_from_scoped_keys(task_sks) - - query = f""" +CLAIM_QUERY = f""" // only match the task if it doesn't have an existing CLAIMS relationship - UNWIND {task_data} AS task_sk + UNWIND $tasks_list AS task_sk MATCH (t:Task {{_scoped_key: task_sk}}) WHERE NOT (t)<-[:CLAIMS]-(:ComputeServiceRegistration) WITH t // create CLAIMS relationship with given compute service - MATCH (csreg:ComputeServiceRegistration {{identifier: '{compute_service_id}'}}) - CREATE (t)<-[cl:CLAIMS {{claimed: localdatetime('{datetime.utcnow().isoformat()}')}}]-(csreg) + MATCH (csreg:ComputeServiceRegistration {{identifier: $compute_service_id}}) + CREATE (t)<-[cl:CLAIMS {{claimed: localdatetime($datetimestr)}}]-(csreg) SET t.status = '{TaskStatusEnum.running.value}' RETURN t - """ - return query +""" class Neo4jStore(AlchemiscaleStateStore): @@ -468,26 +448,21 @@ def _get_node( ) -> Union[Node, Tuple[Node, Subgraph]]: """ If `return_subgraph = True`, also return subgraph for gufe object. - """ - qualname = scoped_key.qualname - - properties = {"_scoped_key": str(scoped_key)} - prop_string = ", ".join( - "{}: '{}'".format(key, value) for key, value in properties.items() - ) - prop_string = f" {{{prop_string}}}" + # Safety: qualname comes from GufeKey which is validated + qualname = scoped_key.qualname + parameters = {"scoped_key": str(scoped_key)} q = f""" - MATCH (n:{qualname}{prop_string}) + MATCH (n:{qualname} {{ _scoped_key: $scoped_key }}) """ if return_subgraph: q += """ OPTIONAL MATCH p = (n)-[r:DEPENDS_ON*]->(m) WHERE NOT (m)-[:DEPENDS_ON]->() - RETURN n,p + RETURN n, p """ else: q += """ @@ -497,10 +472,12 @@ def _get_node( nodes = set() subgraph = Subgraph() - for record in self.execute_query(q).records: + result = self.execute_query(q, parameters_=parameters) + + for record in result.records: node = record_data_to_node(record["n"]) nodes.add(node) - if return_subgraph and record["p"] is not None: + if return_subgraph and record.get("p") is not None: subgraph = subgraph | subgraph_from_path_record(record["p"]) else: subgraph = node @@ -521,8 +498,8 @@ def _query( self, *, qualname: str, - additional: Dict = None, - key: GufeKey = None, + additional: Optional[Dict] = None, + key: Optional[GufeKey] = None, scope: Scope = Scope(), return_gufe=False, ): @@ -532,9 +509,8 @@ def _query( "_project": scope.project, } - for k, v in list(properties.items()): - if v is None: - properties.pop(k) + # Remove None values from properties + properties = {k: v for k, v in properties.items() if v is not None} if key is not None: properties["_gufe_key"] = str(key) @@ -547,7 +523,7 @@ def _query( prop_string = "" else: prop_string = ", ".join( - "{}: '{}'".format(key, value) for key, value in properties.items() + "{}: ${}".format(key, key) for key in properties.keys() ) prop_string = f" {{{prop_string}}}" @@ -568,7 +544,7 @@ def _query( """ with self.transaction() as tx: - res = tx.run(q).to_eager_result() + res = tx.run(q, **properties).to_eager_result() nodes = list() subgraph = Subgraph() @@ -707,8 +683,8 @@ def delete_network( self.delete_taskhub(network) # then delete the network - q = f""" - MATCH (an:AlchemicalNetwork {{_scoped_key: "{network}"}}) + q = """ + MATCH (an:AlchemicalNetwork {_scoped_key: $network}) DETACH DELETE an """ raise NotImplementedError @@ -848,11 +824,14 @@ def query_networks( *, name=None, key=None, - scope: Optional[Scope] = Scope(), + scope: Optional[Scope] = None, state: Optional[str] = None, ) -> List[ScopedKey]: """Query for `AlchemicalNetwork`\s matching given attributes.""" + if scope is None: + scope = Scope() + query_params = dict( name_pattern=name, org_pattern=scope.org, @@ -916,14 +895,14 @@ def query_chemicalsystems(self, *, name=None, key=None, scope: Scope = Scope()): def get_network_transformations(self, network: ScopedKey) -> List[ScopedKey]: """List ScopedKeys for Transformations associated with the given AlchemicalNetwork.""" - q = f""" - MATCH (:AlchemicalNetwork {{_scoped_key: '{network}'}})-[:DEPENDS_ON]->(t:Transformation|NonTransformation) + q = """ + MATCH (:AlchemicalNetwork {_scoped_key: $network})-[:DEPENDS_ON]->(t:Transformation|NonTransformation) WITH t._scoped_key as sk RETURN sk """ sks = [] with self.transaction() as tx: - res = tx.run(q) + res = tx.run(q, network=str(network)) for rec in res: sks.append(rec["sk"]) @@ -931,14 +910,14 @@ def get_network_transformations(self, network: ScopedKey) -> List[ScopedKey]: def get_transformation_networks(self, transformation: ScopedKey) -> List[ScopedKey]: """List ScopedKeys for AlchemicalNetworks associated with the given Transformation.""" - q = f""" - MATCH (:Transformation|NonTransformation {{_scoped_key: '{transformation}'}})<-[:DEPENDS_ON]-(an:AlchemicalNetwork) + q = """ + MATCH (:Transformation|NonTransformation {_scoped_key: $transformation})<-[:DEPENDS_ON]-(an:AlchemicalNetwork) WITH an._scoped_key as sk RETURN sk """ sks = [] with self.transaction() as tx: - res = tx.run(q) + res = tx.run(q, transformation=str(transformation)) for rec in res: sks.append(rec["sk"]) @@ -946,14 +925,14 @@ def get_transformation_networks(self, transformation: ScopedKey) -> List[ScopedK def get_network_chemicalsystems(self, network: ScopedKey) -> List[ScopedKey]: """List ScopedKeys for ChemicalSystems associated with the given AlchemicalNetwork.""" - q = f""" - MATCH (:AlchemicalNetwork {{_scoped_key: '{network}'}})-[:DEPENDS_ON]->(cs:ChemicalSystem) + q = """ + MATCH (:AlchemicalNetwork {_scoped_key: $network})-[:DEPENDS_ON]->(cs:ChemicalSystem) WITH cs._scoped_key as sk RETURN sk """ sks = [] with self.transaction() as tx: - res = tx.run(q) + res = tx.run(q, network=str(network)) for rec in res: sks.append(rec["sk"]) @@ -961,14 +940,14 @@ def get_network_chemicalsystems(self, network: ScopedKey) -> List[ScopedKey]: def get_chemicalsystem_networks(self, chemicalsystem: ScopedKey) -> List[ScopedKey]: """List ScopedKeys for AlchemicalNetworks associated with the given ChemicalSystem.""" - q = f""" - MATCH (:ChemicalSystem {{_scoped_key: '{chemicalsystem}'}})<-[:DEPENDS_ON]-(an:AlchemicalNetwork) + q = """ + MATCH (:ChemicalSystem {_scoped_key: $chemicalsystem})<-[:DEPENDS_ON]-(an:AlchemicalNetwork) WITH an._scoped_key as sk RETURN sk """ sks = [] with self.transaction() as tx: - res = tx.run(q) + res = tx.run(q, chemicalsystem=str(chemicalsystem)) for rec in res: sks.append(rec["sk"]) @@ -978,14 +957,14 @@ def get_transformation_chemicalsystems( self, transformation: ScopedKey ) -> List[ScopedKey]: """List ScopedKeys for the ChemicalSystems associated with the given Transformation.""" - q = f""" - MATCH (:Transformation|NonTransformation {{_scoped_key: '{transformation}'}})-[:DEPENDS_ON]->(cs:ChemicalSystem) + q = """ + MATCH (:Transformation|NonTransformation {_scoped_key: $transformation})-[:DEPENDS_ON]->(cs:ChemicalSystem) WITH cs._scoped_key as sk RETURN sk """ sks = [] with self.transaction() as tx: - res = tx.run(q) + res = tx.run(q, transformation=str(transformation)) for rec in res: sks.append(rec["sk"]) @@ -995,14 +974,14 @@ def get_chemicalsystem_transformations( self, chemicalsystem: ScopedKey ) -> List[ScopedKey]: """List ScopedKeys for the Transformations associated with the given ChemicalSystem.""" - q = f""" - MATCH (:ChemicalSystem {{_scoped_key: '{chemicalsystem}'}})<-[:DEPENDS_ON]-(t:Transformation|NonTransformation) + q = """ + MATCH (:ChemicalSystem {_scoped_key: $chemicalsystem})<-[:DEPENDS_ON]-(t:Transformation|NonTransformation) WITH t._scoped_key as sk RETURN sk """ sks = [] with self.transaction() as tx: - res = tx.run(q) + res = tx.run(q, chemicalsystem=str(chemicalsystem)) for rec in res: sks.append(rec["sk"]) @@ -1093,10 +1072,10 @@ def deregister_computeservice(self, compute_service_id: ComputeServiceID): """ q = f""" - MATCH (n:ComputeServiceRegistration {{identifier: '{compute_service_id}'}}) + MATCH (n:ComputeServiceRegistration {{identifier: $compute_service_id}}) - OPTIONAL MATCH (n)-[cl:CLAIMS]->(t:Task {{status: 'running'}}) - SET t.status = 'waiting' + OPTIONAL MATCH (n)-[cl:CLAIMS]->(t:Task {{status: '{TaskStatusEnum.running.value}'}}) + SET t.status = '{TaskStatusEnum.waiting.value}' WITH n, n.identifier as identifier @@ -1106,7 +1085,7 @@ def deregister_computeservice(self, compute_service_id: ComputeServiceID): """ with self.transaction() as tx: - res = tx.run(q) + res = tx.run(q, compute_service_id=str(compute_service_id)) identifier = next(res)["identifier"] return ComputeServiceID(identifier) @@ -1117,12 +1096,12 @@ def heartbeat_computeservice( """Update the heartbeat for the given ComputeServiceID.""" q = f""" - MATCH (n:ComputeServiceRegistration {{identifier: '{compute_service_id}'}}) + MATCH (n:ComputeServiceRegistration {{identifier: $compute_service_id}}) SET n.heartbeat = localdatetime('{heartbeat.isoformat()}') """ with self.transaction() as tx: - tx.run(q) + tx.run(q, compute_service_id=str(compute_service_id)) return compute_service_id @@ -1134,8 +1113,8 @@ def expire_registrations(self, expire_time: datetime): WITH n - OPTIONAL MATCH (n)-[cl:CLAIMS]->(t:Task {{status: 'running'}}) - SET t.status = 'waiting' + OPTIONAL MATCH (n)-[cl:CLAIMS]->(t:Task {{status: '{TaskStatusEnum.running.value}'}}) + SET t.status = '{TaskStatusEnum.waiting.value}' WITH n, n.identifier as ident @@ -1221,13 +1200,15 @@ def get_taskhub( "`network` ScopedKey does not correspond to an `AlchemicalNetwork`" ) - q = f""" - match (th:TaskHub {{network: "{network}"}})-[:PERFORMS]->(an:AlchemicalNetwork) - return th - """ + q = """ + MATCH (th:TaskHub {network: $network})-[:PERFORMS]->(an:AlchemicalNetwork) + RETURN th + """ try: - node = record_data_to_node(self.execute_query(q).records[0]["th"]) + node = record_data_to_node( + self.execute_query(q, network=str(network)).records[0]["th"] + ) except IndexError: raise KeyError("No such object in database") @@ -1249,11 +1230,11 @@ def delete_taskhub( taskhub = self.get_taskhub(network) - q = f""" - MATCH (th:TaskHub {{_scoped_key: '{taskhub}'}}), + q = """ + MATCH (th:TaskHub {_scoped_key: $taskhub}) DETACH DELETE th """ - self.execute_query(q) + self.execute_query(q, taskhub=str(taskhub)) return taskhub @@ -1314,14 +1295,14 @@ def get_taskhub_actioned_tasks( A list of dicts, one per TaskHub, which contains the Task ScopedKeys that are actioned on the given TaskHub as keys, with their weights as values. """ - - q = f""" - UNWIND {cypher_list_from_scoped_keys(taskhubs)} as th_sk - MATCH (th: TaskHub {{_scoped_key: th_sk}})-[a:ACTIONS]->(t:Task) + th_scoped_keys = [str(taskhub) for taskhub in taskhubs if taskhub is not None] + q = """ + UNWIND $taskhubs as th_sk + MATCH (th: TaskHub {_scoped_key: th_sk})-[a:ACTIONS]->(t:Task) RETURN t._scoped_key, a.weight, th._scoped_key """ - results = self.execute_query(q) + results = self.execute_query(q, taskhubs=th_scoped_keys) data = {taskhub: {} for taskhub in taskhubs} for record in results.records: @@ -1374,13 +1355,17 @@ def get_taskhub_weight(self, networks: List[ScopedKey]) -> List[float]: "`network` ScopedKey does not correspond to an `AlchemicalNetwork`" ) - q = f""" - UNWIND {cypher_list_from_scoped_keys(networks)} as network - MATCH (th:TaskHub {{network: network}}) + networks_scoped_keys = [ + str(network) for network in networks if network is not None + ] + + q = """ + UNWIND $networks as network + MATCH (th:TaskHub {network: network}) RETURN network, th.weight """ - results = self.execute_query(q) + results = self.execute_query(q, networks=networks_scoped_keys) network_weights = {str(network): None for network in networks} for record in results.records: @@ -1411,10 +1396,12 @@ def action_tasks( # so we can properly return `None` if needed task_map = {str(task): None for task in tasks} + tasks_scoped_keys = [str(task) for task in tasks if task is not None] + q = f""" // get our TaskHub - UNWIND {cypher_list_from_scoped_keys(tasks)} AS task_sk - MATCH (th:TaskHub {{_scoped_key: "{taskhub}"}})-[:PERFORMS]->(an:AlchemicalNetwork) + UNWIND $tasks as task_sk + MATCH (th:TaskHub {{_scoped_key: $taskhub}})-[:PERFORMS]->(an:AlchemicalNetwork) // get the task we want to add to the hub; check that it connects to same network MATCH (task:Task {{_scoped_key: task_sk}})-[:PERFORMS]->(tf:Transformation|NonTransformation)<-[:DEPENDS_ON]-(an) @@ -1423,7 +1410,7 @@ def action_tasks( // and where the task is either in 'waiting', 'running', or 'error' status WITH th, an, task WHERE NOT (th)-[:ACTIONS]->(task) - AND task.status IN ['{TaskStatusEnum.waiting.value}', '{TaskStatusEnum.running.value}', '{TaskStatusEnum.error.value}'] + AND task.status IN ['{TaskStatusEnum.waiting.value}', '{TaskStatusEnum.running.value}', '{TaskStatusEnum.error.value}'] // create the connection CREATE (th)-[ar:ACTIONS {{weight: 0.5}}]->(task) @@ -1434,7 +1421,7 @@ def action_tasks( RETURN task """ - results = self.execute_query(q) + results = self.execute_query(q, tasks=tasks_scoped_keys, taskhub=str(taskhub)) # update our map with the results, leaving None for tasks that aren't found for task_record in results.records: @@ -1496,13 +1483,19 @@ def set_task_weights( if not all([0 <= weight <= 1 for weight in tasks.values()]): raise ValueError("weights must be between 0 and 1 (inclusive)") - for t, w in tasks.items(): - q = f""" - MATCH (th:TaskHub {{_scoped_key: '{taskhub}'}})-[ar:ACTIONS]->(task:Task {{_scoped_key: '{t}'}}) - SET ar.weight = {w} - RETURN task, ar - """ - results.append(tx.run(q).to_eager_result()) + tasks_list = [{"task": str(t), "weight": w} for t, w in tasks.items()] + + q = """ + UNWIND $tasks_list AS item + MATCH (th:TaskHub {_scoped_key: $taskhub})-[ar:ACTIONS]->(task:Task {_scoped_key: item.task}) + SET ar.weight = item.weight + RETURN task, ar + """ + results.append( + tx.run( + q, taskhub=str(taskhub), tasks_list=tasks_list + ).to_eager_result() + ) elif isinstance(tasks, list): if weight is None: @@ -1513,14 +1506,19 @@ def set_task_weights( if not 0 <= weight <= 1: raise ValueError("weight must be between 0 and 1 (inclusive)") - # TODO: remove for loop with an unwind clause - for t in tasks: - q = f""" - MATCH (th:TaskHub {{_scoped_key: '{taskhub}'}})-[ar:ACTIONS]->(task:Task {{_scoped_key: '{t}'}}) - SET ar.weight = {weight} - RETURN task, ar - """ - results.append(tx.run(q).to_eager_result()) + tasks_list = [str(t) for t in tasks] + + q = """ + UNWIND $tasks_list AS task_sk + MATCH (th:TaskHub {_scoped_key: $taskhub})-[ar:ACTIONS]->(task:Task {_scoped_key: task_sk}) + SET ar.weight = $weight + RETURN task, ar + """ + results.append( + tx.run( + q, taskhub=str(taskhub), tasks_list=tasks_list, weight=weight + ).to_eager_result() + ) # return ScopedKeys for Tasks we changed; `None` for tasks we didn't for res in results: @@ -1553,22 +1551,18 @@ def get_task_weights( weights Weights for the list of Tasks, in the same order. """ - weights = [] + with self.transaction() as tx: - for t in tasks: - q = f""" - MATCH (th:TaskHub {{_scoped_key: '{taskhub}'}})-[ar:ACTIONS]->(task:Task {{_scoped_key: '{t}'}}) - RETURN ar.weight - """ - result = tx.run(q) + q = """ + UNWIND $tasks_list AS task_scoped_key + OPTIONAL MATCH (th:TaskHub {_scoped_key: $taskhub})-[ar:ACTIONS]->(task:Task {_scoped_key: task_scoped_key}) + RETURN task_scoped_key, ar.weight AS weight + """ - weight = [record.get("ar.weight") for record in result] + result = tx.run(q, taskhub=str(taskhub), tasks_list=list(map(str, tasks))) + results = result.data() - # if no match for the given Task, we put a `None` as result - if len(weight) == 0: - weights.append(None) - else: - weights.extend(weight) + weights = [record["weight"] for record in results] return weights @@ -1609,13 +1603,13 @@ def get_taskhub_tasks( ) -> Union[List[ScopedKey], Dict[ScopedKey, Task]]: """Get a list of Tasks on the TaskHub.""" - q = f""" + q = """ // get list of all tasks associated with the taskhub - MATCH (th:TaskHub {{_scoped_key: '{taskhub}'}})-[:ACTIONS]->(task:Task) + MATCH (th:TaskHub {_scoped_key: $taskhub})-[:ACTIONS]->(task:Task) RETURN task """ with self.transaction() as tx: - res = tx.run(q).to_eager_result() + res = tx.run(q, taskhub=str(taskhub)).to_eager_result() tasks = [] subgraph = Subgraph() @@ -1636,14 +1630,14 @@ def get_taskhub_unclaimed_tasks( ) -> Union[List[ScopedKey], Dict[ScopedKey, Task]]: """Get a list of unclaimed Tasks in the TaskHub.""" - q = f""" + q = """ // get list of all unclaimed tasks in the hub - MATCH (th:TaskHub {{_scoped_key: '{taskhub}'}})-[:ACTIONS]->(task:Task) + MATCH (th:TaskHub {_scoped_key: $taskhub})-[:ACTIONS]->(task:Task) WHERE NOT (task)<-[:CLAIMS]-(:ComputeServiceRegistration) RETURN task """ with self.transaction() as tx: - res = tx.run(q).to_eager_result() + res = tx.run(q, taskhub=str(taskhub)).to_eager_result() tasks = [] subgraph = Subgraph() @@ -1695,7 +1689,7 @@ def claim_taskhub_tasks( raise ValueError("`protocols` must be either `None` or not empty") q = f""" - MATCH (th:TaskHub {{`_scoped_key`: '{taskhub}'}})-[actions:ACTIONS]-(task:Task) + MATCH (th:TaskHub {{_scoped_key: $taskhub}})-[actions:ACTIONS]-(task:Task) WHERE task.status = '{TaskStatusEnum.waiting.value}' AND actions.weight > 0 OPTIONAL MATCH (task)-[:EXTENDS]->(other_task:Task) @@ -1725,14 +1719,15 @@ def claim_taskhub_tasks( _tasks = {} with self.transaction() as tx: tx.run( - f""" - MATCH (th:TaskHub {{_scoped_key: '{taskhub}'}}) + """ + MATCH (th:TaskHub {_scoped_key: $taskhub}) - // lock the TaskHub to avoid other queries from changing its state while we claim - SET th._lock = True - """ + // lock the TaskHub to avoid other queries from changing its state while we claim + SET th._lock = True + """, + taskhub=str(taskhub), ) - _taskpool = tx.run(q) + _taskpool = tx.run(q, taskhub=str(taskhub)) def task_count(task_dict: dict): return sum(map(len, task_dict.values())) @@ -1797,16 +1792,21 @@ def task_count(task_dict: dict): # if tasks is not empty, proceed with claiming if tasks: - q = _generate_claim_query(tasks, compute_service_id) - tx.run(q) + tx.run( + CLAIM_QUERY, + tasks_list=[str(task) for task in tasks if task is not None], + datetimestr=str(datetime.utcnow().isoformat()), + compute_service_id=str(compute_service_id), + ) tx.run( - f""" - MATCH (th:TaskHub {{_scoped_key: '{taskhub}'}}) + """ + MATCH (th:TaskHub {_scoped_key: $taskhub}) - // remove lock on the TaskHub now that we're done with it - SET th._lock = null - """ + // remove lock on the TaskHub now that we're done with it + SET th._lock = null + """, + taskhub=str(taskhub), ) return tasks + [None] * (count - len(tasks)) @@ -1818,13 +1818,13 @@ def _validate_extends_tasks(self, task_list) -> Dict[str, Tuple[Node, str]]: if not task_list: return {} - q = f""" - UNWIND {cypher_list_from_scoped_keys(task_list)} as task - MATCH (t:Task {{`_scoped_key`: task}})-[PERFORMS]->(tf:Transformation|NonTransformation) + q = """ + UNWIND $task_list AS task + MATCH (t:Task {_scoped_key: task})-[PERFORMS]->(tf:Transformation|NonTransformation) return t, tf._scoped_key as tf_sk """ - results = self.execute_query(q) + results = self.execute_query(q, task_list=list(map(str, task_list))) nodes = {} @@ -1919,12 +1919,14 @@ def create_tasks( continue q = f""" - UNWIND {cypher_list_from_scoped_keys(transformation_subset)} as sk + UNWIND $transformation_subset AS sk MATCH (n:{node_type} {{`_scoped_key`: sk}}) RETURN n """ - results = self.execute_query(q) + results = self.execute_query( + q, transformation_subset=list(map(str, transformation_subset)) + ) transformation_nodes = {} for record in results.records: @@ -2007,14 +2009,14 @@ def get_network_tasks( self, network: ScopedKey, status: Optional[TaskStatusEnum] = None ) -> List[ScopedKey]: """List ScopedKeys for all Tasks associated with the given AlchemicalNetwork.""" - q = f""" - MATCH (an:AlchemicalNetwork {{_scoped_key: "{network}"}})-[:DEPENDS_ON]->(tf:Transformation|NonTransformation), + q = """ + MATCH (an:AlchemicalNetwork {_scoped_key: $network})-[:DEPENDS_ON]->(tf:Transformation|NonTransformation), (tf)<-[:PERFORMS]-(t:Task) """ if status is not None: - q += f""" - WHERE t.status = '{status.value}' + q += """ + WHERE t.status = $status """ q += """ @@ -2023,7 +2025,9 @@ def get_network_tasks( """ sks = [] with self.transaction() as tx: - res = tx.run(q) + res = tx.run( + q, network=str(network), status=status.value if status else None + ) for rec in res: sks.append(rec["sk"]) @@ -2031,15 +2035,15 @@ def get_network_tasks( def get_task_networks(self, task: ScopedKey) -> List[ScopedKey]: """List ScopedKeys for AlchemicalNetworks associated with the given Task.""" - q = f""" - MATCH (t:Task {{_scoped_key: '{task}'}})-[:PERFORMS]->(tf:Transformation|NonTransformation), + q = """ + MATCH (t:Task {_scoped_key: $task})-[:PERFORMS]->(tf:Transformation|NonTransformation), (tf)<-[:DEPENDS_ON]-(an:AlchemicalNetwork) WITH an._scoped_key as sk RETURN sk """ sks = [] with self.transaction() as tx: - res = tx.run(q) + res = tx.run(q, task=str(task)) for rec in res: sks.append(rec["sk"]) @@ -2068,18 +2072,18 @@ def get_transformation_tasks( extends """ - q = f""" - MATCH (trans:Transformation|NonTransformation {{_scoped_key: '{transformation}'}})<-[:PERFORMS]-(task:Task) + q = """ + MATCH (trans:Transformation|NonTransformation {_scoped_key: $transformation})<-[:PERFORMS]-(task:Task) """ if status is not None: - q += f""" - WHERE task.status = '{status.value}' + q += """ + WHERE task.status = $status """ if extends: - q += f""" - MATCH (trans)<-[:PERFORMS]-(extends:Task {{_scoped_key: '{extends}'}}) + q += """ + MATCH (trans)<-[:PERFORMS]-(extends:Task {_scoped_key: $extends}) WHERE (task)-[:EXTENDS*]->(extends) RETURN task """ @@ -2089,7 +2093,12 @@ def get_transformation_tasks( """ with self.transaction() as tx: - res = tx.run(q).to_eager_result() + res = tx.run( + q, + transformation=str(transformation), + status=status.value if status else None, + extends=str(extends) if extends else None, + ).to_eager_result() tasks = [] for record in res.records: @@ -2123,14 +2132,14 @@ def get_task_transformation( `ScopedKey`\s for these instead. """ - q = f""" - MATCH (task:Task {{_scoped_key: "{task}"}})-[:PERFORMS]->(trans:Transformation|NonTransformation) + q = """ + MATCH (task:Task {_scoped_key: $task})-[:PERFORMS]->(trans:Transformation|NonTransformation) OPTIONAL MATCH (task)-[:EXTENDS]->(prev:Task)-[:RESULTS_IN]->(result:ProtocolDAGResultRef) RETURN trans, result """ with self.transaction() as tx: - res = tx.run(q).to_eager_result() + res = tx.run(q, task=str(task)).to_eager_result() transformations = [] results = [] @@ -2229,7 +2238,9 @@ def set_task_priority( RETURN scoped_key, t """ res = tx.run( - q, scoped_keys=[str(t) for t in tasks], priority=priority + q, + scoped_keys=list(map(str, tasks)), + priority=priority, ).to_eager_result() task_results = [] @@ -2266,7 +2277,7 @@ def get_task_priority(self, tasks: List[ScopedKey]) -> List[Optional[int]]: WHERE t._scoped_key = scoped_key RETURN t.priority as priority """ - res = tx.run(q, scoped_keys=[str(t) for t in tasks]) + res = tx.run(q, scoped_keys=list(map(str, tasks))) priorities = [rec["priority"] for rec in res] return priorities @@ -2308,7 +2319,7 @@ def get_scope_status( } prop_string = ", ".join( - "{}: '{}'".format(key, value) + "{}: ${}".format(key, key) for key, value in properties.items() if value is not None ) @@ -2325,22 +2336,22 @@ def get_scope_status( RETURN n.status AS status, count(DISTINCT n) as counts """ with self.transaction() as tx: - res = tx.run(q, state_pattern=network_state) + res = tx.run(q, state_pattern=network_state, **properties) counts = {rec["status"]: rec["counts"] for rec in res} return counts def get_network_status(self, networks: List[ScopedKey]) -> List[Dict[str, int]]: """Return status counts for all Tasks associated with the given AlchemicalNetworks.""" - q = f""" - UNWIND {cypher_list_from_scoped_keys(networks)} as network - MATCH (an:AlchemicalNetwork {{_scoped_key: network}})-[:DEPENDS_ON]->(tf:Transformation|NonTransformation), + q = """ + UNWIND $networks AS network + MATCH (an:AlchemicalNetwork {_scoped_key: network})-[:DEPENDS_ON]->(tf:Transformation|NonTransformation), (tf)<-[:PERFORMS]-(t:Task) RETURN an._scoped_key AS sk, t.status AS status, count(t) as counts """ network_data = {str(network_sk): {} for network_sk in networks} - for rec in self.execute_query(q).records: + for rec in self.execute_query(q, networks=list(map(str, networks))).records: sk = rec["sk"] status = rec["status"] counts = rec["counts"] @@ -2350,12 +2361,12 @@ def get_network_status(self, networks: List[ScopedKey]) -> List[Dict[str, int]]: def get_transformation_status(self, transformation: ScopedKey) -> Dict[str, int]: """Return status counts for all Tasks associated with the given Transformation.""" - q = f""" - MATCH (:Transformation|NonTransformation {{_scoped_key: "{transformation}"}})<-[:PERFORMS]-(t:Task) + q = """ + MATCH (:Transformation|NonTransformation {_scoped_key: $transformation})<-[:PERFORMS]-(t:Task) RETURN t.status AS status, count(t) as counts """ with self.transaction() as tx: - res = tx.run(q) + res = tx.run(q, transformation=str(transformation)) counts = {rec["status"]: rec["counts"] for rec in res} return counts @@ -2507,15 +2518,15 @@ def set_task_waiting( """ - q = """ + q = f""" WITH $scoped_keys AS batch UNWIND batch AS scoped_key - OPTIONAL MATCH (t:Task {_scoped_key: scoped_key}) + OPTIONAL MATCH (t:Task {{_scoped_key: scoped_key}}) - OPTIONAL MATCH (t_:Task {_scoped_key: scoped_key}) - WHERE t_.status IN ['waiting', 'running', 'error'] - SET t_.status = 'waiting' + OPTIONAL MATCH (t_:Task {{_scoped_key: scoped_key}}) + WHERE t_.status IN ['{TaskStatusEnum.waiting.value}', '{TaskStatusEnum.running.value}', '{TaskStatusEnum.error.value}'] + SET t_.status = '{TaskStatusEnum.waiting.value}' WITH scoped_key, t, t_ @@ -2541,15 +2552,15 @@ def set_task_running( """ - q = """ + q = f""" WITH $scoped_keys AS batch UNWIND batch AS scoped_key - OPTIONAL MATCH (t:Task {_scoped_key: scoped_key}) + OPTIONAL MATCH (t:Task {{_scoped_key: scoped_key}}) - OPTIONAL MATCH (t_:Task {_scoped_key: scoped_key}) - WHERE t_.status IN ['running', 'waiting'] - SET t_.status = 'running' + OPTIONAL MATCH (t_:Task {{_scoped_key: scoped_key}}) + WHERE t_.status IN ['{TaskStatusEnum.running.value}', '{TaskStatusEnum.waiting.value}'] + SET t_.status = '{TaskStatusEnum.running.value}' RETURN scoped_key, t, t_ """ @@ -2568,15 +2579,15 @@ def set_task_complete( """ - q = """ + q = f""" WITH $scoped_keys AS batch UNWIND batch AS scoped_key - OPTIONAL MATCH (t:Task {_scoped_key: scoped_key}) + OPTIONAL MATCH (t:Task {{_scoped_key: scoped_key}}) - OPTIONAL MATCH (t_:Task {_scoped_key: scoped_key}) - WHERE t_.status IN ['complete', 'running'] - SET t_.status = 'complete' + OPTIONAL MATCH (t_:Task {{_scoped_key: scoped_key}}) + WHERE t_.status IN ['{TaskStatusEnum.complete.value}', '{TaskStatusEnum.running.value}'] + SET t_.status = '{TaskStatusEnum.complete.value}' WITH scoped_key, t, t_ @@ -2609,15 +2620,15 @@ def set_task_error( """ - q = """ + q = f""" WITH $scoped_keys AS batch UNWIND batch AS scoped_key - OPTIONAL MATCH (t:Task {_scoped_key: scoped_key}) + OPTIONAL MATCH (t:Task {{_scoped_key: scoped_key}}) - OPTIONAL MATCH (t_:Task {_scoped_key: scoped_key}) - WHERE t_.status IN ['error', 'running'] - SET t_.status = 'error' + OPTIONAL MATCH (t_:Task {{_scoped_key: scoped_key}}) + WHERE t_.status IN ['{TaskStatusEnum.error.value}', '{TaskStatusEnum.running.value}'] + SET t_.status = '{TaskStatusEnum.error.value}' WITH scoped_key, t, t_ @@ -2647,20 +2658,20 @@ def set_task_invalid( # set the status and delete the ACTIONS relationship # make sure we follow the extends chain and set all tasks to invalid # and remove actions relationships - q = """ + q = f""" WITH $scoped_keys AS batch UNWIND batch AS scoped_key - OPTIONAL MATCH (t:Task {_scoped_key: scoped_key}) + OPTIONAL MATCH (t:Task {{_scoped_key: scoped_key}}) - OPTIONAL MATCH (t_:Task {_scoped_key: scoped_key}) - WHERE NOT t_.status IN ['deleted'] - SET t_.status = 'invalid' + OPTIONAL MATCH (t_:Task {{_scoped_key: scoped_key}}) + WHERE NOT t_.status IN ['{TaskStatusEnum.deleted.value}'] + SET t_.status = '{TaskStatusEnum.invalid.value}' WITH scoped_key, t, t_ OPTIONAL MATCH (t_)<-[er:EXTENDS*]-(extends_task:Task) - SET extends_task.status = 'invalid' + SET extends_task.status = '{TaskStatusEnum.invalid.value}' WITH scoped_key, t, t_, extends_task @@ -2697,20 +2708,20 @@ def set_task_deleted( # set the status and delete the ACTIONS relationship # make sure we follow the extends chain and set all tasks to deleted # and remove actions relationships - q = """ + q = f""" WITH $scoped_keys AS batch UNWIND batch AS scoped_key - OPTIONAL MATCH (t:Task {_scoped_key: scoped_key}) + OPTIONAL MATCH (t:Task {{_scoped_key: scoped_key}}) - OPTIONAL MATCH (t_:Task {_scoped_key: scoped_key}) - WHERE NOT t_.status IN ['invalid'] - SET t_.status = 'deleted' + OPTIONAL MATCH (t_:Task {{_scoped_key: scoped_key}}) + WHERE NOT t_.status IN ['{TaskStatusEnum.invalid.value}'] + SET t_.status = '{TaskStatusEnum.deleted.value}' WITH scoped_key, t, t_ OPTIONAL MATCH (t_)<-[er:EXTENDS*]-(extends_task:Task) - SET extends_task.status = 'deleted' + SET extends_task.status = '{TaskStatusEnum.deleted.value}' WITH scoped_key, t, t_, extends_task diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 3ec702a6..f2f25ef5 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -271,6 +271,59 @@ def test_query_transformations(self, n4js, network_tyk2, multiple_scopes): == 1 ) + def test_query_transformations_exploit(self, n4js, multiple_scopes, network_tyk2): + # This test is to show that common cypher exploits are mitigated by using parameters + + an = network_tyk2 + + n4js.assemble_network(an, multiple_scopes[0]) + n4js.assemble_network(an, multiple_scopes[1]) + + malicious_name = """'}) + WITH {_org: '', _campaign: '', _project: '', _gufe_key: ''} AS n + RETURN n + UNION + MATCH (m) DETACH DELETE m + WITH {_org: '', _campaign: '', _project: '', _gufe_key: ''} AS n + RETURN n + UNION + CREATE (mark:InjectionMark {_scoped_key: 'InjectionMark-12345-test-testcamp-testproj'}) + WITH {_org: '', _campaign: '', _project: '', _gufe_key: ''} AS n // """ + try: + n4js.query_transformations(name=malicious_name) + except AttributeError as e: + # With old _query, AttributeError would be thrown AFTER the transaction has finished, and the database is already corrupted + assert "'dict' object has no attribute 'labels'" in str(e) + assert len(n4js.query_transformations(scope=multiple_scopes[0])) == 0 + + mark_from__query = n4js._query(qualname="InjectionMark") + # Just to be double sure, check explicitly + q = """ + match (m:InjectionMark) + return m + """ + mark_explicit = n4js.execute_query(q).records + + assert len(mark_from__query) == len(mark_explicit) == 0 + + assert len(n4js.query_transformations()) == len(network_tyk2.edges) * 2 + assert len(n4js.query_transformations(scope=multiple_scopes[0])) == len( + network_tyk2.edges + ) + + assert ( + len(n4js.query_transformations(name="lig_ejm_31_to_lig_ejm_50_complex")) + == 2 + ) + assert ( + len( + n4js.query_transformations( + scope=multiple_scopes[0], name="lig_ejm_31_to_lig_ejm_50_complex" + ) + ) + == 1 + ) + def test_query_chemicalsystems(self, n4js, network_tyk2, multiple_scopes): an = network_tyk2 diff --git a/alchemiscale/tests/unit/test_models.py b/alchemiscale/tests/unit/test_models.py index c8285fbf..ba7fc389 100644 --- a/alchemiscale/tests/unit/test_models.py +++ b/alchemiscale/tests/unit/test_models.py @@ -2,7 +2,7 @@ from pydantic import ValidationError -from alchemiscale.models import Scope +from alchemiscale.models import Scope, ScopedKey @pytest.mark.parametrize( @@ -101,3 +101,35 @@ def test_scope_non_alphanumeric_invalid(scope_string): ) def test_underscore_scopes_valid(scope_string): scope = Scope.from_str(scope_string) + + +@pytest.mark.parametrize( + "gufe_key", + [ + "White Space-token", + "WhiteSpace-tok en", + "NoToken", + "Unicode-\u0027MATCH", + "CredentialedEntity) DETACH DELETE n //", + "BadPrefix-token`backtick", + ], +) +def test_gufe_key_invalid(gufe_key): + with pytest.raises(ValidationError): + ScopedKey( + gufe_key=gufe_key, org="org1", campaign="campaignA", project="projectI" + ) + + +@pytest.mark.parametrize( + "gufe_key", + [ + "ClassName-uuid4hex", + "DummyProtocol-1234567890abcdef", + "DummyProtocol-1234567890abcdef41234567890abcdef", + ], +) +def test_gufe_key_valid(gufe_key): + scoped_key = ScopedKey( + gufe_key=gufe_key, org="org1", campaign="campaignA", project="projectI" + ) From 45c84181713c4b5d608559f990ba7aaa1b7664c5 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Tue, 19 Nov 2024 19:57:48 -0700 Subject: [PATCH 093/143] Added long test for bcrypt --- alchemiscale/tests/unit/test_security.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/alchemiscale/tests/unit/test_security.py b/alchemiscale/tests/unit/test_security.py index 11559ed8..8e7050ca 100644 --- a/alchemiscale/tests/unit/test_security.py +++ b/alchemiscale/tests/unit/test_security.py @@ -50,3 +50,16 @@ def test_bcrypt_against_passlib(): # test that we get the same thing back from our bcrypt handler handler = auth.BcryptPasswordHandler() assert handler.verify(test_password, test_hash) + + +def test_bcrypt_against_passlib_long(): + """Test the our bcrypt handler has the same behavior as passlib did for passwords longer than 72 characters, in which bcrypt truncates""" + + # pre-generated hash from + # `passlib.context.CryptContext(schemes=["bcrypt"], deprecated="auto").hash()` + test_password = "this password is so long, it's longer than 72 characters; this should get truncated by bcrypt, so we can ensure we get the same verification behavior as passlib gives" + test_hash = "$2b$12$DQd5IPjlc8z4FZjBIdaquOlVc9whAqnkpZRsnuUUWvfHvwWy.dZ16" + + # test that we get the same thing back from our bcrypt handler + handler = auth.BcryptPasswordHandler() + assert handler.verify(test_password, test_hash) From b55bf524239654d01c4822843968bdf3e7aacd48 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 20 Nov 2024 12:35:40 -0700 Subject: [PATCH 094/143] Updated environment stacks --- devtools/conda-envs/alchemiscale-client.yml | 11 ++++------- devtools/conda-envs/alchemiscale-compute.yml | 7 ++++--- devtools/conda-envs/alchemiscale-server.yml | 11 ++++------- devtools/conda-envs/test.yml | 4 ++-- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/devtools/conda-envs/alchemiscale-client.yml b/devtools/conda-envs/alchemiscale-client.yml index fc462e9c..5c3ccf4d 100644 --- a/devtools/conda-envs/alchemiscale-client.yml +++ b/devtools/conda-envs/alchemiscale-client.yml @@ -8,8 +8,8 @@ dependencies: - python=3.12 # alchemiscale dependencies - - gufe=1.0.0 - - openfe=1.1.0 + - gufe=1.1.0 + - openfe=1.2.0 - requests - click - httpx @@ -26,10 +26,7 @@ dependencies: # additional pins - openmm=8.1.2 - openmmforcefields>=0.14.1 - - # alchemiscale-fah dependencies - - cryptography - - plyvel + - openff-units=0.2.2 - pip: - - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.1 + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.2-release diff --git a/devtools/conda-envs/alchemiscale-compute.yml b/devtools/conda-envs/alchemiscale-compute.yml index eb4dc7b7..36aa5f20 100644 --- a/devtools/conda-envs/alchemiscale-compute.yml +++ b/devtools/conda-envs/alchemiscale-compute.yml @@ -8,8 +8,8 @@ dependencies: - cudatoolkit =11.8 # alchemiscale dependencies - - gufe=1.0.0 - - openfe=1.1.0 + - gufe=1.1.0 + - openfe=1.2.0 - requests - click - httpx @@ -22,6 +22,7 @@ dependencies: # additional pins - openmm=8.1.2 - openmmforcefields>=0.14.1 + - openff-units=0.2.2 - pip: - - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.1 + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.2-release diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index 462361c1..313ea0f7 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -8,8 +8,8 @@ dependencies: - python=3.12 # alchemiscale dependencies - - gufe=1.0.0 - - openfe=1.1.0 + - gufe=1.1.0 + - openfe=1.2.0 - requests - click @@ -30,7 +30,6 @@ dependencies: - gunicorn - python-jose - bcrypt - - python-multipart=0.0.12 # temporarily pinned due to broken 0.14 release on conda-forge - starlette - httpx - cryptography @@ -41,12 +40,10 @@ dependencies: # additional pins - openmm=8.1.2 - openmmforcefields>=0.14.1 + - openff-units=0.2.2 # deployment - curl # used in healthchecks for API services - # alchemiscale-fah dependencies - - plyvel - - pip: - - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.1 + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.2-release diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index 37021327..fc447422 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -7,8 +7,8 @@ dependencies: - pip # alchemiscale dependencies - - gufe>=1.0.0 - - openfe>=1.1.0 + - gufe>=1.1.0 + - openfe>=1.2.0 - pydantic<2.0 - async-lru From 619f78d2f229c40eb7160b47741be86a684bef03 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 20 Nov 2024 22:09:04 -0700 Subject: [PATCH 095/143] Update prod envs to point to v0.5.2 tag --- devtools/conda-envs/alchemiscale-client.yml | 2 +- devtools/conda-envs/alchemiscale-compute.yml | 2 +- devtools/conda-envs/alchemiscale-server.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/devtools/conda-envs/alchemiscale-client.yml b/devtools/conda-envs/alchemiscale-client.yml index 5c3ccf4d..6f2ae9be 100644 --- a/devtools/conda-envs/alchemiscale-client.yml +++ b/devtools/conda-envs/alchemiscale-client.yml @@ -29,4 +29,4 @@ dependencies: - openff-units=0.2.2 - pip: - - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.2-release + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.2 diff --git a/devtools/conda-envs/alchemiscale-compute.yml b/devtools/conda-envs/alchemiscale-compute.yml index 36aa5f20..f93cd1f3 100644 --- a/devtools/conda-envs/alchemiscale-compute.yml +++ b/devtools/conda-envs/alchemiscale-compute.yml @@ -25,4 +25,4 @@ dependencies: - openff-units=0.2.2 - pip: - - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.2-release + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.2 diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index 313ea0f7..ae871cca 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -46,4 +46,4 @@ dependencies: - curl # used in healthchecks for API services - pip: - - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.2-release + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.2 From 27a00e6aa2a898afdef7e937679ca44b81affaca Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 20 Nov 2024 22:09:31 -0700 Subject: [PATCH 096/143] Update user guide to point to v0.5.2 tag --- docs/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 81554dbf..da88b10f 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -16,7 +16,7 @@ Clone alchemiscale from Github, and switch to the latest release tag:: $ git clone https://github.com/OpenFreeEnergy/alchemiscale.git $ cd alchemiscale - $ git checkout v0.5.1 + $ git checkout v0.5.2 Create a conda environment using, e.g. `micromamba`_:: From cec1538e7aff4832d85795efda841699d25db610 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 20 Nov 2024 22:43:29 -0700 Subject: [PATCH 097/143] Updated description in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 50660982..acb4f7b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "alchemiscale" -description = "" +description = "a high-throughput alchemical free energy execution system for use with HPC, cloud, bare metal, and Folding@Home" readme = "README.md" authors = [{name = "OpenFE and OpenFF developers"}] license = {text = "MIT"} From 67f7964cdd60ffe15965b2ac89d310b233178c16 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 25 Nov 2024 12:35:29 -0700 Subject: [PATCH 098/143] Add zstd compression to set_task_result in compute service - Update env files to include zstandard - Update set_task_result in compute api and client to handle base64 encoded data. Rather than JSON serialize the ProtocolDAGResult (PDR) and use this is a the intermediate format, instead: 1) create a keyed chain representation of the PDR 2) JSON serialize this representation 3) compress the utf-8 encoded bytes with zstandard 4) encode with base64 - Use the above base64 encoded data as the intermediate format and reverse the operations above to recover the PDR. --- alchemiscale/compute/api.py | 14 +++++++++++--- alchemiscale/compute/client.py | 19 +++++++++++++++---- devtools/conda-envs/alchemiscale-client.yml | 1 + devtools/conda-envs/alchemiscale-compute.yml | 1 + devtools/conda-envs/alchemiscale-server.yml | 1 + devtools/conda-envs/test.yml | 1 + 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/alchemiscale/compute/api.py b/alchemiscale/compute/api.py index 9337055b..a75854a2 100644 --- a/alchemiscale/compute/api.py +++ b/alchemiscale/compute/api.py @@ -9,10 +9,12 @@ import json from datetime import datetime, timedelta import random +import base64 from fastapi import FastAPI, APIRouter, Body, Depends from fastapi.middleware.gzip import GZipMiddleware -from gufe.tokenization import GufeTokenizable, JSON_HANDLER +from gufe.tokenization import GufeTokenizable, JSON_HANDLER, KeyedChain +import zstandard as zstd from ..base.api import ( QueryGUFEHandler, @@ -328,8 +330,14 @@ def set_task_result( task_sk = ScopedKey.from_str(task_scoped_key) validate_scopes(task_sk.scope, token) - pdr = json.loads(protocoldagresult, cls=JSON_HANDLER.decoder) - pdr = GufeTokenizable.from_dict(pdr) + # decode b64 and decompress the zstd bytes back into json + protocoldagresult = base64.b64decode(protocoldagresult) + decompressor = zstd.ZstdDecompressor() + protocoldagresult = decompressor.decompress(protocoldagresult) + + pdr_keyed_chain_rep = json.loads(protocoldagresult, cls=JSON_HANDLER.decoder) + pdr_keyed_chain = KeyedChain.from_keyed_chain_rep(pdr_keyed_chain_rep) + pdr = pdr_keyed_chain.to_gufe() tf_sk, _ = n4js.get_task_transformation( task=task_scoped_key, diff --git a/alchemiscale/compute/client.py b/alchemiscale/compute/client.py index 901a7516..b703459b 100644 --- a/alchemiscale/compute/client.py +++ b/alchemiscale/compute/client.py @@ -9,11 +9,14 @@ import json from urllib.parse import urljoin from functools import wraps +import base64 import requests from requests.auth import HTTPBasicAuth -from gufe.tokenization import GufeTokenizable, JSON_HANDLER +import zstandard as zstd + +from gufe.tokenization import GufeTokenizable, JSON_HANDLER, KeyedChain from gufe import Transformation from gufe.protocols import ProtocolDAGResult @@ -128,10 +131,18 @@ def set_task_result( protocoldagresult: ProtocolDAGResult, compute_service_id=Optional[ComputeServiceID], ) -> ScopedKey: + + keyed_chain_rep = KeyedChain.from_gufe(protocoldagresult).to_keyed_chain_rep() + json_rep = json.dumps(keyed_chain_rep, cls=JSON_HANDLER.encoder) + json_bytes = json_rep.encode("utf-8") + + compressor = zstd.ZstdCompressor() + compressed = compressor.compress(json_bytes) + + base64_encoded = base64.b64encode(compressed).decode("utf-8") + data = dict( - protocoldagresult=json.dumps( - protocoldagresult.to_dict(), cls=JSON_HANDLER.encoder - ), + protocoldagresult=base64_encoded, compute_service_id=str(compute_service_id), ) diff --git a/devtools/conda-envs/alchemiscale-client.yml b/devtools/conda-envs/alchemiscale-client.yml index 6f2ae9be..81cfd63f 100644 --- a/devtools/conda-envs/alchemiscale-client.yml +++ b/devtools/conda-envs/alchemiscale-client.yml @@ -15,6 +15,7 @@ dependencies: - httpx - pydantic<2.0 - async-lru + - zstandard ## user client - rich diff --git a/devtools/conda-envs/alchemiscale-compute.yml b/devtools/conda-envs/alchemiscale-compute.yml index f93cd1f3..cd39cce2 100644 --- a/devtools/conda-envs/alchemiscale-compute.yml +++ b/devtools/conda-envs/alchemiscale-compute.yml @@ -15,6 +15,7 @@ dependencies: - httpx - pydantic<2.0 - async-lru + - zstandard # openmm protocols - feflow=0.1.0 diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index ae871cca..00102ab5 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -10,6 +10,7 @@ dependencies: # alchemiscale dependencies - gufe=1.1.0 - openfe=1.2.0 + - zstandard - requests - click diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index fc447422..d55ac617 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -11,6 +11,7 @@ dependencies: - openfe>=1.2.0 - pydantic<2.0 - async-lru + - zstandard ## state store - neo4j-python-driver From 9cb9ce5069d9973d135fab2f6af4c37fba8f5550 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Fri, 29 Nov 2024 19:32:00 -0700 Subject: [PATCH 099/143] `SynchronousComputeService` now properly claims tasks with protocols filter We weren't actually using the `protocols` setting for the `SynchronousComputeService`, so `Task` claims weren't being filtered if this was set to anything but `None`. --- alchemiscale/compute/service.py | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/alchemiscale/compute/service.py b/alchemiscale/compute/service.py index 2955555d..31b4a922 100644 --- a/alchemiscale/compute/service.py +++ b/alchemiscale/compute/service.py @@ -157,32 +157,6 @@ def heartbeat(self): self.beat() time.sleep(self.heartbeat_interval) - def claim_tasks( - self, count=1, protocols: Optional[List[str]] = None - ) -> List[Optional[ScopedKey]]: - """Get a Task to execute from compute API. - - Returns `None` if no Task was available matching service configuration. - - Parameters - ---------- - count - The maximum number of Tasks to claim. - protocols - Protocol names to restrict Task claiming to. `None` means no restriction. - Regex patterns are allowed. - - """ - - tasks = self.client.claim_tasks( - scopes=self.scopes, - compute_service_id=self.compute_service_id, - count=count, - protocols=protocols, - ) - - return tasks - def task_to_protocoldag( self, task: ScopedKey ) -> Tuple[ProtocolDAG, Transformation, Optional[ProtocolDAGResult]]: @@ -306,7 +280,12 @@ def cycle(self, max_tasks: Optional[int] = None, max_time: Optional[int] = None) # claim tasks from the compute API self.logger.info("Claiming tasks") - tasks: List[ScopedKey] = self.claim_tasks(self.claim_limit) + tasks: List[ScopedKey] = self.client.claim_tasks( + scopes=self.scopes, + compute_service_id=self.compute_service_id, + count=self.claim_limit, + protocols=self.settings.protocols, + ) self.logger.info("Claimed %d tasks", len([t for t in tasks if t is not None])) # if no tasks claimed, sleep From 41ad1744b8a973e509777f7849e0286572b00120 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 2 Dec 2024 14:12:45 -0700 Subject: [PATCH 100/143] Use request and manually process payload --- alchemiscale/compute/api.py | 13 +++++++++---- alchemiscale/compute/client.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/alchemiscale/compute/api.py b/alchemiscale/compute/api.py index a75854a2..c394bb1c 100644 --- a/alchemiscale/compute/api.py +++ b/alchemiscale/compute/api.py @@ -11,7 +11,7 @@ import random import base64 -from fastapi import FastAPI, APIRouter, Body, Depends +from fastapi import FastAPI, APIRouter, Body, Depends, Request from fastapi.middleware.gzip import GZipMiddleware from gufe.tokenization import GufeTokenizable, JSON_HANDLER, KeyedChain import zstandard as zstd @@ -318,15 +318,20 @@ def retrieve_task_transformation( # TODO: support compression performed client-side @router.post("/tasks/{task_scoped_key}/results", response_model=ScopedKey) -def set_task_result( +async def set_task_result( task_scoped_key, *, - protocoldagresult: str = Body(embed=True), - compute_service_id: Optional[str] = Body(embed=True), + request: Request, n4js: Neo4jStore = Depends(get_n4js_depends), s3os: S3ObjectStore = Depends(get_s3os_depends), token: TokenData = Depends(get_token_data_depends), ): + body = await request.body() + body_ = json.loads(body.decode("utf-8"), cls=JSON_HANDLER.decoder) + + protocoldagresult = body_['protocoldagresult'] + compute_service_id = body_['compute_service_id'] + task_sk = ScopedKey.from_str(task_scoped_key) validate_scopes(task_sk.scope, token) diff --git a/alchemiscale/compute/client.py b/alchemiscale/compute/client.py index b703459b..eb022759 100644 --- a/alchemiscale/compute/client.py +++ b/alchemiscale/compute/client.py @@ -129,7 +129,7 @@ def set_task_result( self, task: ScopedKey, protocoldagresult: ProtocolDAGResult, - compute_service_id=Optional[ComputeServiceID], + compute_service_id: Optional[ComputeServiceID] = None, ) -> ScopedKey: keyed_chain_rep = KeyedChain.from_gufe(protocoldagresult).to_keyed_chain_rep() From d29965e55cb3316a604f463478bc68aa4d2c8041 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 2 Dec 2024 20:26:49 -0700 Subject: [PATCH 101/143] Put claim_tasks method back in SynchronousComputeService to simplify testing --- alchemiscale/compute/service.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/alchemiscale/compute/service.py b/alchemiscale/compute/service.py index 31b4a922..d9332e57 100644 --- a/alchemiscale/compute/service.py +++ b/alchemiscale/compute/service.py @@ -157,6 +157,19 @@ def heartbeat(self): self.beat() time.sleep(self.heartbeat_interval) + def claim_tasks(self) -> List[Optional[ScopedKey]]: + """Get a Task to execute from compute API. + + Returns `None` if no Task was available matching service configuration. + + """ + return self.client.claim_tasks( + scopes=self.scopes, + compute_service_id=self.compute_service_id, + count=self.claim_limit, + protocols=self.settings.protocols, + ) + def task_to_protocoldag( self, task: ScopedKey ) -> Tuple[ProtocolDAG, Transformation, Optional[ProtocolDAGResult]]: @@ -280,12 +293,7 @@ def cycle(self, max_tasks: Optional[int] = None, max_time: Optional[int] = None) # claim tasks from the compute API self.logger.info("Claiming tasks") - tasks: List[ScopedKey] = self.client.claim_tasks( - scopes=self.scopes, - compute_service_id=self.compute_service_id, - count=self.claim_limit, - protocols=self.settings.protocols, - ) + tasks: List[ScopedKey] = self.claim_tasks() self.logger.info("Claimed %d tasks", len([t for t in tasks if t is not None])) # if no tasks claimed, sleep From 45e2e6c263fc5f5603c5799b30c619b70d08e3c6 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 2 Dec 2024 21:06:47 -0700 Subject: [PATCH 102/143] Return to previously tested behavior --- alchemiscale/compute/service.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/alchemiscale/compute/service.py b/alchemiscale/compute/service.py index d9332e57..fc24d109 100644 --- a/alchemiscale/compute/service.py +++ b/alchemiscale/compute/service.py @@ -157,18 +157,25 @@ def heartbeat(self): self.beat() time.sleep(self.heartbeat_interval) - def claim_tasks(self) -> List[Optional[ScopedKey]]: + def claim_tasks(self, count=1) -> List[Optional[ScopedKey]]: """Get a Task to execute from compute API. Returns `None` if no Task was available matching service configuration. + Parameters + ---------- + count + The maximum number of Tasks to claim. """ - return self.client.claim_tasks( + + tasks = self.client.claim_tasks( scopes=self.scopes, compute_service_id=self.compute_service_id, - count=self.claim_limit, + count=count, protocols=self.settings.protocols, - ) + ) + + return tasks def task_to_protocoldag( self, task: ScopedKey @@ -293,7 +300,7 @@ def cycle(self, max_tasks: Optional[int] = None, max_time: Optional[int] = None) # claim tasks from the compute API self.logger.info("Claiming tasks") - tasks: List[ScopedKey] = self.claim_tasks() + tasks: List[ScopedKey] = self.claim_tasks(count=self.claim_limit) self.logger.info("Claimed %d tasks", len([t for t in tasks if t is not None])) # if no tasks claimed, sleep From 3d19003b0dbaaf8d14c872b6956052255930fe15 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 2 Dec 2024 21:07:07 -0700 Subject: [PATCH 103/143] Black! --- alchemiscale/compute/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/compute/service.py b/alchemiscale/compute/service.py index fc24d109..cf8bf492 100644 --- a/alchemiscale/compute/service.py +++ b/alchemiscale/compute/service.py @@ -173,7 +173,7 @@ def claim_tasks(self, count=1) -> List[Optional[ScopedKey]]: compute_service_id=self.compute_service_id, count=count, protocols=self.settings.protocols, - ) + ) return tasks From 6e726f4e54f393d2defba12677a486db2bd5c1f9 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Tue, 3 Dec 2024 11:42:40 -0700 Subject: [PATCH 104/143] Update deployment envs, user guide to use release 0.5.3. --- devtools/conda-envs/alchemiscale-client.yml | 4 ++-- devtools/conda-envs/alchemiscale-compute.yml | 4 ++-- devtools/conda-envs/alchemiscale-server.yml | 4 ++-- devtools/conda-envs/test.yml | 2 +- docs/user_guide.rst | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/devtools/conda-envs/alchemiscale-client.yml b/devtools/conda-envs/alchemiscale-client.yml index 6f2ae9be..8c31ae87 100644 --- a/devtools/conda-envs/alchemiscale-client.yml +++ b/devtools/conda-envs/alchemiscale-client.yml @@ -21,7 +21,7 @@ dependencies: - nest-asyncio # openmm protocols - - feflow=0.1.0 + - feflow=0.1.1 # additional pins - openmm=8.1.2 @@ -29,4 +29,4 @@ dependencies: - openff-units=0.2.2 - pip: - - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.2 + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.3 diff --git a/devtools/conda-envs/alchemiscale-compute.yml b/devtools/conda-envs/alchemiscale-compute.yml index f93cd1f3..39307fc8 100644 --- a/devtools/conda-envs/alchemiscale-compute.yml +++ b/devtools/conda-envs/alchemiscale-compute.yml @@ -17,7 +17,7 @@ dependencies: - async-lru # openmm protocols - - feflow=0.1.0 + - feflow=0.1.1 # additional pins - openmm=8.1.2 @@ -25,4 +25,4 @@ dependencies: - openff-units=0.2.2 - pip: - - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.2 + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.3 diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index ae871cca..87474c87 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -35,7 +35,7 @@ dependencies: - cryptography # openmm protocols - - feflow=0.1.0 + - feflow=0.1.1 # additional pins - openmm=8.1.2 @@ -46,4 +46,4 @@ dependencies: - curl # used in healthchecks for API services - pip: - - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.2 + - git+https://github.com/OpenFreeEnergy/alchemiscale.git@v0.5.3 diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index fc447422..b7dd56a4 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -35,7 +35,7 @@ dependencies: - cryptography # openmm protocols - - feflow>=0.1.0 + - feflow>=0.1.1 ## cli - click diff --git a/docs/user_guide.rst b/docs/user_guide.rst index da88b10f..dec60bf7 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -16,7 +16,7 @@ Clone alchemiscale from Github, and switch to the latest release tag:: $ git clone https://github.com/OpenFreeEnergy/alchemiscale.git $ cd alchemiscale - $ git checkout v0.5.2 + $ git checkout v0.5.3 Create a conda environment using, e.g. `micromamba`_:: From 54cd184879d5852714bb5ec8c66738d7d9048f86 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 2 Dec 2024 15:03:01 -0700 Subject: [PATCH 105/143] Add compression module and send send bytes from API path as latin-1 Use more bytes Move compression and decompression functions to new module Use latin-1 decoded bytes --- alchemiscale/compression.py | 60 +++++++++++++++++++ alchemiscale/compute/api.py | 26 +++----- alchemiscale/compute/client.py | 20 +++---- alchemiscale/interface/api.py | 8 +-- alchemiscale/interface/client.py | 8 +-- alchemiscale/security/auth.py | 1 - alchemiscale/storage/objectstore.py | 46 +++++++------- .../interface/client/test_client.py | 3 +- .../integration/storage/test_objectstore.py | 11 ++-- 9 files changed, 112 insertions(+), 71 deletions(-) create mode 100644 alchemiscale/compression.py diff --git a/alchemiscale/compression.py b/alchemiscale/compression.py new file mode 100644 index 00000000..2004095a --- /dev/null +++ b/alchemiscale/compression.py @@ -0,0 +1,60 @@ +from gufe.tokenization import GufeTokenizable, JSON_HANDLER +import json +import zstandard as zstd + + +def compress_gufe_zstd(gufe_object: GufeTokenizable) -> bytes: + """Compress a GufeTokenizable using zstandard compression. + + After the GufeTokenizable is converted to a KeyedChain, it's + serialized into JSON using the gufe provided + JSON_HANDLER.encoder. The resulting string is utf-8 encoded and + compressed with the zstandard compressor. These bytes are returned + by the function. + + Parameters + ---------- + gufe_object: GufeTokenizable + The GufeTokenizable to compress. + + Returns + ------- + bytes + Compressed byte form of the the GufeTokenizable. + """ + keyed_chain_rep = gufe_object.to_keyed_chain() + json_rep = json.dumps(keyed_chain_rep, cls=JSON_HANDLER.encoder) + json_bytes = json_rep.encode("utf-8") + + compressor = zstd.ZstdCompressor() + compressed_gufe = compressor.compress(json_bytes) + + return compressed_gufe + + +def decompress_gufe_zstd(compressed_bytes: bytes) -> GufeTokenizable: + """Decompress a zstandard compressed GufeTokenizable. + + The bytes encoding a zstandard compressed GufeTokenizable are + decompressed and decoded using the gufe provided + JSON_HANDLER.decoder. It is assumed that the decompressed bytes + are utf-8 encoded. + + Parameters + ---------- + compressed_bytes: bytes + The compressed byte form of a GufeTokenizable. + + Returns + ------- + GufeTokenizable + The decompressed GufeTokenizable. + """ + decompressor = zstd.ZstdDecompressor() + decompressed_gufe: bytes = decompressor.decompress(compressed_bytes) + + keyed_chain_rep = json.loads( + decompressed_gufe.decode("utf-8"), cls=JSON_HANDLER.decoder + ) + gufe_object = GufeTokenizable.from_keyed_chain(keyed_chain_rep) + return gufe_object diff --git a/alchemiscale/compute/api.py b/alchemiscale/compute/api.py index c394bb1c..869a5281 100644 --- a/alchemiscale/compute/api.py +++ b/alchemiscale/compute/api.py @@ -9,7 +9,6 @@ import json from datetime import datetime, timedelta import random -import base64 from fastapi import FastAPI, APIRouter, Body, Depends, Request from fastapi.middleware.gzip import GZipMiddleware @@ -31,6 +30,7 @@ gufe_to_json, GzipRoute, ) +from ..compression import decompress_gufe_zstd from ..settings import ( get_base_api_settings, get_compute_api_settings, @@ -298,18 +298,17 @@ def retrieve_task_transformation( # we keep this as a string to avoid useless deserialization/reserialization here try: - pdr: str = s3os.pull_protocoldagresult( - pdr_sk, transformation_sk, return_as="json", ok=True + pdr_bytes: bytes = s3os.pull_protocoldagresult( + pdr_sk, transformation_sk, ok=True ) except: # if we fail to get the object with the above, fall back to # location-based retrieval - pdr: str = s3os.pull_protocoldagresult( + pdr_bytes: bytes = s3os.pull_protocoldagresult( location=protocoldagresultref.location, - return_as="json", ok=True, ) - + pdr = pdr_bytes.decode("latin-1") else: pdr = None @@ -329,20 +328,13 @@ async def set_task_result( body = await request.body() body_ = json.loads(body.decode("utf-8"), cls=JSON_HANDLER.decoder) - protocoldagresult = body_['protocoldagresult'] - compute_service_id = body_['compute_service_id'] + protocoldagresult_ = body_["protocoldagresult"] + compute_service_id = body_["compute_service_id"] task_sk = ScopedKey.from_str(task_scoped_key) validate_scopes(task_sk.scope, token) - # decode b64 and decompress the zstd bytes back into json - protocoldagresult = base64.b64decode(protocoldagresult) - decompressor = zstd.ZstdDecompressor() - protocoldagresult = decompressor.decompress(protocoldagresult) - - pdr_keyed_chain_rep = json.loads(protocoldagresult, cls=JSON_HANDLER.decoder) - pdr_keyed_chain = KeyedChain.from_keyed_chain_rep(pdr_keyed_chain_rep) - pdr = pdr_keyed_chain.to_gufe() + pdr = decompress_gufe_zstd(protocoldagresult_) tf_sk, _ = n4js.get_task_transformation( task=task_scoped_key, @@ -351,7 +343,7 @@ async def set_task_result( # push the ProtocolDAGResult to the object store protocoldagresultref: ProtocolDAGResultRef = s3os.push_protocoldagresult( - pdr, transformation=tf_sk, creator=compute_service_id + protocoldagresult_, transformation=tf_sk, creator=compute_service_id ) # push the reference to the state store diff --git a/alchemiscale/compute/client.py b/alchemiscale/compute/client.py index eb022759..7a7c498d 100644 --- a/alchemiscale/compute/client.py +++ b/alchemiscale/compute/client.py @@ -9,7 +9,6 @@ import json from urllib.parse import urljoin from functools import wraps -import base64 import requests from requests.auth import HTTPBasicAuth @@ -25,6 +24,7 @@ AlchemiscaleBaseClientError, json_to_gufe, ) +from ..compression import compress_gufe_zstd, decompress_gufe_zstd from ..models import Scope, ScopedKey from ..storage.models import TaskHub, Task, ComputeServiceID, TaskStatusEnum @@ -120,9 +120,14 @@ def retrieve_task_transformation( f"/tasks/{task}/transformation/gufe" ) + if protocoldagresult is not None: + protocoldagresult = decompress_gufe_zstd( + protocoldagresult.encode("latin-1") + ) + return ( json_to_gufe(transformation), - json_to_gufe(protocoldagresult) if protocoldagresult is not None else None, + protocoldagresult, ) def set_task_result( @@ -132,17 +137,8 @@ def set_task_result( compute_service_id: Optional[ComputeServiceID] = None, ) -> ScopedKey: - keyed_chain_rep = KeyedChain.from_gufe(protocoldagresult).to_keyed_chain_rep() - json_rep = json.dumps(keyed_chain_rep, cls=JSON_HANDLER.encoder) - json_bytes = json_rep.encode("utf-8") - - compressor = zstd.ZstdCompressor() - compressed = compressor.compress(json_bytes) - - base64_encoded = base64.b64encode(compressed).decode("utf-8") - data = dict( - protocoldagresult=base64_encoded, + protocoldagresult=compress_gufe_zstd(protocoldagresult), compute_service_id=str(compute_service_id), ) diff --git a/alchemiscale/interface/api.py b/alchemiscale/interface/api.py index 5b6aeb1e..fd6db0c7 100644 --- a/alchemiscale/interface/api.py +++ b/alchemiscale/interface/api.py @@ -1046,17 +1046,15 @@ def get_protocoldagresult( # we leave each ProtocolDAGResult in string form to avoid # deserializing/reserializing here; just passing through to client try: - pdr: str = s3os.pull_protocoldagresult( - pdr_sk, transformation_sk, return_as="json", ok=ok - ) + pdr_bytes: str = s3os.pull_protocoldagresult(pdr_sk, transformation_sk, ok=ok) except Exception: # if we fail to get the object with the above, fall back to # location-based retrieval - pdr: str = s3os.pull_protocoldagresult( + pdr_bytes: str = s3os.pull_protocoldagresult( location=protocoldagresultref.location, - return_as="json", ok=ok, ) + pdr = pdr_bytes.decode("latin-1") return [pdr] diff --git a/alchemiscale/interface/client.py b/alchemiscale/interface/client.py index 7bd1311f..151c1a35 100644 --- a/alchemiscale/interface/client.py +++ b/alchemiscale/interface/client.py @@ -22,6 +22,7 @@ AlchemiscaleBaseClientError, use_session, ) +from ..compression import decompress_gufe_zstd from ..models import Scope, ScopedKey from ..storage.models import ( TaskStatusEnum, @@ -1352,14 +1353,11 @@ def get_tasks_priority( async def _async_get_protocoldagresult( self, protocoldagresultref, transformation, route, compress ): - pdr_json = await self._get_resource_async( + pdr_compressed_latin1 = await self._get_resource_async( f"/transformations/{transformation}/{route}/{protocoldagresultref}", compress=compress, ) - - pdr = GufeTokenizable.from_dict( - json.loads(pdr_json[0], cls=JSON_HANDLER.decoder) - ) + pdr = decompress_gufe_zstd(pdr_compressed_latin1[0].encode("latin-1")) return pdr diff --git a/alchemiscale/security/auth.py b/alchemiscale/security/auth.py index 78c4ac0e..f5c2cc92 100644 --- a/alchemiscale/security/auth.py +++ b/alchemiscale/security/auth.py @@ -5,7 +5,6 @@ """ import secrets -import base64 import hashlib from datetime import datetime, timedelta from typing import Optional, Union diff --git a/alchemiscale/storage/objectstore.py b/alchemiscale/storage/objectstore.py index 1ac76bbe..c1e007f2 100644 --- a/alchemiscale/storage/objectstore.py +++ b/alchemiscale/storage/objectstore.py @@ -12,8 +12,10 @@ from boto3.session import Session from functools import lru_cache +import zstandard as zstd + from gufe.protocols import ProtocolDAGResult -from gufe.tokenization import JSON_HANDLER, GufeTokenizable +from gufe.tokenization import JSON_HANDLER, GufeTokenizable, KeyedChain from ..models import ScopedKey, Scope from .models import ProtocolDAGResultRef @@ -193,7 +195,7 @@ def _get_filename(self, location): def push_protocoldagresult( self, - protocoldagresult: ProtocolDAGResult, + protocoldagresult: bytes, transformation: ScopedKey, creator: Optional[str] = None, ) -> ProtocolDAGResultRef: @@ -213,7 +215,16 @@ def push_protocoldagresult( Reference to the serialized `ProtocolDAGResult` in the object store. """ - ok = protocoldagresult.ok() + + decompressor = zstd.ZstdDecompressor() + decompressed_pdr = decompressor.decompress(protocoldagresult) + + pdr_keyed_chain_rep = json.loads( + decompressed_pdr.decode("utf-8"), cls=JSON_HANDLER.decoder + ) + pdr_keyed_chain = KeyedChain.from_keyed_chain_rep(pdr_keyed_chain_rep) + pdr = pdr_keyed_chain.to_gufe() + ok = pdr.ok() route = "results" if ok else "failures" # build `location` based on gufe key @@ -222,19 +233,15 @@ def push_protocoldagresult( *transformation.scope.to_tuple(), transformation.gufe_key, route, - protocoldagresult.key, + pdr.key, "obj.json", ) - # TODO: add support for compute client-side compressed protocoldagresults - pdr_jb = json.dumps( - protocoldagresult.to_dict(), cls=JSON_HANDLER.encoder - ).encode("utf-8") - response = self._store_bytes(location, pdr_jb) + response = self._store_bytes(location, protocoldagresult) return ProtocolDAGResultRef( location=location, - obj_key=protocoldagresult.key, + obj_key=pdr.key, scope=transformation.scope, ok=ok, datetime_created=datetime.utcnow(), @@ -246,9 +253,8 @@ def pull_protocoldagresult( protocoldagresult: Optional[ScopedKey] = None, transformation: Optional[ScopedKey] = None, location: Optional[str] = None, - return_as="gufe", ok=True, - ) -> Union[ProtocolDAGResult, dict, str]: + ) -> bytes: """Pull the `ProtocolDAGResult` corresponding to the given `ProtocolDAGResultRef`. Parameters @@ -263,9 +269,6 @@ def pull_protocoldagresult( location The full path in the object store to the ProtocolDAGResult. If provided, this will be used to retrieve it. - return_as : ['gufe', 'dict', 'json'] - Form in which to return result; this is provided to avoid - unnecessary deserializations where desired. Returns ------- @@ -297,15 +300,6 @@ def pull_protocoldagresult( ## TODO: want organization alongside `obj.json` of `ProtocolUnit` gufe_keys ## for any file objects stored in the same space + pdr_bytes = self._get_bytes(location) - pdr_j = self._get_bytes(location).decode("utf-8") - - # TODO: add support for interface client-side decompression - if return_as == "gufe": - pdr = GufeTokenizable.from_dict(json.loads(pdr_j, cls=JSON_HANDLER.decoder)) - elif return_as == "dict": - pdr = json.loads(pdr_j, cls=JSON_HANDLER.decoder) - elif return_as == "json": - pdr = pdr_j - - return pdr + return pdr_bytes diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index 7146c50f..e665fb20 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -8,6 +8,7 @@ from gufe.protocols.protocoldag import execute_DAG import networkx as nx +from alchemiscale.compression import compress_gufe_zstd from alchemiscale.models import ScopedKey, Scope from alchemiscale.storage.models import TaskStatusEnum, NetworkStateEnum from alchemiscale.storage.cypher import cypher_list_from_scoped_keys @@ -1850,7 +1851,7 @@ def _execute_tasks(tasks, n4js, s3os_server): protocoldagresults.append(protocoldagresult) protocoldagresultref = s3os_server.push_protocoldagresult( - protocoldagresult, transformation=transformation_sk + compress_gufe_zstd(protocoldagresult), transformation=transformation_sk ) n4js.set_task_result( diff --git a/alchemiscale/tests/integration/storage/test_objectstore.py b/alchemiscale/tests/integration/storage/test_objectstore.py index f8c4fcd2..bb7a1177 100644 --- a/alchemiscale/tests/integration/storage/test_objectstore.py +++ b/alchemiscale/tests/integration/storage/test_objectstore.py @@ -3,6 +3,7 @@ import pytest +from alchemiscale.compression import compress_gufe_zstd, decompress_gufe_zstd from alchemiscale.models import ScopedKey from alchemiscale.storage.objectstore import S3ObjectStore from alchemiscale.storage.models import ProtocolDAGResultRef @@ -21,7 +22,7 @@ def test_push_protocolresult( # try to push the result objstoreref: ProtocolDAGResultRef = s3os.push_protocoldagresult( - protocoldagresults[0], transformation=transformation_sk + compress_gufe_zstd(protocoldagresults[0]), transformation=transformation_sk ) assert objstoreref.obj_key == protocoldagresults[0].key @@ -38,7 +39,7 @@ def test_pull_protocolresult( transformation_sk = ScopedKey(gufe_key=transformation.key, **scope_test.dict()) objstoreref: ProtocolDAGResultRef = s3os.push_protocoldagresult( - protocoldagresults[0], transformation=transformation_sk + compress_gufe_zstd(protocoldagresults[0]), transformation=transformation_sk ) # round trip it @@ -46,13 +47,15 @@ def test_pull_protocolresult( tf_sk = ScopedKey( gufe_key=protocoldagresults[0].transformation_key, **scope_test.dict() ) - pdr = s3os.pull_protocoldagresult(sk, tf_sk) + pdr = decompress_gufe_zstd(s3os.pull_protocoldagresult(sk, tf_sk)) assert pdr.key == protocoldagresults[0].key assert pdr.protocol_unit_results == pdr.protocol_unit_results # test location-based pull - pdr = s3os.pull_protocoldagresult(location=objstoreref.location) + pdr = decompress_gufe_zstd( + s3os.pull_protocoldagresult(location=objstoreref.location) + ) assert pdr.key == protocoldagresults[0].key assert pdr.protocol_unit_results == pdr.protocol_unit_results From f2da1fc3bb71b51e9ba9b56cff9f64c3e2b084c1 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Wed, 11 Dec 2024 10:25:39 -0700 Subject: [PATCH 106/143] Use compression module in objectstore --- alchemiscale/compute/api.py | 2 +- alchemiscale/compute/client.py | 2 +- alchemiscale/storage/objectstore.py | 12 +++--------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/alchemiscale/compute/api.py b/alchemiscale/compute/api.py index 869a5281..16848065 100644 --- a/alchemiscale/compute/api.py +++ b/alchemiscale/compute/api.py @@ -12,7 +12,7 @@ from fastapi import FastAPI, APIRouter, Body, Depends, Request from fastapi.middleware.gzip import GZipMiddleware -from gufe.tokenization import GufeTokenizable, JSON_HANDLER, KeyedChain +from gufe.tokenization import GufeTokenizable, JSON_HANDLER import zstandard as zstd from ..base.api import ( diff --git a/alchemiscale/compute/client.py b/alchemiscale/compute/client.py index 7a7c498d..805657b9 100644 --- a/alchemiscale/compute/client.py +++ b/alchemiscale/compute/client.py @@ -15,7 +15,7 @@ import zstandard as zstd -from gufe.tokenization import GufeTokenizable, JSON_HANDLER, KeyedChain +from gufe.tokenization import GufeTokenizable, JSON_HANDLER from gufe import Transformation from gufe.protocols import ProtocolDAGResult diff --git a/alchemiscale/storage/objectstore.py b/alchemiscale/storage/objectstore.py index c1e007f2..101aff95 100644 --- a/alchemiscale/storage/objectstore.py +++ b/alchemiscale/storage/objectstore.py @@ -15,8 +15,9 @@ import zstandard as zstd from gufe.protocols import ProtocolDAGResult -from gufe.tokenization import JSON_HANDLER, GufeTokenizable, KeyedChain +from gufe.tokenization import JSON_HANDLER, GufeTokenizable +from ..compression import decompress_gufe_zstd from ..models import ScopedKey, Scope from .models import ProtocolDAGResultRef from ..settings import S3ObjectStoreSettings, get_s3objectstore_settings @@ -216,14 +217,7 @@ def push_protocoldagresult( """ - decompressor = zstd.ZstdDecompressor() - decompressed_pdr = decompressor.decompress(protocoldagresult) - - pdr_keyed_chain_rep = json.loads( - decompressed_pdr.decode("utf-8"), cls=JSON_HANDLER.decoder - ) - pdr_keyed_chain = KeyedChain.from_keyed_chain_rep(pdr_keyed_chain_rep) - pdr = pdr_keyed_chain.to_gufe() + pdr = decompress_gufe_zstd(protocoldagresult) ok = pdr.ok() route = "results" if ok else "failures" From 18131f16d29f0d8c6ae371b57e2e61152df020f9 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Fri, 13 Dec 2024 15:26:46 -0700 Subject: [PATCH 107/143] Attempt to decompress object store objects in a try block If a decompression error is raised, assume that the original data was never compressed. --- alchemiscale/compute/client.py | 21 +++--- alchemiscale/interface/client.py | 13 +++- alchemiscale/storage/objectstore.py | 4 +- .../interface/client/test_client.py | 64 ++++++++++++++++++- 4 files changed, 89 insertions(+), 13 deletions(-) diff --git a/alchemiscale/compute/client.py b/alchemiscale/compute/client.py index 805657b9..b2bfe747 100644 --- a/alchemiscale/compute/client.py +++ b/alchemiscale/compute/client.py @@ -115,20 +115,25 @@ def get_task_transformation(self, task: ScopedKey) -> ScopedKey: def retrieve_task_transformation( self, task: ScopedKey - ) -> Tuple[Transformation, Optional[ProtocolDAGResult]]: + ) -> tuple[Transformation, ProtocolDAGResult | None]: transformation, protocoldagresult = self._get_resource( f"/tasks/{task}/transformation/gufe" ) if protocoldagresult is not None: - protocoldagresult = decompress_gufe_zstd( - protocoldagresult.encode("latin-1") - ) - return ( - json_to_gufe(transformation), - protocoldagresult, - ) + protocoldagresult_bytes = protocoldagresult.encode("latin-1") + + try: + # Attempt to decompress the ProtocolDAGResult object + protocoldagresult = decompress_gufe_zstd( + protocoldagresult_bytes + ) + except zstd.ZstdError: + # If decompression fails, assume it's a UTF-8 encoded JSON string + protocoldagresult = json_to_gufe(protocoldagresult_bytes.decode('utf-8')) + + return json_to_gufe(transformation), protocoldagresult def set_task_result( self, diff --git a/alchemiscale/interface/client.py b/alchemiscale/interface/client.py index 151c1a35..1bd7b14c 100644 --- a/alchemiscale/interface/client.py +++ b/alchemiscale/interface/client.py @@ -15,11 +15,13 @@ from gufe import AlchemicalNetwork, Transformation, ChemicalSystem from gufe.tokenization import GufeTokenizable, JSON_HANDLER, KeyedChain from gufe.protocols import ProtocolResult, ProtocolDAGResult +import zstandard as zstd from ..base.client import ( AlchemiscaleBaseClient, AlchemiscaleBaseClientError, + json_to_gufe, use_session, ) from ..compression import decompress_gufe_zstd @@ -1353,11 +1355,18 @@ def get_tasks_priority( async def _async_get_protocoldagresult( self, protocoldagresultref, transformation, route, compress ): - pdr_compressed_latin1 = await self._get_resource_async( + pdr_latin1_decoded = await self._get_resource_async( f"/transformations/{transformation}/{route}/{protocoldagresultref}", compress=compress, ) - pdr = decompress_gufe_zstd(pdr_compressed_latin1[0].encode("latin-1")) + + try: + # Attempt to decompress the ProtocolDAGResult object + pdr_bytes = pdr_latin1_decoded[0].encode("latin-1") + pdr = decompress_gufe_zstd(pdr_bytes) + except zstd.ZstdError: + # If decompress fails, assume it's a UTF-8 encoded JSON string + pdr = json_to_gufe(pdr_bytes.decode('utf-8')) return pdr diff --git a/alchemiscale/storage/objectstore.py b/alchemiscale/storage/objectstore.py index 101aff95..0480bf8f 100644 --- a/alchemiscale/storage/objectstore.py +++ b/alchemiscale/storage/objectstore.py @@ -228,7 +228,7 @@ def push_protocoldagresult( transformation.gufe_key, route, pdr.key, - "obj.json", + "obj", ) response = self._store_bytes(location, protocoldagresult) @@ -289,7 +289,7 @@ def pull_protocoldagresult( transformation.gufe_key, route, protocoldagresult.gufe_key, - "obj.json", + "obj", ) ## TODO: want organization alongside `obj.json` of `ProtocolUnit` gufe_keys diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index e665fb20..3ad66971 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -1,10 +1,12 @@ import pytest from time import sleep +import os from pathlib import Path from itertools import chain +import json from gufe import AlchemicalNetwork -from gufe.tokenization import TOKENIZABLE_REGISTRY, GufeKey +from gufe.tokenization import TOKENIZABLE_REGISTRY, GufeKey, JSON_HANDLER from gufe.protocols.protocoldag import execute_DAG import networkx as nx @@ -1860,6 +1862,66 @@ def _execute_tasks(tasks, n4js, s3os_server): return protocoldagresults + def test_get_transformation_and_network_results_json( + self, + scope_test, + n4js_preloaded, + s3os_server, + user_client: client.AlchemiscaleClient, + network_tyk2, + tmpdir, + ): + n4js = n4js_preloaded + + # select the transformation we want to compute + an = network_tyk2 + transformation = list(t for t in an.edges if "_solvent" in t.name)[0] + + network_sk = user_client.get_scoped_key(an, scope_test) + transformation_sk = user_client.get_scoped_key(transformation, scope_test) + + # user client : create three independent tasks for the transformation + user_client.create_tasks(transformation_sk, count=3) + + # user client : action the tasks for execution + all_tasks = user_client.get_transformation_tasks(transformation_sk) + actioned_tasks = user_client.action_tasks(all_tasks, network_sk) + + # execute the actioned tasks and push results directly using statestore and object store + with tmpdir.as_cwd(): + protocoldagresults = self._execute_tasks(actioned_tasks, n4js, s3os_server) + # overwrite what's in the object store + for protocoldagresult in protocoldagresults: + pdr_jb = json.dumps( + protocoldagresult.to_dict(), cls=JSON_HANDLER.encoder + ).encode("utf-8") + + location = os.path.join( + "protocoldagresult", + *transformation_sk.scope.to_tuple(), + transformation_sk.gufe_key, + "results", + protocoldagresult.key, + "obj", + ) + + s3os_server._store_bytes(location, pdr_jb) + + # clear local gufe registry of pdr objects + # not critical, but ensures we see the objects that are deserialized + # instead of our instances already in memory post-pull + for pdr in protocoldagresults: + TOKENIZABLE_REGISTRY.pop(pdr.key, None) + + # get back protocoldagresults instead + protocoldagresults_r = user_client.get_transformation_results( + transformation_sk, return_protocoldagresults=True + ) + + assert set(protocoldagresults_r) == set(protocoldagresults) + + pass + def test_get_transformation_and_network_results( self, scope_test, From 8ee534941be708749b78d2a06470513ea5cc23f2 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Wed, 18 Dec 2024 14:55:54 -0700 Subject: [PATCH 108/143] Use fixed filename for compressed object --- alchemiscale/compute/client.py | 8 ++++---- alchemiscale/interface/client.py | 2 +- alchemiscale/storage/objectstore.py | 7 +++++-- .../integration/interface/client/test_client.py | 14 +++++++------- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/alchemiscale/compute/client.py b/alchemiscale/compute/client.py index b2bfe747..65581043 100644 --- a/alchemiscale/compute/client.py +++ b/alchemiscale/compute/client.py @@ -126,12 +126,12 @@ def retrieve_task_transformation( try: # Attempt to decompress the ProtocolDAGResult object - protocoldagresult = decompress_gufe_zstd( - protocoldagresult_bytes - ) + protocoldagresult = decompress_gufe_zstd(protocoldagresult_bytes) except zstd.ZstdError: # If decompression fails, assume it's a UTF-8 encoded JSON string - protocoldagresult = json_to_gufe(protocoldagresult_bytes.decode('utf-8')) + protocoldagresult = json_to_gufe( + protocoldagresult_bytes.decode("utf-8") + ) return json_to_gufe(transformation), protocoldagresult diff --git a/alchemiscale/interface/client.py b/alchemiscale/interface/client.py index 1bd7b14c..b14f640a 100644 --- a/alchemiscale/interface/client.py +++ b/alchemiscale/interface/client.py @@ -1366,7 +1366,7 @@ async def _async_get_protocoldagresult( pdr = decompress_gufe_zstd(pdr_bytes) except zstd.ZstdError: # If decompress fails, assume it's a UTF-8 encoded JSON string - pdr = json_to_gufe(pdr_bytes.decode('utf-8')) + pdr = json_to_gufe(pdr_bytes.decode("utf-8")) return pdr diff --git a/alchemiscale/storage/objectstore.py b/alchemiscale/storage/objectstore.py index 0480bf8f..089d25a4 100644 --- a/alchemiscale/storage/objectstore.py +++ b/alchemiscale/storage/objectstore.py @@ -22,6 +22,9 @@ from .models import ProtocolDAGResultRef from ..settings import S3ObjectStoreSettings, get_s3objectstore_settings +# default filename for object store files +OBJECT_FILENAME = "obj.json.zst" + @lru_cache() def get_s3os(settings: S3ObjectStoreSettings, endpoint_url=None) -> "S3ObjectStore": @@ -228,7 +231,7 @@ def push_protocoldagresult( transformation.gufe_key, route, pdr.key, - "obj", + OBJECT_FILENAME, ) response = self._store_bytes(location, protocoldagresult) @@ -289,7 +292,7 @@ def pull_protocoldagresult( transformation.gufe_key, route, protocoldagresult.gufe_key, - "obj", + OBJECT_FILENAME, ) ## TODO: want organization alongside `obj.json` of `ProtocolUnit` gufe_keys diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index 3ad66971..2e9af8e5 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -1863,13 +1863,13 @@ def _execute_tasks(tasks, n4js, s3os_server): return protocoldagresults def test_get_transformation_and_network_results_json( - self, - scope_test, - n4js_preloaded, - s3os_server, - user_client: client.AlchemiscaleClient, - network_tyk2, - tmpdir, + self, + scope_test, + n4js_preloaded, + s3os_server, + user_client: client.AlchemiscaleClient, + network_tyk2, + tmpdir, ): n4js = n4js_preloaded From b32f62df85281583b86bac9169c31fafc51240e9 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Thu, 26 Dec 2024 11:50:19 -0700 Subject: [PATCH 109/143] Implement test_set_task_result_legacy Test getting extends ProtocolDAGResults as if they were stored through the old pdr.to_dict() -> json -> utf-8 encoded format. The new test can be removed in the next major release that drops the old format. --- alchemiscale/storage/objectstore.py | 2 +- .../compute/client/test_compute_client.py | 121 +++++++++++++++++- .../interface/client/test_client.py | 2 +- 3 files changed, 119 insertions(+), 6 deletions(-) diff --git a/alchemiscale/storage/objectstore.py b/alchemiscale/storage/objectstore.py index 089d25a4..d0e7ec3d 100644 --- a/alchemiscale/storage/objectstore.py +++ b/alchemiscale/storage/objectstore.py @@ -234,7 +234,7 @@ def push_protocoldagresult( OBJECT_FILENAME, ) - response = self._store_bytes(location, protocoldagresult) + self._store_bytes(location, protocoldagresult) return ProtocolDAGResultRef( location=location, diff --git a/alchemiscale/tests/integration/compute/client/test_compute_client.py b/alchemiscale/tests/integration/compute/client/test_compute_client.py index 99777c41..ab23e8ef 100644 --- a/alchemiscale/tests/integration/compute/client/test_compute_client.py +++ b/alchemiscale/tests/integration/compute/client/test_compute_client.py @@ -1,12 +1,18 @@ import pytest +import json +import os +from datetime import datetime from time import sleep -from gufe.tokenization import GufeTokenizable +from gufe.tokenization import GufeTokenizable, JSON_HANDLER -from alchemiscale.models import ScopedKey from alchemiscale.compute import client -from alchemiscale.storage.models import TaskStatusEnum, ComputeServiceID - +from alchemiscale.models import ScopedKey +from alchemiscale.storage.models import ( + TaskStatusEnum, + ComputeServiceID, + ProtocolDAGResultRef, +) from alchemiscale.tests.integration.compute.utils import get_compute_settings_override @@ -299,3 +305,110 @@ def test_set_task_result( assert transformation2 == transformation_ assert extends_protocoldagresult2 == protocoldagresults[0] + + def test_set_task_result_legacy( + self, + scope_test, + n4js_preloaded, + compute_client: client.AlchemiscaleComputeClient, + compute_service_id, + network_tyk2, + transformation, + protocoldagresults, + uvicorn_server, + s3os_server, + ): + # register compute service id + compute_client.register(compute_service_id) + + an_sk = ScopedKey(gufe_key=network_tyk2.key, **scope_test.dict()) + tf_sk = ScopedKey(gufe_key=transformation.key, **scope_test.dict()) + taskhub_sk = n4js_preloaded.get_taskhub(an_sk) + + # claim our first task + task_sks = compute_client.claim_taskhub_tasks( + taskhub_sk, compute_service_id=compute_service_id + ) + + # get the transformation corresponding to this task + ( + transformation_, + extends_protocoldagresult, + ) = compute_client.retrieve_task_transformation(task_sks[0]) + + assert transformation_ == transformation + assert extends_protocoldagresult is None + + # push a result for the task + # pdr_sk = compute_client.set_task_result(task_sks[0], protocoldagresults[0]) + + protocoldagresult = protocoldagresults[0] + task_sk = task_sks[0] + + # we need to replicate the behavior of set_task_result: + # + # pdr_sk = compute_client.set_task_result(task_sks[0], protocoldagresults[0]) + # + # This involves pushing the protocoldagresult in the legacy + # to_dict() -> json -> utf-8 encode form, set the task result + # in the statestore, set the task to complete in the + # statestore + # + # + # step 1: Push the protocoldagresult. This needs to be done + # manually since the old behavior was overwritten. + + pdr_bytes_push = json.dumps( + protocoldagresult.to_dict(), cls=JSON_HANDLER.encoder + ).encode("utf-8") + route = "results" if protocoldagresult.ok() else "failures" + + location = os.path.join( + "protocoldagresult", + *tf_sk.scope.to_tuple(), + tf_sk.gufe_key, + route, + protocoldagresult.key, + "obj.json", + ) + + s3os_server._store_bytes(location, pdr_bytes_push) + + pdrr = ProtocolDAGResultRef( + location=location, + obj_key=protocoldagresult.key, + scope=tf_sk.scope, + ok=protocoldagresult.ok(), + datetime_created=datetime.utcnow(), + creator=None, + ) + + # step 2: set the task result in the statestore to reflect the + # protocoldagresult in the objectstore + + result_sk = n4js_preloaded.set_task_result( + task=task_sk, protocoldagresultref=pdrr + ) + + # step 3: set the task to complete in the statestore + + if pdrr.ok: + n4js_preloaded.set_task_complete(tasks=[task_sk]) + else: + n4js_preloaded.set_task_error(tasks=[task_sk]) + + # continue normally and show the protocoldagresult stored in + # the legacy format is properly fetched and decoded + + # create a task that extends the one we just "performed" + task_sk2 = n4js_preloaded.create_task(tf_sk, extends=task_sks[0]) + + # get the transformation and the protocoldagresult for the task this extends + # no need to claim to actually do this + ( + transformation2, + extends_protocoldagresult2, + ) = compute_client.retrieve_task_transformation(task_sk2) + + assert transformation2 == transformation_ + assert extends_protocoldagresult2 == protocoldagresults[0] diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index 2e9af8e5..2c39551c 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -1902,7 +1902,7 @@ def test_get_transformation_and_network_results_json( transformation_sk.gufe_key, "results", protocoldagresult.key, - "obj", + "obj.json", ) s3os_server._store_bytes(location, pdr_jb) From 6f3ce3c3122a5cf9712071a9de55733f16fcf670 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 30 Dec 2024 16:04:25 -0700 Subject: [PATCH 110/143] Separate executing tasks and pushing results in TestClient To allow for better and clearer testing of result pushing and pulling, the act of executing a task and pushing its results were separated. --- .../interface/client/test_client.py | 196 ++++++++++++------ 1 file changed, 134 insertions(+), 62 deletions(-) diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index 2c39551c..2333ee68 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -1,4 +1,5 @@ import pytest +from datetime import datetime from time import sleep import os from pathlib import Path @@ -12,7 +13,11 @@ from alchemiscale.compression import compress_gufe_zstd from alchemiscale.models import ScopedKey, Scope -from alchemiscale.storage.models import TaskStatusEnum, NetworkStateEnum +from alchemiscale.storage.models import ( + ProtocolDAGResultRef, + NetworkStateEnum, + TaskStatusEnum, +) from alchemiscale.storage.cypher import cypher_list_from_scoped_keys from alchemiscale.interface import client from alchemiscale.tests.integration.interface.utils import ( @@ -1798,6 +1803,56 @@ def test_set_tasks_priority_missing_tasks( ### results + @staticmethod + def _execute_task(task_scoped_key, n4js, shared_basedir=None, scratch_basedir=None): + + shared_basedir = shared_basedir or Path("shared").absolute() + shared_basedir.mkdir(exist_ok=True) + + scratch_basedir = scratch_basedir or Path("scratch").absolute() + scratch_basedir.mkdir(exist_ok=True) + + ( + transformation_sk, + extends_protocoldagresultref_sk, + ) = n4js.get_task_transformation(task=task_scoped_key, return_gufe=False) + + transformation = n4js.get_gufe(transformation_sk) + if extends_protocoldagresultref_sk: + extends_protocoldagresultref = n4js.get_gufe( + extends_protocoldagresultref_sk + ) + extends_protocoldagresult = s3os_server.pull_protocoldagresult( + extends_protocoldagresultref, transformation_sk + ) + else: + extends_protocoldagresult = None + + protocoldag = transformation.create( + extends=extends_protocoldagresult, + name=str(task_scoped_key), + ) + + shared = shared_basedir / str(protocoldag.key) + shared.mkdir() + + scratch = scratch_basedir / str(protocoldag.key) + scratch.mkdir() + + protocoldagresult = execute_DAG( + protocoldag, + shared_basedir=shared, + scratch_basedir=scratch, + raise_error=False, + ) + + assert protocoldagresult.transformation_key == transformation.key + + if extends_protocoldagresult: + assert protocoldagresult.extends_key == extends_protocoldagresult.key + + return protocoldagresult + @staticmethod def _execute_tasks(tasks, n4js, s3os_server): shared_basedir = Path("shared").absolute() @@ -1807,60 +1862,85 @@ def _execute_tasks(tasks, n4js, s3os_server): protocoldagresults = [] for task_sk in tasks: - if task_sk is None: - continue - # get the transformation and extending protocoldagresult as if we # were a compute service - ( - transformation_sk, - extends_protocoldagresultref_sk, - ) = n4js.get_task_transformation(task=task_sk, return_gufe=False) - - transformation = n4js.get_gufe(transformation_sk) - if extends_protocoldagresultref_sk: - extends_protocoldagresultref = n4js.get_gufe( - extends_protocoldagresultref_sk - ) - extends_protocoldagresult = s3os_server.pull_protocoldagresult( - extends_protocoldagresultref, transformation_sk - ) - else: - extends_protocoldagresult = None - - protocoldag = transformation.create( - extends=extends_protocoldagresult, - name=str(task_sk), + protocoldagresult = TestClient._execute_task( + task_sk, + n4js, + shared_basedir=shared_basedir, + scratch_basedir=scratch_basedir, ) + protocoldagresults.append(protocoldagresult) - shared = shared_basedir / str(protocoldag.key) - shared.mkdir() + return protocoldagresults - scratch = scratch_basedir / str(protocoldag.key) - scratch.mkdir() + @staticmethod + def _push_result(task_scoped_key, protocoldagresult, n4js, s3os_server): + transformation_sk, _ = n4js.get_task_transformation( + task_scoped_key, return_gufe=False + ) + protocoldagresultref = s3os_server.push_protocoldagresult( + compress_gufe_zstd(protocoldagresult), transformation=transformation_sk + ) + n4js.set_task_result( + task=task_scoped_key, protocoldagresultref=protocoldagresultref + ) + return protocoldagresultref - protocoldagresult = execute_DAG( - protocoldag, - shared_basedir=shared, - scratch_basedir=scratch, - raise_error=False, - ) + # TODO: remove in next major version when to_dict json is no longer supported + @staticmethod + def _push_result_legacy(task_scoped_key, protocoldagresult, n4js, s3os_server): + transformation_scoped_key, _ = n4js.get_task_transformation( + task_scoped_key, return_gufe=False + ) + pdr_jb = json.dumps( + protocoldagresult.to_dict(), cls=JSON_HANDLER.encoder + ).encode("utf-8") + + ok = protocoldagresult.ok() + route = "results" if ok else "failures" + + location = os.path.join( + "protocoldagresult", + *transformation_scoped_key.scope.to_tuple(), + transformation_scoped_key.gufe_key, + route, + protocoldagresult.key, + "obj.json", + ) - assert protocoldagresult.transformation_key == transformation.key - if extends_protocoldagresult: - assert protocoldagresult.extends_key == extends_protocoldagresult.key + s3os_server._store_bytes(location, pdr_jb) - protocoldagresults.append(protocoldagresult) + protocoldagresultref = ProtocolDAGResultRef( + location=location, + obj_key=protocoldagresult.key, + scope=transformation_scoped_key.scope, + ok=ok, + datetime_created=datetime.utcnow(), + creator=None, + ) + n4js.set_task_result( + task=task_scoped_key, protocoldagresultref=protocoldagresultref + ) - protocoldagresultref = s3os_server.push_protocoldagresult( - compress_gufe_zstd(protocoldagresult), transformation=transformation_sk - ) + return protocoldagresultref - n4js.set_task_result( - task=task_sk, protocoldagresultref=protocoldagresultref + @staticmethod + def _push_results( + task_scoped_keys, protocoldagresults, n4js, s3os_server, legacy=True + ): + push_function = ( + TestClient._push_result_legacy if legacy else TestClient._push_result + ) + protocoldagresultrefs = [] + for task_scoped_key, protocoldagresult in zip( + task_scoped_keys, protocoldagresults + ): + protocoldagresultref = push_function( + task_scoped_key, protocoldagresult, n4js, s3os_server ) - - return protocoldagresults + protocoldagresultrefs.append(protocoldagresultref) + return protocoldagresultrefs def test_get_transformation_and_network_results_json( self, @@ -1890,22 +1970,10 @@ def test_get_transformation_and_network_results_json( # execute the actioned tasks and push results directly using statestore and object store with tmpdir.as_cwd(): protocoldagresults = self._execute_tasks(actioned_tasks, n4js, s3os_server) - # overwrite what's in the object store - for protocoldagresult in protocoldagresults: - pdr_jb = json.dumps( - protocoldagresult.to_dict(), cls=JSON_HANDLER.encoder - ).encode("utf-8") - - location = os.path.join( - "protocoldagresult", - *transformation_sk.scope.to_tuple(), - transformation_sk.gufe_key, - "results", - protocoldagresult.key, - "obj.json", - ) - - s3os_server._store_bytes(location, pdr_jb) + protocoldagresultrefs = self._push_results( + actioned_tasks, protocoldagresults, n4js, s3os_server, legacy=True + ) + assert len(protocoldagresultrefs) == len(protocoldagresults) == 3 # clear local gufe registry of pdr objects # not critical, but ensures we see the objects that are deserialized @@ -1920,8 +1988,6 @@ def test_get_transformation_and_network_results_json( assert set(protocoldagresults_r) == set(protocoldagresults) - pass - def test_get_transformation_and_network_results( self, scope_test, @@ -1950,6 +2016,9 @@ def test_get_transformation_and_network_results( # execute the actioned tasks and push results directly using statestore and object store with tmpdir.as_cwd(): protocoldagresults = self._execute_tasks(actioned_tasks, n4js, s3os_server) + self._push_results( + actioned_tasks, protocoldagresults, n4js, s3os_server, legacy=True + ) # clear local gufe registry of pdr objects # not critical, but ensures we see the objects that are deserialized @@ -2049,6 +2118,7 @@ def test_get_transformation_and_network_failures( # execute the actioned tasks and push results directly using statestore and object store with tmpdir.as_cwd(): protocoldagresults = self._execute_tasks(actioned_tasks, n4js, s3os_server) + self._push_results(actioned_tasks, protocoldagresults, n4js, s3os_server) # clear local gufe registry of pdr objects # not critical, but ensures we see the objects that are deserialized @@ -2122,6 +2192,7 @@ def test_get_task_results( # execute the actioned tasks and push results directly using statestore and object store with tmpdir.as_cwd(): protocoldagresults = self._execute_tasks(actioned_tasks, n4js, s3os_server) + self._push_results(actioned_tasks, protocoldagresults, n4js, s3os_server) # clear local gufe registry of pdr objects # not critical, but ensures we see the objects that are deserialized @@ -2188,6 +2259,7 @@ def test_get_task_failures( # execute the actioned tasks and push results directly using statestore and object store with tmpdir.as_cwd(): protocoldagresults = self._execute_tasks(actioned_tasks, n4js, s3os_server) + self._push_results(actioned_tasks, protocoldagresults, n4js, s3os_server) # clear local gufe registry of pdr objects # not critical, but ensures we see the objects that are deserialized From 63f998234e209e05a0a0ea11841fb03c2cce4dfd Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Mon, 30 Dec 2024 20:18:27 -0700 Subject: [PATCH 111/143] Clear leftover state before testing legacy PDR pull Code coverage was artificially low due to run test run order. A reset and reinitialization of the s3os_server shows the correct results. --- .../tests/integration/compute/client/test_compute_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/alchemiscale/tests/integration/compute/client/test_compute_client.py b/alchemiscale/tests/integration/compute/client/test_compute_client.py index ab23e8ef..09c9081e 100644 --- a/alchemiscale/tests/integration/compute/client/test_compute_client.py +++ b/alchemiscale/tests/integration/compute/client/test_compute_client.py @@ -306,6 +306,7 @@ def test_set_task_result( assert transformation2 == transformation_ assert extends_protocoldagresult2 == protocoldagresults[0] + # TODO: Remove in next major release where old to_dict protocoldagresults storage is removed def test_set_task_result_legacy( self, scope_test, @@ -316,8 +317,9 @@ def test_set_task_result_legacy( transformation, protocoldagresults, uvicorn_server, - s3os_server, + s3os_server_fresh, ): + s3os_server = s3os_server_fresh # register compute service id compute_client.register(compute_service_id) From 6985ca7d1c8dfa7c9c20168036a4d3265541c36e Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 31 Dec 2024 11:04:20 -0700 Subject: [PATCH 112/143] Parameterize test_get_transformation_and_network_results It's more robust to paramterize the old tests to use the legacy kwarg for pushing results rather than writing a new test that covers less of the codebase. --- .../interface/client/test_client.py | 59 +++---------------- 1 file changed, 9 insertions(+), 50 deletions(-) diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index 2333ee68..f53f7f5a 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -1887,7 +1887,7 @@ def _push_result(task_scoped_key, protocoldagresult, n4js, s3os_server): ) return protocoldagresultref - # TODO: remove in next major version when to_dict json is no longer supported + # TODO: remove in next major version when to_dict json storage is no longer supported @staticmethod def _push_result_legacy(task_scoped_key, protocoldagresult, n4js, s3os_server): transformation_scoped_key, _ = n4js.get_task_transformation( @@ -1925,9 +1925,10 @@ def _push_result_legacy(task_scoped_key, protocoldagresult, n4js, s3os_server): return protocoldagresultref + # TODO: remove legacy kwarg when to_dict json storage is no longer supported @staticmethod def _push_results( - task_scoped_keys, protocoldagresults, n4js, s3os_server, legacy=True + task_scoped_keys, protocoldagresults, n4js, s3os_server, legacy=False ): push_function = ( TestClient._push_result_legacy if legacy else TestClient._push_result @@ -1942,62 +1943,20 @@ def _push_results( protocoldagresultrefs.append(protocoldagresultref) return protocoldagresultrefs - def test_get_transformation_and_network_results_json( - self, - scope_test, - n4js_preloaded, - s3os_server, - user_client: client.AlchemiscaleClient, - network_tyk2, - tmpdir, - ): - n4js = n4js_preloaded - - # select the transformation we want to compute - an = network_tyk2 - transformation = list(t for t in an.edges if "_solvent" in t.name)[0] - - network_sk = user_client.get_scoped_key(an, scope_test) - transformation_sk = user_client.get_scoped_key(transformation, scope_test) - - # user client : create three independent tasks for the transformation - user_client.create_tasks(transformation_sk, count=3) - - # user client : action the tasks for execution - all_tasks = user_client.get_transformation_tasks(transformation_sk) - actioned_tasks = user_client.action_tasks(all_tasks, network_sk) - - # execute the actioned tasks and push results directly using statestore and object store - with tmpdir.as_cwd(): - protocoldagresults = self._execute_tasks(actioned_tasks, n4js, s3os_server) - protocoldagresultrefs = self._push_results( - actioned_tasks, protocoldagresults, n4js, s3os_server, legacy=True - ) - assert len(protocoldagresultrefs) == len(protocoldagresults) == 3 - - # clear local gufe registry of pdr objects - # not critical, but ensures we see the objects that are deserialized - # instead of our instances already in memory post-pull - for pdr in protocoldagresults: - TOKENIZABLE_REGISTRY.pop(pdr.key, None) - - # get back protocoldagresults instead - protocoldagresults_r = user_client.get_transformation_results( - transformation_sk, return_protocoldagresults=True - ) - - assert set(protocoldagresults_r) == set(protocoldagresults) - + # TODO: remove mark and legacy parameter when to_dict json storage is no longer supported + @pytest.mark.parametrize("legacy", [True, False]) def test_get_transformation_and_network_results( self, scope_test, n4js_preloaded, - s3os_server, + s3os_server_fresh, user_client: client.AlchemiscaleClient, network_tyk2, tmpdir, + legacy, ): n4js = n4js_preloaded + s3os_server = s3os_server_fresh # select the transformation we want to compute an = network_tyk2 @@ -2017,7 +1976,7 @@ def test_get_transformation_and_network_results( with tmpdir.as_cwd(): protocoldagresults = self._execute_tasks(actioned_tasks, n4js, s3os_server) self._push_results( - actioned_tasks, protocoldagresults, n4js, s3os_server, legacy=True + actioned_tasks, protocoldagresults, n4js, s3os_server, legacy=legacy ) # clear local gufe registry of pdr objects From c468b43cf56174ffda38f4bc0463d4c6311e5f3b Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 2 Jan 2025 22:15:21 -0700 Subject: [PATCH 113/143] statestore edits from review --- alchemiscale/storage/statestore.py | 55 ++++++++++++++++-------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 39c4a515..bc37a0ea 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -1443,11 +1443,12 @@ def action_tasks( # so we can properly return `None` if needed task_map = {str(task): None for task in tasks} - tasks_scoped_keys = [str(task) for task in tasks if task is not None] + task_scoped_keys = [str(task) for task in tasks if task is not None] + q = """ // get our TaskHub - UNWIND $tasks as task_sk - MATCH (th:TaskHub {{_scoped_key: $taskhub}})-[:PERFORMS]->(an:AlchemicalNetwork) + UNWIND $task_scoped_keys as task_sk + MATCH (th:TaskHub {{_scoped_key: $taskhub_scoped_key}})-[:PERFORMS]->(an:AlchemicalNetwork) // get the task we want to add to the hub; check that it connects to same network MATCH (task:Task {_scoped_key: task_sk})-[:PERFORMS]->(:Transformation|NonTransformation)<-[:DEPENDS_ON]-(an) @@ -1481,8 +1482,8 @@ def action_tasks( results = self.execute_query( q, - tasks=tasks_scoped_keys, - taskhub=str(taskhub), + task_scoped_keys=task_scoped_keys, + taskhub_scoped_key=str(taskhub), waiting=TaskStatusEnum.waiting.value, running=TaskStatusEnum.running.value, error=TaskStatusEnum.error.value, @@ -2538,17 +2539,17 @@ def add_protocol_dag_result_ref_tracebacks( source_keys.append(puf.source_key) tracebacks.append(puf.traceback) - traceback = Tracebacks(tracebacks, source_keys, failure_keys) + tracebacks = Tracebacks(tracebacks, source_keys, failure_keys) - _, traceback_node, _ = self._gufe_to_subgraph( - traceback.to_shallow_dict(), - labels=["GufeTokenizable", traceback.__class__.__name__], - gufe_key=traceback.key, + _, tracebacks_node, _ = self._gufe_to_subgraph( + tracebacks.to_shallow_dict(), + labels=["GufeTokenizable", tracebacks.__class__.__name__], + gufe_key=tracebacks.key, scope=protocol_dag_result_ref_scoped_key.scope, ) subgraph |= Relationship.type("DETAILS")( - traceback_node, + tracebacks_node, protocol_dag_result_ref_node, ) @@ -2720,7 +2721,7 @@ def set_task_complete( WITH scoped_key, t, t_ // if we changed the status to complete, - // drop all ACTIONS relationships + // drop all taskhub ACTIONS and task restart APPLIES relationships OPTIONAL MATCH (t_)<-[ar:ACTIONS]-(th:TaskHub) OPTIONAL MATCH (t_)<-[applies:APPLIES]-(:TaskRestartPattern) DELETE ar @@ -2806,12 +2807,14 @@ def set_task_invalid( WITH scoped_key, t, t_, extends_task OPTIONAL MATCH (t_)<-[ar:ACTIONS]-(th:TaskHub) - OPTIONAL MATCH (extends_task)<-[are:ACTIONS]-(th:TaskHub) + OPTIONAL MATCH (extends_task)<-[ar_e:ACTIONS]-(th:TaskHub) OPTIONAL MATCH (t_)<-[applies:APPLIES]-(:TaskRestartPattern) + OPTIONAL MATCH (extends_task)<-[applies_e:APPLIES]-(:TaskRestartPattern) DELETE ar - DELETE are + DELETE ar_e DELETE applies + DELETE applies_e WITH scoped_key, t, t_ @@ -2858,12 +2861,14 @@ def set_task_deleted( WITH scoped_key, t, t_, extends_task OPTIONAL MATCH (t_)<-[ar:ACTIONS]-(th:TaskHub) - OPTIONAL MATCH (extends_task)<-[are:ACTIONS]-(th:TaskHub) + OPTIONAL MATCH (extends_task)<-[ar_e:ACTIONS]-(th:TaskHub) OPTIONAL MATCH (t_)<-[applies:APPLIES]-(:TaskRestartPattern) + OPTIONAL MATCH (extends_task)<-[applies_e:APPLIES]-(:TaskRestartPattern) DELETE ar - DELETE are + DELETE ar_e DELETE applies + DELETE applies_e WITH scoped_key, t, t_ @@ -3055,9 +3060,9 @@ def get_task_restart_patterns( Returns ------- dict[ScopedKey, set[tuple[str, int]]] - A dictionary containing whose keys are the ScopedKeys of the TaskHubs provided and whose - values are a set of tuples containing the patterns enforcing each TaskHub along with their - associated maximum number of retries. + A dictionary with ScopedKeys of the TaskHubs provided as keys, and a + set of tuples containing the patterns enforcing each TaskHub along + with their associated maximum number of retries as values. """ q = """ @@ -3167,26 +3172,26 @@ def resolve_task_restarts(self, task_scoped_keys: Iterable[ScopedKey], *, tx=Non cancel_map[task_taskhub_tuple] = False increment_query = """ - UNWIND $trp_and_task_pairs as pairs + UNWIND $task_trp_pairs as pairs WITH pairs[0] as task_scoped_key, pairs[1] as task_restart_pattern_scoped_key MATCH (:Task {`_scoped_key`: task_scoped_key})<-[app:APPLIES]-(:TaskRestartPattern {`_scoped_key`: task_restart_pattern_scoped_key}) SET app.num_retries = app.num_retries + 1 """ - tx.run(increment_query, trp_and_task_pairs=to_increment) + tx.run(increment_query, task_trp_pairs=to_increment) - # cancel all tasks that didn't trigger any restart patterns (None) - # or exceeded a patterns max_retries value (True) + # cancel all tasks (from a taskhub) that didn't trigger any restart patterns (None) + # or exceeded a pattern's max_retries value (True) cancel_groups: defaultdict[str, list[str]] = defaultdict(list) for task_taskhub_pair in all_task_taskhub_pairs: cancel_result = cancel_map.get(task_taskhub_pair) - if cancel_result is True or cancel_result is None: + if cancel_result in (True, None): cancel_groups[task_taskhub_pair[1]].append(task_taskhub_pair[0]) for taskhub, tasks in cancel_groups.items(): self.cancel_tasks(tasks, taskhub, tx=tx) - # any remaining tasks must then be okay to switch to waiting + # any tasks that are still associated with a TaskHub and a TaskRestartPattern must then be okay to switch to waiting renew_waiting_status_query = """ UNWIND $task_scoped_keys AS task_scoped_key MATCH (task:Task {status: $error, `_scoped_key`: task_scoped_key})<-[app:APPLIES]-(trp:TaskRestartPattern)-[:ENFORCES]->(taskhub:TaskHub) From bb5dbcd80d4605702eb7593a0389c0183874e771 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 2 Jan 2025 22:23:13 -0700 Subject: [PATCH 114/143] Tracebacks model doc fix --- alchemiscale/storage/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index 96920a78..cd50b004 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -207,14 +207,14 @@ class Tracebacks(GufeTokenizable): ---------- tracebacks: list[str] The tracebacks returned with the ProtocolUnitFailures. - source_keys:list[ScopedKey] - The ScopedKeys of the Protocols that failed. - failure_keys: list[ScopedKey] - The ScopedKeys of the ProtocolUnitFailures. + source_keys: list[GufeKey] + The GufeKeys of the ProtocolUnits that failed. + failure_keys: list[GufeKey] + The GufeKeys of the ProtocolUnitFailures. """ def __init__( - self, tracebacks: List[str], source_keys: List[str], failure_keys: List[str] + self, tracebacks: List[str], source_keys: List[GufeKey], failure_keys: List[GufeKey] ): value_error = ValueError( "`tracebacks` must be a non-empty list of string values" From 3776c7a761cf3a75abed0843196f7cec29316979 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 2 Jan 2025 22:27:41 -0700 Subject: [PATCH 115/143] Consistency fix to TaskRestartPattern._defaults --- alchemiscale/storage/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index cd50b004..82cc0f77 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -181,7 +181,7 @@ def _gufe_tokenize(self): @classmethod def _defaults(cls): - raise NotImplementedError + return super()._defaults() @classmethod def _from_dict(cls, dct): From b4865fd54838b5307d676771c33ba2c42912cb1f Mon Sep 17 00:00:00 2001 From: David Dotson Date: Fri, 3 Jan 2025 11:09:01 -0700 Subject: [PATCH 116/143] Docstring updates to client; token validation to interface api restart pattern endpoints --- alchemiscale/interface/api.py | 83 ++++++++++++++++++------------ alchemiscale/interface/client.py | 78 +++++++++++++++------------- alchemiscale/storage/statestore.py | 2 +- 3 files changed, 92 insertions(+), 71 deletions(-) diff --git a/alchemiscale/interface/api.py b/alchemiscale/interface/api.py index e2c09701..db214d6a 100644 --- a/alchemiscale/interface/api.py +++ b/alchemiscale/interface/api.py @@ -953,20 +953,46 @@ def get_task_status( return status[0].value -@router.post("/networks/{network_scoped_key}/restartpolicy/add") +@router.get("/tasks/{task_scoped_key}/transformation") +def get_task_transformation( + task_scoped_key, + *, + n4js: Neo4jStore = Depends(get_n4js_depends), + token: TokenData = Depends(get_token_data_depends), +): + sk = ScopedKey.from_str(task_scoped_key) + validate_scopes(sk.scope, token) + + transformation: ScopedKey + + try: + transformation, _ = n4js.get_task_transformation( + task=task_scoped_key, + return_gufe=False, + ) + except KeyError as e: + raise HTTPException(status_code=http_status.HTTP_400_BAD_REQUEST, detail=str(e)) + + return str(transformation) + + +@router.post("/networks/{network_scoped_key}/restartpatterns/add") def add_task_restart_patterns( network_scoped_key: str, *, patterns: list[str] = Body(embed=True), - number_of_retries: int = Body(embed=True), + num_allowed_restarts: int = Body(embed=True), n4js: Neo4jStore = Depends(get_n4js_depends), token: TokenData = Depends(get_token_data_depends), ): - taskhub_scoped_key = n4js.get_taskhub(ScopedKey.from_str(network_scoped_key)) - n4js.add_task_restart_patterns(taskhub_scoped_key, patterns, number_of_retries) + sk = ScopedKey.from_str(network_scoped_key) + validate_scopes(sk.scope, token) + taskhub_scoped_key = n4js.get_taskhub(sk) + n4js.add_task_restart_patterns(taskhub_scoped_key, patterns, num_allowed_restarts) -@router.post("/networks/{network_scoped_key}/restartpolicy/remove") + +@router.post("/networks/{network_scoped_key}/restartpatterns/remove") def remove_task_restart_patterns( network_scoped_key: str, *, @@ -974,23 +1000,29 @@ def remove_task_restart_patterns( n4js: Neo4jStore = Depends(get_n4js_depends), token: TokenData = Depends(get_token_data_depends), ): - taskhub_scoped_key = n4js.get_taskhub(ScopedKey.from_str(network_scoped_key)) + sk = ScopedKey.from_str(network_scoped_key) + validate_scopes(sk.scope, token) + + taskhub_scoped_key = n4js.get_taskhub(sk) n4js.remove_task_restart_patterns(taskhub_scoped_key, patterns) -@router.get("/networks/{network_scoped_key}/restartpolicy/clear") +@router.get("/networks/{network_scoped_key}/restartpatterns/clear") def clear_task_restart_patterns( network_scoped_key: str, *, n4js: Neo4jStore = Depends(get_n4js_depends), token: TokenData = Depends(get_token_data_depends), ): - taskhub_scoped_key = n4js.get_taskhub(ScopedKey.from_str(network_scoped_key)) + sk = ScopedKey.from_str(network_scoped_key) + validate_scopes(sk.scope, token) + + taskhub_scoped_key = n4js.get_taskhub(sk) n4js.clear_task_restart_patterns(taskhub_scoped_key) return [network_scoped_key] -@router.post("/bulk/networks/restartpolicy/get") +@router.post("/bulk/networks/restartpatterns/get") def get_task_restart_patterns( *, networks: list[str] = Body(embed=True), @@ -999,6 +1031,9 @@ def get_task_restart_patterns( ) -> dict[str, set[tuple[str, int]]]: network_scoped_keys = [ScopedKey.from_str(network) for network in networks] + for sk in network_scoped_keys: + validate_scopes(sk.scope, token) + taskhub_scoped_keys = n4js.get_taskhubs(network_scoped_keys) taskhub_network_map = { @@ -1018,7 +1053,7 @@ def get_task_restart_patterns( return as_str -@router.post("/networks/{network_scoped_key}/restartpolicy/maxretries") +@router.post("/networks/{network_scoped_key}/restartpatterns/maxretries") def set_task_restart_patterns_max_retries( network_scoped_key: str, *, @@ -1027,35 +1062,15 @@ def set_task_restart_patterns_max_retries( n4js: Neo4jStore = Depends(get_n4js_depends), token: TokenData = Depends(get_token_data_depends), ): - taskhub_scoped_key = n4js.get_taskhub(ScopedKey.from_str(network_scoped_key)) + sk = ScopedKey.from_str(network_scoped_key) + validate_scopes(sk.scope, token) + + taskhub_scoped_key = n4js.get_taskhub(sk) n4js.set_task_restart_patterns_max_retries( taskhub_scoped_key, patterns, max_retries ) -@router.get("/tasks/{task_scoped_key}/transformation") -def get_task_transformation( - task_scoped_key, - *, - n4js: Neo4jStore = Depends(get_n4js_depends), - token: TokenData = Depends(get_token_data_depends), -): - sk = ScopedKey.from_str(task_scoped_key) - validate_scopes(sk.scope, token) - - transformation: ScopedKey - - try: - transformation, _ = n4js.get_task_transformation( - task=task_scoped_key, - return_gufe=False, - ) - except KeyError as e: - raise HTTPException(status_code=http_status.HTTP_400_BAD_REQUEST, detail=str(e)) - - return str(transformation) - - ### results diff --git a/alchemiscale/interface/client.py b/alchemiscale/interface/client.py index e476180b..72a93ef9 100644 --- a/alchemiscale/interface/client.py +++ b/alchemiscale/interface/client.py @@ -1602,7 +1602,6 @@ def get_transformation_results( visualize If ``True``, show retrieval progress indicators. - """ if not return_protocoldagresults: @@ -1746,48 +1745,51 @@ def add_task_restart_patterns( patterns: list[str], num_allowed_restarts: int, ) -> ScopedKey: - """Add a list of restart patterns to an `AlchemicalNetwork`. + """Add a list of `Task` restart patterns to an `AlchemicalNetwork`. Parameters ---------- - network_scoped_key: ScopedKey - The ScopedKey for the AlchemicalNetwork to add the patterns to. - patterns: list[str] - The regular expression strings to compare to ProtocolUnitFailure tracebacks. - Matching patterns will set the Task status back to 'waiting'. - num_allowed_restarts: int - The number of times each pattern will be able to restart each `Task`. When - this number is exceeded, the `Task` is canceled from the `AlchemicalNetwork` - and left with the `error` status. + network_scoped_key + The `ScopedKey` for the `AlchemicalNetwork` to add the patterns to. + patterns + The regular expression strings to compare to `ProtocolUnitFailure` + tracebacks. Matching patterns will set the `Task` status back to + 'waiting'. + num_allowed_restarts + The number of times each pattern will be able to restart each + `Task`. When this number is exceeded, the `Task` is canceled from + the `AlchemicalNetwork` and left with the `error` status. Returns ------- - network_scoped_key: ScopedKey - The ScopedKey of the AlchemicalNetwork the patterns were added to. + network_scoped_key + The `ScopedKey` of the `AlchemicalNetwork` the patterns were added to. """ - data = {"patterns": patterns, "number_of_retries": num_allowed_restarts} - self._post_resource(f"/networks/{network_scoped_key}/restartpolicy/add", data) + data = {"patterns": patterns, "num_allowed_restarts": num_allowed_restarts} + self._post_resource(f"/networks/{network_scoped_key}/restartpatterns/add", data) return network_scoped_key def get_task_restart_patterns( self, network_scoped_key: ScopedKey ) -> dict[str, int]: - """Get the Task restart patterns enforcing an AlchemicalNetwork along with the number of retries allowed for each pattern. + """Get the `Task` restart patterns applied to an `AlchemicalNetwork` + along with the number of retries allowed for each pattern. Parameters ---------- - network_scoped_key: ScopedKey - The ScopedKey of the AlchemicalNetwork to query. + network_scoped_key + The `ScopedKey` of the `AlchemicalNetwork` to query. Returns ------- - patterns : dict[str, int] - A dictionary whose keys are all of the patterns enforcing the `AlchemicalNetwork` and whose - values are the number of retries each pattern will allow. + patterns + A dictionary whose keys are all of the patterns applied to the + `AlchemicalNetwork` and whose values are the number of retries each + pattern will allow. """ data = {"networks": [str(network_scoped_key)]} mapped_patterns = self._post_resource( - "/bulk/networks/restartpolicy/get", data=data + "/bulk/networks/restartpatterns/get", data=data ) network_patterns = mapped_patterns[str(network_scoped_key)] patterns_with_retries = {pattern: retry for pattern, retry in network_patterns} @@ -1799,37 +1801,40 @@ def set_task_restart_patterns_allowed_restarts( patterns: list[str], num_allowed_restarts: int, ) -> None: - """Set the number of allowed restarts that patterns allowed to perform within an AlchemicalNetwork. + """Set the number of `Task` restarts that patterns are allowed to + perform for the given `AlchemicalNetwork`. Parameters ---------- - network_scoped_key : ScopedKey - The ScopedKey of the `AlchemicalNetwork` enforced by `patterns`. - patterns: list[str] + network_scoped_key + The `ScopedKey` of the `AlchemicalNetwork` the `patterns` are + applied to. + patterns The patterns to set the number of allowed restarts for. - num_allowed_restarts : int + num_allowed_restarts The new number of allowed restarts. """ data = {"patterns": patterns, "max_retries": num_allowed_restarts} self._post_resource( - f"/networks/{network_scoped_key}/restartpolicy/maxretries", data + f"/networks/{network_scoped_key}/restartpatterns/maxretries", data ) def remove_task_restart_patterns( self, network_scoped_key: ScopedKey, patterns: list[str] ) -> None: - """Remove specific patterns from an `AlchemicalNetwork`. + """Remove specific `Task` restart patterns from an `AlchemicalNetwork`. Parameters ---------- - network_scoped_key : ScopedKey - The ScopedKey of the `AlchemicalNetwork` enforced by `patterns`. - patterns: list[str] + network_scoped_key + The `ScopedKey` of the `AlchemicalNetwork` the `patterns` are + applied to. + patterns The patterns to remove from the `AlchemicalNetwork`. """ data = {"patterns": patterns} self._post_resource( - f"/networks/{network_scoped_key}/restartpolicy/remove", data + f"/networks/{network_scoped_key}/restartpatterns/remove", data ) def clear_task_restart_patterns(self, network_scoped_key: ScopedKey) -> None: @@ -1837,7 +1842,8 @@ def clear_task_restart_patterns(self, network_scoped_key: ScopedKey) -> None: Parameters ---------- - network_scoped_key : ScopedKey - The ScopeKey of the `AlchemicalNetwork` to be cleared of restart patterns. + network_scoped_key + The `ScopedKey` of the `AlchemicalNetwork` to be cleared of restart + patterns. """ - self._query_resource(f"/networks/{network_scoped_key}/restartpolicy/clear") + self._query_resource(f"/networks/{network_scoped_key}/restartpatterns/clear") diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index bc37a0ea..88c630ad 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -2884,7 +2884,7 @@ def err_msg(t, status): return self._set_task_status(tasks, q, err_msg, raise_error=raise_error) - ## task restart policy + ## task restart policies def add_task_restart_patterns( self, taskhub: ScopedKey, patterns: list[str], number_of_retries: int From 893a790bdad17de998a35284446704d92fc224b4 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Fri, 3 Jan 2025 12:27:35 -0700 Subject: [PATCH 117/143] Review edits --- alchemiscale/interface/api.py | 8 +++----- alchemiscale/storage/models.py | 11 +++++------ alchemiscale/storage/statestore.py | 3 ++- alchemiscale/tests/unit/test_storage_models.py | 5 ++++- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/alchemiscale/interface/api.py b/alchemiscale/interface/api.py index db214d6a..62175e8f 100644 --- a/alchemiscale/interface/api.py +++ b/alchemiscale/interface/api.py @@ -1045,12 +1045,10 @@ def get_task_restart_patterns( restart_patterns = n4js.get_task_restart_patterns(taskhub_scoped_keys) - as_str = {} - for key, value in restart_patterns.items(): - network_scoped_key = taskhub_network_map[key] - as_str[str(network_scoped_key)] = value + network_patterns = {str(taskhub_network_map[key]): value + for key, value in restart_patterns.items()} - return as_str + return network_patterns @router.post("/networks/{network_scoped_key}/restartpatterns/maxretries") diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index 82cc0f77..5b0acc15 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -217,15 +217,14 @@ def __init__( self, tracebacks: List[str], source_keys: List[GufeKey], failure_keys: List[GufeKey] ): value_error = ValueError( - "`tracebacks` must be a non-empty list of string values" + "`tracebacks` must be a non-empty list of non-empty string values" ) if not isinstance(tracebacks, list) or tracebacks == []: raise value_error - else: - # in the case where tracebacks is not an iterable, this will raise a TypeError - all_string_values = all([isinstance(value, str) for value in tracebacks]) - if not all_string_values or "" in tracebacks: - raise value_error + + all_string_values = all([isinstance(value, str) for value in tracebacks]) + if not all_string_values or "" in tracebacks: + raise value_error # TODO: validate self.tracebacks = tracebacks diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 88c630ad..a53a5da2 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -3050,7 +3050,8 @@ def set_task_restart_patterns_max_retries( def get_task_restart_patterns( self, taskhubs: list[ScopedKey] ) -> dict[ScopedKey, set[tuple[str, int]]]: - """For a list of TaskHub ScopedKeys, get the associated restart patterns along with the maximum number of retries for each pattern. + """For a list of TaskHub ScopedKeys, get the associated restart + patterns along with the maximum number of retries for each pattern. Parameters ---------- diff --git a/alchemiscale/tests/unit/test_storage_models.py b/alchemiscale/tests/unit/test_storage_models.py index f6916430..e7dc1ae5 100644 --- a/alchemiscale/tests/unit/test_storage_models.py +++ b/alchemiscale/tests/unit/test_storage_models.py @@ -136,6 +136,8 @@ def test_from_dict(self): assert trp_reconstructed.max_retries == original_max_retries assert trp_reconstructed.taskhub_scoped_key == original_taskhub_scoped_key + assert trp_orig is trp_reconstructed + class TestTracebacks(object): @@ -146,7 +148,7 @@ class TestTracebacks(object): "ProtocolUnitFailure-DEF456", "ProtocolUnitFailure-GHI789", ] - tracebacks_value_error = "`tracebacks` must be a non-empty list of string values" + tracebacks_value_error = "`tracebacks` must be a non-empty list of non-empty string values" def test_empty_string_element(self): with pytest.raises(ValueError, match=self.tracebacks_value_error): @@ -203,3 +205,4 @@ def test_from_dict(self): tb_reconstructed: TaskRestartPattern = TaskRestartPattern.from_dict(tb_dict) assert tb_reconstructed.tracebacks == self.valid_entry + tb_orig is tb_reconstructed From 27875276e467ff9798df4284d4e9bb96772c20a3 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Sun, 19 Jan 2025 21:25:04 -0700 Subject: [PATCH 118/143] Edits from review --- .../integration/interface/client/test_client.py | 6 ++++-- .../tests/integration/storage/test_statestore.py | 12 +++--------- alchemiscale/tests/integration/storage/utils.py | 5 +---- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index 74e696b3..1b3d4eac 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -2184,12 +2184,14 @@ def test_add_task_restart_patterns( assert len(results.records) == 3 - patterns_list = self.default_patterns[:] + patterns_list = list(self.default_patterns) for record in results.records: trp = record["trp"] assert trp["pattern"] in patterns_list patterns_list.remove(trp["pattern"]) + assert len(patterns_list) == 0 + def test_get_task_restart_patterns( self, user_client: client.AlchemiscaleClient, @@ -2223,7 +2225,7 @@ def test_remove_task_restart_patterns( # check that we have the expected 3 restart patterns assert user_client.get_task_restart_patterns(network_scoped_key) == expected - pattern_to_remove = next(expected.__iter__()) + pattern_to_remove = next(iter(expected)) user_client.remove_task_restart_patterns( network_scoped_key, [pattern_to_remove] ) diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index b0a5b063..6e82c8b4 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -2145,16 +2145,10 @@ def test_task_status_change(self, n4js, network_tyk2, scope_test, status): assert len(results.records) == 1 - target_method = { - "complete": n4js.set_task_complete, - "invalid": n4js.set_task_invalid, - "deleted": n4js.set_task_deleted, - } - if status == "complete": n4js.set_task_running(task_scoped_keys) - assert target_method[status](task_scoped_keys)[0] is not None + assert n4js.set_task_status(task_scoped_keys)[0] is not None query = """ MATCH (:TaskRestartPattern)-[:APPLIES]->(task:Task) @@ -2187,6 +2181,7 @@ def test_add_task_restart_patterns(self, n4js, network_tyk2, scope_test): n4js.action_tasks(task_sks, taskhub_scoped_key) taskhub_sks.append(taskhub_scoped_key) + # test a shared pattern with and without shared number of restarts # this will create 6 unique patterns for network_index in range(3): @@ -2235,7 +2230,7 @@ def test_add_task_restart_patterns(self, n4js, network_tyk2, scope_test): records = n4js.execute_query(applies_query).records - ### one record per taskhub, each with six num_applied + ### one record per taskhub with tasks actioned, each with six num_applied assert len(records) == 2 assert records[0]["num_applied"] == records[1]["num_applied"] == 6 @@ -2398,7 +2393,6 @@ def test_get_task_restart_patterns(self, n4js, network_tyk2, scope_test): def test_resolve_task_restarts( self, - scope_test: Scope, n4js_task_restart_policy: Neo4jStore, ): n4js = n4js_task_restart_policy diff --git a/alchemiscale/tests/integration/storage/utils.py b/alchemiscale/tests/integration/storage/utils.py index 40514a53..520e25c0 100644 --- a/alchemiscale/tests/integration/storage/utils.py +++ b/alchemiscale/tests/integration/storage/utils.py @@ -15,10 +15,7 @@ def tasks_are_not_actioned_on_taskhub( actioned_tasks = n4js.get_taskhub_actioned_tasks([taskhub_scoped_key]) - for task in task_scoped_keys: - if task in actioned_tasks[0].keys(): - return False - return True + return set(actioned_tasks[0].keys()).isdisjoint(set(task_scoped_keys)) def tasks_are_errored(n4js: Neo4jStore, task_scoped_keys: list[ScopedKey]) -> bool: From 7ba1b4ffe0c765937a68fc643789541cd0e8b586 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Sun, 19 Jan 2025 21:25:25 -0700 Subject: [PATCH 119/143] Black! --- alchemiscale/interface/api.py | 5 +++-- alchemiscale/storage/models.py | 5 ++++- alchemiscale/tests/unit/test_storage_models.py | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/alchemiscale/interface/api.py b/alchemiscale/interface/api.py index 62175e8f..a5211010 100644 --- a/alchemiscale/interface/api.py +++ b/alchemiscale/interface/api.py @@ -1045,8 +1045,9 @@ def get_task_restart_patterns( restart_patterns = n4js.get_task_restart_patterns(taskhub_scoped_keys) - network_patterns = {str(taskhub_network_map[key]): value - for key, value in restart_patterns.items()} + network_patterns = { + str(taskhub_network_map[key]): value for key, value in restart_patterns.items() + } return network_patterns diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index 5b0acc15..844e2ebf 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -214,7 +214,10 @@ class Tracebacks(GufeTokenizable): """ def __init__( - self, tracebacks: List[str], source_keys: List[GufeKey], failure_keys: List[GufeKey] + self, + tracebacks: List[str], + source_keys: List[GufeKey], + failure_keys: List[GufeKey], ): value_error = ValueError( "`tracebacks` must be a non-empty list of non-empty string values" diff --git a/alchemiscale/tests/unit/test_storage_models.py b/alchemiscale/tests/unit/test_storage_models.py index e7dc1ae5..4d1bd720 100644 --- a/alchemiscale/tests/unit/test_storage_models.py +++ b/alchemiscale/tests/unit/test_storage_models.py @@ -148,7 +148,9 @@ class TestTracebacks(object): "ProtocolUnitFailure-DEF456", "ProtocolUnitFailure-GHI789", ] - tracebacks_value_error = "`tracebacks` must be a non-empty list of non-empty string values" + tracebacks_value_error = ( + "`tracebacks` must be a non-empty list of non-empty string values" + ) def test_empty_string_element(self): with pytest.raises(ValueError, match=self.tracebacks_value_error): From 0220e001d1684a3856ff44c151efd834887b9b10 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Sun, 19 Jan 2025 21:36:37 -0700 Subject: [PATCH 120/143] User guide fixes, consistency edits --- docs/user_guide.rst | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 1defe1d5..24b2642a 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -514,42 +514,42 @@ If you’re feeling confident, you could set all errored :py:class:`~alchemiscal Re-running Errored Tasks with Task Restart Patterns *************************************************** -Re-running errored :py:class`~alchemiscale.storage.models.Task`\s manually for known failure modes (such as those described in the previous section) quickly becomes tedious, especially for large networks. -Alternatively, you can add `regular expression `_ strings as Task restart patterns to an :external+gufe:py:class`~gufe.network.AlchemicalNetwork`. -These patterns _enforce_ the :external+gufe:py:class`~gufe.network.AlchemicalNetwork` and there is no limit to the number of patterns that can enforce an :external+gufe:py:class`~gufe.network.AlchemicalNetwork`. -As a result, :py:class`~alchemiscale.storage.models.Task`\s actioned on that :external+gufe:py:class`~gufe.network.AlchemicalNetwork` now support automatic restarts if the :py:class`~alchemiscale.storage.models.Task` fails during any part of its execution, provided that an enforcing pattern matches a traceback returned by any of the :py:class`~alchemiscale.storage.models.Task`\'s returned :external+gufe:py:class`~gufe.protocols.ProtocolUnitFailure`\s. +Re-running errored :py:class:`~alchemiscale.storage.models.Task`\s manually for known failure modes (such as those described in the previous section) quickly becomes tedious, especially for large networks. +Alternatively, you can add `regular expression `_ strings as :py:class:`~alchemiscale.storage.models.Task` restart patterns to an :external+gufe:py:class:`~gufe.network.AlchemicalNetwork`. +:py:class:`~alchemiscale.storage.models.Task`\s actioned on that :external+gufe:py:class:`~gufe.network.AlchemicalNetwork` will be automatically restarted if the :py:class:`~alchemiscale.storage.models.Task` fails during any part of its execution, provided that an enforcing pattern matches a traceback within the :py:class:`~alchemiscale.storage.models.Task`\'s failed :external+gufe:py:class:`~gufe.protocols.ProtocolDAGResult`. The number of restarts is controlled by the ``num_allowed_restarts`` parameter of the :py:meth:`~alchemiscale.interface.client.AlchemiscaleClient.add_task_restart_patterns` method. -If a :py:class`~alchemiscale.storage.models.Task` is restarted more than ``num_allowed_restarts`` times, the :py:class`~alchemiscale.storage.models.Task` is canceled and left with an ``error`` status. -As an example, if you wanted to rerun any :py:class`~alchemiscale.storage.models.Task` that failed with a ``RuntimeError`` _or_ a ``MemoryError`` and attempt it at least 5 times, you could add the following patterns::: +If a :py:class:`~alchemiscale.storage.models.Task` is restarted more than ``num_allowed_restarts`` times, the :py:class:`~alchemiscale.storage.models.Task` is canceled on that :external+gufe:py:class:`~gufe.network.AlchemicalNetwork` and left in an ``error`` status. - >>> asc.add_task_restart_patterns(network_scoped_key, [r"RuntimeError: .+", r"MemoryError: Unable to allocate \d+ GiB"], 5) +As an example, if you wanted to rerun any :py:class:`~alchemiscale.storage.models.Task` that failed with a ``RuntimeError`` or a ``MemoryError`` and attempt it at least 5 times, you could add the following patterns:: + + >>> asc.add_task_restart_patterns(an_sk, [r"RuntimeError: .+", r"MemoryError: Unable to allocate \d+ GiB"], 5) Providing too general a pattern, such as the example above, you may consume compute resources on failures that are unavoidable. -On the other hand, an overly strict pattern (such as specifying explicit Gufe keys) will likely do nothing. -Therefore, it is best to find a balance in your patterns that matches your use-case. +On the other hand, an overly strict pattern (such as specifying explicit ``gufe`` keys) will likely do nothing. +Therefore, it is best to find a balance in your patterns that matches your use case. -Restart patterns enforcing an :external+gufe:py:class`~gufe.network.AlchemicalNetwork` can be retrieved with:: +Restart patterns enforcing an :external+gufe:py:class:`~gufe.network.AlchemicalNetwork` can be retrieved with:: - >>> asc.get_task_restart_patterns(network_scoped_key) + >>> asc.get_task_restart_patterns(an_sk) {"RuntimeError: .+": 5, "MemoryError: Unable to allocate \d+ GiB": 5} -The number of allowed restarts can be modified:: +The number of allowed restarts can also be modified:: - >>> asc.set_task_restart_patterns_allowed_restarts(network_scoped_key, ["RuntimeError: .+"], 3) - >>> asc.set_task_restart_patterns_allowed_restarts(network_scoped_key, ["MemoryError: Unable to allocate \d+ GiB"], 2) - >>> asc.get_task_restart_patterns(network_scoped_key) + >>> asc.set_task_restart_patterns_allowed_restarts(an_sk, ["RuntimeError: .+"], 3) + >>> asc.set_task_restart_patterns_allowed_restarts(an_sk, ["MemoryError: Unable to allocate \d+ GiB"], 2) + >>> asc.get_task_restart_patterns(an_sk) {"RuntimeError: .+": 3, "MemoryError: Unable to allocate \d+ GiB": 2} Patterns can be removed by specifying the patterns in a list:: - >>> asc.remove_task_restart_patterns(network_scoped_key, ["MemoryError: Unable to allocate \d+ GiB"]) - >>> asc.get_task_restart_patterns(network_scoped_key) + >>> asc.remove_task_restart_patterns(an_sk, ["MemoryError: Unable to allocate \d+ GiB"]) + >>> asc.get_task_restart_patterns(an_sk) {"RuntimeError: .+": 3} Or by clearing all enforcing patterns:: - >>> asc.clear_task_restart_patterns(network_scoped_key) - >>> asc.get_task_restart_patterns(network_scoped_key) + >>> asc.clear_task_restart_patterns(an_sk) + >>> asc.get_task_restart_patterns(an_sk) {} From ae584ebee538c772939d48654564dedd8a2cb732 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Sun, 19 Jan 2025 21:55:02 -0700 Subject: [PATCH 121/143] Cypher fix --- alchemiscale/storage/statestore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index a53a5da2..f034c727 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -1448,7 +1448,7 @@ def action_tasks( q = """ // get our TaskHub UNWIND $task_scoped_keys as task_sk - MATCH (th:TaskHub {{_scoped_key: $taskhub_scoped_key}})-[:PERFORMS]->(an:AlchemicalNetwork) + MATCH (th:TaskHub {_scoped_key: $taskhub_scoped_key})-[:PERFORMS]->(an:AlchemicalNetwork) // get the task we want to add to the hub; check that it connects to same network MATCH (task:Task {_scoped_key: task_sk})-[:PERFORMS]->(:Transformation|NonTransformation)<-[:DEPENDS_ON]-(an) From e6d1ece14e7a856674f3534bd8a5cd565e2aff78 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 20 Jan 2025 13:49:01 -0700 Subject: [PATCH 122/143] Test fix --- alchemiscale/tests/integration/storage/test_statestore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 6e82c8b4..f2beeb4c 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -2148,7 +2148,7 @@ def test_task_status_change(self, n4js, network_tyk2, scope_test, status): if status == "complete": n4js.set_task_running(task_scoped_keys) - assert n4js.set_task_status(task_scoped_keys)[0] is not None + assert n4js.set_task_status(task_scoped_keys, status)[0] is not None query = """ MATCH (:TaskRestartPattern)-[:APPLIES]->(task:Task) From cf4ebb313db7908af1b9a38d419df092114a5463 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Mon, 20 Jan 2025 18:40:21 -0700 Subject: [PATCH 123/143] Another test fix --- alchemiscale/tests/integration/storage/test_statestore.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index f2beeb4c..5788c6c3 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -2148,7 +2148,10 @@ def test_task_status_change(self, n4js, network_tyk2, scope_test, status): if status == "complete": n4js.set_task_running(task_scoped_keys) - assert n4js.set_task_status(task_scoped_keys, status)[0] is not None + assert ( + n4js.set_task_status(task_scoped_keys, TaskStatusEnum[status])[0] + is not None + ) query = """ MATCH (:TaskRestartPattern)-[:APPLIES]->(task:Task) From 2af71b9c90ab1b41a21a32b6f0b1e348e46cd808 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 21 Jan 2025 16:26:55 -0700 Subject: [PATCH 124/143] Remove testing of GufeTokenizable level dict keys --- .../tests/unit/test_storage_models.py | 53 +++++++------------ 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/alchemiscale/tests/unit/test_storage_models.py b/alchemiscale/tests/unit/test_storage_models.py index 4d1bd720..41cfe3ae 100644 --- a/alchemiscale/tests/unit/test_storage_models.py +++ b/alchemiscale/tests/unit/test_storage_models.py @@ -92,31 +92,21 @@ def test_non_int_max_retries(self): ) def test_to_dict(self): + expected = { + "taskhub_scoped_key": "FakeScopedKey-1234-fake_org-fake_campaign-fake_project", + "max_retries": 3, + "pattern": "Example pattern", + } + trp = TaskRestartPattern( - "Example pattern", - 3, - "FakeScopedKey-1234-fake_org-fake_campaign-fake_project", + expected["pattern"], + expected["max_retries"], + expected["taskhub_scoped_key"], ) dict_trp = trp.to_dict() - assert len(dict_trp.keys()) == 6 - - assert dict_trp.pop("__qualname__") == "TaskRestartPattern" - assert dict_trp.pop("__module__") == "alchemiscale.storage.models" - assert ( - dict_trp.pop("taskhub_scoped_key") - == "FakeScopedKey-1234-fake_org-fake_campaign-fake_project" - ) - - # light test of the version key - try: - dict_trp.pop(":version:") - except KeyError: - raise AssertionError("expected to find :version:") - - expected = {"pattern": "Example pattern", "max_retries": 3} - - assert expected == dict_trp + for key, value in expected.items(): + assert dict_trp[key] == value def test_from_dict(self): @@ -179,19 +169,6 @@ def test_empty_list(self): Tracebacks([], self.source_keys, self.failure_keys) def test_to_dict(self): - tb = Tracebacks(self.valid_entry, self.source_keys, self.failure_keys) - tb_dict = tb.to_dict() - - assert len(tb_dict) == 6 - - assert tb_dict.pop("__qualname__") == "Tracebacks" - assert tb_dict.pop("__module__") == "alchemiscale.storage.models" - - # light test of the version key - try: - tb_dict.pop(":version:") - except KeyError: - raise AssertionError("expected to find :version:") expected = { "tracebacks": self.valid_entry, @@ -199,7 +176,13 @@ def test_to_dict(self): "failure_keys": self.failure_keys, } - assert expected == tb_dict + tb = Tracebacks( + expected["tracebacks"], expected["source_keys"], expected["failure_keys"] + ) + tb_dict = tb.to_dict() + + for key, value in expected.items(): + assert tb_dict[key] == value def test_from_dict(self): tb_orig = Tracebacks(self.valid_entry, self.source_keys, self.failure_keys) From 9e991d68d214382397ca3e87126a6208f93f420f Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 21 Jan 2025 16:50:56 -0700 Subject: [PATCH 125/143] Remove unnecessary comment about taskhub validation --- alchemiscale/storage/statestore.py | 1 - 1 file changed, 1 deletion(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index f034c727..e5087300 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -3046,7 +3046,6 @@ def set_task_restart_patterns_max_retries( max_retries=max_retries, ) - # TODO: validation of taskhubs variable, will fail in weird ways if not enforced def get_task_restart_patterns( self, taskhubs: list[ScopedKey] ) -> dict[ScopedKey, set[tuple[str, int]]]: From d1e4726e1161fd89f0a49ee1147375026c6404e7 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 21 Jan 2025 16:52:09 -0700 Subject: [PATCH 126/143] Remove unused tests that will not be implemented --- alchemiscale/tests/integration/storage/test_statestore.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 5788c6c3..19f00d5c 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -2579,14 +2579,6 @@ def test_resolve_task_restarts( # it should be waiting assert tasks_are_waiting(n4js, [task_to_wait]) - @pytest.mark.xfail(raises=NotImplementedError) - def test_task_actioning_applies_relationship(self): - raise NotImplementedError - - @pytest.mark.xfail(raises=NotImplementedError) - def test_task_deaction_applies_relationship(self): - raise NotImplementedError - ### authentication @pytest.mark.parametrize( From 280ef485ef8b68450ba18802e1ef6b5dc427cedb Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 21 Jan 2025 16:52:35 -0700 Subject: [PATCH 127/143] Compare `applies_count` to expected value in tests --- alchemiscale/tests/integration/storage/test_statestore.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/alchemiscale/tests/integration/storage/test_statestore.py b/alchemiscale/tests/integration/storage/test_statestore.py index 19f00d5c..8027f622 100644 --- a/alchemiscale/tests/integration/storage/test_statestore.py +++ b/alchemiscale/tests/integration/storage/test_statestore.py @@ -1197,6 +1197,8 @@ def test_action_task(self, n4js: Neo4jStore, network_tyk2, scope_test): query, taskhub_scoped_key=str(taskhub_sk) ).records[0]["applies_count"] + assert applies_count == 20 + query = """ MATCH (:TaskRestartPattern)-[applies:APPLIES]->(:Task) RETURN applies From 7f546d0dcdda2ed8a8ed3cdbd1de343926632a5e Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 21 Jan 2025 16:55:23 -0700 Subject: [PATCH 128/143] Removed __eq__ method for TaskRestartPattern --- alchemiscale/storage/models.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/alchemiscale/storage/models.py b/alchemiscale/storage/models.py index 844e2ebf..46eb5c69 100644 --- a/alchemiscale/storage/models.py +++ b/alchemiscale/storage/models.py @@ -194,12 +194,6 @@ def _to_dict(self): "taskhub_scoped_key": self.taskhub_scoped_key, } - # TODO: should this also compare taskhub scoped keys? - def __eq__(self, other): - if not isinstance(other, self.__class__): - return False - return self.pattern == other.pattern - class Tracebacks(GufeTokenizable): """ From 4056752ecdaf945e98ea2309a1d6f98e67d346bf Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Tue, 21 Jan 2025 17:45:10 -0700 Subject: [PATCH 129/143] Use standard dict instead of defaultdict --- alchemiscale/storage/statestore.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index e5087300..18aacc96 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -1238,14 +1238,14 @@ def _node_to_scoped_key(node): return ScopedKey.from_str(node["_scoped_key"]) transform_function = _node_to_gufe if return_gufe else _node_to_scoped_key - transform_results = defaultdict(None) + transform_results = {} for record in query_results.records: node = record_data_to_node(record["th"]) network_scoped_key = record["an"]["_scoped_key"] transform_results[network_scoped_key] = transform_function(node) return [ - transform_results[str(network_scoped_key)] + transform_results.get(str(network_scoped_key)) for network_scoped_key in network_scoped_keys ] From 277611915f5c2655c5bea4f9bdee8298be6d6428 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Tue, 21 Jan 2025 22:36:33 -0700 Subject: [PATCH 130/143] Pydantic 2 fixes Needed to use "before" validation in many cases, since we either want to validate inputs before model is instantiated or we want to modify those inputs before model is instantiated. --- alchemiscale/models.py | 9 +++++++-- alchemiscale/security/models.py | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index ed030ae0..ee9849f5 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -4,7 +4,7 @@ """ -from typing import Optional, Union +from typing import Optional, Union, Any from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict from gufe.tokenization import GufeKey from re import fullmatch @@ -65,19 +65,23 @@ def _validate_component(v, component): return v @field_validator("org") + @classmethod def valid_org(cls, v): return cls._validate_component(v, "org") @field_validator("campaign") + @classmethod def valid_campaign(cls, v): return cls._validate_component(v, "campaign") @field_validator("project") + @classmethod def valid_project(cls, v): return cls._validate_component(v, "project") @model_validator(mode="before") - def check_scope_hierarchy(cls, values): + @classmethod + def check_scope_hierarchy(cls, values: Any) -> Any: if not _hierarchy_valid(values): raise InvalidScopeError( f"Invalid scope hierarchy: {values}, cannot specify wildcard ('*')" @@ -136,6 +140,7 @@ class ScopedKey(BaseModel): model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) @field_validator("gufe_key") + @classmethod def gufe_key_validator(cls, v): v = str(v) diff --git a/alchemiscale/security/models.py b/alchemiscale/security/models.py index 25a63db3..47df3155 100644 --- a/alchemiscale/security/models.py +++ b/alchemiscale/security/models.py @@ -30,9 +30,10 @@ class CredentialedEntity(BaseModel): class ScopedIdentity(BaseModel): identifier: str disabled: bool = False - scopes: List[Union[Scope, str]] = [] + scopes: List[str] = [] - @field_validator("scopes") + @field_validator("scopes", mode="before") + @classmethod def cast_scopes_to_str(cls, scopes): """Ensure that each scope object is correctly cast to its str representation""" scopes_ = [] From 7ff8f66babab9c9aefd9bca7e89ba10280a475ca Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 22 Jan 2025 21:32:23 -0700 Subject: [PATCH 131/143] Add scope hierarchy check to ScopedKeys model; force use of pydantic 2 in all envs --- alchemiscale/models.py | 15 +++++++++++++-- devtools/conda-envs/alchemiscale-client.yml | 2 +- devtools/conda-envs/alchemiscale-compute.yml | 2 +- devtools/conda-envs/alchemiscale-server.yml | 2 +- devtools/conda-envs/test.yml | 2 +- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index ee9849f5..b7dc1f0a 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -132,14 +132,14 @@ class ScopedKey(BaseModel): """ - gufe_key: Union[GufeKey, str] + gufe_key: GufeKey org: str campaign: str project: str model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) - @field_validator("gufe_key") + @field_validator("gufe_key", mode='before') @classmethod def gufe_key_validator(cls, v): v = str(v) @@ -162,6 +162,17 @@ def gufe_key_validator(cls, v): # Cast to GufeKey return GufeKey(v_normalized) + @model_validator(mode="before") + @classmethod + def check_scope_hierarchy(cls, values: Any) -> Any: + if not _hierarchy_valid(values): + raise InvalidScopeError( + f"Invalid scope hierarchy: {values}, cannot specify wildcard ('*')" + " in a scope component if a less specific scope component is not" + " given, unless all components are wildcards (*-*-*)." + ) + return values + def __repr__(self): # pragma: no cover return f"" diff --git a/devtools/conda-envs/alchemiscale-client.yml b/devtools/conda-envs/alchemiscale-client.yml index 1131a604..bef871cd 100644 --- a/devtools/conda-envs/alchemiscale-client.yml +++ b/devtools/conda-envs/alchemiscale-client.yml @@ -13,7 +13,7 @@ dependencies: - requests - click - httpx - - pydantic >1 + - pydantic >2 - pydantic-settings - async-lru diff --git a/devtools/conda-envs/alchemiscale-compute.yml b/devtools/conda-envs/alchemiscale-compute.yml index 3219fb1e..7b63a2ad 100644 --- a/devtools/conda-envs/alchemiscale-compute.yml +++ b/devtools/conda-envs/alchemiscale-compute.yml @@ -13,7 +13,7 @@ dependencies: - requests - click - httpx - - pydantic >1 + - pydantic >2 - pydantic-settings - async-lru diff --git a/devtools/conda-envs/alchemiscale-server.yml b/devtools/conda-envs/alchemiscale-server.yml index c4b722db..93f42053 100644 --- a/devtools/conda-envs/alchemiscale-server.yml +++ b/devtools/conda-envs/alchemiscale-server.yml @@ -13,7 +13,7 @@ dependencies: - requests - click - - pydantic >1 + - pydantic >2 - pydantic-settings - async-lru diff --git a/devtools/conda-envs/test.yml b/devtools/conda-envs/test.yml index bf4740a7..a1ebbc80 100644 --- a/devtools/conda-envs/test.yml +++ b/devtools/conda-envs/test.yml @@ -9,7 +9,7 @@ dependencies: # alchemiscale dependencies - gufe>=1.1.0 - openfe>=1.2.0 - - pydantic >1 + - pydantic >2 - pydantic-settings - async-lru From 305441a7f54eefa6b0155f70b67a3e2692195427 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 22 Jan 2025 21:33:19 -0700 Subject: [PATCH 132/143] Black! --- alchemiscale/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index b7dc1f0a..058b1643 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -139,7 +139,7 @@ class ScopedKey(BaseModel): model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) - @field_validator("gufe_key", mode='before') + @field_validator("gufe_key", mode="before") @classmethod def gufe_key_validator(cls, v): v = str(v) From cda35416616c4c5411bf6e1ae9692acaedff00e7 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 22 Jan 2025 22:40:05 -0700 Subject: [PATCH 133/143] Small docstring adjustment --- alchemiscale/compression.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/alchemiscale/compression.py b/alchemiscale/compression.py index 2004095a..eb3a692e 100644 --- a/alchemiscale/compression.py +++ b/alchemiscale/compression.py @@ -20,7 +20,7 @@ def compress_gufe_zstd(gufe_object: GufeTokenizable) -> bytes: Returns ------- bytes - Compressed byte form of the the GufeTokenizable. + Compressed byte form of the GufeTokenizable. """ keyed_chain_rep = gufe_object.to_keyed_chain() json_rep = json.dumps(keyed_chain_rep, cls=JSON_HANDLER.encoder) @@ -40,6 +40,8 @@ def decompress_gufe_zstd(compressed_bytes: bytes) -> GufeTokenizable: JSON_HANDLER.decoder. It is assumed that the decompressed bytes are utf-8 encoded. + This is the inverse operation of `compress_gufe_zstd`. + Parameters ---------- compressed_bytes: bytes From 0a1513859e6ada35cec14a744df8d989b2af2bb5 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Wed, 22 Jan 2025 22:42:39 -0700 Subject: [PATCH 134/143] Merge fixes --- alchemiscale/compute/api.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/alchemiscale/compute/api.py b/alchemiscale/compute/api.py index 3ffb8891..54e5ca6a 100644 --- a/alchemiscale/compute/api.py +++ b/alchemiscale/compute/api.py @@ -13,11 +13,8 @@ from fastapi import FastAPI, APIRouter, Body, Depends, Request from fastapi.middleware.gzip import GZipMiddleware from gufe.tokenization import GufeTokenizable, JSON_HANDLER -<<<<<<< HEAD import zstandard as zstd -======= from gufe.protocols import ProtocolDAGResult ->>>>>>> main from ..base.api import ( QueryGUFEHandler, @@ -338,7 +335,7 @@ async def set_task_result( task_sk = ScopedKey.from_str(task_scoped_key) validate_scopes(task_sk.scope, token) - pdr = decompress_gufe_zstd(protocoldagresult_) + pdr: ProtocolDAGResult = decompress_gufe_zstd(protocoldagresult_) tf_sk, _ = n4js.get_task_transformation( task=task_scoped_key, From 586320e5cc5c7dbc3e58e3fb933e070ed6d88588 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 23 Jan 2025 20:58:30 -0700 Subject: [PATCH 135/143] Edits from @ianmkenney review --- alchemiscale/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/alchemiscale/models.py b/alchemiscale/models.py index 058b1643..19f93499 100644 --- a/alchemiscale/models.py +++ b/alchemiscale/models.py @@ -5,7 +5,7 @@ """ from typing import Optional, Union, Any -from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict +from pydantic import BaseModel, field_validator, model_validator, ConfigDict from gufe.tokenization import GufeKey from re import fullmatch import unicodedata @@ -17,6 +17,10 @@ class Scope(BaseModel): campaign: Optional[str] = None project: Optional[str] = None + model_config = ConfigDict( + frozen=True, + ) + def __init__(self, org=None, campaign=None, project=None): # we add this to allow for arg-based creation, not just keyword-based super().__init__(org=org, campaign=campaign, project=project) @@ -36,10 +40,6 @@ def __eq__(self, other): return str(self) == str(other) - model_config = ConfigDict( - frozen=True, - ) - @staticmethod def _validate_component(v, component): """ From 8b65c72f5b19a1b84e9a9b9c51d85e4f4aa4319c Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 23 Jan 2025 21:08:59 -0700 Subject: [PATCH 136/143] Removed need to decompress protocoldagresult in S3ObjectStore.push_protocoldagresult --- alchemiscale/compute/api.py | 6 +++++- alchemiscale/storage/objectstore.py | 15 ++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/alchemiscale/compute/api.py b/alchemiscale/compute/api.py index 54e5ca6a..f81252f9 100644 --- a/alchemiscale/compute/api.py +++ b/alchemiscale/compute/api.py @@ -344,7 +344,11 @@ async def set_task_result( # push the ProtocolDAGResult to the object store protocoldagresultref: ProtocolDAGResultRef = s3os.push_protocoldagresult( - protocoldagresult_, transformation=tf_sk, creator=compute_service_id + protocoldagresult=protocoldagresult_, + protocoldagresult_ok=pdr.ok(), + protocoldagresult_gufekey=pdr.key, + transformation=tf_sk, + creator=compute_service_id ) # push the reference to the state store diff --git a/alchemiscale/storage/objectstore.py b/alchemiscale/storage/objectstore.py index d0e7ec3d..ca3fceee 100644 --- a/alchemiscale/storage/objectstore.py +++ b/alchemiscale/storage/objectstore.py @@ -15,7 +15,7 @@ import zstandard as zstd from gufe.protocols import ProtocolDAGResult -from gufe.tokenization import JSON_HANDLER, GufeTokenizable +from gufe.tokenization import JSON_HANDLER, GufeTokenizable, GufeKey from ..compression import decompress_gufe_zstd from ..models import ScopedKey, Scope @@ -200,6 +200,8 @@ def _get_filename(self, location): def push_protocoldagresult( self, protocoldagresult: bytes, + protocoldagresult_ok: bool, + protocoldagresult_gufekey: GufeKey, transformation: ScopedKey, creator: Optional[str] = None, ) -> ProtocolDAGResultRef: @@ -209,6 +211,10 @@ def push_protocoldagresult( ---------- protocoldagresult ProtocolDAGResult to store. + protocoldagresult_ok + ``True`` if ProtocolDAGResult completed successfully; ``False`` if failed. + protocoldagresult_gufekey + The GufeKey of the ProtocolDAGResult. transformation The ScopedKey of the Transformation this ProtocolDAGResult corresponds to. @@ -220,8 +226,7 @@ def push_protocoldagresult( """ - pdr = decompress_gufe_zstd(protocoldagresult) - ok = pdr.ok() + ok = protocoldagresult_ok route = "results" if ok else "failures" # build `location` based on gufe key @@ -230,7 +235,7 @@ def push_protocoldagresult( *transformation.scope.to_tuple(), transformation.gufe_key, route, - pdr.key, + protocoldagresult_gufekey, OBJECT_FILENAME, ) @@ -238,7 +243,7 @@ def push_protocoldagresult( return ProtocolDAGResultRef( location=location, - obj_key=pdr.key, + obj_key=protocoldagresult_gufekey, scope=transformation.scope, ok=ok, datetime_created=datetime.utcnow(), From 11876660b8ad2fddae01f58a350e99d833096ba5 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 23 Jan 2025 22:03:26 -0700 Subject: [PATCH 137/143] Some clarity edits --- alchemiscale/compute/client.py | 6 +++--- alchemiscale/interface/client.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/alchemiscale/compute/client.py b/alchemiscale/compute/client.py index 65581043..ef170763 100644 --- a/alchemiscale/compute/client.py +++ b/alchemiscale/compute/client.py @@ -116,13 +116,13 @@ def get_task_transformation(self, task: ScopedKey) -> ScopedKey: def retrieve_task_transformation( self, task: ScopedKey ) -> tuple[Transformation, ProtocolDAGResult | None]: - transformation, protocoldagresult = self._get_resource( + transformation_json, protocoldagresult_latin1 = self._get_resource( f"/tasks/{task}/transformation/gufe" ) if protocoldagresult is not None: - protocoldagresult_bytes = protocoldagresult.encode("latin-1") + protocoldagresult_bytes = protocoldagresult_latin1.encode("latin-1") try: # Attempt to decompress the ProtocolDAGResult object @@ -133,7 +133,7 @@ def retrieve_task_transformation( protocoldagresult_bytes.decode("utf-8") ) - return json_to_gufe(transformation), protocoldagresult + return json_to_gufe(transformation_json), protocoldagresult def set_task_result( self, diff --git a/alchemiscale/interface/client.py b/alchemiscale/interface/client.py index b7841523..83c06ff8 100644 --- a/alchemiscale/interface/client.py +++ b/alchemiscale/interface/client.py @@ -1360,9 +1360,10 @@ async def _async_get_protocoldagresult( compress=compress, ) + pdr_bytes = pdr_latin1_decoded[0].encode("latin-1") + try: # Attempt to decompress the ProtocolDAGResult object - pdr_bytes = pdr_latin1_decoded[0].encode("latin-1") pdr = decompress_gufe_zstd(pdr_bytes) except zstd.ZstdError: # If decompress fails, assume it's a UTF-8 encoded JSON string From 7d3c86c72cc26a5c4123221d3a2f662dee00617a Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 23 Jan 2025 22:03:52 -0700 Subject: [PATCH 138/143] Black! --- alchemiscale/compute/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/compute/api.py b/alchemiscale/compute/api.py index 9e0defcc..754ce12b 100644 --- a/alchemiscale/compute/api.py +++ b/alchemiscale/compute/api.py @@ -348,7 +348,7 @@ async def set_task_result( protocoldagresult_ok=pdr.ok(), protocoldagresult_gufekey=pdr.key, transformation=tf_sk, - creator=compute_service_id + creator=compute_service_id, ) # push the reference to the state store From e149d09860ebf7eb3446d02226b02a7686405851 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 23 Jan 2025 23:18:31 -0700 Subject: [PATCH 139/143] CI fixes, other edits from review --- alchemiscale/storage/objectstore.py | 4 ++-- .../compute/client/test_compute_client.py | 4 +--- .../interface/client/test_client.py | 22 ++++++++++++----- .../integration/storage/test_objectstore.py | 24 ++++++++++++------- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/alchemiscale/storage/objectstore.py b/alchemiscale/storage/objectstore.py index ca3fceee..c56572b4 100644 --- a/alchemiscale/storage/objectstore.py +++ b/alchemiscale/storage/objectstore.py @@ -210,7 +210,7 @@ def push_protocoldagresult( Parameters ---------- protocoldagresult - ProtocolDAGResult to store. + ProtocolDAGResult to store, in some bytes representation. protocoldagresult_ok ``True`` if ProtocolDAGResult completed successfully; ``False`` if failed. protocoldagresult_gufekey @@ -275,7 +275,7 @@ def pull_protocoldagresult( Returns ------- ProtocolDAGResult - The ProtocolDAGResult corresponding to the given `ProtocolDAGResultRef`. + The ProtocolDAGResult corresponding to the given `ProtocolDAGResultRef`, in a bytes representation. """ route = "results" if ok else "failures" diff --git a/alchemiscale/tests/integration/compute/client/test_compute_client.py b/alchemiscale/tests/integration/compute/client/test_compute_client.py index 09c9081e..443df3be 100644 --- a/alchemiscale/tests/integration/compute/client/test_compute_client.py +++ b/alchemiscale/tests/integration/compute/client/test_compute_client.py @@ -342,8 +342,6 @@ def test_set_task_result_legacy( assert extends_protocoldagresult is None # push a result for the task - # pdr_sk = compute_client.set_task_result(task_sks[0], protocoldagresults[0]) - protocoldagresult = protocoldagresults[0] task_sk = task_sks[0] @@ -352,7 +350,7 @@ def test_set_task_result_legacy( # pdr_sk = compute_client.set_task_result(task_sks[0], protocoldagresults[0]) # # This involves pushing the protocoldagresult in the legacy - # to_dict() -> json -> utf-8 encode form, set the task result + # to_dict() -> json -> utf-8 encoded form, set the task result # in the statestore, set the task to complete in the # statestore # diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index 50a4dd07..002340fb 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -1804,7 +1804,13 @@ def test_set_tasks_priority_missing_tasks( ### results @staticmethod - def _execute_task(task_scoped_key, n4js, shared_basedir=None, scratch_basedir=None): + def _execute_task( + task_scoped_key, + n4js, + s3os_server, + shared_basedir=None, + scratch_basedir=None + ): shared_basedir = shared_basedir or Path("shared").absolute() shared_basedir.mkdir(exist_ok=True) @@ -1867,6 +1873,7 @@ def _execute_tasks(tasks, n4js, s3os_server): protocoldagresult = TestClient._execute_task( task_sk, n4js, + s3os_server, shared_basedir=shared_basedir, scratch_basedir=scratch_basedir, ) @@ -1880,7 +1887,10 @@ def _push_result(task_scoped_key, protocoldagresult, n4js, s3os_server): task_scoped_key, return_gufe=False ) protocoldagresultref = s3os_server.push_protocoldagresult( - compress_gufe_zstd(protocoldagresult), transformation=transformation_sk + compress_gufe_zstd(protocoldagresult), + protocoldagresult.ok(), + protocoldagresult.key, + transformation=transformation_sk ) n4js.set_task_result( task=task_scoped_key, protocoldagresultref=protocoldagresultref @@ -1890,7 +1900,7 @@ def _push_result(task_scoped_key, protocoldagresult, n4js, s3os_server): # TODO: remove in next major version when to_dict json storage is no longer supported @staticmethod def _push_result_legacy(task_scoped_key, protocoldagresult, n4js, s3os_server): - transformation_scoped_key, _ = n4js.get_task_transformation( + transformation_sk, _ = n4js.get_task_transformation( task_scoped_key, return_gufe=False ) pdr_jb = json.dumps( @@ -1902,8 +1912,8 @@ def _push_result_legacy(task_scoped_key, protocoldagresult, n4js, s3os_server): location = os.path.join( "protocoldagresult", - *transformation_scoped_key.scope.to_tuple(), - transformation_scoped_key.gufe_key, + *transformation_sk.scope.to_tuple(), + transformation_sk.gufe_key, route, protocoldagresult.key, "obj.json", @@ -1914,7 +1924,7 @@ def _push_result_legacy(task_scoped_key, protocoldagresult, n4js, s3os_server): protocoldagresultref = ProtocolDAGResultRef( location=location, obj_key=protocoldagresult.key, - scope=transformation_scoped_key.scope, + scope=transformation_sk.scope, ok=ok, datetime_created=datetime.utcnow(), creator=None, diff --git a/alchemiscale/tests/integration/storage/test_objectstore.py b/alchemiscale/tests/integration/storage/test_objectstore.py index bb7a1177..77f3e073 100644 --- a/alchemiscale/tests/integration/storage/test_objectstore.py +++ b/alchemiscale/tests/integration/storage/test_objectstore.py @@ -15,17 +15,21 @@ def test_delete(self, s3os: S3ObjectStore): s3os._store_bytes("_check_test", b"test_check") s3os._delete("_check_test") - def test_push_protocolresult( + def test_push_protocoldagresult( self, s3os: S3ObjectStore, protocoldagresults, transformation, scope_test ): transformation_sk = ScopedKey(gufe_key=transformation.key, **scope_test.dict()) + protocoldagresult = protocoldagresults[0] # try to push the result objstoreref: ProtocolDAGResultRef = s3os.push_protocoldagresult( - compress_gufe_zstd(protocoldagresults[0]), transformation=transformation_sk + compress_gufe_zstd(protocoldagresult), + protocoldagresult.ok(), + protocoldagresult.key, + transformation=transformation_sk ) - assert objstoreref.obj_key == protocoldagresults[0].key + assert objstoreref.obj_key == protocoldagresult.key # examine object metadata objs = list(s3os.resource.Bucket(s3os.bucket).objects.all()) @@ -33,23 +37,27 @@ def test_push_protocolresult( assert len(objs) == 1 assert objs[0].key == os.path.join(s3os.prefix, objstoreref.location) - def test_pull_protocolresult( + def test_pull_protocoldagresult( self, s3os: S3ObjectStore, protocoldagresults, transformation, scope_test ): transformation_sk = ScopedKey(gufe_key=transformation.key, **scope_test.dict()) + protocoldagresult = protocoldagresults[0] objstoreref: ProtocolDAGResultRef = s3os.push_protocoldagresult( - compress_gufe_zstd(protocoldagresults[0]), transformation=transformation_sk + compress_gufe_zstd(protocoldagresult), + protocoldagresult.ok(), + protocoldagresult.key, + transformation=transformation_sk ) # round trip it sk = ScopedKey(gufe_key=objstoreref.obj_key, **scope_test.dict()) tf_sk = ScopedKey( - gufe_key=protocoldagresults[0].transformation_key, **scope_test.dict() + gufe_key=protocoldagresult.transformation_key, **scope_test.dict() ) pdr = decompress_gufe_zstd(s3os.pull_protocoldagresult(sk, tf_sk)) - assert pdr.key == protocoldagresults[0].key + assert pdr.key == protocoldagresult.key assert pdr.protocol_unit_results == pdr.protocol_unit_results # test location-based pull @@ -57,5 +65,5 @@ def test_pull_protocolresult( s3os.pull_protocoldagresult(location=objstoreref.location) ) - assert pdr.key == protocoldagresults[0].key + assert pdr.key == protocoldagresult.key assert pdr.protocol_unit_results == pdr.protocol_unit_results From 847372943a47196ae51483495a74552dce2b2a46 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Thu, 23 Jan 2025 23:18:59 -0700 Subject: [PATCH 140/143] Black! --- .../tests/integration/interface/client/test_client.py | 10 +++------- .../tests/integration/storage/test_objectstore.py | 6 +++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/alchemiscale/tests/integration/interface/client/test_client.py b/alchemiscale/tests/integration/interface/client/test_client.py index 002340fb..09d1353b 100644 --- a/alchemiscale/tests/integration/interface/client/test_client.py +++ b/alchemiscale/tests/integration/interface/client/test_client.py @@ -1805,12 +1805,8 @@ def test_set_tasks_priority_missing_tasks( @staticmethod def _execute_task( - task_scoped_key, - n4js, - s3os_server, - shared_basedir=None, - scratch_basedir=None - ): + task_scoped_key, n4js, s3os_server, shared_basedir=None, scratch_basedir=None + ): shared_basedir = shared_basedir or Path("shared").absolute() shared_basedir.mkdir(exist_ok=True) @@ -1890,7 +1886,7 @@ def _push_result(task_scoped_key, protocoldagresult, n4js, s3os_server): compress_gufe_zstd(protocoldagresult), protocoldagresult.ok(), protocoldagresult.key, - transformation=transformation_sk + transformation=transformation_sk, ) n4js.set_task_result( task=task_scoped_key, protocoldagresultref=protocoldagresultref diff --git a/alchemiscale/tests/integration/storage/test_objectstore.py b/alchemiscale/tests/integration/storage/test_objectstore.py index 77f3e073..e9c75107 100644 --- a/alchemiscale/tests/integration/storage/test_objectstore.py +++ b/alchemiscale/tests/integration/storage/test_objectstore.py @@ -23,10 +23,10 @@ def test_push_protocoldagresult( # try to push the result objstoreref: ProtocolDAGResultRef = s3os.push_protocoldagresult( - compress_gufe_zstd(protocoldagresult), + compress_gufe_zstd(protocoldagresult), protocoldagresult.ok(), protocoldagresult.key, - transformation=transformation_sk + transformation=transformation_sk, ) assert objstoreref.obj_key == protocoldagresult.key @@ -47,7 +47,7 @@ def test_pull_protocoldagresult( compress_gufe_zstd(protocoldagresult), protocoldagresult.ok(), protocoldagresult.key, - transformation=transformation_sk + transformation=transformation_sk, ) # round trip it From d61d133f25a325f7e74417f29d0cb21cd65ce965 Mon Sep 17 00:00:00 2001 From: Ian Kenney Date: Fri, 24 Jan 2025 09:58:18 -0700 Subject: [PATCH 141/143] Assign value to protocoldagresult --- alchemiscale/compute/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/compute/client.py b/alchemiscale/compute/client.py index ef170763..e4d6715f 100644 --- a/alchemiscale/compute/client.py +++ b/alchemiscale/compute/client.py @@ -120,7 +120,7 @@ def retrieve_task_transformation( f"/tasks/{task}/transformation/gufe" ) - if protocoldagresult is not None: + if (protocoldagresult := protocoldagresult_latin1) is not None: protocoldagresult_bytes = protocoldagresult_latin1.encode("latin-1") From 443e193199fb58cc30f81d9160d647731f5f1fa0 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Fri, 24 Jan 2025 11:01:30 -0700 Subject: [PATCH 142/143] Simplification --- alchemiscale/compute/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/compute/client.py b/alchemiscale/compute/client.py index e4d6715f..f860ce53 100644 --- a/alchemiscale/compute/client.py +++ b/alchemiscale/compute/client.py @@ -120,7 +120,7 @@ def retrieve_task_transformation( f"/tasks/{task}/transformation/gufe" ) - if (protocoldagresult := protocoldagresult_latin1) is not None: + if protocoldagresult_latin1 is not None: protocoldagresult_bytes = protocoldagresult_latin1.encode("latin-1") From 321f5d724ea5850c7f645fbe76f8d78689de1e18 Mon Sep 17 00:00:00 2001 From: David Dotson Date: Fri, 24 Jan 2025 11:10:38 -0700 Subject: [PATCH 143/143] Revert "simplification" --- alchemiscale/compute/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alchemiscale/compute/client.py b/alchemiscale/compute/client.py index f860ce53..e4d6715f 100644 --- a/alchemiscale/compute/client.py +++ b/alchemiscale/compute/client.py @@ -120,7 +120,7 @@ def retrieve_task_transformation( f"/tasks/{task}/transformation/gufe" ) - if protocoldagresult_latin1 is not None: + if (protocoldagresult := protocoldagresult_latin1) is not None: protocoldagresult_bytes = protocoldagresult_latin1.encode("latin-1")