Skip to content

v0.6.0 — web parity + analysis depth (Features A–I) - #2

Merged
GrumpyTanker merged 11 commits into
mainfrom
feat/large-session-v2
Jun 4, 2026
Merged

v0.6.0 — web parity + analysis depth (Features A–I)#2
GrumpyTanker merged 11 commits into
mainfrom
feat/large-session-v2

Conversation

@GrumpyTanker

Copy link
Copy Markdown
Owner

Round 2 — web parity + standards-grade analysis

Closes the web memory/parity gap from v0.5.0 and adds six analysis features. All built test-first with Python↔JS parity. Tests: Python 223 ✓ · Web 114 ✓.

Web streaming + parity (A, B)

  • Streaming columnar web parse (column_store.js, parseTrendColumnarStream) — 7-day file (589,877 recs / 438 MB) parses + fully analyses at ~278 MB peak RSS vs old ~1.6 GB. Typed-array columns streamed via 8 MB Blob.slice and Transferred from the worker.
  • web/analysis.js — full JS port of analysis.py (Welford moments, percentile sketch, whole-session stats, ITIC, time-of-day, marker correlation). Stats panel + ToD chart in the UI; stats embedded in HTML export.
  • Fixed latent Math.max(...arr) stack overflow that would crash on ~590K-element sessions.

CT-reversal auto-detection (C)

  • --auto-reverse-cts flags reversed-CT installs (real power negative ≥50% of non-outage time) and applies --reverse-cts with a loud notice; web banner + one-click apply. Flags the real ES.004 (52% negative P, mean −37 kW). Python + JS.

