# API Reference

A.T.O.M exposes a comprehensive REST API for programmatic access to all simulation and optimization features.

## Base URL

```
http://localhost:8080/api
```

Or in production:

```
https://your-domain.com/api
```

## Authentication

Expensive RF routes can require a shared backend key by setting `RF_API_KEY`. Send it as `Authorization: Bearer <key>` or `X-API-Key: <key>`; an invalid or missing key receives `401`. The setting is off for localhost-only development. Public deployments should authenticate users at a TLS gateway and inject this backend key rather than exposing it to browser JavaScript.

The full and bounded building-data routes can independently require `BUILDINGS_API_KEY` through the same headers. Keep this key at an origin gateway when using shared or public caches.

Dataset activation can independently require `DATASET_ADMIN_API_KEY`. `GET /api/datasets` remains readable so the Data tool can show installed packs; only `POST /api/datasets/switch` requires the credential when configured. Inject it at an origin gateway rather than storing it in browser JavaScript.

When Core Lab is enabled, `POST /api/core/scenario` requires `CORE_LAB_API_KEY` through the same headers. The backend validates the client credential and supplies the configured key on its private adapter hop. An enabled deployment without this key rejects scenario mutation with `503`.

## Response Format

Most responses are **JSON**. Explicit GIS export representations use GeoJSON, CSV, or GeoTIFF as documented below:

- `GET /healthz` returns a liveness status object
- `GET /readyz` returns dependency readiness
- `GET /api/meta` returns application, model, and active dataset identity
- `GET /api/datasets` returns installed packs and the active manifest ID
- `GET /api/buildings` and `GET /api/towers` return raw GeoJSON; bounded clients should prefer `/api/collections/buildings/items`
- `GET /api/collections/buildings/items` returns viewport-bounded building GeoJSON or CSV
- `POST /api/analyze-sector` returns `{ simulation, coverage_gaps }` from one shared ray-profile computation
- `POST /api/path-profile` returns an inspectable 2.5D vertical profile and component loss budget
- `POST /api/coverage-surface` returns a compact regular raster, isolines, statistics, and model assumptions, or an export representation
- `POST /api/simulate` returns `{ geojson, stats, rf_profile }`
- `POST /api/coverage-gaps` returns `{ geojson, stats }`
- `POST /api/interference` returns `{ geojson, demand_geojson, stats, model }`
- `POST /api/optimize-azimuth` returns `{ optimal_azimuth, coverage_score, demand_score, residential_score }`
- `POST /api/recommend-sites` returns a baseline plus ranked candidate records and GeoJSON
- `POST /api/measurements/evaluate` returns residual GeoJSON, subgroup diagnostics, uncertainty, and spatially validated bias guidance
- `/api/processes/batch-experiment` and `/api/jobs/{jobID}` expose asynchronous experiment execution, progress, results, and cancellation

Error responses use a simple object with an `error` message.

## Per-Cell RF Profile Contract

Single-sector requests accept `rf_profile` at the request root. Network, interference, recommendation, and measurement requests accept it independently inside every `towers[]` item. Legacy top-level fields remain defaults; an explicit nested property overrides its top-level/default counterpart only for that cell. Normalized simulation, optimized-tower, recommendation, and interference-model responses include resolved profiles for reproducibility.

```json
{
  "tower_lon": 32.8541,
  "tower_lat": 39.9208,
  "azimuth": 45,
  "rf_profile": {
    "schema_version": 1,
    "network_tech": "5g",
    "frequency_ghz": 28,
    "band": "n257",
    "bandwidth_mhz": 100,
    "channel_id": "NR-634666",
    "duplex_mode": "tdd",
    "tx_power_dbm": 37,
    "antenna_gain_dbi": 17,
    "system_loss_db": 2,
    "radius_m": 1200,
    "beam_width": 65,
    "antenna_height_m": 32,
    "mechanical_downtilt_deg": 2,
    "electrical_downtilt_deg": 4,
    "orientation_deg": 0,
    "horizontal_pattern_id": "cosine-sector",
    "vertical_pattern_id": "panel-10deg",
    "load_factor": 0.65,
    "reuse_factor": 1,
    "pci": 321,
    "receiver_height_m": 1.5,
    "receiver_sensitivity_dbm": -110
  }
}
```

