Skip to content
This repository has been archived by the owner on Oct 14, 2023. It is now read-only.

[RFC - do not merge] First OrbitArray API prototype #1445

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

s-m-e
Copy link
Contributor

@s-m-e s-m-e commented Jan 18, 2022

No description provided.

@s-m-e
Copy link
Contributor Author

s-m-e commented Jan 18, 2022

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
Copy link

codecov bot commented Jan 18, 2022

Codecov Report

Merging #1445 (6fa6f16) into main (185a01b) will not change coverage.
The diff coverage is n/a.

❗ Current head 6fa6f16 differs from pull request most recent head 478284c. Consider uploading reports for the commit 478284c to get more accurate results
Impacted file tree graph

@@           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.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 185a01b...478284c. Read the comment docs.

Copy link
Member

@astrojuanlu astrojuanlu left a 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

Comment on lines +54 to +56
"orbarr: OrbitArray = OrbitArray.from_classical(\n",
" **{key: [item[key] for item in data] for k in data[0].keys()}\n",
")\n",
Copy link
Member

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.

Comment on lines +66 to +68
"orbarr_propagated: OrbitArray = orbarr.propagate(dt1d)\n",
"\n",
"assert len(orbarr_propagated) == len(orbarr)\n",
Copy link
Member

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 ✔️

Copy link
Member

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",
Copy link
Member

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 ✔️

if isinstance(value, time.Time) and not isinstance(value, time.TimeDelta):
time_of_flight = value - self.epoch

Copy link
Member

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. ✔️

Comment on lines +91 to +92
"ephem: Ephem = Ephem.from_orbit(orbs[0], **kwargs)\n",
"ephemarr: EphemArray = EphemArray.from_orbitarray(orbarr, **kwargs)\n",
Copy link
Member

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` ..."
Copy link
Member

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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

Comment on lines +671 to +675
" 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",
Copy link
Member

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",
Copy link
Member

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",
Copy link
Member

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",
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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,

  1. There is a way to do it right (in other words, aim for maximum accuracy) with relatively simple tools, but it requires some care.
  2. 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 a to(u.s) to pass the value to the propagator:

cartesian = propagate(self, time_of_flight, method=method, rtol=rtol, **kwargs)
new_epoch = self.epoch + time_of_flight

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.

Copy link
Contributor Author

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants