"""Dynamic electrical load (current) profiles.
Supports constant, cyclic, arbitrary time-series, and CSV import.
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
[docs]
class LoadProfile:
"""Piece-wise-linear current profile I(t).
Parameters
----------
times : array-like
Monotonically increasing time stamps [s].
currents : array-like
RMS current values [A] at each time stamp.
"""
[docs]
def __init__(self, times: np.ndarray, currents: np.ndarray):
self.times = np.asarray(times, dtype=float)
self.currents = np.asarray(currents, dtype=float)
if self.times.shape != self.currents.shape:
raise ValueError("times and currents must have the same length")
[docs]
def current_at(self, t: float) -> float:
"""Linearly interpolated current at time *t* [s]."""
return float(np.interp(t, self.times, self.currents))
[docs]
def current_array(self, times: np.ndarray) -> np.ndarray:
"""Vectorised interpolation."""
return np.interp(times, self.times, self.currents)
@property
def duration(self) -> float:
return float(self.times[-1] - self.times[0])
# ── factory methods ──────────────────────────────────────────────
[docs]
@classmethod
def constant(cls, current_a: float, duration_s: float) -> LoadProfile:
"""Constant current for a given duration."""
return cls(
times=np.array([0.0, duration_s]),
currents=np.array([current_a, current_a]),
)
[docs]
@classmethod
def cyclic(
cls,
peak_current: float,
base_current: float,
period_s: float,
duty_cycle: float,
n_cycles: int,
) -> LoadProfile:
"""Rectangular on/off cyclic load.
Parameters
----------
peak_current, base_current : float
Current during on and off phases [A].
period_s : float
Duration of one full cycle [s].
duty_cycle : float
Fraction of period at peak current (0–1).
n_cycles : int
Number of complete cycles.
"""
t_on = period_s * duty_cycle
times, currents = [0.0], [peak_current]
for i in range(n_cycles):
t_start = i * period_s
times += [t_start + t_on - 1e-3, t_start + t_on, t_start + period_s - 1e-3]
currents += [peak_current, base_current, base_current]
if i < n_cycles - 1:
times.append(t_start + period_s)
currents.append(peak_current)
return cls(times=np.array(times), currents=np.array(currents))
[docs]
@classmethod
def daily_pattern(
cls,
hourly_currents: list[float],
n_days: int = 1,
) -> LoadProfile:
"""Create a profile from 24 hourly current values repeated for *n_days*.
Parameters
----------
hourly_currents : list of 24 floats
Current [A] for each hour of the day (00:00–23:00).
n_days : int
Number of days to repeat.
"""
if len(hourly_currents) != 24:
raise ValueError("Need exactly 24 hourly values")
t_list, i_list = [], []
for day in range(n_days):
for h, amp in enumerate(hourly_currents):
t_list.append((day * 24 + h) * 3600.0)
i_list.append(amp)
t_list.append(n_days * 24 * 3600.0)
i_list.append(hourly_currents[0])
return cls(times=np.array(t_list), currents=np.array(i_list))
[docs]
@classmethod
def from_csv(
cls,
filepath: str | Path,
time_col: int = 0,
current_col: int = 1,
delimiter: str = ",",
skip_header: int = 1,
) -> LoadProfile:
"""Load a profile from a two-column CSV (time [s], current [A])."""
data = np.loadtxt(filepath, delimiter=delimiter, skiprows=skip_header)
return cls(times=data[:, time_col], currents=data[:, current_col])