Skip to main content

molframe_core/contract/policy/
consistency.rs

1//! Which combinations of decisions can be run at all.
2//!
3//! Policy fields are independent words, but not every combination of them is a
4//! question. `assembly` chooses the arrangement of the contents; `symmetry` says
5//! whether symmetry copies take part. Naming a biological assembly while asking
6//! for crystallographic copies, or asking for crystallographic copies without a
7//! radius to bound them, does not describe one system. Such a pair is refused,
8//! naming both fields, before anything runs.
9
10use super::fields::{AssemblyChoice, PolicyField, SymmetryPolicy};
11use super::profile::AnalysisPolicy;
12use crate::diagnostic::{Code, Diagnostic};
13
14impl AnalysisPolicy {
15    /// Checks that the decisions describe one runnable system.
16    ///
17    /// The rules, each a statement about what the copies are:
18    ///
19    /// - the asymmetric unit has no copies, so it goes with `SymmetryPolicy::None`;
20    /// - a biological assembly's copies are the assembly's, so it goes with
21    ///   `None` (the assembly alone) or `BiologicalAssembly`;
22    /// - crystal contacts are crystallographic copies within a radius, so they go
23    ///   with `None` (the radius alone names them) or `Crystallographic`, and the
24    ///   radius must be positive and finite.
25    ///
26    /// # Errors
27    ///
28    /// Returns [`Code::E6004`] naming the two contradictory fields, or
29    /// [`Code::E6016`] for a crystal radius that is not a positive finite number.
30    pub fn check_consistency(&self) -> Result<(), Diagnostic> {
31        let conflict = |reason: &str| {
32            Err(Diagnostic::new(Code::E6004)
33                .with_context(
34                    "fields",
35                    format!(
36                        "{}, {}",
37                        PolicyField::Assembly.name(),
38                        PolicyField::Symmetry.name()
39                    ),
40                )
41                .with_context("assembly", assembly_name(&self.assembly))
42                .with_context("symmetry", symmetry_name(self.symmetry))
43                .with_context("reason", reason))
44        };
45        match (&self.assembly, self.symmetry) {
46            (AssemblyChoice::AsymmetricUnit, SymmetryPolicy::None)
47            | (
48                AssemblyChoice::Biological(_),
49                SymmetryPolicy::None | SymmetryPolicy::BiologicalAssembly,
50            ) => Ok(()),
51            (
52                AssemblyChoice::Crystal { radius },
53                SymmetryPolicy::None | SymmetryPolicy::Crystallographic,
54            ) => {
55                if radius.is_finite() && *radius > 0.0 {
56                    Ok(())
57                } else {
58                    Err(Diagnostic::new(Code::E6016)
59                        .with_context("radius", radius.to_string())
60                        .with_context("reason", "a crystal radius must be positive and finite"))
61                }
62            }
63            (AssemblyChoice::AsymmetricUnit, SymmetryPolicy::Crystallographic) => conflict(
64                "crystallographic copies need AssemblyChoice::Crystal, whose radius bounds them",
65            ),
66            (AssemblyChoice::AsymmetricUnit, SymmetryPolicy::BiologicalAssembly) => conflict(
67                "biological-assembly copies need AssemblyChoice::Biological, which names the assembly",
68            ),
69            (AssemblyChoice::Biological(_), SymmetryPolicy::Crystallographic) => {
70                conflict("a biological assembly is not generated by crystal symmetry")
71            }
72            (AssemblyChoice::Crystal { .. }, SymmetryPolicy::BiologicalAssembly) => {
73                conflict("crystal contacts are not a biological assembly")
74            }
75        }
76    }
77}
78
79fn assembly_name(choice: &AssemblyChoice) -> String {
80    match choice {
81        AssemblyChoice::AsymmetricUnit => "asymmetric-unit".to_owned(),
82        AssemblyChoice::Biological(id) => format!("biological:{id}"),
83        AssemblyChoice::Crystal { radius } => format!("crystal:{radius}"),
84    }
85}
86
87const fn symmetry_name(policy: SymmetryPolicy) -> &'static str {
88    match policy {
89        SymmetryPolicy::None => "none",
90        SymmetryPolicy::Crystallographic => "crystallographic",
91        SymmetryPolicy::BiologicalAssembly => "biological-assembly",
92    }
93}
94
95#[cfg(test)]
96#[path = "consistency_tests.rs"]
97mod tests;