Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1484de4
Add unimplemented merge_networks
ianmkenney Sep 16, 2025
40cb0ab
Make assemble_network chainable
ianmkenney Sep 27, 2025
30e7dd3
Partial implementation of merge_networks
ianmkenney Sep 27, 2025
fc8656e
Merge remote-tracking branch 'origin/main' into an_merge
ianmkenney Sep 28, 2025
865a46f
Make _validate_extends_tasks and create_tasks chainable
ianmkenney Sep 29, 2025
fb28a59
Use Subgraphs for task (+PDRR) and network assembly
ianmkenney Oct 7, 2025
5032295
Update task trees with one query and cache subchains for performance …
ianmkenney Oct 7, 2025
d4f3dda
Remove need for methods to be chainable
ianmkenney Oct 7, 2025
b9a527e
Change confusing, but beneign clashing names
ianmkenney Oct 7, 2025
96b527e
Merge directly into an_subgraph
ianmkenney Oct 7, 2025
8f3b17f
Only create tasks that were complete
ianmkenney Oct 16, 2025
349e05f
Merge remote-tracking branch 'origin/main' into an_merge
ianmkenney Oct 16, 2025
f566d4f
Avoid soft failure for setting tasks to complete/error
ianmkenney Oct 16, 2025
1d8dfde
Also include error task ProtocolDAGResultRefs
ianmkenney Oct 16, 2025
f19985f
Add comments to merge_networks
ianmkenney Oct 20, 2025
c6edcd4
Merge remote-tracking branch 'origin/main' into an_merge
ianmkenney Oct 20, 2025
9374b9b
Typo fix and formatting
dotsdl Nov 12, 2025
6b98cb0
Merge branch 'main' into an_merge
dotsdl Mar 16, 2026
6859746
Expose merge_networks on the user-facing client and API
dotsdl Jun 8, 2026
7e3f5d1
Refactor merge_networks using KeyedChain.decode_subchains
dotsdl Jun 8, 2026
df3a180
Bump gufe to >=1.8.0 in test and server env files
dotsdl Jun 8, 2026
eb3c532
Merge branch 'main' into an_merge
dotsdl Jun 8, 2026
d0bf4ec
Address PR #507 review: PERFORMS wiring, state parameter, optimizations
dotsdl Jun 8, 2026
5a92fbc
Pin gufe to =1.8.0 across all conda env files
dotsdl Jun 9, 2026
beaf68c
Merge branch 'main' into an_merge
dotsdl Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion alchemiscale/interface/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from ..settings import get_base_api_settings
from ..storage.statestore import Neo4jStore
from ..storage.objectstore import S3ObjectStore
from ..storage.models import TaskStatusEnum, StrategyState
from ..storage.models import NetworkStateEnum, TaskStatusEnum, StrategyState
from ..models import Scope, ScopedKey
from ..security.models import TokenData, CredentialedUserIdentity

Expand Down Expand Up @@ -138,6 +138,55 @@ async def create_network(
return an_sk


@router.post("/networks/merge", response_model=ScopedKey)
async def merge_networks(
*,
networks: list[str] = Body(embed=True),
name: str = Body(embed=True),
scope: dict = Body(embed=True),
state: str = Body(embed=True, default=NetworkStateEnum.active.value),
n4js: Neo4jStore = Depends(get_n4js_depends),
token: TokenData = Depends(get_token_data_depends),
):
# validate the destination scope first
try:
target_scope = Scope(**scope)
except (TypeError, ValidationError) as e:
raise HTTPException(
status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=str(e),
)
validate_scopes(target_scope, token)

# validate each source network's scope is accessible to the token
network_sks = []
for network in networks:
try:
network_sk = ScopedKey.from_str(network)
except ValueError as e:
raise HTTPException(
status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=e.args[0],
)
validate_scopes(network_sk.scope, token)
network_sks.append(network_sk)

try:
an_sk = n4js.merge_networks(
network_scoped_keys=network_sks,
name=name,
scope=target_scope,
state=state,
)
except ValueError as e:
raise HTTPException(
status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=e.args[0],
)

return an_sk


@router.post("/bulk/networks/state/set")
def set_networks_state(
*,
Expand Down
97 changes: 97 additions & 0 deletions alchemiscale/interface/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,103 @@ def post():

return ScopedKey.from_dict(scoped_key)

def merge_networks(
self,
networks: list[ScopedKey],
name: str,
scope: Scope,
state: NetworkStateEnum | str = NetworkStateEnum.active,
visualize: bool = True,
) -> ScopedKey:
"""Merge multiple existing AlchemicalNetworks into a new AlchemicalNetwork.

The resulting AlchemicalNetwork contains the union of all
Transformations and NonTransformations from the source networks.
Existing Tasks for those transformations that are in ``complete`` or
``error`` state are cloned into the new network's scope along with
their associated ProtocolDAGResultRefs, so previously-computed results
do not need to be re-run.

Cloned Tasks are wired to their Transformations via ``PERFORMS`` and
are reachable through standard network traversals
(``get_network_tasks``, ``get_network_results``, etc.). They are
intentionally **not** actioned to the new network's TaskHub; to
retry errored Tasks on the merged network, call
:meth:`action_tasks` with the merged network's ScopedKey after the
merge completes.

Parameters
----------
networks
The ScopedKeys of the AlchemicalNetworks to merge. The source
networks may live in different Scopes; the caller must have access
to each.
name
The name of the new AlchemicalNetwork.
scope
The Scope in which to create the new AlchemicalNetwork.
This must be a *specific* Scope; it must not contain wildcards.
state
The starting state of the new AlchemicalNetwork in the database.
See :meth:`AlchemiscaleClient.set_network_state` for valid states.
Defaults to ``"active"``.
visualize
If ``True``, show submission progress indicator.

Returns
-------
ScopedKey
The ScopedKey of the new, merged AlchemicalNetwork.
"""
if not scope.specific():
raise ValueError(
f"`scope` '{scope}' contains wildcards ('*'); `scope` must be *specific*"
)

if not networks:
raise ValueError("`networks` must contain at least one ScopedKey")

network_sks = [
sk if isinstance(sk, ScopedKey) else ScopedKey.from_str(sk)
for sk in networks
]

for network_sk in network_sks:
if network_sk.qualname not in ("AlchemicalNetwork",):
raise ValueError(
f"ScopedKey '{network_sk}' does not refer to an AlchemicalNetwork"
)

state = NetworkStateEnum(state)

data = dict(
networks=[str(sk) for sk in network_sks],
name=name,
scope=scope.to_dict(),
state=state.value,
)

def post():
return self._post_resource("/networks/merge", data)

if visualize:
from rich.progress import Progress

with Progress(*self._rich_waiting_columns(), transient=False) as progress:
task = progress.add_task(
f"Merging [bold]{len(network_sks)}[/bold] networks into "
f"[bold]'{name}'[/bold] in scope [bold]'{scope}'[/bold]...",
total=None,
)

scoped_key = post()
progress.start_task(task)
progress.update(task, total=1, completed=1)
else:
scoped_key = post()

return ScopedKey.from_dict(scoped_key)

def set_network_state(
self, network: ScopedKey, state: NetworkStateEnum | str
) -> ScopedKey | None:
Expand Down
Loading
Loading