Skip to main content

nyx_space/od/msr/trackingdata/
io_parquet.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18use 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    /// Loads a tracking arc from its serialization in parquet.
45    ///
46    /// Warning: no metadata is read from the parquet file, even that written to it by Nyx.
47    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        // Check the schema
58        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        // We can safely unwrap the columns since we've checked for their existance just before.
102        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            // Set the measurements in the tracking arc
187            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    /// Store this tracking arc to a parquet file.
243    pub fn to_parquet_simple<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf, InputOutputError> {
244        self.to_parquet(path, ExportCfg::default())
245    }
246
247    /// Store this tracking arc to a parquet file, with optional metadata and a timestamp appended to the filename.
248    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        // Build the schema
271        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        // Build the schema
287        let schema = Arc::new(Schema::new(hdrs));
288        let mut record: Vec<Arc<dyn Array>> = Vec::new();
289
290        // Build the measurement iterator
291
292        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        // Build all of the records
307
308        // Epochs
309        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        // Device names
317        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        // Measurement data, column by column
324        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        // Rejected flag
337        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        // Serialize all of the devices and add that to the parquet file too.
344        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        // Return the path this was written to
382        Ok(path_buf)
383    }
384}