Skip to content

v0.7.0 — generalized shift splitting (--split-by shifts) - #3

Merged
GrumpyTanker merged 4 commits into
mainfrom
feat/generalized-shifts
Jun 8, 2026
Merged

v0.7.0 — generalized shift splitting (--split-by shifts)#3
GrumpyTanker merged 4 commits into
mainfrom
feat/generalized-shifts

Conversation

@GrumpyTanker

Copy link
Copy Markdown
Owner

Generalized, named, configurable shift periods

Lets users define multiple named time-of-day windows (that may wrap midnight) and compare power usage across them — e.g. day vs night, or A/B/C shifts.

  • --split-by shifts + --shifts "day=06:00-18:00,night=18:00-06:00" (any count; end<=start wraps midnight) or --shifts-file FILE (JSON). Defaults to day/night.
  • Windows evaluated in --tz (UTC fallback, stated in output). Gap/overlap warnings; first-match wins; non-matching → unassigned.
  • Headline output shift_comparison.{csv,json} (per shift: kWh, P avg/min/max, peak demand, PF, V L-N + p5/p95, V THD p95, outage/dip/swell counts, outage minutes) + per-occurrence buckets (a midnight-spanning night = one occurrence by start date) + a table in summary.txt.
  • Python↔JS parity (parse, midnight wrap, tz-localized assignment, aggregate, occurrences, comparison) on a shared golden fixture.

Tests: Python 251 · Web 120, green. Validated on the real ES.004 session (day/night land on 06:00/18:00 Central — no UTC skew).

🤖 Generated with Claude Code

Bill Bai and others added 4 commits June 3, 2026 21:19
…/occurrence/comparison

Add the core generalized shift-splitting model to analysis.py:
- Shift / ShiftSet with parse(), from_spec(), default(), HH:MM validation,
  duplicate-name + zero-length-window rejection, and wrap-past-midnight
  windows (end <= start).
- ShiftSet.coverage_issues() warns on gaps/overlaps in the 24h tiling.
- gather_store(store, indices): slice_store sibling over non-contiguous index
  lists (a named shift recurs daily).
- aggregate_shifts(): group record indices by shift NAME, evaluated in the
  REPORT timezone (localize before applying HH:MM rule). First matching window
  wins; non-matching records -> 'unassigned'.
- shift_occurrences(): contiguous per-instance buckets; a midnight-spanning
  night is ONE occurrence labeled by its start date.
- shift_comparison_rows() + _shift_row(): the headline per-shift aggregate
  (records, hours, kWh, P avg/min/max, peak rolling demand, PF avg, V_LN
  avg/p5/p95, V_THD p95, event counts, outage minutes).

20 new tests cover parsing, wrap logic, tz-localized assignment, non-contiguous
aggregation, gap/overlap/unassigned, occurrences (incl. midnight span), and
comparison-row schema/values. Full suite: 243 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New shifts_file.py loader (JSON {shifts:[{name,start,end}]} or bare list),
  mirroring the --rules-file pattern.
- CLI: --split-by shifts routes to _run_shifts; --shifts SPEC (inline) and
  --shifts-file FILE; default day=06:00-18:00,night=18:00-06:00 when neither
  given. Windows evaluated in --tz (UTC if unset; printed in output).
- _run_shifts emits the two required outputs:
    1. shift_comparison.csv + .json (headline per-shift aggregate;
       tz/spec/coverage_issues/demand_window in the JSON header),
    2. per-occurrence contiguous buckets under <out>/shifts/<name_date>/
       (session.csv, events.json+ITIC, summary.txt), reusing per-bucket
       machinery; midnight-spanning occurrences are one bucket by start date.
- Coverage gap/overlap warnings printed to stderr.
- Shift-comparison table embedded in summary.txt.

Validated read-only on the real ES.004 session with --tz America/Chicago:
day/night split lands exactly on 06:00/18:00 Central (43200 rec each per day).

Tests: 6 CLI E2E (default, boundary-cross, tz-central, shifts-file, 3-shift,
summary-table) + 3 shifts_file loader. Full suite: 251 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Port the shift model to web/analysis.js: Shift, ShiftSet (parse/fromSpec/
  default/coverageIssues), localMinuteOfDay (Intl-based, matching tzutil),
  aggregateShifts, shiftOccurrences, shiftComparisonRows. tz contract mirrors
  Python: minute-of-day is localized to the report tz BEFORE the HH:MM rule.
- Extend the golden generator (test_analysis_parity_golden.py) with a
  deterministic 3-day minute-resolution session: emit comparison_utc,
  occurrences_utc, and a tz-localized A/B/C-in-America/Chicago comparison.
- New web/tests/shifts_parity.test.js recreates that exact session and asserts
  aggregate grouping, occurrences row-for-row, comparison rows (UTC + tz),
  parse-error handling, and coverage gap/overlap — within float tolerance.
- Fix: shiftComparisonRows passed an already-wrapped source into
  aggregateShifts; pass the original source so asColumnSource resolves once.

Tests: py 251 passed; web 120 passed (6 new parity subtests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… bump

- New docs/SHIFTS.md (format, 2-shift + 3-shift examples, tz contract, output
  schema, parity note).
- README options table: --split-by gains 'shifts'; add --shifts / --shifts-file.
- CHANGELOG 0.7.0 entry; ROADMAP 'Shipped in v0.7' section (shift slice of the
  comparison-polish theme).
- Bump version 0.6.0 -> 0.7.0 (pyproject.toml + __init__.py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@GrumpyTanker
GrumpyTanker merged commit 506baad into main Jun 8, 2026
2 checks passed
@GrumpyTanker
GrumpyTanker deleted the feat/generalized-shifts branch June 8, 2026 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant