Source code for decent_bench.agent
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from types import MappingProxyType
from numpy import float64
from numpy.typing import NDArray
from decent_bench.cost_functions import CostFunction
from decent_bench.schemes import AgentActivationScheme
[docs]
class Agent:
"""Agent with unique id, local cost function, and activation scheme."""
def __init__(self, agent_id: int, cost_function: CostFunction, activation_scheme: AgentActivationScheme):
self._id = agent_id
self._cost_function = cost_function
self._activation_scheme = activation_scheme
self._x_per_iteration: list[NDArray[float64]] = []
self._auxiliary_variables: dict[str, NDArray[float64]] = {}
self._received_messages: dict[Agent, NDArray[float64]] = {}
self._n_sent_messages = 0
self._n_received_messages = 0
self._n_sent_messages_dropped = 0
self._n_evaluate_calls = 0
self._n_gradient_calls = 0
self._n_hessian_calls = 0
self._n_proximal_calls = 0
cost_function.evaluate = self._call_counting_evaluate # type: ignore[method-assign]
cost_function.gradient = self._call_counting_gradient # type: ignore[method-assign]
cost_function.hessian = self._call_counting_hessian # type: ignore[method-assign]
cost_function.proximal = self._call_counting_proximal # type: ignore[method-assign]
@property
def id(self) -> int:
"""Unique id for the agent."""
return self._id
@property
def cost_function(self) -> CostFunction:
"""Local cost function."""
return self._cost_function
@property
def x(self) -> NDArray[float64]:
"""
Local optimization variable x.
Raises:
RuntimeError: if x is retrieved before being set or initialized
"""
if not self._x_per_iteration:
raise RuntimeError("x must be initialized before being accessed")
return self._x_per_iteration[-1]
@x.setter
def x(self, x: NDArray[float64]) -> None:
self._x_per_iteration.append(x)
@property
def received_messages(self) -> Mapping[Agent, NDArray[float64]]:
"""Messages received by neighbors."""
return MappingProxyType(self._received_messages)
@property
def aux_vars(self) -> dict[str, NDArray[float64]]:
"""Auxiliary optimization variables used by algorithms that require more variables than x."""
return self._auxiliary_variables
[docs]
def initialize(
self,
*,
x: NDArray[float64] | None = None,
aux_vars: dict[str, NDArray[float64]] | None = None,
received_msgs: dict[Agent, NDArray[float64]] | None = None,
) -> None:
"""
Initialize local variables and messages before running an algorithm.
Args:
x: initial x
aux_vars: initial auxiliary variables
received_msgs: initial messages from neighbors
"""
if x is not None:
self._x_per_iteration = [x]
if aux_vars:
self._auxiliary_variables = aux_vars
if received_msgs:
self._received_messages = received_msgs
def _call_counting_evaluate(self, x: NDArray[float64]) -> float:
self._n_evaluate_calls += 1
return self._cost_function.__class__.evaluate(self.cost_function, x)
def _call_counting_gradient(self, x: NDArray[float64]) -> NDArray[float64]:
self._n_gradient_calls += 1
return self._cost_function.__class__.gradient(self.cost_function, x)
def _call_counting_hessian(self, x: NDArray[float64]) -> NDArray[float64]:
self._n_hessian_calls += 1
return self._cost_function.__class__.hessian(self.cost_function, x)
def _call_counting_proximal(self, y: NDArray[float64], rho: float) -> NDArray[float64]:
self._n_proximal_calls += 1
return self._cost_function.__class__.proximal(self.cost_function, y, rho)
def __index__(self) -> int:
"""Enable using agent as index, for example ``W[a1, a2]`` instead of ``W[a1.id, a2.id]``."""
return self._id
[docs]
@dataclass(frozen=True, eq=False)
class AgentMetricsView:
"""Immutable view of agent that exposes useful properties for calculating metrics."""
cost_function: CostFunction
x_per_iteration: list[NDArray[float64]]
n_evaluate_calls: int
n_gradient_calls: int
n_hessian_calls: int
n_proximal_calls: int
n_sent_messages: int
n_received_messages: int
n_sent_messages_dropped: int
[docs]
@staticmethod
def from_agent(agent: Agent) -> AgentMetricsView:
"""Create from agent."""
return AgentMetricsView(
cost_function=agent.cost_function,
x_per_iteration=agent._x_per_iteration, # noqa: SLF001
n_evaluate_calls=agent._n_evaluate_calls, # noqa: SLF001
n_gradient_calls=agent._n_gradient_calls, # noqa: SLF001
n_hessian_calls=agent._n_hessian_calls, # noqa: SLF001
n_proximal_calls=agent._n_proximal_calls, # noqa: SLF001
n_sent_messages=agent._n_sent_messages, # noqa: SLF001
n_received_messages=agent._n_received_messages, # noqa: SLF001
n_sent_messages_dropped=agent._n_sent_messages_dropped, # noqa: SLF001
)