Skip to content

Releases: yroussev/camber

CAMBER v0.105.0

Choose a tag to compare

@github-actions github-actions released this 17 Oct 17:31

0.105: faithful CalTRACK 2.0 Daily, Billing and Hourly methods, an M&V names registry with the industry-standard names, a TOWT matching LBNL RMV2.0, sturdier M&V data handling, pump drift on hot-water loops, and four synthetic catalog entries with workbook exercises (#144–#149, #151–#180). Renames (#156, #163): mandv.caltrack.caltrack_savings and caltrack_savings_hourly were not CalTRACK-comparable and are now daily_changepoint_savings and towt_savings_hourly; fit_degree_day / DegreeDayModel are now fit_mean_temp_degree_day / MeanTempDegreeDayModel. The old names keep working with a DeprecationWarning until 1.0 and return the same numbers. The caltrack_daily_savings, caltrack_billing_savings and caltrack_hourly_savings functions implement the published methods and match eemeter 2.10 where asked (#156, #166, #167, #170, #171). Reports, legends and the dossier name models by their registry labels (#163, #174, #179). TOWT gains an unseen-bin fallback, optional non-negative predictions, fixed knots and recency weighting, and an RMV2.0 variant that matches the R reference to 1e-10 (#154, #155, #162, #165, #176–#178). M&V numbers can move on messy data: daily frames drop partial days, the fall-back DST hour is no longer double-counted, and implausible temperatures are screened at fit and prediction time (#157–#159, #168, #169); clean data gives the same numbers. best_model and ModelFitError say why no model could be fit (#160, #172, #173, #175), and seasonal_forecast warns on long horizons (#161). On the FDD side, the pump drift family runs on hot-water loops (#152), the pump DP-reset rules judge only running hours (#180), lbnl-chiller's pump rules read pump data (#164), the trust score checks the mixed-air order without airflow stations (#151), and plant-rule context is sharper (#148, #149, #153). Four synthetic entries (G36 VAV fleet, two-chiller plant, AHU maintenance, heat pumps) bring workbook exercises (#144–#147); answers in plant-chw-reset-pumping and plant-sensor-vs-equipment are re-recorded on running hours (#180).

Added

  • mandv.intervalfit.day_coverage (each day's covered share) and day_hours (each day's
    length on the site's clock).

  • The CalTRACK 2.0 Daily, Billing and Hourly methods (#156). mandv.caltrack_daily and
    mandv.caltrack_hourly implement the published methods, rule by rule and with every constant
    cited:

    • caltrack_daily_savings, caltrack_billing_savings and caltrack_hourly_savings, each
      returning the fitted model with a SavingsResult in a CalTRACKResult;
    • the fit_caltrack_* functions;
    • CalTRACKDailyModel, CalTRACKBillingModel and CalTRACKHourlyModel, each with
      predict, as_dict and from_dict, so a baseline can be stored and projected later.

    Daily and Billing apply CalTRACK's data sufficiency, usage-per-day and temperature rules, its
    billing-period rules and its balance-point grid. They fit intercept-only, HDD, CDD and HDD+CDD
    candidates, qualify them on degree days and signs, select by adjusted R² with eemeter's tie
    order, and report CalTRACK's fractional savings uncertainty. Hourly fits one model per
    calendar month, weighting the months either side at 0.5. Its occupancy comes from a
    preliminary model's residuals, and its six fixed temperature bins are merged where sparse. An
    hour of the week a month never saw predicts NaN and is counted.

    Defaults follow the CalTRACK documents. eemeter_compat=True reproduces eemeter 2.10 where
    they differ, except eemeter 2.10's bug on unseen hours. A dev-only cross-check
    (tests/test_caltrack_eemeter_xcheck.py, skipped without a separate eemeter 2.10.11
    environment) matches eemeter to 3e-12 on synthetic cases. See docs/MANDV.md, "CalTRACK 2.0
    methods".

  • An M&V names registry (#163). camber.mandv.names gives every model kind, degree-day kind,
    savings method and M&V function one row: its CAMBER code, the label reports show, the
    industry-standard name, the citation, the implementing function and the known differences from
    the standard (mv_name, label, model_label, citations). The "Method correspondence"
    table of docs/MANDV.md is generated from it (scripts/mv_names_doc.py, checked by the tests
    and scripts/gates.sh), and a new "Naming and citations" section says how to cite a model by
    its standard name and how to describe an ensemble by its components.

  • M&V citations in camber.references: ASHRAE Guideline 14-2014, the RP-1050 Inverse
    Modeling Toolkit (Kissock, Haberl and Claridge 2003), PRISM (Fels 1986), TOWT (Mathieu, Price,
    Kiliccote and Piette 2011), Granderson et al. 2015 (LBNL-187596), the LBNL M&V test-method
    reports LBNL-187225 and LBNL-6963E, CalTRACK 2.0, IPMVP Core Concepts 2022 and the DOE SEP
    50001 M&V Protocol 2019. New kinds PAPER and STANDARD, MV_REFERENCES, the Reference
    fields authors, year, venue and doi, and Reference.citation(). docs/REFERENCES.md
    lists them.

  • mandv.degreeday.fit_mean_temp_degree_day and MeanTempDegreeDayModel, the honest names of
    the period-mean-temperature degree-day model (see Deprecated).

  • best_model says why no model could be fit (#160). It raised a bare ValueError("no model could be fit"). It now raises mandv.models.ModelFitError, a ValueError, whose message
    lists every kind tried with its parameter count and why it was refused: the fit's own error,
    n - p < 1 with the counts, or a non-finite BIC. The list is also on .candidates.
    best_model(..., return_candidates=True) returns (model, candidates), with each fitted
    kind's n, sse, bic and selected. The choice of model is unchanged. The billing
    selector _mvform.best_billing_model raises the same ModelFitError when neither the
    change-point kinds nor the degree-day model can be fit, listing each with its reason.

  • seasonal_forecast warns past its horizon (#161). The forecaster is built for next-day
    to next-week use. A new max_horizon= (default 14 days, None to turn it off) issues a
    forecast.ForecastHorizonWarning when the latest target lies further than that past the end
    of history. The same warning, with the count, says when target intervals take the overall
    mean because history never saw their time-of-week slot. The forecast is still returned.
    backtest passes max_horizon through, so a hold-out longer than 14 days warns too.
    docs/FORECAST.md documents the intended horizon.

  • TOWT can predict hour-of-week bins the fit never saw, on request (#154).
    TOWTModel.predict(..., unseen="fallback") fills such a row from the seen bin of the same hour
    and occupancy class on the nearest day of the same day type, or else from the mean of that
    hour's seen bins (NaN when the hour was never seen); return_counts=True counts each step.
    TOWTAtIndex(unseen=), _design.design_rows(unseen=) and
    towt_savings_hourly(unseen=) carry it through, avoided_energy_savings caveats the count,
    and coverage grades the filled rows unsupported. The default still raises.

  • Optional non-negative predictions (#155). clip_negative=True on TOWTModel.predict,
    TOWTAtIndex, towt_savings_hourly, ChangePointModel.predict and
    MeanTempDegreeDayModel.predict sets negative predictions to zero after X @ beta and counts
    them; the uncertainty stays on the unclipped projection. Off by default.

  • A synthetic catalog entry for a G36 VAV fleet (#144). synthetic-g36-vav-fleet is a VAV
    air handler with six single-duct reheat boxes under Guideline 36 trim-and-respond SAT and
    duct-static resets, hourly for three weeks of spring, as eight air handlers: a healthy fleet,
    a rogue zone dragging the SAT reset, a blocked box holding duct static up, a capped fan
    starving every box, a reset that ignores its requests, single-maximum reheat, overlapping
    heating and cooling setpoints, and a warm zone that is not quite a rogue. Each air handler
    trends its setpoints with the reset requests its boxes sent; each box its space temperature
    and setpoints, airflow and minimum, damper and reheat valve. It is generated by CAMBER
    (generator g36_vav_fleet, seed 144), Apache-2.0, open, labelled and about 0.9 MB, with two
    mappings and a run template that declares the served-by topology. No open dataset in the
    catalog trends reset requests, so nine G36 zone-fleet rules had no case to fire on:
    sat_/static_reset_effectiveness, static_rogue_zone_census,
    sat_/static_cohort_starvation, cohort_airflow, cohort_space_temp,
    reheat_minimization_g36 and zones_heat_cool_census. The workbook exercise
    zone-g36-fleet comes with it, with an instructor key, a figure and answers pinned by the
    test suite, and four variants that judge one air handler each. camber.fleetlab now draws its
    setpoint noise from the integer hash the synthetic entries use (moved to
    camber._hashnoise), so its fleets are the same bytes on every machine; the fleet_fdd
    benchmark is unchanged.

  • TOWT with fixed knots, weights and the LBNL reference settings (#162).
    fit_towt(..., knots=, knot_min_obs=, weights=): fixed temperature knots with unbounded outer
    segments (Mathieu et al. 2011, Table 3), RMV2.0's outer-knot pruning, and weighted least
    squares. towt_lbnl_config() returns the settings verified against LBNL's RMV2.0 1.1.0
    (knots 40, 55, 65, 80, 90 °F, pruning under 20 observations, zero clip, recency weight
    s² / (s² + Δt²) with s = 14 days, lbnl_time_weights); RMV2.0's occupancy rule and its
    ensemble of local fits are not part of fit_towt but of fit_towt_rmv2 (#165), and the
    registry's TOWT row points there. Models with
    these options serialise open_ends / weighted; older stored models load unchanged.
    camber.references adds lbnl-rmv2.

  • clip_negative on the CalTRACK predicts (#171). CalTRACKHourlyModel.predict, and the
    Daily and Billing predicts...

Read more

CAMBER v0.104.0

Choose a tag to compare

@github-actions github-actions released this 17 Oct 16:46

0.104: a safer camber serve, store locks on every write, store verify/normalize, coarse-trend caveats for start counts, pump drift gates that scale to the pump, and two synthetic plant cases (#134–#139, #141, #143, #150). Behaviour change (#134): camber serve now refuses cross-site browser requests (403); curl, scripts and health probes are unaffected, but a link to the viewer from another site no longer opens it. Deprecation (#134): a future release, 0.105 at the earliest, will make --auth token the default on loopback; pass --auth token or --auth none now to choose. Every write to a plain store now takes its single-writer lock, and --lock-timeout / CAMBER_LOCK_TIMEOUT set the wait (#135). camber store verify and camber store normalize check and rewrite parts into the 0.103 canonical layout (#136). boiler_short_cycle, compressor_short_cycle and chiller_staging say when a trend is too coarse to count starts, and a resampled run status no longer hides short cycling (#143). The pump and loop drift detectors size their gates to the pump, so small and constant-speed pumps are checked instead of declined; full-size results are unchanged (#150). Two new synthetic catalog entries bring the plant-boiler-cycling and plant-chiller-staging exercises (#137). Also: README images render on PyPI (#138), a flaky test (#139) and a catalog-rebuild race (#141) are fixed.

Added

  • camber serve --auth none (#134) chooses no authentication explicitly: the same open
    server as leaving --auth out, without the notice below. auth="none" is the Python form and
    CAMBER_API_AUTH=none the python -m camber.api.server form.
  • A camber.api.server entry in docs/API-STABILITY.md (#134): its public names, their
    stability, and the planned change of the auth default.
  • --lock-timeout SECONDS and CAMBER_LOCK_TIMEOUT (#135) set how long a command waits for a
    store or dataset-cache lock that another process holds, on camber datasets fetch|ingest|remove,
    camber lab, camber interop openfdd ingest and camber store migrate-partitions. 0 fails at
    once, naming the holder. Without the flag, the environment variable sets the wait, else it is 30
    seconds as before. A value that is not a number of seconds >= 0 is refused (exit 2).
    camber retention apply takes --lock-timeout too, as the new name for --wait, which still
    works; its default stays 0. In Python: ParquetStore(root, lock_timeout=…), ParquetStore.lock()
    for holding the lock across a bulk load, lock_timeout= on ingest_package, and
    LabApp(lock_timeout=…).
  • camber store verify and camber store normalize (#136). verify reports, for a store or
    one facility (--facility), whether each part file is in the canonical layout of 0.103 (the
    camber.layout marker, no pandas metadata, one row group, rows sorted by the canonical key),
    with counts of canonical, legacy (pre-0.103, no marker), edge and rollup parts, and lists any
    part that fails to read. It takes no lock and changes nothing; --json; exit code 0 when every
    part is readable, 1 when one is not, or with --strict when one is not canonical. normalize
    rewrites the non-canonical parts, partition by partition, under the store lock
    (--lock-timeout). It is a dry run unless --apply --yes, with --reason inside a workspace,
    where it is audited as store.normalize and covers the rollup stores too. Each part keeps its
    name and exactly its rows (a rollup part keeps camber.rollup.covers). The new part is staged
    inside the partition, read back and compared, then moved over the original, so a crash at any
    point loses and duplicates no row and the next run finishes. It is idempotent, invalidates the
    catalog and reports the bytes before and after. It leaves alone canonical parts, edge parts,
    year-only parts (migrate-partitions rewrites those), unreadable parts and facilities that do
    not accept data or are under a legal hold, each with its reason, and refuses such a facility
    named with --facility. In Python: ParquetStore.verify() and ParquetStore.normalize().
  • The edge forwarder's parts stay content-addressed (#136), and docs/EDGE-DEPLOY.md says
    why.
    Canonical bytes would give a batch the forwarder re-sends after an upgrade a new key, so
    its rows would land twice; and the landing checks each name against the sha256 of its bytes.
    store verify counts edge parts on their own and store normalize leaves them as they are.
  • Two more synthetic catalog entries (#137), for plant faults no open dataset shows.
    synthetic-hw-plant-cycling is a hot-water plant, every 15 minutes for three weeks of early
    spring, as four plants: a well-tuned modulating boiler, an oversized on/off boiler that
    short-cycles, a boiler that cycles a little too often, and a loop with three-way valves and a
    constant-speed pump. synthetic-chw-plant-staging is one chiller and its cooling tower, every
    15 minutes for three summer weeks with a heat wave, as five plants: a well-staged chiller, an
    oversized chiller that short-cycles, a chiller left on around the clock, a tower held at a fixed
    85 °F and a tower reset with a high 80 °F floor. Both are generated by CAMBER (generators
    hw_plant_cycling and chw_plant_staging, seed 137), Apache-2.0, open, labelled, under 1 MB,
    with a mapping and a run template. Until now boiler_short_cycle, hw_plant_deltat,
    hw_pump_dp_reset, chiller_staging and condenser_water_reset had no open-data case where
    they flag a problem.
  • Workbook exercises plant-boiler-cycling and plant-chiller-staging (#137), with
    instructor keys, a figure each and answers pinned by the test suite on the generated data. The
    boiler exercise shows a short-cycling boiler's start count fall to a floor when the trend is
    resampled to an hour, and a warn that depends on the boiler's own starts-per-day limit
    (variants --hourly and --10starts). The chiller exercise separates the two ways chiller_staging fails, and shows a
    condenser-water reset that passes the check but saves little (variant --slope05). The
    workbook now has 22 exercises.
  • pumpsim sizes the pump and runs constant-speed pumps (#150). simulate_case and
    make_cases take design_flow_gpm (flow at 100 % speed, default 1000 gpm), which scales flow,
    power and their noise and leaves head, loop DP and ΔT alone, plus fixed_speed (a constant-speed
    pump with a PUMP_STATUS of 1) and speed_point=False (no speed column). The default pump's
    frames are byte-identical to 0.103's.
  • The pump drift family's parameters are documented (#150) in camber.rules.param_docs and
    docs/THRESHOLDS.md, under "Pump and loop drift detectors": every constructor parameter of
    pump_flow_drift, pump_head_drift, loop_deltat_drift, loop_dp_drift and
    pump_power_drift, with its unit, range, basis and how to calibrate it.

Changed

  • camber serve refuses cross-site browser requests (#134), as camber lab does. A request
    with Sec-Fetch-Site: cross-site (or an unknown value), or with an Origin that is not http://
    or https:// plus an allowed Host name, is 403 on every route and method, after the Host check
    and before the token check. same-origin, same-site and none pass. Requests with neither
    header pass, so curl, scripts, the image's HEALTHCHECK and the Kubernetes /health probes
    are unaffected, and so are the trend viewer's own requests. A link to the viewer from a page on
    another site is now refused: open the URL directly. Behind a proxy that rewrites Host, list the
    public name with --allow-host, since browsers send it in Origin.

  • One Host and Origin check for both servers (#134). camber lab's own Host and Origin check
    now uses the shared HostAllowlist and cross-site check in camber._access, with its narrower
    settings: 127.0.0.1 and localhost at the bound port, http origins, and Sec-Fetch-Site
    same-origin or none. The lab answers the same requests with the same errors; host names are
    now matched without regard to letter case.

  • Every write to a plain store takes the store lock (#135). In 0.103 only the dataset
    catalog's writers locked a plain store. Now every ParquetStore write method holds the store's
    single-writer lock (<store>/_lock, or the workspace's lock for a workspace store):
    write_long, write_role_frame, register_facility, write_rollup (on the destination),
    prune, drop_partition, drop_facility and migrate_partitions(apply=True). So do the
    facility registry's changes on a plain store. The lock is re-entrant within a process.
    camber interop openfdd ingest --store, camber store migrate-partitions --apply, the dataset
    ingests and synth_portfolio() take it once for the whole command, and the writes inside
    re-enter it. Reads still take no lock. A plain store now gets a _lock file on its first write.
    A script that writes many times can hold with store.lock(): around the whole load, so the
    lock is taken once; otherwise each write takes it on its own, which costs under a millisecond.
    An ingest's staging area and a workspace's rollup stores take no lock of their own, because the
    lock on the store they belong to already covers them.

  • Start-counting rules say when a trend is too coarse to count starts (#143).
    boiler_short_cycle, compressor_short_cycle and chiller_staging take a new parameter,
    max_interval_min (default 15 minutes). On a trend coarser than that, a start count is a floor,
    since a cycle shorter than the interval is invisible. A count at or above max_starts_per_day
    keeps its verdict and gains a caveat saying so. Below the limit, boiler_short_cycle and
    compressor_short_cycle decline (info, "cannot confirm on a 60-minute trend") when samples
    show the equipment on for only part of an interval, because t...

Read more

CAMBER v0.103.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 20:23

0.103: numpy 2, a hardened camber serve, a better trend viewer and lab, reproducible store files, and a hot-water pump lockout check with a synthetic lab case (#122–#126, #128–#130, #132, #133). Breaking (#129): numpy 2.0 or newer is now required (pandas ≥ 2.2.2, pyarrow ≥ 16, matplotlib ≥ 3.8.4); pyarrow 26 needs numpy 2 but does not declare it. Deployment change (#128): camber serve now answers only the host names it is bound to, a defence against DNS rebinding; behind a proxy or ingress, add the forwarded name with --allow-host or CAMBER_API_ALLOWED_HOSTS, or those requests get 403. Token auth is opt-in (--auth token). The trend viewer shows the whole span with min/max thinning, a date range and brush-to-zoom, and says "local time" when no time zone is recorded (#122). The lab gains a disk check that matches the fetch, extras gating, Remove / From a folder / force re-ingest controls and evidence charts in its report (#123, #125). Stores and caches lock against a second writer, and a read-only shared cache works for a class (#124, #126). New store part files are byte-reproducible for the same inputs, CAMBER and pyarrow versions, and about 1.8x smaller; existing stores read unchanged (#130). hw_pump_summer_lockout checks hot-water pumps in warm weather, and a synthetic catalog kind brings the plant-lockout workbook exercise (#132, #133).

Added

  • camber serve --auth token: optional token auth, like camber lab (#128). Every route
    (/ui, /facilities, /points, /history, /about) then needs this run's access token. The
    command prints a launch URL ending in ?token=... and saves it in a launch file
    serve-<port>.url that only you can read (0600, in a 0700 folder). Opening it sets an
    HttpOnly; SameSite=Strict session cookie camber-serve-<port> and redirects (303) to the same
    page without the token; scripts send Authorization: Bearer <token>. Without credentials a
    request is 401 (403 for a wrong token); a bare GET /health answers only {"ok": true}, for
    health checks. CAMBER_API_TOKEN (16 or more characters) fixes the token. Python:
    make_server(..., auth="token", access_token=...) and serve(...) take the same arguments;
    python -m camber.api.server reads CAMBER_API_AUTH and CAMBER_API_TOKEN. Off by default
    in 0.103
    , so existing deployments keep working; a later release may make token auth the
    default on loopback.
  • camber serve --allow-host NAME (repeatable) and CAMBER_API_ALLOWED_HOSTS (#128), the
    extra Host names to answer for a proxy or DNS name: a bare name on any port, NAME:PORT
    exactly. make_server(..., allowed_hosts=[...]) is the Python form, and
    camber.api.server.check_request the pure request check.
  • Single-writer locks on plain stores and dataset caches (#124). Two processes writing one
    cache or one store, such as two camber labs or a lab and camber datasets ingest, no longer
    corrupt each other. Every write to a dataset cache holds <cache>/_lock: a fetch that
    downloads, ingest --from-dir, an ingest extracting archive members or recording an
    acknowledgement, and remove. An ingest and remove --purge-store hold the store's lock:
    <store>/_lock for a plain store, or the workspace's own lock for a store in a portfolio
    workspace, which is re-entered rather than doubled, so the lab's workspace ingest cannot
    deadlock. It is the portfolio's kernel lock (flock, msvcrt on Windows), released when its
    process exits, so a crash leaves no stale lock. A second writer waits up to 30 seconds, then
    stops with dataset cache … is locked by <pid>@<host> since <time> (or store … is locked by …); camber datasets exits 1. Reads take no lock. See
    DATASETS.md.
  • A read-only shared cache (#124). A dataset cache the user cannot write, such as a course
    folder shared read-only across a classroom, now works for camber datasets and camber lab.
    A fetch whose files are all present and match their pinned SHA-256 downloads and writes
    nothing. An ingest reads the downloads in place and uses the archive members already extracted
    there. Members still needed are extracted into a scratch folder beside the ingest's staging
    area in the store, deleted when the ingest ends. Downloading, ingest --from-dir and remove
    stop with the dataset cache … is read-only for this user and change nothing.
    Using the lab describes setting one up for a class.
  • Trend viewer: date range, brush-to-zoom and a whole-span view (#122). The /ui trend
    viewer that camber serve and camber lab both serve gains From / to date inputs,
    Last 7 days / Last 30 days presets (counted back from the last stored sample) and
    All. Brushing a panel now zooms to that span and reads it again at full resolution (up to
    20,000 samples per series), and still selects its samples for the N selected readout; Zoom
    out
    steps back through the spans viewed. A line under the controls gives the span shown, its
    time-axis label, and how many samples were drawn out of how many stored. See
    Visualization and the lab guide.
  • /history?max_points= (#122). The read API's /history takes an optional max_points, a
    per-series budget: a longer series is cut into max_points / 2 equal time buckets, and each
    keeps its minimum and maximum sample, so spikes, dips and flatlines survive the thinning. The
    reply adds source_count (rows in the window before thinning), downsampled, max_points,
    and first / last (the window's first and last stored timestamp). A request without
    max_points returns the same rows as before.
  • time_axis on /points (#122). /points?facility_id=… also returns how to label that
    facility's time axis: timezone (or null), local_label, and utc_label (null when the
    store records no time zone for the facility).
  • camber lab: remove a dataset from the page (#123). Each row with anything on disk has a
    Remove… button. After you type the dataset id, a job deletes the dataset's downloads and
    extractions from the cache, as camber datasets remove does. A tick box also drops its
    facilities from the store, as --purge-store does. The route is POST /lab/jobs/remove, under
    the lab's usual checks (access token, CSRF token, JSON only, catalog ids only), and needs
    confirm equal to the id. In a portfolio workspace the purge follows the facility lifecycle:
    it is refused, with the reason shown in the dialog, while a facility is under a legal hold or
    is suspended, offboarding or archived, and it is checked again under the workspace lock.
    Removals and purges are audited (lab.remove, lab.purge).
  • camber lab: ingest a manual download from a folder (#123). A manual: true entry has a
    From a folder… button that does what camber datasets ingest --from-dir does
    (POST /lab/jobs/from-dir). The server treats the path as untrusted input. It must be an
    absolute path (~ is expanded) of at most 4096 characters, with no control character, to a
    readable folder. A catalog file that links outside the folder is refused. The folder is only
    read: each pinned file is verified (size and SHA-256), then copied or hard-linked into the
    cache and ingested. A research-only entry needs its acknowledgement; in a workspace the ingest
    is audited (lab.adopt).
  • camber lab: force re-ingest (#123). A force re-ingest tick box next to Ingest
    (already fetched)
    , also used by From a folder…, re-ingests data whose inputs are
    unchanged, which an ingest otherwise skips.
  • camber lab shows each dataset's optional extras (#123). A row lists its
    requires_extras (for example needs xlsx). When an extra is not installed the badge turns
    red with the install command, the row cannot be ticked, and the server refuses its fetch or
    ingest (409) before anything is downloaded. Before, an Excel dataset downloaded in full and
    then failed at ingest.
  • Evidence charts in the audit report (#125). camber report (the default audit layout) and
    the lab's report link now include a Finding evidence section. Each chart shows the
    samples the rule judged, shaded where it flags them, drawn with the run's own rule instances
    and data. To bound the report's size and build time, it draws the charts of the 12 worst
    findings that have one (camber.report.audit.EVIDENCE_LIMIT; about 50 KB each) and says so
    when there were more. The RCx layout (--layout rcx) still gives each issue its own page.
  • compressor_short_cycle gets a recommended action and a Learn more link (#125). The
    recommended action covers the compressor's minimum on/off timers, the stage differential and
    where the calling sensor is mounted, with a site check for the walk-down. The link goes to the
    measures chapter of PNNL's small/medium-sized building re-tuning course (PNNL-SA-92685), new in
    camber.references as pnnl-small-retuning-ch3. It covers packaged units and their
    thermostats.
  • hw_pump_summer_lockout: the hot-water pump's warm-weather lockout (#132). PNNL's
    heating-plant re-tuning locks out the hot-water pumps, not only the boiler, in warm weather; a
    pump left running all summer with the boiler off passed boiler_summer_lockout. The new rule
    reads when the pump runs (its status, else its speed above 5 %, else the loop flow) and
    reports the share of its occupied running hours above the lockout, with the boiler check's
    semantics and severity (warn at 5 %, fault at 20 %), plus max_oat_running_f and
    boiler_off_pct (the pump running with the boiler off). It shares the boiler's
    summer_lockout_oat_f (default 65 °F): a config that sets it on boiler_summer_lockout only
    passes it on, recorded in the findi...
Read more

CAMBER v0.102.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 18:24

0.102: follow-ups from 0.101, sample charts across the docs, a guide to the lab, and an access token on every lab route (#110–#121, #127). The suggester's unitless range check no longer overturns a strong name on a dirty series (#110); the air-economizer workbook exercise teaches the economizer low-limit lockout and the capstone adopts it (#111); and the RCx report ranks uncosted issues of equal severity by confidence before the issue key, so the capstone's stuck-damper issue ranks first again (#112). The docs site gains charts and screenshots rendered by CAMBER's own chart code (#113) and a step-by-step guide to camber lab (#121). Evidence charts shade exactly what each rule flags and name roles with their units (#114–#117, #119). A stuck supply-air sensor on a scheduled fan is now flagged (#118). Behaviour change (#120): free_cooling_missed no longer judges fan-off hours, so missed shares rise on units whose fan runs only when occupied; the air-economizer and capstone answers move with it. Security (#127): camber lab now requires a per-run access token on every route; open the URL it prints.

Added

  • Sample charts and screenshots across the docs (#113). The docs site and README had no
    images. scripts/docs_figures.py renders 36 figures and three README thumbnails into
    docs/img/ with CAMBER's own chart code: the camber.charts primitives, the rules' evidence
    hooks and the drift detectors' frozen-baseline bands. The data is deterministic synthetic data (camber.synth, the ahusim,
    driftsim, vavsim and pumpsim simulators, the DCV simulator and small seeded generators);
    no downloaded dataset is read. A fixed style, the Agg backend, a fixed dpi and size and PNGs
    with no metadata keep reruns byte-stable on one machine.
  • One figure per chart pattern in Visualization (#113), next to its
    code example: readiness ribbon, fault-annotated trend, load carpet (with a new short section),
    quality dashboard, OAT scatter, diagnostic scatter, the fitted drift band, rule evidence, cohort
    small multiples, M&V savings, load profile and load-duration curve, box by hour, and a new
    CUSUM and energy-signature section.
  • A hero figure on the analytics pages (#113): free cooling, chiller, AHU, VAV and pump
    drift, M&V (the change-point fit and the savings chart), sensor health and schedule inference.
  • The evidence chart each workbook exercise discusses (#113), with a caption that marks it as
    a synthetic illustration rather than the exercise's dataset: the stuck outdoor-air damper's OA
    fraction, the leaking cooling valve, the reheat penalty, chiller kW/ton, tower approach, the
    change-point baseline and the rest. The exercises' text and answers are unchanged.
  • Screenshots (#113) of the RCx report, the site report, the camber lab catalog page and
    the camber serve trend viewer, on a synthetic two-AHU site. --screenshots takes them with
    headless Google Chrome, serving on 127.0.0.1 and stopping the servers afterwards; it is
    optional, as CI has no browser.
  • A small gallery in the README (#113): three thumbnails linking to the docs pages.
  • scripts/docs_figures.py --check (#113), run by scripts/gates.sh and by
    tests/test_docs_figures.py. It fails when an image referenced from docs/**/*.md or
    README.md is missing or over 150 KB, when a docs image has no alt text or caption, when a
    generated figure is missing, unreferenced or stale, or when docs/img/ exceeds 5 MB.
  • Using the lab, a step-by-step guide to camber lab (#121), early in the
    docs navigation and linked from the README, DATASETS.md, CLI.md and the workbook. A first
    session on the open ornl-frp-ops dataset (install, start, pick a dataset, Fetch & ingest,
    trends, the report's top finding, the exercise, and the matching CLI commands), then how to
    read the trend viewer and the report, where the cache and the store live and how to clean
    them up, a classroom setup (pre-fetching for an offline room, manual downloads with
    --from-dir, a portfolio workspace, what loopback-only means on a shared computer) and
    troubleshooting. scripts/docs_figures.py --screenshots adds five walkthrough screenshots;
    unlike the other figures they show a real open dataset (credited in their captions), read
    from a local copy through the verified --from-dir path and never downloaded, and they are
    skipped when no local copy is present.

Changed

  • Workbook air-economizer teaches the economizer low-limit lockout (#111). A new step 5
    has learners read free_cooling_missed's warn on the fault-free unit (17.5 %). They then find
    from the trends and the unit's sequence that 899 of its 965 missed hours (93 %) fall below the
    documented 33.8 °F lockout, where the damper is held at its minimum by design. They set
    low_limit_f 33.8 and re-run: the fault-free unit reads ok (1.96 %), and the dampers stuck at
    10 % and 25 % stay a fault (49.6 %). The exercise config leaves the low limit out on purpose,
    and its _comment says so. The declaration pins both readings, and the page (a new step and
    question, with later ones renumbered) and the instructor key are updated. The stand-in now
    models the lockout (four cold days below it), so both readings are also checked offline.

  • Workbook capstone adopts the same low limit (#111), which intentionally changes its
    answers.
    free_cooling_missed in the capstone config now sets low_limit_f 33.8, with a
    basis, to match the dataset template and the economizer exercise. On the real data the
    control unit reads ok (2.0 %, was a 17 % warn) and loses its spurious "enable the economizer"
    issue and walk-down item. The onset unit reads a warn at 21 % (was a 30 % fault): six healthy
    months dilute a 1 July onset in a year-long share. Its damper issue keeps its cause heading
    and confidence H, and it now has stronger cause evidence (commanded_open_pct 89.9 % of 640
    h, was 40.3 % of 673 h). With #112 it ranks first again: among
    uncosted issues of one severity, confidence now decides the order before the issue key. Drift is
    unchanged (-83, fault). The declaration, the page (questions 1 and 2, a caveat) and the key are
    updated.

  • RCx: uncosted issues of one severity rank by confidence before the issue key (#112).
    link_findings used to order uncosted issues of one tier by issue key, a fixed but arbitrary
    tie-break. They now rank by input-trust confidence, H before M before L, and then by key.
    Severity, conditional-last and costed-first by dollars are unchanged, and costed issues keep
    their dollar order. This intentionally changes the RCx ranking where uncosted issues of one
    tier differ in confidence. On the 15 catalog run templates (default subsets), 4 of 96 issues
    change rank, in 2 reports, and each pair swaps. In lbnl-ddahu, DMPRStuck_OA_100's
    outdoor_air_fraction fault (H) moves from 2 to 1 above DMPRStuck_OA_0's g36_afdd fault
    (M). In ornl-frp-ops, base_heating's night_weekend_setback fault (H) moves from 2 to 1
    above sb_heating's compressor_short_cycle fault (M). Both reports keep their chosen week;
    only its evidence score, which weights issues by 1/rank, changes. The RCx golden is unchanged:
    its synthetic site has no uncosted tie with mixed confidence. docs/RCX-REPORT.md describes
    the new order.

  • Workbook capstone: the damper issue ranks first again (#112). On the real data, the onset
    unit's stuck-damper issue (an uncosted warn, confidence H) now ranks 1, above the onset unit's
    supply-air reset and the control's static-pressure reset (uncosted warns, confidence M). It
    ranked 3 in #111. The declaration asserts rank 1 in both modes. The page's question 1 asks what
    puts the issue first when nothing is costed. The instructor key explains the confidence order,
    lists the damper first in the walk-down, and rewords the common mistake about reading an
    uncosted rank as a measure of size.

  • The unitless range check no longer overturns a strong name on a dirty series (#110). On
    the time-series path (the default when a series is passed, from 0.101), a point with no
    declared unit is range-checked in every plausible unit. Two changes act on that check only:

    • an exact zero that the role cannot read in any plausible unit (a 0 °C room, 0 ppm CO2) is
      a dropout and is left out; an all-zero series gives no range verdict;
    • when the role is the name's best lexical match and scores at least 0.6, the check keeps at
      least STRONG_NAME_RANGE_FLOOR (0.75) of its score. A series with no reading inside the
      bounds in any unit (a -999 dead channel) is still demoted in full.

    This intentionally changes suggestions when a series is passed without a unit. A declared
    unit, the use_timeseries=False opt-out and the name-only default are unchanged. At default
    settings with the series passed, top-1 0.101 → 0.102:

    • held-out catalog names, real names, simulated LBNL sets, BTS anonymised: unchanged
      (73.0, 95.7, 74.4, 46.2 %);
    • BTS Brick-class names (upper bound): 89.4 → 93.4 % (name alone 93.2 %);
    • the five synthetic styles: 70.4-89.9 → 73.0-93.9 %; ahu_03_supply_temp 89.9 → 93.9 %
      (name alone 93.9 %).

    No pool loses top-1 or top-3. Before and after tables, and the 4 synthetic points that now
    miss, are in docs/MAPPING-ASSIST.md.

  • camber.mapping_assist.STRONG_NAME_RANGE_FLOOR (new, provisional): the floor above, a
    module constant like WATER_AIR_PENALTY.

None of these figures is a gated benchmark.

Fixed

  • camber lab needs its access token on every route (#127, security). The lab binds
    127.0.0.1, but every GET was unauthenticated and the page carried the CSRF token, so any
    other account on a shared computer could load /lab, read the token, queue fetches and
    ingests, and read the ingested data, reports and trends. Each run now makes...
Read more

CAMBER v0.101.0

Choose a tag to compare

@github-actions github-actions released this 04 Oct 21:16

0.101: a seasonal OA damper minimum, data-led point-role suggestions by default, and a
walk-down that follows the heading cause (#105–#109).
g36_afdd takes a seasonal OA damper
minimum, oa_damper_min_by_month (#105). The lbnl-ddahu template uses it: 45 %, and 28 % in
June to August. The unit's winter heating hours return to OS#1 there, and its winter hours
idling at the minimum are no longer read as free cooling. #105 has a limit: FC6 still cannot
catch the dual-duct damper stuck shut (DMPRStuck_OA_0) on this data. Outside the summer that
run's damper command saturates open, so its hours are OS#5, where FC6 does not apply. The run is
still caught, through FC8, FC10, FC11 and FC12. The time-series point-role suggester gains a
setpoint guard and a range check for points with no unit (#106). Default change (#107):
suggest_roles, review_unmapped and review_bacnet now read the data whenever a series is
passed; use_timeseries=False opts out and gives the 0.100 output byte for byte. With a series
passed, top-1 rises against 0.100 on every evaluation pool. Against the name alone, pools with
strong names dip slightly on dirty series: BTS Brick-class names read 89.4 % with the series
against 93.2 % from the name alone, because site C's negative airflows and dropouts fail the
range check. docs/MAPPING-ASSIST.md advises use_timeseries=False, or cleaning the series,
where the names are already reliable. The RCx walk-down's equipment item now follows the cause
that heads the issue (#108), which changes the two re-headed catalog issues (lbnl-ddahu
DMPRStuck_OA_0, lbnl-sdahu damper_stuck_075). The open-fdd cross-check leaves the
per-verdict list out of its JSON by default (#109). It was re-run once on the merged code with
all three engines: only CAMBER's lbnl-ddahu verdicts change, as #105 reported, and every
engine's any-FC result is unchanged. The synthetic, fleet, LBNL, BDG2 and BDG2 savings benchmark
gates did not move.

Changed

  • The walk-down's equipment item follows the cause that heads the RCx issue (#108). Since
    0.100 (#101) a member finding's equipment-level cause can head an issue, but the "Verify on
    site" equipment item still followed the root finding. camber.walkdown.site_checks takes
    heading_causes={issue key: (rule, cause key)} (additive, provisional), and the RCx report
    passes the cause it chose for each re-headed issue. That issue's equipment item is then the
    member rule's template for that cause, with the member rule's references; an issue the root
    heads keeps the root's item. This intentionally changes the Verify on site section for
    re-headed issues only. On the 15 catalog run templates (default subsets), 2 of 98 issues
    change, the two #101 re-headed: lbnl-ddahu DMPRStuck_OA_0 and lbnl-sdahu
    damper_stuck_075 now ask for the outdoor-air damper's blades, linkage and actuator under a
    full open and close command, not the minimum-position setting or the damper on a hot hour. The
    RCx golden is unchanged (its synthetic site has no re-headed issue).

  • open-fdd cross-check: the per-verdict list is left out of the JSON by default (#109).
    run_crosscheck.py now omits the per-verdict list and collapses the per-equipment "not
    evaluated" reasons to counts unless --keep-verdicts is passed; a month-window run otherwise
    writes about 200k more lines. --omit-verdicts is still accepted (it is the default). The
    Markdown output is the same either way. The committed results were re-run at integration
    with the new default, so none of them carries the per-verdict list.

  • The lbnl-ddahu template sets the unit's seasonal damper minimum (#105). g36_afdd now
    runs with oa_damper_min 45 and oa_damper_min_by_month 28 in June to August (the documented
    sequence positions), where the learned minimum was the 28 % summer position all year. Hourly,
    every verdict holds: the fault-free and DMPRStuck_OA_100 runs stay ok and DMPRStuck_OA_0
    fault. The winter heating hours at the 45 % minimum return to OS#1, so FC6 judges 1,111 h on
    the fault-free run (was 744 h; 0.09 %) and 1,273 h on DMPRStuck_OA_100 (was 762 h; 0.08 %),
    and FC5 and FC7 are evaluated there (367 h and 529 h, 0 %). Winter hours idling at the 45 %
    minimum are no longer free cooling: the fault-free run's 4 h of FC9 hits go, and on
    DMPRStuck_OA_0 FC8 reads 100 % of 48 h (was 39.7 % of 121 h) and FC9 0 % (was 20.7 %).
    FC6 still does not catch DMPRStuck_OA_0 (1.96 % of 766 h, unchanged), which #105 set out
    to do. Outside June to August that run's damper command saturates (median 100 %) while the hot
    deck heats, or both coils run, so its hours are OS#5, where G36 does not apply FC6, and the
    temperature balance there reads a median 17.7 % OA, within G36's 30-point tolerance of the
    31.8 % minimum anyway. lbnl-sdahu is unchanged. g36_afdd is not a scored target, so
    camber datasets score and the gated benchmarks do not move. The open-fdd cross-check's CAMBER
    verdicts on lbnl-ddahu change (any-FC unchanged in both windows); docs/ECOSYSTEM.md has the
    before/after.

  • Default change: the point-role suggester reads the data when a series is passed (#107).
    suggest_roles, review_unmapped and camber.interop.bacnet.review_bacnet now use the
    time-series path (FeatureSuggester(use_timeseries=True)) when a series is passed for a token
    and no suggester is given. This intentionally changes their output whenever a series is
    passed
    , including the BACnet review path when series_by_name is given. The new keyword
    use_timeseries=False opts out and gives the 0.100 output byte for byte. Without a series the
    output is unchanged, and so are roles_from_bacnet and vendor_aliases, which pass no
    series. The FeatureSuggester class default stays name-only. At default settings with the
    series passed (no unit, no outdoor-air series), top-1 before → after:

    • held-out catalog names: 49.4 → 73.0 %;
    • real names: 84.8 → 95.7 %;
    • simulated LBNL sets: 73.1 → 74.4 %;
    • BTS anonymised: 0.0 → 46.2 %;
    • BTS Brick-class names (upper bound): 25.5 → 89.4 %;
    • the five synthetic styles: 15-26 → 70-90 %.

    On strong names the unitless range check costs a little against the name alone. With BTS
    Brick-class names it is 89.4 % against 93.2 %, because site C's negative airflows are out of
    range. The full table is in docs/MAPPING-ASSIST.md.

  • Time-series suggester, step 1: held-out measurement and the two known fixes (#106). Both
    fixes act only on the time-series path, which from #107 is the default when a series is
    passed.

    • Setpoint guard. When a point's name says setpoint (setpoint, SP, STPT,
      RMCLGSPT), the data ranks only the setpoint roles. robod's temp_setpoint stays
      cool_sp, where the data alone had made it space_temp.

    • Range check without a unit. A point with no declared unit is now range-checked on the
      time-series path. Before, it skipped the check. It is read in every plausible unit of the
      role (the units the role templates try), and the best reading counts. A °C room temperature
      passes, and a sentinel such as -999 is demoted, as on the default path.

    • Measured on the held-out catalog names with their data (catalog_names.py --data, new;
      89 of the 223 names have a usable open-tier series). Top-1 / top-3 %:

      • name only: 76.4 / 79.8;
      • name plus data: 76.4 / 82.0 before, 75.3 / 80.9 after;
      • the default suggester with the series passed: 49.4 / 50.6. Its range check reads a
        unitless temperature as °F.

      After the fixes the data helps 3 points and hurts 4. The 4 losses are refrigerated-case air
      temperatures labelled as air-handler roles. Two of them, at about -3 °F, are below those
      roles' physical bounds.

    • Other evaluation pools (name plus data, top-1): real names 95.3 → 95.7 %, with 0 losses
      (was 2); real names excluding in-sample names 89.1 → 90.7 %. Simulated LBNL sets and BTS are
      unchanged. The synthetic names move by at most +0.1. Details and the helped and hurt lists
      are in docs/MAPPING-ASSIST.md.

  • real_names.dataset_points takes keep_series and returns each point's 15-minute series
    with its profile. The evaluation scripts only.

None of these figures is a gated benchmark. The synthetic, fleet, LBNL, BDG2 and BDG2 savings
benchmark gates are not affected.

Added

  • g36_afdd takes a seasonal OA damper minimum (#105). A new oa_damper_min_by_month
    parameter ({month: position %}, mirroring min_oa_pct_by_month) overrides oa_damper_min
    in the months it names, so the operating states judge each interval's OA damper against its own
    month's minimum position: heating (OS#1) at it, free cooling (OS#2) beyond it. It needs
    oa_damper_min, the position in the other months. run_g36_afdd gains the same keyword and
    G36Result a trailing oa_damper_min_by_month field; the finding reports the metric only when
    it is set, and a caveat names the seasonal minimum. The default is None, so default outputs
    are byte-identical. Param docs and docs/THRESHOLDS.md cover it.

CAMBER v0.100.0

Choose a tag to compare

@github-actions github-actions released this 04 Oct 19:51

0.100: the dual-duct unit's seasonal minimum, equipment causes heading RCx issues, and an
opt-in leak drift detector (#97–#104).
g36_afdd FC6 takes a seasonal minimum OA (#97). The
lbnl-ddahu mapping adds its cold-deck SAT setpoint, so G36 FC9, FC11 and FC13 are evaluated
there (#98); the dataset re-ingests once, and the open-fdd cross-check was re-run on the merged
code. reheat_penalty takes a box_type that caps its fan-heat estimate by box (#99). A new
opt-in coil_leak_drift judges coil-valve leaks against the unit's own baseline (#100). Its
results are measured records, not gated. An RCx issue is now headed by a member finding's cause
when that cause names the equipment at fault (#101). The lbnl-ddahu and lbnl-sdahu
templates now run free_cooling_missed, which re-heads two catalog issues. The point-role
suggester reads more public naming conventions and keeps weather-station points outdoors
(#102). This intentionally changes the default suggester output. Setpoint-level grouping for the
sensor-health outlier read was measured and rejected (#103). Config runs resolve a facility's
time zone the way the read API does (#104). The synthetic, fleet, LBNL, BDG2 and BDG2 savings
benchmark gates did not move.

Changed

  • Dataset templates: lbnl-ddahu and lbnl-sdahu run free_cooling_missed (#101). Both
    templates now run it at the documented 60 °F high limit, reading each unit's own OA damper
    command and mixed-air temperature. Hourly, lbnl-ddahu DMPRStuck_OA_0 reads warn (14.5 %
    of free-cooling hours, cause: damper not delivering), and its fault-free and DMPRStuck_OA_100
    runs read ok. lbnl-sdahu also sets the unit's documented 33.8 °F low-limit lockout
    (low_limit_f, below), so 3,374 of its 5,515 sub-60 °F hours count as free-cooling weather.
    There damper_stuck_010 and damper_stuck_025 read fault (49.6 %), and
    onset_damper_stuck_025 and damper_stuck_075 read warn (21.1 % and 20.8 %), all with cause
    damper not delivering. The fault-free and coi_leakage_010 runs read ok (1.96 % and 1.93 %).
    Without the low limit they read warn (17.5 % and 18.3 %), because 93 % of their missed hours
    fall below the lockout, where the sequence holds the damper at its minimum by design. The
    template comment records both. The lbnl-sdahu RCx report has 11 issues, up from 8.
    free_cooling_missed is not a scored target, so camber datasets score and the gated
    benchmarks do not move. The other four air-handler templates do not run it, and each comment
    says why. nuig-ahu101 is 100 % outdoor air with no mixing box. On lbnl-b59, ornl-frp-ops
    and ornl-frp-vav the rooftop units trend no cooling-valve command, so the rule would decline.

  • An RCx issue is headed by a member's cause when it names the equipment at fault (#101).
    An issue's heading came from its chain's root finding, which can hide a more specific cause on
    a member. A cause is equipment-level when it names a component that does not do what it is told:
    a damper that does not deliver the outside air it is commanded to (free_cooling_missed,
    damper_not_delivering), a reheat valve whose position does not follow its demand, a leaking
    valve, a stuck actuator, or a drifting OA damper. The precedence: a root with an equipment-level
    cause keeps the heading; otherwise the most upstream member with one heads the issue; otherwise
    the root's cause, as before. The action, its title and its links stay the root's, and the page
    says in one line which member named the cause. The executive summary's Issue column and the
    cause key of RcxReport.to_dict() issues follow the heading; each issue gains cause_rule,
    the rule of the finding that names it. See docs/RCX-REPORT.md, "Which cause heads an issue".
    On the catalog run templates (15 datasets, default subsets, 98 issues) two headings change,
    both through free_cooling_missed, which the lbnl-ddahu and lbnl-sdahu templates now run
    (see Dataset templates below): lbnl-ddahu DMPRStuck_OA_0 reads "Outdoor-air damper not
    modulating (stuck low)" (was "Outside air below the ventilation minimum"), and lbnl-sdahu
    damper_stuck_075 reads "Outdoor-air damper not modulating (stuck part open)" (was "Economizer
    open above the high limit"). No other heading changes. The RCx golden file is unchanged: its
    synthetic site has no member with an equipment-level cause.

  • lbnl-ddahu maps its cold-deck supply-air setpoint (#98). CSA_TEMPSPT (the
    publisher's Brick model types it as a supply-air temperature setpoint of the cold deck; 55.0 °F
    in every row) now maps to supply_air_temp_sp, next to the cold-deck CSA_TEMP. Both roles are
    the cold deck. The hot deck's own 90 °F setpoint is not mapped, and the mapping, template and
    cross-check caveats say so. The dataset content hash changes, so stores re-ingest
    lbnl-ddahu once.

    • g36_afdd now evaluates FC9, FC11 and FC13 on the unit. DMPRStuck_OA_0 adds FC9 (20.7 %)
      and FC11 (12.2 %) to the FC8, FC10 and FC12 it already flagged. FC13 reads 0 % on every run.
      The fault-free run's FC9 hits fall on its 4 free-cooling hours, under the 24-hour floor, so
      it stays ok. No run's severity changes.
    • The open-fdd cross-check was re-run (examples/openfdd_crosscheck/results/). Only
      lbnl-ddahu verdicts change. Every engine's any-FC result per run is unchanged. The open-fdd
      engines now evaluate FC7, FC9, FC11 and FC13 there: FC11 fires on DMPRStuck_OA_0 in all four
      open-fdd runs, and FC13 in the SQL engine and the pandas engine at its own defaults. In month
      windows, the pandas engine at G36 tolerances goes from TPR 0.58 with FPR 5/12 to TPR 0.62 with
      FPR 6/12, because FC9 fires on one fault-free month. docs/ECOSYSTEM.md has the before/after.
    • Workbook air-sat-reset. supply_air_reset now reads the flat cold-deck setpoint on
      DDAHU__fault_free: "not reset (setpoint flat at ~55F)", where it read "no reset (SAT pinned
      low at ~55 F regardless of OAT)" from the supply air before. The severity (warn), the
      direction (flat) and the 66 % below the G36 target are unchanged. The instructor key, the
      page, the exercise config comment and the stand-in follow, and a new check pins
      sp_range_f 0.0.
  • Unchanged. No gated benchmark key moves: the synthetic, fleet, LBNL, BDG2 and BDG2
    savings benchmarks all hold. The LBNL benchmark's DDAHU family scores outdoor_air_fraction
    only, which does not read the setpoint.

  • Point-role suggester: vocabulary from public naming conventions (#102). This changes the
    default name-only output.
    The 0.96 real-name evaluation showed most misses were vocabulary
    gaps. FeatureSuggester now reads:

    • new abbreviations: AF, WH, STA, Enable, SS, PM / PMP, CHWP / HWP / CWP,
      CT, SAF / SF, BOI, SW / RW / SWT / RWT / LWT / EWT, HWL / CHWL /
      CDWL, HValve / CValve, HC / CC / LAT, presence, occupant, PIR, weather / WX,
      and dry-bulb, wet-bulb and dew point;
    • run-together words (OADMPR, RMCLGSPT) split into known abbreviations, and vowel-dropped
      abbreviations (Sply).

    Context rules use the same lexical score:

    • a water point is not given an air-side role (a chilled-water flow is not airflow);
    • a chiller, boiler or tower places a point on its loop;
    • an unlocated temperature (air_temperature) reads as a space temperature, not outdoor air;
    • temp_setpoint reads as a zone setpoint, not supply_air_temp_sp;
    • a wet-bulb is not a dry-bulb oat;
    • pump and tower-fan speeds no longer read as supply_fan_speed;
    • a UUID or PM2.5 names nothing.

    On a 6,546-case name × unit corpus, the top-1 role changed in 854 cases. The 0.95 golden cases
    are unchanged. Module constants WATER_AIR_PENALTY (0.4) and UNLOCATED_TEMP_PENALTY (0.6)
    hold the two new multipliers. Results on the same inputs (examples/suggester_eval):

    • real names, pooled, name only: 82.5 → 94.3 % top-1 (in-sample: the vocabulary was chosen
      from these misses);
    • held-out catalog names, name only (new catalog_names.py, out of sample): 72.2 → 83.0 %;
    • BTS anonymised: unchanged.
  • Weather-station guard on the time-series path (#102). With use_timeseries=True, the data
    ranks only the outdoor roles when a point's name places it outdoors. For a weather station
    (weather, WX, meteo, the Synoptic _set_N suffix), it ranks only oat, outdoor_rh and
    outdoor_co2. A wet-bulb needs the name to say so. The data no longer turns an outdoor
    temperature into a wet-bulb or an outdoor humidity into a supply-air humidity. On real names,
    the data's top-1 losses fell from 5 to 2.

  • Not changed: the time-series path stays opt-in. docs/MAPPING-ASSIST.md recommends
    turning it on when series are passed. That needs maintainer sign-off.

The synthetic, fleet, LBNL, BDG2 and BDG2 savings benchmark gates are not affected.

Added

  • free_cooling_missed takes an economizer low-limit lockout, low_limit_f (provisional).
    Many sequences lock the economizer out below a low outdoor-air temperature and hold the OA
    damper at its minimum, so mechanical cooling in that weather is not missed free cooling. With
    low_limit_f set, hours below it are neither available nor missed. They are reported as
    low_limit_excluded_hours (and low_limit_cooling_hours, those with mechanical cooling
    running), with a caveat. camber.freecooling.free_cooling_opportunity takes the same
    parameter through the same weather test and reports hours_low_limit_excluded. The RCx
    report's economizer page passes the rule's value to it. The default None keeps both outputs
    byte-identical, with none of the new metrics or fields. lbnl-sdahu sets 33.8 °F. lbnl-ddahu
    does not: no lockout is documented for that unit, and on its fault-free run the OA damper
    modulates above its minimum...
Read more

CAMBER v0.99.1

Choose a tag to compare

@github-actions github-actions released this 03 Oct 22:56

0.99.1 patch: open-fdd-ingested facilities report their time zone (#96). The synthetic, fleet,
LBNL and BDG2 benchmark gates did not move.

Fixed

  • The read API did not report the time zone of an open-fdd-ingested facility (#96).
    camber interop openfdd ingest recorded the site zone only in the facility's openfdd
    provenance. Config runs found it there, but /facilities looked a zone up only on the registry
    entry or through a catalog dataset_id, so these facilities had no timezone and the trend
    viewer labelled their site wall clock as UTC. The importer now also writes the zone to the
    facility's meta["timezone"]. The read API looks up a facility's zone in this order: the
    explicit timezone, the catalog dataset zone, then the openfdd provenance zone, so facilities
    ingested with 0.99.0 report their zone without a re-ingest. An unchanged re-ingest, which is
    skipped, also fills in the missing key. Facilities with no recorded zone are unchanged.

CAMBER v0.99.0

Choose a tag to compare

@github-actions github-actions released this 03 Oct 21:26

0.99: open-fdd interop and G36 heating at minimum OA (#22 items 1–4, #95). CAMBER and
open-fdd now meet at a file and process boundary, with
every result labelled by the engine and version that produced it. A reusable cross-check (#22
item 1) scores CAMBER's g36_afdd and open-fdd's pandas and SQL engines (commit 32a6d44, PyPI
4.4.9) on the same labelled LBNL air-handler data, and explains each difference from public code
and open data. An importer (#22 item 2) reads an open-fdd building package or historian layout
into a CAMBER store, so drift, M&V and sensor-trust checks can run on data collected at the
open-fdd edge. Columns map through a versioned role crosswalk (#22 item 4), and the results go
back as a draft findings-exchange JSON (#22 item 3). docs/ECOSYSTEM.md opens with an entry
point for open-fdd users. In g36_afdd, heating (operating state 1) now needs the OA damper at
its minimum, the companion of #94's free-cooling test (#95).

Added

  • G36 cross-check against current open-fdd (#22 item 1). examples/openfdd_crosscheck
    runs CAMBER's g36_afdd and open-fdd's two engines on the same labelled frames, pinned to
    open-fdd commit 32a6d44 (PyPI 4.4.9). It works through files and processes only: the pandas
    engine runs in its own venv as a subprocess, and the SQL engine runs as fdd_cli in a container
    with no network and read-only mounts.

    The harness ships:

    • a versioned role mapping (role_map.json);
    • tolerance profiles (profiles.json): open-fdd's defaults, and the G36 Table 5.16.14.7
      tolerances CAMBER uses;
    • one normaliser per engine, recording each verdict's denominator;
    • per-engine, per-FC scoring with Wilson intervals, where "not evaluated" is kept apart from
      "not detected";
    • synthetic probes that isolate single engine behaviours.

    Results for all three engines on lbnl-sdahu (full subset) and lbnl-ddahu replace the "not
    yet re-compared" section of docs/ECOSYSTEM.md, which now also says where the 0.1.5
    comparison no longer applies. CAMBER runs with each dataset's run-template parameters, so FC6
    is evaluated on lbnl-sdahu (#94). Synthetic probes run through fdd_cli confirm which G36
    tolerances the SQL tuning file cannot set (FC7, FC9, FC11, FC13–FC15). Offline tests run
    without Docker or open-fdd: tests/test_openfdd_crosscheck.py.

  • CAMBER reads open-fdd data (#22 item 2, provisional). The new
    camber.interop.openfdd module reads an open-fdd building package (openfdd_package_v1, a
    folder or a .zip) or open-fdd's historian Parquet layout. CAMBER's drift, M&V and
    sensor-trust checks can then run on data collected at the open-fdd edge.

    • Boundary. It works through files and processes only. No open-fdd code is imported or
      copied, and nothing is written back.
    • Required inputs. read_package / read_historian take the site time zone and the unit
      system as required arguments, because a package carries neither and CAMBER will not guess.
    • Mapping. Columns map through the package's own map, which is authoritative. Every other
      column is counted and reported with its reason: no name, unknown name, deliberately
      unmapped, wrong equipment type, unusable unit or duplicate role.
    • Units and time. It handles UTC and local stamps, SI and IP units, declared units, and
      0–1 vs 0–100 percent. It reads equipment classes from the equipType stamps and the
      inventory, never from ids.
  • ingest_package and camber interop openfdd ingest (provisional). They write one facility
    per building into a ParquetStore or a portfolio workspace.

    • Lifecycle. In a workspace the facility is registered through the lifecycle
      (provisioning, or active with --activate), the write takes the workspace lock and is
      audited (interop.openfdd.ingest), and a facility in any other state is refused.
    • Provenance. Recorded under openfdd on the registry entry: the source and schema
      version, the sha256 of every file read, the crosswalk version and its pinned docs commit, the
      zone, units and coverage.
    • Re-ingest. It is idempotent, and data is staged and swapped in. --config-out writes a
      starting run config: rules the mapped roles can run, the package weather as the shared
      outdoor temperature, and a daily M&V baseline per metered class. camber interop openfdd inspect does the same read without writing.
  • A versioned open-fdd → CAMBER role crosswalk (#22 item 4, provisional). It ships as
    camber/interop/openfdd/crosswalk.json, version 1, written from open-fdd's documented
    Haystack-name and SQL-role vocabulary at a pinned commit. 50 of its 71 rows map; the other 21
    are deliberate non-mappings, each with its reason. camber interop openfdd crosswalk [--json]
    prints it.

  • A findings-exchange JSON, draft 0.1 (#22 item 3, provisional). findings_document,
    run_findings and camber interop openfdd findings CONFIG --out FILE emit an engine-labelled
    document that a separate process (an open-fdd agent or pipeline) reads back.

    • Every record carries {engine: {name, version}}.
    • declined and not_evaluated are distinct statuses.
    • magnitude names its denominator.
    • Fields CAMBER does not yet produce are null.
    • See docs/INTEROP-OPENFDD.md.

Changed

  • A store facility ingested from open-fdd knows its zone. A store-source config's
    source.timezone now defaults to the zone the facility was ingested in, as it already did for
    catalog datasets. This is additive. A facility without an openfdd provenance block is
    unchanged.
  • G36 heating needs the OA damper at its minimum (#95). g36_afdd now reads a fan-on hour
    with the heating coil alone active as operating state 1 only when the OA damper is at its
    minimum position, per the G36 §5.16.14 operating-state definitions. It uses the same learned or
    configured oa_damper_min and oa_damper_tol as the free-cooling test from #94. A heating
    hour with the damper open beyond the minimum is state 5, where only FC1–FC4 apply. A missing
    damper reading there leaves the hour unclassified. Without an OA damper point the valves-only
    reading stays, and the caveat now names both states.
    • New outputs (provisional). The G36Result.n_heating_above_min_oa field and the finding
      metric heating_above_min_oa_hours.
    • Before/after. No change on lbnl-sdahu (no heating coil), nuig-ahu101 (no OA damper
      point) or the synthetic G36 scenarios, and no gated benchmark moved. On the dual-duct
      lbnl-ddahu the hot deck heats while the OA damper command is well above the learned 28 %
      minimum (median 55 % on the fault-free run). Between 885 and 1,520 heating hours per run move
      from state 1 to state 5, so FC5 is no longer applicable on the two stuck-damper runs. No
      run's flagged FCs or severity changed.

CAMBER v0.98.0

Choose a tag to compare

@github-actions github-actions released this 03 Oct 19:38

0.98: hardening from the workbook (#84-#93). Working through the 0.97 workbook on real data
showed where CAMBER's answers were wrong, silent or hard to tune. On the air side (#84) the damper
census and supply_air_control gate on trended occupancy, and leaking_valve can credit a unit's measured
fan heat. For terminal units and ventilation (#85) a new rule, actuator_stuck, finds a box's
damper or valve stuck against the zone's demand, reheat_penalty reads a valve's measured
position, a heating shortfall is graded on how often it happens as well as how deep it goes, and
cohort deviation gains opt-in grouping and normalisation. The central plant (#86) checks the
sign of the chilled-water reset, learns a pump's VFD floor, runs the boiler rules without a run
status, and accepts a declared drift reference. Sensor health (#87) reads a fan-less unit's
outliers per operating mode and names clipped readings, stray rows and scheduled status points.
The RCx report (#88) names each issue's cause, lists the checks that did not run, says what data
an M&V refusal needs, and ends the issues with a generated "Verify on site" checklist. More
rules link to the PNNL re-tuning material and ornl-frp-vav declares a scored detector (#89).
Every tunable threshold is documented with its basis and a way to calibrate it, and run configs
read from YAML as well as JSON (#90). Three follow-ups close small inconsistencies: one 60 °F
free-cooling high limit (#91), a site elevation for derived wet-bulbs (site_elevation_ft, #92),
and named fan-off hours with an opt-in duration fault in dcv_verification (#93).

Four maintainer decisions shape the benchmarks. S1: the LBNL benchmark reads the
leaking_valve parameters from the lbnl-sdahu template, which moves the SDAHU and pooled
leaking-valve keys (the fan heat was calibrated on a run that is also a scored negative;
docs/VALIDATION.md states the circularity). S2: faultlab's healthy chilled-water reset now
runs the right way, with no synthetic key moving. S3: actuator_stuck is a scored synthetic
scenario (46 scored rules, new actuator_stuck TPR/FPR keys). S4: drift accepts a declared,
never-stored reference (another unit or a known-good period). For sensor health, decision (a)
applies: only units with no fan signal are read per inferred operating mode, and fan-gated units
are unchanged. Every other benchmark is unchanged.

Added

  • roles_any_of (#86 item 4a, provisional). A rule may declare roles_any_of, a tuple of
    role groups of which each needs at least one role present (a run status or a gas input). The
    runners load every role of a group, skip the equipment when a group has none, and include the
    present group roles in the sensor-health gate. camber.model.entities.missing_inputs is the
    one test shared by the runners and runnable_rules, and Runnable.missing_any_of names the
    unmet groups. The mechanism alone leaves default outputs unchanged; the first rules to
    declare groups are the boiler firing rules (see Changed, 098-plant-boiler).
  • Rules that did not run are listed (#88 item 3, #86 item 4b). Registry.run, run_periods
    and run_fleet take an optional skipped= list and append a RuleSkip (rule, equipment,
    class, missing inputs, reason missing_inputs / no_data / no_verdict) for each equipment
    a rule applies to but produced nothing on. A skip is recorded only when the rule applies: its
    declared classes match the equipment's, or at least one of its inputs is present (a chiller
    rule on an air handler is not applicable, not skipped). The default (None) records nothing,
    so the benchmarks and faultlab are untouched. run_config collects them in
    RunResult.rules_skipped, adding a rule-level record when a configured rule found none of its
    inputs on any equipment; they stay out of findings (and findings.json, so datasets score
    decline counts do not move).
    • RCx report. Appendix A gains a "Checks not evaluated (missing inputs)" table (rule,
      missing inputs, equipment: one row per rule and missing set, six units named, then "and N
      more"), and the cover a "Checks not evaluated" row, both only when there is something to
      list. no_verdict records are kept for later use but not listed.
  • An M&V refusal says what data is needed (#88 item 4). New camber.mandv.sufficiency:
    baseline_need(interval, n_have, min_n=...) returns the required, available and missing
    amount, its unit, the calendar days it takes to collect, and a one-line text ("1,440 baseline
    hours needed, 168 available: 1,272 more hours (about 53 days of data)").
    caltrack_savings and caltrack_savings_hourly raise InsufficientBaseline, a ValueError
    whose message is byte-identical to before, with the gap on .need: too few days or hours,
    missing hour-of-week bins, or thinly observed bins. The config's daily M&V decline for too few
    days adds metrics["data_needed"] to its mv_baseline and mv_savings findings
    (declined_reason and summary unchanged), and the RCx "M&V and drift" section prints a
    "Data needed" paragraph per meter. Hourly M&V has no config path, so its need is reported
    through the exception only. Workbook capstone pins the one-week refusal's need at 1,272 hours.
  • Tunable thresholds, documented (#90, provisional). camber/rules/param_docs.py documents
    every numeric, flag and enumerated constructor parameter of every built-in rule, including the
    extra instances. Each entry gives the unit, a sensible range, the basis of the default
    (standard: ... only where the code cites a section, public source: ..., CAMBER judgment or
    calibrated on ...) and how to calibrate it from your own data. Defaults are read from the
    constructors, never copied. FIXED lists thresholds that are still hard-coded, and a test fails
    on any undocumented or stale parameter.
  • camber rules params [RULE] [--json|--yaml]. Prints each parameter with its default,
    basis and calibration hint, plus a ready-to-paste config snippet. The YAML snippet carries each
    note as a comment beside its value.
  • Every built-in rule is tunable from a config. make_rule and a config's params now also
    cover the extra instances (cohort_airflow, cohort_space_temp, sat_/static_reset_effectiveness,
    sat_/static_rogue_zone_census, sat_/static_cohort_starvation); their identity arguments
    (the cohort role, the reset kind) stay fixed. rule_factories() lists them.
  • YAML run configs (the [yaml] extra, pyyaml>=6). camber run, report, explain,
    ask, fleet, drift, mv, python -m camber.config, the portfolio migration and the edge
    config read .yaml / .yml. Without PyYAML the command stops with an error that names the
    extra. The loader types values as JSON does (dates, 07:00, no / on and 012 stay
    strings), so equivalent JSON and YAML configs give identical results, as a test asserts. JSON
    stays the dependency-free default.
  • camber datasets config --format yaml (and --out *.yaml) writes a template as YAML, with
    its _comment notes turned into comments; config_template(..., format=). Writing YAML needs
    no extra.
  • Calibration provenance in findings. A rule entry may carry a basis map
    ({"fan_heat_f": "calibrated on ..."}), which each finding of the rule records as
    metrics["param_basis"] = {param: {"value", "basis"}}. A misspelt parameter name is an
    error. Existing configs are unaffected.
  • Docs. docs/THRESHOLDS.md is generated from the registry by scripts/thresholds_doc.py
    (--check is a test). The new guide docs/TUNING.md, "Tuning thresholds with your own data",
    covers calibrating on a known-good period, avoiding circular calibration, recording provenance
    in config comments and re-checking against labelled data. Its worked examples use
    lbnl-sdahu and nist-heatpump-fdd. Also updated: CLI.md, API-STABILITY (provisional), the
    mkdocs nav, the README, and a "going further" pointer in the workbook index.
  • A declared, never-stored drift reference (#86 items 1 and 5, decision S4; provisional). A
    drift.families[] entry may carry "reference": {"equip": ID} (score every other unit of the
    class against a named healthy one, optionally with its own "period") or
    "reference": {"period": [start, end]} (a known-good window of the same unit). The reference is
    fitted in a scratch in-memory store on every run and never saved, so a run still cannot mint
    its own baseline. Findings carry metrics["baseline_source"] (reference:<equip> or
    period:<start>..<end>) and a caveat, and their summaries read "vs the reference ..." instead
    of "vs frozen baseline". The reference unit declines as is_reference; a reference that cannot
    serve a detector declines every target (reference_missing_inputs, reference_untrusted,
    empty_reference). A section whose families all declare a reference needs no store and no
    windows. DriftFamilyResult.reference is new (also in as_dict); the drift report and
    camber drift run name the reference. Nothing changes for configs without one.
  • The LBNL plant templates score the plant drift detectors. lbnl-chiller.json and
    lbnl-boiler.json (and the plant-cooling-tower / plant-boiler exercise configs) declare
    PLANT__fault_free as the reference for the tower and boiler families, and the catalog
    declares cooling_tower_fan_effort_drift: [tower_fouling] and
    boiler_efficiency_drift: [boiler_fouling] as scored targets. Through camber run the verdicts
    match examples/lbnl_fdd/plant_detectors.py run by run: tower fan effort 2/3 (fouling 065 and
    080 at +17.8 / +10.9 fan %-points; 095 at +3.3 is under the floor), boiler efficiency 3/3
    (+53.7 / +24.9 / +5.1 %; 095 clears the 5 % warn floor by 0.1 point), no false alarm on the
    20 / 13 other runs. The scorer counts the reference run as a correct negative (0/21...
Read more

CAMBER v0.97.0

Choose a tag to compare

@github-actions github-actions released this 03 Oct 19:22

0.97: the PNNL re-tuning workbook (#79, #80, #81, #82, #83). A hands-on course that follows
PNNL's free Building Re-tuning training, run on the open dataset catalog in camber lab. Default
outputs are unchanged, and no gated benchmark moves.

Added

  • The workbook framework (#79, provisional). docs/workbook/ holds a curriculum map from the
    PNNL chapters and guides to exercises and datasets (index.md), an exercise page template
    (_template.md: goal, Learn more, datasets and licence, setup in the lab and on the CLI,
    steps, questions, what CAMBER shows, caveats, going further) and an instructor page with a
    section per exercise (answer key, discussion points, common mistakes). The docs site has a new
    Re-tuning workbook section, and the README a "Learn re-tuning with CAMBER" section.
    • Answer keys pinned by tests. Each exercise declares its datasets, configs, CLI commands and
      expected answers (a finding present or absent on given equipment, a metric within a
      tolerance, a label score, or a free-form check) in tests/workbook/exercises/<id>.py, with a
      synthetic stand-in shaped like each dataset. The offline tests run the exercise's configs on
      the stand-in every time; pytest -m network runs them on the real catalog data. A stale
      answer fails and names the exercise, and the instructor page must quote every pinned figure.
    • Reference links from the registry. Workbook pages link PNNL only through reference-style
      Markdown links labelled with camber.references ids; scripts/workbook_refs.py prints the
      definitions and --check (also a test) rejects an unknown id, a URL that differs from the
      registry's, or a PNNL link made any other way. No mkdocs plugin is needed.
    • Catalog exercise field. suggested_analyses.exercise is validated: a docs-relative
      workbook page (workbook/<id>.md, optionally #<anchor>) or an https URL. camber datasets info prints its published URL. camber lab serves a relative link offline from the local
      docs tree (/lab/docs/workbook/<id>.md, a script-free reading copy; --docs DIR, by default
      the checkout's docs/), and links the published docs site otherwise.
    • Exercise config templates. camber/datasets/configs/exercises/<id>.json (package data)
      holds a tuned config per exercise where the dataset's default doesn't fit;
      camber datasets config <dataset> --exercise <id> (and
      datasets.config_template(..., exercise=)) writes it.
    • Worked example: air-economizer on lbnl-sdahu: a damper stuck open is found by
      outdoor_air_fraction and economizer_high_limit, one stuck near minimum only as missed free
      cooling (free_cooling_missed), and the label score is read against the published labels.
  • Workbook: the air side (#80). Five exercises following PNNL re-tuning chapters 5 and 6 and
    the AHU guides, each with an instructor key pinned by the answer-key tests:
    • air-sat-reset (lbnl-sdahu, lbnl-ddahu, irish-ahu): is the supply-air temperature
      reset, which way should it move with the outdoor air, how far below the Guideline 36 target
      does it sit, and does the unit hold its setpoint.
    • air-static-pressure (lbnl-sdahu, ornl-frp-vav): a flat duct static setpoint, and a
      damper census over one real test day's ten boxes.
    • air-heat-cool (lbnl-sdahu, lbnl-ddahu, irish-ahu): the published 10 % cooling-valve
      leak (which leaking_valve's 3 °F margin misses; the exercise teaches the comparison with
      the fault-free run), both valves open on a dual-duct unit by design, and a real unit's coils.
    • air-economizer gains an irish-ahu part: the economizer rules on a real, unlabelled unit
      with a documented 100 % outdoor-air period, split by period.
    • air-scheduling (ornl-frp-ops): a 24/7 baseline against a night setback held by fan
      cycling, compressor short-cycling, and the supply fan's night energy.
    • Exercise configs air-sat-reset, air-sat-reset--lbnl-ddahu, air-sat-reset--irish-ahu,
      air-static-pressure, air-static-pressure--ornl-frp-vav and air-heat-cool--lbnl-ddahu;
      the catalog links lbnl-ddahu, irish-ahu and ornl-frp-ops to their exercises.
  • Workbook: terminal units and ventilation (#81). Five exercises for PNNL chapter 7 and the
    zone heating and cooling and minimum outdoor-air guides, each with an instructor key pinned by
    offline stand-in tests and -m network tests on the real data:
    • zone-reheat-overcooling (lbnl-fpu): a high minimum airflow that overcools and reheats,
      against a stuck damper that floods the box (airflow_tracking, reheat_penalty,
      overcooling_min_flow); the south-zone box is the one under test.
    • zone-bad-box (ornl-frp-vav): the one box with a flat damper among ten, the SAT rogue-zone
      census, the rooftop unit's airflow and static following the stuck box, and why a comfort rule
      scores poorly as a stuck-damper detector (TPR 33 %, FPR 100 % on the published labels).
    • zone-reheat-saturated (lbnl-fpu, ornl-frp-vav): a zone below its heating setpoint with
      the reheat demand saturated (reheat_capacity_shortfall), a heating shortfall rather than
      overcooling; and a real building whose electric reheat is not trended, where no reheat verdict
      is possible.
    • zone-dcv (finnish-dcv, b4b-windesheim, sdu-ou44): a documented DCV law, a verdict
      that depends on which CO₂ sensor you trust, and the honest "not judged".
    • zone-min-oa (lbnl-b59): the system-level 62.1 Ventilation Rate Procedure with stated
      assumptions (public sources only), measured OA several times the requirement, and CO₂ and
      DCV as second opinions.
    • Four exercise config templates (zone-reheat-overcooling, zone-reheat-saturated,
      zone-reheat-saturated--ornl-frp-vav, zone-min-oa); the catalog entries lbnl-fpu,
      ornl-frp-vav, finnish-dcv, b4b-windesheim, sdu-ou44 and lbnl-b59 link their
      exercise.
  • Workbook: the central plant (#82). Five exercises on the open LBNL plant datasets, each with
    a page, an instructor key, a tuned config and a synthetic stand-in:
    • plant-chiller-efficiency (lbnl-chiller): a generic kW/ton ceiling flags the healthy plant;
      the ceiling calibrated from the fault-free run finds the severe chiller fouling and the tower
      bypass, and the label score shows its false alarms.
    • plant-cooling-tower (lbnl-chiller): a controlled tower hides fouling in fan effort rather
      than approach; condenser-water reset; condenser_bypass_leak on the stuck bypass.
    • plant-chw-reset-pumping (lbnl-chiller): chilled-water reset, a low delta-T that is a
      property of the constant-flow primary loop, a flat DP setpoint, and a VFD floor above the pump
      rule's fixed near-minimum band.
    • plant-boiler (lbnl-boiler): why the lockout, short-cycle and delta-T rules stay silent on
      a plant whose boiler status is an enable, a pump that runs all year, and boiler fouling that no
      point-in-time rule sees.
    • plant-sensor-vs-equipment (lbnl-chiller): where each sensor bias shows up (kW/ton, the
      secondary pump, a sensor-offset attribution) and why the score counts it as a negative.
    • The lbnl-chiller and lbnl-boiler catalog entries link their exercise.
  • lbnl-chiller mapping (#82). The secondary loop's lead pump speed and its DP and DP
    setpoint now map to chw_pump_speed, chw_diff_press and chw_diff_press_sp, so
    chw_pump_dp_reset can run on the plant. Re-ingest (--force) to pick them up. The dataset
    template's rules do not read them, so its findings are unchanged.
  • Workbook: data quality, energy charting, M&V and the capstone (#83, provisional). Four
    exercises, each with its page, instructor key and answer-key test (stand-in offline, real data
    under -m network):
    • data-trend-quality (nuig-ahu101, lbnl-b59, irish-ahu): the core points each unit
      trends, then the sensor-health layer on real faults -- a copied return-air point, mixed-air
      sensors that fail the flow balance, room CO2 clipped at full scale, a meter held over two
      weekends, a gap-filled CO2 stretch -- and an "untrusted" supply air that is a well-controlled
      sensor.
    • data-energy-charting (bdg2, valladolid-uva): weekday and weekend profiles,
      out-of-hours base load, load factor and the daily change-point baselines as the
      weather-dependence chart.
    • mv-baselines (cofactor-drammen, valladolid-uva, bdg2): G14 acceptance and SEP
      50001 §6.4.1 validity, forecast against backcast, SEP chaining, a chain across the spring 2020
      school closure with a non-routine adjustment, and bill-only M&V on synthetic bills cut from
      an open meter (days-weighted fit, bases chosen from the bills, calendarized). Four exercise
      configs: mv-baselines, mv-baselines--forecast, --backcast and --covid.
    • capstone (lbnl-sdahu, ornl-frp-ops): the RCx report on the fault-onset splice, a
      walk-down checklist built from its recommended actions, conditional issues and default
      assumptions, a re-tuning plan, verification by drift (a first-half baseline scored on the
      second half), and a before-and-after scheduling test where hourly M&V is refused for lack of
      baseline data. Exercise config capstone.
    • Catalog exercise links: nuig-ahu101 to data-trend-quality, bdg2 to
      data-energy-charting, valladolid-uva and cofactor-drammen to mv-baselines.

Fixed

  • Workbook network tests (test harness only). pytest -m network now ingests each run's
    subset: the plant exercises that need full get their own sibling store, so the default and
    full ingests of lbnl-chiller no longer replace each other. A dataset already in the store is
    ingested again when its subset or mapping has changed (for example the new lbnl-chiller pump
    roles). A manual download (`lbnl-...
Read more