- implement SQLite and PostgreSQL wrappers to execute benchmark queries - update README to reflect task 09 as implemented - enhance storage size updates to handle (format, variant) replacements - clarify benchmarking methodology for remote PostgreSQL setup
340 lines
12 KiB
Python
340 lines
12 KiB
Python
"""Task 08 - Benchmark query definitions (Q1-Q7 x 5 formats).
|
|
|
|
Writes the runnable query files under ./out/bench/queries/ (SQL for
|
|
SQLite/PostgreSQL, SPARQL text + python drivers for RDF, python drivers
|
|
for CSV/JSON-LD), then produces the canonical expected results: executed
|
|
on PostgreSQL, cross-validated against SQLite row-by-row within 1e-9, and
|
|
stored as ./out/bench/expected/q<N>.rows.csv + q<N>.sha256. CSV, JSON and
|
|
RDF implementations are spot-checked on Q1. Writes ./out/.done/08.ok.
|
|
Spec: docs/specs/08_benchmark_queries.md.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import sqlite3
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import psycopg
|
|
|
|
from common.pg import get_dsn
|
|
from common.pipeline import (
|
|
check_dependencies,
|
|
parse_task_args,
|
|
process_metrics,
|
|
remove_stale_marker,
|
|
write_marker,
|
|
)
|
|
from common.queries import Q1_TRACK_CODE, csv_queries, json_queries, rdf_queries
|
|
from common.results import compare, serialize, sha256_of, sort_rows
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
TASK_ID = "08"
|
|
DEPENDS_ON = ["03", "04", "05", "06", "07"]
|
|
|
|
EXACT_ROW_COUNTS = {1: 1000, 3: 480, 5: 4, 6: 480, 7: 2} # fixed by corpus volumes
|
|
|
|
# --- SQL implementations -------------------------------------------------
|
|
# Q2/Q3/Q4/Q7 use the precomputed track_summary (the relational strength);
|
|
# Q5 must scan raw friction_cycles in every format (spec 08).
|
|
|
|
Q1_SQL = f"""-- Q1: COF vs cycle curve for track {Q1_TRACK_CODE} (point read)
|
|
SELECT fc.cycle, fc.cof
|
|
FROM friction_cycles fc
|
|
JOIN tracks t ON t.track_id = fc.track_id
|
|
WHERE t.track_code = '{Q1_TRACK_CODE}'
|
|
ORDER BY fc.cycle;
|
|
"""
|
|
|
|
Q2_SQL = """-- Q2: steady-state COF per coupon with mean Au = 10 +/- 0.5 wt%
|
|
SELECT c.coupon_code, t.environment, AVG(s.cof_ss_mean) AS cof_ss
|
|
FROM coupons c
|
|
JOIN tracks t ON t.coupon_id = c.coupon_id
|
|
JOIN track_summary s ON s.track_id = t.track_id
|
|
WHERE c.au_wtpct_mean BETWEEN 9.5 AND 10.5
|
|
GROUP BY c.coupon_code, t.environment
|
|
ORDER BY c.coupon_code;
|
|
"""
|
|
|
|
Q3_SQL = """-- Q3: hardness vs steady-state COF per friction coupon, across all batches
|
|
SELECT c.coupon_code,
|
|
b.batch_code,
|
|
(SELECT AVG(n.hardness_gpa) FROM nanoindentation n WHERE n.coupon_id = c.coupon_id) AS hardness_gpa,
|
|
AVG(s.cof_ss_mean) AS cof_ss
|
|
FROM coupons c
|
|
JOIN batches b ON b.batch_id = c.batch_id
|
|
JOIN tracks t ON t.coupon_id = c.coupon_id
|
|
JOIN track_summary s ON s.track_id = t.track_id
|
|
GROUP BY c.coupon_id, c.coupon_code, b.batch_code
|
|
ORDER BY c.coupon_code;
|
|
"""
|
|
|
|
Q4_SQL = """-- Q4: anomaly filter - Dry N2 tracks at 100 mN with cof_ss > 0.20
|
|
SELECT t.track_code, s.cof_ss_mean
|
|
FROM tracks t
|
|
JOIN track_summary s ON s.track_id = t.track_id
|
|
WHERE t.environment = 'dry_n2' AND t.load_mn = 100 AND s.cof_ss_mean > 0.20
|
|
ORDER BY t.track_code;
|
|
"""
|
|
|
|
# Q5: forced full scan over raw cycles; the run-in algorithm is inlined
|
|
# (docs/rules/db-sql-schema.md section 5). SQLite has no stddev built-in,
|
|
# so the 2-sigma band compares squares: (cof-m)^2 < 4*var <=> |cof-m| < 2s.
|
|
Q5_SQLITE = """-- Q5: mean run-in cycles per batch over ALL raw cycle rows (track_summary forbidden)
|
|
WITH tail AS (
|
|
SELECT track_id, AVG(cof) AS m, AVG(cof * cof) - AVG(cof) * AVG(cof) AS var
|
|
FROM friction_cycles
|
|
WHERE cycle >= 501
|
|
GROUP BY track_id
|
|
),
|
|
run_in AS (
|
|
SELECT fc.track_id,
|
|
COALESCE(MIN(CASE WHEN (fc.cof - tl.m) * (fc.cof - tl.m) < 4 * tl.var THEN fc.cycle END), 500) AS run_in
|
|
FROM friction_cycles fc
|
|
JOIN tail tl ON tl.track_id = fc.track_id
|
|
GROUP BY fc.track_id
|
|
)
|
|
SELECT b.batch_code, AVG(r.run_in) AS avg_run_in_cycles
|
|
FROM run_in r
|
|
JOIN tracks t ON t.track_id = r.track_id
|
|
JOIN coupons c ON c.coupon_id = t.coupon_id
|
|
JOIN batches b ON b.batch_id = c.batch_id
|
|
GROUP BY b.batch_code
|
|
ORDER BY b.batch_code;
|
|
"""
|
|
|
|
Q5_PG = """-- Q5: mean run-in cycles per batch over ALL raw cycle rows (track_summary forbidden)
|
|
WITH tail AS (
|
|
SELECT track_id, avg(cof) AS m, stddev_pop(cof) AS s
|
|
FROM friction_cycles
|
|
WHERE cycle >= 501
|
|
GROUP BY track_id
|
|
),
|
|
run_in AS (
|
|
SELECT fc.track_id,
|
|
COALESCE(MIN(fc.cycle) FILTER (WHERE abs(fc.cof - tl.m) < 2 * tl.s), 500) AS run_in
|
|
FROM friction_cycles fc
|
|
JOIN tail tl ON tl.track_id = fc.track_id
|
|
GROUP BY fc.track_id
|
|
)
|
|
SELECT b.batch_code, AVG(r.run_in)::double precision AS avg_run_in_cycles
|
|
FROM run_in r
|
|
JOIN tracks t ON t.track_id = r.track_id
|
|
JOIN coupons c ON c.coupon_id = t.coupon_id
|
|
JOIN batches b ON b.batch_id = c.batch_id
|
|
GROUP BY b.batch_code
|
|
ORDER BY b.batch_code;
|
|
"""
|
|
|
|
Q6_SQL = """-- Q6: mean wear volume vs load per friction coupon
|
|
SELECT c.coupon_code, t.load_mn, AVG(w.wear_volume_um3) AS wear_volume_um3
|
|
FROM wear w
|
|
JOIN tracks t ON t.track_id = w.track_id
|
|
JOIN coupons c ON c.coupon_id = t.coupon_id
|
|
GROUP BY c.coupon_id, c.coupon_code, t.load_mn
|
|
ORDER BY c.coupon_code;
|
|
"""
|
|
|
|
Q7_SQL = """-- Q7: Stribeck-style mean COF by (environment, speed*load bucket) past run-in
|
|
SELECT t.environment,
|
|
CAST(ROUND(t.speed_mm_s * t.load_mn) AS INTEGER) AS speed_load_bucket,
|
|
AVG(fc.cof) AS mean_cof
|
|
FROM friction_cycles fc
|
|
JOIN tracks t ON t.track_id = fc.track_id
|
|
JOIN track_summary s ON s.track_id = fc.track_id
|
|
WHERE fc.cycle > s.run_in_cycles
|
|
GROUP BY t.environment, speed_load_bucket
|
|
ORDER BY t.environment, speed_load_bucket;
|
|
"""
|
|
|
|
SQLITE_SQL = {1: Q1_SQL, 2: Q2_SQL, 3: Q3_SQL, 4: Q4_SQL, 5: Q5_SQLITE, 6: Q6_SQL, 7: Q7_SQL}
|
|
PG_SQL = {1: Q1_SQL, 2: Q2_SQL, 3: Q3_SQL, 4: Q4_SQL, 5: Q5_PG, 6: Q6_SQL, 7: Q7_SQL}
|
|
|
|
RDF_SPARQL_FILES = {
|
|
1: ("q1",), 2: ("q2",), 3: ("q3_hardness", "q3_cycles"),
|
|
4: ("q4",), 5: ("q5",), 6: ("q6",), 7: ("q7",),
|
|
}
|
|
|
|
WRAPPER = '''"""Benchmark query q{n} for the {fmt} format (generated by task 08).
|
|
|
|
Prints the canonical result rows to stdout at full float precision; the
|
|
benchmark runner (task 09) captures and validates them against
|
|
out/bench/expected/q{n}.rows.csv.
|
|
"""
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
from common.queries.{module} import q{n}
|
|
from common.results import print_rows
|
|
|
|
if __name__ == "__main__":
|
|
print_rows(q{n}(REPO_ROOT / "out" / {subpath}))
|
|
'''
|
|
|
|
PY_FORMATS = [
|
|
("csv", "csv_queries", '"csv"'),
|
|
("json", "json_queries", '"json"'),
|
|
("rdf", "rdf_queries", '"rdf" / "oxigraph_store"'),
|
|
]
|
|
|
|
SQLITE_WRAPPER = '''"""Benchmark query q{n} for the sqlite format (generated by task 08).
|
|
|
|
Executes the adjacent q{n}.sql against out/sqlite/tribo.db (read-only) and
|
|
prints the canonical result rows to stdout at full float precision; the
|
|
benchmark runner (task 09) captures and validates them against
|
|
out/bench/expected/q{n}.rows.csv.
|
|
"""
|
|
import sqlite3
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
from common.results import print_rows
|
|
|
|
if __name__ == "__main__":
|
|
sql = Path(__file__).with_suffix(".sql").read_text(encoding="ascii")
|
|
db = REPO_ROOT / "out" / "sqlite" / "tribo.db"
|
|
conn = sqlite3.connect(f"file:{{db.as_posix()}}?mode=ro", uri=True)
|
|
print_rows([tuple(r) for r in conn.execute(sql).fetchall()])
|
|
'''
|
|
|
|
PG_WRAPPER = '''"""Benchmark query q{n} for the pg format (generated by task 08).
|
|
|
|
Executes the adjacent q{n}.sql on the PostgreSQL instance addressed by
|
|
TRIBO_PG_DSN and prints the canonical result rows to stdout at full float
|
|
precision; the benchmark runner (task 09) captures and validates them
|
|
against out/bench/expected/q{n}.rows.csv.
|
|
"""
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
import psycopg
|
|
|
|
from common.pg import get_dsn
|
|
from common.results import print_rows
|
|
|
|
if __name__ == "__main__":
|
|
sql = Path(__file__).with_suffix(".sql").read_text(encoding="ascii")
|
|
with psycopg.connect(get_dsn()) as conn:
|
|
print_rows([tuple(r) for r in conn.execute(sql).fetchall()])
|
|
'''
|
|
|
|
SQL_WRAPPERS = {"sqlite": SQLITE_WRAPPER, "pg": PG_WRAPPER}
|
|
|
|
|
|
def write_query_files(bench_dir: Path) -> int:
|
|
queries_dir = bench_dir / "queries"
|
|
written = 0
|
|
for fmt, module, subpath in PY_FORMATS:
|
|
fmt_dir = queries_dir / fmt
|
|
fmt_dir.mkdir(parents=True, exist_ok=True)
|
|
for n in range(1, 8):
|
|
text = WRAPPER.format(n=n, fmt=fmt, module=module, subpath=subpath)
|
|
(fmt_dir / f"q{n}.py").write_text(text, encoding="ascii", newline="\n")
|
|
written += 1
|
|
for fmt, sql in (("sqlite", SQLITE_SQL), ("pg", PG_SQL)):
|
|
fmt_dir = queries_dir / fmt
|
|
fmt_dir.mkdir(parents=True, exist_ok=True)
|
|
for n, text in sql.items():
|
|
(fmt_dir / f"q{n}.sql").write_text(text, encoding="ascii", newline="\n")
|
|
(fmt_dir / f"q{n}.py").write_text(SQL_WRAPPERS[fmt].format(n=n), encoding="ascii", newline="\n")
|
|
written += 2
|
|
rdf_dir = queries_dir / "rdf"
|
|
for n, keys in RDF_SPARQL_FILES.items():
|
|
text = "\n\n".join(f"# retrieval query: {key}\n{rdf_queries.SPARQL[key]}" for key in keys)
|
|
(rdf_dir / f"q{n}.sparql").write_text(text + "\n", encoding="ascii", newline="\n")
|
|
written += 1
|
|
logger.info("wrote %d query files under %s", written, queries_dir)
|
|
return written
|
|
|
|
|
|
def run_sqlite(db_path: Path) -> dict[int, list[tuple]]:
|
|
conn = sqlite3.connect(db_path)
|
|
try:
|
|
return {n: [tuple(r) for r in conn.execute(sql).fetchall()] for n, sql in SQLITE_SQL.items()}
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def run_pg(dsn: str) -> dict[int, list[tuple]]:
|
|
results: dict[int, list[tuple]] = {}
|
|
with psycopg.connect(dsn) as conn:
|
|
for n, sql in PG_SQL.items():
|
|
results[n] = [tuple(r) for r in conn.execute(sql).fetchall()]
|
|
logger.info("pg q%d: %d rows", n, len(results[n]))
|
|
return results
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_task_args("Task 08: define benchmark queries and canonical expected results")
|
|
out_root: Path = args.out_root
|
|
bench_dir = out_root / "bench"
|
|
try:
|
|
check_dependencies(out_root, DEPENDS_ON)
|
|
remove_stale_marker(out_root, TASK_ID)
|
|
files_written = write_query_files(bench_dir)
|
|
|
|
logger.info("computing canonical results on PostgreSQL")
|
|
pg_rows = run_pg(get_dsn())
|
|
logger.info("cross-validating against SQLite")
|
|
sqlite_rows = run_sqlite(out_root / "sqlite" / "tribo.db")
|
|
for n in range(1, 8):
|
|
diff = compare(pg_rows[n], sqlite_rows[n])
|
|
if diff:
|
|
raise ValueError(f"q{n}: SQLite disagrees with PostgreSQL: {diff}")
|
|
exact = EXACT_ROW_COUNTS.get(n)
|
|
if exact is not None and len(pg_rows[n]) != exact:
|
|
raise ValueError(f"q{n}: {len(pg_rows[n])} rows, expected {exact}")
|
|
if not pg_rows[n]:
|
|
raise ValueError(f"q{n}: empty result set")
|
|
|
|
logger.info("spot-checking csv/json/rdf on Q1")
|
|
for label, rows in (
|
|
("csv", csv_queries.q1(out_root / "csv")),
|
|
("json", json_queries.q1(out_root / "json")),
|
|
("rdf", rdf_queries.q1(out_root / "rdf" / "oxigraph_store")),
|
|
):
|
|
diff = compare(pg_rows[1], rows)
|
|
if diff:
|
|
raise ValueError(f"q1 via {label} disagrees with canonical: {diff}")
|
|
|
|
expected_dir = bench_dir / "expected"
|
|
expected_dir.mkdir(parents=True, exist_ok=True)
|
|
checksums = {}
|
|
for n in range(1, 8):
|
|
rows = sort_rows(pg_rows[n])
|
|
(expected_dir / f"q{n}.rows.csv").write_text(serialize(rows), encoding="ascii", newline="\n")
|
|
checksums[n] = sha256_of(rows)
|
|
(expected_dir / f"q{n}.sha256").write_text(checksums[n] + "\n", encoding="ascii", newline="\n")
|
|
|
|
entries = {"query_files": files_written}
|
|
for n in range(1, 8):
|
|
entries[f"q{n}_rows"] = len(pg_rows[n])
|
|
entries[f"q{n}_sha256"] = checksums[n][:12]
|
|
entries.update(process_metrics())
|
|
marker_path = write_marker(out_root, TASK_ID, entries)
|
|
except Exception:
|
|
logger.critical("task %s failed", TASK_ID, exc_info=True)
|
|
return 1
|
|
|
|
counts = ", ".join(f"q{n}={len(pg_rows[n])}" for n in range(1, 8))
|
|
print(f"task 08 ok: {files_written} query files, canonical results validated (pg == sqlite, 1e-9)")
|
|
print(f"rows: {counts}")
|
|
print(f"expected: {bench_dir / 'expected'} (q<N>.rows.csv + q<N>.sha256)")
|
|
print(f"marker: {marker_path}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|