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

Geoid undulation lookup and orthometric/ellipsoidal height conversion.

The geoid undulation `N` is the height of the geoid (mean sea level) above the
WGS84 ellipsoid in metres. GNSS yields the ellipsoidal height `h`; the
orthometric height (height above mean sea level) is `H = h - N`.

Two entry points are exposed over the `sidereon-core` `geoid` module:

  * zero-setup lookups against the crate's COARSE 30-degree built-in global grid
    (`undulation/2`, `orthometric_height_m/3`, `ellipsoidal_height_m/3`), and
  * a loaded grid handle (`load_grid/1`, `grid/7`) for a real vendor model,
    queried with `grid_undulation_deg/3` / `grid_undulation_rad/3`, and
  * PROJ 9.3.0-compatible EGM96 GTX loading and vertical-grid interpolation
    with an explicit fused or separately rounded arithmetic mode.

The built-in grid is suitable for sanity checks and metre-scale fallback, not
survey work; load a real model for accuracy.

Latitude is positive north, longitude positive east. The built-in lookups and
`grid_undulation_rad/3` take **radians**; `grid_undulation_deg/3` takes degrees.

# `egm2008_spacing`

```elixir
@type egm2008_spacing() :: :one_minute | :two_point_five_minute | String.t()
```

# `grid`

```elixir
@type grid() :: reference()
```

# `proj_vgridshift_arithmetic`

```elixir
@type proj_vgridshift_arithmetic() :: :separate_multiply_add | :fused_multiply_add
```

# `egm96_ellipsoidal_height_m`

```elixir
@spec egm96_ellipsoidal_height_m(number(), number(), number()) :: float()
```

Ellipsoidal height `h = H + N` (metres) from an orthometric height, using the
embedded genuine EGM96 1-degree model. Position in radians.

# `egm96_orthometric_height_m`

```elixir
@spec egm96_orthometric_height_m(number(), number(), number()) :: float()
```

Orthometric height `H = h - N` (metres) from an ellipsoidal height, using the
embedded genuine EGM96 1-degree model. Position in radians.

# `egm96_undulation`

```elixir
@spec egm96_undulation(number(), number()) :: float()
```

Geoid undulation `N` (metres) at a geodetic position in radians, from the
embedded genuine EGM96 1-degree global grid.

This is the recommended zero-setup default for metre-class datum work: its
bilinear lookup agrees with the full 15-arcminute EGM96 grid to ~0.4 m RMS,
far better than the coarse 30-degree built-in `undulation/2`.

# `egm96_undulations`

```elixir
@spec egm96_undulations([{number(), number()}]) :: [float()]
```

Embedded EGM96 1-degree undulations `N` (metres) for positions in radians.

# `egm96_undulations_deg`

```elixir
@spec egm96_undulations_deg([{number(), number()}]) :: [float()]
```

Embedded EGM96 1-degree undulations `N` (metres) for positions in degrees.

# `ellipsoidal_height_m`

```elixir
@spec ellipsoidal_height_m(number(), number(), number()) :: float()
```

Ellipsoidal height `h = H + N` (metres) from an orthometric height, using the
built-in grid. Position in radians.

# `from_egm96_dac`

```elixir
@spec from_egm96_dac(binary()) :: {:ok, grid()} | {:error, term()}
```

Alias for `load_egm96_dac/1`.

# `from_egm2008_raster`

```elixir
@spec from_egm2008_raster(binary(), egm2008_spacing()) ::
  {:ok, grid()} | {:error, term()}
```

Alias for `load_egm2008_raster/2`.

# `from_egm2008_raster_window`

```elixir
@spec from_egm2008_raster_window(binary(), Sidereon.Geoid.Egm2008RasterWindow.t()) ::
  {:ok, grid()} | {:error, term()}
```

Alias for `load_egm2008_raster_window/2`.

# `from_proj_egm96_gtx`

```elixir
@spec from_proj_egm96_gtx(binary()) :: {:ok, grid()} | {:error, term()}
```

Alias for `load_proj_egm96_gtx/1`, matching the core `GeoidGrid` constructor.

# `from_text`

```elixir
@spec from_text(binary()) :: {:ok, grid()} | {:error, term()}
```

Alias for `load_grid/1`, matching the Python `GeoidGrid.from_text` name.

# `grid`

```elixir
@spec grid(
  number(),
  number(),
  number(),
  number(),
  non_neg_integer(),
  non_neg_integer(),
  [number()]
) ::
  {:ok, grid()} | {:error, term()}
```

Build a geoid grid handle from its origin, spacing, dimensions, and row-major
samples (metres).

`values_m` is a flat list of `n_lat * n_lon` floats. Returns
`{:ok, reference()}` or `{:error, reason}`.

# `grid_ellipsoidal_height_deg`

```elixir
@spec grid_ellipsoidal_height_deg(grid(), number(), number(), number()) :: float()
```

Loaded-grid ellipsoidal height `h = H + N` (metres), position in degrees.

# `grid_ellipsoidal_height_rad`

```elixir
@spec grid_ellipsoidal_height_rad(grid(), number(), number(), number()) :: float()
```