| Field group | Accepted values |
|---|---|
| Identity | schema `1`; technology `4g`, `5g`, or `6g`; non-empty band/channel up to 64 UTF-8 bytes; duplex `fdd`, `tdd`, `sdl`, or `sul` |
| Carrier | frequency `>0–300 GHz` and compatible with technology; bandwidth `0.1–2000 MHz` |
| Link budget | TX `0–60 dBm`; gain `-20–80 dBi`; system loss `0–100 dB` |
| Geometry | radius `25–5000 m`; beam `10–360°`; antenna height `0.5–300 m`; receiver height `0.1–100 m`; orientation `0–<360°` |
| Tilt/pattern | mechanical/electrical tilt `-30–90°`; horizontal `ideal-sector`, `cosine-sector`, or `omni`; vertical `flat`, `panel-10deg`, or `panel-20deg` |
| Interference | load `>0–1`; reuse `1–12`; optional PCI `0–503` for LTE or `0–1007` for NR |
| Receiver | sensitivity `-180–-20 dBm` |

The profile pattern IDs are analytic planning presets, not imported vendor radiation diagrams. Interference analysis remains limited to 4G and 5G even though propagation accepts the 6G research profile.

---

## Endpoints

### Health Check

**Endpoint**: `GET /healthz`

Check whether the HTTP process is alive. This endpoint remains `200` even while required datasets or the frontend bundle are unavailable.

**Response**:

```json
{
  "status": "ok",
  "backend": "static-in-memory",
  "buildingIndex": {
    "footprintCount": 161784,
    "treeCount": 161784
  },
  "rtreeFootprints": 161784,
  "towerCount": 451
}
```

**Status Codes**:
- `200 OK` - Process is alive

### Readiness Check

**Endpoint**: `GET /readyz`

Check whether the building index, tower dataset, and frontend bundle are available. Use this route for deployment readiness probes.

Readiness reports only generic dependency state. Detailed dataset paths and validation errors remain in server logs.

```json
{
  "status": "ready",
  "buildings": true,
  "towers": true,
  "frontend": true
}
```

**Status Codes**:
- `200 OK` - Instance is ready to receive traffic
- `503 Service Unavailable` - At least one required dependency is unavailable

### Reproducibility Metadata

**Endpoint**: `GET /api/meta`

Returns the running application and model versions, build commit, supported technology modes, and the validated dataset manifest. Store this response with planning scenarios and reports when exact reproduction matters.

```json
{
  "application_version": "1.0.0",
  "build_commit": "abc1234",
  "model_version": "fspl-walls-2p5d-v3",
  "supported_technologies": ["4g", "5g", "6g-research"],
  "dataset": {
    "id": "ankara-open-planning",
    "version": "2026.07",
    "crs": "EPSG:4326"
  }
}
```

---

### Get Buildings

**Endpoint**: `GET /api/buildings`

Retrieve all building geometries as GeoJSON. This legacy whole-file route can exceed 100 MB. Interactive maps should use the bounded collection endpoint below.

**Query Parameters**: None

**Response**:

```json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "building": "concrete",
        "name": "Ankara Central Tower",
        "height": 45
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [[[33.8, 39.9], [33.801, 39.9], [33.8, 39.9]]]
      }
    }
  ]
}
```

**Caching and limits**: The approximately 117 MB response is identified by a strong SHA-256 `ETag` and served with `Cache-Control: public, max-age=3600, must-revalidate` by default. Revalidation with a matching `If-None-Match` returns `304` without consuming a download budget. Fresh transfers default to two globally, one concurrently per client, and two per minute per client. Configure these limits with `MAX_CONCURRENT_BUILDING_DOWNLOADS`, `MAX_CONCURRENT_BUILDING_DOWNLOADS_PER_CLIENT`, and `BUILDING_DOWNLOADS_PER_MINUTE`.

Set `BUILDINGS_API_KEY` to require `Authorization: Bearer <key>` or `X-API-Key: <key>`. Authenticated responses use a private cache policy so shared caches cannot bypass the origin credential check.

---

### Query Viewport Buildings

**Endpoint**: `GET /api/collections/buildings/items?bbox=minLon,minLat,maxLon,maxLat&limit=1000&offset=0`

The `bbox` parameter is mandatory in OGC CRS84 longitude/latitude order and its diagonal may not exceed 50 km. `limit` defaults to 1,000 and is capped at 5,000. Results are sorted by stable building ID and report `numberMatched`, `numberReturned`, `limit`, and `offset`; a further page includes an HTTP `Link` header with `rel="next"`.

