automaton.rs

41.0 kB · rust · 1107 lines

1use crate::memory::{transfer, Rule};2use crate::zeta::{raise, Complex};3use mrlycore::errors::{value_error, Result};45/// The relative rounding allowance the double-precision matrix ladder charges against the scale it carries.6///7/// The truncation bound is the proved one, carried entrywise by the same recursion that carries the value vector; this constant is a measured allowance for the arithmetic itself and is not a proof.8pub const ROUNDING: f64 = 1e-13;910const PEEL_TARGET: f64 = 100.0;11const PEEL_CAP: usize = 26;12const SHIFT_START: f64 = 6.0;13const SHIFT_STEP: f64 = 6.0;14const SHIFT_CAP: f64 = 400.0;15const CUT_START: usize = 8;16const CUT_STEP: usize = 6;17const CUT_CAP: usize = 120;18const RATIO_CAP: f64 = 0.9;19const COLLATZ_WIELANDT: usize = 60;20const PIVOT_FLOOR: f64 = 1e-12;2122// THE STATE SPACE2324/// A memory design read as a matrix ladder: the rule, the transfer matrix on its `(k-1)`-window states, and the peel depth its Dirichlet series is continued from.25///26/// The elements are the positive integers whose minimal base-`q` string, `q = 2^D`, is accepted by the rule, and the object is `zeta_W(s) = sum n^(-s)` over them.27/// [`crate::memory::Rule::accepts`] holds a word shorter than the width `k` to be accepted, there being no window in it, so every integer below `q^(k-1)` with a nonzero leading digit sits in `S_W` for every rule of that width, and the head polynomial carries them.28/// With `E_j(w)` the vector whose entry `u` sums `n^(-w)` over accepted words of exactly `j` digits ending in state `u`, and `G_P = sum_(j >= P) E_j`, splitting a word on its last digit gives `(I - q^(-w) T) G_P(w) = E_P(w) + sum_(l >= 1) binom(-w, l) q^(-w-l) Gamma_l G_P(w+l)`, where `Gamma_l(u', u) = sum a^l` over the letters `a` with the window `(u, a)` allowed and `u' = shift(u, a)`, and `T = Gamma_0` is [`crate::memory::transfer`] transposed.29/// Then `zeta_W(w) = 1^T D_(P-1)(w) + 1^T G_P(w)` with `D_(P-1)` the Dirichlet polynomial over the accepted words of at most `P-1` digits, and the scalar ladder of [`crate::ladder`] is the width one case with `card F` where `T` stands.30#[derive(Clone, Debug, PartialEq)]31pub struct Automaton {32    rule: Rule,33    base: u64,34    states: usize,35    peel: usize,36    largest: u64,37    arcs: Vec<(usize, usize, u64)>,38    power: Vec<Vec<f64>>,39    low: Vec<f64>,40    mid: Vec<Vec<f64>>,41    seats: Vec<f64>,42    guide: Vec<f64>,43    perron_high: f64,44    perron_low: f64,45    charpoly: Vec<f64>,46    adjugate: Vec<Vec<Vec<f64>>>,47}4849fn walk_words(50    rule: &Rule,51    base: u64,52    depth: usize,53    word: &mut Vec<usize>,54    visit: &mut impl FnMut(usize, usize, u64),55) {56    if word.len() >= depth {57        return;58    }59    let start = usize::from(word.is_empty());60    for c in start..rule.letters() {61        word.push(c);62        if rule.accepts(word) {63            let value = word.iter().fold(0u64, |acc, &d| acc * base + d as u64);64            let span = word.len() - (rule.width - 1).min(word.len());65            let state = word[span..]66                .iter()67                .fold(0usize, |acc, &d| acc * base as usize + d);68            visit(word.len(), state, value);69            walk_words(rule, base, depth, word, visit);70        }71        word.pop();72    }73}7475impl Automaton {76    /// Builds the ladder of a rule, choosing the peel depth, or an error when the rule admits no element or its state space overruns the exact integers.77    ///78    /// ```79    /// let golden = mrlynum::memory::Rule::new(1, 2, 7).unwrap();80    /// let ladder = mrlynum::automaton::Automaton::new(&golden).unwrap();81    /// assert_eq!(ladder.states(), 2);82    /// assert!((ladder.abscissa() - 0.694_241_913_630_617_4).abs() < 1e-15);83    /// ```84    pub fn new(rule: &Rule) -> Result<Automaton> {85        let mut peel = rule.width.max(2);86        let base = rule.letters() as u64;87        while peel < PEEL_CAP && (base as f64).powi(peel as i32 + 1) < 9.0e15 {88            let mut seen = 0f64;89            let mut word = Vec::new();90            walk_words(rule, base, peel, &mut word, &mut |len, _, _| {91                if len == peel {92                    seen += 1.0;93                }94            });95            if seen > PEEL_TARGET {96                break;97            }98            peel += 1;99        }100        Automaton::with_peel(rule, peel)101    }102103    /// Builds the ladder at an explicit peel depth, at least the rule width and at least two, or an error when the rule admits no element or the depth overruns the exact integers.104    ///105    /// ```106    /// let full = mrlynum::memory::Rule::full(1, 2);107    /// let ladder = mrlynum::automaton::Automaton::with_peel(&full, 7).unwrap();108    /// assert_eq!(ladder.peel(), 7);109    /// assert_eq!(ladder.abscissa(), 1.0);110    /// ```111    pub fn with_peel(rule: &Rule, peel: usize) -> Result<Automaton> {112        let base = rule.letters() as u64;113        let states = rule.states();114        if peel < rule.width.max(2) {115            return value_error(format!(116                "peel depth {peel} is under the rule width {} or under two.",117                rule.width118            ));119        }120        if (base as f64).powi(peel as i32) > 9.007_199_254_740_992e15 {121            return value_error(format!("peel depth {peel} overruns the exact integers."));122        }123        let source = transfer(rule);124        let mut arcs: Vec<(usize, usize, u64)> = Vec::new();125        if rule.width == 1 {126            for c in 0..rule.letters() {127                if rule.allowed(c) {128                    arcs.push((0, 0, c as u64));129                }130            }131        } else {132            for s in 0..source.len() {133                for c in 0..rule.letters() {134                    let window = (s << rule.dimension) | c;135                    if rule.allowed(window) {136                        arcs.push((window & (states - 1), s, c as u64));137                    }138                }139            }140        }141        let largest = arcs.iter().map(|&(_, _, a)| a).max().unwrap_or(0);142        let power: Vec<Vec<f64>> = (0..base)143            .map(|a| (0..=CUT_CAP).map(|l| (a as f64).powi(l as i32)).collect())144            .collect();145        let mut low: Vec<f64> = Vec::new();146        let mut mid: Vec<Vec<f64>> = vec![Vec::new(); states];147        let mut seats = vec![0.0f64; states];148        let mut word = Vec::new();149        walk_words(rule, base, peel, &mut word, &mut |len, state, value| {150            if len < peel {151                low.push((value as f64).ln());152            } else {153                mid[state].push((value as f64).ln());154                seats[state] += 1.0;155            }156        });157        if low.is_empty() && seats.iter().all(|&c| c == 0.0) {158            return value_error("the rule accepts no element with a nonzero leading digit.");159        }160        let mut ladder = Automaton {161            rule: *rule,162            base,163            states,164            peel,165            largest,166            arcs,167            power,168            low,169            mid,170            seats,171            guide: vec![1.0; states],172            perron_high: 0.0,173            perron_low: 0.0,174            charpoly: Vec::new(),175            adjugate: Vec::new(),176        };177        ladder.tighten();178        ladder.faddeev();179        Ok(ladder)180    }181182    fn tighten(&mut self) {183        let mut guide = vec![1.0f64; self.states];184        for _ in 0..COLLATZ_WIELANDT {185            let next = self.step_real(0, &guide);186            let peak = next.iter().cloned().fold(0.0f64, f64::max);187            if peak > 1e120 {188                break;189            }190            for (seat, &gain) in guide.iter_mut().zip(next.iter()) {191                *seat += gain;192            }193        }194        let image = self.step_real(0, &guide);195        let mut high = 0.0f64;196        let mut low = f64::INFINITY;197        for (&gain, &seat) in image.iter().zip(guide.iter()) {198            high = high.max(gain / seat);199            low = low.min(gain / seat);200        }201        self.guide = guide;202        self.perron_high = high * (1.0 + 1e-14) + 1e-300;203        self.perron_low = low * (1.0 - 1e-14);204    }205206    fn step_real(&self, power: usize, vector: &[f64]) -> Vec<f64> {207        let mut out = vec![0.0f64; self.states];208        for &(t, s, a) in &self.arcs {209            out[t] += self.power[a as usize][power] * vector[s];210        }211        out212    }213214    fn step_complex(&self, power: usize, vector: &[Complex]) -> Vec<Complex> {215        let mut out = vec![Complex::new(0.0, 0.0); self.states];216        for &(t, s, a) in &self.arcs {217            out[t] = out[t] + vector[s] * self.power[a as usize][power];218        }219        out220    }221222    /// Returns the rule.223    pub fn rule(&self) -> Rule {224        self.rule225    }226227    /// Returns the base `q = 2^D`.228    pub fn base(&self) -> u64 {229        self.base230    }231232    /// Returns the state count `q^(k-1)`.233    pub fn states(&self) -> usize {234        self.states235    }236237    /// Returns the peel depth `P`.238    pub fn peel(&self) -> usize {239        self.peel240    }241242    /// Returns the transfer matrix `T = Gamma_0` the ladder runs on, the transpose of [`crate::memory::transfer`], entry `(u', u)` counting the letters carrying `u` to `u'`.243    ///244    /// ```245    /// let golden = mrlynum::memory::Rule::new(1, 2, 7).unwrap();246    /// let ladder = mrlynum::automaton::Automaton::new(&golden).unwrap();247    /// assert_eq!(ladder.matrix(), vec![vec![1.0, 1.0], vec![1.0, 0.0]]);248    /// ```249    pub fn matrix(&self) -> Vec<Vec<f64>> {250        let mut out = vec![vec![0.0f64; self.states]; self.states];251        for &(t, s, _) in &self.arcs {252            out[t][s] += 1.0;253        }254        out255    }256257    /// Returns the Collatz-Wielandt bracket `(low, high)` of the Perron root of the transfer matrix, the ratios the ladder divides with.258    ///259    /// For any nonnegative `T` and any `v > 0`, `min_u (T v)_u / v_u <= rho <= max_u (T v)_u / v_u`, so the pair brackets the root with no primitivity hypothesis; the guide is `v = (I + T)^m 1` for `m` sixty, or fewer when the entries would overrun the exponent range.260    /// The ends carry a relative `1e-14` allowance of the same measured kind as [`ROUNDING`] and are not a rounding-certified interval, and the bracket is loose exactly when `T` is defective: at code `13` and `k = 2`, a Jordan block of root `1`, the upper end is `64/62`. Only the upper end is load bearing, as the `theta < 1` test that opens the Neumann branch; the root itself is [`crate::memory::perron`].261    pub fn perron(&self) -> (f64, f64) {262        (self.perron_low, self.perron_high)263    }264265    /// Returns the abscissa `alpha = log_q rho`, with `rho` the exact Perron root of [`crate::memory::perron`].266    ///267    /// The series converges absolutely on `Re s > alpha`. The bracket of [`Automaton::perron`] is a bound and not the root, so it is not read here.268    ///269    /// ```270    /// let jordan = mrlynum::memory::Rule::new(1, 2, 13).unwrap();271    /// let ladder = mrlynum::automaton::Automaton::new(&jordan).unwrap();272    /// assert_eq!(ladder.abscissa(), 0.0);273    /// ```274    pub fn abscissa(&self) -> f64 {275        let root = crate::memory::perron(&self.rule);276        if root <= 0.0 {277            return f64::NEG_INFINITY;278        }279        root.ln() / (self.base as f64).ln()280    }281282    /// Returns the pole spacing `2 pi / log q`.283    pub fn period(&self) -> f64 {284        2.0 * std::f64::consts::PI / (self.base as f64).ln()285    }286}287288// THE RESOLVENT289290fn invert(matrix: &[Vec<Complex>]) -> Option<Vec<Vec<Complex>>> {291    let n = matrix.len();292    let mut a: Vec<Vec<Complex>> = matrix.to_vec();293    let mut b: Vec<Vec<Complex>> = (0..n)294        .map(|i| {295            (0..n)296                .map(|j| Complex::new(f64::from(u8::from(i == j)), 0.0))297                .collect()298        })299        .collect();300    let scale = matrix301        .iter()302        .flat_map(|row| row.iter())303        .map(|z| z.abs())304        .fold(0.0f64, f64::max)305        .max(1.0);306    for col in 0..n {307        let mut pick = col;308        for row in col + 1..n {309            if a[row][col].abs() > a[pick][col].abs() {310                pick = row;311            }312        }313        if a[pick][col].abs() < PIVOT_FLOOR * scale {314            return None;315        }316        a.swap(col, pick);317        b.swap(col, pick);318        let inv = Complex::new(1.0, 0.0) / a[col][col];319        for j in 0..n {320            a[col][j] = a[col][j] * inv;321            b[col][j] = b[col][j] * inv;322        }323        let prow = a[col].clone();324        let qrow = b[col].clone();325        for row in 0..n {326            if row == col {327                continue;328            }329            let factor = a[row][col];330            if factor.abs() == 0.0 {331                continue;332            }333            for j in 0..n {334                a[row][j] = a[row][j] - factor * prow[j];335                b[row][j] = b[row][j] - factor * qrow[j];336            }337        }338    }339    Some(b)340}341342fn log_binomial(top: f64, pick: usize) -> f64 {343    (1..=pick)344        .map(|i| ((top - pick as f64 + i as f64) / i as f64).ln())345        .sum()346}347348fn poly(logs: &[f64], w: Complex) -> Complex {349    let mut acc = Complex::new(0.0, 0.0);350    for &l in logs {351        acc = acc + ((-w) * l).exp();352    }353    acc354}355356fn poly_scale(logs: &[f64], sigma: f64) -> f64 {357    logs.iter().map(|&l| (-sigma * l).exp()).sum()358}359360struct Rung {361    value: Vec<Complex>,362    bound: Vec<f64>,363    scale: Vec<f64>,364}365366impl Automaton {367    fn neumann(&self, sigma: f64, seed: &[f64]) -> Option<Vec<f64>> {368        let base = self.base as f64;369        let theta = base.powf(-sigma) * self.perron_high;370        if theta >= 1.0 {371            return None;372        }373        let factor = base.powf(-sigma);374        let beta = seed375            .iter()376            .zip(self.guide.iter())377            .map(|(&y, &v)| y / v)378            .fold(0.0f64, f64::max);379        let mut out = vec![0.0f64; self.states];380        let mut term = seed.to_vec();381        let mut power = 1.0f64;382        for _ in 0..4000 {383            for (seat, &gain) in out.iter_mut().zip(term.iter()) {384                *seat += gain;385            }386            term = self387                .step_real(0, &term)388                .iter()389                .map(|&v| v * factor)390                .collect();391            power *= theta;392            let peak = out.iter().cloned().fold(0.0f64, f64::max);393            if beta * power <= 1e-18 * (1.0 - theta) * peak || term.iter().all(|&v| v == 0.0) {394                break;395            }396        }397        let rest = beta * power / (1.0 - theta);398        for (seat, &v) in out.iter_mut().zip(self.guide.iter()) {399            *seat += rest * v;400        }401        Some(out)402    }403404    fn tail_parts(&self, sigma: f64) -> Option<(f64, Vec<f64>)> {405        let mass = self.neumann(sigma, &self.seats)?;406        let log = -(self.peel as f64 - 1.0) * sigma * (self.base as f64).ln();407        Some((log, mass))408    }409410    fn tail(&self, sigma: f64) -> Option<Vec<f64>> {411        let (log, mass) = self.tail_parts(sigma)?;412        Some(mass.iter().map(|&m| log.exp() * m).collect())413    }414415    fn cut(&self, w: Complex, depth: usize) -> Vec<f64> {416        let blown = vec![f64::INFINITY; self.states];417        if self.largest == 0 {418            return vec![0.0; self.states];419        }420        let (span, sigma) = (w.abs(), w.re);421        let base = self.base as f64;422        let largest = self.largest as f64;423        let ratio = ((span + depth as f64 + 1.0) / (depth as f64 + 2.0)).max(1.0) * largest424            / base.powi(self.peel as i32);425        if ratio >= RATIO_CAP {426            return blown;427        }428        let Some((rest, mass)) = self.tail_parts(sigma + depth as f64 + 1.0) else {429            return blown;430        };431        let log = log_binomial(span + depth as f64, depth + 1)432            + (-sigma - depth as f64 - 1.0) * base.ln()433            + (depth as f64 + 1.0) * largest.ln()434            + rest;435        let front = self.step_real(0, &mass);436        front437            .iter()438            .map(|&m| log.exp() * m / (1.0 - ratio))439            .collect()440    }441442    fn divide(&self, w: Complex, acc: &[Complex], err: &[f64], wide: &[f64]) -> Option<Rung> {443        let base = self.base as f64;444        let x = raise(base, -w);445        let front: Vec<Vec<Complex>> = self446            .matrix()447            .iter()448            .enumerate()449            .map(|(i, row)| {450                row.iter()451                    .enumerate()452                    .map(|(j, &t)| Complex::new(f64::from(u8::from(i == j)), 0.0) - x * t)453                    .collect()454            })455            .collect();456        let inverse = invert(&front)?;457        let value: Vec<Complex> = inverse458            .iter()459            .map(|row| {460                row.iter()461                    .zip(acc.iter())462                    .fold(Complex::new(0.0, 0.0), |a, (&c, &y)| a + c * y)463            })464            .collect();465        let carry = |seed: &[f64]| -> Option<Vec<f64>> {466            if let Some(out) = self.neumann(w.re, seed) {467                return Some(out);468            }469            let mut residual = 0.0f64;470            for (i, source) in front.iter().enumerate() {471                let mut row = 0.0f64;472                for j in 0..self.states {473                    let mut entry = Complex::new(f64::from(u8::from(i == j)), 0.0);474                    for (&near, image) in source.iter().zip(inverse.iter()) {475                        entry = entry - near * image[j];476                    }477                    row += entry.abs();478                }479                residual = residual.max(row);480            }481            if residual >= 1.0 {482                return None;483            }484            let peak = seed.iter().cloned().fold(0.0f64, f64::max);485            let lift = peak * residual / (1.0 - residual);486            Some(487                inverse488                    .iter()489                    .map(|row| {490                        row.iter()491                            .zip(seed.iter())492                            .map(|(&c, &y)| c.abs() * (y + lift))493                            .sum()494                    })495                    .collect(),496            )497        };498        Some(Rung {499            value,500            bound: carry(err)?,501            scale: carry(wide)?,502        })503    }504505    fn rung(&self, s: Complex, shift: f64, depth: usize, whole: bool) -> Option<Rung> {506        let base = self.base as f64;507        let levels = (shift - s.re).ceil().max(0.0) as usize;508        if !whole && levels == 0 {509            let rest = self.tail(s.re)?;510            let lift = self.step_real(0, &rest);511            let factor = base.powf(-s.re);512            let out: Vec<f64> = rest513                .iter()514                .zip(lift.iter())515                .map(|(&r, &l)| r + factor * l)516                .collect();517            return Some(Rung {518                value: vec![Complex::new(0.0, 0.0); self.states],519                bound: out.clone(),520                scale: out,521            });522        }523        let rows = levels + depth + 2;524        let mut value = vec![vec![Complex::new(0.0, 0.0); self.states]; rows];525        let mut bound = vec![vec![0.0f64; self.states]; rows];526        let mut scale = vec![vec![0.0f64; self.states]; rows];527        for j in levels..rows {528            let rest = self.tail(s.re + j as f64)?;529            bound[j] = rest.clone();530            scale[j] = rest;531        }532        for j in (0..levels).rev() {533            let w = s + j as f64;534            let mut acc: Vec<Complex> = self535                .mid536                .iter()537                .map(|logs| poly(logs, w))538                .collect::<Vec<Complex>>();539            let mut err = self.cut(w, depth);540            let mut wide: Vec<f64> = self541                .mid542                .iter()543                .map(|logs| poly_scale(logs, w.re))544                .zip(err.iter())545                .map(|(p, &e)| p + e)546                .collect();547            let mut coefficient = Complex::new(1.0, 0.0);548            for l in 1..=depth {549                coefficient = coefficient * ((-w) - (l as f64 - 1.0)) * (1.0 / l as f64);550                let weight = raise(base, (-w) - l as f64) * coefficient;551                let lift = self.step_complex(l, &value[j + l]);552                let heavy = self.step_real(l, &bound[j + l]);553                let broad = self.step_real(l, &scale[j + l]);554                for u in 0..self.states {555                    acc[u] = acc[u] + weight * lift[u];556                    err[u] += weight.abs() * heavy[u];557                    wide[u] += weight.abs() * broad[u];558                }559            }560            if !whole && j == 0 {561                return Some(Rung {562                    value: acc,563                    bound: err,564                    scale: wide,565                });566            }567            let step = self.divide(w, &acc, &err, &wide)?;568            value[j] = step.value;569            bound[j] = step.bound;570            scale[j] = step.scale;571        }572        Some(Rung {573            value: value[0].clone(),574            bound: bound[0].clone(),575            scale: scale[0].clone(),576        })577    }578579    fn tune(580        &self,581        s: Complex,582        tolerance: f64,583        whole: bool,584        head: f64,585    ) -> Result<(Vec<Complex>, Vec<f64>)> {586        let mut shift = SHIFT_START;587        let mut depth = CUT_START;588        let mut best = f64::INFINITY;589        let mut walked = false;590        while shift <= SHIFT_CAP && depth <= CUT_CAP {591            if let Some(step) = self.rung(s, shift, depth, whole) {592                let carried: Vec<f64> = step593                    .bound594                    .iter()595                    .zip(step.scale.iter())596                    .map(|(&b, &c)| b + ROUNDING * c)597                    .collect();598                let total: f64 = carried.iter().sum::<f64>() + ROUNDING * head;599                if total < tolerance {600                    return Ok((step.value, carried));601                }602                best = best.min(total);603            } else {604                walked = true;605            }606            shift += SHIFT_STEP;607            depth += CUT_STEP;608        }609        if walked {610            return value_error(format!(611                "the ladder cannot invert I - q^(-w) T on the walk up from s = {} + {}i: a level sits on a pole of the resolvent or left of the abscissa with no certified inverse.",612                s.re, s.im613            ));614        }615        value_error(format!(616            "tolerance {tolerance:e} is out of reach at s = {} + {}i, best bound {best:e}.",617            s.re, s.im618        ))619    }620}621622// THE DETERMINANT AND THE ADJUGATE623624impl Automaton {625    fn faddeev(&mut self) {626        let n = self.states;627        let matrix = self.matrix();628        let mut blocks: Vec<Vec<Vec<f64>>> = Vec::with_capacity(n);629        let mut coefficients = vec![1.0f64];630        let mut current: Vec<Vec<f64>> = (0..n)631            .map(|i| (0..n).map(|j| f64::from(u8::from(i == j))).collect())632            .collect();633        for k in 1..=n {634            blocks.push(current.clone());635            let mut product = vec![vec![0.0f64; n]; n];636            for i in 0..n {637                for t in 0..n {638                    if matrix[i][t] == 0.0 {639                        continue;640                    }641                    for j in 0..n {642                        product[i][j] += matrix[i][t] * current[t][j];643                    }644                }645            }646            let trace: f64 = (0..n).map(|i| product[i][i]).sum();647            let coefficient = -trace / k as f64;648            coefficients.push(coefficient);649            for (i, row) in product.iter_mut().enumerate() {650                row[i] += coefficient;651            }652            current = product;653        }654        self.charpoly = coefficients;655        self.adjugate = blocks;656    }657658    /// Returns the coefficients `c_0 .. c_n` of `det(I - x T) = sum c_i x^i`, the ladder denominator read as a polynomial in `x = q^(-s)`.659    ///660    /// The roots in `x` are the reciprocals of the nonzero eigenvalues of `T`, so the poles of `zeta_W` sit at `s = log_q lambda_i - m + (arg lambda_i + 2 pi j) i / log q`: one comb per distinct eigenvalue, offset by the eigenvalue's argument.661    ///662    /// ```663    /// let golden = mrlynum::memory::Rule::new(1, 2, 7).unwrap();664    /// let ladder = mrlynum::automaton::Automaton::new(&golden).unwrap();665    /// let coefficients = ladder.denominator();666    /// assert!((coefficients[1] + 1.0).abs() < 1e-12 && (coefficients[2] + 1.0).abs() < 1e-12);667    /// ```668    pub fn denominator(&self) -> Vec<f64> {669        self.charpoly.clone()670    }671672    fn det(&self, x: Complex) -> Complex {673        let mut acc = Complex::new(0.0, 0.0);674        let mut power = Complex::new(1.0, 0.0);675        for &c in &self.charpoly {676            acc = acc + power * c;677            power = power * x;678        }679        acc680    }681682    fn det_slope(&self, x: Complex) -> Complex {683        let mut acc = Complex::new(0.0, 0.0);684        let mut power = Complex::new(1.0, 0.0);685        for (k, &c) in self.charpoly.iter().enumerate().skip(1) {686            acc = acc + power * (c * k as f64);687            power = power * x;688        }689        acc690    }691692    fn adjugate_apply(&self, x: Complex, vector: &[Complex]) -> Vec<Complex> {693        let mut out = vec![Complex::new(0.0, 0.0); self.states];694        let mut power = Complex::new(1.0, 0.0);695        for block in &self.adjugate {696            for (i, row) in block.iter().enumerate() {697                for (j, &entry) in row.iter().enumerate() {698                    if entry != 0.0 {699                        out[i] = out[i] + power * entry * vector[j];700                    }701                }702            }703            power = power * x;704        }705        out706    }707708    fn adjugate_weights(&self, radius: f64) -> Vec<f64> {709        let mut out = vec![0.0f64; self.states];710        let mut power = 1.0f64;711        for block in &self.adjugate {712            for row in block.iter() {713                for (j, &entry) in row.iter().enumerate() {714                    out[j] += power * entry.abs();715                }716            }717            power *= radius;718        }719        out720    }721}722723// READINGS724725impl Automaton {726    /// Returns `zeta_W(s)` and the bound it is known to, or an error when the tolerance is out of reach or a level of the walk sits on a pole of the resolvent.727    ///728    /// The bound is the propagated truncation bound, carried entrywise as a nonnegative vector through `(I - q^(-w) T)^(-1)`, plus [`ROUNDING`] times the scale the ladder carries, which is a measured allowance and not a proof.729    /// Right of the abscissa the inverse is majorised by its Neumann series, `sum_i (q^(-Re w) T)^i`, which is entrywise nonnegative and needs no norm and no primitivity; left of it the bound runs through the computed inverse certified by its own residual, `abs(C) (y + norm(y) r/(1-r) 1)` with `r = norm(I - (I - q^(-w) T) C)`, and the module raises rather than return when `r >= 1`.730    ///731    /// ```732    /// let full = mrlynum::memory::Rule::full(1, 2);733    /// let ladder = mrlynum::automaton::Automaton::new(&full).unwrap();734    /// let s = mrlynum::zeta::Complex::new(2.0, 0.0);735    /// let (value, bound) = ladder.zeta(s, 1e-10).unwrap();736    /// assert!((value.re - std::f64::consts::PI * std::f64::consts::PI / 6.0).abs() < bound);737    /// ```738    pub fn zeta(&self, s: Complex, tolerance: f64) -> Result<(Complex, f64)> {739        let head = poly_scale(&self.low, s.re);740        let (value, carried) = self.tune(s, tolerance, true, head)?;741        let sum = value.iter().fold(Complex::new(0.0, 0.0), |acc, &z| acc + z);742        let bound = carried.iter().sum::<f64>() + ROUNDING * head;743        Ok((poly(&self.low, s) + sum, bound))744    }745746    /// Returns the matrix Lyndon cofactor `Z_W(s) = det(I - q^(-s) T) zeta_W(s)` and the bound it is known to.747    ///748    /// The scalar `1 - k q^(-s)` becomes the determinant, and the cofactor is carried as `det(I - q^(-s) T) D_(P-1)(s) + 1^T adj(I - q^(-s) T) N(s)` with `N` the ladder numerator, so it is read on the whole `m = 0` pole comb where `zeta_W` itself is singular.749    pub fn cofactor(&self, s: Complex, tolerance: f64) -> Result<(Complex, f64)> {750        let x = raise(self.base as f64, -s);751        let det = self.det(x);752        let head = det.abs() * poly_scale(&self.low, s.re);753        let (value, carried) = self.tune(s, tolerance, false, head)?;754        let lifted = self.adjugate_apply(x, &value);755        let sum = lifted756            .iter()757            .fold(Complex::new(0.0, 0.0), |acc, &z| acc + z);758        let weights = self.adjugate_weights(x.abs());759        let bound = weights760            .iter()761            .zip(carried.iter())762            .map(|(&m, &b)| m * b)763            .sum::<f64>()764            + ROUNDING * head;765        Ok((det * poly(&self.low, s) + sum, bound))766    }767768    /// Returns the residue of `zeta_W` at a simple pole `w0` of the resolvent and the bound it is known to, or an error when `w0` is not a simple root of `det(I - q^(-w) T)`.769    ///770    /// At such a point the resolvent is `adj(I - x T) / det(I - x T)` with `x = q^(-w)`, so the residue is `1^T adj(I - x0 T) N(w0)` over `-x0 log q det'(x0)`: the adjugate is the spectral projector in polynomial form, and no eigenvector is solved for.771    /// The head term `det(I - q^(-s) T) D_(P-1)(s)` is dropped, since the determinant vanishes at the pole.772    pub fn residue(&self, w0: Complex, tolerance: f64) -> Result<(Complex, f64)> {773        let base = self.base as f64;774        let x = raise(base, -w0);775        let slope = self.det_slope(x);776        let den = -x * base.ln() * slope;777        if self.det(x).abs() > 1e-8 * self.charpoly.iter().map(|c| c.abs()).sum::<f64>().max(1.0) {778            return value_error(format!(779                "w0 = {} + {}i is not a root of det(I - q^(-w) T).",780                w0.re, w0.im781            ));782        }783        if den.abs() < 1e-12 {784            return value_error(format!(785                "the root at w0 = {} + {}i is not simple, so the pole order exceeds one.",786                w0.re, w0.im787            ));788        }789        let (value, carried) = self.tune(w0, tolerance, false, 0.0)?;790        let lifted = self.adjugate_apply(x, &value);791        let sum = lifted792            .iter()793            .fold(Complex::new(0.0, 0.0), |acc, &z| acc + z);794        let weights = self.adjugate_weights(x.abs());795        let bound = weights796            .iter()797            .zip(carried.iter())798            .map(|(&m, &b)| m * b)799            .sum::<f64>()800            / den.abs();801        Ok((sum / den, bound))802    }803}804805#[cfg(test)]806mod tests {807    use super::*;808    use crate::ladder::Design;809810    const PINNED: f64 = 1e-9;811812    fn near(value: Complex, re: f64, im: f64, bound: f64) {813        assert!(814            (value.re - re).abs() < bound && (value.im - im).abs() < bound,815            "{value:?} is not {re} + {im}i inside {bound:e}"816        );817    }818819    fn golden() -> Automaton {820        Automaton::new(&Rule::new(1, 2, 7).unwrap()).unwrap()821    }822823    #[test]824    fn the_full_rule_is_the_base_two_design() {825        let design = Design::new(2, &[0, 1]).unwrap();826        for width in [2usize, 3] {827            let ladder = Automaton::new(&Rule::full(1, width)).unwrap();828            assert_eq!(ladder.states(), 1 << (width - 1));829            assert_eq!(ladder.abscissa(), 1.0);830            let mut wanted = vec![0.0; 1 << (width - 1)];831            wanted[0] = 1.0;832            if width > 1 {833                wanted[1] = -2.0;834            }835            wanted.push(0.0);836            assert_eq!(ladder.denominator(), wanted);837            for s in [Complex::new(2.0, 0.0), Complex::new(0.3, 40.0)] {838                let (mine, one) = ladder.zeta(s, 1e-8).unwrap();839                let (theirs, two) = crate::ladder::zeta(&design, s, 1e-8).unwrap();840                assert!(841                    (mine - theirs).abs() < one + two,842                    "width {width} at {s:?}: {mine:?} {theirs:?} {one:e} {two:e}"843                );844            }845        }846    }847848    #[test]849    fn the_product_rule_is_the_mersenne_series() {850        let ladder = Automaton::new(&Rule::new(1, 2, 8).unwrap()).unwrap();851        assert!(ladder.abscissa().abs() < 1e-12);852        for s in [Complex::new(2.0, 0.0), Complex::new(1.5, 3.0)] {853            let (value, bound) = ladder.zeta(s, 1e-9).unwrap();854            let mut want = Complex::new(0.0, 0.0);855            for m in 1..=60u32 {856                want = want + ((-s) * ((2f64.powi(m as i32) - 1.0).ln())).exp();857            }858            assert!(859                (value - want).abs() < bound + 1e-15,860                "{s:?} {value:?} {want:?}"861            );862        }863    }864865    #[test]866    fn the_golden_rule_is_the_fibbinary_series() {867        let ladder = golden();868        assert_eq!(ladder.peel(), 12);869        let (value, bound) = ladder.zeta(Complex::new(3.0, 0.0), PINNED).unwrap();870        let (mut want, mut drift) = (0.0f64, 0.0f64);871        for n in 1u64..1 << 22 {872            if n & (n << 1) == 0 {873                let term = (n as f64).powf(-3.0) - drift;874                let total = want + term;875                drift = (total - want) - term;876                want = total;877            }878        }879        let (mut a, mut b) = (1.0f64, 1.0f64);880        let mut rest = 0.0f64;881        for _ in 1..=22u32 {882            let next = a + b;883            a = b;884            b = next;885        }886        for step in 0..400u32 {887            rest += a * 2f64.powf(-3.0 * (22 + step) as f64);888            let next = a + b;889            a = b;890            b = next;891        }892        assert!(893            value.re > want - bound && value.re < want + rest + bound,894            "{value:?} {want} {rest:e} {bound:e}"895        );896    }897898    #[test]899    fn the_golden_transfer_carries_the_two_combs() {900        let ladder = golden();901        let phi = (1.0 + 5f64.sqrt()) / 2.0;902        let (low, high) = ladder.perron();903        assert!(low <= phi && phi <= high, "{low} {phi} {high}");904        assert!(high - low < 1e-9, "{low} {high}");905        let coefficients = ladder.denominator();906        assert_eq!(coefficients.len(), 3);907        assert!((coefficients[1] + 1.0).abs() < 1e-12);908        assert!((coefficients[2] + 1.0).abs() < 1e-12);909        assert_eq!(ladder.abscissa(), phi.log2());910    }911912    #[test]913    fn the_residues_on_the_first_comb_are_the_contour_averages() {914        let ladder = golden();915        let phi = (1.0 + 5f64.sqrt()) / 2.0;916        for j in 0..3i64 {917            let w0 = Complex::new(phi.log2(), ladder.period() * j as f64);918            let (want, bound) = ladder.residue(w0, PINNED).unwrap();919            let (radius, nodes) = (0.05, 8);920            let mut got = Complex::new(0.0, 0.0);921            let mut carried = 0.0;922            for k in 0..nodes {923                let turn = Complex::turn(std::f64::consts::TAU * k as f64 / nodes as f64);924                let (value, other) = ladder.zeta(w0 + turn * radius, 1e-9).unwrap();925                got = got + turn * radius * value * (1.0 / nodes as f64);926                carried += radius * other / nodes as f64;927            }928            assert!(929                (got - want).abs() < bound + carried + 1e-6,930                "j {j} {got:?} {want:?}"931            );932        }933    }934935    #[test]936    fn the_cofactor_is_the_series_times_the_determinant() {937        let ladder = golden();938        for s in [Complex::new(2.0, 0.0), Complex::new(1.2, 9.0)] {939            let x = raise(2.0, -s);940            let mut det = Complex::new(1.0, 0.0);941            let mut power = Complex::new(1.0, 0.0);942            for &c in ladder.denominator().iter().skip(1) {943                power = power * x;944                det = det + power * c;945            }946            let (value, bound) = ladder.cofactor(s, 1e-8).unwrap();947            let (series, other) = ladder.zeta(s, 1e-8).unwrap();948            assert!(949                (value - det * series).abs() < bound + det.abs() * other,950                "{s:?} {value:?} {:?}",951                det * series952            );953        }954    }955956    #[test]957    fn the_golden_residues_on_the_first_comb_are_pinned() {958        let ladder = golden();959        let phi = (1.0 + 5f64.sqrt()) / 2.0;960        let wanted = [961            (0.946_743_395_641_970_1, 0.0),962            (0.210_170_579_042_707_6, -0.581_938_842_807_736_5),963            (0.062_192_494_764_691_71, -0.051_732_573_832_334_72),964        ];965        for (j, (re, im)) in wanted.iter().enumerate() {966            let w0 = Complex::new(phi.log2(), ladder.period() * j as f64);967            let (value, bound) = ladder.residue(w0, PINNED).unwrap();968            assert!(bound < 1.3e-13, "j {j} bound {bound:e}");969            near(value, *re, *im, bound);970        }971    }972973    #[test]974    fn the_second_comb_is_genuine() {975        let ladder = golden();976        let phi = (1.0 + 5f64.sqrt()) / 2.0;977        let half = std::f64::consts::PI / 2f64.ln();978        let wanted = [979            (-0.259_501_222_742_937_13, -0.592_535_006_433_179_4),980            (0.896_350_590_641_920_6, 1.403_072_744_223_695_2),981            (0.491_379_388_883_392_94, -3.790_264_223_035_055_4),982        ];983        for (j, (re, im)) in wanted.iter().enumerate() {984            let w0 = Complex::new(-phi.log2(), half * (2 * j + 1) as f64);985            let (value, bound) = ladder.residue(w0, 1e-7).unwrap();986            assert!(value.abs() > 0.6, "j {j} residue {value:?}");987            near(value, *re, *im, bound);988            let (radius, nodes) = (0.05, 12);989            let mut got = Complex::new(0.0, 0.0);990            let mut carried = 0.0;991            for k in 0..nodes {992                let turn = Complex::turn(std::f64::consts::TAU * k as f64 / nodes as f64);993                let (point, other) = ladder.zeta(w0 + turn * radius, 1e-5).unwrap();994                got = got + turn * radius * point * (1.0 / nodes as f64);995                carried += radius * other / nodes as f64;996            }997            assert!(998                (got - value).abs() < bound + carried + 1e-6,999                "j {j} {got:?} {value:?}"1000            );1001        }1002    }10031004    #[test]1005    fn the_residue_at_the_abscissa_is_the_limit_of_the_digit_sums() {1006        let ladder = golden();1007        let alpha = ladder.abscissa();1008        let (value, bound) = ladder.residue(Complex::new(alpha, 0.0), PINNED).unwrap();1009        let wanted = [1010            (16usize, 0.946_747_043_404_283_4, 4e-6),1011            (20, 0.946_743_630_023_051_8, 3e-7),1012            (24, 0.946_743_410_426_742, 2e-8),1013        ];1014        for (length, want, reach) in wanted {1015            let (mut sum, mut drift) = (0.0f64, 0.0f64);1016            for n in (1u64 << (length - 1))..(1u64 << length) {1017                if n & (n << 1) == 0 {1018                    let term = (n as f64).powf(-alpha) - drift;1019                    let total = sum + term;1020                    drift = (total - sum) - term;1021                    sum = total;1022                }1023            }1024            let got = sum / 2f64.ln();1025            assert!((got - want).abs() < 1e-12, "L {length} {got}");1026            assert!(1027                (got - value.re).abs() < reach + bound,1028                "L {length} {got} {value:?}"1029            );1030        }1031    }10321033    #[test]1034    fn the_golden_readings_are_pinned() {1035        let ladder = golden();1036        let pinned = [1037            (Complex::new(3.0, 0.0), 1.154_012_963_277_640_1, 0.0),1038            (Complex::new(2.0, 0.0), 1.415_825_532_884_778_4, 0.0),1039            (1040                Complex::new(1.2, 9.0),1041                1.906_409_024_243_908_5,1042                -0.453_243_424_778_265_46,1043            ),1044            (Complex::new(0.8, 0.0), 9.536_379_694_275_015, 0.0),1045        ];1046        for (s, re, im) in pinned {1047            let (value, bound) = ladder.zeta(s, PINNED).unwrap();1048            near(value, re, im, bound);1049        }1050        let cofactors = [1051            (Complex::new(3.0, 0.0), 0.991_729_890_316_722),1052            (Complex::new(2.0, 0.0), 0.973_380_053_858_285_1),1053            (Complex::new(0.8, 0.0), 0.913_335_748_872_126_1),1054        ];1055        for (s, re) in cofactors {1056            let (value, bound) = ladder.cofactor(s, PINNED).unwrap();1057            near(value, re, 0.0, bound);1058        }1059    }10601061    #[test]1062    fn the_fibbinary_set_carries_no_euler_product() {1063        let fib = |n: u64| n & (n << 1) == 0;1064        let mut found = None;1065        'outer: for product in 2u64..1 << 16 {1066            for a in 2u64..=(product as f64).sqrt() as u64 + 1 {1067                if product % a != 0 {1068                    continue;1069                }1070                let b = product / a;1071                if a >= b || !fib(a) || !fib(b) {1072                    continue;1073                }1074                let (mut x, mut y) = (a, b);1075                while y != 0 {1076                    let t = x % y;1077                    x = y;1078                    y = t;1079                }1080                if x != 1 || fib(product) {1081                    continue;1082                }1083                found = Some((a, b, product));1084                break 'outer;1085            }1086        }1087        assert_eq!(found, Some((5, 9, 45)));1088    }10891090    #[test]1091    fn the_abscissa_is_the_exact_perron_root_and_not_the_bracket() {1092        let jordan = Automaton::new(&Rule::new(1, 2, 13).unwrap()).unwrap();1093        assert_eq!(jordan.matrix(), vec![vec![1.0, 1.0], vec![0.0, 1.0]]);1094        assert_eq!(jordan.abscissa(), 0.0);1095        let (_, high) = jordan.perron();1096        assert!(high > 1.03 && high < 1.04, "{high}");1097    }10981099    #[test]1100    fn the_ladder_refuses_a_level_it_cannot_invert() {1101        let ladder = golden();1102        let phi = (1.0 + 5f64.sqrt()) / 2.0;1103        let w0 = Complex::new(phi.log2(), 0.0);1104        assert!(ladder.zeta(w0, 1e-6).is_err());1105        assert!(ladder.residue(Complex::new(2.0, 0.0), 1e-6).is_err());1106    }1107}