Skip to main content

dace_rs/
norm.rs

1//! Norms, order-sorted norms, estimates, and interval bounds of DA values.
2//!
3//! Ports `core/dacenorm.c` and the C++ `DA::convRadius`: the norm type is the
4//! C `ityp` code (0 = max/infinity, 1 = sum, `p > 1` = the `p`-vector norm;
5//! `p = 2` is the Euclidean norm), grouping is by monomial order (`var = 0`)
6//! or by the exponent of a chosen variable.
7
8use crate::context::{Context, pown, truncation_order};
9use crate::da::Da;
10use std::sync::Arc;
11
12/// The type of a coefficient norm (the C `ityp` code).
13///
14/// `ityp = 0` is the maximum (infinity) norm, `ityp = 1` the sum norm, and
15/// `ityp = p > 1` the `p`-th vector norm ( [`NormType::EUCLIDEAN`] is
16/// `p = 2`).
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum NormType {
19    /// Maximum absolute coefficient (`ityp = 0`).
20    Infinity,
21    /// Sum of absolute coefficients (`ityp = 1`).
22    One,
23    /// `p`-th vector norm of the coefficients (`p > 1`).
24    Power(u32),
25}
26
27impl NormType {
28    /// The Euclidean (`L²`) norm.
29    pub const EUCLIDEAN: NormType = NormType::Power(2);
30
31    pub(crate) fn ityp(self) -> u32 {
32        match self {
33            NormType::Infinity => 0,
34            NormType::One => 1,
35            NormType::Power(p) => p,
36        }
37    }
38}
39
40/// A closed interval `[lo, hi]`.
41#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct Interval {
43    /// Lower bound.
44    pub lo: f64,
45    /// Upper bound.
46    pub hi: f64,
47}
48
49/// Accumulate one coefficient into `acc` according to the norm type
50/// (shared by the plain and order-sorted norms).
51#[inline]
52fn accumulate(acc: &mut f64, c: f64, ityp: u32) {
53    if ityp == 1 {
54        *acc += c.abs();
55    } else if ityp > 1 {
56        *acc += pown(c.abs(), ityp);
57    } else {
58        *acc = acc.max(c.abs());
59    }
60}
61
62/// Finalize an accumulated norm (root for vector norms).
63#[inline]
64fn finalize(acc: f64, ityp: u32) -> f64 {
65    if ityp > 1 {
66        acc.powf(1.0 / f64::from(ityp))
67    } else {
68        acc
69    }
70}
71
72impl Da {
73    /// The absolute value (maximum coefficient norm) of the DA
74    /// (`daceAbsoluteValue`).
75    pub fn abs(&self) -> f64 {
76        self.terms.iter().fold(0.0, |m, t| m.max(t.c.abs()))
77    }
78
79    /// A norm of all coefficients (`daceNorm`).
80    pub fn norm(&self, ityp: NormType) -> f64 {
81        let ityp = ityp.ityp();
82        let mut acc = 0.0;
83        for t in &self.terms {
84            accumulate(&mut acc, t.c, ityp);
85        }
86        finalize(acc, ityp)
87    }
88
89    /// Order-sorted norms (`daceOrderedNorm`): one norm per order
90    /// (`var == 0`, length `nomax + 1`) or per exponent of variable `var`
91    /// (1-based, length `nomax + 1`).
92    ///
93    /// Out-of-range variables log a warning and return all zeros.
94    pub fn order_norm(&self, var: u32, ityp: NormType) -> Vec<f64> {
95        let ctx = &self.ctx;
96        let ityp = ityp.ityp();
97        let mut onorm = vec![0.0; ctx.nomax as usize + 1];
98        if var > ctx.nvmax {
99            log::warn!("DACE error 624: invalid independent variable {var} in order_norm");
100            return onorm;
101        }
102        if var == 0 {
103            for t in &self.terms {
104                let io = ctx.order_of(t.idx);
105                accumulate(&mut onorm[io as usize], t.c, ityp);
106            }
107            if ityp > 1 {
108                for v in onorm.iter_mut() {
109                    *v = finalize(*v, ityp);
110                }
111            }
112        } else {
113            let mut jj = vec![0u32; ctx.nvmax as usize];
114            for t in &self.terms {
115                ctx.decode_into(t.idx, &mut jj);
116                accumulate(&mut onorm[jj[(var - 1) as usize] as usize], t.c, ityp);
117            }
118            if ityp > 1 {
119                for v in onorm.iter_mut() {
120                    *v = finalize(*v, ityp);
121                }
122            }
123        }
124        onorm
125    }
126
127    /// Estimate order-sorted norms up to order `nc` by an exponential
128    /// least-squares fit (`daceEstimate`): returns the estimates `c[0..=nc]`.
129    ///
130    /// If fewer than two orders have non-negligible norms, the fit is
131    /// impossible; a warning is logged and zeros are returned (as in C,
132    /// where this is informational).
133    ///
134    /// # Panics
135    ///
136    /// Panics with [`crate::DaceError`] code 651 when `nomax < 2`.
137    pub fn estim_norm(&self, var: u32, ityp: NormType, nc: u32) -> Vec<f64> {
138        self.estim_norm_impl(var, ityp, nc, false).0
139    }
140
141    /// Like [`Da::estim_norm`], also returning the fit residuals per order
142    /// (length `min(nc, nomax) + 1`).
143    pub fn estim_norm_err(&self, var: u32, ityp: NormType, nc: u32) -> (Vec<f64>, Vec<f64>) {
144        self.estim_norm_impl(var, ityp, nc, true)
145    }
146
147    fn estim_norm_impl(
148        &self,
149        var: u32,
150        ityp: NormType,
151        nc: u32,
152        with_err: bool,
153    ) -> (Vec<f64>, Vec<f64>) {
154        let ctx: Arc<Context> = self.ctx.clone();
155        let mut c = vec![0.0; nc as usize + 1];
156        let mut err = vec![0.0; nc.min(ctx.nomax) as usize + 1];
157        if ctx.nomax < 2 {
158            crate::error::dace_panic(651, "No estimate is possible");
159        }
160
161        let (eps, _) = crate::context::eps_nocut();
162        let onorm = self.order_norm(var, ityp);
163
164        // set up xtx and xty for the linear least squares fit
165        let mut ai = [0.0f64; 2];
166        let mut xtx = [[0.0f64; 2]; 2];
167        for (i, &o) in onorm.iter().enumerate().skip(1) {
168            // negated `<=` (as in C) so NaN norms are treated as non-zero
169            #[allow(clippy::neg_cmp_op_on_partial_ord)]
170            if !(o <= eps) {
171                let fi = i as f64;
172                xtx[0][0] += fi * fi;
173                xtx[0][1] -= fi;
174                xtx[1][1] += 1.0;
175                ai[0] += o.ln();
176                ai[1] += fi * o.ln();
177            }
178        }
179
180        if xtx[1][1] < 2.0 {
181            log::warn!("DACE info 163: norm estimate not possible; returning zeros");
182            return (c, err);
183        }
184
185        xtx[1][0] = xtx[0][1];
186        let det = xtx[0][0] * xtx[1][1] - xtx[0][1] * xtx[1][0];
187
188        let a = [
189            (ai[0] * xtx[0][0] + ai[1] * xtx[0][1]) / det,
190            (ai[0] * xtx[1][0] + ai[1] * xtx[1][1]) / det,
191        ];
192
193        for (i, ci) in c.iter_mut().enumerate() {
194            *ci = (a[0] + a[1] * i as f64).exp();
195        }
196
197        if with_err {
198            for i in 0..err.len() {
199                let temp = onorm[i] - c[i];
200                err[i] = if temp > 0.0 { temp } else { 0.0 };
201            }
202        }
203        (c, err)
204    }
205
206    /// An upper and lower bound of the DA over `[-1, 1]^nv`
207    /// (`daceGetBounds`): monomials with all-even exponents contribute
208    /// their signed coefficient to one side, others their absolute value to
209    /// both.
210    pub fn bound(&self) -> Interval {
211        let ctx = &self.ctx;
212        let mut lo = 0.0;
213        let mut hi = 0.0;
214        let mut terms = self.terms.iter().peekable();
215
216        // constant part is special
217        if terms.peek().is_some_and(|t| t.idx == 0) {
218            let c = terms.next().unwrap().c;
219            lo = c;
220            hi = c;
221        }
222
223        let mut jj = vec![0u32; ctx.nvmax as usize];
224        for t in terms {
225            ctx.decode_into(t.idx, &mut jj);
226            let odd = jj.iter().any(|&e| e & 1 != 0);
227            if odd {
228                hi += t.c.abs();
229                lo -= t.c.abs();
230            } else if t.c > 0.0 {
231                hi += t.c;
232            } else {
233                lo += t.c;
234            }
235        }
236        Interval { lo, hi }
237    }
238
239    /// Estimate the convergence radius: the radius at which the estimated
240    /// norm of the next order falls below `eps` (C++ `DA::convRadius`).
241    pub fn conv_radius(&self, eps: f64, ityp: NormType) -> f64 {
242        let ord = truncation_order();
243        let res = self.estim_norm(0, ityp, ord + 1);
244        (eps / res[(ord + 1) as usize]).powf(1.0 / f64::from(ord + 1))
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use crate::test_support::CONTEXT_LOCK;
252
253    #[test]
254    fn norms_and_bounds() {
255        let _g = CONTEXT_LOCK.lock();
256        crate::context::init(4, 2).unwrap();
257        let x = Da::variable(1);
258        let y = Da::variable(2);
259        let f = 1.0 + 2.0 * x.clone() + 3.0 * y.clone();
260
261        assert!((f.abs() - 3.0).abs() < 1e-15);
262        assert!((f.norm(NormType::One) - 6.0).abs() < 1e-15);
263        assert!((f.norm(NormType::EUCLIDEAN) - 14.0f64.sqrt()).abs() < 1e-15);
264        assert!((f.norm(NormType::Power(4)) - 98.0f64.powf(0.25)).abs() < 1e-14);
265
266        let on = f.order_norm(0, NormType::One);
267        assert_eq!(on.len(), 5);
268        assert!((on[0] - 1.0).abs() < 1e-15);
269        assert!((on[1] - 5.0).abs() < 1e-15);
270        let ov = f.order_norm(1, NormType::One);
271        assert!((ov[0] - 4.0).abs() < 1e-15); // constant + y
272        assert!((ov[1] - 2.0).abs() < 1e-15); // x
273
274        // bound of 1+x on [-1,1] is [0,2]
275        let b = (1.0 + x.clone()).bound();
276        assert!((b.lo - 0.0).abs() < 1e-15);
277        assert!((b.hi - 2.0).abs() < 1e-15);
278        // even monomial: x^2 contributes to one side
279        let b2 = (x.clone() * x.clone() - 1.0).bound();
280        assert!((b2.lo + 1.0).abs() < 1e-15);
281        assert!((b2.hi - 0.0).abs() < 1e-15);
282
283        // estim_norm of exp(x): order norms ~ 1/i!, exponential-ish fit
284        let e = crate::elementary::exp(&x);
285        let (c, err) = e.estim_norm_err(0, NormType::One, 6);
286        assert_eq!(c.len(), 7);
287        assert_eq!(err.len(), 5); // min(nc=6, nomax=4)+1
288        assert!(c.iter().all(|v| v.is_finite()));
289
290        // conv_radius is positive and finite for exp(x)
291        let r = e.conv_radius(1e-6, NormType::One);
292        assert!(r.is_finite() && r > 0.0);
293    }
294}