The default representation is `application/geo+json`. Send `f=csv` or `Accept: text/csv` for CSV containing WKT polygon geometry, inferred height/source, normalized material, and demand fields. Discover the collection at `GET /api/collections`, inspect metadata at `GET /api/collections/buildings`, and inspect the standards declaration at `GET /api/conformance`. Its `conformsTo` list is deliberately empty because this project does not claim a complete OGC conformance class.

This interface follows OGC API Features collection and bounding-box concepts. It is not a vector-tile endpoint.

The bounded query shares the `BUILDINGS_API_KEY` policy but uses an independent interactive budget: four globally, two concurrently per client, and 120 per minute per client by default. Configure it with `MAX_CONCURRENT_BUILDING_FEATURE_QUERIES`, `MAX_CONCURRENT_BUILDING_FEATURE_QUERIES_PER_CLIENT`, and `BUILDING_FEATURE_QUERIES_PER_MINUTE`.

---

### Get Towers

**Endpoint**: `GET /api/towers`

Retrieve all 5G/4G tower locations.

**Query Parameters**: None

**Response**:

```json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "cell_id": 20560152,
        "radio_type": "NR",
        "is_simulated": false
      },
      "geometry": {
        "type": "Point",
        "coordinates": [32.8541, 39.9208]
      }
    }
  ]
}
```

---

### Analyze Sector

**Endpoint**: `POST /api/analyze-sector`

This is the preferred browser workflow when both propagation rays and coverage gaps are needed. It accepts the same body as `/api/simulate`, computes ray profiles and building interactions once, and returns:

```json
{
  "simulation": { "geojson": {}, "stats": {}, "rf_profile": {} },
  "coverage_gaps": { "geojson": {}, "stats": {} }
}
```

The standalone `/api/simulate` and `/api/coverage-gaps` endpoints remain available when a client needs only one result.

---

### Analyze A 2.5D Path Profile

**Endpoint**: `POST /api/path-profile`

```json
{
  "transmitter": { "lon": 32.8541, "lat": 39.9208 },
  "receiver": { "lon": 32.861, "lat": 39.924 },
  "sample_spacing_m": 10,
  "model_profile": "urban-short-range",
  "azimuth": 45,
  "rf_profile": { "network_tech": "5g", "frequency_ghz": 28, "antenna_height_m": 30, "receiver_height_m": 1.5 },
  "fidelity": {
    "building_loss_mode": "screen-diffraction",
    "diffraction_model": "single-knife-edge",
    "default_wall_material": "concrete",
    "clutter_specific_attenuation_db_per_km": 0,
    "vegetation_depth_m": 0,
    "vegetation_specific_attenuation_db_per_m": 0,
    "gas_specific_attenuation_db_per_km": 0,
    "rain_specific_attenuation_db_per_km": 0,
    "shadow_sigma_db": 6
  }
}
```

The response contains sampled terrain/building elevations, endpoint height above ground, direct LOS and 60% Fresnel classification, the dominant obstruction, one selected knife-edge approximation, component losses, P50 and shadow-sensitivity bounds, and an applicability statement. `terrain-profile` accepts 0.03–6 GHz, `urban-short-range` accepts 0.3–100 GHz, and `research-sub-thz` is explicitly outside those ITU-R profile ranges.

COG/GeoTIFF support is limited to north-up EPSG:4326, one-band integer/float samples, none/DEFLATE compression, and supported integer predictors. The response lists these limitations.

---

### Generate A Coverage Surface

**Endpoint**: `POST /api/coverage-surface`

Use the normal sector request fields plus `cell_size_m` from 10–250 and one to ten unique `thresholds_dbm`. The regular grid is capped at 100,000 cells.

The default JSON response contains a CRS84 row-major raster (`grid`), marching-square line segments (`contours`), bounds/statistics, and explicit model assumptions. Export the same request using:

- `?f=geotiff` for an uncompressed float32 EPSG:4326 GeoTIFF with `-9999` nodata
- `?f=geojson` for isoline GeoJSON
- `?f=csv` for valid grid-center longitude, latitude, and received power

The surface uses the fast FSPL, antenna-pattern, calibration, and frequency wall-loss model. It does not currently apply the terrain-profile or environmental sensitivity components.

---

### Simulate Propagation

**Endpoint**: `POST /api/simulate`

Run RF propagation simulation with given parameters.

**Request Body**:

