initial commit for public eval

This commit is contained in:
Sebastian Rust
2026-05-27 21:00:28 +02:00
commit d77a1bf412
36 changed files with 5414 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
.git
local.mk
analysis/.venv
out/
derived/
figures/out.bak/
+4
View File
@@ -0,0 +1,4 @@
derived/*
out/
local.mk
analysis/.venv/
+27
View File
@@ -0,0 +1,27 @@
FROM rocker/tidyverse:4.4.1
RUN apt-get update && apt-get install -y --no-install-recommends \
make \
curl \
ca-certificates \
texlive-latex-base \
texlive-latex-recommended \
texlive-fonts-recommended \
&& rm -rf /var/lib/apt/lists/*
ENV RENV_CONFIG_REPOS_OVERRIDE=https://packagemanager.posit.co/cran/__linux__/jammy/latest \
RENV_CONFIG_AUTOLOADER_ENABLED=FALSE
COPY figures/renv.lock /tmp/figures/renv.lock
RUN R -e 'install.packages("renv"); setwd("/tmp/figures"); renv::restore(prompt = FALSE)' && rm -rf /tmp/figures
ADD https://astral.sh/uv/install.sh /uv-installer.sh
RUN sh /uv-installer.sh && rm /uv-installer.sh
ENV UV_PROJECT_ENVIRONMENT=/opt/venv \
PATH=/opt/venv/bin:/root/.local/bin:$PATH
COPY analysis/pyproject.toml analysis/uv.lock /tmp/analysis/
RUN cd /tmp/analysis && uv sync --frozen --no-install-project && rm -rf /tmp/analysis
WORKDIR /work/eval
+30
View File
@@ -0,0 +1,30 @@
# Configuration precedence (highest to lowest):
# 1. command line: `make RAW_DATA_ROOT=/mnt/data ...`
# 2. environment: `RAW_DATA_ROOT=/mnt/data make ...`
# 3. local.mk file (Make syntax, use ?= so env still wins)
# 4. Makefile default (../raw_data)
-include local.mk
export RAW_DATA_ROOT
.PHONY: all figures tikz values derive sanity clean
all: figures values
figures:
$(MAKE) -C figures figures
tikz:
$(MAKE) -C figures tikz
values:
$(MAKE) -C figures values
derive:
$(MAKE) -C figures derive
sanity:
$(MAKE) -C figures sanity
clean:
$(MAKE) -C figures clean
+60
View File
@@ -0,0 +1,60 @@
# Evaluation pipeline
Aggregates raw per-run measurements into figures (PDF + tikz) and a pgfkeys
`values.tex` for the paper.
## Run
With Docker (reproducible toolchain):
docker compose up --build
Outputs land in `out/`:
- `out/values.tex` -- all `\val{...}` keys for the paper
- `out/<experiment>/*.pdf` -- per-experiment figures
- `out/<experiment>/*.tex` -- tikz versions (via `make tikz`)
Without Docker, requires R (with renv), Python 3.12+, uv, LaTeX (for
tikzDevice's metric probe). Then:
make all
## Targets
- `make all` (= `figures values`)
- `make figures` -- PDFs for all experiments
- `make tikz` -- tikz `.tex` for all experiments
- `make values` -- regenerate `out/values.tex` only
- `make derive` -- aggregate raw data into `derived/<experiment>/*.csv`
- `make sanity` -- shape + NaN + solution-coverage checks on derived CSVs
## Configuration
`RAW_DATA_ROOT` controls where the aggregator reads raw aggregates from.
Precedence (highest first):
1. command line: `make RAW_DATA_ROOT=/mnt/data ...`
2. environment: `RAW_DATA_ROOT=/mnt/data make ...`
3. `local.mk` (copy from `local.mk.example`)
4. Makefile default: `../raw_data`
For Docker, the same variable picks the host path that gets bind-mounted as
`/raw_data` inside the container:
RAW_DATA_ROOT=/mnt/data docker compose up
## Layout
analysis/ Python: aggregation, sanity, gen_values, plugins
values/ one plugin per metric family (cpu, rtt, idt, ...)
figures/ R: ggplot scripts, common.R, renv.lock
out/ generated (gitignored)
derived/ intermediate CSVs (gitignored)
## Plugins
`analysis/values/*.py` each expose `compute(derived) -> (keys, sources)`.
Keys are pgfkeys paths (e.g. `datacenter-fq/sender-cpu/cake/mean-pct`);
`gen_values.py` merges them, wraps numbers in `\qty{}{}` / `\num{}` by
suffix, and writes one `values.tex`. Add a new metric by dropping a new
plugin into `analysis/values/`.
+2
View File
@@ -0,0 +1,2 @@
.venv/
__pycache__/
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""Aggregate raw per-run data into tidy unit-of-observation CSVs.
Outputs four CSVs into --out:
runs.csv one row per run: setup, solution, run, throughput_bps, cpu_sender, cpu_receiver
rtts.csv one row per RTT sample: setup, solution, rtt_us
idts.csv one row per per-flow inter-departure-time: setup, solution, run, stream_id, idt_us
firstflow_bins.csv one row per 50 us bin (first stream of run 1): setup, solution, t_ms, packets
"""
import argparse
import json
from pathlib import Path
import numpy as np
import pandas as pd
BIN_US = 50
MIN_PACKETS_PER_STREAM = 100
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--raw", required=True, type=Path, help="raw aggregates directory")
p.add_argument("--setup-prefix", required=True, help="directory prefix before _<solution>")
p.add_argument("--solutions", required=True, nargs="+",
help="solution names. Default lookup: <setup-prefix>_<solution>/. "
"Use 'label:dirname' to point a solution at a non-conforming directory "
"(e.g. cake:datacenter_cake_no-tso).")
p.add_argument("--out", required=True, type=Path, help="derived CSV output directory")
return p.parse_args()
def solution_spec(spec: str, setup_prefix: str) -> tuple[str, str]:
if ":" in spec:
label, dirname = spec.split(":", 1)
return label, dirname
return spec, f"{setup_prefix}_{spec}"
def load_metrics(sol_dir: Path) -> pd.DataFrame:
df = pd.read_csv(sol_dir / "metrics.csv")
return df[["run_num", "throughput_bps", "cpu_sender", "cpu_receiver"]].rename(columns={"run_num": "run"})
def load_rtts(sol_dir: Path) -> pd.DataFrame:
with open(sol_dir / "rtt.json") as f:
rtts = json.load(f)
return pd.DataFrame({"rtt_us": pd.Series(rtts, dtype="float64")})
def load_packets(sol_dir: Path) -> pd.DataFrame:
df = pd.read_csv(sol_dir / "packets.csv")
df = df[["run_num", "stream_id", "p4_timestamp_ns"]].rename(columns={"run_num": "run"})
df = df.astype({"run": "int64", "stream_id": "int64", "p4_timestamp_ns": "int64"})
stream_counts = df.groupby("stream_id").size()
keep = stream_counts[stream_counts >= MIN_PACKETS_PER_STREAM].index
df = df[df["stream_id"].isin(keep)]
return df.sort_values("p4_timestamp_ns", kind="mergesort").reset_index(drop=True)
def per_flow_idts(packets: pd.DataFrame) -> pd.DataFrame:
rows = []
for (run, stream_id), df_flow in packets.groupby(["run", "stream_id"], sort=True):
t = df_flow["p4_timestamp_ns"].to_numpy(dtype=np.float64)
if len(t) < 2:
continue
idts = np.diff(t) / 1e3
rows.append(pd.DataFrame({"run": run, "stream_id": stream_id, "idt_us": idts}))
return pd.concat(rows, ignore_index=True)
def firstflow_bins(packets: pd.DataFrame, bin_us: int) -> pd.DataFrame:
first_run = int(packets["run"].min())
df_run = packets[packets["run"] == first_run]
first_stream = int(df_run["stream_id"].min())
df_flow = df_run[df_run["stream_id"] == first_stream].sort_values("p4_timestamp_ns", kind="mergesort")
assert len(df_flow) >= MIN_PACKETS_PER_STREAM, f"first flow (run={first_run}, stream={first_stream}) too small"
t_s = df_flow["p4_timestamp_ns"].to_numpy(dtype=np.float64) / 1e9
bin_width_s = bin_us / 1e6
t0 = float(t_s.min())
idx = np.floor((t_s - t0) / bin_width_s).astype(np.int64)
n_bins = int(idx.max() + 1)
counts = np.bincount(idx, minlength=n_bins)
t_ms = (np.arange(n_bins) * bin_width_s) * 1000.0
return pd.DataFrame({"t_ms": t_ms, "packets": counts})
def main() -> None:
args = parse_args()
args.out.mkdir(parents=True, exist_ok=True)
runs_frames = []
rtts_frames = []
idts_frames = []
bins_frames = []
for spec in args.solutions:
label, dirname = solution_spec(spec, args.setup_prefix)
sol_dir = args.raw / dirname
if not sol_dir.is_dir():
raise SystemExit(f"missing solution directory: {sol_dir}")
print(f" {label} (from {dirname})")
runs = load_metrics(sol_dir)
runs.insert(0, "solution", label)
runs.insert(0, "setup", args.setup_prefix)
runs_frames.append(runs)
rtts = load_rtts(sol_dir)
rtts.insert(0, "solution", label)
rtts.insert(0, "setup", args.setup_prefix)
rtts_frames.append(rtts)
if not (sol_dir / "packets.csv").exists():
print(f" no packets.csv, skipping idts + firstflow_bins")
continue
packets = load_packets(sol_dir)
if packets.empty:
print(f" no packet data, skipping idts + firstflow_bins")
continue
idts = per_flow_idts(packets)
idts.insert(0, "solution", label)
idts.insert(0, "setup", args.setup_prefix)
idts_frames.append(idts)
bins = firstflow_bins(packets, BIN_US)
bins.insert(0, "solution", label)
bins.insert(0, "setup", args.setup_prefix)
bins_frames.append(bins)
pd.concat(runs_frames, ignore_index=True).to_csv(args.out / "runs.csv", index=False)
print(f"wrote {args.out / 'runs.csv'}")
pd.concat(rtts_frames, ignore_index=True).to_csv(args.out / "rtts.csv", index=False)
print(f"wrote {args.out / 'rtts.csv'}")
if idts_frames:
pd.concat(idts_frames, ignore_index=True).to_csv(args.out / "idts.csv", index=False)
print(f"wrote {args.out / 'idts.csv'}")
if bins_frames:
pd.concat(bins_frames, ignore_index=True).to_csv(args.out / "firstflow_bins.csv", index=False)
print(f"wrote {args.out / 'firstflow_bins.csv'}")
if __name__ == "__main__":
main()
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Check raw_data for duplicate per-run metrics rows."""
from __future__ import annotations
import csv
import hashlib
from pathlib import Path
import argparse
RAW = Path(__file__).parent.parent / "raw_data"
def metrics_payload(p: Path) -> tuple[str, ...]:
with p.open() as f:
rows = list(csv.reader(f))
return tuple(rows[1][2:])
def packet_signature(p: Path) -> tuple[int, int, tuple[int, ...], int, str]:
size = p.stat().st_size
streams: dict[int, int] = {}
timestamps: list[int] = []
with p.open() as f:
next(f)
for line in f:
parts = line.rstrip().split(",")
streams[int(parts[2])] = streams.get(int(parts[2]), 0) + 1
timestamps.append(int(parts[3]))
timestamps.sort()
span = timestamps[-1] - timestamps[0] if timestamps else 0
ts_hash = hashlib.sha1(",".join(map(str, timestamps)).encode()).hexdigest()[:12]
return size, len(timestamps), tuple(sorted(streams)), span, ts_hash
def check_solution(sol_dir: Path) -> list[str]:
payloads: dict[tuple[str, ...], list[int]] = {}
for run_dir in sol_dir.glob("tmp_run_*"):
n = int(run_dir.name.removeprefix("tmp_run_"))
try:
payloads.setdefault(metrics_payload(run_dir / "metrics_row.csv"), []).append(n)
except FileNotFoundError:
continue
report = []
for payload, runs in payloads.items():
if len(runs) < 2:
continue
runs.sort()
base = runs[0]
for other in runs[1:]:
base_pp = sol_dir / f"tmp_run_{base}" / "parsed_packets.csv"
other_pp = sol_dir / f"tmp_run_{other}" / "parsed_packets.csv"
if not base_pp.exists() or not other_pp.exists():
report.append(
f" run {base:>2} = run {other:<2} metrics: same "
f"parsed_packets: MISSING -> duplicate confirmed via metrics only"
)
continue
sig_base, sig_other = packet_signature(base_pp), packet_signature(other_pp)
ts_match = (sig_base[4] == sig_other[4])
if ts_match:
verdict = "DEEPER BUG (timestamps identical despite different stream IDs)"
elif sig_base[1] == sig_other[1] and sig_base[3] == sig_other[3]:
verdict = "BUG + suspicious (rows + span match but timestamps differ)"
else:
verdict = "BUG (independent captures, metrics row stamped from another run)"
report.append(
f" run {base:>2} = run {other:<2} metrics: same -> {verdict}\n"
f" run {base:>2}: size={sig_base[0]:>10} rows={sig_base[1]:>7} "
f"streams={list(sig_base[2])} span_ns={sig_base[3]} ts_hash={sig_base[4]}\n"
f" run {other:>2}: size={sig_other[0]:>10} rows={sig_other[1]:>7} "
f"streams={list(sig_other[2])} span_ns={sig_other[3]} ts_hash={sig_other[4]}"
)
return report
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--data", type=Path, default=RAW, help="raw_data root directory")
args = parser.parse_args()
total_bugs = 0
for exp_dir in sorted(args.data.iterdir()):
agg = exp_dir / "aggregates"
if not agg.is_dir():
continue
for sol_dir in sorted(agg.iterdir()):
if not sol_dir.is_dir():
continue
lines = check_solution(sol_dir)
if not lines:
continue
print(f"\n== {exp_dir.name}/{sol_dir.name} ==")
for line in lines:
print(line)
if "BUG" in line:
total_bugs += 1
print(f"\nconfirmed BUG count: {total_bugs}")
if __name__ == "__main__":
main()
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Generate values.tex from plugins under analysis/values/.
Each plugin exposes compute(derived) -> (keys, sources). Keys are merged into
one \\pgfkeys block; sources are recorded in the header for provenance.
"""
import argparse
import datetime as dt
import importlib.util
from pathlib import Path
from types import ModuleType
def load_plugins(plugin_dir: Path, skip: set[str]) -> list[tuple[str, ModuleType]]:
plugins: list[tuple[str, ModuleType]] = []
for p in sorted(plugin_dir.glob("*.py")):
if p.name.startswith("_") or p.stem in skip:
continue
spec = importlib.util.spec_from_file_location(f"values.{p.stem}", p)
if spec is None or spec.loader is None:
raise SystemExit(f"could not load plugin {p}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
if hasattr(mod, "compute"):
plugins.append((p.stem, mod))
return plugins
def file_mtime_iso(p: Path) -> str:
return dt.datetime.fromtimestamp(p.stat().st_mtime).isoformat(timespec="seconds")
# Map key-name suffix to siunitx unit.
UNIT_SUFFIXES = {
"-us": r"\micro\second",
"-ms": r"\milli\second",
"-gbps": r"\giga\bit\per\second",
"-pct": r"\percent",
"-kb": r"\kilo\byte",
}
NUM_SUFFIXES = ("-p", "-alpha", "-n", "-uncorrected")
NUM_PREFIXES = ("n-",)
def value_to_latex(key: str, value: str) -> str:
leaf = key.rsplit("/", 1)[-1]
for suffix, unit in UNIT_SUFFIXES.items():
if leaf.endswith(suffix):
return f"\\qty{{{value}}}{{{unit}}}"
if any(leaf.startswith(p) for p in NUM_PREFIXES) or any(leaf.endswith(s) for s in NUM_SUFFIXES):
return f"\\num{{{value}}}"
return value
def format_pgfkeys(all_keys: dict[str, str], provenance: list[str]) -> str:
lines: list[str] = []
lines.append("% Auto-generated by analysis/gen_values.py. Do not edit by hand.")
lines.append(f"% Generated: {dt.datetime.now().isoformat(timespec='seconds')}")
lines.extend(provenance)
for k, v in sorted(all_keys.items()):
lines.append(f"\\pgfkeyssetvalue{{/{k}}}{{{value_to_latex(k, v)}}}")
return "\n".join(lines) + "\n"
def main() -> None:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--derived", required=True, type=Path, help="derived/ root with per-experiment subdirs")
p.add_argument("--out", required=True, type=Path, help="output values.tex path")
p.add_argument("--plugins", type=Path, default=Path(__file__).parent / "values",
help="plugin directory (default: analysis/values)")
p.add_argument("--skip", nargs="*", default=[],
help="plugin names to skip (stem only, e.g. --skip cpu rtt)")
args = p.parse_args()
plugins = load_plugins(args.plugins, set(args.skip))
if not plugins:
raise SystemExit(f"no plugins found in {args.plugins} (after --skip)")
all_keys: dict[str, str] = {}
provenance: list[str] = ["% Plugins:"]
for name, mod in plugins:
keys, sources = mod.compute(args.derived)
dup = set(keys) & set(all_keys)
if dup:
raise SystemExit(f"plugin {name} duplicates keys: {sorted(dup)}")
all_keys.update(keys)
provenance.append(f"% {name}: {len(keys)} keys")
for src in sources:
provenance.append(f"% {src} (mtime {file_mtime_iso(src)})")
print(f" {name}: {len(keys)} keys from {len(sources)} sources")
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(format_pgfkeys(all_keys, provenance))
print(f"wrote {args.out} ({len(all_keys)} keys total)")
if __name__ == "__main__":
main()
+10
View File
@@ -0,0 +1,10 @@
[project]
name = "eval-analysis"
version = "0.1.0"
description = "Aggregate raw per-run data to tidy unit-of-observation CSVs"
requires-python = ">=3.12"
dependencies = [
"pandas>=2.2",
"numpy>=2.0",
"scipy>=1.13",
]
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Sanity check derived CSVs: shapes, no NaN, expected solution coverage."""
import argparse
import sys
from pathlib import Path
import pandas as pd
REQUIRED = {
"runs.csv": {"setup", "solution", "run", "throughput_bps", "cpu_sender", "cpu_receiver"},
"rtts.csv": {"setup", "solution", "rtt_us"},
}
OPTIONAL = {
"idts.csv": {"setup", "solution", "run", "stream_id", "idt_us"},
"firstflow_bins.csv": {"setup", "solution", "t_ms", "packets"},
}
def check(derived: Path, expected_solutions: set[str]) -> list[str]:
errors = []
for name, cols in {**REQUIRED, **OPTIONAL}.items():
path = derived / name
if not path.exists():
if name in REQUIRED:
errors.append(f"{name}: missing")
continue
df = pd.read_csv(path)
missing_cols = cols - set(df.columns)
if missing_cols:
errors.append(f"{name}: missing columns {missing_cols}")
if df.isna().any().any():
nan_cols = df.columns[df.isna().any()].tolist()
errors.append(f"{name}: NaN in columns {nan_cols}")
if len(df) == 0:
errors.append(f"{name}: empty")
sols = set(df["solution"].unique())
if sols != expected_solutions:
errors.append(f"{name}: solutions {sols} != expected {expected_solutions}")
return errors
def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("--derived", required=True, type=Path)
p.add_argument("--solutions", required=True, nargs="+")
args = p.parse_args()
errors = check(args.derived, set(args.solutions))
if errors:
for e in errors:
print(f" FAIL: {e}")
sys.exit(1)
print("ok")
if __name__ == "__main__":
main()
+232
View File
@@ -0,0 +1,232 @@
version = 1
revision = 3
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version < '3.14' and sys_platform == 'emscripten'",
"python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
]
[[package]]
name = "numpy"
version = "2.4.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/50/8e/b8041bc719f056afd864478029d52214789341ac6583437b0ee5031e9530/numpy-2.4.5.tar.gz", hash = "sha256:ca670567a5683b7c1670ec03e0ddd5862e10934e92a70751d68d7b7b74ca7f9f", size = 20735669, upload-time = "2026-05-15T20:25:19.492Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/18/3275231e98620002681c922e792db04d72c356e9d8073c387344fc0e4ff1/numpy-2.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:654fb8674b61b1c4bd568f944d13a908566fdcb0d797303521d4149d16da05ef", size = 16689166, upload-time = "2026-05-15T20:22:50.761Z" },
{ url = "https://files.pythonhosted.org/packages/db/23/000aab6a16bdec53307f0f72546b57a3ac9266a62d8c257bee97d85fd078/numpy-2.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4cd9f6fa7ce10dc4627f2bb81dd9075dab67e94632e04c2b638e12575ddaa862", size = 14699514, upload-time = "2026-05-15T20:22:53.678Z" },
{ url = "https://files.pythonhosted.org/packages/47/cc/ddaf3af9c46966fef5be879256f213d85a0c56c75d07a3b7defec7cf6b4c/numpy-2.4.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:4f5bc96d35d94e4ceab8b38a92241b4611e95dc44e63b9f1fa2a331858ee3507", size = 5204601, upload-time = "2026-05-15T20:22:56.257Z" },
{ url = "https://files.pythonhosted.org/packages/07/ea/627fadd11959b3c7759008f34c92a35af8ff942dd8284a66ced648bbe516/numpy-2.4.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4bb33e900ee81730ad77a258965134aa8ceac805124f7e5229347beda4b8d0aa", size = 6551360, upload-time = "2026-05-15T20:22:58.334Z" },
{ url = "https://files.pythonhosted.org/packages/a1/47/0728b986b8682d742ff68c16baa5af9d185484abfc635c5cc700f44e62be/numpy-2.4.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32f8f852273ef32b291201ac2a2c97629c4a1ee8632bb670e3443eaa09fc2e72", size = 15671157, upload-time = "2026-05-15T20:23:01.081Z" },
{ url = "https://files.pythonhosted.org/packages/d1/0b/b905ae82d9419dc38123523862db64978ca2954b69609c3ae8fdaca1084c/numpy-2.4.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685681e956fc8dcb75adc6ff26694e1dfd738b24bd8d4696c51ca0110157f912", size = 16645703, upload-time = "2026-05-15T20:23:04.358Z" },
{ url = "https://files.pythonhosted.org/packages/5f/24/e27fc3f5236b4118ed9eed67111675f5c61a07ea333acec87c869c3b359d/numpy-2.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f64dd84b277a737eb59513f6b9bb6195bf41ab11941ef15b2562dbab43fa8ef", size = 17021018, upload-time = "2026-05-15T20:23:07.021Z" },
{ url = "https://files.pythonhosted.org/packages/d3/a7/9041af38d527ab80a06a93570a77e29425b41507ad41f6acf5da78cfb4a4/numpy-2.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b42d9496f79e3a728192f05a42d86e36163217b7cdecb3813d0028a0aa6b72d7", size = 18368768, upload-time = "2026-05-15T20:23:09.44Z" },
{ url = "https://files.pythonhosted.org/packages/49/82/326a014442f32c2663434fd424d9298791f47f8a0f17585ad60519a5606e/numpy-2.4.5-cp312-cp312-win32.whl", hash = "sha256:86d980970f5110595ca14855768073b08585fc1acc36895de303e039e7dee4a5", size = 5962819, upload-time = "2026-05-15T20:23:11.631Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/cbf5d391b0b3a5e8cad264603e2fae256b0bde8ce43566b13b78faedc659/numpy-2.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:3333dba6a4e611d666f69e177ba8fe4140366ff681a5feb2374d3fd4fff3acb6", size = 12321621, upload-time = "2026-05-15T20:23:14.305Z" },
{ url = "https://files.pythonhosted.org/packages/3c/d0/0f18909d9bc37a5f3f969fc737d2bb5df9f2ff295f71b467e6f52a0d6c4e/numpy-2.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:4593d197270b894efeb538dcbe227e4bcf1c77f88c4c6bf933ead812cfaa4453", size = 10221430, upload-time = "2026-05-15T20:23:16.887Z" },
{ url = "https://files.pythonhosted.org/packages/e3/a4/fb50657c7cab297bf34edcd60a074cb0647f61771430d6363575274160fe/numpy-2.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ef248460b645c102026b82337cc4e88231909c66dd77b59ec6d6cac7e44f277", size = 16684760, upload-time = "2026-05-15T20:23:19.436Z" },
{ url = "https://files.pythonhosted.org/packages/3e/43/87e731299b9408eda705b3b9cb31c7bceb9347d2af9cbb16b2b1e4b5bc0f/numpy-2.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4603622bdcdbf8dccb1d9d5b21d16a7aa4e473ae6c8e14048d846fd4ca2907a0", size = 14694117, upload-time = "2026-05-15T20:23:21.832Z" },
{ url = "https://files.pythonhosted.org/packages/a9/c7/0b2bb8acea222e9dd6e582afc2bc553b89b8833cbdccc68e68f050fb31f8/numpy-2.4.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6c18d49c67689c562854b53fdc433b93e47c12952aa6fa6d59f185e1a5992419", size = 5199141, upload-time = "2026-05-15T20:23:24.066Z" },
{ url = "https://files.pythonhosted.org/packages/39/60/b6972b5d47033d90000f0097c81a98b9486589a2d7003bf725bff275cb0d/numpy-2.4.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b1c663ddc641f4192e90511bec61a09bc231e3bbdb996cdc6edbcaa0e528d685", size = 6546954, upload-time = "2026-05-15T20:23:26.099Z" },
{ url = "https://files.pythonhosted.org/packages/c1/e9/ed667cb12c11ca0adde431f685d3a5dd78e6f78b27228c581c8415198e9e/numpy-2.4.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93793222b524f692f12b2f8752ce8b1d9d9125b2bfd5dbf0fb69c92c5e1ce86c", size = 15669430, upload-time = "2026-05-15T20:23:28.147Z" },
{ url = "https://files.pythonhosted.org/packages/44/e5/679f6ffeb01294b0008e5ada4a113cb47617bc0e1819a529fd7973c6d7f4/numpy-2.4.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1616bde34b2bcba2fa9bde06217ce00da4f3d1bdfb264d54525a99e8fe170d83", size = 16633390, upload-time = "2026-05-15T20:23:31.622Z" },
{ url = "https://files.pythonhosted.org/packages/36/46/42bfffc9a780ec902ccd7470d3219192ee82b7b442710307dd85b4d121b0/numpy-2.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09d7d97da1c2c62f4818b3e150a57572ff8dcf1cf5ac501aac832ffd4ebd9566", size = 17020709, upload-time = "2026-05-15T20:23:34.08Z" },
{ url = "https://files.pythonhosted.org/packages/44/00/3e840bfee0cc6cec22209f2c97057f26eeb30de031e4933b4dfc0395416c/numpy-2.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d68d0b355ab2e39fe0de59001d7151dfdbbb880ef67baeed806661e03df5097", size = 18357818, upload-time = "2026-05-15T20:23:36.965Z" },
{ url = "https://files.pythonhosted.org/packages/72/cb/3447b400b9da84134575486f0f656541559b00d4b262477bce9b678bbca8/numpy-2.4.5-cp313-cp313-win32.whl", hash = "sha256:fe28b64777ddfa0eca9b5f51474034ebe3dcb8324f48f27b28f479085673ae33", size = 5961114, upload-time = "2026-05-15T20:23:39.586Z" },
{ url = "https://files.pythonhosted.org/packages/28/f9/a90d2220ffcdc0798f5d55bb5d5463cd6254ec9ef43f384dae80217d7a2f/numpy-2.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:fb4a6c9c537d6ccec9cc4aeae4261bd3cc79b070c67ddc0646f5b1c07fddde42", size = 12318553, upload-time = "2026-05-15T20:23:41.436Z" },
{ url = "https://files.pythonhosted.org/packages/b8/c9/96f531fb3234545315152d34efdf3de7daee81254448447eb619e8d16967/numpy-2.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:6d7df2da2e7ea0624a43aa368104b3a3ce14aae98ad4bb2c9a93fecef76f1c97", size = 10222200, upload-time = "2026-05-15T20:23:43.681Z" },
{ url = "https://files.pythonhosted.org/packages/e1/f4/a291caab5a3c520babf93ff77c54fd5fdb1ebbc3296cee2eb2146ce773b1/numpy-2.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:2a235607a18df941760a695927051af4b1cd5d3ee85840d0e2af816785771feb", size = 14821438, upload-time = "2026-05-15T20:23:45.911Z" },
{ url = "https://files.pythonhosted.org/packages/85/26/13dbb1159b864370568e7309063fd72667984df89db74e9caeb175d067c7/numpy-2.4.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:58dcf64969d870f36bc7fbd557d2617e997db7dc06261b6e3327148ea460d0a4", size = 5326663, upload-time = "2026-05-15T20:23:48.18Z" },
{ url = "https://files.pythonhosted.org/packages/7c/99/d233408072a0e019e2288e27edd23f7d572ccd4a73d1539baa3270ede85d/numpy-2.4.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:235f54b0156274d8fa3155db3ed6d2f401c7e8f3367c90db0a12f02a58fde6ed", size = 6646874, upload-time = "2026-05-15T20:23:49.856Z" },
{ url = "https://files.pythonhosted.org/packages/c5/00/eeb6f193dfe767725e952e0464f3e51f44145c5dd261cd7389aa36ac0713/numpy-2.4.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3b5bb65437a3555c648e706475db01c645559ca80dc8b03e4f202ea757e0d6", size = 15728147, upload-time = "2026-05-15T20:23:51.655Z" },
{ url = "https://files.pythonhosted.org/packages/e5/c9/b8ed039f1fde1b13a8807c893e7e2f9432a379f4d6401edecf0028da5b2c/numpy-2.4.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f09a7e5f017d7098c66522097c96257411c9620c0926212200d66bc8cee3976", size = 16681770, upload-time = "2026-05-15T20:23:53.933Z" },
{ url = "https://files.pythonhosted.org/packages/11/5b/0198ef6cb7016eca6d895d392106012138127fab23f46637e76d5e25c9f5/numpy-2.4.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:993a88d8fdd8554466a8765cd8bacd97ba56b70ca6b0a04bcdca77f5afed4222", size = 17086218, upload-time = "2026-05-15T20:23:56.646Z" },
{ url = "https://files.pythonhosted.org/packages/f0/fe/8821f3cfc660ae84c92ee158505941874b62c56a42e035a41425228cd8cf/numpy-2.4.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:84f58bed609b5669f5ad3d597901a4f1f86ee5b3c3708aaa55f05b4fe6e0f656", size = 18403542, upload-time = "2026-05-15T20:23:59.173Z" },
{ url = "https://files.pythonhosted.org/packages/0e/00/e64ecaf498865e7b091f57658b2c522503e5d1b70e43b807f5f8247e1d88/numpy-2.4.5-cp313-cp313t-win32.whl", hash = "sha256:7200c58f3f933ca61e66346667dcc8510bb111995e9ce15398a731e6a4afa4bb", size = 6084903, upload-time = "2026-05-15T20:24:01.506Z" },
{ url = "https://files.pythonhosted.org/packages/20/c0/354997dedaf74e8311c2cf9a6027b476fd8d424cb92189cc0ae2b25f501c/numpy-2.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c26c71080d35db5002102f5d9ff614d45de02aa1f7802943e691e063e5ee93bc", size = 12458420, upload-time = "2026-05-15T20:24:03.735Z" },
{ url = "https://files.pythonhosted.org/packages/66/dc/917ee5ea4a31ca1a6e4c9a85386477efa318dcc60db257c5ef4adda096c1/numpy-2.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:2caa576d1707b275cba1aeb60a5c50daa6fa2a3f28ecb08123bc05fd439005db", size = 10291826, upload-time = "2026-05-15T20:24:06.535Z" },
{ url = "https://files.pythonhosted.org/packages/ca/c1/3be0bf102fc17cff5bd142e3be0bfffabec6fa46da0a462396c76b0765d0/numpy-2.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:889ca2c072315de638a5194a772aa1fa2df92bdd6175f6a222d4784040424b61", size = 16683455, upload-time = "2026-05-15T20:24:08.988Z" },
{ url = "https://files.pythonhosted.org/packages/e8/3e/0742d724901fa36bc54b338c6e62e463a7601180da896aa44978f0adf004/numpy-2.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:89e89304fb1f8c3f0ecfa4a7d48f311dd79771336a940e920159d643d1307e77", size = 14704577, upload-time = "2026-05-15T20:24:11.542Z" },
{ url = "https://files.pythonhosted.org/packages/25/1c/196c610ff4c6782d697ba780ebdc1616be143213701bf22c1a270f3bf7dd/numpy-2.4.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:144fcc5a3a17679b2b82543b4a2d8dd29937230a7af13232b5f753872feb6361", size = 5209756, upload-time = "2026-05-15T20:24:14.091Z" },
{ url = "https://files.pythonhosted.org/packages/52/c0/23fb1bc506f774e03db66219a2830e720f4d3dbcaaddf855a7ff7bb6d96f/numpy-2.4.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:398bb16772b265b9fa5c07b07072646ea97137c10ffb62a9a087b277fc825c29", size = 6543937, upload-time = "2026-05-15T20:24:16.223Z" },
{ url = "https://files.pythonhosted.org/packages/9f/49/db4662c26e68520afcc84d672a6f9f5294063dee0e57a46d61afdaa7f9ed/numpy-2.4.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb352e7b8876da1249e72254736d6c58c505fa4e58a3d7e30efca241ca9ca9ce", size = 15685292, upload-time = "2026-05-15T20:24:17.978Z" },
{ url = "https://files.pythonhosted.org/packages/43/80/1315439acedd8398319bac177d6de3d48ab39c62cc0c810f74f0a9a73996/numpy-2.4.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7341b08ff8124d7353939778e2707b8732d03c78c1c30e0815aba2dacbe1245a", size = 16638528, upload-time = "2026-05-15T20:24:20.478Z" },
{ url = "https://files.pythonhosted.org/packages/56/81/364388600932618fe735d97fdd2437cb8dd87a23377ac11d8b9d5db098b7/numpy-2.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:deb01226f012539f3945261ffe1c10aec081a0fa0a5c925419933c70f3ae2d23", size = 17036709, upload-time = "2026-05-15T20:24:22.949Z" },
{ url = "https://files.pythonhosted.org/packages/32/4a/a1185b18a94a6d9587e54b437e7d0ba36ecf6e614f1bea03f5249912c64e/numpy-2.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d888bdf7335f76878c3c7b264ac1ff089863e211ec81249f9fb5795c2183dc25", size = 18363254, upload-time = "2026-05-15T20:24:25.402Z" },
{ url = "https://files.pythonhosted.org/packages/b9/8e/95c1d2ed15ae97750ede8c8a0ac487c9c01207afff430f47078b1d9d7dc5/numpy-2.4.5-cp314-cp314-win32.whl", hash = "sha256:15f90d1256e9b2320aff24fde44815b787ab6d7c49a1a11bfd8138b321c5f080", size = 6010184, upload-time = "2026-05-15T20:24:27.852Z" },
{ url = "https://files.pythonhosted.org/packages/aa/92/d063df4d63d988b20d881856c74df76c0c1786229bb870f3a52af0981d4d/numpy-2.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4bd2cd4ef9c0afa87de73723c0a33c0edff62143e1432917458e26d3d195d87f", size = 12450344, upload-time = "2026-05-15T20:24:29.856Z" },
{ url = "https://files.pythonhosted.org/packages/3d/64/c0ae481f7c3b2f85869bcd8fc5d30aa7c96b394162eef9c9315957f115c5/numpy-2.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:db304568c650e9d7039744d3575d0d287754debb2057d7c7b8cdfdc2c487a957", size = 10495674, upload-time = "2026-05-15T20:24:32.352Z" },
{ url = "https://files.pythonhosted.org/packages/57/89/c5a4c677acf17aa50ba09a15e61812f90baac42bb6ca38d112e005858351/numpy-2.4.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de2883e0d2c63eae1bab1a84b390dca74aabb3d20ea1f5d58f360853c83abf3", size = 14824078, upload-time = "2026-05-15T20:24:34.669Z" },
{ url = "https://files.pythonhosted.org/packages/e7/52/57e7144284f6b51ba93523e495ff239260b1ecd5257e3700a436332e5688/numpy-2.4.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:06760fe73ae5005008748d182de612c733542af3cde063d532cd2127561b27be", size = 5329246, upload-time = "2026-05-15T20:24:36.957Z" },
{ url = "https://files.pythonhosted.org/packages/b9/b3/09dbce80fd4a7db4318f2fc01eec0ae76f29306442b5a32d4b811d082cdf/numpy-2.4.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:4b51a01745cb04cc19278482207444b4d30728ce91c28d27a3bfae5fc6ff24c7", size = 6649877, upload-time = "2026-05-15T20:24:38.861Z" },
{ url = "https://files.pythonhosted.org/packages/30/c2/dbdb23e82d540b757690ef13f011c386fca6a63848eec6136baf8ce7cbed/numpy-2.4.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a05636d7937d0936f271e5ba957fa8d746b5be3c2025caa1a2508f4fe521d40", size = 15730534, upload-time = "2026-05-15T20:24:41.168Z" },
{ url = "https://files.pythonhosted.org/packages/c4/bd/68f6e9b3c20decf40ac06708a7b506757e3a8588efed32988d1b747316be/numpy-2.4.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b86f56048ed09c3bbe48962a7dff077c2fd3274f8cf981800f3b38eac49cc3", size = 16679741, upload-time = "2026-05-15T20:24:44.874Z" },
{ url = "https://files.pythonhosted.org/packages/39/1d/0fcac0b6b4ea1b50ca8fca05a34bed5c8d56e34c1cb5ffb04cf76109ac3c/numpy-2.4.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:130d58151c4db23e9fa860b84784e219a3aa3e030acc88a493ea37006c4dfd4c", size = 17085598, upload-time = "2026-05-15T20:24:47.603Z" },
{ url = "https://files.pythonhosted.org/packages/0b/e8/a472b2564cf6cc498ad7aa9741d9832648221b8ab8cc0dbef41faa248ede/numpy-2.4.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d475afc8cbe935ff5944f753d863bba774d7f4e1feaaa4102901e3e053ca5963", size = 18403855, upload-time = "2026-05-15T20:24:50.474Z" },
{ url = "https://files.pythonhosted.org/packages/b9/a4/da82196f8cc4bd28ecf17bd57008c84f3d4696caf06753d9bad45e4ad749/numpy-2.4.5-cp314-cp314t-win32.whl", hash = "sha256:27f4a6dc26353a860b348961b9aa9e009835688b435cfa105e873b8dc2c726f5", size = 6156900, upload-time = "2026-05-15T20:24:53.134Z" },
{ url = "https://files.pythonhosted.org/packages/98/31/860959b91a73d9a085006554fa3850da51a7ffab64599bac5097243438ab/numpy-2.4.5-cp314-cp314t-win_amd64.whl", hash = "sha256:76ac6e90f5e226011c88f9b7040a4bcae612518bc7e9adc127e697a13b28ad1a", size = 12638906, upload-time = "2026-05-15T20:24:55.009Z" },
{ url = "https://files.pythonhosted.org/packages/9e/2a/bbd3097913083ad07c0f28fc9629666221fc18923e17ce97ae22a5dccdd6/numpy-2.4.5-cp314-cp314t-win_arm64.whl", hash = "sha256:7c392e2c1bf596701d3c6832be7567eab5d5b0a13865036c33365ee097d37f8b", size = 10565875, upload-time = "2026-05-15T20:24:57.425Z" },
]
[[package]]
name = "pandas"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" },
{ url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" },
{ url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" },
{ url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" },
{ url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" },
{ url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" },
{ url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" },
{ url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" },
{ url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" },
{ url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" },
{ url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" },
{ url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" },
{ url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" },
{ url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" },
{ url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" },
{ url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" },
{ url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" },
{ url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" },
{ url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" },
{ url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" },
{ url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" },
{ url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" },
{ url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" },
{ url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" },
{ url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" },
{ url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" },
{ url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" },
{ url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" },
{ url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" },
{ url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" },
{ url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" },
{ url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" },
{ url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" },
{ url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" },
{ url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" },
{ url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" },
{ url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" },
{ url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" },
{ url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" },
]
[[package]]
name = "eval-analysis"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "numpy" },
{ name = "pandas" },
{ name = "scipy" },
]
[package.metadata]
requires-dist = [
{ name = "numpy", specifier = ">=2.0" },
{ name = "pandas", specifier = ">=2.2" },
{ name = "scipy", specifier = ">=1.13" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "scipy"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" },
{ url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" },
{ url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" },
{ url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" },
{ url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" },
{ url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" },
{ url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" },
{ url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" },
{ url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" },
{ url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" },
{ url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" },
{ url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" },
{ url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" },
{ url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" },
{ url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" },
{ url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" },
{ url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" },
{ url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" },
{ url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" },
{ url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" },
{ url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" },
{ url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" },
{ url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" },
{ url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" },
{ url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" },
{ url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" },
{ url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" },
{ url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" },
{ url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" },
{ url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" },
{ url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" },
{ url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" },
{ url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" },
{ url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" },
{ url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" },
{ url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" },
{ url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" },
{ url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" },
{ url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" },
{ url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" },
{ url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" },
{ url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" },
{ url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" },
{ url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" },
{ url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" },
{ url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" },
{ url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" },
{ url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" },
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "tzdata"
version = "2026.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
]
View File
+34
View File
@@ -0,0 +1,34 @@
"""First-flow packet-per-bin stats from firstflow_bins.csv.
"""
from pathlib import Path
import numpy as np
import pandas as pd
def _stats(x: np.ndarray) -> dict[str, str]:
return {
"max-pkts": str(int(x.max())),
"mean-pkts": f"{x.mean():.2f}",
"median-pkts": str(int(np.median(x))),
"p95-pkts": str(int(np.percentile(x, 95))),
"p99-pkts": str(int(np.percentile(x, 99))),
"sd-pkts": f"{x.std(ddof=1):.2f}",
"n-bins": str(x.size),
}
def compute(derived: Path) -> tuple[dict[str, str], list[Path]]:
out: dict[str, str] = {}
sources: list[Path] = []
for exp_dir in sorted(p for p in derived.iterdir() if p.is_dir()):
bins_path = exp_dir / "firstflow_bins.csv"
if not bins_path.exists():
continue
sources.append(bins_path)
df = pd.read_csv(bins_path)
for sol, sub in df.groupby("solution", sort=True):
x = sub["packets"].to_numpy()
for k, v in _stats(x).items():
out[f"{exp_dir.name}/bins/{sol}/{k}"] = v
return out, sources
+28
View File
@@ -0,0 +1,28 @@
"""Sender + receiver CPU per (experiment, solution): mean and sample SD in %."""
from pathlib import Path
import pandas as pd
METRICS = {
"sender-cpu": "cpu_sender",
"receiver-cpu": "cpu_receiver",
}
def compute(derived: Path) -> tuple[dict[str, str], list[Path]]:
out: dict[str, str] = {}
sources: list[Path] = []
for exp_dir in sorted(p for p in derived.iterdir() if p.is_dir()):
runs = exp_dir / "runs.csv"
if not runs.exists():
continue
sources.append(runs)
df = pd.read_csv(runs)
for sol, sub in df.groupby("solution", sort=True):
for metric, col in METRICS.items():
x = sub[col].to_numpy()
base = f"{exp_dir.name}/{metric}/{sol}"
out[f"{base}/mean-pct"] = f"{x.mean():.2f}"
out[f"{base}/sd-pct"] = f"{x.std(ddof=1):.2f}"
return out, sources
+60
View File
@@ -0,0 +1,60 @@
"""Per-flow IDT per (experiment, solution): percentiles, mean, SD in microseconds.
"""
from pathlib import Path
import numpy as np
import pandas as pd
def _stats(x: np.ndarray) -> dict[str, str]:
q05, q25, q50, q75, q95, q99 = np.percentile(x, [5, 25, 50, 75, 95, 99])
return {
"mean-us": f"{x.mean():.2f}",
"sd-us": f"{x.std(ddof=1):.2f}",
"median-us": f"{q50:.2f}",
"p05-us": f"{q05:.2f}",
"p25-us": f"{q25:.2f}",
"p75-us": f"{q75:.2f}",
"p95-us": f"{q95:.2f}",
"p99-us": f"{q99:.2f}",
"iqr-us": f"{q75 - q25:.2f}",
"n-samples": str(x.size),
}
def compute(derived: Path) -> tuple[dict[str, str], list[Path]]:
out: dict[str, str] = {}
sources: list[Path] = []
for exp_dir in sorted(p for p in derived.iterdir() if p.is_dir()):
idts_path = exp_dir / "idts.csv"
if not idts_path.exists():
continue
sources.append(idts_path)
df = pd.read_csv(idts_path, usecols=["solution", "idt_us"])
per_sol: dict[str, dict[str, str]] = {}
for sol, sub in df.groupby("solution", sort=True):
x = sub["idt_us"].to_numpy()
stats = _stats(x)
per_sol[sol] = stats
for k, v in stats.items():
out[f"{exp_dir.name}/idt/{sol}/{k}"] = v
# IQR is meaningful only when the baseline solution actually spreads
# the bulk; pre-pacing IDT is bimodal (back-to-back packets, heavy
# tail above p95) so its IQR is often zero. Guard the ratio.
if "tso-pacing" in per_sol:
pac_sd = float(per_sol["tso-pacing"]["sd-us"])
pac_iqr = float(per_sol["tso-pacing"]["iqr-us"])
for other in ("no-tso", "tso", "cake"):
if other not in per_sol:
continue
o_sd = float(per_sol[other]["sd-us"])
o_iqr = float(per_sol[other]["iqr-us"])
base = f"{exp_dir.name}/idt/tso-pacing-vs-{other}"
if o_sd > 0:
out[f"{base}/sd-ratio-pct"] = f"{100 * pac_sd / o_sd:.1f}"
out[f"{base}/sd-reduction-pct"] = f"{100 * (1 - pac_sd / o_sd):.1f}"
if o_iqr > 0:
out[f"{base}/iqr-ratio-pct"] = f"{100 * pac_iqr / o_iqr:.1f}"
out[f"{base}/iqr-reduction-pct"] = f"{100 * (1 - pac_iqr / o_iqr):.1f}"
return out, sources
+15
View File
@@ -0,0 +1,15 @@
"""NFP-4000 memory constants (capacities + access latencies).
Source: Netronome Network Flow Processor 4xxx Family datasheet
(netronomeNetronomeNetworkFlow2018).
"""
from pathlib import Path
def compute(derived: Path) -> tuple[dict[str, str], list[Path]]:
return {
"nfp4000/lmem/capacity-kb": "4",
"nfp4000/lmem/latency-cycles": "1-3",
"nfp4000/ctm/capacity-kb": "256",
"nfp4000/ctm/latency-cycles": "50-100",
}, []
+60
View File
@@ -0,0 +1,60 @@
"""RTT per (experiment, solution): mean, SD, quantiles, spread.
Also emits tso-pacing's variance-reduction ratios vs the other solutions.
"""
from pathlib import Path
import numpy as np
import pandas as pd
def _stats(x: np.ndarray) -> dict[str, str]:
q05, q25, q50, q75, q95 = np.percentile(x, [5, 25, 50, 75, 95])
return {
"mean-ms": f"{x.mean():.2f}",
"sd-ms": f"{x.std(ddof=1):.2f}",
"median-ms": f"{q50:.2f}",
"iqr-ms": f"{q75 - q25:.2f}",
"p05-ms": f"{q05:.2f}",
"p95-ms": f"{q95:.2f}",
"spread-5-95-ms": f"{q95 - q05:.2f}",
"n-samples": str(x.size),
}
def compute(derived: Path) -> tuple[dict[str, str], list[Path]]:
out: dict[str, str] = {}
sources: list[Path] = []
for exp_dir in sorted(p for p in derived.iterdir() if p.is_dir()):
rtts = exp_dir / "rtts.csv"
if not rtts.exists():
continue
sources.append(rtts)
df = pd.read_csv(rtts)
per_sol: dict[str, dict[str, str]] = {}
for sol, sub in df.groupby("solution", sort=True):
x = sub["rtt_us"].to_numpy() / 1000.0
stats = _stats(x)
per_sol[sol] = stats
for k, v in stats.items():
out[f"{exp_dir.name}/rtt/{sol}/{k}"] = v
if "tso-pacing" in per_sol:
pac_sd = float(per_sol["tso-pacing"]["sd-ms"])
pac_spread = float(per_sol["tso-pacing"]["spread-5-95-ms"])
pac_iqr = float(per_sol["tso-pacing"]["iqr-ms"])
for other in ("no-tso", "tso", "cake"):
if other not in per_sol:
continue
o = per_sol[other]
o_sd = float(o["sd-ms"])
o_spread = float(o["spread-5-95-ms"])
o_iqr = float(o["iqr-ms"])
base = f"{exp_dir.name}/rtt/tso-pacing-vs-{other}"
out[f"{base}/sd-ratio-pct"] = f"{100 * pac_sd / o_sd:.1f}"
out[f"{base}/spread-ratio-pct"] = f"{100 * pac_spread / o_spread:.1f}"
out[f"{base}/iqr-ratio-pct"] = f"{100 * pac_iqr / o_iqr:.1f}"
out[f"{base}/sd-reduction-pct"] = f"{100 * (1 - pac_sd / o_sd):.1f}"
out[f"{base}/spread-reduction-pct"] = f"{100 * (1 - pac_spread / o_spread):.1f}"
out[f"{base}/iqr-reduction-pct"] = f"{100 * (1 - pac_iqr / o_iqr):.1f}"
return out, sources
+109
View File
@@ -0,0 +1,109 @@
"""Pairwise hypothesis tests across solutions.
Means: Welch's t (two-sided + one-sided) and Mann-Whitney U.
Variance: Brown-Forsythe (Levene, median-centered) and Fligner-Killeen.
"""
from pathlib import Path
import numpy as np
import pandas as pd
from scipy import stats
PAIRS = [
("no-tso", "tso"),
("no-tso", "tso-pacing"),
("no-tso", "cake"),
("tso", "tso-pacing"),
("tso", "cake"),
("tso-pacing", "cake"),
]
ALPHA = 0.05
def _fmt(x: float) -> str:
return f"{x:.4g}"
def _mean_tests(out: dict, base: str, a: np.ndarray, b: np.ndarray) -> None:
_, p_t = stats.ttest_ind(a, b, equal_var=False)
_, p_tl = stats.ttest_ind(a, b, equal_var=False, alternative="less")
_, p_tg = stats.ttest_ind(a, b, equal_var=False, alternative="greater")
_, p_u = stats.mannwhitneyu(a, b, alternative="two-sided")
out[f"{base}/welch-p"] = _fmt(p_t)
out[f"{base}/welch-less-p"] = _fmt(p_tl)
out[f"{base}/welch-greater-p"] = _fmt(p_tg)
out[f"{base}/mwu-p"] = _fmt(p_u)
def _variance_tests(out: dict, base: str, a: np.ndarray, b: np.ndarray) -> None:
bf = stats.levene(a, b, center="median")
fl = stats.fligner(a, b)
out[f"{base}/bf-p"] = _fmt(bf.pvalue)
out[f"{base}/fligner-p"] = _fmt(fl.pvalue)
def compute(derived: Path) -> tuple[dict[str, str], list[Path]]:
out: dict[str, str] = {}
sources: list[Path] = []
for exp_dir in sorted(p for p in derived.iterdir() if p.is_dir()):
runs_path = exp_dir / "runs.csv"
rtts_path = exp_dir / "rtts.csv"
if runs_path.exists():
sources.append(runs_path)
runs = pd.read_csv(runs_path)
for metric, col in (("sender-cpu", "cpu_sender"),
("receiver-cpu", "cpu_receiver")):
vals = {sol: sub[col].to_numpy() for sol, sub in runs.groupby("solution")}
for a, b in PAIRS:
if a not in vals or b not in vals:
continue
base = f"{exp_dir.name}/{metric}/test/{a}-vs-{b}"
_mean_tests(out, base, vals[a], vals[b])
if rtts_path.exists():
sources.append(rtts_path)
rtts = pd.read_csv(rtts_path)
vals = {sol: sub["rtt_us"].to_numpy() / 1000.0
for sol, sub in rtts.groupby("solution")}
for a, b in PAIRS:
if a not in vals or b not in vals:
continue
base = f"{exp_dir.name}/rtt/test/{a}-vs-{b}"
_mean_tests(out, base, vals[a], vals[b])
_variance_tests(out, base, vals[a], vals[b])
idts_path = exp_dir / "idts.csv"
if idts_path.exists():
sources.append(idts_path)
idts = pd.read_csv(idts_path, usecols=["solution", "idt_us"])
vals = {sol: sub["idt_us"].to_numpy()
for sol, sub in idts.groupby("solution")}
for a, b in PAIRS:
if a not in vals or b not in vals:
continue
base = f"{exp_dir.name}/idt/test/{a}-vs-{b}"
_mean_tests(out, base, vals[a], vals[b])
_variance_tests(out, base, vals[a], vals[b])
# Bonferroni-corrected thresholds. Count one entry per comparison
# (welch-p for mean families, bf-p for variance) so the threshold reflects
# the number of pairwise comparisons, not the number of test statistics.
def n_with(in_pattern: str, suffix: str) -> int:
return sum(1 for k in out if in_pattern in k and k.endswith(suffix))
families = {
"cpu": n_with("-cpu/test/", "/mwu-p"),
"rtt-mean": n_with("/rtt/test/", "/mwu-p"),
"rtt-variance": n_with("/rtt/test/", "/bf-p"),
"idt-mean": n_with("/idt/test/", "/mwu-p"),
"idt-variance": n_with("/idt/test/", "/bf-p"),
}
for name, n in families.items():
if n > 0:
out[f"bonferroni/{name}-n"] = str(n)
out[f"bonferroni/{name}-alpha"] = _fmt(ALPHA / n)
out["bonferroni/alpha-uncorrected"] = _fmt(ALPHA)
return out, sources
+22
View File
@@ -0,0 +1,22 @@
"""Throughput per (experiment, solution): mean and sample SD in Gbps."""
from pathlib import Path
import pandas as pd
def compute(derived: Path) -> tuple[dict[str, str], list[Path]]:
out: dict[str, str] = {}
sources: list[Path] = []
for exp_dir in sorted(p for p in derived.iterdir() if p.is_dir()):
runs = exp_dir / "runs.csv"
if not runs.exists():
continue
sources.append(runs)
df = pd.read_csv(runs)
for sol, sub in df.groupby("solution", sort=True):
thr = sub["throughput_bps"].to_numpy() / 1e9
base = f"{exp_dir.name}/throughput/{sol}"
out[f"{base}/mean-gbps"] = f"{thr.mean():.3f}"
out[f"{base}/sd-gbps"] = f"{thr.std(ddof=1):.3f}"
out[f"{base}/n-runs"] = str(len(thr))
return out, sources
+21
View File
@@ -0,0 +1,21 @@
# docker compose up -> runs `make all`, writes eval/out/.
# RAW_DATA_ROOT=/path docker compose up -> point at a different raw_data tree.
#
# RAW_DATA_ROOT outside the container = host path to mount (read by compose).
# Inside the container it always resolves to /raw_data via the `environment:`
# clause below; the host value is only consumed by the volume substitution.
services:
eval:
build: .
image: eval
user: "${UID:-1000}:${GID:-1000}"
volumes:
- .:/work/eval
- ${RAW_DATA_ROOT:-../raw_data}:/raw_data:ro
environment:
RAW_DATA_ROOT: /raw_data
HOME: /tmp
UV_CACHE_DIR: /tmp/uv-cache
working_dir: /work/eval
command: make all
+1
View File
@@ -0,0 +1 @@
source("renv/activate.R")
+11
View File
@@ -0,0 +1,11 @@
out/
renv/library/
renv/local/
renv/cellar/
renv/lock/
renv/sandbox/
renv/staging/
.Rproj.user/
.Rhistory
.RData
.Ruserdata
+170
View File
@@ -0,0 +1,170 @@
-include ../local.mk
EXP ?= datacenter-fq
SETUP ?= datacenter_fq
SOLUTIONS ?= no-tso tso tso-pacing
FIG_STEMS ?= throughput throughput_zoom rtt rtt_zoom sender_cpu receiver_cpu timeseries idt_cdf idt_cdf_zoom
# Global figure size and font size (inches, pt). Inherited by every Rscript
# via the export below; common.R reads these as Sys.getenv("FIG_*").
# Override on the command line: make FIG_WIDTH=2.5 FIG_HEIGHT=1.8 FIG_FONTSIZE=8 datacenter-fq
FIG_WIDTH ?= 2.5
FIG_HEIGHT ?= 2.0
FIG_FONTSIZE ?= 9
export FIG_WIDTH FIG_HEIGHT FIG_FONTSIZE
THROUGHPUT_SIZE_ARGS ?=
RTT_SIZE_ARGS ?=
THROUGHPUT_ARGS ?=
RTT_ARGS ?=
THROUGHPUT_ZOOM_ARGS ?= --ymin 3.5 --ymax 4.0 --ystep 0.1
RTT_ZOOM_ARGS ?= --ymin 4 --ymax 14 --ystep 2
SENDER_CPU_ARGS ?=
RECEIVER_CPU_ARGS ?=
TIMESERIES_ARGS ?=
IDT_CDF_ARGS ?=
ROOT := ..
RAW_DATA_ROOT ?= $(ROOT)/../raw_data
RAW := $(RAW_DATA_ROOT)/$(EXP)/aggregates
DERIVED := $(ROOT)/derived/$(EXP)
DERIVED_ROOT := $(ROOT)/derived
OUT := ../out/$(EXP)
VALUES_TEX := ../out/values.tex
VALUES_PLUGINS := $(wildcard $(ROOT)/analysis/values/*.py)
SKIP_PLUGINS ?=
CSVS := $(addprefix $(DERIVED)/, runs.csv rtts.csv idts.csv firstflow_bins.csv)
PDFS := $(addprefix $(OUT)/, $(addsuffix .pdf, $(FIG_STEMS)))
TEXS := $(addprefix $(OUT)/, $(addsuffix .tex, $(FIG_STEMS)))
.PHONY: all derive figures tikz sanity values pdfs tex clean datacenter-fq datacenter-fq-tikz direct-link direct-link-tikz all-experiments all-experiments-tikz
.DELETE_ON_ERROR:
all: all-experiments
derive: $(DERIVED)/.stamp
$(DERIVED)/.stamp: $(ROOT)/analysis/aggregate.py
cd $(ROOT)/analysis && uv run python aggregate.py --raw $(abspath $(RAW)) --setup-prefix $(SETUP) --solutions $(SOLUTIONS) --out $(abspath $(DERIVED))
touch $@
$(CSVS): $(DERIVED)/.stamp
sanity: $(DERIVED)/.stamp
cd $(ROOT)/analysis && uv run python sanity_check.py --derived $(abspath $(DERIVED)) --solutions $(SOLUTIONS)
figures: all-experiments
tikz: all-experiments-tikz
pdfs: $(PDFS)
tex: $(TEXS)
FMT_FLAG = $(if $(filter %.tex,$@),--tikz,--pdf)
$(OUT)/throughput.pdf $(OUT)/throughput.tex: fig_throughput.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) $(FMT_FLAG) $@ $(THROUGHPUT_SIZE_ARGS) $(THROUGHPUT_ARGS)
$(OUT)/throughput_zoom.pdf $(OUT)/throughput_zoom.tex: fig_throughput.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) $(FMT_FLAG) $@ --zoom $(THROUGHPUT_SIZE_ARGS) $(THROUGHPUT_ZOOM_ARGS)
$(OUT)/rtt.pdf $(OUT)/rtt.tex: fig_rtt.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) $(FMT_FLAG) $@ $(RTT_SIZE_ARGS) $(RTT_ARGS)
$(OUT)/rtt_zoom.pdf $(OUT)/rtt_zoom.tex: fig_rtt.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) $(FMT_FLAG) $@ --zoom $(RTT_SIZE_ARGS) $(RTT_ZOOM_ARGS)
$(OUT)/sender_cpu.pdf $(OUT)/sender_cpu.tex: fig_sender_cpu.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) $(FMT_FLAG) $@ $(SENDER_CPU_ARGS)
$(OUT)/receiver_cpu.pdf $(OUT)/receiver_cpu.tex: fig_receiver_cpu.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) $(FMT_FLAG) $@ $(RECEIVER_CPU_ARGS)
$(OUT)/timeseries.pdf $(OUT)/timeseries.tex: fig_timeseries.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) $(FMT_FLAG) $@ $(TIMESERIES_ARGS)
$(OUT)/idt_cdf.pdf $(OUT)/idt_cdf.tex: fig_idt_cdf.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) $(FMT_FLAG) $@ $(IDT_CDF_ARGS)
$(OUT)/idt_cdf_zoom.pdf $(OUT)/idt_cdf_zoom.tex: fig_idt_cdf.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) $(FMT_FLAG) $@ --zoom $(IDT_CDF_ARGS)
$(OUT)/%.pdf: fig_%.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) --pdf $@
$(OUT)/%_zoom.pdf: fig_%.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) --pdf $@ --zoom
$(OUT)/%.tex: fig_%.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) --tikz $@
$(OUT)/%_zoom.tex: fig_%.R common.R $(CSVS) Makefile
@mkdir -p $(OUT)
Rscript $< --data $(DERIVED) --tikz $@ --zoom
values: $(VALUES_TEX)
# SKIP_PLUGINS is a runtime decision, not a file dependency: force rebuild
# whenever the user sets it on the command line.
ifneq ($(SKIP_PLUGINS),)
.PHONY: $(VALUES_TEX)
endif
$(VALUES_TEX): $(ROOT)/analysis/gen_values.py $(VALUES_PLUGINS) $(DERIVED_ROOT)/datacenter-fq/.stamp $(DERIVED_ROOT)/direct-link/.stamp Makefile
@mkdir -p $(dir $(VALUES_TEX))
cd $(ROOT)/analysis && uv run python gen_values.py --derived $(abspath $(DERIVED_ROOT)) --out $(abspath $(VALUES_TEX)) $(if $(SKIP_PLUGINS),--skip $(SKIP_PLUGINS))
clean:
rm -rf $(OUT) $(DERIVED)
# Per-experiment shortcut targets. Each recursively invokes the makefile
# with the right EXP / SETUP / FIG_STEMS / zoom overrides.
# Per-experiment knobs. Each shortcut target reuses these via $(call ...).
QUARTER_SIZE := --width 1.4 --height 1.5
DATACENTER_FQ_ARGS = EXP=datacenter-fq SETUP=datacenter_fq \
SOLUTIONS="no-tso tso tso-pacing cake:datacenter_cake_no-tso" \
FIG_STEMS="throughput throughput_zoom rtt rtt_zoom sender_cpu receiver_cpu timeseries idt_cdf idt_cdf_zoom" \
THROUGHPUT_SIZE_ARGS="$(QUARTER_SIZE)" \
RTT_SIZE_ARGS="$(QUARTER_SIZE)" \
THROUGHPUT_ZOOM_ARGS="--ymin 3.5 --ymax 4.0 --ystep 0.1" \
RTT_ZOOM_ARGS="--ymin 4 --ymax 14 --ystep 2"
DIRECT_LINK_ARGS = EXP=direct-link SETUP=direct-link_fq \
SOLUTIONS="no-tso tso tso-pacing cake:direct-link_cake_no-tso" \
FIG_STEMS="throughput throughput_zoom rtt rtt_zoom sender_cpu receiver_cpu" \
THROUGHPUT_SIZE_ARGS="$(QUARTER_SIZE)" \
RTT_SIZE_ARGS="$(QUARTER_SIZE)" \
THROUGHPUT_ARGS="--ymax 10 --ystep 2" \
THROUGHPUT_ZOOM_ARGS="--ymin 9.0 --ymax 10.0 --ystep 0.2" \
RTT_ZOOM_ARGS="--ymin 1.75 --ymax 4 --ystep 1" \
SENDER_CPU_ARGS="--ystep 1" \
RECEIVER_CPU_ARGS="--ystep 5"
datacenter-fq:
$(MAKE) pdfs $(DATACENTER_FQ_ARGS)
datacenter-fq-tikz:
$(MAKE) tex $(DATACENTER_FQ_ARGS)
direct-link:
$(MAKE) pdfs $(DIRECT_LINK_ARGS)
direct-link-tikz:
$(MAKE) tex $(DIRECT_LINK_ARGS)
all-experiments: datacenter-fq direct-link
all-experiments-tikz: datacenter-fq-tikz direct-link-tikz
+128
View File
@@ -0,0 +1,128 @@
suppressPackageStartupMessages({
library(argparse)
library(ggplot2)
library(dplyr)
library(tidyr)
library(readr)
library(scales)
library(tikzDevice)
})
SOLUTION_ORDER <- c("tso", "tso-pacing", "cake")
SOLUTION_LABELS <- c(
"no-tso" = "TSO Off",
"tso" = "TSO On",
"tso-pacing" = "TSO Pacing",
"cake" = "Cake"
)
SOLUTION_COLORS <- c(
"no-tso" = "#009E73",
"tso" = "#E69F00",
"tso-pacing" = "#0072B2",
"cake" = "#CC79A7"
)
SOLUTION_LINETYPES <- c(
"no-tso" = "longdash",
"tso" = "dashed",
"tso-pacing" = "solid",
"cake" = "twodash"
)
# Global figure size and font size, overridable via env vars
# (Makefile exports FIG_WIDTH / FIG_HEIGHT / FIG_FONTSIZE).
.env_num <- function(var, default) {
v <- Sys.getenv(var)
if (nchar(v) == 0) default else as.numeric(v)
}
DEFAULT_WIDTH <- .env_num("FIG_WIDTH", 3.33)
DEFAULT_HEIGHT <- .env_num("FIG_HEIGHT", 2.4)
DEFAULT_FONTSIZE <- .env_num("FIG_FONTSIZE", 10)
theme_paper <- function(base_size = DEFAULT_FONTSIZE) {
theme_bw(base_size = base_size) +
theme(
legend.position = "bottom",
legend.title = element_blank(),
legend.margin = margin(t = -4, l = -20),
legend.text = element_text(size = base_size - 1),
legend.key.size = unit(base_size, "pt"),
axis.text = element_text(size = base_size - 1),
strip.background = element_rect(fill = "grey90"),
panel.grid.minor = element_blank(),
plot.margin = margin(2, 4, 2, 2)
)
}
prepare_solution <- function(df) {
df %>%
filter(solution %in% SOLUTION_ORDER) %>%
mutate(solution = factor(solution, levels = SOLUTION_ORDER,
labels = SOLUTION_LABELS[SOLUTION_ORDER]))
}
LABEL_COLORS <- setNames(SOLUTION_COLORS[SOLUTION_ORDER], SOLUTION_LABELS[SOLUTION_ORDER])
LABEL_LINETYPES <- setNames(SOLUTION_LINETYPES[SOLUTION_ORDER], SOLUTION_LABELS[SOLUTION_ORDER])
COL_WIDTH <- 3.33
TEXT_WIDTH <- 7.0
# ACM page-fraction presets, in inches.
HALF_WIDTH <- 3.3 # two figures side-by-side under one \begin{figure}
THIRD_WIDTH <- 2.2 # three figures side-by-side
label_pct <- function() percent_format(accuracy = 1)
label_us <- function() "µs"
pct_sign <- function() "%"
fig_parser <- function(default_width = DEFAULT_WIDTH, default_height = DEFAULT_HEIGHT,
description = "Generate figure") {
p <- ArgumentParser(description = description)
p$add_argument("--data", required = TRUE, help = "derived CSV directory")
p$add_argument("--pdf", help = "write PDF to this path")
p$add_argument("--tikz", help = "write tikz .tex to this path")
p$add_argument("--width", type = "double", default = default_width)
p$add_argument("--height", type = "double", default = default_height)
p$add_argument("--zoom", action = "store_true", help = "use zoomed axis limits")
p$add_argument("--debug-margins", action = "store_true",
help = "color plot.background / panel.background / panel.border to visualise layout")
p
}
# Optional theme overlay that colors the plot canvas, plot panel, and panel
# border so layout / whitespace can be inspected. Returns NULL when off
# (adding NULL to a ggplot is a no-op).
theme_debug_margins <- function(args) {
if (!isTRUE(args$debug_margins)) return(NULL)
theme(
plot.background = element_rect(fill = "#FFF7B3", colour = "black"),
panel.background = element_rect(fill = "#CCE5FF", colour = NA),
panel.border = element_rect(colour = "red", fill = NA, linewidth = 0.6)
)
}
save_figure <- function(plot, args) {
if (is.null(args$pdf) && is.null(args$tikz)) {
stop("save_figure: pass --pdf and/or --tikz")
}
if (!is.null(args$pdf)) {
dir.create(dirname(args$pdf), showWarnings = FALSE, recursive = TRUE)
ggsave(args$pdf, plot, width = args$width, height = args$height, device = "pdf")
message("wrote ", args$pdf)
}
if (!is.null(args$tikz)) {
dir.create(dirname(args$tikz), showWarnings = FALSE, recursive = TRUE)
old <- options(
tikzSanitizeCharacters = c(getOption("tikzSanitizeCharacters"), "µ"),
tikzReplacementCharacters = c(getOption("tikzReplacementCharacters"), "$\\mu$")
)
on.exit(options(old), add = TRUE)
tikz(args$tikz, width = args$width, height = args$height,
standAlone = FALSE, sanitize = TRUE)
print(plot)
dev.off()
message("wrote ", args$tikz)
}
}
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env Rscript
source("common.R")
# Random sample size per solution. stat_ecdf emits one tikz path segment per
# unique x value; the raw idts.csv has ~120 K unique values per solution,
# producing a ~500 K-line tikz file that exceeds pdflatex main_memory.
# 5000 samples per solution preserve the CDF shape (including the 92.5-100 %
# zoom tail: ~375 points) and keep the tikz file manageable.
SAMPLE_PER_SOLUTION <- 20000
parser <- fig_parser(description = "Per-flow inter-departure-time CDF")
parser$add_argument("--sample", type = "integer", default = SAMPLE_PER_SOLUTION,
help = "downsample to N points per solution (0 = no downsampling)")
parser$add_argument("--seed", type = "integer", default = 1,
help = "RNG seed for downsampling")
args <- parser$parse_args()
set.seed(args$seed)
idts <- read_csv(file.path(args$data, "idts.csv"), show_col_types = FALSE) %>%
prepare_solution()
if (args$sample > 0) {
idts <- idts %>%
group_by(solution) %>%
slice_sample(n = args$sample) %>%
ungroup()
}
p <- ggplot(idts, aes(x = idt_us, colour = solution, linetype = solution)) +
stat_ecdf(linewidth = 0.9, pad = FALSE, key_glyph = "rect") +
scale_x_log10(labels = label_comma()) +
scale_y_continuous(labels = label_pct()) +
scale_colour_manual(values = LABEL_COLORS) +
scale_linetype_manual(values = LABEL_LINETYPES) +
labs(x = paste0("Inter-departure time within flow (", label_us(), ")"), y = "CDF") +
theme_paper() +
theme(legend.text = element_text(size = rel(0.8)),
legend.key.size = unit(6, "pt"))
if (args$zoom) {
p <- p + coord_cartesian(xlim = c(10, 1000), ylim = c(0.925, 1.0))
}
save_figure(p, args)
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env Rscript
source("common.R")
parser <- fig_parser(description = "Receiver CPU bar chart")
parser$add_argument("--ystep", type = "double",
help = "y-axis major tick spacing (omit = auto)")
args <- parser$parse_args()
runs <- read_csv(file.path(args$data, "runs.csv"), show_col_types = FALSE) %>%
prepare_solution() %>%
group_by(solution) %>%
summarise(mean_pct = mean(cpu_receiver),
sd_pct = sd(cpu_receiver), .groups = "drop")
p <- ggplot(runs, aes(x = solution, y = mean_pct, fill = solution)) +
geom_col(width = 0.6, colour = "black") +
geom_errorbar(aes(ymin = mean_pct - sd_pct, ymax = mean_pct + sd_pct),
width = 0.2) +
scale_fill_manual(values = LABEL_COLORS, guide = "none") +
labs(x = NULL, y = paste0("Average CPU usage (", pct_sign(), ")")) +
theme_paper()
if (!is.null(args$ystep)) {
p <- p + scale_y_continuous(breaks = scales::breaks_width(args$ystep))
}
save_figure(p, args)
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env Rscript
source("common.R")
parser <- fig_parser(description = "RTT box plot")
parser$add_argument("--ymin", type = "double", default = 4)
parser$add_argument("--ymax", type = "double", default = 14)
parser$add_argument("--ystep", type = "double", default = 2)
args <- parser$parse_args()
stats <- read_csv(file.path(args$data, "rtts.csv"), show_col_types = FALSE) %>%
mutate(rtt_ms = rtt_us / 1000) %>%
prepare_solution() %>%
group_by(solution) %>%
summarise(
ymin = quantile(rtt_ms, 0.05),
lower = quantile(rtt_ms, 0.25),
middle = median(rtt_ms),
upper = quantile(rtt_ms, 0.75),
ymax = quantile(rtt_ms, 0.95),
.groups = "drop"
)
p <- ggplot(stats, aes(x = solution, fill = solution)) +
geom_boxplot(aes(ymin = ymin, lower = lower, middle = middle,
upper = upper, ymax = ymax),
stat = "identity", width = 0.6, colour = "black") +
scale_fill_manual(values = LABEL_COLORS, guide = "none") +
labs(x = NULL, y = "RTT (ms)") +
theme_paper() +
theme(axis.text.x = element_text(angle = 45, hjust = 1),
plot.margin = margin(2, 4, 0, 2)) +
theme_debug_margins(args)
if (args$zoom) {
p <- p + coord_cartesian(ylim = c(args$ymin, args$ymax)) +
scale_y_continuous(breaks = seq(args$ymin, args$ymax, args$ystep))
} else {
p <- p + coord_cartesian(ylim = c(0, NA))
}
save_figure(p, args)
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env Rscript
source("common.R")
parser <- fig_parser(description = "Sender CPU bar chart")
parser$add_argument("--ystep", type = "double",
help = "y-axis major tick spacing (omit = auto)")
args <- parser$parse_args()
runs <- read_csv(file.path(args$data, "runs.csv"), show_col_types = FALSE) %>%
prepare_solution() %>%
group_by(solution) %>%
summarise(mean_pct = mean(cpu_sender),
sd_pct = sd(cpu_sender), .groups = "drop")
p <- ggplot(runs, aes(x = solution, y = mean_pct, fill = solution)) +
geom_col(width = 0.6, colour = "black") +
geom_errorbar(aes(ymin = mean_pct - sd_pct, ymax = mean_pct + sd_pct),
width = 0.2) +
scale_fill_manual(values = LABEL_COLORS, guide = "none") +
labs(x = NULL, y = paste0("Average CPU usage (", pct_sign(), ")")) +
theme_paper()
if (!is.null(args$ystep)) {
p <- p + scale_y_continuous(breaks = scales::breaks_width(args$ystep))
}
save_figure(p, args)
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env Rscript
source("common.R")
parser <- fig_parser(description = "Throughput bar chart")
parser$add_argument("--ymin", type = "double")
parser$add_argument("--ymax", type = "double")
parser$add_argument("--ystep", type = "double")
args <- parser$parse_args()
runs <- read_csv(file.path(args$data, "runs.csv"), show_col_types = FALSE) %>%
prepare_solution() %>%
group_by(solution) %>%
summarise(mean_gbps = mean(throughput_bps) / 1e9,
sd_gbps = sd(throughput_bps) / 1e9, .groups = "drop")
p <- ggplot(runs, aes(x = solution, y = mean_gbps, fill = solution)) +
geom_col(width = 0.6, colour = "black") +
geom_errorbar(aes(ymin = mean_gbps - sd_gbps, ymax = mean_gbps + sd_gbps),
width = 0.2) +
scale_fill_manual(values = LABEL_COLORS, guide = "none") +
labs(x = NULL, y = "Throughput (Gbps)") +
theme_paper() +
theme(axis.text.x = element_text(angle = 45, hjust = 1),
plot.margin = margin(2, 4, 0, 2)) +
theme_debug_margins(args)
if (!is.null(args$ymax)) {
ymin <- if (is.null(args$ymin)) 0 else args$ymin
p <- p + coord_cartesian(ylim = c(ymin, args$ymax))
if (!is.null(args$ystep)) {
p <- p + scale_y_continuous(breaks = seq(ymin, args$ymax, args$ystep))
}
}
save_figure(p, args)
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env Rscript
source("common.R")
parser <- fig_parser(description = "First-flow packet timeseries")
parser$add_argument("--xmin", type = "double", default = 100.0)
parser$add_argument("--xmax", type = "double", default = 105.0)
args <- parser$parse_args()
bins <- read_csv(file.path(args$data, "firstflow_bins.csv"), show_col_types = FALSE) %>%
group_by(solution) %>%
mutate(t_ms = t_ms - min(t_ms)) %>%
ungroup() %>%
filter(t_ms >= args$xmin, t_ms <= args$xmax) %>%
prepare_solution()
pacing_label <- SOLUTION_LABELS[["tso-pacing"]]
back <- bins %>% filter(solution == pacing_label)
front <- bins %>% filter(solution != pacing_label)
p <- ggplot() +
geom_area(data = back, aes(x = t_ms, y = packets, fill = solution),
alpha = 0.22, show.legend = FALSE) +
geom_line(data = back, aes(x = t_ms, y = packets, colour = solution,
linetype = solution), linewidth = 0.9, key_glyph = "rect") +
geom_line(data = front, aes(x = t_ms, y = packets, colour = solution,
linetype = solution), linewidth = 0.9, key_glyph = "rect") +
scale_colour_manual(values = LABEL_COLORS, breaks = names(LABEL_COLORS)) +
scale_fill_manual(values = LABEL_COLORS, breaks = names(LABEL_COLORS)) +
scale_linetype_manual(values = LABEL_LINETYPES, breaks = names(LABEL_LINETYPES)) +
scale_x_continuous(breaks = seq(args$xmin, args$xmax, 1)) +
scale_y_continuous(breaks = seq(0, 45, 5), limits = c(0, 45)) +
labs(x = "Time elapsed (ms)", y = paste0("Packets per 50 ", label_us(), " bin")) +
theme_paper() +
theme(legend.text = element_text(size = rel(0.8)),
legend.key.size = unit(6, "pt"))
save_figure(p, args)
+2296
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
library/
local/
cellar/
lock/
python/
sandbox/
staging/
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"bioconductor.version": null,
"external.libraries": [],
"ignored.packages": [],
"lockfile.sanitize": true,
"package.dependency.fields": [
"Imports",
"Depends",
"LinkingTo"
],
"ppm.enabled": null,
"ppm.ignored.urls": [],
"r.version": null,
"snapshot.dev": false,
"snapshot.type": "implicit",
"use.cache": true,
"vcs.ignore.cellar": true,
"vcs.ignore.library": true,
"vcs.ignore.local": true,
"vcs.manage.ignores": true
}
+5
View File
@@ -0,0 +1,5 @@
# Copy to local.mk and edit. Use ?= so a shell env var still overrides.
# Read from both eval/Makefile and eval/figures/Makefile; absolute paths
# avoid resolution ambiguity between the two cwds.
RAW_DATA_ROOT ?= /absolute/path/to/raw_data