diff -r 6105b5cd8d89 -r f0e8704d3f0e src/prox_penalty/radon_squared.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/prox_penalty/radon_squared.rs Mon Feb 17 13:54:53 2025 -0500 @@ -0,0 +1,182 @@ +/*! +Solver for the point source localisation problem using a simplified forward-backward splitting method. + +Instead of the $𝒟$-norm of `fb.rs`, this uses a standard Radon norm for the proximal map. +*/ + +use numeric_literals::replace_float_literals; +use serde::{Serialize, Deserialize}; +use nalgebra::DVector; + +use alg_tools::iterate::{ + AlgIteratorIteration, + AlgIterator +}; +use alg_tools::norms::{L2, Norm}; +use alg_tools::linops::Mapping; +use alg_tools::bisection_tree::{ + BTFN, + Bounds, + BTSearch, + SupportGenerator, + LocalAnalysis, +}; +use alg_tools::mapping::RealMapping; +use alg_tools::nalgebra_support::ToNalgebraRealField; + +use crate::types::*; +use crate::measures::{ + RNDM, + DeltaMeasure, + Radon, +}; +use crate::measures::merging::SpikeMerging; +use crate::regularisation::RegTerm; +use crate::forward_model::{ + ForwardModel, + AdjointProductBoundedBy +}; +use super::{ + FBGenericConfig, + ProxPenalty, +}; + +/// Radon-norm squared proximal penalty + +#[derive(Copy,Clone,Serialize,Deserialize)] +pub struct RadonSquared; + +#[replace_float_literals(F::cast_from(literal))] +impl +ProxPenalty, Reg, N> for RadonSquared +where + F : Float + ToNalgebraRealField, + GA : SupportGenerator + Clone, + BTA : BTSearch>, + S: RealMapping + LocalAnalysis, N>, + Reg : RegTerm, + RNDM : SpikeMerging, +{ + type ReturnMapping = BTFN; + + fn insert_and_reweigh( + &self, + μ : &mut RNDM, + τv : &mut BTFN, + μ_base : &RNDM, + ν_delta: Option<&RNDM>, + τ : F, + ε : F, + config : &FBGenericConfig, + reg : &Reg, + _state : &AlgIteratorIteration, + stats : &mut IterInfo, + ) -> (Option, bool) + where + I : AlgIterator + { + let mut y = μ_base.masses_dvector(); + + assert!(μ_base.len() <= μ.len()); + + 'i_and_w: for i in 0..=1 { + // Optimise weights + if μ.len() > 0 { + // Form finite-dimensional subproblem. The subproblem references to the original μ^k + // from the beginning of the iteration are all contained in the immutable c and g. + // TODO: observe negation of -τv after switch from minus_τv: finite-dimensional + // problems have not yet been updated to sign change. + let g̃ = DVector::from_iterator(μ.len(), + μ.iter_locations() + .map(|ζ| - F::to_nalgebra_mixed(τv.apply(ζ)))); + let mut x = μ.masses_dvector(); + y.extend(std::iter::repeat(0.0.to_nalgebra_mixed()).take(0.max(x.len()-y.len()))); + assert_eq!(y.len(), x.len()); + // Solve finite-dimensional subproblem. + // TODO: This assumes that ν_delta has no common locations with μ-μ_base, to + // ignore it. + stats.inner_iters += reg.solve_findim_l1squared(&y, &g̃, τ, &mut x, ε, config); + + // Update masses of μ based on solution of finite-dimensional subproblem. + μ.set_masses_dvector(&x); + } + + if i>0 { + // Simple debugging test to see if more inserts would be needed. Doesn't seem so. + //let n = μ.dist_matching(μ_base); + //println!("{:?}", reg.find_tolerance_violation_slack(τv, τ, ε, false, config, n)); + break 'i_and_w + } + + // Calculate ‖μ - μ_base‖_ℳ + // TODO: This assumes that ν_delta has no common locations with μ-μ_base. + let n = μ.dist_matching(μ_base) + ν_delta.map_or(0.0, |ν| ν.norm(Radon)); + + // Find a spike to insert, if needed. + // This only check the overall tolerances, not tolerances on support of μ-μ_base or μ, + // which are supposed to have been guaranteed by the finite-dimensional weight optimisation. + match reg.find_tolerance_violation_slack(τv, τ, ε, false, config, n) { + None => { break 'i_and_w }, + Some((ξ, _v_ξ, _in_bounds)) => { + // Weight is found out by running the finite-dimensional optimisation algorithm + // above + *μ += DeltaMeasure { x : ξ, α : 0.0 }; + stats.inserted += 1; + } + }; + } + + (None, true) + } + + fn merge_spikes( + &self, + μ : &mut RNDM, + τv : &mut BTFN, + μ_base : &RNDM, + ν_delta: Option<&RNDM>, + τ : F, + ε : F, + config : &FBGenericConfig, + reg : &Reg, + fitness : Option) -> F>, + ) -> usize + { + if config.fitness_merging { + if let Some(f) = fitness { + return μ.merge_spikes_fitness(config.merging, f, |&v| v) + .1 + } + } + μ.merge_spikes(config.merging, |μ_candidate| { + // Important: μ_candidate's new points are afterwards, + // and do not conflict with μ_base. + // TODO: could simplify to requiring μ_base instead of μ_radon. + // but may complicate with sliding base's exgtra points that need to be + // after μ_candidate's extra points. + // TODO: doesn't seem to work, maybe need to merge μ_base as well? + // Although that doesn't seem to make sense. + let μ_radon = match ν_delta { + None => μ_candidate.sub_matching(μ_base), + Some(ν) => μ_candidate.sub_matching(μ_base) - ν, + }; + reg.verify_merge_candidate_radonsq(τv, μ_candidate, τ, ε, &config, &μ_radon) + //let n = μ_candidate.dist_matching(μ_base); + //reg.find_tolerance_violation_slack(τv, τ, ε, false, config, n).is_none() + }) + } +} + + +impl AdjointProductBoundedBy, RadonSquared> +for A +where + F : Float, + A : ForwardModel, F> +{ + type FloatType = F; + + fn adjoint_product_bound(&self, _ : &RadonSquared) -> Option { + self.opnorm_bound(Radon, L2).powi(2).into() + } +}