Skip to content

Commit 31e9bc5

Browse files
author
paulf81
committed
remove zoh and update docs and tests
1 parent 3fa8c1a commit 31e9bc5

4 files changed

Lines changed: 73 additions & 55 deletions

File tree

docs/timing.md

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ instantaneous convention.
3030

3131
The `interpolate_df` function in `utilities.py` accepts a mandatory
3232
`interpolation_method` parameter that controls how numeric columns are
33-
resampled onto the simulation time grid. Three methods are available:
33+
resampled onto the simulation time grid. Two methods are available:
3434

3535
#### `"averaged_to_instantaneous"` (wind, solar, and similar resource and power signals)
3636

@@ -60,25 +60,6 @@ time value
6060
Querying at 13:00 yields 150 (halfway between midpoints).
6161
```
6262

63-
#### `"zoh_to_instantaneous"` (LMP, external signals)
64-
65-
Input values are piecewise-constant (zero-order hold) with timestamps at the
66-
start of each interval. Each query time receives the value of the last
67-
original timestamp at or before it -- the value is held constant until the
68-
next timestamp. This is appropriate for signals like locational marginal
69-
prices (LMP) that change in discrete steps.
70-
71-
```
72-
Input file:
73-
74-
time_utc value
75-
12:00 100 ← held constant over [12:00, 13:00)
76-
13:00 200 ← held constant over [13:00, 14:00)
77-
78-
Querying at 12:30 yields 100.
79-
Querying at 13:00 yields 200.
80-
```
81-
8263
#### `"instantaneous_to_instantaneous"`
8364

8465
Input values already represent instantaneous measurements at their
@@ -87,11 +68,46 @@ original timestamps with no midpoint shift.
8768

8869
---
8970

90-
In all three methods, datetime columns (e.g. `time_utc`) are linearly
71+
In both methods, datetime columns (e.g. `time_utc`) are linearly
9172
interpolated on the raw timestamps without any shift, because they are
9273
instantaneous coordinate mappings between simulation time and wall-clock
9374
time, not period-averaged measurements.
9475

76+
#### Achieving zero-order-hold (ZOH) behaviour
77+
78+
`interpolate_df` does not provide a dedicated zero-order-hold mode. If you
79+
need step/piecewise-constant semantics -- for example, LMP prices that
80+
should be held constant across each reporting interval -- pre-process your
81+
input data to include an additional row at the end of each interval that
82+
carries the same value as the start-of-interval row, and then use
83+
`"instantaneous_to_instantaneous"`. Linear interpolation between each pair
84+
of identical endpoints reproduces the ZOH shape.
85+
86+
```
87+
Original data (start-of-interval only):
88+
89+
time_utc value
90+
12:00 100
91+
13:00 200
92+
93+
After inserting end-of-interval rows (just before the next start):
94+
95+
time_utc value
96+
12:00 100
97+
12:59:59 100 ← added endpoint
98+
13:00 200
99+
13:59:59 200 ← added endpoint
100+
101+
Querying at 12:30 with "instantaneous_to_instantaneous" yields 100.
102+
Querying at 13:00 yields 200.
103+
```
104+
105+
See
106+
[`generate_locational_marginal_price_dataframe_from_gridstatus`](../hercules/grid/grid_utilities.py)
107+
in `hercules/grid/grid_utilities.py` for a worked example of this
108+
endpoint-insertion pattern (it shifts a copy of the data by `dt - 1` seconds
109+
and merges it back in before handing the frame to Hercules).
110+
95111
## Input Requirements
96112

97113
All Hercules input files must specify start and end times using UTC datetime strings:
@@ -200,7 +216,16 @@ Both wind and solar input CSV/Feather/Parquet files must contain a `time_utc` co
200216

201217
### External Data (LMP, etc.)
202218

203-
External data files loaded via `_read_external_data_file` are interpolated with `"zoh_to_instantaneous"` (zero-order hold), which is appropriate for signals like LMP prices that are piecewise-constant over each interval rather than time-averaged.
219+
External data files loaded via `_read_external_data_file` are upsampled onto
220+
the simulation time grid with `"instantaneous_to_instantaneous"` (linear
221+
interpolation between the supplied timestamps). If you want zero-order-hold
222+
(piecewise-constant) behaviour for signals like LMP prices, pre-process the
223+
file to include end-of-interval rows that repeat the previous value as
224+
described in [Achieving zero-order-hold (ZOH) behaviour](#achieving-zero-order-hold-zoh-behaviour).
225+
The helper
226+
[`generate_locational_marginal_price_dataframe_from_gridstatus`](../hercules/grid/grid_utilities.py)
227+
in `hercules/grid/grid_utilities.py` is a concrete example of adding those
228+
endpoint rows for LMP data.
204229

205230
```text
206231
time_utc,wd_mean,ws_000,ws_001,ws_002

