Source code for climagrid.features.conductor_sag
"""
ConductorSagIndex: thermal sag estimation for overhead T&D lines.
When conductor temperature rises, the aluminum/ACSR strands expand and
the conductor sags downward, reducing ground clearance. Excessive sag
causes regulatory violations and phase-to-ground faults.
Thermal sag is governed by the IEEE 738-2012 (Standard for Calculating
the Current-Temperature Relationship of Bare Overhead Conductors). This
module implements a SIMPLIFIED subset of that heat balance using ambient
temperature, solar irradiance, and wind as inputs.
It is a relative stress index, not a calibrated conductor temperature: it
models forced convection and solar heating only, and omits radiative cooling,
electrical (Joule) load, and the full IEEE 738 air-property and Morgan
convection equations. Use it for screening and prioritization, not for line
rating, which requires the full standard with load.
The output is a normalized index [0, 1] representing sag relative to the
maximum allowable sag (configurable), suitable as an ML feature.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
[docs]
class ConductorSagIndex:
"""
Computes normalized conductor thermal sag index from weather inputs.
Simplified IEEE 738-2012 heat balance:
T_conductor ≈ T_ambient + (I²R + Q_solar - Q_convective) / (thermal_capacity)
For stress *feature* purposes (not full current rating), we approximate
the conductor temperature as ambient + solar heating - convective cooling,
then compute sag as a fraction of the maximum design sag.
Parameters
----------
temp_col:
Ambient temperature column in °C.
solar_col:
Global horizontal irradiance column in W/m².
wind_col:
Wind speed column in m/s. Wind is the primary cooling mechanism.
max_sag_temp_c:
Conductor temperature at which sag reaches the design maximum.
Default 75°C (typical for ACSR "Drake" conductor per IEEE 738).
conductor_absorptivity:
Solar absorptivity of the conductor surface (0-1). Default 0.5.
conductor_emissivity:
Emissivity for radiated cooling (0-1). Default 0.5.
conductor_diameter_mm:
Conductor outer diameter for convective heat loss. Default 28.1 mm (Drake ACSR).
Example
-------
>>> csi = ConductorSagIndex()
>>> df = csi.compute(env_df)
>>> df["feat_conductor_sag_index"]
"""
# Stefan-Boltzmann constant W/(m²·K⁴)
_SIGMA = 5.6704e-8
def __init__(
self,
temp_col: str = "hrrr_temperature_2m",
solar_col: str = "hrrr_solar_irradiance_ghi",
wind_col: str = "hrrr_wind_speed_10m",
max_sag_temp_c: float = 75.0,
conductor_absorptivity: float = 0.5,
conductor_emissivity: float = 0.5,
conductor_diameter_mm: float = 28.1,
):
self._temp_col = temp_col
self._solar_col = solar_col
self._wind_col = wind_col
self._max_sag_temp_c = max_sag_temp_c
self._alpha = conductor_absorptivity
self._eps = conductor_emissivity
self._d = conductor_diameter_mm / 1000.0 # convert to metres
[docs]
def compute(self, df: pd.DataFrame) -> pd.DataFrame:
"""Add feat_conductor_sag_index column [0, 1]. Returns a copy."""
df = df.copy()
temp = self._resolve_col(df, self._temp_col, ["nasa_temperature_2m", "ncei_temperature_max"])
solar = self._resolve_col(df, self._solar_col, ["nasa_solar_irradiance_ghi"])
wind = self._resolve_col(df, self._wind_col, ["nasa_wind_speed_10m", "ncei_wind_speed"])
if temp is None:
df["feat_conductor_sag_index"] = float("nan")
return df
# Defaults when data not available
if solar is None:
solar = np.full_like(temp, 400.0) # moderate irradiance
if wind is None:
wind = np.full_like(temp, 1.0) # near-calm (conservative)
wind = np.maximum(wind, 0.5) # prevent divide-by-zero in convection
# Solar heat gain per unit length (W/m)
q_solar = self._alpha * solar * self._d
# Convective cooling (simplified Morgan formula per IEEE 738 Eq. 3a)
# q_conv = (1.01 + 1.35 * Re^0.52) * k_f * (T_c - T_a)
# Simplified for feature purposes: linear approximation
# Full implementation would require air density, viscosity, thermal conductivity
# We use: q_conv ≈ h_c * d * delta_T where h_c ≈ 10 * sqrt(wind) W/(m²·K) (typical)
h_c = 10.0 * np.sqrt(wind)
q_conv_per_k = h_c * self._d # W/(m·K) per unit temperature rise
# Steady-state conductor temperature rise above ambient (°C)
# Radiation delta omitted for simplicity (dominated by convection at typical loadings)
delta_t = np.maximum(q_solar / q_conv_per_k, 0.0)
t_conductor = temp + delta_t
# Sag index: ratio of conductor temperature to max allowable, clamped [0,1]
# Using linear approximation: sag scales approximately linearly with temperature
# (IEEE 738 Table B.1 confirms near-linear relationship for typical conductors)
baseline_temp = 25.0 # °C design reference temperature
sag_index = (t_conductor - baseline_temp) / (self._max_sag_temp_c - baseline_temp)
df["feat_conductor_sag_index"] = np.clip(sag_index, 0.0, 1.0)
return df
@property
def _sigma_val(self) -> float:
return self._SIGMA
@staticmethod
def _resolve_col(df: pd.DataFrame, primary: str, fallbacks: list[str]):
if primary in df.columns:
return df[primary].values.astype(float)
for fb in fallbacks:
if fb in df.columns:
return df[fb].values.astype(float)
return None