1use crate::resolution::ResolutionParams;
23use nereids_core::constants::NEAR_ZERO_FLOOR;
24
25const N_BOUNDARY_REF: usize = 5;
29
30const MERGE_RELATIVE_TOL: f64 = 1e-10;
33
34const IPTDOP: usize = 9;
38
39const MIN_POINTS_PER_WIDTH: usize = IPTDOP + 1;
44
45const FRACTN: f64 = 2.0 / (IPTDOP as f64 + 5.0);
50
51pub fn build_extended_grid(
71 data_energies: &[f64],
72 resolution: Option<&ResolutionParams>,
73 resonances: &[(f64, f64)],
74) -> (Vec<f64>, Vec<usize>) {
75 build_extended_grid_inner(data_energies, resolution, resonances, true)
76}
77
78pub fn build_extended_grid_boundary_only(
84 data_energies: &[f64],
85 resolution: Option<&ResolutionParams>,
86) -> (Vec<f64>, Vec<usize>) {
87 build_extended_grid_inner(data_energies, resolution, &[], false)
88}
89
90fn build_extended_grid_inner(
91 data_energies: &[f64],
92 resolution: Option<&ResolutionParams>,
93 resonances: &[(f64, f64)],
94 add_intermediate: bool,
95) -> (Vec<f64>, Vec<usize>) {
96 if data_energies.is_empty() {
97 return (vec![], vec![]);
98 }
99 if data_energies.len() == 1 {
100 return (data_energies.to_vec(), vec![0]);
101 }
102
103 let res = match resolution {
104 Some(r) => r,
105 None => {
106 let indices: Vec<usize> = (0..data_energies.len()).collect();
107 return (data_energies.to_vec(), indices);
108 }
109 };
110
111 let n = data_energies.len();
112
113 let n_sigma = 5.0;
117
118 let e_min = data_energies[0];
119 let wg_low = res.gaussian_width(e_min);
120 let extend_low = n_sigma * wg_low;
121
122 let e_max = data_energies[n - 1];
123 let wg_high = res.gaussian_width(e_max);
124 let we_high = res.exp_width(e_max);
125 let extend_high = if we_high > 1e-30 {
126 let rwid = wg_high / we_high;
127 if rwid <= 1.0 {
128 6.25 * we_high
129 } else if rwid <= 2.0 {
130 n_sigma * (3.0 - rwid) * wg_high
131 } else {
132 n_sigma * wg_high
133 }
134 } else {
135 n_sigma * wg_high
136 };
137
138 let mut grid = Vec::with_capacity(n + 200);
139
140 if extend_low > 0.0 && e_min > 0.0 {
143 let n_ref = N_BOUNDARY_REF.min(n);
144 let e_ref = data_energies[n_ref - 1];
145 let d_sqrt = (e_ref.sqrt() - e_min.sqrt()) / (n_ref as f64 - 1.0).max(1.0);
146
147 if d_sqrt > 1e-30 {
148 let target_low = (e_min - extend_low).max(0.001);
149 let sqrt_min = e_min.sqrt();
150 let sqrt_target = target_low.sqrt();
151 let n_ext = ((sqrt_min - sqrt_target) / d_sqrt).ceil() as usize;
152 for k in 1..=n_ext {
153 let sqrt_e = sqrt_min - d_sqrt * k as f64;
154 if sqrt_e > 0.0 {
155 grid.push(sqrt_e * sqrt_e);
156 }
157 }
158 }
159 }
160
161 grid.extend_from_slice(data_energies);
163
164 if extend_high > 0.0 {
166 let n_ref = N_BOUNDARY_REF.min(n);
167 let e_ref = data_energies[n - n_ref];
168 let d_sqrt = (e_max.sqrt() - e_ref.sqrt()) / (n_ref as f64 - 1.0).max(1.0);
169
170 if d_sqrt > 1e-30 {
171 let target_high = e_max + extend_high;
172 let sqrt_max = e_max.sqrt();
173 let sqrt_target = target_high.sqrt();
174 let n_ext = ((sqrt_target - sqrt_max) / d_sqrt).ceil() as usize;
175 for k in 1..=n_ext {
176 let sqrt_e = sqrt_max + d_sqrt * k as f64;
177 grid.push(sqrt_e * sqrt_e);
178 }
179 }
180 }
181
182 grid.sort_unstable_by(|a, b| a.total_cmp(b));
184 dedup(&mut grid);
185
186 if add_intermediate {
201 let mut extra: Vec<f64> = Vec::new();
202 for k in 0..grid.len() - 1 {
203 let e_lo = grid[k];
204 let e_hi = grid[k + 1];
205 let h = e_hi - e_lo;
206 let e_mid = (e_lo + e_hi) * 0.5;
207 let w = res.gaussian_width(e_mid);
208 if w < NEAR_ZERO_FLOOR {
209 continue;
210 }
211 let max_spacing = w * 0.25;
212 if h > max_spacing {
213 let n_ins = (h / max_spacing).ceil() as usize;
215 let step = h / n_ins as f64;
216 for j in 1..n_ins {
217 extra.push(e_lo + step * j as f64);
218 }
219 }
220 }
221 if !extra.is_empty() {
222 grid.extend(extra);
223 grid.sort_unstable_by(|a, b| a.total_cmp(b));
224 dedup(&mut grid);
225 }
226 }
227
228 if !resonances.is_empty() {
235 let mut fine_pts: Vec<f64> = Vec::new();
236 for &(eres, gd) in resonances {
237 let pts = fine_structure_points(&grid, eres, gd);
238 fine_pts.extend(pts);
239 }
240 if !fine_pts.is_empty() {
241 grid.extend(fine_pts);
242 grid.sort_unstable_by(|a, b| a.total_cmp(b));
243 dedup(&mut grid);
244 }
245 }
246
247 grid.retain(|&e| e > 0.0);
249
250 let data_indices = build_data_indices(&grid, data_energies);
252
253 (grid, data_indices)
254}
255
256fn fine_structure_points(grid: &[f64], eres: f64, gd: f64) -> Vec<f64> {
268 if gd < 1e-30 || eres <= 0.0 {
269 return vec![];
270 }
271
272 let xmin = (eres - gd).max(1e-6);
273 let xmax = eres + gd;
274
275 if grid.is_empty() || eres < grid[0] || eres > *grid.last().unwrap() {
278 return vec![];
279 }
280
281 let lo = grid.partition_point(|&e| e < xmin);
284 let hi = grid.partition_point(|&e| e <= xmax);
285 let count = hi - lo;
286
287 if count >= MIN_POINTS_PER_WIDTH {
288 return vec![];
289 }
290
291 let eg = FRACTN * gd;
292 if eg < 1e-30 {
293 return vec![];
294 }
295
296 let mut new_points = Vec::new();
297
298 let n_pts = ((xmax - xmin) / eg).ceil() as usize;
301 for i in 0..=n_pts {
302 let e = xmin + eg * i as f64;
303 if e > 0.0 && e <= xmax + eg * 0.01 {
304 new_points.push(e);
305 }
306 }
307
308 let idx_below = lo; if idx_below > 0 {
317 let e_below = grid[idx_below - 1];
318 let gap = xmin - e_below;
319 if gap > eg * 2.0 {
320 let mut spacing = eg;
321 let mut e = xmin;
322 for _ in 0..20 {
323 spacing *= 2.0;
324 e -= spacing;
325 if e <= e_below + MERGE_RELATIVE_TOL * e_below.abs().max(1e-30) {
326 break;
327 }
328 new_points.push(e);
329 }
330 }
331 }
332
333 if hi < grid.len() {
335 let e_above = grid[hi];
336 let gap = e_above - xmax;
337 if gap > eg * 2.0 {
338 let mut spacing = eg;
339 let mut e = xmax;
340 for _ in 0..20 {
341 spacing *= 2.0;
342 e += spacing;
343 if e >= e_above - MERGE_RELATIVE_TOL * e_above.abs().max(1e-30) {
344 break;
345 }
346 new_points.push(e);
347 }
348 }
349 }
350
351 new_points
352}
353
354fn dedup(grid: &mut Vec<f64>) {
356 if grid.len() < 2 {
357 return;
358 }
359 let mut deduped = Vec::with_capacity(grid.len());
360 deduped.push(grid[0]);
361 for &val in grid.iter().skip(1) {
362 let prev = *deduped.last().unwrap();
363 let tol = MERGE_RELATIVE_TOL * prev.abs().max(1e-30);
364 if (val - prev).abs() > tol {
365 deduped.push(val);
366 }
367 }
368 *grid = deduped;
369}
370
371fn build_data_indices(grid: &[f64], data_energies: &[f64]) -> Vec<usize> {
378 data_energies
379 .iter()
380 .map(|&e| {
381 let idx = grid.partition_point(|&ae| ae < e);
382 let search_range = idx.saturating_sub(1)..grid.len().min(idx + 2);
384 let mut best_idx = idx.min(grid.len() - 1);
385 let mut best_dist = (grid[best_idx] - e).abs();
386 for j in search_range {
387 let dist = (grid[j] - e).abs();
388 if dist < best_dist {
389 best_dist = dist;
390 best_idx = j;
391 }
392 }
393 best_idx
394 })
395 .collect()
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn test_empty_grid() {
404 let (ext, indices) = build_extended_grid(&[], None, &[]);
405 assert!(ext.is_empty());
406 assert!(indices.is_empty());
407 }
408
409 #[test]
410 fn test_single_point() {
411 let energies = vec![100.0];
412 let (ext, indices) = build_extended_grid(&energies, None, &[]);
413 assert_eq!(ext, vec![100.0]);
414 assert_eq!(indices, vec![0]);
415 }
416
417 #[test]
418 fn test_no_resolution_identity() {
419 let data = vec![1.0, 5.0, 10.0];
420 let (ext, indices) = build_extended_grid(&data, None, &[]);
421 assert_eq!(ext, data);
422 assert_eq!(indices, vec![0, 1, 2]);
423 }
424
425 #[test]
426 fn test_data_indices_roundtrip() {
427 let data = vec![1.0, 5.0, 10.0, 100.0];
428 let res = ResolutionParams::new(10.0, 0.01, 0.001, 0.0).unwrap();
429 let (ext, indices) = build_extended_grid(&data, Some(&res), &[]);
430 assert!(ext.len() >= data.len());
431 for (i, &e) in data.iter().enumerate() {
432 assert!(
433 (ext[indices[i]] - e).abs() < 1e-10,
434 "data[{i}]={e} not at ext[{}]={}",
435 indices[i],
436 ext[indices[i]]
437 );
438 }
439 }
440
441 #[test]
442 fn test_extension_covers_5sigma() {
443 let data: Vec<f64> = (0..20).map(|i| 100.0 + i as f64 * 5.0).collect();
444 let res = ResolutionParams::new(10.0, 0.1, 0.01, 0.0).unwrap();
445 let (ext, _) = build_extended_grid(&data, Some(&res), &[]);
446
447 assert!(
448 ext[0] < data[0],
449 "expected extension below data[0]={}, got ext[0]={}",
450 data[0],
451 ext[0]
452 );
453 assert!(
454 *ext.last().unwrap() > *data.last().unwrap(),
455 "expected extension above data max"
456 );
457 }
458
459 #[test]
460 fn test_grid_is_sorted() {
461 let data: Vec<f64> = (0..10).map(|i| 1000.0 + i as f64 * 100.0).collect();
462 let res = ResolutionParams::new(50.0, 0.05, 0.01, 0.0).unwrap();
463 let (ext, _) = build_extended_grid(&data, Some(&res), &[]);
464 for pair in ext.windows(2) {
465 assert!(
466 pair[0] < pair[1],
467 "grid not sorted: {} >= {}",
468 pair[0],
469 pair[1]
470 );
471 }
472 }
473
474 #[test]
475 fn test_grid_all_positive() {
476 let data = vec![1.0, 2.0, 3.0];
477 let res = ResolutionParams::new(10.0, 0.1, 0.01, 0.0).unwrap();
478 let (ext, _) = build_extended_grid(&data, Some(&res), &[]);
479 for &e in &ext {
480 assert!(e > 0.0, "non-positive energy: {e}");
481 }
482 }
483
484 #[test]
485 fn test_fine_structure_adds_points() {
486 let data: Vec<f64> = (0..20).map(|i| 490.0 + i as f64 * 5.0).collect();
491 let res = ResolutionParams::new(10.0, 0.01, 0.001, 0.0).unwrap();
492 let resonances = vec![(500.0, 1.0)]; let (ext_without, _) = build_extended_grid_inner(&data, Some(&res), &[], false);
496 let (ext_with, _) = build_extended_grid_inner(&data, Some(&res), &resonances, false);
498
499 assert!(
500 ext_with.len() > ext_without.len(),
501 "fine-structure should add points: {} vs {}",
502 ext_with.len(),
503 ext_without.len()
504 );
505
506 let lo = ext_with.partition_point(|&e| e < 499.0);
508 let hi = ext_with.partition_point(|&e| e <= 501.0);
509 assert!(
510 hi - lo >= MIN_POINTS_PER_WIDTH,
511 "expected ≥{MIN_POINTS_PER_WIDTH} points in resonance width, got {}",
512 hi - lo
513 );
514 }
515
516 #[test]
517 fn test_fine_structure_skips_dense_grid() {
518 let data: Vec<f64> = (0..100).map(|i| 495.0 + i as f64 * 0.1).collect();
521 let res = ResolutionParams::new(10.0, 0.01, 0.001, 0.0).unwrap();
522 let resonances = vec![(500.0, 1.0)];
523
524 let (ext_without, _) = build_extended_grid(&data, Some(&res), &[]);
525 let (ext_with, _) = build_extended_grid(&data, Some(&res), &resonances);
526
527 assert_eq!(
528 ext_without.len(),
529 ext_with.len(),
530 "dense grid should not get extra fine-structure points"
531 );
532 }
533
534 #[test]
535 fn test_fine_structure_data_indices_valid() {
536 let data: Vec<f64> = (0..20).map(|i| 490.0 + i as f64 * 5.0).collect();
538 let res = ResolutionParams::new(10.0, 0.01, 0.001, 0.0).unwrap();
539 let resonances = vec![(500.0, 1.0), (520.0, 0.5)];
540
541 let (ext, indices) = build_extended_grid(&data, Some(&res), &resonances);
542 assert_eq!(indices.len(), data.len());
543 for (i, &e) in data.iter().enumerate() {
544 assert!(
545 (ext[indices[i]] - e).abs() < 1e-10,
546 "data[{i}]={e} not at ext[{}]={}",
547 indices[i],
548 ext[indices[i]]
549 );
550 }
551 }
552
553 #[test]
554 fn test_fine_structure_outside_range_ignored() {
555 let data: Vec<f64> = (0..10).map(|i| 100.0 + i as f64 * 10.0).collect();
557 let res = ResolutionParams::new(10.0, 0.01, 0.001, 0.0).unwrap();
558 let resonances = vec![(50.0, 1.0), (300.0, 1.0)]; let (ext_without, _) = build_extended_grid(&data, Some(&res), &[]);
561 let (ext_with, _) = build_extended_grid(&data, Some(&res), &resonances);
562
563 assert!(ext_with.len() >= data.len());
567 for pair in ext_with.windows(2) {
568 assert!(
569 pair[0] < pair[1],
570 "grid not sorted: {} >= {}",
571 pair[0],
572 pair[1]
573 );
574 }
575 assert_eq!(ext_without.len(), ext_with.len());
577 }
578}