1use crate::flow::FlowEquation;
7use crate::linalg;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct FixedPoint {
13 pub location: Vec<f64>,
15 pub eigenvalues: Vec<f64>,
17 pub stable: Vec<bool>,
19 pub param_names: Vec<String>,
21}
22
23impl FixedPoint {
24 pub fn classify_direction(&self, index: usize) -> DirectionClass {
27 if index >= self.eigenvalues.len() {
28 return DirectionClass::Marginal;
29 }
30 let ev = self.eigenvalues[index];
31 if ev > 1.0 + 1e-6 {
32 DirectionClass::Relevant
33 } else if ev < 1.0 - 1e-6 {
34 DirectionClass::Irrelevant
35 } else {
36 DirectionClass::Marginal
37 }
38 }
39
40 pub fn n_relevant(&self) -> usize {
42 self.eigenvalues.iter().filter(|&&ev| ev > 1.0 + 1e-6).count()
43 }
44
45 pub fn n_irrelevant(&self) -> usize {
47 self.eigenvalues.iter().filter(|&&ev| ev < 1.0 - 1e-6).count()
48 }
49
50 pub fn codimension(&self) -> usize {
53 self.n_relevant()
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59pub enum DirectionClass {
60 Relevant,
62 Irrelevant,
64 Marginal,
66}
67
68pub fn find_fixed_point(
71 flow_eq: &dyn FlowEquation,
72 initial_guess: &[f64],
73 max_iter: usize,
74 tolerance: f64,
75) -> Option<FixedPoint> {
76 let mut current = initial_guess.to_vec();
77
78 for _ in 0..max_iter {
79 let next = flow_eq.flow_step(¤t, 0);
80 let delta: f64 = next
81 .iter()
82 .zip(current.iter())
83 .map(|(n, c)| (n - c).powi(2))
84 .sum::<f64>()
85 .sqrt();
86
87 if delta < tolerance {
88 let jac = linalg::jacobian(
90 &|p| flow_eq.flow_step(p, 0),
91 ¤t,
92 1e-7,
93 );
94 let n = jac.len();
96 let mut sym_jac = jac.clone();
97 for i in 0..n {
98 for j in 0..n {
99 sym_jac[i][j] = 0.5 * (jac[i][j] + jac.get(j).map(|r| r[i]).unwrap_or(0.0));
100 sym_jac[j][i] = sym_jac[i][j];
101 }
102 }
103 let eigenvalues = linalg::eigenvalues_symmetric(&sym_jac);
104 let stable: Vec<bool> = eigenvalues.iter().map(|&ev| ev.abs() < 1.0).collect();
105 let param_names = (0..current.len())
106 .map(|i| format!("param_{}", i))
107 .collect();
108
109 return Some(FixedPoint {
110 location: current,
111 eigenvalues,
112 stable,
113 param_names,
114 });
115 }
116 current = next;
117 }
118 None
119}
120
121pub fn analyze_stability(
124 flow_eq: &dyn FlowEquation,
125 fixed_point: &[f64],
126) -> FixedPoint {
127 let jac = linalg::jacobian(
128 &|p| flow_eq.flow_step(p, 0),
129 fixed_point,
130 1e-7,
131 );
132 let n = jac.len();
133 let mut sym_jac = jac.clone();
134 for i in 0..n {
135 for j in 0..n {
136 sym_jac[i][j] = 0.5 * (jac[i][j] + jac.get(j).map(|r| r[i]).unwrap_or(0.0));
137 sym_jac[j][i] = sym_jac[i][j];
138 }
139 }
140 let eigenvalues = linalg::eigenvalues_symmetric(&sym_jac);
141 let stable: Vec<bool> = eigenvalues.iter().map(|&ev| ev.abs() < 1.0).collect();
142 let param_names = (0..fixed_point.len())
143 .map(|i| format!("param_{}", i))
144 .collect();
145
146 FixedPoint {
147 location: fixed_point.to_vec(),
148 eigenvalues,
149 stable,
150 param_names,
151 }
152}
153
154pub fn is_near_fixed_point(
156 flow_eq: &dyn FlowEquation,
157 params: &[f64],
158 tolerance: f64,
159) -> bool {
160 let next = flow_eq.flow_step(params, 0);
161 let delta: f64 = next
162 .iter()
163 .zip(params.iter())
164 .map(|(n, c)| (n - c).powi(2))
165 .sum::<f64>()
166 .sqrt();
167 delta < tolerance
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::flow::{GaussianFlow, Ising1DFlow, RGFlow};
174
175 #[test]
176 fn test_ising_1d_zero_fixed_point() {
177 let flow_eq = Ising1DFlow::new(2.0);
178 let fp = find_fixed_point(&flow_eq, &[0.1, 0.0], 100, 1e-10);
179 assert!(fp.is_some());
180 let fp = fp.unwrap();
181 assert!(fp.location[0].abs() < 0.01);
182 }
183
184 #[test]
185 fn test_gaussian_zero_fixed_point() {
186 let flow_eq = GaussianFlow::new(3, 2.0);
188 assert!(is_near_fixed_point(&flow_eq, &[0.0, 0.0], 1e-10));
189 let fp = analyze_stability(&flow_eq, &[0.0, 0.0]);
191 assert!(fp.location[0].abs() < 1e-10);
192 assert!(fp.location[1].abs() < 1e-10);
193 }
194
195 #[test]
196 fn test_gaussian_r_is_relevant() {
197 let flow_eq = GaussianFlow::new(3, 2.0);
199 let fp = analyze_stability(&flow_eq, &[0.0, 0.0]);
200 let max_eigenvalue = fp.eigenvalues.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
202 assert!(max_eigenvalue > 1.0);
203 assert!(fp.n_relevant() >= 1);
204 }
205
206 #[test]
207 fn test_uniform_fleet_trivial_fixed_point() {
208 let flow_eq = GaussianFlow::new(3, 2.0);
210 assert!(is_near_fixed_point(&flow_eq, &[0.0, 0.0], 1e-10));
211 }
212
213 #[test]
214 fn test_direction_classification() {
215 let fp = FixedPoint {
216 location: vec![0.0, 0.0],
217 eigenvalues: vec![4.0, 0.5],
218 stable: vec![false, true],
219 param_names: vec!["r".into(), "u".into()],
220 };
221 assert_eq!(fp.classify_direction(0), DirectionClass::Relevant);
222 assert_eq!(fp.classify_direction(1), DirectionClass::Irrelevant);
223 assert_eq!(fp.n_relevant(), 1);
224 assert_eq!(fp.n_irrelevant(), 1);
225 assert_eq!(fp.codimension(), 1);
226 }
227
228 #[test]
229 fn test_relevant_operator_grows() {
230 let flow_eq = GaussianFlow::new(3, 2.0);
232 let mut flow = RGFlow::new(vec![0.01, 0.0]); flow.evolve(&flow_eq, 5);
234 assert!(flow.parameters[0] > 0.01);
236 }
237
238 #[test]
239 fn test_irrelevant_operator_shrinks() {
240 let flow_eq = GaussianFlow::new(5, 2.0);
242 let mut flow = RGFlow::new(vec![0.0, 0.1]); flow.evolve(&flow_eq, 5);
244 assert!(flow.parameters[1].abs() < 0.1);
246 }
247
248 #[test]
249 fn test_1d_ising_no_finite_t_critical() {
250 let flow_eq = Ising1DFlow::new(2.0);
252 let mut flow = RGFlow::new(vec![2.0, 0.0]); flow.evolve(&flow_eq, 50);
254 assert!(flow.parameters[0] < 1.0);
256 }
257}