1use std::collections::BTreeMap;
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use phasesmith_model::{DomainError, PatternRecord, TofPatternRecord};
10
11type GsasBank = (Vec<String>, Vec<String>);
12type GsasBanks = BTreeMap<usize, GsasBank>;
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub enum PowderFormat {
17 #[default]
19 Auto,
20 Columns,
22 GsasFxye,
24 GsasStd,
26}
27
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30pub enum TofPowderFormat {
31 #[default]
33 Auto,
34 Columns,
36 GsasSlogFxye,
38 GsasConstStd,
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub struct PowderReadLimits {
45 pub max_bytes: usize,
47 pub max_rows: usize,
49}
50
51impl Default for PowderReadLimits {
52 fn default() -> Self {
53 Self {
54 max_bytes: 64 * 1024 * 1024,
55 max_rows: 5_000_000,
56 }
57 }
58}
59
60impl PowderReadLimits {
61 pub fn validate(self) -> Result<(), PowderIoError> {
67 if self.max_bytes == 0 || self.max_rows == 0 {
68 return Err(PowderIoError::InvalidLimits);
69 }
70 Ok(())
71 }
72}
73
74#[derive(Clone, Debug, PartialEq)]
76pub struct PowderData {
77 pub pattern: PatternRecord,
79 pub format: PowderFormat,
81 pub source_path: Option<PathBuf>,
83 pub bank: Option<usize>,
85}
86
87#[derive(Clone, Debug, PartialEq)]
89pub struct TofPowderData {
90 pub pattern: TofPatternRecord,
92 pub format: TofPowderFormat,
94 pub source_path: Option<PathBuf>,
96 pub bank: Option<usize>,
98 pub logarithmic_grid: bool,
100}
101
102#[derive(Debug)]
104pub enum PowderIoError {
105 InvalidLimits,
107 InvalidBank,
109 ByteLimitExceeded {
111 actual: u64,
113 maximum: usize,
115 },
116 RowLimitExceeded {
118 maximum: usize,
120 },
121 Io(std::io::Error),
123 Parse {
125 line: usize,
127 message: String,
129 },
130 MissingBank {
132 requested: usize,
134 available: Vec<usize>,
136 },
137 Domain(DomainError),
139}
140
141impl Display for PowderIoError {
142 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
143 match self {
144 Self::InvalidLimits => formatter.write_str("powder read limits must be positive"),
145 Self::InvalidBank => formatter.write_str("bank must be a positive integer"),
146 Self::ByteLimitExceeded { actual, maximum } => {
147 write!(
148 formatter,
149 "powder input exceeds max_bytes: {actual} > {maximum}"
150 )
151 }
152 Self::RowLimitExceeded { maximum } => {
153 write!(formatter, "powder data exceeds max_rows: {maximum}")
154 }
155 Self::Io(error) => Display::fmt(error, formatter),
156 Self::Parse { line: 0, message } => formatter.write_str(message),
157 Self::Parse { line, message } => write!(formatter, "line {line}: {message}"),
158 Self::MissingBank {
159 requested,
160 available,
161 } => {
162 let values = available
163 .iter()
164 .map(usize::to_string)
165 .collect::<Vec<_>>()
166 .join(", ");
167 write!(
168 formatter,
169 "GSAS bank {requested} not found; available banks: {}",
170 if values.is_empty() { "none" } else { &values }
171 )
172 }
173 Self::Domain(error) => Display::fmt(error, formatter),
174 }
175 }
176}
177
178impl Error for PowderIoError {
179 fn source(&self) -> Option<&(dyn Error + 'static)> {
180 match self {
181 Self::Io(error) => Some(error),
182 Self::Domain(error) => Some(error),
183 _ => None,
184 }
185 }
186}
187
188pub fn read_powder_file(
194 path: impl AsRef<Path>,
195 format: PowderFormat,
196 bank: usize,
197 limits: PowderReadLimits,
198) -> Result<PowderData, PowderIoError> {
199 limits.validate()?;
200 if bank == 0 {
201 return Err(PowderIoError::InvalidBank);
202 }
203 let path = path.as_ref();
204 let size = fs::metadata(path).map_err(PowderIoError::Io)?.len();
205 if size > u64::try_from(limits.max_bytes).unwrap_or(u64::MAX) {
206 return Err(PowderIoError::ByteLimitExceeded {
207 actual: size,
208 maximum: limits.max_bytes,
209 });
210 }
211 let text = fs::read_to_string(path).map_err(PowderIoError::Io)?;
212 parse_powder_text_inner(&text, format, bank, limits, Some(path.to_owned()))
213}
214
215pub fn parse_powder_text(
221 text: &str,
222 format: PowderFormat,
223 bank: usize,
224 limits: PowderReadLimits,
225) -> Result<PowderData, PowderIoError> {
226 parse_powder_text_inner(text, format, bank, limits, None)
227}
228
229pub fn read_tof_powder_file(
238 path: impl AsRef<Path>,
239 bank: usize,
240 limits: PowderReadLimits,
241) -> Result<TofPowderData, PowderIoError> {
242 read_tof_powder_file_as(path, TofPowderFormat::Auto, bank, limits)
243}
244
245pub fn read_tof_powder_file_as(
251 path: impl AsRef<Path>,
252 format: TofPowderFormat,
253 bank: usize,
254 limits: PowderReadLimits,
255) -> Result<TofPowderData, PowderIoError> {
256 limits.validate()?;
257 if bank == 0 {
258 return Err(PowderIoError::InvalidBank);
259 }
260 let path = path.as_ref();
261 let size = fs::metadata(path).map_err(PowderIoError::Io)?.len();
262 if size > u64::try_from(limits.max_bytes).unwrap_or(u64::MAX) {
263 return Err(PowderIoError::ByteLimitExceeded {
264 actual: size,
265 maximum: limits.max_bytes,
266 });
267 }
268 let text = fs::read_to_string(path).map_err(PowderIoError::Io)?;
269 parse_tof_powder_text_inner(&text, format, bank, limits, Some(path.to_owned()))
270}
271
272pub fn parse_tof_powder_text(
278 text: &str,
279 bank: usize,
280 limits: PowderReadLimits,
281) -> Result<TofPowderData, PowderIoError> {
282 parse_tof_powder_text_as(text, TofPowderFormat::Auto, bank, limits)
283}
284
285pub fn parse_tof_powder_text_as(
291 text: &str,
292 format: TofPowderFormat,
293 bank: usize,
294 limits: PowderReadLimits,
295) -> Result<TofPowderData, PowderIoError> {
296 parse_tof_powder_text_inner(text, format, bank, limits, None)
297}
298
299fn parse_tof_powder_text_inner(
300 text: &str,
301 format: TofPowderFormat,
302 bank: usize,
303 limits: PowderReadLimits,
304 source_path: Option<PathBuf>,
305) -> Result<TofPowderData, PowderIoError> {
306 limits.validate()?;
307 if bank == 0 {
308 return Err(PowderIoError::InvalidBank);
309 }
310 if text.len() > limits.max_bytes {
311 return Err(PowderIoError::ByteLimitExceeded {
312 actual: u64::try_from(text.len()).unwrap_or(u64::MAX),
313 maximum: limits.max_bytes,
314 });
315 }
316 let text = text.strip_prefix('\u{feff}').unwrap_or(text);
317 let resolved = match format {
318 TofPowderFormat::Auto
319 if text
320 .lines()
321 .any(|line| line.trim_start().starts_with("BANK")) =>
322 {
323 let banks = gsas_banks(text)?;
324 let (header, _) = selected_bank(&banks, bank)?;
325 if header
326 .last()
327 .is_some_and(|value| value.eq_ignore_ascii_case("FXYE"))
328 {
329 TofPowderFormat::GsasSlogFxye
330 } else {
331 TofPowderFormat::GsasConstStd
332 }
333 }
334 TofPowderFormat::Auto => TofPowderFormat::Columns,
335 selected => selected,
336 };
337 match resolved {
338 TofPowderFormat::Columns => read_tof_columns(text, source_path, limits.max_rows),
339 TofPowderFormat::GsasConstStd => {
340 read_tof_gsas_std(text, source_path, bank, limits.max_rows)
341 }
342 TofPowderFormat::GsasSlogFxye => {
343 read_tof_gsas_slog_fxye(text, source_path, bank, limits.max_rows)
344 }
345 TofPowderFormat::Auto => unreachable!("auto TOF format is resolved above"),
346 }
347}
348
349fn read_tof_gsas_slog_fxye(
350 text: &str,
351 source_path: Option<PathBuf>,
352 bank: usize,
353 max_rows: usize,
354) -> Result<TofPowderData, PowderIoError> {
355 let banks = gsas_banks(text)?;
356 let (header, lines) = selected_bank(&banks, bank)?;
357 if header
358 .get(4)
359 .is_none_or(|value| !value.eq_ignore_ascii_case("SLOG"))
360 || header
361 .last()
362 .is_none_or(|value| !value.eq_ignore_ascii_case("FXYE"))
363 {
364 return Err(parse_error(0, "TOF input requires a GSAS SLOG FXYE bank"));
365 }
366 let declared_rows = header
367 .get(2)
368 .and_then(|value| value.parse::<usize>().ok())
369 .ok_or_else(|| parse_error(0, "invalid GSAS TOF bank dimensions"))?;
370 let rows = numeric_rows(&lines.join("\n"), Some(3), max_rows)?;
371 if rows.len() != declared_rows {
372 return Err(parse_error(
373 0,
374 format!(
375 "GSAS TOF bank contains {} rows; expected {declared_rows}",
376 rows.len()
377 ),
378 ));
379 }
380 if rows.len() < 2 {
381 return Err(parse_error(
382 0,
383 "GSAS TOF SLOG FXYE requires at least two bin-boundary rows",
384 ));
385 }
386 let output_count = rows.len() - 1;
387 let mut tof_us = Vec::with_capacity(output_count);
388 let mut observed_y = Vec::with_capacity(output_count);
389 let mut uncertainty = Vec::with_capacity(output_count);
390 let mut mask = Vec::with_capacity(output_count);
391 for pair in rows.windows(2) {
392 let row = &pair[0];
393 let next = &pair[1];
394 let width = next[0] - row[0];
395 if !width.is_finite() || width <= 0.0 {
396 return Err(parse_error(
397 0,
398 "GSAS TOF bin boundaries must be finite and strictly increasing",
399 ));
400 }
401 let supplied = row[2];
402 if supplied < 0.0 {
403 return Err(parse_error(0, "GSAS TOF uncertainty must be nonnegative"));
404 }
405 tof_us.push(0.5 * (row[0] + next[0]));
406 observed_y.push(row[1] / width);
407 uncertainty.push(if supplied == 0.0 {
408 1.0
409 } else {
410 supplied / width
411 });
412 mask.push(supplied != 0.0 && row[1] != 0.0);
413 }
414 let mask = mask.iter().any(|included| !included).then_some(mask);
415 let pattern = TofPatternRecord::new(tof_us, Some(observed_y), Some(uncertainty), mask, None)
416 .map_err(PowderIoError::Domain)?;
417 Ok(TofPowderData {
418 pattern,
419 format: TofPowderFormat::GsasSlogFxye,
420 source_path,
421 bank: Some(bank),
422 logarithmic_grid: true,
423 })
424}
425
426fn read_tof_columns(
427 text: &str,
428 source_path: Option<PathBuf>,
429 max_rows: usize,
430) -> Result<TofPowderData, PowderIoError> {
431 let rows = numeric_rows(text, None, max_rows)?;
432 let columns = rows
433 .first()
434 .map(Vec::len)
435 .ok_or_else(|| parse_error(0, "powder input contains no numeric rows"))?;
436 if !matches!(columns, 2 | 3) || rows.iter().any(|row| row.len() != columns) {
437 return Err(parse_error(
438 0,
439 "TOF columns require two or three consistent numeric fields",
440 ));
441 }
442 let uncertainty = (columns == 3).then(|| rows.iter().map(|row| row[2]).collect::<Vec<_>>());
443 if uncertainty
444 .as_deref()
445 .is_some_and(|values| values.iter().any(|value| *value <= 0.0))
446 {
447 return Err(parse_error(
448 0,
449 "TOF column uncertainty must be finite and positive",
450 ));
451 }
452 let pattern = TofPatternRecord::new(
453 rows.iter().map(|row| row[0]).collect(),
454 Some(rows.iter().map(|row| row[1]).collect()),
455 uncertainty,
456 None,
457 None,
458 )
459 .map_err(PowderIoError::Domain)?;
460 Ok(TofPowderData {
461 pattern,
462 format: TofPowderFormat::Columns,
463 source_path,
464 bank: None,
465 logarithmic_grid: false,
466 })
467}
468
469fn read_tof_gsas_std(
470 text: &str,
471 source_path: Option<PathBuf>,
472 bank: usize,
473 max_rows: usize,
474) -> Result<TofPowderData, PowderIoError> {
475 let banks = gsas_banks(text)?;
476 let (header, lines) = selected_bank(&banks, bank)?;
477 let encoding = if header.len() >= 10 {
478 header.last().map_or("", String::as_str)
479 } else {
480 "STD"
481 };
482 if header.len() < 7
483 || !header[4].eq_ignore_ascii_case("CONST")
484 || !encoding.eq_ignore_ascii_case("STD")
485 {
486 return Err(parse_error(
487 0,
488 "TOF input requires a packed constant-step GSAS STD bank",
489 ));
490 }
491 let row_count = header[2]
492 .parse::<usize>()
493 .map_err(|_| parse_error(0, "invalid packed GSAS TOF bank dimensions"))?;
494 let lower_us = parse_header_f64(&header[5])?;
495 let step_us = parse_header_f64(&header[6])?;
496 if row_count == 0 || row_count > max_rows || !lower_us.is_finite() || !step_us.is_finite() {
497 return Err(parse_error(
498 0,
499 "packed GSAS TOF bank dimensions exceed limits or are non-finite",
500 ));
501 }
502 if step_us <= 0.0 {
503 return Err(parse_error(0, "packed GSAS TOF step must be positive"));
504 }
505 let (counts, count_sigma) = packed_std_values(lines, row_count)?;
506 let mut tof_us = Vec::with_capacity(row_count);
507 let mut observed_y = Vec::with_capacity(row_count);
508 let mut uncertainty = Vec::with_capacity(row_count);
509 let mut mask = Vec::with_capacity(row_count);
510 for (index, (&count, &sigma)) in counts.iter().zip(&count_sigma).enumerate() {
511 let exact_index = u32::try_from(index)
512 .map_err(|_| parse_error(0, "packed GSAS TOF bank dimensions exceed numeric limits"))?;
513 tof_us.push(lower_us + (f64::from(exact_index) + 0.5) * step_us);
514 observed_y.push(count / step_us);
515 uncertainty.push(if count > 0.0 { sigma / step_us } else { 1.0 });
516 mask.push(count > 0.0);
517 }
518 let pattern = TofPatternRecord::new(
519 tof_us,
520 Some(observed_y),
521 Some(uncertainty),
522 mask.iter().any(|included| !included).then_some(mask),
523 None,
524 )
525 .map_err(PowderIoError::Domain)?;
526 Ok(TofPowderData {
527 pattern,
528 format: TofPowderFormat::GsasConstStd,
529 source_path,
530 bank: Some(bank),
531 logarithmic_grid: false,
532 })
533}
534
535fn parse_powder_text_inner(
536 text: &str,
537 format: PowderFormat,
538 bank: usize,
539 limits: PowderReadLimits,
540 source_path: Option<PathBuf>,
541) -> Result<PowderData, PowderIoError> {
542 limits.validate()?;
543 if bank == 0 {
544 return Err(PowderIoError::InvalidBank);
545 }
546 if text.len() > limits.max_bytes {
547 return Err(PowderIoError::ByteLimitExceeded {
548 actual: text.len() as u64,
549 maximum: limits.max_bytes,
550 });
551 }
552 let text = text.strip_prefix('\u{feff}').unwrap_or(text);
553 let selected = match format {
554 PowderFormat::Auto => detect_format(text, source_path.as_deref()),
555 selected => selected,
556 };
557 match selected {
558 PowderFormat::Columns => read_columns(text, source_path, limits.max_rows),
559 PowderFormat::GsasFxye => read_gsas_fxye(text, source_path, bank, limits.max_rows),
560 PowderFormat::GsasStd => read_gsas_std(text, source_path, bank, limits.max_rows),
561 PowderFormat::Auto => unreachable!("auto format is resolved above"),
562 }
563}
564
565fn detect_format(text: &str, source: Option<&Path>) -> PowderFormat {
566 let headers = text
567 .lines()
568 .map(str::trim)
569 .filter(|line| line.to_ascii_uppercase().starts_with("BANK "))
570 .collect::<Vec<_>>();
571 if headers
572 .iter()
573 .filter_map(|header| header.split_whitespace().next_back())
574 .any(|encoding| encoding.eq_ignore_ascii_case("FXYE"))
575 {
576 return PowderFormat::GsasFxye;
577 }
578 if !headers.is_empty() {
579 return PowderFormat::GsasStd;
580 }
581 if source
582 .and_then(Path::extension)
583 .is_some_and(|extension| extension.eq_ignore_ascii_case("fxye"))
584 {
585 return PowderFormat::GsasFxye;
586 }
587 PowderFormat::Columns
588}
589
590fn numeric_rows(
591 text: &str,
592 required_columns: Option<usize>,
593 max_rows: usize,
594) -> Result<Vec<Vec<f64>>, PowderIoError> {
595 let mut rows = Vec::new();
596 let mut columns = required_columns;
597 for (index, raw) in text.lines().enumerate() {
598 let line_number = index + 1;
599 let mut line = raw.trim();
600 if line.is_empty() || line.starts_with(['#', '!', ';']) {
601 continue;
602 }
603 if let Some(offset) = line.find(['#', '!']) {
604 line = line[..offset].trim();
605 }
606 if line.is_empty() {
607 continue;
608 }
609 let fields = line.replace(',', " ");
610 let fields = fields.split_whitespace().collect::<Vec<_>>();
611 if columns.is_none() && !matches!(fields.len(), 2 | 3) {
612 return Err(parse_error(line_number, "expected two or three columns"));
613 }
614 let expected = *columns.get_or_insert(fields.len());
615 if fields.len() != expected {
616 return Err(parse_error(
617 line_number,
618 format!("expected {expected} columns, got {}", fields.len()),
619 ));
620 }
621 let row = fields
622 .iter()
623 .map(|field| {
624 field
625 .parse::<f64>()
626 .map_err(|_| parse_error(line_number, "non-numeric powder value"))
627 })
628 .collect::<Result<Vec<_>, _>>()?;
629 rows.push(row);
630 if rows.len() > max_rows {
631 return Err(PowderIoError::RowLimitExceeded { maximum: max_rows });
632 }
633 }
634 if rows.is_empty() {
635 return Err(parse_error(0, "powder data contains no numeric rows"));
636 }
637 Ok(rows)
638}
639
640fn powder_data(
641 x_deg: Vec<f64>,
642 observed_y: Vec<f64>,
643 uncertainty: Option<Vec<f64>>,
644 mask: Option<Vec<bool>>,
645 format: PowderFormat,
646 source_path: Option<PathBuf>,
647 bank: Option<usize>,
648) -> Result<PowderData, PowderIoError> {
649 if x_deg.is_empty() {
650 return Err(parse_error(0, "powder data contains no numeric rows"));
651 }
652 let pattern = PatternRecord::new(x_deg, Some(observed_y), uncertainty, mask, None)
653 .map_err(PowderIoError::Domain)?;
654 Ok(PowderData {
655 pattern,
656 format,
657 source_path,
658 bank,
659 })
660}
661
662fn read_columns(
663 text: &str,
664 source: Option<PathBuf>,
665 max_rows: usize,
666) -> Result<PowderData, PowderIoError> {
667 let rows = numeric_rows(text, None, max_rows)?;
668 let has_uncertainty = rows[0].len() == 3;
669 powder_data(
670 rows.iter().map(|row| row[0]).collect(),
671 rows.iter().map(|row| row[1]).collect(),
672 has_uncertainty.then(|| rows.iter().map(|row| row[2]).collect()),
673 None,
674 PowderFormat::Columns,
675 source,
676 None,
677 )
678}
679
680fn gsas_banks(text: &str) -> Result<GsasBanks, PowderIoError> {
681 let mut banks = BTreeMap::new();
682 let mut active = None;
683 for (index, raw) in text.lines().enumerate() {
684 let line = raw.trim();
685 if line.to_ascii_uppercase().starts_with("BANK ") {
686 let fields = line
687 .split_whitespace()
688 .map(str::to_owned)
689 .collect::<Vec<_>>();
690 let bank = fields
691 .get(1)
692 .and_then(|value| value.parse::<usize>().ok())
693 .ok_or_else(|| parse_error(index + 1, "invalid GSAS bank header"))?;
694 if banks.insert(bank, (fields, Vec::new())).is_some() {
695 return Err(parse_error(
696 index + 1,
697 format!("duplicate GSAS bank {bank}"),
698 ));
699 }
700 active = Some(bank);
701 } else if let Some(bank) = active {
702 banks
703 .get_mut(&bank)
704 .expect("active bank exists")
705 .1
706 .push(raw.to_owned());
707 }
708 }
709 Ok(banks)
710}
711
712fn selected_bank(banks: &GsasBanks, bank: usize) -> Result<&GsasBank, PowderIoError> {
713 banks.get(&bank).ok_or_else(|| PowderIoError::MissingBank {
714 requested: bank,
715 available: banks.keys().copied().collect(),
716 })
717}
718
719fn read_gsas_fxye(
720 text: &str,
721 source: Option<PathBuf>,
722 bank: usize,
723 max_rows: usize,
724) -> Result<PowderData, PowderIoError> {
725 let banks = gsas_banks(text)?;
726 if banks.values().any(|(header, _)| {
727 header
728 .last()
729 .is_none_or(|value| !value.eq_ignore_ascii_case("FXYE"))
730 }) {
731 return Err(parse_error(
732 0,
733 "only unpacked GSAS FXYE banks are supported",
734 ));
735 }
736 let (header, lines) = selected_bank(&banks, bank)?;
737 debug_assert!(
738 header
739 .last()
740 .is_some_and(|value| value.eq_ignore_ascii_case("FXYE"))
741 );
742 let rows = numeric_rows(&lines.join("\n"), Some(3), max_rows)?;
743 let mut uncertainty = Vec::with_capacity(rows.len());
744 let mut mask = Vec::with_capacity(rows.len());
745 for row in &rows {
746 let supplied = row[2];
747 uncertainty.push(if supplied == 0.0 { 1.0 } else { supplied });
748 mask.push(supplied != 0.0);
749 }
750 let mask = mask.iter().any(|included| !included).then_some(mask);
751 powder_data(
752 rows.iter().map(|row| row[0] / 100.0).collect(),
753 rows.iter().map(|row| row[1]).collect(),
754 Some(uncertainty),
755 mask,
756 PowderFormat::GsasFxye,
757 source,
758 Some(bank),
759 )
760}
761
762fn read_gsas_std(
763 text: &str,
764 source: Option<PathBuf>,
765 bank: usize,
766 max_rows: usize,
767) -> Result<PowderData, PowderIoError> {
768 let banks = gsas_banks(text)?;
769 let (header, lines) = selected_bank(&banks, bank)?;
770 let encoding = if header.len() >= 10 {
771 header.last().map_or("", String::as_str)
772 } else {
773 "STD"
774 };
775 if header.len() < 7
776 || !header[4].eq_ignore_ascii_case("CONST")
777 || !encoding.eq_ignore_ascii_case("STD")
778 {
779 return Err(parse_error(
780 0,
781 "only packed constant-step GSAS STD banks are supported",
782 ));
783 }
784 let row_count = header[2]
785 .parse::<usize>()
786 .map_err(|_| parse_error(0, "invalid packed GSAS STD bank dimensions"))?;
787 let start_deg = parse_header_f64(&header[5])? / 100.0;
788 let step_deg = parse_header_f64(&header[6])? / 100.0;
789 if row_count == 0 || row_count > max_rows || !start_deg.is_finite() || !step_deg.is_finite() {
790 return Err(parse_error(
791 0,
792 "packed GSAS STD bank dimensions exceed limits or are non-finite",
793 ));
794 }
795 if step_deg <= 0.0 {
796 return Err(parse_error(0, "packed GSAS STD step must be positive"));
797 }
798 let mut intensities = Vec::with_capacity(row_count);
799 let mut uncertainty = Vec::with_capacity(row_count);
800 'lines: for (index, line) in lines.iter().enumerate() {
801 for bytes in line.as_bytes().chunks(8) {
802 let record = std::str::from_utf8(bytes)
803 .map_err(|_| parse_error(index + 1, "invalid UTF-8 fixed-width record"))?;
804 if record.trim().is_empty() {
805 continue;
806 }
807 let normalization_field = record.get(..2).unwrap_or(record).trim();
808 let normalization = if normalization_field.is_empty() {
809 1_u32
810 } else {
811 normalization_field
812 .parse::<u32>()
813 .map_err(|_| parse_error(index + 1, "invalid fixed-width record"))?
814 .max(1)
815 };
816 let parsed_intensity = record
817 .get(2..)
818 .unwrap_or("")
819 .trim()
820 .parse::<f64>()
821 .map_err(|_| parse_error(index + 1, "invalid fixed-width record"))?;
822 if !parsed_intensity.is_finite() {
823 return Err(parse_error(index + 1, "invalid fixed-width record"));
824 }
825 let intensity = parsed_intensity.max(0.0);
826 intensities.push(intensity);
827 uncertainty.push(if intensity > 0.0 {
828 (intensity / f64::from(normalization)).sqrt()
829 } else {
830 1.0
831 });
832 if intensities.len() == row_count {
833 break 'lines;
834 }
835 }
836 }
837 if intensities.len() != row_count {
838 return Err(parse_error(
839 0,
840 format!(
841 "packed GSAS STD bank contains {} records; expected {row_count}",
842 intensities.len()
843 ),
844 ));
845 }
846 powder_data(
847 coordinate_grid(start_deg, step_deg, row_count)?,
848 intensities,
849 Some(uncertainty),
850 None,
851 PowderFormat::GsasStd,
852 source,
853 Some(bank),
854 )
855}
856
857fn packed_std_values(
858 lines: &[String],
859 row_count: usize,
860) -> Result<(Vec<f64>, Vec<f64>), PowderIoError> {
861 let mut intensities = Vec::with_capacity(row_count);
862 let mut uncertainty = Vec::with_capacity(row_count);
863 'lines: for (index, line) in lines.iter().enumerate() {
864 for bytes in line.as_bytes().chunks(8) {
865 let record = std::str::from_utf8(bytes)
866 .map_err(|_| parse_error(index + 1, "invalid UTF-8 fixed-width record"))?;
867 if record.trim().is_empty() {
868 continue;
869 }
870 let normalization_field = record.get(..2).unwrap_or(record).trim();
871 let normalization = if normalization_field.is_empty() {
872 1_u32
873 } else {
874 normalization_field
875 .parse::<u32>()
876 .map_err(|_| parse_error(index + 1, "invalid fixed-width record"))?
877 .max(1)
878 };
879 let parsed_intensity = record
880 .get(2..)
881 .unwrap_or("")
882 .trim()
883 .parse::<f64>()
884 .map_err(|_| parse_error(index + 1, "invalid fixed-width record"))?;
885 if !parsed_intensity.is_finite() {
886 return Err(parse_error(index + 1, "invalid fixed-width record"));
887 }
888 let intensity = parsed_intensity.max(0.0);
889 intensities.push(intensity);
890 uncertainty.push(if intensity > 0.0 {
891 (intensity / f64::from(normalization)).sqrt()
892 } else {
893 1.0
894 });
895 if intensities.len() == row_count {
896 break 'lines;
897 }
898 }
899 }
900 if intensities.len() != row_count {
901 return Err(parse_error(
902 0,
903 format!(
904 "packed GSAS STD bank contains {} records; expected {row_count}",
905 intensities.len()
906 ),
907 ));
908 }
909 Ok((intensities, uncertainty))
910}
911
912fn coordinate_grid(
913 start_deg: f64,
914 step_deg: f64,
915 row_count: usize,
916) -> Result<Vec<f64>, PowderIoError> {
917 (0..row_count)
918 .map(|index| {
919 let exact_index = u32::try_from(index).map_err(|_| {
920 parse_error(
921 0,
922 "packed GSAS STD bank dimensions exceed limits or are non-finite",
923 )
924 })?;
925 Ok(start_deg + step_deg * f64::from(exact_index))
926 })
927 .collect()
928}
929
930fn parse_header_f64(value: &str) -> Result<f64, PowderIoError> {
931 value
932 .parse::<f64>()
933 .map_err(|_| parse_error(0, "invalid packed GSAS STD bank dimensions"))
934}
935
936fn parse_error(line: usize, message: impl Into<String>) -> PowderIoError {
937 PowderIoError::Parse {
938 line,
939 message: message.into(),
940 }
941}