jzxiao commited on
Commit
43d6fa3
·
verified ·
1 Parent(s): 3b49d32

Average compression times over measured columns

Browse files

Compute Compress and Decompress ns/point independently for each measured column, then use an equal-column arithmetic mean across the selected datasets. Preserve compression rates, sizes, configurations, and dataset coverage. Keep aggregate-only historical timings unavailable. Update leaderboard scores, ranks, metric labels, charts, details, and CSV exports.

Files changed (9) hide show
  1. README.md +6 -4
  2. app.js +21 -16
  3. apply_dataset_scope.py +4 -3
  4. build_ui_payload.py +1 -1
  5. data.json +2 -2
  6. export_results.py +94 -33
  7. index.html +9 -9
  8. manifest.json +17 -3
  9. ui-data.json +0 -0
README.md CHANGED
@@ -6,7 +6,7 @@ colorTo: gray
6
  sdk: static
7
  app_file: index.html
8
  pinned: false
9
- short_description: Compression rates and encoding/decoding costs
10
  ---
11
 
12
  # THULab Time Series Compression Benchmark
@@ -24,6 +24,8 @@ python3 deploy/huggingface-compressionbenchmark/export_results.py
24
  python3 deploy/huggingface-compressionbenchmark/apply_dataset_scope.py
25
  # Reapply verified measurement updates before packing the browser payload.
26
  python3 scripts/integrate_dbp_leaderboard.py --measurements /path/to/frozen-measurements.json --expected-sha256 VERIFIED_SHA256 --receipt /path/to/integration-receipt.json
 
 
27
  python3 deploy/huggingface-compressionbenchmark/build_ui_payload.py
28
  ```
29
 
@@ -35,7 +37,7 @@ Only measurements in the authoritative current project scope are exported. Missi
35
 
36
  - Overall and per-dataset leaderboards with runtime, data-type, precision, family, and method filters
37
  - Sortable metrics, optional columns, and CSV download of the selected ranking
38
- - Compression rate versus encoding/decoding cost plots
39
  - Dataset-level method details, snapshot provenance, and metric definitions
40
  - Searchable 169-dataset catalog grouped into six source categories, with explicit measurement availability and source links
41
  - Overall composite score and average dataset Rank, with 66 valid non-negative one-decimal weight combinations, including zero weights
@@ -43,8 +45,8 @@ Only measurements in the authoritative current project scope are exported. Missi
43
 
44
  The summary dataset count and Dataset coverage denominator use the same measured dataset set for the selected runtime, data type, and dataset selection; the default Overall view has 164 measured datasets. All 169 catalog entries remain searchable and selectable in the dataset browser. Dataset coverage can differ by method and runtime; unavailable combinations show **No published measurements**, remain excluded from rankings, and are never converted to zero-valued results. The default family view selects the best measured compression-rate configuration independently for each measured dataset. Switch to **Exact configuration** to hold a configuration fixed. Runtime results stay separate.
45
 
46
- The Overall score is `a × average rate + b × normalized average encode time + c × normalized average decode time`. Time normalization is linear min–max across all eligible rows in the selected runtime, scope and row mode; an equal-time range normalizes to zero. Search, family and coverage filters only hide rows and never change the reference. Average Rank averages per-dataset composite-score ranks with dataset-local time normalization and fractional ties. Missing results are omitted. Coverage and candidate counts can differ across datasets; method details expose dataset scores, ranks and candidate counts. CSV downloads include weights, normalization bounds and rank coverage. The default weights are `a=0.4, b=0.3, c=0.3`.
47
 
48
- Encoding and decoding costs use each result's recorded point-count contract. Native-float measurements divide by valid native values; historical imported rows retain their source 8-byte-equivalent point definition. Timing comparisons should therefore stay within the same runtime and measurement contract.
49
 
50
  Source project: https://github.com/xjz17/web_compression
 
6
  sdk: static
7
  app_file: index.html
8
  pinned: false
9
+ short_description: Compression rates and compress/decompress costs
10
  ---
11
 
12
  # THULab Time Series Compression Benchmark
 
24
  python3 deploy/huggingface-compressionbenchmark/apply_dataset_scope.py
25
  # Reapply verified measurement updates before packing the browser payload.
26
  python3 scripts/integrate_dbp_leaderboard.py --measurements /path/to/frozen-measurements.json --expected-sha256 VERIFIED_SHA256 --receipt /path/to/integration-receipt.json
27
+ # Verify the published source identities and rebuild equal-column timing means.
28
+ python3 scripts/update_site_column_means.py --receipt /path/to/column-mean-receipt.json
29
  python3 deploy/huggingface-compressionbenchmark/build_ui_payload.py
30
  ```
31
 
 
37
 
38
  - Overall and per-dataset leaderboards with runtime, data-type, precision, family, and method filters
39
  - Sortable metrics, optional columns, and CSV download of the selected ranking
40
+ - Compression rate versus compression/decompression cost plots
41
  - Dataset-level method details, snapshot provenance, and metric definitions
42
  - Searchable 169-dataset catalog grouped into six source categories, with explicit measurement availability and source links
43
  - Overall composite score and average dataset Rank, with 66 valid non-negative one-decimal weight combinations, including zero weights
 
45
 
46
  The summary dataset count and Dataset coverage denominator use the same measured dataset set for the selected runtime, data type, and dataset selection; the default Overall view has 164 measured datasets. All 169 catalog entries remain searchable and selectable in the dataset browser. Dataset coverage can differ by method and runtime; unavailable combinations show **No published measurements**, remain excluded from rankings, and are never converted to zero-valued results. The default family view selects the best measured compression-rate configuration independently for each measured dataset. Switch to **Exact configuration** to hold a configuration fixed. Runtime results stay separate.
47
 
48
+ The Overall score is `a × average rate + b × normalized average compression time + c × normalized average decompression time`. Time normalization is linear min–max across all eligible rows in the selected runtime, scope and row mode; an equal-time range normalizes to zero. Search, family and coverage filters only hide rows and never change the reference. Average Rank averages per-dataset composite-score ranks with dataset-local time normalization and fractional ties. Missing results are omitted. Coverage and candidate counts can differ across datasets; method details expose dataset scores, ranks and candidate counts. CSV downloads include weights, normalization bounds and rank coverage. The default weights are `a=0.4, b=0.3, c=0.3`.
49
 
50
+ Compression and decompression costs are calculated independently for each usable numeric column as ns/point, then reported as the arithmetic mean across those columns; each column contributes equally. Overall timing pools all measured columns across the selected datasets. If any selected dataset/configuration lacks column-level timing evidence, that Overall timing is unavailable. Historical aggregate-only timing rows without recoverable per-column evidence remain unavailable and are never treated as one synthetic column. Native-float measurements use valid native values; historical imported rows retain their source 8-byte-equivalent point definition. Timing comparisons should therefore stay within the same runtime and measurement contract.
51
 
52
  Source project: https://github.com/xjz17/web_compression
app.js CHANGED
@@ -3,6 +3,10 @@ const $ = (id) => document.getElementById(id);
3
  const escape = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
4
  const number = (v) => v != null && Number.isFinite(Number(v)) ? Number(v) : null;
5
  const mean = (values) => { const a = values.map(number).filter((n) => n !== null); return a.length ? a.reduce((s,n) => s+n,0)/a.length : null; };
 
 
 
 
6
  const fmt = (v, digits=3) => number(v) === null ? 'Not reported' : Number(v).toLocaleString('en-US',{maximumFractionDigits:digits,minimumFractionDigits:digits});
7
  const countFmt = (v) => Number(v).toLocaleString('en-US');
8
  const runtimeName = (v) => ({cpp:'C++',java:'Java',python:'Python'}[v] || v);
@@ -12,10 +16,11 @@ const cols = [
12
  {key:'composite_score',label:'Composite score ↓',digits:4,visible:true,overallOnly:true},
13
  {key:'compression_rate',label:'Average rate ↓',digits:4,visible:true},
14
  {key:'overall_rate',label:'Overall rate ↓',digits:4,visible:false},
15
- {key:'compression_time',label:'Encode ns/point ↓',digits:2,visible:true},
16
- {key:'decompression_time',label:'Decode ns/point ↓',digits:2,visible:true},
17
- {key:'normalized_compression_time',label:'Normalized encode ↓',digits:4,visible:false,overallOnly:true},
18
- {key:'normalized_decompression_time',label:'Normalized decode ↓',digits:4,visible:false,overallOnly:true},
 
19
  {key:'compressed_bytes',label:'Compressed bytes',digits:0,visible:false},
20
  {key:'original_bytes',label:'Original bytes',digits:0,visible:false},
21
  ];
