1use std::ffi::{c_int, c_long, c_longlong, c_short, c_uint, c_ulong, c_ulonglong, c_ushort};
2use std::mem::size_of;
3use std::ptr;
4
5#[cfg(feature = "half")]
6use half::{bf16, f16};
7use num_traits::{Bounded, Zero};
8#[cfg(feature = "half")]
9use pyo3::sync::PyOnceLock;
10use pyo3::{
11 Borrowed, Bound, Py, PyAny, PyResult, PyTypeInfo, Python,
12 conversion::IntoPyObject,
13 exceptions::{PyIndexError, PyValueError},
14 ffi::{self, PyTuple_Size},
15 pyobject_native_type_named,
16 types::{PyAnyMethods, PyDict, PyDictMethods, PyTuple, PyType},
17};
18
19use crate::npyffi::{
20 self, _PyDataType_GET_ITEM_DATA, NPY_ALIGNED_STRUCT, NPY_BYTEORDER_CHAR, NPY_ITEM_HASOBJECT,
21 NPY_TYPES, NpyTypes, PY_ARRAY_API, PyArray_Descr, PyDataType_ALIGNMENT, PyDataType_ELSIZE,
22 PyDataType_FIELDS, PyDataType_FLAGS, PyDataType_NAMES, PyDataType_SUBARRAY,
23};
24
25pub use num_complex::{Complex32, Complex64};
26
27#[repr(transparent)]
51pub struct PyArrayDescr(PyAny);
52
53pyobject_native_type_named!(PyArrayDescr);
54
55unsafe impl PyTypeInfo for PyArrayDescr {
56 #[inline]
57 fn type_object_raw<'py>(py: Python<'py>) -> *mut ffi::PyTypeObject {
58 unsafe { npyffi::get_type_object(py, NpyTypes::PyArrayDescr_Type) }
59 }
60}
61
62#[inline]
64pub fn dtype<'py, T: Element>(py: Python<'py>) -> Bound<'py, PyArrayDescr> {
65 T::get_dtype(py)
66}
67
68impl PyArrayDescr {
69 #[inline]
75 pub fn new<'a, 'py, T>(py: Python<'py>, ob: T) -> PyResult<Bound<'py, Self>>
76 where
77 T: IntoPyObject<'py>,
78 {
79 fn inner<'py>(
80 py: Python<'py>,
81 obj: Borrowed<'_, 'py, PyAny>,
82 ) -> PyResult<Bound<'py, PyArrayDescr>> {
83 let mut descr: *mut PyArray_Descr = ptr::null_mut();
84 unsafe {
85 PY_ARRAY_API.PyArray_DescrConverter2(py, obj.as_ptr(), &mut descr);
87 Bound::from_owned_ptr_or_err(py, descr.cast()).map(|any| any.cast_into_unchecked())
88 }
89 }
90
91 inner(
92 py,
93 ob.into_pyobject(py)
94 .map_err(Into::into)?
95 .into_any()
96 .as_borrowed(),
97 )
98 }
99
100 #[inline]
102 pub fn object(py: Python<'_>) -> Bound<'_, Self> {
103 Self::from_npy_type(py, NPY_TYPES::NPY_OBJECT)
104 }
105
106 #[inline]
108 pub fn of<'py, T: Element>(py: Python<'py>) -> Bound<'py, Self> {
109 T::get_dtype(py)
110 }
111
112 fn from_npy_type<'py>(py: Python<'py>, npy_type: NPY_TYPES) -> Bound<'py, Self> {
113 unsafe {
114 let descr = PY_ARRAY_API.PyArray_DescrFromType(py, npy_type as _);
115 Bound::from_owned_ptr(py, descr.cast()).cast_into_unchecked()
116 }
117 }
118
119 pub(crate) fn new_from_npy_type<'py>(py: Python<'py>, npy_type: NPY_TYPES) -> Bound<'py, Self> {
120 unsafe {
121 let descr = PY_ARRAY_API.PyArray_DescrNewFromType(py, npy_type as _);
122 Bound::from_owned_ptr(py, descr.cast()).cast_into_unchecked()
123 }
124 }
125}
126
127#[doc(alias = "PyArrayDescr")]
129pub trait PyArrayDescrMethods<'py>: Sealed {
130 fn as_dtype_ptr(&self) -> *mut PyArray_Descr;
132
133 fn into_dtype_ptr(self) -> *mut PyArray_Descr;
137
138 fn is_equiv_to(&self, other: &Self) -> bool;
140
141 fn typeobj(&self) -> Bound<'py, PyType>;
148
149 fn num(&self) -> c_int {
159 unsafe { &*_PyDataType_GET_ITEM_DATA(self.as_dtype_ptr()) }.type_num
160 }
161
162 fn itemsize(&self) -> usize;
168
169 fn alignment(&self) -> usize;
175
176 fn byteorder(&self) -> u8 {
184 unsafe { &*_PyDataType_GET_ITEM_DATA(self.as_dtype_ptr()) }
185 .byteorder
186 .max(0) as _
187 }
188
189 fn char(&self) -> u8 {
197 unsafe { &*_PyDataType_GET_ITEM_DATA(self.as_dtype_ptr()) }
198 .type_
199 .max(0) as _
200 }
201
202 fn kind(&self) -> u8 {
210 unsafe { &*_PyDataType_GET_ITEM_DATA(self.as_dtype_ptr()) }
211 .kind
212 .max(0) as _
213 }
214
215 fn flags(&self) -> u64;
221
222 fn ndim(&self) -> usize;
228
229 fn base(&self) -> Bound<'py, PyArrayDescr>;
237
238 fn shape(&self) -> Vec<usize>;
246
247 fn has_object(&self) -> bool {
253 self.flags() & NPY_ITEM_HASOBJECT != 0
254 }
255
256 fn is_aligned_struct(&self) -> bool {
265 self.flags() & NPY_ALIGNED_STRUCT != 0
266 }
267
268 fn has_subarray(&self) -> bool;
272
273 fn has_fields(&self) -> bool;
277
278 fn is_native_byteorder(&self) -> Option<bool> {
280 match self.byteorder() {
282 b'=' => Some(true),
283 b'|' => None,
284 byteorder => Some(byteorder == NPY_BYTEORDER_CHAR::NPY_NATBYTE as u8),
285 }
286 }
287
288 fn names(&self) -> Option<Vec<String>>;
296
297 fn get_field(&self, name: &str) -> PyResult<(Bound<'py, PyArrayDescr>, usize)>;
308}
309
310mod sealed {
311 pub trait Sealed {}
312}
313
314use sealed::Sealed;
315
316impl<'py> PyArrayDescrMethods<'py> for Bound<'py, PyArrayDescr> {
317 fn as_dtype_ptr(&self) -> *mut PyArray_Descr {
318 self.as_ptr() as _
319 }
320
321 fn into_dtype_ptr(self) -> *mut PyArray_Descr {
322 self.into_ptr() as _
323 }
324
325 fn is_equiv_to(&self, other: &Self) -> bool {
326 let self_ptr = self.as_dtype_ptr();
327 let other_ptr = other.as_dtype_ptr();
328
329 unsafe {
330 self_ptr == other_ptr
331 || PY_ARRAY_API.PyArray_EquivTypes(self.py(), self_ptr, other_ptr) != 0
332 }
333 }
334
335 fn typeobj(&self) -> Bound<'py, PyType> {
336 let dtype_type_ptr = unsafe { &*_PyDataType_GET_ITEM_DATA(self.as_dtype_ptr()) }.typeobj;
337 unsafe { PyType::from_borrowed_type_ptr(self.py(), dtype_type_ptr) }
338 }
339
340 fn itemsize(&self) -> usize {
341 unsafe { PyDataType_ELSIZE(self.py(), self.as_dtype_ptr()).max(0) as _ }
342 }
343
344 fn alignment(&self) -> usize {
345 unsafe { PyDataType_ALIGNMENT(self.py(), self.as_dtype_ptr()).max(0) as _ }
346 }
347
348 fn flags(&self) -> u64 {
349 unsafe { PyDataType_FLAGS(self.py(), self.as_dtype_ptr()) as _ }
350 }
351
352 fn ndim(&self) -> usize {
353 let subarray = unsafe { PyDataType_SUBARRAY(self.py(), self.as_dtype_ptr()).as_ref() };
354 match subarray {
355 None => 0,
356 Some(subarray) => unsafe { PyTuple_Size(subarray.shape) }.max(0) as _,
357 }
358 }
359
360 fn base(&self) -> Bound<'py, PyArrayDescr> {
361 let subarray = unsafe { PyDataType_SUBARRAY(self.py(), self.as_dtype_ptr()).as_ref() };
362 match subarray {
363 None => self.clone(),
364 Some(subarray) => unsafe {
365 Bound::from_borrowed_ptr(self.py(), subarray.base.cast()).cast_into_unchecked()
366 },
367 }
368 }
369
370 fn shape(&self) -> Vec<usize> {
371 let subarray = unsafe { PyDataType_SUBARRAY(self.py(), self.as_dtype_ptr()).as_ref() };
372 match subarray {
373 None => Vec::new(),
374 Some(subarray) => {
375 let shape = unsafe { Borrowed::from_ptr(self.py(), subarray.shape) };
377 shape.extract().unwrap()
378 }
379 }
380 }
381
382 fn has_subarray(&self) -> bool {
383 unsafe { !PyDataType_SUBARRAY(self.py(), self.as_dtype_ptr()).is_null() }
384 }
385
386 fn has_fields(&self) -> bool {
387 unsafe { !PyDataType_NAMES(self.py(), self.as_dtype_ptr()).is_null() }
388 }
389
390 fn names(&self) -> Option<Vec<String>> {
391 if !self.has_fields() {
392 return None;
393 }
394 let names = unsafe {
395 Borrowed::from_ptr(self.py(), PyDataType_NAMES(self.py(), self.as_dtype_ptr()))
396 };
397 names.extract().ok()
398 }
399
400 fn get_field(&self, name: &str) -> PyResult<(Bound<'py, PyArrayDescr>, usize)> {
401 if !self.has_fields() {
402 return Err(PyValueError::new_err(
403 "cannot get field information: type descriptor has no fields",
404 ));
405 }
406 let dict = unsafe {
407 Borrowed::from_ptr(self.py(), PyDataType_FIELDS(self.py(), self.as_dtype_ptr()))
408 };
409 let dict = unsafe { dict.cast_unchecked::<PyDict>() };
410 let tuple = dict
412 .get_item(name)?
413 .ok_or_else(|| PyIndexError::new_err(name.to_owned()))?
414 .cast_into::<PyTuple>()
415 .unwrap();
416 let dtype = tuple
418 .get_item(0)
419 .unwrap()
420 .cast_into::<PyArrayDescr>()
421 .unwrap();
422 let offset = tuple.get_item(1).unwrap().extract().unwrap();
423 Ok((dtype, offset))
424 }
425}
426
427impl Sealed for Bound<'_, PyArrayDescr> {}
428
429pub unsafe trait Element: Sized + Send + Sync {
467 const IS_COPY: bool;
475
476 fn get_dtype(py: Python<'_>) -> Bound<'_, PyArrayDescr>;
478
479 fn clone_ref(&self, py: Python<'_>) -> Self;
481
482 #[inline]
487 fn vec_from_slice(py: Python<'_>, slc: &[Self]) -> Vec<Self> {
488 slc.iter().map(|elem| elem.clone_ref(py)).collect()
489 }
490
491 #[inline]
496 fn array_from_view<D>(
497 py: Python<'_>,
498 view: ::ndarray::ArrayView<'_, Self, D>,
499 ) -> ::ndarray::Array<Self, D>
500 where
501 D: ::ndarray::Dimension,
502 {
503 view.map(|elem| elem.clone_ref(py))
504 }
505}
506
507fn npy_int_type_lookup<T, T0, T1, T2>(npy_types: [NPY_TYPES; 3]) -> NPY_TYPES {
508 match size_of::<T>() {
512 x if x == size_of::<T0>() => npy_types[0],
513 x if x == size_of::<T1>() => npy_types[1],
514 x if x == size_of::<T2>() => npy_types[2],
515 _ => panic!("Unable to match integer type descriptor: {npy_types:?}"),
516 }
517}
518
519fn npy_int_type<T: Bounded + Zero + Sized + PartialEq>() -> NPY_TYPES {
520 let is_unsigned = T::min_value() == T::zero();
521 let bit_width = 8 * size_of::<T>();
522
523 match (is_unsigned, bit_width) {
524 (false, 8) => NPY_TYPES::NPY_BYTE,
525 (false, 16) => NPY_TYPES::NPY_SHORT,
526 (false, 32) => npy_int_type_lookup::<i32, c_long, c_int, c_short>([
527 NPY_TYPES::NPY_LONG,
528 NPY_TYPES::NPY_INT,
529 NPY_TYPES::NPY_SHORT,
530 ]),
531 (false, 64) => npy_int_type_lookup::<i64, c_long, c_longlong, c_int>([
532 NPY_TYPES::NPY_LONG,
533 NPY_TYPES::NPY_LONGLONG,
534 NPY_TYPES::NPY_INT,
535 ]),
536 (true, 8) => NPY_TYPES::NPY_UBYTE,
537 (true, 16) => NPY_TYPES::NPY_USHORT,
538 (true, 32) => npy_int_type_lookup::<u32, c_ulong, c_uint, c_ushort>([
539 NPY_TYPES::NPY_ULONG,
540 NPY_TYPES::NPY_UINT,
541 NPY_TYPES::NPY_USHORT,
542 ]),
543 (true, 64) => npy_int_type_lookup::<u64, c_ulong, c_ulonglong, c_uint>([
544 NPY_TYPES::NPY_ULONG,
545 NPY_TYPES::NPY_ULONGLONG,
546 NPY_TYPES::NPY_UINT,
547 ]),
548 _ => unreachable!(),
549 }
550}
551
552macro_rules! clone_methods_impl {
555 ($Self:ty) => {
556 #[inline]
557 fn clone_ref(&self, _py: ::pyo3::Python<'_>) -> $Self {
558 ::std::clone::Clone::clone(self)
559 }
560
561 #[inline]
562 fn vec_from_slice(_py: ::pyo3::Python<'_>, slc: &[$Self]) -> Vec<$Self> {
563 ::std::borrow::ToOwned::to_owned(slc)
564 }
565
566 #[inline]
567 fn array_from_view<D>(
568 _py: ::pyo3::Python<'_>,
569 view: ::ndarray::ArrayView<'_, $Self, D>,
570 ) -> ::ndarray::Array<$Self, D>
571 where
572 D: ::ndarray::Dimension,
573 {
574 ::ndarray::ArrayView::to_owned(&view)
575 }
576 };
577}
578pub(crate) use clone_methods_impl;
579use pyo3::BoundObject;
580
581macro_rules! impl_element_scalar {
582 (@impl: $ty:ty, $npy_type:expr $(,#[$meta:meta])*) => {
583 $(#[$meta])*
584 unsafe impl Element for $ty {
585 const IS_COPY: bool = true;
586
587 fn get_dtype(py: Python<'_>) -> Bound<'_, PyArrayDescr> {
588 PyArrayDescr::from_npy_type(py, $npy_type)
589 }
590
591 clone_methods_impl!($ty);
592 }
593 };
594 ($ty:ty => $npy_type:ident $(,#[$meta:meta])*) => {
595 impl_element_scalar!(@impl: $ty, NPY_TYPES::$npy_type $(,#[$meta])*);
596 };
597 ($($tys:ty),+) => {
598 $(impl_element_scalar!(@impl: $tys, npy_int_type::<$tys>());)+
599 };
600}
601
602impl_element_scalar!(bool => NPY_BOOL);
603
604impl_element_scalar!(i8, i16, i32, i64);
605impl_element_scalar!(u8, u16, u32, u64);
606
607impl_element_scalar!(f32 => NPY_FLOAT);
608impl_element_scalar!(f64 => NPY_DOUBLE);
609
610#[cfg(feature = "half")]
611impl_element_scalar!(f16 => NPY_HALF);
612
613#[cfg(feature = "half")]
614unsafe impl Element for bf16 {
615 const IS_COPY: bool = true;
616
617 fn get_dtype(py: Python<'_>) -> Bound<'_, PyArrayDescr> {
618 static DTYPE: PyOnceLock<Py<PyArrayDescr>> = PyOnceLock::new();
619
620 DTYPE
621 .get_or_init(py, || {
622 PyArrayDescr::new(py, "bfloat16").expect("A package which provides a `bfloat16` data type for NumPy is required to use the `half::bf16` element type.").unbind()
623 })
624 .clone_ref(py)
625 .into_bound(py)
626 }
627
628 clone_methods_impl!(Self);
629}
630
631impl_element_scalar!(Complex32 => NPY_CFLOAT,
632 #[doc = "Complex type with `f32` components which maps to `numpy.csingle` (`numpy.complex64`)."]);
633impl_element_scalar!(Complex64 => NPY_CDOUBLE,
634 #[doc = "Complex type with `f64` components which maps to `numpy.cdouble` (`numpy.complex128`)."]);
635
636#[cfg(any(target_pointer_width = "32", target_pointer_width = "64"))]
637impl_element_scalar!(usize, isize);
638
639unsafe impl Element for Py<PyAny> {
640 const IS_COPY: bool = false;
641
642 fn get_dtype(py: Python<'_>) -> Bound<'_, PyArrayDescr> {
643 PyArrayDescr::object(py)
644 }
645
646 #[inline]
647 fn clone_ref(&self, py: Python<'_>) -> Self {
648 Py::clone_ref(self, py)
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 use pyo3::types::PyString;
657 use pyo3::{py_run, types::PyTypeMethods};
658
659 use crate::npyffi::{NPY_NEEDS_PYAPI, is_numpy_2};
660
661 #[test]
662 fn test_dtype_new() {
663 Python::attach(|py| {
664 assert!(
665 PyArrayDescr::new(py, "float64")
666 .unwrap()
667 .is(dtype::<f64>(py))
668 );
669
670 let dt = PyArrayDescr::new(py, [("a", "O"), ("b", "?")].as_ref()).unwrap();
671 assert_eq!(dt.names(), Some(vec!["a".to_owned(), "b".to_owned()]));
672 assert!(dt.has_object());
673 assert!(dt.get_field("a").unwrap().0.is(dtype::<Py<PyAny>>(py)));
674 assert!(dt.get_field("b").unwrap().0.is(dtype::<bool>(py)));
675
676 assert!(PyArrayDescr::new(py, 123_usize).is_err());
677 });
678 }
679
680 #[test]
681 fn test_dtype_names() {
682 fn type_name<T: Element>(py: Python<'_>) -> Bound<'_, PyString> {
683 dtype::<T>(py).typeobj().qualname().unwrap()
684 }
685 Python::attach(|py| {
686 if is_numpy_2(py) {
687 assert_eq!(type_name::<bool>(py), "bool");
688 } else {
689 assert_eq!(type_name::<bool>(py), "bool_");
690 }
691
692 assert_eq!(type_name::<i8>(py), "int8");
693 assert_eq!(type_name::<i16>(py), "int16");
694 assert_eq!(type_name::<i32>(py), "int32");
695 assert_eq!(type_name::<i64>(py), "int64");
696 assert_eq!(type_name::<u8>(py), "uint8");
697 assert_eq!(type_name::<u16>(py), "uint16");
698 assert_eq!(type_name::<u32>(py), "uint32");
699 assert_eq!(type_name::<u64>(py), "uint64");
700 assert_eq!(type_name::<f32>(py), "float32");
701 assert_eq!(type_name::<f64>(py), "float64");
702
703 assert_eq!(type_name::<Complex32>(py), "complex64");
704 assert_eq!(type_name::<Complex64>(py), "complex128");
705
706 #[cfg(target_pointer_width = "32")]
707 {
708 assert_eq!(type_name::<usize>(py), "uint32");
709 assert_eq!(type_name::<isize>(py), "int32");
710 }
711
712 #[cfg(target_pointer_width = "64")]
713 {
714 assert_eq!(type_name::<usize>(py), "uint64");
715 assert_eq!(type_name::<isize>(py), "int64");
716 }
717 });
718 }
719
720 #[test]
721 fn test_dtype_methods_scalar() {
722 Python::attach(|py| {
723 let dt = dtype::<f64>(py);
724
725 assert_eq!(dt.num(), NPY_TYPES::NPY_DOUBLE as c_int);
726 assert_eq!(dt.flags(), 0);
727 assert_eq!(dt.typeobj().qualname().unwrap(), "float64");
728 assert_eq!(dt.char(), b'd');
729 assert_eq!(dt.kind(), b'f');
730 assert_eq!(dt.byteorder(), b'=');
731 assert_eq!(dt.is_native_byteorder(), Some(true));
732 assert_eq!(dt.itemsize(), 8);
733 assert_eq!(dt.alignment(), 8);
734 assert!(!dt.has_object());
735 assert!(dt.names().is_none());
736 assert!(!dt.has_fields());
737 assert!(!dt.is_aligned_struct());
738 assert!(!dt.has_subarray());
739 assert!(dt.base().is_equiv_to(&dt));
740 assert_eq!(dt.ndim(), 0);
741 assert_eq!(dt.shape(), Vec::<usize>::new());
742 });
743 }
744
745 #[test]
746 fn test_dtype_methods_subarray() {
747 Python::attach(|py| {
748 let locals = PyDict::new(py);
749 py_run!(
750 py,
751 *locals,
752 "dtype = __import__('numpy').dtype(('f8', (2, 3)))"
753 );
754 let dt = locals
755 .get_item("dtype")
756 .unwrap()
757 .unwrap()
758 .cast_into::<PyArrayDescr>()
759 .unwrap();
760
761 assert_eq!(dt.num(), NPY_TYPES::NPY_VOID as c_int);
762 assert_eq!(dt.flags(), 0);
763 assert_eq!(dt.typeobj().qualname().unwrap(), "void");
764 assert_eq!(dt.char(), b'V');
765 assert_eq!(dt.kind(), b'V');
766 assert_eq!(dt.byteorder(), b'|');
767 assert_eq!(dt.is_native_byteorder(), None);
768 assert_eq!(dt.itemsize(), 48);
769 assert_eq!(dt.alignment(), 8);
770 assert!(!dt.has_object());
771 assert!(dt.names().is_none());
772 assert!(!dt.has_fields());
773 assert!(!dt.is_aligned_struct());
774 assert!(dt.has_subarray());
775 assert_eq!(dt.ndim(), 2);
776 assert_eq!(dt.shape(), vec![2, 3]);
777 assert!(dt.base().is_equiv_to(&dtype::<f64>(py)));
778 });
779 }
780
781 #[test]
782 fn test_dtype_methods_record() {
783 Python::attach(|py| {
784 let locals = PyDict::new(py);
785 py_run!(
786 py,
787 *locals,
788 "dtype = __import__('numpy').dtype([('x', 'u1'), ('y', 'f8'), ('z', 'O')], align=True)"
789 );
790 let dt = locals
791 .get_item("dtype")
792 .unwrap()
793 .unwrap()
794 .cast_into::<PyArrayDescr>()
795 .unwrap();
796
797 assert_eq!(dt.num(), NPY_TYPES::NPY_VOID as c_int);
798 assert_ne!(dt.flags() & NPY_ITEM_HASOBJECT, 0);
799 assert_ne!(dt.flags() & NPY_NEEDS_PYAPI, 0);
800 assert_ne!(dt.flags() & NPY_ALIGNED_STRUCT, 0);
801 assert_eq!(dt.typeobj().qualname().unwrap(), "void");
802 assert_eq!(dt.char(), b'V');
803 assert_eq!(dt.kind(), b'V');
804 assert_eq!(dt.byteorder(), b'|');
805 assert_eq!(dt.is_native_byteorder(), None);
806 assert_eq!(dt.itemsize(), 24);
807 assert_eq!(dt.alignment(), 8);
808 assert!(dt.has_object());
809 assert_eq!(
810 dt.names(),
811 Some(vec!["x".to_owned(), "y".to_owned(), "z".to_owned()])
812 );
813 assert!(dt.has_fields());
814 assert!(dt.is_aligned_struct());
815 assert!(!dt.has_subarray());
816 assert_eq!(dt.ndim(), 0);
817 assert_eq!(dt.shape(), Vec::<usize>::new());
818 assert!(dt.base().is_equiv_to(&dt));
819 let x = dt.get_field("x").unwrap();
820 assert!(x.0.is_equiv_to(&dtype::<u8>(py)));
821 assert_eq!(x.1, 0);
822 let y = dt.get_field("y").unwrap();
823 assert!(y.0.is_equiv_to(&dtype::<f64>(py)));
824 assert_eq!(y.1, 8);
825 let z = dt.get_field("z").unwrap();
826 assert!(z.0.is_equiv_to(&dtype::<Py<PyAny>>(py)));
827 assert_eq!(z.1, 16);
828 });
829 }
830}