jorbit 0.1.1__py3-none-any.whl
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.
- jorbit/__init__.py +23 -0
- jorbit/accelerations/__init__.py +108 -0
- jorbit/accelerations/gr.py +152 -0
- jorbit/accelerations/newtonian.py +51 -0
- jorbit/astrometry/sky_projection.py +113 -0
- jorbit/astrometry/transformations.py +153 -0
- jorbit/data/(12464)_manhattan_horizons_timeseries.txt +2201 -0
- jorbit/data/(12464)_manhattan_mpc_data.txt +2200 -0
- jorbit/data/(274301)_wikipedia_horizons_timeseries.txt +357 -0
- jorbit/data/(274301)_wikipedia_mpc_data.txt +356 -0
- jorbit/data/coefficients.lisp +183 -0
- jorbit/data/constants.py +388 -0
- jorbit/data/observatory_codes.py +988 -0
- jorbit/ephemeris/__init__.py +10 -0
- jorbit/ephemeris/ephemeris.py +129 -0
- jorbit/ephemeris/ephemeris_processors.py +116 -0
- jorbit/ephemeris/process_bsp.py +104 -0
- jorbit/integrators/__init__.py +193 -0
- jorbit/integrators/ias15.py +1219 -0
- jorbit/integrators/iasnn_arbitrary_prec.py +225 -0
- jorbit/integrators/iasnn_dd_prec.py +246 -0
- jorbit/observation.py +304 -0
- jorbit/particle.py +259 -0
- jorbit/system.py +93 -0
- jorbit/utils/doubledouble.py +247 -0
- jorbit/utils/generate_coefficients.py +170 -0
- jorbit/utils/horizons.py +323 -0
- jorbit/utils/mpc.py +62 -0
- jorbit/utils/states.py +94 -0
- jorbit-0.1.1.dist-info/METADATA +19 -0
- jorbit-0.1.1.dist-info/RECORD +33 -0
- jorbit-0.1.1.dist-info/WHEEL +4 -0
- jorbit-0.1.1.dist-info/licenses/LICENSE +21 -0
jorbit/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from astropy.utils.data import download_files_in_parallel, is_url_in_cache
|
|
3
|
+
|
|
4
|
+
from jorbit.data.constants import (
|
|
5
|
+
DEFAULT_PLANET_EPHEMERIS_URL,
|
|
6
|
+
DEFAULT_ASTEROID_EPHEMERIS_URL,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
if (not is_url_in_cache(DEFAULT_PLANET_EPHEMERIS_URL)) or (
|
|
10
|
+
not is_url_in_cache(DEFAULT_ASTEROID_EPHEMERIS_URL)
|
|
11
|
+
):
|
|
12
|
+
print("JPL DE440 ephemeris files not found in astropy cache, downloading now...")
|
|
13
|
+
print(
|
|
14
|
+
"Files are approx. 765 MB, may take several minutes but will not be repeated."
|
|
15
|
+
)
|
|
16
|
+
download_files_in_parallel(
|
|
17
|
+
[DEFAULT_PLANET_EPHEMERIS_URL, DEFAULT_ASTEROID_EPHEMERIS_URL],
|
|
18
|
+
cache=True,
|
|
19
|
+
show_progress=True,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
from jorbit.particle import Particle
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import jax
|
|
2
|
+
|
|
3
|
+
jax.config.update("jax_enable_x64", True)
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
|
|
6
|
+
from jorbit.utils.states import SystemState
|
|
7
|
+
from jorbit.accelerations.newtonian import newtonian_gravity
|
|
8
|
+
from jorbit.accelerations.gr import ppn_gravity
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_newtonian_ephemeris_acceleration_func(ephem_processor):
|
|
12
|
+
|
|
13
|
+
def func(inputs: SystemState) -> jnp.ndarray:
|
|
14
|
+
perturber_xs, perturber_vs = ephem_processor.state(inputs.time)
|
|
15
|
+
perturber_log_gms = ephem_processor.log_gms
|
|
16
|
+
|
|
17
|
+
new_state = SystemState(
|
|
18
|
+
massive_positions=jnp.concatenate([perturber_xs, inputs.massive_positions]),
|
|
19
|
+
massive_velocities=jnp.concatenate(
|
|
20
|
+
[perturber_vs, inputs.massive_velocities]
|
|
21
|
+
),
|
|
22
|
+
tracer_positions=inputs.tracer_positions,
|
|
23
|
+
tracer_velocities=inputs.tracer_velocities,
|
|
24
|
+
log_gms=jnp.concatenate([perturber_log_gms, inputs.log_gms]),
|
|
25
|
+
time=inputs.time,
|
|
26
|
+
acceleration_func_kwargs=inputs.acceleration_func_kwargs,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
accs = newtonian_gravity(new_state)
|
|
30
|
+
|
|
31
|
+
num_perturbers = perturber_xs.shape[0]
|
|
32
|
+
return accs[num_perturbers:]
|
|
33
|
+
|
|
34
|
+
return jax.tree_util.Partial(func)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_gr_ephemeris_acceleration_func(ephem_processor):
|
|
38
|
+
|
|
39
|
+
def func(inputs: SystemState) -> jnp.ndarray:
|
|
40
|
+
perturber_xs, perturber_vs = ephem_processor.state(inputs.time)
|
|
41
|
+
perturber_log_gms = ephem_processor.log_gms
|
|
42
|
+
|
|
43
|
+
new_state = SystemState(
|
|
44
|
+
massive_positions=jnp.concatenate([perturber_xs, inputs.massive_positions]),
|
|
45
|
+
massive_velocities=jnp.concatenate(
|
|
46
|
+
[perturber_vs, inputs.massive_velocities]
|
|
47
|
+
),
|
|
48
|
+
tracer_positions=inputs.tracer_positions,
|
|
49
|
+
tracer_velocities=inputs.tracer_velocities,
|
|
50
|
+
log_gms=jnp.concatenate([perturber_log_gms, inputs.log_gms]),
|
|
51
|
+
time=inputs.time,
|
|
52
|
+
acceleration_func_kwargs=inputs.acceleration_func_kwargs,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
accs = ppn_gravity(new_state)
|
|
56
|
+
|
|
57
|
+
num_perturbers = perturber_xs.shape[0]
|
|
58
|
+
return accs[num_perturbers:]
|
|
59
|
+
|
|
60
|
+
return jax.tree_util.Partial(func)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def create_default_ephemeris_acceleration_func(ephem_processor):
|
|
64
|
+
|
|
65
|
+
def func(inputs: SystemState) -> jnp.ndarray:
|
|
66
|
+
num_gr_perturbers = 10 # the "planets"
|
|
67
|
+
num_newtonian_perturbers = 16 # the asteroids
|
|
68
|
+
|
|
69
|
+
perturber_xs, perturber_vs = ephem_processor.state(inputs.time)
|
|
70
|
+
perturber_log_gms = ephem_processor.log_gms
|
|
71
|
+
|
|
72
|
+
gr_state = SystemState(
|
|
73
|
+
massive_positions=jnp.concatenate(
|
|
74
|
+
[perturber_xs[:num_gr_perturbers], inputs.massive_positions]
|
|
75
|
+
),
|
|
76
|
+
massive_velocities=jnp.concatenate(
|
|
77
|
+
[perturber_vs[:num_gr_perturbers], inputs.massive_velocities]
|
|
78
|
+
),
|
|
79
|
+
tracer_positions=inputs.tracer_positions,
|
|
80
|
+
tracer_velocities=inputs.tracer_velocities,
|
|
81
|
+
log_gms=jnp.concatenate(
|
|
82
|
+
[perturber_log_gms[:num_gr_perturbers], inputs.log_gms]
|
|
83
|
+
),
|
|
84
|
+
time=inputs.time,
|
|
85
|
+
acceleration_func_kwargs=inputs.acceleration_func_kwargs,
|
|
86
|
+
)
|
|
87
|
+
gr_acc = ppn_gravity(gr_state)[num_gr_perturbers:]
|
|
88
|
+
|
|
89
|
+
newtonian_state = SystemState(
|
|
90
|
+
massive_positions=jnp.concatenate(
|
|
91
|
+
[perturber_xs[num_gr_perturbers:], inputs.massive_positions]
|
|
92
|
+
),
|
|
93
|
+
massive_velocities=jnp.concatenate(
|
|
94
|
+
[perturber_vs[num_gr_perturbers:], inputs.massive_velocities]
|
|
95
|
+
),
|
|
96
|
+
tracer_positions=inputs.tracer_positions,
|
|
97
|
+
tracer_velocities=inputs.tracer_velocities,
|
|
98
|
+
log_gms=jnp.concatenate(
|
|
99
|
+
[perturber_log_gms[num_gr_perturbers:], inputs.log_gms]
|
|
100
|
+
),
|
|
101
|
+
time=inputs.time,
|
|
102
|
+
acceleration_func_kwargs=inputs.acceleration_func_kwargs,
|
|
103
|
+
)
|
|
104
|
+
newtonian_acc = newtonian_gravity(newtonian_state)[num_newtonian_perturbers:]
|
|
105
|
+
|
|
106
|
+
return gr_acc + newtonian_acc
|
|
107
|
+
|
|
108
|
+
return jax.tree_util.Partial(func)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# These are pythonized/jaxified versions of acceleration models within REBOUNDx,
|
|
2
|
+
# Tamayo et al. (2020) (DOI: 10.1093/mnras/stz2870). The gr_full function is the
|
|
3
|
+
# equivalent of rebx_calculate_gr_full in REBOUNDx, which is itself based on
|
|
4
|
+
# Newhall et al. (1984) (bibcode: 1983A&A...125..150N)
|
|
5
|
+
# The original code is available at https://github.com/dtamayo/reboundx/blob/502abf3066d9bae174cb20538294c916e73391cd/src/gr_full.c
|
|
6
|
+
# Accessed Fall 2024.
|
|
7
|
+
|
|
8
|
+
# Many thanks to the REBOUNDx developers for their work, and for making it open source!
|
|
9
|
+
|
|
10
|
+
import jax
|
|
11
|
+
|
|
12
|
+
jax.config.update("jax_enable_x64", True)
|
|
13
|
+
import jax.numpy as jnp
|
|
14
|
+
|
|
15
|
+
from functools import partial
|
|
16
|
+
|
|
17
|
+
from jorbit.utils.states import SystemState
|
|
18
|
+
from jorbit.accelerations.newtonian import newtonian_gravity
|
|
19
|
+
from jorbit.data.constants import SPEED_OF_LIGHT
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# equivalent of rebx_calculate_gr_full in reboundx
|
|
23
|
+
@partial(jax.jit, static_argnames=["max_iterations"])
|
|
24
|
+
def ppn_gravity(
|
|
25
|
+
inputs: SystemState,
|
|
26
|
+
max_iterations: int = 10,
|
|
27
|
+
) -> jnp.ndarray:
|
|
28
|
+
|
|
29
|
+
c2 = inputs.acceleration_func_kwargs.get("c2", SPEED_OF_LIGHT**2)
|
|
30
|
+
|
|
31
|
+
# surrendering on the efficient handling of tracers vs. massive particles-
|
|
32
|
+
# lots of unnecessary computation here if T > 0, but ah well for now
|
|
33
|
+
M = inputs.massive_positions.shape[0]
|
|
34
|
+
T = inputs.tracer_positions.shape[0]
|
|
35
|
+
N = M + T
|
|
36
|
+
positions = jnp.concatenate(
|
|
37
|
+
[inputs.massive_positions, inputs.tracer_positions], axis=0
|
|
38
|
+
)
|
|
39
|
+
velocities = jnp.concatenate(
|
|
40
|
+
[inputs.massive_velocities, inputs.tracer_velocities], axis=0
|
|
41
|
+
)
|
|
42
|
+
gms = jnp.concatenate([jnp.exp(inputs.log_gms), jnp.zeros(T)])
|
|
43
|
+
|
|
44
|
+
dx = positions[:, None, :] - positions[None, :, :]
|
|
45
|
+
r2 = jnp.sum(dx * dx, axis=-1)
|
|
46
|
+
r = jnp.sqrt(r2)
|
|
47
|
+
r3 = r2 * jnp.sqrt(r2)
|
|
48
|
+
|
|
49
|
+
# Mask for i!=j calculations
|
|
50
|
+
mask = ~jnp.eye(N, dtype=bool)
|
|
51
|
+
prefac = jnp.where(mask, 1.0 / r3, 0.0)
|
|
52
|
+
|
|
53
|
+
# Newtonian acceleration
|
|
54
|
+
a_newt = -jnp.sum(
|
|
55
|
+
prefac[:, :, None] * dx * gms[None, :, None],
|
|
56
|
+
axis=1,
|
|
57
|
+
) # (N,3)
|
|
58
|
+
|
|
59
|
+
dv = velocities[:, None, :] - velocities[None, :, :] # (N,N,3)
|
|
60
|
+
|
|
61
|
+
x_com = jnp.sum(positions * gms[:, None], axis=0) / jnp.sum(gms)
|
|
62
|
+
v_com = jnp.sum(velocities * gms[:, None], axis=0) / jnp.sum(gms)
|
|
63
|
+
|
|
64
|
+
positions = positions - x_com
|
|
65
|
+
velocities = velocities - v_com
|
|
66
|
+
|
|
67
|
+
# Compute velocity-dependent terms
|
|
68
|
+
v2 = jnp.sum(velocities * velocities, axis=-1) # (N,)
|
|
69
|
+
vdot = jnp.sum(velocities[:, None, :] * velocities[None, :, :], axis=-1) # (N,N)
|
|
70
|
+
|
|
71
|
+
a1 = jnp.sum((4.0 / c2) * gms / r, axis=1, where=mask)
|
|
72
|
+
a1 = jnp.broadcast_to(a1, (N, N)).T
|
|
73
|
+
|
|
74
|
+
a2 = jnp.sum((1.0 / c2) * gms / r, axis=1, where=mask)
|
|
75
|
+
a2 = jnp.broadcast_to(a2, (N, N))
|
|
76
|
+
|
|
77
|
+
a3 = jnp.broadcast_to(-v2 / c2, (N, N)).T
|
|
78
|
+
a4 = -2.0 * jnp.broadcast_to(v2, (N, N)) / c2
|
|
79
|
+
a5 = (4.0 / c2) * vdot
|
|
80
|
+
|
|
81
|
+
a6_0 = jnp.sum(dx * velocities[None, :, :], axis=-1)
|
|
82
|
+
a6 = (3.0 / (2 * c2)) * (a6_0**2) / r2
|
|
83
|
+
|
|
84
|
+
a7 = jnp.sum(dx * a_newt[None, :, :], axis=-1) / (2 * c2)
|
|
85
|
+
|
|
86
|
+
factor1 = a1 + a2 + a3 + a4 + a5 + a6 + a7
|
|
87
|
+
part1 = (
|
|
88
|
+
jnp.broadcast_to(gms, (N, N))[:, :, None]
|
|
89
|
+
* dx
|
|
90
|
+
* factor1[:, :, None]
|
|
91
|
+
/ r3[:, :, None]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
factor2 = jnp.sum(
|
|
95
|
+
dx * (4 * velocities[:, None, :] - 3 * velocities[None, :, :]), axis=-1
|
|
96
|
+
)
|
|
97
|
+
part2 = (
|
|
98
|
+
jnp.broadcast_to(gms, (N, N))[:, :, None]
|
|
99
|
+
* (
|
|
100
|
+
factor2[:, :, None] * dv / r3[:, :, None]
|
|
101
|
+
+ 7.0 / 2.0 * a_newt[None, :, :] / r[:, :, None]
|
|
102
|
+
)
|
|
103
|
+
/ c2
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
a_const = jnp.sum(part1 + part2, axis=1, where=mask[:, :, None])
|
|
107
|
+
|
|
108
|
+
def iteration_step(a_curr):
|
|
109
|
+
rdota = jnp.sum(dx * a_curr[None, :, :], axis=-1) # (N, N)
|
|
110
|
+
non_const = jnp.sum(
|
|
111
|
+
(gms[None, :, None] / (2.0 * c2))
|
|
112
|
+
* (
|
|
113
|
+
(dx * rdota[:, :, None] / r3[:, :, None])
|
|
114
|
+
+ (7.0 * a_curr[None, :, :] / r[:, :, None])
|
|
115
|
+
),
|
|
116
|
+
axis=1,
|
|
117
|
+
where=mask[:, :, None],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return non_const
|
|
121
|
+
|
|
122
|
+
def do_nothing(carry):
|
|
123
|
+
return carry
|
|
124
|
+
|
|
125
|
+
def do_iteration(carry):
|
|
126
|
+
a_prev, a_curr, _ = carry
|
|
127
|
+
non_const = iteration_step(a_curr)
|
|
128
|
+
a_next = a_const + non_const
|
|
129
|
+
|
|
130
|
+
ratio = jnp.max(jnp.abs((a_next - a_curr) / a_next), initial=0.0)
|
|
131
|
+
|
|
132
|
+
return (a_curr, a_next, ratio)
|
|
133
|
+
|
|
134
|
+
def body_fn(carry, _):
|
|
135
|
+
a_prev, a_curr, ratio = carry
|
|
136
|
+
should_continue = ratio > jnp.finfo(jnp.float64).eps
|
|
137
|
+
new_carry = jax.lax.cond(should_continue, do_iteration, do_nothing, carry)
|
|
138
|
+
return new_carry, None
|
|
139
|
+
|
|
140
|
+
# Initialize with constant terms
|
|
141
|
+
init_a = jnp.zeros_like(a_const)
|
|
142
|
+
init_carry = (init_a, a_const, 1.0)
|
|
143
|
+
|
|
144
|
+
# Run fixed number of iterations using scan
|
|
145
|
+
final_carry, _ = jax.lax.scan(body_fn, init_carry, None, length=max_iterations)
|
|
146
|
+
|
|
147
|
+
_, a_final, _ = final_carry
|
|
148
|
+
|
|
149
|
+
# Combine Newtonian and GR terms
|
|
150
|
+
a = a_newt + a_final
|
|
151
|
+
|
|
152
|
+
return a
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import jax
|
|
2
|
+
|
|
3
|
+
jax.config.update("jax_enable_x64", True)
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
|
|
6
|
+
from jorbit.utils.states import SystemState
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@jax.jit
|
|
10
|
+
def newtonian_gravity(inputs: SystemState) -> jnp.ndarray:
|
|
11
|
+
M = inputs.massive_positions.shape[0] # number of massive particles
|
|
12
|
+
T = inputs.tracer_positions.shape[0] # number of tracer particles
|
|
13
|
+
|
|
14
|
+
# 1. Compute accelerations on massive particles due to other massive particles
|
|
15
|
+
dx_massive = (
|
|
16
|
+
inputs.massive_positions[:, None, :] - inputs.massive_positions[None, :, :]
|
|
17
|
+
) # (M,M,3)
|
|
18
|
+
r2_massive = jnp.sum(dx_massive * dx_massive, axis=-1) # (M,M)
|
|
19
|
+
r3_massive = r2_massive * jnp.sqrt(r2_massive) # (M,M)
|
|
20
|
+
|
|
21
|
+
# Mask for i!=j calculations among massive particles
|
|
22
|
+
mask_massive = ~jnp.eye(M, dtype=bool) # (M,M)
|
|
23
|
+
prefac_massive = jnp.where(mask_massive, 1.0 / r3_massive, 0.0)
|
|
24
|
+
|
|
25
|
+
# Accelerations on massive particles from massive particles
|
|
26
|
+
a_massive = -jnp.sum(
|
|
27
|
+
prefac_massive[:, :, None]
|
|
28
|
+
* dx_massive
|
|
29
|
+
* jnp.exp(inputs.log_gms[None, :, None]),
|
|
30
|
+
axis=1,
|
|
31
|
+
) # (M,3)
|
|
32
|
+
|
|
33
|
+
# 2. Compute accelerations on tracer particles due to massive particles
|
|
34
|
+
# (This will work even when T=0 due to JAX's shape polymorphism)
|
|
35
|
+
dx_tracers = (
|
|
36
|
+
inputs.tracer_positions[:, None, :] - inputs.massive_positions[None, :, :]
|
|
37
|
+
) # (T,M,3)
|
|
38
|
+
r2_tracers = jnp.sum(dx_tracers * dx_tracers, axis=-1) # (T,M)
|
|
39
|
+
r3_tracers = r2_tracers * jnp.sqrt(r2_tracers) # (T,M)
|
|
40
|
+
|
|
41
|
+
# Accelerations on tracer particles from massive particles
|
|
42
|
+
a_tracers = -jnp.sum(
|
|
43
|
+
(1.0 / r3_tracers)[:, :, None]
|
|
44
|
+
* dx_tracers
|
|
45
|
+
* jnp.exp(inputs.log_gms[None, :, None]),
|
|
46
|
+
axis=1,
|
|
47
|
+
) # (T,3)
|
|
48
|
+
|
|
49
|
+
# Combine accelerations for all particles
|
|
50
|
+
# This works even when T=0 thanks to JAX's handling of zero-sized arrays
|
|
51
|
+
return jnp.concatenate([a_massive, a_tracers], axis=0)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import jax
|
|
2
|
+
|
|
3
|
+
jax.config.update("jax_enable_x64", True)
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
|
|
6
|
+
from jorbit.utils.states import SystemState
|
|
7
|
+
from jorbit.integrators import initialize_ias15_integrator_state, ias15_evolve
|
|
8
|
+
from jorbit.data.constants import INV_SPEED_OF_LIGHT
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@jax.jit
|
|
12
|
+
def sky_sep(ra1, dec1, ra2, dec2):
|
|
13
|
+
# all inputs are floats, ICRS positions in radians
|
|
14
|
+
# output is in arcsec
|
|
15
|
+
|
|
16
|
+
# following the astropy source on .separation, using Vincenty
|
|
17
|
+
delta = ra2 - ra1
|
|
18
|
+
numerator = jnp.sqrt(
|
|
19
|
+
(jnp.cos(dec2) * jnp.sin(delta)) ** 2
|
|
20
|
+
+ (
|
|
21
|
+
jnp.cos(dec1) * jnp.sin(dec2)
|
|
22
|
+
- jnp.sin(dec1) * jnp.cos(dec2) * jnp.cos(delta)
|
|
23
|
+
)
|
|
24
|
+
** 2
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
denominator = jnp.sin(dec1) * jnp.sin(dec2) + jnp.cos(dec1) * jnp.cos(
|
|
28
|
+
dec2
|
|
29
|
+
) * jnp.cos(delta)
|
|
30
|
+
|
|
31
|
+
return jnp.arctan2(numerator, denominator) * 206264.80624709636
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@jax.jit
|
|
35
|
+
def tangent_plane_projection(ra_ref, dec_ref, ra, dec):
|
|
36
|
+
# Convert to unit vectors
|
|
37
|
+
cos_dec = jnp.cos(dec)
|
|
38
|
+
sin_dec = jnp.sin(dec)
|
|
39
|
+
cos_ra = jnp.cos(ra)
|
|
40
|
+
sin_ra = jnp.sin(ra)
|
|
41
|
+
|
|
42
|
+
# Initial cartesian coordinates
|
|
43
|
+
x = cos_dec * cos_ra
|
|
44
|
+
y = cos_dec * sin_ra
|
|
45
|
+
z = sin_dec
|
|
46
|
+
|
|
47
|
+
# Rotation matrices (combined into single operation)
|
|
48
|
+
cos_ra_ref = jnp.cos(ra_ref)
|
|
49
|
+
sin_ra_ref = jnp.sin(ra_ref)
|
|
50
|
+
cos_dec_ref = jnp.cos(dec_ref)
|
|
51
|
+
sin_dec_ref = jnp.sin(dec_ref)
|
|
52
|
+
|
|
53
|
+
# Apply rotations (optimized matrix multiplication)
|
|
54
|
+
x_rot = (x * cos_ra_ref + y * sin_ra_ref) * cos_dec_ref + z * sin_dec_ref
|
|
55
|
+
y_rot = -x * sin_ra_ref + y * cos_ra_ref
|
|
56
|
+
z_rot = -(x * cos_ra_ref + y * sin_ra_ref) * sin_dec_ref + z * cos_dec_ref
|
|
57
|
+
|
|
58
|
+
# Project to plane
|
|
59
|
+
xi = y_rot / x_rot
|
|
60
|
+
eta = z_rot / x_rot
|
|
61
|
+
|
|
62
|
+
return jnp.array([xi, eta])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@jax.jit
|
|
66
|
+
def on_sky(
|
|
67
|
+
x,
|
|
68
|
+
v,
|
|
69
|
+
time,
|
|
70
|
+
observer_position,
|
|
71
|
+
acc_func,
|
|
72
|
+
acc_func_kwargs={},
|
|
73
|
+
):
|
|
74
|
+
# has to be one particle at one time to get the light travel time right
|
|
75
|
+
state = SystemState(
|
|
76
|
+
massive_positions=jnp.empty((0, 3)),
|
|
77
|
+
massive_velocities=jnp.empty((0, 3)),
|
|
78
|
+
tracer_positions=jnp.array([x]),
|
|
79
|
+
tracer_velocities=jnp.array([v]),
|
|
80
|
+
log_gms=jnp.empty(0),
|
|
81
|
+
time=time,
|
|
82
|
+
acceleration_func_kwargs=acc_func_kwargs,
|
|
83
|
+
)
|
|
84
|
+
a0 = acc_func(state)
|
|
85
|
+
initial_integrator_state = initialize_ias15_integrator_state(a0)
|
|
86
|
+
|
|
87
|
+
def scan_func(carry, scan_over):
|
|
88
|
+
xz = carry
|
|
89
|
+
earth_distance = jnp.linalg.norm(xz - observer_position)
|
|
90
|
+
light_travel_time = earth_distance * INV_SPEED_OF_LIGHT
|
|
91
|
+
|
|
92
|
+
positions, velocities, final_system_state, final_integrator_state = (
|
|
93
|
+
ias15_evolve(
|
|
94
|
+
state,
|
|
95
|
+
acc_func,
|
|
96
|
+
jnp.array([state.time - light_travel_time]),
|
|
97
|
+
initial_integrator_state,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return final_system_state.tracer_positions[0], None
|
|
102
|
+
|
|
103
|
+
xz, _ = jax.lax.scan(
|
|
104
|
+
scan_func,
|
|
105
|
+
state.tracer_positions[0],
|
|
106
|
+
None,
|
|
107
|
+
length=3,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
X = xz - observer_position
|
|
111
|
+
calc_ra = jnp.mod(jnp.arctan2(X[1], X[0]) + 2 * jnp.pi, 2 * jnp.pi)
|
|
112
|
+
calc_dec = jnp.pi / 2 - jnp.arccos(X[-1] / jnp.linalg.norm(X))
|
|
113
|
+
return calc_ra, calc_dec
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import jax
|
|
2
|
+
|
|
3
|
+
jax.config.update("jax_enable_x64", True)
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
|
|
6
|
+
from jorbit.data.constants import (
|
|
7
|
+
ICRS_TO_HORIZONS_ECLIPTIC_ROT_MAT,
|
|
8
|
+
HORIZONS_ECLIPTIC_TO_ICRS_ROT_MAT,
|
|
9
|
+
TOTAL_SOLAR_SYSTEM_GM,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@jax.jit
|
|
14
|
+
def icrs_to_horizons_ecliptic(xs):
|
|
15
|
+
rotated_xs = jnp.dot(xs, ICRS_TO_HORIZONS_ECLIPTIC_ROT_MAT.T)
|
|
16
|
+
return rotated_xs
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@jax.jit
|
|
20
|
+
def horizons_ecliptic_to_icrs(xs):
|
|
21
|
+
rotated_xs = jnp.dot(xs, HORIZONS_ECLIPTIC_TO_ICRS_ROT_MAT.T)
|
|
22
|
+
return rotated_xs
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@jax.jit
|
|
26
|
+
def elements_to_cartesian(a, ecc, nu, inc, Omega, omega):
|
|
27
|
+
# # Each of the elements are (n_particles, )
|
|
28
|
+
# # The angles are in *degrees*. Always assuming orbital element angles are in degrees
|
|
29
|
+
|
|
30
|
+
nu *= jnp.pi / 180
|
|
31
|
+
inc *= jnp.pi / 180
|
|
32
|
+
Omega *= jnp.pi / 180
|
|
33
|
+
omega *= jnp.pi / 180
|
|
34
|
+
|
|
35
|
+
t = (a * (1 - ecc**2))[:, None]
|
|
36
|
+
r_w = (
|
|
37
|
+
t
|
|
38
|
+
/ (1 + ecc[:, None] * jnp.cos(nu[:, None]))
|
|
39
|
+
* jnp.column_stack((jnp.cos(nu), jnp.sin(nu), nu * 0.0))
|
|
40
|
+
)
|
|
41
|
+
v_w = (
|
|
42
|
+
jnp.sqrt(TOTAL_SOLAR_SYSTEM_GM)
|
|
43
|
+
/ jnp.sqrt(t)
|
|
44
|
+
* jnp.column_stack((-jnp.sin(nu), ecc + jnp.cos(nu), nu * 0))
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
zeros = jnp.zeros_like(omega, dtype=jnp.float64)
|
|
48
|
+
ones = jnp.ones_like(omega, dtype=jnp.float64)
|
|
49
|
+
Rot1 = jnp.array(
|
|
50
|
+
[
|
|
51
|
+
[jnp.cos(-omega), -jnp.sin(-omega), zeros],
|
|
52
|
+
[jnp.sin(-omega), jnp.cos(-omega), zeros],
|
|
53
|
+
[zeros, zeros, ones],
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
Rot2 = jnp.array(
|
|
58
|
+
[
|
|
59
|
+
[ones, zeros, zeros],
|
|
60
|
+
[zeros, jnp.cos(-inc), -jnp.sin(-inc)],
|
|
61
|
+
[zeros, jnp.sin(-inc), jnp.cos(-inc)],
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
Rot3 = jnp.array(
|
|
66
|
+
[
|
|
67
|
+
[jnp.cos(-Omega), -jnp.sin(-Omega), zeros],
|
|
68
|
+
[jnp.sin(-Omega), jnp.cos(-Omega), zeros],
|
|
69
|
+
[zeros, zeros, ones],
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
rot = jax.vmap(
|
|
74
|
+
lambda r1, r2, r3: jnp.matmul(jnp.matmul(r1, r2), r3), in_axes=(2, 2, 2)
|
|
75
|
+
)(Rot1, Rot2, Rot3)
|
|
76
|
+
|
|
77
|
+
x = jax.vmap(lambda x, y: jnp.matmul(x, y))(r_w, rot)
|
|
78
|
+
v = jax.vmap(lambda x, y: jnp.matmul(x, y))(v_w, rot)
|
|
79
|
+
|
|
80
|
+
return x, v
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@jax.jit
|
|
84
|
+
def cartesian_to_elements(x, v):
|
|
85
|
+
r_mag = jnp.linalg.norm(x, axis=1)
|
|
86
|
+
v_mag = jnp.linalg.norm(v, axis=1)
|
|
87
|
+
|
|
88
|
+
# Specific angular momentum
|
|
89
|
+
h = jnp.cross(x, v)
|
|
90
|
+
h_mag = jnp.linalg.norm(h, axis=1)
|
|
91
|
+
|
|
92
|
+
# Eccentricity vector
|
|
93
|
+
e_vec = jnp.cross(v, h) / TOTAL_SOLAR_SYSTEM_GM - x / r_mag[:, jnp.newaxis]
|
|
94
|
+
ecc = jnp.linalg.norm(e_vec, axis=1)
|
|
95
|
+
|
|
96
|
+
# Specific orbital energy
|
|
97
|
+
specific_energy = v_mag**2 / 2 - TOTAL_SOLAR_SYSTEM_GM / r_mag
|
|
98
|
+
|
|
99
|
+
a = -TOTAL_SOLAR_SYSTEM_GM / (2 * specific_energy)
|
|
100
|
+
|
|
101
|
+
inc = jnp.arccos(h[:, 2] / h_mag) * 180 / jnp.pi
|
|
102
|
+
|
|
103
|
+
n = jnp.cross(jnp.array([0, 0, 1]), h)
|
|
104
|
+
n_mag = jnp.linalg.norm(n, axis=1)
|
|
105
|
+
|
|
106
|
+
Omega = jnp.where(
|
|
107
|
+
n[:, 1] >= 0,
|
|
108
|
+
jnp.arccos(n[:, 0] / n_mag) * 180 / jnp.pi,
|
|
109
|
+
360.0 - jnp.arccos(n[:, 0] / n_mag) * 180 / jnp.pi,
|
|
110
|
+
)
|
|
111
|
+
Omega = jnp.where(n_mag == 0, 0, Omega)
|
|
112
|
+
|
|
113
|
+
omega = jnp.where(
|
|
114
|
+
n_mag > 0,
|
|
115
|
+
jnp.where(
|
|
116
|
+
e_vec[:, 2] >= 0,
|
|
117
|
+
jnp.arccos(
|
|
118
|
+
jnp.clip(
|
|
119
|
+
jnp.sum(n * e_vec, axis=1)
|
|
120
|
+
/ (n_mag * jnp.linalg.norm(e_vec, axis=1)),
|
|
121
|
+
-1,
|
|
122
|
+
1,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
* 180
|
|
126
|
+
/ jnp.pi,
|
|
127
|
+
360
|
|
128
|
+
- jnp.arccos(
|
|
129
|
+
jnp.clip(
|
|
130
|
+
jnp.sum(n * e_vec, axis=1)
|
|
131
|
+
/ (n_mag * jnp.linalg.norm(e_vec, axis=1)),
|
|
132
|
+
-1,
|
|
133
|
+
1,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
* 180
|
|
137
|
+
/ jnp.pi,
|
|
138
|
+
),
|
|
139
|
+
0,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
nu = jnp.where(
|
|
143
|
+
jnp.sum(x * v, axis=1) >= 0,
|
|
144
|
+
jnp.arccos(jnp.clip(jnp.sum(e_vec * x, axis=1) / (ecc * r_mag), -1, 1))
|
|
145
|
+
* 180
|
|
146
|
+
/ jnp.pi,
|
|
147
|
+
360
|
|
148
|
+
- jnp.arccos(jnp.clip(jnp.sum(e_vec * x, axis=1) / (ecc * r_mag), -1, 1))
|
|
149
|
+
* 180
|
|
150
|
+
/ jnp.pi,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return a, ecc, nu, inc, Omega, omega
|