1use crate::resolution::{ResolutionFunction, ResolutionParams};
24use nereids_core::constants::NEAR_ZERO_FLOOR;
25
26const N_BOUNDARY_REF: usize = 5;
31
32const MERGE_RELATIVE_TOL: f64 = 1e-10;
35
36const IPTDOP: usize = 9;
40
41const MIN_POINTS_PER_WIDTH: usize = IPTDOP + 1;
46
47const FRACTN: f64 = 2.0 / (IPTDOP as f64 + 5.0);
52
53pub fn build_extended_grid(
73 data_energies: &[f64],
74 resolution: Option<&ResolutionParams>,
75 resonances: &[(f64, f64)],
76) -> (Vec<f64>, Vec<usize>) {
77 build_extended_grid_inner(data_energies, resolution, resonances, true)
78}
79
80pub fn build_extended_grid_boundary_only(
86 data_energies: &[f64],
87 resolution: Option<&ResolutionParams>,
88) -> (Vec<f64>, Vec<usize>) {
89 build_extended_grid_inner(data_energies, resolution, &[], false)
90}
91
92pub fn build_extended_grid_for(
96 data_energies: &[f64],
97 resolution: &ResolutionFunction,
98) -> (Vec<f64>, Vec<usize>) {
99 if data_energies.len() < 2 {
100 let indices: Vec<usize> = (0..data_energies.len()).collect();
101 return (data_energies.to_vec(), indices);
102 }
103 let (low, high) = resolution.grid_bounds_ev(data_energies);
104 let spacing = match resolution {
105 ResolutionFunction::Gaussian(_) => Spacing::SqrtEnergy,
107 ResolutionFunction::Tabulated(_) | ResolutionFunction::IkedaCarpenter(_) => {
109 Spacing::TimeOfFlight
110 }
111 };
112 extend_boundaries(data_energies, low, high, spacing)
113}
114
115#[derive(Clone, Copy)]
121enum Spacing {
122 SqrtEnergy,
123 TimeOfFlight,
124}
125
126impl Spacing {
127 fn to_u(self, e: f64) -> f64 {
128 match self {
129 Spacing::SqrtEnergy => e.sqrt(),
130 Spacing::TimeOfFlight => 1.0 / e.sqrt(),
131 }
132 }
133
134 fn to_e(self, u: f64) -> f64 {
135 match self {
136 Spacing::SqrtEnergy => u * u,
137 Spacing::TimeOfFlight => 1.0 / (u * u),
138 }
139 }
140}
141
142fn step_outward(
147 spacing: Spacing,
148 e_edge: f64,
149 e_ref: f64,
150 n_ref: usize,
151 target_e: f64,
152) -> Vec<f64> {
153 let u_edge = spacing.to_u(e_edge);
154 let u_target = spacing.to_u(target_e);
155 let step = (u_edge - spacing.to_u(e_ref)) / (n_ref as f64 - 1.0).max(1.0);
156 let steps = (u_target - u_edge) / step;
157 if step.abs() <= 1e-30 || !steps.is_finite() || steps <= 0.0 {
158 return Vec::new();
159 }
160 let n_between = (steps - MERGE_RELATIVE_TOL * (u_target / step).abs()).floor() as usize;
161 let mut points: Vec<f64> = (1..=n_between)
162 .map(|k| spacing.to_e(u_edge + step * k as f64))
163 .collect();
164 points.push(target_e);
165 points
166}
167
168fn extend_boundaries(
172 data_energies: &[f64],
173 low: f64,
174 high: f64,
175 spacing: Spacing,
176) -> (Vec<f64>, Vec<usize>) {
177 let n = data_energies.len();
178 let e_min = data_energies[0];
179 let e_max = data_energies[n - 1];
180 let n_ref = N_BOUNDARY_REF.min(n);
181
182 let mut below = if low < e_min && e_min > 0.0 {
183 step_outward(spacing, e_min, data_energies[n_ref - 1], n_ref, low)
184 } else {
185 Vec::new()
186 };
187 below.reverse();
188 let above = if high > e_max {
189 step_outward(spacing, e_max, data_energies[n - n_ref], n_ref, high)
190 } else {
191 Vec::new()
192 };
193
194 let indices = (below.len()..below.len() + n).collect();
195 let mut grid = below;
196 grid.extend_from_slice(data_energies);
197 grid.extend(above);
198 (grid, indices)
199}
200
201fn build_extended_grid_inner(
202 data_energies: &[f64],
203 resolution: Option<&ResolutionParams>,
204 resonances: &[(f64, f64)],
205 add_intermediate: bool,
206) -> (Vec<f64>, Vec<usize>) {
207 if data_energies.is_empty() {
208 return (vec![], vec![]);
209 }
210 if data_energies.len() == 1 {
211 return (data_energies.to_vec(), vec![0]);
212 }
213
214 let res = match resolution {
215 Some(r) => r,
216 None => {
217 let indices: Vec<usize> = (0..data_energies.len()).collect();
218 return (data_energies.to_vec(), indices);
219 }
220 };
221
222 let (low, high) = ResolutionFunction::Gaussian(*res).grid_bounds_ev(data_energies);
224 let (mut grid, _) = extend_boundaries(data_energies, low, high, Spacing::SqrtEnergy);
225
226 if add_intermediate {
241 let mut extra: Vec<f64> = Vec::new();
242 for k in 0..grid.len() - 1 {
243 let e_lo = grid[k];
244 let e_hi = grid[k + 1];
245 let h = e_hi - e_lo;
246 let e_mid = (e_lo + e_hi) * 0.5;
247 let w = res.gaussian_width(e_mid);
248 if w < NEAR_ZERO_FLOOR {
249 continue;
250 }
251 let max_spacing = w * 0.25;
252 if h > max_spacing {
253 let n_ins = (h / max_spacing).ceil() as usize;
255 let step = h / n_ins as f64;
256 for j in 1..n_ins {
257 extra.push(e_lo + step * j as f64);
258 }
259 }
260 }
261 if !extra.is_empty() {
262 grid.extend(extra);
263 grid.sort_unstable_by(|a, b| a.total_cmp(b));
264 dedup(&mut grid);
265 }
266 }
267
268 if !resonances.is_empty() {
275 let mut fine_pts: Vec<f64> = Vec::new();
276 for &(eres, gd) in resonances {
277 let pts = fine_structure_points(&grid, eres, gd);
278 fine_pts.extend(pts);
279 }
280 if !fine_pts.is_empty() {
281 grid.extend(fine_pts);
282 grid.sort_unstable_by(|a, b| a.total_cmp(b));
283 dedup(&mut grid);
284 }
285 }
286
287 grid.retain(|&e| e > 0.0);
289
290 let data_indices = build_data_indices(&grid, data_energies);
292
293 (grid, data_indices)
294}
295
296fn fine_structure_points(grid: &[f64], eres: f64, gd: f64) -> Vec<f64> {
308 if gd < 1e-30 || eres <= 0.0 {
309 return vec![];
310 }
311
312 let xmin = (eres - gd).max(1e-6);
313 let xmax = eres + gd;
314
315 if grid.is_empty() || eres < grid[0] || eres > *grid.last().unwrap() {
318 return vec![];
319 }
320
321 let lo = grid.partition_point(|&e| e < xmin);
324 let hi = grid.partition_point(|&e| e <= xmax);
325 let count = hi - lo;
326
327 if count >= MIN_POINTS_PER_WIDTH {
328 return vec![];
329 }
330
331 let eg = FRACTN * gd;
332 if eg < 1e-30 {
333 return vec![];
334 }
335
336 let mut new_points = Vec::new();
337
338 let n_pts = ((xmax - xmin) / eg).ceil() as usize;
341 for i in 0..=n_pts {
342 let e = xmin + eg * i as f64;
343 if e > 0.0 && e <= xmax + eg * 0.01 {
344 new_points.push(e);
345 }
346 }
347
348 let idx_below = lo; if idx_below > 0 {
357 let e_below = grid[idx_below - 1];
358 let gap = xmin - e_below;
359 if gap > eg * 2.0 {
360 let mut spacing = eg;
361 let mut e = xmin;
362 for _ in 0..20 {
363 spacing *= 2.0;
364 e -= spacing;
365 if e <= e_below + MERGE_RELATIVE_TOL * e_below.abs().max(1e-30) {
366 break;
367 }
368 new_points.push(e);
369 }
370 }
371 }
372
373 if hi < grid.len() {
375 let e_above = grid[hi];
376 let gap = e_above - xmax;
377 if gap > eg * 2.0 {
378 let mut spacing = eg;
379 let mut e = xmax;
380 for _ in 0..20 {
381 spacing *= 2.0;
382 e += spacing;
383 if e >= e_above - MERGE_RELATIVE_TOL * e_above.abs().max(1e-30) {
384 break;
385 }
386 new_points.push(e);
387 }
388 }
389 }
390
391 new_points
392}
393
394fn dedup(grid: &mut Vec<f64>) {
396 if grid.len() < 2 {
397 return;
398 }
399 let mut deduped = Vec::with_capacity(grid.len());
400 deduped.push(grid[0]);
401 for &val in grid.iter().skip(1) {
402 let prev = *deduped.last().unwrap();
403 let tol = MERGE_RELATIVE_TOL * prev.abs().max(1e-30);
404 if (val - prev).abs() > tol {
405 deduped.push(val);
406 }
407 }
408 *grid = deduped;
409}
410
411fn build_data_indices(grid: &[f64], data_energies: &[f64]) -> Vec<usize> {
418 data_energies
419 .iter()
420 .map(|&e| {
421 let idx = grid.partition_point(|&ae| ae < e);
422 let search_range = idx.saturating_sub(1)..grid.len().min(idx + 2);
424 let mut best_idx = idx.min(grid.len() - 1);
425 let mut best_dist = (grid[best_idx] - e).abs();
426 for j in search_range {
427 let dist = (grid[j] - e).abs();
428 if dist < best_dist {
429 best_dist = dist;
430 best_idx = j;
431 }
432 }
433 best_idx
434 })
435 .collect()
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
443 fn test_empty_grid() {
444 let (ext, indices) = build_extended_grid(&[], None, &[]);
445 assert!(ext.is_empty());
446 assert!(indices.is_empty());
447 }
448
449 #[test]
450 fn test_single_point() {
451 let energies = vec![100.0];
452 let (ext, indices) = build_extended_grid(&energies, None, &[]);
453 assert_eq!(ext, vec![100.0]);
454 assert_eq!(indices, vec![0]);
455 }
456
457 #[test]
458 fn test_no_resolution_identity() {
459 let data = vec![1.0, 5.0, 10.0];
460 let (ext, indices) = build_extended_grid(&data, None, &[]);
461 assert_eq!(ext, data);
462 assert_eq!(indices, vec![0, 1, 2]);
463 }
464
465 #[test]
466 fn test_data_indices_roundtrip() {
467 let data = vec![1.0, 5.0, 10.0, 100.0];
468 let res = ResolutionParams::new(10.0, 0.01, 0.001, 0.0).unwrap();
469 let (ext, indices) = build_extended_grid(&data, Some(&res), &[]);
470 assert!(ext.len() >= data.len());
471 for (i, &e) in data.iter().enumerate() {
472 assert!(
473 (ext[indices[i]] - e).abs() < 1e-10,
474 "data[{i}]={e} not at ext[{}]={}",
475 indices[i],
476 ext[indices[i]]
477 );
478 }
479 }
480
481 #[test]
482 fn test_extension_covers_5sigma() {
483 let data: Vec<f64> = (0..20).map(|i| 100.0 + i as f64 * 5.0).collect();
484 let res = ResolutionParams::new(10.0, 0.1, 0.01, 0.0).unwrap();
485 let (ext, _) = build_extended_grid(&data, Some(&res), &[]);
486
487 assert!(
488 ext[0] < data[0],
489 "expected extension below data[0]={}, got ext[0]={}",
490 data[0],
491 ext[0]
492 );
493 assert!(
494 *ext.last().unwrap() > *data.last().unwrap(),
495 "expected extension above data max"
496 );
497 }
498
499 #[test]
500 fn test_grid_is_sorted() {
501 let data: Vec<f64> = (0..10).map(|i| 1000.0 + i as f64 * 100.0).collect();
502 let res = ResolutionParams::new(50.0, 0.05, 0.01, 0.0).unwrap();
503 let (ext, _) = build_extended_grid(&data, Some(&res), &[]);
504 for pair in ext.windows(2) {
505 assert!(
506 pair[0] < pair[1],
507 "grid not sorted: {} >= {}",
508 pair[0],
509 pair[1]
510 );
511 }
512 }
513
514 #[test]
515 fn test_grid_all_positive() {
516 let data = vec![1.0, 2.0, 3.0];
517 let res = ResolutionParams::new(10.0, 0.1, 0.01, 0.0).unwrap();
518 let (ext, _) = build_extended_grid(&data, Some(&res), &[]);
519 for &e in &ext {
520 assert!(e > 0.0, "non-positive energy: {e}");
521 }
522 }
523
524 #[test]
525 fn test_fine_structure_adds_points() {
526 let data: Vec<f64> = (0..20).map(|i| 490.0 + i as f64 * 5.0).collect();
531 let res = ResolutionParams::new(10.0, 0.01, 0.001, 0.0).unwrap();
532 let resonances = vec![(500.0, 1.0)]; let (ext_without, _) = build_extended_grid_inner(&data, Some(&res), &[], false);
536 let (ext_with, _) = build_extended_grid_inner(&data, Some(&res), &resonances, false);
538
539 assert!(
540 ext_with.len() > ext_without.len(),
541 "fine-structure should add points: {} vs {}",
542 ext_with.len(),
543 ext_without.len()
544 );
545
546 let lo = ext_with.partition_point(|&e| e < 499.0);
548 let hi = ext_with.partition_point(|&e| e <= 501.0);
549 assert!(
550 hi - lo >= MIN_POINTS_PER_WIDTH,
551 "expected ≥{MIN_POINTS_PER_WIDTH} points in resonance width, got {}",
552 hi - lo
553 );
554 }
555
556 #[test]
557 fn test_fine_structure_skips_dense_grid() {
558 let data: Vec<f64> = (0..100).map(|i| 495.0 + i as f64 * 0.1).collect();
561 let res = ResolutionParams::new(10.0, 0.01, 0.001, 0.0).unwrap();
562 let resonances = vec![(500.0, 1.0)];
563
564 let (ext_without, _) = build_extended_grid(&data, Some(&res), &[]);
565 let (ext_with, _) = build_extended_grid(&data, Some(&res), &resonances);
566
567 assert_eq!(
568 ext_without.len(),
569 ext_with.len(),
570 "dense grid should not get extra fine-structure points"
571 );
572 }
573
574 #[test]
575 fn test_fine_structure_data_indices_valid() {
576 let data: Vec<f64> = (0..20).map(|i| 490.0 + i as f64 * 5.0).collect();
578 let res = ResolutionParams::new(10.0, 0.01, 0.001, 0.0).unwrap();
579 let resonances = vec![(500.0, 1.0), (520.0, 0.5)];
580
581 let (ext, indices) = build_extended_grid(&data, Some(&res), &resonances);
582 assert_eq!(indices.len(), data.len());
583 for (i, &e) in data.iter().enumerate() {
584 assert!(
585 (ext[indices[i]] - e).abs() < 1e-10,
586 "data[{i}]={e} not at ext[{}]={}",
587 indices[i],
588 ext[indices[i]]
589 );
590 }
591 }
592
593 #[test]
594 fn test_fine_structure_outside_range_ignored() {
595 let data: Vec<f64> = (0..10).map(|i| 100.0 + i as f64 * 10.0).collect();
597 let res = ResolutionParams::new(10.0, 0.01, 0.001, 0.0).unwrap();
598 let resonances = vec![(50.0, 1.0), (300.0, 1.0)]; let (ext_without, _) = build_extended_grid(&data, Some(&res), &[]);
601 let (ext_with, _) = build_extended_grid(&data, Some(&res), &resonances);
602
603 assert!(ext_with.len() >= data.len());
607 for pair in ext_with.windows(2) {
608 assert!(
609 pair[0] < pair[1],
610 "grid not sorted: {} >= {}",
611 pair[0],
612 pair[1]
613 );
614 }
615 assert_eq!(ext_without.len(), ext_with.len());
617 }
618
619 #[test]
623 fn grid_ends_exactly_at_its_bounds_through_the_merge() {
624 let data: Vec<f64> = (0..5).map(|i| 100.0 + f64::from(i)).collect();
625 let e_min = data[0];
626 let e_max = data[4];
627 for spacing in [Spacing::SqrtEnergy, Spacing::TimeOfFlight] {
630 let u_max = spacing.to_u(e_max);
631 let step = (u_max - spacing.to_u(data[0])) / 4.0;
632 let high = spacing.to_e(u_max + 7.0 * step * (1.0 + 3.0e-11));
633 let (grid, indices) = extend_boundaries(&data, e_min, high, spacing);
634 assert_eq!(*grid.last().unwrap(), high);
635 assert_eq!(grid.len(), data.len() + 7);
636 assert_eq!(indices, vec![0, 1, 2, 3, 4]);
637
638 let u_min = spacing.to_u(e_min);
639 let step = (spacing.to_u(data[4]) - u_min) / 4.0;
640 let low = spacing.to_e(u_min - 7.0 * step * (1.0 + 3.0e-11));
641 let (grid, indices) = extend_boundaries(&data, low, e_max, spacing);
642 assert_eq!(grid[0], low);
643 assert_eq!(grid.len(), data.len() + 7);
644 assert_eq!(indices, vec![7, 8, 9, 10, 11]);
645 }
646 let high = e_max * (1.0 + 5.0e-11);
648 let (grid, indices) = extend_boundaries(&data, e_min, high, Spacing::SqrtEnergy);
649 assert_eq!(*grid.last().unwrap(), high);
650 assert_eq!(grid.len(), data.len() + 1);
651 assert_eq!(indices, vec![0, 1, 2, 3, 4]);
652 let low = e_min * (1.0 - 5.0e-11);
653 let (grid, indices) = extend_boundaries(&data, low, e_max, Spacing::TimeOfFlight);
654 assert_eq!(grid[0], low);
655 assert_eq!(grid[1], e_min);
656 assert_eq!(grid.len(), data.len() + 1);
657 assert_eq!(indices, vec![1, 2, 3, 4, 5]);
658 }
659
660 #[test]
665 fn extension_stays_bounded_as_the_tail_nears_the_flight_time() {
666 use crate::resolution::{TOF_FACTOR, TabulatedResolution};
667 use std::sync::Arc;
668
669 let offsets: Vec<f64> = (0..=140).map(|k| -20.0 + k as f64).collect();
670 let weights = vec![1.0; offsets.len()];
671 let table = TabulatedResolution::from_kernels(
672 vec![100.0, 300.0],
673 vec![(offsets.clone(), weights.clone()), (offsets, weights)],
674 25.0,
675 )
676 .expect("valid two-block table");
677 let resolution = ResolutionFunction::Tabulated(Arc::new(table));
678
679 let mut previous: Option<usize> = None;
680 for tenths in 2200..=2300 {
681 let e_max = tenths as f64 / 10.0;
682 let data: Vec<f64> = (0..400).map(|i| e_max - 40.0 + i as f64 * 0.1).collect();
683 let (grid, _) = build_extended_grid_for(&data, &resolution);
684 let added = grid.len() - data.len();
685 let tof = |e: f64| TOF_FACTOR * 25.0 / e.sqrt();
686 let e_top = data[data.len() - 1];
687 let channels_above = tof(e_top) / (tof(data[data.len() - 2]) - tof(e_top));
688 let channels_below = 20.0 / (tof(data[0]) - tof(data[1]));
689 assert!(
690 (added as f64) <= channels_above + channels_below + 2.0,
691 "at e_max = {e_max} eV the extension added {added} points, more than the \
692 {channels_above:.0} + {channels_below:.0} channels the instrument has there"
693 );
694 if let Some(p) = previous {
695 assert!(
696 added <= 2 * p + 8 && p <= 2 * added + 8,
697 "the extension jumped from {p} to {added} points between neighbouring \
698 windows ending near {e_max} eV"
699 );
700 }
701 previous = Some(added);
702 }
703 }
704}