```json
{
  "tower_lon": 32.8541,
  "tower_lat": 39.9208,
  "rays": 120,
  "radius_m": 400,
  "frequency_ghz": 28,
  "tx_power_dbm": 30,
  "azimuth": 45,
  "beam_width": 120
}
```

**Parameters**:

| Parameter | Type | Range | Required | Description |
|-----------|------|-------|----------|-------------|
| `tower_lon` | number | -180 - 180 | Yes | Tower longitude |
| `tower_lat` | number | -90 - 90 | Yes | Tower latitude |
| `rays` | number | 8 - 720 | No | Ray count used to sample the sector; defaults to 60 |
| `radius_m` | number | 25 - 5000 | No | Maximum requested simulation radius; defaults to 400 |
| `frequency_ghz` | number | > 0 - 300 | No | Network frequency in GHz; defaults to 28 |
| `tx_power_dbm` | number | 0 - 60 | No | Transmit power before antenna gain; defaults to 30 |
| `azimuth` | number | Any finite angle | No | Antenna direction, normalized to 0-360; defaults to 0 |
| `beam_width` | number | 10 - 360 | No | Sector width in degrees; defaults to 120 |

`rays` and `radius_m` also share a response budget: `rays × ceil(radius_m / 25)` must not exceed 25,000 estimated base features. The full browser range remains valid, including 360 rays at 1,500 meters. Building intersections can split a base segment, so the ray workers also enforce 25,000 as a hard ceiling on actual collected features.

**Response**:

```json
{
  "geojson": {
    "type": "FeatureCollection",
    "features": [
      {
        "type": "Feature",
        "properties": {
          "ray_index": 0,
          "segment_index": 0,
          "signal_dbm": -78.4,
          "is_blocked": false
        },
        "geometry": {
          "type": "LineString",
          "coordinates": [[32.8541, 39.9208], [32.856, 39.922]]
        }
      }
    ]
  },
  "stats": {
    "blocked_pct": 42.5,
    "avg_rx_dbm": -88.2,
    "min_range_m": 24.7,
    "max_range_m": 398.1
  }
}
```

**Status Codes**:
- `200 OK` - Simulation completed successfully
- `400 Bad Request` - Invalid parameters
- `413 Content Too Large` - Request body exceeds 1 MiB
- `422 Unprocessable Content` - Building intersections would exceed the hard 25,000-feature response ceiling
- `429 Too Many Requests` - RF worker capacity is busy; inspect `Retry-After`
- `500 Internal Server Error` - Simulation error

**Performance**:
- Runtime depends on ray count, radius, and local building density.
- Individual bounds remain 720 rays and 5,000 meters, but combinations must stay within the 25,000-feature estimate.
- The reported 720-ray, 5,000-meter combination estimates 144,000 base features and is rejected before ray allocation.
- The server uses a 120-second write timeout and caps each RF job at four workers.

---

### Find Coverage Gaps

**Endpoint**: `POST /api/coverage-gaps`

Find demand-weighted buildings inside the selected sector whose estimated received power is below the usable service threshold.

**Request Body**:

Uses the same payload as `POST /api/simulate`.

```json
{
  "tower_lon": 32.8541,
  "tower_lat": 39.9208,
  "rays": 120,
  "radius_m": 400,
  "frequency_ghz": 28,
  "tx_power_dbm": 30,
  "azimuth": 45,
  "beam_width": 120
}
```

**Response**:

```json
{
  "geojson": {
    "type": "FeatureCollection",
    "features": [
      {
        "type": "Feature",
        "properties": {
          "building_id": "building-2841",
          "rx_dbm": -117.6,
          "total_demand": 42.5,
          "demand_weight": 20,
          "residential_demand": 22.5,
          "severity": "outage",
          "reason": "commercial + residential demand"
        },
        "geometry": {
          "type": "Point",
          "coordinates": [32.8562, 39.9211]
        }
      }
    ]
  },
  "stats": {
    "candidate_buildings": 128,
    "served_buildings": 97,
    "gap_buildings": 31,
    "returned_gaps": 31,
    "gap_pct": 24.2,
    "total_gap_demand": 618.5,
    "worst_rx_dbm": -126.4,
    "threshold_dbm": -105
  }
}
```

**How it works**:

- Candidate buildings must have `demand_weight + residential_demand > 0`
- The building centroid must be inside the requested radius and beam sector
- Received power is estimated with EIRP, FSPL, and cumulative wall penetration loss
- Returned point features are sorted by demand, then by weakest estimated signal

