src/fb.rs

changeset 24
d29d1fcf5423
parent 13
bdc57366d4f5
--- a/src/fb.rs	Sun Dec 11 23:19:17 2022 +0200
+++ b/src/fb.rs	Sun Dec 11 23:25:53 2022 +0200
@@ -6,8 +6,8 @@
  * Valkonen T. - _Proximal methods for point source localisation_,
    [arXiv:2212.02991](https://arxiv.org/abs/2212.02991).
 
-The main routine is [`pointsource_fb`]. It is based on [`generic_pointsource_fb`], which is also
-used by our [primal-dual proximal splitting][crate::pdps] implementation.
+The main routine is [`pointsource_fb_reg`]. It is based on [`generic_pointsource_fb_reg`], which is
+also used by our [primal-dual proximal splitting][crate::pdps] implementation.
 
 FISTA-type inertia can also be enabled through [`FBConfig::meta`].
 
@@ -83,17 +83,17 @@
 use numeric_literals::replace_float_literals;
 use serde::{Serialize, Deserialize};
 use colored::Colorize;
-use nalgebra::DVector;
+use nalgebra::{DVector, DMatrix};
 
 use alg_tools::iterate::{
     AlgIteratorFactory,
     AlgIteratorState,
 };
 use alg_tools::euclidean::Euclidean;
-use alg_tools::norms::Norm;
 use alg_tools::linops::Apply;
 use alg_tools::sets::Cube;
 use alg_tools::loc::Loc;
+use alg_tools::mapping::Mapping;
 use alg_tools::bisection_tree::{
     BTFN,
     PreBTFN,
@@ -113,7 +113,6 @@
 use crate::measures::{
     DiscreteMeasure,
     DeltaMeasure,
-    Radon
 };
 use crate::measures::merging::{
     SpikeMergingMethod,
@@ -124,7 +123,8 @@
     DiscreteMeasureOp, Lipschitz
 };
 use crate::subproblem::{
-    quadratic_nonneg,
+    nonneg::quadratic_nonneg,
+    unconstrained::quadratic_unconstrained,
     InnerSettings,
     InnerMethod,
 };
@@ -134,6 +134,10 @@
     Plotting,
     PlotLookup
 };
+use crate::regularisation::{
+    NonnegRadonRegTerm,
+    RadonRegTerm,
+};
 
 /// Method for constructing $μ$ on each iteration
 #[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Debug)]
@@ -156,7 +160,7 @@
     InertiaFISTA,
 }
 
-/// Settings for [`pointsource_fb`].
+/// Settings for [`pointsource_fb_reg`].
 #[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Debug)]
 #[serde(default)]
 pub struct FBConfig<F : Float> {
@@ -169,7 +173,7 @@
 }
 
 /// Settings for the solution of the stepwise optimality condition in algorithms based on
-/// [`generic_pointsource_fb`].
+/// [`generic_pointsource_fb_reg`].
 #[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Debug)]
 #[serde(default)]
 pub struct FBGenericConfig<F : Float> {
@@ -236,7 +240,7 @@
     }
 }
 
-/// Trait for specialisation of [`generic_pointsource_fb`] to basic FB, FISTA.
+/// Trait for specialisation of [`generic_pointsource_fb_reg`] to basic FB, FISTA.
 ///
 /// The idea is that the residual $Aμ - b$ in the forward step can be replaced by an arbitrary
 /// value. For example, to implement [primal-dual proximal splitting][crate::pdps] we replace it
@@ -301,7 +305,7 @@
     }
 }
 
-/// Specialisation of [`generic_pointsource_fb`] to basic μFB.
+/// Specialisation of [`generic_pointsource_fb_reg`] to basic μFB.
 struct BasicFB<
     'a,
     F : Float + ToNalgebraRealField,
@@ -340,7 +344,7 @@
     }
 }
 
-/// Specialisation of [`generic_pointsource_fb`] to FISTA.
+/// Specialisation of [`generic_pointsource_fb_reg`] to FISTA.
 struct FISTA<
     'a,
     F : Float + ToNalgebraRealField,
@@ -423,62 +427,290 @@
     }
 }
 
