Skip to main content

scirs2_io/tiledb/
array.rs

1//! TileDB array storage engine.
2//!
3//! Implements a tile-based multidimensional array storage system supporting
4//! both dense and sparse arrays with configurable tile sizes, compression,
5//! and range queries.
6
7use std::collections::BTreeMap;
8
9use super::types::{
10    ArraySchema, Compression, DataType, Dimension, TileDBConfig, TileDBError, TileOrder,
11};
12
13/// A TileDB array supporting dense and sparse storage.
14///
15/// Data is organized into tiles for efficient I/O. Dense arrays store all
16/// cells within dimension bounds; sparse arrays store only occupied cells
17/// with explicit coordinates.
18#[derive(Debug)]
19pub struct TileDBArray {
20    /// The array schema.
21    schema: ArraySchema,
22    /// Configuration.
23    config: TileDBConfig,
24    /// Dense storage: flat array in tile order.
25    /// Indexed by linearized cell position.
26    dense_data: Vec<f64>,
27    /// Sparse storage: coordinate -> value mapping.
28    /// Key is a sorted tuple of dimension coordinates serialized as a `Vec<i64>`.
29    sparse_data: BTreeMap<Vec<i64>, f64>,
30    /// Whether this array has been initialized with a write.
31    initialized: bool,
32}
33
34impl TileDBArray {
35    /// Create a new TileDB array from a schema.
36    pub fn create(schema: ArraySchema, config: TileDBConfig) -> Result<Self, TileDBError> {
37        match &schema {
38            ArraySchema::Dense {
39                dimensions,
40                attributes,
41            } => {
42                if dimensions.is_empty() {
43                    return Err(TileDBError::SchemaError(
44                        "dense array must have at least one dimension".to_string(),
45                    ));
46                }
47                if attributes.is_empty() {
48                    return Err(TileDBError::SchemaError(
49                        "array must have at least one attribute".to_string(),
50                    ));
51                }
52                // Pre-allocate dense storage
53                let total_cells: usize = dimensions.iter().map(|d| d.num_cells()).product();
54                let dense_data = vec![f64::NAN; total_cells];
55                Ok(Self {
56                    schema,
57                    config,
58                    dense_data,
59                    sparse_data: BTreeMap::new(),
60                    initialized: false,
61                })
62            }
63            ArraySchema::Sparse {
64                dimensions,
65                attributes,
66            } => {
67                if dimensions.is_empty() {
68                    return Err(TileDBError::SchemaError(
69                        "sparse array must have at least one dimension".to_string(),
70                    ));
71                }
72                if attributes.is_empty() {
73                    return Err(TileDBError::SchemaError(
74                        "array must have at least one attribute".to_string(),
75                    ));
76                }
77                Ok(Self {
78                    schema,
79                    config,
80                    dense_data: Vec::new(),
81                    sparse_data: BTreeMap::new(),
82                    initialized: false,
83                })
84            }
85        }
86    }
87
88    /// Return the schema of this array.
89    pub fn schema(&self) -> &ArraySchema {
90        &self.schema
91    }
92
93    /// Return the config of this array.
94    pub fn config(&self) -> &TileDBConfig {
95        &self.config
96    }
97
98    /// Write data to a dense subarray.
99    ///
100    /// `data` contains the values to write in row-major order.
101    /// `subarray` defines the range for each dimension as (start, end) inclusive.
102    pub fn write_dense(
103        &mut self,
104        data: &[f64],
105        subarray: &[(usize, usize)],
106    ) -> Result<(), TileDBError> {
107        if !self.schema.is_dense() {
108            return Err(TileDBError::SchemaError(
109                "write_dense called on sparse array".to_string(),
110            ));
111        }
112
113        let dims = self.schema.dimensions();
114        if subarray.len() != dims.len() {
115            return Err(TileDBError::InvalidQuery(format!(
116                "subarray has {} ranges but array has {} dimensions",
117                subarray.len(),
118                dims.len()
119            )));
120        }
121
122        // Validate bounds
123        for (i, (start, end)) in subarray.iter().enumerate() {
124            let dim = &dims[i];
125            let dim_start = dim.domain.0 as usize;
126            let dim_end = dim.domain.1 as usize;
127            if *start < dim_start || *end > dim_end || *start > *end {
128                return Err(TileDBError::OutOfBounds(format!(
129                    "subarray range ({start}, {end}) out of bounds for dimension '{}' [{dim_start}, {dim_end}]",
130                    dim.name
131                )));
132            }
133        }
134
135        // Calculate expected number of elements
136        let expected_len: usize = subarray.iter().map(|(s, e)| e - s + 1).product();
137        if data.len() != expected_len {
138            return Err(TileDBError::InvalidQuery(format!(
139                "data length {} does not match subarray size {expected_len}",
140                data.len()
141            )));
142        }
143
144        // Write data into the flat dense array
145        let dim_sizes: Vec<usize> = dims.iter().map(|d| d.num_cells()).collect();
146        let dim_offsets: Vec<usize> = dims.iter().map(|d| d.domain.0 as usize).collect();
147
148        let sub_sizes: Vec<usize> = subarray.iter().map(|(s, e)| e - s + 1).collect();
149
150        for flat_idx in 0..expected_len {
151            // Convert flat index to sub-coordinates
152            let sub_coords = flat_to_nd(flat_idx, &sub_sizes);
153
154            // Convert to global coordinates
155            let global_coords: Vec<usize> = sub_coords
156                .iter()
157                .enumerate()
158                .map(|(i, &c)| subarray[i].0 + c)
159                .collect();
160
161            // Convert global coordinates to storage index
162            let rel_coords: Vec<usize> = global_coords
163                .iter()
164                .enumerate()
165                .map(|(i, &c)| c - dim_offsets[i])
166                .collect();
167
168            let storage_idx = nd_to_flat(&rel_coords, &dim_sizes, &self.config.cell_order);
169
170            if storage_idx < self.dense_data.len() {
171                self.dense_data[storage_idx] = data[flat_idx];
172            }
173        }
174
175        self.initialized = true;
176        Ok(())
177    }
178
179    /// Read data from a dense subarray.
180    ///
181    /// `subarray` defines the range for each dimension as (start, end) inclusive.
182    /// Returns data in row-major order.
183    pub fn read_dense(&self, subarray: &[(usize, usize)]) -> Result<Vec<f64>, TileDBError> {
184        if !self.schema.is_dense() {
185            return Err(TileDBError::SchemaError(
186                "read_dense called on sparse array".to_string(),
187            ));
188        }
189
190        let dims = self.schema.dimensions();
191        if subarray.len() != dims.len() {
192            return Err(TileDBError::InvalidQuery(format!(
193                "subarray has {} ranges but array has {} dimensions",
194                subarray.len(),
195                dims.len()
196            )));
197        }
198
199        // Validate bounds
200        for (i, (start, end)) in subarray.iter().enumerate() {
201            let dim = &dims[i];
202            let dim_start = dim.domain.0 as usize;
203            let dim_end = dim.domain.1 as usize;
204            if *start < dim_start || *end > dim_end || *start > *end {
205                return Err(TileDBError::OutOfBounds(format!(
206                    "subarray range ({start}, {end}) out of bounds for dim '{}' [{dim_start}, {dim_end}]",
207                    dim.name
208                )));
209            }
210        }
211
212        let dim_sizes: Vec<usize> = dims.iter().map(|d| d.num_cells()).collect();
213        let dim_offsets: Vec<usize> = dims.iter().map(|d| d.domain.0 as usize).collect();
214        let sub_sizes: Vec<usize> = subarray.iter().map(|(s, e)| e - s + 1).collect();
215        let total: usize = sub_sizes.iter().product();
216
217        let mut result = Vec::with_capacity(total);
218
219        for flat_idx in 0..total {
220            let sub_coords = flat_to_nd(flat_idx, &sub_sizes);
221            let global_coords: Vec<usize> = sub_coords
222                .iter()
223                .enumerate()
224                .map(|(i, &c)| subarray[i].0 + c)
225                .collect();
226            let rel_coords: Vec<usize> = global_coords
227                .iter()
228                .enumerate()
229                .map(|(i, &c)| c - dim_offsets[i])
230                .collect();
231
232            let storage_idx = nd_to_flat(&rel_coords, &dim_sizes, &self.config.cell_order);
233
234            if storage_idx < self.dense_data.len() {
235                result.push(self.dense_data[storage_idx]);
236            } else {
237                result.push(f64::NAN);
238            }
239        }
240
241        Ok(result)
242    }
243
244    /// Write sparse data to the array.
245    ///
246    /// `coords` is a slice of coordinate vectors (one per dimension).
247    /// `values` contains the attribute values for each coordinate tuple.
248    pub fn write_sparse(&mut self, coords: &[Vec<f64>], values: &[f64]) -> Result<(), TileDBError> {
249        if self.schema.is_dense() {
250            return Err(TileDBError::SchemaError(
251                "write_sparse called on dense array".to_string(),
252            ));
253        }
254
255        let dims = self.schema.dimensions();
256        if coords.len() != dims.len() {
257            return Err(TileDBError::InvalidQuery(format!(
258                "coords has {} dimension vectors but array has {} dimensions",
259                coords.len(),
260                dims.len()
261            )));
262        }
263
264        let num_points = values.len();
265        for (i, coord_vec) in coords.iter().enumerate() {
266            if coord_vec.len() != num_points {
267                return Err(TileDBError::InvalidQuery(format!(
268                    "dimension {} has {} coords but {} values provided",
269                    i,
270                    coord_vec.len(),
271                    num_points
272                )));
273            }
274        }
275
276        // Validate bounds
277        for point_idx in 0..num_points {
278            for (dim_idx, dim) in dims.iter().enumerate() {
279                let c = coords[dim_idx][point_idx];
280                if c < dim.domain.0 || c > dim.domain.1 {
281                    return Err(TileDBError::OutOfBounds(format!(
282                        "coordinate {c} out of bounds for dimension '{}' [{}, {}]",
283                        dim.name, dim.domain.0, dim.domain.1
284                    )));
285                }
286            }
287        }
288
289        // Insert into sparse storage
290        for point_idx in 0..num_points {
291            let key: Vec<i64> = coords.iter().map(|cv| cv[point_idx] as i64).collect();
292            self.sparse_data.insert(key, values[point_idx]);
293        }
294
295        self.initialized = true;
296        Ok(())
297    }
298
299    /// Read sparse data within a range query.
300    ///
301    /// `query_range` defines the (min, max) range for each dimension.
302    /// Returns (coordinates, values) for all cells within the range.
303    pub fn read_sparse(
304        &self,
305        query_range: &[(f64, f64)],
306    ) -> Result<(Vec<Vec<f64>>, Vec<f64>), TileDBError> {
307        if self.schema.is_dense() {
308            return Err(TileDBError::SchemaError(
309                "read_sparse called on dense array".to_string(),
310            ));
311        }
312
313        let dims = self.schema.dimensions();
314        if query_range.len() != dims.len() {
315            return Err(TileDBError::InvalidQuery(format!(
316                "query has {} ranges but array has {} dimensions",
317                query_range.len(),
318                dims.len()
319            )));
320        }
321
322        let num_dims = dims.len();
323        let mut result_coords: Vec<Vec<f64>> = (0..num_dims).map(|_| Vec::new()).collect();
324        let mut result_values: Vec<f64> = Vec::new();
325
326        for (key, &value) in &self.sparse_data {
327            let in_range = key.iter().enumerate().all(|(i, &c)| {
328                let cf = c as f64;
329                cf >= query_range[i].0 && cf <= query_range[i].1
330            });
331
332            if in_range {
333                for (i, &c) in key.iter().enumerate() {
334                    result_coords[i].push(c as f64);
335                }
336                result_values.push(value);
337            }
338        }
339
340        Ok((result_coords, result_values))
341    }
342
343    /// Return the number of non-empty cells in the array.
344    pub fn num_cells(&self) -> usize {
345        if self.schema.is_dense() {
346            self.dense_data.iter().filter(|v| !v.is_nan()).count()
347        } else {
348            self.sparse_data.len()
349        }
350    }
351
352    /// Return the total capacity of the dense array.
353    pub fn total_capacity(&self) -> usize {
354        self.dense_data.len()
355    }
356
357    /// Return the tile information for each dimension.
358    pub fn tile_info(&self) -> Vec<(String, usize, usize)> {
359        self.schema
360            .dimensions()
361            .iter()
362            .map(|d| (d.name.clone(), d.num_cells(), d.num_tiles()))
363            .collect()
364    }
365}
366
367// ─── Helper functions ────────────────────────────────────────────────────────
368
369/// Convert a flat index to N-dimensional coordinates (row-major).
370fn flat_to_nd(flat_idx: usize, sizes: &[usize]) -> Vec<usize> {
371    let ndim = sizes.len();
372    let mut coords = vec![0usize; ndim];
373    let mut remaining = flat_idx;
374
375    for i in (0..ndim).rev() {
376        if sizes[i] > 0 {
377            coords[i] = remaining % sizes[i];
378            remaining /= sizes[i];
379        }
380    }
381
382    coords
383}
384
385/// Convert N-dimensional coordinates to a flat index.
386fn nd_to_flat(coords: &[usize], sizes: &[usize], order: &TileOrder) -> usize {
387    let ndim = coords.len();
388    if ndim == 0 {
389        return 0;
390    }
391
392    match order {
393        TileOrder::RowMajor | TileOrder::Hilbert => {
394            // Row-major: last index varies fastest
395            let mut idx = 0usize;
396            let mut stride = 1usize;
397            for i in (0..ndim).rev() {
398                idx += coords[i] * stride;
399                stride *= sizes[i];
400            }
401            idx
402        }
403        TileOrder::ColMajor => {
404            // Column-major: first index varies fastest
405            let mut idx = 0usize;
406            let mut stride = 1usize;
407            for i in 0..ndim {
408                idx += coords[i] * stride;
409                stride *= sizes[i];
410            }
411            idx
412        }
413    }
414}