use std::path::Path;
use ndarray::Array1;
use num_complex::Complex64;
use crate::PhaseBinData;
use crate::error::{IoError, Result};
const HEADER_RECORD_BYTES: usize = 12;
const FORTRAN_MARKER_BYTES: usize = 4;
const INTEGER_BYTES: usize = 4;
const F64_BYTES: usize = 8;
const COMPLEX64_BYTES: usize = 16;
#[derive(Debug, Clone, PartialEq)]
pub struct EmeshBinData {
pub point_count_declared: usize,
pub horizontal_count: usize,
pub danes_extension_count: usize,
pub energy_hartree: Array1<Complex64>,
}
impl EmeshBinData {
#[must_use]
pub fn point_count(&self) -> usize {
self.energy_hartree.len()
}
}
pub fn emesh_bin_from_phase_bin(phase: &PhaseBinData) -> Result<EmeshBinData> {
let data = EmeshBinData {
point_count_declared: phase.energy_count,
horizontal_count: phase.main_energy_count,
danes_extension_count: phase.auxiliary_energy_count,
energy_hartree: phase.energy_grid.clone(),
};
validate_emesh_bin(&data)?;
Ok(data)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Endian {
Little,
Big,
}
pub fn parse_emesh_bin(bytes: &[u8]) -> Result<EmeshBinData> {
let endian = detect_endian(bytes)?;
let mut position = 0;
let header = read_record(bytes, &mut position, endian, "header")?;
if header.len() != HEADER_RECORD_BYTES {
return invalid_emesh_bin(format!(
"header record has {} byte(s), expected {HEADER_RECORD_BYTES}",
header.len()
));
}
let ne = parse_nonnegative_i32(read_i32(header, 0, endian)?, "ne")?;
let ne1 = parse_nonnegative_i32(read_i32(header, INTEGER_BYTES, endian)?, "ne1")?;
let ne3 = parse_nonnegative_i32(read_i32(header, INTEGER_BYTES * 2, endian)?, "ne3")?;
let payload = read_record(bytes, &mut position, endian, "energy grid")?;
let expected_payload = checked_product(ne, COMPLEX64_BYTES)?;
if payload.len() != expected_payload {
return invalid_emesh_bin(format!(
"energy record has {} byte(s), expected {expected_payload}",
payload.len()
));
}
let mut values = Vec::with_capacity(ne);
for index in 0..ne {
let offset = checked_product(index, COMPLEX64_BYTES)?;
let real = read_f64(payload, offset, endian)?;
let imaginary = read_f64(payload, offset + F64_BYTES, endian)?;
if !(real.is_finite() && imaginary.is_finite()) {
return invalid_emesh_bin(format!("energy point {} is not finite", index + 1));
}
values.push(Complex64::new(real, imaginary));
}
if position != bytes.len() {
return invalid_emesh_bin(format!(
"emesh.bin has {} trailing byte(s)",
bytes.len() - position
));
}
let data = EmeshBinData {
point_count_declared: ne,
horizontal_count: ne1,
danes_extension_count: ne3,
energy_hartree: Array1::from_vec(values),
};
validate_emesh_bin(&data)?;
Ok(data)
}
pub fn emesh_bin_bytes(data: &EmeshBinData) -> Result<Vec<u8>> {
validate_emesh_bin(data)?;
let mut bytes = Vec::new();
let mut header = Vec::with_capacity(HEADER_RECORD_BYTES);
push_i32(&mut header, data.point_count_declared, "ne")?;
push_i32(&mut header, data.horizontal_count, "ne1")?;
push_i32(&mut header, data.danes_extension_count, "ne3")?;
write_record(&mut bytes, &header)?;
let mut payload = Vec::with_capacity(checked_product(data.point_count(), COMPLEX64_BYTES)?);
for value in &data.energy_hartree {
payload.extend_from_slice(&value.re.to_le_bytes());
payload.extend_from_slice(&value.im.to_le_bytes());
}
write_record(&mut bytes, &payload)?;
Ok(bytes)
}
pub fn read_emesh_bin(path: impl AsRef<Path>) -> Result<EmeshBinData> {
let path = path.as_ref();
let bytes = std::fs::read(path).map_err(|source| IoError::io(path, source))?;
parse_emesh_bin(&bytes)
}
pub fn write_emesh_bin(path: impl AsRef<Path>, data: &EmeshBinData) -> Result<()> {
let path = path.as_ref();
std::fs::write(path, emesh_bin_bytes(data)?).map_err(|source| IoError::io(path, source))
}
fn detect_endian(bytes: &[u8]) -> Result<Endian> {
let marker = read_marker_bytes(bytes, 0)?;
let little = u32::from_le_bytes(marker);
if little == HEADER_RECORD_BYTES as u32 {
return Ok(Endian::Little);
}
let big = u32::from_be_bytes(marker);
if big == HEADER_RECORD_BYTES as u32 {
return Ok(Endian::Big);
}
invalid_emesh_bin(format!(
"first record marker is {little} little-endian/{big} big-endian, expected {HEADER_RECORD_BYTES}"
))
}
fn read_record<'a>(
bytes: &'a [u8],
position: &mut usize,
endian: Endian,
label: &'static str,
) -> Result<&'a [u8]> {
let length = read_u32(bytes, *position, endian)? as usize;
*position = checked_add(*position, FORTRAN_MARKER_BYTES)?;
let end = checked_add(*position, length)?;
let payload = bytes.get(*position..end).ok_or_else(|| {
invalid_emesh_bin_value(format!(
"{label} record is truncated by payload length {length}"
))
})?;
*position = end;
let trailing = read_u32(bytes, *position, endian)? as usize;
*position = checked_add(*position, FORTRAN_MARKER_BYTES)?;
if trailing != length {
return invalid_emesh_bin(format!(
"{label} record trailing marker is {trailing}, expected {length}"
));
}
Ok(payload)
}
fn write_record(bytes: &mut Vec<u8>, payload: &[u8]) -> Result<()> {
let length = u32::try_from(payload.len())
.map_err(|_| invalid_emesh_bin_value("record length does not fit in u32"))?;
bytes.extend_from_slice(&length.to_le_bytes());
bytes.extend_from_slice(payload);
bytes.extend_from_slice(&length.to_le_bytes());
Ok(())
}
fn read_i32(bytes: &[u8], offset: usize, endian: Endian) -> Result<i32> {
let raw = read_i32_bytes(bytes, offset)?;
Ok(match endian {
Endian::Little => i32::from_le_bytes(raw),
Endian::Big => i32::from_be_bytes(raw),
})
}
fn read_u32(bytes: &[u8], offset: usize, endian: Endian) -> Result<u32> {
let raw = read_marker_bytes(bytes, offset)?;
Ok(match endian {
Endian::Little => u32::from_le_bytes(raw),
Endian::Big => u32::from_be_bytes(raw),
})
}
fn read_f64(bytes: &[u8], offset: usize, endian: Endian) -> Result<f64> {
let raw = read_f64_bytes(bytes, offset)?;
Ok(match endian {
Endian::Little => f64::from_le_bytes(raw),
Endian::Big => f64::from_be_bytes(raw),
})
}
fn read_marker_bytes(bytes: &[u8], offset: usize) -> Result<[u8; FORTRAN_MARKER_BYTES]> {
let slice = bytes
.get(offset..offset + FORTRAN_MARKER_BYTES)
.ok_or_else(|| invalid_emesh_bin_value("missing Fortran record marker"))?;
let mut raw = [0_u8; FORTRAN_MARKER_BYTES];
raw.copy_from_slice(slice);
Ok(raw)
}
fn read_i32_bytes(bytes: &[u8], offset: usize) -> Result<[u8; INTEGER_BYTES]> {
let slice = bytes
.get(offset..offset + INTEGER_BYTES)
.ok_or_else(|| invalid_emesh_bin_value("missing i32 payload"))?;
let mut raw = [0_u8; INTEGER_BYTES];
raw.copy_from_slice(slice);
Ok(raw)
}
fn read_f64_bytes(bytes: &[u8], offset: usize) -> Result<[u8; F64_BYTES]> {
let slice = bytes
.get(offset..offset + F64_BYTES)
.ok_or_else(|| invalid_emesh_bin_value("missing f64 payload"))?;
let mut raw = [0_u8; F64_BYTES];
raw.copy_from_slice(slice);
Ok(raw)
}
fn parse_nonnegative_i32(value: i32, field: &'static str) -> Result<usize> {
if value < 0 {
return invalid_emesh_bin(format!("{field} must be non-negative"));
}
usize::try_from(value)
.map_err(|_| invalid_emesh_bin_value(format!("{field} does not fit in usize")))
}
fn push_i32(bytes: &mut Vec<u8>, value: usize, field: &'static str) -> Result<()> {
let value = i32::try_from(value)
.map_err(|_| invalid_emesh_bin_value(format!("{field} does not fit in i32")))?;
bytes.extend_from_slice(&value.to_le_bytes());
Ok(())
}
fn validate_emesh_bin(data: &EmeshBinData) -> Result<()> {
if data.point_count_declared == 0 {
return invalid_emesh_bin("ne must be positive");
}
if data.point_count_declared != data.point_count() {
return invalid_emesh_bin(format!(
"ne is {}, but {} energy point(s) were supplied",
data.point_count_declared,
data.point_count()
));
}
if data.horizontal_count > data.point_count_declared {
return invalid_emesh_bin("ne1 must not exceed ne");
}
if data.danes_extension_count > data.point_count_declared {
return invalid_emesh_bin("ne3 must not exceed ne");
}
for (index, value) in data.energy_hartree.iter().enumerate() {
if !(value.re.is_finite() && value.im.is_finite()) {
return invalid_emesh_bin(format!("energy point {} is not finite", index + 1));
}
}
Ok(())
}
fn checked_add(left: usize, right: usize) -> Result<usize> {
left.checked_add(right)
.ok_or_else(|| invalid_emesh_bin_value("byte offset overflows usize"))
}
fn checked_product(left: usize, right: usize) -> Result<usize> {
left.checked_mul(right)
.ok_or_else(|| invalid_emesh_bin_value("record length overflows usize"))
}
fn invalid_emesh_bin<T>(message: impl Into<String>) -> Result<T> {
Err(invalid_emesh_bin_value(message))
}
fn invalid_emesh_bin_value(message: impl Into<String>) -> IoError {
IoError::InvalidEmeshBin {
message: message.into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrips_emesh_bin_bytes() -> Result<()> {
let data = EmeshBinData {
point_count_declared: 2,
horizontal_count: 1,
danes_extension_count: 0,
energy_hartree: Array1::from_vec(vec![
Complex64::new(-1.25, 0.05),
Complex64::new(2.5, -0.125),
]),
};
let bytes = emesh_bin_bytes(&data)?;
let parsed = parse_emesh_bin(&bytes)?;
assert_eq!(parsed, data);
Ok(())
}
#[test]
fn builds_emesh_bin_from_phase_cache() -> Result<()> {
let phase = PhaseBinData {
spin_count: 1,
energy_count: 2,
main_energy_count: 1,
auxiliary_energy_count: 1,
ihole: 1,
fermi_index: 1,
pad_width: 8,
final_state_count: 1,
transition_count: 1,
q_count: 1,
scalars: crate::PhaseBinScalars {
average_norman_radius: 1.0,
fermi_level: 0.0,
edge_energy: 0.25,
},
energy_grid: Array1::from_vec(vec![
Complex64::new(-0.5, 0.02),
Complex64::new(0.5, 0.03),
]),
reference_energy: ndarray::Array2::zeros((2, 1)),
potentials: vec![crate::PhaseBinPotential {
lmax: 0,
atomic_number: 29,
label: "Cu".to_string(),
phase_shifts: ndarray::Array3::zeros((2, 1, 1)),
}],
transition_moments: ndarray::Array4::zeros((2, 1, 1, 1)),
raw_pads: None,
};
let emesh = emesh_bin_from_phase_bin(&phase)?;
assert_eq!(emesh.point_count_declared, 2);
assert_eq!(emesh.horizontal_count, 1);
assert_eq!(emesh.danes_extension_count, 1);
assert_eq!(emesh.energy_hartree, phase.energy_grid);
Ok(())
}
#[test]
fn rejects_invalid_emesh_bin_bytes() {
assert!(parse_emesh_bin(&[]).is_err());
assert!(parse_emesh_bin(&[12, 0, 0, 0]).is_err());
let bad = EmeshBinData {
point_count_declared: 2,
horizontal_count: 1,
danes_extension_count: 0,
energy_hartree: Array1::from_vec(vec![Complex64::new(0.0, 0.0)]),
};
assert!(emesh_bin_bytes(&bad).is_err());
}
}