diff --git a/environment.yml b/environment.yml index a20e4cf4..6e008cf8 100644 --- a/environment.yml +++ b/environment.yml @@ -11,7 +11,7 @@ dependencies: - pint <0.22 # https://github.com/openforcefield/openff-units/issues/69 - openff-models >=0.0.5 - pip - - pydantic + - pydantic <2.0.0 - pytest - pytest-cov - pytest-xdist @@ -20,5 +20,6 @@ dependencies: # docs - pydata-sphinx-theme - sphinx-jsonschema==1.15 + - sphinx <7.1.2 - pip: - - autodoc_pydantic + - autodoc_pydantic<2.0.0 diff --git a/gufe/ligandnetwork.py b/gufe/ligandnetwork.py index c7795dae..01ef5a77 100644 --- a/gufe/ligandnetwork.py +++ b/gufe/ligandnetwork.py @@ -2,6 +2,7 @@ # For details, see https://github.com/OpenFreeEnergy/gufe from __future__ import annotations +from itertools import chain import json import networkx as nx from typing import FrozenSet, Iterable, Optional @@ -31,7 +32,8 @@ def __init__( nodes = [] self._edges = frozenset(edges) - edge_nodes = set.union(*[{edge.componentA, edge.componentB} for edge in edges]) + edge_nodes = set(chain.from_iterable((e.componentA, e.componentB) + for e in edges)) self._nodes = frozenset(edge_nodes) | frozenset(nodes) self._graph = None @@ -47,7 +49,7 @@ def _from_dict(cls, dct: dict): return cls.from_graphml(dct['graphml']) @property - def graph(self) -> nx.Graph: + def graph(self) -> nx.MultiDiGraph: """NetworkX graph for this network""" if self._graph is None: graph = nx.MultiDiGraph() @@ -306,3 +308,11 @@ def to_rbfe_alchemical_network( # autoname=autoname, # autoname_prefix=autoname_prefix # ) + + def is_connected(self) -> bool: + """Are all ligands in the network (indirectly) connected to each other + + A "False" value indicates that either some ligands have no edges or that + there are separate networks that do not link to each other. + """ + return nx.is_weakly_connected(self.graph) diff --git a/gufe/tests/test_ligand_network.py b/gufe/tests/test_ligand_network.py index 41bc5f3f..0601b82e 100644 --- a/gufe/tests/test_ligand_network.py +++ b/gufe/tests/test_ligand_network.py @@ -428,3 +428,16 @@ def test_to_rhfe_alchemical_network(self, real_molecules_network, assert list(edge.mapping) == ['ligand'] assert edge.mapping['ligand'] in real_molecules_network.edges + def test_is_connected(self, simple_network): + assert simple_network.network.is_connected() + + def test_is_not_connected(self, singleton_node_network): + assert not singleton_node_network.network.is_connected() + + +def test_empty_ligand_network(mols): + # issue #217 + n = LigandNetwork(edges=[], nodes=[mols[0]]) + + assert len(n.edges) == 0 + assert len(n.nodes) == 1 diff --git a/gufe/tokenization.py b/gufe/tokenization.py index c537f5a9..2cead759 100644 --- a/gufe/tokenization.py +++ b/gufe/tokenization.py @@ -61,6 +61,24 @@ def __call__(cls, *args, **kwargs): instance = TOKENIZABLE_REGISTRY[key] return instance + def __init__(cls, clsname, bases, attrs): + """ + Restore the signature of __init__ or __new__ + """ + if inspect.signature(cls.__new__) != inspect.signature(object.__new__): + sig = inspect.signature(cls.__new__) + elif inspect.signature(cls.__init__) != inspect.signature(object.__init__): + sig = inspect.signature(cls.__init__) + else: + # No __new__ or __init__ method defined + return super().__init__(clsname, bases, attrs) + + # Remove the first parameter (cls/self) + parameters = tuple(sig.parameters.values()) + cls.__signature__ = sig.replace(parameters=parameters[1:]) + + return super().__init__(clsname, bases, attrs) + class _ABCGufeClassMeta(_GufeTokenizableMeta, abc.ABCMeta): # required to make use of abc.ABC in classes that use _ComponentMeta