---

### Analyze Interference and Radio Quality

**Endpoint**: `POST /api/interference`

Calculate planning-grade LTE or NR RSRP, SINR, RSRQ, RSSI, serving-cell, and strongest-interferer estimates over a bounded spatial grid and demand-building centroids.

```json
{
  "network_tech": "5g",
  "towers": [
    { "id": "cell-1", "tower_lon": 32.8541, "tower_lat": 39.9208, "azimuth": 45 },
    { "id": "cell-2", "tower_lon": 32.8581, "tower_lat": 39.9218, "azimuth": 225 }
  ],
  "radius_m": 400,
  "frequency_ghz": 28,
  "tx_power_dbm": 30,
  "beam_width": 120,
  "bandwidth_mhz": 100,
  "load_factor": 0.7,
  "reuse_factor": 1,
  "noise_figure_db": 7,
  "sample_spacing_m": 40
}
```

The request accepts 2–6 unique cells. LTE bandwidths are `1.4`, `3`, `5`, `10`, `15`, or `20` MHz; 5G NR bandwidths are `50`, `100`, `200`, or `400` MHz. Reuse must be `1` or `3`. 6G is rejected because standardized project-level RSRP/RSRQ assumptions are not defined for the research overlay.

The response contains:

- `geojson`: up to 3,000 grid samples with radio KPIs and serving/interferer context.
- `demand_geojson`: up to 500 affected demand-building centroids.
- `stats`: average and P10 radio quality, serviceable area, interference-limited area, affected demand, and per-cell summaries. `valid_sample_count` reports the number of samples with usable measurements; average and P10 fields are `null` when that count is zero.
- `model`: bandwidth, SCS, resource blocks, load, reuse, effective spacing, and explicit modeling assumptions.

Results are deterministic planning estimates, not measurements reported by a UE or live radio network.

Optional numeric fields receive defaults only when omitted. Explicit zero values remain explicit: for example, `noise_figure_db: 0` is valid, while `load_factor: 0` is rejected by range validation.

---

### Optimize Antenna Placement

**Endpoint**: `POST /api/optimize-azimuth`

Automatically find the optimal antenna azimuth for maximum coverage.

**Request Body**:

```json
{
  "tower_lon": 32.8541,
  "tower_lat": 39.9208,
  "rays": 120,
  "radius_m": 400,
  "frequency_ghz": 28,
  "tx_power_dbm": 30,
  "azimuth": 45,
  "beam_width": 120
}
```

