power_integration
power_integration ¤
Power (kW) to energy (kWh) integration for ts-shape.
Active power is a rate; energy is its integral over time::
E[kWh] = INTEGRAL P[kW] dt
Historians log on change / deadband, so power samples are irregular. That makes both of the obvious shortcuts wrong:
sum()over the raw samples is meaningless -- double the log rate and the "energy" doubles. This is what happens when a*.Total_Active_Power-kWsignal is fed straight into :class:~ts_shape.events.energy.consumption_analysis.EnergyConsumptionEvents, whose methods assume the values are already energy deltas in kWh.mean()over the raw samples is a sample-count average, not a time-weighted one. A machine that sits at 200 kW for 59 minutes and then logs 40 samples around a 900 kW spike in the last minute reads far too high.
:class:PowerIntegrator does the integration properly: every sample is
weighted by how long it was valid, segments are split at window boundaries,
and logging outages are capped so a single stale sample cannot bill hours of
phantom energy.
Typical use -- one number per hour from the raw ts-shape schema::
from ts_shape.transform.calculator.power_integration import PowerIntegrator
PowerIntegrator.energy_by_window(df, window="1h", max_gap="15min")
# start end duration_seconds kwh avg_kw coverage sample_count
Or emit ts-shape-schema energy deltas and hand off to the energy pack::
deltas = PowerIntegrator.to_energy_deltas(df, window="1h", max_gap="15min")
EnergyConsumptionEvents(deltas).consumption_by_window(meter_uuid, window="1h")
Note
If the meter also exposes a cumulative kWh register, prefer it -- a counter difference is billing-accurate and needs no integration. Use this class when instantaneous power is all you have.
PowerIntegrator ¤
PowerIntegrator(
dataframe: DataFrame, time_column: str = "systime"
)
Bases: Base
Transform: integrate an instantaneous power signal into energy.
Stateless classmethods -- each call is a pure function of its inputs, in
line with the calculator/transform pattern described in
:mod:ts_shape.utils.base.
Methods:
- energy_by_window: kWh (and time-weighted mean kW) per time window.
- to_energy_deltas: the same energy, emitted in the standard ts-shape
schema so the events.energy detectors can consume it.
- segment_energy: the per-sample audit trail the two above are built on.
Interpolation (method):
"locf"(default) -- last-observation-carried-forward / step hold. Correct for on-change PLC and meter logging, where a value is by definition valid until the next one is written."linear"-- trapezoidal. Correct for a smoothly varying analog signal sampled on a fixed cycle. On a 0 -> 100 kW ramp over one hour,lineargives exactly 50 kWh whilelocfgives slightly less; that is the interpolation bias, not an error.
Negative power (PV export, regenerative drives) is integrated as-is, so
kwh is net energy. Filter the input first if gross import is wanted.
Example
100 kW for half an hour, then 50 kW, then off (locf holds each value):
import pandas as pd from ts_shape import PowerIntegrator df = pd.DataFrame({ ... "systime": pd.to_datetime([ ... "2025-01-01 00:00", "2025-01-01 00:30", ... "2025-01-01 01:00", "2025-01-01 02:00"]), ... "uuid": "press", ... "value_double": [100.0, 50.0, 0.0, 0.0], ... }) PowerIntegrator.energy_by_window(df, window="1h")[["start", "kwh", "avg_kw"]] start kwh avg_kw 0 2025-01-01 00:00:00 75.0 75.0 1 2025-01-01 01:00:00 0.0 0.0
segment_energy
classmethod
¤
segment_energy(
dataframe: DataFrame,
*,
window: str = "1h",
method: str = "locf",
max_gap: str | None = None,
power_unit: str = "kW",
data_end: Timestamp | str | None = None,
time_column: str = "systime",
uuid_column: str = "uuid",
value_column: str = "value_double"
) -> pd.DataFrame
Energy contribution of every sample, split at window boundaries.
The audit trail behind :meth:energy_by_window: one row per
(sample, window) pair, so a surprising hourly total can be traced back
to the samples that produced it. The kwh column sums exactly to the
windowed result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataframe
|
DataFrame
|
Long-format power data (ts-shape schema by default). |
required |
window
|
str
|
Fixed window size the segments are cut on ('1h', '15min', '1D'). |
'1h'
|
method
|
str
|
'locf' (step hold) or 'linear' (trapezoidal). |
'locf'
|
max_gap
|
str | None
|
Gaps longer than this (e.g. '15min') are treated as
logging outages: the segment is dropped entirely and counts
towards neither energy nor coverage, because the power over
that span is unknown. Without a cap, one sample before a
three-hour outage bills three hours of energy at that power
level; that is what |
None
|
power_unit
|
str
|
Unit of |
'kW'
|
data_end
|
Timestamp | str | None
|
Optional end of the data window. The last sample of each series is held until this timestamp instead of being dropped. |
None
|
time_column
|
str
|
Timestamp column. |
'systime'
|
uuid_column
|
str
|
Column identifying the signal. |
'uuid'
|
value_column
|
str
|
Column holding instantaneous power. |
'value_double'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: uuid, window_start, start, end, |
DataFrame
|
duration_seconds, kw_start, kw_end, kwh. |
energy_by_window
classmethod
¤
energy_by_window(
dataframe: DataFrame,
*,
window: str = "1h",
method: str = "locf",
max_gap: str | None = None,
power_unit: str = "kW",
total: bool = True,
data_end: Timestamp | str | None = None,
time_column: str = "systime",
uuid_column: str = "uuid",
value_column: str = "value_double"
) -> pd.DataFrame
Integrate power into energy per time window.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataframe
|
DataFrame
|
Long-format power data (ts-shape schema by default). |
required |
window
|
str
|
Window size -- '1h' gives kWh per hour. |
'1h'
|
method
|
str
|
'locf' (step hold, default) or 'linear' (trapezoidal). |
'locf'
|
max_gap
|
str | None
|
Cap on how long one sample may be held (e.g. '15min').
Longer gaps are dropped as logging outages -- see
:meth: |
None
|
power_unit
|
str
|
Unit of |
'kW'
|
total
|
bool
|
When True (default), sum across every signal in the frame --
energy is additive across machines. When False, return one row
per signal per window with a |
True
|
data_end
|
Timestamp | str | None
|
Optional end of the data window; the last sample of each series is held until then. |
None
|
time_column
|
str
|
Timestamp column. |
'systime'
|
uuid_column
|
str
|
Column identifying the signal. |
'uuid'
|
value_column
|
str
|
Column holding instantaneous power. |
'value_double'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: start, end, duration_seconds, kwh, avg_kw, |
DataFrame
|
coverage, sample_count (plus |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
Windows with no data at all are omitted rather than reported as |
DataFrame
|
0.0 kWh, so "not measured" never renders as "used nothing". |
Note
window must be a fixed frequency ('15min', '1h', '1D'), and
windows are fixed-length and anchored on the floored sample
timestamp. Elapsed time and total energy are always correct on
tz-aware input, but a '1D' window spans 24 elapsed hours
rather than the 23/25-hour local day, so window labels drift by an
hour after a daylight-saving changeover. Use '1h' and
aggregate to local days afterwards if that matters.
to_energy_deltas
classmethod
¤
to_energy_deltas(
dataframe: DataFrame,
*,
window: str = "1h",
method: str = "locf",
max_gap: str | None = None,
power_unit: str = "kW",
total: bool = False,
event_uuid: str = "energy:kwh",
data_end: Timestamp | str | None = None,
time_column: str = "systime",
uuid_column: str = "uuid",
value_column: str = "value_double"
) -> pd.DataFrame
Emit integrated energy in the standard ts-shape schema.
The bridge into the events.energy pack, whose detectors assume
their input values are already energy deltas in kWh::
deltas = PowerIntegrator.to_energy_deltas(power_df, max_gap="15min")
EnergyConsumptionEvents(deltas).consumption_by_window(
meter_uuid, window="1h", agg="sum"
)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataframe
|
DataFrame
|
Long-format power data. |
required |
window
|
str
|
Delta size -- one row per signal per window. |
'1h'
|
method
|
str
|
'locf' (default) or 'linear'. |
'locf'
|
max_gap
|
str | None
|
Cap on how long one sample may be held; longer gaps
are dropped as logging outages. |
None
|
power_unit
|
str
|
Unit of |
'kW'
|
total
|
bool
|
When True, collapse every signal into one plant-level
series identified by |
False
|
event_uuid
|
str
|
UUID assigned to the summed series when |
'energy:kwh'
|
data_end
|
Timestamp | str | None
|
Optional end of the data window. |
None
|
time_column
|
str
|
Timestamp column. |
'systime'
|
uuid_column
|
str
|
Column identifying the signal. |
'uuid'
|
value_column
|
str
|
Column holding instantaneous power. |
'value_double'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: systime, uuid, value_double, is_delta -- |
DataFrame
|
|
DataFrame
|
|