# Utils/Terrain

Pure math functions for terrain sheets represented as XYZ surface grids.

## What is a Sheet?

A sheet is a 2D grid of XYZ points representing a terrain surface in ECEF coordinates. The shape is `(M, N, 3)` where M is rows (typically latitude), N is columns (typically longitude), and 3 is the XYZ components.

```python
# A sheet is just a numpy array
sheet.shape  # (M, N, 3)
sheet[i, j]  # XYZ point at row i, column j
```

### Why Pure Arrays?

- **Resolution is discoverable**: Measure spacing between adjacent points
- **Bounds are discoverable**: Min/max of coordinates
- **No wrapper needed**: gri-terrain can define its own object if needed
- **Interoperability**: Works with any numpy-based code

## Import and Use

```python
from gri_utils import terrain

sheet = terrain.lla_to_xyz_sheet(lats, lons, alts)
```

Or with a unique name:

```python
from gri_utils import terrain as gri_terrain

sheet = gri_terrain.lla_to_xyz_sheet(lats, lons, alts)
```

## Module Overview

| File | Purpose |
|------|---------|
| `sheet.py` | LLA<->XYZ conversion, interpolation, resolution |
| `sheet_bounds.py` | Radial bounding surfaces for fast pre-filtering |
| `ray.py` | Ray-sheet intersection with adaptive stepping |
| `visibility_mask.py` | Line-of-sight visibility mask |
| `shadow_boundary.py` | Contours separating visible from shadowed regions |
| `shadow_depth.py` | Continuous shadow depth scalar field |
| `marching_squares.py` | Iso-contour extraction from 2D scalar fields |
| `sheet_intersect.py` | Generic terrain intersection with implicit surfaces |
| `source.py` | TerrainSource ABC for terrain data providers |
| `stitch.py` | Mosaic multiple sheets into a single array |
| `adaptive.py` | Adaptive subsampling for efficient intersection |

## Sheet Operations (sheet.py)

### Creating Sheets

```python
import numpy as np
from gri_utils import terrain

lats = np.linspace(39.0, 40.0, 10)
lons = np.linspace(-105.0, -104.0, 10)
alts = np.zeros((10, 10))  # Flat terrain at sea level

sheet = terrain.lla_to_xyz_sheet(lats, lons, alts)
# sheet.shape = (10, 10, 3)
```

### Interpolation

Bicubic interpolation for sub-grid-cell precision:

```python
# Single point
point = terrain.interpolate_sheet(sheet, row_frac=5.5, col_frac=7.2)

# Batch
points = terrain.interpolate_sheet_batch(sheet, row_fracs, col_fracs)
```

Interpolation orders: 0=nearest, 1=bilinear, 3=bicubic (default).

### Resolution and Round-trip

```python
row_spacing, col_spacing = terrain.get_sheet_resolution(sheet)

lats, lons, alts = terrain.xyz_to_lla_sheet(sheet)
```

## Sheet Bounds (sheet_bounds.py)

Fast pre-filtering using radial bounding surfaces. A `SheetBounds` captures min/avg/max radius, geographic bounds, and sheet centroid.

```python
bounds = terrain.get_sheet_bounds(sheet)

# Quick ray rejection
could_hit, approx = terrain.ray_intersects_bounds(origin, direction, bounds)

# Point/bounds tests
terrain.point_within_bounds(point_xyz, bounds)
terrain.bounds_overlap(bounds1, bounds2)
```

## Ray Intersection (ray.py)

Find where a ray intersects a terrain sheet using adaptive step sizing.

```python
hit = terrain.ray_sheet_intersect(sheet, origin, direction)
# Returns XYZ intersection point, or None if no hit

# Quick bounds check
could_hit = terrain.ray_intersects_sheet_bounds(sheet, origin, direction)
```

## Visibility and Shadow Analysis

### Shadow Depth (shadow_depth.py)

Continuous scalar field measuring vertical clearance or obstruction at each grid point relative to an observer. Uses DDA (digital differential analyzer) ray tracing.

```python
depth = terrain.get_shadow_depth(sheet, observer_xyz)
# depth.shape = (M, N)
# Positive: visible, value is vertical clearance in meters
# Negative: shadowed, abs(value) is height needed to become visible
# Zero: on shadow boundary
```

### Visibility Mask (visibility_mask.py)

Boolean mask of which terrain points are visible from an observer:

```python
mask = terrain.get_visibility_mask(sheet, observer_xyz)
# mask.shape = (M, N), dtype=bool
```

### Shadow Boundary (shadow_boundary.py)

