Skip to main content

oxiphysics_core/
measure_theory.rs

1#![allow(clippy::type_complexity)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Measure theory: sigma-algebras, Lebesgue measure, probability measures,
6//! signed measures, product measures, and numerical Lebesgue/Monte Carlo integration.
7//!
8//! This module provides both abstract measure-theoretic structures and concrete
9//! numerical implementations suitable for scientific computing.
10
11#![allow(dead_code)]
12
13use rand::RngExt as RandRng;
14use std::f64::consts::TAU;
15
16// ---------------------------------------------------------------------------
17// Core measure trait
18// ---------------------------------------------------------------------------
19
20/// A measurable set, identified by a tag and a predicate.
21///
22/// For numerical work, sets are represented as closed intervals (boxes) in ℝⁿ.
23#[derive(Debug, Clone)]
24pub struct MeasurableSet {
25    /// Human-readable label for the set.
26    pub label: String,
27    /// Lower bounds of the bounding box (one per dimension).
28    pub lower: Vec<f64>,
29    /// Upper bounds of the bounding box (one per dimension).
30    pub upper: Vec<f64>,
31}
32
33impl MeasurableSet {
34    /// Create a measurable set from lower and upper bounds.
35    ///
36    /// # Panics
37    /// Panics if `lower` and `upper` have different lengths.
38    pub fn new(label: impl Into<String>, lower: Vec<f64>, upper: Vec<f64>) -> Self {
39        assert_eq!(
40            lower.len(),
41            upper.len(),
42            "MeasurableSet: dimension mismatch"
43        );
44        MeasurableSet {
45            label: label.into(),
46            lower,
47            upper,
48        }
49    }
50
51    /// Create a 1-D interval `[a, b]`.
52    pub fn interval(a: f64, b: f64) -> Self {
53        Self::new(format!("[{a},{b}]"), vec![a], vec![b])
54    }
55
56    /// Return the dimension of the set.
57    pub fn dim(&self) -> usize {
58        self.lower.len()
59    }
60
61    /// Check whether a point lies in the closed box `[lower, upper]`.
62    pub fn contains(&self, point: &[f64]) -> bool {
63        assert_eq!(
64            point.len(),
65            self.dim(),
66            "MeasurableSet::contains: dim mismatch"
67        );
68        self.lower
69            .iter()
70            .zip(self.upper.iter())
71            .zip(point.iter())
72            .all(|((lo, hi), x)| x >= lo && x <= hi)
73    }
74
75    /// Return `true` if this set is a null set (measure zero) under Lebesgue measure.
76    ///
77    /// A box is a null set iff at least one dimension has `lower == upper`.
78    pub fn is_null_set(&self) -> bool {
79        self.lower
80            .iter()
81            .zip(self.upper.iter())
82            .any(|(lo, hi)| (hi - lo).abs() < 1e-15)
83    }
84}
85
86/// Abstract measure trait.
87///
88/// A measure `μ` on a sigma-algebra `Σ` over `X` satisfies:
89/// 1. Non-negativity: `μ(A) ≥ 0` for all `A ∈ Σ`
90/// 2. Null empty set: `μ(∅) = 0`
91/// 3. Countable additivity: `μ(⋃ Aᵢ) = Σ μ(Aᵢ)` for pairwise disjoint `Aᵢ`
92pub trait Measure {
93    /// Compute the measure of a set.
94    fn measure(&self, set: &MeasurableSet) -> f64;
95
96    /// Return `true` if `set` is a null set (measure zero).
97    fn is_null(&self, set: &MeasurableSet) -> bool {
98        self.measure(set).abs() < 1e-15
99    }
100
101    /// The measure of the empty set should always be zero.
102    fn empty_set_measure(&self) -> f64 {
103        0.0
104    }
105
106    /// Compute the measure of the union of two disjoint sets.
107    ///
108    /// For additive measures: `μ(A ∪ B) = μ(A) + μ(B)` when `A ∩ B = ∅`.
109    fn union_measure_disjoint(&self, a: &MeasurableSet, b: &MeasurableSet) -> f64 {
110        self.measure(a) + self.measure(b)
111    }
112}
113
114// ---------------------------------------------------------------------------
115// Sigma-algebra over a finite universe
116// ---------------------------------------------------------------------------
117
118/// A sigma-algebra over a finite universe `{0, 1, …, n-1}`.
119///
120/// Stores all subsets that belong to the sigma-algebra as bitmasks.
121/// This is useful for combinatorial and discrete measure theory examples.
122#[derive(Debug, Clone)]
123pub struct FiniteSigmaAlgebra {
124    /// Size of the universe.
125    pub n: usize,
126    /// Bitmask representations of sets in the sigma-algebra.
127    sets: Vec<u64>,
128}
129
130impl FiniteSigmaAlgebra {
131    /// Construct the power sigma-algebra (all 2ⁿ subsets).
132    ///
133    /// # Panics
134    /// Panics if `n > 63`.
135    pub fn power_set(n: usize) -> Self {
136        assert!(n <= 63, "FiniteSigmaAlgebra: n too large (max 63)");
137        let sets = (0u64..(1u64 << n)).collect();
138        Self { n, sets }
139    }
140
141    /// Construct the trivial sigma-algebra `{∅, X}`.
142    pub fn trivial(n: usize) -> Self {
143        let full = if n == 0 { 0 } else { (1u64 << n) - 1 };
144        Self {
145            n,
146            sets: vec![0, full],
147        }
148    }
149
150    /// Check whether a given bitmask belongs to the sigma-algebra.
151    pub fn contains(&self, bitmask: u64) -> bool {
152        self.sets.contains(&bitmask)
153    }
154
155    /// Number of sets in the sigma-algebra.
156    pub fn cardinality(&self) -> usize {
157        self.sets.len()
158    }
159
160    /// Verify closure under complement.
161    pub fn is_closed_under_complement(&self) -> bool {
162        let full = if self.n == 0 { 0 } else { (1u64 << self.n) - 1 };
163        self.sets.iter().all(|&s| self.contains(full & !s))
164    }
165
166    /// Verify closure under finite union.
167    pub fn is_closed_under_union(&self) -> bool {
168        for &a in &self.sets {
169            for &b in &self.sets {
170                if !self.contains(a | b) {
171                    return false;
172                }
173            }
174        }
175        true
176    }
177}
178
179// ---------------------------------------------------------------------------
180// Lebesgue measure
181// ---------------------------------------------------------------------------
182
183/// Lebesgue measure on ℝⁿ (product of interval lengths).
184///
185/// For a box `[a₁,b₁] × … × [aₙ,bₙ]`:
186///
187/// `λ(A) = Π (bᵢ − aᵢ)`
188///
189/// The Lebesgue measure is translation-invariant, countably additive, and
190/// assigns measure zero to all countable sets.
191#[derive(Debug, Clone, Copy, Default)]
192pub struct LebesgueMeasure;
193
194impl Measure for LebesgueMeasure {
195    fn measure(&self, set: &MeasurableSet) -> f64 {
196        set.lower
197            .iter()
198            .zip(set.upper.iter())
199            .map(|(lo, hi)| (hi - lo).max(0.0))
200            .product()
201    }
202}
203
204impl LebesgueMeasure {
205    /// Measure a 1-D interval `[a, b]`.
206    pub fn interval_measure(a: f64, b: f64) -> f64 {
207        (b - a).max(0.0)
208    }
209
210    /// Measure an n-D box given separate lower and upper bounds.
211    pub fn box_measure(lower: &[f64], upper: &[f64]) -> f64 {
212        assert_eq!(lower.len(), upper.len(), "box_measure: dim mismatch");
213        lower
214            .iter()
215            .zip(upper.iter())
216            .map(|(lo, hi)| (hi - lo).max(0.0))
217            .product()
218    }
219
220    /// Compute the measure of the intersection of two 1-D intervals.
221    pub fn interval_intersection(a1: f64, a2: f64, b1: f64, b2: f64) -> f64 {
222        let lo = a1.max(b1);
223        let hi = a2.min(b2);
224        (hi - lo).max(0.0)
225    }
226
227    /// Check translation invariance: `λ([a,b]) = λ([a+t, b+t])`.
228    pub fn translation_invariant(a: f64, b: f64, t: f64) -> bool {
229        let original = Self::interval_measure(a, b);
230        let shifted = Self::interval_measure(a + t, b + t);
231        (original - shifted).abs() < 1e-12
232    }
233}
234
235// ---------------------------------------------------------------------------
236// Probability measure
237// ---------------------------------------------------------------------------
238
239/// A probability measure on ℝⁿ with density function.
240///
241/// `P(A) = ∫_A f(x) dx`  where `∫ f dx = 1` (normalization).
242///
243/// Supports numerical expectation and variance via Monte Carlo estimation.
244pub struct ProbabilityMeasure {
245    /// Probability density function `f(x) ≥ 0`.
246    pub density: Box<dyn Fn(&[f64]) -> f64>,
247    /// Domain of the density (support box).
248    pub domain: MeasurableSet,
249    /// Number of Monte Carlo samples for numerical computations.
250    pub n_samples: usize,
251}
252
253impl ProbabilityMeasure {
254    /// Create a probability measure from a density and domain.
255    pub fn new(
256        density: impl Fn(&[f64]) -> f64 + 'static,
257        domain: MeasurableSet,
258        n_samples: usize,
259    ) -> Self {
260        Self {
261            density: Box::new(density),
262            domain,
263            n_samples,
264        }
265    }
266
267    /// Estimate `P(A)` via Monte Carlo integration.
268    ///
269    /// Samples uniformly from the domain and averages the density over `A`.
270    pub fn probability(&self, set: &MeasurableSet) -> f64 {
271        let mut rng = rand::rng();
272        let domain_vol = LebesgueMeasure.measure(&self.domain);
273        if domain_vol == 0.0 {
274            return 0.0;
275        }
276
277        let mut sum = 0.0;
278        let dim = self.domain.dim();
279        for _ in 0..self.n_samples {
280            let point: Vec<f64> = (0..dim)
281                .map(|d| rng.random_range(self.domain.lower[d]..self.domain.upper[d]))
282                .collect();
283            if set.contains(&point) {
284                sum += (self.density)(&point);
285            }
286        }
287        domain_vol * sum / self.n_samples as f64
288    }
289
290    /// Estimate the total mass (should be ≈ 1 for a true probability measure).
291    pub fn total_mass(&self) -> f64 {
292        let mut rng = rand::rng();
293        let domain_vol = LebesgueMeasure.measure(&self.domain);
294        if domain_vol == 0.0 {
295            return 0.0;
296        }
297
298        let mut sum = 0.0;
299        let dim = self.domain.dim();
300        for _ in 0..self.n_samples {
301            let point: Vec<f64> = (0..dim)
302                .map(|d| rng.random_range(self.domain.lower[d]..self.domain.upper[d]))
303                .collect();
304            sum += (self.density)(&point);
305        }
306        domain_vol * sum / self.n_samples as f64
307    }
308
309    /// Estimate the expectation `E[g(X)]` of a function `g` under this measure.
310    pub fn expectation(&self, g: &dyn Fn(&[f64]) -> f64) -> f64 {
311        let mut rng = rand::rng();
312        let domain_vol = LebesgueMeasure.measure(&self.domain);
313        if domain_vol == 0.0 {
314            return 0.0;
315        }
316
317        let mut numer = 0.0;
318        let mut denom = 0.0;
319        let dim = self.domain.dim();
320        for _ in 0..self.n_samples {
321            let point: Vec<f64> = (0..dim)
322                .map(|d| rng.random_range(self.domain.lower[d]..self.domain.upper[d]))
323                .collect();
324            let f_val = (self.density)(&point);
325            numer += f_val * g(&point);
326            denom += f_val;
327        }
328        if denom.abs() < 1e-15 {
329            return 0.0;
330        }
331        numer / denom
332    }
333
334    /// Estimate `Var[g(X)] = E[g(X)²] − (E[g(X)])²`.
335    pub fn variance(&self, g: &dyn Fn(&[f64]) -> f64) -> f64 {
336        let eg = self.expectation(g);
337        let g2 = |x: &[f64]| g(x).powi(2);
338        let eg2 = self.expectation(&g2);
339        (eg2 - eg * eg).max(0.0)
340    }
341
342    /// Estimate the entropy `H = -∫ f ln(f) dx` via Monte Carlo.
343    pub fn entropy(&self) -> f64 {
344        let mut rng = rand::rng();
345        let domain_vol = LebesgueMeasure.measure(&self.domain);
346        if domain_vol == 0.0 {
347            return 0.0;
348        }
349
350        let mut sum = 0.0;
351        let dim = self.domain.dim();
352        for _ in 0..self.n_samples {
353            let point: Vec<f64> = (0..dim)
354                .map(|d| rng.random_range(self.domain.lower[d]..self.domain.upper[d]))
355                .collect();
356            let f_val = (self.density)(&point);
357            if f_val > 1e-15 {
358                sum += f_val * f_val.ln();
359            }
360        }
361        -domain_vol * sum / self.n_samples as f64
362    }
363}
364
365// ---------------------------------------------------------------------------
366// Signed measure & decompositions
367// ---------------------------------------------------------------------------
368
369/// A signed measure represented as a difference of two non-negative measures.
370///
371/// By the Jordan decomposition theorem, every signed measure `ν` can be
372/// written as `ν = ν⁺ − ν⁻` where `ν⁺, ν⁻ ≥ 0` are mutually singular.
373///
374/// Here we work numerically with density functions.
375pub struct SignedMeasure {
376    /// The positive part density `f⁺(x) ≥ 0`.
377    pub positive_density: Box<dyn Fn(&[f64]) -> f64>,
378    /// The negative part density `f⁻(x) ≥ 0`.
379    pub negative_density: Box<dyn Fn(&[f64]) -> f64>,
380    /// Domain of integration.
381    pub domain: MeasurableSet,
382    /// Number of Monte Carlo samples.
383    pub n_samples: usize,
384}
385
386impl SignedMeasure {
387    /// Create a signed measure from positive and negative densities.
388    pub fn new(
389        positive_density: impl Fn(&[f64]) -> f64 + 'static,
390        negative_density: impl Fn(&[f64]) -> f64 + 'static,
391        domain: MeasurableSet,
392        n_samples: usize,
393    ) -> Self {
394        Self {
395            positive_density: Box::new(positive_density),
396            negative_density: Box::new(negative_density),
397            domain,
398            n_samples,
399        }
400    }
401
402    /// Create a signed measure from a single (possibly negative) density `f`.
403    pub fn from_density(
404        density: impl Fn(&[f64]) -> f64 + 'static,
405        domain: MeasurableSet,
406        n_samples: usize,
407    ) -> Self {
408        let density = std::rc::Rc::new(density);
409        let d1 = density.clone();
410        let d2 = density;
411        Self::new(
412            move |x| d1(x).max(0.0),
413            move |x| (-d2(x)).max(0.0),
414            domain,
415            n_samples,
416        )
417    }
418
419    /// Evaluate the signed measure on a set: `ν(A) = ∫_A (f⁺ − f⁻) dx`.
420    pub fn evaluate(&self, set: &MeasurableSet) -> f64 {
421        let vol = LebesgueMeasure.measure(&self.domain);
422        if vol == 0.0 {
423            return 0.0;
424        }
425        let mut rng = rand::rng();
426        let mut sum = 0.0;
427        let dim = self.domain.dim();
428        for _ in 0..self.n_samples {
429            let point: Vec<f64> = (0..dim)
430                .map(|d| rng.random_range(self.domain.lower[d]..self.domain.upper[d]))
431                .collect();
432            if set.contains(&point) {
433                let fpos = (self.positive_density)(&point);
434                let fneg = (self.negative_density)(&point);
435                sum += fpos - fneg;
436            }
437        }
438        vol * sum / self.n_samples as f64
439    }
440
441    /// Total variation: `|ν|(A) = ∫_A (f⁺ + f⁻) dx`.
442    pub fn total_variation(&self, set: &MeasurableSet) -> f64 {
443        let vol = LebesgueMeasure.measure(&self.domain);
444        if vol == 0.0 {
445            return 0.0;
446        }
447        let mut rng = rand::rng();
448        let mut sum = 0.0;
449        let dim = self.domain.dim();
450        for _ in 0..self.n_samples {
451            let point: Vec<f64> = (0..dim)
452                .map(|d| rng.random_range(self.domain.lower[d]..self.domain.upper[d]))
453                .collect();
454            if set.contains(&point) {
455                let fpos = (self.positive_density)(&point);
456                let fneg = (self.negative_density)(&point);
457                sum += fpos + fneg;
458            }
459        }
460        vol * sum / self.n_samples as f64
461    }
462
463    /// Hahn decomposition: identify the positive set P where `f⁺ > f⁻`.
464    ///
465    /// Returns a sample of points in P within the domain.
466    pub fn hahn_positive_set_samples(&self, n: usize) -> Vec<Vec<f64>> {
467        let mut rng = rand::rng();
468        let dim = self.domain.dim();
469        let mut result = Vec::new();
470        for _ in 0..n {
471            let point: Vec<f64> = (0..dim)
472                .map(|d| rng.random_range(self.domain.lower[d]..self.domain.upper[d]))
473                .collect();
474            if (self.positive_density)(&point) > (self.negative_density)(&point) {
475                result.push(point);
476            }
477        }
478        result
479    }
480
481    /// Jordan decomposition: return estimates of `ν⁺(domain)` and `ν⁻(domain)`.
482    pub fn jordan_decomposition(&self) -> (f64, f64) {
483        let vol = LebesgueMeasure.measure(&self.domain);
484        if vol == 0.0 {
485            return (0.0, 0.0);
486        }
487        let mut rng = rand::rng();
488        let mut pos_sum = 0.0;
489        let mut neg_sum = 0.0;
490        let dim = self.domain.dim();
491        for _ in 0..self.n_samples {
492            let point: Vec<f64> = (0..dim)
493                .map(|d| rng.random_range(self.domain.lower[d]..self.domain.upper[d]))
494                .collect();
495            pos_sum += (self.positive_density)(&point);
496            neg_sum += (self.negative_density)(&point);
497        }
498        let scale = vol / self.n_samples as f64;
499        (scale * pos_sum, scale * neg_sum)
500    }
501}
502
503// ---------------------------------------------------------------------------
504// Product measure (Fubini's theorem)
505// ---------------------------------------------------------------------------
506
507/// Product measure `μ₁ ⊗ μ₂` on a product space `X₁ × X₂`.
508///
509/// By Fubini's theorem, for non-negative measurable `f`:
510/// `∫_{X₁×X₂} f d(μ₁⊗μ₂) = ∫_{X₁} (∫_{X₂} f(x,y) dμ₂(y)) dμ₁(x)`
511///
512/// For Lebesgue measures this reduces to iterated integration.
513#[derive(Debug, Clone)]
514pub struct ProductMeasure {
515    /// First factor domain.
516    pub domain1: MeasurableSet,
517    /// Second factor domain.
518    pub domain2: MeasurableSet,
519}
520
521impl ProductMeasure {
522    /// Create a product measure from two domains.
523    pub fn new(domain1: MeasurableSet, domain2: MeasurableSet) -> Self {
524        Self { domain1, domain2 }
525    }
526
527    /// Measure a product box `A × B` under the product Lebesgue measure.
528    ///
529    /// `(λ₁ ⊗ λ₂)(A × B) = λ₁(A) · λ₂(B)`
530    pub fn measure_product_box(&self, a: &MeasurableSet, b: &MeasurableSet) -> f64 {
531        LebesgueMeasure.measure(a) * LebesgueMeasure.measure(b)
532    }
533
534    /// Compute the product domain as a single concatenated box.
535    pub fn product_domain(&self) -> MeasurableSet {
536        let mut lower = self.domain1.lower.clone();
537        lower.extend(self.domain2.lower.iter().copied());
538        let mut upper = self.domain1.upper.clone();
539        upper.extend(self.domain2.upper.iter().copied());
540        MeasurableSet::new("product_domain", lower, upper)
541    }
542
543    /// Integrate a function `f(x, y)` over the product domain using Fubini.
544    ///
545    /// Uses a simple grid rule: `n_x` × `n_y` quadrature points.
546    pub fn fubini_integrate(&self, f: &dyn Fn(f64, f64) -> f64, n_x: usize, n_y: usize) -> f64 {
547        assert_eq!(
548            self.domain1.dim(),
549            1,
550            "fubini_integrate: domain1 must be 1-D"
551        );
552        assert_eq!(
553            self.domain2.dim(),
554            1,
555            "fubini_integrate: domain2 must be 1-D"
556        );
557
558        let x0 = self.domain1.lower[0];
559        let x1 = self.domain1.upper[0];
560        let y0 = self.domain2.lower[0];
561        let y1 = self.domain2.upper[0];
562        let dx = (x1 - x0) / n_x as f64;
563        let dy = (y1 - y0) / n_y as f64;
564
565        let mut sum = 0.0;
566        for i in 0..n_x {
567            let x = x0 + (i as f64 + 0.5) * dx;
568            let inner: f64 = (0..n_y)
569                .map(|j| {
570                    let y = y0 + (j as f64 + 0.5) * dy;
571                    f(x, y)
572                })
573                .sum();
574            sum += inner * dy;
575        }
576        sum * dx
577    }
578
579    /// Verify Fubini's theorem numerically: iterated vs. direct Monte Carlo.
580    pub fn verify_fubini(&self, f: &dyn Fn(f64, f64) -> f64, n: usize) -> (f64, f64) {
581        let iterated = self.fubini_integrate(f, n, n);
582        // Direct Monte Carlo estimate
583        let mut rng = rand::rng();
584        let x0 = self.domain1.lower[0];
585        let x1 = self.domain1.upper[0];
586        let y0 = self.domain2.lower[0];
587        let y1 = self.domain2.upper[0];
588        let vol = (x1 - x0) * (y1 - y0);
589        let mut mc_sum = 0.0;
590        for _ in 0..(n * n) {
591            let x = rng.random_range(x0..x1);
592            let y = rng.random_range(y0..y1);
593            mc_sum += f(x, y);
594        }
595        let mc = vol * mc_sum / (n * n) as f64;
596        (iterated, mc)
597    }
598}
599
600// ---------------------------------------------------------------------------
601// Lebesgue integral (numerical)
602// ---------------------------------------------------------------------------
603
604/// Numerical Lebesgue integration using midpoint-rule quadrature.
605///
606/// For a function `f: ℝⁿ → ℝ` on a box domain, the integral is approximated
607/// by subdividing each dimension into `n_pts` sub-intervals.
608pub struct MeasureIntegral;
609
610impl MeasureIntegral {
611    /// Integrate `f` over a 1-D interval `[a, b]` using `n` midpoint sub-intervals.
612    pub fn integrate_1d(f: &dyn Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
613        let h = (b - a) / n as f64;
614        (0..n)
615            .map(|i| {
616                let x = a + (i as f64 + 0.5) * h;
617                f(x) * h
618            })
619            .sum()
620    }
621
622    /// Integrate `f` over a 2-D rectangle using `n × n` midpoint sub-intervals.
623    pub fn integrate_2d(
624        f: &dyn Fn(f64, f64) -> f64,
625        a: f64,
626        b: f64,
627        c: f64,
628        d: f64,
629        n: usize,
630    ) -> f64 {
631        let hx = (b - a) / n as f64;
632        let hy = (d - c) / n as f64;
633        let mut sum = 0.0;
634        for i in 0..n {
635            let x = a + (i as f64 + 0.5) * hx;
636            for j in 0..n {
637                let y = c + (j as f64 + 0.5) * hy;
638                sum += f(x, y);
639            }
640        }
641        sum * hx * hy
642    }
643
644    /// Integrate `f` over an n-D box using midpoint quadrature.
645    ///
646    /// The box is given as `lower` and `upper` vectors; `n_pts` sub-intervals
647    /// per dimension. This has exponential cost in the dimension.
648    pub fn integrate_nd(
649        f: &dyn Fn(&[f64]) -> f64,
650        lower: &[f64],
651        upper: &[f64],
652        n_pts: usize,
653    ) -> f64 {
654        let dim = lower.len();
655        assert_eq!(dim, upper.len(), "integrate_nd: dim mismatch");
656        let steps: Vec<f64> = lower
657            .iter()
658            .zip(upper.iter())
659            .map(|(lo, hi)| (hi - lo) / n_pts as f64)
660            .collect();
661        let vol_cell: f64 = steps.iter().product();
662        let total_cells = n_pts.pow(dim as u32);
663
664        (0..total_cells)
665            .map(|idx| {
666                let mut point = vec![0.0f64; dim];
667                let mut rem = idx;
668                for d in 0..dim {
669                    let coord = rem % n_pts;
670                    rem /= n_pts;
671                    point[d] = lower[d] + (coord as f64 + 0.5) * steps[d];
672                }
673                f(&point) * vol_cell
674            })
675            .sum()
676    }
677
678    /// Monte Carlo integration of `f` over an n-D box.
679    ///
680    /// Produces an unbiased estimate with standard error `O(1/√n)`.
681    pub fn monte_carlo(
682        f: &dyn Fn(&[f64]) -> f64,
683        lower: &[f64],
684        upper: &[f64],
685        n_samples: usize,
686    ) -> f64 {
687        let dim = lower.len();
688        assert_eq!(dim, upper.len(), "monte_carlo: dim mismatch");
689        let vol: f64 = lower
690            .iter()
691            .zip(upper.iter())
692            .map(|(lo, hi)| hi - lo)
693            .product();
694
695        let mut rng = rand::rng();
696        let mut sum = 0.0;
697        for _ in 0..n_samples {
698            let point: Vec<f64> = (0..dim)
699                .map(|d| rng.random_range(lower[d]..upper[d]))
700                .collect();
701            sum += f(&point);
702        }
703        vol * sum / n_samples as f64
704    }
705
706    /// Importance-sampling Monte Carlo: `∫ f dx = ∫ (f/g) · g dx`.
707    ///
708    /// Samples are drawn from the proposal density `g` (given as CDF inverse)
709    /// and the estimate is `(1/n) Σ f(xᵢ) / g(xᵢ)`.
710    pub fn importance_sampling(
711        f: &dyn Fn(f64) -> f64,
712        proposal_sample: &dyn Fn() -> f64,
713        proposal_density: &dyn Fn(f64) -> f64,
714        n_samples: usize,
715    ) -> f64 {
716        let mut sum = 0.0;
717        for _ in 0..n_samples {
718            let x = proposal_sample();
719            let g = proposal_density(x);
720            if g.abs() > 1e-15 {
721                sum += f(x) / g;
722            }
723        }
724        sum / n_samples as f64
725    }
726
727    /// Compute the Riemann–Stieltjes-style integral `∫ f dG` numerically.
728    ///
729    /// Approximates `Σ f(xᵢ) · (G(xᵢ₊₁) − G(xᵢ))` over `n` sub-intervals.
730    pub fn riemann_stieltjes(
731        f: &dyn Fn(f64) -> f64,
732        g: &dyn Fn(f64) -> f64,
733        a: f64,
734        b: f64,
735        n: usize,
736    ) -> f64 {
737        let h = (b - a) / n as f64;
738        (0..n)
739            .map(|i| {
740                let x = a + i as f64 * h;
741                let xp = x + h;
742                f(x) * (g(xp) - g(x))
743            })
744            .sum()
745    }
746
747    /// Estimate the standard error of a Monte Carlo integral estimate.
748    pub fn monte_carlo_std_error(
749        f: &dyn Fn(&[f64]) -> f64,
750        lower: &[f64],
751        upper: &[f64],
752        n_samples: usize,
753    ) -> (f64, f64) {
754        let dim = lower.len();
755        let vol: f64 = lower
756            .iter()
757            .zip(upper.iter())
758            .map(|(lo, hi)| hi - lo)
759            .product();
760
761        let mut rng = rand::rng();
762        let mut vals = Vec::with_capacity(n_samples);
763        for _ in 0..n_samples {
764            let point: Vec<f64> = (0..dim)
765                .map(|d| rng.random_range(lower[d]..upper[d]))
766                .collect();
767            vals.push(f(&point));
768        }
769        let mean: f64 = vals.iter().sum::<f64>() / n_samples as f64;
770        let var: f64 =
771            vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n_samples - 1) as f64;
772        let std_err = (var / n_samples as f64).sqrt() * vol;
773        (vol * mean, std_err)
774    }
775}
776
777// ---------------------------------------------------------------------------
778// Radon-Nikodym derivative
779// ---------------------------------------------------------------------------
780
781/// Radon–Nikodym derivative approximation.
782///
783/// Given two measures `μ` and `ν` with `ν ≪ μ` (ν absolutely continuous
784/// w.r.t. μ), the Radon–Nikodym theorem guarantees `dν/dμ` exists.
785///
786/// Here we estimate it numerically by binning.
787pub struct RadonNikodym;
788
789impl RadonNikodym {
790    /// Estimate the Radon–Nikodym derivative `dν/dμ` at point `x` by
791    /// computing the density ratio in a small ball of radius `eps`.
792    ///
793    /// Both `mu_samples` and `nu_samples` are finite sample sets from μ and ν.
794    pub fn estimate_at(x: f64, mu_samples: &[f64], nu_samples: &[f64], eps: f64) -> f64 {
795        let count =
796            |samples: &[f64]| samples.iter().filter(|&&s| (s - x).abs() <= eps).count() as f64;
797        let mu_count = count(mu_samples);
798        let nu_count = count(nu_samples);
799        if mu_count < 1e-15 {
800            return 0.0;
801        }
802        // Normalize by sample sizes
803        (nu_count / nu_samples.len() as f64) / (mu_count / mu_samples.len() as f64)
804    }
805}
806
807// ---------------------------------------------------------------------------
808// Measure-theoretic convergence
809// ---------------------------------------------------------------------------
810
811/// Types of convergence for sequences of measurable functions.
812#[derive(Debug, Clone, Copy, PartialEq, Eq)]
813pub enum ConvergenceType {
814    /// Pointwise almost everywhere.
815    AlmostEverywhere,
816    /// In measure (convergence in probability).
817    InMeasure,
818    /// In L^p norm.
819    InLp,
820    /// Uniform (almost everywhere).
821    Uniform,
822}
823
824/// Check whether a sequence of functions converges in Lp norm.
825///
826/// Evaluates `‖fₙ − f‖_p = (∫ |fₙ(x) − f(x)|^p dx)^(1/p)` over `[a, b]`.
827pub fn check_lp_convergence(
828    fn_seq: &[Box<dyn Fn(f64) -> f64>],
829    limit: &dyn Fn(f64) -> f64,
830    a: f64,
831    b: f64,
832    p: f64,
833    n_pts: usize,
834    tolerance: f64,
835) -> bool {
836    if fn_seq.is_empty() {
837        return true;
838    }
839    let h = (b - a) / n_pts as f64;
840    let last = fn_seq.last().expect("fn_seq is non-empty after check");
841    let lp_norm: f64 = (0..n_pts)
842        .map(|i| {
843            let x = a + (i as f64 + 0.5) * h;
844            (last(x) - limit(x)).abs().powf(p) * h
845        })
846        .sum::<f64>()
847        .powf(1.0 / p);
848    lp_norm < tolerance
849}
850
851// ---------------------------------------------------------------------------
852// Outer measure
853// ---------------------------------------------------------------------------
854
855/// Carathéodory outer measure construction.
856///
857/// The outer measure `μ*(A) = inf { Σ μ(Eᵢ) : A ⊆ ⋃ Eᵢ }` is constructed
858/// from any set function `μ` on covering sets.
859pub struct OuterMeasure {
860    /// The base covering measure (e.g., length of intervals).
861    pub base: LebesgueMeasure,
862}
863
864impl OuterMeasure {
865    /// Create an outer measure from the Lebesgue base measure.
866    pub fn new() -> Self {
867        Self {
868            base: LebesgueMeasure,
869        }
870    }
871
872    /// Estimate `μ*(A)` by covering `A` with `n_covers` equal sub-intervals.
873    pub fn estimate(&self, a: f64, b: f64, n_covers: usize) -> f64 {
874        // For an interval, the outer measure equals the length
875        let _ = n_covers;
876        (b - a).max(0.0)
877    }
878
879    /// Verify Carathéodory's condition: `μ*(E) = μ*(E ∩ A) + μ*(E ∩ Aᶜ)` for
880    /// measurable `A`, numerically on the interval `[0,1]`.
881    pub fn verify_caratheodory(&self, a_lo: f64, a_hi: f64) -> bool {
882        let e_lo = 0.0;
883        let e_hi = 1.0;
884        let full = e_hi - e_lo;
885        let intersection = (a_hi.min(e_hi) - a_lo.max(e_lo)).max(0.0);
886        let complement = ((a_lo - e_lo).max(0.0) + (e_hi - a_hi).max(0.0)).min(full);
887        (full - (intersection + complement)).abs() < 1e-12
888    }
889}
890
891impl Default for OuterMeasure {
892    fn default() -> Self {
893        Self::new()
894    }
895}
896
897// ---------------------------------------------------------------------------
898// Counting measure
899// ---------------------------------------------------------------------------
900
901/// Counting measure on a finite set.
902///
903/// `μ_count(A) = |A|` (number of elements in `A`).
904#[derive(Debug, Clone, Copy, Default)]
905pub struct CountingMeasure;
906
907impl CountingMeasure {
908    /// Measure of a finite set: returns its cardinality.
909    pub fn measure_set(set_size: usize) -> f64 {
910        set_size as f64
911    }
912
913    /// Check that the counting measure is additive: `|A ∪ B| = |A| + |B|` for
914    /// disjoint `A` and `B`.
915    pub fn additivity(a_size: usize, b_size: usize) -> bool {
916        Self::measure_set(a_size + b_size) == Self::measure_set(a_size) + Self::measure_set(b_size)
917    }
918}
919
920// ---------------------------------------------------------------------------
921// Ergodic measure (time-average)
922// ---------------------------------------------------------------------------
923
924/// Time-average (ergodic) measure for discrete dynamical systems.
925///
926/// For an ergodic system, the time average equals the space average by
927/// the Birkhoff ergodic theorem.
928pub struct ErgodicMeasure;
929
930impl ErgodicMeasure {
931    /// Estimate the time-average of `f` along an orbit starting from `x0`.
932    ///
933    /// `T_n f(x) = (1/n) Σ_{k=0}^{n-1} f(φ^k(x))`
934    pub fn time_average(
935        f: &dyn Fn(f64) -> f64,
936        map: &dyn Fn(f64) -> f64,
937        x0: f64,
938        n: usize,
939    ) -> f64 {
940        let mut x = x0;
941        let mut sum = 0.0;
942        for _ in 0..n {
943            sum += f(x);
944            x = map(x);
945        }
946        sum / n as f64
947    }
948
949    /// Check the Birkhoff ergodic theorem numerically for a single orbit.
950    ///
951    /// Returns `true` if the time-average is within `tol` of the space-average.
952    pub fn birkhoff_check(
953        f: &dyn Fn(f64) -> f64,
954        map: &dyn Fn(f64) -> f64,
955        x0: f64,
956        space_average: f64,
957        n: usize,
958        tol: f64,
959    ) -> bool {
960        let ta = Self::time_average(f, map, x0, n);
961        (ta - space_average).abs() < tol
962    }
963}
964
965// ---------------------------------------------------------------------------
966// Haar measure (compact groups — discrete approximation)
967// ---------------------------------------------------------------------------
968
969/// Discrete approximation to Haar measure on the circle group `U(1)`.
970///
971/// Haar measure on `U(1) ≅ [0, 2π)` is `dθ / (2π)`.
972pub struct HaarMeasure;
973
974impl HaarMeasure {
975    /// Estimate the Haar-measure integral of `f` on `[0, 2π)`.
976    pub fn integrate(f: &dyn Fn(f64) -> f64, n: usize) -> f64 {
977        use std::f64::consts::TAU;
978        let h = TAU / n as f64;
979        (0..n).map(|i| f(i as f64 * h) * h / TAU).sum()
980    }
981
982    /// Verify translation invariance: `∫ f(θ) dθ = ∫ f(θ + t) dθ`.
983    pub fn translation_invariant(f: &dyn Fn(f64) -> f64, t: f64, n: usize) -> bool {
984        let original = Self::integrate(f, n);
985        let shifted = Self::integrate(&|theta| f((theta + t) % TAU), n);
986        (original - shifted).abs() < 1e-6
987    }
988}
989
990// ---------------------------------------------------------------------------
991// Tests
992// ---------------------------------------------------------------------------
993
994#[cfg(test)]
995mod tests {
996    use super::*;
997    use std::f64::consts::PI;
998
999    // --- MeasurableSet ---
1000
1001    #[test]
1002    fn test_measurable_set_contains() {
1003        let s = MeasurableSet::interval(0.0, 1.0);
1004        assert!(s.contains(&[0.5]));
1005        assert!(!s.contains(&[1.5]));
1006    }
1007
1008    #[test]
1009    fn test_measurable_set_null() {
1010        let s = MeasurableSet::interval(1.0, 1.0);
1011        assert!(s.is_null_set());
1012    }
1013
1014    #[test]
1015    fn test_measurable_set_dim() {
1016        let s = MeasurableSet::new("box", vec![0.0, 0.0], vec![1.0, 1.0]);
1017        assert_eq!(s.dim(), 2);
1018    }
1019
1020    // --- FiniteSigmaAlgebra ---
1021
1022    #[test]
1023    fn test_sigma_algebra_power_set_cardinality() {
1024        let sa = FiniteSigmaAlgebra::power_set(3);
1025        assert_eq!(sa.cardinality(), 8);
1026    }
1027
1028    #[test]
1029    fn test_sigma_algebra_trivial_cardinality() {
1030        let sa = FiniteSigmaAlgebra::trivial(4);
1031        assert_eq!(sa.cardinality(), 2);
1032    }
1033
1034    #[test]
1035    fn test_sigma_algebra_complement_closure() {
1036        let sa = FiniteSigmaAlgebra::power_set(3);
1037        assert!(sa.is_closed_under_complement());
1038    }
1039
1040    #[test]
1041    fn test_sigma_algebra_union_closure() {
1042        let sa = FiniteSigmaAlgebra::power_set(3);
1043        assert!(sa.is_closed_under_union());
1044    }
1045
1046    #[test]
1047    fn test_sigma_algebra_contains_empty() {
1048        let sa = FiniteSigmaAlgebra::power_set(4);
1049        assert!(sa.contains(0)); // empty set
1050    }
1051
1052    // --- LebesgueMeasure ---
1053
1054    #[test]
1055    fn test_lebesgue_interval() {
1056        let m = LebesgueMeasure;
1057        let s = MeasurableSet::interval(0.0, 3.0);
1058        assert!((m.measure(&s) - 3.0).abs() < 1e-12);
1059    }
1060
1061    #[test]
1062    fn test_lebesgue_box() {
1063        let m = LebesgueMeasure;
1064        let s = MeasurableSet::new("box", vec![0.0, 0.0], vec![2.0, 3.0]);
1065        assert!((m.measure(&s) - 6.0).abs() < 1e-12);
1066    }
1067
1068    #[test]
1069    fn test_lebesgue_null_set() {
1070        let m = LebesgueMeasure;
1071        let s = MeasurableSet::interval(1.0, 1.0);
1072        assert!(m.is_null(&s));
1073    }
1074
1075    #[test]
1076    fn test_lebesgue_empty_set() {
1077        let m = LebesgueMeasure;
1078        assert!((m.empty_set_measure()).abs() < 1e-12);
1079    }
1080
1081    #[test]
1082    fn test_lebesgue_disjoint_union() {
1083        let m = LebesgueMeasure;
1084        let a = MeasurableSet::interval(0.0, 1.0);
1085        let b = MeasurableSet::interval(2.0, 4.0);
1086        let u = m.union_measure_disjoint(&a, &b);
1087        assert!((u - 3.0).abs() < 1e-12);
1088    }
1089
1090    #[test]
1091    fn test_lebesgue_translation_invariant() {
1092        assert!(LebesgueMeasure::translation_invariant(0.0, 1.0, 5.0));
1093    }
1094
1095    #[test]
1096    fn test_lebesgue_intersection() {
1097        let i = LebesgueMeasure::interval_intersection(0.0, 2.0, 1.0, 3.0);
1098        assert!((i - 1.0).abs() < 1e-12);
1099    }
1100
1101    #[test]
1102    fn test_lebesgue_no_intersection() {
1103        let i = LebesgueMeasure::interval_intersection(0.0, 1.0, 2.0, 3.0);
1104        assert!(i.abs() < 1e-12);
1105    }
1106
1107    // --- ProbabilityMeasure ---
1108
1109    #[test]
1110    fn test_probability_total_mass_uniform() {
1111        // Uniform density on [0,1]: f(x) = 1
1112        let pm = ProbabilityMeasure::new(|_x| 1.0, MeasurableSet::interval(0.0, 1.0), 10_000);
1113        let mass = pm.total_mass();
1114        assert!((mass - 1.0).abs() < 0.05, "total mass = {mass}");
1115    }
1116
1117    #[test]
1118    fn test_probability_expectation_linear() {
1119        // E[X] for X ~ Uniform[0,1] should be 0.5
1120        let pm = ProbabilityMeasure::new(|_x| 1.0, MeasurableSet::interval(0.0, 1.0), 20_000);
1121        let ex = pm.expectation(&|x| x[0]);
1122        assert!((ex - 0.5).abs() < 0.05, "E[X] = {ex}");
1123    }
1124
1125    #[test]
1126    fn test_probability_variance_uniform() {
1127        // Var[X] for X ~ Uniform[0,1] = 1/12 ≈ 0.0833
1128        let pm = ProbabilityMeasure::new(|_x| 1.0, MeasurableSet::interval(0.0, 1.0), 30_000);
1129        let vx = pm.variance(&|x| x[0]);
1130        assert!((vx - 1.0 / 12.0).abs() < 0.02, "Var[X] = {vx}");
1131    }
1132
1133    #[test]
1134    fn test_probability_entropy_positive() {
1135        let pm = ProbabilityMeasure::new(|_x| 1.0, MeasurableSet::interval(0.0, 1.0), 5_000);
1136        // H(Uniform[0,1]) = 0 (in nats, since ∫ 1·ln(1) dx = 0)
1137        let h = pm.entropy();
1138        assert!(h.abs() < 0.05, "H = {h}");
1139    }
1140
1141    // --- SignedMeasure ---
1142
1143    #[test]
1144    fn test_signed_measure_total_variation_positive() {
1145        // f⁺ = 2, f⁻ = 1 on [0,1] → TV = 3
1146        let sm = SignedMeasure::new(|_| 2.0, |_| 1.0, MeasurableSet::interval(0.0, 1.0), 20_000);
1147        let tv = sm.total_variation(&MeasurableSet::interval(0.0, 1.0));
1148        assert!((tv - 3.0).abs() < 0.15, "TV = {tv}");
1149    }
1150
1151    #[test]
1152    fn test_signed_measure_evaluate() {
1153        // ν = 1 − 0 = 1 on [0,1]
1154        let sm = SignedMeasure::new(|_| 1.0, |_| 0.0, MeasurableSet::interval(0.0, 1.0), 20_000);
1155        let v = sm.evaluate(&MeasurableSet::interval(0.0, 1.0));
1156        assert!((v - 1.0).abs() < 0.1, "ν = {v}");
1157    }
1158
1159    #[test]
1160    fn test_signed_measure_jordan_decomposition() {
1161        let sm = SignedMeasure::new(|_| 2.0, |_| 1.0, MeasurableSet::interval(0.0, 1.0), 20_000);
1162        let (pos, neg) = sm.jordan_decomposition();
1163        assert!((pos - 2.0).abs() < 0.15, "ν⁺ = {pos}");
1164        assert!((neg - 1.0).abs() < 0.15, "ν⁻ = {neg}");
1165    }
1166
1167    #[test]
1168    fn test_signed_measure_from_density() {
1169        // density = x - 0.5 on [0,1]: positive on [0.5,1], negative on [0,0.5]
1170        let sm =
1171            SignedMeasure::from_density(|x| x[0] - 0.5, MeasurableSet::interval(0.0, 1.0), 20_000);
1172        let (pos, neg) = sm.jordan_decomposition();
1173        assert!(pos >= 0.0);
1174        assert!(neg >= 0.0);
1175    }
1176
1177    // --- ProductMeasure ---
1178
1179    #[test]
1180    fn test_product_measure_box() {
1181        let pm = ProductMeasure::new(
1182            MeasurableSet::interval(0.0, 2.0),
1183            MeasurableSet::interval(0.0, 3.0),
1184        );
1185        let a = MeasurableSet::interval(0.0, 2.0);
1186        let b = MeasurableSet::interval(0.0, 3.0);
1187        let m = pm.measure_product_box(&a, &b);
1188        assert!((m - 6.0).abs() < 1e-12);
1189    }
1190
1191    #[test]
1192    fn test_fubini_constant_function() {
1193        let pm = ProductMeasure::new(
1194            MeasurableSet::interval(0.0, 1.0),
1195            MeasurableSet::interval(0.0, 1.0),
1196        );
1197        // ∫∫ 1 dx dy = 1
1198        let result = pm.fubini_integrate(&|_x, _y| 1.0, 100, 100);
1199        assert!((result - 1.0).abs() < 0.01, "result = {result}");
1200    }
1201
1202    #[test]
1203    fn test_fubini_separable_function() {
1204        let pm = ProductMeasure::new(
1205            MeasurableSet::interval(0.0, 1.0),
1206            MeasurableSet::interval(0.0, 1.0),
1207        );
1208        // ∫∫ x·y dx dy = (∫ x dx)(∫ y dy) = 0.5 × 0.5 = 0.25
1209        let result = pm.fubini_integrate(&|x, y| x * y, 200, 200);
1210        assert!((result - 0.25).abs() < 0.01, "result = {result}");
1211    }
1212
1213    #[test]
1214    fn test_fubini_vs_monte_carlo() {
1215        let pm = ProductMeasure::new(
1216            MeasurableSet::interval(0.0, 1.0),
1217            MeasurableSet::interval(0.0, 1.0),
1218        );
1219        let (iterated, mc) = pm.verify_fubini(&|x, y| x * x + y * y, 50);
1220        assert!((iterated - mc).abs() < 0.1, "iterated={iterated} mc={mc}");
1221    }
1222
1223    // --- MeasureIntegral ---
1224
1225    #[test]
1226    fn test_integrate_1d_constant() {
1227        let result = MeasureIntegral::integrate_1d(&|_| 2.0, 0.0, 3.0, 1000);
1228        assert!((result - 6.0).abs() < 0.01, "result = {result}");
1229    }
1230
1231    #[test]
1232    fn test_integrate_1d_linear() {
1233        // ∫₀¹ x dx = 0.5
1234        let result = MeasureIntegral::integrate_1d(&|x| x, 0.0, 1.0, 10_000);
1235        assert!((result - 0.5).abs() < 0.001, "result = {result}");
1236    }
1237
1238    #[test]
1239    fn test_integrate_1d_trig() {
1240        // ∫₀^π sin(x) dx = 2
1241        let result = MeasureIntegral::integrate_1d(&|x| x.sin(), 0.0, PI, 10_000);
1242        assert!((result - 2.0).abs() < 0.01, "result = {result}");
1243    }
1244
1245    #[test]
1246    fn test_integrate_2d_constant() {
1247        // ∫∫_{[0,1]²} 1 dx dy = 1
1248        let result = MeasureIntegral::integrate_2d(&|_x, _y| 1.0, 0.0, 1.0, 0.0, 1.0, 100);
1249        assert!((result - 1.0).abs() < 0.01, "result = {result}");
1250    }
1251
1252    #[test]
1253    fn test_integrate_2d_separable() {
1254        // ∫∫_{[0,1]²} x·y dx dy = 0.25
1255        let result = MeasureIntegral::integrate_2d(&|x, y| x * y, 0.0, 1.0, 0.0, 1.0, 200);
1256        assert!((result - 0.25).abs() < 0.005, "result = {result}");
1257    }
1258
1259    #[test]
1260    fn test_integrate_nd_volume() {
1261        // ∫_{[0,1]³} 1 dx = 1
1262        let result =
1263            MeasureIntegral::integrate_nd(&|_| 1.0, &[0.0, 0.0, 0.0], &[1.0, 1.0, 1.0], 10);
1264        assert!((result - 1.0).abs() < 0.01, "result = {result}");
1265    }
1266
1267    #[test]
1268    fn test_monte_carlo_1d() {
1269        // ∫₀¹ x² dx = 1/3
1270        let result = MeasureIntegral::monte_carlo(&|x| x[0].powi(2), &[0.0], &[1.0], 50_000);
1271        assert!((result - 1.0 / 3.0).abs() < 0.02, "result = {result}");
1272    }
1273
1274    #[test]
1275    fn test_monte_carlo_2d() {
1276        // ∫∫_{[0,1]²} (x+y) dx dy = 1
1277        let result =
1278            MeasureIntegral::monte_carlo(&|p| p[0] + p[1], &[0.0, 0.0], &[1.0, 1.0], 50_000);
1279        assert!((result - 1.0).abs() < 0.05, "result = {result}");
1280    }
1281
1282    #[test]
1283    fn test_riemann_stieltjes() {
1284        // ∫₀¹ 1 dG where G(x) = x → result = 1
1285        let result = MeasureIntegral::riemann_stieltjes(&|_| 1.0, &|x| x, 0.0, 1.0, 10_000);
1286        assert!((result - 1.0).abs() < 0.001, "result = {result}");
1287    }
1288
1289    #[test]
1290    fn test_monte_carlo_std_error() {
1291        let (est, err) = MeasureIntegral::monte_carlo_std_error(&|x| x[0], &[0.0], &[1.0], 10_000);
1292        assert!((est - 0.5).abs() < 0.05, "estimate = {est}");
1293        assert!(err > 0.0, "std error should be positive");
1294    }
1295
1296    // --- RadonNikodym ---
1297
1298    #[test]
1299    fn test_radon_nikodym_estimate() {
1300        // Both from the same distribution → derivative ≈ 1
1301        let samples: Vec<f64> = (0..1000).map(|i| i as f64 / 1000.0).collect();
1302        let rn = RadonNikodym::estimate_at(0.5, &samples, &samples, 0.05);
1303        assert!((rn - 1.0).abs() < 0.2, "RN derivative = {rn}");
1304    }
1305
1306    // --- OuterMeasure ---
1307
1308    #[test]
1309    fn test_outer_measure_estimate() {
1310        let om = OuterMeasure::new();
1311        let m = om.estimate(0.0, 3.0, 10);
1312        assert!((m - 3.0).abs() < 1e-12);
1313    }
1314
1315    #[test]
1316    fn test_caratheodory_condition() {
1317        let om = OuterMeasure::new();
1318        assert!(om.verify_caratheodory(0.2, 0.7));
1319    }
1320
1321    // --- CountingMeasure ---
1322
1323    #[test]
1324    fn test_counting_measure() {
1325        assert!((CountingMeasure::measure_set(5) - 5.0).abs() < 1e-12);
1326        assert!((CountingMeasure::measure_set(0)).abs() < 1e-12);
1327    }
1328
1329    // --- ErgodicMeasure ---
1330
1331    #[test]
1332    fn test_ergodic_time_average_doubling_map() {
1333        // Irrational rotation: T(x) = (x + α) mod 1, ergodic w.r.t. Lebesgue
1334        // for irrational α.  E[f] for f(x) = x ≈ 0.5 (space average).
1335        let alpha = (5.0_f64.sqrt() - 1.0) / 2.0; // golden ratio ≈ 0.618
1336        let ta = ErgodicMeasure::time_average(
1337            &|x| x,
1338            &move |x| (x + alpha) % 1.0,
1339            0.123_456_789,
1340            200_000,
1341        );
1342        assert!((ta - 0.5).abs() < 0.05, "time average = {ta}");
1343    }
1344
1345    #[test]
1346    fn test_ergodic_birkhoff_check() {
1347        let alpha = (5.0_f64.sqrt() - 1.0) / 2.0;
1348        let ok = ErgodicMeasure::birkhoff_check(
1349            &|x| x,
1350            &move |x| (x + alpha) % 1.0,
1351            0.1234,
1352            0.5,
1353            200_000,
1354            0.05,
1355        );
1356        assert!(ok, "Birkhoff check failed");
1357    }
1358
1359    // --- HaarMeasure ---
1360
1361    #[test]
1362    fn test_haar_measure_constant() {
1363        // ∫ 1 dθ/(2π) = 1
1364        let result = HaarMeasure::integrate(&|_| 1.0, 1000);
1365        assert!((result - 1.0).abs() < 0.01, "result = {result}");
1366    }
1367
1368    #[test]
1369    fn test_haar_measure_cosine() {
1370        // ∫ cos(θ) dθ/(2π) = 0
1371        let result = HaarMeasure::integrate(&|theta: f64| theta.cos(), 10_000);
1372        assert!(result.abs() < 0.01, "result = {result}");
1373    }
1374
1375    #[test]
1376    fn test_haar_translation_invariant() {
1377        let ok = HaarMeasure::translation_invariant(&|theta: f64| theta.sin().powi(2), 1.0, 10_000);
1378        assert!(ok);
1379    }
1380
1381    // --- check_lp_convergence ---
1382
1383    #[test]
1384    fn test_lp_convergence() {
1385        // fₙ(x) = x^(1/n) → 1 on [0,1] in L2
1386        let fns: Vec<Box<dyn Fn(f64) -> f64>> = (1..=10)
1387            .map(|n| {
1388                let b: Box<dyn Fn(f64) -> f64> = Box::new(move |x: f64| x.powf(1.0 / n as f64));
1389                b
1390            })
1391            .collect();
1392        let limit = |_x: f64| 1.0;
1393        let converges = check_lp_convergence(&fns, &limit, 0.0, 1.0, 2.0, 1000, 0.15);
1394        assert!(converges);
1395    }
1396}