Source code for hydrocouple.distributed
"""
HydroCouple distributed-execution abstract base classes.
Python ABC mirrors of the C++ HydroCouple v2.0.0 interfaces defined in
``hydrocoupledistributed.h``: transport-neutral message channels,
distributed model components, proxies for remote components, and
partitioned data items with local virtual (ghost/halo) entity
representation. Transport payloads are NumPy arrays — the Python-side
``BufferDescriptor``.
Transport *implementations* (MPI, in-process, sockets) are SDK territory;
this module only defines the contracts.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from hydrocouple.core import IComponentDataItem, IIdentity, IModelComponent
if TYPE_CHECKING:
import numpy as np
[docs]
class IExchangeRequest(ABC):
"""Handle to an asynchronous communication operation.
Mirrors C++ ``Distributed::IExchangeRequest``. Returned by the
asynchronous :class:`ITransport` operations and by
:meth:`IPartitionedComponentDataItem.synchronize_async`.
"""
[docs]
@abstractmethod
def test(self) -> bool:
"""Non-blocking completion test."""
raise NotImplementedError
[docs]
@abstractmethod
def wait(self) -> None:
"""Block until the operation completes."""
raise NotImplementedError
[docs]
@abstractmethod
def failed(self) -> tuple[bool, str]:
"""Whether the operation failed; returns ``(failed, message)``."""
raise NotImplementedError
[docs]
@dataclass
class Endpoint:
"""One participant reachable through a transport.
Mirrors C++ ``Distributed::ITransport::Endpoint``.
"""
address: str = ""
rank: int = -1
[docs]
class ITransport(IIdentity):
"""A transport-neutral, tagged message channel between processes.
Mirrors C++ ``Distributed::ITransport``. Payloads are NumPy arrays;
message ordering is guaranteed only between a fixed
``(sender, receiver, tag)`` triple.
"""
@property
@abstractmethod
def local_endpoint(self) -> Endpoint:
"""The endpoint of the calling process."""
raise NotImplementedError
@property
@abstractmethod
def participant_count(self) -> int:
"""The number of participants reachable through this transport."""
raise NotImplementedError
[docs]
@abstractmethod
def send(self, to: Endpoint, tag: int, payload: "np.ndarray") -> tuple[bool, str]:
"""Send a typed payload, blocking until the buffer is reusable.
:returns: ``(ok, message)``.
"""
raise NotImplementedError
[docs]
@abstractmethod
def receive(self, source: Endpoint, tag: int, into: "np.ndarray") -> tuple[bool, str]:
"""Receive a typed payload into a pre-allocated array, blocking.
:returns: ``(ok, message)``.
"""
raise NotImplementedError
[docs]
@abstractmethod
def send_async(self, to: Endpoint, tag: int, payload: "np.ndarray") -> IExchangeRequest:
"""Nonblocking send; the payload must stay valid until completion."""
raise NotImplementedError
[docs]
@abstractmethod
def receive_async(self, source: Endpoint, tag: int, into: "np.ndarray") -> IExchangeRequest:
"""Nonblocking receive; ``into`` must stay valid until completion."""
raise NotImplementedError
[docs]
class IDistributedModelComponent(IModelComponent):
"""An :class:`~hydrocouple.core.IModelComponent` participating in
distributed execution through a transport.
Mirrors C++ ``Distributed::IDistributedModelComponent``; advertises
:attr:`~hydrocouple.core.Capability.DistributedExecution`.
"""
@property
@abstractmethod
def transport(self) -> ITransport:
"""The transport this component communicates through."""
raise NotImplementedError
@property
@abstractmethod
def partition_count(self) -> int:
"""Number of partitions the domain is decomposed into (1 if none)."""
raise NotImplementedError
@property
@abstractmethod
def partition_rank(self) -> int:
"""The zero-based partition this instance computes."""
raise NotImplementedError
[docs]
class IProxyModelComponent(IDistributedModelComponent):
"""A local stand-in for a remote model component.
Mirrors C++ ``Distributed::IProxyModelComponent``. Every
:class:`~hydrocouple.core.IModelComponent` method forwards to the
remote peer; an orchestrator cannot — and need not — distinguish a
proxy from a local component. If the peer dies or a request times out,
the proxy transitions to ``Failed``, queues a ``Fatal``
:class:`~hydrocouple.core.ErrorEntry`, and fires its status signal.
"""
@property
@abstractmethod
def remote_endpoint(self) -> Endpoint:
"""The endpoint of the remote component."""
raise NotImplementedError
@property
@abstractmethod
def remote_id(self) -> str:
"""The unique identifier of the remote component."""
raise NotImplementedError
[docs]
@abstractmethod
def connect_remote(self) -> tuple[bool, str]:
"""Establish the connection to the remote component.
:returns: ``(ok, message)``.
.. note:: named ``connect_remote`` to avoid clashing with the
inherited signal method ``connect(slot)``; mirrors C++
``connect()``.
"""
raise NotImplementedError
[docs]
@abstractmethod
def disconnect_remote(self) -> None:
"""Close the connection to the remote component."""
raise NotImplementedError
@property
@abstractmethod
def is_connected(self) -> bool:
"""Whether the proxy currently holds a live connection."""
raise NotImplementedError
[docs]
@abstractmethod
def ping(self, timeout_seconds: float) -> bool:
"""Liveness probe of the remote component."""
raise NotImplementedError
@property
@abstractmethod
def request_timeout(self) -> float:
"""Timeout applied to forwarded requests (non-positive = wait
indefinitely)."""
raise NotImplementedError
@request_timeout.setter
@abstractmethod
def request_timeout(self, seconds: float) -> None:
raise NotImplementedError
[docs]
class IPartitionedComponentDataItem(IComponentDataItem):
"""A data item whose entity dimension is decomposed across partitions,
with local virtual (ghost/halo) representation of remote entities.
Mirrors C++ ``Distributed::IPartitionedComponentDataItem``. The entity
dimension indexes locally resident entities: first the locally owned
entities, then the virtual entities mirrored from other partitions.
Virtual entities carry no degrees of freedom — they are read-only
mirrors overwritten by synchronization, never solved locally.
Components exposing partitioned items advertise
:attr:`~hydrocouple.core.Capability.PartitionedData`.
"""
@property
@abstractmethod
def global_count(self) -> int:
"""Global entity count across all partitions."""
raise NotImplementedError
@property
@abstractmethod
def owned_global_indexes(self) -> "np.ndarray":
"""Global identities of locally owned entities (int64 array)."""
raise NotImplementedError
@property
@abstractmethod
def virtual_global_indexes(self) -> "np.ndarray":
"""Global identities of local virtual (ghost/halo) entities
(int64 array); they follow the owned entities in the local entity
dimension."""
raise NotImplementedError
@property
@abstractmethod
def virtual_owners(self) -> "np.ndarray":
"""Owner partition rank of each virtual entity (int32 array,
parallel to :attr:`virtual_global_indexes`)."""
raise NotImplementedError
@property
@abstractmethod
def synchronization_epoch(self) -> int:
"""Monotonically increasing counter, incremented each time a halo
synchronization completes."""
raise NotImplementedError
[docs]
@abstractmethod
def synchronize_async(self) -> IExchangeRequest:
"""Begin an asynchronous halo synchronization of the virtual
entities. Computation on owned entities may overlap the returned
request; virtual-entity values are defined only after completion."""
raise NotImplementedError