Skip to main content

oxgraph_hyper_bcsr/internal/
view.rs

1//! Borrowed bipartite-CSR hypergraph view and its caller-facing borrowed
2//! section parameter struct.
3
4use core::fmt;
5
6use crate::{
7    error::BcsrError,
8    internal::validation::{BcsrValidation, DerivedCounts, validate_sections},
9    word::{BcsrWords, LayoutWord},
10};
11
12/// Borrowed input slices for a bipartite-CSR hypergraph view.
13///
14/// Offset slices use `OffsetWord` and decode to the incidence index width.
15/// Participant value slices use `VertexWord` and decode to the vertex index
16/// width. Vertex-major relation slices use `RelationWord` and decode to the
17/// relation index width.
18///
19/// # Performance
20///
21/// `perf: unspecified`; this is a borrowed parameter struct.
22#[derive(Clone, Copy, Debug)]
23pub struct BcsrSections<'view, OffsetWord, VertexWord, RelationWord>
24where
25    OffsetWord: LayoutWord,
26    VertexWord: LayoutWord,
27    RelationWord: LayoutWord,
28{
29    /// Hyperedge-major head offsets, length `hyperedge_count + 1`.
30    pub head_offsets: &'view [OffsetWord],
31    /// Flat vertex IDs in head sets, length `P_head`.
32    pub head_participants: &'view [VertexWord],
33    /// Hyperedge-major tail offsets, length `hyperedge_count + 1`.
34    pub tail_offsets: &'view [OffsetWord],
35    /// Flat vertex IDs in tail sets, length `P_tail`.
36    pub tail_participants: &'view [VertexWord],
37    /// Vertex-major outgoing offsets, length `vertex_count + 1`.
38    pub vertex_outgoing_offsets: &'view [OffsetWord],
39    /// Flat hyperedge IDs where the vertex is in head, length `P_outgoing`.
40    pub vertex_outgoing_hyperedges: &'view [RelationWord],
41    /// Vertex-major incoming offsets, length `vertex_count + 1`.
42    pub vertex_incoming_offsets: &'view [OffsetWord],
43    /// Flat hyperedge IDs where the vertex is in tail, length `P_incoming`.
44    pub vertex_incoming_hyperedges: &'view [RelationWord],
45}
46
47#[cfg(kani)]
48impl<'view, OffsetWord, VertexWord, RelationWord>
49    BcsrSections<'view, OffsetWord, VertexWord, RelationWord>
50where
51    OffsetWord: LayoutWord,
52    VertexWord: LayoutWord,
53    RelationWord: LayoutWord,
54{
55    /// Builds a [`BcsrSections`] from eight ordered borrowed slices.
56    ///
57    /// Exists only to deduplicate the kani proof harnesses: each `#[kani::proof]`
58    /// otherwise repeats the eight-field struct literal verbatim. Argument
59    /// order matches the field declaration order above (head, tail,
60    /// vertex-outgoing, vertex-incoming; offsets paired with their value
61    /// slice).
62    ///
63    /// # Performance
64    ///
65    /// `O(1)` field assignment.
66    #[expect(
67        clippy::too_many_arguments,
68        reason = "eight slice arguments mirror the eight BcsrSections fields in the documented declaration order"
69    )]
70    pub(crate) fn for_kani(
71        head_offsets: &'view [OffsetWord],
72        head_participants: &'view [VertexWord],
73        tail_offsets: &'view [OffsetWord],
74        tail_participants: &'view [VertexWord],
75        vertex_outgoing_offsets: &'view [OffsetWord],
76        vertex_outgoing_hyperedges: &'view [RelationWord],
77        vertex_incoming_offsets: &'view [OffsetWord],
78        vertex_incoming_hyperedges: &'view [RelationWord],
79    ) -> Self {
80        Self {
81            head_offsets,
82            head_participants,
83            tail_offsets,
84            tail_participants,
85            vertex_outgoing_offsets,
86            vertex_outgoing_hyperedges,
87            vertex_incoming_offsets,
88            vertex_incoming_hyperedges,
89        }
90    }
91}
92
93/// Borrowed bipartite compressed-sparse-row hypergraph view.
94///
95/// `BcsrHypergraph` borrows the eight section payloads supplied through
96/// [`BcsrSections`] without copying or allocating. The single [`BcsrWords`]
97/// parameter selects the word family — [`NativeWords`](crate::NativeWords)
98/// for host-order build-path views, [`LeWords`](crate::LeWords) for
99/// little-endian snapshot-backed views. Construction validates the borrowed
100/// slices according to the chosen [`BcsrValidation`] level. Once constructed,
101/// every traversal is `O(degree)` in either direction.
102///
103/// # Performance
104///
105/// Construction is `O(P_head + P_tail + P_outgoing + P_incoming)` at
106/// [`BcsrValidation::Layout`]. [`BcsrValidation::Strict`] adds an
107/// `O((P_head + P_tail) · log d)` cross-direction walk where `d` is the
108/// maximum vertex outgoing or incoming degree.
109pub struct BcsrHypergraph<'view, W: BcsrWords> {
110    /// Validated counts cached for `O(1)` access.
111    counts: DerivedCounts,
112    /// The eight borrowed sections backing this view.
113    sections: BcsrSections<'view, W::OffsetWord, W::VertexWord, W::RelationWord>,
114}
115
116// Manual impls instead of derives: the word family `W` is a type-level
117// carrier that never appears as a value, so deriving would demand spurious
118// `W: Clone` / `W: Copy` / `W: Debug` bounds.
119impl<W: BcsrWords> Clone for BcsrHypergraph<'_, W> {
120    fn clone(&self) -> Self {
121        *self
122    }
123}
124
125impl<W: BcsrWords> Copy for BcsrHypergraph<'_, W> {}
126
127impl<W: BcsrWords> fmt::Debug for BcsrHypergraph<'_, W>
128where
129    W::OffsetWord: fmt::Debug,
130    W::VertexWord: fmt::Debug,
131    W::RelationWord: fmt::Debug,
132{
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        f.debug_struct("BcsrHypergraph")
135            .field("counts", &self.counts)
136            .field("sections", &self.sections)
137            .finish()
138    }
139}
140
141impl<'view, W: BcsrWords> BcsrHypergraph<'view, W> {
142    /// Validates `sections` at [`BcsrValidation::Layout`] and returns a view.
143    ///
144    /// # Errors
145    ///
146    /// Returns [`BcsrError`] when any layout invariant fails. See
147    /// [`BcsrValidation::Layout`] for the full list.
148    ///
149    /// # Performance
150    ///
151    /// `O(P_head + P_tail + P_outgoing + P_incoming)`.
152    pub fn open(
153        sections: BcsrSections<'view, W::OffsetWord, W::VertexWord, W::RelationWord>,
154    ) -> Result<Self, BcsrError> {
155        Self::open_with(sections, BcsrValidation::Layout)
156    }
157
158    /// Validates `sections` at the requested level and returns a view.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`BcsrError`] when any invariant visible at `level` fails.
163    ///
164    /// # Performance
165    ///
166    /// `O(P_head + P_tail + P_outgoing + P_incoming)` at
167    /// [`BcsrValidation::Layout`]; adds `O((P_head + P_tail) · log d)` at
168    /// [`BcsrValidation::Strict`].
169    pub fn open_with(
170        sections: BcsrSections<'view, W::OffsetWord, W::VertexWord, W::RelationWord>,
171        level: BcsrValidation,
172    ) -> Result<Self, BcsrError> {
173        let counts = validate_sections(&sections, level)?;
174        Ok(Self { counts, sections })
175    }
176
177    /// Builds a view over already-validated sections, skipping re-validation.
178    ///
179    /// The hypergraph builders validate their frozen output exactly once at
180    /// [`BcsrValidation::Strict`] inside `freeze`, so re-running the
181    /// validation walk for every frozen-wrapper `as_view` borrow would be
182    /// redundant; this constructor is crate-internal so every external input
183    /// still validates through [`Self::open`] / [`Self::open_with`].
184    ///
185    /// Counts are derived structurally from the slice lengths. Sections that
186    /// passed [`validate_sections`] have non-empty offset slices and a total
187    /// incidence count that fits `usize`, so the saturating arithmetic below
188    /// is exact for every valid input.
189    ///
190    /// # Performance
191    ///
192    /// This function is `O(1)`.
193    pub(crate) const fn from_validated_sections(
194        sections: BcsrSections<'view, W::OffsetWord, W::VertexWord, W::RelationWord>,
195    ) -> Self {
196        let p_outgoing = sections.vertex_outgoing_hyperedges.len();
197        let p_incoming = sections.vertex_incoming_hyperedges.len();
198        let counts = DerivedCounts {
199            vertex_count: sections.vertex_outgoing_offsets.len().saturating_sub(1),
200            hyperedge_count: sections.head_offsets.len().saturating_sub(1),
201            p_outgoing,
202            p_incoming,
203            total_incidences: p_outgoing.saturating_add(p_incoming),
204        };
205        Self { counts, sections }
206    }
207
208    /// Returns the number of vertices in this view.
209    ///
210    /// # Performance
211    ///
212    /// This method is `O(1)`.
213    #[must_use]
214    pub const fn vertex_count(&self) -> usize {
215        self.counts.vertex_count
216    }
217
218    /// Returns the number of hyperedges in this view.
219    ///
220    /// # Performance
221    ///
222    /// This method is `O(1)`.
223    #[must_use]
224    pub const fn hyperedge_count(&self) -> usize {
225        self.counts.hyperedge_count
226    }
227
228    /// Returns the number of outgoing incidences (`P_head == P_outgoing`).
229    ///
230    /// # Performance
231    ///
232    /// This method is `O(1)`.
233    #[must_use]
234    pub const fn outgoing_incidence_count(&self) -> usize {
235        self.counts.p_outgoing
236    }
237
238    /// Returns the number of incoming incidences (`P_tail == P_incoming`).
239    ///
240    /// # Performance
241    ///
242    /// This method is `O(1)`.
243    #[must_use]
244    pub const fn incoming_incidence_count(&self) -> usize {
245        self.counts.p_incoming
246    }
247
248    /// Returns the validated count cache.
249    pub(in crate::internal) const fn counts(&self) -> DerivedCounts {
250        self.counts
251    }
252
253    /// Returns the borrowed sections.
254    pub(in crate::internal) const fn sections(
255        &self,
256    ) -> &BcsrSections<'view, W::OffsetWord, W::VertexWord, W::RelationWord> {
257        &self.sections
258    }
259}