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