Source code for decent_bench.algorithms.p2p._dlm
from dataclasses import dataclass
import decent_bench.utils.interoperability as iop
from decent_bench.agents import Agent
from decent_bench.algorithms.utils import initial_states
from decent_bench.networks import P2PNetwork
from decent_bench.utils._tags import tags
from decent_bench.utils.array import Array
from decent_bench.utils.types import InitialStates
from ._p2p_algorithm import P2PAlgorithm
[docs]
@tags("peer-to-peer", "ADMM", "gradient-based")
@dataclass(eq=False)
class DLM(P2PAlgorithm):
r"""
Decentralized Linearized ADMM (DLM) :footcite:p:`Alg_DLM_1, Alg_DLM_2` characterized by the update steps below.
.. math::
\mathbf{x}_{i,k+1} = \mathbf{x}_{i,k} - \mu \left( \nabla f_i(\mathbf{x}_{i,k})
+ \rho \sum_j (\mathbf{x}_{i,k} - \mathbf{x}_{j,k}) + \mathbf{z}_{i,k} \right)
.. math::
\mathbf{z}_{i, k+1} = \mathbf{z}_{i, k} + \rho \sum_j (\mathbf{x}_{i,k+1} - \mathbf{x}_{j,k+1})
where
:math:`\mathbf{x}_{i, k}` is agent i's local optimization variable at iteration k,
:math:`\mathbf{z}_{i,k}` is the local dual variable,
:math:`f_i` is i's local cost function,
j is a neighbor of i. The parameters are: the penalty :math:`\rho > 0` and the step-size :math:`\mu > 0`.
Alias: :class:`DecentralizedLinearizedADMM`
.. footbibliography::
"""
iterations: int = 100
step_size: float = 0.001
penalty: float = 1
x0: InitialStates = None
name: str = "DLM"
def __post_init__(self) -> None:
"""
Validate hyperparameters.
Raises:
ValueError: if hyperparameters are invalid.
"""
if self.step_size <= 0:
raise ValueError("`step_size` must be positive")
if self.penalty <= 0:
raise ValueError("`penalty` must be positive")
def initialize(self, network: P2PNetwork) -> None:
self.x0 = initial_states(self.x0, network)
for i in network.agents():
y = iop.zeros_like(self.x0[i]) # y must be initialized to zero
i.initialize(x=self.x0[i], aux_vars={"y": y, "s": y})
def step(self, network: P2PNetwork, iteration: int) -> None:
if iteration == 0:
# step 0: first communication round
for i in network.active_agents():
network.broadcast(i, i.x)
# compute and store \sum_j (\mathbf{x}_{i,0} - \mathbf{x}_{j,0})
for i in network.active_agents():
s = self._sum_messages(i)
if s is not None:
i.aux_vars["s"] = len(i.messages()) * i.x - s
else:
# step 1: update primal variable
for i in network.active_agents():
i.x = i.x - self.step_size * (i.cost.gradient(i.x) + self.penalty * i.aux_vars["s"] + i.aux_vars["y"])
# step 2: communication round
for i in network.active_agents():
network.broadcast(i, i.x)
# compute and store \sum_j (\mathbf{x}_{i,k+1} - \mathbf{x}_{j,k+1})
for i in network.active_agents():
s = self._sum_messages(i)
if s is not None:
i.aux_vars["s"] = len(i.messages()) * i.x - s
# step 3: update dual variable
for i in network.active_agents():
i.aux_vars["y"] += self.penalty * i.aux_vars["s"]
def _sum_messages(self, i: "Agent") -> "Array | None":
s = None
for val in i.messages().values():
if s is None:
s = val
else:
s += val
return s
DecentralizedLinearizedADMM = DLM # alias