refeff-io 0.2.0

FEFF file-format readers and writers (feff.inp, .dat/.bin handoffs, PAD encoding) for the refeff FEFF10 port
Documentation
use crate::{IoError, Result};

use super::{DmdwCalculation, DmdwPdosOptions, DmdwSelfEnergyOptions};

pub(super) fn validate_dmdw_calculation(calculation: &DmdwCalculation) -> Result<()> {
    if calculation.path_count != calculation.paths.len() {
        return Err(IoError::Parse {
            path: "dmdw.inp".into(),
            line: 0,
            message: format!(
                "DMDW path_count is {} but has {} path row(s)",
                calculation.path_count,
                calculation.paths.len()
            ),
        });
    }
    if calculation.temperature_flag <= 0 {
        return Err(IoError::Parse {
            path: "dmdw.inp".into(),
            line: 0,
            message: format!(
                "DMDW temperature count must be positive, got {}",
                calculation.temperature_flag
            ),
        });
    }
    if !calculation.temperature.is_finite() {
        return Err(IoError::Parse {
            path: "dmdw.inp".into(),
            line: 0,
            message: "DMDW temperature must be finite".to_string(),
        });
    }
    if calculation.temperature_flag == 1 && calculation.temperature_max.is_some() {
        return Err(IoError::Parse {
            path: "dmdw.inp".into(),
            line: 0,
            message: "DMDW single-temperature run must not set an upper temperature".to_string(),
        });
    }
    if calculation.temperature_flag > 1 {
        let Some(temperature_max) = calculation.temperature_max else {
            return Err(IoError::Parse {
                path: "dmdw.inp".into(),
                line: 0,
                message: "DMDW multi-temperature run requires an upper temperature".to_string(),
            });
        };
        if !temperature_max.is_finite() {
            return Err(IoError::Parse {
                path: "dmdw.inp".into(),
                line: 0,
                message: "DMDW upper temperature must be finite".to_string(),
            });
        }
    }
    validate_dmdw_filename("DMDW dynamical-matrix filename", &calculation.dym_file)?;
    if calculation.calculation_type == 2 {
        validate_self_energy_options(calculation.self_energy_options.as_ref())?;
        if calculation.path_count != 0 || !calculation.paths.is_empty() {
            return Err(IoError::Parse {
                path: "dmdw.inp".into(),
                line: 0,
                message: "DMDW run type 2 must not include path rows".to_string(),
            });
        }
    } else if calculation.self_energy_options.is_some() {
        return Err(IoError::Parse {
            path: "dmdw.inp".into(),
            line: 0,
            message: "DMDW self-energy options are only valid for calculation type 2".to_string(),
        });
    }
    if calculation.calculation_type == 5 {
        validate_pdos_options(calculation.pdos_options.as_ref())?;
    } else if calculation.pdos_options.is_some() {
        return Err(IoError::Parse {
            path: "dmdw.inp".into(),
            line: 0,
            message: "DMDW PDOS options are only valid for calculation type 5".to_string(),
        });
    }
    for (index, path) in calculation.paths.iter().enumerate() {
        if !path.max_distance.is_finite() {
            return Err(IoError::Parse {
                path: "dmdw.inp".into(),
                line: 0,
                message: format!("DMDW path {index} maximum distance must be finite"),
            });
        }
    }
    Ok(())
}

fn validate_self_energy_options(options: Option<&DmdwSelfEnergyOptions>) -> Result<()> {
    let Some(options) = options else {
        return Err(IoError::Parse {
            path: "dmdw.inp".into(),
            line: 0,
            message: "DMDW run type 2 requires self-energy options".to_string(),
        });
    };
    if !options.electron_energy.is_finite() {
        return Err(IoError::Parse {
            path: "dmdw.inp".into(),
            line: 0,
            message: "DMDW self-energy electron energy must be finite".to_string(),
        });
    }
    validate_dmdw_filename("DMDW self-energy PDS filename", &options.pds_file)?;
    validate_dmdw_filename("DMDW self-energy a2f filename", &options.a2f_file)?;
    Ok(())
}

fn validate_dmdw_filename(field: &'static str, value: &str) -> Result<()> {
    if value
        .chars()
        .any(|character| matches!(character, '\n' | '\r'))
    {
        return Err(IoError::Parse {
            path: "dmdw.inp".into(),
            line: 0,
            message: format!("{field} must not contain a line terminator"),
        });
    }
    Ok(())
}

fn validate_pdos_options(options: Option<&DmdwPdosOptions>) -> Result<()> {
    let options = options.cloned().unwrap_or_default();
    if !matches!(options.format, 0 | 1 | 2 | 10) {
        return Err(IoError::Parse {
            path: "dmdw.inp".into(),
            line: 0,
            message: format!(
                "DMDW PDOS format {} is not supported by FEFF",
                options.format
            ),
        });
    }
    if !options.gaussian_broadening_thz.is_finite() || options.gaussian_broadening_thz <= 0.0 {
        return Err(IoError::Parse {
            path: "dmdw.inp".into(),
            line: 0,
            message: "DMDW PDOS Gaussian broadening must be positive and finite".to_string(),
        });
    }
    if !options.gaussian_resolution_thz.is_finite() || options.gaussian_resolution_thz <= 0.0 {
        return Err(IoError::Parse {
            path: "dmdw.inp".into(),
            line: 0,
            message: "DMDW PDOS Gaussian resolution must be positive and finite".to_string(),
        });
    }
    Ok(())
}