Skip to main content

scirs2_optimize/distributed_admm/
types.rs

1//! Types for Distributed ADMM, PDMM, and EXTRA algorithms.
2//!
3//! Provides configuration, result, and node structures for
4//! consensus-based distributed optimization.
5//!
6//! # References
7//! - Boyd et al. (2011). "Distributed Optimization and Statistical Learning via
8//!   the Alternating Direction Method of Multipliers." Foundations and Trends in ML.
9//! - Chang et al. (2015). "Multi-Agent Distributed Optimization via Inexact
10//!   Consensus ADMM." IEEE Trans. Signal Processing.
11//! - Shi et al. (2015). "EXTRA: An Exact First-Order Algorithm for Decentralized
12//!   Consensus Optimization." SIAM J. Optim.
13
14/// Configuration for ADMM (Alternating Direction Method of Multipliers).
15///
16/// ADMM solves the consensus problem: min Σ_i f_i(x_i) s.t. x_i = z
17/// using augmented Lagrangian decomposition.
18#[derive(Debug, Clone)]
19pub struct AdmmConfig {
20    /// Augmented Lagrangian penalty parameter ρ > 0.
21    pub rho: f64,
22    /// Maximum number of ADMM iterations.
23    pub max_iter: usize,
24    /// Absolute stopping tolerance for primal/dual residuals.
25    pub abs_tol: f64,
26    /// Relative stopping tolerance.
27    pub rel_tol: f64,
28    /// Enable warm-starting from a previous solution.
29    pub warm_start: bool,
30    /// Over-relaxation parameter α ∈ (0,2). Default 1.0 = no relaxation.
31    pub over_relaxation: f64,
32}
33
34impl Default for AdmmConfig {
35    fn default() -> Self {
36        Self {
37            rho: 1.0,
38            max_iter: 1000,
39            abs_tol: 1e-4,
40            rel_tol: 1e-3,
41            warm_start: false,
42            over_relaxation: 1.0,
43        }
44    }
45}
46
47/// Configuration for PDMM (Primal-Dual Method of Multipliers).
48///
49/// PDMM is a decentralized method where each agent communicates only
50/// with its immediate neighbours in a graph.
51#[derive(Debug, Clone)]
52pub struct PdmmConfig {
53    /// Step-size for primal and dual updates.
54    pub stepsize: f64,
55    /// Maximum number of iterations.
56    pub max_iter: usize,
57    /// Stopping tolerance for consensus (||x_i - x_j||).
58    pub tol: f64,
59}
60
61impl Default for PdmmConfig {
62    fn default() -> Self {
63        Self {
64            stepsize: 0.5,
65            max_iter: 500,
66            tol: 1e-4,
67        }
68    }
69}
70
71/// Configuration for EXTRA (Exact first-order decentralized algorithm).
72///
73/// EXTRA uses gradient tracking to converge to the exact consensus
74/// without diminishing step sizes.
75#[derive(Debug, Clone)]
76pub struct ExtraConfig {
77    /// Fixed step size α (must satisfy α < 2/(L_max(W̃) + ρ_max(∇²F)) for convergence).
78    pub alpha: f64,
79    /// Maximum number of iterations.
80    pub max_iter: usize,
81    /// Stopping tolerance.
82    pub tol: f64,
83}
84
85impl Default for ExtraConfig {
86    fn default() -> Self {
87        Self {
88            alpha: 0.05,
89            max_iter: 500,
90            tol: 1e-4,
91        }
92    }
93}
94
95/// Result of a distributed optimization run.
96#[derive(Debug, Clone)]
97pub struct AdmmResult {
98    /// Consensus solution x* (averaged over all agents at convergence).
99    pub x: Vec<f64>,
100    /// History of primal residuals ||x - z||₂ per iteration.
101    pub primal_residual: Vec<f64>,
102    /// History of dual residuals ρ||Δz||₂ per iteration.
103    pub dual_residual: Vec<f64>,
104    /// Whether the algorithm converged within the given tolerances.
105    pub converged: bool,
106    /// Number of iterations taken.
107    pub iterations: usize,
108}
109
110/// A single ADMM consensus node (agent).
111///
112/// Stores the local primal variable, the consensus variable copy,
113/// and the corresponding dual variable (scaled Lagrange multiplier).
114#[derive(Debug, Clone)]
115pub struct ConsensusNode {
116    /// Local primal variable x_i.
117    pub local_x: Vec<f64>,
118    /// Agent's copy of the consensus variable z (shared globally).
119    pub local_z: Vec<f64>,
120    /// Scaled dual variable y_i = u_i = λ_i/ρ.
121    pub dual_y: Vec<f64>,
122}
123
124impl ConsensusNode {
125    /// Create a new node initialised at zero.
126    pub fn new(n_vars: usize) -> Self {
127        Self {
128            local_x: vec![0.0; n_vars],
129            local_z: vec![0.0; n_vars],
130            dual_y: vec![0.0; n_vars],
131        }
132    }
133
134    /// Initialise from a warm-start solution.
135    pub fn warm(x0: Vec<f64>) -> Self {
136        let n = x0.len();
137        Self {
138            local_z: x0.clone(),
139            dual_y: vec![0.0; n],
140            local_x: x0,
141        }
142    }
143
144    /// Primal residual contribution: x_i - z.
145    pub fn primal_residual(&self) -> Vec<f64> {
146        self.local_x
147            .iter()
148            .zip(self.local_z.iter())
149            .map(|(xi, zi)| xi - zi)
150            .collect()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_admm_config_default() {
160        let cfg = AdmmConfig::default();
161        assert!((cfg.rho - 1.0).abs() < 1e-15);
162        assert_eq!(cfg.max_iter, 1000);
163        assert!((cfg.abs_tol - 1e-4).abs() < 1e-15);
164        assert!(!cfg.warm_start);
165    }
166
167    #[test]
168    fn test_pdmm_config_default() {
169        let cfg = PdmmConfig::default();
170        assert!((cfg.stepsize - 0.5).abs() < 1e-15);
171        assert_eq!(cfg.max_iter, 500);
172    }
173
174    #[test]
175    fn test_extra_config_default() {
176        let cfg = ExtraConfig::default();
177        assert!((cfg.alpha - 0.05).abs() < 1e-15);
178        assert_eq!(cfg.max_iter, 500);
179    }
180
181    #[test]
182    fn test_consensus_node_new() {
183        let node = ConsensusNode::new(3);
184        assert_eq!(node.local_x.len(), 3);
185        assert!(node.local_x.iter().all(|&v| v == 0.0));
186    }
187
188    #[test]
189    fn test_consensus_node_warm() {
190        let node = ConsensusNode::warm(vec![1.0, 2.0, 3.0]);
191        assert_eq!(node.local_x, vec![1.0, 2.0, 3.0]);
192        assert_eq!(node.local_z, vec![1.0, 2.0, 3.0]);
193        assert!(node.dual_y.iter().all(|&v| v == 0.0));
194    }
195
196    #[test]
197    fn test_primal_residual() {
198        let mut node = ConsensusNode::new(2);
199        node.local_x = vec![1.0, 2.0];
200        node.local_z = vec![0.5, 1.5];
201        let res = node.primal_residual();
202        assert!((res[0] - 0.5).abs() < 1e-15);
203        assert!((res[1] - 0.5).abs() < 1e-15);
204    }
205}