1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
use crateBoxConstraints;
use crate;
use crate;
use crateSolver;
use crateBasicState;
use crateTerminationReason;
use crate;
/// Projected gradient descent for box-constrained problems.
///
/// Steepest-descent step along `−∇f` followed by an element-wise
/// projection back into `[lower, upper]`. The first n-D constrained
/// solver in basin and the smallest vehicle for the
/// [`BoxConstraints`] trait — handing this solver an unconstrained
/// problem is a compile error per tenet 4.
///
/// # Algorithm
///
/// At [`init`](Solver::init) the iterate is projected onto the feasible
/// box once, so an infeasible starting point is silently corrected
/// (and downstream termination criteria see a feasible iterate at iter
/// 0). Each [`next_iter`](Solver::next_iter) then computes
///
/// ```text
/// d ← −∇f(x)
/// α ← line_search.next(...) # on the unconstrained step
/// x ← π_C(x + α d) # project after the step
/// ```
///
/// where `π_C` clamps each component into `[lower, upper]`.
///
/// # Contract
///
/// - **Caller must:** implement [`BoxConstraints`] on the problem with
/// `lower[i] ≤ upper[i]` for every component. Equal bounds are
/// allowed (the corresponding component is pinned).
/// - **Caller must:** pair with a feasible **or** infeasible initial
/// param; an infeasible start is projected at `init`.
/// - **Implementor (this solver) must:** maintain feasibility across
/// iterations — once the loop has run, every iterate the executor
/// sees is in the box.
///
/// The line search runs against the *unconstrained* trial step
/// `f(x + α d)`. If the projection moves the post-step iterate
/// substantially, Armijo guarantees on the unconstrained step do not
/// transfer to `f(π_C(x + α d))`. For tighter guarantees use a small
/// fixed step ([`Constant`]) or wait on a constraint-aware (SPG-style)
/// line search.
///
/// # Termination
///
/// No solver-internal optimality test; the canonical first-order
/// metric is provided as a framework-level criterion,
/// [`ProjectedGradientTolerance`](crate::core::termination::ProjectedGradientTolerance),
/// which captures the bounds at construction so it does not need
/// problem access in `check`. The framework's
/// [`MaxIter`](crate::core::termination::MaxIter),
/// [`CostTolerance`](crate::core::termination::CostTolerance),
/// [`ParamTolerance`](crate::core::termination::ParamTolerance), and
/// [`MaxTime`](crate::core::termination::MaxTime) work on
/// [`BasicState`] for free.
///
/// # Backends
///
/// Backend-generic — works with any `V` implementing
/// [`ScaledAdd<f64>`](crate::core::math::ScaledAdd) +
/// [`NegInPlace`] + [`ClampInPlace`] + `Clone`. That covers
/// `Vec<f64>`, `nalgebra::DVector<f64>` (feature `nalgebra`),
/// `ndarray::Array1<f64>` (feature `ndarray`), and `faer::Col<f64>`
/// (feature `faer`). The problem must implement [`BoxConstraints`].
///
/// # Examples
///
/// Box-constrained gradient descent. The bounds live on the problem via
/// [`BoxConstraints`]; the projection keeps every iterate feasible. Here
/// the unconstrained minimum of the shifted sphere is at `(2, 2)`, but the
/// box caps it at `(1, 1)`:
///
/// ```
/// use basin::{BasicState, BoxConstraints, CostFunction, Executor, Gradient, ProjectedGradientDescent};
///
/// struct ShiftedSphere {
/// lower: Vec<f64>,
/// upper: Vec<f64>,
/// }
/// impl CostFunction for ShiftedSphere {
/// type Param = Vec<f64>;
/// type Output = f64;
/// type Error = std::convert::Infallible;
/// fn cost(&self, x: &Vec<f64>) -> Result<f64, Self::Error> {
/// Ok((x[0] - 2.0).powi(2) + (x[1] - 2.0).powi(2))
/// }
/// }
/// impl Gradient for ShiftedSphere {
/// type Gradient = Vec<f64>;
/// fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
/// Ok(vec![2.0 * (x[0] - 2.0), 2.0 * (x[1] - 2.0)])
/// }
/// }
/// impl BoxConstraints for ShiftedSphere {
/// fn lower(&self) -> &Vec<f64> { &self.lower }
/// fn upper(&self) -> &Vec<f64> { &self.upper }
/// }
///
/// let problem = ShiftedSphere { lower: vec![-1.0, -1.0], upper: vec![1.0, 1.0] };
/// let result = Executor::new(
/// problem,
/// ProjectedGradientDescent::new(0.1),
/// BasicState::new(vec![0.0, 0.0]),
/// )
/// .max_iter(1_000)
/// .run()
/// .unwrap();
/// assert!((result.param()[0] - 1.0).abs() < 1e-6);
/// assert!((result.param()[1] - 1.0).abs() < 1e-6);
/// ```