**Parameters**:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tower_lon` | number | Yes | Tower longitude |
| `tower_lat` | number | Yes | Tower latitude |
| `rays` | number | Yes | Ray count used to sample the sector |
| `radius_m` | number | Yes | Maximum requested simulation radius |
| `frequency_ghz` | number | Yes | Network frequency in GHz |
| `tx_power_dbm` | number | Yes | Transmit power before antenna gain |
| `azimuth` | number | Yes | Current azimuth seed value |
| `beam_width` | number | Yes | Sector width in degrees |

**Response**:

```json
{
  "optimal_azimuth": 42,
  "coverage_score": 18320.5,
  "demand_score": 140000,
  "residential_score": 86000,
  "hit_demand_buildings": 18,
  "data_quality": "good"
}
```

**Response Fields**:

| Field | Description |
|-------|-------------|
| `optimal_azimuth` | Recommended antenna direction (0-360°) |
| `coverage_score` | Capped distance-based tie-breaker score |
| `demand_score` | POI/commercial/critical-building score |
| `residential_score` | Residential-density demand score |
| `hit_demand_buildings` | Unique demand-weighted buildings reached by the winning sector |
| `data_quality` | Summary of local demand metadata quality |

**Status Codes**:
- `200 OK` - Optimization succeeded
- `400 Bad Request` - Invalid parameters
- `413 Content Too Large` - Request body exceeds 1 MiB
- `429 Too Many Requests` - RF worker capacity is busy
- `500 Internal Server Error` - Optimization error

**Performance**:
- Typical response time: 3-5 seconds
- Parallelization: Up to four workers per RF request
- Server write timeout: 120 seconds

### Evaluate Or Optimize A Network

**Endpoints**: `POST /api/evaluate-network` and `POST /api/optimize-network`

Network requests contain two to six `towers` and may add an `optimization` object:

```json
{
  "towers": [
    { "id": "101", "tower_lon": 32.85, "tower_lat": 39.92, "azimuth": 90 },
    { "id": "102", "tower_lon": 32.86, "tower_lat": 39.93, "azimuth": 210 }
  ],
  "radius_m": 400,
  "frequency_ghz": 28,
  "optimization": {
    "objectives": [
      { "id": "coverage", "weight": 2 },
      { "id": "demand", "weight": 1 },
      { "id": "overlap", "weight": 3 }
    ],
    "constraints": {
      "min_unique_demand_buildings": 10,
      "max_overlap_buildings": 25
    }
  }
}
```

Objective IDs are `coverage`, `demand`, `residential`, and `overlap`; each weight is greater than zero and at most 100. Constraints may set minimum coverage score, minimum unique demand/residential buildings, and maximum overlap buildings. Responses include the objective score, feasibility/violations, adjusted parameter list, and up to 25 feasible non-dominated evaluated azimuth sets with explanations. This version adjusts azimuth only.

---

## Planning Product Endpoints

### Recommend Candidate Cells

**Endpoint**: `POST /api/recommend-sites`

Ranks known, unselected tower records inside a search polygon for a 4G or 5G network containing two to five selected cells. The backend prefilters candidates by nearby unmet demand, optimizes only the candidate azimuth, and returns at most the requested number of deterministic recommendations.

```json
{
  "network_tech": "5g",
  "towers": [
    { "id": "101", "tower_lon": 32.85, "tower_lat": 39.92, "azimuth": 90 },
    { "id": "102", "tower_lon": 32.852, "tower_lat": 39.921, "azimuth": 180 }
  ],
  "rays": 120,
  "radius_m": 400,
  "frequency_ghz": 28,
  "tx_power_dbm": 30,
  "beam_width": 120,
  "search_polygon": [[32.84, 39.91], [32.87, 39.91], [32.87, 39.94], [32.84, 39.94]],
  "max_results": 5
}
```

The response stores complete candidate details only in `recommendations`. Each `geojson.features[]` entry contains its point geometry, an empty `properties` object, and a top-level `id` that references the canonical recommendation with the same `id`:

```json
{
  "recommendations": [
    {
      "id": "LTE-3",
      "cell_id": 3,
      "tower_lon": 32.851,
      "tower_lat": 39.92,
      "optimal_azimuth": 90,
      "marginal_network_score": 120,
      "stats": { "network_score": 8120 },
      "reason": "adds demand with limited overlap"
    }
  ],
  "geojson": {
    "type": "FeatureCollection",
    "features": [
      {
        "type": "Feature",
        "id": "LTE-3",
        "properties": {},
        "geometry": { "type": "Point", "coordinates": [32.851, 39.92] }
      }
    ]
  }
}
```

Candidate records are not approved deployment sites. Cost, backhaul, permitting, and interference are not included in candidate scoring; run `/api/interference` after applying a candidate.

### Evaluate Field Measurements

**Endpoint**: `POST /api/measurements/evaluate`

Compares one to 5,000 measured 4G or 5G RSRP points with the deterministic model. At least one selected cell is required. With 20 or more valid predictions, at least five distinct 50 m spatial areas, and a campaign span of at least 100 m, the response includes a robust global bias suggestion evaluated through deterministic spatially blocked five-fold validation.

```json
{
  "network_tech": "5g",
  "towers": [{ "id": "101", "tower_lon": 32.85, "tower_lat": 39.92, "azimuth": 90 }],
  "radius_m": 400,
  "frequency_ghz": 28,
  "tx_power_dbm": 30,
  "beam_width": 120,
  "bandwidth_mhz": 100,
  "noise_figure_db": 7,
  "calibration_provenance": {
    "campaign_id": "ankara-drive-2026-07",
    "source": "drive-test.csv",
    "collected_at": "2026-07-01T10:00:00Z",
    "expires_at": "2027-01-01T00:00:00Z"
  },
  "samples": [
    { "id": "drive-001", "lon": 32.851, "lat": 39.921, "technology": "5g", "rsrp_dbm": -91, "cell_id": "101" }
  ]
}
```

The response separates valid predictions, no-signal samples, and requested-cell mismatches before reporting residual MAE/RMSE/bias, P50/P90 absolute error, per-cell and per-band summaries, distance/obstruction bins, robust MAD outliers, fold metrics, a 95% median-adjustment interval, and provenance/expiration state. The correction remains a single dB path-loss offset, not full propagation calibration. When applied, send `calibration_offset_db` with compatible simulation, network, interference, recommendation, and measurement requests. Accepted range is `-40` to `40` dB.

### Run Batch Experiments

**Discovery**: `GET /api/processes/batch-experiment`

**Execution**: `POST /api/processes/batch-experiment/execution`

```json
{
  "name": "Azimuth and power sweep",
  "base": { "tower_lon": 32.8541, "tower_lat": 39.9208, "radius_m": 400, "frequency_ghz": 28, "tx_power_dbm": 30, "beam_width": 120 },
  "matrix": { "tx_powers_dbm": [27, 30, 33], "azimuths_deg": [0, 30, 60, 90] }
}
```

The Cartesian product is capped at 64 runs. Execution returns an asynchronous job with `job_id`, `status`, `progress`, `fingerprint`, and run counts. Poll `GET /api/jobs/{jobID}`; cancel an accepted or running job with `DELETE /api/jobs/{jobID}`. Successful results contain compact metrics, per-run fingerprints, Pareto labels/explanations, and cache state. The fingerprint includes the normalized definition, dataset identity/hashes, and model version.

Run the same definition headlessly:

```bash
cd backend-go
go run ./cmd/run-experiment -definition experiment.json -base-url http://localhost:8080
```

The process/job resource shape is inspired by OGC API Processes; this implementation does not advertise full standard conformance.

### Dataset Packs

The backend loads the initial validated pack from `ATOM_DATASET_DIR`. Schema-v1 packs remain compatible. Schema v2 adds per-layer provenance and confidence, geometry/missing-field/coverage QA, and optional terrain, clutter, building-height, and material layers. All referenced files require SHA-256 hashes. A supported terrain COG/GeoTIFF is consumed by `/api/path-profile`; other optional sidecar layers remain metadata until explicitly integrated.

`GET /api/datasets` lists packs discovered at `ATOM_DATASETS_ROOT` itself and its immediate child directories:

```json
{
  "active_id": "ankara-open-planning",
  "datasets": [
    {
      "id": "ankara-open-planning",
      "name": "Ankara Open Planning Dataset",
      "version": "2026.07",
      "schema_version": 2,
      "crs": "EPSG:4326",
      "bounds": [32.45, 39.55, 33.25, 40.25],
      "sources": ["OpenStreetMap"],
      "licenses": ["ODbL 1.0"],
      "confidence": "Planning dataset; not an operator inventory.",
      "files": { "towers": "towers.geojson", "buildings": "buildings.geojson" },
      "sha256": { "towers.geojson": "...", "buildings.geojson": "..." },
      "active": true,
      "available": true
    }
  ]
}
```

`POST /api/datasets/switch` accepts only an installed ID:

```json
{ "id": "izmir-planning-2026" }
```

The server resolves the ID inside the configured root, validates all hashes and required geometry, and swaps the immutable runtime snapshot only after success. It returns `404` for an unknown ID, `422` for a failed installed-pack validation, and keeps the old pack active in both cases. Configure `DATASET_ADMIN_API_KEY` to protect activation.

Validate a pack before starting the server:

```bash
cd backend-go
go run ./cmd/validate-dataset ../data-pipeline
```

Use [Dataset Pack Studio](dataset-pack-studio.html) to inspect, repair, reproject, and build arbitrary-region schema-v2 packs locally.

The complete machine-readable contract is available as [`openapi.yaml`](openapi.yaml).

## Usage Examples

### Example 1: Get Health Status

```bash
curl -X GET http://localhost:8080/healthz
```

### Example 2: Simulate 5G Coverage

```bash
curl -X POST http://localhost:8080/api/simulate \
  -H "Content-Type: application/json" \
  -d '{
    "tower_lon": 32.8541,
    "tower_lat": 39.9208,
    "rays": 120,
    "radius_m": 400,
    "frequency_ghz": 28,
    "tx_power_dbm": 30,
    "azimuth": 90,
    "beam_width": 120
  }'
