Merge pull request 'feat(docs): enhance README and build pipeline rules for clarity' (#10) from dev_masha into master

Reviewed-on: #10
This commit was merged in pull request #10.
This commit is contained in:
2026-07-12 11:19:47 -04:00
3 changed files with 893 additions and 28 deletions

View File

@@ -3,7 +3,8 @@
A reproducible pipeline that answers one practical question: **which storage
format should a materials-science laboratory use for its measurement data?**
The pipeline simulates a tribology laboratory (Sandia Pt-Au LDRD context),
The pipeline simulates a tribology laboratory (Pt-Au LDRD context, Sandia
National Laboratories),
generates ~150 MB of physically plausible raw measurement data in CSV,
converts the corpus into four additional storage formats, benchmarks seven
laboratory-standard retrieval scenarios against all five formats, extrapolates
@@ -145,7 +146,8 @@ docs/
rules/ binding conventions (HOW work is done)
research/ measured design studies (e.g. FK key-schema study)
examples/ imported reference materials (not binding)
out/ ALL generated artifacts (git-ignored, reproducible):
out/ ALL generated artifacts (reproducible; bulk git-ignored,
report/ config/ .done/ committed - see .gitignore):
config/ csv/ json/ sqlite/ pg/ rdf/ bench/ report/ .done/
common/ shared helpers (storage_sizes.csv contract, ...)
queries/ Q1-Q7 implementations for csv / json / rdf formats
@@ -163,10 +165,13 @@ report.py task 11 entry script (REPORT.md, charts, tables, scoring)
requirements.txt closed dependency list (docs/rules/code-python-style.md)
```
The `out/` tree - including the ~150 MB CSV corpus, the ~1 GB JSON-LD variant,
databases, and benchmark results - is **excluded from git**. Sources of truth
are the specs, the rules, and the code; every artifact regenerates
deterministically from them. [docs/Initial_Prompt.md](docs/Initial_Prompt.md)
The bulk of the `out/` tree - the ~150 MB CSV corpus, the JSON-LD variants,
databases, stores, archives, and raw benchmark data - is **excluded from
git**; every artifact regenerates deterministically from the specs, the
rules, and the code. A curated results subset IS committed (decision of
2026-07-11): `out/report/` (REPORT.md, charts, tables, diagrams),
`out/config/` (lab_config.yaml, hw_prices.yaml), and the `out/.done/`
markers - see `.gitignore`. [docs/Initial_Prompt.md](docs/Initial_Prompt.md)
is the original prompt the 12 specification files were generated from - kept
for provenance only, never authoritative: where it disagrees with the specs,
rules, or code (e.g. the SQLite key schema, the JSON-LD vocabulary), those win.
@@ -203,10 +208,14 @@ Benchmark execution is planned for a dedicated Linux host.
## Results
This section is populated by tasks 02, 09, 10, and 11. All artifacts land
under `out/` (git-ignored); the placeholders below name the exact files the
pipeline produces, so the section can be filled in by copying the generated
tables and linking the generated images.
Results of the full pipeline run of 2026-07-11 (seed 20260711, 141.8 MiB
corpus, all 35 benchmark cells checksum-valid). The generated sources of
truth are committed under [out/report/](out/report/) - the full decision
document is [out/report/REPORT.md](out/report/REPORT.md); the tables below
are copied from it. An interactive single-file companion (sidebar
navigation, per-query and per-scale views, cost explorer) is
[docs/research/Tribology_Storage_Evaluation.html](docs/research/Tribology_Storage_Evaluation.html).
Bulk measurement files (`out/bench/`) are git-ignored and regenerable.
### Process flow diagrams (task 02)
@@ -214,21 +223,58 @@ tables and linking the generated images.
D1 coupon assembly, D2 characterization and batch assembly, D3 testing tree,
D4 tribometer session sequence, D5 execution loop, D6 data hierarchy.
### Measured results (task 09) - to be generated
### Measured results (task 09)
| Placeholder | Source file |
|---|---|
| Storage footprint per format (measured + compressed) | `out/bench/storage_sizes.csv` |
| Q1-Q7 median wall time / peak RAM / CPU / read MB | `out/bench/results_median.csv` |
Storage footprint per format (working + compressed archival):
### Projections (task 10) - to be generated
| Format | Measured | vs CSV | Archival (compressed) |
|---|---|---|---|
| CSV | 149 MB | 1.00x | 52.8 MB (tar.gz) |
| JSON-LD (full) | 311 MB | 2.09x | 57.2 MB (tar.gz) |
| SQLite | 123 MB | 0.83x | 55.1 MB (gzip) |
| PostgreSQL | 397 MB | 2.67x | 53.1 MB (pg_dump -Fc) |
| RDF triplestore | 1.74 GB | 11.70x | 126 MB (nt.gz) |
| Placeholder | Source file |
|---|---|
| Projected wall time per format at 0.6 / 1.2 / 6 TB with IMPRACTICAL/FAIL flags | `out/bench/extrapolation.csv` |
| Hardware sizing and cost per format at scale | `out/bench/hardware_sizing.csv` |
Median wall time on the 150 MB corpus, warm cache (peak RAM / CPU / read
bytes: [out/report/tables/t2_measured_medians.csv](out/report/tables/t2_measured_medians.csv)):
### Charts (task 11) - to be generated
| Format | Q1 | Q2 | Q3 | Q4 | Q5 | Q6 | Q7 |
|---|---|---|---|---|---|---|---|
| CSV | 0.06 s | 0.17 s | 1.15 s | 0.72 s | 1.09 s | 0.28 s | 1.27 s |
| JSON-LD | 0.11 s | 1.48 s | 9.9 s | 9.9 s | 10 s | 9.8 s | 10 s |
| SQLite | 0.06 s | 0.06 s | 0.11 s | 0.06 s | 0.77 s | 0.06 s | 0.71 s |
| PostgreSQL | 0.22 s | 0.22 s | 0.22 s | 0.22 s | 0.44 s | 0.22 s | 0.33 s |
| RDF triplestore | 0.11 s | 2.81 s | 46 s | 20 s | 60 s | 0.17 s | 41 s |
### Projections (task 10)
Projected wall times at 600 GB - `(!)` = IMPRACTICAL (over 1 hour),
`FAIL` = over 24 hours; 1.2 TB and 6 TB scales:
[out/report/tables/t3_projected_600gb.csv](out/report/tables/t3_projected_600gb.csv)
and `out/bench/extrapolation.csv`:
| Format | Q1 | Q2 | Q3 | Q4 | Q5 | Q6 | Q7 |
|---|---|---|---|---|---|---|---|
| CSV | 0.06 s | 7.1 min | 1.2 h (!) | 44 min | 1.2 h (!) | 15 min | 1.4 h (!) |
| JSON-LD | 0.11 s | 1.5 h (!) | 11 h (!) | 11 h (!) | 11 h (!) | 10.9 h (!) | 11.2 h (!) |
| SQLite | 0.06 s | 1.3 s | 5.7 min | 0.06 s | 47 min | 5.2 s | 44 min |
| PostgreSQL | 0.22 s | 0.22 s | 3.4 s | 11 s | 3.7 min | 4.1 s | 1.9 min |
| RDF triplestore | 0.11 s | 3.0 h (!) | 2.1 d FAIL | 22 h (!) | 2.8 d FAIL | 5.9 min | 1.9 d FAIL |
Hardware sizing at 600 GB - bill of materials with street prices as of
2026-07-11 (component sources: REPORT.md section 7 and
[out/config/hw_prices.yaml](out/config/hw_prices.yaml)):
| Format | Configuration | Total |
|---|---|---|
| CSV, JSON-LD, SQLite, PostgreSQL (each) | 1 x 1U Supermicro AS-1015CS-TNR (16 cores); 1 x 64 GB DDR5 ECC RDIMM ($1,887); 1 x 7.68 TB NVMe U.2 ($3,995) | $10,500 |
| RDF triplestore | 2 nodes; 22 x 64 GB DDR5 ECC RDIMM; 2 x 7.68 TB NVMe U.2 | $58,740 |
At 6 TB PostgreSQL still fits one 16-core node - 9 RDIMM modules, 3 NVMe
drives, $33,586; the RDF triplestore needs 14 nodes and $527,732
(projected; full table: `out/bench/hardware_sizing.csv`).
### Charts (task 11)
![C1 - disk footprint per format, log scale](out/report/charts/c1_disk_footprint.png)
![C2 - RAM requirement per format, log scale](out/report/charts/c2_ram_requirement.png)
@@ -237,8 +283,9 @@ D4 tribometer session sequence, D5 execution loop, D6 data hierarchy.
![C5 - weighted score, stacked bars](out/report/charts/c5_weighted_score.png)
![C6 - cost vs performance at 600 GB, log-log](out/report/charts/c6_cost_vs_performance.png)
(Images appear after running the full pipeline; task 11 writes them to
`out/report/charts/` under exactly these names.)
(Task 11 writes the charts to `out/report/charts/` under exactly these
names; c3 covers all seven queries - `c3_q1..q7_wall_time.png` - and c4 has
three query classes: `point_read`, `indexed`, `full_scan`.)
### Final scoreboard (task 11)

View File

@@ -0,0 +1,812 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Storage Format Evaluation - Tribology Lab Data</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{font-family:"IBM Plex Sans",system-ui,sans-serif;color:#16181d;background:#eef0f2;-webkit-font-smoothing:antialiased}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-thumb{background:#cfd4da;border-radius:6px;border:2px solid #eef0f2}
::-webkit-scrollbar-track{background:transparent}
.mono{font-family:"IBM Plex Mono",monospace}
.app{display:flex;flex-direction:column;height:100vh;width:100%;overflow:hidden;background:#eef0f2}
header.top{display:flex;align-items:center;gap:14px;height:56px;flex:none;padding:0 22px;background:#fff;border-bottom:1px solid #e2e5ea}
.logo{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:7px;background:#16181d;color:#fff;font-family:"IBM Plex Mono",monospace;font-weight:600;font-size:12px}
.htitle{display:flex;flex-direction:column;line-height:1.25;white-space:nowrap}
.htitle b{font-size:14.5px;font-weight:600;letter-spacing:-.01em}
.htitle span{font-size:11.5px;color:#838a95}
.hright{margin-left:auto;display:flex;align-items:center;gap:8px}
.chip{font-family:"IBM Plex Mono",monospace;font-size:11px;color:#838a95;background:#f3f5f7;border:1px solid #e6e8ec;padding:4px 10px;border-radius:6px}
.chip-ok{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:#0f7a45;background:#e7f6ee;padding:4px 10px;border-radius:6px;font-weight:600}
.chip-ok i{width:7px;height:7px;border-radius:7px;background:#18a05a}
.frame{display:flex;flex:1;min-height:0}
nav.side{width:252px;flex:none;background:#fff;border-right:1px solid #e2e5ea;padding:14px 12px;display:flex;flex-direction:column;overflow-y:auto}
.navcap{font-family:"IBM Plex Mono",monospace;font-size:10px;letter-spacing:.1em;color:#a4abb4;padding:6px 11px 8px}
nav.side a{display:flex;align-items:center;gap:11px;width:100%;padding:8px 11px;border-radius:8px;cursor:pointer;text-decoration:none;font-size:13.5px;margin:1px 0;color:#475059;font-weight:500}
nav.side a .tag{width:24px;height:19px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-family:"IBM Plex Mono",monospace;font-size:9px;font-weight:600;flex:none;background:#eceef1;color:#8a909b}
nav.side a.active{background:#eaf1fe;color:#16181d;font-weight:600}
nav.side a.active .tag{background:#1f5fdb;color:#fff}
.sidefoot{margin-top:auto;padding:14px 11px 6px;border-top:1px solid #eef0f2}
.sidefoot .cap{font-family:"IBM Plex Mono",monospace;font-size:10px;letter-spacing:.08em;color:#a4abb4;margin-bottom:8px}
.sidefoot .txt{font-size:12px;color:#6b727c;line-height:1.6}
main{flex:1;min-width:0;overflow-y:auto;padding:26px 30px 60px}
main section{display:none}
main section.on{display:block}
h1{margin:0;font-size:21px;font-weight:600;letter-spacing:-.02em}
.sub{margin:4px 0 18px;font-size:13px;color:#838a95;max-width:900px;line-height:1.55}
.card{background:#fff;border:1px solid #e6e8ec;border-radius:11px;padding:18px 20px;margin-bottom:14px}
.card h3{margin:0 0 10px;font-size:14px;font-weight:600}
.card p{margin:0 0 10px;font-size:13px;color:#3d424b;line-height:1.6;max-width:900px}
.card p:last-child{margin-bottom:0}
.card ul{margin:6px 0 4px;padding-left:20px}
.card li{font-size:13px;color:#3d424b;line-height:1.65;margin-bottom:6px;max-width:860px}
.muted{color:#838a95}
.kpis{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-bottom:16px}
.kpis.four{grid-template-columns:repeat(4,1fr)}
.kpi{background:#fff;border:1px solid #e6e8ec;border-radius:10px;padding:15px 16px}
.kpi .l{font-size:11px;color:#838a95;font-weight:500;text-transform:uppercase;letter-spacing:.04em}
.kpi .v{font-family:"IBM Plex Mono",monospace;font-size:24px;font-weight:600;margin-top:7px;line-height:1}
.kpi .s{font-size:11.5px;color:#a4abb4;margin-top:5px}
.v-bad{color:#b23b4e}.v-good{color:#0f7a45}.v-neutral{color:#16181d}
table.data{width:100%;border-collapse:collapse}
table.data th{font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:#838a95;font-weight:600;text-align:left;padding:9px 12px;background:#f7f8fa;border-bottom:1px solid #eef0f2}
table.data td{font-size:12.5px;color:#3d424b;padding:10px 12px;border-bottom:1px solid #f4f5f7;vertical-align:top}
table.data th.num, table.data td.num{text-align:right;font-family:"IBM Plex Mono",monospace}
table.data td.mono{font-family:"IBM Plex Mono",monospace}
.hbars{display:flex;flex-direction:column;gap:11px;margin-top:6px}
.hbar{display:flex;align-items:center;gap:14px}
.hbar .lab{width:190px;flex:none;font-size:12.5px;color:#3d424b}
.hbar .lab small{display:block;color:#a4abb4;font-size:11px}
.hbar .trk{flex:1;background:#f1f3f6;border-radius:6px;overflow:hidden}
.hbar .fill{height:14px;border-radius:6px;min-width:2px}
.hbar .val{font-family:"IBM Plex Mono",monospace;font-size:12.5px;color:#16181d;width:130px;flex:none;text-align:right}
.legend{display:flex;gap:16px;flex-wrap:wrap;margin:2px 0 10px}
.legend span{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:#5b6472}
.legend i{width:10px;height:10px;border-radius:3px;display:inline-block}
.stack{display:flex;align-items:center;gap:14px;margin-bottom:11px}
.stack .lab{width:190px;flex:none;font-size:12.5px;color:#3d424b}
.stack .trk{flex:1;display:flex;background:#f1f3f6;border-radius:6px;overflow:hidden;height:16px}
.stack .seg{height:16px}
.stack .val{font-family:"IBM Plex Mono",monospace;font-size:12.5px;width:70px;flex:none;text-align:right;font-weight:600}
.tabs{display:inline-flex;gap:4px;background:#eceef1;border-radius:8px;padding:3px;margin:0 0 14px}
.tabs button{border:0;background:transparent;font-family:"IBM Plex Sans",system-ui,sans-serif;font-size:12px;font-weight:600;color:#5b6472;padding:5px 12px;border-radius:6px;cursor:pointer}
.tabs button.on{background:#fff;color:#16181d;box-shadow:0 1px 3px rgba(20,30,50,.08)}
.callout{border-left:3px solid #1f5fdb;background:#f5f8fe;border-radius:0 9px 9px 0;padding:12px 16px;font-size:13px;color:#2b3038;line-height:1.6;margin:12px 0;max-width:900px}
.callout.warn{border-left-color:#b23b4e;background:#fdf3f5}
.callout.good{border-left-color:#18a05a;background:#f0faf4}
.pill{display:inline-flex;align-items:center;padding:2px 9px;border-radius:20px;font-size:10.5px;font-weight:600;white-space:nowrap}
.pill.ok{color:#0f7a45;background:#e7f6ee}
.pill.imp{color:#a06a12;background:#fdf3e0}
.pill.fail{color:#b23b4e;background:#fbe9ec}
.pill.rec{color:#0f7a45;background:#e7f6ee}
.pill.base{color:#5b6472;background:#eef0f2}
pre.env{background:#16181d;color:#dfe3e9;border-radius:9px;padding:14px 16px;font-family:"IBM Plex Mono",monospace;font-size:11.5px;line-height:1.65;overflow-x:auto;margin:10px 0 0}
.tree{font-family:"IBM Plex Mono",monospace;font-size:12px;line-height:1.8;color:#3d424b;background:#f7f8fa;border:1px solid #eef0f2;border-radius:9px;padding:14px 18px;margin-top:8px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:1100px){.grid2{grid-template-columns:1fr}.kpis{grid-template-columns:repeat(2,1fr)}}
.foot{font-size:11.5px;color:#a4abb4;border-top:1px solid #e6e8ec;padding-top:14px;margin-top:30px;line-height:1.7}
.src a, a.ext{color:#1f5fdb;text-decoration:none;word-break:break-all}
.src a:hover, a.ext:hover{text-decoration:underline}
a.chip{text-decoration:none}
a.chip:hover{border-color:#1f5fdb;color:#1f5fdb}
svg text{font-family:"IBM Plex Sans",system-ui,sans-serif}
</style>
</head>
<body>
<div class="app">
<header class="top">
<div class="logo">SE</div>
<div class="htitle">
<b>Storage Format Evaluation</b>
<span>Tribology lab data &middot; CSV vs JSON-LD vs SQLite vs PostgreSQL vs RDF</span>
</div>
<div class="hright">
<a class="chip" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation" target="_blank" rel="noopener">Git repository</a>
<span class="chip">7 queries x 5 formats &middot; 35 cells</span>
<span class="chip">seed 20260711</span>
<span class="chip-ok"><i></i>Measured 2026-07-11 &middot; all checksums OK</span>
</div>
</header>
<div class="frame">
<nav class="side" id="sidenav">
<div class="navcap">NAVIGATE</div>
<a href="#dashboard" class="active"><span class="tag">DB</span><span>Dashboard</span></a>
<a href="#lab"><span class="tag">LM</span><span>Lab Model &amp; Corpus</span></a>
<a href="#method"><span class="tag">ME</span><span>Methodology</span></a>
<a href="#storage"><span class="tag">SF</span><span>Storage Footprint</span></a>
<a href="#measured"><span class="tag">MR</span><span>Measured Results</span></a>
<a href="#proj"><span class="tag">PR</span><span>Projections 0.6-6 TB</span></a>
<a href="#hw"><span class="tag">HW</span><span>Hardware &amp; Cost</span></a>
<a href="#score"><span class="tag">SC</span><span>Scoring &amp; Verdict</span></a>
<a href="#about"><span class="tag">AB</span><span>About</span></a>
<div class="sidefoot">
<div class="cap">CONTEXT</div>
<div class="txt">Final results of the LabDataStorageEvaluation pipeline (tasks 01-11): a simulated Pt-Au tribology laboratory corpus benchmarked across five storage formats. Sources of truth: out/report/ and out/bench/.</div>
<div class="cap" style="margin-top:12px">AUTHOR &middot; SOURCE</div>
<div class="txt">Mary Goncharenko - University of Florida / Sandia National Laboratories.<br>
<a class="ext" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation" target="_blank" rel="noopener">git.boskadoff.com/Public/LabDataStorageEvaluation</a></div>
</div>
</nav>
<main id="main">
<!-- ================= DASHBOARD ================= -->
<section id="dashboard" class="on">
<h1>Dashboard</h1>
<p class="sub">A 141.8 MiB physically plausible CSV corpus (the canonical raw format) was converted into four other storage formats and benchmarked on seven laboratory retrieval scenarios with subprocess-isolated resource metering; results were extrapolated to 600 GB, 1.2 TB and 6 TB. Every measured run validated its rows against canonical results (PostgreSQL cross-validated with SQLite, tolerance 1e-9).</p>
<div class="kpis">
<div class="kpi"><div class="l">Benchmark cells</div><div class="v v-good">35 / 35</div><div class="s">OK; 0 invalid, 0 timeouts</div></div>
<div class="kpi"><div class="l">Corpus</div><div class="v v-neutral">141.8 MiB</div><div class="s">8,718 CSV files, byte-reproducible</div></div>
<div class="kpi"><div class="l">Winner @600 GB</div><div class="v v-good">58.6 / 80</div><div class="s">PostgreSQL weighted score</div></div>
<div class="kpi"><div class="l">Worst blow-up</div><div class="v v-bad">11.7x</div><div class="s">RDF store vs raw CSV size</div></div>
<div class="kpi"><div class="l">Session</div><div class="v v-neutral">916 s</div><div class="s">whole matrix, quiet machine</div></div>
</div>
<div class="callout good"><b>Recommendation (confirmed by data):</b> PostgreSQL as the system of record; CSV retained as the raw archive (best compressor, ~2.8x); hybrid JSON-LD for inter-lab exchange; RDF only as a virtual layer (e.g. Ontop OBDA) over PostgreSQL - a materialized triplestore fails at scale on speed, RAM and cost simultaneously.</div>
<div class="card">
<h3>Typical query time per format (geometric mean Q1-Q7)</h3>
<div id="dash-geo" class="hbars"></div>
<p class="muted" style="margin-top:12px">Left value: measured on the 150 MB corpus. Right value in parentheses: projected at 600 GB. Linear bars scaled within the measured values; every projected number in this document is labeled as projected.</p>
</div>
<div class="card">
<h3>Where each format breaks</h3>
<table class="data">
<thead><tr><th>Format</th><th>Strength</th><th>Breaking point</th><th>@600 GB flags</th></tr></thead>
<tbody id="dash-breaks"></tbody>
</table>
</div>
</section>
<!-- ================= LAB MODEL ================= -->
<section id="lab">
<h1>Lab Model &amp; Corpus</h1>
<p class="sub">A simulated tribology laboratory (Pt-Au LDRD study context, Sandia National Laboratories): Ti-6Al-4V coupons with a sputtered Cr adhesion layer and a Pt-Au composition-gradient coating, tested on a 6-probe tribometer in two environments. All data is generated deterministically from seed 20260711; regeneration is byte-identical (MANIFEST.csv sha256 proof).</p>
<div class="kpis four">
<div class="kpi"><div class="l">Deposition</div><div class="v v-neutral">4 &middot; 12 &middot; 588</div><div class="s">batches &middot; wafers &middot; coupons (7x7 grid)</div></div>
<div class="kpi"><div class="l">Friction testing</div><div class="v v-neutral">1,440</div><div class="s">tracks: 480 coupons x 3 replicates</div></div>
<div class="kpi"><div class="l">Bulk rows</div><div class="v v-neutral">4.32 M</div><div class="s">1.44M COF cycles + 2.88M loop points</div></div>
<div class="kpi"><div class="l">Characterization</div><div class="v v-neutral">235,200</div><div class="s">XRF points + nanoindentation, AFM, profilometry</div></div>
</div>
<div class="grid2">
<div class="card">
<h3>Data hierarchy</h3>
<div class="tree">batch (B721-B724, deposition matrix)
&nbsp;&nbsp;-&gt; wafer (x3 per batch)
&nbsp;&nbsp;&nbsp;&nbsp;-&gt; coupon (x49 per wafer; 480 friction + 108 reserve)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt; xrf_map (400 pts) &middot; nanoindentation (25) &middot; afm &middot; profilometry (100)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt; track (x3, counterface rotation)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt; cof_vs_cycle (1,000 rows) &middot; friction_loops (10x200) &middot; wear</div>
<p style="margin-top:10px">Runs R1-R4 map one-to-one onto batches; two environments (lab air / dry N2). The Q1 benchmark target track is <span class="mono">B722-W2-C13-T2</span>.</p>
</div>
<div class="card">
<h3>Physical models behind the numbers</h3>
<ul>
<li>COF: exponential run-in to a steady state depending on Au wt% and environment; noise floor 0.12.</li>
<li>Hardness / modulus: linear in Au wt% with measurement noise (25 indents per coupon).</li>
<li>Wear: Archard model, k0 = 1.5e-7 (lab air) / 6e-8 (dry N2) mm3/(N*m), log-normal noise.</li>
<li>Au gradient across wafer position; batch-dependent center 8 / 25 / 10 / 60 wt%.</li>
</ul>
<p>Benchmark queries therefore return physically meaningful answers (e.g. Q2 selects the Au 10 +/- 0.5 wt% composition window), not random noise.</p>
</div>
</div>
<div class="card">
<h3>The five storage formats under test</h3>
<table class="data">
<thead><tr><th>Format</th><th>Variant benchmarked</th><th>How Q1-Q7 run</th></tr></thead>
<tbody>
<tr><td><b>CSV</b> (canonical raw)</td><td>hierarchical file tree + MANIFEST</td><td>python csv module, streaming; Q1 = direct path read</td></tr>
<tr><td><b>JSON-LD</b></td><td>FULL (per-coupon files, 588); HYBRID kept for exchange</td><td>ijson event streaming with early exit</td></tr>
<tr><td><b>SQLite</b></td><td>single file, composite natural PKs WITHOUT ROWID, enforced FKs</td><td>SQL; precomputed track_summary for Q2-Q4/Q7</td></tr>
<tr><td><b>PostgreSQL 16</b></td><td>remote host, range-partitioned bulk tables (12+12), BRIN + b-tree</td><td>SQL; parallel scans (4 workers), track_summary matview</td></tr>
<tr><td><b>RDF</b></td><td>pyoxigraph on-disk store, 17,419,713 triples (compact modeling)</td><td>SPARQL; run-in/steady-state derived from raw cycles</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ================= METHODOLOGY ================= -->
<section id="method">
<h1>Methodology</h1>
<p class="sub">The product of this study is its measurements; the protocol is designed so a sloppy number cannot survive it (docs/rules/bench-methodology.md).</p>
<div class="grid2">
<div class="card">
<h3>Measurement protocol</h3>
<ul>
<li><b>One subprocess per run</b> - RSS / CPU / read bytes attribute to exactly that cell; psutil samples the whole process tree every 50 ms (the venv launcher stub alone would report 4 MB).</li>
<li><b>1 warm-up + 3 measured runs</b> per cell; medians reported, min/max kept in raw data.</li>
<li><b>Randomized cell order</b> (deterministic shuffle from the corpus seed); order recorded.</li>
<li><b>30-minute hard timeout</b> per run; a timeout is recorded as data, never retried.</li>
<li><b>Correctness gates speed:</b> every run's rows are compared to the canonical expected results (sorted rows, float tolerance 1e-9); a fast run that fails the checksum is an invalid measurement.</li>
</ul>
</div>
<div class="card">
<h3>Fairness and honesty</h3>
<ul>
<li>Identical result-set contract for all five implementations of each query; no format answers a simplified question.</li>
<li>Each format plays its idiomatic strength (CSV direct path read, SQL indexes, matviews) - but no LIMIT/sampling that changes the answer.</li>
<li><b>Q5 must scan raw cycles in every format</b>; the PostgreSQL EXPLAIN plan is checked to prove track_summary was not used.</li>
<li>Cold-cache pass skipped (Windows host has no page-cache drop) - marked, not faked.</li>
<li>PostgreSQL is remote: client wall times include the LAN round-trip; server-side cost captured via pg_stat_statements deltas and Prometheus host windows.</li>
</ul>
</div>
</div>
<div class="card">
<h3>The seven retrieval scenarios</h3>
<table class="data">
<thead><tr><th>ID</th><th>Scenario</th><th class="num">Result rows</th></tr></thead>
<tbody id="method-queries"></tbody>
</table>
</div>
<div class="card">
<h3>Environment (recorded per session)</h3>
<pre class="env" id="method-env"></pre>
</div>
</section>
<!-- ================= STORAGE ================= -->
<section id="storage">
<h1>Storage Footprint</h1>
<p class="sub">Working footprint is what the query engine reads; archival is the compressed form for shelving. All archival sizes converge to 53-57 MB - the information content is the same, the working overhead is not.</p>
<div class="kpis four">
<div class="kpi"><div class="l">Most compact</div><div class="v v-good">0.83x</div><div class="s">SQLite: 123 MB, indexes included</div></div>
<div class="kpi"><div class="l">PostgreSQL</div><div class="v v-neutral">2.67x</div><div class="s">397 MB live; 34.1% of it is indexes</div></div>
<div class="kpi"><div class="l">RDF store</div><div class="v v-bad">11.70x</div><div class="s">1.74 GB for the same data</div></div>
<div class="kpi"><div class="l">Best archive</div><div class="v v-good">52.8 MB</div><div class="s">csv tree tar.gz (2.8x compression)</div></div>
</div>
<div class="card">
<h3>Working footprint, measured on the 150 MB corpus</h3>
<div id="st-work" class="hbars"></div>
</div>
<div class="grid2">
<div class="card">
<h3>Archival (compressed) footprint</h3>
<div id="st-arch" class="hbars"></div>
<p class="muted" style="margin-top:10px">pg_dump -Fc is internally gzip-compressed; RDF also has a Turtle serialization (596 MB, not the archival form). Hybrid JSON-LD is 1.9 MB of metadata + sourceFile links.</p>
</div>
<div class="card">
<h3>Projected working footprint (linear scaling)</h3>
<table class="data">
<thead><tr><th>Format</th><th class="num">@600 GB</th><th class="num">@1.2 TB</th><th class="num">@6 TB</th></tr></thead>
<tbody id="st-proj"></tbody>
</table>
<p class="muted" style="margin-top:10px">All projected. Storage scales linearly in data volume; coefficients are the measured ratios above.</p>
</div>
</div>
</section>
<!-- ================= MEASURED ================= -->
<section id="measured">
<h1>Measured Results <span class="pill base" style="vertical-align:3px">150 MB corpus &middot; warm cache</span></h1>
<p class="sub">Median of 3 isolated runs per cell. Pick a query and a metric; bars are linear with the actual value on each bar. Client peak RSS below ~50 ms of runtime under-reports (sampling cadence is spec-fixed at 50 ms) - wall times are exact.</p>
<div class="tabs" id="mq-tabs"></div>
<div class="tabs" id="mm-tabs" style="margin-left:10px"></div>
<div class="card">
<h3 id="m-title"></h3>
<div id="m-bars" class="hbars"></div>
</div>
<div class="card">
<h3>Full median matrix - wall time</h3>
<table class="data"><thead id="m-mat-h"></thead><tbody id="m-mat"></tbody></table>
</div>
<div class="card">
<h3>PostgreSQL server side (remote host, 4 cores / 3.8 GiB)</h3>
<table class="data">
<thead><tr><th>Query</th><th class="num">Server exec per call</th><th class="num">Shared blocks hit</th><th class="num">Read from disk</th></tr></thead>
<tbody id="m-pgsrv"></tbody>
</table>
<p class="muted" style="margin-top:10px">pg_stat_statements deltas over the session (4 calls per query). Zero disk reads: the whole 397 MB database sits in the 1 GB shared_buffers. Host peak CPU during the session: 9.2%; RAM delta ~5 MB - the server is nowhere near its limits at this scale.</p>
</div>
</section>
<!-- ================= PROJECTIONS ================= -->
<section id="proj">
<h1>Projections <span class="pill imp" style="vertical-align:3px">every number here is projected, not measured</span></h1>
<p class="sub">Per-cell scaling laws calibrated on the measured 150 MB point after subtracting each format's harness floor (interpreter start, imports, connection). Flags: IMPRACTICAL over 1 hour, FAIL over 24 hours. Pick a target scale.</p>
<div class="tabs" id="p-tabs"></div>
<div class="card">
<h3 id="p-title"></h3>
<table class="data"><thead id="p-mat-h"></thead><tbody id="p-mat"></tbody></table>
<p class="muted" style="margin-top:10px" id="p-note"></p>
</div>
<div class="grid2">
<div class="card">
<h3>Scaling law per access pattern</h3>
<table class="data">
<thead><tr><th>Law</th><th>Growth</th><th>Applied to</th></tr></thead>
<tbody>
<tr><td class="mono">O(1)</td><td>flat</td><td>CSV / JSON-LD Q1 (one file, size fixed)</td></tr>
<tr><td class="mono">O(log n)</td><td>index depth</td><td>SQLite / PostgreSQL / RDF Q1</td></tr>
<tr><td class="mono">O(log n + k)</td><td>~linear (result set grows)</td><td>relational + RDF Q2, Q4</td></tr>
<tr><td class="mono">O(k log n)</td><td>linear x depth</td><td>relational Q3, Q6; RDF Q6</td></tr>
<tr><td class="mono">O(n)</td><td>linear, 1 core</td><td>all CSV/JSON scans; SQLite Q5/Q7; RDF Q3/Q5/Q7</td></tr>
<tr><td class="mono">O(n / cores)</td><td>linear / parallel workers</td><td>PostgreSQL Q5, Q7 (partitioned scan)</td></tr>
</tbody>
</table>
</div>
<div class="card">
<h3>Full-scan degradation (Q5 + Q7 geometric mean)</h3>
<div id="p-degr"></div>
<p class="muted" style="margin-top:8px">First column measured, the rest projected. PostgreSQL rides its parallel partitioned scan; single-threaded engines grow linearly.</p>
</div>
</div>
<div class="callout">PostgreSQL is the only format with all seven queries below one hour at every scale - Q5 at 6 TB projects to 37 minutes on 16 cores. SQLite holds to 600 GB, then its single-threaded scans pass the hour mark. JSON-LD full scans reach 4.6 days at 6 TB; RDF Q5 reaches 27.8 days.</div>
</section>
<!-- ================= HARDWARE ================= -->
<section id="hw">
<h1>Hardware &amp; Cost <span class="pill base" style="vertical-align:3px">street prices as of 2026-07-11</span></h1>
<p class="sub">Bill-of-materials costing: whole 64 GB RDIMM modules, whole 7.68 TB NVMe drives, a priced 1U chassis; disk includes 30% free-space headroom. RAM model: relational hot set = 10% of index size + 4 GB working set; RDF hot set = 20% of the store; streaming formats are flat. Single-node RAM ceiling: 1 TB.</p>
<div class="tabs" id="h-tabs"></div>
<div class="card">
<h3 id="h-title"></h3>
<table class="data">
<thead><tr><th>Format</th><th>Exact configuration</th><th class="num">Chassis</th><th class="num">RAM</th><th class="num">Disk</th><th class="num">Extra nodes</th><th class="num">Total</th></tr></thead>
<tbody id="h-bom"></tbody>
</table>
</div>
<div class="grid2">
<div class="card">
<h3>Total cost comparison</h3>
<div id="h-bars" class="hbars"></div>
</div>
<div class="card">
<h3>Component prices &amp; sources <span class="pill base">retrieved 2026-07-11</span></h3>
<ul class="src">
<li>64 GB DDR5-5600 ECC RDIMM (A-Tech, 2x32GB): <b>$1,887</b><br><a href="https://atechmemory.com/collections/ddr5-memory-ram">atechmemory.com/collections/ddr5-memory-ram</a></li>
<li>7.68 TB NVMe U.2 1DWPD enterprise SSD (Cloud Ninjas): <b>$3,995</b><br><a href="https://cloudninjas.com/products/new-7-68tb-nvme-u-2-1dwpd-sie-2-5-enterprise-solid-state-drive-for-14th-15th-16th-gen-dell">cloudninjas.com/products/new-7-68tb-nvme-u-2-1dwpd...</a></li>
<li>1U AMD EPYC chassis (Broadberry starting configs): AS-1015CS-TNR <b>$4,618</b>, AS-1115CS-TNR <b>$6,272</b><br><a href="https://www.broadberry.com/amd-epyc-9004-supermicro-servers">broadberry.com/amd-epyc-9004-supermicro-servers</a></li>
<li>Market context - server DRAM 2025-2026 price surge:<br><a href="https://www.techpowerup.com/342331/server-dram-pricing-jumps-50-only-70-of-orders-getting-filled">techpowerup.com/342331/server-dram-pricing-jumps-50...</a></li>
</ul>
<p class="muted" style="margin-top:10px">Spot prices during a documented DRAM surge; re-cost by editing out/config/hw_prices.yaml and re-running tasks 10-11.</p>
</div>
</div>
<div class="card">
<h3>Cost vs speed at 600 GB (projected)</h3>
<div id="h-scatter"></div>
</div>
</section>
<!-- ================= SCORING ================= -->
<section id="score">
<h1>Scoring &amp; Verdict</h1>
<p class="sub">Subscores are 10 x best/value per metric, weighted: search speed x5 (0-50, inverse geometric mean of Q1-Q7), RAM economy x2 (0-20), disk economy x1 (0-10); maximum 80. A FAIL projection at 600 GB zeroes the search subscore.</p>
<div class="grid2">
<div class="card">
<h3>Measured scale (150 MB)</h3>
<div class="legend"><span><i style="background:#1f5fdb"></i>search x5</span><span><i style="background:#e0892e"></i>RAM x2</span><span><i style="background:#0f9d8a"></i>disk x1</span></div>
<div id="sc-measured"></div>
<p class="muted" style="margin-top:8px">At laptop scale SQLite is the honest winner: near-instant indexed queries in one file with the smallest footprint.</p>
</div>
<div class="card">
<h3>Projected at 600 GB</h3>
<div class="legend"><span><i style="background:#1f5fdb"></i>search x5</span><span><i style="background:#e0892e"></i>RAM x2</span><span><i style="background:#0f9d8a"></i>disk x1</span></div>
<div id="sc-proj"></div>
<p class="muted" style="margin-top:8px">At production scale the ranking flips: parallel indexed retrieval dominates the weights, and PostgreSQL takes the lead.</p>
</div>
</div>
<div class="card">
<h3>Use-case mapping</h3>
<table class="data">
<thead><tr><th>Use case</th><th>Recommended format</th></tr></thead>
<tbody>
<tr><td>Interactive analysis (joins, aggregations)</td><td><b>PostgreSQL</b>; SQLite acceptable single-user up to ~600 GB</td></tr>
<tr><td>Report generation (repeated summaries)</td><td><b>PostgreSQL</b> (track_summary materialized view)</td></tr>
<tr><td>Search / filtering</td><td><b>PostgreSQL or SQLite</b> (indexed); flat formats need full scans</td></tr>
<tr><td>Archiving</td><td><b>CSV tree + tar.gz</b> (canonical raw) plus pg_dump of the system of record</td></tr>
<tr><td>Inter-lab exchange</td><td><b>Hybrid JSON-LD</b> (metadata + sourceFile links to CSV)</td></tr>
<tr><td>Semantic / ontology queries</td><td><b>Virtual RDF layer</b> over PostgreSQL (e.g. Ontop OBDA), not a materialized triplestore</td></tr>
</tbody>
</table>
</div>
<div class="card">
<h3>Limitations</h3>
<ul>
<li>Single-node measurements on one Windows host + one remote PostgreSQL host; no cluster variance.</li>
<li>Simulated corpus (physically plausible, fixed seed); real instrument data may distribute differently.</li>
<li>Extrapolation is analytic, calibrated on one 150 MB point; no intermediate-scale validation runs.</li>
<li>Warm-cache only (no page-cache drop on Windows); harness floor assumed constant across scales.</li>
<li>RDF numbers reflect compact modeling and client-side derivation of run-in/steady-state; a SPARQL-side aggregation engine could shift, not remove, the scan penalty.</li>
<li>Hardware prices are spot street prices (2026-07-11) during a DRAM price surge.</li>
</ul>
</div>
<div class="foot">
Generated from the LabDataStorageEvaluation pipeline artifacts (run of 2026-07-11, seed 20260711): out/report/REPORT.md, out/bench/results_median.csv, extrapolation.csv, hardware_sizing.csv, storage_sizes.csv, out/config/hw_prices.yaml. The full decision document with charts is out/report/REPORT.md; this page is the interactive companion. Related study: <a class="ext" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation/raw/branch/master/docs/research/Tribology_FK_Architecture.html" target="_blank" rel="noopener">Foreign-Key Architecture Study</a>.
</div>
</section>
<!-- ================= ABOUT ================= -->
<section id="about">
<h1>About</h1>
<p class="sub">Attribution and provenance for this document. It is distributed as a single standalone HTML file; everything needed to verify or reproduce the numbers is referenced below.</p>
<div class="card">
<h3>Author</h3>
<p>This study was conducted by <b>Mary Goncharenko</b>, PhD student at the Tribology Laboratory, University of Florida, Gainesville, FL, during a research internship at the tribology laboratory of Sandia National Laboratories, with an assignment on laboratory ontology development.</p>
<p>The research was carried out with the technical assistance of her father, <b>Vasiliy Goncharenko</b>, Chief Technology Architect at SoftCreator, LLC (enterprise data architecture across relational and semantic stores; 35 years in technology).</p>
<p class="muted">Profiles: <a class="ext" href="https://www.linkedin.com/in/marygoncharenko/" target="_blank" rel="noopener">linkedin.com/in/marygoncharenko</a> &middot; <a class="ext" href="https://linkedin.com/in/vasiliy-goncharenko" target="_blank" rel="noopener">linkedin.com/in/vasiliy-goncharenko</a></p>
</div>
<div class="card">
<h3>Source repository</h3>
<p>The complete evaluation pipeline - task specifications, binding conventions, source code, and the generated report - is publicly available at:</p>
<p class="mono"><a class="ext" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation" target="_blank" rel="noopener">https://git.boskadoff.com/Public/LabDataStorageEvaluation</a></p>
<p>This page is the interactive companion to the repository's <span class="mono">out/report/REPORT.md</span>; all numbers embedded here come from the pipeline run of 2026-07-11. The corpus regenerates byte-identically from seed 20260711 (MANIFEST.csv sha256 proof), so every figure is independently reproducible from the repository alone.</p>
</div>
<div class="card">
<h3>Data statement</h3>
<p>The measurement corpus is simulated: physically plausible models of a Pt-Au LDRD tribology study context at Sandia National Laboratories, generated from a fixed random seed. It contains no experimental measurements and no export-controlled data. Hardware prices cited in the cost model are public street prices retrieved 2026-07-11 (sources in the Hardware &amp; Cost section).</p>
<p>Related study in the same repository: <a class="ext" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation/raw/branch/master/docs/research/Tribology_FK_Architecture.html" target="_blank" rel="noopener">Foreign-Key Architecture Study</a>.</p>
</div>
<div class="foot">Standalone distributable file. If you received this document outside the repository, the canonical source is <a class="ext" href="https://git.boskadoff.com/Public/LabDataStorageEvaluation" target="_blank" rel="noopener">git.boskadoff.com/Public/LabDataStorageEvaluation</a>.</div>
</section>
</main>
</div>
</div>
<script>
"use strict";
/* ---------- embedded data (from the 2026-07-11 pipeline run) ---------- */
const FMT = ["csv","json","sqlite","pg","rdf"];
const NAME = {csv:"CSV", json:"JSON-LD", sqlite:"SQLite", pg:"PostgreSQL", rdf:"RDF triplestore"};
const COLOR = {csv:"#9aa6b6", json:"#e0892e", sqlite:"#0f9d8a", pg:"#1f5fdb", rdf:"#c0476b"};
const QN = [1,2,3,4,5,6,7];
const QDESC = {
1:["COF vs cycle curve for one track (B722-W2-C13-T2)",1000],
2:["Steady-state COF for coupons with mean Au = 10 +/- 0.5 wt%",33],
3:["Hardness vs steady-state COF across all batches",480],
4:["Anomaly filter: dry N2, 100 mN, cof_ss > 0.20",469],
5:["Mean run-in cycles per batch over ALL raw cycles (no summaries)",4],
6:["Wear volume vs load per coupon",480],
7:["Stribeck-style mean COF by (environment, speed x load)",2]
};
/* measured medians, per format: [q1..q7] */
const WALL = {
csv:[0.06,0.1661,1.1521,0.715,1.0948,0.2756,1.2735],
json:[0.1113,1.4788,9.9408,9.9218,9.9623,9.7927,10.0781],
sqlite:[0.0617,0.0607,0.1134,0.0604,0.7659,0.0612,0.7094],
pg:[0.2214,0.2206,0.2211,0.2232,0.4405,0.2212,0.3342],
rdf:[0.1131,2.8054,46.1273,19.8245,59.5884,0.1682,40.5534]
};
const RSS = {
csv:[3.9,20.5,21.1,20.9,20.7,20.2,21.0],
json:[22.2,23.9,24.4,24.5,24.4,24.1,24.1],
sqlite:[3.9,3.9,4.1,3.9,25.0,3.9,25.0],
pg:[42.2,42.1,42.0,41.9,42.0,41.6,42.1],
rdf:[31.6,85.4,263.9,167.9,264.0,45.9,263.6]
};
const READ = {
csv:[0,2.45,31.76,16.82,31.78,1.23,32.47],
json:[1.4,53.08,297.57,297.75,297.57,297.82,297.94],
sqlite:[0,0,0,0,117.91,0,92.97],
pg:[4.08,4.08,4.08,4.08,4.08,4.08,4.08],
rdf:[3.62,375.56,5445.1,2694.56,5390.46,15.92,5380.51]
};
/* projections: scale -> format -> [[seconds,flag] x7]; flag "" | "I" | "F" */
const PROJ = {
600: {
csv:[[0.06,""],[428.301,""],[4408.0,"I"],[2643.775,""],[4176.725,"I"],[870.266,""],[4897.995,"I"]],
json:[[0.111,""],[5519.622,"I"],[39673.993,"I"],[39597.306,"I"],[39760.772,"I"],[39076.231,"I"],[40228.164,"I"]],
sqlite:[[0.062,""],[1.271,""],[339.238,""],[0.06,""],[2847.603,""],[5.18,""],[2619.558,""]],
pg:[[0.222,""],[0.221,""],[3.42,""],[10.715,""],[222.111,""],[4.06,""],[114.849,""]],
rdf:[[0.113,""],[10866.789,"I"],[185722.881,"F"],[79559.375,"I"],[240054.647,"F"],[352.729,""],[163225.475,"F"]]
},
1200: {
csv:[[0.06,""],[856.543,""],[8815.94,"I"],[5287.489,"I"],[8353.391,"I"],[1740.472,""],[9795.931,"I"]],
json:[[0.111,""],[11039.133,"I"],[79347.876,"I"],[79194.5,"I"],[79521.432,"I"],[78152.351,"I"],[80456.218,"I"]],
sqlite:[[0.063,""],[2.482,""],[699.328,""],[0.06,""],[5695.146,"I"],[10.615,""],[5239.055,"I"]],
pg:[[0.222,""],[0.221,""],[6.817,""],[21.209,""],[444.001,""],[8.137,""],[229.477,""]],
rdf:[[0.113,""],[21733.465,"I"],[371445.649,"F"],[159118.636,"F"],[480109.181,"F"],[727.088,""],[326450.837,"F"]]
},
6000: {
csv:[[0.06,""],[4282.474,"I"],[44079.46,"I"],[26437.206,"I"],[41766.715,"I"],[8702.119,"I"],[48979.414,"I"]],
json:[[0.111,""],[55195.222,"I"],[396738.933,"F"],[395972.054,"F"],[397606.717,"F"],[390761.312,"F"],[402280.643,"F"]],
sqlite:[[0.063,""],[12.169,""],[3739.195,"I"],[0.06,""],[28475.49,"I"],[56.5,""],[26195.034,"I"]],
pg:[[0.222,""],[0.221,""],[35.495,""],[105.162,""],[2219.125,""],[42.55,""],[1146.503,""]],
rdf:[[0.113,""],[108666.874,"F"],[1857227.792,"F"],[795592.727,"F"],[2400545.453,"F"],[3887.402,"I"],[1632253.733,"F"]]
}
};
/* storage bytes */
const WORK = {csv:148654472, json:311300222, sqlite:123400192, pg:397467648, rdf:1739414085};
const ARCH = {csv:[52827471,"tar.gz of the tree"], json:[57162674,"tar.gz (full + hybrid)"], sqlite:[55149977,"gzip of tribo.db"], pg:[53054380,"pg_dump -Fc"], rdf:[126487650,"N-Triples gz"]};
const COEFF = {csv:1.0, json:2.09, sqlite:0.83, pg:2.67, rdf:11.70};
/* hardware sizing: scale -> format -> {modules,drives,nodes,cores,base,ram,disk,extra,total} */
const HW = {
600: {
csv:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
json:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
sqlite:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
pg:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
rdf:{m:22,d:2,n:2,c:16,base:4618,ram:41514,disk:7990,extra:4618,total:58740}
},
1200: {
csv:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
json:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
sqlite:{m:1,d:1,n:1,c:16,base:4618,ram:1887,disk:3995,extra:0,total:10500},
pg:{m:2,d:1,n:1,c:16,base:4618,ram:3774,disk:3995,extra:0,total:12387},
rdf:{m:44,d:3,n:3,c:16,base:4618,ram:83028,disk:11985,extra:9236,total:108867}
},
6000: {
csv:{m:1,d:2,n:1,c:16,base:4618,ram:1887,disk:7990,extra:0,total:14495},
json:{m:1,d:3,n:1,c:16,base:4618,ram:1887,disk:11985,extra:0,total:18490},
sqlite:{m:3,d:1,n:1,c:16,base:4618,ram:5661,disk:3995,extra:0,total:14274},
pg:{m:9,d:3,n:1,c:16,base:4618,ram:16983,disk:11985,extra:0,total:33586},
rdf:{m:220,d:12,n:14,c:16,base:4618,ram:415140,disk:47940,extra:60034,total:527732}
}
};
/* weighted scores: [search, ram, disk, total] */
const SCORES = {
measured:{csv:[15.5,20.0,8.3,43.8], json:[1.7,17.2,4.0,22.9], sqlite:[50.0,16.9,10.0,76.9], pg:[26.2,10.0,3.1,39.3], rdf:[1.3,1.6,0.7,3.6]},
proj:{csv:[0.6,20.0,8.3,28.9], json:[0.1,20.0,4.0,24.0], sqlite:[21.8,15.2,10.0,47.0], pg:[50.0,5.5,3.1,58.6], rdf:[0.0,0.2,0.7,0.9]}
};
/* pg server per-call exec ms + shared blocks (session deltas / 4 calls) */
const PGSRV = {1:[0.42,72],2:[0.43,1312],3:[4.92,6500],4:[1.09,140],5:[237.26,189692],6:[1.68,172],7:[107.14,32456]};
const ENVIRONMENT = [
"benchmark_host_os: Windows-2025Server-10.0.26100-SP0",
"benchmark_host_cpu: Intel64 Family 6 Model 141 (4 cores physical/logical), 8 GiB RAM",
"benchmark_host_disks: VMware Virtual NVMe Disk / SSD / 100 GB",
"python: 3.12.10 &middot; sqlite 3.49.1 &middot; psycopg 3.3.4 &middot; pyoxigraph 0.5.9 &middot; ijson 3.5.1 &middot; psutil 7.2.2",
"postgresql_server: PostgreSQL 16.14 (Ubuntu 24.04), remote host, 4 cores / 3.8 GiB",
"pg tuning: shared_buffers=1GB &middot; work_mem=32MB &middot; effective_cache_size=2500MB &middot; max_parallel_workers_per_gather=4",
"cold_cache_pass: skipped (Windows host, no page-cache drop)",
"pg_backend_sampling: remote server - Prometheus windows + pg_stat_statements deltas instead"
].join("\n");
/* ---------- formatting helpers ---------- */
function fmtTime(s){
if(s < 10) return s.toFixed(2) + " s";
if(s < 120) return s.toFixed(0) + " s";
if(s < 7200) return (s/60).toFixed(1) + " min";
if(s < 172800) return (s/3600).toFixed(1) + " h";
return (s/86400).toFixed(1) + " d";
}
function fmtSize(b){
if(b >= 1e12) return (b/1e12).toFixed(2) + " TB";
if(b >= 1e9) return (b/1e9).toFixed(2) + " GB";
return (b/1e6).toFixed(0) + " MB";
}
function fmtMoney(v){ return "$" + v.toLocaleString("en-US"); }
function esc(t){ return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;"); }
function el(id){ return document.getElementById(id); }
function flagPill(f){
if(f === "F") return ' <span class="pill fail">FAIL</span>';
if(f === "I") return ' <span class="pill imp">&gt; 1 h</span>';
return "";
}
function geomean(a){ return Math.exp(a.reduce((s,v)=>s+Math.log(v),0)/a.length); }
/* one horizontal bar row; linear scale against max */
function barRow(label, sub, frac, color, val){
return '<div class="hbar"><div class="lab">' + label + (sub ? '<small>' + sub + '</small>' : '') + '</div>' +
'<div class="trk"><div class="fill" style="width:' + Math.max(0.4, frac*100).toFixed(2) + '%;background:' + color + '"></div></div>' +
'<div class="val">' + val + '</div></div>';
}
function tabs(containerId, items, onPick){
const box = el(containerId);
box.innerHTML = items.map((t,i) => '<button data-i="' + i + '"' + (i===0 ? ' class="on"' : '') + '>' + t + '</button>').join("");
box.querySelectorAll("button").forEach(b => b.addEventListener("click", () => {
box.querySelectorAll("button").forEach(x => x.classList.remove("on"));
b.classList.add("on");
onPick(parseInt(b.dataset.i,10));
}));
}
/* ---------- navigation ---------- */
const navLinks = document.querySelectorAll("nav.side a");
function show(id){
document.querySelectorAll("main section").forEach(s => s.classList.toggle("on", s.id === id));
navLinks.forEach(a => a.classList.toggle("active", a.getAttribute("href") === "#" + id));
el("main").scrollTop = 0;
}
navLinks.forEach(a => a.addEventListener("click", e => {
e.preventDefault();
const id = a.getAttribute("href").slice(1);
show(id);
history.replaceState(null, "", "#" + id);
}));
if(location.hash && document.getElementById(location.hash.slice(1))) show(location.hash.slice(1));
/* ---------- dashboard ---------- */
(function(){
const rows = FMT.map(f => {
const g150 = geomean(WALL[f]);
const g600 = geomean(PROJ[600][f].map(x => x[0]));
return [NAME[f], "geometric mean of Q1-Q7", g150, fmtTime(g150) + ' <span class="muted">(' + fmtTime(g600) + ' @600 GB)</span>', COLOR[f]];
});
const max = Math.max(...rows.map(r => r[2]));
el("dash-geo").innerHTML = rows.map(r => barRow(r[0], r[1], r[2]/max, r[4], r[3])).join("");
const breaks = {
csv:["Direct path reads; canonical archive; best compression","Full scans pass 1 h just above 600 GB"],
json:["Self-describing exchange files; hybrid variant is 1.9 MB","Every scan reads the whole corpus: hours at 600 GB"],
sqlite:["Fastest at 150 MB; one file; smallest footprint","Single-threaded scans pass 1 h at ~1.2 TB"],
pg:["Parallel partitioned scans + matviews; flat 0.2-0.4 s today","None found up to 6 TB (Q5 = 37 min)"],
rdf:["Native semantics, SPARQL","11.7x disk, 20% hot set in RAM, scans measured in days"]
};
el("dash-breaks").innerHTML = FMT.map(f => {
const fl = PROJ[600][f];
const nI = fl.filter(x => x[1]==="I").length, nF = fl.filter(x => x[1]==="F").length;
let flags = (nI+nF)===0 ? '<span class="pill ok">all 7 OK</span>' :
(nI ? '<span class="pill imp">' + nI + ' x &gt;1 h</span> ' : '') + (nF ? '<span class="pill fail">' + nF + ' x FAIL</span>' : '');
return "<tr><td><b>" + NAME[f] + "</b></td><td>" + breaks[f][0] + "</td><td>" + breaks[f][1] + "</td><td>" + flags + "</td></tr>";
}).join("");
})();
/* ---------- methodology ---------- */
el("method-queries").innerHTML = QN.map(q =>
'<tr><td class="mono">Q' + q + '</td><td>' + QDESC[q][0] + '</td><td class="num">' + QDESC[q][1].toLocaleString("en-US") + '</td></tr>').join("");
el("method-env").innerHTML = ENVIRONMENT;
/* ---------- storage ---------- */
(function(){
const workMax = WORK.rdf;
el("st-work").innerHTML = FMT.map(f =>
barRow(NAME[f], COEFF[f].toFixed(2) + "x vs raw CSV", WORK[f]/workMax, COLOR[f], fmtSize(WORK[f]))).join("");
const archMax = ARCH.rdf[0];
el("st-arch").innerHTML = FMT.map(f =>
barRow(NAME[f], ARCH[f][1], ARCH[f][0]/archMax, COLOR[f], fmtSize(ARCH[f][0]))).join("");
el("st-proj").innerHTML = FMT.map(f =>
"<tr><td><b>" + NAME[f] + "</b></td>" + [600,1200,6000].map(s =>
'<td class="num">' + fmtSize(COEFF[f]*s*1e9) + "</td>").join("") + "</tr>").join("");
})();
/* ---------- measured results ---------- */
(function(){
const METRICS = [
["Wall time", WALL, fmtTime],
["Client peak RSS", RSS, v => v.toFixed(0) + " MB"],
["Read bytes", READ, v => v >= 1000 ? (v/1000).toFixed(2) + " GB" : v.toFixed(1) + " MB"]
];
let q = 0, m = 0;
function render(){
const [mname, data, fmt] = METRICS[m];
el("m-title").textContent = "Q" + QN[q] + " - " + QDESC[QN[q]][0] + " - " + mname.toLowerCase() + " (median of 3 runs)";
const max = Math.max(...FMT.map(f => data[f][q])) || 1;
el("m-bars").innerHTML = FMT.map(f =>
barRow(NAME[f], "", (data[f][q])/max, COLOR[f], fmt(data[f][q]))).join("");
}
tabs("mq-tabs", QN.map(n => "Q" + n), i => { q = i; render(); });
tabs("mm-tabs", METRICS.map(x => x[0]), i => { m = i; render(); });
render();
el("m-mat-h").innerHTML = "<tr><th>Format</th>" + QN.map(n => '<th class="num">Q' + n + "</th>").join("") + "</tr>";
el("m-mat").innerHTML = FMT.map(f =>
"<tr><td><b>" + NAME[f] + "</b></td>" + WALL[f].map(v => '<td class="num">' + fmtTime(v) + "</td>").join("") + "</tr>").join("");
el("m-pgsrv").innerHTML = QN.map(n =>
'<tr><td class="mono">Q' + n + "</td><td class=\"num\">" + PGSRV[n][0].toFixed(2) + " ms</td><td class=\"num\">" +
PGSRV[n][1].toLocaleString("en-US") + '</td><td class="num">0</td></tr>').join("");
})();
/* ---------- projections ---------- */
(function(){
const SCALES = [600, 1200, 6000];
const SLBL = {600:"600 GB", 1200:"1.2 TB", 6000:"6 TB"};
function render(scale){
el("p-title").textContent = "Projected wall times at " + SLBL[scale] + " of raw data";
el("p-mat-h").innerHTML = "<tr><th>Format</th>" + QN.map(n => '<th class="num">Q' + n + "</th>").join("") + "</tr>";
el("p-mat").innerHTML = FMT.map(f =>
"<tr><td><b>" + NAME[f] + "</b></td>" + PROJ[scale][f].map(x =>
'<td class="num">' + fmtTime(x[0]) + flagPill(x[1]) + "</td>").join("") + "</tr>").join("");
const total = FMT.reduce((acc,f) => {
PROJ[scale][f].forEach(x => { if(x[1]==="I") acc.i++; if(x[1]==="F") acc.f++; });
return acc;
}, {i:0, f:0});
el("p-note").textContent = "Flags at this scale: " + total.i + " x IMPRACTICAL (over 1 hour), " + total.f +
" x FAIL (over 24 hours), " + (35 - total.i - total.f) + " x OK. Scaling ratio vs the measured corpus: x" +
Math.round(scale*1e9/WORK.csv).toLocaleString("en-US") + ".";
}
tabs("p-tabs", SCALES.map(s => SLBL[s]), i => render(SCALES[i]));
render(600);
/* degradation mini-table: full scan class geometric mean */
const cols = [["measured 150 MB", f => geomean([WALL[f][4], WALL[f][6]])]]
.concat(SCALES.map(s => ["projected " + SLBL[s], f => geomean([PROJ[s][f][4][0], PROJ[s][f][6][0]])]));
let html = '<table class="data"><thead><tr><th>Format</th>' +
cols.map(c => '<th class="num">' + c[0] + "</th>").join("") + "</tr></thead><tbody>";
html += FMT.map(f => "<tr><td><b>" + NAME[f] + "</b></td>" +
cols.map(c => '<td class="num">' + fmtTime(c[1](f)) + "</td>").join("") + "</tr>").join("");
html += "</tbody></table>";
el("p-degr").innerHTML = html;
})();
/* ---------- hardware ---------- */
(function(){
const SCALES = [600, 1200, 6000];
const SLBL = {600:"600 GB", 1200:"1.2 TB", 6000:"6 TB"};
function config(h){
return h.n + " x 1U Supermicro AS-1015CS-TNR (" + h.c + " cores) &middot; " +
h.m + " x 64 GB DDR5 ECC RDIMM &middot; " + h.d + " x 7.68 TB NVMe U.2";
}
function render(scale){
el("h-title").textContent = "Exact configuration and cost breakdown at " + SLBL[scale] + " (projected sizing)";
el("h-bom").innerHTML = FMT.map(f => {
const h = HW[scale][f];
return "<tr><td><b>" + NAME[f] + "</b></td><td>" + config(h) + "</td>" +
[h.base, h.ram, h.disk, h.extra, h.total].map((v,i) =>
'<td class="num"' + (i===4 ? ' style="font-weight:600"' : '') + ">" + fmtMoney(v) + "</td>").join("") + "</tr>";
}).join("");
const max = Math.max(...FMT.map(f => HW[scale][f].total));
el("h-bars").innerHTML = FMT.map(f => {
const h = HW[scale][f];
return barRow(NAME[f], h.n > 1 ? h.n + " nodes" : "single node", h.total/max, COLOR[f], fmtMoney(h.total));
}).join("");
scatter(scale);
}
function scatter(scale){
const W = 860, H = 340, L = 70, R = 30, T = 20, B = 46;
const pts = FMT.map(f => ({f, cost: HW[scale][f].total, geo: geomean(PROJ[scale][f].map(x => x[0]))}));
const maxC = Math.max(...pts.map(p => p.cost)) * 1.15;
const maxG = Math.max(...pts.map(p => p.geo)) * 1.15;
const X = c => L + (W-L-R) * c / maxC;
const Y = g => T + (H-T-B) * (1 - g / maxG);
let s = '<svg viewBox="0 0 ' + W + ' ' + H + '" style="width:100%;max-width:900px">';
/* axes + linear ticks in real units */
for(let i = 1; i <= 4; i++){
const c = maxC * i / 4, x = X(c);
s += '<line x1="' + x + '" y1="' + T + '" x2="' + x + '" y2="' + (H-B) + '" stroke="#eef0f2"/>';
s += '<text x="' + x + '" y="' + (H-B+18) + '" font-size="11" fill="#838a95" text-anchor="middle">$' + Math.round(c/1000) + 'k</text>';
const g = maxG * i / 4, y = Y(g);
s += '<line x1="' + L + '" y1="' + y + '" x2="' + (W-R) + '" y2="' + y + '" stroke="#eef0f2"/>';
s += '<text x="' + (L-8) + '" y="' + (y+4) + '" font-size="11" fill="#838a95" text-anchor="end">' + fmtTime(g) + '</text>';
}
s += '<line x1="' + L + '" y1="' + (H-B) + '" x2="' + (W-R) + '" y2="' + (H-B) + '" stroke="#cfd4da"/>';
s += '<line x1="' + L + '" y1="' + T + '" x2="' + L + '" y2="' + (H-B) + '" stroke="#cfd4da"/>';
s += '<text x="' + ((L+W-R)/2) + '" y="' + (H-6) + '" font-size="11.5" fill="#5b6472" text-anchor="middle">estimated hardware cost (bill of materials, linear)</text>';
s += '<text x="14" y="' + ((T+H-B)/2) + '" font-size="11.5" fill="#5b6472" text-anchor="middle" transform="rotate(-90 14 ' + ((T+H-B)/2) + ')">typical query time, projected (linear)</text>';
/* better = lower-left cue */
s += '<text x="' + (L+16) + '" y="' + (H-B-10) + '" font-size="11.5" fill="#0f7a45" font-weight="600">better: lower-left</text>';
const OFF = {csv:[12,-8], json:[12,4], sqlite:[12,14], pg:[12,-6], rdf:[-8,-12]};
const ANCH = {csv:"start", json:"start", sqlite:"start", pg:"start", rdf:"end"};
pts.forEach(p => {
s += '<circle cx="' + X(p.cost) + '" cy="' + Y(p.geo) + '" r="7" fill="' + COLOR[p.f] + '"/>';
s += '<text x="' + (X(p.cost)+OFF[p.f][0]) + '" y="' + (Y(p.geo)+OFF[p.f][1]) + '" font-size="11.5" fill="#3d424b" text-anchor="' + ANCH[p.f] + '">' +
NAME[p.f] + ': ' + fmtMoney(p.cost) + ', ' + fmtTime(p.geo) + '</text>';
});
s += "</svg>";
el("h-scatter").innerHTML = s;
}
tabs("h-tabs", SCALES.map(s => SLBL[s]), i => render(SCALES[i]));
render(600);
})();
/* ---------- scoring ---------- */
function renderScores(containerId, board){
const order = FMT.slice().sort((a,b) => board[b][3] - board[a][3]);
el(containerId).innerHTML = order.map(f => {
const [se, ra, di, total] = board[f];
return '<div class="stack"><div class="lab">' + NAME[f] + '</div><div class="trk">' +
'<div class="seg" style="width:' + (se/80*100) + '%;background:#1f5fdb"></div>' +
'<div class="seg" style="width:' + (ra/80*100) + '%;background:#e0892e"></div>' +
'<div class="seg" style="width:' + (di/80*100) + '%;background:#0f9d8a"></div>' +
'</div><div class="val">' + total.toFixed(1) + '</div></div>';
}).join("");
}
renderScores("sc-measured", SCORES.measured);
renderScores("sc-proj", SCORES.proj);
</script>
</body>
</html>

View File

@@ -31,10 +31,14 @@ out/
- Tasks communicate exclusively through these artifacts - never through
shared in-process state
(see [code-python-style.md](code-python-style.md)).
- **`./out/` is git-ignored** (except nothing - the whole tree). Sources
of truth are the specs, the rules, and the code; artifacts are
reproducible from them ([data-determinism.md](data-determinism.md)).
Never commit generated artifacts "for convenience".
- **The bulk of `./out/` is git-ignored.** Sources of truth are the
specs, the rules, and the code; artifacts are reproducible from them
([data-determinism.md](data-determinism.md)). A curated results subset
IS committed (owner decision, 2026-07-11): `out/report/` (REPORT.md,
charts, tables, diagrams), `out/config/` (lab_config.yaml,
hw_prices.yaml), and the `out/.done/` markers - exactly the exceptions
listed in `.gitignore`. Never commit the bulk artifacts (corpus,
databases, stores, archives, raw benchmark data) "for convenience".
## 2. Completion markers
@@ -92,7 +96,9 @@ out/
- **Writing an artifact outside `./out/`** or a task writing into another
task's output directory.
- **Committing `./out/` content to git.**
- **Committing bulk `./out/` content to git** (corpus, databases, stores,
archives, raw benchmark data) - only the curated subset in `.gitignore`
(`report/`, `config/`, `.done/`) is committed.
- **A marker written on partial success** - see
[code-error-handling.md](code-error-handling.md).
- **Running benchmarks while converters are still running.**