[RFC - do not merge] First OrbitArray API prototype#1445
[RFC - do not merge] First OrbitArray API prototype#1445s-m-e wants to merge 5 commits intopoliastro:mainfrom
OrbitArray API prototype#1445Conversation
|
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.
|
astrojuanlu
left a comment
There was a problem hiding this comment.
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.
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.
This is consistent with the current Orbit.propagate ✔️
There was a problem hiding this comment.
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.
This is also consistent with Orbit.propagate ✔️
poliastro/src/poliastro/twobody/orbit.py
Lines 1092 to 1093 in 726c95d
There was a problem hiding this comment.
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.
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.
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", |
| " 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", |
| " 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.
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.
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.
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.
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.
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.
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
TimeDeltascales 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
poliastro/src/poliastro/twobody/propagation.py
Lines 516 to 520 in 726c95d
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.
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.