```

### Example 3: Auto-Optimize Antenna

```bash
curl -X POST http://localhost:8080/api/optimize-azimuth \
  -H "Content-Type: application/json" \
  -d '{
    "tower_lon": 32.8541,
    "tower_lat": 39.9208,
    "rays": 120,
    "radius_m": 400,
    "frequency_ghz": 28,
    "tx_power_dbm": 30,
    "azimuth": 90,
    "beam_width": 120
  }'
```

### Example 4: Find Coverage Gaps

```bash
curl -X POST http://localhost:8080/api/coverage-gaps \
  -H "Content-Type: application/json" \
  -d '{
    "tower_lon": 32.8541,
    "tower_lat": 39.9208,
    "rays": 120,
    "radius_m": 400,
    "frequency_ghz": 28,
    "tx_power_dbm": 30,
    "azimuth": 90,
    "beam_width": 120
  }'
```

### Example 5: Fetch All Towers

```bash
curl -X GET "http://localhost:8080/api/towers"
```

---

## Client Examples

### Go Standard Library

```go
package main

import (
    "bytes"
    "net/http"
)

func main() {
    payload := []byte(`{
      "tower_lon": 32.8541,
      "tower_lat": 39.9208,
      "frequency_ghz": 28,
      "tx_power_dbm": 30,
      "rays": 120,
      "radius_m": 400,
      "azimuth": 90,
      "beam_width": 120
    }`)
    request, _ := http.NewRequest(
        http.MethodPost,
        "http://localhost:8080/api/simulate",
        bytes.NewReader(payload),
    )
    request.Header.Set("Content-Type", "application/json")
    response, err := http.DefaultClient.Do(request)
    if err != nil {
        panic(err)
    }
    defer response.Body.Close()
}
```

### Python Client

```python
import requests

