oxgraph-hyper-bcsr 0.3.2

Borrowed bipartite CSR hypergraph views implementing oxgraph-hyper traits.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//! Validation and snapshot-binding errors for bipartite-CSR hypergraph views.

use core::fmt;

use oxgraph_snapshot::SectionViewError;

/// Bipartite-CSR validation error.
///
/// Returned by [`BcsrHypergraph::open`](crate::BcsrHypergraph::open) and
/// [`BcsrHypergraph::open_with`](crate::BcsrHypergraph::open_with) when one
/// of the eight section payloads fails validation.
///
/// # Performance
///
/// `perf: unspecified`; errors are returned only from validation paths.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BcsrError {
    /// `count + 1` overflowed `usize`, so the offset slice length cannot fit.
    OffsetLengthOverflow {
        /// The count for which `count + 1` overflowed.
        count: usize,
    },
    /// An offset slice has the wrong length.
    OffsetLength {
        /// Which offset section this error came from.
        section: BcsrSection,
        /// Expected length (`count + 1`).
        expected: usize,
        /// Actual length seen.
        actual: usize,
    },
    /// `head_offsets` and `tail_offsets` disagree on `hyperedge_count + 1`.
    HyperedgeOffsetLengthMismatch {
        /// `head_offsets.len()`.
        head_offsets_len: usize,
        /// `tail_offsets.len()`.
        tail_offsets_len: usize,
    },
    /// `vertex_outgoing_offsets` and `vertex_incoming_offsets` disagree on
    /// `vertex_count + 1`.
    VertexOffsetLengthMismatch {
        /// `vertex_outgoing_offsets.len()`.
        outgoing_offsets_len: usize,
        /// `vertex_incoming_offsets.len()`.
        incoming_offsets_len: usize,
    },
    /// The first offset in an offset slice was not zero.
    FirstOffset {
        /// Which offset section this error came from.
        section: BcsrSection,
        /// Actual first offset.
        actual: usize,
    },
    /// Offsets were not monotonically non-decreasing.
    NonMonotonicOffset {
        /// Which offset section this error came from.
        section: BcsrSection,
        /// Offset index where monotonicity failed.
        index: usize,
        /// Previous offset value.
        previous: usize,
        /// Actual offset value at `index`.
        actual: usize,
    },
    /// Final offset does not match the corresponding value slice length.
    FinalOffset {
        /// Which offset section this error came from.
        section: BcsrSection,
        /// Final offset value.
        final_offset: usize,
        /// Length of the value slice this offset references.
        value_len: usize,
    },
    /// A vertex ID was outside `0..vertex_count`.
    VertexOutOfRange {
        /// Which value section the bad ID came from.
        section: BcsrSection,
        /// Position within that section.
        index: usize,
        /// The bad vertex ID.
        vertex: usize,
        /// `vertex_count`.
        vertex_count: usize,
    },
    /// A hyperedge ID was outside `0..hyperedge_count`.
    HyperedgeOutOfRange {
        /// Which value section the bad ID came from.
        section: BcsrSection,
        /// Position within that section.
        index: usize,
        /// The bad hyperedge ID.
        hyperedge: usize,
        /// `hyperedge_count`.
        hyperedge_count: usize,
    },
    /// `head_participants.len()` and `vertex_outgoing_hyperedges.len()`
    /// disagree on the total outgoing-incidence count.
    OutgoingTotalMismatch {
        /// `head_participants.len()` (`P_head`).
        head_participants_len: usize,
        /// `vertex_outgoing_hyperedges.len()` (`P_outgoing`).
        outgoing_hyperedges_len: usize,
    },
    /// `tail_participants.len()` and `vertex_incoming_hyperedges.len()`
    /// disagree on the total incoming-incidence count.
    IncomingTotalMismatch {
        /// `tail_participants.len()` (`P_tail`).
        tail_participants_len: usize,
        /// `vertex_incoming_hyperedges.len()` (`P_incoming`).
        incoming_hyperedges_len: usize,
    },
    /// A range-local sequence (e.g. one hyperedge's head participants, or
    /// one vertex's outgoing hyperedges) was not strictly ascending.
    ///
    /// Bipartite-CSR requires set semantics within each range: vertex IDs
    /// inside a single hyperedge's head/tail and hyperedge IDs inside a
    /// single vertex's outgoing/incoming must be strictly increasing.
    NotStrictlyAscending {
        /// Which value section the bad pair came from.
        section: BcsrSection,
        /// Position of the offending value within the section.
        index: usize,
        /// Previous value at `index - 1`.
        previous: usize,
        /// Actual value at `index`.
        actual: usize,
    },
    /// A stored index value did not fit in `usize` on this target platform.
    UsizeOverflow {
        /// Value that could not be represented as `usize`.
        value: usize,
    },
    /// `P_head + P_tail` overflowed `usize`, so the incidence ID space cannot
    /// be indexed on this target.
    TotalIncidenceCountOverflow {
        /// Total head-side incidences (`P_head == P_outgoing`).
        p_head: usize,
        /// Total tail-side incidences (`P_tail == P_incoming`).
        p_tail: usize,
    },
    /// Cross-CSR consistency check (Strict-only) found a hyperedge that is
    /// recorded in one direction but missing from the other.
    CrossDirectionMismatch {
        /// Which side of the bipartite index disagreed.
        side: BcsrRoleSide,
        /// The hyperedge ID that did not match across the two indexes.
        hyperedge: usize,
        /// The vertex ID that did not match across the two indexes.
        vertex: usize,
    },
}

