Open Sec42 opened 11 months ago
It turns out that this is a very big and interesting question for Skyfield, because its initial design was around having lots of times — a Time vector of every day in a whole decade, for example — and trying to make those operations NumPy-efficient. Your problem is a challenge because it's orthogonal to that: given a single time, what are n objects doing?
Instead of trying to write up a huge answer that tackles everything at once, I'm going to try making a little daily comment that moves one step along in our understanding of how we're going to do this efficiently in Skyfield! Otherwise it might be weeks before you hear anything from me. 🙂 But one little step per day should be doable for my schedule, and let you see the progress we can make towards making your problem faster.
So, today, a first observation is that Skyfield's frame methods are a little unfortunate in your case, because .frame_latlon_and_rates()
calls .frame_xyz_and_velocity()
(with the plan of converting its x,y,z coordinates to angles) which then thinks, okay, where was the frame pointing at the time .t
that this vector was measured, so it calls the frame's .rotation_at()
method to find out.
So your opos
object is getting hammered with n requests, where n is the number of satellites, for the answer to the question “how exactly were you oriented at time t
?” And each time it performs exactly the same expensive computation and returns the same answer. Which is wasteful: because Skyfield doesn't see the big picture that you're going to ask this over and over for n satellites, it duplicates the same work over and over.
Skyfield's design actually tries to side-step this problem in one particular case: when .altaz()
is called, Skyfield accesses a cached private property named _altaz_rotation
on the observer's geographic location, instead of asking it for its orientation directly. This means that it's computed only on the first try, instead of every time.
But that doesn't work for (a) the general case where the frame doesn't happen to be alt-az, or (b) even for the specific case you yourself have fallen into where the frame is in fact an alt-az horizon-based frame but you don't just want the angles altitude and azimuth and distance, but where you want their rates of change as well, and so have to call the general-purpose .frame_latlon_and_rates()
method instead of the specific special .altaz()
method.
Maybe Skyfield needs to switch to a more general caching mechanism that works for any frame? We'll have to see, there's lots of investigation to do here! For today, I'll just suggest you try adding a cache yourself to the opos
, and see if you get a speedup. Try this:
from functools import lru_cache
opos.rotation_at = lru_cache(maxsize=None)(opos.rotation_at)
opos._dRdt_times_RT_at = lru_cache(maxsize=None)(opos._dRdt_times_RT_at)
That teaches the opos
object to cache repeated calls to its frame-rotation methods if the same time gets asked about multiple times. Add it at the top of your code, and let me know if you see a difference — that would validate for us our initial idea here, that a big expense is that the frame is being asked for its orientation repeatedly!
Hi,
Thank you for the explanation and for taking the time to helping find a solution for this issue.
I tested your suggestion of adding caching the two methods of opos
. On the sample test file with 750 lines it yielded something between 15% and 20% improvement:
./freq_sat_match.py < example.tf > /tmp/exO 112,18s user 120,00s system 1155% cpu 20,092 total
./freq_sat_match.py < example.tf > /tmp/exO 113,52s user 122,10s system 1157% cpu 20,355 total
./freq_sat_match.py < example.tf > /tmp/exN 94,54s user 100,23s system 1176% cpu 16,559 total
./freq_sat_match.py < example.tf > /tmp/exN 95,30s user 100,40s system 1137% cpu 17,206 total
I'm wondering if there is a cheaper way to check for visibility (elevation >0) to maybe save a bunch of work, but I'm not sure if after the addition of caching it would make much difference.
Not sure if this is informative for you, but I ran the modified version with a larger (~10k lines) input file through cProf, and these are the top lines:
143529615 function calls (141654588 primitive calls) in 402.656 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
5392065 38.646 0.000 38.646 0.000 {built-in method numpy.core._multiarray_umath.c_einsum}
8237809/7318714 31.144 0.000 111.714 0.000 {built-in method numpy.core._multiarray_umath.implement_array
_function}
885760 27.042 0.000 68.831 0.000 functions.py:88(_to_spherical_and_rates)
885760 19.822 0.000 161.199 0.000 sgp4lib.py:186(_at)
2657300 17.599 0.000 17.599 0.000 {method 'reduce' of 'numpy.ufunc' objects}
66433 13.825 0.000 13.825 0.000 {method 'dot' of 'numpy.ndarray' objects}
1771520 13.817 0.000 31.018 0.000 functions.py:45(length_of)
885760 12.829 0.000 69.820 0.000 positionlib.py:440(frame_xyz_and_velocity)
1 12.102 12.102 402.657 402.657 freq_sat_match.py:1(<module>)
3631896 11.695 0.000 11.695 0.000 {built-in method numpy.array}
885760 11.105 0.000 161.858 0.000 positionlib.py:474(frame_latlon_and_rates)
26971397 10.923 0.000 10.923 0.000 einsumfunc.py:1001(_einsum_dispatcher)
885760 10.280 0.000 47.895 0.000 sgp4lib.py:151(_position_and_velocity_TEME_km)
5392065 9.584 0.000 88.933 0.000 <__array_function__ internals>:177(einsum)
907905 8.819 0.000 14.494 0.000 functions.py:141(rot_z)
885760 7.970 0.000 7.970 0.000 sgp4lib.py:301(theta_GMST1982)
885760 7.459 0.000 17.224 0.000 positionlib.py:169(__sub__)
1782592 7.206 0.000 17.254 0.000 positionlib.py:95(__init__)
5392065 6.994 0.000 45.640 0.000 einsumfunc.py:1009(einsum)
Hi,
I have a list of reception results (timestamp, frequency) that I am trying to match a specific satellite within a TLE file by matching the doppler shift of the signal to the speed of the satellite.
I was able to produce a working example using
frame_latlon_and_rates
, however the result is quite slow (in the order of 30 matches / second on my test server).My input data contains about 14M measurements from a 24h period, so I am at least one order of magnitude away from it being usable.
Am I making some obvious mistake here? Most of the satellites are below the horizon, but as elevation is also a result of
frame_latlon_and_rates
checking it doesn't really give an improvement.I am hoping you can help here.
The snippet requires an TLE file (e.g. iridium-NEXT.txt) and the input file looks like this: