nyx_space/io/gravity.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::DMatrix;
20use crate::NyxError;
21use flate2::read::GzDecoder;
22use std::fs::File;
23use std::io::prelude::*;
24use std::str::FromStr;
25
26/// `HarmonicsMem` loads the requested gravity potential files and stores them in memory (in a HashMap).
27///
28/// WARNING: This memory backend may require a lot of RAM (e.g. EMG2008 2190x2190 requires nearly 400 MB of RAM).
29#[derive(Clone)]
30pub struct HarmonicsMem {
31 degree: usize,
32 order: usize,
33 c_nm: DMatrix<f64>,
34 s_nm: DMatrix<f64>,
35}
36
37impl HarmonicsMem {
38 /// Initialize `HarmonicsMem` with a custom J2 value
39 pub fn from_j2(j2: f64) -> HarmonicsMem {
40 let mut c_nm = DMatrix::from_element(3, 3, 0.0);
41 c_nm[(2, 0)] = j2;
42
43 HarmonicsMem {
44 degree: 2 + 1,
45 order: 0,
46 c_nm,
47 s_nm: DMatrix::from_element(3, 3, 0.0),
48 }
49 }
50
51 /// Initialize `HarmonicsMem` as an EARTH J<sub>2</sub> only using the JGM3 model (available in GMAT)
52 ///
53 /// Use the embedded Earth parameter. If others are needed, load from `from_shadr` or `from_egm`.
54 /// *WARNING:* This is an EARTH gravity model, and _should not_ be used around any other body.
55 pub fn j2_jgm3() -> HarmonicsMem {
56 Self::from_j2(-4.841_653_748_864_70e-04)
57 }
58
59 /// Initialize `HarmonicsMem` as an EARTH J<sub>2</sub> only using the JGM2 model (available in GMAT)
60 ///
61 /// Use the embedded Earth parameter. If others are needed, load from `from_shadr` or `from_egm`.
62 /// *WARNING:* This is an EARTH gravity model, and _should not_ be used around any other body.
63 pub fn j2_jgm2() -> HarmonicsMem {
64 Self::from_j2(-4.841_653_9e-04)
65 }
66
67 /// Initialize `HarmonicsMem` as J<sub>2</sub> only using the EGM2008 model (from the GRACE mission, best model as of 2018)
68 ///
69 /// *WARNING:* This is an EARTH gravity model, and _should not_ be used around any other body.
70 pub fn j2_egm2008() -> HarmonicsMem {
71 Self::from_j2(-0.484_165_143_790_815e-03)
72 }
73
74 /// Initialize `HarmonicsMem` from the file path (must be a gunzipped file)
75 ///
76 /// Gravity models provided by `nyx`:
77 /// + EMG2008 to 2190 for Earth (tide free)
78 /// + Moon to 1500 (from SHADR file)
79 /// + Mars to 120 (from SHADR file)
80 /// + Venus to 150 (from SHADR file)
81 pub fn from_shadr(
82 filepath: &str,
83 degree: usize,
84 order: usize,
85 gunzipped: bool,
86 ) -> Result<HarmonicsMem, NyxError> {
87 Self::load(
88 gunzipped, true, //SHADR has a header which we ignore
89 degree, order, filepath,
90 )
91 }
92
93 pub fn from_egm(
94 filepath: &str,
95 degree: usize,
96 order: usize,
97 gunzipped: bool,
98 ) -> Result<HarmonicsMem, NyxError> {
99 Self::load(gunzipped, false, degree, order, filepath)
100 }
101
102 pub fn from_cof(
103 filepath: &str,
104 degree: usize,
105 order: usize,
106 gunzipped: bool,
107 ) -> Result<HarmonicsMem, NyxError> {
108 let mut f = File::open(filepath).map_err(|_| NyxError::FileUnreadable {
109 msg: format!("File not found: {filepath}"),
110 })?;
111 let mut buffer = vec![0; 0];
112 if gunzipped {
113 let mut d = GzDecoder::new(f);
114 d.read_to_end(&mut buffer)
115 .map_err(|_| NyxError::FileUnreadable {
116 msg: "could not read file as gunzip".to_string(),
117 })?;
118 } else {
119 f.read_to_end(&mut buffer)
120 .map_err(|_| NyxError::FileUnreadable {
121 msg: "could not read file to end".to_string(),
122 })?;
123 }
124
125 let data_as_str = String::from_utf8(buffer).map_err(|_| NyxError::FileUnreadable {
126 msg: "could not decode file contents as utf8".to_string(),
127 })?;
128
129 // Since the COF files are so specific, we just code everything up in here.
130
131 let mut c_nm_mat = DMatrix::from_element(degree + 1, degree + 1, 0.0);
132 let mut s_nm_mat = DMatrix::from_element(degree + 1, degree + 1, 0.0);
133 let mut max_order: usize = 0;
134 let mut max_degree: usize = 0;
135 for (lno, line) in data_as_str.split('\n').enumerate() {
136 if line.is_empty() || !line.starts_with('R') {
137 continue; // This is either a comment, a header or "END"
138 }
139 // These variables need to be declared as mutable because rustc does not know
140 // we nwon't match each ino more than once.
141 let mut cur_degree: usize = 0;
142 let mut cur_order: usize = 0;
143 let mut c_nm: f64 = 0.0;
144 let mut s_nm: f64 = 0.0;
145 for (ino, item) in line.split_whitespace().enumerate() {
146 match ino {
147 0 => continue, // We need this so we don't break at every first item
148 1 => match usize::from_str(item) {
149 Ok(val) => cur_degree = val,
150 Err(_) => {
151 return Err(NyxError::FileUnreadable {
152 msg: format!(
153 "Harmonics file:
154 could not parse degree `{item}` on line {lno}"
155 ),
156 });
157 }
158 },
159 2 => match usize::from_str(item) {
160 Ok(val) => cur_order = val,
161 Err(_) => {
162 return Err(NyxError::FileUnreadable {
163 msg: format!(
164 "Harmonics file:
165 could not parse order `{item}` on line {lno}"
166 ),
167 });
168 }
169 },
170 3 => {
171 // If we are at degree zero, then there is only one item, so we can parse that and
172 // set the S_nm to zero.
173 if degree == 0 {
174 s_nm = 0.0;
175 match f64::from_str(item) {
176 Ok(val) => c_nm = val,
177 Err(_) => {
178 return Err(NyxError::FileUnreadable {
179 msg: format!(
180 "Harmonics file:
181 could not parse C_nm `{item}` on line {lno}"
182 ),
183 });
184 }
185 }
186 } else {
187 // There is a space as a delimiting character between the C_nm and S_nm only if the S_nm
188 // is a positive number, otherwise, they are continuous (what a great format).
189 if (item.matches('-').count() == 3 && !item.starts_with('-'))
190 || item.matches('-').count() == 4
191 {
192 // Now we have two items concatenated into one... great
193 let parts: Vec<&str> = item.split('-').collect();
194 if parts.len() == 5 {
195 // That mean we have five minus signs, so both the C and S are negative.
196 let c_nm_str = "-".to_owned() + parts[1] + "-" + parts[2];
197 match f64::from_str(&c_nm_str) {
198 Ok(val) => c_nm = val,
199 Err(_) => {
200 return Err(NyxError::FileUnreadable {
201 msg: format!(
202 "Harmonics file:
203 could not parse C_nm `{item}` on line {lno}"
204 ),
205 });
206 }
207 }
208 // That mean we have five minus signs, so both the C and S are negative.
209 let s_nm_str = "-".to_owned() + parts[3] + "-" + parts[4];
210 match f64::from_str(&s_nm_str) {
211 Ok(val) => s_nm = val,
212 Err(_) => {
213 return Err(NyxError::FileUnreadable {
214 msg: format!(
215 "Harmonics file:
216 could not parse S_nm `{item}` on line {lno}"
217 ),
218 });
219 }
220 }
221 } else {
222 // That mean we have fouve minus signs, and since both values are concatenated, C_nm is positive and S_nm is negative
223 let c_nm_str = parts[0].to_owned() + "-" + parts[1];
224 match f64::from_str(&c_nm_str) {
225 Ok(val) => c_nm = val,
226 Err(_) => {
227 return Err(NyxError::FileUnreadable {
228 msg: format!(
229 "Harmonics file:
230 could not parse C_nm `{item}` on line {lno}"
231 ),
232 });
233 }
234 }
235 // That mean we have five minus signs, so both the C and S are negative.
236 let s_nm_str = "-".to_owned() + parts[2] + "-" + parts[3];
237 match f64::from_str(&s_nm_str) {
238 Ok(val) => s_nm = val,
239 Err(_) => {
240 return Err(NyxError::FileUnreadable {
241 msg: format!(
242 "Harmonics file:
243 could not parse S_nm `{item}` on line {lno}"
244 ),
245 });
246 }
247 }
248 }
249 } else {
250 // We only have the first item, and that's the C_nm
251 match f64::from_str(item) {
252 Ok(val) => c_nm = val,
253 Err(_) => {
254 return Err(NyxError::FileUnreadable {
255 msg: format!(
256 "Harmonics file:
257 could not parse C_nm `{item}` on line {lno}"
258 ),
259 });
260 }
261 }
262 }
263 }
264 }
265 4 => match f64::from_str(item) {
266 // If this exists, then the S_nm is positive.
267 Ok(val) => s_nm = val,
268 Err(_) => {
269 return Err(NyxError::FileUnreadable {
270 msg: format!(
271 "Harmonics file:
272 could not parse S_nm `{item}` on line {lno}"
273 ),
274 });
275 }
276 },
277 _ => break, // We aren't storing the covariance of these harmonics
278 }
279 }
280
281 if cur_degree > degree {
282 // The file is organized by degree, so once we've passed the maximum degree we want,
283 // we can safely stop reading the file.
284 break;
285 }
286
287 // Only insert this data into the hashmap if it's within the required order as well
288 if cur_order <= order {
289 c_nm_mat[(cur_degree, cur_order)] = c_nm;
290 s_nm_mat[(cur_degree, cur_order)] = s_nm;
291 }
292 // This serves as a warning.
293 max_order = if cur_order > max_order {
294 cur_order
295 } else {
296 max_order
297 };
298 max_degree = if cur_degree > max_degree {
299 cur_degree
300 } else {
301 max_degree
302 };
303 }
304 if max_degree < degree || max_order < order {
305 warn!(
306 "{} only contained (degree, order) of ({}, {}) instead of requested ({}, {})",
307 filepath, max_degree, max_order, degree, order
308 );
309 } else {
310 info!(
311 "{} loaded with (degree, order) = ({}, {})",
312 filepath, degree, order
313 );
314 }
315 Ok(HarmonicsMem {
316 degree: max_degree,
317 order: max_order,
318 c_nm: c_nm_mat,
319 s_nm: s_nm_mat,
320 })
321 }
322
323 /// `load` handles the actual loading in memory.
324 fn load(
325 gunzipped: bool,
326 skip_first_line: bool,
327 degree: usize,
328 order: usize,
329 filepath: &str,
330 ) -> Result<HarmonicsMem, NyxError> {
331 let mut f = File::open(filepath).map_err(|_| NyxError::FileUnreadable {
332 msg: format!("File not found: {filepath}"),
333 })?;
334 let mut buffer = vec![0; 0];
335 if gunzipped {
336 let mut d = GzDecoder::new(f);
337 d.read_to_end(&mut buffer)
338 .map_err(|_| NyxError::FileUnreadable {
339 msg: "could not read file as gunzip".to_string(),
340 })?;
341 } else {
342 f.read_to_end(&mut buffer)
343 .map_err(|_| NyxError::FileUnreadable {
344 msg: "could not read file to end".to_string(),
345 })?;
346 }
347
348 let data_as_str = String::from_utf8(buffer).map_err(|_| NyxError::FileUnreadable {
349 msg: "could not decode file contents as utf8".to_string(),
350 })?;
351
352 let mut c_nm_mat = DMatrix::from_element(degree + 1, degree + 1, 0.0);
353 let mut s_nm_mat = DMatrix::from_element(degree + 1, degree + 1, 0.0);
354
355 let mut max_degree: usize = 0;
356 let mut max_order: usize = 0;
357 for (lno, line) in data_as_str.split('\n').enumerate() {
358 if lno == 0 && skip_first_line {
359 continue;
360 }
361 // These variables need to be declared as mutable because rustc does not know
362 // we won't match each ino more than once.
363 let mut cur_order: usize = 0;
364 let mut cur_degree: usize = 0;
365 let mut c_nm: f64 = 0.0;
366 let mut s_nm: f64 = 0.0;
367 for (ino, item) in line.replace(',', " ").split_whitespace().enumerate() {
368 match ino {
369 0 => match usize::from_str(item) {
370 Ok(val) => cur_degree = val,
371 Err(_) => {
372 return Err(NyxError::FileUnreadable {
373 msg: format!(
374 "Harmonics file:
375 could not parse degree on line {lno} (`{item}`)",
376 ),
377 });
378 }
379 },
380 1 => match usize::from_str(item) {
381 Ok(val) => cur_order = val,
382 Err(_) => {
383 return Err(NyxError::FileUnreadable {
384 msg: format!(
385 "Harmonics file:
386 could not parse order on line {lno} (`{item}`)"
387 ),
388 });
389 }
390 },
391 2 => match f64::from_str(&item.replace('D', "E")) {
392 Ok(val) => c_nm = val,
393 Err(_) => {
394 return Err(NyxError::FileUnreadable {
395 msg: format!(
396 "Harmonics file:
397 could not parse C_nm `{item}` on line {lno}"
398 ),
399 });
400 }
401 },
402 3 => match f64::from_str(&item.replace('D', "E")) {
403 Ok(val) => s_nm = val,
404 Err(_) => {
405 return Err(NyxError::FileUnreadable {
406 msg: format!(
407 "Harmonics file:
408 could not parse S_nm `{item}` on line {lno}"
409 ),
410 });
411 }
412 },
413 _ => break, // We aren't storing the covariance of these harmonics
414 }
415 }
416
417 if cur_degree > degree {
418 // The file is organized by degree, so once we've passed the maximum degree we want,
419 // we can safely stop reading the file.
420 break;
421 }
422
423 // Only insert this data into the hashmap if it's within the required order as well
424 if cur_order <= order {
425 c_nm_mat[(cur_degree, cur_order)] = c_nm;
426 s_nm_mat[(cur_degree, cur_order)] = s_nm;
427 }
428 // This serves as a warning.
429 max_order = if cur_order > max_order {
430 cur_order
431 } else {
432 max_order
433 };
434 max_degree = if cur_degree > max_degree {
435 cur_degree
436 } else {
437 max_degree
438 };
439 }
440 if max_degree < degree || max_order < order {
441 warn!(
442 "{filepath} only contained (degree, order) of ({max_degree}, {max_order}) instead of requested ({degree}, {order})",
443 );
444 } else {
445 info!("{filepath} loaded with (degree, order) = ({degree}, {order})");
446 }
447 Ok(HarmonicsMem {
448 order: max_order,
449 degree: max_degree,
450 c_nm: c_nm_mat,
451 s_nm: s_nm_mat,
452 })
453 }
454
455 /// Returns the maximum order of this gravity potential storage (Jnm=Jn2,Jn3...)
456 pub fn max_order_m(&self) -> usize {
457 self.order
458 }
459
460 /// Returns the maximum degree of this gravity potential storage (Jn=J2,J3...)
461 pub fn max_degree_n(&self) -> usize {
462 self.degree
463 }
464
465 /// Returns the C_nm and S_nm for the provided order and degree.
466 pub fn cs_nm(&self, degree: usize, order: usize) -> (f64, f64) {
467 (self.c_nm[(degree, order)], self.s_nm[(degree, order)])
468 }
469}
470
471#[test]
472fn test_load_harmonic_files() {
473 HarmonicsMem::from_cof("data/JGM3.cof.gz", 50, 50, true).expect("could not load JGM3");
474
475 HarmonicsMem::from_egm("data/EGM2008_to2190_TideFree.gz", 120, 120, true)
476 .expect("could not load EGM2008");
477
478 HarmonicsMem::from_shadr("data/Luna_jggrx_1500e_sha.tab.gz", 1500, 1500, true)
479 .expect("could not load jggrx");
480}