1use nalgebra::{DMatrix, DVector};
2
3use crate::{GaussianLinearBelief, LinearSolverError, SpdLinearSystem};
4
5const INFORMATION_TOLERANCE: f64 = 128.0 * f64::EPSILON;
6
7#[derive(Debug, Clone, PartialEq)]
9pub struct SelectedLinearDirection {
10 direction: Vec<f64>,
11 index: usize,
12 trace_reduction: f64,
13}
14
15impl SelectedLinearDirection {
16 #[must_use]
18 pub fn direction(&self) -> &[f64] {
19 &self.direction
20 }
21
22 #[must_use]
24 pub const fn index(&self) -> usize {
25 self.index
26 }
27
28 #[must_use]
30 pub const fn trace_reduction(&self) -> f64 {
31 self.trace_reduction
32 }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum CovarianceGreedyTermination {
38 CovarianceTraceToleranceReached,
40 ProjectionBudgetReached,
42 CandidatesExhausted,
44 NoInformativeDirection,
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub struct CovarianceGreedyStep {
51 direction: Vec<f64>,
52 predicted_trace_reduction: f64,
53 posterior_trace: f64,
54}
55
56impl CovarianceGreedyStep {
57 #[must_use]
59 pub fn direction(&self) -> &[f64] {
60 &self.direction
61 }
62
63 #[must_use]
65 pub const fn predicted_trace_reduction(&self) -> f64 {
66 self.predicted_trace_reduction
67 }
68
69 #[must_use]
71 pub const fn posterior_trace(&self) -> f64 {
72 self.posterior_trace
73 }
74}
75
76#[derive(Debug, Clone, PartialEq)]
78pub struct CovarianceGreedySolveResult {
79 belief: GaussianLinearBelief,
80 steps: Vec<CovarianceGreedyStep>,
81 remaining_candidates: Vec<Vec<f64>>,
82 termination: CovarianceGreedyTermination,
83}
84
85impl CovarianceGreedySolveResult {
86 #[must_use]
88 pub const fn belief(&self) -> &GaussianLinearBelief {
89 &self.belief
90 }
91
92 #[must_use]
94 pub fn steps(&self) -> &[CovarianceGreedyStep] {
95 &self.steps
96 }
97
98 #[must_use]
100 pub fn remaining_candidates(&self) -> &[Vec<f64>] {
101 &self.remaining_candidates
102 }
103
104 #[must_use]
106 pub const fn termination(&self) -> CovarianceGreedyTermination {
107 self.termination
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq)]
113pub struct CovarianceTraceAcquisition;
114
115impl CovarianceTraceAcquisition {
116 pub fn reduction(
129 system: &SpdLinearSystem,
130 belief: &GaussianLinearBelief,
131 direction: &[f64],
132 ) -> Result<f64, LinearSolverError> {
133 if system.dimension() != belief.dimension() || direction.len() != system.dimension() {
134 return Err(LinearSolverError::VectorDimensionMismatch);
135 }
136 if direction.iter().any(|value| !value.is_finite()) {
137 return Err(LinearSolverError::NonFiniteVectorEntry);
138 }
139
140 let dimension = system.dimension();
141 let matrix = DMatrix::from_row_slice(dimension, dimension, system.matrix());
142 let covariance = DMatrix::from_row_slice(dimension, dimension, belief.covariance());
143 let search = DVector::from_column_slice(direction);
144 let h = matrix.transpose() * search;
145 let covariance_h = &covariance * &h;
146 let denominator = h.dot(&covariance_h);
147 let scale = covariance
148 .iter()
149 .fold(1.0_f64, |acc, value| acc.max(value.abs()));
150 let tolerance = INFORMATION_TOLERANCE * scale;
151 if denominator <= tolerance {
152 return Ok(0.0);
153 }
154
155 Ok(covariance_h.dot(&covariance_h) / denominator)
156 }
157
158 pub fn select_best(
166 system: &SpdLinearSystem,
167 belief: &GaussianLinearBelief,
168 candidates: &[Vec<f64>],
169 ) -> Result<SelectedLinearDirection, LinearSolverError> {
170 if candidates.is_empty() {
171 return Err(LinearSolverError::EmptyCandidateDirections);
172 }
173
174 let mut best = SelectedLinearDirection {
175 direction: candidates[0].clone(),
176 index: 0,
177 trace_reduction: Self::reduction(system, belief, &candidates[0])?,
178 };
179 for (index, candidate) in candidates.iter().enumerate().skip(1) {
180 let reduction = Self::reduction(system, belief, candidate)?;
181 if reduction > best.trace_reduction {
182 best = SelectedLinearDirection {
183 direction: candidate.clone(),
184 index,
185 trace_reduction: reduction,
186 };
187 }
188 }
189 Ok(best)
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq)]
195pub struct CovarianceGreedyProjectionSolver {
196 covariance_trace_tolerance: f64,
197 max_projections: usize,
198}
199
200impl CovarianceGreedyProjectionSolver {
201 pub fn new(
207 covariance_trace_tolerance: f64,
208 max_projections: usize,
209 ) -> Result<Self, LinearSolverError> {
210 if !covariance_trace_tolerance.is_finite() {
211 return Err(LinearSolverError::NonFiniteTolerance);
212 }
213 if covariance_trace_tolerance < 0.0 {
214 return Err(LinearSolverError::NegativeTolerance);
215 }
216 Ok(Self {
217 covariance_trace_tolerance,
218 max_projections,
219 })
220 }
221
222 pub fn solve(
232 &self,
233 system: &SpdLinearSystem,
234 initial_belief: &GaussianLinearBelief,
235 candidates: &[Vec<f64>],
236 ) -> Result<CovarianceGreedySolveResult, LinearSolverError> {
237 if system.dimension() != initial_belief.dimension() {
238 return Err(LinearSolverError::VectorDimensionMismatch);
239 }
240 if candidates.is_empty() {
241 return Err(LinearSolverError::EmptyCandidateDirections);
242 }
243
244 let mut belief = initial_belief.clone();
245 let mut remaining_candidates = candidates.to_vec();
246 let mut steps = Vec::new();
247
248 if covariance_trace(&belief) <= self.covariance_trace_tolerance {
249 return Ok(CovarianceGreedySolveResult {
250 belief,
251 steps,
252 remaining_candidates,
253 termination: CovarianceGreedyTermination::CovarianceTraceToleranceReached,
254 });
255 }
256
257 for _ in 0..self.max_projections {
258 if remaining_candidates.is_empty() {
259 return Ok(CovarianceGreedySolveResult {
260 belief,
261 steps,
262 remaining_candidates,
263 termination: CovarianceGreedyTermination::CandidatesExhausted,
264 });
265 }
266
267 let selected =
268 CovarianceTraceAcquisition::select_best(system, &belief, &remaining_candidates)?;
269 if selected.trace_reduction() <= INFORMATION_TOLERANCE {
270 return Ok(CovarianceGreedySolveResult {
271 belief,
272 steps,
273 remaining_candidates,
274 termination: CovarianceGreedyTermination::NoInformativeDirection,
275 });
276 }
277
278 let trace_before = covariance_trace(&belief);
279 let updated = belief.condition_on_projection(system, selected.direction())?;
280 let trace_after = covariance_trace(&updated);
281 let actual_reduction = trace_before - trace_after;
282 let tolerance = 1.0e-10 * selected.trace_reduction().abs().max(1.0);
283 debug_assert!((actual_reduction - selected.trace_reduction()).abs() <= tolerance);
284
285 remaining_candidates.remove(selected.index());
286 steps.push(CovarianceGreedyStep {
287 direction: selected.direction,
288 predicted_trace_reduction: selected.trace_reduction,
289 posterior_trace: trace_after,
290 });
291 belief = updated;
292
293 if trace_after <= self.covariance_trace_tolerance {
294 return Ok(CovarianceGreedySolveResult {
295 belief,
296 steps,
297 remaining_candidates,
298 termination: CovarianceGreedyTermination::CovarianceTraceToleranceReached,
299 });
300 }
301 }
302
303 let termination = if remaining_candidates.is_empty() {
304 CovarianceGreedyTermination::CandidatesExhausted
305 } else {
306 CovarianceGreedyTermination::ProjectionBudgetReached
307 };
308 Ok(CovarianceGreedySolveResult {
309 belief,
310 steps,
311 remaining_candidates,
312 termination,
313 })
314 }
315}
316
317fn covariance_trace(belief: &GaussianLinearBelief) -> f64 {
318 let dimension = belief.dimension();
319 (0..dimension)
320 .map(|index| belief.covariance()[index * dimension + index])
321 .sum()
322}
323
324#[cfg(test)]
325mod tests {
326 use super::{
327 CovarianceGreedyProjectionSolver, CovarianceGreedyTermination, CovarianceTraceAcquisition,
328 };
329 use crate::{GaussianLinearBelief, LinearSolverError, SpdLinearSystem};
330
331 fn identity_belief(dimension: usize) -> GaussianLinearBelief {
332 let mut covariance = vec![0.0; dimension * dimension];
333 for index in 0..dimension {
334 covariance[index * dimension + index] = 1.0;
335 }
336 GaussianLinearBelief::new(&vec![0.0; dimension], &covariance, dimension)
337 .expect("identity belief is valid")
338 }
339
340 #[test]
341 fn predicted_trace_reduction_matches_actual_conditioning_drop() {
342 let system =
343 SpdLinearSystem::new(&[4.0, 1.0, 1.0, 3.0], &[1.0, 2.0], 2).expect("system is valid");
344 let belief = identity_belief(2);
345 let direction = [1.0, 0.0];
346 let predicted = CovarianceTraceAcquisition::reduction(&system, &belief, &direction)
347 .expect("direction is valid");
348 let updated = belief
349 .condition_on_projection(&system, &direction)
350 .expect("direction is informative");
351 let actual = 2.0 - updated.covariance()[0] - updated.covariance()[3];
352 assert!((predicted - actual).abs() <= 1.0e-12);
353 }
354
355 #[test]
356 fn selector_returns_global_trace_reduction_maximum() {
357 let system =
358 SpdLinearSystem::new(&[4.0, 1.0, 1.0, 3.0], &[1.0, 2.0], 2).expect("system is valid");
359 let belief = identity_belief(2);
360 let candidates = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 1.0]];
361 let selected = CovarianceTraceAcquisition::select_best(&system, &belief, &candidates)
362 .expect("candidates are valid");
363 for candidate in &candidates {
364 let reduction = CovarianceTraceAcquisition::reduction(&system, &belief, candidate)
365 .expect("candidate is valid");
366 assert!(selected.trace_reduction() + 1.0e-12 >= reduction);
367 }
368 }
369
370 #[test]
371 fn sequential_selection_does_not_use_rhs_values() {
372 let matrix = [4.0, 1.0, 1.0, 3.0];
373 let first_system = SpdLinearSystem::new(&matrix, &[1.0, 2.0], 2).expect("valid system");
374 let second_system = SpdLinearSystem::new(&matrix, &[-7.0, 4.0], 2).expect("valid system");
375 let candidates = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 1.0]];
376 let solver = CovarianceGreedyProjectionSolver::new(0.0, 2).expect("solver is valid");
377 let first = solver
378 .solve(&first_system, &identity_belief(2), &candidates)
379 .expect("solve is valid");
380 let second = solver
381 .solve(&second_system, &identity_belief(2), &candidates)
382 .expect("solve is valid");
383 let first_directions: Vec<&[f64]> = first
384 .steps()
385 .iter()
386 .map(super::CovarianceGreedyStep::direction)
387 .collect();
388 let second_directions: Vec<&[f64]> = second
389 .steps()
390 .iter()
391 .map(super::CovarianceGreedyStep::direction)
392 .collect();
393 assert_eq!(first_directions, second_directions);
394 }
395
396 #[test]
397 fn reports_projection_budget_termination() {
398 let system =
399 SpdLinearSystem::new(&[4.0, 1.0, 1.0, 3.0], &[1.0, 2.0], 2).expect("system is valid");
400 let candidates = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 1.0]];
401 let solver = CovarianceGreedyProjectionSolver::new(0.0, 1).expect("solver is valid");
402 let result = solver
403 .solve(&system, &identity_belief(2), &candidates)
404 .expect("solve is valid");
405 assert_eq!(
406 result.termination(),
407 CovarianceGreedyTermination::ProjectionBudgetReached
408 );
409 assert_eq!(result.steps().len(), 1);
410 }
411
412 #[test]
413 fn rejects_empty_candidates() {
414 let system = SpdLinearSystem::new(&[1.0], &[1.0], 1).expect("system is valid");
415 let solver = CovarianceGreedyProjectionSolver::new(0.0, 1).expect("solver is valid");
416 assert_eq!(
417 solver.solve(&system, &identity_belief(1), &[]),
418 Err(LinearSolverError::EmptyCandidateDirections)
419 );
420 }
421}