hercules/hercules_model.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,18 @@ def _read_external_data_file(self, filename):
173173
Read and interpolate external data from a CSV, feather, or pickle file.
174174
175175
This method reads external data from the specified file (CSV, feather, or
176-
pickle) and interpolates it onto the simulation time grid using zero-order
177-
hold (``"zoh_to_instantaneous"``). ZOH is appropriate because external
178-
signals such as LMP prices are piecewise-constant over each reporting
179-
interval, unlike time-averaged weather data used by wind/solar components.
176+
pickle) and upsamples it onto the simulation time grid using
177+
``"instantaneous_to_instantaneous"`` (linear interpolation between the
178+
values at the supplied timestamps).
179+
180+
If zero-order-hold (piecewise-constant / step) behavior is desired --
181+
for example, LMP prices that should be held constant across each
182+
reporting interval -- the external data file must be pre-processed to
183+
include an additional row at the end of each interval carrying the
184+
same value. Linear interpolation between each pair of identical
185+
endpoints then reproduces the ZOH shape. See
186+
``hercules.grid.grid_utilities.generate_locational_marginal_price_dataframe_from_gridstatus``
187+
for a worked example of this endpoint-insertion pattern.
180188
181189
The external data must include a ``time_utc`` column which will be
182190
converted to simulation time. The interpolated data is stored in
@@ -222,7 +230,7 @@ def _read_external_data_file(self, filename):
222230

223231
# Interpolate using the utility function
224232
df_interpolated = interpolate_df(
225-
df_ext, new_times, interpolation_method="zoh_to_instantaneous"
233+
df_ext, new_times, interpolation_method="instantaneous_to_instantaneous"
226234
)
227235

228236
# Convert interpolated DataFrame to dictionary format

hercules/utilities.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,6 @@ def close_logging(logger):
450450

451451
_VALID_INTERPOLATION_METHODS = {
452452
"averaged_to_instantaneous",
453-
"zoh_to_instantaneous",
454453
"instantaneous_to_instantaneous",
455454
}
456455

@@ -465,10 +464,6 @@ def interpolate_df(df, new_time, interpolation_method):
465464
timestamps mark the **start** of each period. Each value is assigned to
466465
the midpoint of its interval and then linearly interpolated. Use for
467466
wind speed, solar irradiance, and similar time-averaged signals.
468-
- ``"zoh_to_instantaneous"``: Input values are piecewise-constant
469-
(zero-order hold) with timestamps at the start of each interval. Each
470-
query time receives the value of the last original timestamp at or
471-
before it. Use for LMP prices and other step-change signals.
472467
- ``"instantaneous_to_instantaneous"``: Input values already represent
473468
instantaneous measurements. Standard linear interpolation is performed
474469
directly on the original timestamps with no midpoint shift.
@@ -477,11 +472,21 @@ def interpolate_df(df, new_time, interpolation_method):
477472
the raw timestamps regardless of the chosen method, because they map
478473
simulation time to wall-clock time directly.
479474
475+
Note:
476+
A dedicated zero-order-hold (ZOH) mode is intentionally not provided.
477+
If you need step/piecewise-constant behaviour (e.g. LMP prices that
478+
should be held constant across each reporting interval), pre-process
479+
the input DataFrame to include an extra row at the end of each
480+
interval carrying the same value, and then call this function with
481+
``"instantaneous_to_instantaneous"``. Linear interpolation between
482+
each pair of identical endpoints reproduces the ZOH shape. See
483+
``hercules.grid.grid_utilities.generate_locational_marginal_price_dataframe_from_gridstatus``
484+
for an example of this endpoint-insertion pattern.
485+
480486
Args:
481487
df (pd.DataFrame): DataFrame with 'time' column and data columns.
482488
new_time (array-like): New time points for interpolation.
483-
interpolation_method (str): One of ``"averaged_to_instantaneous"``,
484-
``"zoh_to_instantaneous"``, or
489+
interpolation_method (str): One of ``"averaged_to_instantaneous"`` or
485490
``"instantaneous_to_instantaneous"``.
486491
487492
Returns:
@@ -519,14 +524,7 @@ def interpolate_df(df, new_time, interpolation_method):
519524

520525
for col in numeric_cols:
521526
col_values = df_pl[col].to_numpy()
522-
if interpolation_method == "zoh_to_instantaneous":
523-
indices = np.searchsorted(time_values, new_time, side="right") - 1
524-
indices = np.clip(indices, 0, len(col_values) - 1)
525-
interpolated_values = col_values[indices].astype(hercules_float_type)
526-
else:
527-
interpolated_values = np.interp(new_time, x_coords, col_values).astype(
528-
hercules_float_type
529-
)
527+
interpolated_values = np.interp(new_time, x_coords, col_values).astype(hercules_float_type)
530528
result_pl = result_pl.with_columns(pl.lit(interpolated_values).alias(col))
531529

532530
# Process datetime columns

tests/utilities_test.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,6 @@ def test_downsampling():
8282
assert np.allclose(result["value"], expected_values)
8383

8484

85-
def test_zoh_interpolation():
86-
"""Test zero-order hold interpolation with interpolate_df.
87-
88-
Each query time should receive the value at the last original
89-
timestamp at or before it (piecewise-constant / step behaviour).
90-
"""
91-
df = pd.DataFrame({"time": [0, 5, 10], "value": [100, 200, 300]})
92-
new_time = np.array([0, 2, 5, 7, 10, 12])
93-
result = interpolate_df(df, new_time, interpolation_method="zoh_to_instantaneous")
94-
expected = [100, 100, 200, 200, 300, 300]
95-
assert np.allclose(result["value"], expected)
96-
97-
9885
def test_datetime_interpolation():
9986
"""
10087
Test interpolation of datetime columns with interpolate_df function.

0 commit comments

Comments
 (0)