@@ -50,7 +55,7 @@ function aggregate(input) {
50
  for(const [method,source] of groups) {
51
  const hasBytes=source.every(r=>r.original_bytes!==null && r.original_bytes>0 && r.compressed_bytes!==null && r.compressed_bytes>0);
52
  const original=hasBytes?source.reduce((s,r)=>s+r.original_bytes,0):null, compressed=hasBytes?source.reduce((s,r)=>s+r.compressed_bytes,0):null;
53
- rows.push({method,family:source[0].family,runtime:source[0].runtime,compression_rate:mean(source.map(r=>r.compression_rate)),compression_time:mean(source.map(r=>r.compression_time)),decompression_time:mean(source.map(r=>r.decompression_time)),overall_rate:hasBytes?compressed/original:null,original_bytes:original,compressed_bytes:compressed,coverage:new Set(source.map(r=>r.dataset)).size,totalDatasets,source});
54
  }
55
  const s=state();
56
  return addRankingMetrics(rows,parseWeights($('weights').value)).filter(r=>(s.family==='all'||r.family===s.family)&&(!s.search||(s.mode==='family'?r.family:`${r.method} ${r.family}`).toLowerCase().includes(s.search))&&(!s.complete||r.coverage===totalDatasets)).sort((a,b)=>{
@@ -64,7 +69,7 @@ function renderTable() {
64
  const visible=cols.filter(c=>c.visible&&(!c.overallOnly||activeTab==='overall'));
65
  $('leaderboard').querySelector('thead').innerHTML=`<tr><th>Position</th><th>Method / configuration</th>${visible.map(c=>`<th aria-sort="${sort.key===c.key?(sort.direction===1?'ascending':'descending'):'none'}"><button data-sort="${c.key}" title="Sort ${escape(c.label)}">${escape(c.label)}${sort.key===c.key?(sort.direction===1?' ▴':' ▾'):''}</button></th>`).join('')}<th>Dataset coverage</th></tr>`;
66
  const minima=Object.fromEntries(visible.map(c=>[c.key,Math.min(...currentRows.map(r=>number(r[c.key])).filter(n=>n!==null))]));
67
- $('leaderboard').querySelector('tbody').innerHTML=currentRows.length?currentRows.map((r,i)=>`<tr><td class="rank numeric">${i+1}</td><td><button class="method-button" data-method="${escape(r.method)}">${escape(r.method)}</button><span class="method-family">${escape(r.family)} · ${escape(runtimeName(r.runtime))}</span></td>${visible.map(c=>`<td class="numeric ${!['compressed_bytes','original_bytes','rank_coverage'].includes(c.key)&&number(r[c.key])!==null&&r[c.key]===minima[c.key]?'best':''}">${c.key==='rank_coverage'?`${r.rank_coverage} / ${r.totalDatasets}`:fmt(r[c.key],c.digits)}</td>`).join('')}<td><span class="coverage ${r.coverage===r.totalDatasets?'full':''}">${r.coverage} / ${r.totalDatasets}</span></td></tr>`).join(''):`<tr><td colspan="${visible.length+3}" class="empty-cell">No measurements match this selection. Try another runtime, data type, or dataset.</td></tr>`;
68
  for(const button of $('leaderboard').querySelectorAll('[data-sort]'))button.addEventListener('click',()=>{const key=button.dataset.sort;sort={key,direction:sort.key===key?-sort.direction:1};refresh();});
69
  for(const button of $('leaderboard').querySelectorAll('[data-method]'))button.addEventListener('click',()=>showMethod(button.dataset.method,true));
70
  $('row-count').textContent=`${currentRows.length} ${state().mode==='family'?'method families':'method configurations'} · sorted by ${cols.find(c=>c.key===sort.key)?.label??sort.key}`;
@@ -73,7 +78,7 @@ function renderTable() {
73
  function refresh() {
74
  if(!snapshot)return;
75
  $('precision').disabled=$('dtype').value!=='float';
76
- const selectedDataset=state().dataset,selectedHasMeasurements=selectedDataset==='all'||records.some(r=>r.dataset===selectedDataset),columnCoverageNote='Metrics aggregate the numeric columns measured for each method; column coverage may differ.';
77
  if(!selectedHasMeasurements) {
78
  $('results-note').textContent=`No published measurements are available for this catalog dataset. It is excluded from rankings rather than treated as zero. ${columnCoverageNote}`;
79
  } else if(scopeKey()==='float'&&!records.some(r=>r.scope==='float')) {
@@ -83,7 +88,7 @@ function refresh() {
83
  } else $('results-note').textContent=`${state().mode==='family'?'Each family uses its lowest-rate measured configuration per dataset. Configurations may differ across datasets; use Exact configuration for fixed parameters.':'Each row retains one exact configuration. Lower compression rates and lower ns/point are better. Coverage counts datasets, not identical column sets.'} ${columnCoverageNote}`;
84
  currentRows=aggregate(filteredRecords(true));
85
  const weights=parseWeights($('weights').value);
86
- $('score-note').textContent=`Score = ${weights[0].toFixed(1)} × average rate + ${weights[1].toFixed(1)} × normalized encode + ${weights[2].toFixed(1)} × normalized decode. Lower is better. Time normalization: (time − minimum) / (maximum − minimum), across all measured rows in this runtime, scope and row mode. Search and visibility filters keep the reference fixed. Average Rank: mean of per-dataset score ranks with dataset-local time normalization; ties share their average position. Missing results are omitted. Dataset candidate counts differ, so compare rank coverage and inspect method details.`;
87
  const s=state(),scope=scopeKey(),measuredCount=coverageDatasetCount(),ds=s.dataset==='all'?`all ${measuredCount} measured datasets`:datasetDisplayName(s.dataset);
88
  $('dataset-count').textContent=countFmt(measuredCount);$('dataset-count-label').textContent='measured datasets';
89
  const names={overall:'available numeric measurements',integer:'integer columns',float:'floating-point columns',fixed_float:'fixed-precision float columns',nonfixed_float:'non-fixed-precision float columns'};
@@ -110,7 +115,7 @@ function showMethod(method,focus=false) {
110
  if(!row){selectedMethod=null;detail.hidden=true;return;}selectedMethod=method;
111
  const meta=methods.get(method)??methods.get(row.source[0].configuration), pipeline=meta?.operator_pipeline;
112
  const source=row.source.slice().sort((a,b)=>a.dataset.localeCompare(b.dataset));
113
- detail.innerHTML=`<div class="detail-head"><div><h2 id="detail-title">${escape(method)}</h2><p class="muted">${escape(row.family)} · ${escape(runtimeName(row.runtime))} · ${row.coverage} measured datasets</p></div><button id="close-detail" class="quiet-button">Close details</button></div><div class="detail-grid"><div><strong>${fmt(row.compression_rate,4)}</strong><span>average compression rate</span></div><div><strong>${fmt(row.compression_time,2)}</strong><span>encoding ns/point</span></div><div><strong>${fmt(row.decompression_time,2)}</strong><span>decoding ns/point</span></div><div><strong>${row.coverage} / ${row.totalDatasets}</strong><span>dataset coverage</span></div></div>${pipeline?`<p class="source-code">Interpretive operator mapping: ${escape(typeof pipeline==='string'?pipeline:JSON.stringify(pipeline))}</p>`:''}<p class="muted">Dataset metrics below come from the published result files. Measurement boundaries, source hashes, and available verification evidence are recorded in the manifest.</p><div class="detail-table"><table><thead><tr><th>Dataset</th><th>Configuration</th><th>Rate ↓</th><th>Encode ns/point ↓</th><th>Decode ns/point ↓</th><th>Original bytes</th><th>Compressed bytes</th><th>Dataset score ↓</th><th>Dataset Rank ↓</th><th>Rank candidates</th></tr></thead><tbody>${source.map(r=>`<tr><td>${escape(r.dataset)}</td><td>${escape(r.configuration)}</td><td class="numeric">${fmt(r.compression_rate,4)}</td><td class="numeric">${fmt(r.compression_time,2)}</td><td class="numeric">${fmt(r.decompression_time,2)}</td><td class="numeric">${fmt(r.original_bytes,0)}</td><td class="numeric">${fmt(r.compressed_bytes,0)}</td><td class="numeric">${fmt(r.dataset_score,4)}</td><td class="numeric">${fmt(r.dataset_rank,2)}</td><td class="numeric">${fmt(r.dataset_candidate_count,0)}</td></tr>`).join('')}</tbody></table></div>`;
114
  detail.hidden=false;$('close-detail').addEventListener('click',()=>{detail.hidden=true;selectedMethod=null;});
115
  if(focus){detail.scrollIntoView({block:'nearest',behavior:'auto'});detail.focus({preventScroll:true});}
116
  }
@@ -125,16 +130,16 @@ function renderChart() {
125
  let graph='';
126
  for(let i=0;i<=5;i++){const rate=maxRate*i/5,px=x(rate);graph+=`<line class="gridline" x1="${px}" y1="${pad.top}" x2="${px}" y2="${height-pad.bottom}"/><text class="axis-label" x="${px}" y="${height-pad.bottom+25}" text-anchor="middle">${fmt(rate,2)}</text>`;}
127
  for(let i=minLog;i<=maxLog;i++){const n=10**i,py=y(n);graph+=`<line class="gridline" x1="${pad.left}" y1="${py}" x2="${width-pad.right}" y2="${py}"/><text class="axis-label" x="${pad.left-12}" y="${py+4}" text-anchor="end">${n.toLocaleString('en-US',{maximumFractionDigits:3})}</text>`;}
128
- for(const r of valid)graph+=`<circle class="point" tabindex="0" role="button" aria-label="${escape(r.method)}: compression rate ${fmt(r.compression_rate,4)}, ${metric==='compression_time'?'encoding':'decoding'} ${fmt(r[metric],2)} ns per point" data-method="${escape(r.method)}" cx="${x(r.compression_rate)}" cy="${y(r[metric])}" r="6"><title>${escape(r.method)} | rate ${fmt(r.compression_rate,4)} | ${fmt(r[metric],2)} ns/point | coverage ${r.coverage}/${r.totalDatasets}</title></circle>`;
129
- graph+=`<text class="axis-label" x="${width/2}" y="${height-13}" text-anchor="middle">Average compression rate (compressed / original) ↓</text><text class="axis-label" transform="translate(20 ${height/2}) rotate(-90)" text-anchor="middle">${metric==='compression_time'?'Encoding':'Decoding'} ns/point (log scale) ↓</text>`;
130
- $('chart').innerHTML=`<svg viewBox="0 0 ${width} ${height}" role="img" aria-label="Measured compression rates versus ${metric==='compression_time'?'encoding':'decoding'} cost">${graph}</svg>`;
131
  for(const p of $('chart').querySelectorAll('[data-method]')){p.addEventListener('click',()=>showMethod(p.dataset.method,true));p.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();showMethod(p.dataset.method,true);}});}
132
  }
133
  function csv() {
134
  const quote=v=>{let text=String(v??'');if(typeof v==='string'&&/^[=+@-]/.test(text))text="'"+text;return `"${text.replace(/"/g,'""')}"`;};
135
  const weights=parseWeights($('weights').value);
136
- const columns=['position','method','runtime','scope','row_mode','selected_dataset','sort_metric','sort_direction','average_compression_rate','overall_compression_rate','encode_ns_per_point','decode_ns_per_point','weight_a','weight_b','weight_c','normalized_encode','normalized_decode','composite_score','average_rank','rank_dataset_coverage','normalization_reference_rows','encode_min','encode_max','decode_min','decode_max','normalization','rank_definition','dataset_coverage','datasets_in_scope','selected_configuration_by_dataset'];
137
- const body=currentRows.map((r,i)=>[i+1,r.method,r.runtime,scopeKey(),state().mode,state().dataset,sort.key,sort.direction===1?'ascending':'descending',r.compression_rate,r.overall_rate,r.compression_time,r.decompression_time,...weights.map(w=>w.toFixed(1)),r.normalized_compression_time,r.normalized_decompression_time,r.composite_score,r.average_rank,r.rank_coverage,r.normalization_reference_rows,r.encode_min,r.encode_max,r.decode_min,r.decode_max,'min-max over all measured rows in selected runtime/scope/row-mode before search and visibility filters','mean per-dataset composite-score fractional rank; per-dataset min-max times',r.coverage,r.totalDatasets,JSON.stringify(Object.fromEntries(r.source.map(s=>[s.dataset,s.configuration])))].map(quote).join(','));
138
  const url=URL.createObjectURL(new Blob(['\ufeff'+[columns.join(','),...body].join('\r\n')],{type:'text/csv;charset=utf-8'}));const a=document.createElement('a');a.href=url;a.download=`THULab-compression-${state().runtime}-${scopeKey()}-${state().dataset}.csv`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
139
  }
140
  function safeHttpUrl(value) {
@@ -276,7 +281,7 @@ function renderAbout() {
276
  $('provenance').innerHTML=`<table class="provenance-table"><tbody>${Object.entries(info).map(([k,v])=>`<tr><th>${escape(k)}</th><td class="source-code">${escape(v)}</td></tr>`).join('')}</tbody></table>${exclusions.length?`<h3>Excluded or unavailable results</h3><p class="source-code">${escape(typeof exclusions==='string'?exclusions:JSON.stringify(exclusions))}</p>`:''}`;
277
  }
278
  function init() {
279
- if(snapshot.compact_records){const t=snapshot.record_tables;snapshot.dataset_results=snapshot.compact_records.map(r=>({dataset:t.datasets[r[0]],scope:t.scopes[r[1]],runtime:t.runtimes[r[2]],family:t.families[r[3]],method:t.configurations[r[4]],algorithm_variant:t.configurations[r[4]],compression_rate:r[5],compression_time_ns_per_point:r[6],decompression_time_ns_per_point:r[7],original_size_bytes:r[8],compressed_size_bytes:r[9]}));delete snapshot.compact_records;}
280
  for(const m of snapshot.methods||[]) { const method=typeof m==='string'?{name:m}:m;methods.set(method.name??method.method,method);for(const c of method.configurations||[])methods.set(c.algorithm_variant,{...method,name:c.algorithm_variant,family:method.name}); }
281
  for(const d of snapshot.datasets||[])datasets.set(d.key??d.dataset,d);
282
  records=(snapshot.dataset_results||snapshot.records||[]).map(normalizeRecord).filter(r=>r.method&&r.dataset&&r.compression_rate>0);
@@ -308,5 +313,5 @@ function setupTheme() {
308
  setupTheme();
309
  void loadReferenceCatalog();
310
  void loadDatasetCatalog();
311
- try { const response=await fetch('./ui-data.json?v=20261009-dbp-integrated-v1',{cache:'no-cache'});if(!response.ok)throw new Error(`Snapshot request returned ${response.status}`);snapshot=await response.json();init(); }
312
  catch(e){$('load-error').hidden=false;$('load-error').textContent=`The result snapshot could not be loaded. Please reload the page or download the JSON from the repository. ${e.message}`;$('snapshot').textContent='Snapshot unavailable';$('leaderboard').querySelector('tbody').innerHTML='<tr><td class="empty-cell">Results unavailable.</td></tr>';}
 
3
  const escape = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
4
  const number = (v) => v != null && Number.isFinite(Number(v)) ? Number(v) : null;
5
  const mean = (values) => { const a = values.map(number).filter((n) => n !== null); return a.length ? a.reduce((s,n) => s+n,0)/a.length : null; };
6
+ function columnMean(input,key) {
7
+ if(!input.length||input.some(r=>number(r[key])===null||!Number.isInteger(r.timing_column_count)||r.timing_column_count<=0))return null;
8
+ return input.reduce((sum,r)=>sum+r[key]*r.timing_column_count,0)/input.reduce((sum,r)=>sum+r.timing_column_count,0);
9
+ }
10
  const fmt = (v, digits=3) => number(v) === null ? 'Not reported' : Number(v).toLocaleString('en-US',{maximumFractionDigits:digits,minimumFractionDigits:digits});
11
  const countFmt = (v) => Number(v).toLocaleString('en-US');
12
  const runtimeName = (v) => ({cpp:'C++',java:'Java',python:'Python'}[v] || v);
 
16
  {key:'composite_score',label:'Composite score ↓',digits:4,visible:true,overallOnly:true},
17
  {key:'compression_rate',label:'Average rate ↓',digits:4,visible:true},
18
  {key:'overall_rate',label:'Overall rate ↓',digits:4,visible:false},
19
+ {key:'compression_time',label:'Compress ns/point ↓',digits:2,visible:true},
20
+ {key:'decompression_time',label:'Decompress ns/point ↓',digits:2,visible:true},
21
+ {key:'normalized_compression_time',label:'Normalized compress ↓',digits:4,visible:false,overallOnly:true},
22
+ {key:'normalized_decompression_time',label:'Normalized decompress ↓',digits:4,visible:false,overallOnly:true},
23
+ {key:'timing_column_count',label:'Measured columns',digits:0,visible:false},
24
  {key:'compressed_bytes',label:'Compressed bytes',digits:0,visible:false},
25
  {key:'original_bytes',label:'Original bytes',digits:0,visible:false},
26
  ];
 
55
  for(const [method,source] of groups) {
56
  const hasBytes=source.every(r=>r.original_bytes!==null && r.original_bytes>0 && r.compressed_bytes!==null && r.compressed_bytes>0);
57
  const original=hasBytes?source.reduce((s,r)=>s+r.original_bytes,0):null, compressed=hasBytes?source.reduce((s,r)=>s+r.compressed_bytes,0):null;
58
+ rows.push({method,family:source[0].family,runtime:source[0].runtime,compression_rate:mean(source.map(r=>r.compression_rate)),compression_time:columnMean(source,'compression_time'),decompression_time:columnMean(source,'decompression_time'),timing_column_count:source.reduce((sum,r)=>sum+r.timing_column_count,0),overall_rate:hasBytes?compressed/original:null,original_bytes:original,compressed_bytes:compressed,coverage:new Set(source.map(r=>r.dataset)).size,totalDatasets,source});
59
  }
60
  const s=state();
61
  return addRankingMetrics(rows,parseWeights($('weights').value)).filter(r=>(s.family==='all'||r.family===s.family)&&(!s.search||(s.mode==='family'?r.family:`${r.method} ${r.family}`).toLowerCase().includes(s.search))&&(!s.complete||r.coverage===totalDatasets)).sort((a,b)=>{
 
69
  const visible=cols.filter(c=>c.visible&&(!c.overallOnly||activeTab==='overall'));
70
  $('leaderboard').querySelector('thead').innerHTML=`<tr><th>Position</th><th>Method / configuration</th>${visible.map(c=>`<th aria-sort="${sort.key===c.key?(sort.direction===1?'ascending':'descending'):'none'}"><button data-sort="${c.key}" title="Sort ${escape(c.label)}">${escape(c.label)}${sort.key===c.key?(sort.direction===1?' ▴':' ▾'):''}</button></th>`).join('')}<th>Dataset coverage</th></tr>`;
71
  const minima=Object.fromEntries(visible.map(c=>[c.key,Math.min(...currentRows.map(r=>number(r[c.key])).filter(n=>n!==null))]));
72
+ $('leaderboard').querySelector('tbody').innerHTML=currentRows.length?currentRows.map((r,i)=>`<tr><td class="rank numeric">${i+1}</td><td><button class="method-button" data-method="${escape(r.method)}">${escape(r.method)}</button><span class="method-family">${escape(r.family)} · ${escape(runtimeName(r.runtime))}</span></td>${visible.map(c=>`<td class="numeric ${!['compressed_bytes','original_bytes','rank_coverage','timing_column_count'].includes(c.key)&&number(r[c.key])!==null&&r[c.key]===minima[c.key]?'best':''}">${c.key==='rank_coverage'?`${r.rank_coverage} / ${r.totalDatasets}`:fmt(r[c.key],c.digits)}</td>`).join('')}<td><span class="coverage ${r.coverage===r.totalDatasets?'full':''}">${r.coverage} / ${r.totalDatasets}</span></td></tr>`).join(''):`<tr><td colspan="${visible.length+3}" class="empty-cell">No measurements match this selection. Try another runtime, data type, or dataset.</td></tr>`;
73
  for(const button of $('leaderboard').querySelectorAll('[data-sort]'))button.addEventListener('click',()=>{const key=button.dataset.sort;sort={key,direction:sort.key===key?-sort.direction:1};refresh();});
74
  for(const button of $('leaderboard').querySelectorAll('[data-method]'))button.addEventListener('click',()=>showMethod(button.dataset.method,true));
75
  $('row-count').textContent=`${currentRows.length} ${state().mode==='family'?'method families':'method configurations'} · sorted by ${cols.find(c=>c.key===sort.key)?.label??sort.key}`;
 
78
  function refresh() {
79
  if(!snapshot)return;
80
  $('precision').disabled=$('dtype').value!=='float';
81
+ const selectedDataset=state().dataset,selectedHasMeasurements=selectedDataset==='all'||records.some(r=>r.dataset===selectedDataset),columnCoverageNote='Compress and Decompress times are arithmetic means of each measured column’s ns/point; every column has equal weight. Column coverage may differ by method. Historical aggregate-only timings are not reported as column means.';
82
  if(!selectedHasMeasurements) {
83
  $('results-note').textContent=`No published measurements are available for this catalog dataset. It is excluded from rankings rather than treated as zero. ${columnCoverageNote}`;
84
  } else if(scopeKey()==='float'&&!records.some(r=>r.scope==='float')) {
 
88
  } else $('results-note').textContent=`${state().mode==='family'?'Each family uses its lowest-rate measured configuration per dataset. Configurations may differ across datasets; use Exact configuration for fixed parameters.':'Each row retains one exact configuration. Lower compression rates and lower ns/point are better. Coverage counts datasets, not identical column sets.'} ${columnCoverageNote}`;
89
  currentRows=aggregate(filteredRecords(true));
90
  const weights=parseWeights($('weights').value);
91
+ $('score-note').textContent=`Score = ${weights[0].toFixed(1)} × average rate + ${weights[1].toFixed(1)} × normalized compress + ${weights[2].toFixed(1)} × normalized decompress. Lower is better. Time normalization: (time − minimum) / (maximum − minimum), across all measured rows in this runtime, scope and row mode. Search and visibility filters keep the reference fixed. Average Rank: mean of per-dataset score ranks with dataset-local time normalization; ties share their average position. Missing results are omitted. Dataset candidate counts differ, so compare rank coverage and inspect method details.`;
92
  const s=state(),scope=scopeKey(),measuredCount=coverageDatasetCount(),ds=s.dataset==='all'?`all ${measuredCount} measured datasets`:datasetDisplayName(s.dataset);
93
  $('dataset-count').textContent=countFmt(measuredCount);$('dataset-count-label').textContent='measured datasets';
94
  const names={overall:'available numeric measurements',integer:'integer columns',float:'floating-point columns',fixed_float:'fixed-precision float columns',nonfixed_float:'non-fixed-precision float columns'};
 
115
  if(!row){selectedMethod=null;detail.hidden=true;return;}selectedMethod=method;
116
  const meta=methods.get(method)??methods.get(row.source[0].configuration), pipeline=meta?.operator_pipeline;
117
  const source=row.source.slice().sort((a,b)=>a.dataset.localeCompare(b.dataset));
118
+ detail.innerHTML=`<div class="detail-head"><div><h2 id="detail-title">${escape(method)}</h2><p class="muted">${escape(row.family)} · ${escape(runtimeName(row.runtime))} · ${row.coverage} measured datasets</p></div><button id="close-detail" class="quiet-button">Close details</button></div><div class="detail-grid"><div><strong>${fmt(row.compression_rate,4)}</strong><span>average compression rate</span></div><div><strong>${fmt(row.compression_time,2)}</strong><span>Compress ns/point</span></div><div><strong>${fmt(row.decompression_time,2)}</strong><span>Decompress ns/point</span></div><div><strong>${row.coverage} / ${row.totalDatasets}</strong><span>dataset coverage</span></div></div>${pipeline?`<p class="source-code">Interpretive operator mapping: ${escape(typeof pipeline==='string'?pipeline:JSON.stringify(pipeline))}</p>`:''}<p class="muted">Dataset metrics below come from the published result files. Measurement boundaries, source hashes, and available verification evidence are recorded in the manifest.</p><div class="detail-table"><table><thead><tr><th>Dataset</th><th>Configuration</th><th>Rate ↓</th><th>Compress ns/point ↓</th><th>Decompress ns/point ↓</th><th>Original bytes</th><th>Compressed bytes</th><th>Dataset score ↓</th><th>Dataset Rank ↓</th><th>Rank candidates</th></tr></thead><tbody>${source.map(r=>`<tr><td>${escape(r.dataset)}</td><td>${escape(r.configuration)}</td><td class="numeric">${fmt(r.compression_rate,4)}</td><td class="numeric">${fmt(r.compression_time,2)}</td><td class="numeric">${fmt(r.decompression_time,2)}</td><td class="numeric">${fmt(r.original_bytes,0)}</td><td class="numeric">${fmt(r.compressed_bytes,0)}</td><td class="numeric">${fmt(r.dataset_score,4)}</td><td class="numeric">${fmt(r.dataset_rank,2)}</td><td class="numeric">${fmt(r.dataset_candidate_count,0)}</td></tr>`).join('')}</tbody></table></div>`;
119
  detail.hidden=false;$('close-detail').addEventListener('click',()=>{detail.hidden=true;selectedMethod=null;});
120
  if(focus){detail.scrollIntoView({block:'nearest',behavior:'auto'});detail.focus({preventScroll:true});}
121
  }
 
130
  let graph='';
131
  for(let i=0;i<=5;i++){const rate=maxRate*i/5,px=x(rate);graph+=`<line class="gridline" x1="${px}" y1="${pad.top}" x2="${px}" y2="${height-pad.bottom}"/><text class="axis-label" x="${px}" y="${height-pad.bottom+25}" text-anchor="middle">${fmt(rate,2)}</text>`;}
132
  for(let i=minLog;i<=maxLog;i++){const n=10**i,py=y(n);graph+=`<line class="gridline" x1="${pad.left}" y1="${py}" x2="${width-pad.right}" y2="${py}"/><text class="axis-label" x="${pad.left-12}" y="${py+4}" text-anchor="end">${n.toLocaleString('en-US',{maximumFractionDigits:3})}</text>`;}
133
+ for(const r of valid)graph+=`<circle class="point" tabindex="0" role="button" aria-label="${escape(r.method)}: compression rate ${fmt(r.compression_rate,4)}, ${metric==='compression_time'?'compression':'decompression'} ${fmt(r[metric],2)} ns per point" data-method="${escape(r.method)}" cx="${x(r.compression_rate)}" cy="${y(r[metric])}" r="6"><title>${escape(r.method)} | rate ${fmt(r.compression_rate,4)} | ${fmt(r[metric],2)} ns/point | coverage ${r.coverage}/${r.totalDatasets}</title></circle>`;
134
+ graph+=`<text class="axis-label" x="${width/2}" y="${height-13}" text-anchor="middle">Average compression rate (compressed / original) ↓</text><text class="axis-label" transform="translate(20 ${height/2}) rotate(-90)" text-anchor="middle">${metric==='compression_time'?'Compress':'Decompress'} ns/point (log scale) ↓</text>`;
135
+ $('chart').innerHTML=`<svg viewBox="0 0 ${width} ${height}" role="img" aria-label="Measured compression rates versus ${metric==='compression_time'?'compression':'decompression'} cost">${graph}</svg>`;
136
  for(const p of $('chart').querySelectorAll('[data-method]')){p.addEventListener('click',()=>showMethod(p.dataset.method,true));p.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();showMethod(p.dataset.method,true);}});}
137
  }
138
  function csv() {
139
  const quote=v=>{let text=String(v??'');if(typeof v==='string'&&/^[=+@-]/.test(text))text="'"+text;return `"${text.replace(/"/g,'""')}"`;};
140
  const weights=parseWeights($('weights').value);
141
+ const columns=['position','method','runtime','scope','row_mode','selected_dataset','sort_metric','sort_direction','average_compression_rate','overall_compression_rate','compress_ns_per_point','decompress_ns_per_point','weight_a','weight_b','weight_c','normalized_compress','normalized_decompress','composite_score','average_rank','rank_dataset_coverage','normalization_reference_rows','compress_min','compress_max','decompress_min','decompress_max','normalization','rank_definition','dataset_coverage','datasets_in_scope','selected_configuration_by_dataset','timing_column_count','timing_aggregation'];
142
+ const body=currentRows.map((r,i)=>[i+1,r.method,r.runtime,scopeKey(),state().mode,state().dataset,sort.key,sort.direction===1?'ascending':'descending',r.compression_rate,r.overall_rate,r.compression_time,r.decompression_time,...weights.map(w=>w.toFixed(1)),r.normalized_compression_time,r.normalized_decompression_time,r.composite_score,r.average_rank,r.rank_coverage,r.normalization_reference_rows,r.encode_min,r.encode_max,r.decode_min,r.decode_max,'min-max over all measured rows in selected runtime/scope/row-mode before search and visibility filters','mean per-dataset composite-score fractional rank; per-dataset min-max times',r.coverage,r.totalDatasets,JSON.stringify(Object.fromEntries(r.source.map(s=>[s.dataset,s.configuration]))),r.timing_column_count,'arithmetic mean of per-column ns/point; equal weight per measured column'].map(quote).join(','));
143
  const url=URL.createObjectURL(new Blob(['\ufeff'+[columns.join(','),...body].join('\r\n')],{type:'text/csv;charset=utf-8'}));const a=document.createElement('a');a.href=url;a.download=`THULab-compression-${state().runtime}-${scopeKey()}-${state().dataset}.csv`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
144
  }
145
  function safeHttpUrl(value) {
 
281
  $('provenance').innerHTML=`<table class="provenance-table"><tbody>${Object.entries(info).map(([k,v])=>`<tr><th>${escape(k)}</th><td class="source-code">${escape(v)}</td></tr>`).join('')}</tbody></table>${exclusions.length?`<h3>Excluded or unavailable results</h3><p class="source-code">${escape(typeof exclusions==='string'?exclusions:JSON.stringify(exclusions))}</p>`:''}`;
282
  }
283
  function init() {
284
+ if(snapshot.compact_records){const t=snapshot.record_tables;snapshot.dataset_results=snapshot.compact_records.map(r=>({dataset:t.datasets[r[0]],scope:t.scopes[r[1]],runtime:t.runtimes[r[2]],family:t.families[r[3]],method:t.configurations[r[4]],algorithm_variant:t.configurations[r[4]],compression_rate:r[5],compression_time_ns_per_point:r[6],decompression_time_ns_per_point:r[7],original_size_bytes:r[8],compressed_size_bytes:r[9],timing_column_count:r[10]}));delete snapshot.compact_records;}
285
  for(const m of snapshot.methods||[]) { const method=typeof m==='string'?{name:m}:m;methods.set(method.name??method.method,method);for(const c of method.configurations||[])methods.set(c.algorithm_variant,{...method,name:c.algorithm_variant,family:method.name}); }
286
  for(const d of snapshot.datasets||[])datasets.set(d.key??d.dataset,d);
287
  records=(snapshot.dataset_results||snapshot.records||[]).map(normalizeRecord).filter(r=>r.method&&r.dataset&&r.compression_rate>0);
 
313
  setupTheme();
314
  void loadReferenceCatalog();
315
  void loadDatasetCatalog();
316
+ try { const response=await fetch('./ui-data.json?v=20261009-column-mean-v1',{cache:'no-cache'});if(!response.ok)throw new Error(`Snapshot request returned ${response.status}`);snapshot=await response.json();init(); }
317
  catch(e){$('load-error').hidden=false;$('load-error').textContent=`The result snapshot could not be loaded. Please reload the page or download the JSON from the repository. ${e.message}`;$('snapshot').textContent='Snapshot unavailable';$('leaderboard').querySelector('tbody').innerHTML='<tr><td class="empty-cell">Results unavailable.</td></tr>';}
apply_dataset_scope.py CHANGED
@@ -47,9 +47,10 @@ def apply() -> None:
47
  r = dict(original)
48
  # Preserve the exporter's 8-byte-equivalent denominator, including
49
  # historical rows whose source byte totals are not divisible by eight.
50
- r["point_count"] = r["original_size_bytes"] / 8.0
51
- r["compression_time_ns"] = r["compression_time_ns_per_point"] * r["point_count"]
52
- r["decompression_time_ns"] = r["decompression_time_ns_per_point"] * r["point_count"]
 
53
  configurations[r["runtime"], r["scope"], r["family"], r["method"]].append(r)
54
  if r["is_best_rate_in_family"]:
55
  families[r["runtime"], r["scope"], r["family"]].append(r)
 
47
  r = dict(original)
48
  # Preserve the exporter's 8-byte-equivalent denominator, including
49
  # historical rows whose source byte totals are not divisible by eight.
50
+ r.setdefault("point_count", r["original_size_bytes"] / 8.0)
51
+ # A per-column arithmetic mean cannot be expanded back into elapsed
52
+ # time from a dataset point total. Leave weighted elapsed-time metrics
53
+ # unavailable unless the source row still carries the original totals.
54
  configurations[r["runtime"], r["scope"], r["family"], r["method"]].append(r)
55
  if r["is_best_rate_in_family"]:
56
  families[r["runtime"], r["scope"], r["family"]].append(r)
build_ui_payload.py CHANGED
@@ -20,7 +20,7 @@ def build() -> None:
20
  compact.append([*(indices[key][r[field]] for key,field in fields.items()),
21
  r['compression_rate'], r['compression_time_ns_per_point'],
22
  r['decompression_time_ns_per_point'], r['original_size_bytes'],
23
- r['compressed_size_bytes']])
24
  payload = {key: data[key] for key in ('schema_version','benchmark','coverage','datasets','methods','scopes','source')}
25
  if 'dataset_summary' in data:
26
  payload['dataset_summary'] = data['dataset_summary']
 
20
  compact.append([*(indices[key][r[field]] for key,field in fields.items()),
21
  r['compression_rate'], r['compression_time_ns_per_point'],
22
  r['decompression_time_ns_per_point'], r['original_size_bytes'],
23
+ r['compressed_size_bytes'], r.get('timing_column_count', 0)])
24
  payload = {key: data[key] for key in ('schema_version','benchmark','coverage','datasets','methods','scopes','source')}
25
  if 'dataset_summary' in data:
26
  payload['dataset_summary'] = data['dataset_summary']
data.json CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:605712c81fe1189cbfb804e52a680c17c55570ac4dfaa296cdd7e2c747ece9c9
3
- size 35484449
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:099f735c91c5ef4dff2b15ea2851caa6edd16f304f0063bfe6de948f475b4ba7
3
+ size 37791099
export_results.py CHANGED
@@ -158,12 +158,27 @@ def _configuration_rows(
158
  )
159
 
160
  bundle_dir = BACKEND_DIR / "dataset" / dataset_key
161
- report = read_benchmark_report(
162
- bundle_dir,
163
- dataset_key,
164
- data_type=scope["data_type"],
165
- implement_language=runtime,
166
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  if report is None or report.empty:
168
  return []
169
  if scope["data_type"] == "float":
@@ -217,8 +232,32 @@ def _configuration_rows(
217
  original_bytes = _finite(record.originalSize, positive=True)
218
  compressed_bytes = _finite(record.compressedSize, positive=True)
219
  points = original_bytes / 8.0
220
- compression_time_ns = _finite(record.compressionTime)
221
- decompression_time_ns = _finite(record.decompressionTime)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  rows.append(
223
  {
224
  "dataset": dataset_key,
@@ -231,11 +270,12 @@ def _configuration_rows(
231
  "original_size_bytes": int(round(original_bytes)),
232
  "compressed_size_bytes": int(round(compressed_bytes)),
233
  "point_count": int(round(points)),
234
- "compression_time_ns": compression_time_ns,
235
- "decompression_time_ns": decompression_time_ns,
236
  "compression_rate": compressed_bytes / original_bytes,
237
- "compression_time_ns_per_point": compression_time_ns / points,
238
- "decompression_time_ns_per_point": decompression_time_ns / points,
 
239
  }
240
  )
241
  return sorted(rows, key=lambda row: (row["method"], row["algorithm_variant"]))
@@ -308,8 +348,33 @@ def _aggregate_rows(
308
  original_sum = sum(row["original_size_bytes"] for row in rows)
309
  compressed_sum = sum(row["compressed_size_bytes"] for row in rows)
310
  point_sum = sum(row["point_count"] for row in rows)
311
- comp_time_sum = sum(row["compression_time_ns"] for row in rows)
312
- decomp_time_sum = sum(row["decompression_time_ns"] for row in rows)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  return {
314
  "runtime": runtime,
315
  "scope": scope_key,
@@ -319,16 +384,15 @@ def _aggregate_rows(
319
  "dataset_count": len(rows),
320
  "overall_compression_rate": compressed_sum / original_sum,
321
  "average_compression_rate": sum(row["compression_rate"] for row in rows) / len(rows),
322
- "weighted_compression_time_ns_per_point": comp_time_sum / point_sum,
323
- "average_compression_time_ns_per_point": sum(
324
- row["compression_time_ns_per_point"] for row in rows
325
- )
326
- / len(rows),
327
- "weighted_decompression_time_ns_per_point": decomp_time_sum / point_sum,
328
- "average_decompression_time_ns_per_point": sum(
329
- row["decompression_time_ns_per_point"] for row in rows
330
- )
331
- / len(rows),
332
  "original_size_bytes": original_sum,
333
  "compressed_size_bytes": compressed_sum,
334
  "point_count": point_sum,
@@ -378,13 +442,10 @@ def main() -> int:
378
  )
379
  )
380
  configuration_aggregates, family_best_aggregates = _aggregate_current_rows(dataset_rows)
381
- # Keep the published per-dataset payload compact. These totals are used above
382
- # to produce the weighted aggregates; the UI needs the byte totals and
383
- # per-point times, not duplicate time totals or the derivable point count.
384
- for row in dataset_rows:
385
- row.pop("compression_time_ns", None)
386
- row.pop("decompression_time_ns", None)
387
- row.pop("point_count", None)
388
 
389
  families = sorted({row["family"] for row in dataset_rows})
390
  configurations = sorted(
@@ -466,7 +527,7 @@ def main() -> int:
466
  "current_result_selection": "visible configurations of methods categorized as lossless-design by the project; result-level float round trips are not established",
467
  "paper_result_selection": "the separate paper_table3 section contains its frozen 20-method C++ snapshot",
468
  "scope_note": "Current result rows cover numeric int/float report evidence for the retained 19 public datasets. They reflect the columns present in each report; they do not assert full-column coverage beyond those files.",
469
- "aggregation_note": "Average metrics are arithmetic means over available per-dataset measurements. Overall compression rate and weighted times use summed measured bytes, points, and nanoseconds only.",
470
  "selection_note": "Configuration views retain measured configurations passing the project's method-name filter. This does not certify every result as lossless. Family views choose the lowest-rate configuration independently for each dataset, scope, and runtime.",
471
  "notes": [
472
  "Missing combinations are unavailable rather than zero.",
@@ -477,7 +538,7 @@ def main() -> int:
477
  "compression_rate": "compressed bytes divided by original numeric bytes; lower is better",
478
  "average_compression_rate": "arithmetic mean of per-dataset compression rates",
479
  "overall_compression_rate": "sum of compressed bytes divided by sum of original numeric bytes",
480
- "average_time_ns_per_point": "arithmetic mean of per-dataset nanoseconds per 8-byte numeric point",
481
  "weighted_time_ns_per_point": "sum of measured nanoseconds divided by sum of 8-byte numeric points",
482
  "paper_median_balanced_score": "stored paper snapshot value; normalization is paper-scope specific and lower is better",
483
  },
 
158
  )
159
 
160
  bundle_dir = BACKEND_DIR / "dataset" / dataset_key
161
+ if scope["data_type"] == "overall":
162
+ report_parts = []
163
+ for report_type in ("int", "float"):
164
+ part = read_benchmark_report(
165
+ bundle_dir, dataset_key, data_type=report_type, implement_language=runtime
166
+ )
167
+ if part is not None and not part.empty:
168
+ part = part.copy()
169
+ part["_timing_source_type"] = report_type
170
+ report_parts.append(part)
171
+ report = pd.concat(report_parts, ignore_index=True) if report_parts else None
172
+ else:
173
+ report = read_benchmark_report(
174
+ bundle_dir,
175
+ dataset_key,
176
+ data_type=scope["data_type"],
177
+ implement_language=runtime,
178
+ )
179
+ if report is not None and not report.empty:
180
+ report = report.copy()
181
+ report["_timing_source_type"] = scope["data_type"]
182
  if report is None or report.empty:
183
  return []
184
  if scope["data_type"] == "float":
 
232
  original_bytes = _finite(record.originalSize, positive=True)
233
  compressed_bytes = _finite(record.compressedSize, positive=True)
234
  points = original_bytes / 8.0
235
+ timing_rows = work[
236
+ (work["algorithm"].astype(str) == str(record.algorithm))
237
+ & (pd.to_numeric(work["column_index"], errors="coerce") >= 0)
238
+ ].copy()
239
+ timing_column_count = len(timing_rows)
240
+ if timing_column_count:
241
+ timing_columns = timing_rows.groupby(
242
+ ["_timing_source_type", "column_index"], as_index=False
243
+ ).agg(
244
+ originalSize=("originalSize", "sum"),
245
+ compressionTime=("compressionTime", "sum"),
246
+ decompressionTime=("decompressionTime", "sum"),
247
+ )
248
+ timing_column_count = len(timing_columns)
249
+ timing_points = timing_columns["originalSize"] / 8.0
250
+ compression_time_ns_per_point = float(
251
+ (timing_columns["compressionTime"] / timing_points).mean()
252
+ )
253
+ decompression_time_ns_per_point = float(
254
+ (timing_columns["decompressionTime"] / timing_points).mean()
255
+ )
256
+ else:
257
+ # A column_index=-1 row is a whole-report aggregate. It cannot prove
258
+ # the requested arithmetic mean of individual column timings.
259
+ compression_time_ns_per_point = None
260
+ decompression_time_ns_per_point = None
261
  rows.append(
262
  {
263
  "dataset": dataset_key,
 
270
  "original_size_bytes": int(round(original_bytes)),
271
  "compressed_size_bytes": int(round(compressed_bytes)),
272
  "point_count": int(round(points)),
273
+ "compression_time_ns": _finite(record.compressionTime),
274
+ "decompression_time_ns": _finite(record.decompressionTime),
275
  "compression_rate": compressed_bytes / original_bytes,
276
+ "compression_time_ns_per_point": compression_time_ns_per_point,
277
+ "decompression_time_ns_per_point": decompression_time_ns_per_point,
278
+ "timing_column_count": timing_column_count,
279
  }
280
  )
281
  return sorted(rows, key=lambda row: (row["method"], row["algorithm_variant"]))
 
348
  original_sum = sum(row["original_size_bytes"] for row in rows)
349
  compressed_sum = sum(row["compressed_size_bytes"] for row in rows)
350
  point_sum = sum(row["point_count"] for row in rows)
351
+ elapsed_rows = [
352
+ row for row in rows
353
+ if row.get("point_count", 0) > 0
354
+ and row.get("compression_time_ns") is not None
355
+ and row.get("decompression_time_ns") is not None
356
+ ]
357
+ elapsed_point_sum = sum(row["point_count"] for row in elapsed_rows)
358
+ comp_time_sum = sum(row["compression_time_ns"] for row in elapsed_rows)
359
+ decomp_time_sum = sum(row["decompression_time_ns"] for row in elapsed_rows)
360
+ timing_rows = [
361
+ row for row in rows
362
+ if row.get("timing_column_count", 0) > 0
363
+ and row.get("compression_time_ns_per_point") is not None
364
+ and row.get("decompression_time_ns_per_point") is not None
365
+ ]
366
+ timing_column_count = sum(row["timing_column_count"] for row in timing_rows)
367
+ timing_complete = len(timing_rows) == len(rows)
368
+ average_compression_time = (
369
+ sum(row["compression_time_ns_per_point"] * row["timing_column_count"] for row in timing_rows)
370
+ / timing_column_count
371
+ if timing_column_count and timing_complete else None
372
+ )
373
+ average_decompression_time = (
374
+ sum(row["decompression_time_ns_per_point"] * row["timing_column_count"] for row in timing_rows)
375
+ / timing_column_count
376
+ if timing_column_count and timing_complete else None
377
+ )
378
  return {
379
  "runtime": runtime,
380
  "scope": scope_key,
 
384
  "dataset_count": len(rows),
385
  "overall_compression_rate": compressed_sum / original_sum,
386
  "average_compression_rate": sum(row["compression_rate"] for row in rows) / len(rows),
387
+ "weighted_compression_time_ns_per_point": (
388
+ comp_time_sum / elapsed_point_sum if elapsed_point_sum else None
389
+ ),
390
+ "average_compression_time_ns_per_point": average_compression_time,
391
+ "weighted_decompression_time_ns_per_point": (
392
+ decomp_time_sum / elapsed_point_sum if elapsed_point_sum else None
393
+ ),
394
+ "average_decompression_time_ns_per_point": average_decompression_time,
395
+ "timing_column_count": timing_column_count,
 
396
  "original_size_bytes": original_sum,
397
  "compressed_size_bytes": compressed_sum,
398
  "point_count": point_sum,
 
442
  )
443
  )
444
  configuration_aggregates, family_best_aggregates = _aggregate_current_rows(dataset_rows)
445
+ # Retain the source elapsed-time and point totals. A per-column arithmetic
446
+ # mean cannot be expanded back into elapsed time from a dataset point total,
447
+ # so later scope transforms must use these measured totals rather than infer
448
+ # them from the displayed mean.
 
 
 
449
 
450
  families = sorted({row["family"] for row in dataset_rows})
451
  configurations = sorted(
 
527
  "current_result_selection": "visible configurations of methods categorized as lossless-design by the project; result-level float round trips are not established",
528
  "paper_result_selection": "the separate paper_table3 section contains its frozen 20-method C++ snapshot",
529
  "scope_note": "Current result rows cover numeric int/float report evidence for the retained 19 public datasets. They reflect the columns present in each report; they do not assert full-column coverage beyond those files.",
530
+ "aggregation_note": "Compression and decompression time averages are arithmetic means of available per-column nanoseconds per point. Dataset rows without column-level timing evidence are unavailable for timing; any aggregate containing one of these rows also has unavailable timing and is excluded from timing-based scores. Overall compression rate and weighted times use summed measured bytes, points, and nanoseconds only.",
531
  "selection_note": "Configuration views retain measured configurations passing the project's method-name filter. This does not certify every result as lossless. Family views choose the lowest-rate configuration independently for each dataset, scope, and runtime.",
532
  "notes": [
533
  "Missing combinations are unavailable rather than zero.",
 
538
  "compression_rate": "compressed bytes divided by original numeric bytes; lower is better",
539
  "average_compression_rate": "arithmetic mean of per-dataset compression rates",
540
  "overall_compression_rate": "sum of compressed bytes divided by sum of original numeric bytes",
541
+ "average_time_ns_per_point": "arithmetic mean of per-column nanoseconds per numeric point; legacy CSV columns retain their 8-byte-equivalent point denominator",
542
  "weighted_time_ns_per_point": "sum of measured nanoseconds divided by sum of 8-byte numeric points",
543
  "paper_median_balanced_score": "stored paper snapshot value; normalization is paper-scope specific and lower is better",
544
  },
index.html CHANGED
@@ -3,11 +3,11 @@
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <meta name="description" content="THULab time-series compression benchmark: measured compression rates, encoding and decoding costs, and dataset coverage.">
7
  <meta name="color-scheme" content="light dark">
8
  <title>THULab | Time Series Compression Benchmark</title>
9
  <link rel="stylesheet" href="style.css?v=20261009-dbp-integrated-v1">
10
- <script src="app.js?v=20261009-coverage-v1" type="module"></script>
11
  </head>
12
  <body>
13
  <a class="skip-link" href="#content">Skip to results</a>
@@ -19,7 +19,7 @@
19
  <section class="intro" aria-labelledby="title">
20
  <p class="eyebrow">Time-series systems research</p>
21
  <h1 id="title">Time Series Compression Leaderboard</h1>
22
- <p class="intro-text">Compare published storage, encoding, and decoding measurements across time-series datasets.</p>
23
  <p class="snapshot" id="snapshot">Loading benchmark snapshot…</p>
24
  </section>
25
  <div class="summary-strip" id="summary" aria-label="Snapshot coverage">
@@ -62,9 +62,9 @@
62
  <div><h2 id="view-title">Overall leaderboard</h2><p id="selection-summary" class="muted" aria-live="polite">Reading published results…</p></div>
63
  <div class="toolbar-actions"><button id="reset" class="quiet-button">Reset filters</button><button id="download" class="primary-button" disabled>Download CSV</button></div>
64
  </div>
65
- <div class="results-note" id="results-note">Compression rate = compressed bytes / original bytes. Lower rates and lower ns/point are better. Metrics aggregate the numeric columns measured for each method; column coverage may differ.</div>
66
  <div id="score-controls" class="score-controls">
67
- <label>Score weights · a: rate / b: encode / c: decode<select id="weights" aria-label="Score weights a b c"></select></label>
68
  <p id="score-note" class="muted" aria-live="polite">Non-negative weights in steps of 0.1, summing to 1.0.</p>
69
  </div>
70
  <div class="table-settings">
@@ -72,7 +72,7 @@
72
  <details><summary>Display columns</summary><div id="column-options" class="column-options"></div></details>
73
  </div>
74
  <div id="chart-view" hidden>
75
- <div class="chart-controls"><label>Vertical axis<select id="chart-metric"><option value="compression_time">Encoding cost (ns/point)</option><option value="decompression_time">Decoding cost (ns/point)</option></select></label><span class="muted">Lower and further left is better. Click a point for method details.</span></div>
76
  <div id="chart" class="chart" aria-label="Compression rate and execution cost scatter plot"></div>
77
  </div>
78
  <div id="leaderboard-wrap" class="table-wrap" tabindex="0" aria-label="Scrollable sortable leaderboard">
@@ -108,9 +108,9 @@
108
  <dl class="definitions">
109
  <div><dt>Average compression rate ↓</dt><dd>Arithmetic mean of the per-dataset compression rates in the selected scope.</dd></div>
110
  <div><dt>Overall compression rate ↓</dt><dd>Total compressed bytes divided by total original bytes over the selected measured records.</dd></div>
111
- <div><dt>Encoding / decoding cost ↓</dt><dd>Nanoseconds per point under each result's recorded measurement contract. Native-float measurements use valid native values; historical imported rows retain their source 8-byte-equivalent point definition. Compare timings within the same runtime and measurement contract.</dd></div>
112
  <div><dt>Coverage</dt><dd>Datasets with usable measurements for a method out of the measured datasets in the selected runtime, data type, and dataset selection. The summary dataset count uses the same selection. Missing results are never filled with zero.</dd></div>
113
- <div><dt>Composite score ↓</dt><dd>a × average compression rate + b × normalized average encoding time + c × normalized average decoding time. Each time uses min–max over all measured rows in the selected runtime, scope and row mode, before search, family and coverage filters. If all times are equal, their normalized value is zero. All measured inputs must have usable timing metrics.</dd></div>
114
  <div><dt>Average Rank ↓</dt><dd>Compute the same weighted score within each dataset, normalizing times within that dataset. Rank lower scores first; ties share their average position. Then take the arithmetic mean of the method's available dataset ranks. Weights are non-negative tenths summing to 1.0. Missing measurements do not receive ranks.</dd></div>
115
  </dl>
116
  <h3>Reading the rankings</h3>
@@ -121,7 +121,7 @@
121
  <p><a href="data.json" download>Result snapshot (JSON)</a> · <a href="manifest.json" download>Source manifest (JSON)</a></p>
122
  <div id="provenance"></div>
123
  <h3>Contribute a result</h3>
124
- <p>Run the project benchmark and provide the codec/configuration, input identity, encoding and decoding measurements, execution environment, and round-trip validation evidence. Results can be updated from the source project using the snapshot exporter included in the Space repository.</p>
125
  </section>
126
  <noscript><p class="error-message">Enable JavaScript to filter and sort this leaderboard. You can also download <a href="data.json">the results JSON</a>.</p></noscript>
127
  <footer><span>THULab · Time Series Compression Benchmark</span><span id="footer-snapshot"></span></footer>
 
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="description" content="THULab time-series compression benchmark: measured compression rates, compression and decompression costs, and dataset coverage.">
7
  <meta name="color-scheme" content="light dark">
8
  <title>THULab | Time Series Compression Benchmark</title>
9
  <link rel="stylesheet" href="style.css?v=20261009-dbp-integrated-v1">
10
+ <script src="app.js?v=20261009-column-mean-v1" type="module"></script>
11
  </head>
12
  <body>
13
  <a class="skip-link" href="#content">Skip to results</a>
 
19
  <section class="intro" aria-labelledby="title">
20
  <p class="eyebrow">Time-series systems research</p>
21
  <h1 id="title">Time Series Compression Leaderboard</h1>
22
+ <p class="intro-text">Compare published storage, compression, and decompression measurements across time-series datasets.</p>
23
  <p class="snapshot" id="snapshot">Loading benchmark snapshot…</p>
24
  </section>
25
  <div class="summary-strip" id="summary" aria-label="Snapshot coverage">
 
62
  <div><h2 id="view-title">Overall leaderboard</h2><p id="selection-summary" class="muted" aria-live="polite">Reading published results…</p></div>
63
  <div class="toolbar-actions"><button id="reset" class="quiet-button">Reset filters</button><button id="download" class="primary-button" disabled>Download CSV</button></div>
64
  </div>
65
+ <div class="results-note" id="results-note">Compression rate = compressed bytes / original bytes. Lower rates and lower ns/point are better. Compression and decompression costs are the arithmetic mean of the measured columns' ns/point values; column coverage may differ. Historical aggregate-only timings without per-column evidence are not inferred.</div>
66
  <div id="score-controls" class="score-controls">
67
+ <label>Score weights · a: rate / b: compress / c: decompress<select id="weights" aria-label="Score weights a b c"></select></label>
68
  <p id="score-note" class="muted" aria-live="polite">Non-negative weights in steps of 0.1, summing to 1.0.</p>
69
  </div>
70
  <div class="table-settings">
 
72
  <details><summary>Display columns</summary><div id="column-options" class="column-options"></div></details>
73
  </div>
74
  <div id="chart-view" hidden>
75
+ <div class="chart-controls"><label>Vertical axis<select id="chart-metric"><option value="compression_time">Compression cost (ns/point)</option><option value="decompression_time">Decompression cost (ns/point)</option></select></label><span class="muted">Lower and further left is better. Click a point for method details.</span></div>
76
  <div id="chart" class="chart" aria-label="Compression rate and execution cost scatter plot"></div>
77
  </div>
78
  <div id="leaderboard-wrap" class="table-wrap" tabindex="0" aria-label="Scrollable sortable leaderboard">
 
108
  <dl class="definitions">
109
  <div><dt>Average compression rate ↓</dt><dd>Arithmetic mean of the per-dataset compression rates in the selected scope.</dd></div>
110
  <div><dt>Overall compression rate ↓</dt><dd>Total compressed bytes divided by total original bytes over the selected measured records.</dd></div>
111
+ <div><dt>Compression / decompression cost ↓</dt><dd>For each result with column-level timing evidence, calculate ns/point independently for every usable numeric column, then take the arithmetic mean across those columns. Each column contributes equally. Overall timing pools all measured columns across the selected datasets; if any selected dataset/configuration lacks column-level timing evidence, that timing is Not reported. Historical aggregate-only timing rows without recoverable per-column values are not inferred. Native-float measurements use valid native values; historical imported rows retain their source 8-byte-equivalent point definition. Compare timings within the same runtime and measurement contract.</dd></div>
112
  <div><dt>Coverage</dt><dd>Datasets with usable measurements for a method out of the measured datasets in the selected runtime, data type, and dataset selection. The summary dataset count uses the same selection. Missing results are never filled with zero.</dd></div>
113
+ <div><dt>Composite score ↓</dt><dd>a × average compression rate + b × normalized average compression time + c × normalized average decompression time. Each time uses min–max over all measured rows in the selected runtime, scope and row mode, before search, family and coverage filters. If all times are equal, their normalized value is zero. All measured inputs must have usable timing metrics.</dd></div>
114
  <div><dt>Average Rank ↓</dt><dd>Compute the same weighted score within each dataset, normalizing times within that dataset. Rank lower scores first; ties share their average position. Then take the arithmetic mean of the method's available dataset ranks. Weights are non-negative tenths summing to 1.0. Missing measurements do not receive ranks.</dd></div>
115
  </dl>
116
  <h3>Reading the rankings</h3>
 
121
  <p><a href="data.json" download>Result snapshot (JSON)</a> · <a href="manifest.json" download>Source manifest (JSON)</a></p>
122
  <div id="provenance"></div>
123
  <h3>Contribute a result</h3>
124
+ <p>Run the project benchmark and provide the codec/configuration, input identity, compression and decompression measurements, execution environment, and round-trip validation evidence. Results can be updated from the source project using the snapshot exporter included in the Space repository.</p>
125
  </section>
126
  <noscript><p class="error-message">Enable JavaScript to filter and sort this leaderboard. You can also download <a href="data.json">the results JSON</a>.</p></noscript>
127
  <footer><span>THULab · Time Series Compression Benchmark</span><span id="footer-snapshot"></span></footer>
manifest.json CHANGED
@@ -25,10 +25,13 @@
25
  "python"
26
  ],
27
  "scope_count": 5,
28
- "source_report_file_count": 108
 
 
 
29
  },
30
  "data_file": "data.json",
31
- "data_sha256": "605712c81fe1189cbfb804e52a680c17c55570ac4dfaa296cdd7e2c747ece9c9",
32
  "dataset_catalog_sha256": "d69abc85742dc050e4cdab1e5a03fd3392a0e0f1964b2a88ca87ba7f768b7385",
33
  "excluded_content": [
34
  "raw datasets",
@@ -762,5 +765,16 @@
762
  "sha256": "d69abc85742dc050e4cdab1e5a03fd3392a0e0f1964b2a88ca87ba7f768b7385",
763
  "status": "reviewed_public_catalog"
764
  }
765
- ]
 
 
 
 
 
 
 
 
 
 
 
766
  }
 
25
  "python"
26
  ],
27
  "scope_count": 5,
28
+ "source_report_file_count": 108,
29
+ "timing_column_mean_dataset_result_count": 64879,
30
+ "timing_column_observation_count": 1435539,
31
+ "timing_unavailable_dataset_result_count": 340
32
  },
33
  "data_file": "data.json",
34
+ "data_sha256": "099f735c91c5ef4dff2b15ea2851caa6edd16f304f0063bfe6de948f475b4ba7",
35
  "dataset_catalog_sha256": "d69abc85742dc050e4cdab1e5a03fd3392a0e0f1964b2a88ca87ba7f768b7385",
36
  "excluded_content": [
37
  "raw datasets",
 
765
  "sha256": "d69abc85742dc050e4cdab1e5a03fd3392a0e0f1964b2a88ca87ba7f768b7385",
766
  "status": "reviewed_public_catalog"
767
  }
768
+ ],
769
+ "timing_recalculation": {
770
+ "aggregate_only_rows_excluded_from_timing": 340,
771
+ "dbp_float_audit_path": "analysis_outputs/dbp_float_e2e_20261009/publish_payload/dbp-audit.json",
772
+ "dbp_float_audit_sha256": "febe0d4494cf3778bb3e386ac8674f24406e29d1d7d9b47bd834b332b0e201fb",
773
+ "dbp_precision_source_file_count": 376,
774
+ "dbp_precision_source_hash_status": "all_match",
775
+ "generated_at": "2026-10-09T06:52:14.348090+00:00",
776
+ "policy": "arithmetic_mean_of_per_column_ns_per_point",
777
+ "result_csv_file_count": 108,
778
+ "result_csv_sha256_status": "all_match"
779
+ }
780
  }
ui-data.json CHANGED
The diff for this file is too large to render. See raw diff