Using SQL rules outside Open-FDD

The 77 rule SQL files are ordinary PostgreSQL 14+. They use no time_bucket, hypertables or continuous aggregates. Timescale is only the runtime that builds the wide history input, so any database that can build that table can run them.

Quick start (DuckDB, no server)

Checked on generate(seed=0, equips=2, days=3, profile="basic"): FC1, FC2 and CMD-1 match the pandas reference within 1e-6 h. Run from a repo checkout (pip install duckdb pandas numpy pyyaml).

import json, duckdb, pandas as pd
from open_fdd.parity import generate, reference

ds = generate(seed=0, equips=2, days=3, profile="basic")
data = ds.to_engine_input()
pack = json.load(open("nf-app/rules/pack-postgres.json"))   # rule SQL rendered at default parameters

hist = pd.DataFrame(data["history"])
hist["timestamp_utc"] = pd.to_datetime(hist.pop("ts"), unit="ms", utc=True)
con = duckdb.connect()
con.execute("SET TimeZone = 'UTC'")
con.register("hist_df", hist)
con.execute("CREATE TABLE history AS SELECT * FROM hist_df")

ref = reference(ds)
rows = con.execute(pack["rules"]["FC1"]["sql"]).fetchall()   # (equipment_id, fault_hours, covered_hours, fault_pct)
print(rows, {e: v["pandas"] for e, v in ref["FC1"].items()})

open_fdd.parity is missing from the older PyPI releases (4.5.4 and earlier). Until a later release is published, install from git: pip install "open-fdd[oracle] @ git+https://github.com/bbartling/open-fdd".

Parquet replay (DuckDB)

open-fdd nf pull --out history.parquet exports NF history directly to Parquet (with pyarrow), and DuckDB queries .parquet files natively via read_parquet(...) with no database server:

import json, duckdb, pandas as pd
from open_fdd.parity import generate, reference

ds = generate(seed=0, equips=2, days=3, profile="basic")
pack = json.load(open("nf-app/rules/pack-postgres.json"))
hist = pd.DataFrame(ds.to_engine_input()["history"])
hist["timestamp_utc"] = pd.to_datetime(hist.pop("ts"), unit="ms", utc=True)

con = duckdb.connect()
con.execute("SET TimeZone = 'UTC'")
con.register("hist_df", hist)
con.execute("COPY hist_df TO '/tmp/history.parquet' (FORMAT PARQUET)")
con.execute("CREATE TABLE history AS SELECT * FROM read_parquet('/tmp/history.parquet')")

ref = reference(ds)
for eq, fh, _cov, _pct in con.execute(pack["rules"]["FC1"]["sql"]).fetchall():
    assert abs(fh - ref["FC1"][eq]["pandas"]) < 1e-6  # deliberately shifting fh raises AssertionError

Published SI fixture sizes

open_fdd.parity.generate(seed=0, equips, days, profile) produces deterministic SI tables on a 5-minute grid:

Profile Size (equips x days) history rows sensor_history rows weather rows Content hash prefix
basic 2 x 3 1,728 25,920 216 c75a43e4dc93
basic 4 x 3 3,456 51,840 216 608fd164bfa0
basic 8 x 7 16,128 241,920 504 7e2801387b73
edge 2 x 3 1,705 25,575 216 60ed33ab47d3
edge 4 x 3 3,403 51,045 216 998f2b5c0d4e
edge 8 x 7 16,069 241,035 504 ed82a649d58e

Runnable DuckDB and SPARQL tutorial

The standalone DuckDB tutorial includes executable pandas-versus-SQL assertions for FC1, FC2 and CMD-1, plus SPARQL point discovery joined to a DuckDB long-form historian. Both scripts run locally without an NF server. Follow its README for PowerShell or Linux setup.

Setup by database

Database Rule SQL Shims
PostgreSQL 14+ nf-app/rules/pack-postgres.json or sql/fdd/postgres/rules/*.sql Run sql/fdd/postgres/postgres_helpers.sql once (fdd.try_double, fdd.mask_sweep_val and the unit gates); SET TimeZone = 'UTC'
Timescale same as PostgreSQL same; Timescale only supplies history
DuckDB the PostgreSQL pack Define the fdd.* helpers as DuckDB macros (plpgsql does not parse); needed only by the occupancy and sensor-sweep rules; SET TimeZone = 'UTC'
SQLite nf-app/rules/pack-sqlite.json Built in; use the SQLite pack, not the PostgreSQL one

DATE_TRUNC('hour', timestamp_utc) follows the session time zone, so always set UTC.

Input contract

Create the tables from sql/fdd/input_contract.sql (scratch DDL generated from sql/fdd/input_contract.json) and fill them yourself.

Table Content
history One row per equipment_id and timestamp_utc (UTC), one DOUBLE PRECISION column per role
weather As-of weather: timestamp_utc, oa_t, web_oa_t, web_oa_dp
sensor_history Long form for the sensor sweeps: equipment_id, series_id, sensor_role, unit, timestamp_utc, value
utility_interval, utility_monthly Only for the UTIL-* rules

Values are SI: °C, Δ°C, Pa, L/s, kW, kWh; commands and positions 0-1; statuses 0/1; relative humidity 0-100 %RH. Rules never convert units (see the units convention).

If the numbers do not match pandas

Most likely cause first.

# Check Fix
1 Imperial instead of SI, or 0-100 instead of 0-1 fractions Convert to SI before loading
2 Naive timestamps or a non-UTC session zone Store timestamptz in UTC; SET TimeZone = 'UTC'
3 Contract column names (duct_static) vs Haystack names (duct-static-pressure) on the pandas side Map with sql/fdd/role_map.json
4 resample().mean() instead of forward sample-and-hold Hold each sample forward; a gap over 1 h is unknown
5 pandas startup holdoff of 600 s; SQL startup_delay_min defaults to 0 Set the same delay on both sides
6 NULL handling vs fillna(False) Keep unknown as unknown; compare on covered time
7 fault_hours denominator It is eligible hold time, not calendar hours or a row count
8 Weather as-of window, duplicate timestamps, gate order De-duplicate and sort first; SV-RATE uses a different algorithm (known divergence)

Rules that differ on purpose are listed on the parity page.

Seeded data and pandas twins

open_fdd.parity generates the same SI dataset every time and runs the pandas twin of every rule. Use it as the oracle for your database.

Call Use
generate(seed, equips, days, profile) Seeded SI history, weather and model; content_hash() is stable
reference(ds) pandas fault hours per rule and equipment
compare(rows, ref, Policy.default()) Tolerance 1e-6 h plus the known-difference list

Full API, profiles and the adapter interface: parity harness. The pandas cookbook lists every twin.