save reconstructed arrays for lossy
15 changed files+468−9
python/ephys_compression_tests/algorithms/ans/__init__.pymodified+1−1View file
@@ -316,7 +316,7 @@ for ar_order in [2, 8]:
316316 return decode0_ar_lossy
317317 algorithm_dicts.append({
318318 "name": f"ans-ar{ar_order}-lossy-tol{tolerance}",
319- "version": "2",
319+ "version": "3",
320320 "encode": make_encode_ar_lossy(),
321321 "decode": make_decode_ar_lossy(),
322322 "description": f"ANS with lossy auto-regressive prediction encoding of order {ar_order} and tolerance {tolerance}",
python/ephys_compression_tests/algorithms/wavpack/__init__.pymodified+1−1View file
@@ -89,7 +89,7 @@ for bps in [2.5, 3, 4, 5, 6]:
8989 return wavpack_decode(x, dtype, shape)
9090 algorithm_dicts.append({
9191 "name": f"wavpack-lossy-{bps}",
92- "version": "2",
92+ "version": "3",
9393 "encode": encode_lossy,
9494 "decode": decode_lossy,
9595 "description": f"WavPack lossy with {bps} bits per sample",
python/ephys_compression_tests/run_benchmarks/_memobin.pymodified+26−0View file
@@ -136,6 +136,32 @@ def construct_dataset_url(
136136 return f"https://tempory.net/f/memobin/ephys_compression_tests/{path}"
137137
138138
139+def construct_reconstructed_url(
140+ algorithm_name: str,
141+ dataset_name: str,
142+ algorithm_version: str,
143+ dataset_version: str,
144+ system_version: str,
145+ format: str = "dat",
146+) -> str:
147+ """Construct the memobin URL for a reconstructed array.
148+
149+ Args:
150+ algorithm_name: Name of the algorithm
151+ dataset_name: Name of the dataset
152+ algorithm_version: Version of the algorithm
153+ dataset_version: Version of the dataset
154+ system_version: Version of the system
155+ format: File format ("dat", "npy", or "json")
156+
157+ Returns:
158+ The constructed memobin URL for the reconstructed array
159+ """
160+ version_str = f"v{algorithm_version}-{dataset_version}-{system_version}"
161+ path = f"reconstructed/{algorithm_name}/{dataset_name}/{version_str}/reconstructed.{format}"
162+ return f"https://tempory.net/f/memobin/ephys_compression_tests/{path}"
163+
164+
139165 def upload_to_memobin(
140166 data: dict | bytes,
141167 url: str,
python/ephys_compression_tests/run_benchmarks/benchmark_timing.pymodified+4−2View file
@@ -48,7 +48,7 @@ def run_compression_benchmark(
4848 decode_fn: Callable,
4949 verbose: bool = True,
5050 lossy: bool = False,
51-) -> Tuple[Dict[str, Any], bytes]:
51+) -> Tuple[Dict[str, Any], bytes, np.ndarray]:
5252 """Run compression and decompression benchmarks for an algorithm.
5353
5454 Args:
@@ -57,11 +57,13 @@ def run_compression_benchmark(
5757 encode_fn: Compression function
5858 decode_fn: Decompression function
5959 verbose: Whether to print progress messages
60+ lossy: Whether the algorithm is lossy
6061
6162 Returns:
6263 Tuple containing:
6364 - result: Dictionary with benchmark metrics
6465 - encoded: Compressed data bytes
66+ - decoded: Decompressed data array
6567 """
6668 if data.ndim == 1:
6769 data = data[:, np.newaxis]
@@ -132,4 +134,4 @@ def run_compression_benchmark(
132134 "max_error": max_error,
133135 }
134136
135- return result, encoded
137+ return result, encoded, decoded
python/ephys_compression_tests/run_benchmarks/cache_management.pymodified+9−0View file
@@ -1,6 +1,7 @@
11 import os
22 import json
33 from typing import Optional, Dict, Any
4+import numpy as np
45 from ._memobin import (
56 construct_memobin_url,
67 download_from_memobin,
@@ -95,6 +96,7 @@ def save_result_to_cache(
9596 cache_dir: str,
9697 dataset_name: str,
9798 algorithm_name: str,
99+ reconstructed_data: Optional[np.ndarray] = None,
98100 ) -> None:
99101 """Save benchmark result and compressed data to cache.
100102
@@ -104,10 +106,12 @@ def save_result_to_cache(
104106 cache_dir: Directory to store cached results
105107 dataset_name: Name of the dataset
106108 algorithm_name: Name of the algorithm
109+ reconstructed_data: Optional reconstructed array for lossy algorithms
107110 """
108111 test_dir = os.path.join(cache_dir, dataset_name, algorithm_name)
109112 metadata_file = os.path.join(test_dir, "metadata.json")
110113 compressed_file = os.path.join(test_dir, "compressed.dat")
114+ reconstructed_file = os.path.join(test_dir, "reconstructed.dat")
111115
112116 os.makedirs(test_dir, exist_ok=True)
113117 cache_data = {"result": result}
@@ -116,3 +120,8 @@ def save_result_to_cache(
116120 json.dump(cache_data, f, indent=2)
117121 with open(compressed_file, "wb") as f:
118122 f.write(encoded_data)
123+
124+ # Save reconstructed data for lossy algorithms
125+ if reconstructed_data is not None:
126+ with open(reconstructed_file, "wb") as f:
127+ f.write(reconstructed_data.tobytes())
python/ephys_compression_tests/run_benchmarks/collect_info.pymodified+30−1View file
@@ -1,5 +1,5 @@
11 from typing import List, Dict, Any
2-from ._memobin import construct_dataset_url
2+from ._memobin import construct_dataset_url, construct_reconstructed_url
33 from ..types import Algorithm
44
55 GITHUB_ALGORITHMS_PREFIX = "https://github.com/magland/ephys_compression_tests/blob/main/python/ephys_compression_tests/algorithms/"
@@ -61,3 +61,32 @@ def collect_dataset_info(datasets: List[Dict[str, Any]]) -> List[Dict[str, Any]]
6161 info["source_file"] = GITHUB_DATASETS_PREFIX + dataset.source_file
6262 dataset_info.append(info)
6363 return dataset_info
64+
65+
66+def add_reconstructed_urls_to_results(results: List[Dict[str, Any]], algorithms: List[Algorithm]) -> None:
67+ """Add reconstructed data URL to results for lossy algorithms.
68+
69+ Args:
70+ results: List of benchmark result dictionaries (modified in-place)
71+ algorithms: List of algorithm objects
72+ """
73+ # Create a lookup dict for algorithm tags
74+ alg_tags_map = {alg.name: alg.tags for alg in algorithms}
75+
76+ for result in results:
77+ alg_name = result.get("algorithm")
78+ if not alg_name:
79+ continue
80+
81+ alg_tags = alg_tags_map.get(alg_name, [])
82+
83+ # Only add reconstructed URL for lossy algorithms (just .dat format)
84+ if "lossy" in alg_tags:
85+ result["reconstructed_url_raw"] = construct_reconstructed_url(
86+ alg_name,
87+ result["dataset"],
88+ result["algorithm_version"],
89+ result["dataset_version"],
90+ result["system_version"],
91+ "dat",
92+ )
python/ephys_compression_tests/run_benchmarks/run_benchmarks.pymodified+25−3View file
@@ -7,6 +7,7 @@ from ..algorithms import algorithms
77 from ..datasets import datasets
88 from ._memobin import construct_memobin_url, upload_to_memobin
99 from .upload_dataset import upload_dataset_to_memobin
10+from .upload_reconstructed import upload_reconstructed_to_memobin
1011 from .cache_management import check_cached_result, save_result_to_cache
1112 from .benchmark_timing import run_compression_benchmark
1213 from .collect_info import collect_algorithm_info, collect_dataset_info
@@ -152,7 +153,7 @@ def run_benchmarks(
152153
153154 # Run the benchmark
154155 lossy = "lossy" in alg_tags
155- result, encoded = run_compression_benchmark(
156+ result, encoded, decoded = run_compression_benchmark(
156157 data,
157158 alg_name,
158159 algorithm.encode,
@@ -173,13 +174,14 @@ def run_benchmarks(
173174 )
174175 results.append(result)
175176
176- # Save result and compressed data
177+ # Save result, compressed data, and reconstructed data (for lossy algorithms)
177178 save_result_to_cache(
178179 result,
179180 encoded,
180181 cache_dir,
181182 dataset.name,
182183 alg_name,
184+ reconstructed_data=decoded if lossy else None,
183185 )
184186 print(
185187 f" Results saved to: {os.path.join(cache_dir, dataset.name, alg_name)}"
@@ -201,15 +203,35 @@ def run_benchmarks(
201203 memobin_api_key,
202204 )
203205 if verbose:
204- print(" Successfully uploaded to memobin")
206+ print(" Successfully uploaded benchmark result to memobin")
205207 except Exception as e:
206208 print(f" Warning: Failed to upload to memobin: {str(e)}")
207209
210+ # Upload reconstructed array for lossy algorithms
211+ if lossy:
212+ try:
213+ upload_reconstructed_to_memobin(
214+ decoded,
215+ alg_name,
216+ dataset.name,
217+ algorithm.version,
218+ dataset.version,
219+ system_version,
220+ memobin_api_key,
221+ verbose,
222+ )
223+ except Exception as e:
224+ print(f" Warning: Failed to upload reconstructed data to memobin: {str(e)}")
225+
208226 print("\n=== Benchmark Run Complete ===\n")
209227
210228 # Collect algorithm and dataset information
211229 algorithm_info = collect_algorithm_info(algorithms)
212230 dataset_info = collect_dataset_info(datasets)
231+
232+ # Add reconstructed URLs to results for lossy algorithms
233+ from .collect_info import add_reconstructed_urls_to_results
234+ add_reconstructed_urls_to_results(results, algorithms_to_run)
213235
214236 # Upload final benchmark status
215237 if memobin_api_key and upload_enabled:
python/ephys_compression_tests/run_benchmarks/upload_reconstructed.pyadded+48−0View file
@@ -0,0 +1,48 @@
1+import numpy as np
2+from ._memobin import (
3+ construct_reconstructed_url,
4+ exists_in_memobin,
5+ upload_to_memobin,
6+)
7+
8+
9+def upload_reconstructed_to_memobin(
10+ data: np.ndarray,
11+ algorithm_name: str,
12+ dataset_name: str,
13+ algorithm_version: str,
14+ dataset_version: str,
15+ system_version: str,
16+ memobin_api_key: str,
17+ verbose: bool = True,
18+) -> None:
19+ """Upload reconstructed array to memobin as raw .dat format.
20+
21+ Args:
22+ data: The reconstructed numpy array to upload
23+ algorithm_name: Name of the algorithm
24+ dataset_name: Name of the dataset
25+ algorithm_version: Version of the algorithm
26+ dataset_version: Version of the dataset
27+ system_version: Version of the system
28+ memobin_api_key: API key for memobin
29+ verbose: Whether to print progress messages
30+ """
31+ try:
32+ # Upload raw .dat format
33+ reconstructed_url_raw = construct_reconstructed_url(
34+ algorithm_name, dataset_name, algorithm_version, dataset_version, system_version, "dat"
35+ )
36+ if not exists_in_memobin(reconstructed_url_raw):
37+ if verbose:
38+ print(" Uploading reconstructed array to memobin...")
39+ upload_to_memobin(
40+ data.tobytes(),
41+ reconstructed_url_raw,
42+ memobin_api_key,
43+ content_type="application/octet-stream",
44+ )
45+ if verbose:
46+ print(" Successfully uploaded reconstructed data")
47+ except Exception as e:
48+ print(f" Warning: Failed to upload reconstructed data to memobin: {str(e)}")
test_wavpack_issue.pyadded+127−0View file
@@ -0,0 +1,127 @@
1+#!/usr/bin/env python3
2+"""
3+Test script to isolate wavpack lossy compression issue with multi-channel data.
4+Tests compression ratio for single channel vs all channels with bps=3.
5+"""
6+
7+import numpy as np
8+import requests
9+import sys
10+from io import BytesIO
11+
12+# URL for test data
13+DATA_URL = "https://tempory.net/ephys-compression-tests/aind/aind_compression_np2_probeB_ch101-110.raw.npy"
14+
15+def download_data():
16+ """Download test data from URL."""
17+ print(f"Downloading data from {DATA_URL}...")
18+ response = requests.get(DATA_URL)
19+ response.raise_for_status()
20+ arr = np.load(BytesIO(response.content))
21+ print(f"Data shape: {arr.shape}, dtype: {arr.dtype}")
22+ return arr
23+
24+def wavpack_encode(x: np.ndarray, bps: float = None) -> bytes:
25+ """Encode array using WavPack."""
26+ from wavpack_numcodecs import WavPack
27+ if bps is not None:
28+ codec = WavPack(bps=bps)
29+ else:
30+ codec = WavPack()
31+ encoded = codec.encode(x)
32+ assert isinstance(encoded, bytes)
33+ return encoded
34+
35+def test_compression(data: np.ndarray, bps: int = 3):
36+ """Test compression for single channel vs all channels."""
37+
38+ print(f"\n{'='*60}")
39+ print(f"Testing WavPack with bps={bps}")
40+ print(f"{'='*60}\n")
41+
42+ # Test single channel (first channel)
43+ print("--- Single Channel Test ---")
44+ single_channel = data[:, 0:1].copy() # Keep 2D shape (N, 1) and ensure contiguous
45+ print(f"Single channel shape: {single_channel.shape}")
46+ print(f"Single channel size: {single_channel.nbytes} bytes")
47+
48+ encoded_single = wavpack_encode(single_channel, bps=bps)
49+ size_single = len(encoded_single)
50+ ratio_single = single_channel.nbytes / size_single
51+
52+ print(f"Compressed size: {size_single} bytes")
53+ print(f"Compression ratio: {ratio_single:.3f}x")
54+
55+ # Test all channels
56+ print(f"\n--- All Channels Test ---")
57+ all_channels = np.ascontiguousarray(data) # Ensure contiguous
58+ print(f"All channels shape: {all_channels.shape}")
59+ print(f"All channels size: {all_channels.nbytes} bytes")
60+
61+ encoded_all = wavpack_encode(all_channels, bps=bps)
62+ size_all = len(encoded_all)
63+ ratio_all = all_channels.nbytes / size_all
64+
65+ print(f"Compressed size: {size_all} bytes")
66+ print(f"Compression ratio: {ratio_all:.3f}x")
67+
68+ # Compare
69+ print(f"\n--- Comparison ---")
70+ print(f"Single channel compression ratio: {ratio_single:.3f}x")
71+ print(f"All channels compression ratio: {ratio_all:.3f}x")
72+
73+ # Expected: similar ratios if working correctly
74+ # If all-channel ratio is much worse, there may be an issue
75+ if ratio_all < ratio_single * 0.5:
76+ print(f"\n⚠️ WARNING: All-channel compression ratio is significantly worse!")
77+ print(f" This suggests a potential issue with multi-channel compression.")
78+ elif ratio_all < ratio_single * 0.9:
79+ print(f"\n⚠️ NOTICE: All-channel compression ratio is somewhat worse.")
80+ else:
81+ print(f"\n✓ Compression ratios are similar - working as expected.")
82+
83+ return {
84+ 'single_channel_ratio': ratio_single,
85+ 'all_channels_ratio': ratio_all,
86+ 'single_channel_size': size_single,
87+ 'all_channels_size': size_all
88+ }
89+
90+def main():
91+ """Main test function."""
92+ try:
93+ # Download data
94+ data = download_data()
95+
96+ # Test with bps=3 (the reported issue)
97+ results = test_compression(data, bps=3)
98+
99+ # Also test with lossless for comparison
100+ print(f"\n\n{'='*60}")
101+ print(f"For comparison, testing lossless compression:")
102+ print(f"{'='*60}\n")
103+
104+ # Lossless single channel
105+ single_channel = data[:, 0:1].copy()
106+ encoded_single_lossless = wavpack_encode(single_channel)
107+ ratio_single_lossless = single_channel.nbytes / len(encoded_single_lossless)
108+ print(f"Single channel lossless ratio: {ratio_single_lossless:.3f}x")
109+
110+ # Lossless all channels
111+ all_channels = np.ascontiguousarray(data)
112+ encoded_all_lossless = wavpack_encode(all_channels)
113+ ratio_all_lossless = all_channels.nbytes / len(encoded_all_lossless)
114+ print(f"All channels lossless ratio: {ratio_all_lossless:.3f}x")
115+
116+ print(f"\n{'='*60}")
117+ print("Test completed successfully!")
118+ print(f"{'='*60}")
119+
120+ except Exception as e:
121+ print(f"\n❌ Error during test: {e}", file=sys.stderr)
122+ import traceback
123+ traceback.print_exc()
124+ sys.exit(1)
125+
126+if __name__ == "__main__":
127+ main()
web-ui/src/components/dataset/ComparisonModeSelector.tsxadded+57−0View file
@@ -0,0 +1,57 @@
1+import { ComparisonMode } from "../../types/comparison";
2+
3+interface ComparisonModeSelectorProps {
4+ mode: ComparisonMode;
5+ onModeChange: (mode: ComparisonMode) => void;
6+}
7+
8+export const ComparisonModeSelector = ({
9+ mode,
10+ onModeChange,
11+}: ComparisonModeSelectorProps) => {
12+ const modes: { value: ComparisonMode; label: string }[] = [
13+ { value: "original", label: "Original Only" },
14+ { value: "overlay", label: "Overlay (Original + Reconstructed)" },
15+ { value: "residuals", label: "Residuals (Original - Reconstructed)" },
16+ { value: "side-by-side", label: "Side-by-Side" },
17+ ];
18+
19+ return (
20+ <div style={{ marginBottom: "16px" }}>
21+ <label
22+ style={{
23+ display: "block",
24+ marginBottom: "8px",
25+ fontSize: "14px",
26+ fontWeight: "500",
27+ color: "#333",
28+ }}
29+ >
30+ View mode:
31+ </label>
32+ <div style={{ display: "flex", gap: "12px", flexWrap: "wrap" }}>
33+ {modes.map((m) => (
34+ <label
35+ key={m.value}
36+ style={{
37+ display: "flex",
38+ alignItems: "center",
39+ cursor: "pointer",
40+ fontSize: "14px",
41+ }}
42+ >
43+ <input
44+ type="radio"
45+ name="comparison-mode"
46+ value={m.value}
47+ checked={mode === m.value}
48+ onChange={() => onModeChange(m.value)}
49+ style={{ marginRight: "6px" }}
50+ />
51+ {m.label}
52+ </label>
53+ ))}
54+ </div>
55+ </div>
56+ );
57+};
web-ui/src/components/dataset/DatasetContent.tsxmodified+33−1View file
@@ -2,6 +2,9 @@ import { Dataset, BenchmarkData } from "../../types";
22 import { useEffect, useRef, useState } from "react";
33 import TimeseriesView from "./TimeseriesView";
44 import { BaseContent } from "../shared/BaseContent";
5+import { LossyAlgorithmSelector } from "./LossyAlgorithmSelector";
6+import { ComparisonModeSelector } from "./ComparisonModeSelector";
7+import { ReconstructedDataInfo, ComparisonMode } from "../../types/comparison";
58 import "../shared/ContentStyles.css";
69
710 interface DatasetContentProps {
@@ -25,6 +28,8 @@ export const DatasetContent = ({
2528 }: DatasetContentProps) => {
2629 const containerRef = useRef<HTMLDivElement>(null);
2730 const [containerWidth, setContainerWidth] = useState(1200);
31+ const [reconstructedInfo, setReconstructedInfo] = useState<ReconstructedDataInfo | null>(null);
32+ const [comparisonMode, setComparisonMode] = useState<ComparisonMode>("original");
2833
2934 useEffect(() => {
3035 if (!containerRef.current) return;
@@ -71,6 +76,27 @@ export const DatasetContent = ({
7176
7277 const timeseriesSection = (
7378 <div className="content-container">
79+ {benchmarkData && (
80+ <>
81+ <LossyAlgorithmSelector
82+ dataset={dataset}
83+ benchmarkResults={benchmarkData.results}
84+ selectedAlgorithm={reconstructedInfo?.algorithm || null}
85+ onSelectAlgorithm={(info) => {
86+ setReconstructedInfo(info);
87+ if (info === null) {
88+ setComparisonMode("original");
89+ }
90+ }}
91+ />
92+ {reconstructedInfo && (
93+ <ComparisonModeSelector
94+ mode={comparisonMode}
95+ onModeChange={setComparisonMode}
96+ />
97+ )}
98+ </>
99+ )}
74100 <div
75101 ref={containerRef}
76102 style={{
@@ -81,7 +107,13 @@ export const DatasetContent = ({
81107 padding: "1rem",
82108 }}
83109 >
84- <TimeseriesView width={containerWidth} height={250} dataset={dataset} />
110+ <TimeseriesView
111+ width={containerWidth}
112+ height={250}
113+ dataset={dataset}
114+ comparisonMode={comparisonMode}
115+ reconstructedInfo={reconstructedInfo}
116+ />
85117 </div>
86118 </div>
87119 );
web-ui/src/components/dataset/LossyAlgorithmSelector.tsxadded+87−0View file
@@ -0,0 +1,87 @@
1+import { BenchmarkResult, Dataset } from "../../types";
2+import { ReconstructedDataInfo } from "../../types/comparison";
3+
4+interface LossyAlgorithmSelectorProps {
5+ dataset: Dataset;
6+ benchmarkResults: BenchmarkResult[];
7+ selectedAlgorithm: string | null;
8+ onSelectAlgorithm: (info: ReconstructedDataInfo | null) => void;
9+}
10+
11+export const LossyAlgorithmSelector = ({
12+ dataset,
13+ benchmarkResults,
14+ selectedAlgorithm,
15+ onSelectAlgorithm,
16+}: LossyAlgorithmSelectorProps) => {
17+ // Filter for lossy algorithms with results for this dataset
18+ const lossyResults = benchmarkResults.filter(
19+ (result) =>
20+ result.dataset === dataset.name &&
21+ result.reconstructed_url_raw != null &&
22+ result.rmse != null
23+ );
24+
25+ if (lossyResults.length === 0) {
26+ return null;
27+ }
28+
29+ const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
30+ const value = event.target.value;
31+ if (value === "") {
32+ onSelectAlgorithm(null);
33+ return;
34+ }
35+
36+ const result = lossyResults.find((r) => r.algorithm === value);
37+ if (result && result.reconstructed_url_raw) {
38+ onSelectAlgorithm({
39+ algorithm: result.algorithm,
40+ rmse: result.rmse || 0,
41+ max_error: result.max_error || 0,
42+ reconstructedUrl: result.reconstructed_url_raw,
43+ datasetUrl: dataset.data_url_raw || "",
44+ datasetJsonUrl: dataset.data_url_json || "",
45+ });
46+ }
47+ };
48+
49+ return (
50+ <div style={{ marginBottom: "16px" }}>
51+ <label
52+ htmlFor="lossy-algorithm-select"
53+ style={{
54+ display: "block",
55+ marginBottom: "8px",
56+ fontSize: "14px",
57+ fontWeight: "500",
58+ color: "#333",
59+ }}
60+ >
61+ Compare with lossy reconstruction:
62+ </label>
63+ <select
64+ id="lossy-algorithm-select"
65+ value={selectedAlgorithm || ""}
66+ onChange={handleChange}
67+ style={{
68+ width: "100%",
69+ padding: "8px 12px",
70+ fontSize: "14px",
71+ borderRadius: "4px",
72+ border: "1px solid #ccc",
73+ backgroundColor: "white",
74+ cursor: "pointer",
75+ }}
76+ >
77+ <option value="">Original data only</option>
78+ {lossyResults.map((result) => (
79+ <option key={result.algorithm} value={result.algorithm}>
80+ {result.algorithm} (RMSE: {result.rmse?.toFixed(3)}, Max Error:{" "}
81+ {result.max_error?.toFixed(3)})
82+ </option>
83+ ))}
84+ </select>
85+ </div>
86+ );
87+};
web-ui/src/components/dataset/TimeseriesView.tsxmodified+9−0View file
@@ -5,22 +5,31 @@ import { useTimeseriesDataClient } from "../../hooks/useTimeseriesDataClient";
55 import { Dataset } from "../../types";
66 import { Margins, Range, WorkerMessage } from "./WorkerTypes";
77 import { initialState, timeseriesViewReducer } from "./timeseriesViewReducer";
8+import { ReconstructedDataInfo, ComparisonMode } from "../../types/comparison";
9+import { TimeseriesDataClient } from "../../hooks/TimeseriesDataClient";
810
911 interface TimeseriesViewProps {
1012 width: number;
1113 height: number;
1214 dataset: Dataset;
15+ comparisonMode?: ComparisonMode;
16+ reconstructedInfo?: ReconstructedDataInfo | null;
1317 }
1418
1519 const TimeseriesView: React.FC<TimeseriesViewProps> = ({
1620 width,
1721 height,
1822 dataset,
23+ comparisonMode = "original",
24+ reconstructedInfo = null,
1925 }) => {
2026 const { client, error: clientError } = useTimeseriesDataClient(dataset);
2127 const [dataT, setDataT] = useState<number[] | null>(null);
2228 const [dataY, setDataY] = useState<SupportedTypedArray | null>(null);
2329 const [dataYAll, setDataYAll] = useState<SupportedTypedArray[] | null>(null);
30+ const [dataYReconstructed, setDataYReconstructed] = useState<SupportedTypedArray | null>(null);
31+ const [dataYResiduals, setDataYResiduals] = useState<SupportedTypedArray | null>(null);
32+ const [reconstructedClient, setReconstructedClient] = useState<TimeseriesDataClient | null>(null);
2433 const [error, setError] = useState<string | null>(clientError);
2534 const [isLoading, setIsLoading] = useState(false);
2635 const [selectedChannel, setSelectedChannel] = useState<number | "all">(0);
web-ui/src/types.tsmodified+1−0View file
@@ -16,6 +16,7 @@ export interface BenchmarkResult {
1616 timestamp: number;
1717 rmse?: number;
1818 max_error?: number;
19+ reconstructed_url_raw?: string;
1920 }
2021
2122 export interface Algorithm {
web-ui/src/types/comparison.tsadded+10−0View file
@@ -0,0 +1,10 @@
1+export type ComparisonMode = "original" | "overlay" | "residuals" | "side-by-side";
2+
3+export interface ReconstructedDataInfo {
4+ algorithm: string;
5+ rmse: number;
6+ max_error: number;
7+ reconstructedUrl: string;
8+ datasetUrl: string;
9+ datasetJsonUrl: string;
10+}