Skip to main content

cugraph/structure/
typed_results.rs

1//! Runtime-typed result value enums for algorithm outputs.
2//!
3//! These enums wrap the two possible element widths for vertex IDs and
4//! score/weight values returned by cuGraph algorithms, following the same
5//! pattern as [`DegreeValues`](super::DegreeValues).
6
7use crate::{
8    array::{DeviceArrayView, RawDeviceArrayView},
9    error::{Error, ErrorKind, Result},
10    handle::EnteredContext,
11    types::DataType,
12};
13
14pub(super) fn widen_values<T: Into<U>, U>(values: Vec<T>) -> Vec<U> {
15    values.into_iter().map(Into::into).collect()
16}
17
18// ═══════════════════════════════════════════════════════════════════════════
19// VertexIds; typed enum for runtime-width vertex id arrays
20// ═══════════════════════════════════════════════════════════════════════════
21
22/// Runtime-typed vertex IDs from algorithm results.
23///
24/// The cuGraph C API returns vertex ID arrays as either `i32` or `i64`
25/// depending on graph index type. This enum wraps either variant.
26///
27/// [`to_vec_i64`](VertexIds::to_vec_i64) widens `i32` values to `i64`.
28#[derive(Debug)]
29pub enum VertexIds<'a> {
30    /// Vertex IDs stored as 32-bit integers.
31    I32(DeviceArrayView<'a, i32>),
32    /// Vertex IDs stored as 64-bit integers.
33    I64(DeviceArrayView<'a, i64>),
34}
35
36// ═══════════════════════════════════════════════════════════════════════════
37// IntegerValues; typed enum for runtime-width integer value arrays
38// ═══════════════════════════════════════════════════════════════════════════
39
40/// Runtime-typed integer values from algorithm results.
41///
42/// This is used for integer-valued outputs that are not necessarily graph
43/// vertex IDs, such as component labels, core numbers, and triangle counts.
44#[derive(Debug)]
45pub enum IntegerValues<'a> {
46    /// Values stored as 32-bit signed integers.
47    I32(DeviceArrayView<'a, i32>),
48    /// Values stored as 64-bit signed integers.
49    I64(DeviceArrayView<'a, i64>),
50}
51
52impl<'a> IntegerValues<'a> {
53    /// Construct from a type-erased raw view after checking the dtype.
54    ///
55    /// Returns an error if the dtype is neither `Int32` nor `Int64`.
56    pub(crate) fn from_raw(raw: RawDeviceArrayView<'a>) -> Result<Self> {
57        match raw.dtype()? {
58            DataType::Int32 => Ok(Self::I32(DeviceArrayView::from_raw_unchecked(raw))),
59            DataType::Int64 => Ok(Self::I64(DeviceArrayView::from_raw_unchecked(raw))),
60            other => Err(Error::new(ErrorKind::InvalidState, "IntegerValues::from_raw")
61                .with_message(format!("unexpected integer value dtype: {other}"))),
62        }
63    }
64
65    /// Number of elements in the value array.
66    pub fn len(&self) -> usize {
67        match self {
68            Self::I32(v) => v.len(),
69            Self::I64(v) => v.len(),
70        }
71    }
72
73    /// Whether the value array is empty.
74    pub fn is_empty(&self) -> bool {
75        self.len() == 0
76    }
77
78    /// Copy to host as `Vec<i64>`, widening `i32` values if needed.
79    pub fn to_vec_i64(&self, handle: &EnteredContext<'_>) -> Result<Vec<i64>> {
80        match self {
81            Self::I32(v) => v.to_vec(handle).map(widen_values),
82            Self::I64(v) => v.to_vec(handle),
83        }
84    }
85
86    /// Copy to host as `Vec<i32>`.
87    ///
88    /// Returns an error if the underlying values are `i64`, since narrowing
89    /// could lose data.
90    pub fn to_vec_i32(&self, handle: &EnteredContext<'_>) -> Result<Vec<i32>> {
91        match self {
92            Self::I32(v) => v.to_vec(handle),
93            Self::I64(_) => Err(Error::new(ErrorKind::InvalidState, "IntegerValues::to_vec_i32")
94                .with_message("integer values are i64; use to_vec_i64() to avoid data loss")),
95        }
96    }
97
98    /// Get the inner view as `&DeviceArrayView<i32>`, or `None` if `i64`.
99    pub fn as_i32(&self) -> Option<&DeviceArrayView<'a, i32>> {
100        match self {
101            Self::I32(v) => Some(v),
102            Self::I64(_) => None,
103        }
104    }
105
106    /// Get the inner view as `&DeviceArrayView<i64>`, or `None` if `i32`.
107    pub fn as_i64(&self) -> Option<&DeviceArrayView<'a, i64>> {
108        match self {
109            Self::I32(_) => None,
110            Self::I64(v) => Some(v),
111        }
112    }
113}
114
115impl<'a> VertexIds<'a> {
116    /// Construct from a type-erased raw view after checking the dtype.
117    ///
118    /// Returns an error if the dtype is neither `Int32` nor `Int64`.
119    pub(crate) fn from_raw(raw: RawDeviceArrayView<'a>) -> Result<Self> {
120        match raw.dtype()? {
121            DataType::Int32 => Ok(Self::I32(DeviceArrayView::from_raw_unchecked(raw))),
122            DataType::Int64 => Ok(Self::I64(DeviceArrayView::from_raw_unchecked(raw))),
123            other => Err(Error::new(ErrorKind::InvalidState, "VertexIds::from_raw")
124                .with_message(format!("unexpected vertex id dtype: {other}"))),
125        }
126    }
127
128    /// Number of elements in the vertex id array.
129    pub fn len(&self) -> usize {
130        match self {
131            Self::I32(v) => v.len(),
132            Self::I64(v) => v.len(),
133        }
134    }
135
136    /// Whether the vertex id array is empty.
137    pub fn is_empty(&self) -> bool {
138        self.len() == 0
139    }
140
141    /// Copy to host as `Vec<i64>`, widening `i32` values if needed.
142    pub fn to_vec_i64(&self, handle: &EnteredContext<'_>) -> Result<Vec<i64>> {
143        match self {
144            Self::I32(v) => v.to_vec(handle).map(widen_values),
145            Self::I64(v) => v.to_vec(handle),
146        }
147    }
148
149    /// Copy to host as `Vec<i32>`.
150    ///
151    /// Returns an error if the underlying values are `i64`, since narrowing
152    /// could lose data.
153    pub fn to_vec_i32(&self, handle: &EnteredContext<'_>) -> Result<Vec<i32>> {
154        match self {
155            Self::I32(v) => v.to_vec(handle),
156            Self::I64(_) => Err(Error::new(ErrorKind::InvalidState, "VertexIds::to_vec_i32")
157                .with_message("vertex ids are i64; use to_vec_i64() to avoid data loss")),
158        }
159    }
160
161    /// Get the inner view as `&DeviceArrayView<i32>`, or `None` if `i64`.
162    pub fn as_i32(&self) -> Option<&DeviceArrayView<'a, i32>> {
163        match self {
164            Self::I32(v) => Some(v),
165            Self::I64(_) => None,
166        }
167    }
168
169    /// Get the inner view as `&DeviceArrayView<i64>`, or `None` if `i32`.
170    pub fn as_i64(&self) -> Option<&DeviceArrayView<'a, i64>> {
171        match self {
172            Self::I32(_) => None,
173            Self::I64(v) => Some(v),
174        }
175    }
176}
177
178// ═══════════════════════════════════════════════════════════════════════════
179// SizeValues; typed enum for runtime-width size_t arrays
180// ═══════════════════════════════════════════════════════════════════════════
181
182/// Runtime-typed `size_t` values from algorithm results.
183///
184/// cuGraph uses `size_t` for offsets such as per-ego subgraph edge offsets.
185#[derive(Debug)]
186pub struct SizeValues<'a>(DeviceArrayView<'a, usize>);
187
188impl<'a> SizeValues<'a> {
189    /// Construct from a type-erased raw view after checking the dtype.
190    pub(crate) fn from_raw(raw: RawDeviceArrayView<'a>) -> Result<Self> {
191        match raw.dtype()? {
192            DataType::SizeT => Ok(Self(DeviceArrayView::from_raw_unchecked(raw))),
193            other => Err(Error::new(ErrorKind::InvalidState, "SizeValues::from_raw")
194                .with_message(format!("unexpected size_t value dtype: {other}"))),
195        }
196    }
197
198    /// Number of elements in the value array.
199    pub fn len(&self) -> usize {
200        self.0.len()
201    }
202
203    /// Whether the value array is empty.
204    pub fn is_empty(&self) -> bool {
205        self.0.len() == 0
206    }
207
208    /// Copy to host as `Vec<usize>`.
209    pub fn to_vec_usize(&self, handle: &EnteredContext<'_>) -> Result<Vec<usize>> {
210        self.0.to_vec(handle)
211    }
212
213    /// Get the inner device array view.
214    pub fn as_usize(&self) -> &DeviceArrayView<'a, usize> {
215        &self.0
216    }
217}
218
219// ═══════════════════════════════════════════════════════════════════════════
220// ScoreValues; typed enum for runtime-width float score arrays
221// ═══════════════════════════════════════════════════════════════════════════
222
223/// Runtime-typed score/weight values from algorithm results.
224///
225/// The cuGraph C API returns score arrays as either `f32` or `f64`
226/// depending on graph weight type. This enum wraps either variant.
227///
228/// [`to_vec_f64`](ScoreValues::to_vec_f64) widens `f32` values to `f64`.
229#[derive(Debug)]
230pub enum ScoreValues<'a> {
231    /// Score values stored as 32-bit floats.
232    F32(DeviceArrayView<'a, f32>),
233    /// Score values stored as 64-bit floats.
234    F64(DeviceArrayView<'a, f64>),
235}
236
237impl<'a> ScoreValues<'a> {
238    /// Construct from a type-erased raw view after checking the dtype.
239    ///
240    /// Returns an error if the dtype is neither `Float32` nor `Float64`.
241    pub(crate) fn from_raw(raw: RawDeviceArrayView<'a>) -> Result<Self> {
242        match raw.dtype()? {
243            DataType::Float32 => Ok(Self::F32(DeviceArrayView::from_raw_unchecked(raw))),
244            DataType::Float64 => Ok(Self::F64(DeviceArrayView::from_raw_unchecked(raw))),
245            other => Err(Error::new(ErrorKind::InvalidState, "ScoreValues::from_raw")
246                .with_message(format!("unexpected score dtype: {other}"))),
247        }
248    }
249
250    /// Number of elements in the score array.
251    pub fn len(&self) -> usize {
252        match self {
253            Self::F32(v) => v.len(),
254            Self::F64(v) => v.len(),
255        }
256    }
257
258    /// Whether the score array is empty.
259    pub fn is_empty(&self) -> bool {
260        self.len() == 0
261    }
262
263    /// Copy to host as `Vec<f64>`, widening `f32` values if needed.
264    pub fn to_vec_f64(&self, handle: &EnteredContext<'_>) -> Result<Vec<f64>> {
265        match self {
266            Self::F32(v) => v.to_vec(handle).map(widen_values),
267            Self::F64(v) => v.to_vec(handle),
268        }
269    }
270
271    /// Copy to host as `Vec<f32>`.
272    ///
273    /// Returns an error if the underlying values are `f64`, since narrowing
274    /// could lose data.
275    pub fn to_vec_f32(&self, handle: &EnteredContext<'_>) -> Result<Vec<f32>> {
276        match self {
277            Self::F32(v) => v.to_vec(handle),
278            Self::F64(_) => Err(Error::new(ErrorKind::InvalidState, "ScoreValues::to_vec_f32")
279                .with_message("score values are f64; use to_vec_f64() to avoid data loss")),
280        }
281    }
282
283    /// Get the inner view as `&DeviceArrayView<f32>`, or `None` if `f64`.
284    pub fn as_f32(&self) -> Option<&DeviceArrayView<'a, f32>> {
285        match self {
286            Self::F32(v) => Some(v),
287            Self::F64(_) => None,
288        }
289    }
290
291    /// Get the inner view as `&DeviceArrayView<f64>`, or `None` if `f32`.
292    pub fn as_f64(&self) -> Option<&DeviceArrayView<'a, f64>> {
293        match self {
294            Self::F32(_) => None,
295            Self::F64(v) => Some(v),
296        }
297    }
298}
299
300// ═══════════════════════════════════════════════════════════════════════════
301// Distances; typed enum for algorithm distance arrays (integer or float)
302// ═══════════════════════════════════════════════════════════════════════════
303
304/// Runtime-typed distance values from traversal algorithm results.
305///
306/// Traversal algorithms return `cugraph_paths_result_t` containing either
307/// integer hop counts (`I32`/`I64` from BFS) or floating-point weighted
308/// distances (`F32`/`F64` from SSSP).
309///
310/// Conversion methods:
311/// - [`to_vec_i64`](Distances::to_vec_i64) widens `i32` values (returns an error for float variants)
312/// - [`to_vec_f64`](Distances::to_vec_f64) converts all variants to `f64`
313#[derive(Debug)]
314pub enum Distances<'a> {
315    /// Integer distances stored as 32-bit integers (BFS hop counts).
316    I32(DeviceArrayView<'a, i32>),
317    /// Integer distances stored as 64-bit integers (BFS hop counts).
318    I64(DeviceArrayView<'a, i64>),
319    /// Float distances stored as 32-bit floats (SSSP weighted distances).
320    F32(DeviceArrayView<'a, f32>),
321    /// Float distances stored as 64-bit floats (SSSP weighted distances).
322    F64(DeviceArrayView<'a, f64>),
323}
324
325impl<'a> Distances<'a> {
326    /// Construct from a type-erased raw view after checking the dtype.
327    ///
328    /// Accepts `Int32`, `Int64`, `Float32`, and `Float64`.
329    pub(crate) fn from_raw(raw: RawDeviceArrayView<'a>) -> Result<Self> {
330        match raw.dtype()? {
331            DataType::Int32 => Ok(Self::I32(DeviceArrayView::from_raw_unchecked(raw))),
332            DataType::Int64 => Ok(Self::I64(DeviceArrayView::from_raw_unchecked(raw))),
333            DataType::Float32 => Ok(Self::F32(DeviceArrayView::from_raw_unchecked(raw))),
334            DataType::Float64 => Ok(Self::F64(DeviceArrayView::from_raw_unchecked(raw))),
335            other => Err(Error::new(ErrorKind::InvalidState, "Distances::from_raw")
336                .with_message(format!("unexpected distance dtype: {other}"))),
337        }
338    }
339
340    /// Number of elements in the distance array.
341    pub fn len(&self) -> usize {
342        match self {
343            Self::I32(v) => v.len(),
344            Self::I64(v) => v.len(),
345            Self::F32(v) => v.len(),
346            Self::F64(v) => v.len(),
347        }
348    }
349
350    /// Whether the distance array is empty.
351    pub fn is_empty(&self) -> bool {
352        self.len() == 0
353    }
354
355    /// Copy to host as `Vec<i64>`, widening `i32` values if needed.
356    ///
357    /// Returns an error for float variants; use [`to_vec_f64`](Distances::to_vec_f64)
358    /// instead.
359    pub fn to_vec_i64(&self, handle: &EnteredContext<'_>) -> Result<Vec<i64>> {
360        match self {
361            Self::I32(v) => v.to_vec(handle).map(widen_values),
362            Self::I64(v) => v.to_vec(handle),
363            Self::F32(_) | Self::F64(_) => Err(Error::new(ErrorKind::InvalidState, "Distances::to_vec_i64")
364                .with_message("distances are floating-point; use to_vec_f64()")),
365        }
366    }
367
368    /// Copy to host as `Vec<i32>`.
369    ///
370    /// Returns an error if the underlying values are `i64` (narrowing) or
371    /// floating-point.
372    pub fn to_vec_i32(&self, handle: &EnteredContext<'_>) -> Result<Vec<i32>> {
373        match self {
374            Self::I32(v) => v.to_vec(handle),
375            Self::I64(_) => Err(Error::new(ErrorKind::InvalidState, "Distances::to_vec_i32")
376                .with_message("distances are i64; use to_vec_i64() to avoid data loss")),
377            Self::F32(_) | Self::F64(_) => Err(Error::new(ErrorKind::InvalidState, "Distances::to_vec_i32")
378                .with_message("distances are floating-point; use to_vec_f64()")),
379        }
380    }
381
382    /// Copy to host as `Vec<f64>`, widening all variants.
383    ///
384    /// Integer distances are converted to `f64` losslessly (for i32) or with
385    /// possible precision loss for very large i64 values.
386    pub fn to_vec_f64(&self, handle: &EnteredContext<'_>) -> Result<Vec<f64>> {
387        match self {
388            Self::I32(v) => v.to_vec(handle).map(widen_values),
389            Self::I64(v) => {
390                let vals = v.to_vec(handle)?;
391                Ok(vals.into_iter().map(|x| x as f64).collect())
392            },
393            Self::F32(v) => v.to_vec(handle).map(widen_values),
394            Self::F64(v) => v.to_vec(handle),
395        }
396    }
397
398    /// Copy to host as `Vec<f32>`.
399    ///
400    /// Returns an error if the underlying values are `i64`, `f64`, or `i32`
401    /// with values outside `f32` range. Only succeeds for `F32` variant.
402    pub fn to_vec_f32(&self, handle: &EnteredContext<'_>) -> Result<Vec<f32>> {
403        match self {
404            Self::F32(v) => v.to_vec(handle),
405            _ => Err(Error::new(ErrorKind::InvalidState, "Distances::to_vec_f32")
406                .with_message("distances are not f32; use to_vec_f64() for a lossless conversion")),
407        }
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use cudf::testing::run_named_cases;
414
415    use super::*;
416    use crate::{
417        ResourceHandle,
418        array::{DeviceArray, RawDeviceArrayView},
419        error::ErrorKind,
420        structure::DegreeValues,
421        types::DataType,
422    };
423
424    #[test]
425    fn native_owner_cases_1() {
426        run_named_cases(&[
427            (
428                "structure::typed_results::tests::vertex_ids_i64_to_vec_i32_is_invalid_state",
429                vertex_ids_i64_to_vec_i32_is_invalid_state,
430            ),
431            (
432                "structure::typed_results::tests::score_values_f64_to_vec_f32_is_invalid_state",
433                score_values_f64_to_vec_f32_is_invalid_state,
434            ),
435            (
436                "structure::typed_results::tests::distances_float_to_vec_i64_is_invalid_state",
437                distances_float_to_vec_i64_is_invalid_state,
438            ),
439            (
440                "structure::typed_results::tests::distances_i64_to_vec_i32_is_invalid_state",
441                distances_i64_to_vec_i32_is_invalid_state,
442            ),
443            (
444                "structure::typed_results::tests::distances_non_f32_to_vec_f32_is_invalid_state",
445                distances_non_f32_to_vec_f32_is_invalid_state,
446            ),
447            (
448                "structure::typed_results::tests::vertex_ids_from_raw_unexpected_dtype_is_invalid_state",
449                vertex_ids_from_raw_unexpected_dtype_is_invalid_state,
450            ),
451            (
452                "structure::typed_results::tests::score_values_from_raw_unexpected_dtype_is_invalid_state",
453                score_values_from_raw_unexpected_dtype_is_invalid_state,
454            ),
455            (
456                "structure::typed_results::tests::distances_from_raw_unexpected_dtype_is_invalid_state",
457                distances_from_raw_unexpected_dtype_is_invalid_state,
458            ),
459            (
460                "structure::degrees::tests::degree_values_i64_to_vec_i32_is_invalid_state",
461                degree_values_i64_to_vec_i32_is_invalid_state,
462            ),
463            (
464                "structure::degrees::tests::degree_values_from_raw_unexpected_dtype_is_invalid_state",
465                degree_values_from_raw_unexpected_dtype_is_invalid_state,
466            ),
467            ("array::tests::test_from_raw_roundtrip", test_from_raw_roundtrip),
468        ]);
469    }
470
471    fn vertex_ids_i64_to_vec_i32_is_invalid_state() {
472        let handle = ResourceHandle::new().expect("handle");
473        let entered = handle.enter("vertex_ids_i64_to_vec_i32_is_invalid_state").expect("entered");
474        let ids = DeviceArray::from_slice(&entered, &[1i64, 2, 3]).expect("ids");
475        let values = VertexIds::I64(ids.view().expect("view"));
476
477        let err = values.to_vec_i32(&entered).expect_err("i64 vertex ids should reject i32 copy");
478        assert_eq!(err.kind(), ErrorKind::InvalidState);
479        assert_eq!(err.operation(), "VertexIds::to_vec_i32");
480    }
481
482    fn score_values_f64_to_vec_f32_is_invalid_state() {
483        let handle = ResourceHandle::new().expect("handle");
484        let entered = handle.enter("score_values_f64_to_vec_f32_is_invalid_state").expect("entered");
485        let scores = DeviceArray::from_slice(&entered, &[1.0f64, 2.0, 3.0]).expect("scores");
486        let values = ScoreValues::F64(scores.view().expect("view"));
487
488        let err = values.to_vec_f32(&entered).expect_err("f64 score values should reject f32 copy");
489        assert_eq!(err.kind(), ErrorKind::InvalidState);
490        assert_eq!(err.operation(), "ScoreValues::to_vec_f32");
491    }
492
493    fn distances_float_to_vec_i64_is_invalid_state() {
494        let handle = ResourceHandle::new().expect("handle");
495        let entered = handle.enter("distances_float_to_vec_i64_is_invalid_state").expect("entered");
496        let distances = DeviceArray::from_slice(&entered, &[1.5f32, 2.5, 3.5]).expect("distances");
497        let values = Distances::F32(distances.view().expect("view"));
498
499        let err = values.to_vec_i64(&entered).expect_err("float distances should reject integer copy");
500        assert_eq!(err.kind(), ErrorKind::InvalidState);
501        assert_eq!(err.operation(), "Distances::to_vec_i64");
502    }
503
504    fn distances_i64_to_vec_i32_is_invalid_state() {
505        let handle = ResourceHandle::new().expect("handle");
506        let entered = handle.enter("distances_i64_to_vec_i32_is_invalid_state").expect("entered");
507        let distances = DeviceArray::from_slice(&entered, &[10i64, 20, 30]).expect("distances");
508        let values = Distances::I64(distances.view().expect("view"));
509
510        let err = values.to_vec_i32(&entered).expect_err("i64 distances should reject i32 copy");
511        assert_eq!(err.kind(), ErrorKind::InvalidState);
512        assert_eq!(err.operation(), "Distances::to_vec_i32");
513    }
514
515    fn distances_non_f32_to_vec_f32_is_invalid_state() {
516        let handle = ResourceHandle::new().expect("handle");
517        let entered = handle.enter("distances_non_f32_to_vec_f32_is_invalid_state").expect("entered");
518        let distances = DeviceArray::from_slice(&entered, &[10i32, 20, 30]).expect("distances");
519        let values = Distances::I32(distances.view().expect("view"));
520
521        let err = values.to_vec_f32(&entered).expect_err("non-f32 distances should reject f32 copy");
522        assert_eq!(err.kind(), ErrorKind::InvalidState);
523        assert_eq!(err.operation(), "Distances::to_vec_f32");
524    }
525
526    fn vertex_ids_from_raw_unexpected_dtype_is_invalid_state() {
527        let handle = ResourceHandle::new().expect("handle");
528        let entered = handle.enter("vertex_ids_from_raw_unexpected_dtype_is_invalid_state").expect("entered");
529        let ids = DeviceArray::from_slice(&entered, &[1i16, 2, 3]).expect("ids");
530        let view = ids.view().expect("view");
531        // SAFETY: The pointer/length pair comes from a live `DeviceArrayView`, and the dtype is
532        // intentionally mismatched to verify `VertexIds::from_raw` rejects unsupported inputs.
533        let raw = unsafe {
534            RawDeviceArrayView::try_from_raw(view.device_ptr(), view.len(), DataType::Int16, "test_raw")
535                .expect("raw view")
536        };
537
538        let err = VertexIds::from_raw(raw).expect_err("unexpected vertex dtype should be rejected");
539        assert_eq!(err.kind(), ErrorKind::InvalidState);
540        assert_eq!(err.operation(), "VertexIds::from_raw");
541    }
542
543    fn score_values_from_raw_unexpected_dtype_is_invalid_state() {
544        let handle = ResourceHandle::new().expect("handle");
545        let entered = handle.enter("score_values_from_raw_unexpected_dtype_is_invalid_state").expect("entered");
546        let scores = DeviceArray::from_slice(&entered, &[1i32, 2, 3]).expect("scores");
547        let view = scores.view().expect("view");
548        // SAFETY: The pointer/length pair comes from a live `DeviceArrayView`, and the dtype is
549        // intentionally mismatched to verify `ScoreValues::from_raw` rejects unsupported inputs.
550        let raw = unsafe {
551            RawDeviceArrayView::try_from_raw(view.device_ptr(), view.len(), DataType::Int32, "test_raw")
552                .expect("raw view")
553        };
554
555        let err = ScoreValues::from_raw(raw).expect_err("unexpected score dtype should be rejected");
556        assert_eq!(err.kind(), ErrorKind::InvalidState);
557        assert_eq!(err.operation(), "ScoreValues::from_raw");
558    }
559
560    fn distances_from_raw_unexpected_dtype_is_invalid_state() {
561        let handle = ResourceHandle::new().expect("handle");
562        let entered = handle.enter("distances_from_raw_unexpected_dtype_is_invalid_state").expect("entered");
563        let distances = DeviceArray::from_slice(&entered, &[1i16, 2, 3]).expect("distances");
564        let view = distances.view().expect("view");
565        // SAFETY: The pointer/length pair comes from a live `DeviceArrayView`, and the dtype is
566        // intentionally mismatched to verify `Distances::from_raw` rejects unsupported inputs.
567        let raw = unsafe {
568            RawDeviceArrayView::try_from_raw(view.device_ptr(), view.len(), DataType::Int16, "test_raw")
569                .expect("raw view")
570        };
571
572        let err = Distances::from_raw(raw).expect_err("unexpected distance dtype should be rejected");
573        assert_eq!(err.kind(), ErrorKind::InvalidState);
574        assert_eq!(err.operation(), "Distances::from_raw");
575    }
576
577    fn degree_values_i64_to_vec_i32_is_invalid_state() {
578        let handle = ResourceHandle::new().expect("handle");
579        let entered = handle.enter("degree_values_i64_to_vec_i32_is_invalid_state").expect("entered");
580        let degrees = DeviceArray::from_slice(&entered, &[1i64, 2, 3]).expect("degrees");
581        let values = DegreeValues::I64(degrees.view().expect("view"));
582
583        let err = values.to_vec_i32(&entered).expect_err("i64 degree values should reject i32 copy");
584        assert_eq!(err.kind(), ErrorKind::InvalidState);
585        assert_eq!(err.operation(), "DegreeValues::to_vec_i32");
586    }
587
588    fn degree_values_from_raw_unexpected_dtype_is_invalid_state() {
589        let handle = ResourceHandle::new().expect("handle");
590        let entered = handle.enter("degree_values_from_raw_unexpected_dtype_is_invalid_state").expect("entered");
591        let degrees = DeviceArray::from_slice(&entered, &[1.0f32, 2.0, 3.0]).expect("degrees");
592        let view = degrees.view().expect("view");
593        // SAFETY: The pointer/length pair comes from a live `DeviceArrayView`, and the dtype is
594        // intentionally mismatched to exercise Rust-side validation in `DegreeValues::from_raw`.
595        let raw = unsafe {
596            RawDeviceArrayView::try_from_raw(view.device_ptr(), view.len(), DataType::Float32, "test_raw")
597                .expect("raw view")
598        };
599
600        let err = DegreeValues::from_raw(raw).expect_err("unexpected degree dtype should be rejected");
601        assert_eq!(err.kind(), ErrorKind::InvalidState);
602        assert_eq!(err.operation(), "DegreeValues::from_raw");
603    }
604
605    fn test_from_raw_roundtrip() {
606        let handle = ResourceHandle::new().expect("handle");
607        let entered = handle.enter("test_from_raw_roundtrip").expect("entered");
608        let arr = DeviceArray::from_slice(&entered, &[100i32, 200, 300]).expect("create");
609
610        let owned_view = arr.view().expect("owned_view");
611        let raw_ptr = owned_view.device_ptr();
612
613        // SAFETY: `raw_ptr` comes from a live device allocation with exactly 3 i32 values,
614        // and `arr` is kept alive for the duration of `raw_view` usage.
615        let raw_view = unsafe { DeviceArrayView::<i32>::from_raw(raw_ptr, 3) }.expect("raw_view");
616        assert_eq!(raw_view.len(), 3);
617
618        let result: Vec<i32> = raw_view.to_vec(&entered).expect("to_vec");
619        assert_eq!(result, vec![100, 200, 300]);
620    }
621}