Skip to main content

refeff_io/
gg_dat.rs

1//! FEFF `gg.dat`/`gg.bin` 2-D complex Green's-function support.
2//!
3//! FMS writes `gg.bin` and MKGTR can write `gg.dat` through FEFF's generic
4//! `Write2D` path. Despite the `.bin` suffix, FEFF's generated files are
5//! sectioned text records with a `#SN#` marker, a `#DT#` shape line, and one
6//! formatted complex matrix per section. The Rust parser keeps the sections
7//! typed so generated FMS Green's-function handoff files can be checked
8//! directly.
9
10use std::fmt::Write as _;
11use std::path::Path;
12
13use ndarray::{Array2, Array3, Axis};
14use num_complex::Complex64;
15
16use crate::error::{IoError, Result};
17use crate::format::write_fortran_zero_scaled_exp;
18
19const GG_DAT_PATH: &str = "gg.dat";
20
21/// One `Write2D` section from FEFF `gg.dat`.
22#[derive(Debug, Clone, PartialEq)]
23pub struct GgDatSection {
24    /// One-based section number from the `#SN#` marker.
25    pub section_number: usize,
26    /// Complex Green's-function matrix for this section.
27    pub values: Array2<Complex64>,
28    /// Raw section prefix lines through the `#DT#`/`#H#` boilerplate.
29    ///
30    /// Generated FEFF `gg.dat` and `gg.bin` files can contain non-UTF bytes in
31    /// `#DF#` descriptor lines. Byte readers preserve those lines here so
32    /// byte-level roundtrips do not lose the original descriptor payload.
33    pub raw_prefix_lines: Option<Vec<Vec<u8>>>,
34}
35
36impl GgDatSection {
37    /// Matrix shape as `(rows, columns)`.
38    #[must_use]
39    pub fn shape(&self) -> (usize, usize) {
40        self.values.dim()
41    }
42
43    /// Number of matrix rows.
44    #[must_use]
45    pub fn row_count(&self) -> usize {
46        self.values.len_of(Axis(0))
47    }
48
49    /// Number of matrix columns.
50    #[must_use]
51    pub fn column_count(&self) -> usize {
52        self.values.len_of(Axis(1))
53    }
54}
55
56/// Parsed contents of FEFF `gg.dat`.
57#[derive(Debug, Clone, PartialEq)]
58pub struct GgDatData {
59    /// `Write2D` sections in file order.
60    pub sections: Vec<GgDatSection>,
61}
62
63impl GgDatData {
64    /// Number of matrix sections.
65    #[must_use]
66    pub fn section_count(&self) -> usize {
67        self.sections.len()
68    }
69
70    /// Assemble this FEFF Green-function stream for RIXS core kernels.
71    pub fn to_rixs_handoff(&self) -> Result<GgDatRixsHandoff> {
72        gg_dat_rixs_handoff(self)
73    }
74}
75
76/// RIXS-ready view of a FEFF `gg.dat`/`gg.bin` Green-function stream.
77#[derive(Debug, Clone, PartialEq)]
78pub struct GgDatRixsHandoff {
79    /// Square Green-function matrix order, FEFF angular-channel count.
80    pub angular_count: usize,
81    /// Number of Green-function energy sections.
82    pub energy_count: usize,
83    /// Green functions in RIXS `(L1, L2, energy)` order.
84    pub green: Array3<Complex64>,
85}
86
87/// Parse FEFF `gg.dat` text.
88pub fn parse_gg_dat(text: &str) -> Result<GgDatData> {
89    let lines = text.lines().enumerate().collect::<Vec<_>>();
90    let mut sections = Vec::new();
91    let mut position = 0;
92
93    while position < lines.len() {
94        let (line_number, raw) = lines[position];
95        let line = raw.trim();
96        if line.is_empty() {
97            position += 1;
98            continue;
99        }
100        if !line.starts_with("#SN#") {
101            return parse_error(
102                line_number + 1,
103                format!("expected #SN# section marker, found {line:?}"),
104            );
105        }
106        let section_number = parse_section_number(line_number + 1, line)?;
107        position += 1;
108
109        let (rows, columns) = find_section_shape(&lines, &mut position)?;
110        let values = parse_section_matrix(&lines, &mut position, rows, columns)?;
111        sections.push(GgDatSection {
112            section_number,
113            values,
114            raw_prefix_lines: None,
115        });
116    }
117
118    let data = GgDatData { sections };
119    validate_gg_dat(&data)?;
120    Ok(data)
121}
122
123/// Parse FEFF `gg.bin` text.
124pub fn parse_gg_bin(text: &str) -> Result<GgDatData> {
125    parse_gg_dat(text)
126}
127
128/// Parse FEFF `gg.dat` bytes while preserving raw section prefix lines.
129pub fn parse_gg_dat_bytes(bytes: &[u8]) -> Result<GgDatData> {
130    let text = semantic_gg_text(bytes);
131    let mut data = parse_gg_dat(&text)?;
132    let prefixes = collect_raw_prefix_lines(bytes)?;
133    if prefixes.len() != data.sections.len() {
134        return parse_error(
135            0,
136            format!(
137                "found {} raw section prefix block(s), expected {}",
138                prefixes.len(),
139                data.sections.len()
140            ),
141        );
142    }
143    for (section, prefix) in data.sections.iter_mut().zip(prefixes) {
144        section.raw_prefix_lines = Some(prefix);
145    }
146    Ok(data)
147}
148
149/// Parse FEFF `gg.bin` bytes while preserving raw section prefix lines.
150pub fn parse_gg_bin_bytes(bytes: &[u8]) -> Result<GgDatData> {
151    parse_gg_dat_bytes(bytes)
152}
153
154/// Render FEFF-compatible `gg.dat` text.
155pub fn gg_dat_string(data: &GgDatData) -> Result<String> {
156    validate_gg_dat(data)?;
157    let mut out = String::new();
158    for section in &data.sections {
159        write_canonical_section_prefix(&mut out, section)?;
160        for row in section.values.rows() {
161            for value in row {
162                write_fortran_zero_scaled_exp(&mut out, value.re, 20, 10)?;
163                out.push(' ');
164                write_fortran_zero_scaled_exp(&mut out, value.im, 20, 10)?;
165            }
166            writeln!(out)?;
167        }
168    }
169    Ok(out)
170}
171
172/// Render FEFF-compatible `gg.dat` bytes, preserving raw non-UTF section
173/// descriptor lines when they came from [`parse_gg_dat_bytes`].
174pub fn gg_dat_bytes(data: &GgDatData) -> Result<Vec<u8>> {
175    validate_gg_dat(data)?;
176    let mut out = Vec::new();
177    for section in &data.sections {
178        if let Some(prefix_lines) = &section.raw_prefix_lines {
179            for line in prefix_lines {
180                out.extend_from_slice(line);
181                out.push(b'\n');
182            }
183        } else {
184            let mut prefix = String::new();
185            write_canonical_section_prefix(&mut prefix, section)?;
186            out.extend_from_slice(prefix.as_bytes());
187        }
188
189        let mut row_text = String::new();
190        for row in section.values.rows() {
191            row_text.clear();
192            for value in row {
193                write_fortran_zero_scaled_exp(&mut row_text, value.re, 20, 10)?;
194                row_text.push(' ');
195                write_fortran_zero_scaled_exp(&mut row_text, value.im, 20, 10)?;
196            }
197            row_text.push('\n');
198            out.extend_from_slice(row_text.as_bytes());
199        }
200    }
201    Ok(out)
202}
203
204/// Render FEFF-compatible `gg.bin` text.
205pub fn gg_bin_string(data: &GgDatData) -> Result<String> {
206    gg_dat_string(data)
207}
208
209/// Render FEFF-compatible `gg.bin` bytes.
210pub fn gg_bin_bytes(data: &GgDatData) -> Result<Vec<u8>> {
211    gg_dat_bytes(data)
212}
213
214fn write_canonical_section_prefix(out: &mut String, section: &GgDatSection) -> Result<()> {
215    let (rows, columns) = section.shape();
216    writeln!(out, "#SN#   Section: {:4}", section.section_number)?;
217    writeln!(out, "#DF# This section written in txt.")?;
218    writeln!(out, "#H#")?;
219    writeln!(
220        out,
221        "#DT# 2D complex array with sizes {:4}{:4}",
222        rows, columns
223    )?;
224    writeln!(
225        out,
226        "#H# File is organized as follows:  Array(1,i)     Array(1,i+1)    Array(1,i+2)  . . ."
227    )?;
228    writeln!(out, "#H#                                Array(2,i)")?;
229    writeln!(out, "#H#                                     .")?;
230    writeln!(out, "#H#                                     .")?;
231    writeln!(out, "#H#                                     .")?;
232    Ok(())
233}
234
235/// Read FEFF `gg.dat` from a file.
236///
237/// FEFF can emit non-UTF-8 bytes in the descriptive `#DF#` line. This reader
238/// decodes the file lossily because the parser only relies on ASCII section
239/// markers and numeric rows.
240pub fn read_gg_dat(path: impl AsRef<Path>) -> Result<GgDatData> {
241    let path = path.as_ref();
242    let bytes = std::fs::read(path).map_err(|source| IoError::io(path, source))?;
243    parse_gg_dat_bytes(&bytes)
244}
245
246/// Read FEFF `gg.bin` from a file.
247///
248/// FEFF's `gg.bin` uses the same sectioned text format as `gg.dat` in the
249/// generated reference suite.
250pub fn read_gg_bin(path: impl AsRef<Path>) -> Result<GgDatData> {
251    read_gg_dat(path)
252}
253
254/// Write FEFF `gg.dat` text to a file.
255pub fn write_gg_dat(path: impl AsRef<Path>, data: &GgDatData) -> Result<()> {
256    let path = path.as_ref();
257    std::fs::write(path, gg_dat_bytes(data)?).map_err(|source| IoError::io(path, source))
258}
259
260/// Write FEFF `gg.bin` text to a file.
261pub fn write_gg_bin(path: impl AsRef<Path>, data: &GgDatData) -> Result<()> {
262    write_gg_dat(path, data)
263}
264
265/// Build the FEFF RIXS Green-function handoff from parsed `gg.dat`/`gg.bin`.
266///
267/// FEFF writes one `gg(L1,L2)` matrix per energy section. RIXS core kernels
268/// consume the same values as a single `(L1, L2, energy)` tensor.
269pub fn gg_dat_rixs_handoff(data: &GgDatData) -> Result<GgDatRixsHandoff> {
270    validate_gg_dat(data)?;
271    let first = data
272        .sections
273        .first()
274        .ok_or_else(|| parse_error_value(0, "at least one section is required"))?;
275    let (rows, columns) = first.shape();
276    if rows != columns {
277        return parse_error(
278            first.section_number,
279            format!("RIXS Green-function matrix must be square, got {rows}x{columns}"),
280        );
281    }
282
283    let energy_count = data.section_count();
284    let mut green = Array3::zeros((rows, columns, energy_count));
285    for (energy, section) in data.sections.iter().enumerate() {
286        if section.shape() != (rows, columns) {
287            return parse_error(
288                section.section_number,
289                format!(
290                    "RIXS Green-function section shape {:?} does not match first section shape {:?}",
291                    section.shape(),
292                    (rows, columns)
293                ),
294            );
295        }
296        for row in 0..rows {
297            for column in 0..columns {
298                green[(row, column, energy)] = section.values[(row, column)];
299            }
300        }
301    }
302
303    Ok(GgDatRixsHandoff {
304        angular_count: rows,
305        energy_count,
306        green,
307    })
308}
309
310fn collect_raw_prefix_lines(bytes: &[u8]) -> Result<Vec<Vec<Vec<u8>>>> {
311    let mut prefixes = Vec::new();
312    let mut current = Vec::new();
313    let mut in_prefix = false;
314    let mut in_descriptor = false;
315
316    for raw in bytes.split(|byte| *byte == b'\n') {
317        let line = strip_trailing_cr(raw);
318        let trimmed = trim_ascii(line);
319        if trimmed.is_empty() {
320            if in_prefix {
321                current.push(line.to_vec());
322            }
323            continue;
324        }
325
326        if trimmed.starts_with(b"#SN#") {
327            if in_prefix && !current.is_empty() {
328                return parse_error(0, "section prefix ended before a data row");
329            }
330            current.clear();
331            current.push(line.to_vec());
332            in_prefix = true;
333            in_descriptor = false;
334            continue;
335        }
336
337        if !in_prefix {
338            continue;
339        }
340
341        if in_descriptor {
342            if trimmed.starts_with(b"#H#") {
343                current.push(line.to_vec());
344                in_descriptor = false;
345                continue;
346            }
347            if trimmed.starts_with(b"#SN#")
348                || trimmed.starts_with(b"#DF#")
349                || trimmed.starts_with(b"#DT#")
350            {
351                in_descriptor = false;
352            } else {
353                current.push(line.to_vec());
354                continue;
355            }
356        }
357
358        if is_prefix_line(trimmed) {
359            current.push(line.to_vec());
360            in_descriptor = trimmed.starts_with(b"#DF#");
361            continue;
362        }
363
364        if current.is_empty() {
365            return parse_error(0, "data row appeared before a #SN# section marker");
366        }
367        prefixes.push(std::mem::take(&mut current));
368        in_prefix = false;
369    }
370
371    if in_prefix && !current.is_empty() {
372        return parse_error(0, "section prefix ended before a data row");
373    }
374
375    Ok(prefixes)
376}
377
378/// Build a UTF-8 semantic view without interpreting arbitrary descriptor
379/// bytes as matrix rows.
380///
381/// FEFF's generic `WriteDComplex2D` path can write an uninitialized `FlType`
382/// payload after `#DF#`. That payload may contain an embedded LF, leaving a
383/// fragment such as `.` on the following physical line. The raw byte parser
384/// preserves every prefix line separately; the semantic parser only needs the
385/// marker and skips descriptor continuations through the following `#H#`.
386fn semantic_gg_text(bytes: &[u8]) -> String {
387    let lines = bytes
388        .split(|byte| *byte == b'\n')
389        .map(strip_trailing_cr)
390        .collect::<Vec<_>>();
391    let mut out = String::new();
392    let mut index = 0;
393
394    while index < lines.len() {
395        let line = lines[index];
396        let trimmed = trim_ascii(line);
397        if trimmed.starts_with(b"#DF#") {
398            out.push_str("#DF#\n");
399            let mut continuation_end = None;
400            let mut candidate_index = index + 1;
401            while candidate_index < lines.len() {
402                let candidate = trim_ascii(lines[candidate_index]);
403                if candidate.starts_with(b"#H#") {
404                    continuation_end = Some(candidate_index);
405                    break;
406                }
407                if candidate.starts_with(b"#SN#")
408                    || candidate.starts_with(b"#DF#")
409                    || candidate.starts_with(b"#DT#")
410                {
411                    break;
412                }
413                candidate_index += 1;
414            }
415            index = continuation_end.unwrap_or(index + 1);
416            continue;
417        }
418        out.push_str(&String::from_utf8_lossy(line));
419        out.push('\n');
420        index += 1;
421    }
422    out
423}
424
425fn is_prefix_line(line: &[u8]) -> bool {
426    line.starts_with(b"#DF#") || line.starts_with(b"#H#") || line.starts_with(b"#DT#")
427}
428
429fn strip_trailing_cr(line: &[u8]) -> &[u8] {
430    if let Some(rest) = line.strip_suffix(b"\r") {
431        rest
432    } else {
433        line
434    }
435}
436
437fn trim_ascii(bytes: &[u8]) -> &[u8] {
438    let start = match bytes.iter().position(|byte| !byte.is_ascii_whitespace()) {
439        Some(index) => index,
440        None => bytes.len(),
441    };
442    let end = match bytes.iter().rposition(|byte| !byte.is_ascii_whitespace()) {
443        Some(index) => index + 1,
444        None => start,
445    };
446    &bytes[start..end]
447}
448
449fn find_section_shape(lines: &[(usize, &str)], position: &mut usize) -> Result<(usize, usize)> {
450    let mut shape = None;
451    while *position < lines.len() {
452        let (index, raw) = lines[*position];
453        let line_number = index + 1;
454        let line = raw.trim();
455        if line.is_empty() || is_header_line(line) {
456            *position += 1;
457            continue;
458        }
459        if line.starts_with("#SN#") {
460            return parse_error(line_number, "section is missing #DT# shape line");
461        }
462        if line.starts_with("#DT#") {
463            shape = Some(parse_shape_line(line_number, line)?);
464            *position += 1;
465            continue;
466        }
467        if let Some(shape) = shape {
468            return Ok(shape);
469        }
470        return parse_error(
471            line_number,
472            format!("expected #DT# shape line before data, found {line:?}"),
473        );
474    }
475    shape.ok_or_else(|| parse_error_value(0, "section is missing #DT# shape line"))
476}
477
478fn parse_section_matrix(
479    lines: &[(usize, &str)],
480    position: &mut usize,
481    rows: usize,
482    columns: usize,
483) -> Result<Array2<Complex64>> {
484    let mut values = Vec::with_capacity(checked_product(rows, columns)?);
485    for row_index in 0..rows {
486        let (line_number, line) = next_data_line(lines, position, row_index + 1)?;
487        let row = parse_complex_row(line_number, line, columns)?;
488        values.extend(row);
489    }
490    Array2::from_shape_vec((rows, columns), values)
491        .map_err(|source| parse_error_value(0, format!("invalid section matrix shape: {source}")))
492}
493
494fn next_data_line<'a>(
495    lines: &'a [(usize, &'a str)],
496    position: &mut usize,
497    row: usize,
498) -> Result<(usize, &'a str)> {
499    while *position < lines.len() {
500        let (index, raw) = lines[*position];
501        *position += 1;
502        let line_number = index + 1;
503        let line = raw.trim();
504        if line.is_empty() || is_header_line(line) {
505            continue;
506        }
507        if line.starts_with("#SN#") {
508            return parse_error(
509                line_number,
510                format!("section ended before matrix row {row} was read"),
511            );
512        }
513        if let Some(data) = line.strip_prefix("#HD#") {
514            return Ok((line_number, data.trim()));
515        }
516        if line.starts_with('#') {
517            return parse_error(line_number, format!("unexpected marker {line:?} in data"));
518        }
519        return Ok((line_number, line));
520    }
521    parse_error(0, format!("missing matrix row {row}"))
522}
523
524fn parse_complex_row(line_number: usize, line: &str, columns: usize) -> Result<Vec<Complex64>> {
525    let tokens = line.split_whitespace().collect::<Vec<_>>();
526    let expected = checked_product(columns, 2)?;
527    if tokens.len() != expected {
528        return parse_error(
529            line_number,
530            format!(
531                "matrix row has {} token(s), expected {expected}",
532                tokens.len()
533            ),
534        );
535    }
536
537    tokens
538        .chunks_exact(2)
539        .map(|pair| {
540            Ok(Complex64::new(
541                parse_f64(line_number, "real", pair[0])?,
542                parse_f64(line_number, "imaginary", pair[1])?,
543            ))
544        })
545        .collect()
546}
547
548fn parse_section_number(line_number: usize, line: &str) -> Result<usize> {
549    let token = line
550        .split_whitespace()
551        .last()
552        .ok_or_else(|| parse_error_value(line_number, "missing section number"))?;
553    parse_usize(line_number, "section_number", token)
554}
555
556fn parse_shape_line(line_number: usize, line: &str) -> Result<(usize, usize)> {
557    let tokens = line.split_whitespace().collect::<Vec<_>>();
558    let has_supported_type = tokens.iter().any(|token| token.eq_ignore_ascii_case("2D"))
559        && tokens
560            .iter()
561            .any(|token| token.eq_ignore_ascii_case("complex"))
562        && tokens
563            .iter()
564            .any(|token| token.eq_ignore_ascii_case("array"));
565    if !has_supported_type {
566        return parse_error(line_number, "expected 2D complex array shape line");
567    }
568    let rows_token = tokens
569        .get(tokens.len().saturating_sub(2))
570        .ok_or_else(|| parse_error_value(line_number, "missing matrix row count"))?;
571    let columns_token = tokens
572        .last()
573        .ok_or_else(|| parse_error_value(line_number, "missing matrix column count"))?;
574    let rows = parse_usize(line_number, "rows", rows_token)?;
575    let columns = parse_usize(line_number, "columns", columns_token)?;
576    if rows == 0 || columns == 0 {
577        return parse_error(line_number, "matrix dimensions must be positive");
578    }
579    Ok((rows, columns))
580}
581
582fn validate_gg_dat(data: &GgDatData) -> Result<()> {
583    if data.section_count() == 0 {
584        return parse_error(0, "at least one section is required");
585    }
586    for (index, section) in data.sections.iter().enumerate() {
587        let row = index + 1;
588        if section.section_number == 0 {
589            return parse_error(row, "section number must be positive");
590        }
591        if section.row_count() == 0 || section.column_count() == 0 {
592            return parse_error(row, "section matrix dimensions must be positive");
593        }
594        for value in &section.values {
595            validate_complex("value", *value, row)?;
596        }
597    }
598    Ok(())
599}
600
601fn validate_complex(field: &'static str, value: Complex64, row: usize) -> Result<()> {
602    validate_finite(field, value.re, row)?;
603    validate_finite(field, value.im, row)
604}
605
606fn validate_finite(field: &'static str, value: f64, row: usize) -> Result<()> {
607    if value.is_finite() {
608        Ok(())
609    } else {
610        parse_error(row, format!("{field} must be finite"))
611    }
612}
613
614fn checked_product(rows: usize, columns: usize) -> Result<usize> {
615    rows.checked_mul(columns)
616        .ok_or_else(|| parse_error_value(0, "matrix shape overflows usize"))
617}
618
619fn is_header_line(line: &str) -> bool {
620    line.starts_with("#DF#") || line.starts_with("#H#")
621}
622
623fn parse_f64(line: usize, field: &'static str, token: &str) -> Result<f64> {
624    token
625        .replace(['D', 'd'], "E")
626        .parse::<f64>()
627        .map_err(|_| parse_error_value(line, format!("could not parse {field} from {token:?}")))
628}
629
630fn parse_usize(line: usize, field: &'static str, token: &str) -> Result<usize> {
631    token
632        .parse::<usize>()
633        .map_err(|_| parse_error_value(line, format!("could not parse {field} from {token:?}")))
634}
635
636fn parse_error<T>(line: usize, message: impl Into<String>) -> Result<T> {
637    Err(parse_error_value(line, message))
638}
639
640fn parse_error_value(line: usize, message: impl Into<String>) -> IoError {
641    IoError::Parse {
642        path: GG_DAT_PATH.into(),
643        line,
644        message: message.into(),
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651
652    #[test]
653    fn parses_gg_dat_sections() -> Result<()> {
654        let parsed = parse_gg_dat(GG_DAT)?;
655        assert_eq!(parsed.section_count(), 2);
656        assert_eq!(parsed.sections[0].section_number, 1);
657        assert_eq!(parsed.sections[0].shape(), (2, 2));
658        assert_eq!(parsed.sections[0].values[(0, 0)], Complex64::new(1.0, -0.5));
659        assert_eq!(
660            parsed.sections[0].values[(1, 1)],
661            Complex64::new(-4.0, 0.75)
662        );
663        assert_eq!(parsed.sections[1].shape(), (1, 2));
664        assert_eq!(parsed.sections[1].values[(0, 1)], Complex64::new(6.0, -6.5));
665
666        let rendered = gg_dat_string(&parsed)?;
667        assert_eq!(parse_gg_dat(&rendered)?, parsed);
668        Ok(())
669    }
670
671    #[test]
672    fn accepts_fortran_d_exponents_and_header_data_prefix() -> Result<()> {
673        let parsed = parse_gg_dat(
674            r#"#SN#   Section:    1
675#DT# 2D double complex array with sizes    1   1
676#DT# 2D complex array with sizes    1   1
677#HD# 1.0D+00 -2.5D+00
678"#,
679        )?;
680        assert_eq!(parsed.section_count(), 1);
681        assert_eq!(parsed.sections[0].values[(0, 0)], Complex64::new(1.0, -2.5));
682        Ok(())
683    }
684
685    #[test]
686    fn preserves_non_utf_descriptor_bytes() -> Result<()> {
687        let bytes = b"#SN#   Section:    1\n#DF# This section written in \0\xc0\xc2v.\n#H#\n#DT# 2D complex array with sizes    1   1\n#H# File is organized as follows:  Array(1,i)     Array(1,i+1)    Array(1,i+2)  . . .\n#H#                                Array(2,i)\n#H#                                     .\n#H#                                     .\n#H#                                     .\n    0.1000000000E+01    -0.2500000000E+01\n";
688        let parsed = parse_gg_dat_bytes(bytes)?;
689        assert_eq!(parsed.section_count(), 1);
690        assert_eq!(parsed.sections[0].values[(0, 0)], Complex64::new(1.0, -2.5));
691        assert_eq!(gg_dat_bytes(&parsed)?, bytes);
692        Ok(())
693    }
694
695    #[test]
696    fn preserves_descriptor_bytes_with_embedded_line_feed() -> Result<()> {
697        let bytes = b"#SN#   Section:    1\n#DF# This section written in \0\xc0\xc2v\n.\n#H#\n#DT# 2D complex array with sizes    1   1\n#H# File is organized as follows:  Array(1,i)     Array(1,i+1)    Array(1,i+2)  . . .\n#H#                                Array(2,i)\n#H#                                     .\n#H#                                     .\n#H#                                     .\n    0.1000000000E+01    -0.2500000000E+01\n";
698        let parsed = parse_gg_dat_bytes(bytes)?;
699        assert_eq!(parsed.section_count(), 1);
700        assert_eq!(parsed.sections[0].values[(0, 0)], Complex64::new(1.0, -2.5));
701        assert_eq!(gg_dat_bytes(&parsed)?, bytes);
702        Ok(())
703    }
704
705    #[test]
706    fn gg_dat_builds_rixs_green_handoff() -> Result<()> {
707        let data = sample_square_gg_dat();
708        let handoff = data.to_rixs_handoff()?;
709
710        assert_eq!(handoff.angular_count, 2);
711        assert_eq!(handoff.energy_count, 2);
712        assert_eq!(handoff.green.dim(), (2, 2, 2));
713        assert_eq!(handoff.green[(0, 0, 0)], Complex64::new(1.0, -1.0));
714        assert_eq!(handoff.green[(1, 0, 1)], Complex64::new(7.0, -7.0));
715        Ok(())
716    }
717
718    #[test]
719    fn gg_dat_rixs_handoff_rejects_non_square_or_mismatched_sections() {
720        let non_square = GgDatData {
721            sections: vec![GgDatSection {
722                section_number: 1,
723                values: Array2::from_shape_vec(
724                    (1, 2),
725                    vec![Complex64::new(1.0, 0.0), Complex64::new(2.0, 0.0)],
726                )
727                .expect("sample shape"),
728                raw_prefix_lines: None,
729            }],
730        };
731        assert!(gg_dat_rixs_handoff(&non_square).is_err());
732
733        let mut mismatched = sample_square_gg_dat();
734        mismatched.sections.push(GgDatSection {
735            section_number: 3,
736            values: Array2::from_elem((3, 3), Complex64::new(0.0, 0.0)),
737            raw_prefix_lines: None,
738        });
739        assert!(gg_dat_rixs_handoff(&mismatched).is_err());
740    }
741
742    #[test]
743    fn rejects_bad_gg_dat_inputs() {
744        assert!(parse_gg_dat("").is_err());
745        assert!(
746            parse_gg_dat("#SN# Section: 0\n#DT# 2D complex array with sizes 1 1\n1 2\n").is_err()
747        );
748        assert!(parse_gg_dat("#SN# Section: 1\n1 2\n").is_err());
749        assert!(parse_gg_dat("#SN# Section: 1\n#DT# 2D real array with sizes 1 1\n1 2\n").is_err());
750        assert!(
751            parse_gg_dat("#SN# Section: 1\n#DT# 2D complex array with sizes 1 1\n1\n").is_err()
752        );
753        assert!(
754            parse_gg_dat("#SN# Section: 1\n#DT# 2D complex array with sizes 1 1\nNaN 1\n").is_err()
755        );
756    }
757
758    fn sample_square_gg_dat() -> GgDatData {
759        GgDatData {
760            sections: vec![
761                GgDatSection {
762                    section_number: 1,
763                    values: Array2::from_shape_vec(
764                        (2, 2),
765                        vec![
766                            Complex64::new(1.0, -1.0),
767                            Complex64::new(2.0, -2.0),
768                            Complex64::new(3.0, -3.0),
769                            Complex64::new(4.0, -4.0),
770                        ],
771                    )
772                    .expect("sample shape"),
773                    raw_prefix_lines: None,
774                },
775                GgDatSection {
776                    section_number: 2,
777                    values: Array2::from_shape_vec(
778                        (2, 2),
779                        vec![
780                            Complex64::new(5.0, -5.0),
781                            Complex64::new(6.0, -6.0),
782                            Complex64::new(7.0, -7.0),
783                            Complex64::new(8.0, -8.0),
784                        ],
785                    )
786                    .expect("sample shape"),
787                    raw_prefix_lines: None,
788                },
789            ],
790        }
791    }
792
793    const GG_DAT: &str = r#"#SN#   Section:    1
794#DF# This section written in txt.
795#H#
796#DT# 2D complex array with sizes    2   2
797#H# File is organized as follows:  Array(1,i)     Array(1,i+1)    Array(1,i+2)  . . .
798#H#                                Array(2,i)
799    1.0000000000E+00    -5.0000000000E-01    2.0000000000E+00     2.5000000000E+00
800    3.0000000000E+00     0.0000000000E+00   -4.0000000000E+00     7.5000000000E-01
801#SN#   Section:    2
802#DT# 2D complex array with sizes    1   2
803    5.0000000000E+00    -5.5000000000E+00    6.0000000000E+00    -6.5000000000E+00
804"#;
805}