from __future__ import annotations
import math
import numpy as np
from benchmarks.gpu_comparison.implementations import astrojax_kernels
from benchmarks.gpu_comparison.implementations.jax_utils import shard_across_devices
from benchmarks.gpu_comparison.tasks.base import BatchConfig, BatchTask
R_EARTH = 6378137.0
GM_EARTH = 3.986004418e14
class ForceModelGrav5x5Task(BatchTask):
name = "force_model.grav_5x5"
module = "force_model"
description = (
"RK4-propagate N LEO orbits over ~1 orbital period (30s step) with "
"5x5 spherical-harmonic gravity (EGM2008)."
)
configs = [
BatchConfig(name="brahe-rust-rayon", dtype="f64", backend="rust"),
BatchConfig(name="astrojax-cpu", dtype="f64", backend="astrojax-cpu"),
BatchConfig(name="astrojax-gpu", dtype="f32", backend="astrojax-gpu"),
BatchConfig(name="astrojax-multigpu", dtype="f32", backend="astrojax-multigpu"),
]
def multigpu_min_batch(self) -> int:
return 1_000
STEP_SIZE = 30.0
N_STEPS = 180
def batch_sizes(self) -> list[int]:
return [1, 10, 100, 1_000, 10_000, 100_000]
def generate_inputs(self, batch_size: int, seed: int) -> dict:
rng = np.random.default_rng(seed)
a = R_EARTH + rng.uniform(400e3, 800e3, batch_size)
v = np.sqrt(GM_EARTH / a)
nu = rng.uniform(0.0, 2 * np.pi, batch_size)
states = np.empty((batch_size, 6), dtype=np.float64)
states[:, 0] = a * np.cos(nu)
states[:, 1] = a * np.sin(nu)
states[:, 2] = 0.0
states[:, 3] = -v * np.sin(nu)
states[:, 4] = v * np.cos(nu)
states[:, 5] = 0.0
return {
"states_eci": states.tolist(),
"step_size": self.STEP_SIZE,
"n_steps": self.N_STEPS,
"gravity_degree": 5,
"gravity_order": 5,
}
def _jnp_dtype(dtype: str):
import jax.numpy as jnp
return jnp.float32 if dtype == "f32" else jnp.float64
def _build_grav_5x5(task, batch_size, dtype, seed, devices):
import jax
import jax.numpy as jnp
from astrojax import Epoch, set_dtype
from astrojax.eop import zero_eop
from astrojax.integrators import rk4_step
from astrojax.orbit_dynamics.config import ForceModelConfig
from astrojax.orbit_dynamics.factory import create_orbit_dynamics
from astrojax.orbit_dynamics.gravity import GravityModel
set_dtype(jnp.float32 if dtype == "f32" else jnp.float64)
grav = GravityModel.from_type("JGM3")
epoch_0 = Epoch(2024, 6, 15, 12, 0, 0.0)
cfg = ForceModelConfig(
gravity_type="spherical_harmonics",
gravity_model=grav,
gravity_degree=5,
gravity_order=5,
)
dyn = create_orbit_dynamics(zero_eop(), epoch_0, cfg)
dt = task.STEP_SIZE
n_steps = task.N_STEPS
params = task.generate_inputs(batch_size, seed)
states = jnp.array(params["states_eci"], dtype=_jnp_dtype(dtype))
def _propagate_one(x0):
def body(x, _):
return rk4_step(dyn, 0.0, x, dt).state, None
final, _ = jax.lax.scan(body, x0, None, length=n_steps)
return final
if len(devices) == 1 and hasattr(devices[0], "device_kind"):
placed = jax.device_put(states, devices[0])
compiled = jax.jit(jax.vmap(_propagate_one), device=devices[0])
return (lambda _: compiled(placed)), {}
elif len(devices) == 1:
return (lambda _: states), {}
else:
n_dev = len(devices)
padded = ((batch_size + n_dev - 1) // n_dev) * n_dev
if padded > batch_size:
pad = jnp.zeros((padded - batch_size, 6), dtype=states.dtype)
states = jnp.concatenate([states, pad], axis=0)
reshaped = states.reshape(n_dev, -1, 6)
placed = shard_across_devices(reshaped, devices)
compiled = jax.pmap(jax.vmap(_propagate_one))
return (lambda _: compiled(placed)), {}
astrojax_kernels.register("force_model.grav_5x5", _build_grav_5x5)