Weather Window Optimization
Weather window optimization is the decision layer that converts raw meteorological telemetry into a defensible go/no-go verdict for every scheduled tower climb. It sits inside the broader Intelligent Inspection Scheduling & Technician Routing framework, standing between the forecast feeds and the dispatch queue: it evaluates site-specific wind, lightning, and precipitation limits against contractual and OSHA constraints, then either releases a work order into routing or defers it with a signed audit record. For infrastructure operators, lease administrators, municipal compliance teams, and the Python automation engineers who maintain these pipelines, the margin between a productive site visit and a regulatory violation is measured in the precision of that verdict. Static calendars collapse under dynamic atmospheric conditions, producing wasted dispatches, lease non-compliance penalties, and needless exposure to fall and electrocution hazards. This page details the data model, the evaluation logic, and the runnable implementation that make those verdicts deterministic and auditable.
The Core Challenge
The failure mode this subsystem exists to prevent is subtle because it usually looks like success. Consider tower TWR-4471, a 180 ft lattice structure whose lease with the site landlord caps climbing work at 25 mph sustained wind and whose municipal permit (MUN-07-221) enforces a 30-minute lightning hold-down within a 10-mile radius. A fixed Tuesday-morning maintenance slot ignores the fact that a squall line is forecast to push gusts to 34 mph by 09:00. A crew dispatched on the calendar alone either climbs into an unsafe envelope — creating liability the moment an OSHA inspector pulls the record — or arrives, waits, and stands down, burning a four-hour travel-and-labor block that the operator still pays for. Multiply that across a portfolio of several hundred sites spanning multiple microclimates and the annualized cost of calendar-driven scheduling runs into six figures of wasted dispatch plus an uninsurable tail of compliance exposure.
The core challenge, then, is not “read the weather.” It is to encode every site’s contractual envelope as machine-readable thresholds, evaluate live telemetry against those thresholds fast enough to hold or release a work order, and emit an immutable record proving the decision was made correctly — for every site, every window, every time. That record is what turns a subjective safety judgment into an auditable gatekeeping decision.
Data Model & Schema
Reliable evaluation begins with a strict, strongly typed representation of both the site’s operating envelope and the observed conditions. Two dataclasses carry the contract. SiteConstraints codifies the lease and municipal limits that rarely change; WeatherTelemetry carries the volatile observation for a single evaluation instant.
| Field | Type | Constraint | Source |
|---|---|---|---|
site_id |
str |
TWR-XXXX format |
Asset registry |
max_sustained_wind_mph |
float |
> 0, typically 25.0 | Lease covenant |
lightning_radius_miles |
float |
> 0, typically 10.0 | Municipal permit |
allow_freezing_precip |
bool |
default False |
OSHA 1926 Subpart M |
lease_tier |
str |
standard | priority | critical |
Lease taxonomy |
override_authorized |
bool |
default False |
Regional safety director |
A canonical evaluation payload — the object that gets hashed and logged — serializes to compact JSON so it can be deduplicated and forensically compared across audit cycles:
{
"site_id": "TWR-4471",
"threshold_version": "v1.4.0",
"wind_speed_mph": 34.0,
"lightning_distance_miles": 6.2,
"is_freezing_precip": false,
"verdict": "DENIED"
}
The threshold version string is load-bearing: it pins the exact envelope that produced a verdict, so a decision made in one ordinance regime never appears to conflict with a re-evaluation made after the limits changed. Freezing-precipitation limits derive from fall-protection standards rather than the lease, which is why allow_freezing_precip defaults to the conservative False and can only be relaxed deliberately.
Evaluating the Window: Architectural Approach
The evaluator is a short-circuit gate. Conditions are checked in descending order of hazard severity — wind, then lightning proximity, then freezing precipitation — and the first breach denies the window without evaluating the remainder. This ordering makes the audit record self-explanatory: the logged reason is always the most severe active hazard, which is exactly what a safety reviewer needs to see first. When no threshold is breached the window is approved; when one is breached the request falls through to the override path, where a supervisor with standing authorization can escalate to a risk-quantified OVERRIDE_REQUIRED state that mandates enhanced PPE and separate logging.
The forecast telemetry itself arrives from the ingestion pipeline described in Integrating NOAA Weather APIs for Safe Tower Climb Scheduling, which normalizes National Weather Service grid-point strings into the numeric fields this evaluator consumes. Keeping ingestion and evaluation as separate stages means the gate logic stays a pure function of its inputs — trivially testable and free of network side effects.
Figure: weather-window safety decision flow with emergency override path.
Validation & Compliance Gates
A verdict is only trustworthy if it is reproducible and tamper-evident. Every evaluation produces a SHA-256 audit hash computed over the canonical payload plus the active threshold version, so two evaluations of the same site under the same conditions and limits yield an identical hash — enabling deduplication — while any drift in conditions, limits, or the threshold version yields a different one. Denied windows are not discarded; they are written to the audit history exactly like approvals, because the absence of a climb is itself a compliance event that a municipal reviewer may need to reconstruct.
Malformed or incomplete telemetry never silently passes the gate. If a required numeric field is missing or a lightning reading arrives without a distance, the evaluator raises a typed WeatherWindowError and records an EVALUATION_ERROR verdict rather than defaulting to “approved.” This fail-closed posture mirrors the quarantine behavior used across the parent framework: a record the system cannot confidently evaluate is held for manual review, never released into dispatch. Emergency overrides are subject to the strictest gate of all — an override request against a site whose override_authorized flag is unset is logged at CRITICAL and denied, so an unauthorized attempt leaves a louder trail than an ordinary deferral.
Integration Points
Weather window optimization is a hinge between two sibling subsystems. Upstream, Frequency Logic & Threshold Tuning decides how often a site must be visited and supplies the priority tier that widens or narrows an acceptable window — a critical structural inspection tolerates a marginal forecast that a routine vegetation task would defer. The mechanics of that tiering are covered in Dynamic Inspection Frequency Calculation Based on Tower Age and Load, whose output feeds directly into the lease_tier field of the constraint model above.
Downstream, an approved window is a precondition, not a dispatch. The verdict propagates into Technician Assignment Algorithms, which match a certified crew to the site only once the environmental gate clears, and into the route construction detailed in Optimizing technician routes for multi-site maintenance windows, where a mid-route deferral must be able to reshuffle the remaining stops without stranding a crew. Because each of these stages consumes the same audit hash, a single climb can be traced end to end — from the forecast that authorized it to the technician who executed it.
Python Implementation
The module below is a complete, runnable evaluator. It carries structured audit logging, a custom WeatherWindowError exception, fail-closed handling of malformed telemetry, and a SHA-256 audit hash emitted with every verdict. Identifiers use realistic telecom formats.
import hashlib
import json
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Optional
# Dedicated audit logger for compliance traceability.
audit_logger = logging.getLogger("weather_window_compliance")
audit_logger.setLevel(logging.INFO)
_handler = logging.StreamHandler()
_handler.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s | %(message)s"))
audit_logger.addHandler(_handler)
THRESHOLD_VERSION = "v1.4.0"
class ComplianceStatus(Enum):
APPROVED = "APPROVED"
DENIED = "DENIED"
OVERRIDE_REQUIRED = "OVERRIDE_REQUIRED"
EVALUATION_ERROR = "EVALUATION_ERROR"
class WeatherWindowError(Exception):
"""Raised when telemetry is too incomplete to evaluate safely."""
@dataclass
class SiteConstraints:
site_id: str
max_sustained_wind_mph: float = 25.0
lightning_radius_miles: float = 10.0
allow_freezing_precip: bool = False
lease_tier: str = "standard"
override_authorized: bool = False
@dataclass
class WeatherTelemetry:
timestamp: datetime
wind_speed_mph: float
lightning_detected: bool
lightning_distance_miles: Optional[float] = None
is_freezing_precip: bool = False
def audit_hash(site: SiteConstraints, weather: WeatherTelemetry, verdict: str) -> str:
"""Deterministic SHA-256 over the canonical decision payload + threshold version."""
payload = json.dumps({
"site_id": site.site_id,
"threshold_version": THRESHOLD_VERSION,
"wind_speed_mph": weather.wind_speed_mph,
"lightning_distance_miles": weather.lightning_distance_miles,
"is_freezing_precip": weather.is_freezing_precip,
"verdict": verdict,
}, sort_keys=True)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
class WeatherWindowOptimizer:
"""Evaluates meteorological telemetry against lease and municipal constraints."""
def __init__(self) -> None:
self._history: list[dict] = []
def _record(self, site: SiteConstraints, weather: WeatherTelemetry,
status: ComplianceStatus) -> dict:
digest = audit_hash(site, weather, status.value)
record = {
"site_id": site.site_id,
"status": status.value,
"timestamp": weather.timestamp.isoformat(),
"audit_hash": digest,
}
self._history.append(record)
return record
def evaluate_window(self, site: SiteConstraints, weather: WeatherTelemetry) -> dict:
try:
if weather.wind_speed_mph is None:
raise WeatherWindowError(f"missing wind reading for {site.site_id}")
if weather.wind_speed_mph > site.max_sustained_wind_mph:
audit_logger.warning(
"Wind breach at %s: %.1f > %.1f mph",
site.site_id, weather.wind_speed_mph, site.max_sustained_wind_mph)
return self._record(site, weather, ComplianceStatus.DENIED)
if weather.lightning_detected:
if weather.lightning_distance_miles is None:
raise WeatherWindowError(f"lightning flagged without distance for {site.site_id}")
if weather.lightning_distance_miles <= site.lightning_radius_miles:
audit_logger.warning(
"Lightning proximity at %s: %.1f mi",
site.site_id, weather.lightning_distance_miles)
return self._record(site, weather, ComplianceStatus.DENIED)
if weather.is_freezing_precip and not site.allow_freezing_precip:
audit_logger.warning("Freezing precipitation at %s", site.site_id)
return self._record(site, weather, ComplianceStatus.DENIED)
audit_logger.info("Window approved for %s", site.site_id)
return self._record(site, weather, ComplianceStatus.APPROVED)
except WeatherWindowError as exc:
audit_logger.error("Evaluation error: %s", exc)
return self._record(site, weather, ComplianceStatus.EVALUATION_ERROR)
def request_override(self, site: SiteConstraints, weather: WeatherTelemetry,
reason: str) -> dict:
"""Emergency override with mandatory, separately tagged logging."""
if not site.override_authorized:
audit_logger.critical("Unauthorized override for %s: %s", site.site_id, reason)
return self._record(site, weather, ComplianceStatus.DENIED)
audit_logger.warning("OVERRIDE GRANTED for %s | reason=%s", site.site_id, reason)
return self._record(site, weather, ComplianceStatus.OVERRIDE_REQUIRED)
if __name__ == "__main__":
optimizer = WeatherWindowOptimizer()
site = SiteConstraints(site_id="TWR-4471", override_authorized=False)
reading = WeatherTelemetry(
timestamp=datetime.now(timezone.utc),
wind_speed_mph=34.0,
lightning_detected=True,
lightning_distance_miles=6.2,
)
result = optimizer.evaluate_window(site, reading)
print(json.dumps(result, indent=2))
Testing & Verification
Because the gate is a pure function of its inputs, verification is a set of table-driven assertions — no network, no clock mocking beyond the timestamp. The stubs below cover the approval path, each denial branch, and the fail-closed error path.
from datetime import datetime, timezone
NOW = datetime(2026, 7, 3, 14, 0, tzinfo=timezone.utc)
def _reading(**kw):
base = dict(timestamp=NOW, wind_speed_mph=12.0,
lightning_detected=False, lightning_distance_miles=None)
base.update(kw)
return WeatherTelemetry(**base)
def test_calm_conditions_approve():
opt = WeatherWindowOptimizer()
result = opt.evaluate_window(SiteConstraints("TWR-4471"), _reading())
assert result["status"] == "APPROVED"
def test_high_wind_denies():
opt = WeatherWindowOptimizer()
result = opt.evaluate_window(SiteConstraints("TWR-4471"), _reading(wind_speed_mph=34.0))
assert result["status"] == "DENIED"
def test_nearby_lightning_denies():
opt = WeatherWindowOptimizer()
r = _reading(lightning_detected=True, lightning_distance_miles=6.2)
assert opt.evaluate_window(SiteConstraints("TWR-4471"), r)["status"] == "DENIED"
def test_lightning_without_distance_is_error():
opt = WeatherWindowOptimizer()
r = _reading(lightning_detected=True, lightning_distance_miles=None)
assert opt.evaluate_window(SiteConstraints("TWR-4471"), r)["status"] == "EVALUATION_ERROR"
def test_hash_is_stable_and_condition_sensitive():
a = audit_hash(SiteConstraints("TWR-4471"), _reading(wind_speed_mph=34.0), "DENIED")
b = audit_hash(SiteConstraints("TWR-4471"), _reading(wind_speed_mph=34.0), "DENIED")
c = audit_hash(SiteConstraints("TWR-4471"), _reading(wind_speed_mph=12.0), "APPROVED")
assert a == b and a != c
Running the module directly prints the TWR-4471 verdict — a DENIED status (wind at 34 mph clears the 25 mph cap before lightning is even checked) carrying a 64-character hex audit_hash. A failing test almost always points to one of two mistakes: a threshold comparison using >= where the lease specifies a strict >, or telemetry constructed without the timestamp field, which surfaces as a TypeError at the dataclass boundary rather than an EVALUATION_ERROR.
Operational Considerations
Field reality complicates the clean gate. Ruggedized tablets in remote coverage gaps cannot always reach the forecast feed at decision time; the optimizer must therefore accept a cached-telemetry mode with a staleness bound, denying any window whose backing observation is older than the site’s configured tolerance rather than trusting a possibly obsolete “calm” reading. Microclimate variance is the second recurring trap: a valley-floor grid point can under-report the wind shear a 200 ft crew actually experiences, which is why elevation-adjusted thresholds live in SiteConstraints per site rather than as a global constant.
Multi-jurisdiction portfolios add a lightning-hold subtlety — some municipalities mandate a fixed post-strike hold-down measured in minutes, not just a distance radius, so a site can clear the 10-mile check yet remain inside a regulatory quiet period. Finally, every timestamp in the audit trail is stored in UTC precisely so that a window spanning a daylight-saving transition, or a crew working across a state line, never produces two records that appear to reorder in time. These edge cases are the difference between a demo and a system a compliance auditor will accept.
FAQ
How do weather exclusions affect downstream route feasibility?
A denial removes a site from the current dispatch pool, which can invalidate an already-optimized multi-stop route. The verdict therefore propagates to the routing layer with its audit hash so the route can be reconstructed around the removed stop, and the deferred site re-enters scheduling at the next viable window computed from its priority tier.
What happens when a telemetry field is missing?
The evaluator fails closed. A missing wind reading, or a lightning flag with no accompanying distance, raises WeatherWindowError and records an EVALUATION_ERROR verdict. The window is never approved on incomplete data — it is held for manual review, exactly like a quarantined record elsewhere in the pipeline.
Can a supervisor override a denied window?
Only when the site’s override_authorized flag is set, which is granted by a regional safety director. An authorized override returns OVERRIDE_REQUIRED — mandating enhanced PPE and separate logging — while an unauthorized attempt is logged at CRITICAL and denied, leaving a louder trail than an ordinary deferral.
Why hash the decision instead of just logging it?
The SHA-256 hash over the canonical payload and threshold version makes each verdict tamper-evident and deduplicable. Identical conditions under identical limits yield an identical hash, so a re-run confirms nothing changed, while any drift in conditions, limits, or the pinned threshold version produces a visibly different hash during an audit.