Contours separating visible from shadowed regions:

```python
contours = terrain.get_shadow_boundary(sheet, observer_xyz)
# List of (K, 3) arrays in ECEF meters
```

## Marching Squares (marching_squares.py)

Extract iso-contours from 2D scalar fields on grids.

```python
# Find zero-crossings in a scalar field
contours = terrain.find_contours(values, level=0.0)
# Returns list of polylines in fractional grid coordinates

# Convert grid-coordinate contours to ECEF
xyz_contours = terrain.contours_to_xyz(contours, sheet)
```

## Sheet Intersection (sheet_intersect.py)

Generic intersection of a terrain sheet with any implicit surface defined by a residual function. The zero-crossing of the residual defines the iso-surface.

```python
from gri_utils import terrain, observables

# Create a residual function from an observable
residual_fn = observables.get_tdoa_residual(c1_xyz, c2_xyz, tdoa_s)

# Find where the TDOA hyperboloid intersects the terrain
contours = terrain.intersect_sheet(sheet, residual_fn)
# Returns list of (K, 3) XYZ contour arrays
```

Works with any observable residual (range, TDOA, FDOA, AOA, PDOA) or any custom residual function that takes `(N, 3)` XYZ points and returns `(N,)` scalar values.

A scalar (point-by-point) variant is also available:

```python
contours = terrain.intersect_sheet_scalar(sheet, scalar_residual_fn)
```

## TerrainSource ABC (source.py)

Abstract base class for terrain data providers. Implementations (e.g., in gri-terrain) handle file I/O and tile management; gri-utils handles the math.

```python
from gri_utils.terrain import TerrainSource, TerrainBounds

class MyTerrainSource(TerrainSource):
    def get_covering_sheets(self, lats, lons):
        # Return list of (sheet, bounds) tuples covering the region
        ...

    def covers(self, lat, lon):
        # Return True if data exists at this location
        ...
```

`TerrainBounds` is a NamedTuple with `lat_min`, `lat_max`, `lon_min`, `lon_max` fields and `contains(lat, lon)` and `overlaps(other)` methods.

## Sheet Stitching (stitch.py)

Mosaic multiple terrain tiles into a single contiguous array:

```python
sheets_with_bounds = source.get_covering_sheets(lats, lons)
combined, combined_bounds = terrain.stitch_sheets(sheets_with_bounds)

lat_res, lon_res = terrain.get_stitch_resolution(sheets_with_bounds)
```

Verifies compatible resolution (within 1%). Overlapping regions use the first sheet. Gaps are filled with NaN.

## Adaptive Intersection (adaptive.py)

For large terrain datasets, adaptive subsampling avoids evaluating the full grid. The algorithm:

1. Loads full-resolution terrain tiles covering the region
2. Stitches multiple tiles if the region spans boundaries
3. Subsamples to a coarse grid (e.g., every 10th point)
4. Evaluates residual on the coarse grid to find cells with sign changes
5. Runs full-resolution marching squares only on intersection cells
6. Returns contours in ECEF coordinates

### Generic Interface

```python
residual_fn = observables.get_tdoa_residual(c1_xyz, c2_xyz, tdoa_s)
contours = terrain.intersect_adaptive(source, residual_fn, lats, lons)
```

### Observable-Specific Convenience Functions

```python
# TDOA hyperboloid
contours = terrain.intersect_source_with_tdoa(
    source, lats, lons, c1_xyz, c2_xyz, tdoa_seconds
)

# Range sphere
contours = terrain.intersect_source_with_range(
    source, lats, lons, center_xyz, range_m
)

# FDOA iso-Doppler
contours = terrain.intersect_source_with_fdoa(
    source, lats, lons, c1_xyz, c1_vel, c2_xyz, c2_vel, fdoa_hz, freq_hz
)

# AOA bearing line
contours = terrain.intersect_source_with_aoa(
    source, lats, lons, collector_xyz, aoa_cosines
)
```

All accept an optional `subsample_factor` parameter (default 10).

## Performance Notes

- Sheet operations are vectorized using numpy
- Bounds checking enables fast rejection before expensive operations
- Shadow depth uses DDA ray tracing: O(M x N x max(M,N))
- Adaptive intersection: For a 3600x3600 DTED2 tile with `subsample_factor=10`, evaluates ~200K-500K points instead of 13M

## Coordinate Systems

- All positions in ECEF (XYZ) coordinates, meters
- Altitudes in meters above WGS84 ellipsoid
- Latitudes/longitudes in decimal degrees
