# `Sidereon.Ephemeris`
[🔗](https://github.com/neilberkman/sidereon-ex/blob/main/lib/sidereon/ephemeris.ex#L1)

JPL/NAIF SPK (DAF `.bsp`) ephemeris kernel reader.

Computes positions and velocities of solar system bodies, spacecraft, and
minor planets from JPL SPK/BSP kernels (DE421, DE440, Horizons exports, etc.).

The kernel is parsed once into a loaded handle by `load/1`; querying never
re-reads the file. Reading and evaluation are delegated to
`sidereon_core::astro::spk`, the validated SPK reader shared by the rest of the
engine and by the other language bindings. It evaluates SPK segment types 2
(Chebyshev position), 3 (Chebyshev state), and 21 (Extended Modified Difference
Arrays), so DE-series planetary kernels and Horizons spacecraft / asteroid
kernels are all supported through the same code path.

## Example

    {:ok, eph} = Sidereon.Ephemeris.load("de421.bsp")

    # Position and velocity at an ephemeris epoch (TDB seconds past J2000):
    {:ok, state} = Sidereon.Ephemeris.state(eph, :moon, :earth, 0.0)
    state.position_km
    state.velocity_km_s

    # Convenience: position only, from a DateTime or Julian Date (TDB):
    {:ok, {x, y, z}} = Sidereon.Ephemeris.position(eph, :sun, :earth, ~U[2024-01-01 12:00:00Z])

    # The parsed segment table:
    Sidereon.Ephemeris.segments(eph)

## Bodies

Bodies may be given as atoms (`:ssb` / `:solar_system_barycenter`, `:mercury`,
`:venus`, `:earth_moon_barycenter` / `:emb`, `:mars`, `:jupiter`, `:saturn`,
`:uranus`, `:neptune`, `:pluto`, `:sun`, `:moon`, `:earth`) or as raw NAIF
integer codes. Integer codes pass straight through to the reader, which is how
spacecraft and minor-planet kernels are queried (e.g. `20000433` for 433 Eros).

# `body`

```elixir
@type body() :: atom() | integer()
```

# `epoch`

```elixir
@type epoch() :: DateTime.t() | NaiveDateTime.t() | float() | integer()
```

# `load_error`

```elixir
@type load_error() ::
  {:file_error, File.posix()} | {:invalid_path, term()} | {:parse_error, term()}
```

# `position_error`

```elixir
@type position_error() :: state_error() | {:invalid_datetime, term()}
```

# `segment`

```elixir
@type segment() :: %{
  name: String.t(),
  target: integer(),
  center: integer(),
  frame: integer(),
  data_type: integer(),
  start_et: float(),
  stop_et: float(),
  start_address: integer(),
  end_address: integer()
}
```

One parsed SPK segment descriptor.

# `state`

```elixir
@type state() :: %{
  target: integer(),
  center: integer(),
  position_km: vec3(),
  velocity_km_s: vec3() | nil,
  frame: integer()
}
```

A body-to-center state from a kernel query.

# `state_error`

```elixir
@type state_error() ::
  {:invalid_body, term()}
  | {:unknown_body, integer()}
  | {:no_segment_path, integer(), integer()}
  | {:nif_error, term()}
```

# `t`

```elixir
@type t() :: %Sidereon.Ephemeris{handle: reference()}
```

A loaded SPK kernel handle.

# `vec3`

```elixir
@type vec3() :: {float(), float(), float()}
```

# `internal_name`

```elixir
@spec internal_name(t()) :: String.t()
```

The DAF internal file name recorded in the kernel header.

# `load`

```elixir
@spec load(term()) :: {:ok, t()} | {:error, load_error()}
```

Load and parse an SPK/BSP ephemeris file into a handle.

The file is read and parsed exactly once; the returned handle holds the parsed
kernel and is passed to `state/4`, `segments/1`, and `position/4`. Returns
`{:ok, ephemeris}` or `{:error, reason}` when the file cannot be read or parsed.

## Example

    {:ok, eph} = Sidereon.Ephemeris.load("/path/to/de421.bsp")

# `load!`

```elixir
@spec load!(String.t()) :: t()
```

Like `load/1` but raises on failure.

# `load_bytes`

```elixir
@spec load_bytes(binary()) :: {:ok, t()} | {:error, {:parse_error, term()}}
```

Parse an SPK/BSP ephemeris kernel from an in-memory byte buffer.

Returns `{:ok, ephemeris}` or `{:error, {:parse_error, reason}}`.

# `position`

```elixir
@spec position(t(), body(), body(), epoch()) ::
  {:ok, vec3()} | {:error, position_error()}
```

Compute the position of `target` relative to `observer` at the given time.

A position-only convenience over `state/4` that accepts a calendar epoch.
Returns `{:ok, {x, y, z}}` in km in the J2000/ICRF reference frame, or
`{:error, reason}`.

The `target` and `observer` are body atoms (see module docs) or NAIF integer
codes. The `datetime` can be a `DateTime`, a `NaiveDateTime`, or a Julian Date
(TDB) as a float.

## Examples

    {:ok, {x, y, z}} = Sidereon.Ephemeris.position(eph, :moon, :earth, datetime)

    # Raw NAIF code (433 Eros) against a Horizons kernel:
    {:ok, {x, y, z}} = Sidereon.Ephemeris.position(eph, 20_000_433, :sun, jd_tdb)

# `position!`

```elixir
@spec position!(t(), body(), body(), epoch()) :: vec3()
```

Like `position/4` but raises on failure.

# `segments`

```elixir
@spec segments(t()) :: [segment()]
```

The kernel's parsed segment descriptors, in DAF summary order.

Each entry is a map with `:name`, `:target`, `:center`, `:frame`,
`:data_type`, `:start_et`, `:stop_et`, `:start_address`, and `:end_address`.
Coverage epochs (`:start_et` / `:stop_et`) are ephemeris (TDB) seconds past
J2000.

# `state`

```elixir
@spec state(t(), body(), body(), number()) :: {:ok, state()} | {:error, state_error()}
```

Query the state of `target` relative to `center` at ephemeris epoch
`et_seconds` (TDB seconds past J2000).

This is the primary query, matching the other language bindings: it resolves
and chains segments as needed and returns both position and velocity. Returns
`{:ok, state}` where `state` is a map with `:target`, `:center`,
`:position_km` (a `{x, y, z}` tuple, km), `:velocity_km_s` (a `{vx, vy, vz}`
tuple in km/s, or `nil` when the resolved path runs through a position-only
type-2 segment), and `:frame` (the NAIF reference-frame id, J2000/ICRF for
standard kernels).

`target` and `center` are body atoms (see module docs) or NAIF integer codes.

Returns `{:error, {:invalid_body, body}}` for an unknown atom,
`{:error, {:unknown_body, code}}` when a body is absent from the kernel,
`{:error, {:no_segment_path, target, center}}` when no segment chain connects
them, and `{:error, {:nif_error, reason}}` when a chain exists but none covers
the epoch or the path needs an unsupported segment type.

## Example

    {:ok, state} = Sidereon.Ephemeris.state(eph, :moon, :earth, 0.0)

# `state!`

```elixir
@spec state!(t(), body(), body(), number()) :: state()
```

Like `state/4` but raises on failure.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
