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