Skip to content

Commit 65e1dcb

Browse files
Add support for CCSDS TDM transmit/receive frequencies
1 parent 83b8577 commit 65e1dcb

File tree

6 files changed

+169
-7
lines changed

6 files changed

+169
-7
lines changed

src/io/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ pub enum InputOutputError {
169169
ParseDhall { data: String, err: String },
170170
#[snafu(display("error serializing {what} to Dhall: {err}"))]
171171
SerializeDhall { what: String, err: String },
172-
#[snafu(display("empty dataset error when (de)serializing for {action}"))]
172+
#[snafu(display("empty dataset error when (de)serializing {action}"))]
173173
EmptyDataset { action: &'static str },
174174
}
175175

src/od/msr/measurement.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ impl Measurement {
6969

7070
/// Returns a vector specifying which measurement types are available.
7171
pub fn availability(&self, types: &IndexSet<MeasurementType>) -> Vec<bool> {
72-
let mut rtn = Vec::with_capacity(types.len());
72+
let mut rtn = vec![false; types.len()];
7373
for (i, t) in types.iter().enumerate() {
7474
if self.data.contains_key(t) {
7575
rtn[i] = true;

src/od/msr/sensitivity.rs

+5
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ impl ScalarSensitivityT<Spacecraft, Spacecraft, GroundStation>
229229
_tx: PhantomData::<_>,
230230
})
231231
}
232+
MeasurementType::ReceiveFrequency | MeasurementType::TransmitFrequency => {
233+
Err(ODError::MeasurementSimError {
234+
details: format!("{msr_type:?} is only supported in CCSDS TDM parsing"),
235+
})
236+
}
232237
}
233238
}
234239
}

src/od/msr/trackingdata/io_ccsds_tdm.rs

+150-4
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ use crate::io::watermark::prj_name_ver;
2020
use crate::io::ExportCfg;
2121
use crate::io::{InputOutputError, StdIOSnafu};
2222
use crate::od::msr::{Measurement, MeasurementType};
23+
use anise::constants::SPEED_OF_LIGHT_KM_S;
2324
use hifitime::efmt::{Format, Formatter};
2425
use hifitime::prelude::Epoch;
2526
use hifitime::TimeScale;
26-
use indexmap::IndexMap;
27+
use indexmap::{IndexMap, IndexSet};
2728
use snafu::ResultExt;
2829
use std::collections::{BTreeMap, HashMap};
2930
use std::fs::File;
@@ -44,13 +45,18 @@ impl TrackingDataArc {
4445
action: "opening CCSDS TDM file for tracking arc",
4546
})?;
4647

48+
let source = path.as_ref().to_path_buf().display().to_string();
49+
info!("parsing CCSDS TDM {source}");
50+
4751
let mut measurements = BTreeMap::new();
52+
let mut metadata = HashMap::new();
4853

4954
let reader = BufReader::new(file);
5055

5156
let mut in_data_section = false;
5257
let mut current_tracker = String::new();
5358
let mut time_system = TimeScale::UTC;
59+
let mut has_freq_data = false;
5460

5561
for line in reader.lines() {
5662
let line = line.context(StdIOSnafu {
@@ -88,10 +94,27 @@ impl TrackingDataArc {
8894
}
8995
}
9096
}
97+
98+
let mut splt = line.split('=');
99+
if let Some(keyword) = splt.nth(0) {
100+
// Get the zeroth item again since we've consumed the first zeroth one.
101+
if let Some(value) = splt.nth(0) {
102+
metadata.insert(keyword.trim().to_string(), value.trim().to_string());
103+
}
104+
}
105+
91106
continue;
92107
}
93108