/// Names a single bipartite-CSR section for error reporting.
///
/// Carrying this in error variants avoids stringly-typed reasons while
/// keeping the failing section identifiable from the error alone.
///
/// # Performance
///
/// `perf: unspecified`; this is a metadata enum.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BcsrSection {
    /// `BCSR_HEAD_OFFSETS`.
    HeadOffsets,
    /// `BCSR_HEAD_PARTICIPANTS`.
    HeadParticipants,
    /// `BCSR_TAIL_OFFSETS`.
    TailOffsets,
    /// `BCSR_TAIL_PARTICIPANTS`.
    TailParticipants,
    /// `BCSR_VERTEX_OUTGOING_OFFSETS`.
    VertexOutgoingOffsets,
    /// `BCSR_VERTEX_OUTGOING_HYPEREDGES`.
    VertexOutgoingHyperedges,
    /// `BCSR_VERTEX_INCOMING_OFFSETS`.
    VertexIncomingOffsets,
    /// `BCSR_VERTEX_INCOMING_HYPEREDGES`.
    VertexIncomingHyperedges,
}

impl fmt::Display for BcsrSection {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::HeadOffsets => "BCSR_HEAD_OFFSETS",
            Self::HeadParticipants => "BCSR_HEAD_PARTICIPANTS",
            Self::TailOffsets => "BCSR_TAIL_OFFSETS",
            Self::TailParticipants => "BCSR_TAIL_PARTICIPANTS",
            Self::VertexOutgoingOffsets => "BCSR_VERTEX_OUTGOING_OFFSETS",
            Self::VertexOutgoingHyperedges => "BCSR_VERTEX_OUTGOING_HYPEREDGES",
            Self::VertexIncomingOffsets => "BCSR_VERTEX_INCOMING_OFFSETS",
            Self::VertexIncomingHyperedges => "BCSR_VERTEX_INCOMING_HYPEREDGES",
        })
    }
}

/// Side of the bipartite index that produced a cross-direction mismatch.
///
/// # Performance
///
/// `perf: unspecified`; this is a metadata enum.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BcsrRoleSide {
    /// Hyperedge-major head versus vertex-major outgoing.
    Outgoing,
    /// Hyperedge-major tail versus vertex-major incoming.
    Incoming,
}

impl fmt::Display for BcsrRoleSide {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::Outgoing => "outgoing",
            Self::Incoming => "incoming",
        })
    }
}

