sunpeek.core_methods.power_check.cache_service#

Reuse cache system for Power Check outputs.

Power Check computations are expensive. The API endpoint (evaluations.py) uses two caching levels, both implemented in this module:

  • Level 1, persist after computation: after a full computation, split the result into per-day rows and save each to the DB, feeding future Level 2 hits.

  • Level 2, day-based partial reuse: reuse cached results for individual days and only compute the missing ones. Freshly computed full-day outputs are persisted immediately.

run_power_check_api tries Level 2 first (finer-grained), then falls back to a full computation (which triggers Level 1). Any cache failure falls back transparently to full computation.

Note that this cache system for Power Check outputs is only available via SunPeek’s http REST API, not when using SunPeek as a Python package.

Storage granularity#

Each cached PowerCheckOutput row represents exactly one full calendar day (midnight-to-midnight in the plant’s local timezone). A DB unique constraint on (plant_id, evaluation_mode, formula, wind_used, datetime_eval_start, datetime_eval_end, safety_pipes, safety_uncertainty, safety_others, interval_length, max_nan_density, min_data_in_interval, max_gap_in_interval, min_intervals_in_output) prevents duplicate rows. accuracy_level is excluded (reporting-only) and safety_combined is excluded (derived from the three component safety factors).

  • Writing: split_output_into_day_rows splits a full-range output into midnight-aligned day slices; persist_day_output saves each one, catching IntegrityError from the unique constraint (concurrent duplicate → skip).

  • Reading: get_cached_day_outputs queries for rows whose datetime_eval_start and datetime_eval_end exactly match a single midnight-to-midnight day boundary.

This means a request for Jan 5–15 does not store or retrieve a single “Jan 5–15” row. Instead, 11 individual day-rows (Jan 5->6, Jan 6->7, …, Jan 15->16) are looked up independently; cached days are reused and only missing and partial days are computed fresh.

Example – day splitting#

Request: Jan 3 14:00 -> Jan 7 10:00
├── leading partial:  Jan 3 14:00 -> Jan 4 00:00   (always recomputed)
├── full days:        Jan 4->5, Jan 5->6, Jan 6->7   (unit of caching)
└── trailing partial: Jan 7 00:00 -> Jan 7 10:00   (always recomputed)

Full days are midnight-to-midnight in the plant’s local timezone.

Level 2 steps#

  1. split_request_into_day_segments: split into full days + partial edges.

  2. get_cached_day_outputs: look up cached rows for each full day, matched by signature (plant, method, formula, wind, settings via CACHE_MATCH_COLUMNS).

  3. build_day_reuse_plan -> DayReusePlan: cached days, missing days, partial edges.

  4. compute_missing_day_segments: run run_power_check only for uncached full days and partial edges.

  5. persist_day_output: save each freshly computed full-day output to the DB.

  6. assemble_power_check_output: merge cached + fresh intervals chronologically (fresh wins on overlap),

    recompute summary stats.

Cache invalidation#

invalidate_power_check_cache_for_range is called from files.py after data upload; it deletes cached rows overlapping the new data.

Algorithm versioning#

Each PowerCheckOutput row stores algorithm_version, set to POWER_CHECK_ALGORITHM_VERSION (from __init__.py) at creation time. Cache lookups only return rows matching the current version; stale rows are deleted lazily before persisting new results (handles both upgrades and downgrades).

When to bump: increment POWER_CHECK_ALGORITHM_VERSION when any change to files in core_methods/power_check/ or core_methods/virtuals/ affects computed outputs. A CI job warns on MRs that touch these files without a version bump. Use [power-check-no-version-bump] in a commit subject line to silence the warning for non-output-affecting changes (docstrings, logging, imports, etc.). The CI job only inspects commit subjects (git log --format=%s), so the tag must appear there, not in the commit body.

Functions

assemble_power_check_output(eval_start, ...)

Assemble a runtime merged PowerCheckOutput from cached and fresh segment outputs.

build_day_reuse_plan(session, eval_start, ...)

Build a day-based reuse plan for a request interval.

compute_missing_day_segments(plant, ...[, ...])

Compute fresh Power Check outputs for missing day segments only.

get_cached_day_outputs(session, plant_id, ...)

Lookup cached PowerCheckOutput rows for full-day ranges.

invalidate_power_check_cache_for_plant(...)

Delete all persisted Power Check outputs for one plant.

invalidate_power_check_cache_for_range(...)

Delete persisted Power Check outputs for one plant overlapping [start, end).

make_persistable_power_check_output_copy(...)

Build isolated ORM graph copy for DB persistence only.

persist_day_output(day_output, sess, crd)

Persist a single day-output to the DB.

split_output_into_day_rows(output, plant_tz)

Split a full-range PowerCheckOutput into per-day outputs for caching.

split_request_into_day_segments(eval_start, ...)

Classes

DayReusePlan(leading_partial, ...)

MissingDaySegmentOutputs(leading_partial, ...)

TimeRange(start, end)