format
7 changed files+273−6
.pre-commit-config.yamladded+8−0View file
@@ -0,0 +1,8 @@
1+repos:
2+ - repo: local
3+ hooks:
4+ - id: format-code
5+ name: Format Code
6+ entry: devel/format_code.sh
7+ language: script
8+ pass_filenames: false
README.mdmodified+22−0View file
@@ -30,6 +30,28 @@ Key components are located in the `zia_benchmark/src/zia_benchmark/` directory:
3030 - Core functionality:
3131 - `run_benchmarks.py`: Main benchmarking engine
3232
33+## Development
34+
35+### Code Formatting
36+
37+This project uses pre-commit hooks to automatically format code before each commit. The formatting includes:
38+- Python code formatting using black
39+- TypeScript/JavaScript code formatting using npm scripts
40+
41+To set up the pre-commit hooks after cloning the repository:
42+
43+1. Install pre-commit:
44+```bash
45+pip install pre-commit
46+```
47+
48+2. Install the git hook scripts:
49+```bash
50+pre-commit install
51+```
52+
53+After this setup, code will be automatically formatted when you make a commit.
54+
3355 ## Results
3456
3557 Latest benchmark results: https://magland.github.io/zia/
zia_benchmark/setup.pymodified+8−1View file
@@ -11,9 +11,16 @@ setup(
1111 "zstandard",
1212 "simple_ans",
1313 "requests",
14- "lindi"
14+ "lindi",
15+ "brotli",
16+ "click"
1517 ],
1618 python_requires=">=3.8",
19+ entry_points={
20+ "console_scripts": [
21+ "zia-benchmark=zia_benchmark.cli:main",
22+ ],
23+ },
1724 author="Jeremy Magland",
1825 description="Benchmarking compression methods for integer arrays",
1926 long_description=open("README.md").read(),
zia_benchmark/src/zia_benchmark/algorithms/__init__.pymodified+8−1View file
@@ -2,5 +2,12 @@ from .zlib import algorithms as zlib_algorithms
22 from .zstd import algorithms as zstd_algorithms
33 from .simple_ans import algorithms as simple_ans_algorithms
44 from .lzma import algorithms as lzma_algorithms
5+from .brotli import algorithms as brotli_algorithms
56
6-algorithms = zlib_algorithms + zstd_algorithms + simple_ans_algorithms + lzma_algorithms
7+algorithms = (
8+ zlib_algorithms
9+ + zstd_algorithms
10+ + simple_ans_algorithms
11+ + lzma_algorithms
12+ + brotli_algorithms
13+)
zia_benchmark/src/zia_benchmark/algorithms/brotli/__init__.pyadded+73−0View file
@@ -0,0 +1,73 @@
1+import numpy as np
2+import brotli
3+
4+
5+SOURCE_FILE = "brotli/__init__.py"
6+
7+
8+def brotli_delta_encode(x: np.ndarray, level: int) -> bytes:
9+ assert x.ndim == 1
10+ y = np.diff(x)
11+ y = np.insert(y, 0, x[0])
12+ buf = y.tobytes()
13+ compressed = brotli.compress(buf, quality=level)
14+ return compressed
15+
16+
17+def brotli_delta_decode(x: bytes, dtype: str) -> np.ndarray:
18+ buf = brotli.decompress(x)
19+ y = np.frombuffer(buf, dtype=dtype)
20+ return np.cumsum(y)
21+
22+
23+def brotli_encode(x: np.ndarray, level: int) -> bytes:
24+ assert x.ndim == 1
25+ buf = x.tobytes()
26+ compressed = brotli.compress(buf, quality=level)
27+ return compressed
28+
29+
30+def brotli_decode(x: bytes, dtype: str) -> np.ndarray:
31+ buf = brotli.decompress(x)
32+ y = np.frombuffer(buf, dtype=dtype)
33+ return y
34+
35+
36+algorithms = [
37+ {
38+ "name": "brotli-4",
39+ "version": "1",
40+ "encode": lambda x: brotli_encode(x, level=4),
41+ "decode": lambda x, dtype: brotli_decode(x, dtype),
42+ "source_file": SOURCE_FILE,
43+ },
44+ {
45+ "name": "brotli-6",
46+ "version": "1",
47+ "encode": lambda x: brotli_encode(x, level=6),
48+ "decode": lambda x, dtype: brotli_decode(x, dtype),
49+ "source_file": SOURCE_FILE,
50+ },
51+ {
52+ "name": "brotli-8",
53+ "version": "1",
54+ "encode": lambda x: brotli_encode(x, level=8),
55+ "decode": lambda x, dtype: brotli_decode(x, dtype),
56+ "source_file": SOURCE_FILE,
57+ },
58+ {
59+ "name": "brotli-11",
60+ "version": "1",
61+ "encode": lambda x: brotli_encode(x, level=11),
62+ "decode": lambda x, dtype: brotli_decode(x, dtype),
63+ "source_file": SOURCE_FILE,
64+ },
65+ {
66+ "name": "brotli-11-delta",
67+ "version": "1",
68+ "encode": lambda x: brotli_delta_encode(x, level=11),
69+ "decode": lambda x, dtype: brotli_delta_decode(x, dtype),
70+ "tags": ["delta_encoding"],
71+ "source_file": SOURCE_FILE,
72+ },
73+]
zia_benchmark/src/zia_benchmark/cli.pyadded+141−0View file
@@ -0,0 +1,141 @@
1+#!/usr/bin/env python3
2+
3+import click
4+from typing import List, Optional
5+from .run_benchmarks import run_benchmarks
6+from .algorithms import algorithms
7+from .datasets import datasets
8+
9+
10+def get_available_algorithms() -> List[str]:
11+ """Get list of available algorithm names"""
12+ return [alg["name"] for alg in algorithms]
13+
14+
15+def get_available_datasets() -> List[str]:
16+ """Get list of available dataset names"""
17+ return [ds["name"] for ds in datasets]
18+
19+
20+def filter_algorithms(selected: Optional[List[str]] = None) -> List[dict]:
21+ """Filter algorithms based on selected names"""
22+ if not selected:
23+ return algorithms
24+ return [alg for alg in algorithms if alg["name"] in selected]
25+
26+
27+def filter_datasets(selected: Optional[List[str]] = None) -> List[dict]:
28+ """Filter datasets based on selected names"""
29+ if not selected:
30+ return datasets
31+ return [ds for ds in datasets if ds["name"] in selected]
32+
33+
34+def validate_algorithms(ctx, param, value):
35+ if not value:
36+ return None
37+ available = get_available_algorithms()
38+ invalid = [alg for alg in value if alg not in available]
39+ if invalid:
40+ raise click.BadParameter(
41+ f"Invalid algorithm(s): {', '.join(invalid)}. "
42+ f"Available algorithms: {', '.join(available)}"
43+ )
44+ return value
45+
46+
47+def validate_datasets(ctx, param, value):
48+ if not value:
49+ return None
50+ available = get_available_datasets()
51+ invalid = [ds for ds in value if ds not in available]
52+ if invalid:
53+ raise click.BadParameter(
54+ f"Invalid dataset(s): {', '.join(invalid)}. "
55+ f"Available datasets: {', '.join(available)}"
56+ )
57+ return value
58+
59+
60+@click.group()
61+def cli():
62+ """Benchmark compression algorithms for integer arrays"""
63+ pass
64+
65+
66+@cli.command()
67+def list():
68+ """List available algorithms and datasets"""
69+ click.echo("\nAvailable Algorithms:")
70+ for alg in algorithms:
71+ desc = alg.get("description", "No description")
72+ click.echo(f" {alg['name']:<20} - {desc}")
73+
74+ click.echo("\nAvailable Datasets:")
75+ for ds in datasets:
76+ desc = ds.get("description", "No description")
77+ click.echo(f" {ds['name']:<20} - {desc}")
78+
79+
80+@cli.command()
81+@click.option(
82+ "--algorithm",
83+ "-a",
84+ multiple=True,
85+ callback=validate_algorithms,
86+ help="Algorithm(s) to benchmark (can be specified multiple times)",
87+)
88+@click.option(
89+ "--dataset",
90+ "-d",
91+ multiple=True,
92+ callback=validate_datasets,
93+ help="Dataset(s) to benchmark (can be specified multiple times)",
94+)
95+@click.option(
96+ "--cache-dir",
97+ default=".benchmark_cache",
98+ help="Directory to store cached results",
99+ type=click.Path(),
100+)
101+@click.option("--quiet", "-q", is_flag=True, help="Reduce output verbosity")
102+def run(algorithm, dataset, cache_dir, quiet):
103+ """Run benchmarks with specified options"""
104+ # Filter algorithms and datasets
105+ filtered_algorithms = filter_algorithms(algorithm)
106+ filtered_datasets = filter_datasets(dataset)
107+
108+ if not filtered_algorithms:
109+ click.echo("Error: No matching algorithms found", err=True)
110+ ctx = click.get_current_context()
111+ ctx.exit(1)
112+ if not filtered_datasets:
113+ click.echo("Error: No matching datasets found", err=True)
114+ ctx = click.get_current_context()
115+ ctx.exit(1)
116+
117+ # Run benchmarks with filtered options
118+ results = run_benchmarks(
119+ cache_dir=cache_dir,
120+ verbose=not quiet,
121+ selected_algorithms=filtered_algorithms,
122+ selected_datasets=filtered_datasets,
123+ )
124+
125+ # Print summary
126+ click.echo("\nBenchmark Summary:")
127+ for result in results["results"]:
128+ click.echo(
129+ f"\n{result['dataset']} + {result['algorithm']}:"
130+ f"\n Compression ratio: {result['compression_ratio']:.2f}x"
131+ f"\n Encode speed: {result['encode_mb_per_sec']:.2f} MB/s"
132+ f"\n Decode speed: {result['decode_mb_per_sec']:.2f} MB/s"
133+ )
134+
135+
136+def main():
137+ cli()
138+
139+
140+if __name__ == "__main__":
141+ main()
zia_benchmark/src/zia_benchmark/run_benchmarks.pymodified+13−4View file
@@ -1,7 +1,7 @@
11 import time
22 import json
33 import os
4-from typing import Dict, Any, Tuple, List
4+from typing import Dict, Any, Tuple, List, Optional
55 import numpy as np
66 from statistics import median
77 from .algorithms import algorithms
@@ -41,7 +41,10 @@ def is_compatible(algorithm_tags: List[str], dataset_tags: List[str]) -> bool:
4141
4242
4343 def run_benchmarks(
44- cache_dir: str = ".benchmark_cache", verbose: bool = True
44+ cache_dir: str = ".benchmark_cache",
45+ verbose: bool = True,
46+ selected_algorithms: Optional[List[dict]] = None,
47+ selected_datasets: Optional[List[dict]] = None,
4548 ) -> Dict[str, Any]:
4649 """Run all benchmarks, with caching based on algorithm and dataset versions.
4750
@@ -66,8 +69,14 @@ def run_benchmarks(
6669 results = []
6770 print("\nRunning benchmarks for all dataset-algorithm combinations...")
6871
72+ # Use selected datasets/algorithms or fall back to all
73+ datasets_to_run = selected_datasets if selected_datasets is not None else datasets
74+ algorithms_to_run = (
75+ selected_algorithms if selected_algorithms is not None else algorithms
76+ )
77+
6978 # Run benchmarks for each dataset and algorithm combination
70- for dataset in datasets:
79+ for dataset in datasets_to_run:
7180 dataset_tags = dataset.get("tags", [])
7281 print(f"\n*** Dataset: {dataset['name']} (tags: {dataset_tags}) ***")
7382
@@ -76,7 +85,7 @@ def run_benchmarks(
7685 original_size = None
7786 dtype = None
7887
79- for algorithm in algorithms:
88+ for algorithm in algorithms_to_run:
8089 alg_name = algorithm["name"]
8190 alg_tags = algorithm.get("tags", [])
8291