Skip to content

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-kW signal 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, linear gives exactly 50 kWh while locf gives 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 (default) does. Set it above the signal's normal logging cadence -- an on-change signal on a steady machine can legitimately stay silent for a long time, and too tight a cap silently discards real consumption (watch coverage).

None
power_unit str

Unit of value_column -- 'W', 'kW' or 'MW'.

'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:segment_energy. None (default) holds every sample until the next one, however long the gap.

None
power_unit str

Unit of value_column -- 'W', 'kW' or 'MW'.

'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 uuid column. Never average across signals: power averages, energy adds.

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 uuid when total=False).

DataFrame
  • kwh -- integrated energy in the window. Net of any negative (exported/regenerated) power.
DataFrame
  • avg_kw -- time-weighted mean power over the covered time, i.e. kwh divided by the hours actually logged. With full coverage and window='1h' this equals kwh.
DataFrame
  • coverage -- fraction of the window backed by data (with total=True, that of the least-covered signal). Anything below 1.0 means the window is under-reported.
DataFrame
  • sample_count -- sample intervals contributing to the window. One interval straddling a boundary counts in both windows.
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 (default) holds every sample until the next one, however long the gap.

None
power_unit str

Unit of value_column -- 'W', 'kW' or 'MW'.

'kW'
total bool

When True, collapse every signal into one plant-level series identified by event_uuid.

False
event_uuid str

UUID assigned to the summed series when total=True.

'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

value_double being kWh consumed in the window starting at

DataFrame

systime.