1use std::collections::{HashMap, HashSet, VecDeque};
2
3use serde::{Deserialize, Serialize};
4
5use crate::config::SimulationConfig;
6use crate::sweep::deterministic_drive;
7use crate::AddError;
8
9pub const RLT_EXAMPLE_STEPS: usize = 240;
10pub const RLT_BOUNDED_THRESHOLD: f64 = 0.05;
11pub const RLT_EXPANDING_THRESHOLD: f64 = 0.95;
12pub const RLT_PERTURBATION_STRENGTH: f64 = 0.025;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct RltSweep {
16 pub escape_rate: Vec<f64>,
17 pub expansion_ratio: Vec<f64>,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub enum RltExampleKind {
22 Bounded,
23 Expanding,
24}
25
26impl RltExampleKind {
27 pub fn filename_prefix(self) -> &'static str {
28 match self {
29 Self::Bounded => "bounded",
30 Self::Expanding => "expanding",
31 }
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct RltTrajectoryPoint {
37 pub step: usize,
38 pub lambda: f64,
39 pub vertex_id: i64,
40 pub x: i32,
41 pub y: i32,
42 pub distance_from_start: usize,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46struct Vertex {
47 x: i32,
48 y: i32,
49}
50
51#[derive(Debug, Clone, Copy)]
52enum RltRegime {
53 Bounded,
54 Transitional,
55 Expanding,
56}
57
58pub fn run_rlt_sweep(config: &SimulationConfig, lambda_grid: &[f64]) -> Result<RltSweep, AddError> {
59 run_rlt_sweep_with_progress(config, lambda_grid, |_completed, _total| {})
60}
61
62pub fn run_rlt_sweep_perturbed(
63 config: &SimulationConfig,
64 lambda_grid: &[f64],
65) -> Result<RltSweep, AddError> {
66 run_rlt_sweep_perturbed_with_progress(config, lambda_grid, |_completed, _total| {})
67}
68
69pub(crate) fn run_rlt_sweep_with_progress<F>(
70 config: &SimulationConfig,
71 lambda_grid: &[f64],
72 progress: F,
73) -> Result<RltSweep, AddError>
74where
75 F: FnMut(usize, usize),
76{
77 run_rlt_sweep_with_perturbation(config, lambda_grid, 0.0, progress)
78}
79
80pub(crate) fn run_rlt_sweep_perturbed_with_progress<F>(
81 config: &SimulationConfig,
82 lambda_grid: &[f64],
83 progress: F,
84) -> Result<RltSweep, AddError>
85where
86 F: FnMut(usize, usize),
87{
88 run_rlt_sweep_with_perturbation(config, lambda_grid, RLT_PERTURBATION_STRENGTH, progress)
89}
90
91fn run_rlt_sweep_with_perturbation<F>(
92 config: &SimulationConfig,
93 lambda_grid: &[f64],
94 perturbation_strength: f64,
95 mut progress: F,
96) -> Result<RltSweep, AddError>
97where
98 F: FnMut(usize, usize),
99{
100 let mut escape_rate = Vec::with_capacity(lambda_grid.len());
101 let mut expansion_ratio = Vec::with_capacity(lambda_grid.len());
102 let total = lambda_grid.len();
103
104 for (idx, &lambda) in lambda_grid.iter().enumerate() {
105 let vertices = simulate_vertices_with_perturbation(
106 config,
107 lambda,
108 config.steps_per_run,
109 perturbation_strength,
110 );
111 let (escape, expansion) = summarize_trajectory(&vertices, config.steps_per_run);
112 escape_rate.push(escape);
113 expansion_ratio.push(expansion);
114 progress(idx + 1, total);
115 }
116
117 Ok(RltSweep {
118 escape_rate,
119 expansion_ratio,
120 })
121}
122
123pub fn simulate_example_trajectory(
124 config: &SimulationConfig,
125 lambda: f64,
126 steps: usize,
127) -> Vec<RltTrajectoryPoint> {
128 let vertices = simulate_vertices(config, lambda, steps);
129 let mut adjacency: HashMap<Vertex, Vec<Vertex>> = HashMap::new();
130 let origin = *vertices.first().unwrap_or(&Vertex { x: 0, y: 0 });
131 let mut points = Vec::with_capacity(vertices.len());
132
133 for (step, &vertex) in vertices.iter().enumerate() {
134 if step > 0 {
135 add_edge(&mut adjacency, vertices[step - 1], vertex);
136 } else {
137 adjacency.entry(vertex).or_default();
138 }
139
140 let distance_from_start = bfs_distance(&adjacency, origin, vertex).unwrap_or(step);
141 points.push(RltTrajectoryPoint {
142 step,
143 lambda,
144 vertex_id: encode_vertex(vertex),
145 x: vertex.x,
146 y: vertex.y,
147 distance_from_start,
148 });
149 }
150
151 points
152}
153
154pub fn find_representative_regime_indices(escape_rate: &[f64]) -> (usize, usize) {
155 let bounded_idx = escape_rate
156 .iter()
157 .position(|&value| value <= RLT_BOUNDED_THRESHOLD)
158 .unwrap_or_else(|| nearest_index(escape_rate, 0.0));
159
160 let expanding_idx = escape_rate
161 .iter()
162 .position(|&value| value >= RLT_EXPANDING_THRESHOLD)
163 .unwrap_or_else(|| nearest_index(escape_rate, 1.0));
164
165 (bounded_idx, expanding_idx)
166}
167
168fn nearest_index(values: &[f64], target: f64) -> usize {
169 values
170 .iter()
171 .enumerate()
172 .min_by(|(_, left), (_, right)| {
173 let left_distance = (*left - target).abs();
174 let right_distance = (*right - target).abs();
175 left_distance
176 .partial_cmp(&right_distance)
177 .unwrap_or(std::cmp::Ordering::Equal)
178 })
179 .map(|(idx, _)| idx)
180 .unwrap_or(0)
181}
182
183fn simulate_vertices(config: &SimulationConfig, lambda: f64, steps: usize) -> Vec<Vertex> {
184 simulate_vertices_with_perturbation(config, lambda, steps, 0.0)
185}
186
187fn simulate_vertices_with_perturbation(
188 config: &SimulationConfig,
189 lambda: f64,
190 steps: usize,
191 perturbation_strength: f64,
192) -> Vec<Vertex> {
193 let lambda_norm = config.normalized_lambda(lambda);
194 let drive = deterministic_drive(config.random_seed, lambda, 0xB170_u64);
195 let mut current = Vertex { x: 0, y: 0 };
196 let mut vertices = Vec::with_capacity(steps + 1);
197 vertices.push(current);
198
199 for step in 0..steps {
200 current = resonance_step(
201 current,
202 step,
203 lambda,
204 lambda_norm,
205 drive,
206 perturbation_strength,
207 );
208 vertices.push(current);
209 }
210
211 vertices
212}
213
214fn summarize_trajectory(vertices: &[Vertex], steps: usize) -> (f64, f64) {
215 let origin = *vertices.first().unwrap_or(&Vertex { x: 0, y: 0 });
216 let goal = *vertices.last().unwrap_or(&origin);
217 let mut visited = HashSet::new();
218 let mut adjacency: HashMap<Vertex, Vec<Vertex>> = HashMap::new();
219
220 for (idx, &vertex) in vertices.iter().enumerate() {
221 visited.insert(vertex);
222 if idx > 0 {
223 add_edge(&mut adjacency, vertices[idx - 1], vertex);
224 } else {
225 adjacency.entry(vertex).or_default();
226 }
227 }
228
229 let distance = bfs_distance(&adjacency, origin, goal).unwrap_or(steps);
230 (
231 distance as f64 / steps.max(1) as f64,
232 visited.len() as f64 / steps.max(1) as f64,
233 )
234}
235
236fn resonance_step(
237 current: Vertex,
238 step: usize,
239 lambda: f64,
240 lambda_norm: f64,
241 drive: crate::sweep::DriveSignal,
242 perturbation_strength: f64,
243) -> Vertex {
244 let lambda_perturbation = perturbation_strength
245 * ((step as f64) * 0.0175 + lambda * 6.0 + drive.drift_bias * 2.0).sin();
246 let lambda_effective = (lambda_norm + lambda_perturbation).clamp(0.0, 1.0);
247 let regime = classify_regime(lambda_effective);
248 let phase_bucket = (lambda_effective * 11.0).round() as i32
249 + (drive.phase_bias * 5.0).round() as i32
250 + (perturbation_strength * 12.0 * ((step as f64) * 0.025 + lambda * 3.0).cos()).round()
251 as i32;
252 let trust_sign = if drive.trust_bias >= 0.0 { 1 } else { -1 };
253
254 match regime {
255 RltRegime::Bounded => bounded_step(step, phase_bucket, trust_sign),
256 RltRegime::Transitional => transitional_step(
257 current,
258 step,
259 lambda_effective,
260 phase_bucket,
261 trust_sign,
262 perturbation_strength,
263 ),
264 RltRegime::Expanding => expanding_step(
265 current,
266 step,
267 phase_bucket,
268 trust_sign,
269 perturbation_strength,
270 ),
271 }
272}
273
274fn classify_regime(lambda_norm: f64) -> RltRegime {
275 if lambda_norm < 0.22 {
276 RltRegime::Bounded
277 } else if lambda_norm < 0.58 {
278 RltRegime::Transitional
279 } else {
280 RltRegime::Expanding
281 }
282}
283
284fn bounded_step(step: usize, phase_bucket: i32, trust_sign: i32) -> Vertex {
285 const CYCLE: [(i32, i32); 6] = [(0, 0), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0)];
286 let idx = (step as i32 + phase_bucket).rem_euclid(CYCLE.len() as i32) as usize;
287 let (x, y) = CYCLE[idx];
288 Vertex {
289 x: x * trust_sign,
290 y,
291 }
292}
293
294fn transitional_step(
295 current: Vertex,
296 step: usize,
297 lambda_norm: f64,
298 phase_bucket: i32,
299 trust_sign: i32,
300 perturbation_strength: f64,
301) -> Vertex {
302 let leash = 2
303 + (lambda_norm * 10.0).round() as i32
304 + (perturbation_strength * 6.0 * ((step as f64) * 0.05 + lambda_norm * 4.0).sin()).round()
305 as i32;
306 let resonance_class = (step as i32 + phase_bucket).rem_euclid(6);
307 let mut next = match resonance_class {
308 0 => Vertex {
309 x: current.x + 1,
310 y: current.y,
311 },
312 1 => Vertex {
313 x: current.x,
314 y: current.y + 1,
315 },
316 2 => Vertex {
317 x: current.x - 1,
318 y: current.y + trust_sign,
319 },
320 3 => Vertex {
321 x: current.x + trust_sign,
322 y: current.y - 1,
323 },
324 4 => Vertex {
325 x: current.x + 1,
326 y: current.y + 1,
327 },
328 _ => Vertex {
329 x: current.x - trust_sign,
330 y: current.y,
331 },
332 };
333
334 let reset_period = ((16.0 - 10.0 * lambda_norm).round() as usize).clamp(6, 16);
335 if step % reset_period == 0 {
336 next = Vertex {
337 x: phase_bucket.rem_euclid(3) - 1,
338 y: (step / reset_period) as i32 % 3 - 1,
339 };
340 }
341
342 next.x = next.x.clamp(-leash, leash);
343 next.y = next.y.clamp(-leash, leash);
344 next
345}
346
347fn expanding_step(
348 current: Vertex,
349 step: usize,
350 phase_bucket: i32,
351 trust_sign: i32,
352 perturbation_strength: f64,
353) -> Vertex {
354 let resonance_class = (step as i32 + phase_bucket).rem_euclid(5);
355 let perturbation_dy =
356 (perturbation_strength * 10.0 * ((step as f64) * 0.0375).sin()).round() as i32;
357 let dy = match resonance_class {
358 0 => 0,
359 1 | 2 => 1,
360 _ => 2,
361 } + perturbation_dy.max(0);
362
363 Vertex {
364 x: current.x + 1,
365 y: current.y + dy + trust_sign.max(0),
366 }
367}
368
369fn encode_vertex(vertex: Vertex) -> i64 {
370 ((vertex.x as i64) << 32) ^ (vertex.y as u32 as i64)
371}
372
373fn add_edge(adjacency: &mut HashMap<Vertex, Vec<Vertex>>, a: Vertex, b: Vertex) {
374 adjacency.entry(a).or_default();
375 adjacency.entry(b).or_default();
376
377 if let Some(neighbors) = adjacency.get_mut(&a) {
378 if !neighbors.contains(&b) {
379 neighbors.push(b);
380 }
381 }
382
383 if let Some(neighbors) = adjacency.get_mut(&b) {
384 if !neighbors.contains(&a) {
385 neighbors.push(a);
386 }
387 }
388}
389
390fn bfs_distance(
391 adjacency: &HashMap<Vertex, Vec<Vertex>>,
392 start: Vertex,
393 goal: Vertex,
394) -> Option<usize> {
395 if start == goal {
396 return Some(0);
397 }
398
399 let mut seen = HashSet::from([start]);
400 let mut queue = VecDeque::from([(start, 0_usize)]);
401
402 while let Some((vertex, distance)) = queue.pop_front() {
403 if let Some(neighbors) = adjacency.get(&vertex) {
404 for &neighbor in neighbors {
405 if !seen.insert(neighbor) {
406 continue;
407 }
408
409 if neighbor == goal {
410 return Some(distance + 1);
411 }
412
413 queue.push_back((neighbor, distance + 1));
414 }
415 }
416 }
417
418 None
419}