nyx_space/od/groundpnt/sensitivity.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*/
18
19use crate::linalg::DefaultAllocator;
20use crate::linalg::allocator::Allocator;
21use crate::od::groundpnt::GroundAsset;
22use crate::od::interlink::InterlinkTxSpacecraft;
23use crate::od::msr::{Measurement, MeasurementType, sensitivity::ScalarSensitivity};
24use crate::od::prelude::sensitivity::{ScalarSensitivityT, TrackerSensitivity};
25use crate::od::{ODAlmanacSnafu, ODError};
26use crate::{Spacecraft, State};
27use anise::errors::AlmanacError;
28use anise::prelude::Almanac;
29use indexmap::IndexSet;
30use nalgebra::{DimName, OMatrix, U1};
31use snafu::ResultExt;
32use std::marker::PhantomData;
33
34impl TrackerSensitivity<GroundAsset, GroundAsset> for InterlinkTxSpacecraft
35where
36 DefaultAllocator: Allocator<<Spacecraft as State>::Size>
37 + Allocator<<Spacecraft as State>::VecLength>
38 + Allocator<<Spacecraft as State>::Size, <Spacecraft as State>::Size>,
39{
40 fn h_tilde<M: DimName>(
41 &self,
42 msr: &Measurement,
43 msr_types: &IndexSet<MeasurementType>,
44 rx: &GroundAsset,
45 almanac: &Almanac,
46 ) -> Result<OMatrix<f64, M, <GroundAsset as State>::Size>, ODError>
47 where
48 DefaultAllocator: Allocator<M> + Allocator<M, <GroundAsset as State>::Size>,
49 {
50 // Rebuild each row of the scalar sensitivities.
51 let mut mat = OMatrix::<f64, M, <GroundAsset as State>::Size>::identity();
52 for (ith_row, msr_type) in msr_types.iter().enumerate() {
53 if !msr.data.contains_key(msr_type) {
54 // Skip computation, this row is zero anyway.
55 continue;
56 }
57 let scalar_h =
58 <ScalarSensitivity<GroundAsset, GroundAsset, InterlinkTxSpacecraft> as ScalarSensitivityT<
59 GroundAsset,
60 GroundAsset,
61 InterlinkTxSpacecraft,
62 >>::new(*msr_type, msr, rx, self, almanac)?;
63
64 mat.set_row(ith_row, &scalar_h.sensitivity_row);
65 }
66 Ok(mat)
67 }
68}
69
70impl ScalarSensitivityT<GroundAsset, GroundAsset, InterlinkTxSpacecraft>
71 for ScalarSensitivity<GroundAsset, GroundAsset, InterlinkTxSpacecraft>
72{
73 /// First, we ensure that the transmitter vehicle is expressed in the same frame as the
74 /// ground asset. Then we compute the AER as seen from the ground asset.
75 fn new(
76 msr_type: MeasurementType,
77 msr: &Measurement,
78 rx: &GroundAsset,
79 tx: &InterlinkTxSpacecraft,
80 almanac: &Almanac,
81 ) -> Result<Self, ODError> {
82 let receiver = rx.orbit();
83 let loc = rx.to_location();
84
85 // Compute the device location in the receiver frame because we compute the sensitivity in that frame.
86 // This frame is required because the scalar measurements are frame independent, but the sensitivity
87 // must be in the estimation frame.
88
89 let transmitter = tx
90 .traj
91 .at(receiver.epoch)
92 .map_err(|source| ODError::ODTrajError {
93 source,
94 details: "computing sensitivity ground asset / interlink".into(),
95 })?
96 .orbit;
97
98 let jac = rx
99 .geodetic_to_cartesian_jacobian()
100 .map_err(|e| ODError::ODAlmanac {
101 source: Box::new(AlmanacError::AlmanacPhysics {
102 action: "computing Jacobian for geodetics",
103 source: Box::new(e),
104 }),
105 action: "computing Jacobian for geodetics",
106 })?;
107
108 let delta_r = receiver.radius_km - transmitter.radius_km;
109 let delta_v = receiver.velocity_km_s - transmitter.velocity_km_s;
110
111 match msr_type {
112 MeasurementType::Doppler => {
113 // If we have a simultaneous measurement of the range, use that, otherwise we compute the expected range.
114 let ρ_km = match msr.data.get(&MeasurementType::Range) {
115 Some(range_km) => *range_km,
116 None => {
117 almanac
118 .azimuth_elevation_range_sez_from_location(transmitter, loc, None, None)
119 .context(ODAlmanacSnafu {
120 action: "computing range for Doppler measurement",
121 })?
122 .range_km
123 }
124 };
125
126 let ρ_dot_km_s = msr.data.get(&MeasurementType::Doppler).unwrap();
127 let m11 = delta_r.x / ρ_km;
128 let m12 = delta_r.y / ρ_km;
129 let m13 = delta_r.z / ρ_km;
130 let m21 = delta_v.x / ρ_km - ρ_dot_km_s * delta_r.x / ρ_km.powi(2);
131 let m22 = delta_v.y / ρ_km - ρ_dot_km_s * delta_r.y / ρ_km.powi(2);
132 let m23 = delta_v.z / ρ_km - ρ_dot_km_s * delta_r.z / ρ_km.powi(2);
133
134 let sensitivity_row =
135 OMatrix::<f64, U1, <GroundAsset as State>::Size>::from_row_slice(&[
136 m21, m22, m23, m11, m12, m13,
137 ]) * jac;
138
139 Ok(Self {
140 sensitivity_row,
141 _rx: PhantomData::<_>,
142 _tx: PhantomData::<_>,
143 })
144 }
145 MeasurementType::Range => {
146 let ρ_km = msr.data.get(&MeasurementType::Range).unwrap();
147 let m11 = delta_r.x / ρ_km;
148 let m12 = delta_r.y / ρ_km;
149 let m13 = delta_r.z / ρ_km;
150
151 let sensitivity_row =
152 OMatrix::<f64, U1, <GroundAsset as State>::Size>::from_row_slice(&[
153 m11, m12, m13, 0.0, 0.0, 0.0,
154 ]) * jac;
155
156 Ok(Self {
157 sensitivity_row,
158 _rx: PhantomData::<_>,
159 _tx: PhantomData::<_>,
160 })
161 }
162 _ => Err(ODError::MeasurementSimError {
163 details: format!("{msr_type:?} is only supported in CCSDS TDM parsing"),
164 }),
165 }
166 // let rx_orbit = rx.orbit();
167
168 // // Compute the SEZ DCM
169 // // SEZ DCM is topo to fixed
170 // let sez_dcm = rx_orbit
171 // .dcm_from_topocentric_to_body_fixed()
172 // .context(EphemerisPhysicsSnafu { action: "" })
173 // .context(EphemerisSnafu {
174 // action: "computing SEZ DCM for sensitivity",
175 // })
176 // .context(ODAlmanacSnafu { action: "" })?;
177
178 // let rx_sez = (sez_dcm.transpose() * rx_orbit)
179 // .context(EphemerisPhysicsSnafu { action: "" })
180 // .context(EphemerisSnafu {
181 // action: "transforming ground asset to SEZ",
182 // })
183 // .context(ODAlmanacSnafu { action: "" })?;
184
185 // // Convert the transmitter/PNT vehicle into the body fixed transmitter frame.
186 // let tx_in_rx_frame = almanac
187 // .transform_to(tx.traj.at(rx.epoch).unwrap().orbit, rx.frame, None)
188 // .context(ODAlmanacSnafu {
189 // action: "computing transmitter location when computing sensitivity matrix",
190 // })?;
191
192 // // Convert into SEZ frame
193 // let tx_sez = (sez_dcm.transpose() * tx_in_rx_frame)
194 // .context(EphemerisPhysicsSnafu { action: "" })
195 // .context(EphemerisSnafu {
196 // action: "transforming received to SEZ",
197 // })
198 // .context(ODAlmanacSnafu { action: "" })?;
199
200 // // Compute the range ρ in the SEZ frame
201 // let delta_r_km = tx_sez.radius_km - rx_sez.radius_km;
202 // let ρ_km_sez = delta_r_km.norm();
203 // // Compute the velocity difference - BUT note that rx_in_tx_frame is already the relative velocity of rx wrt tx!
204 // let delta_v_km_s = tx_in_rx_frame.velocity_km_s;
205
206 // match msr_type {
207 // MeasurementType::Doppler => {
208 // // If we have a simultaneous measurement of the range, use that, otherwise we compute the expected range.
209 // let ρ_km = match msr.data.get(&MeasurementType::Range) {
210 // Some(range_km) => *range_km,
211 // None => ρ_km_sez,
212 // };
213
214 // let ρ_dot_km_s = msr.data.get(&MeasurementType::Doppler).unwrap();
215 // let m11 = delta_r_km.x / ρ_km;
216 // let m12 = delta_r_km.y / ρ_km;
217 // let m13 = delta_r_km.z / ρ_km;
218 // let m21 = delta_v_km_s.x / ρ_km - ρ_dot_km_s * delta_r_km.x / ρ_km.powi(2);
219 // let m22 = delta_v_km_s.y / ρ_km - ρ_dot_km_s * delta_r_km.y / ρ_km.powi(2);
220 // let m23 = delta_v_km_s.z / ρ_km - ρ_dot_km_s * delta_r_km.z / ρ_km.powi(2);
221
222 // let sensitivity_row =
223 // OMatrix::<f64, U1, <GroundAsset as State>::Size>::from_row_slice(&[
224 // m21, m22, m23, m11, m12, m13,
225 // ]);
226
227 // Ok(Self {
228 // sensitivity_row,
229 // _rx: PhantomData::<_>,
230 // _tx: PhantomData::<_>,
231 // })
232 // }
233 // MeasurementType::Range => {
234 // let ρ_km = msr.data.get(&MeasurementType::Range).unwrap();
235 // let m11 = delta_r_km.x / ρ_km;
236 // let m12 = delta_r_km.y / ρ_km;
237 // let m13 = delta_r_km.z / ρ_km;
238
239 // let sensitivity_row =
240 // OMatrix::<f64, U1, <GroundAsset as State>::Size>::from_row_slice(&[
241 // m11, m12, m13, 0.0, 0.0, 0.0,
242 // ]);
243
244 // Ok(Self {
245 // sensitivity_row,
246 // _rx: PhantomData::<_>,
247 // _tx: PhantomData::<_>,
248 // })
249 // }
250 // MeasurementType::Azimuth
251 // | MeasurementType::Elevation
252 // | MeasurementType::ReceiveFrequency
253 // | MeasurementType::TransmitFrequency => Err(ODError::MeasurementSimError {
254 // details: format!("{msr_type:?} is not supported for interlink"),
255 // }),
256 // }
257 }
258}