use std::error::Error;
use std::fmt::{Display, Formatter};
use std::fs;
use std::path::{Path, PathBuf};
use phasesmith_core::{
TOF_INCIDENT_SPECTRUM_COEFFICIENT_COUNT, TofBankGeometry, TofError, TofIncidentSpectrum,
TofInstrument,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GsasTofInstrumentReadLimits {
pub max_bytes: usize,
}
impl Default for GsasTofInstrumentReadLimits {
fn default() -> Self {
Self {
max_bytes: 4 * 1024 * 1024,
}
}
}
impl GsasTofInstrumentReadLimits {
pub fn validate(self) -> Result<(), GsasTofInstrumentIoError> {
if self.max_bytes == 0 {
return Err(GsasTofInstrumentIoError::InvalidLimits);
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct GsasTofInstrumentData {
pub instrument: TofInstrument,
pub bank: usize,
pub profile_function: usize,
pub bank_geometry: Option<TofBankGeometry>,
pub incident_spectrum: Option<TofIncidentSpectrum>,
pub source_path: Option<PathBuf>,
}
#[derive(Debug)]
pub enum GsasTofInstrumentIoError {
InvalidLimits,
InvalidBank,
ByteLimitExceeded {
actual: u64,
maximum: usize,
},
Io(std::io::Error),
MissingRecord {
bank: usize,
record: &'static str,
},
InvalidRecord {
bank: usize,
record: &'static str,
},
UnsupportedProfileFunction {
bank: usize,
found: usize,
},
UnsupportedIncidentSpectrumFunction {
bank: usize,
found: usize,
},
Profile(TofError),
}
impl Display for GsasTofInstrumentIoError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidLimits => {
formatter.write_str("GSAS TOF instrument max_bytes must be positive")
}
Self::InvalidBank => formatter.write_str("GSAS TOF bank must lie in 1..=99"),
Self::ByteLimitExceeded { actual, maximum } => write!(
formatter,
"GSAS TOF instrument input exceeds max_bytes: {actual} > {maximum}"
),
Self::Io(error) => Display::fmt(error, formatter),
Self::MissingRecord { bank, record } => {
write!(formatter, "GSAS TOF bank {bank} has no {record} record")
}
Self::InvalidRecord { bank, record } => {
write!(
formatter,
"GSAS TOF bank {bank} has an invalid {record} record"
)
}
Self::UnsupportedProfileFunction { bank, found } => write!(
formatter,
"GSAS TOF bank {bank} uses unsupported profile function {found}; only functions 1 and 3 are supported"
),
Self::UnsupportedIncidentSpectrumFunction { bank, found } => write!(
formatter,
"GSAS TOF bank {bank} uses unsupported incident-spectrum function {found}; only functions 0 and 4 are supported"
),
Self::Profile(error) => Display::fmt(error, formatter),
}
}
}
impl Error for GsasTofInstrumentIoError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Io(error) => Some(error),
Self::Profile(error) => Some(error),
_ => None,
}
}
}
pub fn read_gsas_tof_instrument_file(
path: impl AsRef<Path>,
bank: usize,
limits: GsasTofInstrumentReadLimits,
) -> Result<GsasTofInstrumentData, GsasTofInstrumentIoError> {
validate_request(bank, limits)?;
let path = path.as_ref();
let size = fs::metadata(path)
.map_err(GsasTofInstrumentIoError::Io)?
.len();
if size > u64::try_from(limits.max_bytes).unwrap_or(u64::MAX) {
return Err(GsasTofInstrumentIoError::ByteLimitExceeded {
actual: size,
maximum: limits.max_bytes,
});
}
let text = fs::read_to_string(path).map_err(GsasTofInstrumentIoError::Io)?;
parse_inner(&text, bank, limits, Some(path.to_owned()))
}
pub fn parse_gsas_tof_instrument_text(
text: &str,
bank: usize,
limits: GsasTofInstrumentReadLimits,
) -> Result<GsasTofInstrumentData, GsasTofInstrumentIoError> {
parse_inner(text, bank, limits, None)
}
fn parse_inner(
text: &str,
bank: usize,
limits: GsasTofInstrumentReadLimits,
source_path: Option<PathBuf>,
) -> Result<GsasTofInstrumentData, GsasTofInstrumentIoError> {
validate_request(bank, limits)?;
if text.len() > limits.max_bytes {
return Err(GsasTofInstrumentIoError::ByteLimitExceeded {
actual: u64::try_from(text.len()).unwrap_or(u64::MAX),
maximum: limits.max_bytes,
});
}
let text = text.strip_prefix('\u{feff}').unwrap_or(text);
let icons = record_values(text, bank, "ICONS", &format!("INS {bank:>2} ICONS"))?;
if icons.len() < 4 {
return Err(invalid(bank, "ICONS"));
}
let bank_geometry = parse_optional_bank_geometry(text, bank)?;
let incident_spectrum = parse_optional_incident_spectrum(text, bank)?;
let compact_header = format!("INS {bank:>2}PRCF1 ");
let spaced_header = format!("INS {bank:>2}PRCF ");
let compact = text.lines().any(|line| line.starts_with(&compact_header));
let function = profile_function(
text,
bank,
if compact {
&compact_header
} else {
&spaced_header
},
)?;
if !matches!(function, 1 | 3) {
return Err(GsasTofInstrumentIoError::UnsupportedProfileFunction {
bank,
found: function,
});
}
let (exponential_prefix, gaussian_prefix) = if compact {
(
format!("INS {bank:>2}PRCF11"),
format!("INS {bank:>2}PRCF12"),
)
} else {
(
format!("INS {bank:>2}PRCF 1"),
format!("INS {bank:>2}PRCF 2"),
)
};
let exponential = record_values(text, bank, "PRCF11", &exponential_prefix)?;
let gaussian = record_values(text, bank, "PRCF12", &gaussian_prefix)?;
if exponential.len() < if function == 1 { 4 } else { 3 } {
return Err(invalid(bank, "PRCF11"));
}
if gaussian.len() < if function == 1 { 3 } else { 2 } {
return Err(invalid(bank, "PRCF12"));
}
let (alpha, beta0, beta1, sigma1, sigma2) = if function == 1 {
(
exponential[1],
exponential[2],
exponential[3],
gaussian[1],
gaussian[2],
)
} else {
(
exponential[0],
exponential[1],
exponential[2],
gaussian[0],
gaussian[1],
)
};
let instrument = TofInstrument {
zero_us: icons[2],
difc_us_per_angstrom: icons[0],
difa_us_per_angstrom2: icons[1],
difb_us_angstrom: 0.0,
alpha_coefficient: alpha,
beta0_per_us: beta0,
beta1_angstrom4_per_us: beta1,
betaq_angstrom2_per_us: 0.0,
sigma0_us2: 0.0,
sigma1_us2_per_angstrom2: sigma1,
sigma2_us2_per_angstrom4: sigma2,
sigmaq_us2_per_angstrom: 0.0,
x_us_per_angstrom: 0.0,
y_us_per_angstrom2: 0.0,
z_us: 0.0,
};
instrument
.validate()
.map_err(GsasTofInstrumentIoError::Profile)?;
Ok(GsasTofInstrumentData {
instrument,
bank,
profile_function: function,
bank_geometry,
incident_spectrum,
source_path,
})
}
fn validate_request(
bank: usize,
limits: GsasTofInstrumentReadLimits,
) -> Result<(), GsasTofInstrumentIoError> {
limits.validate()?;
if !(1..=99).contains(&bank) {
return Err(GsasTofInstrumentIoError::InvalidBank);
}
Ok(())
}
fn record_values(
text: &str,
bank: usize,
record: &'static str,
prefix: &str,
) -> Result<Vec<f64>, GsasTofInstrumentIoError> {
let line = text
.lines()
.find(|line| line.starts_with(prefix))
.ok_or(GsasTofInstrumentIoError::MissingRecord { bank, record })?;
let values = line[prefix.len()..]
.split_whitespace()
.map(str::parse::<f64>)
.collect::<Result<Vec<_>, _>>()
.map_err(|_| invalid(bank, record))?;
if values.is_empty() || values.iter().any(|value| !value.is_finite()) {
return Err(invalid(bank, record));
}
Ok(values)
}
fn optional_record_values(
text: &str,
bank: usize,
record: &'static str,
prefix: &str,
) -> Result<Option<Vec<f64>>, GsasTofInstrumentIoError> {
let Some(line) = text.lines().find(|line| line.starts_with(prefix)) else {
return Ok(None);
};
let values = line[prefix.len()..]
.split_whitespace()
.map(str::parse::<f64>)
.collect::<Result<Vec<_>, _>>()
.map_err(|_| invalid(bank, record))?;
if values.is_empty() || values.iter().any(|value| !value.is_finite()) {
return Err(invalid(bank, record));
}
Ok(Some(values))
}
fn parse_optional_bank_geometry(
text: &str,
bank: usize,
) -> Result<Option<TofBankGeometry>, GsasTofInstrumentIoError> {
optional_record_values(text, bank, "BNKPAR", &format!("INS {bank:>2}BNKPAR"))?
.map(|values| {
if values.len() < 2 {
return Err(invalid(bank, "BNKPAR"));
}
let geometry = TofBankGeometry {
two_theta_deg: values[1],
};
geometry.validate().map_err(|_| invalid(bank, "BNKPAR"))?;
Ok(geometry)
})
.transpose()
}
fn parse_optional_incident_spectrum(
text: &str,
bank: usize,
) -> Result<Option<TofIncidentSpectrum>, GsasTofInstrumentIoError> {
let prefix = format!("INS {bank:>2}I ITYP");
let Some(line) = text.lines().find(|line| line.starts_with(&prefix)) else {
return Ok(None);
};
let mut tokens = line[prefix.len()..].split_whitespace();
let function = tokens
.next()
.ok_or_else(|| invalid(bank, "I ITYP"))?
.parse::<usize>()
.map_err(|_| invalid(bank, "I ITYP"))?;
let min_tof_ms = tokens
.next()
.ok_or_else(|| invalid(bank, "I ITYP"))?
.parse::<f64>()
.map_err(|_| invalid(bank, "I ITYP"))?;
let max_tof_ms = tokens
.next()
.ok_or_else(|| invalid(bank, "I ITYP"))?
.parse::<f64>()
.map_err(|_| invalid(bank, "I ITYP"))?;
if function == 0 {
return Ok(None);
}
if function != 4 {
return Err(
GsasTofInstrumentIoError::UnsupportedIncidentSpectrumFunction {
bank,
found: function,
},
);
}
let mut coefficients = Vec::with_capacity(TOF_INCIDENT_SPECTRUM_COEFFICIENT_COUNT);
for record_index in 1..=3 {
coefficients.extend(record_values(
text,
bank,
"ICOFF",
&format!("INS {bank:>2}ICOFF{record_index}"),
)?);
}
if coefficients.len() != TOF_INCIDENT_SPECTRUM_COEFFICIENT_COUNT {
return Err(invalid(bank, "ICOFF"));
}
let coefficients: [f64; TOF_INCIDENT_SPECTRUM_COEFFICIENT_COUNT] = coefficients
.try_into()
.map_err(|_| invalid(bank, "ICOFF"))?;
TofIncidentSpectrum::new(min_tof_ms * 1_000.0, max_tof_ms * 1_000.0, coefficients)
.map(Some)
.map_err(|_| invalid(bank, "I ITYP"))
}
fn profile_function(
text: &str,
bank: usize,
prefix: &str,
) -> Result<usize, GsasTofInstrumentIoError> {
let line = text.lines().find(|line| line.starts_with(prefix)).ok_or(
GsasTofInstrumentIoError::MissingRecord {
bank,
record: "PRCF1",
},
)?;
let mut tokens = line[prefix.len()..].split_whitespace();
let function = tokens
.next()
.ok_or_else(|| invalid(bank, "PRCF1"))?
.parse::<usize>()
.map_err(|_| invalid(bank, "PRCF1"))?;
Ok(function)
}
const fn invalid(bank: usize, record: &'static str) -> GsasTofInstrumentIoError {
GsasTofInstrumentIoError::InvalidRecord { bank, record }
}