1use bytemuck::{cast_slice, Pod};
2use std::collections::HashMap;
3use std::path::Path;
4
5use crate::dtype::{DType, TrxScalar};
6use crate::error::{Result, TrxError};
7use crate::header::Header;
8use crate::mmap_backing::{vec_to_bytes, MmapBacking};
9use crate::typed_view::TypedView2D;
10
11#[derive(Debug)]
13pub struct DataArray {
14 backing: MmapBacking,
15 ncols: usize,
16 dtype: DType,
17}
18
19impl DataArray {
20 pub fn owned_bytes(backing: Vec<u8>, ncols: usize, dtype: DType) -> Self {
21 Self {
22 backing: MmapBacking::Owned(backing),
23 ncols,
24 dtype,
25 }
26 }
27
28 pub(crate) fn from_backing(backing: MmapBacking, ncols: usize, dtype: DType) -> Self {
29 Self {
30 backing,
31 ncols,
32 dtype,
33 }
34 }
35
36 pub fn clone_owned(&self) -> Self {
37 Self::owned_bytes(self.backing.as_bytes().to_vec(), self.ncols, self.dtype)
38 }
39
40 pub fn ncols(&self) -> usize {
41 self.ncols
42 }
43
44 pub fn dtype(&self) -> DType {
45 self.dtype
46 }
47
48 pub fn len_bytes(&self) -> usize {
49 self.backing.len()
50 }
51
52 pub fn nrows(&self) -> usize {
53 let row_bytes = self.ncols * self.dtype.size_of();
54 self.len_bytes().checked_div(row_bytes).unwrap_or(0)
55 }
56
57 pub fn as_bytes(&self) -> &[u8] {
58 self.backing.as_bytes()
59 }
60
61 pub(crate) fn as_bytes_mut(&mut self) -> Result<&mut [u8]> {
62 self.backing.as_bytes_mut()
63 }
64
65 pub fn cast_slice<T: Pod>(&self) -> &[T] {
66 self.backing.cast_slice()
67 }
68
69 pub(crate) fn cast_slice_mut<T: Pod>(&mut self) -> Result<&mut [T]> {
70 self.backing.cast_slice_mut()
71 }
72
73 pub fn typed_view<T: Pod>(&self) -> TypedView2D<'_, T> {
74 let data: &[T] = cast_slice(self.as_bytes());
75 TypedView2D::new(data, self.ncols)
76 }
77
78 pub fn to_u32_vec(&self) -> Vec<u32> {
80 match self.dtype {
81 DType::UInt32 => self.cast_slice::<u32>().to_vec(),
82 DType::Int32 => self.cast_slice::<i32>().iter().map(|&x| x as u32).collect(),
83 DType::UInt64 => self.cast_slice::<u64>().iter().map(|&x| x as u32).collect(),
84 DType::Int64 => self.cast_slice::<i64>().iter().map(|&x| x as u32).collect(),
85 _ => self.cast_slice::<u32>().to_vec(),
86 }
87 }
88}
89
90pub type DataPerGroup = HashMap<String, HashMap<String, DataArray>>;
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct DataArrayInfo {
94 pub ncols: usize,
95 pub nrows: usize,
96 pub dtype: DType,
97}
98
99pub(crate) struct TrxParts {
100 pub header: Header,
101 pub positions_backing: MmapBacking,
102 pub offsets_backing: MmapBacking,
103 pub dps: HashMap<String, DataArray>,
104 pub dpv: HashMap<String, DataArray>,
105 pub groups: HashMap<String, DataArray>,
106 pub dpg: DataPerGroup,
107 pub tempdir: Option<tempfile::TempDir>,
108}
109
110pub struct TrxFile<P: TrxScalar> {
119 header: Header,
120
121 positions_backing: MmapBacking,
123
124 offsets_backing: MmapBacking,
127
128 dps: HashMap<String, DataArray>,
130
131 dpv: HashMap<String, DataArray>,
133
134 groups: HashMap<String, DataArray>,
136
137 dpg: DataPerGroup,
139
140 _tempdir: Option<tempfile::TempDir>,
142
143 _phantom: std::marker::PhantomData<P>,
144}
145
146impl<P: TrxScalar> TrxFile<P> {
147 pub fn empty(header: Header) -> Self {
149 Self {
150 header,
151 positions_backing: MmapBacking::Owned(Vec::new()),
152 offsets_backing: MmapBacking::Owned(Vec::new()),
153 dps: HashMap::new(),
154 dpv: HashMap::new(),
155 groups: HashMap::new(),
156 dpg: HashMap::new(),
157 _tempdir: None,
158 _phantom: std::marker::PhantomData,
159 }
160 }
161
162 pub(crate) fn from_parts(parts: TrxParts) -> Self {
164 Self {
165 header: parts.header,
166 positions_backing: parts.positions_backing,
167 offsets_backing: parts.offsets_backing,
168 dps: parts.dps,
169 dpv: parts.dpv,
170 groups: parts.groups,
171 dpg: parts.dpg,
172 _tempdir: parts.tempdir,
173 _phantom: std::marker::PhantomData,
174 }
175 }
176
177 pub fn header(&self) -> &Header {
178 &self.header
179 }
180
181 pub fn with_updated_header(self, header: Header) -> Self {
184 Self { header, ..self }
185 }
186
187 pub fn positions(&self) -> &[[P; 3]] {
191 cast_slice(self.positions_backing.as_bytes())
192 }
193
194 pub fn positions_2d(&self) -> TypedView2D<'_, P> {
196 let flat: &[P] = cast_slice(self.positions_backing.as_bytes());
197 TypedView2D::new(flat, 3)
198 }
199
200 pub fn positions_bytes(&self) -> &[u8] {
202 self.positions_backing.as_bytes()
203 }
204
205 pub fn nb_vertices(&self) -> usize {
207 self.positions().len()
208 }
209
210 pub fn offsets(&self) -> &[u32] {
215 self.offsets_backing.cast_slice()
216 }
217
218 pub fn offsets_vec(&self) -> Vec<u32> {
219 self.offsets().to_vec()
220 }
221
222 pub fn nb_streamlines(&self) -> usize {
224 let offsets = self.offsets();
225 if offsets.is_empty() {
226 0
227 } else {
228 offsets.len() - 1
229 }
230 }
231
232 pub fn streamline(&self, i: usize) -> &[[P; 3]] {
236 let offsets = self.offsets();
237 let start = offsets[i] as usize;
238 let end = offsets[i + 1] as usize;
239 &self.positions()[start..end]
240 }
241
242 pub fn streamlines(&self) -> StreamlineIter<'_, P> {
244 StreamlineIter {
245 positions: self.positions(),
246 offsets: self.offsets(),
247 index: 0,
248 }
249 }
250
251 pub fn streamline_lengths(&self) -> Vec<usize> {
253 let offsets = self.offsets();
254 offsets.windows(2).map(|w| (w[1] - w[0]) as usize).collect()
255 }
256
257 pub fn dps<T: Pod>(&self, name: &str) -> Result<TypedView2D<'_, T>> {
261 let arr = self.lookup_dps(name)?;
262 Ok(arr.typed_view())
263 }
264
265 pub fn dpv<T: Pod>(&self, name: &str) -> Result<TypedView2D<'_, T>> {
267 let arr = self.lookup_dpv(name)?;
268 Ok(arr.typed_view())
269 }
270
271 pub fn group(&self, name: &str) -> Result<&[u32]> {
273 let arr = self.lookup_group(name)?;
274 Ok(arr.cast_slice())
275 }
276
277 pub fn dpg<T: Pod>(&self, group: &str, name: &str) -> Result<TypedView2D<'_, T>> {
279 let arr = self.lookup_dpg(group, name)?;
280 Ok(arr.typed_view())
281 }
282
283 pub fn dps_names(&self) -> Vec<&str> {
285 self.dps.keys().map(|s| s.as_str()).collect()
286 }
287
288 pub fn dpv_names(&self) -> Vec<&str> {
290 self.dpv.keys().map(|s| s.as_str()).collect()
291 }
292
293 pub fn group_names(&self) -> Vec<&str> {
295 self.groups.keys().map(|s| s.as_str()).collect()
296 }
297
298 pub fn dpg_group_names(&self) -> Vec<&str> {
300 self.dpg.keys().map(|s| s.as_str()).collect()
301 }
302
303 pub fn iter_dps(&self) -> impl Iterator<Item = (&str, DataArrayInfo)> + '_ {
304 self.dps
305 .iter()
306 .map(|(name, arr)| (name.as_str(), arr.info()))
307 }
308
309 pub fn iter_dpv(&self) -> impl Iterator<Item = (&str, DataArrayInfo)> + '_ {
310 self.dpv
311 .iter()
312 .map(|(name, arr)| (name.as_str(), arr.info()))
313 }
314
315 pub fn iter_groups(&self) -> impl Iterator<Item = (&str, &[u32])> + '_ {
323 self.groups
324 .iter()
325 .map(|(name, arr)| (name.as_str(), arr.cast_slice::<u32>()))
326 }
327
328 pub fn dpg_entries(&self, group: &str) -> Result<Vec<(String, DataArrayInfo)>> {
329 let entries = self
330 .dpg
331 .get(group)
332 .ok_or_else(|| TrxError::Argument(format!("no DPG group named '{group}'")))?;
333 Ok(entries
334 .iter()
335 .map(|(name, arr)| (name.clone(), arr.info()))
336 .collect())
337 }
338
339 pub fn dps_info(&self, name: &str) -> Result<DataArrayInfo> {
340 Ok(self.lookup_dps(name)?.info())
341 }
342
343 pub fn dps_array(&self, name: &str) -> Result<&DataArray> {
344 self.lookup_dps(name)
345 }
346
347 pub fn dpv_info(&self, name: &str) -> Result<DataArrayInfo> {
348 Ok(self.lookup_dpv(name)?.info())
349 }
350
351 pub fn dpv_array(&self, name: &str) -> Result<&DataArray> {
352 self.lookup_dpv(name)
353 }
354
355 pub fn group_info(&self, name: &str) -> Result<DataArrayInfo> {
356 Ok(self.lookup_group(name)?.info())
357 }
358
359 pub fn group_array(&self, name: &str) -> Result<&DataArray> {
360 self.lookup_group(name)
361 }
362
363 pub fn dpg_info(&self, group: &str, name: &str) -> Result<DataArrayInfo> {
364 Ok(self.lookup_dpg(group, name)?.info())
365 }
366
367 pub fn dpg_array(&self, group: &str, name: &str) -> Result<&DataArray> {
368 self.lookup_dpg(group, name)
369 }
370
371 pub fn scalar_dps_f32(&self, name: &str) -> Result<Vec<f32>> {
372 read_scalar_array_as_f32(self.lookup_dps(name)?, "DPS", name)
373 }
374
375 pub fn scalar_dpv_f32(&self, name: &str) -> Result<Vec<f32>> {
376 read_scalar_array_as_f32(self.lookup_dpv(name)?, "DPV", name)
377 }
378
379 pub fn group_entries_owned(&self) -> Vec<(String, Vec<u32>)> {
380 self.groups
381 .iter()
382 .map(|(name, arr)| (name.clone(), arr.to_u32_vec()))
383 .collect()
384 }
385
386 pub fn load(path: &Path) -> Result<Self> {
390 crate::io::load::<P>(path)
391 }
392
393 pub fn save_to_directory(&self, path: &Path) -> Result<()> {
398 crate::io::directory::save_to_directory(self, path)
399 }
400
401 pub fn save_to_zip(&self, path: &Path) -> Result<()> {
405 crate::io::zip::save_to_zip(self, path)
406 }
407
408 pub fn save_to_zip_deflate_groups(&self, path: &Path) -> Result<()> {
411 crate::io::zip::save_to_zip_with(self, path, zip::CompressionMethod::Deflated)
412 }
413
414 pub fn save_to_zip_stored(&self, path: &Path) -> Result<()> {
416 crate::io::zip::save_to_zip(self, path)
417 }
418
419 pub fn save(&self, path: &Path) -> Result<()> {
421 if path.extension().and_then(|e| e.to_str()) == Some("trx") {
422 self.save_to_zip(path)
423 } else {
424 self.save_to_directory(path)
425 }
426 }
427
428 pub fn is_file_backed(&self) -> bool {
429 self._tempdir.is_some() && self.positions_backing.is_mapped()
430 }
431
432 pub(crate) fn dps_arrays(&self) -> &HashMap<String, DataArray> {
433 &self.dps
434 }
435
436 pub(crate) fn dpv_arrays(&self) -> &HashMap<String, DataArray> {
437 &self.dpv
438 }
439
440 pub(crate) fn group_arrays(&self) -> &HashMap<String, DataArray> {
441 &self.groups
442 }
443
444 pub(crate) fn dpg_arrays(&self) -> &DataPerGroup {
445 &self.dpg
446 }
447
448 pub(crate) fn dps_arrays_mut(&mut self) -> &mut HashMap<String, DataArray> {
449 &mut self.dps
450 }
451
452 pub(crate) fn dpv_arrays_mut(&mut self) -> &mut HashMap<String, DataArray> {
453 &mut self.dpv
454 }
455
456 pub(crate) fn group_arrays_mut(&mut self) -> &mut HashMap<String, DataArray> {
457 &mut self.groups
458 }
459
460 pub(crate) fn dpg_arrays_mut(&mut self) -> &mut DataPerGroup {
461 &mut self.dpg
462 }
463
464 pub(crate) fn clone_with_positions_dtype<Q>(&self) -> TrxFile<Q>
465 where
466 Q: TrxScalar + FromF32,
467 {
468 let positions: Vec<[Q; 3]> = self
469 .positions()
470 .iter()
471 .map(|point| {
472 [
473 Q::from_f32(point[0].to_f32()),
474 Q::from_f32(point[1].to_f32()),
475 Q::from_f32(point[2].to_f32()),
476 ]
477 })
478 .collect();
479
480 TrxFile::from_parts(TrxParts {
481 header: self.header.clone(),
482 positions_backing: MmapBacking::Owned(vec_to_bytes(positions)),
483 offsets_backing: MmapBacking::Owned(vec_to_bytes(self.offsets_vec())),
484 dps: clone_data_map(&self.dps),
485 dpv: clone_data_map(&self.dpv),
486 groups: clone_data_map(&self.groups),
487 dpg: clone_dpg_map(&self.dpg),
488 tempdir: None,
489 })
490 }
491
492 fn lookup_dps(&self, name: &str) -> Result<&DataArray> {
493 self.dps
494 .get(name)
495 .ok_or_else(|| TrxError::Argument(format!("no DPS named '{name}'")))
496 }
497
498 fn lookup_dpv(&self, name: &str) -> Result<&DataArray> {
499 self.dpv
500 .get(name)
501 .ok_or_else(|| TrxError::Argument(format!("no DPV named '{name}'")))
502 }
503
504 fn lookup_group(&self, name: &str) -> Result<&DataArray> {
505 self.groups
506 .get(name)
507 .ok_or_else(|| TrxError::Argument(format!("no group named '{name}'")))
508 }
509
510 fn lookup_dpg(&self, group: &str, name: &str) -> Result<&DataArray> {
511 let group_map = self
512 .dpg
513 .get(group)
514 .ok_or_else(|| TrxError::Argument(format!("no DPG group named '{group}'")))?;
515 group_map
516 .get(name)
517 .ok_or_else(|| TrxError::Argument(format!("no DPG named '{name}' in group '{group}'")))
518 }
519}
520
521impl<P: TrxScalar> std::fmt::Debug for TrxFile<P> {
522 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
523 f.debug_struct("TrxFile")
524 .field("dtype", &P::DTYPE)
525 .field("nb_streamlines", &self.nb_streamlines())
526 .field("nb_vertices", &self.nb_vertices())
527 .field("dps", &self.dps_names())
528 .field("dpv", &self.dpv_names())
529 .field("groups", &self.group_names())
530 .field("dpg_group_names", &self.dpg_group_names())
531 .finish()
532 }
533}
534
535pub struct StreamlineIter<'a, P: TrxScalar> {
537 positions: &'a [[P; 3]],
538 offsets: &'a [u32],
539 index: usize,
540}
541
542impl<'a, P: TrxScalar> Iterator for StreamlineIter<'a, P> {
543 type Item = &'a [[P; 3]];
544
545 fn next(&mut self) -> Option<Self::Item> {
546 if self.index + 1 >= self.offsets.len() {
547 return None;
548 }
549 let start = self.offsets[self.index] as usize;
550 let end = self.offsets[self.index + 1] as usize;
551 self.index += 1;
552 Some(&self.positions[start..end])
553 }
554
555 fn size_hint(&self) -> (usize, Option<usize>) {
556 let remaining = if self.offsets.is_empty() {
557 0
558 } else {
559 self.offsets.len() - 1 - self.index
560 };
561 (remaining, Some(remaining))
562 }
563}
564
565impl<'a, P: TrxScalar> ExactSizeIterator for StreamlineIter<'a, P> {}
566
567impl DataArray {
568 pub fn info(&self) -> DataArrayInfo {
569 DataArrayInfo {
570 ncols: self.ncols,
571 nrows: self.nrows(),
572 dtype: self.dtype,
573 }
574 }
575}
576
577fn read_scalar_array_as_f32(arr: &DataArray, kind: &str, name: &str) -> Result<Vec<f32>> {
578 if arr.ncols() != 1 {
579 return Err(TrxError::Argument(format!(
580 "{kind} '{name}' has {} columns; expected a scalar field",
581 arr.ncols()
582 )));
583 }
584
585 let values = match arr.dtype() {
586 DType::Float16 => arr
587 .cast_slice::<half::f16>()
588 .iter()
589 .map(|value| value.to_f32())
590 .collect(),
591 DType::Float32 => arr.cast_slice::<f32>().to_vec(),
592 DType::Float64 => arr
593 .cast_slice::<f64>()
594 .iter()
595 .map(|&value| value as f32)
596 .collect(),
597 DType::Int8 => arr
598 .cast_slice::<i8>()
599 .iter()
600 .map(|&value| value as f32)
601 .collect(),
602 DType::Int16 => arr
603 .cast_slice::<i16>()
604 .iter()
605 .map(|&value| value as f32)
606 .collect(),
607 DType::Int32 => arr
608 .cast_slice::<i32>()
609 .iter()
610 .map(|&value| value as f32)
611 .collect(),
612 DType::UInt8 => arr
613 .cast_slice::<u8>()
614 .iter()
615 .map(|&value| value as f32)
616 .collect(),
617 DType::UInt16 => arr
618 .cast_slice::<u16>()
619 .iter()
620 .map(|&value| value as f32)
621 .collect(),
622 DType::UInt32 => arr
623 .cast_slice::<u32>()
624 .iter()
625 .map(|&value| value as f32)
626 .collect(),
627 other => {
628 return Err(TrxError::DType(format!(
629 "{kind} '{name}' uses unsupported scalar dtype {other}"
630 )))
631 }
632 };
633
634 Ok(values)
635}
636
637fn clone_data_map(map: &HashMap<String, DataArray>) -> HashMap<String, DataArray> {
638 map.iter()
639 .map(|(name, arr)| (name.clone(), arr.clone_owned()))
640 .collect()
641}
642
643fn clone_dpg_map(map: &DataPerGroup) -> DataPerGroup {
644 map.iter()
645 .map(|(group, entries)| (group.clone(), clone_data_map(entries)))
646 .collect()
647}
648
649pub(crate) trait FromF32 {
650 fn from_f32(value: f32) -> Self;
651}
652
653impl FromF32 for half::f16 {
654 fn from_f32(value: f32) -> Self {
655 half::f16::from_f32(value)
656 }
657}
658
659impl FromF32 for f32 {
660 fn from_f32(value: f32) -> Self {
661 value
662 }
663}
664
665impl FromF32 for f64 {
666 fn from_f32(value: f32) -> Self {
667 value as f64
668 }
669}