molframe-core 0.6.1

Core types, storage and analysis contracts for molframe
Documentation
//! Which combinations of decisions can be run at all.
//!
//! Policy fields are independent words, but not every combination of them is a
//! question. `assembly` chooses the arrangement of the contents; `symmetry` says
//! whether symmetry copies take part. Naming a biological assembly while asking
//! for crystallographic copies, or asking for crystallographic copies without a
//! radius to bound them, does not describe one system. Such a pair is refused,
//! naming both fields, before anything runs.

use super::fields::{AssemblyChoice, PolicyField, SymmetryPolicy};
use super::profile::AnalysisPolicy;
use crate::diagnostic::{Code, Diagnostic};

impl AnalysisPolicy {
    /// Checks that the decisions describe one runnable system.
    ///
    /// The rules, each a statement about what the copies are:
    ///
    /// - the asymmetric unit has no copies, so it goes with `SymmetryPolicy::None`;
    /// - a biological assembly's copies are the assembly's, so it goes with
    ///   `None` (the assembly alone) or `BiologicalAssembly`;
    /// - crystal contacts are crystallographic copies within a radius, so they go
    ///   with `None` (the radius alone names them) or `Crystallographic`, and the
    ///   radius must be positive and finite.
    ///
    /// # Errors
    ///
    /// Returns [`Code::E6004`] naming the two contradictory fields, or
    /// [`Code::E6016`] for a crystal radius that is not a positive finite number.
    pub fn check_consistency(&self) -> Result<(), Diagnostic> {
        let conflict = |reason: &str| {
            Err(Diagnostic::new(Code::E6004)
                .with_context(
                    "fields",
                    format!(
                        "{}, {}",
                        PolicyField::Assembly.name(),
                        PolicyField::Symmetry.name()
                    ),
                )
                .with_context("assembly", assembly_name(&self.assembly))
                .with_context("symmetry", symmetry_name(self.symmetry))
                .with_context("reason", reason))
        };
        match (&self.assembly, self.symmetry) {
            (AssemblyChoice::AsymmetricUnit, SymmetryPolicy::None)
            | (
                AssemblyChoice::Biological(_),
                SymmetryPolicy::None | SymmetryPolicy::BiologicalAssembly,
            ) => Ok(()),
            (
                AssemblyChoice::Crystal { radius },
                SymmetryPolicy::None | SymmetryPolicy::Crystallographic,
            ) => {
                if radius.is_finite() && *radius > 0.0 {
                    Ok(())
                } else {
                    Err(Diagnostic::new(Code::E6016)
                        .with_context("radius", radius.to_string())
                        .with_context("reason", "a crystal radius must be positive and finite"))
                }
            }
            (AssemblyChoice::AsymmetricUnit, SymmetryPolicy::Crystallographic) => conflict(
                "crystallographic copies need AssemblyChoice::Crystal, whose radius bounds them",
            ),
            (AssemblyChoice::AsymmetricUnit, SymmetryPolicy::BiologicalAssembly) => conflict(
                "biological-assembly copies need AssemblyChoice::Biological, which names the assembly",
            ),
            (AssemblyChoice::Biological(_), SymmetryPolicy::Crystallographic) => {
                conflict("a biological assembly is not generated by crystal symmetry")
            }
            (AssemblyChoice::Crystal { .. }, SymmetryPolicy::BiologicalAssembly) => {
                conflict("crystal contacts are not a biological assembly")
            }
        }
    }
}

fn assembly_name(choice: &AssemblyChoice) -> String {
    match choice {
        AssemblyChoice::AsymmetricUnit => "asymmetric-unit".to_owned(),
        AssemblyChoice::Biological(id) => format!("biological:{id}"),
        AssemblyChoice::Crystal { radius } => format!("crystal:{radius}"),
    }
}

const fn symmetry_name(policy: SymmetryPolicy) -> &'static str {
    match policy {
        SymmetryPolicy::None => "none",
        SymmetryPolicy::Crystallographic => "crystallographic",
        SymmetryPolicy::BiologicalAssembly => "biological-assembly",
    }
}

#[cfg(test)]
#[path = "consistency_tests.rs"]
mod tests;