diff --git a/alchemiscale/interface/api.py b/alchemiscale/interface/api.py index 8a629c2f..3165c58a 100644 --- a/alchemiscale/interface/api.py +++ b/alchemiscale/interface/api.py @@ -116,7 +116,14 @@ def create_network( validate_scopes(scope, token) an = AlchemicalNetwork.from_dict(network) - an_sk = n4js.create_network(network=an, scope=scope) + + try: + an_sk = n4js.create_network(network=an, scope=scope) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=e.args[0], + ) # create taskhub for this network n4js.create_taskhub(an_sk) diff --git a/alchemiscale/interface/client.py b/alchemiscale/interface/client.py index 2f9d00f7..47a112a3 100644 --- a/alchemiscale/interface/client.py +++ b/alchemiscale/interface/client.py @@ -29,6 +29,7 @@ from ..storage.models import Task, ProtocolDAGResultRef, TaskStatusEnum from ..strategies import Strategy from ..security.models import CredentialedUserIdentity +from ..validators import validate_network_nonself class AlchemiscaleClientError(AlchemiscaleBaseClientError): @@ -116,6 +117,8 @@ def create_network( f"`scope` '{scope}' contains wildcards ('*'); `scope` must be *specific*" ) + validate_network_nonself(network) + from rich.progress import Progress sk = self.get_scoped_key(network, scope) diff --git a/alchemiscale/storage/statestore.py b/alchemiscale/storage/statestore.py index 0f62f7ca..e287948b 100644 --- a/alchemiscale/storage/statestore.py +++ b/alchemiscale/storage/statestore.py @@ -36,6 +36,7 @@ from ..security.models import CredentialedEntity from ..settings import Neo4jStoreSettings, get_neo4jstore_settings +from ..validators import validate_network_nonself @lru_cache() @@ -610,6 +611,7 @@ def create_network(self, network: AlchemicalNetwork, scope: Scope): some of its components already exist in the database. """ + validate_network_nonself(network) ndict = network.to_shallow_dict() diff --git a/alchemiscale/tests/unit/test_validators.py b/alchemiscale/tests/unit/test_validators.py new file mode 100644 index 00000000..ba210e3d --- /dev/null +++ b/alchemiscale/tests/unit/test_validators.py @@ -0,0 +1,64 @@ +import pytest + +from openfe_benchmarks import tyk2 +from gufe import ChemicalSystem, Transformation, AlchemicalNetwork +from gufe.tests.test_protocol import DummyProtocol, BrokenProtocol + +from alchemiscale import validators + + +@pytest.fixture(scope="session") +def network_self_transformation(): + tyk2s = tyk2.get_system() + ligand = tyk2s.ligand_components[0] + + cs = ChemicalSystem( + components={"ligand": ligand, "solvent": tyk2s.solvent_component}, + name=f"{ligand.name}_water", + ) + + tf = Transformation( + stateA=cs, + stateB=cs, + protocol=DummyProtocol(settings=DummyProtocol.default_settings()), + name=f"{ligand.name}->{ligand.name}_water", + ) + + return AlchemicalNetwork(edges=[tf], name="self_transformation") + + +@pytest.fixture(scope="session") +def network_nonself_transformation(): + tyk2s = tyk2.get_system() + ligand = tyk2s.ligand_components[0] + ligand2 = tyk2s.ligand_components[1] + + cs = ChemicalSystem( + components={"ligand": ligand, "solvent": tyk2s.solvent_component}, + name=f"{ligand.name}_water", + ) + + cs2 = ChemicalSystem( + components={"ligand": ligand2, "solvent": tyk2s.solvent_component}, + name=f"{ligand2.name}_water", + ) + + tf = Transformation( + stateA=cs, + stateB=cs2, + protocol=DummyProtocol(settings=DummyProtocol.default_settings()), + name=f"{ligand.name}->{ligand2.name}_water", + ) + + return AlchemicalNetwork(edges=[tf], name="nonself_transformation") + + +def test_validate_network_nonself( + network_self_transformation, network_nonself_transformation +): + with pytest.raises(ValueError, match="uses the same `ChemicalSystem`"): + validators.validate_network_nonself(network_self_transformation) + + out = validators.validate_network_nonself(network_nonself_transformation) + + assert out is None diff --git a/alchemiscale/validators.py b/alchemiscale/validators.py new file mode 100644 index 00000000..0b9ba2b9 --- /dev/null +++ b/alchemiscale/validators.py @@ -0,0 +1,22 @@ +""" +:mod:`alchemiscale.validators` --- validation guardrails for user input +======================================================================= + +""" + +from gufe import AlchemicalNetwork, Transformation + + +def validate_network_nonself(network: AlchemicalNetwork): + """Check that the given AlchemicalNetwork features no Transformations with + the same ChemicalSystem for its two states. + + A ``ValueError`` is raised if a `Transformation` is detected. + + """ + for transformation in network.edges: + if transformation.stateA == transformation.stateB: + raise ValueError( + f"`Transformation` '{transformation.key}' uses the same `ChemicalSystem` '{transformation.stateA.key}' for both states; " + "this is currently not supported in `alchemiscale`" + )