1use crate::resonance::*;
25use nereids_core::elements::isotope_from_za;
26
27const ENDF_RADIUS_TO_FM: f64 = 10.0;
37
38pub fn parse_endf_file2(endf_text: &str) -> Result<ResonanceData, EndfParseError> {
51 let data = parse_file2_records(endf_text)?;
52
53 if !data.has_evaluable_range() {
61 let detail = if data.ranges.is_empty() {
62 "the file carries no resonance-parameter ranges at all".to_string()
63 } else {
64 let skipped: Vec<String> = data.ranges.iter().map(|r| r.skip_description()).collect();
65 format!(
66 "every range in this file is a parse-and-skip placeholder: {}",
67 skipped.join("; ")
68 )
69 };
70 return Err(EndfParseError::UnsupportedFormat(format!(
71 "No evaluable resonance ranges: NEREIDS evaluates resolved LRF=3 \
72 (Reich-Moore) ranges, LRF=1/2 (SLBW/MLBW) ranges without a competitive \
73 width (LRX=0), and LRF=7 (R-Matrix Limited) ranges in the Reich-Moore \
74 form (KRM=3); {detail}. No resonance would contribute a cross section \
75 anywhere on the energy grid."
76 )));
77 }
78
79 Ok(data)
80}
81
82fn parse_file2_records(endf_text: &str) -> Result<ResonanceData, EndfParseError> {
84 let lines = section_151(endf_text, "2");
85
86 if lines.is_empty() {
87 return Err(EndfParseError::MissingSection(
88 "No MF=2, MT=151 data found".to_string(),
89 ));
90 }
91
92 let mut pos = 0;
93
94 let head = parse_cont(&lines, &mut pos)?;
96 let za = head.c1 as u32;
97 let awr = head.c2;
98 let nis = checked_count(head.n1, "NIS")?; if nis == 0 {
105 return Err(EndfParseError::UnsupportedFormat(
106 "MF=2 NIS=0: no isotopes declared. ENDF-6 §2.1 requires NIS >= 1 \
107 for a valid resonance evaluation."
108 .into(),
109 ));
110 }
111 if nis > 1 {
123 return Err(EndfParseError::UnsupportedFormat(format!(
124 "MF=2 NIS={nis} > 1 multi-isotope materials are not supported. \
125 Each NEREIDS ResonanceData represents a single isotope. \
126 For multi-isotope ENDF evaluations, split the material into per-isotope \
127 files or use sammy_to_resonance_data_multi from nereids_endf::sammy."
128 )));
129 }
130
131 let isotope = isotope_from_za(za)?;
132 let mut all_ranges = Vec::new();
133
134 for _ in 0..nis {
135 let iso_cont = parse_cont(&lines, &mut pos)?;
137 let _zai = iso_cont.c1 as u32;
138 let _abn = iso_cont.c2; let lfw = iso_cont.l2; let ner = checked_count(iso_cont.n1, "NER")?; for _ in 0..ner {
143 let range_cont = parse_cont(&lines, &mut pos)?;
145 let energy_low = range_cont.c1;
146 let energy_high = range_cont.c2;
147 let lru = range_cont.l1; let lrf = range_cont.l2; if lru == 2 {
151 let nro_urr = range_cont.n1; let naps_urr = range_cont.n2; let ap_table_urr = if nro_urr != 0 {
168 let mut tab = parse_tab1(&lines, &mut pos)?;
169 for pt in &mut tab.points {
170 pt.1 *= ENDF_RADIUS_TO_FM;
171 }
172 Some(tab)
173 } else {
174 None
175 };
176
177 if lrf != 1 && lrf != 2 {
183 return Err(EndfParseError::UnsupportedFormat(format!(
184 "LRU=2 (URR) with LRF={lrf}: ENDF-6 §2.2.2 restricts \
185 the unresolved region to LRF=1 or LRF=2"
186 )));
187 }
188
189 let (spi_urr, ap_urr_fm) = if lfw != 0 && lrf == 1 {
192 skip_urr_lfw1_lrf1(&lines, &mut pos)?
193 } else {
194 skip_urr_range(&lines, &mut pos, lrf)?
195 };
196
197 all_ranges.push(ResonanceRange {
198 energy_low,
199 energy_high,
200 resolved: false,
201 formalism: ResonanceFormalism::Unresolved,
202 target_spin: spi_urr,
203 scattering_radius: ap_urr_fm,
204 naps: naps_urr,
205 ap_table: ap_table_urr,
206 l_groups: Vec::new(),
207 r_external: vec![],
208 r_matrix_limited: None,
209 });
210 continue;
211 }
212
213 if lru == 0 {
214 let nro_lru0 = range_cont.n1;
225 let naps_lru0 = range_cont.n2;
226 let ap_table_lru0 = if nro_lru0 != 0 {
227 let mut tab = parse_tab1(&lines, &mut pos)?;
229 for pt in &mut tab.points {
230 pt.1 *= ENDF_RADIUS_TO_FM;
231 }
232 Some(tab)
233 } else {
234 None
235 };
236 let spi_cont = parse_cont(&lines, &mut pos)?;
241 if spi_cont.l1 != 0 {
245 return Err(EndfParseError::UnsupportedFormat(format!(
246 "LRU=0 range: L1={} in SPI/AP CONT record must be 0",
247 spi_cont.l1
248 )));
249 }
250 if spi_cont.l2 != 0 {
251 return Err(EndfParseError::UnsupportedFormat(format!(
252 "LRU=0 range: L2={} in SPI/AP CONT record must be 0",
253 spi_cont.l2
254 )));
255 }
256 if spi_cont.n1 != 0 {
257 return Err(EndfParseError::UnsupportedFormat(format!(
258 "LRU=0 range: NLS={} in SPI/AP CONT record must be 0 \
259 (scattering-radius-only ranges have no L-groups)",
260 spi_cont.n1
261 )));
262 }
263 if spi_cont.n2 != 0 {
264 return Err(EndfParseError::UnsupportedFormat(format!(
265 "LRU=0 range: N2={} in SPI/AP CONT record must be 0",
266 spi_cont.n2
267 )));
268 }
269 all_ranges.push(ResonanceRange {
270 energy_low,
271 energy_high,
272 resolved: false,
273 formalism: ResonanceFormalism::ScatteringRadiusOnly,
274 target_spin: spi_cont.c1,
275 scattering_radius: spi_cont.c2 * ENDF_RADIUS_TO_FM,
276 naps: naps_lru0,
277 ap_table: ap_table_lru0,
278 l_groups: Vec::new(),
279 r_external: vec![],
280 r_matrix_limited: None,
281 });
282 continue;
283 }
284
285 if lru != 1 {
286 return Err(EndfParseError::UnsupportedFormat(format!(
287 "LRU={} not supported (expected 0=scattering-radius-only, 1=resolved, or 2=unresolved)",
288 lru
289 )));
290 }
291
292 let nro = range_cont.n1; let naps = range_cont.n2; let ap_table = if nro != 0 {
299 let mut tab = parse_tab1(&lines, &mut pos)?;
300 for pt in &mut tab.points {
302 pt.1 *= ENDF_RADIUS_TO_FM;
303 }
304 Some(tab)
305 } else {
306 None
307 };
308
309 let formalism = match lrf {
316 1 => ResonanceFormalism::SLBW,
317 2 => ResonanceFormalism::MLBW,
318 3 => ResonanceFormalism::ReichMoore,
319 7 => ResonanceFormalism::RMatrixLimited,
320 _ => {
321 return Err(EndfParseError::UnsupportedFormat(format!(
322 "LRF={} not yet supported",
323 lrf
324 )));
325 }
326 };
327
328 let mut ctx = RangeParseContext {
329 lines: &lines,
330 pos: &mut pos,
331 energy_low,
332 energy_high,
333 naps,
334 ap_table,
335 };
336 let range = match formalism {
337 ResonanceFormalism::MLBW | ResonanceFormalism::SLBW => {
338 parse_bw_range(&mut ctx, formalism)?
339 }
340 ResonanceFormalism::ReichMoore => parse_reich_moore_range(&mut ctx)?,
341 ResonanceFormalism::RMatrixLimited => parse_rmatrix_limited_range(&mut ctx)?,
342 ResonanceFormalism::Unresolved => {
343 unreachable!("Unresolved formalism should not appear in LRU=1 dispatch");
345 }
346 ResonanceFormalism::ScatteringRadiusOnly => {
347 unreachable!(
350 "ScatteringRadiusOnly formalism should not appear in LRU=1 dispatch"
351 );
352 }
353 };
354 all_ranges.push(range);
355 }
356 }
357
358 if pos < lines.len() {
359 return Err(EndfParseError::UnsupportedFormat(
360 "Multiple materials detected in MF=2/MT=151: unconsumed data lines \
361 remain after parsing the first material. Multi-MAT files are not \
362 supported; split the file into single-material ENDF files."
363 .to_string(),
364 ));
365 }
366
367 let mat = lines[0].get(66..70).unwrap_or("").trim();
368 if let Some(lrp) = section(endf_text, mat, "1", "451")
369 .first()
370 .map(|line| parse_endf_int(line, 2))
371 .transpose()?
372 && lrp == 2
373 {
374 return Err(EndfParseError::UnsupportedFormat(
375 "LRP=2: File 3 holds the whole cross section and its resonance parameters are not \
376 to be added to it; only LRP=1 evaluations are supported"
377 .into(),
378 ));
379 }
380 Ok(ResonanceData {
381 isotope,
382 za,
383 awr,
384 smooth: parse_file3(endf_text, mat)?,
385 ranges: all_ranges,
386 })
387}
388
389fn section_151<'a>(endf_text: &'a str, mf: &str) -> Vec<&'a str> {
390 endf_text
391 .lines()
392 .filter(|line| line.len() >= 75 && line[70..72].trim() == mf && &line[72..75] == "151")
393 .collect()
394}
395
396fn section<'a>(endf_text: &'a str, mat: &str, mf: &str, mt: &str) -> Vec<&'a str> {
398 endf_text
399 .lines()
400 .filter(|line| {
401 line.len() >= 75
402 && line[66..70].trim() == mat
403 && line[70..72].trim() == mf
404 && line[72..75].trim() == mt
405 })
406 .collect()
407}
408
409fn parse_file3(endf_text: &str, mat: &str) -> Result<SmoothCrossSections, EndfParseError> {
413 let table = |mt: &str| -> Result<Option<Tab1>, EndfParseError> {
414 let lines = section(endf_text, mat, "3", mt);
415 if lines.is_empty() {
416 return Ok(None);
417 }
418 let mut pos = 1;
419 let table = parse_tab1_with(&lines, &mut pos, true)?;
420 if pos < lines.len() {
421 return Err(EndfParseError::UnsupportedFormat(format!(
422 "MF=3 MT={mt} of MAT {mat} holds records after its table"
423 )));
424 }
425 Ok(Some(table))
426 };
427 Ok(SmoothCrossSections {
428 total: table("1")?,
429 elastic: table("2")?,
430 capture: table("102")?,
431 fission: table("18")?,
432 })
433}
434
435const FILE32_TOLERANCE: f64 = 1e-5;
436
437pub fn parse_resonance_covariance(
461 endf_text: &str,
462 data: &ResonanceData,
463 window_ev: std::ops::RangeInclusive<f64>,
464) -> Result<Option<ResonanceCovariance>, EndfParseError> {
465 if window_ev.is_empty() {
466 return Err(EndfParseError::InvalidNumber(format!(
467 "the energy window {window_ev:?} must run from a lower to a higher energy"
468 )));
469 }
470 let lines = section_151(endf_text, "32");
471 if lines.is_empty() {
472 return Ok(None);
473 }
474 let known: Vec<([usize; 3], &Resonance)> = data
475 .ranges
476 .iter()
477 .enumerate()
478 .filter(|(_, range)| range.formalism == ResonanceFormalism::ReichMoore)
479 .flat_map(|(a, range)| {
480 range
481 .l_groups
482 .iter()
483 .enumerate()
484 .flat_map(move |(b, group)| {
485 let resonances = group.resonances.iter().enumerate();
486 resonances.map(move |(c, r)| ([a, b, c], r))
487 })
488 })
489 .collect();
490 let agree = |a: [f64; 3], b: [f64; 3]| {
491 a.iter()
492 .zip(b)
493 .all(|(a, b)| (a - b).abs() <= FILE32_TOLERANCE * a.abs())
494 };
495 let pos = &mut 0;
496 let (head, isotope) = (parse_cont(&lines, pos)?, parse_cont(&lines, pos)?);
497 if [head.c1, isotope.c1] != [f64::from(data.za); 2] || head.n1 != 1 {
498 return Err(EndfParseError::UnsupportedFormat(format!(
499 "File 32 is read for the one isotope `data` holds, ZA={}; got ZA={}, ZAI={}, NIS={}",
500 data.za, head.c1, isotope.c1, head.n1
501 )));
502 }
503 let ranges = checked_count(isotope.n1, "NER")?;
504 let (mut resonances, mut entries) = (Vec::new(), Vec::new());
505 for r in 0..ranges {
506 let range = parse_cont(&lines, pos)?;
507 if range.l1 == 2 && range.l2 == 1 && r + 1 == ranges {
508 let groups = checked_count(parse_cont(&lines, pos)?.n1, "NLS")?;
509 for _ in 0..=groups {
510 let list = parse_cont(&lines, pos)?;
511 parse_list_values(&lines, pos, checked_count(list.n1, "NPL")?)?;
512 }
513 break;
514 }
515 let (form, sections) = (parse_cont(&lines, pos)?, parse_cont(&lines, pos)?);
516 if range.l1 != 1
517 || range.l2 != 3
518 || range.n1 != 0
519 || form.l2 != 1
520 || form.n2 != 0
521 || sections.n2 != 0
522 {
523 return Err(EndfParseError::UnsupportedFormat(format!(
524 "File 32 is read for one isotope's resolved Reich-Moore ranges in the \
525 general form, without a scattering-radius covariance or long-range \
526 sections, followed by at most one unresolved range (LRF=1); got LRU={}, \
527 LRF={}, NRO={}, LCOMP={}, ISR={}, NLRS={}",
528 range.l1, range.l2, range.n1, form.l2, form.n2, sections.n2
529 )));
530 }
531 for _ in 0..checked_count(sections.n1, "NSRS")? {
532 let list = parse_cont(&lines, pos)?;
533 let count = checked_count(list.n2, "NRB")?;
534 let n = 3 * count;
535 let length = 6 * count + n * (n + 1) / 2;
536 if list.l1 != 3 || usize::try_from(list.n1) != Ok(length) {
537 return Err(EndfParseError::UnsupportedFormat(format!(
538 "File 32 is read over each resonance's energy, neutron width and capture \
539 width (MPAR=3, NPL=6*NRB+NVS); got MPAR={}, NPL={}, NRB={count}",
540 list.l1, list.n1
541 )));
542 }
543 let values = parse_list_values(&lines, pos, length)?;
544 if !values.iter().all(|v| v.is_finite()) {
545 return Err(EndfParseError::UnsupportedFormat(
546 "File 32 holds a number that is not finite".into(),
547 ));
548 }
549 let (table, packed) = values.split_at(6 * count);
550 let mut slots = Vec::new();
551 for (k, row) in table.chunks(6).enumerate() {
552 let (energy, spin) = (row[0], row[1]);
553 let mut same = known.iter().filter(|(_, r)| {
554 r.j == spin && agree([r.energy, r.gn, r.gg], [row[0], row[2], row[3]])
555 });
556 let found = match (same.next(), same.next()) {
557 (Some(&(at, r)), None) => Some((at, r.energy)),
558 _ => None,
559 };
560 if !window_ev.contains(&found.map_or(energy, |(_, e)| e)) {
561 continue;
562 }
563 let found = found.map(|(at, _)| at);
564 let Some(at) = found.filter(|at| slots.iter().all(|&(_, s)| resonances[s] != *at))
565 else {
566 return Err(EndfParseError::UnsupportedFormat(format!(
567 "File 32 lists a resonance at {energy} eV of spin {spin} that matches no \
568 Reich-Moore resonance of File 2 of that spin to a relative \
569 {FILE32_TOLERANCE:e} in energy and widths, more than one, or one its \
570 block already lists"
571 )));
572 };
573 if !resonances.contains(&at) {
574 resonances.push(at);
575 }
576 slots.push((k, resonances.iter().position(|&r| r == at).expect("listed")));
577 }
578 for &(k, a) in &slots {
579 for &(l, b) in &slots {
580 for (x, y) in (0..3).flat_map(|x| (0..3).map(move |y| (x, y))) {
581 let (u, v) = ((3 * k + x).min(3 * l + y), (3 * k + x).max(3 * l + y));
582 entries.push((3 * a + x, 3 * b + y, packed[u * n - u * (u + 1) / 2 + v]));
583 }
584 }
585 }
586 }
587 }
588 if *pos != lines.len() {
589 return Err(EndfParseError::UnsupportedFormat(
590 "File 32 holds records past its last range".into(),
591 ));
592 }
593 let n = 3 * resonances.len();
594 if n == 0 {
595 return Ok(None);
596 }
597 let mut covariance = vec![0.0; n * n];
598 for (x, y, value) in entries {
599 covariance[x * n + y] += value;
600 }
601 Ok(Some(ResonanceCovariance {
602 resonances,
603 covariance: covariance.into(),
604 }))
605}
606
607struct RangeParseContext<'a> {
613 lines: &'a [&'a str],
614 pos: &'a mut usize,
615 energy_low: f64,
616 energy_high: f64,
617 naps: i32,
618 ap_table: Option<Tab1>,
619}
620
621fn parse_bw_range(
631 ctx: &mut RangeParseContext<'_>,
632 formalism: ResonanceFormalism,
633) -> Result<ResonanceRange, EndfParseError> {
634 let cont = parse_cont(ctx.lines, ctx.pos)?;
637 let target_spin = cont.c1;
638 let scattering_radius = cont.c2 * ENDF_RADIUS_TO_FM;
639 let nls = checked_count(cont.n1, "NLS")?; if nls == 0 {
647 return Err(EndfParseError::UnsupportedFormat(format!(
648 "{formalism:?} range: NLS=0 (ENDF-6 §2.2.1.1 requires at least one \
649 L-group in a resolved range; NLS=0 is reserved for LRU=0)"
650 )));
651 }
652
653 let mut l_groups = Vec::with_capacity(nls);
654
655 for _ in 0..nls {
656 let l_cont = parse_cont(ctx.lines, ctx.pos)?;
658 let awr_l = l_cont.c1;
659 let qx = l_cont.c2; if l_cont.l1 < 0 {
662 return Err(EndfParseError::UnsupportedFormat(format!(
663 "BW range: negative L={}",
664 l_cont.l1
665 )));
666 }
667 let l_val = l_cont.l1 as u32;
668 let lrx = l_cont.l2; let n1 = checked_count(l_cont.n1, "N1")?; let nrs = checked_count(l_cont.n2, "NRS")?; if n1 != 6 * nrs {
675 return Err(EndfParseError::UnsupportedFormat(format!(
676 "BW range L={l_val}: N1={n1} != 6*NRS={} (NRS={nrs})",
677 6 * nrs
678 )));
679 }
680
681 let mut resonances = Vec::with_capacity(nrs);
682
683 let total_values = nrs * 6;
686 let values = parse_list_values(ctx.lines, ctx.pos, total_values)?;
687
688 for i in 0..nrs {
689 let base = i * 6;
690 resonances.push(Resonance {
691 energy: values[base], j: values[base + 1], gn: values[base + 3], gg: values[base + 4], gfa: values[base + 5], gfb: 0.0, });
700 }
701
702 l_groups.push(LGroup {
703 l: l_val,
704 awr: awr_l,
705 apl: 0.0, qx,
707 lrx,
708 resonances,
709 });
710 }
711
712 Ok(ResonanceRange {
722 energy_low: ctx.energy_low,
723 energy_high: ctx.energy_high,
724 resolved: true,
725 formalism,
726 target_spin,
727 scattering_radius,
728 naps: ctx.naps,
729 ap_table: ctx.ap_table.take(),
730 l_groups,
731 r_external: vec![],
732 r_matrix_limited: None,
733 })
734}
735
736fn parse_reich_moore_range(
748 ctx: &mut RangeParseContext<'_>,
749) -> Result<ResonanceRange, EndfParseError> {
750 let cont = parse_cont(ctx.lines, ctx.pos)?;
753 let target_spin = cont.c1;
754 let scattering_radius = cont.c2 * ENDF_RADIUS_TO_FM;
755 let nls = checked_count(cont.n1, "NLS")?; if nls == 0 {
763 return Err(EndfParseError::UnsupportedFormat(
764 "Reich-Moore range: NLS=0 (ENDF-6 §2.2.1.1 requires at least one \
765 L-group in a resolved range; NLS=0 is reserved for LRU=0)"
766 .to_string(),
767 ));
768 }
769
770 let mut l_groups = Vec::with_capacity(nls);
771
772 for _ in 0..nls {
773 let l_cont = parse_cont(ctx.lines, ctx.pos)?;
775 let awr_l = l_cont.c1;
776 let apl = l_cont.c2 * ENDF_RADIUS_TO_FM; if l_cont.l1 < 0 {
779 return Err(EndfParseError::UnsupportedFormat(format!(
780 "Reich-Moore range: negative L={}",
781 l_cont.l1
782 )));
783 }
784 let l_val = l_cont.l1 as u32;
785 let n1 = checked_count(l_cont.n1, "N1")?; let nrs = checked_count(l_cont.n2, "NRS")?; if n1 != 6 * nrs {
791 return Err(EndfParseError::UnsupportedFormat(format!(
792 "Reich-Moore range L={l_val}: N1={n1} != 6*NRS={} (NRS={nrs})",
793 6 * nrs
794 )));
795 }
796
797 let mut resonances = Vec::with_capacity(nrs);
798
799 let total_values = nrs * 6;
801 let values = parse_list_values(ctx.lines, ctx.pos, total_values)?;
802
803 for i in 0..nrs {
804 let base = i * 6;
805 resonances.push(Resonance {
806 energy: values[base], j: values[base + 1], gn: values[base + 2], gg: values[base + 3], gfa: values[base + 4], gfb: values[base + 5], });
813 }
814
815 l_groups.push(LGroup {
816 l: l_val,
817 awr: awr_l,
818 apl,
819 qx: 0.0, lrx: 0, resonances,
822 });
823 }
824
825 Ok(ResonanceRange {
835 energy_low: ctx.energy_low,
836 energy_high: ctx.energy_high,
837 resolved: true,
838 formalism: ResonanceFormalism::ReichMoore,
839 target_spin,
840 scattering_radius,
841 naps: ctx.naps,
842 ap_table: ctx.ap_table.take(),
843 l_groups,
844 r_external: vec![],
845 r_matrix_limited: None,
846 })
847}
848
849fn parse_rmatrix_limited_range(
862 ctx: &mut RangeParseContext<'_>,
863) -> Result<ResonanceRange, EndfParseError> {
864 if ctx.ap_table.is_some() {
865 return Err(rml_error(
866 "NRO=1 (energy-dependent AP) is not supported; LRF=7 gives radii per channel",
867 ));
868 }
869
870 let cont = parse_cont(ctx.lines, ctx.pos)?;
872 let target_spin = cont.c1;
873 let scattering_radius = cont.c2 * ENDF_RADIUS_TO_FM;
874 if cont.l1 != 0 {
875 return Err(rml_error(format!(
876 "IFG={} (reduced-width amplitudes) is not supported; only IFG=0 (widths in eV)",
877 cont.l1
878 )));
879 }
880 if cont.l2 != 3 {
881 return Err(rml_error(format!(
882 "KRM={} is not supported; only KRM=3 (Reich-Moore) is evaluated",
883 cont.l2
884 )));
885 }
886 let njs = checked_count(cont.n1, "NJS")?;
887 if cont.n2 != 0 {
888 return Err(rml_error(format!(
889 "KRL={} (relativistic kinematics) is not supported; only KRL=0",
890 cont.n2
891 )));
892 }
893
894 let pair_head = parse_cont(ctx.lines, ctx.pos)?;
896 let npp = checked_count(pair_head.l1, "NPP")?;
897 let npl = checked_count(pair_head.n1, "NPL")?;
898 if npl != 12 * npp {
899 return Err(rml_error(format!(
900 "particle-pair LIST: NPL={npl} != 12*NPP={}",
901 12 * npp
902 )));
903 }
904 let pair_values = parse_list_values(ctx.lines, ctx.pos, npl)?;
905 let pairs = pair_values
906 .chunks_exact(12)
907 .enumerate()
908 .map(|(i, values)| parse_particle_pair(i + 1, values))
909 .collect::<Result<Vec<_>, _>>()?;
910
911 let photon_pair = sole_index(&pairs, |p| p.mass_a == 0.0 && p.mt == 102).ok_or_else(|| {
912 rml_error("KRM=3 needs exactly one photon pair (MA=0, MT=102) for the eliminated channel")
913 })?;
914 let incident_pair = sole_index(&pairs, |p| p.mt == 2)
915 .ok_or_else(|| rml_error("exactly one particle pair must have MT=2 (neutron + target)"))?;
916 let incident = &pairs[incident_pair];
917 if incident.mass_a != 1.0 || incident.charge_a != 0.0 || incident.q_value != 0.0 {
918 return Err(rml_error(format!(
919 "the MT=2 pair must be a neutron with Q=0 (MA={}, ZA={}, Q={})",
920 incident.mass_a, incident.charge_a, incident.q_value
921 )));
922 }
923 for (i, pair) in pairs.iter().enumerate() {
924 let threshold = pair.threshold(incident);
925 if pair.shift && threshold > ctx.energy_low {
926 return Err(rml_error(format!(
927 "particle pair {}: SHF=1 on a channel with threshold {threshold:.6e} eV inside \
928 the range is not supported (closed-channel shift factor)",
929 i + 1
930 )));
931 }
932 }
933
934 let mut spin_groups = Vec::with_capacity(njs);
935 for group in 1..=njs {
936 spin_groups.push(parse_rml_spin_group(
937 ctx,
938 group,
939 &pairs,
940 photon_pair,
941 incident_pair,
942 )?);
943 }
944
945 Ok(ResonanceRange {
946 energy_low: ctx.energy_low,
947 energy_high: ctx.energy_high,
948 resolved: true,
949 formalism: ResonanceFormalism::RMatrixLimited,
950 target_spin,
951 scattering_radius,
952 naps: ctx.naps,
953 ap_table: None,
954 l_groups: Vec::new(),
955 r_external: vec![],
956 r_matrix_limited: Some(RmlRange {
957 pairs,
958 incident_pair,
959 photon_pair,
960 spin_groups,
961 }),
962 })
963}
964
965fn rml_error(message: impl std::fmt::Display) -> EndfParseError {
967 EndfParseError::UnsupportedFormat(format!("LRF=7 {message}"))
968}
969
970fn sole_index<T>(items: &[T], predicate: impl Fn(&T) -> bool) -> Option<usize> {
972 let mut found = None;
973 for (i, item) in items.iter().enumerate() {
974 if predicate(item) {
975 if found.is_some() {
976 return None;
977 }
978 found = Some(i);
979 }
980 }
981 found
982}
983
984fn rml_integer(value: f64, field: &str, location: &str) -> Result<i64, EndfParseError> {
986 if !value.is_finite() || value.fract() != 0.0 || value.abs() > f64::from(MAX_ENDF_COUNT) {
987 return Err(rml_error(format!(
988 "{location}: {field}={value} is not an integer"
989 )));
990 }
991 Ok(value as i64)
992}
993
994fn parse_particle_pair(number: usize, values: &[f64]) -> Result<ParticlePair, EndfParseError> {
999 let location = format!("particle pair {number}");
1000 let &[ma, mb, za, zb, ia, ib, q, pnt, shf, mt, _pa, _pb] = values else {
1001 return Err(rml_error(format!("{location}: expected 12 values")));
1002 };
1003 for (field, value) in [
1004 ("MA", ma),
1005 ("MB", mb),
1006 ("ZA", za),
1007 ("ZB", zb),
1008 ("IA", ia),
1009 ("IB", ib),
1010 ("Q", q),
1011 ] {
1012 if !value.is_finite() {
1013 return Err(rml_error(format!(
1014 "{location}: {field}={value} is not finite"
1015 )));
1016 }
1017 }
1018 let pnt = rml_integer(pnt, "PNT", &location)?;
1019 let shf = rml_integer(shf, "SHF", &location)?;
1020 let mt = rml_integer(mt, "MT", &location)?;
1021 if mt <= 0 {
1022 return Err(rml_error(format!(
1023 "{location}: MT={mt} is not a reaction type"
1024 )));
1025 }
1026 let mt = mt as u32;
1027 let penetrability = match pnt {
1028 1 => true,
1029 -1 => false,
1030 0 => !(mt == 102 || mt == 19),
1031 _ => {
1032 return Err(rml_error(format!(
1033 "{location}: PNT={pnt} is not defined (ENDF-6 allows -1, 0 and 1)"
1034 )));
1035 }
1036 };
1037 let shift = match shf {
1038 0 => false,
1039 1 => true,
1040 _ => {
1041 return Err(rml_error(format!(
1042 "{location}: SHF={shf} is not defined (ENDF-6 allows 0 and 1)"
1043 )));
1044 }
1045 };
1046 if ma == 0.0 && penetrability {
1047 return Err(rml_error(format!(
1048 "{location}: a photon pair (MA=0) cannot calculate the penetrability (PNT={pnt}, MT={mt})"
1049 )));
1050 }
1051 if shift && !penetrability {
1052 return Err(rml_error(format!(
1053 "{location}: SHF=1 needs the penetrability calculated (PNT={pnt}, MT={mt})"
1054 )));
1055 }
1056 if penetrability {
1057 let reduced_mass = ma * mb / (ma + mb);
1058 if !(ma > 0.0 && mb > 0.0 && reduced_mass.is_finite() && reduced_mass > 0.0) {
1059 return Err(rml_error(format!(
1060 "{location}: a penetrating pair needs positive masses (MA={ma}, MB={mb})"
1061 )));
1062 }
1063 }
1064 if matches!(mt, 18..=21 | 38) {
1065 return Err(rml_error(format!(
1066 "{location}: fission pair (MT={mt}) is not supported"
1067 )));
1068 }
1069 Ok(ParticlePair {
1070 mass_a: ma,
1071 mass_b: mb,
1072 charge_a: za,
1073 charge_b: zb,
1074 spin_a: ia,
1075 spin_b: ib,
1076 q_value: q,
1077 mt,
1078 penetrability,
1079 shift,
1080 })
1081}
1082
1083fn parse_rml_spin_group(
1085 ctx: &mut RangeParseContext<'_>,
1086 group: usize,
1087 pairs: &[ParticlePair],
1088 photon_pair: usize,
1089 incident_pair: usize,
1090) -> Result<RmlSpinGroup, EndfParseError> {
1091 let location = format!("spin group {group}");
1092
1093 let head = parse_cont(ctx.lines, ctx.pos)?;
1095 let (aj, pj) = (head.c1, head.c2);
1096 if head.l2 != 0 {
1097 return Err(rml_error(format!(
1098 "{location}: KPS={} (tabulated phase shifts) is not supported",
1099 head.l2
1100 )));
1101 }
1102 let kbk = checked_count(head.l1, "KBK")?;
1103 let nch = checked_count(head.n2, "NCH")?;
1104 let npl = checked_count(head.n1, "NPL")?;
1105 if npl != 6 * nch {
1106 return Err(rml_error(format!(
1107 "{location}: channel LIST NPL={npl} != 6*NCH={}",
1108 6 * nch
1109 )));
1110 }
1111 if !aj.is_finite() || (2.0 * aj).fract() != 0.0 {
1112 return Err(rml_error(format!(
1113 "{location}: AJ={aj} is not a multiple of 1/2"
1114 )));
1115 }
1116 let parity = if aj < 0.0 || (aj == 0.0 && pj < 0.0) {
1117 -1
1118 } else {
1119 1
1120 };
1121
1122 let channel_values = parse_list_values(ctx.lines, ctx.pos, npl)?;
1123 let mut channels = Vec::with_capacity(nch);
1124 for (c, v) in channel_values.chunks_exact(6).enumerate() {
1125 let channel_location = format!("{location} channel {}", c + 1);
1126 let ppi = rml_integer(v[0], "PPI", &channel_location)?;
1127 if ppi < 1 || ppi as usize > pairs.len() {
1128 return Err(rml_error(format!(
1129 "{channel_location}: PPI={ppi} is out of range 1..={}",
1130 pairs.len()
1131 )));
1132 }
1133 let l = rml_integer(v[1], "L", &channel_location)?;
1134 if l < 0 {
1135 return Err(rml_error(format!("{channel_location}: L={l} is negative")));
1136 }
1137 let (sch, bnd, ape, apt) = (v[2], v[3], v[4], v[5]);
1138 if ![sch, bnd, ape, apt].iter().all(|x| x.is_finite()) || ape < 0.0 || apt < 0.0 {
1139 return Err(rml_error(format!(
1140 "{channel_location}: SCH={sch}, BND={bnd}, APE={ape}, APT={apt} must be finite \
1141 with non-negative radii"
1142 )));
1143 }
1144 let pair = ppi as usize - 1;
1145 if pairs[pair].penetrability && apt == 0.0 {
1146 return Err(rml_error(format!(
1147 "{channel_location}: APT=0 for a pair that calculates the penetrability"
1148 )));
1149 }
1150 channels.push(RmlChannel {
1151 pair,
1152 l: l as u32,
1153 channel_spin: sch,
1154 boundary: bnd,
1155 radius_phase: ape * ENDF_RADIUS_TO_FM,
1156 radius_true: apt * ENDF_RADIUS_TO_FM,
1157 });
1158 }
1159 if channels.iter().filter(|c| c.pair == photon_pair).count() != 1 {
1160 return Err(rml_error(format!(
1161 "{location}: needs exactly one channel of the photon pair (the eliminated channel)"
1162 )));
1163 }
1164 if !channels.iter().any(|c| c.pair == incident_pair) {
1165 return Err(rml_error(format!(
1166 "{location}: has no neutron entrance channel"
1167 )));
1168 }
1169
1170 let res_head = parse_cont(ctx.lines, ctx.pos)?;
1173 let nrs = checked_count(res_head.l2, "NRS")?;
1174 let nx = checked_count(res_head.n2, "NX")?;
1175 let res_npl = checked_count(res_head.n1, "NPL")?;
1176 let lines_per_resonance = nch / 6 + 1;
1177 let expected_nx = if nrs == 0 {
1178 1
1179 } else {
1180 nrs * lines_per_resonance
1181 };
1182 if nx != expected_nx || res_npl != 6 * nx {
1183 return Err(rml_error(format!(
1184 "{location}: resonance LIST NX={nx}, NPL={res_npl}; NRS={nrs} and NCH={nch} need \
1185 NX={expected_nx} and NPL={}",
1186 6 * expected_nx
1187 )));
1188 }
1189 let res_values = parse_list_values(ctx.lines, ctx.pos, res_npl)?;
1190 let mut resonances = Vec::with_capacity(nrs);
1191 if nrs > 0 {
1192 for row in res_values.chunks_exact(6 * lines_per_resonance) {
1193 let energy = row[0];
1194 let widths = row[1..=nch].to_vec();
1195 if !energy.is_finite() || !widths.iter().all(|w| w.is_finite()) {
1196 return Err(rml_error(format!(
1197 "{location}: resonance at ER={energy} has a non-finite value"
1198 )));
1199 }
1200 for (channel, &width) in channels.iter().zip(&widths) {
1202 let pair = &pairs[channel.pair];
1203 if pair.penetrability
1204 && width != 0.0
1205 && energy == pair.threshold(&pairs[incident_pair])
1206 {
1207 return Err(rml_error(format!(
1208 "{location}: resonance at ER={energy} eV sits on the threshold of a \
1209 penetrating channel with width {width} eV"
1210 )));
1211 }
1212 }
1213 resonances.push(RmlResonance { energy, widths });
1214 }
1215 }
1216
1217 let mut background = Vec::new();
1219 let mut seen = vec![false; nch];
1220 for _ in 0..kbk {
1221 let record = parse_cont(ctx.lines, ctx.pos)?;
1222 let lch = checked_count(record.l1, "LCH")?;
1223 if lch == 0 || lch > nch {
1224 return Err(rml_error(format!(
1225 "{location}: background LCH={lch} is out of range 1..={nch}"
1226 )));
1227 }
1228 let channel = lch - 1;
1229 if seen[channel] {
1230 return Err(rml_error(format!(
1231 "{location}: background given twice for channel {lch}"
1232 )));
1233 }
1234 seen[channel] = true;
1235 if channels[channel].pair == photon_pair {
1236 return Err(rml_error(format!(
1237 "{location}: background on the eliminated photon channel {lch}"
1238 )));
1239 }
1240 match record.l2 {
1241 0 => {}
1242 2 => {
1243 let list = parse_cont(ctx.lines, ctx.pos)?;
1245 if list.n1 != 5 {
1246 return Err(rml_error(format!(
1247 "{location}: LBK=2 LIST has NPL={}, expected 5",
1248 list.n1
1249 )));
1250 }
1251 let v = parse_list_values(ctx.lines, ctx.pos, 5)?;
1252 let (e_down, e_up) = (list.c1, list.c2);
1253 let valid = e_down < ctx.energy_low
1254 && e_up > ctx.energy_high
1255 && v.iter().all(|x| x.is_finite());
1256 if !valid {
1257 return Err(rml_error(format!(
1258 "{location}: LBK=2 needs finite terms and ED < EL, EU > EH \
1259 (ED={e_down}, EU={e_up})"
1260 )));
1261 }
1262 background.push(RmlBackground {
1263 channel,
1264 e_down,
1265 e_up,
1266 r_con: v[0],
1267 r_lin: v[1],
1268 r_quad: v[2],
1269 s_con: v[3],
1270 s_lin: v[4],
1271 });
1272 }
1273 1 | 3 => {
1274 return Err(rml_error(format!(
1275 "{location}: LBK={} background is not supported (only LBK=0 and 2)",
1276 record.l2
1277 )));
1278 }
1279 lbk => {
1280 return Err(rml_error(format!("{location}: LBK={lbk} is not defined")));
1281 }
1282 }
1283 }
1284
1285 Ok(RmlSpinGroup {
1286 j: aj.abs(),
1287 parity,
1288 channels,
1289 resonances,
1290 background,
1291 })
1292}
1293
1294const MAX_ENDF_COUNT: i32 = 1_000_000;
1301
1302fn checked_count(value: i32, label: &str) -> Result<usize, EndfParseError> {
1308 if value < 0 {
1309 return Err(EndfParseError::UnsupportedFormat(format!(
1310 "Negative ENDF count: {label}={value}"
1311 )));
1312 }
1313 if value > MAX_ENDF_COUNT {
1314 return Err(EndfParseError::UnsupportedFormat(format!(
1315 "ENDF count too large: {label}={value} (maximum {MAX_ENDF_COUNT})"
1316 )));
1317 }
1318 Ok(value as usize)
1319}
1320
1321struct ContRecord {
1327 c1: f64,
1328 c2: f64,
1329 l1: i32,
1330 l2: i32,
1331 n1: i32,
1332 n2: i32,
1333}
1334
1335fn parse_cont(lines: &[&str], pos: &mut usize) -> Result<ContRecord, EndfParseError> {
1337 if *pos >= lines.len() {
1338 return Err(EndfParseError::UnexpectedEof(
1339 "Expected CONT record but reached end of data".to_string(),
1340 ));
1341 }
1342 let line = lines[*pos];
1343 *pos += 1;
1344
1345 Ok(ContRecord {
1346 c1: parse_endf_float(line, 0)?,
1347 c2: parse_endf_float(line, 1)?,
1348 l1: parse_endf_int(line, 2)?,
1349 l2: parse_endf_int(line, 3)?,
1350 n1: parse_endf_int(line, 4)?,
1351 n2: parse_endf_int(line, 5)?,
1352 })
1353}
1354
1355fn parse_list_values(
1359 lines: &[&str],
1360 pos: &mut usize,
1361 n_values: usize,
1362) -> Result<Vec<f64>, EndfParseError> {
1363 let mut values = Vec::with_capacity(n_values.min(6 * lines.len().saturating_sub(*pos)));
1364 let n_lines = n_values.div_ceil(6);
1365
1366 for _ in 0..n_lines {
1367 if *pos >= lines.len() {
1368 return Err(EndfParseError::UnexpectedEof(
1369 "Expected LIST data but reached end".to_string(),
1370 ));
1371 }
1372 let line = lines[*pos];
1373 *pos += 1;
1374
1375 let remaining = n_values - values.len();
1376 let fields_on_line = remaining.min(6);
1377
1378 for field in 0..fields_on_line {
1379 values.push(parse_endf_float(line, field)?);
1380 }
1381 }
1382
1383 Ok(values)
1384}
1385
1386fn parse_endf_float(line: &str, field_index: usize) -> Result<f64, EndfParseError> {
1393 let start = field_index * 11;
1394 let end = start + 11;
1395
1396 if line.len() < end {
1397 return Ok(0.0);
1399 }
1400
1401 let field = &line[start..end];
1402 let trimmed = field.trim();
1403
1404 if trimmed.is_empty() {
1405 return Ok(0.0);
1406 }
1407
1408 if let Ok(v) = trimmed.parse::<f64>() {
1410 return Ok(v);
1411 }
1412
1413 let bytes = trimmed.as_bytes();
1416 for i in 1..bytes.len() {
1417 if (bytes[i] == b'+' || bytes[i] == b'-')
1418 && bytes[i - 1] != b'e'
1419 && bytes[i - 1] != b'E'
1420 && bytes[i - 1] != b'd'
1421 && bytes[i - 1] != b'D'
1422 && bytes[i - 1] != b'+'
1423 && bytes[i - 1] != b'-'
1424 {
1425 let mantissa = &trimmed[..i];
1426 let exp_slice = &trimmed[i..];
1427 let with_e = if exp_slice.contains(' ') {
1430 let exponent: String = exp_slice.chars().filter(|c| !c.is_whitespace()).collect();
1431 format!("{}E{}", mantissa, exponent)
1432 } else {
1433 format!("{}E{}", mantissa, exp_slice)
1434 };
1435 if let Ok(v) = with_e.parse::<f64>() {
1436 return Ok(v);
1437 }
1438 }
1439 }
1440
1441 Err(EndfParseError::InvalidNumber(format!(
1442 "Cannot parse ENDF float: '{}'",
1443 field
1444 )))
1445}
1446
1447fn parse_endf_int(line: &str, field_index: usize) -> Result<i32, EndfParseError> {
1449 let start = field_index * 11;
1450 let end = start + 11;
1451
1452 if line.len() < end {
1453 return Ok(0);
1454 }
1455
1456 let field = &line[start..end];
1457 let trimmed = field.trim();
1458
1459 if trimmed.is_empty() {
1460 return Ok(0);
1461 }
1462
1463 if let Ok(v) = trimmed.parse::<i32>() {
1466 return Ok(v);
1467 }
1468
1469 if let Ok(v) = parse_endf_float(line, field_index) {
1475 if (v - v.round()).abs() > 1e-6 {
1476 return Err(EndfParseError::InvalidNumber(format!(
1477 "Non-integral value in ENDF int field: '{}' (={})",
1478 field, v
1479 )));
1480 }
1481 return Ok(v.round() as i32);
1482 }
1483
1484 Err(EndfParseError::InvalidNumber(format!(
1485 "Cannot parse ENDF int: '{}'",
1486 field
1487 )))
1488}
1489
1490fn skip_urr_lfw1_lrf1(lines: &[&str], pos: &mut usize) -> Result<(f64, f64), EndfParseError> {
1506 let header = parse_cont(lines, pos)?;
1508 let spi = header.c1;
1509 let ap_fm = header.c2 * ENDF_RADIUS_TO_FM;
1510 let ne = checked_count(header.n1, "NE")?;
1511 let nls = checked_count(header.n2, "NLS")?;
1512
1513 parse_list_values(lines, pos, ne)?;
1515
1516 for _ in 0..nls {
1517 let l_cont = parse_cont(lines, pos)?;
1519 let njs = checked_count(l_cont.n1, "NJS")?;
1520
1521 if njs == 0 {
1526 return Err(EndfParseError::UnsupportedFormat(format!(
1527 "URR LFW=1/LRF=1 L={}: NJS=0 (at least one J-group required)",
1528 l_cont.l1
1529 )));
1530 }
1531
1532 for _ in 0..njs {
1533 let j_cont = parse_cont(lines, pos)?;
1536 let n1 = checked_count(j_cont.n1, "N1")?;
1537 if n1 != ne + 6 {
1539 return Err(EndfParseError::UnsupportedFormat(format!(
1540 "URR LFW=1/LRF=1: per-J N1={n1} ≠ NE+6={} (NE={ne})",
1541 ne + 6
1542 )));
1543 }
1544 parse_list_values(lines, pos, n1)?;
1546 }
1547 }
1548
1549 Ok((spi, ap_fm))
1550}
1551
1552fn skip_urr_range(lines: &[&str], pos: &mut usize, lrf: i32) -> Result<(f64, f64), EndfParseError> {
1569 let spi_cont = parse_cont(lines, pos)?;
1571 let spi = spi_cont.c1;
1572 let ap_fm = spi_cont.c2 * ENDF_RADIUS_TO_FM;
1573 let nls = checked_count(spi_cont.n1, "NLS")?;
1574
1575 if lrf == 1 {
1576 for _ in 0..nls {
1578 let l_cont = parse_cont(lines, pos)?;
1580 if l_cont.l1 < 0 {
1581 return Err(EndfParseError::UnsupportedFormat(format!(
1582 "URR LRF=1: negative L={}",
1583 l_cont.l1
1584 )));
1585 }
1586 let l = l_cont.l1 as u32;
1587 let n1 = checked_count(l_cont.n1, "N1")?; let njs = checked_count(l_cont.n2, "NJS")?;
1589 if njs == 0 || n1 != 6 * njs {
1590 return Err(EndfParseError::UnsupportedFormat(format!(
1591 "URR LRF=1 L={l}: N1={n1} ≠ 6×NJS={} (NJS={njs})",
1592 6 * njs
1593 )));
1594 }
1595 parse_list_values(lines, pos, n1)?;
1597 }
1598 } else {
1599 for _ in 0..nls {
1601 let l_cont = parse_cont(lines, pos)?;
1603 if l_cont.l1 < 0 {
1604 return Err(EndfParseError::UnsupportedFormat(format!(
1605 "URR LRF=2: negative L={}",
1606 l_cont.l1
1607 )));
1608 }
1609 let l = l_cont.l1 as u32;
1610 let njs = checked_count(l_cont.n1, "NJS")?; if njs == 0 {
1615 return Err(EndfParseError::UnsupportedFormat(format!(
1616 "URR LRF=2 L={l}: NJS=0 (at least one J-group required)"
1617 )));
1618 }
1619
1620 for _ in 0..njs {
1621 let j_cont = parse_cont(lines, pos)?;
1623 let int_code = j_cont.l1; if !(1..=5).contains(&int_code) {
1628 return Err(EndfParseError::UnsupportedFormat(format!(
1629 "URR LRF=2: INT={int_code} out of spec (expected 1..=5)"
1630 )));
1631 }
1632 let n1 = checked_count(j_cont.n1, "N1")?; let ne = checked_count(j_cont.n2, "NE")?; let expected_n1 = 6 * (ne + 1);
1638 if n1 != expected_n1 {
1639 return Err(EndfParseError::UnsupportedFormat(format!(
1640 "URR LRF=2: N1={n1} ≠ 6*(NE+1)={expected_n1} (NE={ne})"
1641 )));
1642 }
1643 parse_list_values(lines, pos, n1)?;
1645 }
1646 }
1647 }
1648
1649 Ok((spi, ap_fm))
1650}
1651
1652fn parse_tab1(lines: &[&str], pos: &mut usize) -> Result<Tab1, EndfParseError> {
1663 parse_tab1_with(lines, pos, false)
1664}
1665
1666fn parse_tab1_with(lines: &[&str], pos: &mut usize, jumps: bool) -> Result<Tab1, EndfParseError> {
1675 let cont = parse_cont(lines, pos)?;
1676 let nr = checked_count(cont.n1, "NR")?; let np = checked_count(cont.n2, "NP")?; let interp_raw = parse_list_values(lines, pos, nr * 2)?;
1689 let mut boundaries = Vec::with_capacity(nr);
1690 let mut interp_codes = Vec::with_capacity(nr);
1691 for i in 0..nr {
1692 let nbt_raw = interp_raw[i * 2];
1693 let int_raw = interp_raw[i * 2 + 1];
1694
1695 if (nbt_raw - nbt_raw.round()).abs() > 1e-6 || nbt_raw < 1.0 {
1700 return Err(EndfParseError::UnsupportedFormat(format!(
1701 "TAB1 NBT[{}] is not a positive integer: {}",
1702 i, nbt_raw
1703 )));
1704 }
1705 if (int_raw - int_raw.round()).abs() > 1e-6 {
1706 return Err(EndfParseError::UnsupportedFormat(format!(
1707 "TAB1 INT[{}] is not an integer: {}",
1708 i, int_raw
1709 )));
1710 }
1711 let int_code = int_raw.round() as u32;
1712 if !(1..=5).contains(&int_code) {
1713 return Err(EndfParseError::UnsupportedFormat(format!(
1714 "TAB1 INT[{}]={} is out of range 1..=5",
1715 i, int_code
1716 )));
1717 }
1718 let nbt = nbt_raw.round() as usize;
1719
1720 if let Some(&prev) = boundaries.last()
1722 && nbt <= prev
1723 {
1724 return Err(EndfParseError::UnsupportedFormat(format!(
1725 "TAB1 NBT[{}]={} is not greater than NBT[{}]={}",
1726 i,
1727 nbt,
1728 i - 1,
1729 prev
1730 )));
1731 }
1732 boundaries.push(nbt);
1733 interp_codes.push(int_code);
1734 }
1735
1736 if nr > 0 {
1738 let last_nbt = *boundaries.last().unwrap();
1739 if last_nbt != np {
1740 return Err(EndfParseError::UnsupportedFormat(format!(
1741 "TAB1 last NBT={} does not equal NP={}",
1742 last_nbt, np
1743 )));
1744 }
1745 }
1746
1747 if np == 0 {
1748 return Err(EndfParseError::UnsupportedFormat(
1749 "TAB1 NP=0: table must have at least one point".to_string(),
1750 ));
1751 }
1752
1753 let data_raw = parse_list_values(lines, pos, np * 2)?;
1755 let mut points = Vec::with_capacity(np);
1756 for i in 0..np {
1757 let x = data_raw[i * 2];
1758 let y = data_raw[i * 2 + 1];
1759 if !(x.is_finite() && y.is_finite()) {
1760 return Err(EndfParseError::UnsupportedFormat(format!(
1761 "TAB1 point {i} ({x}, {y}) is not finite"
1762 )));
1763 }
1764 if jumps
1766 && let &[.., (x_first, _), (x_last, y_last)] = points.as_slice()
1767 && x_first == x
1768 && x_last == x
1769 && y_last != y
1770 {
1771 points.pop();
1772 drop_point(&mut boundaries, &mut interp_codes, points.len());
1773 }
1774 if jumps && points.last() == Some(&(x, y)) {
1775 drop_point(&mut boundaries, &mut interp_codes, points.len());
1776 continue;
1777 }
1778 let jump = jumps && points.last().is_some_and(|&(x_prev, _)| x == x_prev);
1781 if let Some(&(x_prev, _)) = points.last()
1782 && x <= x_prev
1783 && !jump
1784 {
1785 return Err(EndfParseError::UnsupportedFormat(format!(
1786 "TAB1 x[{}]={} is not greater than x[{}]={} (x must be strictly increasing)",
1787 i,
1788 x,
1789 i - 1,
1790 x_prev
1791 )));
1792 }
1793 points.push((x, y));
1794 }
1795
1796 Ok(Tab1 {
1797 boundaries,
1798 interp_codes,
1799 points,
1800 })
1801}
1802
1803fn drop_point(boundaries: &mut Vec<usize>, interp_codes: &mut Vec<u32>, index: usize) {
1806 for nbt in boundaries.iter_mut().filter(|nbt| **nbt > index) {
1807 *nbt -= 1;
1808 }
1809 let empty = if boundaries.first() == Some(&1) {
1811 Some(0)
1812 } else {
1813 (1..boundaries.len()).find(|&r| boundaries[r] == boundaries[r - 1])
1814 };
1815 if let Some(empty) = empty {
1816 boundaries.remove(empty);
1817 interp_codes.remove(empty);
1818 }
1819}
1820
1821#[derive(Debug, thiserror::Error)]
1823pub enum EndfParseError {
1824 #[error("Missing section: {0}")]
1825 MissingSection(String),
1826
1827 #[error("Unsupported format: {0}")]
1828 UnsupportedFormat(String),
1829
1830 #[error("Invalid number: {0}")]
1831 InvalidNumber(String),
1832
1833 #[error("Unexpected end of file: {0}")]
1834 UnexpectedEof(String),
1835
1836 #[error("Invalid isotope: {0}")]
1837 InvalidIsotope(#[from] nereids_core::error::NereidsError),
1838}
1839
1840#[cfg(test)]
1841mod tests {
1842 use super::*;
1843
1844 #[test]
1845 fn test_parse_endf_float_standard() {
1846 let line = " 1.23456+02 2.34567-01 0.00000+00 ";
1850 assert!((parse_endf_float(line, 0).unwrap() - 123.456).abs() < 0.01);
1851 assert!((parse_endf_float(line, 1).unwrap() - 0.234567).abs() < 1e-6);
1852 assert!((parse_endf_float(line, 2).unwrap() - 0.0).abs() < 1e-10);
1853 }
1854
1855 #[test]
1856 fn test_parse_endf_float_with_e() {
1857 let line = "1.23456E+022.34567E-01 ";
1859 assert!((parse_endf_float(line, 0).unwrap() - 123.456).abs() < 0.01);
1860 assert!((parse_endf_float(line, 1).unwrap() - 0.234567).abs() < 1e-6);
1861 }
1862
1863 #[test]
1864 fn test_parse_endf_float_negative() {
1865 let line = "-1.23456+02-2.34567-01 ";
1866 assert!((parse_endf_float(line, 0).unwrap() - (-123.456)).abs() < 0.01);
1867 assert!((parse_endf_float(line, 1).unwrap() - (-0.234567)).abs() < 1e-6);
1868 }
1869
1870 #[test]
1874 fn test_parse_endf_float_spaced_exponent() {
1875 let line =
1877 " 9.22330+ 4 1.23400- 2 ";
1878 assert!((parse_endf_float(line, 0).unwrap() - 92_233.0).abs() < 1.0);
1879 assert!((parse_endf_float(line, 1).unwrap() - 0.01234).abs() < 1e-6);
1880 }
1881
1882 #[test]
1883 fn test_parse_endf_int() {
1884 let line = " 0 1 2 3 4 5 ";
1885 assert_eq!(parse_endf_int(line, 0).unwrap(), 0);
1886 assert_eq!(parse_endf_int(line, 1).unwrap(), 1);
1887 assert_eq!(parse_endf_int(line, 2).unwrap(), 2);
1888 }
1889
1890 #[test]
1900 fn test_parse_u238_sammy_endf() {
1901 let endf_path =
1906 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/u238_ex027.endf");
1907
1908 let endf_text = std::fs::read_to_string(&endf_path)
1909 .unwrap_or_else(|e| panic!("vendored U-238 fixture missing at {endf_path:?}: {e}"));
1910 let data = parse_endf_file2(&endf_text).unwrap();
1911
1912 assert_eq!(data.za, 92238, "Should be U-238");
1914 assert!((data.awr - 236.006).abs() < 0.01, "AWR should be ~236");
1915 assert!(!data.ranges.is_empty(), "Should have at least one range");
1916
1917 let range = &data.ranges[0];
1918 assert!(range.resolved, "First range should be resolved");
1919 assert_eq!(
1920 range.formalism,
1921 ResonanceFormalism::ReichMoore,
1922 "U-238 ENDF uses Reich-Moore (LRF=3)"
1923 );
1924 assert!(
1925 (range.target_spin - 0.0).abs() < 1e-10,
1926 "U-238 target spin I=0"
1927 );
1928 assert!(
1929 (range.scattering_radius - 9.4285).abs() < 0.01,
1930 "Scattering radius ~9.4285 fm (ENDF 0.94285 × 10)"
1931 );
1932 assert_eq!(range.l_groups.len(), 2, "Should have L=0 and L=1 groups");
1933
1934 let l0 = &range.l_groups[0];
1936 assert_eq!(l0.l, 0, "First group should be L=0");
1937 assert!(
1938 l0.resonances.len() > 500,
1939 "L=0 should have hundreds of resonances"
1940 );
1941
1942 let first_positive = l0
1944 .resonances
1945 .iter()
1946 .find(|r| r.energy > 0.0)
1947 .expect("Should have positive-energy resonances");
1948 assert!(
1949 (first_positive.energy - 6.674).abs() < 0.01,
1950 "First positive resonance should be at 6.674 eV, got {}",
1951 first_positive.energy
1952 );
1953 assert!(
1954 (first_positive.j - 0.5).abs() < 1e-10,
1955 "6.674 eV resonance has J=0.5"
1956 );
1957
1958 assert!(
1960 (first_positive.gn - 1.493e-3).abs() < 1e-5,
1961 "Neutron width should be ~1.493e-3 eV, got {}",
1962 first_positive.gn
1963 );
1964 assert!(
1966 (first_positive.gg - 2.3e-2).abs() < 1e-3,
1967 "Gamma width should be ~2.3e-2 eV, got {}",
1968 first_positive.gg
1969 );
1970
1971 let total = data.total_resonance_count();
1975 let resolved_sum: usize = range.l_groups.iter().map(|g| g.resonances.len()).sum();
1976 assert!(
1977 total >= resolved_sum && resolved_sum > 500,
1978 "total_resonance_count ({total}) must cover the resolved range's \
1979 {resolved_sum} resonances"
1980 );
1981 }
1982
1983 #[test]
2001 fn test_parse_ta181_endf8_0_resonance_count() {
2002 let endf_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
2003 .parent()
2004 .unwrap()
2005 .parent()
2006 .unwrap()
2007 .join("tests/data/endf/Ta-181.endf");
2008 let endf_text = std::fs::read_to_string(&endf_path).unwrap_or_else(|e| {
2012 panic!(
2013 "vendored Ta-181 regression fixture must be present at \
2014 {endf_path:?} (committed test data): {e}"
2015 )
2016 });
2017 let data = parse_endf_file2(&endf_text).unwrap();
2018
2019 assert_eq!(data.za, 73181, "Should be Ta-181");
2020 assert!(
2021 (data.awr - 179.3936).abs() < 0.01,
2022 "AWR should be ~179.3936, got {}",
2023 data.awr
2024 );
2025
2026 assert_eq!(
2028 data.total_resonance_count(),
2029 76,
2030 "Ta-181 VIII.0 has 76 discrete resonances (sparse RRR to 330 eV)"
2031 );
2032 assert_eq!(
2033 data.ranges.len(),
2034 2,
2035 "Ta-181 VIII.0 has NER=2 ranges (resolved MLBW + unresolved URR)"
2036 );
2037
2038 let resolved = &data.ranges[0];
2040 assert!(resolved.resolved, "Range 0 must be resolved (LRU=1)");
2041 assert_eq!(
2042 resolved.formalism,
2043 ResonanceFormalism::MLBW,
2044 "Ta-181 VIII.0 resolved range uses MLBW (LRF=2)"
2045 );
2046 assert_eq!(
2047 resolved.resonance_count(),
2048 76,
2049 "All 76 resonances belong to the resolved MLBW range"
2050 );
2051
2052 let unresolved = &data.ranges[1];
2054 assert!(!unresolved.resolved, "Range 1 must be unresolved (LRU=2)");
2055 assert_eq!(
2056 unresolved.formalism,
2057 ResonanceFormalism::Unresolved,
2058 "Ta-181 VIII.0 second range is the URR (LRU=2)"
2059 );
2060 assert_eq!(
2061 unresolved.resonance_count(),
2062 0,
2063 "URR range carries no discrete resonances"
2064 );
2065
2066 assert!(
2070 data.has_unevaluated_ranges(),
2071 "Ta-181's URR range must be flagged as unevaluated"
2072 );
2073 assert_eq!(
2074 data.unevaluated_ranges().len(),
2075 1,
2076 "exactly the URR range is unevaluated"
2077 );
2078 }
2079
2080 #[track_caller]
2088 fn assert_rejected_no_evaluable_ranges(endf: &str) {
2089 let err = parse_endf_file2(endf).unwrap_err();
2090 match err {
2091 EndfParseError::UnsupportedFormat(ref msg) => {
2092 assert!(
2093 msg.contains("No evaluable resonance ranges"),
2094 "expected no-evaluable-ranges rejection, got: {msg}"
2095 );
2096 }
2097 other => panic!("expected UnsupportedFormat, got: {other:?}"),
2098 }
2099 }
2100
2101 #[test]
2117 fn test_parse_nro1_tab1() {
2118 let endf = include_str!("../../../tests/data/synthetic/lrf3_nro1_tab1.endf");
2135
2136 let data = parse_endf_file2(endf).expect("NRO=1 fixture must parse cleanly");
2137 assert_eq!(data.ranges.len(), 1, "one energy range");
2138
2139 let range = &data.ranges[0];
2140 assert_eq!(
2141 range.formalism,
2142 ResonanceFormalism::ReichMoore,
2143 "must be LRF=3"
2144 );
2145
2146 let table = range
2147 .ap_table
2148 .as_ref()
2149 .expect("NRO=1 range must have ap_table");
2150 assert_eq!(table.points.len(), 2, "TAB1 must have 2 points");
2151
2152 assert!(
2154 (table.evaluate(1.0) - 80.0).abs() < 1e-10,
2155 "AP(1 eV) = 80.0 fm"
2156 );
2157 assert!(
2158 (table.evaluate(1000.0) - 100.0).abs() < 1e-10,
2159 "AP(1000 eV) = 100.0 fm"
2160 );
2161 let mid = table.evaluate(500.5);
2163 assert!((mid - 90.0).abs() < 0.1, "AP midpoint ≈ 90.0 fm, got {mid}");
2164
2165 assert!(
2167 (range.scattering_radius_at(1.0) - 80.0).abs() < 1e-10,
2168 "scattering_radius_at(1 eV) = 80.0"
2169 );
2170 assert!(
2171 (range.scattering_radius_at(1000.0) - 100.0).abs() < 1e-10,
2172 "scattering_radius_at(1000 eV) = 100.0"
2173 );
2174
2175 assert_eq!(range.l_groups.len(), 1, "one L-group");
2177 let res = &range.l_groups[0].resonances[0];
2178 assert!((res.energy - 6.674).abs() < 1e-6);
2179 }
2180
2181 #[test]
2200 fn test_parse_u233_lfw1_lrf2_urr_parsed() {
2201 const ENDF: &str = include_str!("../../../tests/data/synthetic/u233_lfw1_lrf2_urr.endf");
2206
2207 let data = parse_endf_file2(ENDF)
2208 .expect("U-233 LFW=1/LRF=2 fixture must parse (record layout = LFW=0/LRF=2)");
2209
2210 assert_eq!(data.ranges.len(), 2, "must have 2 ranges (resolved + URR)");
2212
2213 let resolved_count = data.ranges.iter().filter(|r| r.resolved).count();
2214 assert_eq!(resolved_count, 1, "exactly one resolved range");
2215
2216 let urr_count = data
2221 .ranges
2222 .iter()
2223 .filter(|r| r.formalism == ResonanceFormalism::Unresolved)
2224 .count();
2225 assert_eq!(
2226 urr_count, 1,
2227 "LFW=1/LRF=2 URR range must be present (skipped)"
2228 );
2229 let urr_range = data
2230 .ranges
2231 .iter()
2232 .find(|r| r.formalism == ResonanceFormalism::Unresolved)
2233 .expect("URR range must exist");
2234 assert!(!urr_range.resolved, "URR range must not be resolved");
2235
2236 assert!(data.has_unevaluated_ranges());
2239 assert_eq!(data.unevaluated_ranges().len(), 1);
2240 }
2241
2242 #[test]
2248 fn test_parse_lrf1_urr_roundtrip() {
2249 const ENDF: &str = include_str!("../../../tests/data/synthetic/lrf1_urr_roundtrip.endf");
2261
2262 let data = parse_endf_file2(ENDF).expect("LRF=1 URR fixture must parse cleanly");
2263
2264 assert_eq!(data.ranges.len(), 2, "must have 2 ranges");
2266
2267 let urr_range = &data.ranges[1];
2268 assert!(!urr_range.resolved, "URR range must not be resolved");
2269 assert_eq!(
2270 urr_range.formalism,
2271 ResonanceFormalism::Unresolved,
2272 "formalism must be Unresolved"
2273 );
2274
2275 assert!(data.has_unevaluated_ranges());
2282 assert_eq!(data.unevaluated_ranges().len(), 1);
2283 }
2284
2285 #[test]
2290 fn test_parse_lrf2_urr_int3_rejected_no_evaluable_ranges() {
2291 const ENDF: &str =
2299 include_str!("../../../tests/data/synthetic/lrf2_urr_int3_roundtrip.endf");
2300 assert_rejected_no_evaluable_ranges(ENDF);
2301 }
2302
2303 #[test]
2310 fn test_parse_lrf2_urr_int0_rejected() {
2311 const ENDF: &str =
2313 include_str!("../../../tests/data/synthetic/lrf2_urr_int0_rejected.endf");
2314
2315 let err = parse_endf_file2(ENDF).unwrap_err();
2316 match err {
2321 EndfParseError::UnsupportedFormat(ref msg) => {
2322 assert!(
2323 msg.contains("INT=0") && msg.contains("out of spec"),
2324 "expected INT-out-of-spec rejection in UnsupportedFormat payload, \
2325 got: {msg}"
2326 );
2327 }
2328 other => panic!("expected EndfParseError::UnsupportedFormat for INT=0, got: {other:?}"),
2329 }
2330 }
2331
2332 #[test]
2338 fn test_checked_count_negative() {
2339 let err = checked_count(-1, "NLS").unwrap_err();
2340 assert!(
2341 err.to_string().contains("Negative"),
2342 "expected negative error, got: {err}"
2343 );
2344 }
2345
2346 #[test]
2349 fn test_checked_count_upper_bound() {
2350 let err = checked_count(MAX_ENDF_COUNT + 1, "NRS").unwrap_err();
2352 assert!(
2353 err.to_string().contains("too large"),
2354 "expected upper-bound error, got: {err}"
2355 );
2356
2357 assert_eq!(checked_count(MAX_ENDF_COUNT, "NRS").unwrap(), 1_000_000);
2359
2360 let err = checked_count(i32::MAX, "NPL").unwrap_err();
2362 assert!(
2363 err.to_string().contains("too large"),
2364 "expected upper-bound error for i32::MAX, got: {err}"
2365 );
2366 }
2367
2368 #[test]
2381 fn test_parse_endf_int_rejects_non_integral_float() {
2382 const LINE: &str = " 1.000000+0 1.900000+0";
2385
2386 let ok = parse_endf_int(LINE, 0).expect("integral float must parse");
2388 assert_eq!(ok, 1);
2389
2390 let err = parse_endf_int(LINE, 1).expect_err("non-integral must be rejected");
2392 match err {
2393 EndfParseError::InvalidNumber(ref msg) => {
2394 assert!(
2395 msg.contains("Non-integral"),
2396 "expected Non-integral diagnostic, got: {msg}"
2397 );
2398 }
2399 other => panic!("expected InvalidNumber, got: {other:?}"),
2400 }
2401 }
2402
2403 #[test]
2408 fn test_bw_negative_l_rejected() {
2409 const ENDF: &str =
2412 include_str!("../../../tests/data/synthetic/lrf1_bw_negative_l_rejected.endf");
2413
2414 let err = parse_endf_file2(ENDF).unwrap_err();
2415 assert!(
2416 err.to_string().contains("negative L"),
2417 "expected negative L error, got: {err}"
2418 );
2419 }
2420
2421 #[test]
2423 fn test_rm_negative_l_rejected() {
2424 const ENDF: &str =
2425 include_str!("../../../tests/data/synthetic/lrf3_rm_negative_l_rejected.endf");
2426
2427 let err = parse_endf_file2(ENDF).unwrap_err();
2428 assert!(
2429 err.to_string().contains("negative L"),
2430 "expected negative L error, got: {err}"
2431 );
2432 }
2433
2434 #[test]
2439 fn test_lru0_nonzero_nls_rejected() {
2440 const ENDF: &str =
2441 include_str!("../../../tests/data/synthetic/lru0_nonzero_nls_rejected.endf");
2442
2443 let err = parse_endf_file2(ENDF).unwrap_err();
2444 assert!(
2445 err.to_string().contains("NLS=3"),
2446 "expected LRU=0 NLS validation error, got: {err}"
2447 );
2448 }
2449
2450 #[test]
2457 fn test_mlbw_nls0_rejected() {
2458 const ENDF: &str = include_str!("../../../tests/data/synthetic/mlbw_nls0_rejected.endf");
2459
2460 let err = parse_endf_file2(ENDF).unwrap_err();
2461 assert!(
2462 err.to_string().contains("NLS=0"),
2463 "expected resolved NLS=0 rejection, got: {err}"
2464 );
2465 }
2466
2467 #[test]
2473 fn test_mlbw_nrs0_everywhere_rejected() {
2474 const ENDF: &str =
2475 include_str!("../../../tests/data/synthetic/mlbw_nrs0_everywhere_rejected.endf");
2476
2477 let err = parse_endf_file2(ENDF).unwrap_err();
2478 let msg = err.to_string();
2479 assert!(
2480 msg.contains("No evaluable resonance ranges"),
2481 "expected no-evaluable-content rejection, got: {msg}"
2482 );
2483 assert!(
2484 msg.contains("resolved range with no resonances"),
2485 "rejection must name the inert resolved span, got: {msg}"
2486 );
2487 }
2488
2489 #[test]
2494 fn test_mlbw_nrs0_plus_resolved_mixed_loads_and_flags() {
2495 const ENDF: &str =
2496 include_str!("../../../tests/data/synthetic/mlbw_nrs0_plus_resolved_mixed.endf");
2497
2498 let data = parse_endf_file2(ENDF).expect("mixed populated + inert file must load");
2499 assert_eq!(data.ranges.len(), 2);
2500 assert!(data.has_evaluable_range());
2501 assert!(data.has_unevaluated_ranges());
2502 assert_eq!(data.total_resonance_count(), 1);
2503 let inert = data
2504 .ranges
2505 .iter()
2506 .find(|r| !r.is_evaluable())
2507 .expect("the inert range must be present");
2508 assert!(
2509 inert
2510 .skip_description()
2511 .contains("LRF=2 (MLBW) resolved range with no resonances"),
2512 "skip_description must name the inert shape, got: {}",
2513 inert.skip_description()
2514 );
2515 }
2516
2517 #[test]
2520 fn test_mlbw_lrx1_range_is_parsed_but_not_evaluated() {
2521 const ENDF: &str =
2522 include_str!("../../../tests/data/synthetic/mlbw_lrx1_plus_resolved_mixed.endf");
2523
2524 let data = parse_endf_file2(ENDF).expect("the LRX=0 range keeps the file loadable");
2525 assert_eq!(data.ranges.len(), 2);
2526 let competitive = &data.ranges[1].l_groups[0];
2527 assert_eq!((competitive.lrx, competitive.qx), (1, -4.49e4));
2528 assert!(data.ranges[0].is_evaluable());
2529 assert_eq!(data.unevaluated_ranges().len(), 1);
2530 assert!(
2531 data.ranges[1]
2532 .skip_description()
2533 .contains("LRF=2 (MLBW) resolved range with a competitive width (LRX=1)"),
2534 "got: {}",
2535 data.ranges[1].skip_description()
2536 );
2537 }
2538
2539 #[test]
2541 fn test_rm_nls0_rejected() {
2542 const ENDF: &str = include_str!("../../../tests/data/synthetic/lrf3_rm_nls0_rejected.endf");
2543
2544 let err = parse_endf_file2(ENDF).unwrap_err();
2545 assert!(
2546 err.to_string().contains("NLS=0"),
2547 "expected resolved NLS=0 rejection, got: {err}"
2548 );
2549 }
2550
2551 #[test]
2568 fn test_lfw1_lrf1_urr_rejected_no_evaluable_ranges() {
2569 const ENDF: &str =
2570 include_str!("../../../tests/data/synthetic/lfw1_urr_gracefully_skipped.endf");
2571
2572 assert_rejected_no_evaluable_ranges(ENDF);
2579 }
2580
2581 #[test]
2586 fn test_lfw1_lrf1_urr_rejects_bad_perj_n1() {
2587 const ENDF: &str = include_str!("../../../tests/data/synthetic/lfw1_urr_bad_perj_n1.endf");
2588
2589 let err = parse_endf_file2(ENDF).unwrap_err();
2590 assert!(
2591 err.to_string().contains("N1=") && err.to_string().contains("NE+6"),
2592 "expected per-J N1 != NE+6 rejection, got: {err}"
2593 );
2594 }
2595
2596 #[test]
2603 fn test_urr_lrf3_rejected() {
2604 const ENDF: &str = include_str!("../../../tests/data/synthetic/urr_lrf3_rejected.endf");
2605
2606 let err = parse_endf_file2(ENDF).unwrap_err();
2607 let msg = err.to_string();
2608 assert!(
2609 msg.contains("LRF=3") && msg.contains("URR"),
2610 "expected URR LRF=3 rejection naming the LRF, got: {msg}"
2611 );
2612 }
2613
2614 #[test]
2620 fn test_lru0_nonzero_l1_rejected() {
2621 const ENDF: &str =
2622 include_str!("../../../tests/data/synthetic/lru0_nonzero_l1_rejected.endf");
2623
2624 let err = parse_endf_file2(ENDF).unwrap_err();
2625 assert!(
2626 err.to_string().contains("L1=5"),
2627 "expected LRU=0 L1 validation error, got: {err}"
2628 );
2629 }
2630
2631 #[test]
2641 fn test_lru0_only_rejected_names_lru0_span() {
2642 const ENDF: &str =
2643 include_str!("../../../tests/data/synthetic/lru0_only_rejected_no_evaluable.endf");
2644
2645 let err = parse_endf_file2(ENDF).unwrap_err();
2646 let msg = err.to_string();
2647 assert!(
2648 msg.contains("No evaluable resonance ranges"),
2649 "expected no-evaluable-ranges rejection, got: {msg}"
2650 );
2651 assert!(
2652 msg.contains("LRU=0 (scattering-radius-only, no resonance parameters)"),
2653 "rejection must name the LRU=0 span, got: {msg}"
2654 );
2655 }
2656
2657 #[test]
2662 fn test_lru0_plus_resolved_mixed_loads_and_flags_lru0() {
2663 const ENDF: &str =
2664 include_str!("../../../tests/data/synthetic/lru0_plus_resolved_mixed.endf");
2665
2666 let data = parse_endf_file2(ENDF).expect("mixed LRU=0 + resolved file must load");
2667 assert_eq!(data.ranges.len(), 2, "LRU=0 range + resolved range");
2668 assert!(
2669 data.has_evaluable_range(),
2670 "resolved range keeps it evaluable"
2671 );
2672 assert!(data.has_unevaluated_ranges(), "LRU=0 range is unevaluated");
2673
2674 let lru0 = data
2675 .ranges
2676 .iter()
2677 .find(|r| r.formalism == ResonanceFormalism::ScatteringRadiusOnly)
2678 .expect("the LRU=0 range must be present");
2679 assert!(!lru0.resolved, "LRU=0 range must not be resolved");
2680 assert!(
2681 lru0.skip_description()
2682 .contains("LRU=0 (scattering-radius-only, no resonance parameters)"),
2683 "LRU=0 skip_description must name the span, got: {}",
2684 lru0.skip_description()
2685 );
2686
2687 assert_eq!(lru0.target_spin, 1.5);
2692 assert_eq!(lru0.scattering_radius, 5.0);
2693 let resolved = data
2694 .ranges
2695 .iter()
2696 .find(|r| r.is_evaluable())
2697 .expect("the resolved range must be evaluable");
2698 assert_eq!(resolved.target_spin, 2.5);
2699 assert!((resolved.scattering_radius - 9.6931).abs() < 1e-9);
2700 }
2701
2702 #[test]
2704 fn test_bw_n1_nrs_mismatch_rejected() {
2705 const ENDF: &str =
2706 include_str!("../../../tests/data/synthetic/lrf1_bw_n1_nrs_mismatch_rejected.endf");
2707
2708 let err = parse_endf_file2(ENDF).unwrap_err();
2709 assert!(
2710 err.to_string().contains("N1=7"),
2711 "expected N1/NRS mismatch error, got: {err}"
2712 );
2713 }
2714
2715 #[test]
2718 fn test_multi_mat_detection() {
2719 const ENDF: &str = include_str!("../../../tests/data/synthetic/multi_mat_detection.endf");
2722
2723 let err = parse_endf_file2(ENDF).unwrap_err();
2724 assert!(
2725 err.to_string().contains("Multiple materials"),
2726 "expected multi-MAT error, got: {err}"
2727 );
2728 }
2729
2730 #[test]
2745 fn test_parse_endf_rejects_nis_gt_1() {
2746 const ENDF: &str = include_str!("../../../tests/data/synthetic/nis_gt_1_rejected.endf");
2754
2755 let err = parse_endf_file2(ENDF).unwrap_err();
2756 match &err {
2757 EndfParseError::UnsupportedFormat(msg) => {
2758 assert!(
2759 msg.contains("NIS=2"),
2760 "expected NIS=2 in error message, got: {msg}"
2761 );
2762 assert!(
2763 msg.contains("multi-isotope"),
2764 "expected 'multi-isotope' in error message, got: {msg}"
2765 );
2766 }
2767 other => panic!("expected UnsupportedFormat for NIS>1, got {other:?}"),
2768 }
2769 }
2770
2771 #[test]
2780 fn test_parse_endf_rejects_nis_zero() {
2781 const ENDF: &str = include_str!("../../../tests/data/synthetic/nis_zero_rejected.endf");
2786
2787 let err = parse_endf_file2(ENDF).unwrap_err();
2788 match &err {
2789 EndfParseError::UnsupportedFormat(msg) => {
2790 assert!(
2791 msg.contains("NIS=0"),
2792 "expected NIS=0 in error message, got: {msg}"
2793 );
2794 assert!(
2795 msg.contains("NIS >= 1"),
2796 "expected 'NIS >= 1' guidance in error message, got: {msg}"
2797 );
2798 }
2799 other => panic!("expected UnsupportedFormat for NIS=0, got {other:?}"),
2800 }
2801 }
2802
2803 fn file32_fixture() -> String {
2804 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
2805 .join("../../tests/data/endf/Ta-181-file32.endf");
2806 std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path:?}: {e}"))
2807 }
2808
2809 #[test]
2810 fn test_file32_covariance_of_the_resonances_in_a_window() {
2811 let text = file32_fixture();
2812 let data = parse_endf_file2(&text).unwrap();
2813 let read = |window| parse_resonance_covariance(&text, &data, window).unwrap();
2814 let all = read(0.0..=100.0).expect("listed");
2815 assert_eq!(all.resonances, [[0, 0, 0], [0, 0, 1], [0, 0, 2], [0, 0, 3]]);
2816 assert_eq!(all.covariance[0], 1.229472e-6);
2817 let window = read(14.0..=30.0).expect("listed");
2818 assert_eq!(window.resonances, [[0, 0, 1], [0, 0, 2], [0, 0, 3]]);
2819 let at = |x: usize, y: usize| window.covariance[9 * x + y];
2820 for (x, y, expected) in [
2821 (0, 0, 1.558071e-6),
2822 (1, 2, -1.216048e-9),
2823 (0, 3, 6.691704e-9),
2824 (2, 7, 4.216599e-9),
2825 (5, 6, 8.726475e-9),
2826 (8, 8, 1.540406e-6),
2827 ] {
2828 assert_eq!((at(x, y), at(y, x)), (expected, expected), "{x}, {y}");
2829 }
2830 assert!(read(100.0..=200.0).is_none());
2831 }
2832
2833 #[test]
2834 fn test_file32_forms_not_read_are_refused() {
2835 let text = file32_fixture();
2836 let data = parse_endf_file2(&text).unwrap();
2837 let read = |text: &str| parse_resonance_covariance(text, &data, 0.0..=100.0);
2838 let edit = |text: &str, line: usize, field: usize, value: &str| {
2839 let change = |(k, l): (usize, &str)| match k + 1 == line {
2840 true => format!("{}{value:>11}{}\n", &l[..11 * field], &l[11 * field + 11..]),
2841 false => format!("{l}\n"),
2842 };
2843 text.lines().enumerate().map(change).collect::<String>()
2844 };
2845 let edited = |line: usize, field: usize, value: &str| edit(&text, line, field, value);
2846 let edits = |changes: &[(usize, usize, &str)]| {
2847 changes
2848 .iter()
2849 .fold(text.clone(), |text, &(l, f, v)| edit(&text, l, f, v))
2850 };
2851 let unresolved =
2852 " 2.554000+3 1.000000+5 2 1 0 0732832151
2853 3.500000+0 7.860000-1 0 0 1 0732832151
2854 1.793936+2 0.000000+0 0 0 6 1732832151
2855 1.000000+0 3.000000+0 1.000000-3 6.000000-2 0.000000+0 0.000000+0732832151
2856 0.000000+0 0.000000+0 1 0 1 1732832151
2857 1.000000-2 732832151
2858";
2859 let head = |text: &str, n| text.split_inclusive('\n').take(n).collect::<String>();
2860 let only = head(&text, 11) + unresolved;
2861 for refused in [
2862 edit(&only, 11, 4, "2"),
2863 edit(&only, 12, 3, "2"),
2864 edits(&[
2865 (17, 0, "1.034682+1"),
2866 (17, 2, "4.816293-3"),
2867 (17, 3, "6.164143-2"),
2868 ]),
2869 edited(10, 0, "7.318200+4"),
2870 edited(11, 0, "7.318200+4"),
2871 edited(10, 4, "2"),
2872 edits(&[(10, 4, "2"), (12, 2, "2"), (12, 3, "1")]),
2873 edited(12, 2, "2"),
2874 edits(&[(11, 4, "2"), (12, 2, "2"), (12, 3, "1")]),
2875 edited(11, 4, "0"),
2876 edited(12, 2, "0"),
2877 edited(12, 3, "2"),
2878 edited(12, 4, "1"),
2879 edited(13, 3, "2"),
2880 edited(13, 5, "1"),
2881 edited(14, 5, "1"),
2882 edited(15, 2, "2"),
2883 edited(15, 4, "101"),
2884 edited(16, 0, "nan"),
2885 edited(17, 0, "2.031200+1"),
2886 edited(17, 1, "4.000000+0"),
2887 edited(17, 2, "1.066000-3"),
2888 edited(11, 4, "2") + unresolved + &unresolved[..76],
2889 ] {
2890 let refused = read(&refused);
2891 assert!(
2892 matches!(refused, Err(EndfParseError::UnsupportedFormat(_))),
2893 "{refused:?}"
2894 );
2895 }
2896 let all = read(&text).unwrap().expect("listed");
2897 for accepted in [
2898 edited(17, 0, "2.031171+1"),
2899 edited(17, 2, "1.065878-3"),
2900 edited(11, 4, "2") + unresolved,
2901 ] {
2902 assert_eq!(read(&accepted).unwrap().as_ref(), Some(&all));
2903 }
2904 assert!(read(&(edited(11, 4, "2") + &unresolved[..380])).is_err());
2905 let edge = parse_resonance_covariance(&edited(17, 0, "2.031171+1"), &data, 0.0..=20.3117);
2906 assert_eq!(
2907 edge.unwrap().expect("listed").resonances,
2908 all.resonances[..2]
2909 );
2910 let mut grouped: Vec<String> = edits(&[(4, 4, "2"), (5, 4, "12"), (5, 5, "2")])
2911 .lines()
2912 .map(String::from)
2913 .collect();
2914 let group =
2915 " 1.793936+2 7.860000-1 1 0 12 27328 2151 0";
2916 grouped.insert(7, group.to_string());
2917 let grouped = grouped.join("\n");
2918 let split = parse_endf_file2(&grouped).unwrap();
2919 let regrouped = parse_resonance_covariance(&grouped, &split, 0.0..=100.0).unwrap();
2920 let regrouped = regrouped.expect("listed");
2921 let indices = [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1]];
2922 assert_eq!(regrouped.resonances, indices);
2923 assert_eq!(regrouped.covariance, all.covariance);
2924 let doublet = edits(&[
2925 (8, 0, "2.031170+1"),
2926 (8, 2, "1.065877-3"),
2927 (8, 3, "6.370451-2"),
2928 ]);
2929 let mut multilevel = data.clone();
2930 multilevel.ranges[0].formalism = ResonanceFormalism::MLBW;
2931 for (file, file2, window) in [
2932 (&text, multilevel, 0.0..=100.0),
2933 (&doublet, parse_endf_file2(&doublet).unwrap(), 15.0..=21.0),
2934 ] {
2935 let refused = parse_resonance_covariance(file, &file2, window);
2936 assert!(matches!(refused, Err(EndfParseError::UnsupportedFormat(_))));
2937 }
2938 for window in [f64::NAN..=1.0, 30.0..=14.0] {
2939 let refused = parse_resonance_covariance(&text, &data, window);
2940 assert!(matches!(refused, Err(EndfParseError::InvalidNumber(_))));
2941 }
2942 for empty in [head(&text, 9), only] {
2943 assert!(read(&empty).unwrap().is_none());
2944 }
2945 let field = |v: f64| format!("{:>11}", format!("{v:e}").replace('e', ""));
2946 let members = [[0, 1], [1, 2]];
2947 let block = |rows: [usize; 2]| {
2948 let at = |p: usize| 3 * rows[p / 3] + p % 3;
2949 let packed = (0..6).flat_map(|p| (p..6).map(move |q| (p, q)));
2950 let values: Vec<String> = packed
2951 .map(|(p, q)| field(all.covariance[12 * at(p) + at(q)]))
2952 .collect();
2953 let head = " 0.0 0.0 3 0 33 2".into();
2954 let table = rows.map(|r| text.lines().nth(15 + r).unwrap()[..66].to_string());
2955 let body = values.chunks(6).map(|c| c.concat()).collect::<Vec<_>>();
2956 std::iter::once(head)
2957 .chain(table)
2958 .chain(body)
2959 .map(|l| format!("{l:<66}732832151 0\n"))
2960 .collect::<String>()
2961 };
2962 let sections = head(&edited(14, 4, "2"), 14);
2963 let overlapping = sections + &members.map(block).concat();
2964 let summed = read(&overlapping).unwrap().expect("listed");
2965 assert_eq!(summed.resonances, all.resonances[..3]);
2966 for (a, b) in (0..9).flat_map(|a| (0..9).map(move |b| (a, b))) {
2967 let shared = members
2968 .iter()
2969 .filter(|m| m.contains(&(a / 3)) && m.contains(&(b / 3)));
2970 let expected = shared.count() as f64 * all.covariance[12 * a + b];
2971 assert_eq!(summed.covariance[9 * a + b], expected, "{a}, {b}");
2972 }
2973 }
2974}
2975
2976#[cfg(test)]
2977mod rml_tests {
2978 use super::*;
2979
2980 const PHOTON: [f64; 12] = [
2981 0.0, 183.37, 0.0, 74.0, 1.0, 0.0, 0.0, 0.0, 0.0, 102.0, 0.0, 0.0,
2982 ];
2983 const NEUTRON: [f64; 12] = [
2984 1.0, 182.37, 0.0, 74.0, 0.5, 0.0, 0.0, 1.0, 0.0, 2.0, 0.0, 1.0,
2985 ];
2986
2987 struct Background {
2989 lch: i64,
2990 lbk: i64,
2991 list: Option<[f64; 7]>,
2992 }
2993
2994 struct Group {
2995 aj: f64,
2996 pj: f64,
2997 kps: i64,
2998 channels: Vec<[f64; 6]>,
2999 resonances: Vec<Vec<f64>>,
3000 nx: Option<usize>,
3001 background: Vec<Background>,
3002 }
3003
3004 struct Fixture {
3006 krm: i64,
3007 ifg: i64,
3008 krl: i64,
3009 nro: bool,
3010 pairs: Vec<[f64; 12]>,
3011 groups: Vec<Group>,
3012 }
3013
3014 impl Fixture {
3015 fn basic() -> Self {
3017 Self {
3018 krm: 3,
3019 ifg: 0,
3020 krl: 0,
3021 nro: false,
3022 pairs: vec![PHOTON, NEUTRON],
3023 groups: vec![Group {
3024 aj: 0.5,
3025 pj: 0.0,
3026 kps: 0,
3027 channels: vec![
3028 [1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
3029 [2.0, 0.0, 0.5, 0.0, 0.764, 0.75],
3030 ],
3031 resonances: vec![vec![-12.0, 0.05, 0.3], vec![101.9, 0.06, -0.02]],
3032 nx: None,
3033 background: vec![],
3034 }],
3035 }
3036 }
3037
3038 fn text(&self) -> String {
3039 fn float(x: f64) -> String {
3040 format!("{:>11}", format!("{x:.5E}"))
3041 }
3042 fn int(n: i64) -> String {
3043 format!("{n:>11}")
3044 }
3045 fn list(record: &mut dyn FnMut(Vec<String>), values: &[f64]) {
3046 for chunk in values.chunks(6) {
3047 let mut fields: Vec<String> = chunk.iter().map(|&v| float(v)).collect();
3048 fields.resize(6, " ".repeat(11));
3049 record(fields);
3050 }
3051 }
3052 let mut lines: Vec<String> = Vec::new();
3053 let mut record = |fields: Vec<String>| {
3054 let seq = lines.len() + 1;
3055 lines.push(format!("{:66}7437 2151{seq:>5}", fields.concat()));
3056 };
3057 let zero = || float(0.0);
3058 record(vec![
3059 float(74184.0),
3060 float(182.37),
3061 int(0),
3062 int(0),
3063 int(1),
3064 int(0),
3065 ]);
3066 record(vec![
3067 float(74184.0),
3068 float(1.0),
3069 int(0),
3070 int(0),
3071 int(1),
3072 int(0),
3073 ]);
3074 record(vec![
3075 float(1e-5),
3076 float(1e4),
3077 int(1),
3078 int(7),
3079 int(i64::from(self.nro)),
3080 int(0),
3081 ]);
3082 if self.nro {
3083 record(vec![zero(), zero(), int(0), int(0), int(1), int(2)]);
3084 record(vec![
3085 int(2),
3086 int(2),
3087 " ".repeat(11),
3088 " ".repeat(11),
3089 " ".repeat(11),
3090 " ".repeat(11),
3091 ]);
3092 list(&mut record, &[1e-5, 0.7, 1e4, 0.7]);
3093 }
3094 record(vec![
3095 zero(),
3096 zero(),
3097 int(self.ifg),
3098 int(self.krm),
3099 int(self.groups.len() as i64),
3100 int(self.krl),
3101 ]);
3102 let npp = self.pairs.len() as i64;
3103 record(vec![
3104 zero(),
3105 zero(),
3106 int(npp),
3107 int(0),
3108 int(12 * npp),
3109 int(2 * npp),
3110 ]);
3111 list(&mut record, &self.pairs.concat());
3112 for g in &self.groups {
3113 let nch = g.channels.len();
3114 record(vec![
3115 float(g.aj),
3116 float(g.pj),
3117 int(g.background.len() as i64),
3118 int(g.kps),
3119 int(6 * nch as i64),
3120 int(nch as i64),
3121 ]);
3122 list(&mut record, &g.channels.concat());
3123 let per = 6 * (nch / 6 + 1);
3124 let mut values: Vec<f64> = Vec::new();
3125 for r in &g.resonances {
3126 let mut row = r.clone();
3127 row.resize(per, 0.0);
3128 values.extend(row);
3129 }
3130 if g.resonances.is_empty() {
3131 values.resize(6, 0.0);
3132 }
3133 let nx = g.nx.unwrap_or(values.len() / 6);
3134 values.resize(6 * nx, 0.0);
3135 record(vec![
3136 zero(),
3137 zero(),
3138 int(0),
3139 int(g.resonances.len() as i64),
3140 int(6 * nx as i64),
3141 int(nx as i64),
3142 ]);
3143 list(&mut record, &values);
3144 for b in &g.background {
3145 record(vec![zero(), zero(), int(b.lch), int(b.lbk), int(0), int(0)]);
3146 if let Some(v) = b.list {
3147 record(vec![
3148 float(v[0]),
3149 float(v[1]),
3150 int(0),
3151 int(0),
3152 int(5),
3153 int(0),
3154 ]);
3155 list(&mut record, &v[2..]);
3156 }
3157 }
3158 }
3159 lines.join("\n")
3160 }
3161
3162 fn parse(&self) -> Result<ResonanceData, EndfParseError> {
3163 parse_file2_records(&self.text())
3164 }
3165 }
3166
3167 fn rml(data: &ResonanceData) -> &RmlRange {
3168 data.ranges[0]
3169 .r_matrix_limited
3170 .as_ref()
3171 .expect("parsed LRF=7 payload")
3172 }
3173
3174 fn assert_rejects(fixture: &Fixture, needle: &str) {
3175 match fixture.parse() {
3176 Err(EndfParseError::UnsupportedFormat(msg)) => {
3177 assert!(msg.contains(needle), "expected '{needle}' in: {msg}");
3178 }
3179 other => panic!("expected UnsupportedFormat with '{needle}', got {other:?}"),
3180 }
3181 }
3182
3183 #[test]
3184 fn parses_pairs_channels_and_widths() {
3185 let data = Fixture::basic().parse().unwrap();
3186 let range = &data.ranges[0];
3187 assert_eq!(range.formalism, ResonanceFormalism::RMatrixLimited);
3188 let rml = rml(&data);
3189 assert_eq!((rml.photon_pair, rml.incident_pair), (0, 1));
3190 assert!(
3191 !rml.pairs[0].penetrability,
3192 "PNT=0 on MT=102 means no penetrability"
3193 );
3194 assert!(rml.pairs[1].penetrability);
3195 let group = &rml.spin_groups[0];
3196 assert_eq!((group.j, group.parity), (0.5, 1));
3197 assert_eq!(group.channels[1].pair, 1);
3198 assert!((group.channels[1].radius_phase - 7.64).abs() < 1e-12);
3199 assert!((group.channels[1].radius_true - 7.5).abs() < 1e-12);
3200 assert_eq!(group.resonances.len(), 2);
3201 assert_eq!(group.resonances[1].energy, 101.9);
3202 assert_eq!(group.resonances[1].widths, vec![0.06, -0.02]);
3203 assert_eq!(rml.resonance_count(), 2);
3204 }
3205
3206 #[test]
3208 fn accepts_pnt_minus_one_on_photon_pair() {
3209 let mut fixture = Fixture::basic();
3210 fixture.pairs[0][7] = -1.0;
3211 let data = fixture.parse().unwrap();
3212 assert!(!rml(&data).pairs[0].penetrability);
3213 }
3214
3215 #[test]
3216 fn pnt_zero_takes_the_mt_default() {
3217 let mut fixture = Fixture::basic();
3218 fixture.pairs[1][7] = 0.0;
3219 let data = fixture.parse().unwrap();
3220 assert!(rml(&data).pairs[1].penetrability, "MT=2 defaults to PNT=1");
3221 }
3222
3223 #[test]
3224 fn rejects_undefined_or_fractional_pnt() {
3225 let mut fixture = Fixture::basic();
3226 fixture.pairs[1][7] = 2.0;
3227 assert_rejects(&fixture, "PNT=2 is not defined");
3228 fixture.pairs[1][7] = 1.5;
3229 assert_rejects(&fixture, "PNT=1.5 is not an integer");
3230 }
3231
3232 #[test]
3233 fn rejects_penetrability_on_photon_pair() {
3234 let mut fixture = Fixture::basic();
3235 fixture.pairs[0][7] = 1.0;
3236 assert_rejects(
3237 &fixture,
3238 "photon pair (MA=0) cannot calculate the penetrability",
3239 );
3240 }
3241
3242 #[test]
3243 fn rejects_penetrating_pair_without_mass() {
3244 let mut fixture = Fixture::basic();
3245 fixture.pairs[1][1] = 0.0;
3246 assert_rejects(&fixture, "needs positive masses");
3247 }
3248
3249 #[test]
3250 fn rejects_unsupported_header_flags() {
3251 let mut fixture = Fixture::basic();
3252 fixture.krm = 2;
3253 assert_rejects(&fixture, "KRM=2 is not supported");
3254 let mut fixture = Fixture::basic();
3255 fixture.ifg = 1;
3256 assert_rejects(&fixture, "IFG=1");
3257 let mut fixture = Fixture::basic();
3258 fixture.krl = 1;
3259 assert_rejects(&fixture, "KRL=1");
3260 let mut fixture = Fixture::basic();
3261 fixture.nro = true;
3262 assert_rejects(&fixture, "NRO=1");
3263 let mut fixture = Fixture::basic();
3264 fixture.groups[0].kps = 1;
3265 assert_rejects(&fixture, "KPS=1");
3266 }
3267
3268 #[test]
3269 fn accepts_charged_and_rejects_fission_pairs() {
3270 let mut fixture = Fixture::basic();
3271 let mut proton = NEUTRON;
3272 proton[0] = 0.9986235;
3273 proton[2] = 1.0;
3274 proton[3] = 73.0;
3275 proton[6] = 6.15e5;
3276 proton[9] = 600.0;
3277 fixture.pairs.push(proton);
3278 let data = fixture.parse().unwrap();
3279 let pair = &rml(&data).pairs[2];
3280 assert_eq!((pair.charge_a, pair.charge_b, pair.mt), (1.0, 73.0, 600));
3281 let mut fixture = Fixture::basic();
3282 let mut fission = PHOTON;
3283 fission[0] = 1.0;
3284 fission[9] = 18.0;
3285 fission[7] = -1.0;
3286 fixture.pairs.push(fission);
3287 assert_rejects(&fixture, "fission pair (MT=18)");
3288 }
3289
3290 #[test]
3291 fn shift_factor_needs_an_open_channel() {
3292 let mut fixture = Fixture::basic();
3293 fixture.pairs[1][8] = 1.0;
3294 let data = fixture.parse().unwrap();
3295 assert!(
3296 rml(&data).pairs[1].shift,
3297 "SHF=1 on the incident pair is open everywhere"
3298 );
3299
3300 let mut fixture = Fixture::basic();
3301 let mut inelastic = NEUTRON;
3302 inelastic[6] = -1.0e3;
3303 inelastic[8] = 1.0;
3304 inelastic[9] = 51.0;
3305 fixture.pairs.push(inelastic);
3306 assert_rejects(&fixture, "SHF=1 on a channel with threshold");
3307 }
3308
3309 #[test]
3311 fn finds_photon_channel_by_pair_not_position() {
3312 let mut fixture = Fixture::basic();
3313 fixture.groups[0].channels.swap(0, 1);
3314 for r in &mut fixture.groups[0].resonances {
3315 r.swap(1, 2);
3316 }
3317 let data = fixture.parse().unwrap();
3318 let group = &rml(&data).spin_groups[0];
3319 assert_eq!(group.channels[1].pair, 0);
3320 assert_eq!(group.resonances[1].widths, vec![-0.02, 0.06]);
3321 }
3322
3323 #[test]
3324 fn rejects_missing_or_duplicate_photon_and_entrance_channels() {
3325 let mut fixture = Fixture::basic();
3326 fixture.pairs[0][7] = -1.0;
3327 fixture.pairs[0][9] = 103.0;
3328 assert_rejects(&fixture, "exactly one photon pair");
3329 let mut fixture = Fixture::basic();
3330 fixture.groups[0].channels.truncate(1);
3331 for r in &mut fixture.groups[0].resonances {
3332 r.truncate(2);
3333 }
3334 assert_rejects(&fixture, "has no neutron entrance channel");
3335 let mut fixture = Fixture::basic();
3336 fixture.groups[0].channels[0] = fixture.groups[0].channels[1];
3337 assert_rejects(&fixture, "exactly one channel of the photon pair");
3338 let mut fixture = Fixture::basic();
3339 fixture.groups[0].channels[1][0] = 1.0;
3340 assert_rejects(&fixture, "needs exactly one channel of the photon pair");
3341 let mut fixture = Fixture::basic();
3342 fixture.groups[0].channels[1][0] = 3.0;
3343 assert_rejects(&fixture, "PPI=3 is out of range");
3344 }
3345
3346 #[test]
3347 fn rejects_resonance_on_a_penetrating_threshold() {
3348 let mut fixture = Fixture::basic();
3349 fixture.groups[0].resonances[0][0] = 0.0;
3350 assert_rejects(&fixture, "sits on the threshold");
3351 }
3352
3353 #[test]
3354 fn negative_aj_and_pj_give_negative_parity() {
3355 let mut fixture = Fixture::basic();
3356 fixture.groups[0].aj = -1.5;
3357 let data = fixture.parse().unwrap();
3358 assert_eq!(
3359 (
3360 rml(&data).spin_groups[0].j,
3361 rml(&data).spin_groups[0].parity
3362 ),
3363 (1.5, -1)
3364 );
3365 let mut fixture = Fixture::basic();
3366 fixture.groups[0].aj = 0.0;
3367 fixture.groups[0].pj = -1.0;
3368 let data = fixture.parse().unwrap();
3369 assert_eq!(rml(&data).spin_groups[0].parity, -1);
3370 }
3371
3372 #[test]
3373 fn empty_group_keeps_one_filler_line() {
3374 let mut fixture = Fixture::basic();
3375 fixture.groups[0].resonances.clear();
3376 let data = fixture.parse().unwrap();
3377 assert!(rml(&data).spin_groups[0].resonances.is_empty());
3378 fixture.groups[0].nx = Some(2);
3379 assert_rejects(&fixture, "NX=2");
3380 }
3381
3382 #[test]
3384 fn wide_group_reads_two_lines_per_resonance() {
3385 let mut fixture = Fixture::basic();
3386 let mut channels = vec![[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]];
3387 for l in 0..5 {
3388 channels.push([2.0, f64::from(l), 0.5, 0.0, 0.7, 0.7]);
3389 }
3390 let group = &mut fixture.groups[0];
3391 group.channels = channels;
3392 group.resonances = vec![
3393 vec![5.0, 0.04, 1.0, 2.0, 3.0, 4.0, 5.0],
3394 vec![9.0, 0.05, 6.0, 7.0, 8.0, 9.0, 10.0],
3395 ];
3396 let data = fixture.parse().unwrap();
3397 let group = &rml(&data).spin_groups[0];
3398 assert_eq!(group.resonances[1].energy, 9.0);
3399 assert_eq!(
3400 group.resonances[1].widths,
3401 vec![0.05, 6.0, 7.0, 8.0, 9.0, 10.0]
3402 );
3403 fixture.groups[0].nx = Some(2);
3404 assert_rejects(&fixture, "need NX=4");
3405 }
3406
3407 #[test]
3408 fn reads_logarithmic_background() {
3409 let mut fixture = Fixture::basic();
3410 fixture.groups[0].background = vec![Background {
3411 lch: 2,
3412 lbk: 2,
3413 list: Some([0.0, 2.0e4, -0.043, 1.0e-6, 2.0e-9, 0.01, 3.0e-7]),
3414 }];
3415 let data = fixture.parse().unwrap();
3416 let background = &rml(&data).spin_groups[0].background;
3417 assert_eq!(background.len(), 1);
3418 let b = &background[0];
3419 assert_eq!(b.channel, 1);
3420 assert_eq!((b.e_down, b.e_up), (0.0, 2.0e4));
3421 assert_eq!(
3422 [b.r_con, b.r_lin, b.r_quad, b.s_con, b.s_lin],
3423 [-0.043, 1.0e-6, 2.0e-9, 0.01, 3.0e-7]
3424 );
3425 }
3426
3427 #[test]
3428 fn lbk_zero_adds_no_term() {
3429 let mut fixture = Fixture::basic();
3430 fixture.groups[0].background = vec![Background {
3431 lch: 2,
3432 lbk: 0,
3433 list: None,
3434 }];
3435 let data = fixture.parse().unwrap();
3436 assert!(rml(&data).spin_groups[0].background.is_empty());
3437 }
3438
3439 #[test]
3440 fn rejects_unsupported_or_misplaced_background() {
3441 let background = |lch, lbk, list| {
3442 let mut fixture = Fixture::basic();
3443 fixture.groups[0].background = vec![Background { lch, lbk, list }];
3444 fixture
3445 };
3446 let list = Some([0.0, 2.0e4, 0.1, 0.0, 0.0, 0.0, 0.0]);
3447 assert_rejects(&background(2, 1, None), "LBK=1 background is not supported");
3448 assert_rejects(&background(2, 3, None), "LBK=3 background is not supported");
3449 assert_rejects(&background(1, 2, list), "eliminated photon channel");
3450 assert_rejects(&background(3, 2, list), "LCH=3 is out of range");
3451 assert_rejects(
3452 &background(2, 2, Some([1.0, 2.0e4, 0.1, 0.0, 0.0, 0.0, 0.0])),
3453 "ED < EL, EU > EH",
3454 );
3455 let mut fixture = background(2, 0, None);
3456 fixture.groups[0].background.push(Background {
3457 lch: 2,
3458 lbk: 2,
3459 list,
3460 });
3461 assert_rejects(&fixture, "given twice for channel 2");
3462 }
3463
3464 fn mf2_fixture(name: &str) -> String {
3465 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
3466 .join("../../tests/data/endf/mf2")
3467 .join(format!("{name}.endf"));
3468 std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path:?}: {e}"))
3469 }
3470
3471 #[test]
3474 fn parses_endf_b_viii_1_lrf7_evaluations() {
3475 let cases = [
3476 ("W-182", 3, 312),
3477 ("W-183", 5, 374),
3478 ("W-184", 3, 218),
3479 ("W-186", 3, 175),
3480 ("Fe-54", 5, 737),
3481 ("Fe-57", 5, 123),
3482 ("Si-28", 7, 55),
3483 ("Si-29", 10, 31),
3484 ("Si-30", 5, 30),
3485 ("Rh-103", 5, 555),
3486 ("Cu-63", 6, 1172),
3487 ("Cu-65", 6, 969),
3488 ("V-51", 6, 136),
3489 ("Sr-88", 7, 443),
3490 ("Cl-35", 8, 272),
3491 ("Ca-40", 5, 246),
3492 ];
3493 for (name, groups, resonances) in cases {
3494 let data =
3495 parse_file2_records(&mf2_fixture(name)).unwrap_or_else(|e| panic!("{name}: {e}"));
3496 let rml = data
3497 .ranges
3498 .iter()
3499 .find_map(|r| r.r_matrix_limited.as_ref())
3500 .unwrap_or_else(|| panic!("{name}: no LRF=7 range"));
3501 assert_eq!(rml.spin_groups.len(), groups, "{name}");
3502 assert_eq!(rml.resonance_count(), resonances, "{name}");
3503 assert_eq!(rml.pairs[rml.photon_pair].mt, 102, "{name}");
3504 assert!(!rml.pairs[rml.photon_pair].penetrability, "{name}");
3505 }
3506 }
3507
3508 #[test]
3510 fn parses_sr88_backgrounds() {
3511 let data = parse_file2_records(&mf2_fixture("Sr-88")).unwrap();
3512 let rml = rml(&data);
3513 assert_eq!(rml.pairs[0].mt, 102);
3514 assert!(
3515 !rml.pairs[0].penetrability,
3516 "Sr-88 writes PNT=-1 on the photon pair"
3517 );
3518 for group in &rml.spin_groups {
3519 assert_eq!(group.background.len(), 1);
3520 assert_eq!(group.channels[group.background[0].channel].pair, 1);
3521 }
3522 let first = &rml.spin_groups[0].background[0];
3523 assert_eq!((first.e_down, first.e_up), (0.0, 9.55e5));
3524 assert_eq!((first.r_con, first.s_con, first.s_lin), (-0.043, 0.01, 0.0));
3525 }
3526
3527 #[test]
3529 fn parses_cu63_shift_and_boundary() {
3530 let data = parse_file2_records(&mf2_fixture("Cu-63")).unwrap();
3531 let rml = rml(&data);
3532 assert!(rml.pairs[rml.incident_pair].shift);
3533 let p_wave = rml
3534 .spin_groups
3535 .iter()
3536 .flat_map(|g| &g.channels)
3537 .find(|c| c.pair == rml.incident_pair && c.l == 1)
3538 .unwrap();
3539 assert_eq!(p_wave.boundary, -1.0);
3540 }
3541
3542 #[test]
3545 fn parses_fe57_inelastic_pair() {
3546 let data = parse_file2_records(&mf2_fixture("Fe-57")).unwrap();
3547 let rml = rml(&data);
3548 let inelastic = rml.pairs.iter().find(|p| p.mt == 51).unwrap();
3549 assert_eq!(inelastic.q_value, -14413.0);
3550 assert!(inelastic.penetrability && !inelastic.shift);
3551 }
3552}