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