use crate::{CoreError, EntityId};
use std::sync::Arc;
#[cfg(test)]
#[path = "topology_tests.rs"]
mod tests;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct TopologyBond {
atoms: [u32; 2],
aromatic: bool,
}
impl TopologyBond {
pub fn new(atom_a: u32, atom_b: u32, aromatic: bool) -> Result<Self, CoreError> {
if atom_a == atom_b {
return Err(invalid("dynamic topology cannot contain self-bonds"));
}
Ok(Self {
atoms: [atom_a.min(atom_b), atom_a.max(atom_b)],
aromatic,
})
}
#[must_use]
pub const fn atoms(self) -> [u32; 2] {
self.atoms
}
#[must_use]
pub const fn is_aromatic(self) -> bool {
self.aromatic
}
const fn endpoint_key(self) -> [u32; 2] {
self.atoms
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct BondTopologyFrame {
index: u64,
time_seconds: f32,
atom_count: u32,
bonds: Arc<[TopologyBond]>,
provenance: Arc<str>,
}
impl BondTopologyFrame {
pub fn new(
index: u64,
time_seconds: f32,
atom_count: u32,
bonds: Arc<[TopologyBond]>,
provenance: impl Into<Arc<str>>,
) -> Result<Self, CoreError> {
if !time_seconds.is_finite() || atom_count == 0 {
return Err(invalid("topology frame time and atom count must be valid"));
}
if bonds.len() > EntityId::MAX_INDEX as usize {
return Err(invalid("topology frame exceeds the pickable row limit"));
}
if bonds
.iter()
.any(|bond| bond.atoms[1] >= atom_count || bond.atoms[0] >= bond.atoms[1])
{
return Err(invalid("topology bond endpoints escape the atom table"));
}
if bonds
.windows(2)
.any(|pair| pair[0].endpoint_key() >= pair[1].endpoint_key())
{
return Err(invalid(
"topology bonds must be strictly endpoint-sorted without duplicates",
));
}
let provenance = provenance.into();
if provenance.trim().is_empty() {
return Err(invalid("topology frame provenance must not be empty"));
}
Ok(Self {
index,
time_seconds,
atom_count,
bonds,
provenance,
})
}
#[must_use]
pub const fn index(&self) -> u64 {
self.index
}
#[must_use]
pub const fn time_seconds(&self) -> f32 {
self.time_seconds
}
#[must_use]
pub const fn atom_count(&self) -> u32 {
self.atom_count
}
#[must_use]
pub fn bonds(&self) -> &[TopologyBond] {
&self.bonds
}
#[must_use]
pub fn provenance(&self) -> &str {
&self.provenance
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct ActiveTopologyBond {
bond: TopologyBond,
weight: f32,
}
impl ActiveTopologyBond {
#[must_use]
pub const fn bond(self) -> TopologyBond {
self.bond
}
#[must_use]
pub const fn weight(self) -> f32 {
self.weight
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
struct TransitionBond {
start: Option<TopologyBond>,
end: Option<TopologyBond>,
active: ActiveTopologyBond,
}
#[derive(Clone, PartialEq, Debug)]
pub struct BondTopologySegment {
start: BondTopologyFrame,
end: BondTopologyFrame,
sample_seconds: f32,
interpolation: f32,
merged: Vec<TransitionBond>,
}
impl BondTopologySegment {
pub fn new(
start: BondTopologyFrame,
end: BondTopologyFrame,
sample_seconds: f32,
) -> Result<Self, CoreError> {
if start.atom_count != end.atom_count {
return Err(invalid("topology frames must use one fixed atom table"));
}
if start.index >= end.index || start.time_seconds >= end.time_seconds {
return Err(invalid("topology frame indices and times must increase"));
}
let merged = merge_frames(start.bonds(), end.bonds())?;
let mut segment = Self {
start,
end,
sample_seconds: 0.0,
interpolation: 0.0,
merged,
};
segment.set_sample_time(sample_seconds)?;
Ok(segment)
}
pub fn set_sample_time(&mut self, sample_seconds: f32) -> Result<(), CoreError> {
if !sample_seconds.is_finite()
|| sample_seconds < self.start.time_seconds
|| sample_seconds > self.end.time_seconds
{
return Err(invalid(
"topology sample must lie within its resident interval",
));
}
let duration = self.end.time_seconds - self.start.time_seconds;
let alpha = (sample_seconds - self.start.time_seconds) / duration;
for transition in &mut self.merged {
let weight = match (transition.start, transition.end) {
(Some(_), Some(_)) => 1.0,
(Some(_), None) => 1.0 - alpha,
(None, Some(_)) => alpha,
(None, None) => 0.0,
};
let bond = match (transition.start, transition.end) {
(Some(_), Some(end)) if alpha >= 0.5 => end,
(Some(start), _) => start,
(None, Some(end)) => end,
(None, None) => transition.active.bond,
};
transition.active = ActiveTopologyBond { bond, weight };
}
self.sample_seconds = sample_seconds;
self.interpolation = alpha;
Ok(())
}
#[must_use]
pub const fn start(&self) -> &BondTopologyFrame {
&self.start
}
#[must_use]
pub const fn end(&self) -> &BondTopologyFrame {
&self.end
}
#[must_use]
pub const fn sample_seconds(&self) -> f32 {
self.sample_seconds
}
#[must_use]
pub const fn interpolation(&self) -> f32 {
self.interpolation
}
#[must_use]
pub fn bonds(&self) -> impl ExactSizeIterator<Item = &ActiveTopologyBond> {
self.merged.iter().map(|transition| &transition.active)
}
#[must_use]
pub fn bond(&self, row: u32) -> Option<&ActiveTopologyBond> {
self.merged.get(row as usize).map(|value| &value.active)
}
}
fn merge_frames(
start: &[TopologyBond],
end: &[TopologyBond],
) -> Result<Vec<TransitionBond>, CoreError> {
let capacity = start
.len()
.checked_add(end.len())
.ok_or_else(|| invalid("topology union size overflowed"))?;
if capacity > EntityId::MAX_INDEX as usize {
return Err(invalid("topology union exceeds the pickable row limit"));
}
let mut merged = Vec::with_capacity(capacity);
let (mut left, mut right) = (0, 0);
while left < start.len() || right < end.len() {
let a = start.get(left).copied();
let b = end.get(right).copied();
let (from, to) = match (a, b) {
(Some(a), Some(b)) if a.endpoint_key() == b.endpoint_key() => {
left += 1;
right += 1;
(Some(a), Some(b))
}
(Some(a), Some(b)) if a.endpoint_key() < b.endpoint_key() => {
left += 1;
(Some(a), None)
}
(Some(_) | None, Some(b)) => {
right += 1;
(None, Some(b))
}
(Some(a), None) => {
left += 1;
(Some(a), None)
}
(None, None) => break,
};
let bond = from
.or(to)
.ok_or_else(|| invalid("empty topology transition"))?;
merged.push(TransitionBond {
start: from,
end: to,
active: ActiveTopologyBond { bond, weight: 0.0 },
});
}
Ok(merged)
}
const fn invalid(reason: &'static str) -> CoreError {
CoreError::InvalidTrajectory { reason }
}