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