client = requests.Session()
response = client.post(
    'http://localhost:8080/api/simulate',
    json={
        'tower_lon': 32.8541,
        'tower_lat': 39.9208,
        'rays': 120,
        'radius_m': 400,
        'frequency_ghz': 28,
        'tx_power_dbm': 30,
        'azimuth': 45,
        'beam_width': 120
    }
)

data = response.json()
print(data['geojson'])
```

### JavaScript Client

```javascript
async function simulateRF(params) {
  const response = await fetch('http://localhost:8080/api/simulate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(params)
  });
  
  const data = await response.json();
  return data.geojson;
}

const coverage = await simulateRF({
  tower_lon: 32.8541,
  tower_lat: 39.9208,
  rays: 120,
  radius_m: 400,
  frequency_ghz: 28,
  tx_power_dbm: 30,
  azimuth: 45,
  beam_width: 120
});
```

---

## Capacity and Rate Limiting

The server allows two RF jobs globally but only one active job per client by default. A client also has a 20-request-per-minute budget. Configure these with `MAX_CONCURRENT_RF_REQUESTS`, `MAX_CONCURRENT_RF_REQUESTS_PER_CLIENT`, and `RF_REQUESTS_PER_MINUTE`.

Rejected requests return `429` with `Retry-After`, `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`. Client identity comes from the socket peer unless `TRUSTED_PROXIES` explicitly lists the proxy CIDRs allowed to supply forwarding headers. These controls are process-local; a multi-replica deployment still needs a gateway-level shared budget.

Set `RF_REQUEST_TIMEOUT_SECONDS` to bound compute time; the default is 60 seconds and expiration returns `504`. Keep RF concurrency aligned with CPU allocation and set `RF_API_KEY` for any non-private backend hop.

`GET /api/buildings` has a separate transfer budget because each cache miss serves the complete dataset. Its conditional `304` responses bypass transfer admission, while uncached responses return the same `RateLimit-*` and `Retry-After` headers as RF routes when applicable. Configure a gateway-level bandwidth budget for multi-replica or internet-facing deployments.

---

## Versioning

**Current API Version**: `1.0`

Future versions will:
- Add webhook support for async simulations
- Support batch optimization requests
- Include terrain elevation models
- Add custom propagation model endpoints

Breaking changes will increment major version (e.g., `/v2`).

---

## Troubleshooting

### 503 Service Unavailable

**Cause**: Data not yet loaded at startup

**Solution**: Wait 5-10 seconds and retry

### 400 Bad Request

**Cause**: Invalid parameter values

**Solution**: Check parameter types and ranges in documentation

### 500 Internal Server Error

**Cause**: Computation timeout or backend crash

**Solution**: 
- Check logs: `docker logs atom-simulator`
- Reduce `rays` or `radius_m` for faster computation
- Increase timeout values if needed

### Slow Response Times

**Cause**: Overlapping concurrent requests

**Solution**:
- Reduce grid size (10 m → 20 m)
- Increase container resources (CPU/RAM)
- Implement client-side request batching

---

**Next**: See [Getting Started](getting-started.md) or [Deployment](deployment.md).
