Refactor orbital tracking with vectorized methods, bug fixes and improved utilities#206
Refactor orbital tracking with vectorized methods, bug fixes and improved utilities#206JaskRendix wants to merge 13 commits intopytroll:mainfrom
Conversation
djhoese
left a comment
There was a problem hiding this comment.
Nice. At a high level I have two big questions:
- Is it possible to make the original methods work vectorized without needed separate "vectorized" specific methods?
- Would using something like numpy.vectorize help? https://numpy.org/doc/stable/reference/generated/numpy.vectorize.html. I think in this case the outputs would always be numpy arrays but I think that's OK.
One other concern is the use of np.array to convert to array types and isinstance checks on numpy arrays. It'd be nice if these functions were compatible with dask arrays which likely (hopefully) just treating things like numpy arrays without specifically checking. There are also functions like https://numpy.org/doc/2.2/reference/generated/numpy.asanyarray.html#numpy.asanyarray which may help with being a little more flexible.
In the long run I suppose it'd be nice to have the low-level functions support multiple values instead of having to iterate over everything every time. I think I tried doing that once but decided it was too hard.
Thoughts?
ff630f9 to
61eb774
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #206 +/- ##
==========================================
+ Coverage 90.46% 91.06% +0.60%
==========================================
Files 19 20 +1
Lines 3040 3313 +273
==========================================
+ Hits 2750 3017 +267
- Misses 290 296 +6
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
I'm pretty sure |
|
@mraspaud absolutely nothing, I'm going to remove these edit: I’ve removed all of the _vectorized wrapper methods. While they offered an array-friendly API, they didn't provide true performance. I'm convinced that the SGP4 calculation is the real performance constraint and fixing it there will automatically make all public methods correctly array-aware. |
c30d4e4 to
b35dfb8
Compare
djhoese
left a comment
There was a problem hiding this comment.
Thanks for responding so quickly to the reviews! Do these current changes change the existing return type to any of these methods?
Have you (could you) run mypy on your the files you've changed? I don't think we currently run any type checkers on pyorbital automatically (CI or pre-commit) so it'd be good to know that mypy agrees.
|
@djhoese I usually run I checked and:
I'm going to run mypy less strict soon |
|
Typically I just go with |
|
@djhoese yeah, I created 2 typehints, but I have fixed both now back to 5 |
|
I'll have a go running the Satpy tests. |
|
I also have tests I can run against other libs |
|
Unrelated: @JaskRendix this project is planned to be relicensed from GPL to Apache version 2. Once we organize some of the other dependencies we'll ask all past contributor's if they're OK with this in a GitHub issue. This is just a heads up. |
|
With Satpy I only get an unrelated failure in downloading TLEs from Celestrak, most likely our network is being throttled or something. So all good from Satpy's perspective! |
|
Pytroll-collectors and Trollflow2 are fine, but I got a failure in Pytroll-Schedule in https://github.com/pytroll/pytroll-schedule/blob/main/trollsched/tests/test_schedule.py#L221 , the expected rise time and resulting value differ by few milliseconds. Might be close enough 😅 This is most likely coming from the bug you fixed in the previous PR. I'll fix this in Pytroll-Schedule, we might not need sub-second accuracy in scheduling... rt1 = datetime.datetime(2018, 11, 28, 10, 53, 42, 79483)
rise_times = [datetime.datetime(2018, 11, 28, 10, 53, 42, 66585),
datetime.datetime(2018, 11, 28, 12, 34, 44, 662042)] |
|
Now that pytroll-schedule is cleaned up, do we have any other issues with this PR? Merge? |
|
I haven't tested it yet, I'll try to get to it tonight |
|
Ah sorry, I misread your previous comment to mean you had tested those things. |
mraspaud
left a comment
There was a problem hiding this comment.
Thanks a lot for this PR, I appreciate the huge refactoring effort!
I have some comments and questions inline.
| azi, elev = get_observer_look(sat_lon, sat_lat, sat_alt, self.t, lon, lat, alt) | ||
| np.testing.assert_allclose(azi.data.compute(), self.exp_azi) | ||
| np.testing.assert_allclose(elev.data.compute(), self.exp_elev) | ||
| np.testing.assert_allclose(elev, self.exp_elev) |
There was a problem hiding this comment.
why remove the compute call here and not on the line above?
There was a problem hiding this comment.
Because azi is still a dask‑backed DataArray, so you must trigger computation, while elev is already a plain NumPy array coming out of the math path and has nothing to compute.
|
|
||
| def get_observer_look(sat_lon, sat_lat, sat_alt, utc_time, lon, lat, alt): | ||
| """Calculate observers look angle to a satellite. | ||
| """Calculate observer's look angle to a satellite. |
There was a problem hiding this comment.
Would it be possible to use the Orbital's get_observer_look to refactor some of this function.
There was a problem hiding this comment.
I pulled the shared ECEF > topocentric math into a small helper and had both functions call it, so the standalone version keeps its lon/lat/alt semantics and Orbital.get_observer_look keeps its TLE‑based propagation, but the duplicated geometry is gone.
| if f_a == f_b == f_c: | ||
| return b | ||
|
|
||
| for _ in range(max_iter): |
There was a problem hiding this comment.
I understand this is more secure to ensure the loop ever ends, thanks a lot!
One question I have is how was the default value of 50 chosen?
There was a problem hiding this comment.
50‑iteration cap wasn’t chosen for any deep numerical reason, it’s a safe upper bound that guarantees the loop can’t run forever. Parabolic interpolation settles in well under 10–15 steps, so 50 is above any realistic need. We could drop it to something like 20 without changing the behavior in normal cases.
| except ZeroDivisionError: | ||
| return b |
There was a problem hiding this comment.
why this except clause if we already test above that denom is 0?
There was a problem hiding this comment.
Because the denom == 0 check only catches the exact 0.0 case, while the division can still blow up on things like -0.0 or values so close to zero that floating‑point quirks trigger a ZeroDivisionError. The except is just a safety net for those edge cases.
Co-authored-by: Martin Raspaud <martin.raspaud@smhi.se>
Co-authored-by: Martin Raspaud <martin.raspaud@smhi.se>
Co-authored-by: Martin Raspaud <martin.raspaud@smhi.se>
Co-authored-by: Martin Raspaud <martin.raspaud@smhi.se>
Co-authored-by: Martin Raspaud <martin.raspaud@smhi.se>
Co-authored-by: Martin Raspaud <martin.raspaud@smhi.se>
Co-authored-by: Martin Raspaud <martin.raspaud@smhi.se>
Co-authored-by: Martin Raspaud <martin.raspaud@smhi.se>
Co-authored-by: Martin Raspaud <martin.raspaud@smhi.se>
Co-authored-by: Martin Raspaud <martin.raspaud@smhi.se>
493bffa to
ea14329
Compare
PR improves the orbital tracking module and related utilities.
fixed a bug in the main script where observer longitude and latitude were being converted to radians before being passed to
get_observer_look, which already performs this conversion internally, coordinates are now passed in degrees as expected, and conversion is handled inside the methodrefactored the
get_observer_lookfunction: split the logic into two helper functions:ecef_to_topocentricandcompute_azimuth_elevation, this isolates coordinate transformations and makes the azimuth/elevation computation easier to test and reuse, pytest coverage addedrefactored
_get_rootand_get_max_parab:_get_rootnow includes a check for valid root bracketing and usesxtolandrtolfor better convergence control._get_max_parabnow includes a fallback for flat functions, a maximum iteration limit, and improved numerical stability to avoid divergence and handle edge cases.cleaned up
test_orbital: ranisortandblackfor formatting and added new pytest coverage for all newly introduced vectorized methods and utilitiesimplemented
find_aosandfind_aolmethods for detecting Acquisition of Signal and Loss of Signal events based on observer location and elevation mask; includes robust horizon-crossing logic and full pytest coverageimproved
get_last_an_timelogic by refactoring shared node detection into a private_find_last_node_timemethod, which uses direct geometric analysis of the satellite’s Z-position and velocity to identify node crossings; this approach is distinct from orbit-number-based equatorial crossing methods and supports both ascending and descending detection, addedget_last_dn_timeto retrieve the last descending node, addressing feature request Would be nice to have aget_last_dn_timeto get the last descending node #95; includes parameterized pytest coverage for velocity sign, node classification, and temporal accuracyCloses Would be nice to have a
get_last_dn_timeto get the last descending node #95Tests added
Fully documented