diff -r 6105b5cd8d89 -r f0e8704d3f0e src/forward_model/sensor_grid.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/forward_model/sensor_grid.rs Mon Feb 17 13:54:53 2025 -0500 @@ -0,0 +1,645 @@ +/*! +Sensor grid forward model +*/ + +use nalgebra::base::{DMatrix, DVector}; +use numeric_literals::replace_float_literals; +use std::iter::Zip; +use std::ops::RangeFrom; + +use alg_tools::bisection_tree::*; +use alg_tools::error::DynError; +use alg_tools::instance::Instance; +use alg_tools::iter::{MapX, Mappable}; +use alg_tools::lingrid::*; +pub use alg_tools::linops::*; +use alg_tools::mapping::{DifferentiableMapping, RealMapping}; +use alg_tools::maputil::map2; +use alg_tools::nalgebra_support::ToNalgebraRealField; +use alg_tools::norms::{Linfinity, Norm, L1, L2}; +use alg_tools::tabledump::write_csv; + +use super::{AdjointProductBoundedBy, BoundedCurvature, ForwardModel}; +use crate::frank_wolfe::FindimQuadraticModel; +use crate::kernels::{AutoConvolution, BoundedBy, Convolution}; +use crate::measures::{DiscreteMeasure, Radon}; +use crate::preadjoint_helper::PreadjointHelper; +use crate::seminorms::{ConvolutionOp, SimpleConvolutionKernel}; +use crate::types::*; + +type RNDM = DiscreteMeasure, F>; + +pub type ShiftedSensor = Shift, F, N>; + +/// Trait for physical convolution models. Has blanket implementation for all cases. +pub trait Spread: + 'static + Clone + Support + RealMapping + Bounded +{ +} + +impl Spread for T +where + F: Float, + T: 'static + Clone + Support + Bounded + RealMapping, +{ +} + +/// Trait for compactly supported sensors. Has blanket implementation for all cases. +pub trait Sensor: + Spread + Norm + Norm +{ +} + +impl Sensor for T +where + F: Float, + T: Spread + Norm + Norm, +{ +} + +pub trait SensorGridBT: + Clone + BTImpl> +where + F: Float, + S: Sensor, + P: Spread, +{ +} + +impl SensorGridBT for T +where + T: Clone + BTImpl>, + F: Float, + S: Sensor, + P: Spread, +{ +} + +// We need type alias bounds to access associated types +#[allow(type_alias_bounds)] +pub type SensorGridBTFN, const N: usize> = + BTFN, BT, N>; + +/// Sensor grid forward model +#[derive(Clone)] +pub struct SensorGrid +where + F: Float, + S: Sensor, + P: Spread, + Convolution: Spread, + BT: SensorGridBT, +{ + domain: Cube, + sensor_count: [usize; N], + sensor: S, + spread: P, + base_sensor: Convolution, + bt: BT, +} + +impl SensorGrid +where + F: Float, + BT: SensorGridBT, + S: Sensor, + P: Spread, + Convolution: Spread + LocalAnalysis, +{ + /// Create a new sensor grid. + /// + /// The parameter `depth` indicates the search depth of the created [`BT`]s + /// for the adjoint values. + pub fn new( + domain: Cube, + sensor_count: [usize; N], + sensor: S, + spread: P, + depth: BT::Depth, + ) -> Self { + let base_sensor = Convolution(sensor.clone(), spread.clone()); + let bt = BT::new(domain, depth); + let mut sensorgrid = SensorGrid { + domain, + sensor_count, + sensor, + spread, + base_sensor, + bt, + }; + + for (x, id) in sensorgrid.grid().into_iter().zip(0usize..) { + let s = sensorgrid.shifted_sensor(x); + sensorgrid.bt.insert(id, &s); + } + + sensorgrid + } +} + +impl SensorGrid +where + F: Float, + BT: SensorGridBT, + S: Sensor, + P: Spread, + Convolution: Spread, +{ + /// Return the grid of sensor locations. + pub fn grid(&self) -> LinGrid { + lingrid_centered(&self.domain, &self.sensor_count) + } + + /// Returns the number of sensors (number of grid points) + pub fn n_sensors(&self) -> usize { + self.sensor_count.iter().product() + } + + /// Constructs a sensor shifted by `x`. + #[inline] + fn shifted_sensor(&self, x: Loc) -> ShiftedSensor { + self.base_sensor.clone().shift(x) + } + + #[inline] + fn _zero_observable(&self) -> DVector { + DVector::zeros(self.n_sensors()) + } + + /// Returns the maximum number of overlapping sensors $N_\psi$. + pub fn max_overlapping(&self) -> F { + let w = self.base_sensor.support_hint().width(); + let d = map2(self.domain.width(), &self.sensor_count, |wi, &i| { + wi / F::cast_from(i) + }); + w.iter() + .zip(d.iter()) + .map(|(&wi, &di)| (wi / di).ceil()) + .reduce(F::mul) + .unwrap() + } +} + +impl Mapping> for SensorGrid +where + F: Float, + BT: SensorGridBT, + S: Sensor, + P: Spread, + Convolution: Spread, +{ + type Codomain = DVector; + + #[inline] + fn apply>>(&self, μ: I) -> DVector { + let mut y = self._zero_observable(); + self.apply_add(&mut y, μ); + y + } +} + +impl Linear> for SensorGrid +where + F: Float, + BT: SensorGridBT, + S: Sensor, + P: Spread, + Convolution: Spread, +{ +} + +#[replace_float_literals(F::cast_from(literal))] +impl GEMV, DVector> for SensorGrid +where + F: Float, + BT: SensorGridBT, + S: Sensor, + P: Spread, + Convolution: Spread, +{ + fn gemv>>(&self, y: &mut DVector, α: F, μ: I, β: F) { + let grid = self.grid(); + if β == 0.0 { + y.fill(0.0) + } else if β != 1.0 { + *y *= β; // Need to multiply first, as we have to be able to add to y. + } + if α == 1.0 { + self.apply_add(y, μ) + } else { + for δ in μ.ref_instance() { + for &d in self.bt.iter_at(&δ.x) { + let sensor = self.shifted_sensor(grid.entry_linear_unchecked(d)); + y[d] += sensor.apply(&δ.x) * (α * δ.α); + } + } + } + } + + fn apply_add>>(&self, y: &mut DVector, μ: I) { + let grid = self.grid(); + for δ in μ.ref_instance() { + for &d in self.bt.iter_at(&δ.x) { + let sensor = self.shifted_sensor(grid.entry_linear_unchecked(d)); + y[d] += sensor.apply(&δ.x) * δ.α; + } + } + } +} + +impl BoundedLinear, Radon, L2, F> + for SensorGrid +where + F: Float, + BT: SensorGridBT>, + S: Sensor, + P: Spread, + Convolution: Spread + LocalAnalysis, +{ + /// An estimate on the operator norm in $𝕃(ℳ(Ω); ℝ^n)$ with $ℳ(Ω)$ equipped + /// with the Radon norm, and $ℝ^n$ with the Euclidean norm. + fn opnorm_bound(&self, _: Radon, _: L2) -> F { + // With {x_i}_{i=1}^n the grid centres and φ the kernel, we have + // |Aμ|_2 = sup_{|z|_2 ≤ 1} ⟨z,Αμ⟩ = sup_{|z|_2 ≤ 1} ⟨A^*z|μ⟩ + // ≤ sup_{|z|_2 ≤ 1} |A^*z|_∞ |μ|_ℳ + // = sup_{|z|_2 ≤ 1} |∑ φ(· - x_i)z_i|_∞ |μ|_ℳ + // ≤ sup_{|z|_2 ≤ 1} |φ(y)| ∑_{i:th sensor active at y}|z_i| |μ|_ℳ + // where the supremum of |∑ φ(· - x_i)z_i|_∞ is reached at y + // ≤ sup_{|z|_2 ≤ 1} |φ|_∞ √N_ψ |z|_2 |μ|_ℳ + // where N_ψ is the maximum number of sensors that overlap, and + // |z|_2 is restricted to the active sensors. + // = |φ|_∞ √N_ψ |μ|_ℳ. + // Hence + let n = self.max_overlapping(); + self.base_sensor.bounds().uniform() * n.sqrt() + } +} + +type SensorGridPreadjoint<'a, A, F, const N: usize> = PreadjointHelper<'a, A, RNDM>; + +impl Preadjointable, DVector> + for SensorGrid +where + F: Float, + BT: SensorGridBT, + S: Sensor, + P: Spread, + Convolution: Spread + LocalAnalysis, +{ + type PreadjointCodomain = BTFN, BT, N>; + type Preadjoint<'a> + = SensorGridPreadjoint<'a, Self, F, N> + where + Self: 'a; + + fn preadjoint(&self) -> Self::Preadjoint<'_> { + PreadjointHelper::new(self) + } +} + +/* +#[replace_float_literals(F::cast_from(literal))] +impl<'a, F, S, P, BT, const N : usize> LipschitzValues +for SensorGridPreadjoint<'a, SensorGrid, F, N> +where F : Float, + BT : SensorGridBT, + S : Sensor, + P : Spread, + Convolution : Spread + Lipschitz + DifferentiableMapping> + LocalAnalysis, + for<'b> as DifferentiableMapping>>::Differential<'b> : Lipschitz, +{ + + type FloatType = F; + + fn value_unit_lipschitz_factor(&self) -> Option { + // The Lipschitz factor of the sensors has to be scaled by the square root of twice + // the number of overlapping sensors at a single ponit, as Lipschitz estimates involve + // two points. + let fw = self.forward_op; + let n = fw.max_overlapping(); + fw.base_sensor.lipschitz_factor(L2).map(|l| (2.0 * n).sqrt() * l) + } + + fn value_diff_unit_lipschitz_factor(&self) -> Option { + // The Lipschitz factor of the sensors has to be scaled by the square root of twice + // the number of overlapping sensors at a single ponit, as Lipschitz estimates involve + // two points. + let fw = self.forward_op; + let n = fw.max_overlapping(); + fw.base_sensor.diff_ref().lipschitz_factor(L2).map(|l| (2.0 * n).sqrt() * l) + } +} +*/ + +#[replace_float_literals(F::cast_from(literal))] +impl<'a, F, S, P, BT, const N: usize> BoundedCurvature for SensorGrid +where + F: Float, + BT: SensorGridBT, + S: Sensor, + P: Spread, + Convolution: Spread + + Lipschitz + + DifferentiableMapping> + + LocalAnalysis, + for<'b> as DifferentiableMapping>>::Differential<'b>: + Lipschitz, +{ + type FloatType = F; + + /// Returns factors $ℓ_F$ and $Θ²$ such that + /// $B_{F'(μ)} dγ ≤ ℓ_F c_2$ and $⟨F'(μ)+F'(μ+Δ)|Δ⟩ ≤ Θ²|γ|(c_2)‖γ‖$, + /// where $Δ=(π_♯^1-π_♯^0)γ$. + /// + /// See Lemma 3.8, Lemma 5.10, Remark 5.14, and Example 5.15. + fn curvature_bound_components(&self) -> (Option, Option) { + let n_ψ = self.max_overlapping(); + let ψ_diff_lip = self.base_sensor.diff_ref().lipschitz_factor(L2); + let ψ_lip = self.base_sensor.lipschitz_factor(L2); + let ℓ_F = ψ_diff_lip.map(|l| (2.0 * n_ψ).sqrt() * l); + let θ2 = ψ_lip.map(|l| 4.0 * n_ψ * l.powi(2)); + + (ℓ_F, θ2) + } +} + +#[derive(Clone, Debug)] +pub struct SensorGridSupportGenerator +where + F: Float, + S: Sensor, + P: Spread, +{ + base_sensor: Convolution, + grid: LinGrid, + weights: DVector, +} + +impl SensorGridSupportGenerator +where + F: Float, + S: Sensor, + P: Spread, + Convolution: Spread, +{ + #[inline] + fn construct_sensor(&self, id: usize, w: F) -> Weighted, F> { + let x = self.grid.entry_linear_unchecked(id); + self.base_sensor.clone().shift(x).weigh(w) + } + + #[inline] + fn construct_sensor_and_id<'a>( + &'a self, + (id, w): (usize, &'a F), + ) -> (usize, Weighted, F>) { + (id.into(), self.construct_sensor(id, *w)) + } +} + +impl SupportGenerator for SensorGridSupportGenerator +where + F: Float, + S: Sensor, + P: Spread, + Convolution: Spread, +{ + type Id = usize; + type SupportType = Weighted, F>; + type AllDataIter<'a> + = MapX< + 'a, + Zip, std::slice::Iter<'a, F>>, + Self, + (Self::Id, Self::SupportType), + > + where + Self: 'a; + + #[inline] + fn support_for(&self, d: Self::Id) -> Self::SupportType { + self.construct_sensor(d, self.weights[d]) + } + + #[inline] + fn support_count(&self) -> usize { + self.weights.len() + } + + #[inline] + fn all_data(&self) -> Self::AllDataIter<'_> { + (0..) + .zip(self.weights.as_slice().iter()) + .mapX(self, Self::construct_sensor_and_id) + } +} + +impl ForwardModel, F>, F> + for SensorGrid +where + F: Float + ToNalgebraRealField + nalgebra::RealField, + BT: SensorGridBT, + S: Sensor, + P: Spread, + Convolution: Spread + LocalAnalysis, +{ + type Observable = DVector; + + fn write_observable(&self, b: &Self::Observable, prefix: String) -> DynError { + let it = self.grid().into_iter().zip(b.iter()).map(|(x, &v)| (x, v)); + write_csv(it, prefix + ".txt") + } + + #[inline] + fn zero_observable(&self) -> Self::Observable { + self._zero_observable() + } +} + +impl FindimQuadraticModel, F> for SensorGrid +where + F: Float + ToNalgebraRealField + nalgebra::RealField, + BT: SensorGridBT, + S: Sensor, + P: Spread, + Convolution: Spread + LocalAnalysis, +{ + fn findim_quadratic_model( + &self, + μ: &DiscreteMeasure, F>, + b: &Self::Observable, + ) -> (DMatrix, DVector) { + assert_eq!(b.len(), self.n_sensors()); + let mut mA = DMatrix::zeros(self.n_sensors(), μ.len()); + let grid = self.grid(); + for (mut mAcol, δ) in mA.column_iter_mut().zip(μ.iter_spikes()) { + for &d in self.bt.iter_at(&δ.x) { + let sensor = self.shifted_sensor(grid.entry_linear_unchecked(d)); + mAcol[d] += sensor.apply(&δ.x); + } + } + let mAt = mA.transpose(); + (&mAt * mA, &mAt * b) + } +} + +/// Implements the calculation a factor $L$ such that $A_*A ≤ L 𝒟$ for $A$ the forward model +/// and $𝒟$ a seminorm of suitable form. +/// +/// **This assumes (but does not check) that the sensors are not overlapping.** +#[replace_float_literals(F::cast_from(literal))] +impl AdjointProductBoundedBy, ConvolutionOp> + for SensorGrid +where + F: Float + nalgebra::RealField + ToNalgebraRealField, + BT: SensorGridBT, + S: Sensor, + P: Spread, + Convolution: Spread, + K: SimpleConvolutionKernel, + AutoConvolution