-/// Iteratively solve the pointsource localisation problem using forward-backward splitting
-///
-/// The settings in `config` have their [respective documentation](FBConfig). `opA` is the
-/// forward operator $A$, $b$ the observable, and $\lambda$ the regularisation weight.
-/// The operator `op𝒟` is used for forming the proximal term. Typically it is a convolution
-/// operator. Finally, the `iterator` is an outer loop verbosity and iteration count control
-/// as documented in [`alg_tools::iterate`].
-///
-/// For details on the mathematical formulation, see the [module level](self) documentation.
-///
-/// Returns the final iterate.
+
+/// Abstraction of regularisation terms for [`generic_pointsource_fb_reg`].
+pub trait RegTerm<F : Float + ToNalgebraRealField, const N : usize>
+: for<'a> Apply<&'a DiscreteMeasure<Loc<F, N>, F>, Output = F> {
+    /// Approximately solve the problem
+    /// <div>$$
+    ///     \min_{x ∈ ℝ^n} \frac{1}{2} x^⊤Ax - g^⊤ x + τ G(x)
+    /// $$</div>
+    /// for $G$ depending on the trait implementation.
+    ///
+    /// The parameter `mA` is $A$. An estimate for its opeator norm should be provided in
+    /// `mA_normest`. The initial iterate and output is `x`. The current main tolerance is `ε`.
+    ///
+    /// Returns the number of iterations taken.
+    fn solve_findim(
+        &self,
+        mA : &DMatrix<F::MixedType>,
+        g : &DVector<F::MixedType>,
+        τ : F,
+        x : &mut DVector<F::MixedType>,
+        mA_normest : F,
+        ε : F,
+        config : &FBGenericConfig<F>
+    ) -> usize;
+
+    /// Find a point where `d` may violate the tolerance `ε`.
+    ///
+    /// If `skip_by_rough_check` is set, do not find the point if a rough check indicates that we
+    /// are in bounds. `ε` is the current main tolerance and `τ` a scaling factor for the
+    /// regulariser.
+    ///
+    /// Returns `None` if `d` is in bounds either based on the rough check, or a more precise check
+    /// terminating early. Otherwise returns a possibly violating point, the value of `d` there,
+    /// and a boolean indicating whether the found point is in bounds.
+    fn find_tolerance_violation<G, BT>(
+        &self,
+        d : &mut BTFN<F, G, BT, N>,
+        τ : F,
+        ε : F,
+        skip_by_rough_check : bool,
+        config : &FBGenericConfig<F>,
+    ) -> Option<(Loc<F, N>, F, bool)>
+    where BT : BTSearch<F, N, Agg=Bounds<F>>,
+          G : SupportGenerator<F, N, Id=BT::Data>,
+          G::SupportType : Mapping<Loc<F, N>,Codomain=F>
+                           + LocalAnalysis<F, Bounds<F>, N>;
+
+    /// Verify that `d` is in bounds `ε` for a merge candidate `μ`
+    ///
+    /// `ε` is the current main tolerance and `τ` a scaling factor for the regulariser.
+    fn verify_merge_candidate<G, BT>(
+        &self,
+        d : &mut BTFN<F, G, BT, N>,
+        μ : &DiscreteMeasure<Loc<F, N>, F>,
+        τ : F,
+        ε : F,
+        config : &FBGenericConfig<F>,
+    ) -> bool
+    where BT : BTSearch<F, N, Agg=Bounds<F>>,
+          G : SupportGenerator<F, N, Id=BT::Data>,
+          G::SupportType : Mapping<Loc<F, N>,Codomain=F>
+                           + LocalAnalysis<F, Bounds<F>, N>;
+
+    fn target_bounds(&self, τ : F, ε : F) -> Option<Bounds<F>>;
+
+    /// Returns a scaling factor for the tolerance sequence.
+    ///
+    /// Typically this is the regularisation parameter.
+    fn tolerance_scaling(&self) -> F;
+}
+
 #[replace_float_literals(F::cast_from(literal))]
