Skip to main content

dsfb_add/
rlt.rs

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}