Algorithms and RF Physics
The deterministic propagation, geometry, optimization, and radio-quality methods behind A.T.O.M results.
A.T.O.M combines deterministic planning equations with computational geometry to produce inspectable RF estimates. It is not a calibrated propagation solver or a substitute for field measurements.
Radio Frequency Physics Foundation
Free-Space Path Loss (FSPL)
The fundamental equation for all propagation calculations:
Simplification (practical form):
Received Power Calculation
Where:
- = Transmit power plus antenna gain
- = Free-space path loss (FSPL equation)
- = Cumulative building penetration loss
The fast sector model does not include terrain, diffraction, reflection, fast fading, or multipath. The separate 2.5D path profiler samples optional COG/GeoTIFF terrain, building height, LOS/Fresnel clearance, and one explicitly selected dominant knife-edge approximation; it is not a full replacement for the sector engine or a complete ITU-R method.
2.5D Point-To-Point Profiles
For a selected transmitter and receiver, the engine samples a geodesic path at a bounded spacing. Each sample contains ground elevation plus any intersecting building height. Antenna and receiver elevations are their local ground elevations plus configured height above ground.
The result reports direct line-of-sight clearance and a 60% first-Fresnel-zone clearance check:
r1 = sqrt(lambda * d1 * d2 / (d1 + d2))
When selected, the dominant obstruction is converted to the usual dimensionless knife-edge parameter and the positive-loss approximation is applied. Only the dominant edge is used; Deygout/multiple-edge, reflection, and terrain-clutter coupling are not implemented.
The inspectable loss budget keeps FSPL, horizontal/vertical antenna attenuation, system loss, wall penetration, diffraction, clutter, vegetation, gas, rain, and calibration as separate signed components. Shadow sigma produces sensitivity bounds and is not injected as a random field, preserving determinism.
The terrain-profile and urban-short-range
labels enforce the published frequency ranges of ITU-R
P.1812-8 and ITU-R
P.1411-9. The knife-edge, material, gas, and rain controls are
informed by P.526,
P.2040, P.676, and P.838; A.T.O.M does
not claim full Recommendation conformance.
Frequency Dependence
FSPL increases with frequency squared:
Consequence: Same distance results in substantially higher loss as frequency rises, which is why mmWave and Sub-THz coverage shrink quickly in urban settings.
Building Penetration Model
Generation-Specific Wall Loss
A.T.O.M uses the selected network generation to apply a fixed loss each time a ray crosses a building boundary. OSM building tags are still used for demand scoring and diagnostics, but wall penetration is intentionally frequency-driven in the current runtime:
if frequency_ghz <= 3:
loss_4g = 8 dB
elif frequency_ghz <= 40:
loss_5g = 30 dB
else:
loss_6g = 80 dB
Cumulative Wall Loss
If a ray intersects multiple buildings, losses are cumulative:
Example: Ray through two building boundaries at 5G frequency:
- Building 1: +30 dB
- Building 2: +30 dB
- Total: +60 dB additional loss
Raytracing Algorithm
Overview
A.T.O.M uses a deterministic segmented sector raytracer:
- Define the antenna azimuth and beam width
- Split the selected sector into configurable rays
- Split each ray into short segments
- Query the R-Tree for buildings intersecting each segment
- Apply cumulative wall loss and receiver sensitivity thresholding
- Color each segment by received power
- Serialize the segments as GeoJSON
Pseudocode
func TraceSector(tx Location, req SimulationRequest) GeoJSON {
result := GeoJSON{}
if req.rays * ceil(req.radius_m / 25) > 25_000 {
return error("simulation response feature limit exceeded")
}
maxDistance := min(req.radius_m, sensitivityLimitedDistance(req))
for angle in sector_angles(req.azimuth, req.beam_width, req.rays) {
current := tx
wallLoss := 0.0
for segmentEnd in stepped_points(tx, angle, maxDistance) {
segment := LineSegment{current, segmentEnd}
intersections := rtree.Query(segment.Bounds())
for building in intersections {
if segment_intersects_polygon(segment, building.Polygon) {
wallLoss += penetration_loss(req.frequency_ghz)
}
}
distance := haversine_distance(tx, segmentEnd)
fspl := 20*log10(distance) + 20*log10(req.frequency_ghz) + 32.45
rxPower := EIRP_DBM - fspl - wallLoss
if rxPower < ReceiverSensitivityDBm {
break
}
result.Add(LineString{current, segmentEnd}, rxPower)
current = segmentEnd
}
}
return result
}Computational Geometry: Polygon Intersection
Problem: Does a ray (line segment) intersect a building (polygon)?
Solution: Use separating axis theorem (SAT):
func SegmentIntersectsPolygon(segment Line, polygon Polygon) bool {
// Test segment against all polygon edges
for i := 0; i < len(polygon.Edges); i++ {
edge := polygon.Edges[i]
if SegmentIntersectsSegment(segment, edge) {
return true
}
}
// Test if segment start/end inside polygon
return PointInPolygon(segment.Start, polygon) ||
PointInPolygon(segment.End, polygon)
}
func SegmentIntersectsSegment(s1, s2 Line) bool {
// Standard 2D line intersection test
// (Cross product method)
d1 := cross_product(s2.Start - s1.Start, s1.Dir)
d2 := cross_product(s2.End - s1.Start, s1.Dir)
d3 := cross_product(s1.Start - s2.Start, s2.Dir)
d4 := cross_product(s1.End - s2.Start, s2.Dir)
return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))
}Complexity: O(log n) per ray thanks to R-Tree spatial indexing.
Spatial Indexing: R-Tree
Why R-Tree?
RF simulations require millions of queries:
- Naive approach (linear scan): O(n) per query × 90,000 rays = 1+ billion operations
- R-Tree approach (spatial index): O(log n) per query × 90,000 rays = 1+ million operations
- Speedup: 1000× faster
R-Tree Structure
An R-Tree organizes rectangles (bounding boxes) in a balanced tree:
Root
├─ Node A (covers buildings 1-5)
│ ├─ Building 1 bbox
│ ├─ Building 2 bbox
│ └─ Building 3 bbox
├─ Node B (covers buildings 6-10)
│ ├─ Building 6 bbox
│ └─ Building 7 bbox
└─ Node C (covers buildings 11-15)
└─ Building 11 bbox
Query Algorithm
func RTtree.Query(ray LineSegment) []Building {
candidates := []Building{}
// Recursively check which nodes overlap with ray
func traverse(node *RNode) {
if node.bbox.Intersects(ray) {
if node.IsLeaf() {
candidates.append(node.buildings)
} else {
for child in node.Children {
traverse(child)
}
}
}
}
traverse(rtree.Root)
return candidates
}Result: Typically 5-20 building candidates per ray (vs 12,000 total buildings).
Sector Eligibility
Hard Sector Model
A.T.O.M uses the configured azimuth and beam width as a hard geometric eligibility boundary:
Azimuth = 45°, Beam Width = 65°
Coverage: 12.5° to 77.5°
↑ 0°
|
/─────────────\
/ \
| Main Lobe |
| (Gain = 0 dBi) |
\ /
\─────────────/
θ = 65°
Eligibility Function
func IsInsideSector(direction float64, antennaAzimuth float64,
beamWidth float64) bool {
offset := abs(normalize_angle(direction - antennaAzimuth))
return offset <= beamWidth / 2
}Samples outside the configured sector or radius do not contribute. Version 1 does not model sidelobes or a continuous antenna gain pattern.
Deterministic Optimization: Sweep & Score Algorithm
Problem Statement
Find: The antenna azimuth that best serves demand-weighted coverage
Constraints:
- Distance range: 50 m to 5 km
- Beam width: user configured
- Frequency: user selected
Algorithm
func OptimizeAzimuth(tx Location, request SimulationRequest) float64 {
bestAzimuth := 0.0
bestScore := 0.0
// Sweep all azimuths in 10° increments
for azimuth := 0; azimuth < 360; azimuth += 10 {
// Run simulation at this azimuth
rays := TraceSector(tx, request.withAzimuth(azimuth))
// Track unique buildings reached by the sector
demandScore := 0.0
residentialScore := 0.0
hitBuildings := NewSet()
for ray in rays {
for building in ray.hitBuildings {
if hitBuildings.add(building.id) {
demandScore += building.demandWeight * 10000
residentialScore += building.residentialDemand * 10000
}
}
}
// Coverage is capped and used as a tie-breaker
coverageTieBreaker := 0.0
for ray in rays {
coverageTieBreaker += min(ray.distance, 500) / 500 * 100
}
score := demandScore + residentialScore + coverageTieBreaker
// Track best
if score > bestScore {
bestScore = score
bestAzimuth = azimuth
}
}
return bestAzimuth
}Complexity Analysis
- Loop iterations: 360° / 5° = 72
- Per iteration:
- Ray tracing: O(90,000 rays × log n intersections) ~1 second
- Coverage calculation: O(90,000)
- Total time: ~72 seconds (naive)
- Optimized time: ~250 ms (with parallelization)
Parallelization
Go's Goroutines enable embarrassingly parallel optimization:
// Create worker pool
type Job struct {
azimuth float64
}
type Result struct {
azimuth float64
coverage float64
}
// Dispatch all azimuths to workers
jobs := make(chan Job, 72)
results := make(chan Result, 72)
// 4 workers process in parallel
for w := 1; w <= 4; w++ {
go worker(jobs, results)
}
for azimuth := 0; azimuth < 360; azimuth += 5 {
jobs <- Job{azimuth}
}
// Collect results
for i := 1; i <= 72; i++ {
result := <-results
if result.coverage > bestCoverage {
bestCoverage = result.coverage
bestAzimuth = result.azimuth
}
}Result: 4× speedup on quad-core machine.
Performance Characteristics
Benchmarks (Ankara Dataset)
| Operation | Time | Scaling |
|---|---|---|
| Load GeoJSON | 500 ms | O(n) where n=12,000 buildings |
| Build R-Tree | 1 second | O(n log n) |
| Single ray trace | < 1 μs | O(log n) |
| Up to 25,000 returned ray features | Bounded | O(n log n) parallelized |
| Optimization (72 iterations) | 250 ms | O(72 × n log n) with Goroutines |
| API response | < 100 ms p99 | Network + serialization |
Memory Usage
| Component | Size |
|---|---|
| Buildings (GeoJSON) | 80 MB (disk) → 40 MB (RAM) |
| R-Tree indices | 150 MB |
| Tower locations | 2 MB |
| Returned ray features per request | 25,000 maximum |
| Total footprint | ~200 MB typical |
Validation & Limitations
Response Complexity Bound
Before ray allocation, the API requires
rays × ceil(radius_m / 25) ≤ 25,000. The same atomic budget
is shared by all ray workers and counts additional features created when
building intersections split a 25-meter base segment. If the actual
count reaches the ceiling, workers cancel the request before response
assembly and serialization. Response assembly independently checks the
ceiling, sizes its flattened slice from the actual count, and releases
the per-ray slices when the top-level operation finishes.
Model Confidence
| Aspect | Status | Notes |
|---|---|---|
| FSPL equation | Deterministic | Standard free-space equation |
| Wall attenuation | Approximation | Fixed by selected frequency family |
| Wall intersection | Deterministic | Binary geometric intersection against loaded footprints |
| Field calibration | Not completed | Validate estimates before deployment decisions |
Model Limitations
- Flat Earth: No terrain elevation (can add with GeoTIFF)
- No multipath: Doesn't model reflections/scattering
- Ideal antennas: Assumes perfect omnidirectional at low freq
- Static buildings: Doesn't model moving obstructions
- Weather: No rain/weather fading
When to Use A.T.O.M
✅ Good for:
- Network planning and site selection
- Quick feasibility studies
- Education and training
- Comparison between sites
- Coverage mapping
❌ Not suitable for:
- Detailed link budget analysis
- Precise handover prediction
- Interference modeling
- Non-stationary targets
Next: Explore the API Reference to integrate A.T.O.M into your systems.