94109
if let Some((mtype, epoch, value)) = parse_measurement_line(line, time_system)? {
110+
if [
111+
MeasurementType::ReceiveFrequency,
112+
MeasurementType::TransmitFrequency,
113+
]
114+
.contains(&mtype)
115+
{
116+
has_freq_data = true;
117+
}
95118
measurements
96119
.entry(epoch)
97120
.or_insert_with(|| Measurement {
@@ -104,10 +127,127 @@ impl TrackingDataArc {
104127
}
105128
}
106129

107-
Ok(Self {
130+
let mut turnaround_ratio = None;
131+
let drop_freq_data;
132+
if has_freq_data {
133+
// If there is any frequency measurement, compute the turn-around ratio.
134+
if let Some(ta_num_str) = metadata.get("TURNAROUND_NUMERATOR") {
135+
if let Some(ta_denom_str) = metadata.get("TURNAROUND_DENOMINATOR") {
136+
if let Ok(ta_num) = ta_num_str.parse::<i32>() {
137+
if let Ok(ta_denom) = ta_denom_str.parse::<i32>() {
138+
// turn-around ratio is set.
139+
turnaround_ratio = Some(f64::from(ta_num) / f64::from(ta_denom));
140+
info!("turn-around ratio is {ta_num}/{ta_denom}");
141+
drop_freq_data = false;
142+
} else {
143+
error!("turn-around denominator `{ta_denom_str}` is not a valid double precision float");
144+
drop_freq_data = true;
145+
}
146+
} else {
147+
error!("turn-around numerator `{ta_num_str}` is not a valid double precision float");
148+
drop_freq_data = true;
149+
}
150+
} else {
151+
error!("required turn-around denominator missing from metadata -- dropping ALL RECEIVE/TRANSMIT data");
152+
drop_freq_data = true;
153+
}
154+
} else {
155+
error!("required turn-around numerator missing from metadata -- dropping ALL RECEIVE/TRANSMIT data");
156+
drop_freq_data = true;
157+
}
158+
} else {
159+
drop_freq_data = true;
160+
}
161+
162+
// Now, let's convert the receive and transmit frequencies to Doppler measurements in velocity units.
163+
// We expect the transmit and receive frequencies to have the exact same timestamp.
164+
let mut freq_types = IndexSet::new();
165+
freq_types.insert(MeasurementType::ReceiveFrequency);
166+
freq_types.insert(MeasurementType::TransmitFrequency);
167+
let mut latest_transmit_freq = None;
168+
for (epoch, measurement) in measurements.iter_mut() {
169+
if drop_freq_data {
170+
for freq in &freq_types {
171+
measurement.data.swap_remove(freq);
172+
}
173+
continue;
174+
}
175+
176+
let avail = measurement.availability(&freq_types);
177+
let use_prev_transmit_freq;
178+
let num_freq_msr = avail
179+
.iter()
180+
.copied()
181+
.map(|v| if v { 1 } else { 0 })
182+
.sum::<u8>();
183+
if num_freq_msr == 0 {
184+
// No frequency measurements
185+
continue;
186+
} else if num_freq_msr == 1 {
187+
// avail[0] means that Receive Freq is available
188+
// avail[1] means that Transmit Freq is available
189+
// We can only compute Doppler data from one data point if that data point
190+
// if the receive frequency and the transmit frequency was previously set.
191+
if latest_transmit_freq.is_some() && avail[0] {
192+
use_prev_transmit_freq = true;
193+
warn!(
194+
"no transmit frequency at {epoch}, using previous value of {} Hz",
195+
latest_transmit_freq.unwrap()
196+
);
197+
} else {
198+
warn!("only one of receive or transmit frequencies found at {epoch}, ignoring");
199+
for freq in &freq_types {
200+
measurement.data.swap_remove(freq);
201+
}
202+
continue;
203+
}
204+
} else {
205+
use_prev_transmit_freq = false;
206+
}
207+
208+
if !use_prev_transmit_freq {
209+
// Update the latest transmit frequency since it's set.
210+
latest_transmit_freq = Some(
211+
*measurement
212+
.data
213+
.get(&MeasurementType::TransmitFrequency)
214+
.unwrap(),
215+
);
216+
}
217+
218+
let transmit_freq_hz = latest_transmit_freq.unwrap();
219+
let receive_freq_hz = *measurement
220+
.data
221+
.get(&MeasurementType::ReceiveFrequency)
222+
.unwrap();
223+
224+
// Compute the Doppler shift, equation from section 3.5.2.8.2 of CCSDS TDM v2 specs
225+
let doppler_shift_hz = transmit_freq_hz * turnaround_ratio.unwrap() - receive_freq_hz;
226+
// Compute the expected Doppler measurement as range-rate.
227+
let rho_dot_km_s = (doppler_shift_hz * SPEED_OF_LIGHT_KM_S)
228+
/ (2.0 * transmit_freq_hz * turnaround_ratio.unwrap());
229+
230+
// Finally, replace the frequency data with a Doppler measurement.
231+
for freq in &freq_types {
232+
measurement.data.swap_remove(freq);
233+
}
234+
measurement
235+
.data
236+
.insert(MeasurementType::Doppler, rho_dot_km_s);
237+
}
238+
239+
let trk = Self {
108240
measurements,
109-
source: Some(path.as_ref().to_path_buf().display().to_string()),
110-
})
241+
source: Some(source),
242+
};
243+
244+
if trk.unique_types().is_empty() {
245+
Err(InputOutputError::EmptyDataset {
246+
action: "CCSDS TDM file",
247+
})
248+
} else {
249+
Ok(trk)
250+
}
111251
}
112252

113253
/// Store this tracking arc to a CCSDS TDM file, with optional metadata and a timestamp appended to the filename.
@@ -254,6 +394,8 @@ impl TrackingDataArc {
254394
MeasurementType::Doppler => "DOPPLER_INTEGRATED",
255395
MeasurementType::Azimuth => "ANGLE_1",
256396
MeasurementType::Elevation => "ANGLE_2",
397+
MeasurementType::ReceiveFrequency => "RECEIVE_FREQ",
398+
MeasurementType::TransmitFrequency => "TRANSMIT_FREQ",
257399
};
258400

259401
writeln!(
@@ -295,6 +437,10 @@ fn parse_measurement_line(
295437
"DOPPLER_INSTANTANEOUS" | "DOPPLER_INTEGRATED" => MeasurementType::Doppler,
296438
"ANGLE_1" => MeasurementType::Azimuth,
297439
"ANGLE_2" => MeasurementType::Elevation,
440+
"RECEIVE_FREQ" | "RECEIVE_FREQ_1" | "RECEIVE_FREQ_2" | "RECEIVE_FREQ_3"
441+
| "RECEIVE_FREQ_4" | "RECEIVE_FREQ_5" => MeasurementType::ReceiveFrequency,
442+
"TRANSMIT_FREQ" | "TRANSMIT_FREQ_1" | "TRANSMIT_FREQ_2" | "TRANSMIT_FREQ_3"
443+
| "TRANSMIT_FREQ_4" | "TRANSMIT_FREQ_5" => MeasurementType::TransmitFrequency,
298444
_ => {
299445
return Err(InputOutputError::UnsupportedData {
300446
which: mtype_str.to_string(),

src/od/msr/trackingdata/io_parquet.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ impl TrackingDataArc {
228228
ensure!(
229229
!self.is_empty(),
230230
EmptyDatasetSnafu {
231-
action: "exporting tracking data arc"
231+
action: "tracking data arc to parquet"
232232
}
233233
);
234234

src/od/msr/types.rs

+11
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ pub enum MeasurementType {
3333
Azimuth,
3434
#[serde(rename = "elevation_deg")]
3535
Elevation,
36+
#[serde(rename = "receive_freq")]
37+
ReceiveFrequency,
38+
#[serde(rename = "transmit_freq")]
39+
TransmitFrequency,
3640
}
3741

3842
impl MeasurementType {
@@ -42,6 +46,7 @@ impl MeasurementType {
4246
Self::Range => "km",
4347
Self::Doppler => "km/s",
4448
Self::Azimuth | Self::Elevation => "deg",
49+
Self::ReceiveFrequency | Self::TransmitFrequency => "Hz",
4550
}
4651
}
4752

@@ -66,6 +71,9 @@ impl MeasurementType {
6671
Self::Doppler => Ok(aer.range_rate_km_s + noise),
6772
Self::Azimuth => Ok(aer.azimuth_deg + noise),
6873
Self::Elevation => Ok(aer.elevation_deg + noise),
74+
Self::ReceiveFrequency | Self::TransmitFrequency => Err(ODError::MeasurementSimError {
75+
details: format!("{self:?} is only supported in CCSDS TDM parsing"),
76+
}),
6977
}
7078
}
7179

@@ -94,6 +102,9 @@ impl MeasurementType {
94102
let el_deg = (aer_t1.elevation_deg + aer_t0.elevation_deg) * 0.5;
95103
Ok(el_deg + noise / 2.0_f64.sqrt())
96104
}
105+
Self::ReceiveFrequency | Self::TransmitFrequency => Err(ODError::MeasurementSimError {
106+
details: format!("{self:?} is only supported in CCSDS TDM parsing"),
107+
}),
97108
}
98109
}
99110
}

0 commit comments

Comments
 (0)