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, 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    /// 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        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        // We can safely unwrap the columns since we've checked for their existance just before.
100        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            // Set the measurements in the tracking arc
172            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    /// Store this tracking arc to a parquet file.
222    pub fn to_parquet_simple<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf, InputOutputError> {
223        self.to_parquet(path, ExportCfg::default())
224    }
225
226    /// Store this tracking arc to a parquet file, with optional metadata and a timestamp appended to the filename.
227    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        // Build the schema
250        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        // Build the schema
264        let schema = Arc::new(Schema::new(hdrs));
265        let mut record: Vec<Arc<dyn Array>> = Vec::new();
266
267        // Build the measurement iterator
268
269        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        // Build all of the records
284
285        // Epochs
286        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        // Device names
294        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        // Measurement data, column by column
301        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        // Serialize all of the devices and add that to the parquet file too.
314        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        // Return the path this was written to
352        Ok(path_buf)
353    }
354}