use crate::flow::FlowEquation;
use crate::linalg;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixedPoint {
pub location: Vec<f64>,
pub eigenvalues: Vec<f64>,
pub stable: Vec<bool>,
pub param_names: Vec<String>,
}
impl FixedPoint {
pub fn classify_direction(&self, index: usize) -> DirectionClass {
if index >= self.eigenvalues.len() {
return DirectionClass::Marginal;
}
let ev = self.eigenvalues[index];
if ev > 1.0 + 1e-6 {
DirectionClass::Relevant
} else if ev < 1.0 - 1e-6 {
DirectionClass::Irrelevant
} else {
DirectionClass::Marginal
}
}
pub fn n_relevant(&self) -> usize {
self.eigenvalues.iter().filter(|&&ev| ev > 1.0 + 1e-6).count()
}
pub fn n_irrelevant(&self) -> usize {
self.eigenvalues.iter().filter(|&&ev| ev < 1.0 - 1e-6).count()
}
pub fn codimension(&self) -> usize {
self.n_relevant()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DirectionClass {
Relevant,
Irrelevant,
Marginal,
}
pub fn find_fixed_point(
flow_eq: &dyn FlowEquation,
initial_guess: &[f64],
max_iter: usize,
tolerance: f64,
) -> Option<FixedPoint> {
let mut current = initial_guess.to_vec();
for _ in 0..max_iter {
let next = flow_eq.flow_step(¤t, 0);
let delta: f64 = next
.iter()
.zip(current.iter())
.map(|(n, c)| (n - c).powi(2))
.sum::<f64>()
.sqrt();
if delta < tolerance {
let jac = linalg::jacobian(
&|p| flow_eq.flow_step(p, 0),
¤t,
1e-7,
);
let n = jac.len();
let mut sym_jac = jac.clone();
for i in 0..n {
for j in 0..n {
sym_jac[i][j] = 0.5 * (jac[i][j] + jac.get(j).map(|r| r[i]).unwrap_or(0.0));
sym_jac[j][i] = sym_jac[i][j];
}
}
let eigenvalues = linalg::eigenvalues_symmetric(&sym_jac);
let stable: Vec<bool> = eigenvalues.iter().map(|&ev| ev.abs() < 1.0).collect();
let param_names = (0..current.len())
.map(|i| format!("param_{}", i))
.collect();
return Some(FixedPoint {
location: current,
eigenvalues,
stable,
param_names,
});
}
current = next;
}
None
}
pub fn analyze_stability(
flow_eq: &dyn FlowEquation,
fixed_point: &[f64],
) -> FixedPoint {
let jac = linalg::jacobian(
&|p| flow_eq.flow_step(p, 0),
fixed_point,
1e-7,
);
let n = jac.len();
let mut sym_jac = jac.clone();
for i in 0..n {
for j in 0..n {
sym_jac[i][j] = 0.5 * (jac[i][j] + jac.get(j).map(|r| r[i]).unwrap_or(0.0));
sym_jac[j][i] = sym_jac[i][j];
}
}
let eigenvalues = linalg::eigenvalues_symmetric(&sym_jac);
let stable: Vec<bool> = eigenvalues.iter().map(|&ev| ev.abs() < 1.0).collect();
let param_names = (0..fixed_point.len())
.map(|i| format!("param_{}", i))
.collect();
FixedPoint {
location: fixed_point.to_vec(),
eigenvalues,
stable,
param_names,
}
}
pub fn is_near_fixed_point(
flow_eq: &dyn FlowEquation,
params: &[f64],
tolerance: f64,
) -> bool {
let next = flow_eq.flow_step(params, 0);
let delta: f64 = next
.iter()
.zip(params.iter())
.map(|(n, c)| (n - c).powi(2))
.sum::<f64>()
.sqrt();
delta < tolerance
}
#[cfg(test)]
mod tests {
use super::*;
use crate::flow::{GaussianFlow, Ising1DFlow, RGFlow};
#[test]
fn test_ising_1d_zero_fixed_point() {
let flow_eq = Ising1DFlow::new(2.0);
let fp = find_fixed_point(&flow_eq, &[0.1, 0.0], 100, 1e-10);
assert!(fp.is_some());
let fp = fp.unwrap();
assert!(fp.location[0].abs() < 0.01);
}
#[test]
fn test_gaussian_zero_fixed_point() {
let flow_eq = GaussianFlow::new(3, 2.0);
assert!(is_near_fixed_point(&flow_eq, &[0.0, 0.0], 1e-10));
let fp = analyze_stability(&flow_eq, &[0.0, 0.0]);
assert!(fp.location[0].abs() < 1e-10);
assert!(fp.location[1].abs() < 1e-10);
}
#[test]
fn test_gaussian_r_is_relevant() {
let flow_eq = GaussianFlow::new(3, 2.0);
let fp = analyze_stability(&flow_eq, &[0.0, 0.0]);
let max_eigenvalue = fp.eigenvalues.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
assert!(max_eigenvalue > 1.0);
assert!(fp.n_relevant() >= 1);
}
#[test]
fn test_uniform_fleet_trivial_fixed_point() {
let flow_eq = GaussianFlow::new(3, 2.0);
assert!(is_near_fixed_point(&flow_eq, &[0.0, 0.0], 1e-10));
}
#[test]
fn test_direction_classification() {
let fp = FixedPoint {
location: vec![0.0, 0.0],
eigenvalues: vec![4.0, 0.5],
stable: vec![false, true],
param_names: vec!["r".into(), "u".into()],
};
assert_eq!(fp.classify_direction(0), DirectionClass::Relevant);
assert_eq!(fp.classify_direction(1), DirectionClass::Irrelevant);
assert_eq!(fp.n_relevant(), 1);
assert_eq!(fp.n_irrelevant(), 1);
assert_eq!(fp.codimension(), 1);
}
#[test]
fn test_relevant_operator_grows() {
let flow_eq = GaussianFlow::new(3, 2.0);
let mut flow = RGFlow::new(vec![0.01, 0.0]); flow.evolve(&flow_eq, 5);
assert!(flow.parameters[0] > 0.01);
}
#[test]
fn test_irrelevant_operator_shrinks() {
let flow_eq = GaussianFlow::new(5, 2.0);
let mut flow = RGFlow::new(vec![0.0, 0.1]); flow.evolve(&flow_eq, 5);
assert!(flow.parameters[1].abs() < 0.1);
}
#[test]
fn test_1d_ising_no_finite_t_critical() {
let flow_eq = Ising1DFlow::new(2.0);
let mut flow = RGFlow::new(vec![2.0, 0.0]); flow.evolve(&flow_eq, 50);
assert!(flow.parameters[0] < 1.0);
}
}