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, BooleanBuilder, Float64Builder, StringBuilder};
23use arrow::datatypes::{DataType, Field, Schema};
24use arrow::record_batch::RecordBatch;
25use arrow::{
26 array::{BooleanArray, 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 let mut rejected_avail = false;
65 for field in &reader.schema().fields {
66 match field.name().as_str() {
67 "Epoch (UTC)" => has_epoch = true,
68 "Tracking device" => has_tracking_dev = true,
69 "Range (km)" => range_avail = true,
70 "Doppler (km/s)" => doppler_avail = true,
71 "Azimuth (deg)" => az_avail = true,
72 "Elevation (deg)" => el_avail = true,
73 "Rejected" => rejected_avail = true,
74 _ => {}
75 }
76 }
77
78 ensure!(
79 has_epoch,
80 MissingDataSnafu {
81 which: "Epoch (UTC)"
82 }
83 );
84
85 ensure!(
86 has_tracking_dev,
87 MissingDataSnafu {
88 which: "Tracking device"
89 }
90 );
91
92 ensure!(
93 range_avail || doppler_avail || az_avail || el_avail,
94 MissingDataSnafu {
95 which: "`Range (km)` or `Doppler (km/s)` or `Azimuth (deg)` or `Elevation (deg)`"
96 }
97 );
98
99 let mut measurements = Vec::new();
100
101 for maybe_batch in reader {
103 let batch = maybe_batch.context(ArrowSnafu {
104 action: "reading batch of tracking data",
105 })?;
106
107 let tracking_device = batch
108 .column_by_name("Tracking device")
109 .unwrap()
110 .as_any()
111 .downcast_ref::<StringArray>()
112 .unwrap();
113
114 let epochs = batch
115 .column_by_name("Epoch (UTC)")
116 .unwrap()
117 .as_any()
118 .downcast_ref::<StringArray>()
119 .unwrap();
120
121 let range_data: Option<&PrimitiveArray<datatypes::Float64Type>> = if range_avail {
122 Some(
123 batch
124 .column_by_name("Range (km)")
125 .unwrap()
126 .as_any()
127 .downcast_ref::<Float64Array>()
128 .unwrap(),
129 )
130 } else {
131 None
132 };
133
134 let doppler_data: Option<&PrimitiveArray<datatypes::Float64Type>> = if doppler_avail {
135 Some(
136 batch
137 .column_by_name("Doppler (km/s)")
138 .unwrap()
139 .as_any()
140 .downcast_ref::<Float64Array>()
141 .unwrap(),
142 )
143 } else {
144 None
145 };
146
147 let azimuth_data: Option<&PrimitiveArray<datatypes::Float64Type>> = if az_avail {
148 Some(
149 batch
150 .column_by_name("Azimuth (deg)")
151 .unwrap()
152 .as_any()
153 .downcast_ref::<Float64Array>()
154 .unwrap(),
155 )
156 } else {
157 None
158 };
159
160 let elevation_data: Option<&PrimitiveArray<datatypes::Float64Type>> = if el_avail {
161 Some(
162 batch
163 .column_by_name("Elevation (deg)")
164 .unwrap()
165 .as_any()
166 .downcast_ref::<Float64Array>()
167 .unwrap(),
168 )
169 } else {
170 None
171 };
172
173 let rejected_data: Option<&BooleanArray> = if rejected_avail {
174 Some(
175 batch
176 .column_by_name("Rejected")
177 .unwrap()
178 .as_any()
179 .downcast_ref::<BooleanArray>()
180 .unwrap(),
181 )
182 } else {
183 None
184 };
185
186 for i in 0..batch.num_rows() {
188 let epoch = Epoch::from_gregorian_str(epochs.value(i)).map_err(|e| {
189 InputOutputError::Inconsistency {
190 msg: format!("{e} when parsing epoch"),
191 }
192 })?;
193
194 let rejected = if let Some(rej_data) = rejected_data {
195 rej_data.value(i)
196 } else {
197 false
198 };
199
200 let mut measurement = Measurement {
201 epoch,
202 tracker: tracking_device.value(i).to_string(),
203 data: IndexMap::new(),
204 rejected,
205 };
206
207 if range_avail {
208 measurement
209 .data
210 .insert(MeasurementType::Range, range_data.unwrap().value(i));
211 }
212
213 if doppler_avail {
214 measurement
215 .data
216 .insert(MeasurementType::Doppler, doppler_data.unwrap().value(i));
217 }
218
219 if az_avail {
220 measurement
221 .data
222 .insert(MeasurementType::Azimuth, azimuth_data.unwrap().value(i));
223 }
224
225 if el_avail {
226 measurement
227 .data
228 .insert(MeasurementType::Elevation, elevation_data.unwrap().value(i));
229 }
230
231 measurements.push(measurement);
232 }
233 }
234
235 Ok(Self {
236 measurements,
237 moduli: None,
238 source: Some(path.as_ref().to_path_buf().display().to_string()),
239 force_reject: false,
240 })
241 }
242 pub fn to_parquet_simple<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf, InputOutputError> {
244 self.to_parquet(path, ExportCfg::default())
245 }
246
247 pub fn to_parquet<P: AsRef<Path>>(
249 &self,
250 path: P,
251 cfg: ExportCfg,
252 ) -> Result<PathBuf, InputOutputError> {
253 ensure!(
254 !self.is_empty(),
255 EmptyDatasetSnafu {
256 action: "tracking data arc to parquet"
257 }
258 );
259
260 let path_buf = cfg.actual_path(path);
261
262 if cfg.step.is_some() {
263 warn!("The `step` parameter in the export is not supported for tracking arcs.");
264 }
265
266 if cfg.fields.is_some() {
267 warn!("The `fields` parameter in the export is not supported for tracking arcs.");
268 }
269
270 let mut hdrs = vec![
272 Field::new("Epoch (UTC)", DataType::Utf8, false),
273 Field::new("Tracking device", DataType::Utf8, false),
274 ];
275
276 let msr_types = self.unique_types();
277 let mut msr_fields = msr_types
278 .iter()
279 .map(|msr_type| msr_type.to_field())
280 .collect::<Vec<Field>>();
281
282 hdrs.append(&mut msr_fields);
283
284 hdrs.push(Field::new("Rejected", DataType::Boolean, false));
285
286 let schema = Arc::new(Schema::new(hdrs));
288 let mut record: Vec<Arc<dyn Array>> = Vec::new();
289
290 let measurements =
293 if cfg.start_epoch.is_some() || cfg.end_epoch.is_some() || cfg.step.is_some() {
294 let start = cfg
295 .start_epoch
296 .unwrap_or_else(|| self.start_epoch().unwrap());
297 let end = cfg.end_epoch.unwrap_or_else(|| self.end_epoch().unwrap());
298
299 info!("Exporting measurements from {start} to {end}.");
300
301 self.clone().filter_by_epoch(start..end).measurements
302 } else {
303 self.measurements.clone()
304 };
305
306 let mut utc_epoch = StringBuilder::new();
310 for msr in &measurements {
311 let epoch = msr.epoch;
312 utc_epoch.append_value(epoch.to_time_scale(TimeScale::UTC).to_isoformat());
313 }
314 record.push(Arc::new(utc_epoch.finish()));
315
316 let mut device_names = StringBuilder::new();
318 for m in &measurements {
319 device_names.append_value(m.tracker.clone());
320 }
321 record.push(Arc::new(device_names.finish()));
322
323 for msr_type in msr_types {
325 let mut data_builder = Float64Builder::new();
326
327 for m in &measurements {
328 match m.data.get(&msr_type) {
329 Some(value) => data_builder.append_value(*value),
330 None => data_builder.append_null(),
331 };
332 }
333 record.push(Arc::new(data_builder.finish()));
334 }
335
336 let mut rejected_builder = BooleanBuilder::new();
338 for m in &measurements {
339 rejected_builder.append_value(m.rejected);
340 }
341 record.push(Arc::new(rejected_builder.finish()));
342
343 let mut metadata = HashMap::new();
345 metadata.insert("Purpose".to_string(), "Tracking Arc Data".to_string());
346 if let Some(add_meta) = cfg.metadata {
347 for (k, v) in add_meta {
348 metadata.insert(k, v);
349 }
350 }
351
352 if let Some(modulos) = &self.moduli {
353 for (msr_type, v) in modulos {
354 metadata.insert(format!("MODULUS:{msr_type:?}"), v.to_string());
355 }
356 }
357
358 let props = pq_writer(Some(metadata));
359
360 let file = File::create(&path_buf).context(StdIOSnafu {
361 action: "creating tracking data arc file",
362 })?;
363
364 let mut writer =
365 ArrowWriter::try_new(file, schema.clone(), props).context(ParquetSnafu {
366 action: "creating tracking data arc writer",
367 })?;
368
369 let batch = RecordBatch::try_new(schema, record).context(ArrowSnafu {
370 action: "creating tracking data arc batch record",
371 })?;
372 writer.write(&batch).context(ParquetSnafu {
373 action: "writing tracking data arc batch",
374 })?;
375 writer.close().context(ParquetSnafu {
376 action: "closing tracking data arc file",
377 })?;
378
379 info!("Serialized {self} to {}", path_buf.display());
380
381 Ok(path_buf)
383 }
384}