diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3209450..a095c26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,9 +24,9 @@ env: RUST_BACKTRACE: 1 ACTIONLINT_VERSION: "1.7.10" MARKDOWNLINT_VERSION: "0.47.0" - CSPELL_VERSION: "9.4.0" + CSPELL_VERSION: "9.6.2" SHFMT_VERSION: "3.12.0" - UV_VERSION: "0.9.21" + UV_VERSION: "0.9.28" jobs: build: @@ -58,7 +58,7 @@ jobs: - name: Install just if: matrix.os != 'windows-latest' - uses: taiki-e/install-action@29feb09ac22f4fde4175fe7b5c3548952234f69a # v2.67.17 + uses: taiki-e/install-action@29feb09ac22f4fde4175fe7b5c3548952234f69a # v2.67.17 with: tool: just diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index b8aeaab..49d094f 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -48,7 +48,7 @@ jobs: fi - name: Install just - uses: taiki-e/install-action@29feb09ac22f4fde4175fe7b5c3548952234f69a # v2.67.17 + uses: taiki-e/install-action@29feb09ac22f4fde4175fe7b5c3548952234f69a # v2.67.17 with: tool: just diff --git a/WARP.md b/AGENTS.md similarity index 94% rename from WARP.md rename to AGENTS.md index 3c97a7b..60752b1 100644 --- a/WARP.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ -# WARP.md +# AGENTS.md -This file provides guidance to WARP (warp.dev) when working with code in this repository. +This file provides guidance for automated agents (including Warp at warp.dev) when working with code in this repository. ## Priorities diff --git a/Cargo.lock b/Cargo.lock index dafc92c..1b4a5cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,11 +291,11 @@ dependencies = [ [[package]] name = "equator" -version = "0.4.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +checksum = "02da895aab06bbebefb6b2595f6d637b18c9ff629b4cd840965bb3164e4194b0" dependencies = [ - "equator-macro 0.4.2", + "equator-macro 0.6.0", ] [[package]] @@ -311,14 +311,9 @@ dependencies = [ [[package]] name = "equator-macro" -version = "0.4.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] +checksum = "2b14b339eb76d07f052cdbad76ca7c1310e56173a138095d3bf42a23c06ef5d8" [[package]] name = "errno" @@ -332,14 +327,13 @@ dependencies = [ [[package]] name = "faer" -version = "0.23.2" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb922206162d9405f9fc059052b3f997bdc92745da7bfd620645f5092df20d1" +checksum = "02d2ecfb80b6f8b0c569e36988a052e64b14d8def9d372390b014e8bf79f299a" dependencies = [ "bytemuck", "dyn-stack", - "equator 0.4.2", - "faer-macros", + "equator 0.6.0", "faer-traits", "gemm", "generativity", @@ -352,26 +346,14 @@ dependencies = [ "reborrow", ] -[[package]] -name = "faer-macros" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc4b8cd876795d3b19ddfd59b03faa303c0b8adb9af6e188e81fc647c485bb9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "faer-traits" -version = "0.23.2" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24b69235b5f54416286c485fb047f2f499fc935a4eee2caadf4757f3c94c7b62" +checksum = "b87d23ed7ab1f26c0cba0e5b9e061a796fbb7dc170fa8bee6970055a1308bb0f" dependencies = [ "bytemuck", "dyn-stack", - "faer-macros", "generativity", "libm", "num-complex", @@ -401,9 +383,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "gemm" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451" +checksum = "aa0673db364b12263d103b68337a68fbecc541d6f6b61ba72fe438654709eacb" dependencies = [ "dyn-stack", "gemm-c32", @@ -421,9 +403,9 @@ dependencies = [ [[package]] name = "gemm-c32" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847" +checksum = "086936dbdcb99e37aad81d320f98f670e53c1e55a98bee70573e83f95beb128c" dependencies = [ "dyn-stack", "gemm-common", @@ -436,9 +418,9 @@ dependencies = [ [[package]] name = "gemm-c64" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf" +checksum = "20c8aeeeec425959bda4d9827664029ba1501a90a0d1e6228e48bef741db3a3f" dependencies = [ "dyn-stack", "gemm-common", @@ -451,9 +433,9 @@ dependencies = [ [[package]] name = "gemm-common" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3" +checksum = "88027625910cc9b1085aaaa1c4bc46bb3a36aad323452b33c25b5e4e7c8e2a3e" dependencies = [ "bytemuck", "dyn-stack", @@ -471,9 +453,9 @@ dependencies = [ [[package]] name = "gemm-f16" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109" +checksum = "e3df7a55202e6cd6739d82ae3399c8e0c7e1402859b30e4cb780e61525d9486e" dependencies = [ "dyn-stack", "gemm-common", @@ -488,9 +470,9 @@ dependencies = [ [[package]] name = "gemm-f32" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864" +checksum = "02e0b8c9da1fbec6e3e3ab2ce6bc259ef18eb5f6f0d3e4edf54b75f9fd41a81c" dependencies = [ "dyn-stack", "gemm-common", @@ -503,9 +485,9 @@ dependencies = [ [[package]] name = "gemm-f64" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd" +checksum = "056131e8f2a521bfab322f804ccd652520c79700d81209e9d9275bbdecaadc6a" dependencies = [ "dyn-stack", "gemm-common", @@ -782,9 +764,9 @@ dependencies = [ [[package]] name = "nano-gemm" -version = "0.1.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb5ba2bea1c00e53de11f6ab5bd0761ba87dc0045d63b0c87ee471d2d3061376" +checksum = "9e04345dc84b498ff89fe0d38543d1f170da9e43a2c2bcee73a0f9069f72d081" dependencies = [ "equator 0.2.2", "nano-gemm-c32", @@ -798,9 +780,9 @@ dependencies = [ [[package]] name = "nano-gemm-c32" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40449e57a5713464c3a1208c4c3301c8d29ee1344711822cf022bc91373a91b" +checksum = "0775b1e2520e64deee8fc78b7732e3091fb7585017c0b0f9f4b451757bbbc562" dependencies = [ "nano-gemm-codegen", "nano-gemm-core", @@ -809,9 +791,9 @@ dependencies = [ [[package]] name = "nano-gemm-c64" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743a6e6211358fba85d1009616751e4107da86f4c95b24e684ce85f25c25b3bf" +checksum = "9af49a20d58816e6b5ee65f64142e50edb5eba152678d4bb7377fcbf63f8437a" dependencies = [ "nano-gemm-codegen", "nano-gemm-core", @@ -820,21 +802,21 @@ dependencies = [ [[package]] name = "nano-gemm-codegen" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963bf7c7110d55430169dc74c67096375491ed580cd2ef84842550ac72e781fa" +checksum = "6cc8d495c791627779477a2cf5df60049f5b165342610eb0d76bee5ff5c5d74c" [[package]] name = "nano-gemm-core" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe3fc4f83ae8861bad79dc3c016bd6b0220da5f9de302e07d3112d16efc24aa6" +checksum = "d998dfa644de87a0f8660e5ea511d7cb5c33b5a2d9847b7af57a2565105089f0" [[package]] name = "nano-gemm-f32" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3681b7ce35658f79da94b7f62c60a005e29c373c7111ed070e3bf64546a8bb" +checksum = "879d962e79bc8952e4ad21ca4845a21132540ed3f5e01184b2ff7f720e666523" dependencies = [ "nano-gemm-codegen", "nano-gemm-core", @@ -842,9 +824,9 @@ dependencies = [ [[package]] name = "nano-gemm-f64" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc1e619ed04d801809e1f63e61b669d380c4119e8b0cdd6ed184c6b111f046d8" +checksum = "b9a513473dce7dc00c7e7c318481ca4494034e76997218d8dad51bd9f007a815" dependencies = [ "nano-gemm-codegen", "nano-gemm-core", @@ -1023,23 +1005,32 @@ dependencies = [ [[package]] name = "pulp" -version = "0.21.5" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907" +checksum = "2e205bb30d5b916c55e584c22201771bcf2bad9aabd5d4127f38387140c38632" dependencies = [ "bytemuck", "cfg-if", "libm", "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", "reborrow", "version_check", ] +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e24eee682d89fb193496edf918a7f407d30175b2e785fe057e4392dfd182e0" + [[package]] name = "qd" -version = "0.7.7" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff8bb755b6008c3b41bf8a0866c8dd4e1245a2f011ceaa22a13ee55c538493e2" +checksum = "15f1304a5aecdcfe9ee72fbba90aa37b3aa067a69d14cb7f3d9deada0be7c07c" dependencies = [ "bytemuck", "libm", diff --git a/Cargo.toml b/Cargo.toml index 172db11..709ae8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "la-stack" version = "0.1.2" edition = "2024" -rust-version = "1.92" +rust-version = "1.93" license = "BSD-3-Clause" description = "Small, stack-allocated linear algebra for fixed dimensions" readme = "README.md" @@ -12,19 +12,24 @@ categories = [ "mathematics", "science" ] keywords = [ "linear-algebra", "geometry", "const-generics" ] [dependencies] -# Intentionally empty +# Intentionally empty (bench-only deps are optional features below) +criterion = { version = "0.8.1", features = [ "html_reports" ], optional = true } +faer = { version = "0.24.0", default-features = false, features = [ "std", "linalg" ], optional = true } +nalgebra = { version = "0.34.1", optional = true } [dev-dependencies] approx = "0.5.1" -criterion = { version = "0.8.1", features = [ "html_reports" ] } -faer = { version = "0.23.2", default-features = false, features = [ "std", "linalg" ] } -nalgebra = "0.34.1" pastey = "0.2.1" proptest = "1.9.0" +[features] +default = [ ] +bench = [ "criterion", "faer", "nalgebra" ] + [[bench]] name = "vs_linalg" harness = false +required-features = [ "bench" ] [lints.rust] unsafe_code = "forbid" diff --git a/README.md b/README.md index 399bf5b..f5dd76c 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ just fix # apply auto-fixes (mutating) just ci # lint + tests + examples + bench compile ``` -For the full set of developer commands, see `just --list` and `WARP.md`. +For the full set of developer commands, see `just --list` and `AGENTS.md`. ## 📝 Citation diff --git a/cspell.json b/cspell.json index 9ee589b..0958793 100644 --- a/cspell.json +++ b/cspell.json @@ -76,6 +76,7 @@ "sarif", "Schreiber", "semgrep", + "serde", "setuptools", "shellcheck", "SHFMT", diff --git a/docs/assets/bench/vs_linalg_lu_solve_median.csv b/docs/assets/bench/vs_linalg_lu_solve_median.csv index 9675e7d..8afa472 100644 --- a/docs/assets/bench/vs_linalg_lu_solve_median.csv +++ b/docs/assets/bench/vs_linalg_lu_solve_median.csv @@ -1,9 +1,5 @@ D,la_stack,la_lo,la_hi,nalgebra,na_lo,na_hi,faer,fa_lo,fa_hi -2,2.0426140654904827,2.0391714186102856,2.0478594146918607,18.27778331516731,18.25588547829167,18.353343071154377,159.2814967810837,158.47282250661482,159.94929876504062 -3,13.449391348041551,13.441149735016948,13.468703234496012,23.33714807833139,23.197918644389595,23.432750888142905,196.59112056791784,195.46791977222858,197.64819469379614 -4,27.80669858961578,27.79483103644443,27.82507517021301,54.198756567150525,54.1371885181747,54.25850823046414,226.2216645280771,225.69147147746128,226.77489608417022 -5,46.075071909216696,46.0009302878795,46.22542128093703,73.54836237475409,73.15376033523773,73.65083570024034,290.9144674933806,290.3368842481312,292.2770703780476 -8,138.18689626060086,137.6385886896043,138.71964020251693,177.4526788255892,176.72965942802895,177.76610014816572,379.88559054369557,378.0171460866489,381.43214918535807 -16,626.0776434176701,622.7027039893363,627.8191979162416,594.0545445505171,590.9848069960763,596.1921326044237,897.0439411793589,892.3923679060665,903.3721630306105 -32,2684.695543866568,2682.287244245524,2692.145801224854,2502.0311961897173,2498.0753625161688,2507.013007391502,2909.4663798139577,2905.3646097654746,2914.4703556771547 -64,16721.57637997433,16681.169824561402,16765.606725146197,14875.769757096798,14835.11001141987,14911.022279129322,12493.628154592803,12481.801167582416,12502.518368055556 +8,139.1757887813422,138.85553999302812,139.43884658648437,178.21202693227167,177.52836844843281,178.55635798393308,385.32108423907505,384.06743118586223,386.57307928983295 +16,621.9073704935589,609.1396166716988,629.7385504068142,593.0675282677521,591.0085279317698,593.8912579957356,914.6753125472296,910.9807147912244,918.442061653845 +32,2686.106579251768,2673.9338182855513,2693.15666392619,2494.9662128806867,2492.8615451388887,2498.0652466220645,2928.5380530973453,2923.4865662122297,2933.5632483081727 +64,16748.679299149127,16714.879960962913,16783.847576211894,14695.374476410936,14657.57544191919,14718.787518037518,12441.879753474954,12411.776004717805,12477.712264150943 diff --git a/justfile b/justfile index 96a088a..acfefb6 100644 --- a/justfile +++ b/justfile @@ -75,12 +75,12 @@ action-lint: _ensure-actionlint # Benchmarks bench: - cargo bench + cargo bench --features bench # Compile benchmarks without running them, treating warnings as errors. # This catches bench/release-profile-only warnings that won't show up in normal debug-profile runs. bench-compile: - RUSTFLAGS='-D warnings' cargo bench --no-run + RUSTFLAGS='-D warnings' cargo bench --no-run --features bench # Bench the la-stack vs nalgebra/faer comparison suite. bench-vs-linalg filter="": @@ -88,9 +88,9 @@ bench-vs-linalg filter="": set -euo pipefail filter="{{filter}}" if [ -n "$filter" ]; then - cargo bench --bench vs_linalg -- "$filter" + cargo bench --features bench --bench vs_linalg -- "$filter" else - cargo bench --bench vs_linalg + cargo bench --features bench --bench vs_linalg fi # Quick iteration (reduced runtime, no Criterion HTML). @@ -99,9 +99,9 @@ bench-vs-linalg-quick filter="": set -euo pipefail filter="{{filter}}" if [ -n "$filter" ]; then - cargo bench --bench vs_linalg -- "$filter" --quick --noplot + cargo bench --features bench --bench vs_linalg -- "$filter" --quick --noplot else - cargo bench --bench vs_linalg -- --quick --noplot + cargo bench --features bench --bench vs_linalg -- --quick --noplot fi # Build commands @@ -139,7 +139,7 @@ clippy: # Common tarpaulin arguments for all coverage runs # Note: -t 300 sets per-test timeout to 5 minutes (needed for slow CI environments) _coverage_base_args := '''--exclude-files 'benches/*' --exclude-files 'examples/*' \ - --workspace --lib --tests --all-features \ + --workspace --lib --tests \ -t 300 --verbose --implicit-test-threads''' # Coverage analysis for local development (HTML output) @@ -300,6 +300,7 @@ python-sync: _ensure-uv uv sync --group dev python-typecheck: python-sync + uv run ty check scripts/ uv run mypy scripts/criterion_dim_plot.py # Setup diff --git a/pyproject.toml b/pyproject.toml index 5ab9ed2..44d7b9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,4 +113,5 @@ dev = [ "mypy>=1.19.0", "pytest>=8.0.0", "ruff>=0.12.11", + "ty>=0.0.8", ] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 102ef90..542a499 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,6 @@ [toolchain] # Pin to MSRV as specified in Cargo.toml -channel = "1.92.0" +channel = "1.93.0" # Essential components for development components = [ diff --git a/scripts/criterion_dim_plot.py b/scripts/criterion_dim_plot.py index 2a63d42..e220d55 100644 --- a/scripts/criterion_dim_plot.py +++ b/scripts/criterion_dim_plot.py @@ -40,6 +40,9 @@ class PlotRequest: title: str stat: str dims: tuple[int, ...] + la_label: str + na_label: str + fa_label: str log_y: bool @@ -145,6 +148,92 @@ def _discover_dims(criterion_dir: Path) -> list[int]: return sorted(dims) +def _strip_toml_comment(line: str) -> str: + return line.split("#", 1)[0].strip() + + +def _read_cargo_package_version(cargo_toml: Path) -> str | None: + if not cargo_toml.exists(): + return None + + in_package = False + for raw_line in cargo_toml.read_text(encoding="utf-8").splitlines(): + line = _strip_toml_comment(raw_line) + if not line: + continue + if line.startswith("[") and line.endswith("]"): + in_package = line == "[package]" + continue + if in_package: + match = re.match(r'version\s*=\s*"([^"]+)"', line) + if match: + return match.group(1) + return None + + +def _read_cargo_dependency_versions(cargo_toml: Path, names: set[str]) -> dict[str, str]: + if not cargo_toml.exists(): + return {} + + versions: dict[str, str] = {} + section: str | None = None + + for raw_line in cargo_toml.read_text(encoding="utf-8").splitlines(): + line = _strip_toml_comment(raw_line) + if not line: + continue + section_match = re.match(r"^\[([^\]]+)\]$", line) + if section_match: + section = section_match.group(1) + continue + if section not in {"dependencies", "dev-dependencies", "build-dependencies"}: + continue + + dep_match = re.match(r"^([A-Za-z0-9_-]+)\s*=\s*(.+)$", line) + if not dep_match: + continue + + name = dep_match.group(1) + if name not in names: + continue + + value = dep_match.group(2).strip() + if value.startswith("{"): + version_match = re.search(r'version\s*=\s*"([^"]+)"', value) + if version_match: + versions[name] = version_match.group(1) + else: + version_match = re.match(r'^"([^"]+)"$', value) + if version_match: + versions[name] = version_match.group(1) + + return versions + + +def _detect_versions(root: Path) -> dict[str, str]: + cargo_toml = root / "Cargo.toml" + package_version = _read_cargo_package_version(cargo_toml) or "unknown" + dep_versions = _read_cargo_dependency_versions(cargo_toml, {"nalgebra", "faer"}) + + return { + "la-stack": package_version, + "nalgebra": dep_versions.get("nalgebra", "unknown"), + "faer": dep_versions.get("faer", "unknown"), + } + + +def _print_versions(versions: dict[str, str]) -> None: + order = ["la-stack", "nalgebra", "faer"] + text = ", ".join(f"{name}={versions.get(name, 'unknown')}" for name in order) + print(f"Detected crate versions for legend: {text}", file=sys.stderr) + + +def _format_legend_label(name: str, version: str) -> str: + if version == "unknown": + return name + return f"{name} v{version}" + + def _read_estimate(estimates_json: Path, stat: str) -> tuple[float, float, float]: data = json.loads(estimates_json.read_text(encoding="utf-8")) @@ -266,9 +355,9 @@ def _render_svg_with_gnuplot(req: PlotRequest) -> None: gp_lines.extend( [ "plot \\", - f" {_gp_quote(str(req.csv_path))} using 1:2:3:4 with yerrorlines ls 1 title 'la-stack', \\", - f" {_gp_quote(str(req.csv_path))} using 1:5:6:7 with yerrorlines ls 2 title 'nalgebra', \\", - f" {_gp_quote(str(req.csv_path))} using 1:8:9:10 with yerrorlines ls 3 title 'faer'", + f" {_gp_quote(str(req.csv_path))} using 1:2:3:4 with yerrorlines ls 1 title {_gp_quote(req.la_label)}, \\", + f" {_gp_quote(str(req.csv_path))} using 1:5:6:7 with yerrorlines ls 2 title {_gp_quote(req.na_label)}, \\", + f" {_gp_quote(str(req.csv_path))} using 1:8:9:10 with yerrorlines ls 3 title {_gp_quote(req.fa_label)}", ] ) @@ -437,6 +526,13 @@ def main(argv: list[str] | None = None) -> int: root = _repo_root() + versions = _detect_versions(root) + _print_versions(versions) + + la_label = _format_legend_label("la-stack", versions.get("la-stack", "unknown")) + na_label = _format_legend_label("nalgebra", versions.get("nalgebra", "unknown")) + fa_label = _format_legend_label("faer", versions.get("faer", "unknown")) + criterion_dir = _resolve_under_root(root, args.criterion_dir) dims = _discover_dims(criterion_dir) if criterion_dir.exists() else [] @@ -477,6 +573,9 @@ def main(argv: list[str] | None = None) -> int: title=title, stat=args.stat, dims=tuple(dims_present), + la_label=la_label, + na_label=na_label, + fa_label=fa_label, log_y=args.log_y, ) diff --git a/scripts/tests/test_criterion_dim_plot.py b/scripts/tests/test_criterion_dim_plot.py index f29c79b..5896f16 100644 --- a/scripts/tests/test_criterion_dim_plot.py +++ b/scripts/tests/test_criterion_dim_plot.py @@ -1,7 +1,9 @@ from __future__ import annotations +import argparse import json -from typing import TYPE_CHECKING +import tomllib +from typing import TYPE_CHECKING, cast import pytest @@ -11,6 +13,49 @@ from pathlib import Path +def _toml_dependency_version(data: dict[str, object], name: str) -> str | None: + for section in ("dependencies", "dev-dependencies", "build-dependencies"): + table = data.get(section) + if not isinstance(table, dict): + continue + table_dict = cast("dict[str, object]", table) + value = table_dict.get(name) + if value is None: + continue + if isinstance(value, str): + return value + if isinstance(value, dict): + value_dict = cast("dict[str, object]", value) + version = value_dict.get("version") + if isinstance(version, str): + return version + return None + + +def test_detect_versions_matches_cargo_toml() -> None: + root = criterion_dim_plot._repo_root() + cargo_toml = root / "Cargo.toml" + + data = tomllib.loads(cargo_toml.read_text(encoding="utf-8")) + + package_version: str | None = None + package = data.get("package") + if isinstance(package, dict): + version = package.get("version") + if isinstance(version, str): + package_version = version + + expected_la = package_version or "unknown" + expected_na = _toml_dependency_version(data, "nalgebra") or "unknown" + expected_fa = _toml_dependency_version(data, "faer") or "unknown" + + versions = criterion_dim_plot._detect_versions(root) + + assert versions["la-stack"] == expected_la + assert versions["nalgebra"] == expected_na + assert versions["faer"] == expected_fa + + def test_readme_table_markers_are_stable() -> None: begin, end = criterion_dim_plot._readme_table_markers("lu_solve", "median", "new") assert begin == "" @@ -84,17 +129,20 @@ def test_gp_quote_escapes_backslashes_and_quotes() -> None: def test_maybe_render_plot_handles_gnuplot_failure(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None: # Simulate gnuplot existing but failing to run (CalledProcessError). def boom(_req: object) -> None: - raise criterion_dim_plot.subprocess.CalledProcessError(1, ["gnuplot"]) # type: ignore[arg-type] + raise criterion_dim_plot.subprocess.CalledProcessError(1, ["gnuplot"]) monkeypatch.setattr(criterion_dim_plot, "_render_svg_with_gnuplot", boom) - args = type("Args", (), {"no_plot": False})() + args = argparse.Namespace(no_plot=False) req = criterion_dim_plot.PlotRequest( csv_path=criterion_dim_plot.Path("out.csv"), out_svg=criterion_dim_plot.Path("out.svg"), title="t", stat="median", dims=(2,), + la_label="la-stack v0.1.2", + na_label="nalgebra v0.34.1", + fa_label="faer v0.24.0", log_y=False, ) @@ -246,3 +294,204 @@ def write_estimates(path: Path, median: float) -> None: assert "placeholder" not in readme_text assert "| 2 | 10.000 | 20.000 | 40.000 | +50.0% | +75.0% |" in readme_text assert "| 8 | 100.000 | 50.000 | 200.000 | -100.0% | +50.0% |" in readme_text + + +def test_dim_parsing_and_discovery(tmp_path: Path) -> None: + assert criterion_dim_plot._dim_from_group_dir("d2") == 2 + assert criterion_dim_plot._dim_from_group_dir("d10") == 10 + assert criterion_dim_plot._dim_from_group_dir("dx") is None + assert criterion_dim_plot._dim_from_group_dir("2") is None + + (tmp_path / "d2").mkdir() + (tmp_path / "d10").mkdir() + (tmp_path / "not_a_dim").mkdir() + dims = criterion_dim_plot._discover_dims(tmp_path) + assert dims == [2, 10] + + +def test_toml_helpers_read_versions(tmp_path: Path) -> None: + cargo_toml = tmp_path / "Cargo.toml" + cargo_toml.write_text( + "\n".join( + [ + "# comment line", + "[package]", + 'version = "1.2.3" # inline comment', + "", + "[dependencies]", + 'nalgebra = "0.34.0"', + 'faer = { version = "0.21.4" }', + "", + "[dev-dependencies]", + 'serde = "1.0"', + ] + ), + encoding="utf-8", + ) + + assert criterion_dim_plot._strip_toml_comment('version = "1.0" # c') == 'version = "1.0"' + assert criterion_dim_plot._read_cargo_package_version(cargo_toml) == "1.2.3" + deps = criterion_dim_plot._read_cargo_dependency_versions(cargo_toml, {"nalgebra", "faer"}) + assert deps["nalgebra"] == "0.34.0" + assert deps["faer"] == "0.21.4" + + +def test_format_legend_label() -> None: + assert criterion_dim_plot._format_legend_label("la-stack", "0.1.0") == "la-stack v0.1.0" + assert criterion_dim_plot._format_legend_label("faer", "unknown") == "faer" + + +def test_read_estimate_errors_and_success(tmp_path: Path) -> None: + estimates = tmp_path / "estimates.json" + estimates.write_text( + json.dumps( + { + "median": { + "point_estimate": 5.0, + "confidence_interval": {"lower_bound": 4.0, "upper_bound": 6.0}, + } + } + ), + encoding="utf-8", + ) + point, lo, hi = criterion_dim_plot._read_estimate(estimates, "median") + assert (point, lo, hi) == (5.0, 4.0, 6.0) + + with pytest.raises(KeyError, match="stat 'mean' not found"): + criterion_dim_plot._read_estimate(estimates, "mean") + + +def test_write_csv_and_collect_rows(tmp_path: Path) -> None: + rows = [ + criterion_dim_plot.Row( + dim=2, + la_time=1.0, + la_lo=0.9, + la_hi=1.1, + na_time=2.0, + na_lo=1.9, + na_hi=2.1, + fa_time=3.0, + fa_lo=2.9, + fa_hi=3.1, + ) + ] + out_csv = tmp_path / "out.csv" + criterion_dim_plot._write_csv(out_csv, rows) + text = out_csv.read_text(encoding="utf-8") + assert text.startswith("D,la_stack,la_lo,la_hi,nalgebra,na_lo,na_hi,faer,fa_lo,fa_hi") + assert "2,1.0,0.9,1.1,2.0,1.9,2.1,3.0,2.9,3.1" in text + + criterion_dir = tmp_path / "criterion" + metric = criterion_dim_plot.METRICS["lu_solve"] + d2 = criterion_dir / "d2" + d2.mkdir(parents=True) + # Only la_stack exists; should be skipped. + (d2 / metric.la_bench / "new").mkdir(parents=True) + (d2 / metric.la_bench / "new" / "estimates.json").write_text( + json.dumps({"median": {"point_estimate": 1.0}}), + encoding="utf-8", + ) + rows2, skipped = criterion_dim_plot._collect_rows(criterion_dir, [2], metric, "median", "new") + assert rows2 == [] + assert skipped == ["d2 (missing la_stack_lu_solve, nalgebra_lu_solve, or faer_lu_solve)"] + + +def test_resolve_paths(tmp_path: Path) -> None: + root = tmp_path + resolved = criterion_dim_plot._resolve_under_root(root, "foo/bar.csv") + assert resolved == root / "foo/bar.csv" + + svg, csv = criterion_dim_plot._resolve_output_paths(root, "lu_solve", "median", None, None) + assert svg == root / "docs/assets/bench/vs_linalg_lu_solve_median.svg" + assert csv == root / "docs/assets/bench/vs_linalg_lu_solve_median.csv" + + +def test_maybe_render_plot_no_plot_path(capsys: pytest.CaptureFixture[str]) -> None: + args = argparse.Namespace(no_plot=True) + req = criterion_dim_plot.PlotRequest( + csv_path=criterion_dim_plot.Path("out.csv"), + out_svg=criterion_dim_plot.Path("out.svg"), + title="t", + stat="median", + dims=(2,), + la_label="la", + na_label="na", + fa_label="fa", + log_y=False, + ) + rc = criterion_dim_plot._maybe_render_plot(args, req, skipped=[]) + assert rc == 0 + captured = capsys.readouterr() + assert "Wrote CSV: out.csv" in captured.out + + +def test_maybe_render_plot_success_path(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None: + def no_op(_req: object) -> None: + return None + + monkeypatch.setattr(criterion_dim_plot, "_render_svg_with_gnuplot", no_op) + + args = argparse.Namespace(no_plot=False) + req = criterion_dim_plot.PlotRequest( + csv_path=criterion_dim_plot.Path("out.csv"), + out_svg=criterion_dim_plot.Path("out.svg"), + title="t", + stat="median", + dims=(2,), + la_label="la", + na_label="na", + fa_label="fa", + log_y=False, + ) + + rc = criterion_dim_plot._maybe_render_plot(args, req, skipped=["d2 (missing)"]) + assert rc == 0 + captured = capsys.readouterr() + assert "Warning: some dimension groups were skipped:" in captured.out + assert "Wrote CSV: out.csv" in captured.out + assert "Wrote SVG: out.svg" in captured.out + + +def test_maybe_update_readme_errors(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + args = argparse.Namespace( + update_readme=True, + readme="missing.md", + metric="lu_solve", + stat="median", + sample="new", + ) + rc = criterion_dim_plot._maybe_update_readme(tmp_path, args, []) + assert rc == 2 + captured = capsys.readouterr() + assert "No such file or directory" in captured.err + + +def test_main_error_paths(tmp_path: Path) -> None: + # Missing Criterion directory. + rc = criterion_dim_plot.main( + [ + "--criterion-dir", + str(tmp_path / "missing"), + "--no-plot", + ] + ) + assert rc == 2 + + # Criterion directory exists but has no usable rows. + criterion_dir = tmp_path / "criterion" + (criterion_dir / "d2").mkdir(parents=True) + rc = criterion_dim_plot.main( + [ + "--criterion-dir", + str(criterion_dir), + "--metric", + "lu_solve", + "--stat", + "median", + "--sample", + "new", + "--no-plot", + ] + ) + assert rc == 2 diff --git a/ty.toml b/ty.toml new file mode 100644 index 0000000..618fa19 --- /dev/null +++ b/ty.toml @@ -0,0 +1,15 @@ +# ty.toml +# Astral ty configuration for la-stack +# Python is auxiliary tooling only (scripts/), Rust is primary + +[src] +# Restrict analysis strictly to Python tooling. +include = [ "scripts" ] + +[environment] +# Match the project's minimum supported Python version. +python-version = "3.11" + +[terminal] +# Keep output concise by default. +output-format = "concise"