Skip to main content

trx_rs/
any_trx_file.rs

1use half::f16;
2use std::path::Path;
3
4use crate::dtype::DType;
5use crate::error::{Result, TrxError};
6use crate::header::Header;
7use crate::io::filename::TrxFilename;
8use crate::trx_file::{DataArrayInfo, TrxFile};
9
10/// References to positions data, dispatched by runtime dtype.
11pub enum PositionsRef<'a> {
12    F16(&'a [[f16; 3]]),
13    F32(&'a [[f32; 3]]),
14    F64(&'a [[f64; 3]]),
15}
16
17/// A type-erased TRX container that can hold any position dtype.
18///
19/// Use this when the position dtype is not known at compile time (e.g. CLI tools
20/// that accept arbitrary `.trx` files).
21pub enum AnyTrxFile {
22    F16(TrxFile<f16>),
23    F32(TrxFile<f32>),
24    F64(TrxFile<f64>),
25}
26
27impl AnyTrxFile {
28    /// Load a TRX file, detecting the positions dtype at runtime.
29    pub fn load(path: &Path) -> Result<Self> {
30        let dtype = detect_positions_dtype(path)?;
31        match dtype {
32            DType::Float16 => Ok(AnyTrxFile::F16(TrxFile::<f16>::load(path)?)),
33            DType::Float32 => Ok(AnyTrxFile::F32(TrxFile::<f32>::load(path)?)),
34            DType::Float64 => Ok(AnyTrxFile::F64(TrxFile::<f64>::load(path)?)),
35            other => Err(TrxError::DType(format!(
36                "positions dtype {other} is not a float type"
37            ))),
38        }
39    }
40
41    /// Get a reference to the positions, dispatched by dtype.
42    pub fn positions_ref(&self) -> PositionsRef<'_> {
43        match self {
44            AnyTrxFile::F16(f) => PositionsRef::F16(f.positions()),
45            AnyTrxFile::F32(f) => PositionsRef::F32(f.positions()),
46            AnyTrxFile::F64(f) => PositionsRef::F64(f.positions()),
47        }
48    }
49
50    /// The positions dtype.
51    pub fn dtype(&self) -> DType {
52        match self {
53            AnyTrxFile::F16(_) => DType::Float16,
54            AnyTrxFile::F32(_) => DType::Float32,
55            AnyTrxFile::F64(_) => DType::Float64,
56        }
57    }
58
59    /// The header.
60    pub fn header(&self) -> &Header {
61        match self {
62            AnyTrxFile::F16(f) => f.header(),
63            AnyTrxFile::F32(f) => f.header(),
64            AnyTrxFile::F64(f) => f.header(),
65        }
66    }
67
68    /// Number of streamlines.
69    pub fn nb_streamlines(&self) -> usize {
70        match self {
71            AnyTrxFile::F16(f) => f.nb_streamlines(),
72            AnyTrxFile::F32(f) => f.nb_streamlines(),
73            AnyTrxFile::F64(f) => f.nb_streamlines(),
74        }
75    }
76
77    /// Number of vertices.
78    pub fn nb_vertices(&self) -> usize {
79        match self {
80            AnyTrxFile::F16(f) => f.nb_vertices(),
81            AnyTrxFile::F32(f) => f.nb_vertices(),
82            AnyTrxFile::F64(f) => f.nb_vertices(),
83        }
84    }
85
86    /// Dispatch to a closure with a concrete `&TrxFile<P>`.
87    pub fn with_typed<R>(
88        &self,
89        on_f16: impl FnOnce(&TrxFile<f16>) -> R,
90        on_f32: impl FnOnce(&TrxFile<f32>) -> R,
91        on_f64: impl FnOnce(&TrxFile<f64>) -> R,
92    ) -> R {
93        match self {
94            AnyTrxFile::F16(f) => on_f16(f),
95            AnyTrxFile::F32(f) => on_f32(f),
96            AnyTrxFile::F64(f) => on_f64(f),
97        }
98    }
99
100    pub fn positions_f32(&self) -> Vec<[f32; 3]> {
101        match self.positions_ref() {
102            PositionsRef::F16(data) => data
103                .iter()
104                .map(|point| [point[0].to_f32(), point[1].to_f32(), point[2].to_f32()])
105                .collect(),
106            PositionsRef::F32(data) => data.to_vec(),
107            PositionsRef::F64(data) => data
108                .iter()
109                .map(|point| [point[0] as f32, point[1] as f32, point[2] as f32])
110                .collect(),
111        }
112    }
113
114    pub fn offsets_vec(&self) -> Vec<u32> {
115        self.with_typed(
116            TrxFile::<f16>::offsets_vec,
117            TrxFile::<f32>::offsets_vec,
118            TrxFile::<f64>::offsets_vec,
119        )
120    }
121
122    pub fn dpv_entries(&self) -> Vec<(String, DataArrayInfo)> {
123        self.with_typed(
124            |trx| {
125                trx.iter_dpv()
126                    .map(|(name, info)| (name.to_string(), info))
127                    .collect()
128            },
129            |trx| {
130                trx.iter_dpv()
131                    .map(|(name, info)| (name.to_string(), info))
132                    .collect()
133            },
134            |trx| {
135                trx.iter_dpv()
136                    .map(|(name, info)| (name.to_string(), info))
137                    .collect()
138            },
139        )
140    }
141
142    pub fn dps_entries(&self) -> Vec<(String, DataArrayInfo)> {
143        self.with_typed(
144            |trx| {
145                trx.iter_dps()
146                    .map(|(name, info)| (name.to_string(), info))
147                    .collect()
148            },
149            |trx| {
150                trx.iter_dps()
151                    .map(|(name, info)| (name.to_string(), info))
152                    .collect()
153            },
154            |trx| {
155                trx.iter_dps()
156                    .map(|(name, info)| (name.to_string(), info))
157                    .collect()
158            },
159        )
160    }
161
162    pub fn groups_owned(&self) -> Vec<(String, Vec<u32>)> {
163        self.with_typed(
164            TrxFile::<f16>::group_entries_owned,
165            TrxFile::<f32>::group_entries_owned,
166            TrxFile::<f64>::group_entries_owned,
167        )
168    }
169
170    pub fn dpg_group_entries(&self) -> Vec<(String, Vec<(String, DataArrayInfo)>)> {
171        self.with_typed(
172            collect_dpg_group_entries,
173            collect_dpg_group_entries,
174            collect_dpg_group_entries,
175        )
176    }
177
178    pub fn scalar_dpv_f32(&self, name: &str) -> Result<Vec<f32>> {
179        self.with_typed(
180            |trx| trx.scalar_dpv_f32(name),
181            |trx| trx.scalar_dpv_f32(name),
182            |trx| trx.scalar_dpv_f32(name),
183        )
184    }
185
186    pub fn scalar_dps_f32(&self, name: &str) -> Result<Vec<f32>> {
187        self.with_typed(
188            |trx| trx.scalar_dps_f32(name),
189            |trx| trx.scalar_dps_f32(name),
190            |trx| trx.scalar_dps_f32(name),
191        )
192    }
193
194    /// Return a new `AnyTrxFile` with the header replaced.
195    pub fn with_updated_header(self, header: Header) -> Self {
196        match self {
197            AnyTrxFile::F16(f) => AnyTrxFile::F16(f.with_updated_header(header)),
198            AnyTrxFile::F32(f) => AnyTrxFile::F32(f.with_updated_header(header)),
199            AnyTrxFile::F64(f) => AnyTrxFile::F64(f.with_updated_header(header)),
200        }
201    }
202
203    pub fn save(&self, path: &Path) -> Result<()> {
204        self.with_typed(
205            |trx| trx.save(path),
206            |trx| trx.save(path),
207            |trx| trx.save(path),
208        )
209    }
210
211    pub fn convert_positions_dtype(&self, dtype: DType) -> Result<Self> {
212        match dtype {
213            DType::Float16 => self.with_typed(
214                |trx| Ok(Self::F16(trx.clone_with_positions_dtype::<f16>())),
215                |trx| Ok(Self::F16(trx.clone_with_positions_dtype::<f16>())),
216                |trx| Ok(Self::F16(trx.clone_with_positions_dtype::<f16>())),
217            ),
218            DType::Float32 => self.with_typed(
219                |trx| Ok(Self::F32(trx.clone_with_positions_dtype::<f32>())),
220                |trx| Ok(Self::F32(trx.clone_with_positions_dtype::<f32>())),
221                |trx| Ok(Self::F32(trx.clone_with_positions_dtype::<f32>())),
222            ),
223            DType::Float64 => self.with_typed(
224                |trx| Ok(Self::F64(trx.clone_with_positions_dtype::<f64>())),
225                |trx| Ok(Self::F64(trx.clone_with_positions_dtype::<f64>())),
226                |trx| Ok(Self::F64(trx.clone_with_positions_dtype::<f64>())),
227            ),
228            other => Err(TrxError::DType(format!(
229                "TRX positions must be float16, float32, or float64, got {other}"
230            ))),
231        }
232    }
233}
234
235impl std::fmt::Debug for AnyTrxFile {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        match self {
238            AnyTrxFile::F16(t) => t.fmt(f),
239            AnyTrxFile::F32(t) => t.fmt(f),
240            AnyTrxFile::F64(t) => t.fmt(f),
241        }
242    }
243}
244
245fn collect_dpg_group_entries<P: crate::dtype::TrxScalar>(
246    trx: &TrxFile<P>,
247) -> Vec<(String, Vec<(String, DataArrayInfo)>)> {
248    trx.dpg_group_names()
249        .into_iter()
250        .map(|group| {
251            let entries = trx
252                .dpg_entries(group)
253                .expect("group name came from dpg_group_names()");
254            (group.to_string(), entries)
255        })
256        .collect()
257}
258
259/// Detect the positions dtype from a TRX path (directory or zip).
260pub fn detect_positions_dtype(path: &Path) -> Result<DType> {
261    if path.is_dir() {
262        detect_positions_dtype_dir(path)
263    } else if path.is_file() {
264        detect_positions_dtype_zip(path)
265    } else {
266        Err(TrxError::FileNotFound(path.to_path_buf()))
267    }
268}
269
270fn detect_positions_dtype_dir(dir: &Path) -> Result<DType> {
271    for entry in std::fs::read_dir(dir)? {
272        let entry = entry?;
273        let name = entry.file_name();
274        let name_str = name.to_string_lossy();
275        if name_str.starts_with("positions.") {
276            let parsed = TrxFilename::parse(&name_str)?;
277            return Ok(parsed.dtype);
278        }
279    }
280    Err(TrxError::Format(
281        "no positions file found in directory".into(),
282    ))
283}
284
285fn detect_positions_dtype_zip(path: &Path) -> Result<DType> {
286    let file = std::fs::File::open(path)?;
287    let archive = zip::ZipArchive::new(file)?;
288
289    for i in 0..archive.len() {
290        let name = archive.name_for_index(i).unwrap_or("");
291        let basename = name.rsplit('/').next().unwrap_or(name);
292        if basename.starts_with("positions.") {
293            let parsed = TrxFilename::parse(basename)?;
294            return Ok(parsed.dtype);
295        }
296    }
297    Err(TrxError::Format(
298        "no positions file found in zip archive".into(),
299    ))
300}