1use std::collections::HashMap;
16use std::sync::Arc;
17
18use arrow::array::{
19 Array, FixedSizeListArray, Int32Array, ListArray, MutableArrayData, StructArray, UInt64Array,
20 make_array,
21};
22use arrow::compute::cast;
23use arrow::datatypes::{DataType, Field, Fields};
24use arrow_schema::extension::{EXTENSION_TYPE_METADATA_KEY, EXTENSION_TYPE_NAME_KEY};
25use serde::{Deserialize, Serialize};
26
27use crate::NdArrowArray;
28use crate::column::{NdColumn, NdColumnBuilder};
29use crate::dimensions::Dimensions;
30use crate::error::{Result, nd_err};
31use crate::extension::is_nd_field;
32
33pub const FIXED_SHAPE_TENSOR: &str = "arrow.fixed_shape_tensor";
35pub const VARIABLE_SHAPE_TENSOR: &str = "arrow.variable_shape_tensor";
37
38#[derive(Debug, Default, Serialize, Deserialize)]
39struct TensorMetadata {
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 shape: Option<Vec<usize>>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 dim_names: Option<Vec<String>>,
44 #[serde(
45 default,
46 alias = "permutations",
47 skip_serializing_if = "Option::is_none"
48 )]
49 permutation: Option<Vec<usize>>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 uniform_shape: Option<Vec<Option<i32>>>,
52}
53
54fn tensor_field(
55 name: &str,
56 data_type: DataType,
57 extension: &str,
58 metadata: &TensorMetadata,
59) -> Field {
60 let json = serde_json::to_string(metadata).expect("metadata serializes");
61 Field::new(name, data_type, true).with_metadata(HashMap::from([
62 (EXTENSION_TYPE_NAME_KEY.to_string(), extension.to_string()),
63 (EXTENSION_TYPE_METADATA_KEY.to_string(), json),
64 ]))
65}
66
67fn read_metadata(field: &Field) -> Result<TensorMetadata> {
68 match field.metadata().get(EXTENSION_TYPE_METADATA_KEY) {
69 None => Ok(TensorMetadata::default()),
70 Some(json) if json.trim().is_empty() => Ok(TensorMetadata::default()),
71 Some(json) => serde_json::from_str(json)
72 .map_err(|e| nd_err!(InvalidEncoding, "invalid tensor metadata: {e}")),
73 }
74}
75
76fn shared_dims(column: &NdColumn, sizes: bool) -> Result<Dimensions> {
78 let mut shared: Option<Dimensions> = None;
79 for row in (0..column.len()).filter(|&r| !column.is_null(r)) {
80 let dims = column.dimensions_at(row);
81 match &shared {
82 None => shared = Some(dims),
83 Some(first) if first.names() != dims.names() || (sizes && *first != dims) => {
84 return Err(nd_err!(
85 NotSupported,
86 "tensor types need one set of dimensions, got {first} and {dims}"
87 ));
88 }
89 Some(_) => {}
90 }
91 }
92 Ok(shared.unwrap_or_default())
93}
94
95fn check_no_null_elements(column: &NdColumn) -> Result<()> {
96 if column.values_list().values().null_count() > 0 {
97 return Err(nd_err!(
98 NotSupported,
99 "tensor elements cannot be null; fill the nulls first"
100 ));
101 }
102 Ok(())
103}
104
105fn element_field(value_type: &DataType) -> Arc<Field> {
106 Arc::new(Field::new_list_field(value_type.clone(), false))
107}
108
109fn names_of(dims: &Dimensions) -> Option<Vec<String>> {
110 (dims.rank() > 0).then(|| dims.names().iter().map(|n| n.to_string()).collect())
111}
112
113pub fn to_variable_shape_tensor(column: &NdColumn, name: &str) -> Result<(Field, StructArray)> {
116 check_no_null_elements(column)?;
117 let dims = shared_dims(column, false)?;
118 let rank = dims.rank();
119
120 let mut shapes: Vec<i32> = Vec::with_capacity(column.len() * rank);
121 let mut uniform: Vec<Option<i32>> = Vec::new();
122 for row in 0..column.len() {
123 let row_dims = (!column.is_null(row)).then(|| column.dimensions_at(row));
124 for axis in 0..rank {
125 let size = match &row_dims {
126 Some(d) => i32::try_from(d.as_slice()[axis].size())
127 .map_err(|_| nd_err!(NotSupported, "dimension size exceeds Int32"))?,
128 None => 0,
129 };
130 shapes.push(size);
131 if row_dims.is_some() {
132 match uniform.get_mut(axis) {
133 None => uniform.push(Some(size)),
134 Some(slot) if *slot != Some(size) => *slot = None,
135 Some(_) => {}
136 }
137 }
138 }
139 }
140
141 let values = column.values_list();
142 let data = ListArray::try_new(
143 element_field(column.value_type()),
144 values.offsets().clone(),
145 values.values().clone(),
146 None,
147 )?;
148 let shape = FixedSizeListArray::try_new(
149 Arc::new(Field::new_list_field(DataType::Int32, false)),
150 rank as i32,
151 Arc::new(Int32Array::from(shapes)),
152 None,
153 )?;
154 let fields = Fields::from(vec![
155 Field::new("data", data.data_type().clone(), false),
156 Field::new("shape", shape.data_type().clone(), false),
157 ]);
158 let storage = StructArray::try_new(
159 fields,
160 vec![Arc::new(data), Arc::new(shape)],
161 column.storage().nulls().cloned(),
162 )?;
163 let metadata = TensorMetadata {
164 dim_names: names_of(&dims),
165 uniform_shape: (rank > 0 && uniform.len() == rank).then_some(uniform),
166 ..Default::default()
167 };
168 let field = tensor_field(
169 name,
170 storage.data_type().clone(),
171 VARIABLE_SHAPE_TENSOR,
172 &metadata,
173 );
174 Ok((field, storage))
175}
176
177pub fn to_fixed_shape_tensor(column: &NdColumn, name: &str) -> Result<(Field, FixedSizeListArray)> {
180 check_no_null_elements(column)?;
181 let dims = shared_dims(column, true)?;
182 let size = dims.num_elements();
183
184 let values = column.values_list();
185 let data = values.values().to_data();
186 let offsets = values.value_offsets();
187 let mut out = MutableArrayData::new(vec![&data], true, column.len() * size);
188 for row in 0..column.len() {
189 if column.is_null(row) {
190 out.extend_nulls(size);
191 } else {
192 out.extend(0, offsets[row] as usize, offsets[row + 1] as usize);
193 }
194 }
195 let array = FixedSizeListArray::try_new(
196 element_field(column.value_type()),
197 size as i32,
198 make_array(out.freeze()),
199 column.storage().nulls().cloned(),
200 )?;
201 let metadata = TensorMetadata {
202 shape: Some(dims.shape()),
203 dim_names: names_of(&dims),
204 ..Default::default()
205 };
206 let field = tensor_field(
207 name,
208 array.data_type().clone(),
209 FIXED_SHAPE_TENSOR,
210 &metadata,
211 );
212 Ok((field, array))
213}
214
215fn dimension_names(names: Option<Vec<String>>, rank: usize) -> Result<Vec<String>> {
216 match names {
217 Some(names) if names.len() == rank => Ok(names),
218 Some(names) => Err(nd_err!(
219 InvalidEncoding,
220 "tensor has {} dimension names for rank {rank}",
221 names.len()
222 )),
223 None => Ok((0..rank).map(|i| format!("dim_{i}")).collect()),
224 }
225}
226
227fn from_variable_shape_tensor(storage: &StructArray, metadata: TensorMetadata) -> Result<NdColumn> {
228 let (Some(data), Some(shape)) = (
229 storage.column(0).as_any().downcast_ref::<ListArray>(),
230 storage
231 .column(1)
232 .as_any()
233 .downcast_ref::<FixedSizeListArray>(),
234 ) else {
235 return Err(nd_err!(
236 InvalidEncoding,
237 "variable shape tensor storage must be Struct{{data: List, shape: FixedSizeList}}"
238 ));
239 };
240 let rank = shape.value_length() as usize;
241 let names = dimension_names(metadata.dim_names, rank)?;
242 let sizes = cast(shape.values().as_ref(), &DataType::UInt64)?;
243 let sizes = sizes
244 .as_any()
245 .downcast_ref::<UInt64Array>()
246 .expect("cast to UInt64");
247
248 let value_type = match data.data_type() {
249 DataType::List(item) => item.data_type().clone(),
250 _ => unreachable!(),
251 };
252 let mut builder = NdColumnBuilder::with_value_type(value_type);
253 for row in 0..storage.len() {
254 if storage.is_null(row) {
255 builder.append_null();
256 continue;
257 }
258 let base = shape.value_offset(row) as usize;
259 let dims = Dimensions::try_new(
260 names
261 .iter()
262 .enumerate()
263 .map(|(axis, name)| (name.as_str(), sizes.value(base + axis) as usize)),
264 )?;
265 builder.append(&NdArrowArray::try_new(data.value(row), dims)?)?;
266 }
267 builder.finish()
268}
269
270fn from_fixed_shape_tensor(
271 array: &FixedSizeListArray,
272 metadata: TensorMetadata,
273) -> Result<NdColumn> {
274 let shape = metadata
275 .shape
276 .ok_or_else(|| nd_err!(InvalidEncoding, "fixed shape tensor metadata has no shape"))?;
277 let names = dimension_names(metadata.dim_names, shape.len())?;
278 let dims = Dimensions::try_new(names.iter().map(String::as_str).zip(shape))?;
279 if dims.num_elements() != array.value_length() as usize {
280 return Err(nd_err!(
281 InvalidEncoding,
282 "fixed shape tensor shape {dims} does not match list size {}",
283 array.value_length()
284 ));
285 }
286 let value_type = array.value_type();
287 let mut builder = NdColumnBuilder::with_value_type(value_type);
288 for row in 0..array.len() {
289 if array.is_null(row) {
290 builder.append_null();
291 } else {
292 builder.append(&NdArrowArray::try_new(array.value(row), dims.clone())?)?;
293 }
294 }
295 builder.finish()
296}
297
298pub fn column_from_field(field: &Field, array: &dyn Array) -> Result<NdColumn> {
301 if is_nd_field(field) {
302 return NdColumn::try_from_array(array);
303 }
304 match field.extension_type_name() {
305 Some(VARIABLE_SHAPE_TENSOR) => {
306 let storage = array
307 .as_any()
308 .downcast_ref::<StructArray>()
309 .ok_or_else(|| {
310 nd_err!(
311 InvalidEncoding,
312 "variable shape tensor storage must be a Struct"
313 )
314 })?;
315 from_variable_shape_tensor(storage, read_metadata(field)?)
316 }
317 Some(FIXED_SHAPE_TENSOR) => {
318 let storage = array
319 .as_any()
320 .downcast_ref::<FixedSizeListArray>()
321 .ok_or_else(|| {
322 nd_err!(
323 InvalidEncoding,
324 "fixed shape tensor storage must be a FixedSizeList"
325 )
326 })?;
327 from_fixed_shape_tensor(storage, read_metadata(field)?)
328 }
329 other => Err(nd_err!(
330 InvalidEncoding,
331 "field '{}' has extension type {other:?}, not an nd or tensor type",
332 field.name()
333 )),
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use arrow::array::{AsArray, Float32Array};
340 use arrow::datatypes::Float32Type;
341
342 use super::*;
343
344 fn nd(values: Vec<f32>, dims: &[(&str, usize)]) -> NdArrowArray {
345 NdArrowArray::try_new(
346 Arc::new(Float32Array::from(values)),
347 Dimensions::try_new(dims.iter().copied()).unwrap(),
348 )
349 .unwrap()
350 }
351
352 fn column(arrays: &[Option<NdArrowArray>]) -> NdColumn {
353 let mut builder = NdColumnBuilder::with_value_type(DataType::Float32);
354 for a in arrays {
355 builder.append_option(a.as_ref()).unwrap();
356 }
357 builder.finish().unwrap()
358 }
359
360 #[test]
361 fn variable_shape_round_trip() {
362 let a = nd(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[("y", 2), ("x", 3)]);
363 let b = nd(vec![7.0, 8.0], &[("y", 1), ("x", 2)]);
364 let col = column(&[Some(a.clone()), None, Some(b.clone())]);
365 let (field, storage) = to_variable_shape_tensor(&col, "t").unwrap();
366 assert_eq!(
367 field.metadata()[EXTENSION_TYPE_METADATA_KEY],
368 r#"{"dim_names":["y","x"],"uniform_shape":[null,null]}"#
369 );
370 let back = column_from_field(&field, &storage).unwrap();
371 assert_eq!(back.value(0), a);
372 assert!(back.is_null(1));
373 assert_eq!(back.value(2), b);
374 }
375
376 #[test]
377 fn fixed_shape_round_trip() {
378 let a = nd(vec![1.0, 2.0, 3.0, 4.0], &[("y", 2), ("x", 2)]);
379 let col = column(&[Some(a.clone()), None, Some(a.clone())]);
380 let (field, array) = to_fixed_shape_tensor(&col, "t").unwrap();
381 assert_eq!(array.value_length(), 4);
382 let back = column_from_field(&field, &array).unwrap();
383 assert_eq!(back.value(2), a);
384 assert!(back.is_null(1));
385 assert_eq!(
386 back.value(0)
387 .values()
388 .as_primitive::<Float32Type>()
389 .values(),
390 &[1.0, 2.0, 3.0, 4.0]
391 );
392 }
393
394 #[test]
395 fn reads_metadata_from_other_writers() {
396 let array = FixedSizeListArray::try_new(
397 element_field(&DataType::Float32),
398 2,
399 Arc::new(Float32Array::from(vec![1.0, 2.0])),
400 None,
401 )
402 .unwrap();
403 let field =
404 Field::new("t", array.data_type().clone(), true).with_metadata(HashMap::from([
405 (
406 EXTENSION_TYPE_NAME_KEY.to_string(),
407 FIXED_SHAPE_TENSOR.to_string(),
408 ),
409 (
410 EXTENSION_TYPE_METADATA_KEY.to_string(),
411 r#"{"shape":[2],"permutation":[0]}"#.to_string(),
412 ),
413 ]));
414 let col = column_from_field(&field, &array).unwrap();
415 assert_eq!(col.value(0).dims().names(), vec!["dim_0"]);
416 }
417
418 #[test]
419 fn mismatched_rows_and_null_elements_rejected() {
420 let a = nd(vec![1.0, 2.0], &[("x", 2)]);
421 let b = nd(vec![1.0, 2.0], &[("y", 2)]);
422 assert!(to_variable_shape_tensor(&column(&[Some(a.clone()), Some(b)]), "t").is_err());
423 let c = nd(vec![1.0, 2.0, 3.0], &[("x", 3)]);
424 assert!(to_fixed_shape_tensor(&column(&[Some(a), Some(c)]), "t").is_err());
425 let with_null = NdArrowArray::try_new(
426 Arc::new(Float32Array::from(vec![Some(1.0), None])),
427 Dimensions::try_new([("x", 2)]).unwrap(),
428 )
429 .unwrap();
430 assert!(to_variable_shape_tensor(&column(&[Some(with_null)]), "t").is_err());
431 }
432}