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}