Public interfaces

API Reference

Request contracts, response shapes, validation behavior, overload handling, and examples for integrating with A.T.O.M.

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.

{
  "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:

{
  "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.

{
  "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.

{
  "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:

{
  "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:

{
  "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:

{
  "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

{
  "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:

{
  "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:

{
  "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.

{
  "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:

{
  "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.

{
  "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:

{
  "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:

{
  "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:

{
  "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.

{
  "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:

{
  "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.

{
  "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

{
  "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:

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:

{
  "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:

{ "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:

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

Use Dataset Pack Studio to inspect, repair, reproject, and build arbitrary-region schema-v2 packs locally.

The complete machine-readable contract is available as openapi.yaml.

Usage Examples

Example 1: Get Health Status

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

Example 2: Simulate 5G Coverage

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

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

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

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

Client Examples

Go Standard Library

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

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

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 or Deployment.