From 7cde40bcfe26f35d237b6baea10ad777e892f0de Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 31 Jan 2026 07:45:55 -0800 Subject: [PATCH 1/2] Changed: Update tooling, Rust version, and dependencies Updates several tooling versions in CI, including cspell and uv. Increases the minimum supported Rust version (MSRV) to 1.93. Renames WARP.md to AGENTS.md for broader applicability. Refreshes dependencies in Cargo.lock and pyproject.toml, including faer. Adds a type checker (ty) for Python scripts. (internal) --- .github/workflows/ci.yml | 6 +- .github/workflows/codecov.yml | 2 +- WARP.md => AGENTS.md | 4 +- Cargo.lock | 111 ++++---- Cargo.toml | 4 +- README.md | 2 +- cspell.json | 1 + .../bench/vs_linalg_lu_solve_median.csv | 12 +- justfile | 1 + pyproject.toml | 1 + rust-toolchain.toml | 2 +- scripts/criterion_dim_plot.py | 105 +++++++- scripts/tests/test_criterion_dim_plot.py | 255 +++++++++++++++++- ty.toml | 15 ++ 14 files changed, 437 insertions(+), 84 deletions(-) rename WARP.md => AGENTS.md (94%) create mode 100644 ty.toml 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..602f072 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" @@ -17,7 +17,7 @@ keywords = [ "linear-algebra", "geometry", "const-generics" ] [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" ] } +faer = { version = "0.24.0", default-features = false, features = [ "std", "linalg" ] } nalgebra = "0.34.1" pastey = "0.2.1" proptest = "1.9.0" 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..fe8f38e 100644 --- a/justfile +++ b/justfile @@ -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" From d701b586e89dd3583a77cd7c96512a7e6d6343a9 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Sat, 31 Jan 2026 08:07:14 -0800 Subject: [PATCH 2/2] Changed: Enables benchmark feature for criterion Enables the "bench" feature for criterion and other benchmark dependencies. This allows running benchmarks using the command `cargo bench --features bench`. It also ensures that benchmarks are compiled with warnings treated as errors by adding the `--features bench` flag to the `bench-compile` target. The coverage exclude has also been simplified. --- Cargo.toml | 13 +++++++++---- justfile | 14 +++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 602f072..709ae8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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.24.0", 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/justfile b/justfile index fe8f38e..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)