1use core::fmt;
2use std::{
3 collections::{HashMap, HashSet},
4 io::{BufReader, Cursor, Read, Seek, SeekFrom},
5 sync::{Arc, Mutex, MutexGuard},
6};
7
8use byteorder::{LittleEndian, ReadBytesExt};
9use image::ImageFormat;
10use nalgebra::Vector2;
11
12use crate::{
13 channel::{AcquisitionChannel, ChannelIdentifier},
14 convert::DCMLocation,
15 error::{MCDError, Result},
16 mcd::AcquisitionXML,
17 transform::AffineTransform,
18 BoundingBox, ChannelImage, OnSlide, OpticalImage, Print, Region,
19};
20
21#[derive(Debug, Clone)]
22pub enum DataFormat {
23 Float,
24}
25
26#[derive(Debug)]
36pub enum AcquisitionIdentifier {
37 Id(u16),
39 Order(i16),
41 Description(String),
43}
44
45impl AcquisitionIdentifier {
46 pub fn description(description: &str) -> Self {
48 AcquisitionIdentifier::Description(description.into())
49 }
50}
51
52impl From<&str> for AcquisitionIdentifier {
53 fn from(description: &str) -> Self {
54 Self::Description(description.into())
55 }
56}
57
58impl fmt::Display for AcquisitionIdentifier {
59 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60 match self {
61 AcquisitionIdentifier::Id(id) => {
62 write!(f, "acquisition id: {}", id)
63 }
64 AcquisitionIdentifier::Order(order) => {
65 write!(f, "acquisition order: {}", order)
66 }
67 AcquisitionIdentifier::Description(description) => {
68 write!(f, "acquisition description: {}", description)
69 }
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy)]
75pub enum ProfilingType {
76 Global,
77}
78
79pub trait Acquisitions {
82 fn channels(&self) -> Vec<&AcquisitionChannel>;
84}
85
86impl<R> Acquisitions for Vec<&Acquisition<R>> {
87 fn channels(&self) -> Vec<&AcquisitionChannel> {
88 let mut channels = HashMap::new();
89
90 for acquisition in self {
91 for channel in acquisition.channels() {
92 if !channels.contains_key(channel.name()) {
93 channels.insert(channel.name(), channel);
94 }
95 }
96 }
97
98 let mut ordered_channels = Vec::new();
99 for (_, channel) in channels.drain() {
100 ordered_channels.push(channel);
101 }
102
103 ordered_channels.sort_by_key(|a| a.label().to_ascii_lowercase());
104
105 ordered_channels
106 }
107}
108
109#[derive(Debug)]
111pub struct Acquisition<R> {
112 pub(crate) reader: Option<Arc<Mutex<BufReader<R>>>>,
113 pub(crate) dcm_location: Option<DCMLocation>,
114
115 id: u16,
116 description: String,
117 ablation_power: f64,
118 ablation_distance_between_shots_x: f64,
119 ablation_distance_between_shots_y: f64,
120 ablation_frequency: f64,
121 acquisition_roi_id: i16,
122 order_number: i16,
123 signal_type: String,
124 dual_count_start: String,
125 data_start_offset: i64,
126 data_end_offset: i64,
127 start_timestamp: String,
128 end_timestamp: String,
129 after_ablation_image_start_offset: i64,
130 after_ablation_image_end_offset: i64,
131 before_ablation_image_start_offset: i64,
132 before_ablation_image_end_offset: i64,
133 roi_start_x_pos_um: f64,
134 roi_start_y_pos_um: f64,
135 roi_end_x_pos_um: f64,
136 roi_end_y_pos_um: f64,
137 movement_type: String,
138 segment_data_format: DataFormat,
139 value_bytes: u8,
140 max_x: i32,
141 max_y: i32,
142 plume_start: i32,
143 plume_end: i32,
144 template: String,
145
146 profiling_type: Option<ProfilingType>,
147
148 channels: Vec<AcquisitionChannel>,
149}
150
151impl<R> Clone for Acquisition<R> {
152 fn clone(&self) -> Self {
153 Self {
154 reader: self.reader.clone(),
155 dcm_location: self.dcm_location.clone(),
156 id: self.id,
157 description: self.description.clone(),
158 ablation_power: self.ablation_power,
159 ablation_distance_between_shots_x: self.ablation_distance_between_shots_x,
160 ablation_distance_between_shots_y: self.ablation_distance_between_shots_y,
161 ablation_frequency: self.ablation_frequency,
162 acquisition_roi_id: self.acquisition_roi_id,
163 order_number: self.order_number,
164 signal_type: self.signal_type.clone(),
165 dual_count_start: self.dual_count_start.clone(),
166 data_start_offset: self.data_start_offset,
167 data_end_offset: self.data_end_offset,
168 start_timestamp: self.start_timestamp.clone(),
169 end_timestamp: self.end_timestamp.clone(),
170 after_ablation_image_start_offset: self.after_ablation_image_start_offset,
171 after_ablation_image_end_offset: self.after_ablation_image_end_offset,
172 before_ablation_image_start_offset: self.before_ablation_image_start_offset,
173 before_ablation_image_end_offset: self.before_ablation_image_end_offset,
174 roi_start_x_pos_um: self.roi_start_x_pos_um,
175 roi_start_y_pos_um: self.roi_start_y_pos_um,
176 roi_end_x_pos_um: self.roi_end_x_pos_um,
177 roi_end_y_pos_um: self.roi_end_y_pos_um,
178 movement_type: self.movement_type.clone(),
179 segment_data_format: self.segment_data_format.clone(),
180 value_bytes: self.value_bytes,
181 max_x: self.max_x,
182 max_y: self.max_y,
183 plume_start: self.plume_start,
184 plume_end: self.plume_end,
185 template: self.template.clone(),
186 profiling_type: self.profiling_type,
187 channels: self.channels.clone(),
188 }
189 }
190}
191
192pub struct SpectrumIterator<'a, R> {
193 acquisition: &'a Acquisition<R>,
194 reader: MutexGuard<'a, BufReader<R>>,
195 buffer: Vec<u8>,
196}
197
198impl<'a, R: Seek> SpectrumIterator<'a, R> {
199 fn new(acquisition: &'a Acquisition<R>) -> Self {
200 let mut reader = acquisition.reader.as_ref().unwrap().lock().unwrap();
201
202 let offset = acquisition.data_start_offset as u64;
203
204 reader.seek(SeekFrom::Start(offset)).unwrap();
206
207 SpectrumIterator {
208 acquisition,
209 reader,
210 buffer: vec![0u8; 4 * acquisition.channels.len()],
211 }
212 }
213}
214
215impl<'a, R: Read + Seek> Iterator for SpectrumIterator<'a, R> {
216 type Item = Vec<f32>;
217
218 fn next(&mut self) -> Option<Vec<f32>> {
219 let cur_pos = self.reader.seek(SeekFrom::Current(0)).unwrap();
220 if cur_pos >= self.acquisition.data_end_offset as u64 {
221 None
222 } else {
223 let mut spectrum = Vec::with_capacity(self.acquisition.channels.len());
224
225 self.reader.read_exact(&mut self.buffer).unwrap();
226 let mut buffer = Cursor::new(&mut self.buffer);
227
228 for _i in 0..self.acquisition.channels.len() {
229 let float = buffer.read_f32::<LittleEndian>().unwrap();
230
231 spectrum.push(float);
232 }
233
234 Some(spectrum)
235 }
236 }
237}
238
239impl<R> Acquisition<R> {
240 pub fn id(&self) -> u16 {
242 self.id
243 }
244
245 pub fn description(&self) -> &str {
247 &self.description
248 }
249
250 pub fn order_number(&self) -> i16 {
252 self.order_number
253 }
254
255 pub fn width(&self) -> i32 {
257 self.max_x
258 }
259
260 pub fn height(&self) -> i32 {
262 self.max_y
263 }
264
265 pub fn ablation_frequency(&self) -> f64 {
267 self.ablation_frequency
268 }
269
270 pub fn acquisition_roi_id(&self) -> i16 {
272 self.acquisition_roi_id
273 }
274
275 pub fn profiling_type(&self) -> Option<&ProfilingType> {
277 self.profiling_type.as_ref()
278 }
279
280 pub fn before_ablation_image(&self) -> OpticalImage<R> {
298 OpticalImage {
299 reader: self.reader.as_ref().unwrap().clone(),
300 start_offset: self.before_ablation_image_start_offset,
301 end_offset: self.before_ablation_image_end_offset,
302 image_format: ImageFormat::Png,
303 }
304 }
312
313 pub fn after_ablation_image(&self) -> OpticalImage<R> {
315 OpticalImage {
316 reader: self.reader.as_ref().unwrap().clone(),
317 start_offset: self.after_ablation_image_start_offset,
318 end_offset: self.after_ablation_image_end_offset,
319 image_format: ImageFormat::Png,
320 }
321 }
322
323 pub fn channels(&self) -> &[AcquisitionChannel] {
325 &self.channels
326 }
327
328 pub(crate) fn channels_mut(&mut self) -> &mut Vec<AcquisitionChannel> {
329 &mut self.channels
330 }
331
332 pub fn is_complete(&self) -> bool {
335 let expected_size: usize = self.channels().len()
336 * self.max_x as usize
337 * self.max_y as usize
338 * self.value_bytes as usize;
339 let measured_size: usize = self.data_end_offset as usize - self.data_start_offset as usize;
340
341 expected_size == measured_size
344 }
345
346 pub fn pixels_in(&self, region: &BoundingBox<f64>) -> Option<Region> {
348 let transform = self.to_slide_transform();
349
350 let top_left = transform.transform_from_slide(region.min_x, region.min_y)?;
351 let top_right = transform.transform_from_slide(region.max_x(), region.min_y)?;
352
353 let bottom_right = transform.transform_from_slide(region.max_x(), region.max_y())?;
354 let bottom_left = transform.transform_from_slide(region.min_x, region.max_y())?;
355
356 let min_x = top_left
357 .x
358 .min(top_right.x)
359 .min(bottom_right.x)
360 .min(bottom_left.x)
361 .max(0.0)
362 .floor();
363
364 let max_x = top_left
365 .x
366 .max(top_right.x)
367 .max(bottom_right.x)
368 .max(bottom_left.x)
369 .min(self.width() as f64)
370 .ceil();
371
372 let min_y = top_left
373 .y
374 .min(top_right.y)
375 .min(bottom_right.y)
376 .min(bottom_left.y)
377 .max(0.0)
378 .floor();
379
380 let max_y = top_left
381 .y
382 .max(top_right.y)
383 .max(bottom_right.y)
384 .max(bottom_left.y)
385 .min(self.height() as f64)
386 .ceil();
387
388 Some(Region {
389 x: min_x as u32,
390 y: (self.height() as u32 - max_y as u32),
391 width: (max_x - min_x) as u32,
392 height: (max_y - min_y) as u32,
393 })
394 }
395
396 pub fn in_region(&self, region: &BoundingBox<f64>) -> bool {
398 let slide_box = self.slide_bounding_box();
399
400 if slide_box.min_x < region.max_x()
401 && slide_box.max_x() > region.min_x
402 && slide_box.max_y() > region.min_y
403 && slide_box.min_y < region.max_y()
404 {
405 return true;
406 }
407
408 false
409 }
410
411 #[inline]
413 pub fn spectrum_size(&self) -> usize {
414 self.channels().len() * self.value_bytes as usize
415 }
416
417 #[inline]
419 pub fn num_spectra(&self) -> usize {
420 let measured_size: usize = self.data_end_offset as usize - self.data_start_offset as usize;
421
422 measured_size / self.spectrum_size()
423 }
424}
425
426impl<R: Read + Seek> Acquisition<R> {
427 pub fn channel_image<C: Into<ChannelIdentifier>>(
430 &self,
431 identifier: C,
432 region: Option<Region>,
433 ) -> Result<ChannelImage> {
434 Ok(self
435 .channel_images(&[identifier.into()], region)?
436 .drain(..)
437 .last()
438 .expect("A channel image should always be returned, as we always pass one identifier"))
439 }
440
441 pub fn channel_images<C: AsRef<ChannelIdentifier>>(
444 &self,
445 identifiers: &[C],
446 region: Option<Region>,
447 ) -> Result<Vec<ChannelImage>> {
448 let channels: Vec<_> = identifiers
452 .iter()
453 .map(|identifier| {
454 self.channel(identifier).ok_or(MCDError::InvalidChannel {
455 channel: identifier.as_ref().clone(),
456 })
457 })
458 .collect::<Result<Vec<_>>>()?;
459
460 let order_numbers: Vec<_> = channels
461 .iter()
462 .map(|channel| channel.order_number() as usize)
463 .collect();
464
465 let region = match region {
466 Some(region) => region,
467 None => crate::Region {
468 x: 0,
469 y: 0,
470 width: self.width() as u32,
471 height: self.height() as u32,
472 },
473 };
474
475 let last_row = self.num_spectra() / self.width() as usize;
476 let last_col = self.width() as usize - (self.num_spectra() % self.width() as usize);
477
478 let valid_region_row = (region.y + region.height).min(last_row as u32);
479 let valid_region_col = (region.x + region.width).min(last_col as u32);
480
481 let valid_pixels = if region.y >= valid_region_row {
483 (valid_region_row - 1) * region.width + valid_region_col
484 } else {
485 ((valid_region_row - region.y - 1) * region.width) + (valid_region_col - region.x)
486 };
487
488 let mut data = if let Some(data_location) = &self.dcm_location {
489 data_location.read_channels(&order_numbers, ®ion)?
490 } else {
491 let mut data: Vec<Vec<f32>> =
492 vec![
493 Vec::with_capacity((region.width * region.height) as usize);
494 identifiers.len()
495 ];
496
497 let order_hash: HashSet<usize> = HashSet::from_iter(order_numbers.iter().copied());
498
499 for y in region.y..(region.y + region.height) {
500 for x in region.x..(region.x + region.width) {
501 for (channel_index, intensity) in self
502 .spectrum(x, y)?
503 .iter()
504 .enumerate()
505 .filter(|(index, _intensity)| order_hash.contains(index))
506 .map(|(_, intensity)| *intensity)
507 .enumerate()
508 {
509 data[channel_index].push(intensity);
510 }
511 }
512 }
513
514 data
515 };
516
517 let images: Vec<_> = data
518 .drain(..)
519 .zip(channels.iter())
520 .map(|(data, channel)| {
521 let mut min_value = f32::MAX;
522 let mut max_value = f32::MIN;
523
524 for &data_point in data.iter() {
525 if data_point < min_value {
526 min_value = data_point;
527 }
528 if data_point > max_value {
529 max_value = data_point;
530 }
531 }
532
533 ChannelImage {
534 region,
535 acquisition_id: channel.acquisition_id(),
536 name: channel.name().to_string(),
537 label: channel.label().to_string(),
538 range: (min_value, max_value),
539 valid_pixels: valid_pixels as usize,
540 data,
541 }
542 })
543 .collect();
544
545 Ok(images)
546
547 }
554
555 pub fn channel<C: AsRef<ChannelIdentifier>>(
557 &self,
558 identifier: C,
559 ) -> Option<&AcquisitionChannel> {
560 self.channels
561 .iter()
562 .find(|&channel| channel.is(identifier.as_ref()))
563 }
564
565 pub(crate) fn fix_roi_positions(&mut self) {
577 if self.roi_start_x_pos_um > 75000.0 {
579 self.roi_start_x_pos_um /= 1000.0;
580 }
581
582 if self.roi_start_y_pos_um > 75000.0 {
584 self.roi_start_y_pos_um /= 1000.0;
585 }
586
587 if (self.roi_start_x_pos_um == self.roi_end_x_pos_um) || self.roi_end_x_pos_um == 0.0 {
589 self.roi_end_x_pos_um = self.roi_start_x_pos_um
590 + (self.max_x as f64 * self.ablation_distance_between_shots_x);
591 }
592
593 if self.roi_end_y_pos_um == 0.0 {
594 self.roi_end_y_pos_um = self.roi_start_y_pos_um
595 - (self.max_y as f64 * self.ablation_distance_between_shots_y);
596 }
597 }
598}
599
600impl<R: Read + Seek> Acquisition<R> {
601 pub fn spectra(&self) -> SpectrumIterator<R> {
603 SpectrumIterator::new(self)
604 }
605
606 pub fn spectrum(&self, x: u32, y: u32) -> Result<Vec<f32>> {
608 let index = y as usize * self.max_x as usize + x as usize;
609
610 if index >= self.num_spectra() {
611 return Err(MCDError::InvalidIndex {
612 index,
613 num_spectra: self.num_spectra(),
614 });
615 }
616
617 let offset = self.data_start_offset as u64
618 + (index * self.channels.len() * self.value_bytes as usize) as u64;
619
620 let mut spectrum = Vec::with_capacity(self.channels.len());
621 let mut reader = self.reader.as_ref().unwrap().lock().unwrap();
622
623 reader.seek(SeekFrom::Start(offset))?;
625
626 let mut buffer = [0u8; 4];
627 for _i in 0..self.channels.len() {
628 reader.read_exact(&mut buffer)?;
629 let float = f32::from_le_bytes(buffer);
630 spectrum.push(float);
631 }
632
633 Ok(spectrum)
634 }
635}
636
637impl<R> OnSlide for Acquisition<R> {
638 fn to_slide_transform(&self) -> AffineTransform<f64> {
640 let mut moving_points = Vec::new();
641 let mut fixed_points = Vec::new();
642
643 moving_points.push(Vector2::new(
644 self.roi_start_x_pos_um,
645 self.roi_start_y_pos_um,
646 ));
648 moving_points.push(Vector2::new(
649 self.roi_end_x_pos_um,
650 self.roi_start_y_pos_um,
651 ));
653 moving_points.push(Vector2::new(
654 self.roi_start_x_pos_um,
655 self.roi_end_y_pos_um,
656 ));
658 let y = (self.roi_start_y_pos_um - self.roi_end_y_pos_um)
661 / self.ablation_distance_between_shots_y;
662
663 fixed_points.push(Vector2::new(0.0, y));
664 fixed_points.push(Vector2::new(self.max_x as f64, y));
665 fixed_points.push(Vector2::new(0.0, 0.0));
667
668 AffineTransform::from_points(moving_points, fixed_points)
671 }
672
673 fn slide_bounding_box(&self) -> BoundingBox<f64> {
675 BoundingBox {
676 min_x: f64::min(self.roi_start_x_pos_um, self.roi_end_x_pos_um),
677 min_y: f64::min(self.roi_start_y_pos_um, self.roi_end_y_pos_um),
678 width: (self.roi_end_x_pos_um - self.roi_start_x_pos_um).abs(),
679 height: (self.roi_end_y_pos_um - self.roi_start_y_pos_um).abs(),
680 }
681 }
682}
683
684#[rustfmt::skip]
685impl<R> Print for Acquisition<R> {
686 fn print<W: fmt::Write + ?Sized>(&self, writer: &mut W, indent: usize) -> fmt::Result {
687 write!(writer, "{:indent$}", "", indent = indent)?;
688 writeln!(writer, "{:-^1$}", "Acquisition", 48)?;
689
690 writeln!(writer, "{:indent$}{: <22} | {}", "", "ID", self.id, indent = indent)?;
691 writeln!(writer, "{:indent$}{: <22} | {}", "", "Description", self.description, indent = indent)?;
692 writeln!(writer, "{:indent$}{: <22} | {}", "", "Order number", self.order_number, indent = indent)?;
693 writeln!(
694 writer,
695 "{:indent$}{: <22} | {} x {}",
696 "",
697 "Dimensions (pixels)",
698 self.max_x,
699 self.max_y,
700 indent = indent
701 )?;
702 writeln!(
703 writer,
704 "{:indent$}{: <22} | {} x {}",
705 "",
706 "Distance between shots",
707 self.ablation_distance_between_shots_x,
708 self.ablation_distance_between_shots_y,
709 indent = indent
710 )?;
711 writeln!(
712 writer,
713 "{:indent$}{: <22} | {}",
714 "",
715 "Signal type",
716 self.signal_type,
717 indent = indent
718 )?;
719 writeln!(
720 writer,
721 "{:indent$}{: <22} | {}",
722 "",
723 "Ablation power",
724 self.ablation_power,
725 indent = indent
726 )?;
727 writeln!(
728 writer,
729 "{:indent$}{: <22} | {}",
730 "",
731 "Dual count start",
732 self.dual_count_start,
733 indent = indent
734 )?;
735 writeln!(
736 writer,
737 "{:indent$}{: <22} | {}",
738 "",
739 "Start timestamp",
740 self.start_timestamp,
741 indent = indent
742 )?;
743 writeln!(
744 writer,
745 "{:indent$}{: <22} | {}",
746 "",
747 "End timestamp",
748 self.end_timestamp,
749 indent = indent
750 )?;
751 writeln!(
752 writer,
753 "{:indent$}{: <22} | ({:.4} μm, {:.4} μm)",
754 "",
755 "ROI",
756 self.roi_start_x_pos_um,
757 self.roi_start_y_pos_um,
758 indent = indent
759 )?;
760 writeln!(
761 writer,
762 "{:indent$}{: <22} | ({:.4} μm, {:.4} μm)",
763 "",
764 "",
765 self.roi_end_x_pos_um,
766 self.roi_end_y_pos_um,
767 indent = indent
768 )?;
769 writeln!(
770 writer,
771 "{:indent$}{: <22} | {}",
772 "",
773 "Movement type",
774 self.movement_type,
775 indent = indent
776 )?;
777 writeln!(
778 writer,
779 "{:indent$}{: <22} | {:?}",
780 "",
781 "Segment data format",
782 self.segment_data_format,
783 indent = indent
784 )?;
785 writeln!(
786 writer,
787 "{:indent$}{: <22} | {}",
788 "",
789 "Value bytes",
790 self.value_bytes,
791 indent = indent
792 )?;
793 writeln!(
794 writer,
795 "{:indent$}{: <22} | {}",
796 "",
797 "Plume start",
798 self.plume_start,
799 indent = indent
800 )?;
801 writeln!(
802 writer,
803 "{:indent$}{: <22} | {}",
804 "",
805 "Plume end",
806 self.plume_end,
807 indent = indent
808 )?;
809 writeln!(
810 writer,
811 "{:indent$}{: <22} | {}",
812 "",
813 "Template",
814 self.template,
815 indent = indent
816 )?;
817
818 Ok(())
819 }
820}
821
822impl<R> fmt::Display for Acquisition<R> {
823 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
824 self.print(f, 0)
825 }
826}
827
828impl<R> From<AcquisitionXML> for Acquisition<R> {
829 fn from(acquisition: AcquisitionXML) -> Self {
830 Acquisition {
831 reader: None,
832 dcm_location: None,
833
834 id: acquisition.id.unwrap(),
835 description: acquisition.description.unwrap(),
836 ablation_power: acquisition.ablation_power.unwrap(),
837 ablation_distance_between_shots_x: acquisition
838 .ablation_distance_between_shots_x
839 .unwrap(),
840 ablation_distance_between_shots_y: acquisition
841 .ablation_distance_between_shots_y
842 .unwrap(),
843 ablation_frequency: acquisition.ablation_frequency.unwrap(),
844 acquisition_roi_id: acquisition.acquisition_roi_id.unwrap(),
845 order_number: acquisition.order_number.unwrap(),
846 signal_type: acquisition.signal_type.unwrap(),
847 dual_count_start: acquisition.dual_count_start.unwrap(),
848 data_start_offset: acquisition.data_start_offset.unwrap(),
849 data_end_offset: acquisition.data_end_offset.unwrap(),
850 start_timestamp: acquisition.start_timestamp.unwrap(),
851 end_timestamp: acquisition.end_timestamp.unwrap(),
852 after_ablation_image_start_offset: acquisition
853 .after_ablation_image_start_offset
854 .unwrap(),
855 after_ablation_image_end_offset: acquisition.after_ablation_image_end_offset.unwrap(),
856 before_ablation_image_start_offset: acquisition
857 .before_ablation_image_start_offset
858 .unwrap(),
859 before_ablation_image_end_offset: acquisition.before_ablation_image_end_offset.unwrap(),
860 roi_start_x_pos_um: acquisition.roi_start_x_pos_um.unwrap(),
861 roi_start_y_pos_um: acquisition.roi_start_y_pos_um.unwrap(),
862 roi_end_x_pos_um: acquisition.roi_end_x_pos_um.unwrap(),
863 roi_end_y_pos_um: acquisition.roi_end_y_pos_um.unwrap(),
864 movement_type: acquisition.movement_type.unwrap(),
865 segment_data_format: acquisition.segment_data_format.unwrap(),
866 value_bytes: acquisition.value_bytes.unwrap(),
867 max_x: acquisition.max_x.unwrap(),
868 max_y: acquisition.max_y.unwrap(),
869 plume_start: acquisition.plume_start.unwrap(),
870 plume_end: acquisition.plume_end.unwrap(),
871 template: acquisition.template.unwrap(),
872
873 profiling_type: acquisition.profiling_type,
874
875 channels: Vec::new(),
876 }
877 }
878}