Skip to content

Faster, more robust processes: shared feed I/O, base classes, tests and bug fixes - #39

Open
TrystanLea wants to merge 5 commits into
masterfrom
development
Open

TrystanLea wants to merge 5 commits into
masterfrom
development

Conversation

@TrystanLea

Copy link
Copy Markdown
Member

Reworks how post processes read and write feeds. Most processes now share block-based feed access and a few base classes, so each one is only its calculation. Adds a test suite that needs no database or redis, and fixes a number of bugs the tests found, several of which silently corrupted output data.

Speed and memory

Measured on synthetic data, old vs new; outputs are identical apart from the bug fixes listed below.

Process Data Before After
mergefeeds 1M points 1.03s 0.06s
importcalc 1M points 0.93s 0.07s
liquidorairflow_tokwh 1M points 1.33s 0.12s
basic_formula (f1-0.5*f2+max(f3-21,0)) 1M points 3.14s 0.30s
scalefeed 3M points 0.40s 0.11s
carnot_cop_simulator 1 year of 10s data 7.8s, 51 MB 2.6s, 3 MB
batterysimulator 1 year of 10s data 6.2s, 228 MB 4.5s, 8 MB
solarbatterykwh 1 year of 10s data 1.8s, 3 MB 1.6s, 3 MB

Memory no longer grows with the amount of data processed. Previously batterysimulator held all 18 outputs in memory until the end, which would need over 1 GB to recalculate several years of data.

Structure

  • FeedReader / FeedWriter (common.php): block reads and writes, resampling onto a common timeline, checked writes (a full disk is an error rather than silent truncation), meta written with the data, redis last time/value set to the last datapoint.
  • resolve_start_time(): one implementation of recent / from / all.
  • Base classes:
    • MapProcess (input → output) and InPlaceProcess (modify a feed in place): implement value($x).
    • CombineProcess (several inputs → output, optionally accumulating): implement row($a, $b, ...).
  • ModelHelper keeps the same API, rebuilt on the reader/writer.
  • New process: backfillfeed. Extends a feed that has stopped recording from another feed recording the same quantity. It only ever appends after the target's current end.

Bug fixes

  • powertokwh / accumulator: a value outside the min/max limit was dropped instead of written, shifting all later output one interval earlier.
  • basic_formula:
    • a formula using f1 and f12 never read feed 12;
    • "from" mode appended a second copy of the output;
    • division by zero gave 0 (now a missing value);
    • f1*0 was evaluated as f1*1.
  • "from" mode past the end of the output left a 0.0 gap. "Reprocess all" left stale data beyond the end of the input.
  • carnot_cop_simulator: the first run shifted every output one interval later; each later run counted the step it continued from twice.
  • batterysimulator: each run counted the step it continued from twice (state of charge and all kWh totals), and off-peak charging stopped at every run boundary.
  • solarbatterykwh: the 12 hour backcast counted one 10s step twice on every run.
  • solarbatterykwh, batterysimulator, carnot_cop_simulator: a run starting inside a gap held 0 instead of the last valid value.
  • Other:
    • solardirectcalc updated redis using a setting that doesn't exist;
    • constantflow / liquidorairflow ignored the process mode;
    • combine processes overwrote the output meta in recent mode;
    • to_signed always reported 0 datapoints modified.

Behaviour changes to be aware of

  • Finer inputs are averaged, not sampled. Where a combine process has inputs with different intervals, each output interval gets the average of the finer feed's values in it, not one sample. Only complete output intervals are written.
  • "Reprocess all" replaces an output feed whose start time or interval doesn't match. "recent" and "from" refuse with an error.
  • Redis time is the timestamp of the last datapoint, not one interval after it.
  • Existing outputs written by the old code keep the old errors until reprocessed: carnot_cop_simulator outputs are shifted, and basic_formula outputs from formulas that divide by zero or multiply by 0 have the old values. The simulators have no "reprocess all" mode, so their output feeds need clearing and the process running again.

