nyx_space/od/msr/trackingdata/
io_parquet.rs1use crate::io::watermark::pq_writer;
19use crate::io::{ArrowSnafu, InputOutputError, MissingDataSnafu, ParquetSnafu, StdIOSnafu};
20use crate::io::{EmptyDatasetSnafu, ExportCfg};
21use crate::od::msr::{Measurement, MeasurementType};
22use arrow::array::{Array, Float64Builder, StringBuilder};
23use arrow::datatypes::{DataType, Field, Schema};
24use arrow::record_batch::RecordBatch;
25use arrow::{
26 array::{Float64Array, PrimitiveArray, StringArray},
27 datatypes,
28 record_batch::RecordBatchReader,
29};
30use hifitime::{Epoch, TimeScale};
31use indexmap::IndexMap;
32use log::{info, warn};
33use parquet::arrow::ArrowWriter;
34use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
35use snafu::{ResultExt, ensure};
36use std::collections::HashMap;
37use std::fs::File;
38use std::path::{Path, PathBuf};
39use std::sync::Arc;
40
41use super::TrackingDataArc;
42
43impl TrackingDataArc {
44 pub fn from_parquet<P: AsRef<Path>>(path: P) -> Result<Self, InputOutputError> {
48 let file = File::open(&path).context(StdIOSnafu {
49 action: "opening file for tracking arc",
50 })?;
51 let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
52
53 let reader = builder.build().context(ParquetSnafu {
54 action: "reading tracking arc",
55 })?;
56
57 let mut has_epoch = false;
59 let mut has_tracking_dev = false;
60 let mut range_avail = false;
61 let mut doppler_avail = false;
62 let mut az_avail = false;
63 let mut el_avail = false;
64 for field in &reader.schema().fields {
65 match field.name().as_str() {
66 "Epoch (UTC)" => has_epoch = true,
67 "Tracking device" => has_tracking_dev = true,
68 "Range (km)" => range_avail = true,
69 "Doppler (km/s)" => doppler_avail = true,
70 "Azimuth (deg)" => az_avail = true,
71 "Elevation (deg)" => el_avail = true,
72 _ => {}
73 }
74 }
75
76 ensure!(
77 has_epoch,
78 MissingDataSnafu {
79 which: "Epoch (UTC)"
80 }
81 );
82
83 ensure!(
84 has_tracking_dev,
85 MissingDataSnafu {
86 which: "Tracking device"
87 }
88 );
89
90 ensure!(
91 range_avail || doppler_avail || az_avail || el_avail,
92 MissingDataSnafu {
93 which: "`Range (km)` or `Doppler (km/s)` or `Azimuth (deg)` or `Elevation (deg)`"
94 }
95 );
96
97 let mut measurements = Vec::new();
98
99 for maybe_batch in reader {
101 let batch = maybe_batch.context(ArrowSnafu {
102 action: "reading batch of tracking data",
103 })?;
104
105 let tracking_device = batch
106 .column_by_name("Tracking device")
107 .unwrap()
108 .as_any()
109 .downcast_ref::<StringArray>()
110 .unwrap();
111
112 let epochs = batch
113 .column_by_name("Epoch (UTC)")
114 .unwrap()
115 .as_any()
116 .downcast_ref::<StringArray>()
117 .unwrap();
118
119 let range_data: Option<&PrimitiveArray<datatypes::Float64Type>> = if range_avail {
120 Some(
121 batch
122 .column_by_name("Range (km)")
123 .unwrap()
124 .as_any()
125 .downcast_ref::<Float64Array>()
126 .unwrap(),
127 )
128 } else {
129 None
130 };
131
132 let doppler_data: Option<&PrimitiveArray<datatypes::Float64Type>> = if doppler_avail {
133 Some(
134 batch
135 .column_by_name("Doppler (km/s)")
136 .unwrap()
137 .as_any()
138 .downcast_ref::<Float64Array>()
139 .unwrap(),
140 )
141 } else {
142 None
143 };
144
145 let azimuth_data: Option<&PrimitiveArray<datatypes::Float64Type>> = if az_avail {
146 Some(
147 batch
148 .column_by_name("Azimuth (deg)")
149 .unwrap()
150 .as_any()
151 .downcast_ref::<Float64Array>()
152 .unwrap(),
153 )
154 } else {
155 None
156 };
157
158 let elevation_data: Option<&PrimitiveArray<datatypes::Float64Type>> = if el_avail {
159 Some(
160 batch
161 .column_by_name("Elevation (deg)")
162 .unwrap()
163 .as_any()
164 .downcast_ref::<Float64Array>()
165 .unwrap(),
166 )
167 } else {
168 None
169 };
170
171 for i in 0..batch.num_rows() {
173 let epoch = Epoch::from_gregorian_str(epochs.value(i)).map_err(|e| {
174 InputOutputError::Inconsistency {
175 msg: format!("{e} when parsing epoch"),
176 }
177 })?;
178
179 let mut measurement = Measurement {
180 epoch,
181 tracker: tracking_device.value(i).to_string(),
182 data: IndexMap::new(),
183 rejected: false,
184 };
185
186 if range_avail {
187 measurement
188 .data
189 .insert(MeasurementType::Range, range_data.unwrap().value(i));
190 }
191
192 if doppler_avail {
193 measurement
194 .data
195 .insert(MeasurementType::Doppler, doppler_data.unwrap().value(i));
196 }
197
198 if az_avail {
199 measurement
200 .data
201 .insert(MeasurementType::Azimuth, azimuth_data.unwrap().value(i));
202 }
203
204 if el_avail {
205 measurement
206 .data
207 .insert(MeasurementType::Elevation, elevation_data.unwrap().value(i));
208 }
209
210 measurements.push(measurement);
211 }
212 }
213
214 Ok(Self {
215 measurements,
216 moduli: None,
217 source: Some(path.as_ref().to_path_buf().display().to_string()),
218 force_reject: false,
219 })
220 }
221 pub fn to_parquet_simple<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf, InputOutputError> {
223 self.to_parquet(path, ExportCfg::default())
224 }
225
226 pub fn to_parquet<P: AsRef<Path>>(
228 &self,
229 path: P,
230 cfg: ExportCfg,
231 ) -> Result<PathBuf, InputOutputError> {
232 ensure!(
233 !self.is_empty(),
234 EmptyDatasetSnafu {
235 action: "tracking data arc to parquet"
236 }
237 );
238
239 let path_buf = cfg.actual_path(path);
240
241 if cfg.step.is_some() {
242 warn!("The `step` parameter in the export is not supported for tracking arcs.");
243 }
244
245 if cfg.fields.is_some() {
246 warn!("The `fields` parameter in the export is not supported for tracking arcs.");
247 }
248
249 let mut hdrs = vec![
251 Field::new("Epoch (UTC)", DataType::Utf8, false),
252 Field::new("Tracking device", DataType::Utf8, false),
253 ];
254
255 let msr_types = self.unique_types();
256 let mut msr_fields = msr_types
257 .iter()
258 .map(|msr_type| msr_type.to_field())
259 .collect::<Vec<Field>>();
260
261 hdrs.append(&mut msr_fields);
262
263 let schema = Arc::new(Schema::new(hdrs));
265 let mut record: Vec<Arc<dyn Array>> = Vec::new();
266
267 let measurements =
270 if cfg.start_epoch.is_some() || cfg.end_epoch.is_some() || cfg.step.is_some() {
271 let start = cfg
272 .start_epoch
273 .unwrap_or_else(|| self.start_epoch().unwrap());
274 let end = cfg.end_epoch.unwrap_or_else(|| self.end_epoch().unwrap());
275
276 info!("Exporting measurements from {start} to {end}.");
277
278 self.clone().filter_by_epoch(start..end).measurements
279 } else {
280 self.measurements.clone()
281 };
282
283 let mut utc_epoch = StringBuilder::new();
287 for msr in &measurements {
288 let epoch = msr.epoch;
289 utc_epoch.append_value(epoch.to_time_scale(TimeScale::UTC).to_isoformat());
290 }
291 record.push(Arc::new(utc_epoch.finish()));
292
293 let mut device_names = StringBuilder::new();
295 for m in &measurements {
296 device_names.append_value(m.tracker.clone());
297 }
298 record.push(Arc::new(device_names.finish()));
299
300 for msr_type in msr_types {
302 let mut data_builder = Float64Builder::new();
303
304 for m in &measurements {
305 match m.data.get(&msr_type) {
306 Some(value) => data_builder.append_value(*value),
307 None => data_builder.append_null(),
308 };
309 }
310 record.push(Arc::new(data_builder.finish()));
311 }
312
313 let mut metadata = HashMap::new();
315 metadata.insert("Purpose".to_string(), "Tracking Arc Data".to_string());
316 if let Some(add_meta) = cfg.metadata {
317 for (k, v) in add_meta {
318 metadata.insert(k, v);
319 }
320 }
321
322 if let Some(modulos) = &self.moduli {
323 for (msr_type, v) in modulos {
324 metadata.insert(format!("MODULUS:{msr_type:?}"), v.to_string());
325 }
326 }
327
328 let props = pq_writer(Some(metadata));
329
330 let file = File::create(&path_buf).context(StdIOSnafu {
331 action: "creating tracking data arc file",
332 })?;
333
334 let mut writer =
335 ArrowWriter::try_new(file, schema.clone(), props).context(ParquetSnafu {
336 action: "creating tracking data arc writer",
337 })?;
338
339 let batch = RecordBatch::try_new(schema, record).context(ArrowSnafu {
340 action: "creating tracking data arc batch record",
341 })?;
342 writer.write(&batch).context(ParquetSnafu {
343 action: "writing tracking data arc batch",
344 })?;
345 writer.close().context(ParquetSnafu {
346 action: "closing tracking data arc file",
347 })?;
348
349 info!("Serialized {self} to {}", path_buf.display());
350
351 Ok(path_buf)
353 }
354}