Loaded-grid ellipsoidal height `h = H + N` (metres), position in radians.

# `grid_orthometric_height_deg`

```elixir
@spec grid_orthometric_height_deg(grid(), number(), number(), number()) :: float()
```

Loaded-grid orthometric height `H = h - N` (metres), position in degrees.

# `grid_orthometric_height_rad`

```elixir
@spec grid_orthometric_height_rad(grid(), number(), number(), number()) :: float()
```

Loaded-grid orthometric height `H = h - N` (metres), position in radians.

# `grid_undulation_deg`

```elixir
@spec grid_undulation_deg(grid(), number(), number()) :: float()
```

Bilinearly interpolated undulation `N` (metres) from a loaded grid handle, at a
geodetic position in degrees.

# `grid_undulation_proj_rad`

```elixir
@spec grid_undulation_proj_rad(
  grid(),
  number(),
  number(),
  proj_vgridshift_arithmetic()
) ::
  {:ok, float()} | {:error, Sidereon.Geoid.ProjVgridshiftError.t()}
```

Interpolate a loaded EGM96 GTX grid using PROJ 9.3.0's radian indexing and
blend order.

`arithmetic` must be `:separate_multiply_add` or `:fused_multiply_add`. PROJ's
source does not prescribe floating-point contraction, so the mode is required
rather than inferred from the current CPU. Invalid coordinates return a
typed `ProjVgridshiftError` and are never clamped or extrapolated.

# `grid_undulation_rad`

```elixir
@spec grid_undulation_rad(grid(), number(), number()) :: float()
```

Bilinearly interpolated undulation `N` (metres) from a loaded grid handle, at a
geodetic position in radians.

# `grid_undulations_deg`

```elixir
@spec grid_undulations_deg(grid(), [{number(), number()}]) :: [float()]
```

Bilinearly interpolated undulations `N` (metres) from a loaded grid, with
positions in degrees.

# `grid_undulations_rad`

```elixir
@spec grid_undulations_rad(grid(), [{number(), number()}]) :: [float()]
```

Bilinearly interpolated undulations `N` (metres) from a loaded grid, with
positions in radians.

# `load_egm96_dac`

```elixir
@spec load_egm96_dac(binary()) :: {:ok, grid()} | {:error, term()}
```

Parse a full EGM96 WW15MGH.DAC byte buffer into a grid handle.

# `load_egm2008_raster`

```elixir
@spec load_egm2008_raster(binary(), egm2008_spacing()) ::
  {:ok, grid()} | {:error, term()}
```

Parse a full NGA EGM2008 interpolation raster into a grid handle.

`spacing` is `:one_minute` or `:two_point_five_minute`. The returned handle
uses the same `grid_undulation_*` and height-conversion functions as every
loaded grid.

# `load_egm2008_raster_window`

```elixir
@spec load_egm2008_raster_window(binary(), Sidereon.Geoid.Egm2008RasterWindow.t()) ::
  {:ok, grid()} | {:error, term()}
```

Parse a cropped EGM2008 interpolation-raster window descriptor into a grid handle.

# `load_egm2008_raster_window`

```elixir
@spec load_egm2008_raster_window(
  binary(),
  egm2008_spacing(),
  number(),
  number(),
  pos_integer(),
  pos_integer()
) :: {:ok, grid()} | {:error, term()}
```

Parse a cropped NGA EGM2008 interpolation-raster window into a grid handle.

`lat_min_deg` and `lon_min_deg` are the southwest node of the crop in degrees.
`n_lat` and `n_lon` are the row and column counts carried by `bytes`.

# `load_grid`

```elixir
@spec load_grid(binary()) :: {:ok, grid()} | {:error, term()}
```

Parse a geoid grid in the crate's documented text format into a handle.

The format is whitespace-delimited with `#` comments: a six-field header
`lat_min lon_min dlat dlon n_lat n_lon` (degrees) followed by `n_lat * n_lon`
undulation samples in metres, row-major (latitude ascending outer). Returns
`{:ok, reference()}` or `{:error, reason}`.

# `load_proj_egm96_gtx`

```elixir
@spec load_proj_egm96_gtx(binary()) :: {:ok, grid()} | {:error, term()}
```

Parse PROJ's public EGM96 15-arcminute `egm96_15.gtx` into a grid handle.

Query the returned handle with `grid_undulation_proj_rad/4`. The ordinary
loaded-grid functions retain Sidereon's general interpolation contract.

# `orthometric_height_m`

```elixir
@spec orthometric_height_m(number(), number(), number()) :: float()
```

Orthometric height `H = h - N` (metres) from an ellipsoidal height, using the
built-in grid. Position in radians.

# `undulation`

```elixir
@spec undulation(number(), number()) :: float()
```

Built-in coarse-grid geoid undulation `N` (metres) at a geodetic position in
radians.

# `undulations`

```elixir
@spec undulations([{number(), number()}]) :: [float()]
```

Built-in coarse-grid geoid undulations `N` (metres) for positions in radians.

# `undulations_deg`

```elixir
@spec undulations_deg([{number(), number()}]) :: [float()]
```

Built-in coarse-grid geoid undulations `N` (metres) for positions in degrees.

---

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