Source code for thermal_cable_model.visualization

"""Plotting utilities for thermal simulation results."""

from __future__ import annotations

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.figure import Figure

from thermal_cable_model.solver import TransientResult


[docs] def plot_temperature_history( result: TransientResult, cable_indices: list[int] | None = None, show_ambient: bool = True, show_current: bool = True, time_unit: str = "hours", figsize: tuple[float, float] = (14, 8), ) -> Figure: """Plot conductor, insulation, surface, and soil temperatures vs time. Parameters ---------- result : TransientResult cable_indices : list[int], optional Which cables to plot. Defaults to all. show_ambient : bool Overlay the ambient ground temperature. show_current : bool Add a secondary axis with the load current. time_unit : {"seconds", "hours", "days"} figsize : tuple """ divisor = {"seconds": 1.0, "hours": 3600.0, "days": 86400.0}[time_unit] t = result.times / divisor if cable_indices is None: cable_indices = list(range(result.n_cables)) n_panels = 1 + int(show_current) fig, axes = plt.subplots( n_panels, 1, figsize=figsize, sharex=True, gridspec_kw={"height_ratios": [3, 1] if n_panels == 2 else [1]}, ) if n_panels == 1: axes = [axes] ax_temp = axes[0] colors = plt.cm.tab10.colors # type: ignore[attr-defined] for ci in cable_indices: c = colors[ci % len(colors)] label = result.cable_names[ci] if result.cable_names else f"Cable {ci}" ax_temp.plot(t, result.conductor_temps[:, ci], color=c, linewidth=1.8, label=f"{label} — conductor") ax_temp.plot(t, result.insulation_temps[:, ci], color=c, linewidth=1.4, linestyle="--", label=f"{label} — insulation") ax_temp.plot(t, result.sheath_temps[:, ci], color=c, linewidth=1.1, linestyle=(0, (5, 2)), label=f"{label} — sheath") ax_temp.plot(t, result.armour_temps[:, ci], color=c, linewidth=0.9, linestyle=(0, (3, 2, 1, 2)), label=f"{label} — armour") ax_temp.plot(t, result.surface_temps[:, ci], color=c, linewidth=0.8, linestyle=":", label=f"{label} — surface") ax_temp.plot(t, result.soil_temps[:, ci], color=c, linewidth=0.7, linestyle="-.", label=f"{label} — soil") if show_ambient: ax_temp.plot(t, result.ambient_temps[:, ci], color=c, linewidth=0.6, linestyle=(0, (5, 10)), alpha=0.5, label=f"{label} — ambient") ax_temp.set_ylabel("Temperature [°C]") ax_temp.legend(fontsize=7, ncol=2, loc="upper right") ax_temp.grid(True, alpha=0.3) ax_temp.set_title("Cable Temperature History") if show_current and n_panels == 2: ax_cur = axes[1] for ci in cable_indices: c = colors[ci % len(colors)] label = result.cable_names[ci] if result.cable_names else f"Cable {ci}" ax_cur.plot(t, result.currents[:, ci], color=c, linewidth=1.2, label=label) ax_cur.set_ylabel("Current [A]") ax_cur.set_xlabel(f"Time [{time_unit}]") ax_cur.legend(fontsize=7) ax_cur.grid(True, alpha=0.3) else: ax_temp.set_xlabel(f"Time [{time_unit}]") fig.tight_layout() return fig
[docs] def plot_cross_section( positions_x: list[float], depths: list[float], outer_radii: list[float], temperatures: list[float] | None = None, cable_names: list[str] | None = None, soil_extent: tuple[float, float, float] = (-1.0, 1.0, 2.5), figsize: tuple[float, float] = (10, 8), ) -> Figure: """Plot a 2-D cross-section of the cable arrangement. Parameters ---------- positions_x : list[float] Horizontal positions [m]. depths : list[float] Burial depths [m] (positive downward). outer_radii : list[float] Cable outer radii [m]. temperatures : list[float], optional Conductor temperatures to annotate. cable_names : list[str], optional soil_extent : (x_min, x_max, y_max_depth) """ fig, ax = plt.subplots(1, 1, figsize=figsize) x_min, x_max, y_max = soil_extent # Soil background ax.axhspan(0, y_max, color="#d2b48c", alpha=0.3, label="Soil") ax.axhline(0, color="green", linewidth=2, label="Ground surface") for i, (x, d, r) in enumerate(zip(positions_x, depths, outer_radii)): circle = plt.Circle((x, d), r, color="gray", ec="black", linewidth=1.5) ax.add_patch(circle) # conductor r_cond = r * 0.4 conductor = plt.Circle((x, d), r_cond, color="#b87333", ec="black", linewidth=0.8) ax.add_patch(conductor) name = cable_names[i] if cable_names else f"Cable {i}" label_text = name if temperatures is not None: label_text += f"\n{temperatures[i]:.1f} °C" ax.annotate( label_text, xy=(x, d), xytext=(x, d - r - 0.08), fontsize=8, ha="center", va="bottom", bbox=dict(boxstyle="round,pad=0.3", fc="lightyellow", ec="gray"), arrowprops=dict(arrowstyle="->", color="gray"), ) ax.set_xlim(x_min, x_max) ax.set_ylim(y_max, -0.2) ax.set_xlabel("Horizontal position [m]") ax.set_ylabel("Depth [m]") ax.set_title("Cable Cross-Section") ax.set_aspect("equal") ax.grid(True, alpha=0.3) fig.tight_layout() return fig