1use std::error::Error;
8use std::fmt::{Display, Formatter};
9use std::fs;
10use std::path::{Path, PathBuf};
11
12use phasesmith_core::{
13 TOF_INCIDENT_SPECTRUM_COEFFICIENT_COUNT, TofBankGeometry, TofError, TofIncidentSpectrum,
14 TofInstrument,
15};
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct GsasTofInstrumentReadLimits {
20 pub max_bytes: usize,
22}
23
24impl Default for GsasTofInstrumentReadLimits {
25 fn default() -> Self {
26 Self {
27 max_bytes: 4 * 1024 * 1024,
28 }
29 }
30}
31
32impl GsasTofInstrumentReadLimits {
33 pub fn validate(self) -> Result<(), GsasTofInstrumentIoError> {
39 if self.max_bytes == 0 {
40 return Err(GsasTofInstrumentIoError::InvalidLimits);
41 }
42 Ok(())
43 }
44}
45
46#[derive(Clone, Debug, PartialEq)]
48pub struct GsasTofInstrumentData {
49 pub instrument: TofInstrument,
51 pub bank: usize,
53 pub profile_function: usize,
55 pub bank_geometry: Option<TofBankGeometry>,
57 pub incident_spectrum: Option<TofIncidentSpectrum>,
59 pub source_path: Option<PathBuf>,
61}
62
63#[derive(Debug)]
65pub enum GsasTofInstrumentIoError {
66 InvalidLimits,
68 InvalidBank,
70 ByteLimitExceeded {
72 actual: u64,
74 maximum: usize,
76 },
77 Io(std::io::Error),
79 MissingRecord {
81 bank: usize,
83 record: &'static str,
85 },
86 InvalidRecord {
88 bank: usize,
90 record: &'static str,
92 },
93 UnsupportedProfileFunction {
95 bank: usize,
97 found: usize,
99 },
100 UnsupportedIncidentSpectrumFunction {
102 bank: usize,
104 found: usize,
106 },
107 Profile(TofError),
109}
110
111impl Display for GsasTofInstrumentIoError {
112 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
113 match self {
114 Self::InvalidLimits => {
115 formatter.write_str("GSAS TOF instrument max_bytes must be positive")
116 }
117 Self::InvalidBank => formatter.write_str("GSAS TOF bank must lie in 1..=99"),
118 Self::ByteLimitExceeded { actual, maximum } => write!(
119 formatter,
120 "GSAS TOF instrument input exceeds max_bytes: {actual} > {maximum}"
121 ),
122 Self::Io(error) => Display::fmt(error, formatter),
123 Self::MissingRecord { bank, record } => {
124 write!(formatter, "GSAS TOF bank {bank} has no {record} record")
125 }
126 Self::InvalidRecord { bank, record } => {
127 write!(
128 formatter,
129 "GSAS TOF bank {bank} has an invalid {record} record"
130 )
131 }
132 Self::UnsupportedProfileFunction { bank, found } => write!(
133 formatter,
134 "GSAS TOF bank {bank} uses unsupported profile function {found}; only functions 1 and 3 are supported"
135 ),
136 Self::UnsupportedIncidentSpectrumFunction { bank, found } => write!(
137 formatter,
138 "GSAS TOF bank {bank} uses unsupported incident-spectrum function {found}; only functions 0 and 4 are supported"
139 ),
140 Self::Profile(error) => Display::fmt(error, formatter),
141 }
142 }
143}
144
145impl Error for GsasTofInstrumentIoError {
146 fn source(&self) -> Option<&(dyn Error + 'static)> {
147 match self {
148 Self::Io(error) => Some(error),
149 Self::Profile(error) => Some(error),
150 _ => None,
151 }
152 }
153}
154
155pub fn read_gsas_tof_instrument_file(
162 path: impl AsRef<Path>,
163 bank: usize,
164 limits: GsasTofInstrumentReadLimits,
165) -> Result<GsasTofInstrumentData, GsasTofInstrumentIoError> {
166 validate_request(bank, limits)?;
167 let path = path.as_ref();
168 let size = fs::metadata(path)
169 .map_err(GsasTofInstrumentIoError::Io)?
170 .len();
171 if size > u64::try_from(limits.max_bytes).unwrap_or(u64::MAX) {
172 return Err(GsasTofInstrumentIoError::ByteLimitExceeded {
173 actual: size,
174 maximum: limits.max_bytes,
175 });
176 }
177 let text = fs::read_to_string(path).map_err(GsasTofInstrumentIoError::Io)?;
178 parse_inner(&text, bank, limits, Some(path.to_owned()))
179}
180
181pub fn parse_gsas_tof_instrument_text(
188 text: &str,
189 bank: usize,
190 limits: GsasTofInstrumentReadLimits,
191) -> Result<GsasTofInstrumentData, GsasTofInstrumentIoError> {
192 parse_inner(text, bank, limits, None)
193}
194
195fn parse_inner(
196 text: &str,
197 bank: usize,
198 limits: GsasTofInstrumentReadLimits,
199 source_path: Option<PathBuf>,
200) -> Result<GsasTofInstrumentData, GsasTofInstrumentIoError> {
201 validate_request(bank, limits)?;
202 if text.len() > limits.max_bytes {
203 return Err(GsasTofInstrumentIoError::ByteLimitExceeded {
204 actual: u64::try_from(text.len()).unwrap_or(u64::MAX),
205 maximum: limits.max_bytes,
206 });
207 }
208 let text = text.strip_prefix('\u{feff}').unwrap_or(text);
209 let icons = record_values(text, bank, "ICONS", &format!("INS {bank:>2} ICONS"))?;
210 if icons.len() < 4 {
211 return Err(invalid(bank, "ICONS"));
212 }
213 let bank_geometry = parse_optional_bank_geometry(text, bank)?;
214 let incident_spectrum = parse_optional_incident_spectrum(text, bank)?;
215 let compact_header = format!("INS {bank:>2}PRCF1 ");
216 let spaced_header = format!("INS {bank:>2}PRCF ");
217 let compact = text.lines().any(|line| line.starts_with(&compact_header));
218 let function = profile_function(
219 text,
220 bank,
221 if compact {
222 &compact_header
223 } else {
224 &spaced_header
225 },
226 )?;
227 if !matches!(function, 1 | 3) {
228 return Err(GsasTofInstrumentIoError::UnsupportedProfileFunction {
229 bank,
230 found: function,
231 });
232 }
233 let (exponential_prefix, gaussian_prefix) = if compact {
234 (
235 format!("INS {bank:>2}PRCF11"),
236 format!("INS {bank:>2}PRCF12"),
237 )
238 } else {
239 (
240 format!("INS {bank:>2}PRCF 1"),
241 format!("INS {bank:>2}PRCF 2"),
242 )
243 };
244 let exponential = record_values(text, bank, "PRCF11", &exponential_prefix)?;
245 let gaussian = record_values(text, bank, "PRCF12", &gaussian_prefix)?;
246 if exponential.len() < if function == 1 { 4 } else { 3 } {
247 return Err(invalid(bank, "PRCF11"));
248 }
249 if gaussian.len() < if function == 1 { 3 } else { 2 } {
250 return Err(invalid(bank, "PRCF12"));
251 }
252
253 let (alpha, beta0, beta1, sigma1, sigma2) = if function == 1 {
258 (
259 exponential[1],
260 exponential[2],
261 exponential[3],
262 gaussian[1],
263 gaussian[2],
264 )
265 } else {
266 (
267 exponential[0],
268 exponential[1],
269 exponential[2],
270 gaussian[0],
271 gaussian[1],
272 )
273 };
274 let instrument = TofInstrument {
275 zero_us: icons[2],
276 difc_us_per_angstrom: icons[0],
277 difa_us_per_angstrom2: icons[1],
278 difb_us_angstrom: 0.0,
279 alpha_coefficient: alpha,
280 beta0_per_us: beta0,
281 beta1_angstrom4_per_us: beta1,
282 betaq_angstrom2_per_us: 0.0,
283 sigma0_us2: 0.0,
284 sigma1_us2_per_angstrom2: sigma1,
285 sigma2_us2_per_angstrom4: sigma2,
286 sigmaq_us2_per_angstrom: 0.0,
287 x_us_per_angstrom: 0.0,
288 y_us_per_angstrom2: 0.0,
289 z_us: 0.0,
290 };
291 instrument
292 .validate()
293 .map_err(GsasTofInstrumentIoError::Profile)?;
294 Ok(GsasTofInstrumentData {
295 instrument,
296 bank,
297 profile_function: function,
298 bank_geometry,
299 incident_spectrum,
300 source_path,
301 })
302}
303
304fn validate_request(
305 bank: usize,
306 limits: GsasTofInstrumentReadLimits,
307) -> Result<(), GsasTofInstrumentIoError> {
308 limits.validate()?;
309 if !(1..=99).contains(&bank) {
310 return Err(GsasTofInstrumentIoError::InvalidBank);
311 }
312 Ok(())
313}
314
315fn record_values(
316 text: &str,
317 bank: usize,
318 record: &'static str,
319 prefix: &str,
320) -> Result<Vec<f64>, GsasTofInstrumentIoError> {
321 let line = text
322 .lines()
323 .find(|line| line.starts_with(prefix))
324 .ok_or(GsasTofInstrumentIoError::MissingRecord { bank, record })?;
325 let values = line[prefix.len()..]
326 .split_whitespace()
327 .map(str::parse::<f64>)
328 .collect::<Result<Vec<_>, _>>()
329 .map_err(|_| invalid(bank, record))?;
330 if values.is_empty() || values.iter().any(|value| !value.is_finite()) {
331 return Err(invalid(bank, record));
332 }
333 Ok(values)
334}
335
336fn optional_record_values(
337 text: &str,
338 bank: usize,
339 record: &'static str,
340 prefix: &str,
341) -> Result<Option<Vec<f64>>, GsasTofInstrumentIoError> {
342 let Some(line) = text.lines().find(|line| line.starts_with(prefix)) else {
343 return Ok(None);
344 };
345 let values = line[prefix.len()..]
346 .split_whitespace()
347 .map(str::parse::<f64>)
348 .collect::<Result<Vec<_>, _>>()
349 .map_err(|_| invalid(bank, record))?;
350 if values.is_empty() || values.iter().any(|value| !value.is_finite()) {
351 return Err(invalid(bank, record));
352 }
353 Ok(Some(values))
354}
355
356fn parse_optional_bank_geometry(
357 text: &str,
358 bank: usize,
359) -> Result<Option<TofBankGeometry>, GsasTofInstrumentIoError> {
360 optional_record_values(text, bank, "BNKPAR", &format!("INS {bank:>2}BNKPAR"))?
361 .map(|values| {
362 if values.len() < 2 {
363 return Err(invalid(bank, "BNKPAR"));
364 }
365 let geometry = TofBankGeometry {
366 two_theta_deg: values[1],
367 };
368 geometry.validate().map_err(|_| invalid(bank, "BNKPAR"))?;
369 Ok(geometry)
370 })
371 .transpose()
372}
373
374fn parse_optional_incident_spectrum(
375 text: &str,
376 bank: usize,
377) -> Result<Option<TofIncidentSpectrum>, GsasTofInstrumentIoError> {
378 let prefix = format!("INS {bank:>2}I ITYP");
379 let Some(line) = text.lines().find(|line| line.starts_with(&prefix)) else {
380 return Ok(None);
381 };
382 let mut tokens = line[prefix.len()..].split_whitespace();
383 let function = tokens
384 .next()
385 .ok_or_else(|| invalid(bank, "I ITYP"))?
386 .parse::<usize>()
387 .map_err(|_| invalid(bank, "I ITYP"))?;
388 let min_tof_ms = tokens
389 .next()
390 .ok_or_else(|| invalid(bank, "I ITYP"))?
391 .parse::<f64>()
392 .map_err(|_| invalid(bank, "I ITYP"))?;
393 let max_tof_ms = tokens
394 .next()
395 .ok_or_else(|| invalid(bank, "I ITYP"))?
396 .parse::<f64>()
397 .map_err(|_| invalid(bank, "I ITYP"))?;
398 if function == 0 {
399 return Ok(None);
400 }
401 if function != 4 {
402 return Err(
403 GsasTofInstrumentIoError::UnsupportedIncidentSpectrumFunction {
404 bank,
405 found: function,
406 },
407 );
408 }
409 let mut coefficients = Vec::with_capacity(TOF_INCIDENT_SPECTRUM_COEFFICIENT_COUNT);
410 for record_index in 1..=3 {
411 coefficients.extend(record_values(
412 text,
413 bank,
414 "ICOFF",
415 &format!("INS {bank:>2}ICOFF{record_index}"),
416 )?);
417 }
418 if coefficients.len() != TOF_INCIDENT_SPECTRUM_COEFFICIENT_COUNT {
419 return Err(invalid(bank, "ICOFF"));
420 }
421 let coefficients: [f64; TOF_INCIDENT_SPECTRUM_COEFFICIENT_COUNT] = coefficients
422 .try_into()
423 .map_err(|_| invalid(bank, "ICOFF"))?;
424 TofIncidentSpectrum::new(min_tof_ms * 1_000.0, max_tof_ms * 1_000.0, coefficients)
425 .map(Some)
426 .map_err(|_| invalid(bank, "I ITYP"))
427}
428
429fn profile_function(
430 text: &str,
431 bank: usize,
432 prefix: &str,
433) -> Result<usize, GsasTofInstrumentIoError> {
434 let line = text.lines().find(|line| line.starts_with(prefix)).ok_or(
435 GsasTofInstrumentIoError::MissingRecord {
436 bank,
437 record: "PRCF1",
438 },
439 )?;
440 let mut tokens = line[prefix.len()..].split_whitespace();
441 let function = tokens
442 .next()
443 .ok_or_else(|| invalid(bank, "PRCF1"))?
444 .parse::<usize>()
445 .map_err(|_| invalid(bank, "PRCF1"))?;
446 Ok(function)
447}
448
449const fn invalid(bank: usize, record: &'static str) -> GsasTofInstrumentIoError {
450 GsasTofInstrumentIoError::InvalidRecord { bank, record }
451}