Skip to main content

molframe_core/provider/
bond_payload.rs

1//! Shared bond payloads whose atom endpoints remain globally addressable.
2//!
3//! A chunk retains one immutable structure snapshot and a bond-table range.
4//! Construction and random access are O(1); no coordinate or topology column
5//! is copied. The consumer converts endpoints to compact local indices only
6//! after locating the corresponding resident atom pages.
7
8use super::source::validate_atom_endpoint_range;
9use super::{ChunkDescriptor, DatasetId, LocalRow, LogicalRow, ProviderError};
10use crate::optional::OptionalSymbol;
11use crate::{BondIndex, BondOrder, BondProvenance, BondRecord, Structure};
12use std::ops::Range;
13
14/// Stable atom identity independent of chunk residency.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub struct AtomEndpoint {
17    dataset: DatasetId,
18    row: LogicalRow,
19}
20
21impl AtomEndpoint {
22    /// Creates an endpoint in an atom dataset's global logical row space.
23    #[must_use]
24    pub const fn new(dataset: DatasetId, row: LogicalRow) -> Self {
25        Self { dataset, row }
26    }
27
28    /// Atom dataset containing the endpoint.
29    #[must_use]
30    pub const fn dataset(self) -> DatasetId {
31        self.dataset
32    }
33
34    /// Stable atom row, never a chunk-local index.
35    #[must_use]
36    pub const fn row(self) -> LogicalRow {
37        self.row
38    }
39}
40
41/// One bond projected from native storage with global atom endpoints.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct BondChunkRecord {
44    /// First global atom endpoint.
45    pub atom_a: AtomEndpoint,
46    /// Second global atom endpoint.
47    pub atom_b: AtomEndpoint,
48    /// Chemical order retained from the source.
49    pub order: BondOrder,
50    /// Provenance retained from the source.
51    pub provenance: BondProvenance,
52    /// The connection's own classification, interned in the source dictionary.
53    ///
54    /// A consumer that reads a bond across a chunk boundary must not lose the
55    /// distinction between coordination and covalent connectivity, so the class
56    /// travels with the record. Resolve it against
57    /// [`BondChunk::structure`] before comparing it to a class from another
58    /// structure, exactly as for the other interned fields.
59    pub kind: OptionalSymbol,
60}
61
62/// One independently requestable range of an immutable bond table.
63#[derive(Clone, Debug)]
64pub struct BondChunk {
65    descriptor: ChunkDescriptor,
66    structure: Structure,
67    source_range: Range<u32>,
68    atom_dataset: DatasetId,
69    atom_logical_start: LogicalRow,
70}
71
72impl BondChunk {
73    /// Retains an existing bond-table range without materialising edge columns.
74    ///
75    /// # Errors
76    ///
77    /// Rejects unavailable topology, mismatched lengths, ranges outside the
78    /// source table, or atom identities that overflow the global row space.
79    pub fn shared(
80        descriptor: ChunkDescriptor,
81        structure: Structure,
82        source_range: Range<u32>,
83        atom_dataset: DatasetId,
84        atom_logical_start: LogicalRow,
85    ) -> Result<Self, ProviderError> {
86        if !structure.data().bonds.is_available() {
87            return Err(ProviderError::BondTopologyUnavailable);
88        }
89        let actual = source_range.end.checked_sub(source_range.start).ok_or(
90            ProviderError::BondRangeUnavailable {
91                first: u64::from(source_range.start),
92                end: u64::from(source_range.end),
93                rows: structure.data().bonds.len() as u64,
94            },
95        )?;
96        if actual != descriptor.rows() {
97            return Err(ProviderError::PayloadLengthMismatch {
98                declared: descriptor.rows(),
99                actual,
100            });
101        }
102        let table_rows = u64::try_from(structure.data().bonds.len()).map_err(|_| {
103            ProviderError::IdentityOverflow {
104                dataset: descriptor.dataset(),
105                field: "bond table rows",
106            }
107        })?;
108        if u64::from(source_range.end) > table_rows {
109            return Err(ProviderError::BondRangeUnavailable {
110                first: u64::from(source_range.start),
111                end: u64::from(source_range.end),
112                rows: table_rows,
113            });
114        }
115        validate_atom_endpoint_range(
116            descriptor.dataset(),
117            atom_logical_start,
118            structure.atom_count(),
119        )?;
120        Ok(Self {
121            descriptor,
122            structure,
123            source_range,
124            atom_dataset,
125            atom_logical_start,
126        })
127    }
128
129    /// Stable bond-chunk metadata.
130    #[must_use]
131    pub const fn descriptor(&self) -> ChunkDescriptor {
132        self.descriptor
133    }
134
135    /// Atom dataset referenced by both endpoint columns.
136    #[must_use]
137    pub const fn atom_dataset(&self) -> DatasetId {
138        self.atom_dataset
139    }
140
141    /// Retained immutable source; its coordinate columns are never copied.
142    #[must_use]
143    pub const fn structure(&self) -> &Structure {
144        &self.structure
145    }
146
147    /// Projects one local bond row to global atom identities in O(1).
148    ///
149    /// # Errors
150    ///
151    /// Returns a typed error for a local row outside the chunk or identity
152    /// arithmetic overflow.
153    pub fn record(&self, local: LocalRow) -> Result<BondChunkRecord, ProviderError> {
154        self.descriptor.resolve(local)?;
155        let source = self.source_range.start.checked_add(local.get()).ok_or(
156            ProviderError::IdentityOverflow {
157                dataset: self.descriptor.dataset(),
158                field: "bond source row",
159            },
160        )?;
161        let record = self
162            .structure
163            .data()
164            .bonds
165            .get(BondIndex::new(source))
166            .ok_or(ProviderError::BondRangeUnavailable {
167                first: u64::from(source),
168                end: u64::from(source) + 1,
169                rows: self.structure.data().bonds.len() as u64,
170            })?;
171        self.global_record(record)
172    }
173
174    /// Minimum bytes represented by the retained native edge columns.
175    ///
176    /// # Errors
177    ///
178    /// Returns a typed error if byte accounting exceeds `u64`.
179    pub fn minimum_host_bytes(&self) -> Result<u64, ProviderError> {
180        u64::from(self.descriptor.rows())
181            .checked_mul(std::mem::size_of::<BondRecord>() as u64)
182            .ok_or(ProviderError::IdentityOverflow {
183                dataset: self.descriptor.dataset(),
184                field: "bond payload bytes",
185            })
186    }
187
188    fn global_record(&self, record: BondRecord) -> Result<BondChunkRecord, ProviderError> {
189        Ok(BondChunkRecord {
190            atom_a: self.endpoint(record.atom_a.get())?,
191            atom_b: self.endpoint(record.atom_b.get())?,
192            order: record.order,
193            provenance: record.provenance,
194            kind: record.kind,
195        })
196    }
197
198    fn endpoint(&self, atom: u32) -> Result<AtomEndpoint, ProviderError> {
199        let row = self
200            .atom_logical_start
201            .get()
202            .checked_add(u64::from(atom))
203            .ok_or(ProviderError::IdentityOverflow {
204                dataset: self.atom_dataset,
205                field: "atom endpoint",
206            })?;
207        Ok(AtomEndpoint::new(self.atom_dataset, LogicalRow::new(row)))
208    }
209}
210
211#[cfg(test)]
212#[path = "bond_payload_tests.rs"]
213mod tests;