1use half::f16;
2use std::collections::HashMap;
3
4use crate::any_trx_file::{AnyTrxFile, PositionsRef};
5use crate::dtype::{DType, TrxScalar};
6use crate::error::{Result, TrxError};
7use crate::header::Header;
8use crate::mmap_backing::{vec_to_bytes, MmapBacking};
9use crate::trx_file::{DataArray, DataPerGroup, TrxFile, TrxParts};
10
11#[derive(Debug)]
19pub struct Tractogram {
20 header: Header,
21 positions: Vec<[f32; 3]>,
22 offsets: Vec<u32>,
23 dps: HashMap<String, DataArray>,
24 dpv: HashMap<String, DataArray>,
25 groups: HashMap<String, Vec<u32>>,
26 dpg: DataPerGroup,
27}
28
29impl Clone for Tractogram {
30 fn clone(&self) -> Self {
31 Self {
32 header: self.header.clone(),
33 positions: self.positions.clone(),
34 offsets: self.offsets.clone(),
35 dps: clone_arrays(&self.dps),
36 dpv: clone_arrays(&self.dpv),
37 groups: self.groups.clone(),
38 dpg: clone_dpg(&self.dpg),
39 }
40 }
41}
42
43impl Tractogram {
44 pub fn new() -> Self {
46 Self::with_header(Header {
47 voxel_to_rasmm: Header::identity_affine(),
48 dimensions: [1, 1, 1],
49 nb_streamlines: 0,
50 nb_vertices: 0,
51 extra: Default::default(),
52 })
53 }
54
55 pub fn with_header(mut header: Header) -> Self {
57 header.nb_streamlines = 0;
58 header.nb_vertices = 0;
59 Self {
60 header,
61 positions: Vec::new(),
62 offsets: vec![0],
63 dps: HashMap::new(),
64 dpv: HashMap::new(),
65 groups: HashMap::new(),
66 dpg: HashMap::new(),
67 }
68 }
69
70 pub fn from_positions_and_offsets(
72 positions: Vec<[f32; 3]>,
73 offsets: Vec<u32>,
74 mut header: Header,
75 ) -> Self {
76 header.nb_streamlines = offsets.len().saturating_sub(1) as u64;
77 header.nb_vertices = positions.len() as u64;
78 Self {
79 header,
80 positions,
81 offsets,
82 dps: HashMap::new(),
83 dpv: HashMap::new(),
84 groups: HashMap::new(),
85 dpg: HashMap::new(),
86 }
87 }
88
89 pub fn from_trx<P: TrxScalar>(trx: &TrxFile<P>) -> Self {
91 let positions = trx
92 .positions()
93 .iter()
94 .map(|point| [point[0].to_f32(), point[1].to_f32(), point[2].to_f32()])
95 .collect();
96 Self {
97 header: trx.header().clone(),
98 positions,
99 offsets: trx.offsets().to_vec(),
100 dps: clone_arrays(trx.dps_arrays()),
101 dpv: clone_arrays(trx.dpv_arrays()),
102 groups: clone_groups(trx.group_arrays()),
103 dpg: clone_dpg(trx.dpg_arrays()),
104 }
105 }
106
107 pub fn from_any_trx(trx: &AnyTrxFile) -> Self {
109 let positions = match trx.positions_ref() {
110 PositionsRef::F16(data) => data
111 .iter()
112 .map(|point| [point[0].to_f32(), point[1].to_f32(), point[2].to_f32()])
113 .collect(),
114 PositionsRef::F32(data) => data.to_vec(),
115 PositionsRef::F64(data) => data
116 .iter()
117 .map(|point| [point[0] as f32, point[1] as f32, point[2] as f32])
118 .collect(),
119 };
120
121 Self {
122 header: trx.header().clone(),
123 positions,
124 offsets: trx.offsets_vec(),
125 dps: trx.with_typed(
126 |inner| clone_arrays(inner.dps_arrays()),
127 |inner| clone_arrays(inner.dps_arrays()),
128 |inner| clone_arrays(inner.dps_arrays()),
129 ),
130 dpv: trx.with_typed(
131 |inner| clone_arrays(inner.dpv_arrays()),
132 |inner| clone_arrays(inner.dpv_arrays()),
133 |inner| clone_arrays(inner.dpv_arrays()),
134 ),
135 groups: trx.groups_owned().into_iter().collect(),
136 dpg: trx.with_typed(
137 |inner| clone_dpg(inner.dpg_arrays()),
138 |inner| clone_dpg(inner.dpg_arrays()),
139 |inner| clone_dpg(inner.dpg_arrays()),
140 ),
141 }
142 }
143
144 pub fn header(&self) -> &Header {
145 &self.header
146 }
147
148 pub fn set_spatial_metadata(&mut self, voxel_to_rasmm: [[f64; 4]; 4], dimensions: [u64; 3]) {
149 self.header.voxel_to_rasmm = voxel_to_rasmm;
150 self.header.dimensions = dimensions;
151 }
152
153 pub fn set_header(&mut self, header: Header) {
154 self.header = header;
155 }
156
157 pub fn extra(&self) -> &HashMap<String, serde_json::Value> {
158 &self.header.extra
159 }
160
161 pub fn extra_mut(&mut self) -> &mut HashMap<String, serde_json::Value> {
162 &mut self.header.extra
163 }
164
165 pub fn positions(&self) -> &[[f32; 3]] {
166 &self.positions
167 }
168
169 pub fn positions_mut(&mut self) -> &mut [[f32; 3]] {
176 &mut self.positions
177 }
178
179 pub fn offsets(&self) -> &[u32] {
180 &self.offsets
181 }
182
183 pub fn group_names(&self) -> impl Iterator<Item = &str> {
184 self.groups.keys().map(String::as_str)
185 }
186
187 pub fn group(&self, name: &str) -> Option<&[u32]> {
188 self.groups.get(name).map(Vec::as_slice)
189 }
190
191 pub fn groups(&self) -> &HashMap<String, Vec<u32>> {
192 &self.groups
193 }
194
195 pub fn insert_group(&mut self, name: impl Into<String>, members: Vec<u32>) {
196 self.groups.insert(name.into(), members);
197 }
198
199 pub fn insert_dpg(
200 &mut self,
201 group: impl Into<String>,
202 name: impl Into<String>,
203 data: DataArray,
204 ) {
205 self.dpg
206 .entry(group.into())
207 .or_default()
208 .insert(name.into(), data);
209 }
210
211 pub fn dpg(&self) -> &DataPerGroup {
212 &self.dpg
213 }
214
215 pub fn dps_arrays(&self) -> &HashMap<String, DataArray> {
216 &self.dps
217 }
218
219 pub fn dpv_arrays(&self) -> &HashMap<String, DataArray> {
220 &self.dpv
221 }
222
223 pub fn dps_names(&self) -> impl Iterator<Item = &str> {
224 self.dps.keys().map(String::as_str)
225 }
226
227 pub fn dpv_names(&self) -> impl Iterator<Item = &str> {
228 self.dpv.keys().map(String::as_str)
229 }
230
231 pub fn insert_dps(&mut self, name: impl Into<String>, data: DataArray) {
234 self.dps.insert(name.into(), data);
235 }
236
237 pub fn insert_dpv(&mut self, name: impl Into<String>, data: DataArray) {
240 self.dpv.insert(name.into(), data);
241 }
242
243 pub fn subset_streamlines(&self, indices: &[usize]) -> Result<Self> {
244 let mut tractogram = Tractogram::with_header(self.header.clone());
245 let mut remap = HashMap::with_capacity(indices.len());
246
247 for (new_index, &old_index) in indices.iter().enumerate() {
248 let window = self.offsets.get(old_index..=old_index + 1).ok_or_else(|| {
249 TrxError::Argument(format!("streamline index {old_index} out of bounds"))
250 })?;
251 tractogram.push_streamline(&self.positions[window[0] as usize..window[1] as usize])?;
252 remap.insert(old_index as u32, new_index as u32);
253 }
254
255 for (name, members) in &self.groups {
256 let remapped = members
257 .iter()
258 .filter_map(|member| remap.get(member).copied())
259 .collect::<Vec<_>>();
260 if remapped.is_empty() {
261 continue;
262 }
263 tractogram.insert_group(name.clone(), remapped);
264 if let Some(entries) = self.dpg.get(name) {
265 for (entry_name, array) in entries {
266 tractogram.insert_dpg(name.clone(), entry_name.clone(), array.clone_owned());
267 }
268 }
269 }
270
271 Ok(tractogram)
272 }
273
274 pub fn push_streamline(&mut self, points: &[[f32; 3]]) -> Result<()> {
276 self.positions.extend_from_slice(points);
277 let next_offset = u32::try_from(self.positions.len())
278 .map_err(|_| TrxError::Argument("tractogram has more than u32::MAX vertices".into()))?;
279 self.offsets.push(next_offset);
280 self.header.nb_streamlines += 1;
281 self.header.nb_vertices = self.positions.len() as u64;
282 Ok(())
283 }
284
285 pub fn nb_streamlines(&self) -> usize {
287 self.offsets.len().saturating_sub(1)
288 }
289
290 pub fn nb_vertices(&self) -> usize {
292 self.positions.len()
293 }
294
295 pub fn streamline(&self, index: usize) -> &[[f32; 3]] {
297 let start = self.offsets[index] as usize;
298 let end = self.offsets[index + 1] as usize;
299 &self.positions[start..end]
300 }
301
302 pub fn streamlines(&self) -> impl Iterator<Item = &[[f32; 3]]> {
304 self.offsets.windows(2).map(|window| {
305 let start = window[0] as usize;
306 let end = window[1] as usize;
307 &self.positions[start..end]
308 })
309 }
310
311 pub fn to_trx(&self, dtype: DType) -> Result<AnyTrxFile> {
313 match dtype {
314 DType::Float16 => Ok(AnyTrxFile::F16(self.to_trx_typed::<f16>()?)),
315 DType::Float32 => Ok(AnyTrxFile::F32(self.to_trx_typed::<f32>()?)),
316 DType::Float64 => Ok(AnyTrxFile::F64(self.to_trx_typed::<f64>()?)),
317 other => Err(TrxError::DType(format!(
318 "TRX positions must be float16, float32, or float64, got {other}"
319 ))),
320 }
321 }
322
323 fn to_trx_typed<P>(&self) -> Result<TrxFile<P>>
324 where
325 P: TrxScalar + FromF32,
326 {
327 let positions: Vec<[P; 3]> = self
328 .positions
329 .iter()
330 .map(|point| {
331 [
332 P::from_f32(point[0]),
333 P::from_f32(point[1]),
334 P::from_f32(point[2]),
335 ]
336 })
337 .collect();
338
339 let mut header = self.header.clone();
340 header.nb_streamlines = self.nb_streamlines() as u64;
341 header.nb_vertices = self.nb_vertices() as u64;
342
343 Ok(TrxFile::from_parts(TrxParts {
344 header,
345 positions_backing: MmapBacking::Owned(vec_to_bytes(positions)),
346 offsets_backing: MmapBacking::Owned(vec_to_bytes(self.offsets.clone())),
347 dps: clone_arrays(&self.dps),
348 dpv: clone_arrays(&self.dpv),
349 groups: groups_to_data_arrays(&self.groups),
350 dpg: clone_dpg(&self.dpg),
351 }))
352 }
353}
354
355impl<P: TrxScalar> From<&TrxFile<P>> for Tractogram {
356 fn from(value: &TrxFile<P>) -> Self {
357 Self::from_trx(value)
358 }
359}
360
361impl From<&AnyTrxFile> for Tractogram {
362 fn from(value: &AnyTrxFile) -> Self {
363 Self::from_any_trx(value)
364 }
365}
366
367impl Default for Tractogram {
368 fn default() -> Self {
369 Self::new()
370 }
371}
372
373fn clone_groups(groups: &HashMap<String, DataArray>) -> HashMap<String, Vec<u32>> {
374 groups
375 .iter()
376 .map(|(name, arr)| (name.clone(), arr.to_u32_vec()))
377 .collect()
378}
379
380fn clone_arrays(arrays: &HashMap<String, DataArray>) -> HashMap<String, DataArray> {
381 arrays
382 .iter()
383 .map(|(name, arr)| (name.clone(), arr.clone_owned()))
384 .collect()
385}
386
387fn groups_to_data_arrays(groups: &HashMap<String, Vec<u32>>) -> HashMap<String, DataArray> {
388 groups
389 .iter()
390 .map(|(name, members)| {
391 (
392 name.clone(),
393 DataArray::owned_bytes(vec_to_bytes(members.clone()), 1, DType::UInt32),
394 )
395 })
396 .collect()
397}
398
399fn clone_dpg(dpg: &DataPerGroup) -> DataPerGroup {
400 dpg.iter()
401 .map(|(group, entries)| {
402 (
403 group.clone(),
404 entries
405 .iter()
406 .map(|(name, arr)| (name.clone(), arr.clone_owned()))
407 .collect(),
408 )
409 })
410 .collect()
411}
412
413trait FromF32 {
414 fn from_f32(value: f32) -> Self;
415}
416
417impl FromF32 for f16 {
418 fn from_f32(value: f32) -> Self {
419 f16::from_f32(value)
420 }
421}
422
423impl FromF32 for f32 {
424 fn from_f32(value: f32) -> Self {
425 value
426 }
427}
428
429impl FromF32 for f64 {
430 fn from_f32(value: f32) -> Self {
431 value as f64
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn streamline_push_updates_counts() {
441 let mut tractogram = Tractogram::new();
442 tractogram
443 .push_streamline(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
444 .unwrap();
445 tractogram.push_streamline(&[[7.0, 8.0, 9.0]]).unwrap();
446
447 assert_eq!(tractogram.nb_streamlines(), 2);
448 assert_eq!(tractogram.nb_vertices(), 3);
449 assert_eq!(tractogram.offsets, vec![0, 2, 3]);
450 assert_eq!(tractogram.streamline(1), &[[7.0, 8.0, 9.0]]);
451 }
452}