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