Testing

php tests/run.php runs 86 tests against synthetic feeds in a temporary directory:

  • Existing behaviour: each converted process was checked against the old code before conversion, and the intended changes above are covered by tests that fail on the old code.
  • Simulator snapshots: single 100k-step runs (spanning a summer time change) recorded from the old code; the new code matches them exactly.
  • Incremental runs: for the simulators, processing in several runs must give the same output as one run.
  • basic_formula: compared byte for byte with a per-datapoint reference evaluation on random data with gaps and zeros.

Not yet converted: removenan, average, downsample, upsample.

🤖 Generated with Claude Code

TrystanLea and others added 5 commits October 9, 2026 23:03
- tests/run.php runs process tests against synthetic feeds, no database
  or redis required, with tests for existing processes and common code
- FeedReader/FeedWriter in common.php read and write feeds in blocks,
  check writes, resample to a common timeline and update redis
- resolve_start_time() implements recent/from/all in one place
- Runner reports exceptions thrown by a process as a failed process
- Fix interval setting validation using $processitem->key
- backfillfeed uses FeedReader/FeedWriter, rejects an empty target

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Simple value by value processes now only implement value($value):
- MapProcess: scalefeed, offsetfeed, allowpositive, allownegative
- InPlaceProcess: to_signed, limit_morethan_lessthan,
  remove_morethan_lessthan, removenan_lastvalue

Fixes:
- from mode past the end of the output left a 0.0 gap
- reprocess all left stale data beyond the end of the input
- redis time is the last datapoint, NAN is never set as last value
- to_signed always reported 0 datapoints modified
- remove_morethan_lessthan setting label typos

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Converted: addfeeds, mergefeeds, importcalc, exportcalc, solardirectcalc,
powertokwh, accumulator, constantflow_tokwh, liquidorairflow_tokwh and
basic_formula. Inputs are read in blocks, 11-17x faster for two and three
input processes. Inputs with a shorter interval are averaged into each
output interval and only complete output intervals are written.

Fixes:
- powertokwh/accumulator: filtered values shifted later output data
- basic_formula: feed ids that are prefixes of others (f1, f12) not read
- basic_formula: from mode appended a duplicate copy of the output
- solardirectcalc: redis update used a non existent setting
- constantflow/liquidorairflow ignored the process mode
- combine processes overwrote output meta in recent mode

Reprocess all now replaces an output feed with a different start time or
interval. Removes unused compute_meta, bfo takes values rather than
file handles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ModelHelper reads inputs and writes outputs in 8192 datapoint blocks
rather than one at a time and keeping all output in memory. On a year
of 10s data: carnot_cop_simulator 7.8s to 2.6s, 51MB to 3MB;
batterysimulator 6.2s to 4.5s, 228MB to 8MB; solarbatterykwh unchanged
at 3MB and slightly faster.

Fixes, tested by processing in several runs giving the same output as
a single run:
- carnot_cop_simulator: first run shifted outputs one interval later,
  each run counted the step it continued from twice
- batterysimulator: each run counted the step it continued from twice
  in state of charge and kWh totals, off-peak charging stopped at run
  boundaries (now restored from the stored state of charge)
- solarbatterykwh: the 12 hour backcast counted one step twice
- runs starting in a gap hold the last valid value from before the run

batterysimulator works out the local hour only when it changes. Tests
add snapshots of single runs from the previous code, which the new code
matches exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parsed formula is evaluated one operation at a time over a block of
datapoints rather than for each datapoint separately, 5-7x faster on
1M datapoints. A test compares the output byte for byte with a per
datapoint reference evaluation on random data with gaps and zeros.

Fixes:
- division by zero gives a missing value rather than 0
- a literal 0 is kept, f1*0 was evaluated as f1*1
- result message no longer converts a NAN last value to a string

Parsing is split out into parse_formula(), ftoa() moves from common.php
to basic_formula.php and bfo() is replaced by the block evaluator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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