Multi-session stitching (D)

  • fluke-analyze stitch S1 S2 … -o OUT — gap-aware continuous timeline with per-source provenance (beats the meter's 7-day cap). Validated ES.001+ES.002 → 79,897 recs, 802 s gap detected.

Executive summary (E)

  • Deterministic rule-based auto-narrative (narrative.md + top of summary.txt/HTML/XLSX). Python + JS parity.

Power-quality standards (F)

  • IEEE 519 voltage-THD compliance per phase + IEEE 1159 / SARFI-90/80/70/50/10 indices. docs/PQ_STANDARDS.md.

Demand, timezone, per-asset rules (G, H, I)

  • Rolling 15-min peak demand (--demand-window); timezone-aware anchors/reports (--tz); per-asset threshold overrides (--rules-file).

🤖 Generated with Claude Code

Bill Bai and others added 11 commits June 2, 2026 23:22
Add web/column_store.js (JS port of python store.ColumnStore) holding ~26
analysis channels as packed Float32Array columns + Float64Array timestamps
instead of 590K record objects (~1.6 GB -> ~55 MB on the 7-day file).

Add parser.js:
- parseTrendColumnar: one-shot ArrayBuffer -> Transferable typed-array columns
- parseTrendColumnarStream: chunked Blob.slice path so the full 438 MB
  ArrayBuffer is never resident; record-aligned 8 MB chunks
- decodeColumnarSlice / allocColumns helpers

Wire parser_worker.js to support parse-stream (Blob) and columnar parse,
transferring the typed-array buffers back zero-copy. Legacy parseTrendBin
path retained for small-file/CSV flows and existing tests.

Tests: web/tests/columnar.test.js asserts record count + value parity with
the legacy parse, streaming==one-shot, bounded per-chunk reads, and
ColumnStore round-trips. Web suite 82 -> 87 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
App now streams the dropped File through the worker (parse-stream), holds a
resident ColumnStore (~62 MB) instead of 590 K record objects, and runs event
/ snapshot / insight detection + charts + range + tariff straight off the
store. currentRecords stays null on the streaming path; exports materialise a
transient records array via recordsForExport() and drop it.

Make the analysis engines store-aware via web/column_source.js:
- events.js, snapshots.js, insights.js accept a ColumnStore OR a records array
- plots.js buildPlotData reads columns from a store (charts memory-bounded)
- range_select.js renderRangeSelector + tariff.js computeCost read the store

Fix a latent stack-overflow: ruleCurrentSpikeRatio / ruleBreakerMargin used
Math.max(...valid) which blows the call stack on a ~590 K-element session;
replaced with a loop-based arrayMax. This would have crashed the web app on
the 7-day file.

Validated headlessly on the real ES.004 (438 MB, 589,877 recs): streaming
parse + full analysis peaks at ~278 MB RSS (vs the old ~1.6 GB), parses in
~0.5 s, analysis ~2 s, 18 events / 3 snapshots / 2 findings — all correct.

Tests: store==records equivalence for events/snapshots/insights added to
columnar.test.js. Web suite 87 -> 88 green; python 176 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add web/analysis.js mirroring python analysis.py numerically:
- RunningMoments (Welford), PercentileSketch (fixed-width histogram)
- wholeSessionStats (per-channel count/min/p1/p5/median/mean/p95/p99/max/stdev
  + under-voltage / over-current second accounting)
- classifyItic / eventItic (ITIC/CBEMA ride-through)
- parsePeriod / parseTodWindow / timeOfDayProfile
- correlateMarkers
All accept a ColumnStore or records array via column_source.

Surface in the web UI: a Statistics section (per-channel table + threshold
note) and a time-of-day profile chart, computed off the resident store. Embed
the whole-session stats table (and a narrative slot for Feature E) into the
exported HTML report.

Parity: python test_analysis_parity_golden.py emits a deterministic golden
JSON; web/tests/analysis_parity.test.js recreates the identical session and
asserts stats/ITIC/eventItic/ToD match within float tolerance (percentiles are
exact since both use the same sketch).

Web 88 -> 92 green; python 176 -> 177 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A load wired with backwards iFlex CTs reads as a persistent generator
(P_total < 0). Detect when real power is negative for >= 50% of non-outage
time (default threshold; non-finite P samples skipped) and emit a loud notice.

Python:
- analysis.detect_ct_reversal + ct_reversal_notice
- cli --auto-reverse-cts: when flagged and no explicit --reverse-cts, re-run
  the single-pass parse with reverse-CTs applied and report the corrected stats
- always print the notice when flagged; write ct_reversal.json for reports/web

Web (parity):
- analysis.detectCtReversal / ctReversalNotice (identical algorithm)
- a dismissable banner in the summary with an "Apply Reverse CTs" button that
  ticks all phase boxes and re-parses

Validated on the real ES.004: flagged True, 52.05% of non-outage time negative,
mean P = -37.0 kW (the raw load reads as export until the CTs are flipped).

Tests: python test_ct_reversal.py (5) + golden CT entry; web parity test.
Python 177 -> 182; web 92 -> 93 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fluke-analyze stitch S1 S2 ... -o OUT concatenates consecutive sessions into
one continuous timeline that beats the meter's 7-day cap, then runs the normal
analysis over the stitched series.

Python:
- stitch.py: stitch_stores() sorts labelled ColumnStores by start time,
  concatenates retained columns + absolute ticks, records boundary gaps beyond
  a tolerance (no synthetic fill rows), and carries per-source provenance
  (label, record range, time span).
- cli_stitch.py: the `stitch` subcommand (sibling to `compare`) — parses each
  input to a store, stitches, writes stitch.json provenance, events/insights/
  stats over the stitched series, and a session.csv with a `source` column.
- wired into cli.main dispatch.

Web:
- multi_session.js: stitchStores() (JS port) + MultiSession.buildStitched()
  for a stitched timeline alongside the existing compare overlay.

Validated on the real consecutive captures ES.001 (5,024 recs) + ES.002
(74,873 recs): stitched to 79,897 continuous records with a correctly detected
801.8 s gap; full analysis ran clean over the stitched series.

Tests: python test_stitch.py (8, incl. CLI e2e); web multi_session stitch
parity (5). Python 182 -> 190; web 93 -> 98 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rule-based, deterministic plain-English summary (no LLM) built from events +
insights + whole-session stats + the CT-reversal check: scope, headline event
(worst outage/dip/swell with leading-dip context), PF/imbalance, a CT-reversal
warning when flagged, and a bottom line.

Python:
- narrative.py: build_narrative() + narrative_markdown()
- cli writes narrative.md, prepends the summary to summary.txt, and threads it
  into the HTML report (Executive summary section) and the XLSX Summary sheet.

Web (parity):
- narrative.js (identical prose) + an Executive summary section in the UI, and
  the narrative embedded at the top of the exported HTML report.

Parity: golden narrative built from fixed events/findings/stats/ct in the
Python generator; web/tests asserts buildNarrative matches it character-for-
character.

Tests: python test_narrative.py (6); web narrative parity (2).
Python 190 -> 196; web 98 -> 100 green.

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

Python (analysis.py):
- ieee519_compliance: per-phase voltage-THD compliance assessed at p95 against
  the IEEE 519-2014 <=1 kV limits (8% total, 5% planning); current THD reported
  as p95 (informational, no hard limit without Isc/IL). all_voltage_compliant
  rolls up the three phases.
- sarfi_indices: SARFI-90/80/70/50/10 counts of voltage events (dips+outages)
  below each residual-voltage threshold (IEEE 1159 / 1564).
- cli writes pq_standards.json + a one-line [pq] summary.

Added the six THD avg channels to the Python store STORE_COLUMNS (they were
already in the JS store) so IEEE 519 can read them; no spec change.

Web (parity): ieee519Compliance + sarfiIndices in analysis.js, surfaced in the
Statistics panel and the exported HTML report.

docs/PQ_STANDARDS.md explains the limits, the p95 assessment method, the SARFI
bins, and the caveats.

Parity: golden ieee519/sarfi entries; web parity asserts both match Python.

Tests: python test_pq_standards.py (5); web parity (2).
Python 196 -> 201; web 100 -> 102 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sliding-window average real-power demand (default 15 min, --demand-window),
reporting peak demand, the window it occurred in, mean demand, and an optional
decimated demand series. Non-finite P samples treated as 0.

Python: analysis.demand_analysis; cli --demand-window writes demand.json + a
one-line [demand] summary and adds Peak demand rows to the XLSX Summary sheet.

Web (parity): analysis.demandAnalysis, surfaced in the Statistics panel and the
exported HTML report.

Parity: golden demand entry on a deterministic ramp (P = i*100 W over 600 s,
120 s window); web parity asserts peak/mean/series match (timestamps compared
as instants — JS emits Z, Python +00:00).

Tests: python test_demand.py (5); web parity (1). Python 201 -> 206; web
102 -> 103 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Timestamps stay UTC throughout; with --tz ZONE (IANA) reports additionally
render local wall-clock alongside UTC. Default (no --tz) is unchanged: UTC only.
Anchors already honoured explicit ISO offsets — --tz is the display complement.

Python: tzutil.py (resolve_tz / to_utc / format_local_utc / tz_label using
zoneinfo); cli --tz flag (fails fast on a bad zone) adds a local+UTC Time range
block to summary.txt.

Web (parity): tzutil.js (formatLocalUtc / isoInZone / isoUtc via Intl, matching
Python isoformat incl. dropping zero-millisecond fractions); a report-timezone
input under the summary (persisted) that renders the local+UTC range and is
passed into the exported HTML report's Time range header.

Parity: golden timezone entry (fixed instant in UTC + America/Chicago);
web/tests/tzutil.test.js asserts both render identically.

Tests: python test_timezone.py (9); web tzutil parity (5).
Python 206 -> 215; web 103 -> 108 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Override EventRules thresholds from a JSON/TOML file, keyed by asset_id/name so
one file can hold a whole fleet's known trip points. Precedence: built-in
defaults -> file [defaults] -> file [assets.<name>] (or [assets.default]).
Unknown keys are rejected; min/gap keys coerced to int, the rest to float.

Python: rules_file.py (load_rules / describe_overrides, JSON + TOML via
tomllib); cli --rules-file resolves rules for the session's asset, prints a
one-line [rules] diff, and threads them into detect_events.

Web (parity): events.rulesFromObject (same precedence + validation); a
rules-file picker in the summary that loads JSON, re-runs detection for the
active asset, and a Clear button to revert to defaults.

docs/RULES_FILE.md documents the format + every overridable key.

Tests: python test_rules_file.py (8, incl. a CLI e2e where a 95% dip threshold
flags a 93.9% dip the defaults miss); web events rules-file (6, incl. the same
detection-change check). Python 215 -> 223; web 108 -> 114 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump version to 0.6.0 (pyproject + __init__). Add the v0.6.0 CHANGELOG entry
covering Features A-I. Update ROADMAP: new "Shipped in v0.6" section, mark the
deferred v0.5 web streaming/typed-array items DONE. README: new CLI flag rows
(--demand-window, --tz, --auto-reverse-cts, --rules-file), stitch subcommand,
and a power-quality/per-asset-rules section. New docs: STITCHING.md, DEMAND.md
(PQ_STANDARDS.md and RULES_FILE.md landed with their features).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@GrumpyTanker
GrumpyTanker merged commit d3dc456 into main Jun 4, 2026
2 checks passed
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