Repository navigation
Invalidate the preprocess cache when its inputs change - #156
Conversation
AssessmentInputs.from_cfg reused preprocess.pickle even after the config or the input data changed, so edits such as new exclusion periods were silently ignored. with_pickle_cache now takes an optional key that is saved in a .key file next to the pickle, and from_cfg passes a hash of the wind-up version, the config, and the SCADA, metadata and reanalysis data. The cache is used only when the key matches. Turbine coordinates are left out of the config hash, because they are copied into cfg from the metadata after preprocessing. The old key file is removed before the pickle is rewritten, so an interrupted write cannot leave a stale key behind. Closes resgroup#52
There was a problem hiding this comment.
🟡 Changes recommended
The cache key can reuse incorrectly aligned SCADA data when index timezone information changes, while shared turbine-coordinate mutations can also trigger unnecessary preprocessing reruns.
2 open issues
What changed in this PR
This PR addresses #52 by making preprocessing cache reuse depend on configuration and input data, rather than file existence alone.
Changes:
- Adds optional pickle cache keys stored in sidecar files.
- Computes a preprocessing key from the wind-up version, configuration, and input data.
- Adds tests for keyed cache reuse, invalidation, and legacy caches without keys.
| File | Description |
|---|---|
wind_up/interface.py |
Computes and supplies the preprocessing cache key. |
wind_up/caching.py |
Checks saved keys and invalidates them before rewriting cached data. |
tests/test_interface.py |
Tests key stability and changes caused by configuration or data updates. |
tests/test_helpers.py |
Tests cache hits, changed keys, and migration from unkeyed caches. |
Review effort: Balanced
There was a problem hiding this comment.
Thanks very much, Konrad! This is a good fix for #52 and the keyed pickle cache in caching.py is clean. I checked out the branch, ran the new tests (9 passed) and confirmed CI is green.
Must fix
1. The lat/long exclusion needs to cover test_wtgs and ref_wtgs too.
from_yaml builds test_wtgs and ref_wtgs from the same Turbine instances as asset.wtgs, so after add_smart_lat_long_to_cfg the coordinates also appear under those two lists in model_dump_json. I computed the key before and after the lat/long copy on the test_LSA_T13 config and it changes, so a second from_cfg call with the same cfg object still misses the cache once. Suggested:
coords = {"__all__": {"latitude", "longitude"}}
cfg_json = cfg.model_dump_json(exclude={"asset": {"wtgs": coords}, "test_wtgs": coords, "ref_wtgs": coords})2. The regression test currently can't see this.
test_preprocess_cache_key_changes_with_inputs mutates asset.wtgs[0], which is LSA_T01, neither a test nor a ref turbine. Mutating test_lsa_t13_config.test_wtgs[0].latitude instead makes the last assertion fail on this branch, which is what the test should guard. Even better would be to call add_smart_lat_long_to_cfg on a small metadata frame so the test runs the real path.
3. Add dtypes and index dtype(s) to the hashed header.
hash_pandas_object gives identical hashes for a naive and a UTC-aware DatetimeIndex with the same stored values (Copilot's point: with a non-UTC metadata time zone that would reuse misaligned data), and also for int and bool columns with equal values. One line covers both and works for a MultiIndex:
index_dtypes = [str(lvl.dtype) for lvl in getattr(df.index, "levels", [df.index])]
h.update(f"{name}{list(df.columns)}{list(map(str, df.dtypes))}{index_dtypes}".encode())Release
Could you please also bump version in pyproject.toml to 0.5.1 in this PR? That way I can publish a release as soon as it's merged.
Optional
- Hashing cost. The 60 ms figure is for the small Homer fixture. On a farm-sized frame I measured roughly 2 to 3 s for 3M rows with a UTC
DatetimeIndex, and about 5.5 s per 1M rows with a MultiIndex. Still small next to preprocessing, but could update description. - Editable installs.
__version__comes from package metadata, so code changes to the preprocessing steps don't invalidate the cache in a dev install. Pre-existing and outside #52, but a short note in the_preprocess_cache_keydocstring would address. - Keeping
out_dirin the key is OK. Callers I know of keepout_dirfixed per assessment. - Nit:
caching.pywrapsfpinPath()several times. Normalising once (fp = Path(fp)) and usingfp.with_suffix(fp.suffix + ".key")is an option. - Follow-up idea, not for this PR: the root cause of the exclusion is that
add_smart_lat_long_to_cfgmutates the caller'scfgin place. Returning a copy would remove the special case altogether.
test_wtgs and ref_wtgs share their Turbine instances with asset.wtgs, so the coordinates copied in by add_smart_lat_long_to_cfg changed the key through those lists too. The regression test now runs the real add_smart_lat_long_to_cfg on the LSA_T13 config. hash_pandas_object gives the same hash for int and bool values and for a naive and a UTC index, so the column and index dtypes are now part of the hashed header. Also bump the version to 0.5.1.
|
Thanks for the careful review, and for timing it on farm-sized frames. Pushed b2ac803:
Agreed that returning a copy from |


Closes #52.
AssessmentInputs.from_cfgreusedpreprocess.picklewhenever the file existed, so changes to the config (for exampleexclusion_periods_utc) or to the input data had no effect until the cache was deleted by hand.Changes:
with_pickle_cachetakes an optionalkey. When it is given, the key is written topreprocess.pickle.keynext to the pickle, and the cached result is only used if the saved key matches. The old key file is removed before the pickle is rewritten, so an interrupted write cannot leave a stale key behind.from_cfgpasses a SHA-256 of the wind-up version,cfg.model_dump_json(), and the columns, column dtypes, index dtypes andpd.util.hash_pandas_objectof the SCADA, metadata and reanalysis data. The dtypes are needed becausehash_pandas_objectgives the same hash for int and bool values and for a naive and a UTC index.asset.wtgs,test_wtgsandref_wtgs, which share theirTurbineinstances), becauseadd_smart_lat_long_to_cfgcopies them intocfgfrom the metadata after preprocessing. Otherwise a secondfrom_cfgcall with the samecfgobject would miss the cache once.plot_cfgis left out, since it only affects the plots.out_diris part of the config, so changing it also invalidates the cache. That is on the safe side, but I can exclude it if you prefer.preprocess.picklewithout a key file is recomputed once.versionis bumped to 0.5.1.Hashing is small next to preprocessing: about 60 ms for the 8,928-row Homer test file, and, from the timings in the review, roughly 2 to 3 s for 3M rows with a UTC
DatetimeIndexand about 5.5 s per 1M rows with a MultiIndex.Tests: three new tests for the keyed pickle cache and two for the key (config, data, coordinates via
add_smart_lat_long_to_cfg, dtypes and index time zone). I don't have the LFS test data locally, so I ran the suite withouttest_smarteole.py,test_wedowind.pyandtests/plots/test_input_data.py: 189 passed. The two errors inTestCompareActiveAndReactivePowerPrePostalso happen onmainwithout the LFS files.ruff format --check,ruff checkandmypy .pass. The smarteole and wedowind tests go throughfrom_cfgwith acache_dir, so CI will exercise the new path on real data.