Skip to main content

oxgraph_property/
weights.rs

1//! Topology weight adapters over selected primitive property layers.
2//!
3//! Defines the borrowed [`GraphPropertyLayers`] / [`HyperPropertyLayers`]
4//! partitions, the [`DenseWeights`] / [`SparseWeights`] adapters that opt a
5//! selected primitive layer into the per-axis topology weight traits, and the
6//! selection-validation helpers backing their constructors.
7
8use arrow_array::{Array, PrimitiveArray, types::ArrowPrimitiveType};
9use oxgraph_topology::{
10    ElementIndex, ElementWeight, IncidenceBase, IncidenceIndex, IncidenceWeight, RelationIndex,
11    RelationWeight, TopologyBase,
12};
13
14use crate::{
15    model::{IdFamily, PropertyError, PropertyLayer, PropertyLayerData, ensure_no_nulls},
16    width::{AxisIndex, ElementAxis, IncidenceAxis, PropertyAxis, PropertyIndex, RelationAxis},
17};
18
19/// Borrowed graph property layers partitioned by topology ID family.
20///
21/// # Performance
22///
23/// Copying this struct is `O(1)`.
24#[derive(Clone, Copy, Debug)]
25pub struct GraphPropertyLayers<'view, Id, NodeIndex, EdgeIndex>
26where
27    NodeIndex: PropertyIndex,
28    EdgeIndex: PropertyIndex,
29{
30    /// Element/node-keyed property layers.
31    pub element: &'view [PropertyLayer<Id, NodeIndex>],
32    /// Relation/edge-keyed property layers.
33    pub relation: &'view [PropertyLayer<Id, EdgeIndex>],
34}
35
36/// Borrowed hypergraph property layers partitioned by topology ID family.
37///
38/// # Performance
39///
40/// Copying this struct is `O(1)`.
41#[derive(Clone, Copy, Debug)]
42pub struct HyperPropertyLayers<'view, Id, VertexIndex, RelationIndex, IncidenceIndex>
43where
44    VertexIndex: PropertyIndex,
45    RelationIndex: PropertyIndex,
46    IncidenceIndex: PropertyIndex,
47{
48    /// Element/vertex-keyed property layers.
49    pub element: &'view [PropertyLayer<Id, VertexIndex>],
50    /// Relation/hyperedge-keyed property layers.
51    pub relation: &'view [PropertyLayer<Id, RelationIndex>],
52    /// Incidence/participant-keyed property layers.
53    pub incidence: &'view [PropertyLayer<Id, IncidenceIndex>],
54}
55
56/// Stamps the `TopologyBase` impl shared by every axis of a weight-storage type.
57///
58/// Each `(storage, axis)` pair forwards the same associated `ElementId` /
59/// `RelationId` types from the underlying topology view; this keeps that one
60/// body in a single place instead of six hand-written copies.
61macro_rules! impl_axis_topology_base {
62    ($storage:ident, $axis:ty, $index_trait:ident) => {
63        impl<T, Id, I, P> TopologyBase for $storage<'_, $axis, T, Id, I, P>
64        where
65            T: $index_trait,
66            I: PropertyIndex,
67            P: ArrowPrimitiveType,
68        {
69            type ElementId = T::ElementId;
70            type RelationId = T::RelationId;
71        }
72    };
73}
74
75/// Stamps the per-axis topology weight impls for [`DenseWeights`].
76///
77/// Each axis (`Element` / `Relation` / `Incidence`) opts dense storage into the
78/// matching topology weight trait. The three axis weight traits expose distinct
79/// method names (`element_weight` / `relation_weight` / `incidence_weight`) and
80/// parameter types, so a single blanket `impl` over the axis marker is not
81/// expressible; this macro keeps the one dense lookup body — read the value
82/// slot at the topology's dense index for the axis ID — and stamps it per axis
83/// instead of hand-writing three near-identical blocks.
84macro_rules! impl_dense_axis_weight {
85    (
86        $axis:ty, $index_trait:ident, $id_ty:ident, $index_fn:ident,
87        $weight_trait:ident, $weight_fn:ident
88    ) => {
89        impl<T, Id, I, P> $weight_trait for DenseWeights<'_, $axis, T, Id, I, P>
90        where
91            T: $index_trait,
92            I: PropertyIndex,
93            P: ArrowPrimitiveType,
94            P::Native: Copy,
95        {
96            type Weight = P::Native;
97
98            fn $weight_fn(&self, id: Self::$id_ty) -> Self::Weight {
99                self.values.value(self.topology.$index_fn(id))
100            }
101        }
102    };
103}
104
105/// Stamps the per-axis topology weight impls for [`SparseWeights`].
106///
107/// Mirrors [`impl_dense_axis_weight`] for sparse storage: the one sparse lookup
108/// body — binary-search the explicit indexes via [`sparse_value`], falling back
109/// to the totalizing default — is kept here and stamped per axis rather than
110/// repeated across three near-identical blocks.
111macro_rules! impl_sparse_axis_weight {
112    (
113        $axis:ty, $index_trait:ident, $id_ty:ident, $index_fn:ident,
114        $weight_trait:ident, $weight_fn:ident
115    ) => {
116        impl<T, Id, I, P> $weight_trait for SparseWeights<'_, $axis, T, Id, I, P>
117        where
118            T: $index_trait,
119            I: PropertyIndex,
120            P: ArrowPrimitiveType,
121            P::Native: Copy,
122        {
123            type Weight = P::Native;
124
125            fn $weight_fn(&self, id: Self::$id_ty) -> Self::Weight {
126                sparse_value::<I, P>(
127                    self.indices,
128                    self.values,
129                    self.default,
130                    self.topology.$index_fn(id),
131                )
132            }
133        }
134    };
135}
136
137/// Stamps the `IncidenceBase` impl shared by the incidence axis of a storage type.
138///
139/// Only the incidence axis carries incidence-specific associated types; this
140/// keeps that one forwarding body in a single place for both storage types.
141macro_rules! impl_axis_incidence_base {
142    ($storage:ident) => {
143        impl<T, Id, I, P> IncidenceBase for $storage<'_, IncidenceAxis, T, Id, I, P>
144        where
145            T: IncidenceIndex,
146            I: PropertyIndex,
147            P: ArrowPrimitiveType,
148        {
149            type IncidenceId = T::IncidenceId;
150            type Role = T::Role;
151        }
152    };
153}
154
155/// Selected dense primitive weights bound to one axis of a topology view.
156///
157/// `A` is one of [`ElementAxis`], [`RelationAxis`], or [`IncidenceAxis`];
158/// the per-axis `new` constructor selects the right topology bound.
159///
160/// # Performance
161///
162/// Weight lookup is `O(1)`.
163pub struct DenseWeights<'view, A, T, Id, I, P>
164where
165    A: PropertyAxis,
166    I: PropertyIndex,
167    P: ArrowPrimitiveType,
168{
169    /// Topology view that supplies ID-to-index mapping.
170    topology: &'view T,
171    /// Primitive values.
172    values: &'view PrimitiveArray<P>,
173    /// Property axis, ID, and index marker.
174    property: core::marker::PhantomData<(A, Id, I)>,
175}
176
177impl<'view, A, T, Id, I, P> DenseWeights<'view, A, T, Id, I, P>
178where
179    A: PropertyAxis,
180    T: AxisIndex<A>,
181    I: PropertyIndex,
182    P: ArrowPrimitiveType,
183{
184    /// Selects a dense primitive layer as weights for `topology` along axis
185    /// `A` ([`ElementAxis`], [`RelationAxis`], or [`IncidenceAxis`]).
186    ///
187    /// # Errors
188    ///
189    /// Returns [`PropertyError`] if the layer is not `A`-keyed, dense,
190    /// primitive type `P`, non-null, or long enough.
191    ///
192    /// # Performance
193    ///
194    /// Validation is `O(layer.len())` for the null check.
195    pub fn new(
196        topology: &'view T,
197        layer: &'view PropertyLayer<Id, I>,
198    ) -> Result<Self, PropertyError> {
199        let values = validate_dense_primitive_selection::<Id, I, P>(
200            layer,
201            A::id_family(),
202            topology.axis_bound(),
203        )?;
204        Ok(Self {
205            topology,
206            values,
207            property: core::marker::PhantomData,
208        })
209    }
210}
211
212impl_axis_topology_base!(DenseWeights, ElementAxis, ElementIndex);
213impl_axis_topology_base!(DenseWeights, RelationAxis, RelationIndex);
214impl_axis_topology_base!(DenseWeights, IncidenceAxis, IncidenceIndex);
215impl_axis_incidence_base!(DenseWeights);
216
217impl_dense_axis_weight!(
218    ElementAxis,
219    ElementIndex,
220    ElementId,
221    element_index,
222    ElementWeight,
223    element_weight
224);
225impl_dense_axis_weight!(
226    RelationAxis,
227    RelationIndex,
228    RelationId,
229    relation_index,
230    RelationWeight,
231    relation_weight
232);
233impl_dense_axis_weight!(
234    IncidenceAxis,
235    IncidenceIndex,
236    IncidenceId,
237    incidence_index,
238    IncidenceWeight,
239    incidence_weight
240);
241
242/// Selected sparse primitive weights bound to one axis of a topology view.
243///
244/// `A` is one of [`ElementAxis`], [`RelationAxis`], or [`IncidenceAxis`];
245/// the per-axis `new` constructor selects the right topology bound.
246///
247/// # Performance
248///
249/// Weight lookup is `O(log k)` for `k` explicitly stored values.
250pub struct SparseWeights<'view, A, T, Id, I, P>
251where
252    A: PropertyAxis,
253    I: PropertyIndex,
254    P: ArrowPrimitiveType,
255{
256    /// Topology view that supplies ID-to-index mapping.
257    topology: &'view T,
258    /// Sparse indexes.
259    indices: &'view PrimitiveArray<I::ArrowType>,
260    /// Sparse values.
261    values: &'view PrimitiveArray<P>,
262    /// Totalizing default value.
263    default: P::Native,
264    /// Property axis and ID marker.
265    property: core::marker::PhantomData<(A, Id)>,
266}
267
268impl<'view, A, T, Id, I, P> SparseWeights<'view, A, T, Id, I, P>
269where
270    A: PropertyAxis,
271    T: AxisIndex<A>,
272    I: PropertyIndex,
273    P: ArrowPrimitiveType,
274    P::Native: Copy,
275{
276    /// Selects a sparse primitive layer as total weights for `topology`
277    /// along axis `A` ([`ElementAxis`], [`RelationAxis`], or
278    /// [`IncidenceAxis`]).
279    ///
280    /// # Errors
281    ///
282    /// Returns [`PropertyError`] when the sparse layer is not total or
283    /// type-compatible.
284    ///
285    /// # Performance
286    ///
287    /// Validation is `O(1)` plus default downcast.
288    pub fn new(
289        topology: &'view T,
290        layer: &'view PropertyLayer<Id, I>,
291    ) -> Result<Self, PropertyError> {
292        let (indices, values, default) = validate_sparse_primitive_selection::<I, P, Id>(
293            layer,
294            A::id_family(),
295            topology.axis_bound(),
296        )?;
297        Ok(Self {
298            topology,
299            indices,
300            values,
301            default,
302            property: core::marker::PhantomData,
303        })
304    }
305}
306
307impl_axis_topology_base!(SparseWeights, ElementAxis, ElementIndex);
308impl_axis_topology_base!(SparseWeights, RelationAxis, RelationIndex);
309impl_axis_topology_base!(SparseWeights, IncidenceAxis, IncidenceIndex);
310impl_axis_incidence_base!(SparseWeights);
311
312impl_sparse_axis_weight!(
313    ElementAxis,
314    ElementIndex,
315    ElementId,
316    element_index,
317    ElementWeight,
318    element_weight
319);
320impl_sparse_axis_weight!(
321    RelationAxis,
322    RelationIndex,
323    RelationId,
324    relation_index,
325    RelationWeight,
326    relation_weight
327);
328impl_sparse_axis_weight!(
329    IncidenceAxis,
330    IncidenceIndex,
331    IncidenceId,
332    incidence_index,
333    IncidenceWeight,
334    incidence_weight
335);
336
337/// Validates a dense primitive layer selection.
338///
339/// # Performance
340///
341/// This function is `O(layer.len())` for the null check.
342fn validate_dense_primitive_selection<Id, I, P>(
343    layer: &PropertyLayer<Id, I>,
344    expected: IdFamily,
345    required: usize,
346) -> Result<&PrimitiveArray<P>, PropertyError>
347where
348    I: PropertyIndex,
349    P: ArrowPrimitiveType,
350{
351    if layer.descriptor().id_family != expected {
352        return Err(PropertyError::IdFamilyMismatch {
353            expected,
354            actual: layer.descriptor().id_family,
355        });
356    }
357    if layer.len() < required {
358        return Err(PropertyError::LayerTooShort {
359            required,
360            actual: layer.len(),
361        });
362    }
363    let PropertyLayerData::Dense { values } = layer.data() else {
364        return Err(PropertyError::ExpectedDenseStorage {
365            name: layer.descriptor().name.clone(),
366        });
367    };
368    let primitive = values
369        .as_any()
370        .downcast_ref::<PrimitiveArray<P>>()
371        .ok_or_else(|| PropertyError::ArrowTypeMismatch {
372            name: layer.descriptor().name.clone(),
373        })?;
374    ensure_no_nulls(primitive)?;
375    Ok(primitive)
376}
377
378/// Borrowed sparse primitive selection parts.
379type SparsePrimitiveSelection<'layer, I, P> = (
380    &'layer PrimitiveArray<<I as PropertyIndex>::ArrowType>,
381    &'layer PrimitiveArray<P>,
382    <P as ArrowPrimitiveType>::Native,
383);
384
385/// Validates a sparse primitive layer selection.
386///
387/// # Performance
388///
389/// This function is `O(1)` plus default downcast.
390fn validate_sparse_primitive_selection<I, P, Id>(
391    layer: &PropertyLayer<Id, I>,
392    expected: IdFamily,
393    required: usize,
394) -> Result<SparsePrimitiveSelection<'_, I, P>, PropertyError>
395where
396    I: PropertyIndex,
397    P: ArrowPrimitiveType,
398    P::Native: Copy,
399{
400    if layer.descriptor().id_family != expected {
401        return Err(PropertyError::IdFamilyMismatch {
402            expected,
403            actual: layer.descriptor().id_family,
404        });
405    }
406    if layer.len() < required {
407        return Err(PropertyError::LayerTooShort {
408            required,
409            actual: layer.len(),
410        });
411    }
412    let PropertyLayerData::Sparse {
413        indices,
414        values,
415        default,
416    } = layer.data()
417    else {
418        return Err(PropertyError::ExpectedSparseStorage {
419            name: layer.descriptor().name.clone(),
420        });
421    };
422    let Some(default_array) = default else {
423        return Err(PropertyError::SparseNullMissingNotTotal {
424            name: layer.descriptor().name.clone(),
425        });
426    };
427    let primitive = values
428        .as_any()
429        .downcast_ref::<PrimitiveArray<P>>()
430        .ok_or_else(|| PropertyError::ArrowTypeMismatch {
431            name: layer.descriptor().name.clone(),
432        })?;
433    ensure_no_nulls(primitive)?;
434    let default_primitive = default_array
435        .as_any()
436        .downcast_ref::<PrimitiveArray<P>>()
437        .ok_or_else(|| PropertyError::ArrowTypeMismatch {
438            name: layer.descriptor().name.clone(),
439        })?;
440    if default_primitive.len() != 1 || default_primitive.is_null(0) {
441        return Err(PropertyError::DefaultPolicyMismatch {
442            name: layer.descriptor().name.clone(),
443        });
444    }
445    Ok((indices.as_ref(), primitive, default_primitive.value(0)))
446}
447
448/// Returns a sparse primitive value or the layer default.
449///
450/// # Performance
451///
452/// This function is `O(log k)` for `k` sparse indexes.
453fn sparse_value<I, P>(
454    indices: &PrimitiveArray<I::ArrowType>,
455    values: &PrimitiveArray<P>,
456    default: P::Native,
457    index: usize,
458) -> P::Native
459where
460    I: PropertyIndex,
461    P: ArrowPrimitiveType,
462    P::Native: Copy,
463{
464    let Some(target) = I::from_usize(index) else {
465        return default;
466    };
467    let mut low = 0_usize;
468    let mut high = indices.len();
469    while low < high {
470        let mid = low + ((high - low) / 2);
471        let value = indices.value(mid);
472        if value < target {
473            low = mid + 1;
474        } else {
475            high = mid;
476        }
477    }
478    if low < indices.len() && indices.value(low) == target {
479        values.value(low)
480    } else {
481        default
482    }
483}