Tuning geofence thresholds for yard tracking
Yard tracking in modern container terminals operates at the intersection of physical logistics and real-time telemetry. When automated guided vehicles (AGVs), rubber-tired gantry cranes (RTGs), and terminal tractors traverse operational zones, the precision of geofence boundaries dictates alert fidelity. Shipping operations teams and port authorities routinely encounter false positives when static radius thresholds fail to account for GPS multipath errors, AIS transmission latency, or terminal-specific layout constraints. The discipline of Threshold Tuning for Alerts directly addresses these operational friction points by replacing rigid boundaries with adaptive, context-aware parameters that align with actual yard throughput and equipment kinematics.
Normalizing Heterogeneous Telemetry Streams
AIS data streams and Terminal Operating System (TOS) payloads rarely share identical coordinate projections, timestamp resolutions, or message schemas. Format drift between NMEA 0183 sentences, AIS Type 1/2/3 position reports, and proprietary RTLS (Real-Time Location System) payloads introduces coordinate misalignment that corrupts spatial evaluation. When synchronizing these heterogeneous streams under Container Tracking & AIS Event Synchronization, engineers must normalize spatial references to a common datum (typically WGS84) and apply temporal smoothing before evaluating geofence crossings. Without deterministic normalization, threshold calculations inherit projection artifacts that trigger phantom entry/exit events, forcing yard supervisors to manually reconcile conflicting asset states.
Real-world telemetry exhibits three primary quirks:
- Timestamp jitter: AIS broadcasts at variable intervals (2–10s), while RTLS pings at 1–5 Hz. Out-of-order delivery is common over congested VHF channels or cellular backhaul.
- Coordinate drift: Multipath reflection between stacked containers or quay walls introduces 3–15m positional noise.
- Missing kinematic fields: Speed over ground (SOG) or course over ground (COG) may be null during low-maneuverability states.
Normalization pipelines must clamp coordinates to terminal bounding boxes, interpolate missing SOG using finite-difference approximations, and discard stale updates exceeding a configurable TTL.
Memory-Efficient Spatial Evaluation
High-density yards processing 500+ concurrent vessel and equipment tracks quickly exhaust memory when naive geofence evaluation stores full historical trajectories in RAM. Continuous ingestion of multi-Hz telemetry creates unbounded list growth. A sliding-window approach combined with spatial pre-filtering reduces memory overhead by 60–80%. Threshold evaluation should operate on delta-distance metrics rather than raw coordinate arrays, preventing garbage collection pauses from disrupting SLA-critical alert pipelines.
Using bounded collections.deque structures and discarding positional states outside the active evaluation window ensures predictable heap allocation even during peak vessel berthing operations. Spatial indexing via bounding-box pre-checks eliminates expensive Haversine calculations for assets clearly outside the evaluation radius.
import math
import logging
import json
from collections import deque
from datetime import datetime, timezone
from dataclasses import dataclass, field
from typing import Optional
# Structured JSON formatter for audit-compliant logging
class JSONFormatter(logging.Formatter):
def format(self, record):
log_obj = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"asset_id": getattr(record, "asset_id", None),
"geofence_id": getattr(record, "geofence_id", None),
"latency_ms": getattr(record, "latency_ms", None),
}
return json.dumps(log_obj, separators=(",", ":"))
logger = logging.getLogger("yard_geofence")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)
@dataclass
class PositionRecord:
asset_id: str
lat: float
lon: float
timestamp: float # Unix epoch
sog_knots: Optional[float] = None
class YardGeofenceEvaluator:
"""
Production-grade geofence evaluator for terminal yard tracking.
Handles GPS jitter, out-of-order telemetry, and memory-constrained environments.
Consumes telemetry from AIS units conforming to IMO MSC.74(69) and applies data minimization principles.
"""
def __init__(
self,
geofence_id: str,
center_lat: float,
center_lon: float,
base_radius_m: float = 25.0,
velocity_scale_factor: float = 2.5,
hysteresis_margin_m: float = 5.0,
window_size: int = 12,
max_data_retention_hours: int = 24
):
self.geofence_id = geofence_id
self.center = (center_lat, center_lon)
self.base_radius_m = base_radius_m
self.velocity_scale_factor = velocity_scale_factor
self.hysteresis_margin_m = hysteresis_margin_m
self.history: deque[PositionRecord] = deque(maxlen=window_size)
self.state = "OUTSIDE"
self.max_retention_hours = max_data_retention_hours
@staticmethod
def _haversine_m(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
R = 6371000.0
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon/2)**2
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
def _calculate_adaptive_radius(self, current_pos: PositionRecord) -> float:
"""
Dynamically scales geofence threshold based on asset velocity and dwell behavior.
Prevents boundary flapping during high-speed transits or GPS multipath events.
"""
sog_ms = (current_pos.sog_knots or 0.0) * 0.514444
# Velocity-adaptive expansion: higher speed = larger lookahead buffer
dynamic_radius = self.base_radius_m + (sog_ms * self.velocity_scale_factor)
# Apply hysteresis to prevent rapid state oscillation
if self.state == "INSIDE":
return dynamic_radius + self.hysteresis_margin_m
return dynamic_radius
def evaluate(self, record: PositionRecord) -> str:
"""
Evaluates asset position against adaptive threshold.
Returns: 'ENTER', 'EXIT', 'DWELL', or 'NONE'
"""
# Regulatory constraint: discard records outside retention window
now_epoch = datetime.now(timezone.utc).timestamp()
if (now_epoch - record.timestamp) > (self.max_retention_hours * 3600):
logger.warning("Stale telemetry discarded", extra={"asset_id": record.asset_id})
return "NONE"
# Handle out-of-order timestamps gracefully
if self.history and record.timestamp < self.history[-1].timestamp:
logger.info("Out-of-order telemetry skipped", extra={"asset_id": record.asset_id})
return "NONE"
self.history.append(record)
dist_m = self._haversine_m(self.center[0], self.center[1], record.lat, record.lon)
threshold_m = self._calculate_adaptive_radius(record)
prev_state = self.state
if dist_m <= threshold_m:
self.state = "INSIDE"
else:
self.state = "OUTSIDE"
# State transition logic
if prev_state == "OUTSIDE" and self.state == "INSIDE":
logger.info("Geofence ENTER", extra={"asset_id": record.asset_id, "geofence_id": self.geofence_id, "dist_m": round(dist_m, 2), "threshold_m": round(threshold_m, 2)})
return "ENTER"
elif prev_state == "INSIDE" and self.state == "OUTSIDE":
logger.info("Geofence EXIT", extra={"asset_id": record.asset_id, "geofence_id": self.geofence_id, "dist_m": round(dist_m, 2), "threshold_m": round(threshold_m, 2)})
return "EXIT"
elif self.state == "INSIDE":
return "DWELL"
return "NONE"
Adaptive Threshold Mechanics
stateDiagram-v2 [*] --> OUTSIDE OUTSIDE --> INSIDE: dist ≤ adaptive radius INSIDE --> INSIDE: still inside · DWELL INSIDE --> OUTSIDE: dist exceeds radius + hysteresis
Effective threshold configuration requires a multi-layered approach that accounts for asset velocity, dwell behavior, and terminal topology. Static baselines should be initialized to terminal lane width plus GPS error margin (typically 15–30m for RTK-corrected yard assets, 50–100m for standard marine AIS). Velocity-adaptive scaling expands thresholds proportionally to asset speed to accommodate braking distances and telemetry latency. Dwell-time hysteresis prevents boundary flapping when assets linger near perimeter edges due to multipath reflection or queuing.
Topology-aware boundaries further refine evaluation by masking non-operational zones (e.g., maintenance bays, administrative corridors) using polygon clipping before radius evaluation. This reduces computational load and eliminates false alerts triggered by equipment parked in non-monitored sectors.
Structured Logging & Regulatory Compliance
Terminal automation systems operate under strict regulatory frameworks. IMO AIS performance standards mandate deterministic position reporting intervals, while port authority data governance policies require audit trails that exclude personally identifiable information (PII) and enforce strict retention limits. Structured logging ensures every geofence transition is traceable without violating data minimization principles.
The logging implementation above serializes operational metadata into JSON, enabling direct ingestion into ELK or Splunk pipelines. By attaching asset_id, geofence_id, and latency metrics, operators can correlate threshold behavior with network congestion or equipment degradation. Compliance is maintained by:
- Enforcing configurable TTL windows for positional history
- Stripping PII from log payloads
- Maintaining immutable audit trails for incident reconstruction
- Aligning threshold parameters with Terminal API Polling Strategies to prevent over-querying legacy TOS endpoints
When integrated with Container Status Mapping Rules and a tiered fallback chain, adaptive geofencing becomes a resilient component of terminal orchestration. Engineers should validate threshold parameters against historical yard throughput data, applying A/B testing during low-traffic maintenance windows before deploying to production SLA pipelines.