-pub fn pointsource_fb<'a, F, I, A, GA, 𝒟, BTA, G𝒟, S, K, const N : usize>(
-    opA : &'a A,
-    b : &A::Observable,
-    α : F,
-    op𝒟 : &'a 𝒟,
-    config : &FBConfig<F>,
-    iterator : I,
-    plotter : SeqPlotter<F, N>
-) -> DiscreteMeasure<Loc<F, N>, F>
-where F : Float + ToNalgebraRealField,
-      I : AlgIteratorFactory<IterInfo<F, N>>,
-      for<'b> &'b A::Observable : std::ops::Neg<Output=A::Observable>,
-                                  //+ std::ops::Mul<F, Output=A::Observable>,  <-- FIXME: compiler overflow
-      A::Observable : std::ops::MulAssign<F>,
-      GA : SupportGenerator<F, N, SupportType = S, Id = usize> + Clone,
-      A : ForwardModel<Loc<F, N>, F, PreadjointCodomain = BTFN<F, GA, BTA, N>>
-          + Lipschitz<𝒟, FloatType=F>,
-      BTA : BTSearch<F, N, Data=usize, Agg=Bounds<F>>,
-      G𝒟 : SupportGenerator<F, N, SupportType = K, Id = usize> + Clone,
-      𝒟 : DiscreteMeasureOp<Loc<F, N>, F, PreCodomain = PreBTFN<F, G𝒟, N>>,
-      𝒟::Codomain : RealMapping<F, N>,
-      S: RealMapping<F, N> + LocalAnalysis<F, Bounds<F>, N>,
-      K: RealMapping<F, N> + LocalAnalysis<F, Bounds<F>, N>,
-      BTNodeLookup: BTNode<F, usize, Bounds<F>, N>,
-      Cube<F, N>: P2Minimise<Loc<F, N>, F>,
-      PlotLookup : Plotting<N>,
-      DiscreteMeasure<Loc<F, N>, F> : SpikeMerging<F> {
+impl<F : Float + ToNalgebraRealField, const N : usize> RegTerm<F, N> for NonnegRadonRegTerm<F>
+where Cube<F, N> : P2Minimise<Loc<F, N>, F> {
+    fn solve_findim(
+        &self,
+        mA : &DMatrix<F::MixedType>,
+        g : &DVector<F::MixedType>,
+        τ : F,
+        x : &mut DVector<F::MixedType>,
+        mA_normest : F,
+        ε : F,
+        config : &FBGenericConfig<F>
+    ) -> usize {
+        let inner_tolerance = ε * config.inner.tolerance_mult;
+        let inner_it = config.inner.iterator_options.stop_target(inner_tolerance);
+        let inner_τ = config.inner.τ0 / mA_normest;
+        quadratic_nonneg(config.inner.method, mA, g, τ * self.α(), x,
+                         inner_τ, inner_it)
+    }
+
+    #[inline]
+    fn find_tolerance_violation<G, BT>(
+        &self,
+        d : &mut BTFN<F, G, BT, N>,
+        τ : F,
+        ε : F,
+        skip_by_rough_check : bool,
+        config : &FBGenericConfig<F>,
+    ) -> Option<(Loc<F, N>, F, bool)>
+    where BT : BTSearch<F, N, Agg=Bounds<F>>,
+          G : SupportGenerator<F, N, Id=BT::Data>,
+          G::SupportType : Mapping<Loc<F, N>,Codomain=F>
+                           + LocalAnalysis<F, Bounds<F>, N> {
+        let τα = τ * self.α();
+        let keep_below = τα + ε;
+        let maximise_above = τα + ε * config.insertion_cutoff_factor;
+        let refinement_tolerance = ε * config.refinement.tolerance_mult;
 
-    let initial_residual = -b;
-    let τ = config.τ0/opA.lipschitz_factor(&op𝒟).unwrap();
+        // If preliminary check indicates that we are in bonds, and if it otherwise matches
+        // the insertion strategy, skip insertion.
+        if skip_by_rough_check && d.bounds().upper() <= keep_below {
+            None
+        } else {
+            // If the rough check didn't indicate no insertion needed, find maximising point.
+            d.maximise_above(maximise_above, refinement_tolerance, config.refinement.max_steps)
+             .map(|(ξ, v_ξ)| (ξ, v_ξ, v_ξ <= keep_below))
+        }
+    }
 
-    match config.meta {
-        FBMetaAlgorithm::None => generic_pointsource_fb(
-            opA, α, op𝒟, τ, &config.insertion, iterator, plotter, initial_residual,
-            BasicFB{ b, opA }
-        ),
-        FBMetaAlgorithm::InertiaFISTA => generic_pointsource_fb(
-            opA, α, op𝒟, τ, &config.insertion, iterator, plotter, initial_residual,
-            FISTA{ b, opA, λ : 1.0, μ_prev : DiscreteMeasure::new() }
-        ),
+    fn verify_merge_candidate<G, BT>(
+        &self,
+        d : &mut BTFN<F, G, BT, N>,
+        μ : &DiscreteMeasure<Loc<F, N>, F>,
+        τ : F,
+        ε : F,
+        config : &FBGenericConfig<F>,
+    ) -> bool
+    where BT : BTSearch<F, N, Agg=Bounds<F>>,
+          G : SupportGenerator<F, N, Id=BT::Data>,
+          G::SupportType : Mapping<Loc<F, N>,Codomain=F>
+                           + LocalAnalysis<F, Bounds<F>, N> {
+        let τα = τ * self.α();
+        let refinement_tolerance = ε * config.refinement.tolerance_mult;
+        let merge_tolerance = config.merge_tolerance_mult * ε;
+        let keep_below = τα + merge_tolerance;
+        let keep_supp_above = τα - merge_tolerance;
+        let bnd = d.bounds();
+
+        return (
+            bnd.lower() >= keep_supp_above
+            ||
+            μ.iter_spikes().map(|&DeltaMeasure{ α : β, ref x }| {
+                (β == 0.0) || d.apply(x) >= keep_supp_above
+            }).all(std::convert::identity)
+         ) && (
+            bnd.upper() <= keep_below
+            ||
+            d.has_upper_bound(keep_below, refinement_tolerance, config.refinement.max_steps)
+        )
+    }
+
+    fn target_bounds(&self, τ : F, ε : F) -> Option<Bounds<F>> {
+        let τα = τ * self.α();
+        Some(Bounds(τα - ε,  τα + ε))
+    }
+
+    fn tolerance_scaling(&self) -> F {
+        self.α()
     }
 }
 
-/// Generic implementation of [`pointsource_fb`].
+#[replace_float_literals(F::cast_from(literal))]
+impl<F : Float + ToNalgebraRealField, const N : usize> RegTerm<F, N> for RadonRegTerm<F>
+where Cube<F, N> : P2Minimise<Loc<F, N>, F> {
+    fn solve_findim(
+        &self,
+        mA : &DMatrix<F::MixedType>,
+        g : &DVector<F::MixedType>,
+        τ : F,
+        x : &mut DVector<F::MixedType>,
+        mA_normest: F,
+        ε : F,
+        config : &FBGenericConfig<F>
+    ) -> usize {
+        let inner_tolerance = ε * config.inner.tolerance_mult;
+        let inner_it = config.inner.iterator_options.stop_target(inner_tolerance);
+        let inner_τ = config.inner.τ0 / mA_normest;
+        quadratic_unconstrained(config.inner.method, mA, g, τ * self.α(), x,
+                                inner_τ, inner_it)
+    }
+
+   fn find_tolerance_violation<G, BT>(
+        &self,
+        d : &mut BTFN<F, G, BT, N>,
+        τ : F,
+        ε : F,
+        skip_by_rough_check : bool,
+        config : &FBGenericConfig<F>,
+    ) -> Option<(Loc<F, N>, F, bool)>
+    where BT : BTSearch<F, N, Agg=Bounds<F>>,
+          G : SupportGenerator<F, N, Id=BT::Data>,
+          G::SupportType : Mapping<Loc<F, N>,Codomain=F>
+                           + LocalAnalysis<F, Bounds<F>, N> {
+        let τα = τ * self.α();
+        let keep_below = τα + ε;
+        let keep_above = -τα - ε;
+        let maximise_above = τα + ε * config.insertion_cutoff_factor;
+        let minimise_below = -τα - ε * config.insertion_cutoff_factor;
+        let refinement_tolerance = ε * config.refinement.tolerance_mult;
+
+        // If preliminary check indicates that we are in bonds, and if it otherwise matches
+        // the insertion strategy, skip insertion.
+        if skip_by_rough_check && Bounds(keep_above, keep_below).superset(&d.bounds()) {
+            None
+        } else {
+            // If the rough check didn't indicate no insertion needed, find maximising point.
+            let mx = d.maximise_above(maximise_above, refinement_tolerance,
+                                      config.refinement.max_steps);
+            let mi = d.minimise_below(minimise_below, refinement_tolerance,
+                                      config.refinement.max_steps);
+
+            match (mx, mi) {
+                (None, None) => None,
+                (Some((ξ, v_ξ)), None) => Some((ξ, v_ξ, keep_below >= v_ξ)),
+                (None, Some((ζ, v_ζ))) => Some((ζ, v_ζ, keep_above <= v_ζ)),
+                (Some((ξ, v_ξ)), Some((ζ, v_ζ))) => {
+                    if v_ξ - τα > τα - v_ζ {
+                        Some((ξ, v_ξ, keep_below >= v_ξ))
+                    } else {
+                        Some((ζ, v_ζ, keep_above <= v_ζ))
+                    }
+                }
+            }
+        }
+    }
+
+    fn verify_merge_candidate<G, BT>(
+        &self,
+        d : &mut BTFN<F, G, BT, N>,
+        μ : &DiscreteMeasure<Loc<F, N>, F>,
+        τ : F,
+        ε : F,
+        config : &FBGenericConfig<F>,
+    ) -> bool
+    where BT : BTSearch<F, N, Agg=Bounds<F>>,
+          G : SupportGenerator<F, N, Id=BT::Data>,
+          G::SupportType : Mapping<Loc<F, N>,Codomain=F>
+                           + LocalAnalysis<F, Bounds<F>, N> {
+        let τα = τ * self.α();
+        let refinement_tolerance = ε * config.refinement.tolerance_mult;
+        let merge_tolerance = config.merge_tolerance_mult * ε;
+        let keep_below = τα + merge_tolerance;
+        let keep_above = -τα - merge_tolerance;
+        let keep_supp_pos_above = τα - merge_tolerance;
+        let keep_supp_neg_below = -τα + merge_tolerance;
+        let bnd = d.bounds();
+
+        return (
+            (bnd.lower() >= keep_supp_pos_above && bnd.upper() <= keep_supp_neg_below)
+            ||
+            μ.iter_spikes().map(|&DeltaMeasure{ α : β, ref x }| {
+                use std::cmp::Ordering::*;
+                match β.partial_cmp(&0.0) {
+                    Some(Greater) => d.apply(x) >= keep_supp_pos_above,
+                    Some(Less) => d.apply(x) <= keep_supp_neg_below,
+                    _ => true,
+                }
+            }).all(std::convert::identity)
+        ) && (
+            bnd.upper() <= keep_below
+            ||
+            d.has_upper_bound(keep_below, refinement_tolerance,
+                              config.refinement.max_steps)
+        ) && (
+            bnd.lower() >= keep_above
+            ||
+            d.has_lower_bound(keep_above, refinement_tolerance,
+                              config.refinement.max_steps)
+        )
+    }
+
+    fn target_bounds(&self, τ : F, ε : F) -> Option<Bounds<F>> {
+        let τα = τ * self.α();
+        Some(Bounds(-τα - ε,  τα + ε))
+    }
+
+    fn tolerance_scaling(&self) -> F {
+        self.α()
+    }
+}
+
+
+/// Generic implementation of [`pointsource_fb_reg`].
 ///
 /// The method can be specialised to even primal-dual proximal splitting through the
 /// [`FBSpecialisation`] parameter `specialisation`.
@@ -497,16 +729,18 @@
 ///
 /// Returns the final iterate.
 #[replace_float_literals(F::cast_from(literal))]
-pub fn generic_pointsource_fb<'a, F, I, A, GA, 𝒟, BTA, G𝒟, S, K, Spec, const N : usize>(
+pub fn generic_pointsource_fb_reg<
+    'a, F, I, A, GA, 𝒟, BTA, G𝒟, S, K, Spec, Reg, const N : usize
+>(
     opA : &'a A,
-    α : F,
+    reg : Reg,
     op𝒟 : &'a 𝒟,
     mut τ : F,
     config : &FBGenericConfig<F>,
     iterator : I,
     mut plotter : SeqPlotter<F, N>,
     mut residual : A::Observable,
-    mut specialisation : Spec,
+    mut specialisation : Spec
 ) -> DiscreteMeasure<Loc<F, N>, F>
 where F : Float + ToNalgebraRealField,
       I : AlgIteratorFactory<IterInfo<F, N>>,
@@ -522,17 +756,16 @@
       S: RealMapping<F, N> + LocalAnalysis<F, Bounds<F>, N>,
       K: RealMapping<F, N> + LocalAnalysis<F, Bounds<F>, N>,
       BTNodeLookup: BTNode<F, usize, Bounds<F>, N>,
-      Cube<F, N>: P2Minimise<Loc<F, N>, F>,
       PlotLookup : Plotting<N>,
-      DiscreteMeasure<Loc<F, N>, F> : SpikeMerging<F> {
+      DiscreteMeasure<Loc<F, N>, F> : SpikeMerging<F>,
+      Reg : RegTerm<F, N> {
 
     // Set up parameters
     let quiet = iterator.is_quiet();
     let op𝒟norm = op𝒟.opnorm_bound();
-    // We multiply tolerance by τ for FB since
-    // our subproblems depending on tolerances are scaled by τ compared to the conditional
-    // gradient approach.
-    let tolerance = config.tolerance * τ * α;
+    // We multiply tolerance by τ for FB since our subproblems depending on tolerances are scaled
+    // by τ compared to the conditional gradient approach.
+    let tolerance = config.tolerance * τ * reg.tolerance_scaling();
     let mut ε = tolerance.initial();
 
     // Initialise operators
@@ -568,17 +801,6 @@
 
     // Run the algorithm
     iterator.iterate(|state| {
-        // Calculate subproblem tolerances, and update main tolerance for next iteration
-        let τα = τ * α;
-        let target_bounds = Bounds(τα - ε,  τα + ε);
-        let merge_tolerance = config.merge_tolerance_mult * ε;
-        let merge_target_bounds = Bounds(τα - merge_tolerance,  τα + merge_tolerance);
-        let inner_tolerance = ε * config.inner.tolerance_mult;
-        let refinement_tolerance = ε * config.refinement.tolerance_mult;
-        let maximise_above = τα + ε * config.insertion_cutoff_factor;
-        let ε_prev = ε;
-        ε = tolerance.update(ε, state.iteration());
-
         // Maximum insertion count and measure difference calculation depend on insertion style.
         let (m, warn_insertions) = match (state.iteration(), config.bootstrap_insertions) {
             (i, Some((l, k))) if i <= l => (k, false),
@@ -626,12 +848,10 @@
                 // ≤ sup_{|z|_2 ≤ 1} |Cz|_ℳ |𝒟Cx|_∞ ≤  sup_{|z|_2 ≤ 1} |Cz|_ℳ |𝒟| |Cx|_ℳ
                 // ≤ sup_{|z|_2 ≤ 1} |z|_1 |𝒟| |x|_1 ≤ sup_{|z|_2 ≤ 1} n |z|_2 |𝒟| |x|_2
                 // = n |𝒟| |x|_2, where n is the number of points. Therefore
-                let inner_τ = config.inner.τ0 / (op𝒟norm * F::cast_from(μ.len()));
+                let Ã_normest = op𝒟norm * F::cast_from(μ.len());
 
                 // Solve finite-dimensional subproblem.
-                let inner_it = config.inner.iterator_options.stop_target(inner_tolerance);
-                inner_iters += quadratic_nonneg(config.inner.method, &Ã, &g̃, τ*α, &mut x,
-                                                inner_τ, inner_it);
+                inner_iters += reg.solve_findim(&Ã, &g̃, τ, &mut x, Ã_normest, ε, config);
 
                 // Update masses of μ based on solution of finite-dimensional subproblem.
                 μ.set_masses_dvector(&x);
@@ -644,36 +864,23 @@
             // If no merging heuristic is used, let's be more conservative about spike insertion,
             // and skip it after first round. If merging is done, being more greedy about spike
             // insertion also seems to improve performance.
-            let may_break = if let SpikeMergingMethod::None = config.merging {
+            let skip_by_rough_check = if let SpikeMergingMethod::None = config.merging {
                 false
             } else {
                 count > 0
             };
 
-            // If preliminary check indicates that we are in bonds, and if it otherwise matches
-            // the insertion strategy, skip insertion.
-            if may_break && target_bounds.superset(&d.bounds()) {
-                break 'insertion (true, d)
-            }
-
-            // If the rough check didn't indicate stopping, find maximising point, maintaining for
-            // the calculations in the beginning of the loop that v_ξ = (ω0-τv-𝒟μ)(ξ) = d(ξ),
-            // where 𝒟μ is now distinct from μ0 after the insertions already performed.
-            // We do not need to check lower bounds, as a solution of the finite-dimensional
-            // subproblem should always satisfy them.
-
-            // If μ has some spikes, only find a maximum of d if it is above a threshold
-            // defined by the refinment tolerance.
-            let (ξ, v_ξ) =  match d.maximise_above(maximise_above, refinement_tolerance,
-                                                   config.refinement.max_steps) {
+            // Find a spike to insert, if needed
+            let (ξ, _v_ξ, in_bounds) =  match reg.find_tolerance_violation(
+                &mut d, τ, ε, skip_by_rough_check, config
+            ) {
                 None => break 'insertion (true, d),
                 Some(res) => res,
             };
 
             // Break if maximum insertion count reached
             if count >= max_insertions {
-                let in_bounds2 = target_bounds.upper() >= v_ξ;
-                break 'insertion (in_bounds2, d)
+                break 'insertion (in_bounds, d)
             }
 
             // No point in optimising the weight here; the finite-dimensional algorithm is fast.
@@ -695,21 +902,8 @@
             μ.merge_spikes(config.merging, |μ_candidate| {
                 let mut d = &minus_τv + op𝒟.preapply(μ_diff(&μ_candidate, &μ_base));
 
-                if merge_target_bounds.superset(&d.bounds()) {
-                    return Some(())
-                }
-
-                let d_min_supp = μ_candidate.iter_spikes().filter_map(|&DeltaMeasure{ α, ref x }| {
-                    (α != 0.0).then(|| d.apply(x))
-                }).reduce(F::min);
-
-                if d_min_supp.map_or(true, |b| b >= merge_target_bounds.lower()) &&
-                d.has_upper_bound(merge_target_bounds.upper(), refinement_tolerance,
-                                    config.refinement.max_steps) {
-                    Some(())
-                } else {
-                    None
-                }
+                reg.verify_merge_candidate(&mut d, μ_candidate, τ, ε, &config)
+                   .then_some(())
             });
             debug_assert!(μ.len() >= n_before_merge);
             merged += μ.len() - n_before_merge;
@@ -725,6 +919,10 @@
 
         this_iters += 1;
 
+        // Update main tolerance for next iteration
+        let ε_prev = ε;
+        ε = tolerance.update(ε, state.iteration());
+
         // Give function value if needed
         state.if_verbose(|| {
             let value_μ = specialisation.value_μ(&μ);
@@ -732,12 +930,12 @@
             plotter.plot_spikes(
                 format!("iter {} end; {}", state.iteration(), within_tolerances), &d,
                 "start".to_string(), Some(&minus_τv),
-                Some(target_bounds), value_μ,
+                reg.target_bounds(τ, ε_prev), value_μ,
             );
             // Calculate mean inner iterations and reset relevant counters.
             // Return the statistics
             let res = IterInfo {
-                value : specialisation.calculate_fit(&μ, &residual) + α * value_μ.norm(Radon),
+                value : specialisation.calculate_fit(&μ, &residual) + reg.apply(value_μ),
                 n_spikes : value_μ.len(),
                 inner_iters,
                 this_iters,
@@ -757,6 +955,128 @@
     specialisation.postprocess(μ, config.final_merging)
 }
 
+/// Iteratively solve the pointsource localisation problem using forward-backward splitting
+///
+/// The settings in `config` have their [respective documentation](FBConfig). `opA` is the
+/// forward operator $A$, $b$ the observable, and $\lambda$ the regularisation weight.
+/// The operator `op𝒟` is used for forming the proximal term. Typically it is a convolution
+/// operator. Finally, the `iterator` is an outer loop verbosity and iteration count control
+/// as documented in [`alg_tools::iterate`].
+///
+/// For details on the mathematical formulation, see the [module level](self) documentation.
+///
+/// Returns the final iterate.
+#[replace_float_literals(F::cast_from(literal))]
+pub fn pointsource_fb_reg<'a, F, I, A, GA, 𝒟, BTA, G𝒟, S, K, Reg, const N : usize>(
+    opA : &'a A,
+    b : &A::Observable,
+    reg : Reg,
+    op𝒟 : &'a 𝒟,
+    config : &FBConfig<F>,
+    iterator : I,
+    plotter : SeqPlotter<F, N>,
+) -> DiscreteMeasure<Loc<F, N>, F>
+where F : Float + ToNalgebraRealField,
+      I : AlgIteratorFactory<IterInfo<F, N>>,
+      for<'b> &'b A::Observable : std::ops::Neg<Output=A::Observable>,
+                                  //+ std::ops::Mul<F, Output=A::Observable>,  <-- FIXME: compiler overflow
+      A::Observable : std::ops::MulAssign<F>,
+      GA : SupportGenerator<F, N, SupportType = S, Id = usize> + Clone,
+      A : ForwardModel<Loc<F, N>, F, PreadjointCodomain = BTFN<F, GA, BTA, N>>
+          + Lipschitz<𝒟, FloatType=F>,
+      BTA : BTSearch<F, N, Data=usize, Agg=Bounds<F>>,
+      G𝒟 : SupportGenerator<F, N, SupportType = K, Id = usize> + Clone,
+      𝒟 : DiscreteMeasureOp<Loc<F, N>, F, PreCodomain = PreBTFN<F, G𝒟, N>>,
+      𝒟::Codomain : RealMapping<F, N>,
+      S: RealMapping<F, N> + LocalAnalysis<F, Bounds<F>, N>,
+      K: RealMapping<F, N> + LocalAnalysis<F, Bounds<F>, N>,
+      BTNodeLookup: BTNode<F, usize, Bounds<F>, N>,
+      Cube<F, N>: P2Minimise<Loc<F, N>, F>,
+      PlotLookup : Plotting<N>,
+      DiscreteMeasure<Loc<F, N>, F> : SpikeMerging<F>,
+      Reg : RegTerm<F, N> {
+
+    let initial_residual = -b;
+    let τ = config.τ0/opA.lipschitz_factor(&op𝒟).unwrap();
+
+    match config.meta {
+        FBMetaAlgorithm::None => generic_pointsource_fb_reg(
+            opA, reg, op𝒟, τ, &config.insertion, iterator, plotter, initial_residual,
+            BasicFB{ b, opA },
+        ),
+        FBMetaAlgorithm::InertiaFISTA => generic_pointsource_fb_reg(
+            opA, reg, op𝒟, τ, &config.insertion, iterator, plotter, initial_residual,
+            FISTA{ b, opA, λ : 1.0, μ_prev : DiscreteMeasure::new() },
+        ),
+    }
+}
+
+//
+// Deprecated interfaces
+//
+
+#[deprecated(note = "Use `pointsource_fb_reg`")]
+pub fn pointsource_fb<'a, F, I, A, GA, 𝒟, BTA, G𝒟, S, K, const N : usize>(
+    opA : &'a A,
+    b : &A::Observable,
+    α : F,
+    op𝒟 : &'a 𝒟,
+    config : &FBConfig<F>,
+    iterator : I,
+    plotter : SeqPlotter<F, N>
+) -> DiscreteMeasure<Loc<F, N>, F>
+where F : Float + ToNalgebraRealField,
+      I : AlgIteratorFactory<IterInfo<F, N>>,
+      for<'b> &'b A::Observable : std::ops::Neg<Output=A::Observable>,
+      A::Observable : std::ops::MulAssign<F>,
+      GA : SupportGenerator<F, N, SupportType = S, Id = usize> + Clone,
+      A : ForwardModel<Loc<F, N>, F, PreadjointCodomain = BTFN<F, GA, BTA, N>>
+          + Lipschitz<𝒟, FloatType=F>,
+      BTA : BTSearch<F, N, Data=usize, Agg=Bounds<F>>,
+      G𝒟 : SupportGenerator<F, N, SupportType = K, Id = usize> + Clone,
+      𝒟 : DiscreteMeasureOp<Loc<F, N>, F, PreCodomain = PreBTFN<F, G𝒟, N>>,
+      𝒟::Codomain : RealMapping<F, N>,
+      S: RealMapping<F, N> + LocalAnalysis<F, Bounds<F>, N>,
+      K: RealMapping<F, N> + LocalAnalysis<F, Bounds<F>, N>,
+      BTNodeLookup: BTNode<F, usize, Bounds<F>, N>,
+      Cube<F, N>: P2Minimise<Loc<F, N>, F>,
+      PlotLookup : Plotting<N>,
+      DiscreteMeasure<Loc<F, N>, F> : SpikeMerging<F> {
+
+    pointsource_fb_reg(opA, b, NonnegRadonRegTerm(α), op𝒟, config, iterator, plotter)
+}
 
 
+#[deprecated(note = "Use `generic_pointsource_fb_reg`")]
+pub fn generic_pointsource_fb<'a, F, I, A, GA, 𝒟, BTA, G𝒟, S, K, Spec, const N : usize>(
+    opA : &'a A,
+    α : F,
+    op𝒟 : &'a 𝒟,
+    τ : F,
+    config : &FBGenericConfig<F>,
+    iterator : I,
+    plotter : SeqPlotter<F, N>,
+    residual : A::Observable,
+    specialisation : Spec,
+) -> DiscreteMeasure<Loc<F, N>, F>
+where F : Float + ToNalgebraRealField,
+      I : AlgIteratorFactory<IterInfo<F, N>>,
+      Spec : FBSpecialisation<F, A::Observable, N>,
+      A::Observable : std::ops::MulAssign<F>,
+      GA : SupportGenerator<F, N, SupportType = S, Id = usize> + Clone,
+      A : ForwardModel<Loc<F, N>, F, PreadjointCodomain = BTFN<F, GA, BTA, N>>
+          + Lipschitz<𝒟, FloatType=F>,
+      BTA : BTSearch<F, N, Data=usize, Agg=Bounds<F>>,
+      G𝒟 : SupportGenerator<F, N, SupportType = K, Id = usize> + Clone,
+      𝒟 : DiscreteMeasureOp<Loc<F, N>, F, PreCodomain = PreBTFN<F, G𝒟, N>>,
+      𝒟::Codomain : RealMapping<F, N>,
+      S: RealMapping<F, N> + LocalAnalysis<F, Bounds<F>, N>,
+      K: RealMapping<F, N> + LocalAnalysis<F, Bounds<F>, N>,
+      BTNodeLookup: BTNode<F, usize, Bounds<F>, N>,
+      Cube<F, N>: P2Minimise<Loc<F, N>, F>,
+      PlotLookup : Plotting<N>,
+      DiscreteMeasure<Loc<F, N>, F> : SpikeMerging<F> {
 
+      generic_pointsource_fb_reg(opA, NonnegRadonRegTerm(α), op𝒟, τ, config, iterator, plotter,
+                                 residual, specialisation)
+}

mercurial