-
-
Notifications
You must be signed in to change notification settings - Fork 276
[RFC - do not merge] First OrbitArray
API prototype
#1445
base: main
Are you sure you want to change the base?
Conversation
I have put some of my thinking down into a notebook and actually built a little prototype - which does Brownian motion, not Orbits, to make stuff simple enough to reason about the user-facing API as well as some internal aspects. |
Codecov Report
@@ Coverage Diff @@
## main #1445 +/- ##
=======================================
Coverage 91.83% 91.83%
=======================================
Files 95 95
Lines 4444 4444
Branches 427 427
=======================================
Hits 4081 4081
Misses 273 273
Partials 90 90 Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did a joint review with @s-m-e, @jorgepiloto, left a few comments and will follow-up asynchronously
"orbarr: OrbitArray = OrbitArray.from_classical(\n", | ||
" **{key: [item[key] for item in data] for k in data[0].keys()}\n", | ||
")\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's initially stick with a single attractor and a single common plane.
"orbarr_propagated: OrbitArray = orbarr.propagate(dt1d)\n", | ||
"\n", | ||
"assert len(orbarr_propagated) == len(orbarr)\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is consistent with the current Orbit.propagate
✔️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And to generate an array of ephemerides we could add an OrbitArray.to_ephem
as current consensus on #1364 is, but no need to jump straight to it right now.
"## Propagate array with array of time stamps (hiding the time-deltas)\n", | ||
"\n", | ||
"# Consistent with Orbit.propagate as it returns another OrbitArray\n", | ||
"orbarr_propagated: OrbitArray = orbarr.propagate(times)\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also consistent with Orbit.propagate
✔️
poliastro/src/poliastro/twobody/orbit.py
Lines 1092 to 1093 in 726c95d
if isinstance(value, time.Time) and not isinstance(value, time.TimeDelta): | |
time_of_flight = value - self.epoch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea after an inquiry from @jorgepiloto: if one wants to propagate an OrbitArray
to a single Time
, the .propagate
method should be smart enough to broadcast the time array. ✔️
"ephem: Ephem = Ephem.from_orbit(orbs[0], **kwargs)\n", | ||
"ephemarr: EphemArray = EphemArray.from_orbitarray(orbarr, **kwargs)\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or OrbitArray.to_ephem
as discussed above ☝🏽
"\n", | ||
"To **simplify** matters, this is not astrodynamics but **Brownian motion**, of sorts. This notebook is looking at the relationships between `Orbit`, `State`, `OrbitArray` and `StateArray` objects and allows some kind of propagation on all of them. \n", | ||
"\n", | ||
"This notebook uses type annotations and run-time type checks - for testing only. This stuff is not going into `poliastro` ..." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's again restrict ourselves to a single kind of state, which according to @s-m-e simplifies the implementation ✔️
" + '\\n]>'\n", | ||
" )\n", | ||
" \n", | ||
" def __getitem__(self, idx) -> 'Union[StateArray, State]':\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯
" def __getitem__(self, idx) -> 'Union[OrbitArray, Orbit]':\n", | ||
" target = self._statearray[idx]\n", | ||
" if isinstance(target, State):\n", | ||
" return Orbit(state = target)\n", | ||
" return OrbitArray(statearray = target)\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✔️
" def propagate(self, timedelta: TimeDelta, inplace: bool = False) -> 'OrbitArray':\n", | ||
" \n", | ||
" if timedelta.ndim == 0:\n", | ||
" timedelta = np.repeat(timedelta.to_value(u.d), self.size).reshape(self.shape) << u.d\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The broadcasting rules mentioned above ☝🏽
" return Orbit(state = target)\n", | ||
" return OrbitArray(statearray = target)\n", | ||
"\n", | ||
" def propagate(self, timedelta: TimeDelta, inplace: bool = False) -> 'OrbitArray':\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above, let's not close the door on inplace=True
but let's not do this in this first iteration so we can focus.
But @s-m-e is veeeeeeeeeeeeeeeeeeeery stubborn and wants to burn hours writing 2x as many tests 😉 so let's give him the chance!
"source": [ | ||
"## Further thinking\n", | ||
"\n", | ||
"- [Structured arrays](https://numpy.org/doc/stable/user/basics.rec.html) can actually be n-dimensional. This could be a foundation for `StateArray` classes, opening the back door for alternative libraries which are exposing an [array interface](https://numpy.org/doc/stable/reference/arrays.interface.html). In a bigger picture, the state array could be the place where stuff like `cupy.ndarray` or `dask.array` are transparently accepted.\n", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very interesting idea - are structured arrays something that has a future in the NumPy roadmap? Maybe we could rethink some things on top of this.
However, epochs are going to be an issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NumPy datatime is known to have lots of issues, or that's what I recall from reading Twitter threads and GitHub comments. I don't have any pointers at hand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deleted a highly misleading comment I made: the time of flight is definitely used by the propagation algorithm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To @s-m-e's point: by passing tof
(elapsed time in seconds) to the propagation algorithms, we have already chosen a unit. Therefore, one could take this approach one step further and work with elapsed seconds everywhere internally, to avoid the alleged performance issues of astropy.time
and the difficulties to store them.
I guess this approach would work as long as folks don't want to propagate trans-Neptunian objects or the Solar System barycenter with respect to the Milky Way. But, if it doesn't work, it's already slightly wrong in the current implementation.
Important reading: https://docs.astropy.org/en/stable/time/index.html#internal-representation
The Time object maintains an internal representation of time as a pair of double precision numbers expressing Julian days. The sum of the two numbers is the Julian Date for that time relative to the given time scale.
And about what would happen by using a single-float representation:
Users requiring no better than microsecond precision over human time scales (~100 years) can safely ignore the internal representation details and skip this section.
On the other hand, about time scales:
We have shown in the above that the difference between two UTC times is a TimeDelta with a scale of TAI. This is because a UTC time difference cannot be uniquely defined unless the user knows the two times that were differenced (because of leap seconds, a day does not always have 86400 seconds). For all other time scales, the TimeDelta inherits the scale of the first Time object.
Therefore,
- There is a way to do it right (in other words, aim for maximum accuracy) with relatively simple tools, but it requires some care.
- We might (I haven't deeply analyzed it) have tiny precision bugs in the code at the moment, given that we are not being particularly careful with the
TimeDelta
scales and just performing ato(u.s)
to pass the value to the propagator:
poliastro/src/poliastro/twobody/orbit.py
Lines 1098 to 1099 in 726c95d
cartesian = propagate(self, time_of_flight, method=method, rtol=rtol, **kwargs) | |
new_epoch = self.epoch + time_of_flight |
poliastro/src/poliastro/twobody/propagation.py
Lines 516 to 520 in 726c95d
rr, vv = method( | |
orbit.attractor.k, | |
orbit.r, | |
orbit.v, | |
time_of_flight.reshape(-1).to(u.s), |
On the other hand, we might be getting too deep into this topic, and I don't want this to block our efforts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Time object maintains an internal representation of time as a pair of double precision numbers expressing Julian days. The sum of the two numbers is the Julian Date for that time relative to the given time scale.
Then I was not digging deep enough. Interesting. This would actually allow a really clean implementation.
No description provided.