Skip to main content

molframe_core/contract/policy/
profile.rs

1//! The policy itself, and the named profile it defaults to.
2//!
3//! "We used the defaults" is not a statement anyone can reproduce. "We used
4//! molframe-default-1.0" is, which is why the default is a named, versioned
5//! profile recorded exactly as an explicit policy would be.
6
7use super::fields::{
8    AlignmentPolicy, AltlocPolicy, AssemblyChoice, ContactDefinition, EquivalencePolicy,
9    HydrogenPolicy, MissingPolicy, ModelChoice, Namespace, PeriodicPolicy, Precision, ProfileId,
10    RadiiSet, SymmetryPolicy, Tolerance,
11};
12use super::fingerprint::{Fingerprint, Fnv1a};
13use std::fmt;
14use std::hash::{Hash, Hasher};
15
16/// Every semantic decision an analysis depends on.
17///
18/// # Examples
19///
20/// ```
21/// use molframe_core::contract::{AnalysisPolicy, Namespace, ProfileId};
22///
23/// let policy = AnalysisPolicy::default();
24/// assert_eq!(policy.profile(), Some(ProfileId::DEFAULT));
25/// assert_eq!(policy.identifiers, Namespace::Auth);
26///
27/// let explicit = AnalysisPolicy::default().with_identifiers(Namespace::Explicit);
28/// assert_eq!(explicit.profile(), None, "no longer the named profile");
29/// assert_ne!(explicit.fingerprint(), policy.fingerprint());
30/// ```
31#[derive(Clone, PartialEq, Debug, Default)]
32pub struct AnalysisPolicy {
33    /// Which arrangement of the contents to analyse.
34    pub assembly: AssemblyChoice,
35    /// Which model.
36    pub model: ModelChoice,
37    /// How alternate conformations resolve.
38    pub altloc: AltlocPolicy,
39    /// Which identifier namespace an unqualified selector means.
40    pub identifiers: Namespace,
41    /// What to do about atoms that were not modelled.
42    pub missing_atoms: MissingPolicy,
43    /// How hydrogens enter.
44    pub hydrogens: HydrogenPolicy,
45    /// How atoms are matched between structures.
46    pub atom_equivalence: EquivalencePolicy,
47    /// Whether symmetry copies participate.
48    pub symmetry: SymmetryPolicy,
49    /// How structures are put into correspondence.
50    pub alignment: AlignmentPolicy,
51    /// The width accumulation runs at.
52    pub precision: Precision,
53    /// How periodic boundaries are handled.
54    pub periodic: PeriodicPolicy,
55    /// Which radii set.
56    pub vdw_radii: RadiiSet,
57    /// What counts as a contact.
58    pub contact_def: ContactDefinition,
59    /// How close two floating-point values must be to count as equal.
60    pub float_tolerance: Tolerance,
61}
62
63impl AnalysisPolicy {
64    /// The named profile this policy is, if it has not been altered.
65    ///
66    /// A result recording a profile name is reproducible by someone who has only
67    /// the name. A result recording "the defaults" is not.
68    #[must_use]
69    pub fn profile(&self) -> Option<ProfileId> {
70        (*self == Self::default()).then_some(ProfileId::DEFAULT)
71    }
72
73    /// A stable fingerprint of every field.
74    ///
75    /// Two policies that differ anywhere differ here, and the same policy always
76    /// fingerprints the same way, within and across runs. It is a fingerprint
77    /// for telling policies apart, not a cryptographic commitment.
78    #[must_use]
79    pub fn fingerprint(&self) -> Fingerprint {
80        let mut hasher = Fnv1a::new();
81        self.assembly.hash(&mut hasher);
82        self.model.hash(&mut hasher);
83        self.altloc.hash(&mut hasher);
84        self.identifiers.hash(&mut hasher);
85        self.missing_atoms.hash(&mut hasher);
86        self.hydrogens.hash(&mut hasher);
87        self.atom_equivalence.hash(&mut hasher);
88        self.symmetry.hash(&mut hasher);
89        self.alignment.hash(&mut hasher);
90        self.precision.hash(&mut hasher);
91        self.periodic.hash(&mut hasher);
92        self.vdw_radii.hash(&mut hasher);
93        match self.contact_def {
94            ContactDefinition::DistanceCutoff { tolerance } => {
95                (0u8, tolerance.to_bits()).hash(&mut hasher);
96            }
97            ContactDefinition::SurfaceBased { probe } => {
98                (1u8, probe.to_bits()).hash(&mut hasher);
99            }
100        }
101        (
102            self.float_tolerance.relative.to_bits(),
103            self.float_tolerance.absolute.to_bits(),
104        )
105            .hash(&mut hasher);
106        Fingerprint::from_hash(hasher.finish())
107    }
108
109    /// Returns this policy with a different identifier namespace.
110    #[must_use]
111    pub fn with_identifiers(mut self, namespace: Namespace) -> Self {
112        self.identifiers = namespace;
113        self
114    }
115
116    /// Returns this policy with a different alternate-conformation rule.
117    #[must_use]
118    pub fn with_altloc(mut self, altloc: AltlocPolicy) -> Self {
119        self.altloc = altloc;
120        self
121    }
122
123    /// Returns this policy with a different model choice.
124    #[must_use]
125    pub fn with_model(mut self, model: ModelChoice) -> Self {
126        self.model = model;
127        self
128    }
129
130    /// Returns this policy with a different assembly choice.
131    #[must_use]
132    pub fn with_assembly(mut self, assembly: AssemblyChoice) -> Self {
133        self.assembly = assembly;
134        self
135    }
136
137    /// Returns this policy with a different rule for unmodelled atoms.
138    #[must_use]
139    pub fn with_missing_atoms(mut self, missing: MissingPolicy) -> Self {
140        self.missing_atoms = missing;
141        self
142    }
143}
144
145impl fmt::Display for AnalysisPolicy {
146    /// Writes the policy as one field per line, which is what the command line
147    /// prints and what a written record contains.
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        match self.profile() {
150            Some(profile) => writeln!(f, "{profile}")?,
151            None => writeln!(f, "(modified from {})", ProfileId::DEFAULT)?,
152        }
153        writeln!(f, "  assembly         = {:?}", self.assembly)?;
154        writeln!(f, "  model            = {:?}", self.model)?;
155        writeln!(f, "  altloc           = {:?}", self.altloc)?;
156        writeln!(f, "  identifiers      = {:?}", self.identifiers)?;
157        writeln!(f, "  missing_atoms    = {:?}", self.missing_atoms)?;
158        writeln!(f, "  hydrogens        = {:?}", self.hydrogens)?;
159        writeln!(f, "  atom_equivalence = {:?}", self.atom_equivalence)?;
160        writeln!(f, "  symmetry         = {:?}", self.symmetry)?;
161        writeln!(f, "  alignment        = {:?}", self.alignment)?;
162        writeln!(f, "  precision        = {:?}", self.precision)?;
163        writeln!(f, "  periodic         = {:?}", self.periodic)?;
164        writeln!(f, "  vdw_radii        = {:?}", self.vdw_radii)?;
165        writeln!(f, "  contact_def      = {:?}", self.contact_def)?;
166        write!(f, "  float_tolerance  = {:?}", self.float_tolerance)
167    }
168}
169
170#[cfg(test)]
171#[path = "profile_tests.rs"]
172mod tests;