impl fmt::Display for BcsrError {
    #[expect(
        clippy::too_many_lines,
        reason = "one flat exhaustive Display match over every BcsrError variant; splitting it reintroduces the silent _ => Ok(()) wildcards this consolidation removed"
    )]
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        // One flat, exhaustive match: a new variant forces a Display arm at
        // compile time, and there is no `_ => Ok(())` wildcard that could
        // silently format nothing for a mis-routed variant.
        match self {
            Self::OffsetLengthOverflow { count } => {
                write!(formatter, "offset length overflow for count {count}")
            }
            Self::OffsetLength {
                section,
                expected,
                actual,
            } => write!(
                formatter,
                "{section} has wrong length: expected {expected}, got {actual}"
            ),
            Self::HyperedgeOffsetLengthMismatch {
                head_offsets_len,
                tail_offsets_len,
            } => write!(
                formatter,
                "head_offsets length {head_offsets_len} disagrees with tail_offsets length {tail_offsets_len}"
            ),
            Self::VertexOffsetLengthMismatch {
                outgoing_offsets_len,
                incoming_offsets_len,
            } => write!(
                formatter,
                "vertex_outgoing_offsets length {outgoing_offsets_len} disagrees with vertex_incoming_offsets length {incoming_offsets_len}"
            ),
            Self::FirstOffset { section, actual } => {
                write!(formatter, "{section} first offset must be 0, got {actual}")
            }
            Self::NonMonotonicOffset {
                section,
                index,
                previous,
                actual,
            } => write!(
                formatter,
                "{section} offset at index {index} is not monotonic: previous {previous}, got {actual}"
            ),
            Self::FinalOffset {
                section,
                final_offset,
                value_len,
            } => write!(
                formatter,
                "{section} final offset {final_offset} does not match value length {value_len}"
            ),
            Self::VertexOutOfRange {
                section,
                index,
                vertex,
                vertex_count,
            } => write!(
                formatter,
                "{section} vertex {vertex} at index {index} is out of range (vertex count {vertex_count})"
            ),
            Self::HyperedgeOutOfRange {
                section,
                index,
                hyperedge,
                hyperedge_count,
            } => write!(
                formatter,
                "{section} hyperedge {hyperedge} at index {index} is out of range (hyperedge count {hyperedge_count})"
            ),
            Self::NotStrictlyAscending {
                section,
                index,
                previous,
                actual,
            } => write!(
                formatter,
                "{section} value at index {index} is not strictly ascending: previous {previous}, got {actual}"
            ),
            Self::OutgoingTotalMismatch {
                head_participants_len,
                outgoing_hyperedges_len,
            } => write!(
                formatter,
                "head_participants length {head_participants_len} disagrees with vertex_outgoing_hyperedges length {outgoing_hyperedges_len}"
            ),
            Self::IncomingTotalMismatch {
                tail_participants_len,
                incoming_hyperedges_len,
            } => write!(
                formatter,
                "tail_participants length {tail_participants_len} disagrees with vertex_incoming_hyperedges length {incoming_hyperedges_len}"
            ),
            Self::UsizeOverflow { value } => {
                write!(formatter, "BCSR index value {value} does not fit usize")
            }
            Self::TotalIncidenceCountOverflow { p_head, p_tail } => write!(
                formatter,
                "incidence ID space P_head ({p_head}) + P_tail ({p_tail}) overflows usize"
            ),
            Self::CrossDirectionMismatch {
                side,
                hyperedge,
                vertex,
            } => write!(
                formatter,
                "cross-direction mismatch on {side}: hyperedge {hyperedge} and vertex {vertex} disagree"
            ),
        }
    }
}

impl core::error::Error for BcsrError {}

/// Error returned when a snapshot cannot be opened as a bipartite-CSR
/// hypergraph view.
///
/// # Performance
///
/// `perf: unspecified`; errors are returned only from snapshot-bound paths.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BcsrSnapshotError {
    /// The snapshot is missing one of the eight required sections.
    MissingSection {
        /// Which section was missing.
        section: BcsrSection,
        /// The kind constant the lookup used.
        kind: u32,
    },
    /// A required section was present but its version did not match.
    VersionMismatch {
        /// Which section had the wrong version.
        section: BcsrSection,
        /// The kind constant the lookup used.
        kind: u32,
        /// Version the reader required.
        expected: u32,
        /// Version recorded in the snapshot.
        actual: u32,
    },
    /// A required section payload could not be borrowed as `[U32<LE>]`.
    SectionView {
        /// Which section failed the typed-slice cast.
        section: BcsrSection,
        /// The underlying snapshot error.
        error: SectionViewError,
    },
    /// One of the offset sections was empty; bipartite-CSR requires at least
    /// one entry for the n-plus-one layout.
    OffsetsEmpty {
        /// Which offset section was empty.
        section: BcsrSection,
    },
    /// A derived count would not fit in `u32`.
    CountOverflow {
        /// Which section's length triggered the overflow.
        section: BcsrSection,
        /// Length of the offsets section.
        offsets_len: usize,
    },
    /// Bipartite-CSR layout-shape error surfaced through the borrowed sections.
    Bcsr(BcsrError),
}

impl fmt::Display for BcsrSnapshotError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MissingSection { section, kind } => write!(
                formatter,
                "snapshot has no {section} section (kind 0x{kind:04x})"
            ),
            Self::VersionMismatch {
                section,
                kind,
                expected,
                actual,
            } => write!(
                formatter,
                "{section} section (kind 0x{kind:04x}) version {actual} does not match expected {expected}"
            ),
            Self::SectionView { section, error } => write!(
                formatter,
                "{section} cannot be borrowed as little-endian u32: {error}"
            ),
            Self::OffsetsEmpty { section } => write!(formatter, "{section} section is empty"),
            Self::CountOverflow {
                section,
                offsets_len,
            } => write!(
                formatter,
                "derived count from {section} length {offsets_len} does not fit u32"
            ),
            Self::Bcsr(error) => {
                write!(formatter, "bipartite-CSR validation failed: {error}")
            }
        }
    }
}

impl core::error::Error for BcsrSnapshotError {}

impl From<BcsrError> for BcsrSnapshotError {
    fn from(error: BcsrError) -> Self {
        Self::Bcsr(error)
    }
}