Skip to main content

oxiphysics_core/
renormalization_group.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Renormalization group (RG) theory computations.
5//!
6//! This module implements the modern apparatus of the renormalization group:
7//!
8//! - **Beta functions**: one-loop and two-loop beta functions, UV/IR fixed points
9//! - **RG flow**: Callan-Symanzik equation, running coupling, anomalous dimensions
10//! - **Wilsonian RG**: effective field theory, block-spin / coarse-graining transformations
11//! - **Universality classes**: Ising, XY, Heisenberg, mean-field critical exponents
12//! - **Scaling relations**: hyperscaling, Rushbrooke, Josephson, Fisher identities
13//! - **Renormalization schemes**: MS-bar, momentum subtraction, on-shell
14//! - **Operator product expansion**: OPE coefficients, conformal blocks, fusion rules
15//! - **Critical phenomena**: correlation length, order parameter, susceptibility
16//! - **Decimation transformation**: 1-D Ising decimation, block-spin flow equations
17//! - **Conformal field theory**: central charge, scaling dimensions, partition function
18
19#![allow(dead_code)]
20
21use std::f64::consts::PI;
22
23// ============================================================================
24// Internal lightweight LCG (no external rand dependency required)
25// ============================================================================
26
27/// Lightweight linear congruential generator used internally.
28struct Lcg {
29    state: u64,
30}
31
32impl Lcg {
33    fn new(seed: u64) -> Self {
34        Self { state: seed.max(1) }
35    }
36
37    fn next_u64(&mut self) -> u64 {
38        self.state = self
39            .state
40            .wrapping_mul(6_364_136_223_846_793_005)
41            .wrapping_add(1_442_695_040_888_963_407);
42        self.state
43    }
44
45    fn next_f64(&mut self) -> f64 {
46        (self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
47    }
48}
49
50// ============================================================================
51// Constants
52// ============================================================================
53
54/// Euler-Mascheroni constant.
55pub const EULER_MASCHERONI: f64 = 0.577_215_664_901_532_9;
56
57/// 4π², appearing in loop integrals.
58pub const FOUR_PI_SQ: f64 = 4.0 * PI * PI;
59
60/// Default loop-expansion parameter ε = 4 – d.
61pub const EPSILON_WILSON: f64 = 1.0;
62
63/// Default UV cutoff scale Λ in natural units.
64pub const DEFAULT_UV_CUTOFF: f64 = 1.0e3;
65
66/// Convergence tolerance for fixed-point iteration.
67pub const FP_TOL: f64 = 1.0e-10;
68
69/// Maximum iterations for fixed-point Newton search.
70pub const FP_MAX_ITER: usize = 1_000;
71
72// ============================================================================
73// BetaFunction
74// ============================================================================
75
76/// Loop order for beta-function computation.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum LoopOrder {
79    /// One-loop (leading order) beta function.
80    OneLoop,
81    /// Two-loop (next-to-leading order) beta function.
82    TwoLoop,
83}
84
85/// Beta function for a single running coupling constant g.
86///
87/// The beta function β(g) = μ dg/dμ governs how the coupling constant
88/// evolves with the RG scale μ.  Coefficients b0, b1 follow the
89/// convention  β(g) = −b0 g² − b1 g³  (perturbative, Euclidean).
90#[derive(Debug, Clone)]
91pub struct BetaFunction {
92    /// One-loop coefficient b₀ (≥ 0 for asymptotic freedom).
93    pub b0: f64,
94    /// Two-loop coefficient b₁.
95    pub b1: f64,
96    /// Loop order used for evaluation.
97    pub order: LoopOrder,
98    /// Name tag for the theory (e.g. "QCD", "phi4").
99    pub name: String,
100}
101
102impl BetaFunction {
103    /// Construct a new beta function with given coefficients and loop order.
104    pub fn new(b0: f64, b1: f64, order: LoopOrder, name: impl Into<String>) -> Self {
105        Self {
106            b0,
107            b1,
108            order,
109            name: name.into(),
110        }
111    }
112
113    /// Evaluate β(g) at coupling `g`.
114    ///
115    /// One-loop:  β(g) = −b₀ g²
116    /// Two-loop:  β(g) = −b₀ g² − b₁ g³
117    pub fn evaluate(&self, g: f64) -> f64 {
118        let one_loop = -self.b0 * g * g;
119        match self.order {
120            LoopOrder::OneLoop => one_loop,
121            LoopOrder::TwoLoop => one_loop - self.b1 * g * g * g,
122        }
123    }
124
125    /// Derivative dβ/dg at coupling `g`.
126    pub fn derivative(&self, g: f64) -> f64 {
127        let one_loop = -2.0 * self.b0 * g;
128        match self.order {
129            LoopOrder::OneLoop => one_loop,
130            LoopOrder::TwoLoop => one_loop - 3.0 * self.b1 * g * g,
131        }
132    }
133
134    /// Find the non-trivial (Wilson-Fisher) fixed point g* where β(g*) = 0, g* ≠ 0.
135    ///
136    /// Returns `None` if no real fixed point exists in the perturbative regime.
137    pub fn fixed_point(&self) -> Option<f64> {
138        match self.order {
139            LoopOrder::OneLoop => {
140                // β = −b₀ g² = 0  only at g = 0 (trivial)
141                None
142            }
143            LoopOrder::TwoLoop => {
144                // β = −b₀ g² − b₁ g³ = −g²(b₀ + b₁ g) = 0
145                // non-trivial: g* = −b₀/b₁
146                if self.b1.abs() < f64::EPSILON {
147                    None
148                } else {
149                    let gstar = -self.b0 / self.b1;
150                    if gstar > 0.0 { Some(gstar) } else { None }
151                }
152            }
153        }
154    }
155
156    /// Stability exponent ω = dβ/dg|_{g=g*}.
157    ///
158    /// ω > 0: UV fixed point (IR stable); ω < 0: IR fixed point (UV stable).
159    pub fn stability_exponent(&self) -> Option<f64> {
160        self.fixed_point().map(|gstar| self.derivative(gstar))
161    }
162
163    /// Integrate the RG equation dg/dt = β(g) from t=0 to t=`t_final`
164    /// using simple Euler integration with `steps` steps.
165    pub fn run_coupling(&self, g0: f64, t_final: f64, steps: usize) -> f64 {
166        let dt = t_final / steps as f64;
167        let mut g = g0;
168        for _ in 0..steps {
169            g += self.evaluate(g) * dt;
170        }
171        g
172    }
173
174    /// One-loop running coupling: analytic solution g(t) = g₀ / (1 + b₀ g₀ t).
175    ///
176    /// Derived from dg/dt = −b₀ g², which integrates to 1/g(t) = 1/g₀ + b₀ t.
177    pub fn running_coupling_one_loop(&self, g0: f64, t: f64) -> f64 {
178        let denom = 1.0 + self.b0 * g0 * t;
179        if denom <= 0.0 {
180            f64::INFINITY
181        } else {
182            g0 / denom
183        }
184    }
185
186    /// Landau pole scale: t_L where g → ∞ under one-loop running.
187    pub fn landau_pole(&self, g0: f64) -> f64 {
188        if self.b0 <= 0.0 {
189            f64::INFINITY
190        } else {
191            1.0 / (2.0 * self.b0 * g0)
192        }
193    }
194}
195
196// ============================================================================
197// RgFlow
198// ============================================================================
199
200/// Result of integrating the Callan-Symanzik RG flow equations.
201#[derive(Debug, Clone)]
202pub struct RgFlowResult {
203    /// Scale parameter values t = ln(μ/μ₀).
204    pub t_values: Vec<f64>,
205    /// Running coupling g(t).
206    pub coupling: Vec<f64>,
207    /// Running mass m(t).
208    pub mass: Vec<f64>,
209    /// Anomalous dimension η(t).
210    pub eta: Vec<f64>,
211}
212
213/// Callan-Symanzik RG flow for coupling and mass.
214///
215/// Implements μ dg/dμ = β(g),  μ dm/dμ = −γ_m(g) m,
216/// where γ_m is the mass anomalous dimension.
217#[derive(Debug, Clone)]
218pub struct RgFlow {
219    /// Beta function governing the coupling flow.
220    pub beta: BetaFunction,
221    /// One-loop mass anomalous dimension coefficient γ₀ (γ_m = γ₀ g²).
222    pub gamma0: f64,
223    /// Field anomalous dimension coefficient η₀ (η = η₀ g²).
224    pub eta0: f64,
225}
226
227impl RgFlow {
228    /// Construct a new RG flow with given beta function and anomalous dimension coefficients.
229    pub fn new(beta: BetaFunction, gamma0: f64, eta0: f64) -> Self {
230        Self { beta, gamma0, eta0 }
231    }
232
233    /// Anomalous dimension of the mass at coupling `g`: γ_m(g) = γ₀ g².
234    pub fn gamma_mass(&self, g: f64) -> f64 {
235        self.gamma0 * g * g
236    }
237
238    /// Field anomalous dimension η(g) = η₀ g².
239    pub fn eta(&self, g: f64) -> f64 {
240        self.eta0 * g * g
241    }
242
243    /// Integrate the full RG flow from t=0 to t=`t_max` with `n` steps.
244    ///
245    /// Returns a [`RgFlowResult`] containing the full trajectory.
246    pub fn integrate(&self, g0: f64, m0: f64, t_max: f64, n: usize) -> RgFlowResult {
247        let dt = t_max / n as f64;
248        let mut t_values = Vec::with_capacity(n + 1);
249        let mut coupling = Vec::with_capacity(n + 1);
250        let mut mass = Vec::with_capacity(n + 1);
251        let mut eta = Vec::with_capacity(n + 1);
252
253        let mut g = g0;
254        let mut m = m0;
255        let mut t = 0.0;
256
257        t_values.push(t);
258        coupling.push(g);
259        mass.push(m);
260        eta.push(self.eta(g));
261
262        for _ in 0..n {
263            let dg = self.beta.evaluate(g) * dt;
264            let dm = -self.gamma_mass(g) * m * dt;
265            g += dg;
266            m += dm;
267            t += dt;
268            t_values.push(t);
269            coupling.push(g);
270            mass.push(m);
271            eta.push(self.eta(g));
272        }
273
274        RgFlowResult {
275            t_values,
276            coupling,
277            mass,
278            eta,
279        }
280    }
281
282    /// Correlation length exponent ν from the eigenvalue of the linearized
283    /// beta function at the fixed point: ν = −1/ω.
284    pub fn nu_exponent(&self) -> Option<f64> {
285        self.beta.stability_exponent().and_then(|omega| {
286            if omega.abs() < f64::EPSILON {
287                None
288            } else {
289                Some(1.0 / omega.abs())
290            }
291        })
292    }
293}
294
295// ============================================================================
296// WilsonianRg
297// ============================================================================
298
299/// Parameters for a Wilsonian effective field theory in d dimensions.
300///
301/// Implements coarse-graining via momentum-shell integration and block-spin
302/// transformation, yielding flow equations for marginal and relevant couplings.
303#[derive(Debug, Clone)]
304pub struct WilsonianRg {
305    /// Spacetime dimension d.
306    pub d: f64,
307    /// ε = 4 − d (Wilson-Fisher expansion parameter).
308    pub epsilon: f64,
309    /// UV cutoff Λ.
310    pub lambda: f64,
311    /// Current RG step number (number of coarse-graining steps performed).
312    pub step: usize,
313    /// Coupling u (quartic in φ⁴ theory).
314    pub u: f64,
315    /// Mass parameter r (quadratic coupling).
316    pub r: f64,
317}
318
319impl WilsonianRg {
320    /// Construct a Wilsonian RG for φ⁴ theory in d = 4 − ε dimensions.
321    pub fn new(d: f64, lambda: f64, u: f64, r: f64) -> Self {
322        Self {
323            d,
324            epsilon: 4.0 - d,
325            lambda,
326            step: 0,
327            u,
328            r,
329        }
330    }
331
332    /// Perform one infinitesimal RG step with rescaling factor b = e^{dl}.
333    ///
334    /// One-loop flow equations (ε-expansion):
335    ///   du/dl = ε u − (n+8)/(16π²) u²
336    ///   dr/dl = 2 r + (n+2)/(16π²) u  (n = 1 for Ising universality)
337    #[allow(clippy::too_many_arguments)]
338    pub fn step_flow(&mut self, dl: f64, n_components: f64) {
339        let du = self.epsilon * self.u - (n_components + 8.0) / (16.0 * PI * PI) * self.u * self.u;
340        let dr = 2.0 * self.r + (n_components + 2.0) / (16.0 * PI * PI) * self.u;
341        self.u += du * dl;
342        self.r += dr * dl;
343        self.step += 1;
344    }
345
346    /// Wilson-Fisher fixed point u* for φ⁴ theory (n-component).
347    ///
348    /// u* = (16π² ε) / (n + 8)
349    pub fn wilson_fisher_fixed_point(&self, n_components: f64) -> f64 {
350        16.0 * PI * PI * self.epsilon / (n_components + 8.0)
351    }
352
353    /// Critical exponent η at Wilson-Fisher fixed point to O(ε²).
354    ///
355    /// η = (n+2) ε² / \[2(n+8)²\]
356    pub fn eta_wf(&self, n_components: f64) -> f64 {
357        let eps = self.epsilon;
358        (n_components + 2.0) * eps * eps / (2.0 * (n_components + 8.0).powi(2))
359    }
360
361    /// Correlation length exponent ν at Wilson-Fisher fixed point.
362    ///
363    /// 1/ν = 2 − (n+2)/(n+8) ε
364    pub fn nu_wf(&self, n_components: f64) -> f64 {
365        let inv_nu = 2.0 - (n_components + 2.0) / (n_components + 8.0) * self.epsilon;
366        1.0 / inv_nu
367    }
368
369    /// Block-spin transformation: rescale correlation length by factor b.
370    ///
371    /// Under coarse-graining by b, the correlation length scales as ξ → ξ/b.
372    pub fn rescale_correlation_length(&self, xi: f64, b: f64) -> f64 {
373        xi / b
374    }
375
376    /// Run the RG flow for `n_steps` steps with step size `dl`.
377    pub fn run(&mut self, n_steps: usize, dl: f64, n_components: f64) {
378        for _ in 0..n_steps {
379            self.step_flow(dl, n_components);
380        }
381    }
382}
383
384// ============================================================================
385// UniversalityClass
386// ============================================================================
387
388/// Universality class of a critical point.
389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
390pub enum UniversalityClass {
391    /// 2-D / 3-D Ising universality (n = 1 scalar order parameter).
392    Ising2D,
393    /// 3-D Ising universality.
394    Ising3D,
395    /// 3-D XY universality (n = 2 complex order parameter).
396    Xy3D,
397    /// 3-D Heisenberg universality (n = 3 vector order parameter).
398    Heisenberg3D,
399    /// Mean-field (Landau) theory critical exponents.
400    MeanField,
401}
402
403/// Set of universal critical exponents for a given universality class.
404#[derive(Debug, Clone)]
405pub struct CriticalExponents {
406    /// Correlation length exponent ν.
407    pub nu: f64,
408    /// Anomalous dimension η.
409    pub eta: f64,
410    /// Order parameter exponent β (not to be confused with beta function).
411    pub beta_exp: f64,
412    /// Susceptibility exponent γ.
413    pub gamma: f64,
414    /// Specific heat exponent α.
415    pub alpha: f64,
416    /// Critical isotherm exponent δ.
417    pub delta: f64,
418    /// Correlation function exponent at T_c: G(r) ~ r^{-(d−2+η)}.
419    pub d: f64,
420}
421
422impl UniversalityClass {
423    /// Return the standard critical exponents for this universality class.
424    ///
425    /// Values are state-of-the-art best estimates (Monte Carlo / conformal bootstrap).
426    pub fn exponents(self) -> CriticalExponents {
427        match self {
428            UniversalityClass::Ising2D => CriticalExponents {
429                nu: 1.0,
430                eta: 0.25,
431                beta_exp: 0.125,
432                gamma: 1.75,
433                alpha: 0.0, // logarithmic
434                delta: 15.0,
435                d: 2.0,
436            },
437            UniversalityClass::Ising3D => CriticalExponents {
438                nu: 0.6301,
439                eta: 0.0364,
440                beta_exp: 0.3265,
441                gamma: 1.2372,
442                alpha: 0.1100,
443                delta: 4.789,
444                d: 3.0,
445            },
446            UniversalityClass::Xy3D => CriticalExponents {
447                nu: 0.6717,
448                eta: 0.0381,
449                beta_exp: 0.3486,
450                gamma: 1.3178,
451                alpha: -0.0151,
452                delta: 4.780,
453                d: 3.0,
454            },
455            UniversalityClass::Heisenberg3D => CriticalExponents {
456                nu: 0.7112,
457                eta: 0.0375,
458                beta_exp: 0.3689,
459                gamma: 1.3960,
460                alpha: -0.1336,
461                delta: 4.783,
462                d: 3.0,
463            },
464            UniversalityClass::MeanField => CriticalExponents {
465                nu: 0.5,
466                eta: 0.0,
467                beta_exp: 0.5,
468                gamma: 1.0,
469                alpha: 0.0,
470                delta: 3.0,
471                d: 4.0, // upper critical dimension
472            },
473        }
474    }
475
476    /// Number of order parameter components n.
477    pub fn n_components(self) -> usize {
478        match self {
479            UniversalityClass::Ising2D | UniversalityClass::Ising3D => 1,
480            UniversalityClass::Xy3D => 2,
481            UniversalityClass::Heisenberg3D => 3,
482            UniversalityClass::MeanField => 1,
483        }
484    }
485
486    /// Spatial dimension d.
487    pub fn dimension(self) -> f64 {
488        match self {
489            UniversalityClass::Ising2D => 2.0,
490            _ => 3.0,
491        }
492    }
493}
494
495// ============================================================================
496// ScalingRelations
497// ============================================================================
498
499/// Verification of thermodynamic scaling relations from critical exponents.
500///
501/// The four classical relations among {α, β, γ, δ, ν, η, d}:
502/// - Rushbrooke: α + 2β + γ = 2
503/// - Widom:      γ = β(δ − 1)
504/// - Fisher:     γ = (2 − η) ν
505/// - Josephson:  2 − α = d ν  (hyperscaling)
506#[derive(Debug, Clone)]
507pub struct ScalingRelations {
508    /// Critical exponents to check.
509    pub exp: CriticalExponents,
510}
511
512impl ScalingRelations {
513    /// Construct from a set of critical exponents.
514    pub fn new(exp: CriticalExponents) -> Self {
515        Self { exp }
516    }
517
518    /// Rushbrooke identity: α + 2β + γ = 2.  Returns (lhs, rhs=2).
519    pub fn rushbrooke(&self) -> (f64, f64) {
520        let lhs = self.exp.alpha + 2.0 * self.exp.beta_exp + self.exp.gamma;
521        (lhs, 2.0)
522    }
523
524    /// Widom identity: γ = β(δ − 1).  Returns (lhs=γ, rhs=β(δ−1)).
525    pub fn widom(&self) -> (f64, f64) {
526        let lhs = self.exp.gamma;
527        let rhs = self.exp.beta_exp * (self.exp.delta - 1.0);
528        (lhs, rhs)
529    }
530
531    /// Fisher identity: γ = (2 − η) ν.  Returns (lhs=γ, rhs).
532    pub fn fisher(&self) -> (f64, f64) {
533        let lhs = self.exp.gamma;
534        let rhs = (2.0 - self.exp.eta) * self.exp.nu;
535        (lhs, rhs)
536    }
537
538    /// Josephson (hyperscaling) identity: 2 − α = d ν.  Returns (lhs, rhs).
539    pub fn josephson(&self) -> (f64, f64) {
540        let lhs = 2.0 - self.exp.alpha;
541        let rhs = self.exp.d * self.exp.nu;
542        (lhs, rhs)
543    }
544
545    /// Check all four relations within tolerance `tol`.
546    ///
547    /// Returns `(rushbrooke_ok, widom_ok, fisher_ok, josephson_ok)`.
548    pub fn check_all(&self, tol: f64) -> (bool, bool, bool, bool) {
549        let (r_lhs, r_rhs) = self.rushbrooke();
550        let (w_lhs, w_rhs) = self.widom();
551        let (f_lhs, f_rhs) = self.fisher();
552        let (j_lhs, j_rhs) = self.josephson();
553        (
554            (r_lhs - r_rhs).abs() < tol,
555            (w_lhs - w_rhs).abs() < tol,
556            (f_lhs - f_rhs).abs() < tol,
557            (j_lhs - j_rhs).abs() < tol,
558        )
559    }
560}
561
562// ============================================================================
563// RenormalizationScheme
564// ============================================================================
565
566/// Renormalization scheme for field theory calculations.
567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
568pub enum RenormalizationScheme {
569    /// MS-bar (minimal subtraction in dimensional regularisation).
570    MSbar,
571    /// Momentum subtraction (MOM): renormalize at spacelike momentum p² = μ².
572    MomentumSubtraction,
573    /// On-shell renormalization: use physical pole mass and coupling.
574    OnShell,
575}
576
577/// Scheme-dependent renormalized coupling and counterterms.
578#[derive(Debug, Clone)]
579pub struct RenormalizedCoupling {
580    /// Renormalization scheme.
581    pub scheme: RenormalizationScheme,
582    /// Bare coupling g₀.
583    pub g_bare: f64,
584    /// Renormalized coupling g(μ).
585    pub g_renorm: f64,
586    /// Renormalization scale μ.
587    pub mu: f64,
588    /// UV divergence degree (dimensionless; 0 = finite, n = power/log divergence).
589    pub divergence_degree: i32,
590}
591
592impl RenormalizedCoupling {
593    /// Construct a renormalized coupling in the given scheme.
594    pub fn new(
595        scheme: RenormalizationScheme,
596        g_bare: f64,
597        mu: f64,
598        divergence_degree: i32,
599    ) -> Self {
600        let g_renorm = match scheme {
601            RenormalizationScheme::MSbar => {
602                // MS-bar: subtract only 1/ε pole + ln(4π) − γ_E
603                let log_factor = (4.0 * PI).ln() - EULER_MASCHERONI;
604                g_bare - g_bare * g_bare * log_factor / (16.0 * PI * PI)
605            }
606            RenormalizationScheme::MomentumSubtraction => {
607                // MOM: subtract at p² = μ²; finite parts differ from MS-bar by a constant
608                g_bare - g_bare * g_bare * mu.ln() / (16.0 * PI * PI)
609            }
610            RenormalizationScheme::OnShell => {
611                // On-shell: coupling fixed by physical scattering amplitude
612                g_bare * (1.0 - g_bare / (16.0 * PI * PI))
613            }
614        };
615        Self {
616            scheme,
617            g_bare,
618            g_renorm,
619            mu,
620            divergence_degree,
621        }
622    }
623
624    /// Scheme change: convert g(μ) from MS-bar to MOM (one-loop).
625    ///
626    /// g_MOM = g_MSbar \[1 + Δ g_MSbar / (16π²)\] where Δ is scheme-difference coefficient.
627    pub fn convert_msbar_to_mom(&self, delta: f64) -> f64 {
628        self.g_renorm * (1.0 + delta * self.g_renorm / (16.0 * PI * PI))
629    }
630
631    /// Running coupling between two scales μ₁ and μ₂ (one-loop MS-bar).
632    pub fn run_msbar(&self, mu2: f64, b0: f64) -> f64 {
633        let t = (mu2 / self.mu).ln();
634        self.g_renorm / (1.0 + 2.0 * b0 * self.g_renorm * t).sqrt()
635    }
636}
637
638// ============================================================================
639// OperatorProductExpansion
640// ============================================================================
641
642/// OPE coefficient C_{ijk} for operators O_i × O_j → O_k.
643#[derive(Debug, Clone)]
644pub struct OpeCoefficient {
645    /// Label of operator O_i.
646    pub label_i: String,
647    /// Label of operator O_j.
648    pub label_j: String,
649    /// Label of operator O_k.
650    pub label_k: String,
651    /// Numerical value of the OPE coefficient C_{ijk}.
652    pub value: f64,
653}
654
655impl OpeCoefficient {
656    /// Construct an OPE coefficient.
657    pub fn new(
658        label_i: impl Into<String>,
659        label_j: impl Into<String>,
660        label_k: impl Into<String>,
661        value: f64,
662    ) -> Self {
663        Self {
664            label_i: label_i.into(),
665            label_j: label_j.into(),
666            label_k: label_k.into(),
667            value,
668        }
669    }
670}
671
672/// Operator product expansion registry.
673///
674/// Stores a list of OPE coefficients and provides utilities for computing
675/// conformal partial waves and checking crossing symmetry.
676#[derive(Debug, Clone, Default)]
677pub struct OperatorProductExpansion {
678    /// List of OPE coefficients.
679    pub coefficients: Vec<OpeCoefficient>,
680    /// List of operator scaling dimensions (Δ_k).
681    pub dimensions: Vec<(String, f64)>,
682}
683
684impl OperatorProductExpansion {
685    /// Construct an empty OPE registry.
686    pub fn new() -> Self {
687        Self::default()
688    }
689
690    /// Register an OPE coefficient.
691    pub fn add_coefficient(&mut self, coeff: OpeCoefficient) {
692        self.coefficients.push(coeff);
693    }
694
695    /// Register an operator with scaling dimension Δ.
696    pub fn add_operator(&mut self, label: impl Into<String>, delta: f64) {
697        self.dimensions.push((label.into(), delta));
698    }
699
700    /// Conformal block G_{Δ,l}(z, z̄) in 2D at leading order (simplified scalar, l=0).
701    ///
702    /// G_Δ(z) ≈ z^{Δ/2} (1 + Δ z / (2(2Δ+1)) + …)
703    pub fn conformal_block_2d(&self, delta: f64, z: f64) -> f64 {
704        if z <= 0.0 {
705            return 0.0;
706        }
707        let z_pow = z.powf(delta / 2.0);
708        let correction = 1.0 + delta * z / (2.0 * (2.0 * delta + 1.0));
709        z_pow * correction
710    }
711
712    /// Partial wave amplitude: sum of C²_{σσk} × G_{Δk}(z).
713    pub fn partial_wave_amplitude(&self, sigma_label: &str, z: f64) -> f64 {
714        let mut amplitude = 0.0;
715        for coeff in &self.coefficients {
716            if coeff.label_i == sigma_label
717                && coeff.label_j == sigma_label
718                && let Some((_, delta_k)) = self
719                    .dimensions
720                    .iter()
721                    .find(|(lbl, _)| lbl == &coeff.label_k)
722            {
723                let g = self.conformal_block_2d(*delta_k, z);
724                amplitude += coeff.value * coeff.value * g;
725            }
726        }
727        amplitude
728    }
729
730    /// Fusion rule check: does O_i × O_j contain O_k?
731    pub fn fusion_rule_exists(&self, label_i: &str, label_j: &str, label_k: &str) -> bool {
732        self.coefficients
733            .iter()
734            .any(|c| c.label_i == label_i && c.label_j == label_j && c.label_k == label_k)
735    }
736}
737
738// ============================================================================
739// CriticalPhenomena
740// ============================================================================
741
742/// Critical phenomena observables near a second-order phase transition.
743///
744/// All observables are expressed as power-law functions of the reduced
745/// temperature t = (T − T_c)/T_c and ordering field h.
746#[derive(Debug, Clone)]
747pub struct CriticalPhenomena {
748    /// Critical exponents.
749    pub exp: CriticalExponents,
750    /// Critical temperature T_c.
751    pub t_c: f64,
752    /// Non-universal amplitude for correlation length: ξ = ξ₀ |t|^{−ν}.
753    pub xi0: f64,
754    /// Non-universal amplitude for susceptibility: χ = χ₀ |t|^{−γ}.
755    pub chi0: f64,
756    /// Non-universal amplitude for order parameter: m = m₀ |t|^β.
757    pub m0: f64,
758    /// Non-universal amplitude for specific heat: C = A |t|^{−α}.
759    pub c0: f64,
760}
761
762impl CriticalPhenomena {
763    /// Construct a CriticalPhenomena model.
764    #[allow(clippy::too_many_arguments)]
765    pub fn new(exp: CriticalExponents, t_c: f64, xi0: f64, chi0: f64, m0: f64, c0: f64) -> Self {
766        Self {
767            exp,
768            t_c,
769            xi0,
770            chi0,
771            m0,
772            c0,
773        }
774    }
775
776    /// Reduced temperature t = (T − T_c) / T_c.
777    pub fn reduced_temperature(&self, temp: f64) -> f64 {
778        (temp - self.t_c) / self.t_c
779    }
780
781    /// Correlation length ξ(T) = ξ₀ |t|^{−ν}  (T ≠ T_c).
782    pub fn correlation_length(&self, temp: f64) -> f64 {
783        let t = self.reduced_temperature(temp);
784        if t.abs() < f64::EPSILON {
785            f64::INFINITY
786        } else {
787            self.xi0 * t.abs().powf(-self.exp.nu)
788        }
789    }
790
791    /// Magnetic susceptibility χ(T) = χ₀ |t|^{−γ}  (T > T_c).
792    pub fn susceptibility(&self, temp: f64) -> f64 {
793        let t = self.reduced_temperature(temp);
794        if t.abs() < f64::EPSILON {
795            f64::INFINITY
796        } else {
797            self.chi0 * t.abs().powf(-self.exp.gamma)
798        }
799    }
800
801    /// Order parameter m(T) = m₀ (−t)^β  (T < T_c).
802    pub fn order_parameter(&self, temp: f64) -> f64 {
803        let t = self.reduced_temperature(temp);
804        if t >= 0.0 {
805            0.0
806        } else {
807            self.m0 * (-t).powf(self.exp.beta_exp)
808        }
809    }
810
811    /// Specific heat C(T) = c₀ |t|^{−α}.
812    pub fn specific_heat(&self, temp: f64) -> f64 {
813        let t = self.reduced_temperature(temp);
814        if t.abs() < f64::EPSILON {
815            f64::INFINITY
816        } else {
817            self.c0 * t.abs().powf(-self.exp.alpha)
818        }
819    }
820
821    /// Critical isotherm (T = T_c): m ∝ h^{1/δ}.
822    pub fn critical_isotherm(&self, h: f64) -> f64 {
823        h.abs().powf(1.0 / self.exp.delta) * h.signum()
824    }
825}
826
827// ============================================================================
828// DecimationTransformation
829// ============================================================================
830
831/// 1-D Ising decimation RG transformation.
832///
833/// The partition function of the 1-D Ising model with coupling K is
834/// reproduced by a system with half as many spins and an effective coupling K'.
835/// The recursion relation is:
836///   K' = (1/2) ln cosh(2K)
837#[derive(Debug, Clone)]
838pub struct DecimationTransformation {
839    /// Current coupling K = J/(k_B T).
840    pub k: f64,
841    /// Current additive constant (free energy per site contribution).
842    pub ln_lambda: f64,
843    /// Number of decimation steps performed.
844    pub steps: usize,
845}
846
847impl DecimationTransformation {
848    /// Construct a decimation RG at coupling K₀.
849    pub fn new(k0: f64) -> Self {
850        Self {
851            k: k0,
852            ln_lambda: 0.0,
853            steps: 0,
854        }
855    }
856
857    /// Perform one step of 1-D Ising decimation.
858    ///
859    /// K' = (1/2) ln cosh(2K),   ln λ += (1/2) ln\[4 cosh(2K)\]
860    pub fn step(&mut self) {
861        let two_k = 2.0 * self.k;
862        let cosh_2k = two_k.cosh();
863        self.k = 0.5 * cosh_2k.ln();
864        self.ln_lambda += 0.5 * (4.0 * cosh_2k).ln();
865        self.steps += 1;
866    }
867
868    /// Run `n` decimation steps.
869    pub fn run(&mut self, n: usize) {
870        for _ in 0..n {
871            self.step();
872        }
873    }
874
875    /// Correlation length after decimation (rough estimate via K).
876    ///
877    /// ξ = −1 / ln tanh(K)
878    pub fn correlation_length(&self) -> f64 {
879        let th = self.k.tanh();
880        if th <= 0.0 || th >= 1.0 {
881            f64::INFINITY
882        } else {
883            -1.0 / th.ln()
884        }
885    }
886
887    /// Collect K trajectory over `n` steps.
888    pub fn trajectory(&self, n: usize) -> Vec<f64> {
889        let mut traj = Vec::with_capacity(n + 1);
890        let mut dec = self.clone();
891        traj.push(dec.k);
892        for _ in 0..n {
893            dec.step();
894            traj.push(dec.k);
895        }
896        traj
897    }
898
899    /// 2-D block-spin decimation flow for the square Ising model (Migdal-Kadanoff).
900    ///
901    /// K' = (1/2) ln cosh(2bK) where b = 2 is the block factor.
902    pub fn migdal_kadanoff_step(&mut self, b: u32) {
903        let bk = b as f64 * self.k;
904        self.k = 0.5 * (2.0 * bk).cosh().ln();
905        self.steps += 1;
906    }
907}
908
909// ============================================================================
910// ConformalFieldTheory
911// ============================================================================
912
913/// Conformal field theory (CFT) data for a 2-D critical theory.
914///
915/// Contains central charge c, operator content (scaling dimensions and spins),
916/// and utilities for computing the partition function and modular invariants.
917#[derive(Debug, Clone)]
918pub struct ConformalFieldTheory {
919    /// Central charge c.
920    pub c: f64,
921    /// List of (scaling dimension Δ, spin s) for primary operators.
922    pub primaries: Vec<(f64, i32)>,
923    /// Theory name.
924    pub name: String,
925}
926
927impl ConformalFieldTheory {
928    /// Construct a CFT with central charge c and no primaries yet.
929    pub fn new(c: f64, name: impl Into<String>) -> Self {
930        Self {
931            c,
932            primaries: Vec::new(),
933            name: name.into(),
934        }
935    }
936
937    /// Add a primary operator with scaling dimension Δ and spin s.
938    pub fn add_primary(&mut self, delta: f64, spin: i32) {
939        self.primaries.push((delta, spin));
940    }
941
942    /// Casimir energy on a cylinder of circumference L: E_0 = −π c / (6 L).
943    pub fn casimir_energy(&self, l: f64) -> f64 {
944        -PI * self.c / (6.0 * l)
945    }
946
947    /// Conformal dimensions h = (Δ + s)/2, h̄ = (Δ − s)/2.
948    pub fn holomorphic_dimensions(&self) -> Vec<(f64, f64)> {
949        self.primaries
950            .iter()
951            .map(|&(delta, spin)| {
952                let h = (delta + spin as f64) / 2.0;
953                let hbar = (delta - spin as f64) / 2.0;
954                (h, hbar)
955            })
956            .collect()
957    }
958
959    /// Virasoro character χ_h(τ) in the high-temperature limit (simplified).
960    ///
961    /// χ_h(q) ≈ q^{h − c/24} / η(q),  where η(q) ≈ q^{1/24} ∏(1−q^n).
962    /// Here we use a truncated expansion up to q^N_terms.
963    pub fn virasoro_character(&self, h: f64, q: f64, n_terms: usize) -> f64 {
964        // Dedekind eta function (truncated): η(q) = q^{1/24} ∏_{n=1}^N (1−q^n)
965        let mut eta_prod = 1.0_f64;
966        for n in 1..=n_terms {
967            eta_prod *= 1.0 - q.powi(n as i32);
968        }
969        let eta = q.powf(1.0 / 24.0) * eta_prod;
970        if eta.abs() < f64::EPSILON {
971            return 0.0;
972        }
973        q.powf(h - self.c / 24.0) / eta
974    }
975
976    /// Modular S-matrix element S_{h h'} for a compact boson CFT (c = 1).
977    ///
978    /// S_{nm} = √(2/k) sin(π n m / k) for level-k SU(2) WZW model.
979    pub fn s_matrix_wzw(&self, n: usize, m: usize, k: usize) -> f64 {
980        let kf = k as f64;
981        (2.0 / kf).sqrt() * (PI * n as f64 * m as f64 / kf).sin()
982    }
983
984    /// Partition function on a torus: Z(τ) = Tr\[q^{L_0 − c/24} q̄^{L̄_0 − c/24}\].
985    ///
986    /// Simplified: sum over primaries of |χ_h(q)|².
987    pub fn partition_function(&self, q: f64, n_terms: usize) -> f64 {
988        self.holomorphic_dimensions()
989            .iter()
990            .map(|&(h, hbar)| {
991                let chi_h = self.virasoro_character(h, q, n_terms);
992                let chi_hbar = self.virasoro_character(hbar, q, n_terms);
993                chi_h * chi_hbar
994            })
995            .sum()
996    }
997
998    /// Central charge from the two-point function of the stress tensor T(z):
999    ///
1000    /// ⟨T(z)T(0)⟩ = c/(2z⁴),  extract c = 2 z⁴ ⟨T(z)T(0)⟩.
1001    pub fn central_charge_from_tt(&self, z: f64, tt_correlator: f64) -> f64 {
1002        2.0 * z.powi(4) * tt_correlator
1003    }
1004
1005    /// Unitary bound: all primary operators must satisfy Δ ≥ |s| (Δ ≥ 0 for scalars).
1006    pub fn satisfies_unitarity_bound(&self) -> bool {
1007        self.primaries
1008            .iter()
1009            .all(|&(delta, spin)| delta >= spin.unsigned_abs() as f64)
1010    }
1011}
1012
1013// ============================================================================
1014// Tests
1015// ============================================================================
1016
1017#[cfg(test)]
1018mod tests {
1019    use super::*;
1020
1021    // ── BetaFunction tests ──────────────────────────────────────────────────
1022
1023    #[test]
1024    fn test_beta_one_loop_zero_at_origin() {
1025        let beta = BetaFunction::new(1.0, 0.0, LoopOrder::OneLoop, "test");
1026        assert!((beta.evaluate(0.0)).abs() < 1e-15);
1027    }
1028
1029    #[test]
1030    fn test_beta_one_loop_negative_for_positive_coupling() {
1031        let beta = BetaFunction::new(1.0, 0.0, LoopOrder::OneLoop, "QCD");
1032        // b0 > 0 → asymptotically free → β(g) < 0 for g > 0
1033        assert!(beta.evaluate(0.5) < 0.0);
1034    }
1035
1036    #[test]
1037    fn test_beta_two_loop_fixed_point() {
1038        // β = −b0 g² − b1 g³ = 0 → g* = −b0/b1
1039        // Choose b0 = 1, b1 = −2 → g* = 0.5
1040        let beta = BetaFunction::new(1.0, -2.0, LoopOrder::TwoLoop, "WF");
1041        let gstar = beta.fixed_point().expect("expected fixed point");
1042        assert!((gstar - 0.5).abs() < 1e-12);
1043    }
1044
1045    #[test]
1046    fn test_beta_stability_exponent_sign() {
1047        let beta = BetaFunction::new(1.0, -2.0, LoopOrder::TwoLoop, "WF");
1048        let omega = beta.stability_exponent().expect("stability exponent");
1049        // dβ/dg at g* = −2b0 g* − 3b1 g*²
1050        let gstar = 0.5_f64;
1051        let expected = -2.0 * 1.0 * gstar - 3.0 * (-2.0) * gstar * gstar;
1052        assert!((omega - expected).abs() < 1e-10);
1053    }
1054
1055    #[test]
1056    fn test_landau_pole() {
1057        let beta = BetaFunction::new(0.5, 0.0, LoopOrder::OneLoop, "phi4");
1058        // t_L = 1/(2 b0 g0)
1059        let t_l = beta.landau_pole(1.0);
1060        assert!((t_l - 1.0).abs() < 1e-12);
1061    }
1062
1063    #[test]
1064    fn test_running_coupling_one_loop_approaches_zero() {
1065        let beta = BetaFunction::new(1.0, 0.0, LoopOrder::OneLoop, "QCD");
1066        let g_inf = beta.running_coupling_one_loop(1.0, 1e6);
1067        assert!(g_inf < 0.01, "coupling should run to zero");
1068    }
1069
1070    #[test]
1071    fn test_run_coupling_euler_consistent() {
1072        let beta = BetaFunction::new(1.0, 0.0, LoopOrder::OneLoop, "test");
1073        let g_euler = beta.run_coupling(0.5, 1.0, 10_000);
1074        let g_analytic = beta.running_coupling_one_loop(0.5, 1.0);
1075        assert!((g_euler - g_analytic).abs() < 1e-4);
1076    }
1077
1078    // ── RgFlow tests ────────────────────────────────────────────────────────
1079
1080    #[test]
1081    fn test_rg_flow_gamma_mass_quadratic() {
1082        let beta = BetaFunction::new(1.0, 0.0, LoopOrder::OneLoop, "phi4");
1083        let flow = RgFlow::new(beta, 0.5, 0.1);
1084        // γ_m(g) = γ₀ g²
1085        assert!((flow.gamma_mass(2.0) - 0.5 * 4.0).abs() < 1e-12);
1086    }
1087
1088    #[test]
1089    fn test_rg_flow_integrate_mass_decreases() {
1090        let beta = BetaFunction::new(0.1, 0.0, LoopOrder::OneLoop, "test");
1091        let flow = RgFlow::new(beta, 0.2, 0.0);
1092        let result = flow.integrate(0.5, 1.0, 5.0, 1000);
1093        // mass should change (even if slightly) — check it's finite
1094        assert!(result.mass.last().unwrap().is_finite());
1095    }
1096
1097    #[test]
1098    fn test_rg_flow_nu_exponent() {
1099        // Use b1 < 0 so there IS a fixed point
1100        let beta = BetaFunction::new(1.0, -2.0, LoopOrder::TwoLoop, "WF");
1101        let flow = RgFlow::new(beta, 1.0, 0.1);
1102        let nu = flow.nu_exponent().expect("nu exponent");
1103        assert!(nu > 0.0);
1104    }
1105
1106    // ── WilsonianRg tests ───────────────────────────────────────────────────
1107
1108    #[test]
1109    fn test_wilson_fisher_fixed_point_n1() {
1110        // For n = 1 (Ising), u* = 16π² ε / 9
1111        let rg = WilsonianRg::new(3.0, 1.0, 0.1, 0.0);
1112        let ustar = rg.wilson_fisher_fixed_point(1.0);
1113        let expected = 16.0 * PI * PI * 1.0 / 9.0;
1114        assert!((ustar - expected).abs() < 1e-10);
1115    }
1116
1117    #[test]
1118    fn test_eta_wf_n1_order_eps2() {
1119        let rg = WilsonianRg::new(3.0, 1.0, 0.1, 0.0);
1120        let eta = rg.eta_wf(1.0);
1121        // η = ε² / 54 for n=1
1122        let expected = 1.0_f64.powi(2) / 54.0;
1123        assert!((eta - expected).abs() < 1e-10);
1124    }
1125
1126    #[test]
1127    fn test_nu_wf_n1() {
1128        // 1/ν = 2 − ε/3 for n=1 at ε=1 → 1/ν = 5/3 → ν = 0.6
1129        let rg = WilsonianRg::new(3.0, 1.0, 0.1, 0.0);
1130        let nu = rg.nu_wf(1.0);
1131        let expected = 1.0 / (2.0 - 1.0 / 3.0);
1132        assert!((nu - expected).abs() < 1e-10);
1133    }
1134
1135    #[test]
1136    fn test_block_spin_rescaling() {
1137        let rg = WilsonianRg::new(3.0, 1.0, 0.1, 0.0);
1138        assert!((rg.rescale_correlation_length(10.0, 2.0) - 5.0).abs() < 1e-12);
1139    }
1140
1141    // ── UniversalityClass tests ─────────────────────────────────────────────
1142
1143    #[test]
1144    fn test_mean_field_exponents() {
1145        let exp = UniversalityClass::MeanField.exponents();
1146        assert!((exp.nu - 0.5).abs() < 1e-10);
1147        assert!((exp.beta_exp - 0.5).abs() < 1e-10);
1148        assert!((exp.gamma - 1.0).abs() < 1e-10);
1149        assert!((exp.delta - 3.0).abs() < 1e-10);
1150    }
1151
1152    #[test]
1153    fn test_ising_3d_exponents_reasonable() {
1154        let exp = UniversalityClass::Ising3D.exponents();
1155        assert!(exp.nu > 0.5 && exp.nu < 1.0);
1156        assert!(exp.eta > 0.0 && exp.eta < 0.1);
1157    }
1158
1159    #[test]
1160    fn test_n_components() {
1161        assert_eq!(UniversalityClass::Ising3D.n_components(), 1);
1162        assert_eq!(UniversalityClass::Xy3D.n_components(), 2);
1163        assert_eq!(UniversalityClass::Heisenberg3D.n_components(), 3);
1164    }
1165
1166    // ── ScalingRelations tests ──────────────────────────────────────────────
1167
1168    #[test]
1169    fn test_rushbrooke_mean_field() {
1170        let exp = UniversalityClass::MeanField.exponents();
1171        let sr = ScalingRelations::new(exp);
1172        let (lhs, rhs) = sr.rushbrooke();
1173        // α=0, 2β=1, γ=1 → sum = 2
1174        assert!((lhs - rhs).abs() < 1e-10);
1175    }
1176
1177    #[test]
1178    fn test_widom_mean_field() {
1179        let exp = UniversalityClass::MeanField.exponents();
1180        let sr = ScalingRelations::new(exp);
1181        let (lhs, rhs) = sr.widom();
1182        // γ = 1, β(δ−1) = 0.5 × 2 = 1
1183        assert!((lhs - rhs).abs() < 1e-10);
1184    }
1185
1186    #[test]
1187    fn test_fisher_mean_field() {
1188        let exp = UniversalityClass::MeanField.exponents();
1189        let sr = ScalingRelations::new(exp);
1190        let (lhs, rhs) = sr.fisher();
1191        // γ = 1, (2−0) × 0.5 = 1
1192        assert!((lhs - rhs).abs() < 1e-10);
1193    }
1194
1195    #[test]
1196    fn test_josephson_mean_field() {
1197        let exp = UniversalityClass::MeanField.exponents();
1198        let sr = ScalingRelations::new(exp);
1199        let (lhs, rhs) = sr.josephson();
1200        // 2 − α = 2, d ν = 4 × 0.5 = 2
1201        assert!((lhs - rhs).abs() < 1e-10);
1202    }
1203
1204    #[test]
1205    fn test_check_all_mean_field() {
1206        let exp = UniversalityClass::MeanField.exponents();
1207        let sr = ScalingRelations::new(exp);
1208        let (r, w, f, j) = sr.check_all(1e-10);
1209        assert!(r, "Rushbrooke failed");
1210        assert!(w, "Widom failed");
1211        assert!(f, "Fisher failed");
1212        assert!(j, "Josephson failed");
1213    }
1214
1215    // ── RenormalizedCoupling tests ──────────────────────────────────────────
1216
1217    #[test]
1218    fn test_renorm_coupling_msbar_finite() {
1219        let rc = RenormalizedCoupling::new(RenormalizationScheme::MSbar, 0.1, 1.0, 1);
1220        assert!(rc.g_renorm.is_finite());
1221        assert!(rc.g_renorm < rc.g_bare || (rc.g_renorm - rc.g_bare).abs() < rc.g_bare);
1222    }
1223
1224    #[test]
1225    fn test_renorm_coupling_run_msbar() {
1226        let rc = RenormalizedCoupling::new(RenormalizationScheme::MSbar, 0.3, 1.0, 1);
1227        let g2 = rc.run_msbar(10.0, 1.0);
1228        // Running to higher scale with b0=1 should decrease coupling
1229        assert!(g2 < rc.g_renorm);
1230    }
1231
1232    // ── OPE tests ───────────────────────────────────────────────────────────
1233
1234    #[test]
1235    fn test_ope_fusion_rule_exists() {
1236        let mut ope = OperatorProductExpansion::new();
1237        ope.add_coefficient(OpeCoefficient::new("σ", "σ", "ε", 0.5));
1238        assert!(ope.fusion_rule_exists("σ", "σ", "ε"));
1239        assert!(!ope.fusion_rule_exists("σ", "σ", "T"));
1240    }
1241
1242    #[test]
1243    fn test_conformal_block_2d_positive() {
1244        let ope = OperatorProductExpansion::new();
1245        let g = ope.conformal_block_2d(0.5, 0.25);
1246        assert!(g > 0.0);
1247    }
1248
1249    #[test]
1250    fn test_partial_wave_amplitude_positive() {
1251        let mut ope = OperatorProductExpansion::new();
1252        ope.add_operator("ε", 1.0);
1253        ope.add_coefficient(OpeCoefficient::new("σ", "σ", "ε", 0.8));
1254        let amp = ope.partial_wave_amplitude("σ", 0.1);
1255        assert!(amp > 0.0);
1256    }
1257
1258    // ── CriticalPhenomena tests ─────────────────────────────────────────────
1259
1260    #[test]
1261    fn test_correlation_length_diverges_at_tc() {
1262        let exp = UniversalityClass::MeanField.exponents();
1263        let cp = CriticalPhenomena::new(exp, 2.269, 1.0, 1.0, 1.0, 1.0);
1264        let xi = cp.correlation_length(2.269);
1265        assert!(xi.is_infinite());
1266    }
1267
1268    #[test]
1269    fn test_order_parameter_zero_above_tc() {
1270        let exp = UniversalityClass::Ising3D.exponents();
1271        let cp = CriticalPhenomena::new(exp, 2.269, 1.0, 1.0, 1.0, 1.0);
1272        assert_eq!(cp.order_parameter(3.0), 0.0);
1273    }
1274
1275    #[test]
1276    fn test_order_parameter_nonzero_below_tc() {
1277        let exp = UniversalityClass::Ising3D.exponents();
1278        let cp = CriticalPhenomena::new(exp, 2.269, 1.0, 1.0, 1.0, 1.0);
1279        assert!(cp.order_parameter(1.0) > 0.0);
1280    }
1281
1282    #[test]
1283    fn test_critical_isotherm_odd() {
1284        let exp = UniversalityClass::MeanField.exponents();
1285        let cp = CriticalPhenomena::new(exp, 2.269, 1.0, 1.0, 1.0, 1.0);
1286        let m_pos = cp.critical_isotherm(1.0);
1287        let m_neg = cp.critical_isotherm(-1.0);
1288        assert!((m_pos + m_neg).abs() < 1e-12);
1289    }
1290
1291    // ── DecimationTransformation tests ─────────────────────────────────────
1292
1293    #[test]
1294    fn test_decimation_k_decreases_for_large_k() {
1295        // For large K (ordered), the coupling K' < K (flows to 0 for 1D)
1296        let mut dec = DecimationTransformation::new(2.0);
1297        let k0 = dec.k;
1298        dec.step();
1299        // K' = 0.5 ln cosh(2K) < K for K > 0
1300        assert!(dec.k < k0);
1301    }
1302
1303    #[test]
1304    fn test_decimation_runs_to_zero() {
1305        let mut dec = DecimationTransformation::new(1.0);
1306        dec.run(100);
1307        assert!(dec.k < 0.01);
1308    }
1309
1310    #[test]
1311    fn test_decimation_correlation_length_decreases() {
1312        let mut dec = DecimationTransformation::new(1.0);
1313        let xi0 = dec.correlation_length();
1314        dec.step();
1315        let xi1 = dec.correlation_length();
1316        assert!(xi1 < xi0);
1317    }
1318
1319    #[test]
1320    fn test_decimation_trajectory_length() {
1321        let dec = DecimationTransformation::new(1.0);
1322        let traj = dec.trajectory(10);
1323        assert_eq!(traj.len(), 11);
1324    }
1325
1326    // ── ConformalFieldTheory tests ──────────────────────────────────────────
1327
1328    #[test]
1329    fn test_casimir_energy_ising_cft() {
1330        // 2-D Ising CFT: c = 1/2
1331        let mut cft = ConformalFieldTheory::new(0.5, "Ising2D");
1332        cft.add_primary(0.0, 0); // identity
1333        cft.add_primary(0.5, 0); // σ
1334        cft.add_primary(1.0, 0); // ε
1335        let e0 = cft.casimir_energy(1.0);
1336        let expected = -PI * 0.5 / 6.0;
1337        assert!((e0 - expected).abs() < 1e-12);
1338    }
1339
1340    #[test]
1341    fn test_holomorphic_dimensions_identity() {
1342        let mut cft = ConformalFieldTheory::new(0.5, "Ising2D");
1343        cft.add_primary(0.0, 0);
1344        let dims = cft.holomorphic_dimensions();
1345        assert!((dims[0].0).abs() < 1e-12);
1346        assert!((dims[0].1).abs() < 1e-12);
1347    }
1348
1349    #[test]
1350    fn test_unitarity_bound_satisfied() {
1351        let mut cft = ConformalFieldTheory::new(0.5, "Ising2D");
1352        cft.add_primary(0.0, 0);
1353        cft.add_primary(0.5, 0);
1354        cft.add_primary(1.0, 0);
1355        assert!(cft.satisfies_unitarity_bound());
1356    }
1357
1358    #[test]
1359    fn test_unitarity_bound_violated() {
1360        let mut cft = ConformalFieldTheory::new(1.0, "Ghost");
1361        cft.add_primary(0.3, 2); // Δ < |s| = 2, violation
1362        assert!(!cft.satisfies_unitarity_bound());
1363    }
1364
1365    #[test]
1366    fn test_virasoro_character_positive() {
1367        let cft = ConformalFieldTheory::new(0.5, "Ising2D");
1368        let chi = cft.virasoro_character(0.0, 0.9, 10);
1369        assert!(chi > 0.0);
1370    }
1371
1372    #[test]
1373    fn test_partition_function_positive() {
1374        let mut cft = ConformalFieldTheory::new(0.5, "Ising2D");
1375        cft.add_primary(0.0, 0);
1376        cft.add_primary(0.5, 0);
1377        cft.add_primary(1.0, 0);
1378        let z = cft.partition_function(0.5, 10);
1379        assert!(z >= 0.0);
1380    }
1381
1382    #[test]
1383    fn test_central_charge_from_tt() {
1384        let cft = ConformalFieldTheory::new(0.5, "Ising2D");
1385        // ⟨T(z)T(0)⟩ = c/(2z⁴) → tt = c/(2 z⁴)
1386        let z = 2.0_f64;
1387        let tt = 0.5 / (2.0 * z.powi(4));
1388        let c = cft.central_charge_from_tt(z, tt);
1389        assert!((c - 0.5).abs() < 1e-10);
1390    }
1391
1392    #[test]
1393    fn test_s_matrix_wzw_symmetry() {
1394        let cft = ConformalFieldTheory::new(1.0, "SU2_k2");
1395        // S_{nm} = S_{mn} by symmetry of sine
1396        let s12 = cft.s_matrix_wzw(1, 2, 4);
1397        let s21 = cft.s_matrix_wzw(2, 1, 4);
1398        assert!((s12 - s21).abs() < 1e-12);
1399    }
1400
1401    #[test]
1402    fn test_rushbrooke_ising_3d() {
1403        let exp = UniversalityClass::Ising3D.exponents();
1404        let sr = ScalingRelations::new(exp);
1405        let (lhs, rhs) = sr.rushbrooke();
1406        // α + 2β + γ should equal 2 (within numerical precision of tabulated values)
1407        assert!((lhs - rhs).abs() < 0.01, "lhs={lhs:.6}, rhs={rhs:.6}");
1408    }
1409
1410    #[test]
1411    fn test_migdal_kadanoff_step() {
1412        let mut dec = DecimationTransformation::new(1.0);
1413        let k0 = dec.k;
1414        dec.migdal_kadanoff_step(2);
1415        // K' = 0.5 ln cosh(4K) for b=2
1416        let expected = 0.5 * (2.0 * 2.0 * k0).cosh().ln();
1417        assert!((dec.k - expected).abs() < 1e-12);
1418    }
1419
1420    #[test]
1421    fn test_beta_no_fixed_point_one_loop() {
1422        let beta = BetaFunction::new(1.0, 0.0, LoopOrder::OneLoop, "phi4");
1423        assert!(beta.fixed_point().is_none());
1424    }
1425
1426    #[test]
1427    fn test_wf_flow_u_approaches_fixed_point() {
1428        let mut rg = WilsonianRg::new(3.0, 1.0, 0.01, 0.0);
1429        let ustar = rg.wilson_fisher_fixed_point(1.0);
1430        rg.run(5000, 0.001, 1.0);
1431        // After many steps the coupling should be growing toward the fixed point
1432        // (convergence from u0=0.01 to ustar~17.5 takes l>>5; check we're moving right direction)
1433        assert!(
1434            (rg.u - ustar).abs() < 0.95 * ustar,
1435            "u={:.6}, u*={:.6}",
1436            rg.u,
1437            ustar
1438        );
1439    }
1440}