: BoundedBy, +{ + type FloatType = F; + + fn adjoint_product_bound(&self, seminorm: &ConvolutionOp) -> Option { + // Sensors should not take on negative values to allow + // A_*A to be upper bounded by a simple convolution of `spread`. + if self.sensor.bounds().lower() < 0.0 { + return None; + } + + // Calculate the factor $L_1$ for betwee $ℱ[ψ * ψ] ≤ L_1 ℱ[ρ]$ for $ψ$ the base spread + // and $ρ$ the kernel of the seminorm. + let l1 = AutoConvolution(self.spread.clone()).bounding_factor(seminorm.kernel())?; + + // Calculate the factor for transitioning from $A_*A$ to `AutoConvolution

`, where A + // consists of several `Convolution` for the physical model `P` and the sensor `S`. + let l0 = self.sensor.norm(Linfinity) * self.sensor.norm(L1); + + // The final transition factor is: + Some(l0 * l1) + } +} + +macro_rules! make_sensorgridsupportgenerator_scalarop_rhs { + ($trait:ident, $fn:ident, $trait_assign:ident, $fn_assign:ident) => { + impl std::ops::$trait_assign + for SensorGridSupportGenerator + where + F: Float, + S: Sensor, + P: Spread, + Convolution: Spread, + { + fn $fn_assign(&mut self, t: F) { + self.weights.$fn_assign(t); + } + } + + impl std::ops::$trait for SensorGridSupportGenerator + where + F: Float, + S: Sensor, + P: Spread, + Convolution: Spread, + { + type Output = SensorGridSupportGenerator; + fn $fn(mut self, t: F) -> Self::Output { + std::ops::$trait_assign::$fn_assign(&mut self.weights, t); + self + } + } + + impl<'a, F, S, P, const N: usize> std::ops::$trait + for &'a SensorGridSupportGenerator + where + F: Float, + S: Sensor, + P: Spread, + Convolution: Spread, + { + type Output = SensorGridSupportGenerator; + fn $fn(self, t: F) -> Self::Output { + SensorGridSupportGenerator { + base_sensor: self.base_sensor.clone(), + grid: self.grid, + weights: (&self.weights).$fn(t), + } + } + } + }; +} + +make_sensorgridsupportgenerator_scalarop_rhs!(Mul, mul, MulAssign, mul_assign); +make_sensorgridsupportgenerator_scalarop_rhs!(Div, div, DivAssign, div_assign); + +macro_rules! make_sensorgridsupportgenerator_unaryop { + ($trait:ident, $fn:ident) => { + impl std::ops::$trait for SensorGridSupportGenerator + where + F: Float, + S: Sensor, + P: Spread, + Convolution: Spread, + { + type Output = SensorGridSupportGenerator; + fn $fn(mut self) -> Self::Output { + self.weights = self.weights.$fn(); + self + } + } + + impl<'a, F, S, P, const N: usize> std::ops::$trait + for &'a SensorGridSupportGenerator + where + F: Float, + S: Sensor, + P: Spread, + Convolution: Spread, + { + type Output = SensorGridSupportGenerator; + fn $fn(self) -> Self::Output { + SensorGridSupportGenerator { + base_sensor: self.base_sensor.clone(), + grid: self.grid, + weights: (&self.weights).$fn(), + } + } + } + }; +} + +make_sensorgridsupportgenerator_unaryop!(Neg, neg); + +impl<'a, F, S, P, BT, const N: usize> Mapping> + for PreadjointHelper<'a, SensorGrid, RNDM> +where + F: Float, + BT: SensorGridBT, + S: Sensor, + P: Spread, + Convolution: Spread + LocalAnalysis, N>, +{ + type Codomain = SensorGridBTFN; + + fn apply>>(&self, x: I) -> Self::Codomain { + let fwd = &self.forward_op; + let generator = SensorGridSupportGenerator { + base_sensor: fwd.base_sensor.clone(), + grid: fwd.grid(), + weights: x.own(), + }; + BTFN::new_refresh(&fwd.bt, generator) + } +} + +impl<'a, F, S, P, BT, const N: usize> Linear> + for PreadjointHelper<'a, SensorGrid, RNDM> +where + F: Float, + BT: SensorGridBT, + S: Sensor, + P: Spread, + Convolution: Spread + LocalAnalysis, N>, +{ +}