Source code for thermal_cable_model.cable

"""Cable construction definitions for LV and MV applications.

Cables are described by concentric cylindrical layers from conductor outward.
Factory class-methods provide common cable constructions; users can also
build custom cables layer-by-layer.
"""

from __future__ import annotations

import math
from dataclasses import dataclass, field

from thermal_cable_model.materials import (
    ALUMINUM,
    COPPER,
    EPR,
    LEAD_SHEATH,
    PE_JACKET,
    PVC,
    PVC_JACKET,
    STEEL_ARMOUR,
    XLPE,
    ThermalMaterial,
)


[docs] @dataclass class CableLayer: """Single concentric cylindrical layer. Parameters ---------- material : ThermalMaterial inner_radius : float [m] outer_radius : float [m] """ material: ThermalMaterial inner_radius: float outer_radius: float @property def thickness(self) -> float: return self.outer_radius - self.inner_radius @property def thermal_resistance_per_length(self) -> float: """Radial thermal resistance per unit length [K·m/W].""" if self.thickness < 1e-9: return 0.0 return ( self.material.thermal_resistivity / (2.0 * math.pi) * math.log(self.outer_radius / self.inner_radius) ) @property def thermal_capacitance_per_length(self) -> float: """Thermal capacitance per unit length [J/(m·K)].""" area = math.pi * (self.outer_radius ** 2 - self.inner_radius ** 2) return self.material.volumetric_heat_capacity * area
[docs] @dataclass class Cable: """Complete cable construction. Parameters ---------- name : str Descriptive identifier. voltage_class : str ``"LV"`` or ``"MV"``. n_conductors : int Number of power conductors (1 for single-core, 3 for three-core). conductor_area : float Cross-sectional area of one conductor [m²]. conductor_material : ThermalMaterial layers : list[CableLayer] Ordered list from innermost insulation outward (excluding the conductor itself). ac_resistance_20c : float AC resistance at 20 °C per unit length [Ω/m]. temp_coeff_resistance : float Temperature coefficient of resistance [1/K] (≈ 3.93e-3 for Cu, 4.03e-3 for Al). max_conductor_temp : float Maximum continuous conductor temperature [°C]. loss_factor_sheath : float λ₁ — ratio of sheath/screen losses to conductor losses. loss_factor_armour : float λ₂ — ratio of armour losses to conductor losses. dielectric_loss : float W_d — dielectric loss per phase per unit length [W/m]. """ name: str voltage_class: str n_conductors: int conductor_area: float conductor_material: ThermalMaterial layers: list[CableLayer] = field(default_factory=list) ac_resistance_20c: float = 0.0 temp_coeff_resistance: float = 3.93e-3 max_conductor_temp: float = 90.0 loss_factor_sheath: float = 0.0 loss_factor_armour: float = 0.0 dielectric_loss: float = 0.0 # ── derived geometry ───────────────────────────────────────────── @property def conductor_radius(self) -> float: """Equivalent solid conductor radius [m].""" return math.sqrt(self.conductor_area / math.pi) @property def outer_radius(self) -> float: if not self.layers: return self.conductor_radius return self.layers[-1].outer_radius @property def outer_diameter(self) -> float: return 2.0 * self.outer_radius # ── electrical properties ────────────────────────────────────────
[docs] def ac_resistance(self, temperature: float) -> float: """Temperature-corrected AC resistance [Ω/m].""" return self.ac_resistance_20c * ( 1.0 + self.temp_coeff_resistance * (temperature - 20.0) )
[docs] def conductor_loss(self, current: float, temperature: float) -> float: """Joule loss per conductor per unit length W_c [W/m].""" return current ** 2 * self.ac_resistance(temperature)
[docs] def total_heat_per_length(self, current: float, temperature: float) -> float: """Total heat generation per cable per unit length [W/m]. Includes conductor, sheath, armour, and dielectric losses. """ wc = self.conductor_loss(current, temperature) return self.n_conductors * ( wc * (1.0 + self.loss_factor_sheath + self.loss_factor_armour) + self.dielectric_loss )
# ── thermal properties ─────────────────────────────────────────── @property def conductor_capacitance_per_length(self) -> float: """Thermal capacitance of the conductor [J/(m·K)].""" return ( self.conductor_material.volumetric_heat_capacity * self.conductor_area ) # ── factory methods for common constructions ─────────────────────
[docs] @classmethod def single_core_xlpe_cu( cls, conductor_area_mm2: float, voltage_class: str = "MV", voltage_kv: float = 20.0, ) -> Cable: """Single-core copper XLPE cable (typical MV distribution cable). Approximate geometry generated from the conductor area; for exact modelling supply layer dimensions directly. """ A = conductor_area_mm2 * 1e-6 # m² r_c = math.sqrt(A / math.pi) r_ins_inner = r_c + 0.5e-3 # semi-conducting screen insulation_thickness = _insulation_thickness_xlpe(voltage_kv) r_ins_outer = r_ins_inner + insulation_thickness r_screen_outer = r_ins_outer + 0.3e-3 # copper tape screen r_jacket_outer = r_screen_outer + 2.0e-3 layers = [ CableLayer(XLPE, r_c, r_ins_outer), CableLayer(COPPER, r_ins_outer, r_screen_outer), CableLayer(PE_JACKET, r_screen_outer, r_jacket_outer), ] rac20 = _rac_from_area_cu(conductor_area_mm2) return cls( name=f"1×{conductor_area_mm2:.0f} mm² Cu XLPE {voltage_kv:.0f} kV", voltage_class=voltage_class, n_conductors=1, conductor_area=A, conductor_material=COPPER, layers=layers, ac_resistance_20c=rac20, temp_coeff_resistance=3.93e-3, max_conductor_temp=90.0, loss_factor_sheath=0.05, loss_factor_armour=0.0, dielectric_loss=_dielectric_loss(voltage_kv, insulation_thickness, r_c), )
[docs] @classmethod def single_core_xlpe_al( cls, conductor_area_mm2: float, voltage_class: str = "MV", voltage_kv: float = 20.0, ) -> Cable: """Single-core aluminium XLPE cable.""" A = conductor_area_mm2 * 1e-6 r_c = math.sqrt(A / math.pi) r_ins_inner = r_c + 0.5e-3 insulation_thickness = _insulation_thickness_xlpe(voltage_kv) r_ins_outer = r_ins_inner + insulation_thickness r_screen_outer = r_ins_outer + 0.3e-3 r_jacket_outer = r_screen_outer + 2.0e-3 layers = [ CableLayer(XLPE, r_c, r_ins_outer), CableLayer(ALUMINUM, r_ins_outer, r_screen_outer), CableLayer(PE_JACKET, r_screen_outer, r_jacket_outer), ] rac20 = _rac_from_area_al(conductor_area_mm2) return cls( name=f"1×{conductor_area_mm2:.0f} mm² Al XLPE {voltage_kv:.0f} kV", voltage_class=voltage_class, n_conductors=1, conductor_area=A, conductor_material=ALUMINUM, layers=layers, ac_resistance_20c=rac20, temp_coeff_resistance=4.03e-3, max_conductor_temp=90.0, loss_factor_sheath=0.05, loss_factor_armour=0.0, dielectric_loss=_dielectric_loss(voltage_kv, insulation_thickness, r_c), )
[docs] @classmethod def three_core_xlpe_cu( cls, conductor_area_mm2: float, voltage_class: str = "LV", voltage_kv: float = 0.6, ) -> Cable: """Three-core copper XLPE cable (typical LV distribution cable).""" A = conductor_area_mm2 * 1e-6 r_c = math.sqrt(A / math.pi) insulation_thickness = _insulation_thickness_xlpe(voltage_kv) r_ins_outer = r_c + insulation_thickness # Three-core equivalent: assume trefoil touching -> effective outer radius # enclosing three insulated cores in a round cross section r_trefoil = r_ins_outer * (1.0 + 2.0 / math.sqrt(3.0)) / 2.0 + 0.5e-3 r_bedding_outer = r_trefoil + 1.5e-3 r_armour_outer = r_bedding_outer + 2.0e-3 r_jacket_outer = r_armour_outer + 2.0e-3 layers = [ CableLayer(XLPE, r_c, r_ins_outer), CableLayer(PVC, r_ins_outer, r_bedding_outer), CableLayer(STEEL_ARMOUR, r_bedding_outer, r_armour_outer), CableLayer(PVC_JACKET, r_armour_outer, r_jacket_outer), ] rac20 = _rac_from_area_cu(conductor_area_mm2) return cls( name=f"3×{conductor_area_mm2:.0f} mm² Cu XLPE {voltage_kv:.1f} kV", voltage_class=voltage_class, n_conductors=3, conductor_area=A, conductor_material=COPPER, layers=layers, ac_resistance_20c=rac20, temp_coeff_resistance=3.93e-3, max_conductor_temp=70.0, loss_factor_sheath=0.0, loss_factor_armour=0.03, dielectric_loss=0.0, )
[docs] @classmethod def three_core_pvc_cu( cls, conductor_area_mm2: float, voltage_class: str = "LV", voltage_kv: float = 0.6, ) -> Cable: """Three-core copper PVC cable (LV).""" A = conductor_area_mm2 * 1e-6 r_c = math.sqrt(A / math.pi) insulation_thickness = _insulation_thickness_pvc(voltage_kv) r_ins_outer = r_c + insulation_thickness r_trefoil = r_ins_outer * (1.0 + 2.0 / math.sqrt(3.0)) / 2.0 + 0.5e-3 r_bedding_outer = r_trefoil + 1.5e-3 r_armour_outer = r_bedding_outer + 2.0e-3 r_jacket_outer = r_armour_outer + 2.0e-3 layers = [ CableLayer(PVC, r_c, r_ins_outer), CableLayer(PVC, r_ins_outer, r_bedding_outer), CableLayer(STEEL_ARMOUR, r_bedding_outer, r_armour_outer), CableLayer(PVC_JACKET, r_armour_outer, r_jacket_outer), ] rac20 = _rac_from_area_cu(conductor_area_mm2) return cls( name=f"3×{conductor_area_mm2:.0f} mm² Cu PVC {voltage_kv:.1f} kV", voltage_class=voltage_class, n_conductors=3, conductor_area=A, conductor_material=COPPER, layers=layers, ac_resistance_20c=rac20, temp_coeff_resistance=3.93e-3, max_conductor_temp=70.0, loss_factor_sheath=0.0, loss_factor_armour=0.03, dielectric_loss=0.0, )
[docs] def iec_line_capacitance_per_length( relative_permittivity: float, diameter_over_inner_screen_mm: float, diameter_over_insulation_mm: float, ) -> float: """Line capacitance [F/m], IEC 60287-1-1 §2.2 (form used in CIGRE TB880). ``C = (ε_r / (18·ln(D_i / d_c)))·10⁻⁹`` with both diameters in millimetres. """ return ( relative_permittivity / ( 18.0 * math.log(diameter_over_insulation_mm / diameter_over_inner_screen_mm) ) ) * 1e-9
[docs] def dielectric_loss_per_length( capacitance_f_per_m: float, voltage_kv_phase_to_phase: float, tan_delta: float, frequency_hz: float = 50.0, ) -> float: """Dielectric loss per unit length [W/m]: ω C U₀² tan δ. ``U₀`` is RMS phase voltage [V] from line voltage ``U/√3``. """ omega = 2.0 * math.pi * frequency_hz u0_v = voltage_kv_phase_to_phase * 1e3 / math.sqrt(3.0) return omega * capacitance_f_per_m * u0_v**2 * tan_delta
# ── private helpers ────────────────────────────────────────────────── def _insulation_thickness_xlpe(voltage_kv: float) -> float: """Approximate XLPE insulation thickness [m] from rated voltage.""" if voltage_kv <= 1.0: return 1.0e-3 if voltage_kv <= 6.0: return 3.4e-3 if voltage_kv <= 10.0: return 3.4e-3 if voltage_kv <= 20.0: return 5.5e-3 if voltage_kv <= 30.0: return 8.0e-3 return 10.0e-3 def _insulation_thickness_pvc(voltage_kv: float) -> float: if voltage_kv <= 1.0: return 1.5e-3 return 2.5e-3 def _rac_from_area_cu(area_mm2: float) -> float: """Approximate 20 °C AC resistance for stranded copper conductor [Ω/m].""" rho_dc = 1.7241e-8 # Ω·m at 20 °C # AC effects: skin + proximity factor ≈ 1.02–1.10 for typical sizes ks = 1.0 + 0.02 * min(area_mm2 / 500.0, 1.0) return rho_dc / (area_mm2 * 1e-6) * ks def _rac_from_area_al(area_mm2: float) -> float: """Approximate 20 °C AC resistance for aluminium conductor [Ω/m].""" rho_dc = 2.8264e-8 ks = 1.0 + 0.02 * min(area_mm2 / 500.0, 1.0) return rho_dc / (area_mm2 * 1e-6) * ks def _dielectric_loss(voltage_kv: float, insulation_thickness: float, conductor_radius: float) -> float: """Approximate dielectric loss per phase [W/m] for XLPE.""" if voltage_kv < 3.0: return 0.0 eps_r = 2.5 tan_delta = 4e-4 # XLPE typical omega = 2.0 * math.pi * 50.0 U0 = voltage_kv * 1e3 / math.sqrt(3.0) r_c = conductor_radius r_i = r_c + insulation_thickness C = 2.0 * math.pi * eps_r * 8.854e-12 / math.log(r_i / r_c) return omega * C * U0 ** 2 * tan_delta