predict_rs/
observer.rs

1use crate::consts::{
2    DEG_TO_RAD, EARTH_ANGULAR_VELOCITY, EARTH_RADIUS_KM_WGS84, EARTH_ROTATIONS_PER_SIDERIAL_DAY,
3    FLATTENING_FACTOR, JULIAN_TIME_DIFF, MAXELE_MAX_NUM_ITERATIONS, MAXELE_TIME_EQUALITY_THRESHOLD,
4    MINUTES_PER_DAY, NAUTICAL_TWILIGHT_SUN_ELEVATION, RAD_TO_DEG, SECONDS_PER_DAY,
5};
6use crate::geodetic::Geodetic;
7use crate::julian_date::{predict_to_julian_double, theta_g_jd};
8use crate::math::{asin_clamped, fmod2p, vec3_dot, vec3_length, vec3_sub, Vec3};
9use crate::orbit::{
10    predict_aos_happens, predict_is_geosynchronous, predict_orbit, OrbitPredictionError,
11};
12use crate::predict::{
13    ObserverElements, Pass, Passes, PredictObservation, PredictObserver, PredictPosition,
14};
15use crate::sun::predict_observe_sun;
16use core::f64::consts::PI;
17
18/// Pass stepping direction used for pass stepping function below.
19#[non_exhaustive]
20#[derive(PartialEq)]
21pub enum StepPassDirection {
22    PositiveDirection,
23    NegativeDirection,
24}
25
26/// Refinement mode, AOS or LOS
27#[non_exhaustive]
28#[derive(Debug, PartialEq)]
29pub enum RefineMode {
30    AOS,
31    LOS,
32}
33
34/// Calculates range, azimuth, elevation and relative velocity from the given observer position.
35/// and whether orbit is visible if sun elevation is low enough and the orbit is above the horizon, but still in sunlight.
36#[must_use]
37pub fn predict_observe_orbit(
38    observer: &PredictObserver,
39    orbit: &PredictPosition,
40) -> PredictObservation {
41    let mut observation = observer_calculate(observer, orbit);
42    observation.visible = false;
43
44    let sun_obs = predict_observe_sun(observer, orbit.time);
45    if !orbit.eclipsed
46        && (sun_obs.elevation * 180.0 / PI < NAUTICAL_TWILIGHT_SUN_ELEVATION)
47        && (observation.elevation * 180.0 / PI > 0.0)
48    {
49        observation.visible = true;
50    }
51    observation
52}
53
54/// The procedures `observer_calculate` calculates
55#[must_use]
56#[allow(clippy::similar_names)]
57pub fn observer_calculate(
58    observer: &PredictObserver,
59    orbit: &PredictPosition,
60) -> PredictObservation {
61    let jultime = predict_to_julian_double(orbit.time) + JULIAN_TIME_DIFF;
62
63    let mut geodetic = Geodetic {
64        lat: observer.latitude,
65        lon: observer.longitude,
66        alt: observer.altitude / 1000.0,
67        theta: 0.0,
68    };
69    let (obs_pos, obs_vel) = calculate_user_posvel(jultime, &mut geodetic);
70
71    let range = vec3_sub(orbit.position, obs_pos);
72    let rgvel = vec3_sub(orbit.velocity, obs_vel);
73
74    let range_length = vec3_length(range);
75    let range_rate_length = vec3_dot(range, rgvel) / range_length;
76
77    let theta_dot = 2.0 * PI * EARTH_ROTATIONS_PER_SIDERIAL_DAY / SECONDS_PER_DAY;
78    let sin_lat = geodetic.lat.sin();
79    let cos_lat = geodetic.lat.cos();
80    let sin_theta = geodetic.theta.sin();
81    let cos_theta = geodetic.theta.cos();
82
83    let top_s = sin_lat * cos_theta * range.0 + sin_lat * sin_theta * range.1 - cos_lat * range.2;
84    let top_e = -sin_theta * range.0 + cos_theta * range.1;
85    let top_z = cos_lat * cos_theta * range.0 + cos_lat * sin_theta * range.1 + sin_lat * range.2;
86
87    let top_s_dot = sin_lat * (cos_theta * rgvel.0 - sin_theta * range.0 * theta_dot)
88        + sin_lat * (sin_theta * rgvel.1 + cos_theta * range.1 * theta_dot)
89        - cos_lat * rgvel.2;
90    let top_e_dot = -(sin_theta * rgvel.0 + cos_theta * range.0 * theta_dot)
91        + (cos_theta * rgvel.1 - sin_theta * range.1 * theta_dot);
92
93    let top_z_dot = cos_lat
94        * (cos_theta * (rgvel.0 + range.1 * theta_dot)
95            + sin_theta * (rgvel.1 - range.0 * theta_dot))
96        + sin_lat * rgvel.2;
97
98    // Azimut
99    let y = -top_e / top_s;
100    let mut az = (-top_e / top_s).atan();
101
102    if top_s > 0.0 {
103        az += PI;
104    }
105    if az < 0.0 {
106        az += 2.0 * PI;
107    }
108
109    // Azimut rate
110    let y_dot = -(top_e_dot * top_s - top_s_dot * top_e) / (top_s * top_s);
111    let az_dot = y_dot / (1.0 + y * y);
112
113    // Elevation
114    let x = top_z / range_length;
115    let el = asin_clamped(x);
116
117    // Elevation rate
118    let x_dot =
119        (top_z_dot * range_length - range_rate_length * top_z) / (range_length * range_length);
120    let el_dot = x_dot / (1.0 - x * x).sqrt();
121    PredictObservation {
122        azimuth: az,
123        azimuth_rate: az_dot,
124        elevation: el,
125        elevation_rate: el_dot,
126        range: range_length,
127        range_rate: range_rate_length,
128        range_x: range.0,
129        range_y: range.1,
130        range_z: range.2,
131        time: orbit.time,
132        revolutions: orbit.revolutions,
133        visible: false,
134    }
135}
136
137/// # Errors
138///
139/// `predict_orbit` can fail
140pub fn predict_next_aos(
141    oe: &ObserverElements,
142    start_utc: f64,
143) -> Result<PredictObservation, OrbitPredictionError> {
144    let mut curr_time = start_utc;
145    let mut time_step;
146
147    let mut orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
148    let mut obs = predict_observe_orbit(oe.observer, &orbit);
149
150    //check whether AOS can happen after specified start time
151    if predict_aos_happens(oe.elements, oe.observer.latitude)
152        && !predict_is_geosynchronous(oe.elements)
153        && !orbit.decayed
154    {
155        // TODO: Time steps have been found in FindAOS/LOS().
156        // Might be based on some pre-existing source, root-finding techniques
157        // or something. Find them, and improve readability of the code and so that
158        // the mathematical stability of the iteration can be checked.
159        // Bisection method, Brent's algorithm? Given a coherent root finding algorithm,
160        // can rather have one function for iterating the orbit and then let get_next_aos/los
161        // specify bounding intervals for the root finding.
162
163        //skip the rest of the pass if the satellite is currently in range, since we want the _next_ AOS.
164        if obs.elevation > 0.0 {
165            let los = predict_next_los(oe, curr_time)?;
166            curr_time = los.time;
167            curr_time += 1.0 / (MINUTES_PER_DAY * 1.0) * 20.0; //skip 20 minutes. LOS might still be within the elevation threshold. (rough quickfix from predict)
168            orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
169            obs = predict_observe_orbit(oe.observer, &orbit);
170        }
171
172        // iteration until the orbit is roughly in range again, before the satellite pass
173        while (obs.elevation * 180.0 / PI < -1.0) || (obs.elevation_rate < 0.0) {
174            time_step =
175                0.00035 * (obs.elevation * 180.0 / PI * ((orbit.altitude / 8400.0) + 0.46) - 2.0);
176            curr_time -= time_step;
177            orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
178            obs = predict_observe_orbit(oe.observer, &orbit);
179        }
180
181        // fine tune the results until the elevation is within a low enough threshold
182        while (obs.elevation * 180.0 / PI).abs() > oe.observer.min_elevation {
183            time_step = obs.elevation * 180.0 / PI * orbit.altitude.sqrt() / 530_000.0;
184            curr_time -= time_step;
185            orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
186            obs = predict_observe_orbit(oe.observer, &orbit);
187        }
188    }
189    Ok(obs)
190}
191
192/// Rough stepping through a pass. Uses weird time steps from Predict.
193///
194/// param `observer` Ground station
195/// param `orbital_elements` Orbital elements of satellite
196/// param `curr_time` Time from which to start stepping
197/// param `direction` Either `POSITIVE_DIRECTION` (step from current time to pass end) or `NEGATIVE_DIRECTION` (step from current time to start of pass). In case of the former, the pass will be stepped until either elevation is negative or the derivative of the elevation is negative
198/// return Time for when we have stepped out of the pass
199/// # Errors
200///
201/// `predict_orbit` can fail
202pub fn step_pass(
203    oe: &ObserverElements,
204    mut curr_time: f64,
205    direction: &StepPassDirection,
206) -> Result<(PredictObservation, f64), OrbitPredictionError> {
207    loop {
208        let orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
209        let obs = predict_observe_orbit(oe.observer, &orbit);
210
211        // weird time stepping from Predict, but which magically works
212        let mut time_step = (obs.elevation - 1.0).cos() * orbit.altitude.sqrt() / 25000.0;
213        if ((*direction == StepPassDirection::PositiveDirection) && time_step < 0.0)
214            || ((*direction == StepPassDirection::NegativeDirection) && time_step > 0.0)
215        {
216            time_step = -time_step;
217        }
218
219        curr_time += time_step;
220        if (obs.elevation < oe.observer.min_elevation * DEG_TO_RAD)
221            || ((*direction == StepPassDirection::PositiveDirection) && (obs.elevation_rate <= 0.0))
222        {
223            return Ok((obs, curr_time));
224        }
225    }
226}
227
228/// # Errors
229///
230/// `predict_orbit` can fail
231pub fn refine_obs_elevation(
232    oe: &ObserverElements,
233    mut curr_time: f64,
234    mode: &RefineMode,
235) -> Result<(PredictPosition, PredictObservation, f64), OrbitPredictionError> {
236    let time_step = 0.001;
237    loop {
238        let orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
239        let obs = predict_observe_orbit(oe.observer, &orbit);
240        curr_time += time_step;
241        if *mode == RefineMode::AOS
242            && obs.elevation_rate > 0.0
243            && (obs.elevation * RAD_TO_DEG > oe.observer.min_elevation)
244        {
245            return Ok((orbit, obs, curr_time));
246        }
247        if *mode == RefineMode::LOS
248            && obs.elevation_rate < 0.0
249            && (obs.elevation * RAD_TO_DEG < oe.observer.min_elevation)
250        {
251            return Ok((orbit, obs, curr_time));
252        }
253    }
254}
255
256/// # Errors
257///
258/// `predict_orbit` can fail
259pub fn predict_next_los(
260    oe: &ObserverElements,
261    start_utc: f64,
262) -> Result<PredictObservation, OrbitPredictionError> {
263    let mut curr_time = start_utc;
264    let mut time_step;
265
266    let mut orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
267    let mut obs = predict_observe_orbit(oe.observer, &orbit);
268
269    // check whether AOS/LOS can happen after specified start time
270    if predict_aos_happens(oe.elements, oe.observer.latitude)
271        && !predict_is_geosynchronous(oe.elements)
272        && !orbit.decayed
273    {
274        // iteration algorithm from Predict, see comments in predict_next_aos().
275        // iterate until next satellite pass
276
277        if obs.elevation < 0.0 {
278            let aos = predict_next_aos(oe, curr_time)?;
279            curr_time = aos.time;
280            orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
281            obs = predict_observe_orbit(oe.observer, &orbit);
282        }
283
284        // step through the pass
285        (_, curr_time) = step_pass(oe, curr_time, &StepPassDirection::PositiveDirection)?;
286
287        // fine tune to elevation threshold
288        loop {
289            time_step = obs.elevation * 180.0 / PI * orbit.altitude.sqrt() / 502_500.0;
290            curr_time += time_step;
291            orbit = predict_orbit(oe.elements, oe.constants, curr_time)?;
292            obs = predict_observe_orbit(oe.observer, &orbit);
293            if (obs.elevation * 180.0 / PI).abs() <= oe.observer.min_elevation {
294                break;
295            }
296        }
297    }
298    Ok(obs)
299}
300
301/// Convenience function for calculation of derivative of elevation at specific time.
302///
303/// param `observer` Ground station
304/// param `orbital_elements` Orbital elements for satellite
305/// param `time` Time
306/// return Derivative of elevation at input time
307/// # Errors
308///
309/// `predict_orbit` can fail
310pub fn elevation_derivative(oe: &ObserverElements, time: f64) -> Result<f64, OrbitPredictionError> {
311    let orbit = predict_orbit(oe.elements, oe.constants, time)?;
312    let obs = predict_observe_orbit(oe.observer, &orbit);
313    Ok(obs.elevation_rate)
314}
315
316/// Find maximum elevation bracketed by input lower and upper time.
317///
318/// param `observer` Ground station
319/// param `orbital_elements` Orbital elements of satellite
320/// param `lower_time` Lower time bracket
321/// param `upper_time` Upper time bracket
322/// return Observation of satellite for maximum elevation between lower and upper time brackets
323/// # Errors
324///
325/// Returns `OrbitPredictionError` on failure of various subroutines
326pub fn find_max_elevation(
327    oe: &ObserverElements,
328    lower_time: f64,
329    upper_time: f64,
330) -> Result<PredictObservation, OrbitPredictionError> {
331    let mut max_ele_time_candidate = (upper_time + lower_time) / 2.0;
332    let mut iteration = 0.0;
333    let mut lower_time = lower_time;
334    let mut upper_time = upper_time;
335    while ((lower_time - upper_time).abs() > MAXELE_TIME_EQUALITY_THRESHOLD)
336        && (iteration < MAXELE_MAX_NUM_ITERATIONS)
337    {
338        max_ele_time_candidate = (upper_time + lower_time) / 2.0;
339
340        //calculate derivatives for lower, upper and candidate
341        let candidate_deriv = elevation_derivative(oe, max_ele_time_candidate)?;
342        let lower_deriv = elevation_derivative(oe, lower_time)?;
343        let upper_deriv = elevation_derivative(oe, upper_time)?;
344
345        //check whether derivative has changed sign
346        if candidate_deriv * lower_deriv < 0.0 {
347            upper_time = max_ele_time_candidate;
348        } else if candidate_deriv * upper_deriv < 0.0 {
349            lower_time = max_ele_time_candidate;
350        } else {
351            break;
352        }
353        iteration += 1.0;
354    }
355
356    //prepare output
357    let orbit = predict_orbit(oe.elements, oe.constants, max_ele_time_candidate)?;
358    Ok(predict_observe_orbit(oe.observer, &orbit))
359}
360
361/// passes the user's geodetic position and the time of interest and returns the ECI position and velocity of the observer
362///
363/// The velocity calculation assumes the geodetic position is stationary relative to the earth's surface
364///
365/// Reference:  The 1992 Astronomical Almanac, page K11.
366pub fn calculate_user_posvel(time: f64, geodetic: &mut Geodetic) -> (Vec3, Vec3) {
367    geodetic.theta = fmod2p(theta_g_jd(time) + geodetic.lon); /* LMST */
368
369    let c = 1.0
370        / (1.0 + FLATTENING_FACTOR * (FLATTENING_FACTOR - 2.0) * geodetic.lat.sin().powf(2.0))
371            .sqrt();
372
373    let sq = (1.0 - FLATTENING_FACTOR).powf(2.0) * c;
374    let achcp = (EARTH_RADIUS_KM_WGS84 * c + geodetic.alt) * geodetic.lat.cos();
375    let obs_pos = Vec3(
376        achcp * geodetic.theta.cos(), /* kilometers */
377        achcp * geodetic.theta.sin(),
378        (EARTH_RADIUS_KM_WGS84 * sq + geodetic.alt) * geodetic.lat.sin(),
379    );
380    (
381        obs_pos,
382        Vec3(
383            -EARTH_ANGULAR_VELOCITY * obs_pos.1, /* kilometers/second */
384            EARTH_ANGULAR_VELOCITY * obs_pos.0,
385            0.0,
386        ),
387    )
388}
389
390/// # Errors
391///
392/// Returns `OrbitPredictionError` on failure of various subroutines
393pub fn get_passes(
394    oe: &ObserverElements,
395    start_utc: f64,
396    stop_utc: f64,
397) -> Result<Passes, OrbitPredictionError> {
398    let mut orbit = predict_orbit(oe.elements, oe.constants, start_utc)?;
399    let mut obs = predict_observe_orbit(oe.observer, &orbit);
400    let satellite_el = obs.elevation * RAD_TO_DEG;
401    let mut currtime = start_utc;
402    let mut passes = vec![];
403
404    if satellite_el.abs() >= oe.observer.min_elevation {
405        // Already in a pass, find AOS by going backwards in time
406        let (_, real_aos) = step_pass(oe, currtime, &StepPassDirection::NegativeDirection)?;
407        currtime = real_aos - 1.0;
408    }
409    'outer: loop {
410        let mut pass = Pass {
411            aos: None,
412            los: None,
413            satellite_position_at_aos: None,
414            satellite_position_at_los: None,
415            max_elevation: None,
416        };
417        // rough time step until AOS
418        loop {
419            if currtime >= stop_utc {
420                break 'outer;
421            }
422            orbit = predict_orbit(oe.elements, oe.constants, currtime)?;
423            obs = predict_observe_orbit(oe.observer, &orbit);
424            let satellite_el = obs.elevation * RAD_TO_DEG;
425            if satellite_el >= oe.observer.min_elevation && obs.elevation_rate > 0.0 {
426                currtime -= 1.0;
427                let (satpos, observation, _) =
428                    refine_obs_elevation(oe, currtime, &RefineMode::AOS)?;
429                pass.aos = Some(observation);
430                pass.satellite_position_at_aos = Some(satpos);
431                currtime += 1.0;
432                break;
433            }
434            currtime += 1.0;
435        }
436        if pass.aos.is_none() {
437            println!("Shouldn't be here");
438        }
439        // now find LOS
440        loop {
441            orbit = predict_orbit(oe.elements, oe.constants, currtime)?;
442            obs = predict_observe_orbit(oe.observer, &orbit);
443            let satellite_el = obs.elevation * RAD_TO_DEG;
444            if satellite_el <= oe.observer.min_elevation && obs.elevation_rate < 0.0 {
445                currtime -= 1.0;
446                let (satpos, observation, _) =
447                    refine_obs_elevation(oe, currtime, &RefineMode::LOS)?;
448                pass.los = Some(observation);
449                pass.satellite_position_at_los = Some(satpos);
450                currtime += 1.0;
451                break;
452            }
453            currtime += 1.0;
454            if currtime >= stop_utc {
455                break;
456            }
457        }
458        if pass.aos.is_some() && pass.los.is_some() {
459            let maxel_obs = find_max_elevation(
460                oe,
461                pass.aos.as_ref().expect("already checked").time,
462                pass.los.as_ref().expect("already checked").time,
463            )?;
464            pass.max_elevation = Some(maxel_obs.elevation);
465        }
466        passes.push(pass);
467    }
468    Ok(Passes { passes })
469}
470
471#[cfg(test)]
472#[allow(clippy::unwrap_used)]
473mod tests {
474    use crate::observer::{get_passes, DEG_TO_RAD};
475    use crate::predict::{ObserverElements, PredictObserver};
476
477    // https://github.com/neuromorphicsystems/sgp4/blob/master/src/tle.rs
478    #[allow(clippy::float_cmp)]
479    fn assert_eq_f64(first: f64, second: f64) {
480        if second == 0.0 {
481            assert_eq!(first, 0.0);
482        } else {
483            assert!((first - second).abs() / second < f64::EPSILON);
484        }
485    }
486
487    #[test]
488    fn test_observer() {
489        let elements = sgp4::Elements::from_tle(
490            Some("RADARSAT-2".to_string()),
491            "1 32382U 07061A   25046.11055603  .00000166  00000+0  81462-4 0  9997".as_bytes(),
492            "2 32382  98.5808  54.9834 0001175  88.7553 271.3764 14.29984911896451".as_bytes(),
493        )
494        .unwrap();
495        let observer = PredictObserver {
496            name: "TEST".to_string(),
497            latitude: 50.0 * DEG_TO_RAD,
498            longitude: -130.0 * DEG_TO_RAD,
499            altitude: 0.0905,
500            min_elevation: 0.0,
501        };
502        let constants = sgp4::Constants::from_elements(&elements).unwrap();
503        let oe = ObserverElements {
504            elements: &elements,
505            constants: &constants,
506            observer: &observer,
507        };
508        #[allow(clippy::cast_precision_loss)]
509        let start_epoch = elements.datetime.and_utc().timestamp() as f64;
510        let mut passes = get_passes(&oe, start_epoch, start_epoch + 3.0 * 3600.0)
511            .unwrap()
512            .passes;
513
514        let second = passes.pop().unwrap();
515        let first = passes.pop().unwrap();
516        assert_eq_f64(first.aos.as_ref().unwrap().time, 1_739_587_541.817_650_3);
517        assert_eq_f64(first.los.as_ref().unwrap().time, 1_739_588_409.207_694_5);
518        assert_eq_f64(second.aos.as_ref().unwrap().time, 1_739_593_848.217_693_8);
519        assert_eq_f64(second.los.as_ref().unwrap().time, 1_739_594_238.896_644_6);
520        assert_eq_f64(
521            first.aos.as_ref().unwrap().revolutions,
522            89_645.818_476_411_63,
523        );
524        assert_eq_f64(
525            second.aos.as_ref().unwrap().revolutions,
526            89_646.862_134_186_38,
527        );
528    }
529}