orbix 1.2.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {orbix-1.2.0 → orbix-2.0.0}/.github/workflows/docs.yml +1 -1
- {orbix-1.2.0 → orbix-2.0.0}/.gitignore +1 -0
- {orbix-1.2.0 → orbix-2.0.0}/.pre-commit-config.yaml +1 -1
- {orbix-1.2.0 → orbix-2.0.0}/.zenodo.json +1 -1
- {orbix-1.2.0 → orbix-2.0.0}/CHANGELOG.md +25 -0
- {orbix-1.2.0 → orbix-2.0.0}/CITATION.cff +1 -1
- {orbix-1.2.0 → orbix-2.0.0}/PKG-INFO +16 -2
- {orbix-1.2.0 → orbix-2.0.0}/README.md +14 -1
- {orbix-1.2.0 → orbix-2.0.0}/docs/conf.py +1 -1
- {orbix-1.2.0 → orbix-2.0.0}/docs/examples/orbits.md +19 -9
- orbix-2.0.0/docs/examples/prepared.md +304 -0
- {orbix-1.2.0 → orbix-2.0.0}/docs/gallery.md +73 -12
- {orbix-1.2.0 → orbix-2.0.0}/docs/index.md +4 -1
- {orbix-1.2.0 → orbix-2.0.0}/pyproject.toml +1 -1
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/_version.py +2 -2
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/kepler/__init__.py +8 -1
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/kepler/core.py +24 -9
- orbix-2.0.0/src/orbix/kepler/markley.py +126 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/viz/__init__.py +6 -0
- orbix-2.0.0/src/orbix/viz/_prepare.py +521 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/viz/anim.py +227 -184
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/viz/orbit.py +129 -115
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_kepler_core.py +36 -1
- orbix-2.0.0/tests/test_kepler_markley.py +123 -0
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_viz_anim.py +62 -11
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_viz_orbit.py +125 -36
- orbix-2.0.0/tests/test_viz_package.py +140 -0
- orbix-2.0.0/tests/test_viz_prepared.py +818 -0
- orbix-1.2.0/tests/test_viz_package.py +0 -71
- {orbix-1.2.0 → orbix-2.0.0}/.github/dependabot.yml +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/.github/workflows/release-please.yml +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/.github/workflows/tests.yml +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/.python-version +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/.readthedocs.yaml +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/LICENSE +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/__init__.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/equations/__init__.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/equations/lambert.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/equations/orbit.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/equations/phase.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/equations/propagation.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/kepler/shortcuts/__init__.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/kepler/shortcuts/fixed_e.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/kepler/shortcuts/grid.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/kepler/shortcuts/guess.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/observatory/__init__.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/observatory/data/L2_halo_orbit_six_month.npz +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/observatory/keepout.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/observatory/observatory.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/observatory/orbit.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/observatory/solar_system.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/orbit.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/py.typed +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/src/orbix/viz/_require.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/tests/conftest.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_equations_grads.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_fixed_e_solvers.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_grid_solvers.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_internal_refs_guard.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_kepler_grads.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_lambert.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_observatory.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_observatory_wrapper.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_orbit.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/tests/test_vs_exosims.py +0 -0
- {orbix-1.2.0 → orbix-2.0.0}/tools/check_internal_refs.py +0 -0
|
@@ -11,6 +11,31 @@
|
|
|
11
11
|
* The broken predict/pdet functions and the `discrete_e` factory were removed
|
|
12
12
|
(they did not work correctly and had no working callers).
|
|
13
13
|
|
|
14
|
+
## [2.0.0](https://github.com/CoreySpohn/orbix/compare/v1.2.0...v2.0.0) (2026-10-12)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### ⚠ BREAKING CHANGES
|
|
18
|
+
|
|
19
|
+
* **viz:** prepare sky tracks for shared renderers
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* **kepler:** add a table-free Markley solver with implicit derivatives ([886bec0](https://github.com/CoreySpohn/orbix/commit/886bec02b6dfda444cf78a84710e1aadc2a2f150))
|
|
24
|
+
* **viz:** prepare sky tracks for shared renderers ([84fd1ef](https://github.com/CoreySpohn/orbix/commit/84fd1ef7dd7fdb51fd0751cef4afd7f75f80dabd))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* **viz:** accept a quarter-step clock rounding when no precision is exact ([67a3822](https://github.com/CoreySpohn/orbix/commit/67a3822eee0ed7b8474768a7b3b91acf3d048861))
|
|
30
|
+
* **viz:** keep per-track colors on sky tracks ([693e5da](https://github.com/CoreySpohn/orbix/commit/693e5da6be5b55372bd7ae8b7a4d0b63733ee3af))
|
|
31
|
+
* **viz:** print exact clock offsets, take clock_fmt, and weight heads ([022f04a](https://github.com/CoreySpohn/orbix/commit/022f04ac45b6d121f95a42a309b250fd44f94674))
|
|
32
|
+
* **viz:** weight the star and observations at the top track weight so heads fade with their paths ([b190d40](https://github.com/CoreySpohn/orbix/commit/b190d40e2ec876ca5fafd3975485c3268e294cd7))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Performance Improvements
|
|
36
|
+
|
|
37
|
+
* **kepler:** select the reduced angle before the trig polynomial and gather RPP coefficients once ([02d71da](https://github.com/CoreySpohn/orbix/commit/02d71dae0984e3e36f58ae70d502ab7703257362))
|
|
38
|
+
|
|
14
39
|
## [1.2.0](https://github.com/CoreySpohn/orbix/compare/v1.1.0...v1.2.0) (2026-08-28)
|
|
15
40
|
|
|
16
41
|
|
|
@@ -7,7 +7,7 @@ authors:
|
|
|
7
7
|
- family-names: Spohn
|
|
8
8
|
given-names: Corey
|
|
9
9
|
orcid: "https://orcid.org/0000-0002-3138-0240"
|
|
10
|
-
affiliation: "NASA Goddard Space Flight Center"
|
|
10
|
+
affiliation: "University of Maryland, Baltimore County; NASA Goddard Space Flight Center; CRESST II"
|
|
11
11
|
repository-code: "https://github.com/CoreySpohn/orbix"
|
|
12
12
|
license: MIT
|
|
13
13
|
doi: "10.5281/zenodo.20819376"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: orbix
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: A JAX library of functions useful for exoplanet simulations
|
|
5
5
|
Project-URL: Homepage, https://github.com/CoreySpohn/orbix
|
|
6
6
|
Project-URL: Issues, https://github.com/CoreySpohn/orbix/issues
|
|
@@ -44,6 +44,7 @@ Requires-Dist: sphinx-autodoc-typehints; extra == 'docs'
|
|
|
44
44
|
Requires-Dist: sphinx-book-theme; extra == 'docs'
|
|
45
45
|
Provides-Extra: test
|
|
46
46
|
Requires-Dist: eyepiece>=0.4.0; extra == 'test'
|
|
47
|
+
Requires-Dist: mpmath>=1.3; extra == 'test'
|
|
47
48
|
Requires-Dist: pytest; extra == 'test'
|
|
48
49
|
Provides-Extra: viz
|
|
49
50
|
Requires-Dist: eyepiece>=0.4.0; extra == 'viz'
|
|
@@ -71,7 +72,13 @@ from orbix.orbit import KeplerianOrbit
|
|
|
71
72
|
from orbix.kepler.shortcuts.grid import get_grid_solver
|
|
72
73
|
|
|
73
74
|
orbit = KeplerianOrbit(
|
|
74
|
-
a_AU=1.0,
|
|
75
|
+
a_AU=1.0,
|
|
76
|
+
e=0.0167,
|
|
77
|
+
W_rad=0.0,
|
|
78
|
+
i_rad=0.0,
|
|
79
|
+
w_rad=0.0,
|
|
80
|
+
M0_rad=0.0,
|
|
81
|
+
t0_d=0.0,
|
|
75
82
|
)
|
|
76
83
|
trig_solver = get_grid_solver(level="scalar", E=False, trig=True)
|
|
77
84
|
t_jd = jnp.linspace(2451545.0, 2451545.0 + 365.25, 5)
|
|
@@ -84,6 +91,13 @@ contract `trig_solver(M, e) -> (sinE, cosE)`; `KeplerianOrbit.propagate` uses
|
|
|
84
91
|
it to convert orbital elements and times into position vectors, phase angle,
|
|
85
92
|
and star-planet distance.
|
|
86
93
|
|
|
94
|
+
For an alternative without interpolation tables, use
|
|
95
|
+
`from orbix.kepler import markley_solve_trig` and pass `markley_solve_trig` as the
|
|
96
|
+
orbit's `trig_solver`. This Markley-based solver supports scalar inputs,
|
|
97
|
+
broadcasting, and both forward and reverse derivatives. Enable
|
|
98
|
+
`jax.config.update("jax_enable_x64", True)` for double-precision accuracy.
|
|
99
|
+
The RPP/orvara solver remains available in `orbix.kepler.core`.
|
|
100
|
+
|
|
87
101
|
## Scope
|
|
88
102
|
|
|
89
103
|
orbix provides Keplerian orbit geometry and Kepler-equation solvers; scene
|
|
@@ -20,7 +20,13 @@ from orbix.orbit import KeplerianOrbit
|
|
|
20
20
|
from orbix.kepler.shortcuts.grid import get_grid_solver
|
|
21
21
|
|
|
22
22
|
orbit = KeplerianOrbit(
|
|
23
|
-
a_AU=1.0,
|
|
23
|
+
a_AU=1.0,
|
|
24
|
+
e=0.0167,
|
|
25
|
+
W_rad=0.0,
|
|
26
|
+
i_rad=0.0,
|
|
27
|
+
w_rad=0.0,
|
|
28
|
+
M0_rad=0.0,
|
|
29
|
+
t0_d=0.0,
|
|
24
30
|
)
|
|
25
31
|
trig_solver = get_grid_solver(level="scalar", E=False, trig=True)
|
|
26
32
|
t_jd = jnp.linspace(2451545.0, 2451545.0 + 365.25, 5)
|
|
@@ -33,6 +39,13 @@ contract `trig_solver(M, e) -> (sinE, cosE)`; `KeplerianOrbit.propagate` uses
|
|
|
33
39
|
it to convert orbital elements and times into position vectors, phase angle,
|
|
34
40
|
and star-planet distance.
|
|
35
41
|
|
|
42
|
+
For an alternative without interpolation tables, use
|
|
43
|
+
`from orbix.kepler import markley_solve_trig` and pass `markley_solve_trig` as the
|
|
44
|
+
orbit's `trig_solver`. This Markley-based solver supports scalar inputs,
|
|
45
|
+
broadcasting, and both forward and reverse derivatives. Enable
|
|
46
|
+
`jax.config.update("jax_enable_x64", True)` for double-precision accuracy.
|
|
47
|
+
The RPP/orvara solver remains available in `orbix.kepler.core`.
|
|
48
|
+
|
|
36
49
|
## Scope
|
|
37
50
|
|
|
38
51
|
orbix provides Keplerian orbit geometry and Kepler-equation solvers; scene
|
|
@@ -31,7 +31,7 @@ intersphinx_mapping = {
|
|
|
31
31
|
"jax": ("https://docs.jax.dev/en/latest/", None),
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
|
34
|
+
exclude_patterns = ["_build", ".build", "Thumbs.db", ".DS_Store"]
|
|
35
35
|
|
|
36
36
|
autoapi_dirs = ["../src"]
|
|
37
37
|
autoapi_ignore = ["**/_version.py"]
|
|
@@ -14,8 +14,9 @@ kernelspec:
|
|
|
14
14
|
`orbix.viz` turns an orbit into a figure in one call. It is installed with
|
|
15
15
|
the `viz` extra (`pip install 'orbix[viz]'`) and is built on eyepiece, so
|
|
16
16
|
every figure here follows the same conventions as any other figure in the
|
|
17
|
-
eyepiece fleet: stateless
|
|
18
|
-
|
|
17
|
+
eyepiece fleet: stateless functions that draw into a given axes or a new
|
|
18
|
+
figure, a result back that holds every artist drawn, and style resolved from
|
|
19
|
+
hwostyle at call time.
|
|
19
20
|
|
|
20
21
|
Every page starts the same way: activate a style mode, import the plotting
|
|
21
22
|
module, and declare the document's cast.
|
|
@@ -50,7 +51,10 @@ styles = ep.SourceStyles(["planet b", "planet c", "planet d"])
|
|
|
50
51
|
`plot_sky_track` propagates an orbit and draws its sky-plane track in
|
|
51
52
|
arcseconds: RA offset on the x axis (increasing to the left, per the
|
|
52
53
|
astronomical convention), Dec offset on the y axis, the star at the
|
|
53
|
-
origin, and an equal aspect so the ellipse is not distorted.
|
|
54
|
+
origin, and an equal aspect so the ellipse is not distorted. The sky-plane
|
|
55
|
+
functions share one preparation step that propagates the orbit once; the
|
|
56
|
+
{doc}`prepared` page uses it directly for a still, a strip, and a movie from
|
|
57
|
+
one set of tracks.
|
|
54
58
|
|
|
55
59
|
```{code-cell} ipython3
|
|
56
60
|
MSUN_KG = 1.988409870698051e30
|
|
@@ -108,7 +112,7 @@ t_system = jnp.linspace(T0_D, T0_D + OUTER_PERIOD_D, 240)
|
|
|
108
112
|
|
|
109
113
|
result = viz.plot_sky_track(
|
|
110
114
|
orbit, t_jd, Ms_kg=MSUN_KG, dist_pc=10.0,
|
|
111
|
-
style=styles["planet b"],
|
|
115
|
+
style=styles["planet b"], iwa_arcsec=0.06,
|
|
112
116
|
)
|
|
113
117
|
```
|
|
114
118
|
|
|
@@ -132,9 +136,14 @@ t_obs = T0_D + np.array([70.0, 250.0, 430.0])
|
|
|
132
136
|
ra_true, dec_true = orbit.position_arcsec(
|
|
133
137
|
t_jd=jnp.asarray(t_obs), Ms_kg=MSUN_KG, dist_pc=10.0
|
|
134
138
|
)
|
|
139
|
+
# Observations are (xy, xerr, yerr): positions shaped (P, 2) and one-sigma
|
|
140
|
+
# errors along RA and Dec.
|
|
135
141
|
epochs = (
|
|
136
|
-
np.
|
|
137
|
-
|
|
142
|
+
np.column_stack([
|
|
143
|
+
np.asarray(ra_true)[0] + SIGMA_ARCSEC * rng.standard_normal(3),
|
|
144
|
+
np.asarray(dec_true)[0] + SIGMA_ARCSEC * rng.standard_normal(3),
|
|
145
|
+
]),
|
|
146
|
+
np.full(3, SIGMA_ARCSEC),
|
|
138
147
|
np.full(3, SIGMA_ARCSEC),
|
|
139
148
|
)
|
|
140
149
|
|
|
@@ -151,7 +160,7 @@ draws = dict(
|
|
|
151
160
|
fan = KeplerianOrbit.from_period(**draws, Ms_kg=MSUN_KG)
|
|
152
161
|
result = viz.plot_sky_track(
|
|
153
162
|
fan, t_jd, Ms_kg=MSUN_KG, dist_pc=10.0,
|
|
154
|
-
style=styles["planet b"],
|
|
163
|
+
style=styles["planet b"], iwa_arcsec=0.06, observations=epochs,
|
|
155
164
|
)
|
|
156
165
|
```
|
|
157
166
|
|
|
@@ -213,7 +222,7 @@ ax3d.set_title("plot_orbit -- star-centric, AU")
|
|
|
213
222
|
|
|
214
223
|
viz.plot_sky_track(
|
|
215
224
|
system, t_system, Ms_kg=MSUN_KG, dist_pc=10.0, ax=axsky,
|
|
216
|
-
colors=[styles[name]["color"] for name in PLANETS],
|
|
225
|
+
colors=[styles[name]["color"] for name in PLANETS], iwa_arcsec=0.06,
|
|
217
226
|
fan_kw={"lw": 1.8},
|
|
218
227
|
)
|
|
219
228
|
axsky.set_title("plot_sky_track -- as seen from Earth")
|
|
@@ -250,7 +259,8 @@ keeps a trailing window, `"none"` moves the head markers alone. The base
|
|
|
250
259
|
marker size is the anchor the depth cue swells around, so it is where
|
|
251
260
|
physical meaning lives: `size_by_radius` maps planet radii onto marker
|
|
252
261
|
diameters. The same call with `kind="sky"` animates the sky-plane track
|
|
253
|
-
instead
|
|
262
|
+
instead, played in physical time from the prepared sequence described in
|
|
263
|
+
{doc}`prepared`.
|
|
254
264
|
|
|
255
265
|
```{code-cell} ipython3
|
|
256
266
|
from IPython.display import HTML
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
---
|
|
2
|
+
jupytext:
|
|
3
|
+
text_representation:
|
|
4
|
+
extension: .md
|
|
5
|
+
format_name: myst
|
|
6
|
+
format_version: 0.13
|
|
7
|
+
kernelspec:
|
|
8
|
+
display_name: Python 3
|
|
9
|
+
language: python
|
|
10
|
+
name: python3
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Sky tracks, prepared once
|
|
14
|
+
|
|
15
|
+
A sky-plane figure of a planet's orbit is needed in several forms: a paper
|
|
16
|
+
still of a posterior fan, a strip of chosen epochs, a movie, and a clip in a
|
|
17
|
+
talk. Each form needs the same science: the orbits propagated over the time
|
|
18
|
+
grid, projected to arcseconds, and moved from the device to the host. The
|
|
19
|
+
preparation functions in `orbix.viz` do that work once and return
|
|
20
|
+
[eyepiece](https://eyepiece.readthedocs.io) prepared records, which
|
|
21
|
+
eyepiece's renderers draw. None of the outputs below propagates an orbit
|
|
22
|
+
again, so they cannot disagree about where the planet is.
|
|
23
|
+
|
|
24
|
+
- `prepare_sky_view` returns a static `TrackView` of full tracks, with no
|
|
25
|
+
time axis.
|
|
26
|
+
- `prepare_sky_track` returns a `Sequence` whose samples are the propagated
|
|
27
|
+
epochs, with a head per track, an optional trail, and a clock.
|
|
28
|
+
|
|
29
|
+
Their module imports NumPy, JAX, the orbit types, and `eyepiece.prepared`,
|
|
30
|
+
and no renderer. Drawing belongs to
|
|
31
|
+
`eyepiece.mpl` for figures and movies and `eyepiece.manim` for native Manim
|
|
32
|
+
clips. The records, scales, and sample-and-hold time rules are described in
|
|
33
|
+
eyepiece's
|
|
34
|
+
[prepared views guide](https://eyepiece.readthedocs.io/en/latest/prepared-views.html).
|
|
35
|
+
|
|
36
|
+
```{code-cell} ipython3
|
|
37
|
+
import jax
|
|
38
|
+
|
|
39
|
+
# Julian days near 2.46 million need double precision to resolve a fraction
|
|
40
|
+
# of a day, and the clock below prints the sampled times.
|
|
41
|
+
jax.config.update("jax_enable_x64", True)
|
|
42
|
+
|
|
43
|
+
import hwostyle
|
|
44
|
+
import matplotlib
|
|
45
|
+
import matplotlib.pyplot as plt
|
|
46
|
+
import numpy as np
|
|
47
|
+
from hwoutils.constants import Msun2kg
|
|
48
|
+
from IPython.display import HTML
|
|
49
|
+
|
|
50
|
+
import eyepiece.mpl as mpl
|
|
51
|
+
from eyepiece.prepared import find_element
|
|
52
|
+
from eyepiece.style import SourceCast, snapshot_profile
|
|
53
|
+
from orbix import KeplerianOrbit
|
|
54
|
+
from orbix.viz import prepare_sky_track, prepare_sky_view
|
|
55
|
+
|
|
56
|
+
hwostyle.use("dark")
|
|
57
|
+
|
|
58
|
+
# Docs-builder concerns, not lines to copy into your own scripts: a fallback
|
|
59
|
+
# face for builders without the style's fonts, and small page images.
|
|
60
|
+
matplotlib.rcParams["font.sans-serif"] = list(
|
|
61
|
+
matplotlib.rcParams["font.sans-serif"]
|
|
62
|
+
) + ["DejaVu Sans"]
|
|
63
|
+
matplotlib.rcParams["savefig.dpi"] = 120
|
|
64
|
+
|
|
65
|
+
# One cast and one profile for every output on this page, sized from the
|
|
66
|
+
# active style's rc settings.
|
|
67
|
+
CAST = SourceCast(["planet b"])
|
|
68
|
+
PROFILE = snapshot_profile(
|
|
69
|
+
text_size_pt=plt.rcParams["font.size"],
|
|
70
|
+
stroke_width_pt=plt.rcParams["lines.linewidth"],
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## A posterior and its data
|
|
75
|
+
|
|
76
|
+
The planet below is synthetic: an eccentric, inclined orbit around a solar
|
|
77
|
+
mass star at 10 pc, three measured positions drawn from it with 8 mas
|
|
78
|
+
errors, and forty posterior draws scattered around it, each with a weight.
|
|
79
|
+
|
|
80
|
+
```{code-cell} ipython3
|
|
81
|
+
DIST_PC = 10.0
|
|
82
|
+
A_AU, ECC, INC_RAD = 1.3, 0.31, 1.05
|
|
83
|
+
BIG_OMEGA_RAD, SMALL_OMEGA_RAD, M0_RAD = 2.3, -0.7, 0.4
|
|
84
|
+
T0_D = 2460000.0
|
|
85
|
+
PERIOD_D = A_AU**1.5 * 365.25
|
|
86
|
+
PERIAPSIS_D = T0_D - M0_RAD * PERIOD_D / (2.0 * np.pi)
|
|
87
|
+
|
|
88
|
+
orbit = KeplerianOrbit(
|
|
89
|
+
a_AU=A_AU, e=ECC, W_rad=BIG_OMEGA_RAD, i_rad=INC_RAD,
|
|
90
|
+
w_rad=SMALL_OMEGA_RAD, M0_rad=M0_RAD, t0_d=T0_D,
|
|
91
|
+
)
|
|
92
|
+
rng = np.random.default_rng(0)
|
|
93
|
+
|
|
94
|
+
# Observed positions: the true orbit at three epochs, perturbed by the error.
|
|
95
|
+
SIGMA_ARCSEC = 0.008
|
|
96
|
+
t_obs = T0_D + np.array([70.0, 250.0, 430.0])
|
|
97
|
+
ra, dec = orbit.position_arcsec(t_jd=t_obs, Ms_kg=Msun2kg, dist_pc=DIST_PC)
|
|
98
|
+
xy_obs = np.column_stack([np.asarray(ra)[0], np.asarray(dec)[0]])
|
|
99
|
+
xy_obs = xy_obs + SIGMA_ARCSEC * rng.standard_normal(xy_obs.shape)
|
|
100
|
+
sigma = np.full(len(t_obs), SIGMA_ARCSEC)
|
|
101
|
+
observations = (xy_obs, sigma, sigma)
|
|
102
|
+
|
|
103
|
+
# Forty posterior draws around the same orbit, and a weight for each.
|
|
104
|
+
K = 40
|
|
105
|
+
fan = KeplerianOrbit.from_period(
|
|
106
|
+
T_d=PERIOD_D * (1.0 + 0.04 * rng.standard_normal(K)),
|
|
107
|
+
e=np.clip(ECC + 0.04 * rng.standard_normal(K), 0.0, 0.9),
|
|
108
|
+
cos_i=np.clip(np.cos(INC_RAD) + 0.06 * rng.standard_normal(K), -1.0, 1.0),
|
|
109
|
+
W_rad=BIG_OMEGA_RAD + 0.08 * rng.standard_normal(K),
|
|
110
|
+
cos_w=np.cos(SMALL_OMEGA_RAD + 0.15 * rng.standard_normal(K)),
|
|
111
|
+
sin_w=np.sin(SMALL_OMEGA_RAD + 0.15 * rng.standard_normal(K)),
|
|
112
|
+
tp_d=PERIAPSIS_D + 12.0 * rng.standard_normal(K),
|
|
113
|
+
Ms_kg=Msun2kg,
|
|
114
|
+
)
|
|
115
|
+
weights = rng.dirichlet(np.full(K, 3.0))
|
|
116
|
+
|
|
117
|
+
# One orbital period in 180 steps of about three days.
|
|
118
|
+
t_jd = np.linspace(T0_D, T0_D + PERIOD_D, 181)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## A still of the fan
|
|
122
|
+
|
|
123
|
+
`prepare_sky_view` propagates every draw over `t_jd` and returns one
|
|
124
|
+
`TrackView`. Each draw is its own `Path` (`path/0` to `path/39`), so every
|
|
125
|
+
draw stays addressable, while `source_ids="planet b"` names the one planet
|
|
126
|
+
they all describe, so they share one color from the cast. The weights become
|
|
127
|
+
relative opacity. The observations carry their error bars, and
|
|
128
|
+
`iwa_arcsec` adds the inner working angle as a shaded disk about the star.
|
|
129
|
+
RA offset increases to the left and the two axes share one scale; both are
|
|
130
|
+
part of the view, not options.
|
|
131
|
+
|
|
132
|
+
```{code-cell} ipython3
|
|
133
|
+
view = prepare_sky_view(
|
|
134
|
+
fan,
|
|
135
|
+
t_jd=t_jd,
|
|
136
|
+
Ms_kg=Msun2kg,
|
|
137
|
+
dist_pc=DIST_PC,
|
|
138
|
+
source_ids="planet b",
|
|
139
|
+
weights=weights,
|
|
140
|
+
observations=observations,
|
|
141
|
+
iwa_arcsec=0.06,
|
|
142
|
+
)
|
|
143
|
+
fig, ax = plt.subplots(figsize=(4.2, 4.0), layout="constrained")
|
|
144
|
+
fan_still = mpl.render(view, ax=ax, cast=CAST, profile=PROFILE)
|
|
145
|
+
print(len(fan_still.parts), "parts, including", sorted(fan_still.parts)[:4])
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`fan_still.parts` maps each element ID to its Matplotlib artist, so one
|
|
149
|
+
draw, the star, or the IWA disk is adjusted through its handle. The
|
|
150
|
+
Matplotlib convenience `plot_sky_track` makes this same view and render in
|
|
151
|
+
one call.
|
|
152
|
+
|
|
153
|
+
## A sequence of epochs
|
|
154
|
+
|
|
155
|
+
`prepare_sky_track` takes the same arguments and returns a `Sequence` over
|
|
156
|
+
the propagated epochs. Its template adds a head per draw (`head/<k>`), which
|
|
157
|
+
holds the propagated position at the current sample, and a clock. `history`
|
|
158
|
+
adds a trail per draw: `"all"` grows from the first sample, and an integer
|
|
159
|
+
keeps a sliding window of that many samples, here about ninety days.
|
|
160
|
+
|
|
161
|
+
```{code-cell} ipython3
|
|
162
|
+
sequence = prepare_sky_track(
|
|
163
|
+
fan,
|
|
164
|
+
t_jd=t_jd,
|
|
165
|
+
Ms_kg=Msun2kg,
|
|
166
|
+
dist_pc=DIST_PC,
|
|
167
|
+
source_ids="planet b",
|
|
168
|
+
weights=weights,
|
|
169
|
+
observations=observations,
|
|
170
|
+
iwa_arcsec=0.06,
|
|
171
|
+
history=30,
|
|
172
|
+
)
|
|
173
|
+
print(f"{len(sequence.times)} samples in {sequence.time_unit!r} after JD {sequence.epoch}")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Times are days since the first sample, and the first sample's Julian day is
|
|
177
|
+
the sequence `epoch`. A head never moves along the chord between two
|
|
178
|
+
samples. Asked for a time between samples, the sequence holds the earlier
|
|
179
|
+
one, so a head is always a propagated position. On a sparsely sampled orbit
|
|
180
|
+
an interpolated head could cut across the inner working angle while the
|
|
181
|
+
orbit itself never enters it; a held head cannot.
|
|
182
|
+
|
|
183
|
+
```{code-cell} ipython3
|
|
184
|
+
between = 0.5 * (sequence.times[60] + sequence.times[61])
|
|
185
|
+
sample = sequence.at(between)
|
|
186
|
+
held = find_element(sample.view, "head/0").xy[0]
|
|
187
|
+
propagated = find_element(sequence.template, "path/0").xy[60]
|
|
188
|
+
print(f"t = {between:.1f} d holds sample {sample.index}:", np.array_equal(held, propagated))
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## A still and a strip
|
|
192
|
+
|
|
193
|
+
`sequence.frame(i)` is the complete state at sample `i`, and
|
|
194
|
+
`sequence.strip(indices)` lays chosen samples side by side. Each strip slot's
|
|
195
|
+
IDs are prefixed with its position, and each slot's clock is its time label,
|
|
196
|
+
restamped with that slot's time. The heads show where the planet could be at each epoch: the spread of
|
|
197
|
+
the forty heads is the posterior uncertainty in its position at that time.
|
|
198
|
+
|
|
199
|
+
```{code-cell} ipython3
|
|
200
|
+
picks = [0, 60, 120]
|
|
201
|
+
fig, axes = plt.subplots(1, 3, figsize=(10.0, 3.6), layout="constrained")
|
|
202
|
+
strip = mpl.render(sequence.strip(picks), axes=axes, cast=CAST, profile=PROFILE)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## A movie
|
|
206
|
+
|
|
207
|
+
`eyepiece.mpl.animate` plays the sequence in physical time: three seconds at
|
|
208
|
+
ten frames per second is thirty output frames, spaced evenly from the first
|
|
209
|
+
sample to the last. Each output frame holds the latest sample acquired at or
|
|
210
|
+
before its time, so the heads move at their projected speed on the sky:
|
|
211
|
+
quickly near periapsis and slowly near apoapsis.
|
|
212
|
+
|
|
213
|
+
```{code-cell} ipython3
|
|
214
|
+
fig, ax = plt.subplots(figsize=(3.6, 3.4), layout="constrained")
|
|
215
|
+
movie = mpl.animate(
|
|
216
|
+
sequence, run_time=3.0, fps=10, ax=ax, cast=CAST, profile=PROFILE
|
|
217
|
+
)
|
|
218
|
+
html = movie.jshtml(dpi=100)
|
|
219
|
+
plt.close(fig)
|
|
220
|
+
print("output frames:", movie.n_frames)
|
|
221
|
+
HTML(html)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`animate_orbit(..., kind="sky")` wraps preparation and this call in one step,
|
|
225
|
+
for a movie that needs no other output.
|
|
226
|
+
|
|
227
|
+
## Bare tracks
|
|
228
|
+
|
|
229
|
+
Tracks that were propagated elsewhere, or loaded from a file, enter through
|
|
230
|
+
the same functions: an array of arcsecond offsets shaped `(K, T, 2)`, or an
|
|
231
|
+
`(ra, dec)` pair of `(T,)` or `(K, T)` arrays. A bare array is borrowed as
|
|
232
|
+
given, without a copy. `prepare_sky_view` draws bare tracks with no times at
|
|
233
|
+
all, and `prepare_sky_track` takes the times they were sampled at. A row with
|
|
234
|
+
a nonfinite coordinate is a gap in the track.
|
|
235
|
+
|
|
236
|
+
```{code-cell} ipython3
|
|
237
|
+
tracks = np.stack(
|
|
238
|
+
[find_element(view, f"path/{k}").xy for k in range(K)]
|
|
239
|
+
) # (K, T, 2) arcsec
|
|
240
|
+
bare = prepare_sky_view(tracks, source_ids="planet b", iwa_arcsec=0.06)
|
|
241
|
+
print(np.shares_memory(find_element(bare, "path/0").xy, tracks))
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## In a talk
|
|
245
|
+
|
|
246
|
+
The same sequence plays as a native Manim clip through `eyepiece.manim`,
|
|
247
|
+
with a profile sized for slides and the same cast, so the planet keeps its
|
|
248
|
+
color from paper to talk. The code below is shown rather than run, because
|
|
249
|
+
rendering Manim media needs system libraries a documentation builder does
|
|
250
|
+
not have.
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
import manim
|
|
254
|
+
import eyepiece.manim as em
|
|
255
|
+
from matplotlib.colors import to_hex
|
|
256
|
+
|
|
257
|
+
TALK = snapshot_profile() # 24 pt text and 1.5 pt strokes
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class Orbit(manim.Scene):
|
|
261
|
+
def construct(self):
|
|
262
|
+
self.camera.background_color = manim.ManimColor(to_hex(TALK.background_color))
|
|
263
|
+
clip = em.animate(sequence, cast=CAST, profile=TALK)
|
|
264
|
+
self.add(clip.mobject)
|
|
265
|
+
self.play(clip.playback(run_time=8))
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Render it with Manim's caching disabled (`manim --disable_caching`). The
|
|
269
|
+
playback follows the same physical-time schedule as the Matplotlib movie.
|
|
270
|
+
Eyepiece's [Manim guide](https://eyepiece.readthedocs.io/en/latest/manim.html)
|
|
271
|
+
covers the parts, reveals, and Manim Slides.
|
|
272
|
+
|
|
273
|
+
## Migrating from the earlier sky-track functions
|
|
274
|
+
|
|
275
|
+
Earlier releases drew sky tracks directly through `eyepiece.sky_fan`.
|
|
276
|
+
`plot_sky_track` and `animate_orbit(kind="sky")` now prepare the tracks
|
|
277
|
+
first and render them through eyepiece, which changes the following
|
|
278
|
+
behavior.
|
|
279
|
+
|
|
280
|
+
- Bare tracks are shaped `(K, T, 2)`, (RA, Dec) last, or given as an
|
|
281
|
+
`(ra, dec)` pair of `(T,)` or `(K, T)` arrays. The earlier `(2, T)` and
|
|
282
|
+
`(K, 2, T)` arrays are no longer read: pass a `(2, T)` array as the pair
|
|
283
|
+
`tuple(tracks)`, and a `(K, 2, T)` array as `np.moveaxis(tracks, 1, 2)`.
|
|
284
|
+
- `invert_ra=` is removed. RA always increases to the left.
|
|
285
|
+
- `plot_sky_track` returns an `eyepiece.mpl.MplResult`, not a `PlotResult`.
|
|
286
|
+
Track `k`'s line is `result.parts["path/k"]`, and the IWA disk and the
|
|
287
|
+
observations are `result.parts["iwa"]` and `result.parts["observations"]`.
|
|
288
|
+
- Bare tracks are in arcsec and get the same RA and Dec offset axis labels
|
|
289
|
+
as propagated orbits.
|
|
290
|
+
- A lone track is drawn at opacity 0.80, the top of the shared weight
|
|
291
|
+
mapping, rather than 0.75. Heads carry their track's weight and fade with
|
|
292
|
+
it; the star and the observations carry the top track weight, so they
|
|
293
|
+
stay at full opacity.
|
|
294
|
+
- An `"alpha"` in `fan_kw=` is set last, so it replaces the weight-derived
|
|
295
|
+
opacity instead of scaling it.
|
|
296
|
+
- `plot_sky_track` still accepts `data=(ra, dec, err)` and `iwa=` and
|
|
297
|
+
converts them to `observations=` and `iwa_arcsec=`. `animate_orbit` with
|
|
298
|
+
`kind="sky"` takes only `observations=(xy, xerr, yerr)` and `iwa_arcsec=`,
|
|
299
|
+
so convert there by hand: `observations=(np.column_stack([ra, dec]), err,
|
|
300
|
+
err)`.
|
|
301
|
+
- `animate_orbit(kind="sky")` refuses `style=`, `base_ms=`, and `marks=`,
|
|
302
|
+
which apply to `kind="3d"` only; sky colors come from the cast. It needs
|
|
303
|
+
`t_jd=` for bare tracks too, because the sample times drive its clock and
|
|
304
|
+
its physical-time playback.
|