Skip to main content

ad_core_rs/
ndarray.rs

1use crate::attributes::NDAttributeList;
2use crate::codec::Codec;
3use crate::error::{ADError, ADResult};
4use crate::timestamp::EpicsTimestamp;
5
6/// Maximum number of NDArray dimensions (C++ `ND_ARRAY_MAX_DIMS`,
7/// NDArray.h:26). The `Dimensions` int32-array param is always posted at this
8/// fixed length, zero-filled beyond the array's actual `ndims`, matching
9/// `doCallbacksInt32Array(dimsPrev_, ND_ARRAY_MAX_DIMS, …)`.
10pub const ND_ARRAY_MAX_DIMS: usize = 10;
11
12/// NDArray data types matching areaDetector NDDataType_t.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[repr(u8)]
15pub enum NDDataType {
16    Int8 = 0,
17    UInt8 = 1,
18    Int16 = 2,
19    UInt16 = 3,
20    Int32 = 4,
21    UInt32 = 5,
22    Int64 = 6,
23    UInt64 = 7,
24    Float32 = 8,
25    Float64 = 9,
26}
27
28impl NDDataType {
29    pub fn element_size(&self) -> usize {
30        match self {
31            Self::Int8 | Self::UInt8 => 1,
32            Self::Int16 | Self::UInt16 => 2,
33            Self::Int32 | Self::UInt32 | Self::Float32 => 4,
34            Self::Int64 | Self::UInt64 | Self::Float64 => 8,
35        }
36    }
37
38    pub fn from_ordinal(v: u8) -> Option<Self> {
39        match v {
40            0 => Some(Self::Int8),
41            1 => Some(Self::UInt8),
42            2 => Some(Self::Int16),
43            3 => Some(Self::UInt16),
44            4 => Some(Self::Int32),
45            5 => Some(Self::UInt32),
46            6 => Some(Self::Int64),
47            7 => Some(Self::UInt64),
48            8 => Some(Self::Float32),
49            9 => Some(Self::Float64),
50            _ => None,
51        }
52    }
53}
54
55/// Typed buffer for NDArray data.
56#[derive(Debug, Clone)]
57pub enum NDDataBuffer {
58    I8(Vec<i8>),
59    U8(Vec<u8>),
60    I16(Vec<i16>),
61    U16(Vec<u16>),
62    I32(Vec<i32>),
63    U32(Vec<u32>),
64    I64(Vec<i64>),
65    U64(Vec<u64>),
66    F32(Vec<f32>),
67    F64(Vec<f64>),
68}
69
70impl NDDataBuffer {
71    pub fn zeros(data_type: NDDataType, count: usize) -> Self {
72        match data_type {
73            NDDataType::Int8 => Self::I8(vec![0; count]),
74            NDDataType::UInt8 => Self::U8(vec![0; count]),
75            NDDataType::Int16 => Self::I16(vec![0; count]),
76            NDDataType::UInt16 => Self::U16(vec![0; count]),
77            NDDataType::Int32 => Self::I32(vec![0; count]),
78            NDDataType::UInt32 => Self::U32(vec![0; count]),
79            NDDataType::Int64 => Self::I64(vec![0; count]),
80            NDDataType::UInt64 => Self::U64(vec![0; count]),
81            NDDataType::Float32 => Self::F32(vec![0.0; count]),
82            NDDataType::Float64 => Self::F64(vec![0.0; count]),
83        }
84    }
85
86    pub fn data_type(&self) -> NDDataType {
87        match self {
88            Self::I8(_) => NDDataType::Int8,
89            Self::U8(_) => NDDataType::UInt8,
90            Self::I16(_) => NDDataType::Int16,
91            Self::U16(_) => NDDataType::UInt16,
92            Self::I32(_) => NDDataType::Int32,
93            Self::U32(_) => NDDataType::UInt32,
94            Self::I64(_) => NDDataType::Int64,
95            Self::U64(_) => NDDataType::UInt64,
96            Self::F32(_) => NDDataType::Float32,
97            Self::F64(_) => NDDataType::Float64,
98        }
99    }
100
101    pub fn len(&self) -> usize {
102        match self {
103            Self::I8(v) => v.len(),
104            Self::U8(v) => v.len(),
105            Self::I16(v) => v.len(),
106            Self::U16(v) => v.len(),
107            Self::I32(v) => v.len(),
108            Self::U32(v) => v.len(),
109            Self::I64(v) => v.len(),
110            Self::U64(v) => v.len(),
111            Self::F32(v) => v.len(),
112            Self::F64(v) => v.len(),
113        }
114    }
115
116    pub fn is_empty(&self) -> bool {
117        self.len() == 0
118    }
119
120    pub fn total_bytes(&self) -> usize {
121        self.len() * self.data_type().element_size()
122    }
123
124    /// Capacity of the underlying Vec in bytes.
125    pub fn capacity_bytes(&self) -> usize {
126        let cap = match self {
127            Self::I8(v) => v.capacity(),
128            Self::U8(v) => v.capacity(),
129            Self::I16(v) => v.capacity(),
130            Self::U16(v) => v.capacity(),
131            Self::I32(v) => v.capacity(),
132            Self::U32(v) => v.capacity(),
133            Self::I64(v) => v.capacity(),
134            Self::U64(v) => v.capacity(),
135            Self::F32(v) => v.capacity(),
136            Self::F64(v) => v.capacity(),
137        };
138        cap * self.data_type().element_size()
139    }
140
141    /// Resize the buffer, zeroing new elements if growing.
142    pub fn resize(&mut self, new_len: usize) {
143        match self {
144            Self::I8(v) => v.resize(new_len, 0),
145            Self::U8(v) => v.resize(new_len, 0),
146            Self::I16(v) => v.resize(new_len, 0),
147            Self::U16(v) => v.resize(new_len, 0),
148            Self::I32(v) => v.resize(new_len, 0),
149            Self::U32(v) => v.resize(new_len, 0),
150            Self::I64(v) => v.resize(new_len, 0),
151            Self::U64(v) => v.resize(new_len, 0),
152            Self::F32(v) => v.resize(new_len, 0.0),
153            Self::F64(v) => v.resize(new_len, 0.0),
154        }
155    }
156
157    /// View the underlying data as a byte slice.
158    pub fn as_u8_slice(&self) -> &[u8] {
159        match self {
160            Self::I8(v) => unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len()) },
161            Self::U8(v) => v.as_slice(),
162            Self::I16(v) => unsafe {
163                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 2)
164            },
165            Self::U16(v) => unsafe {
166                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 2)
167            },
168            Self::I32(v) => unsafe {
169                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 4)
170            },
171            Self::U32(v) => unsafe {
172                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 4)
173            },
174            Self::I64(v) => unsafe {
175                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 8)
176            },
177            Self::U64(v) => unsafe {
178                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 8)
179            },
180            Self::F32(v) => unsafe {
181                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 4)
182            },
183            Self::F64(v) => unsafe {
184                std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 8)
185            },
186        }
187    }
188
189    /// Get element at index as f64.
190    pub fn get_as_f64(&self, index: usize) -> Option<f64> {
191        match self {
192            Self::I8(v) => v.get(index).map(|&x| x as f64),
193            Self::U8(v) => v.get(index).map(|&x| x as f64),
194            Self::I16(v) => v.get(index).map(|&x| x as f64),
195            Self::U16(v) => v.get(index).map(|&x| x as f64),
196            Self::I32(v) => v.get(index).map(|&x| x as f64),
197            Self::U32(v) => v.get(index).map(|&x| x as f64),
198            Self::I64(v) => v.get(index).map(|&x| x as f64),
199            Self::U64(v) => v.get(index).map(|&x| x as f64),
200            Self::F32(v) => v.get(index).map(|&x| x as f64),
201            Self::F64(v) => v.get(index).copied(),
202        }
203    }
204
205    /// Set element at index from f64 value.
206    pub fn set_from_f64(&mut self, index: usize, value: f64) {
207        match self {
208            Self::I8(v) => {
209                if let Some(e) = v.get_mut(index) {
210                    *e = value as i8;
211                }
212            }
213            Self::U8(v) => {
214                if let Some(e) = v.get_mut(index) {
215                    *e = value as u8;
216                }
217            }
218            Self::I16(v) => {
219                if let Some(e) = v.get_mut(index) {
220                    *e = value as i16;
221                }
222            }
223            Self::U16(v) => {
224                if let Some(e) = v.get_mut(index) {
225                    *e = value as u16;
226                }
227            }
228            Self::I32(v) => {
229                if let Some(e) = v.get_mut(index) {
230                    *e = value as i32;
231                }
232            }
233            Self::U32(v) => {
234                if let Some(e) = v.get_mut(index) {
235                    *e = value as u32;
236                }
237            }
238            Self::I64(v) => {
239                if let Some(e) = v.get_mut(index) {
240                    *e = value as i64;
241                }
242            }
243            Self::U64(v) => {
244                if let Some(e) = v.get_mut(index) {
245                    *e = value as u64;
246                }
247            }
248            Self::F32(v) => {
249                if let Some(e) = v.get_mut(index) {
250                    *e = value as f32;
251                }
252            }
253            Self::F64(v) => {
254                if let Some(e) = v.get_mut(index) {
255                    *e = value;
256                }
257            }
258        }
259    }
260}
261
262/// A single dimension of an NDArray.
263#[derive(Debug, Clone)]
264pub struct NDDimension {
265    pub size: usize,
266    pub offset: usize,
267    pub binning: usize,
268    pub reverse: bool,
269}
270
271impl NDDimension {
272    pub fn new(size: usize) -> Self {
273        Self {
274            size,
275            offset: 0,
276            binning: 1,
277            reverse: false,
278        }
279    }
280}
281
282/// Computed info about an NDArray's layout (matching C++ NDArrayInfo_t).
283#[derive(Debug, Clone)]
284pub struct NDArrayInfo {
285    pub total_bytes: usize,
286    pub bytes_per_element: usize,
287    pub num_elements: usize,
288    pub x_size: usize,
289    pub y_size: usize,
290    pub color_size: usize,
291    /// Which dimension index is X.
292    pub x_dim: usize,
293    /// Which dimension index is Y.
294    pub y_dim: usize,
295    /// Which dimension index is color (0 if mono).
296    pub color_dim: usize,
297    /// Elements between successive X values.
298    pub x_stride: usize,
299    /// Elements between successive Y values.
300    pub y_stride: usize,
301    /// Elements between successive color values.
302    pub color_stride: usize,
303    /// Resolved color mode.
304    pub color_mode: crate::color::NDColorMode,
305}
306
307impl NDArrayInfo {
308    /// C++ `userDims = {xDim, yDim, colorDim}` (`NDPluginROI.cpp:80-82`,
309    /// `NDPluginTransform.cpp:492-494`): the map from a *logical* image axis
310    /// (`0` = X, `1` = Y, `2` = color) to the *physical* [`NDArray::dims`] slot
311    /// that carries it.
312    ///
313    /// Any plugin whose user-facing Dim0/Dim1/Dim2 mean X/Y/color must reach
314    /// `dims` through this map and never with the logical index itself: the two
315    /// only coincide for Mono/Bayer/YUV and RGB3 layouts. For RGB1 the array is
316    /// `[color, x, y]`, so the X axis lives in `dims[1]` — indexing `dims[0]`
317    /// for "Dim0" hands back the color axis.
318    pub fn user_dims(&self) -> [usize; 3] {
319        [self.x_dim, self.y_dim, self.color_dim]
320    }
321}
322
323/// N-dimensional array with typed data buffer.
324#[derive(Debug, Clone)]
325pub struct NDArray {
326    pub unique_id: i32,
327    pub timestamp: EpicsTimestamp,
328    /// Separate double-precision timestamp (C++ `double timeStamp`), independent of `epicsTS`.
329    pub time_stamp: f64,
330    pub dims: Vec<NDDimension>,
331    pub data: NDDataBuffer,
332    pub attributes: NDAttributeList,
333    pub codec: Option<Codec>,
334    /// Identity of the pool that allocated this array (C++ `pNDArrayPool`).
335    /// `0` means the array was not allocated through any pool. `NDArrayPool::release`
336    /// verifies this matches its own id before returning the buffer to the free list.
337    pub pool_id: u64,
338    /// Requested byte count at allocation time (C++ `dataSize`). This is the exact
339    /// `num_elements * element_size` requested, NOT the allocator-rounded Vec capacity.
340    /// Pool memory accounting adds/subtracts this exact value.
341    pub data_size: usize,
342}
343
344impl NDArray {
345    /// Create a new NDArray with zeroed buffer matching dimensions.
346    pub fn new(dims: Vec<NDDimension>, data_type: NDDataType) -> Self {
347        let num_elements: usize = if dims.is_empty() {
348            0
349        } else {
350            dims.iter().map(|d| d.size).product()
351        };
352        Self {
353            unique_id: 0,
354            timestamp: EpicsTimestamp::default(),
355            time_stamp: 0.0,
356            dims,
357            data: NDDataBuffer::zeros(data_type, num_elements),
358            attributes: NDAttributeList::new(),
359            codec: None,
360            pool_id: 0,
361            data_size: num_elements * data_type.element_size(),
362        }
363    }
364
365    /// Create an NDArray wrapping an already-built data buffer.
366    ///
367    /// The array is not pool-allocated (`pool_id == 0`); `data_size` is taken
368    /// from the buffer's element count. Use this when a producer fills its own
369    /// buffer instead of allocating through an [`crate::ndarray_pool::NDArrayPool`].
370    pub fn with_data(dims: Vec<NDDimension>, data: NDDataBuffer) -> Self {
371        let data_size = data.len() * data.data_type().element_size();
372        Self {
373            unique_id: 0,
374            timestamp: EpicsTimestamp::default(),
375            time_stamp: 0.0,
376            dims,
377            data,
378            attributes: NDAttributeList::new(),
379            codec: None,
380            pool_id: 0,
381            data_size,
382        }
383    }
384
385    /// Stamp both timestamps from one time source.
386    ///
387    /// The single owner of the timestamp pair, mirroring C++
388    /// `asynNDArrayDriver::updateTimeStamps` (asynNDArrayDriver.cpp:832-836):
389    ///
390    /// ```text
391    /// updateTimeStamp(&pArray->epicsTS);
392    /// pArray->timeStamp = pArray->epicsTS.secPastEpoch + pArray->epicsTS.nsec/1.e9;
393    /// ```
394    ///
395    /// `timeStamp` is *derived* from `epicsTS`, so the two can never disagree.
396    /// Nothing else may write one without the other — `NDArrayPool::alloc` sets
397    /// neither, exactly as C does.
398    pub fn update_time_stamps(&mut self, epics_ts: EpicsTimestamp) {
399        self.timestamp = epics_ts;
400        self.time_stamp = epics_ts.as_f64();
401    }
402
403    /// Stamp both timestamps from the current wall clock.
404    ///
405    /// Convenience form of [`NDArray::update_time_stamps`] for drivers with no
406    /// custom time source (C++ `updateTimeStamp` falls back to
407    /// `epicsTimeGetCurrent` when no timestamp source is registered).
408    pub fn update_time_stamps_now(&mut self) {
409        self.update_time_stamps(EpicsTimestamp::now());
410    }
411
412    /// Compute layout info for this array (matching C++ NDArray::getInfo).
413    ///
414    /// For 3D arrays, reads the `ColorMode` attribute to determine which
415    /// dimension is X, Y, and color (RGB1, RGB2, or RGB3 layout).
416    pub fn info(&self) -> NDArrayInfo {
417        use crate::color::NDColorMode;
418
419        let bytes_per_element = self.data.data_type().element_size();
420        let num_elements = self.data.len();
421        let total_bytes = num_elements * bytes_per_element;
422
423        let ndims = self.dims.len();
424
425        // Read ColorMode attribute if present (C++ does this for 3D arrays)
426        let color_mode = self
427            .attributes
428            .get("ColorMode")
429            .and_then(|a| a.value.as_i64())
430            .map(|v| NDColorMode::from_i32(v as i32))
431            .unwrap_or(NDColorMode::Mono);
432
433        let (x_size, y_size, color_size, x_dim, y_dim, color_dim, x_stride, y_stride, color_stride) =
434            match ndims {
435                // C++ getInfo: ySize/colorSize/strides stay 0-initialized for <2-D.
436                // The `colorSize == 0` idiom signals "no color dimension"; keeping it
437                // at 0 (not 1) preserves C parity for that check.
438                0 => (0, 0, 0, 0, 0, 0, 0, 0, 0),
439                1 => (self.dims[0].size, 0, 0, 0, 0, 0, 1, 0, 0),
440                2 => {
441                    let xs = self.dims[0].size;
442                    let ys = self.dims[1].size;
443                    // C++ getInfo for 2-D: yStride = xSize, colorSize/colorStride = 0.
444                    (xs, ys, 0, 0, 1, 0, 1, xs, 0)
445                }
446                3 => {
447                    // 3D: layout depends on ColorMode
448                    match color_mode {
449                        NDColorMode::RGB1 => {
450                            // dim[0]=color, dim[1]=X, dim[2]=Y
451                            let cs = self.dims[0].size;
452                            let xs = self.dims[1].size;
453                            let ys = self.dims[2].size;
454                            (xs, ys, cs, 1, 2, 0, cs, xs * cs, 1)
455                        }
456                        NDColorMode::RGB2 => {
457                            // dim[0]=X, dim[1]=color, dim[2]=Y
458                            let xs = self.dims[0].size;
459                            let cs = self.dims[1].size;
460                            let ys = self.dims[2].size;
461                            (xs, ys, cs, 0, 2, 1, 1, xs * cs, xs)
462                        }
463                        NDColorMode::RGB3 => {
464                            // dim[0]=X, dim[1]=Y, dim[2]=color
465                            let xs = self.dims[0].size;
466                            let ys = self.dims[1].size;
467                            let cs = self.dims[2].size;
468                            (xs, ys, cs, 0, 1, 2, 1, xs, xs * ys)
469                        }
470                        _ => {
471                            // Mono / Bayer / YUV444 / YUV422 / YUV411: treated
472                            // as a plain 3-D array (dim[0]=X, dim[1]=Y,
473                            // dim[2]=Z). G4: C++ NDArray::getInfo has the SAME
474                            // limitation — it only special-cases RGB1/RGB2/RGB3
475                            // and falls through to this generic 3-D layout for
476                            // YUV modes. This is a shared C-parity gap, not a
477                            // Rust regression; YUV layout-awareness would have
478                            // to be added to both implementations together.
479                            let xs = self.dims[0].size;
480                            let ys = self.dims[1].size;
481                            let cs = self.dims[2].size;
482                            (xs, ys, cs, 0, 1, 2, 1, xs, xs * ys)
483                        }
484                    }
485                }
486                // C++ getInfo gates the color-dimension block on
487                // `ndims == 3` exactly. For 4-D and higher arrays it leaves
488                // colorSize/colorDim/colorStride at 0 and only fills xDim/yDim
489                // from the first two dimensions — same as the 2-D case.
490                _ => {
491                    let xs = self.dims[0].size;
492                    let ys = self.dims[1].size;
493                    (xs, ys, 0, 0, 1, 0, 1, xs, 0)
494                }
495            };
496
497        NDArrayInfo {
498            total_bytes,
499            bytes_per_element,
500            num_elements,
501            x_size,
502            y_size,
503            color_size,
504            x_dim,
505            y_dim,
506            color_dim,
507            x_stride,
508            y_stride,
509            color_stride,
510            color_mode,
511        }
512    }
513
514    /// Produce a diagnostic text dump (matching C++ `NDArray::report`).
515    ///
516    /// `details > 5` additionally lists attributes.
517    pub fn report(&self, details: i32) -> String {
518        let mut out = String::new();
519        out.push('\n');
520        out.push_str("NDArray:\n");
521        let dim_sizes: Vec<String> = self.dims.iter().map(|d| d.size.to_string()).collect();
522        out.push_str(&format!(
523            "  ndims={} dims=[{}]\n",
524            self.dims.len(),
525            dim_sizes.join(" ")
526        ));
527        out.push_str(&format!(
528            "  dataType={:?}, dataSize={}, numElements={}\n",
529            self.data.data_type(),
530            self.data_size,
531            self.data.len()
532        ));
533        out.push_str(&format!(
534            "  uniqueId={}, timeStamp={}, epicsTS.secPastEpoch={}, epicsTS.nsec={}\n",
535            self.unique_id, self.time_stamp, self.timestamp.sec, self.timestamp.nsec
536        ));
537        out.push_str(&format!("  poolId={}\n", self.pool_id));
538        match &self.codec {
539            Some(c) => out.push_str(&format!(
540                "  codec={:?}, compressedSize={}\n",
541                c.name, c.compressed_size
542            )),
543            None => out.push_str("  codec=none\n"),
544        }
545        out.push_str(&format!(
546            "  number of attributes={}\n",
547            self.attributes.len()
548        ));
549        if details > 5 {
550            for attr in self.attributes.iter() {
551                out.push_str(&format!(
552                    "    attribute name={}, value={}, source={:?}\n",
553                    attr.name,
554                    attr.value.as_string(),
555                    attr.source
556                ));
557            }
558        }
559        out
560    }
561
562    /// Validate that buffer length matches dimension product.
563    pub fn validate(&self) -> ADResult<()> {
564        let expected: usize = if self.dims.is_empty() {
565            0
566        } else {
567            self.dims.iter().map(|d| d.size).product()
568        };
569        if self.data.len() != expected {
570            return Err(ADError::BufferSizeMismatch {
571                expected,
572                actual: self.data.len(),
573            });
574        }
575        Ok(())
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582
583    #[test]
584    fn test_element_size_all_types() {
585        assert_eq!(NDDataType::Int8.element_size(), 1);
586        assert_eq!(NDDataType::UInt8.element_size(), 1);
587        assert_eq!(NDDataType::Int16.element_size(), 2);
588        assert_eq!(NDDataType::UInt16.element_size(), 2);
589        assert_eq!(NDDataType::Int32.element_size(), 4);
590        assert_eq!(NDDataType::UInt32.element_size(), 4);
591        assert_eq!(NDDataType::Int64.element_size(), 8);
592        assert_eq!(NDDataType::UInt64.element_size(), 8);
593        assert_eq!(NDDataType::Float32.element_size(), 4);
594        assert_eq!(NDDataType::Float64.element_size(), 8);
595    }
596
597    #[test]
598    fn test_from_ordinal_roundtrip() {
599        for i in 0..10u8 {
600            let dt = NDDataType::from_ordinal(i).unwrap();
601            assert_eq!(dt as u8, i);
602        }
603        assert!(NDDataType::from_ordinal(10).is_none());
604    }
605
606    #[test]
607    fn test_buffer_zeros_type_and_len() {
608        let buf = NDDataBuffer::zeros(NDDataType::UInt16, 100);
609        assert_eq!(buf.data_type(), NDDataType::UInt16);
610        assert_eq!(buf.len(), 100);
611        assert_eq!(buf.total_bytes(), 200);
612    }
613
614    #[test]
615    fn test_buffer_zeros_all_types() {
616        for i in 0..10u8 {
617            let dt = NDDataType::from_ordinal(i).unwrap();
618            let buf = NDDataBuffer::zeros(dt, 10);
619            assert_eq!(buf.data_type(), dt);
620            assert_eq!(buf.len(), 10);
621            assert_eq!(buf.total_bytes(), 10 * dt.element_size());
622        }
623    }
624
625    #[test]
626    fn test_buffer_as_u8_slice() {
627        let buf = NDDataBuffer::U8(vec![1, 2, 3]);
628        assert_eq!(buf.as_u8_slice(), &[1, 2, 3]);
629    }
630
631    #[test]
632    fn test_ndarray_new_allocates() {
633        let dims = vec![NDDimension::new(256), NDDimension::new(256)];
634        let arr = NDArray::new(dims, NDDataType::UInt8);
635        assert_eq!(arr.data.len(), 256 * 256);
636        assert_eq!(arr.data.data_type(), NDDataType::UInt8);
637    }
638
639    #[test]
640    fn test_ndarray_validate_ok() {
641        let dims = vec![NDDimension::new(10), NDDimension::new(20)];
642        let arr = NDArray::new(dims, NDDataType::Float64);
643        arr.validate().unwrap();
644    }
645
646    #[test]
647    fn test_ndarray_validate_mismatch() {
648        let mut arr = NDArray::new(
649            vec![NDDimension::new(10), NDDimension::new(20)],
650            NDDataType::UInt8,
651        );
652        arr.data = NDDataBuffer::U8(vec![0; 100]);
653        assert!(arr.validate().is_err());
654    }
655
656    #[test]
657    fn test_ndarray_info_2d_mono() {
658        let dims = vec![NDDimension::new(640), NDDimension::new(480)];
659        let arr = NDArray::new(dims, NDDataType::UInt16);
660        let info = arr.info();
661        assert_eq!(info.x_size, 640);
662        assert_eq!(info.y_size, 480);
663        // C parity: 2-D arrays have colorSize == 0 (no color dimension).
664        assert_eq!(info.color_size, 0);
665        assert_eq!(info.num_elements, 640 * 480);
666        assert_eq!(info.bytes_per_element, 2);
667        assert_eq!(info.total_bytes, 640 * 480 * 2);
668    }
669
670    #[test]
671    fn test_ndarray_info_3d_rgb() {
672        use crate::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
673        use crate::color::NDColorMode;
674
675        // Without ColorMode attribute: defaults to Mono (x=dim0, y=dim1, color=dim2)
676        let dims = vec![
677            NDDimension::new(3),
678            NDDimension::new(640),
679            NDDimension::new(480),
680        ];
681        let arr = NDArray::new(dims, NDDataType::UInt8);
682        let info = arr.info();
683        assert_eq!(info.x_size, 3);
684        assert_eq!(info.y_size, 640);
685        assert_eq!(info.color_size, 480);
686
687        // With ColorMode=RGB1: dim[0]=color, dim[1]=x, dim[2]=y
688        let dims = vec![
689            NDDimension::new(3),
690            NDDimension::new(640),
691            NDDimension::new(480),
692        ];
693        let mut arr = NDArray::new(dims, NDDataType::UInt8);
694        arr.attributes.add(NDAttribute {
695            name: "ColorMode".into(),
696            description: "Color Mode".into(),
697            source: NDAttrSource::Driver,
698            value: NDAttrValue::Int32(NDColorMode::RGB1 as i32),
699            source_impl: None,
700        });
701        let info = arr.info();
702        assert_eq!(info.color_size, 3);
703        assert_eq!(info.x_size, 640);
704        assert_eq!(info.y_size, 480);
705        assert_eq!(info.x_dim, 1);
706        assert_eq!(info.y_dim, 2);
707        assert_eq!(info.color_dim, 0);
708        assert_eq!(info.num_elements, 3 * 640 * 480);
709    }
710
711    #[test]
712    fn test_ndarray_info_1d() {
713        let dims = vec![NDDimension::new(1024)];
714        let arr = NDArray::new(dims, NDDataType::Float64);
715        let info = arr.info();
716        assert_eq!(info.x_size, 1024);
717        // C parity: 1-D arrays leave ySize / colorSize at 0.
718        assert_eq!(info.y_size, 0);
719        assert_eq!(info.color_size, 0);
720    }
721
722    #[test]
723    fn test_ndarray_info_4d_not_color() {
724        // C++ getInfo gates the color block on `ndims == 3` exactly.
725        // A 4-D array must leave color_size / color_dim / color_stride at 0
726        // and only fill x/y from the first two dimensions.
727        let dims = vec![
728            NDDimension::new(8),
729            NDDimension::new(640),
730            NDDimension::new(480),
731            NDDimension::new(5),
732        ];
733        let arr = NDArray::new(dims, NDDataType::UInt8);
734        let info = arr.info();
735        assert_eq!(info.x_size, 8);
736        assert_eq!(info.y_size, 640);
737        assert_eq!(info.color_size, 0, "4-D array must not get a color size");
738        assert_eq!(info.x_dim, 0);
739        assert_eq!(info.y_dim, 1);
740        assert_eq!(info.color_dim, 0);
741        assert_eq!(info.x_stride, 1);
742        assert_eq!(info.y_stride, 8);
743        assert_eq!(info.color_stride, 0);
744        assert_eq!(info.num_elements, 8 * 640 * 480 * 5);
745    }
746
747    #[test]
748    fn test_buffer_is_empty() {
749        let buf = NDDataBuffer::zeros(NDDataType::UInt8, 0);
750        assert!(buf.is_empty());
751        let buf2 = NDDataBuffer::zeros(NDDataType::UInt8, 1);
752        assert!(!buf2.is_empty());
753    }
754
755    #[test]
756    fn test_codec_field_preserved() {
757        let mut arr = NDArray::new(vec![NDDimension::new(10)], NDDataType::UInt8);
758        arr.codec = Some(Codec {
759            name: crate::codec::CodecName::JPEG,
760            compressed_size: 42,
761            level: 0,
762            shuffle: 0,
763            compressor: 0,
764            original_data_type: NDDataType::UInt8,
765        });
766        let cloned = arr.clone();
767        assert_eq!(cloned.codec.as_ref().unwrap().compressed_size, 42);
768    }
769}