diff --git a/CHANGELOG.md b/CHANGELOG.md index 18ae3c5..07dd792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed +- **Lazy `VectorVariable` fast paths**: LP extraction, bounds handling, and solver setup now preserve vector-backed metadata longer instead of forcing immediate scalar `Variable` materialization. +- **Single-vector NLP cold-start path**: Unconstrained `VectorVariable` objectives such as `x.dot(x) - x.sum()` now compile through a contiguous vector layout, reuse vector-backed solver metadata, and avoid first-solve scalar materialization on the hot path. +- **Lazy solution value loading**: SciPy solve results continue to defer building large name-to-value maps until values are actually accessed, while keeping warm-start behavior intact. + +### Performance +- **Single-vector NLP**: Benchmarked cold overhead for the `x.dot(x) - x.sum()` path is now near raw SciPy parity at large `n`, with warm solves staying close to SciPy while keeping the vector cache lazy. + ## [1.3.0] - 2026-04-07 ### Added diff --git a/benchmarks/results/bench_vs_scipy_overhead_breakdown.png b/benchmarks/results/bench_vs_scipy_overhead_breakdown.png index 79d72f7..648c28a 100644 Binary files a/benchmarks/results/bench_vs_scipy_overhead_breakdown.png and b/benchmarks/results/bench_vs_scipy_overhead_breakdown.png differ diff --git a/benchmarks/results/benchmark_metadata.json b/benchmarks/results/benchmark_metadata.json index bbc9d53..84f3a2b 100644 --- a/benchmarks/results/benchmark_metadata.json +++ b/benchmarks/results/benchmark_metadata.json @@ -9,5 +9,5 @@ "python_implementation": "CPython", "python_version": "3.12.1", "scipy_version": "1.16.3", - "timestamp_utc": "2026-04-21T10:21:56.933482+00:00" + "timestamp_utc": "2026-05-14T14:42:29.831269+00:00" } diff --git a/benchmarks/results/benchmark_output.txt b/benchmarks/results/benchmark_output.txt index 0393f39..2656f32 100644 --- a/benchmarks/results/benchmark_output.txt +++ b/benchmarks/results/benchmark_output.txt @@ -23,19 +23,141 @@ Measures: Build (vars + problem + constraints) + Solve Compared against: SciPy linprog (no build phase) --- Loop-based Variable (n ≤ 500, slow cold solve) --- - Loop n= 10: Build= 0.5ms, Cold= 18.2ms, Warm= 1.9ms | SciPy= 1.8ms | Cold overhead= 10.2x, Warm overhead= 1.1x - Loop n= 25: Build= 1.4ms, Cold= 15.4ms, Warm= 2.5ms | SciPy= 2.5ms | Cold overhead= 6.6x, Warm overhead= 1.0x - Loop n= 50: Build= 4.8ms, Cold= 56.2ms, Warm= 2.4ms | SciPy= 2.1ms | Cold overhead= 29.0x, Warm overhead= 1.1x - Loop n= 100: Build= 11.8ms, Cold= 110.7ms, Warm= 3.6ms | SciPy= 3.0ms | Cold overhead= 40.3x, Warm overhead= 1.2x - Loop n= 200: Build= 69.9ms, Cold= 424.6ms, Warm= 11.1ms | SciPy= 8.0ms | Cold overhead= 62.1x, Warm overhead= 1.4x - Loop n= 500: Build= 505.8ms, Cold= 4262.3ms, Warm= 58.2ms | SciPy= 53.8ms | Cold overhead= 88.7x, Warm overhead= 1.1x + Loop n= 10: Build= 0.5ms, Cold= 10.5ms, Warm= 2.1ms | SciPy= 1.8ms | Cold overhead= 6.0x, Warm overhead= 1.1x + Loop n= 25: Build= 1.6ms, Cold= 14.0ms, Warm= 2.2ms | SciPy= 2.0ms | Cold overhead= 7.7x, Warm overhead= 1.1x + Loop n= 50: Build= 10.8ms, Cold= 57.4ms, Warm= 3.3ms | SciPy= 3.1ms | Cold overhead= 22.3x, Warm overhead= 1.1x + Loop n= 100: Build= 23.2ms, Cold= 234.0ms, Warm= 3.4ms | SciPy= 3.0ms | Cold overhead= 85.3x, Warm overhead= 1.1x + Loop n= 200: Build= 68.1ms, Cold= 444.5ms, Warm= 9.0ms | SciPy= 8.7ms | Cold overhead= 58.6x, Warm overhead= 1.0x + Loop n= 500: Build= 543.4ms, Cold= 4419.6ms, Warm= 56.3ms | SciPy= 53.9ms | Cold overhead= 92.0x, Warm overhead= 1.0x --- VectorVariable (n ≤ 5,000) --- - Vec n= 10: Build= 0.2ms, Cold= 1.7ms, Warm= 1.3ms | SciPy= 1.2ms | Cold overhead= 1.6x, Warm overhead= 1.2x - Vec n= 25: Build= 0.1ms, Cold= 1.6ms, Warm= 1.6ms | SciPy= 1.3ms | Cold overhead= 1.4x, Warm overhead= 1.3x - Vec n= 50: Build= 0.3ms, Cold= 2.4ms, Warm= 2.0ms | SciPy= 1.7ms | Cold overhead= 1.5x, Warm overhead= 1.2x - Vec n= 100: Build= 0.5ms, Cold= 4.2ms, Warm= 3.3ms | SciPy= 3.2ms | Cold overhead= 1.5x, Warm overhead= 1.0x - Vec n= 200: Build= 1.0ms, Cold= 11.3ms, Warm= 9.3ms | SciPy= 12.3ms | Cold overhead= 1.0x, Warm overhead= 0.8x - Vec n= 500: Build= 10.3ms, Cold= 94.7ms, Warm= 55.8ms | SciPy= 52.8ms | Cold overhead= 2.0x, Warm overhead= 1.1x - Vec n= 1000: Build= 7.2ms, Cold= 239.9ms, Warm= 274.6ms | SciPy= 286.8ms | Cold overhead= 0.9x, Warm overhead= 1.0x - Vec n= 2000: Build= 10.0ms, Cold= 938.4ms, Warm= 1028.2ms | SciPy= 1014.3ms | Cold overhead= 0.9x, Warm overhead= 1.0x + Vec n= 10: Build= 0.1ms, Cold= 1.9ms, Warm= 1.5ms | SciPy= 1.1ms | Cold overhead= 1.8x, Warm overhead= 1.4x + Vec n= 25: Build= 0.1ms, Cold= 1.8ms, Warm= 1.6ms | SciPy= 1.2ms | Cold overhead= 1.6x, Warm overhead= 1.3x + Vec n= 50: Build= 0.2ms, Cold= 2.6ms, Warm= 2.4ms | SciPy= 1.7ms | Cold overhead= 1.6x, Warm overhead= 1.4x + Vec n= 100: Build= 0.3ms, Cold= 4.9ms, Warm= 5.2ms | SciPy= 3.5ms | Cold overhead= 1.5x, Warm overhead= 1.5x + Vec n= 200: Build= 0.5ms, Cold= 13.1ms, Warm= 11.5ms | SciPy= 23.0ms | Cold overhead= 0.6x, Warm overhead= 0.5x + Vec n= 500: Build= 2.7ms, Cold= 67.8ms, Warm= 67.5ms | SciPy= 53.6ms | Cold overhead= 1.3x, Warm overhead= 1.3x + Vec n= 1000: Build= 2.7ms, Cold= 254.4ms, Warm= 304.3ms | SciPy= 219.2ms | Cold overhead= 1.2x, Warm overhead= 1.4x + Vec n= 2000: Build= 5.0ms, Cold= 1004.3ms, Warm= 1041.6ms | SciPy= 953.8ms | Cold overhead= 1.1x, Warm overhead= 1.1x + Vec n= 5000: Build= 12.2ms, Cold= 9593.3ms, Warm= 8834.7ms | SciPy= 8650.5ms | Cold overhead= 1.1x, Warm overhead= 1.0x + +Saved: /workspaces/optix/benchmarks/results/lp_scaling_comparison.png + +================================================================================ +UNCONSTRAINED NLP SCALING BENCHMARK (End-to-End) +================================================================================ + +Objective: min Σx²ᵢ - Σxᵢ (optimal at x* = 0.5) +Measures: Build + Solve (includes gradient compilation) + +--- Loop-based Variable (n ≤ 500, slow cold solve) --- + Loop n= 10: Build= 0.2ms, Cold= 6.3ms, Warm= 0.5ms | SciPy= 0.2ms | Cold overhead= 38.5x, Warm overhead= 3.1x + Loop n= 25: Build= 0.2ms, Cold= 12.0ms, Warm= 0.3ms | SciPy= 0.2ms | Cold overhead= 77.5x, Warm overhead= 2.1x + Loop n= 50: Build= 0.3ms, Cold= 38.0ms, Warm= 0.4ms | SciPy= 0.2ms | Cold overhead=235.0x, Warm overhead= 2.6x + Loop n= 100: Build= 0.5ms, Cold= 158.9ms, Warm= 0.6ms | SciPy= 0.2ms | Cold overhead=954.0x, Warm overhead= 3.4x + Loop n= 200: Build= 0.9ms, Cold= 575.9ms, Warm= 0.9ms | SciPy= 0.2ms | Cold overhead=2966.5x, Warm overhead= 4.6x + Loop n= 500: Build= 2.0ms, Cold= 3938.2ms, Warm= 2.1ms | SciPy= 0.2ms | Cold overhead=17200.8x, Warm overhead= 9.1x + +--- VectorVariable with x.dot(x) - x.sum() (n ≤ 5,000) --- + Vec n= 10: Build= 0.0ms, Cold= 0.6ms, Warm= 0.2ms | SciPy= 0.2ms | Cold overhead= 3.7x, Warm overhead= 1.3x + Vec n= 25: Build= 0.0ms, Cold= 0.5ms, Warm= 0.2ms | SciPy= 0.2ms | Cold overhead= 3.5x, Warm overhead= 1.3x + Vec n= 50: Build= 0.0ms, Cold= 0.6ms, Warm= 0.3ms | SciPy= 0.3ms | Cold overhead= 2.0x, Warm overhead= 0.9x + Vec n= 100: Build= 0.0ms, Cold= 0.5ms, Warm= 0.4ms | SciPy= 0.2ms | Cold overhead= 2.4x, Warm overhead= 1.6x + Vec n= 200: Build= 0.0ms, Cold= 0.6ms, Warm= 0.4ms | SciPy= 0.2ms | Cold overhead= 2.4x, Warm overhead= 1.6x + Vec n= 500: Build= 0.0ms, Cold= 0.5ms, Warm= 0.4ms | SciPy= 0.4ms | Cold overhead= 1.3x, Warm overhead= 0.9x + Vec n= 1000: Build= 0.0ms, Cold= 0.7ms, Warm= 0.6ms | SciPy= 0.4ms | Cold overhead= 1.6x, Warm overhead= 1.3x + Vec n= 2000: Build= 0.0ms, Cold= 0.8ms, Warm= 0.7ms | SciPy= 0.5ms | Cold overhead= 1.6x, Warm overhead= 1.2x + Vec n= 5000: Build= 0.0ms, Cold= 1.1ms, Warm= 1.0ms | SciPy= 0.8ms | Cold overhead= 1.4x, Warm overhead= 1.2x + +Saved: /workspaces/optix/benchmarks/results/nlp_scaling_comparison.png + +================================================================================ +CONSTRAINED QP SCALING BENCHMARK (End-to-End) +================================================================================ + +Objective: min Σx²ᵢ s.t. Σxᵢ ≥ 1, xᵢ ≥ 0 +Measures: Build + Solve (includes gradient/Jacobian compilation) + +--- Loop-based Variable (n ≤ 500, slow cold solve) --- + Loop n= 10: Build= 0.2ms, Cold= 1.8ms, Warm= 0.4ms | SciPy= 0.3ms | Cold overhead= 7.2x, Warm overhead= 1.4x + Loop n= 25: Build= 0.2ms, Cold= 5.1ms, Warm= 1.4ms | SciPy= 0.5ms | Cold overhead= 11.3x, Warm overhead= 3.0x + Loop n= 50: Build= 0.3ms, Cold= 14.1ms, Warm= 1.0ms | SciPy= 0.6ms | Cold overhead= 25.7x, Warm overhead= 1.7x + Loop n= 100: Build= 1.8ms, Cold= 71.1ms, Warm= 3.0ms | SciPy= 1.7ms | Cold overhead= 41.8x, Warm overhead= 1.7x + Loop n= 200: Build= 3.4ms, Cold= 321.8ms, Warm= 5.4ms | SciPy= 1.9ms | Cold overhead=169.4x, Warm overhead= 2.8x + Loop n= 500: Build= 3.8ms, Cold= 749.0ms, Warm= 31.9ms | SciPy= 14.3ms | Cold overhead= 52.5x, Warm overhead= 2.2x + +--- VectorVariable with x.dot(x), x.sum() (n ≤ 5000) --- + Vec n= 10: Build= 0.1ms, Cold= 1.4ms, Warm= 0.5ms | SciPy= 0.3ms | Cold overhead= 4.2x, Warm overhead= 1.4x + Vec n= 25: Build= 0.0ms, Cold= 2.6ms, Warm= 1.2ms | SciPy= 0.5ms | Cold overhead= 5.5x, Warm overhead= 2.5x + Vec n= 50: Build= 0.0ms, Cold= 1.2ms, Warm= 0.9ms | SciPy= 1.1ms | Cold overhead= 1.1x, Warm overhead= 0.8x + Vec n= 100: Build= 0.0ms, Cold= 2.1ms, Warm= 1.4ms | SciPy= 0.9ms | Cold overhead= 2.4x, Warm overhead= 1.6x + Vec n= 200: Build= 0.0ms, Cold= 3.4ms, Warm= 2.8ms | SciPy= 2.0ms | Cold overhead= 1.8x, Warm overhead= 1.4x + Vec n= 500: Build= 0.0ms, Cold= 18.5ms, Warm= 13.1ms | SciPy= 12.8ms | Cold overhead= 1.5x, Warm overhead= 1.0x + Vec n= 1000: Build= 0.1ms, Cold= 168.4ms, Warm= 104.4ms | SciPy= 88.8ms | Cold overhead= 1.9x, Warm overhead= 1.2x + Vec n= 2000: Build= 0.1ms, Cold= 466.6ms, Warm= 506.4ms | SciPy= 515.8ms | Cold overhead= 0.9x, Warm overhead= 1.0x + Vec n= 5000: Build= 0.1ms, Cold= 6665.1ms, Warm= 6287.6ms | SciPy= 6314.0ms | Cold overhead= 1.1x, Warm overhead= 1.0x + +Saved: /workspaces/optix/benchmarks/results/cqp_scaling_comparison.png + +================================================================================ +MILP SCALING BENCHMARK (End-to-End) +================================================================================ + +Measures: Build (vars + problem + constraints) + Solve +Compared against: SciPy milp (no build phase) +Problem: Single-constraint binary knapsack (sum(x) <= n//2) + +--- Loop-based Variable (n ≤ 500, slow cold solve) --- + Loop n= 10: Build= 0.2ms, Cold= 4.6ms, Warm= 2.5ms | SciPy= 1.7ms | Cold overhead= 2.8x, Warm overhead= 1.5x + Loop n= 25: Build= 0.3ms, Cold= 9.0ms, Warm= 2.1ms | SciPy= 1.5ms | Cold overhead= 6.3x, Warm overhead= 1.4x + Loop n= 50: Build= 0.4ms, Cold= 2.6ms, Warm= 1.5ms | SciPy= 1.4ms | Cold overhead= 2.2x, Warm overhead= 1.1x + Loop n= 100: Build= 1.4ms, Cold= 9.0ms, Warm= 2.5ms | SciPy= 6.8ms | Cold overhead= 1.5x, Warm overhead= 0.4x + Loop n= 200: Build= 2.2ms, Cold= 16.9ms, Warm= 7.3ms | SciPy= 3.3ms | Cold overhead= 5.8x, Warm overhead= 2.2x + Loop n= 500: Build= 2.1ms, Cold= 41.7ms, Warm= 9.8ms | SciPy= 9.1ms | Cold overhead= 4.8x, Warm overhead= 1.1x + +--- VectorVariable (n ≤ 5000) --- + Vec n= 10: Build= 0.1ms, Cold= 1.2ms, Warm= 1.0ms | SciPy= 1.0ms | Cold overhead= 1.3x, Warm overhead= 1.0x + Vec n= 25: Build= 0.0ms, Cold= 1.3ms, Warm= 1.1ms | SciPy= 1.1ms | Cold overhead= 1.2x, Warm overhead= 1.0x + Vec n= 50: Build= 0.0ms, Cold= 1.5ms, Warm= 1.4ms | SciPy= 1.1ms | Cold overhead= 1.4x, Warm overhead= 1.3x + Vec n= 100: Build= 0.0ms, Cold= 2.0ms, Warm= 2.2ms | SciPy= 1.7ms | Cold overhead= 1.2x, Warm overhead= 1.3x + Vec n= 200: Build= 0.0ms, Cold= 3.3ms, Warm= 3.5ms | SciPy= 2.9ms | Cold overhead= 1.1x, Warm overhead= 1.2x + Vec n= 500: Build= 0.0ms, Cold= 11.7ms, Warm= 9.3ms | SciPy= 8.3ms | Cold overhead= 1.4x, Warm overhead= 1.1x + Vec n= 1000: Build= 0.0ms, Cold= 31.8ms, Warm= 27.4ms | SciPy= 25.7ms | Cold overhead= 1.2x, Warm overhead= 1.1x + Vec n= 2000: Build= 0.1ms, Cold= 116.8ms, Warm= 93.3ms | SciPy= 93.9ms | Cold overhead= 1.2x, Warm overhead= 1.0x + Vec n= 5000: Build= 0.1ms, Cold= 600.8ms, Warm= 622.7ms | SciPy= 553.4ms | Cold overhead= 1.1x, Warm overhead= 1.1x + +Saved: /workspaces/optix/benchmarks/results/milp_scaling_comparison.png +Structured benchmark results saved to: /workspaces/optix/benchmarks/results/benchmark_results.json + +================================================================================ +OVERHEAD SUMMARY BY PROBLEM TYPE +================================================================================ +LP n=50: Cold=1.6x, Warm=1.4x +LP n=5000: Cold=1.1x, Warm=1.0x +NLP n=50: Cold=2.0x, Warm=0.9x +NLP n=5000: Cold=1.4x, Warm=1.2x +CQP n=50: Cold=1.1x, Warm=0.8x +CQP n=5000: Cold=1.1x, Warm=1.0x +MILP n=50: Cold=1.4x, Warm=1.3x +MILP n=5000: Cold=1.1x, Warm=1.1x + +Saved: /workspaces/optix/benchmarks/results/overhead_breakdown.png + +================================================================================ +BENCHMARK COMPLETE +================================================================================ + +Plots saved to: /workspaces/optix/benchmarks/results + - bench_vs_scipy_overhead_breakdown.png + - cqp_scaling_comparison.png + - lp_cache_benefit.png + - lp_scaling_comparison.png + - milp_scaling_comparison.png + - multi_problem_scaling.png + - nlp_quadratic_scaling.png + - nlp_scaling_comparison.png + - overhead_breakdown.png + - scipy_lp_scaling.png + - sparse_memory_reduction.png + - sparse_solve_end_to_end.png + - sparse_vs_dense_comparison.png diff --git a/benchmarks/results/benchmark_results.json b/benchmarks/results/benchmark_results.json index b962dea..54a8b0f 100644 --- a/benchmarks/results/benchmark_results.json +++ b/benchmarks/results/benchmark_results.json @@ -14,138 +14,138 @@ "benchmark_suite": "run_benchmarks", "overhead_summary": [ { - "cold_overhead": 1.0501582337223818, + "cold_overhead": 1.64644252317742, "problem_type": "LP", "size": 50, - "warm_overhead": 0.812985045888142 + "warm_overhead": 1.3869239483696907 }, { - "cold_overhead": 1.1275902207716624, + "cold_overhead": 1.110394944099998, "problem_type": "LP", "size": 5000, - "warm_overhead": 1.0140021159852615 + "warm_overhead": 1.0212860660463705 }, { - "cold_overhead": 3.026941579482231, + "cold_overhead": 1.968910777419289, "problem_type": "NLP", "size": 50, - "warm_overhead": 2.0825227513121036 + "warm_overhead": 0.8642755582338756 }, { - "cold_overhead": 81.94926695989462, + "cold_overhead": 1.4302990865719043, "problem_type": "NLP", "size": 5000, - "warm_overhead": 42.13192875480498 + "warm_overhead": 1.2186439631434345 }, { - "cold_overhead": 1.4299693678646468, + "cold_overhead": 1.0824487449877866, "problem_type": "CQP", "size": 50, - "warm_overhead": 0.788050478137063 + "warm_overhead": 0.7885143099133539 }, { - "cold_overhead": 1.0946971400391046, + "cold_overhead": 1.0556033640045543, "problem_type": "CQP", "size": 5000, - "warm_overhead": 1.001488479208934 + "warm_overhead": 0.9958139221028426 }, { - "cold_overhead": 1.4566705000975524, + "cold_overhead": 1.4118825457729465, "problem_type": "MILP", "size": 50, - "warm_overhead": 1.2941061236838522 + "warm_overhead": 1.2746938094029638 }, { - "cold_overhead": 1.0031123576531944, + "cold_overhead": 1.0858302442506533, "problem_type": "MILP", "size": 5000, - "warm_overhead": 1.179959153413669 + "warm_overhead": 1.1252722039991174 } ], "performance_summary": [ { - "cold_overhead": 1.0501582337223818, + "cold_overhead": 1.64644252317742, "note": "Near-parity with SciPy linprog", "problem_type": "LP", "size": 50, - "warm_overhead": 0.812985045888142 + "warm_overhead": 1.3869239483696907 }, { - "cold_overhead": 1.211487886529665, + "cold_overhead": 1.3155652933969881, "note": "Near-parity with SciPy linprog", "problem_type": "LP", "size": 500, - "warm_overhead": 1.0357210528392964 + "warm_overhead": 1.25864301363049 }, { - "cold_overhead": 1.1275902207716624, + "cold_overhead": 1.110394944099998, "note": "Scales to large LPs while staying near parity", "problem_type": "LP", "size": 5000, - "warm_overhead": 1.0140021159852615 + "warm_overhead": 1.0212860660463705 }, { - "cold_overhead": 3.026941579482231, + "cold_overhead": 1.968910777419289, "note": "Autodiff overhead on a trivially simple objective", "problem_type": "NLP", "size": 50, - "warm_overhead": 2.0825227513121036 + "warm_overhead": 0.8642755582338756 }, { - "cold_overhead": 53.78946185114727, + "cold_overhead": 1.2737089423498595, "note": "Autodiff overhead on a trivially simple objective", "problem_type": "NLP", "size": 500, - "warm_overhead": 8.802874053183821 + "warm_overhead": 0.8639030284240333 }, { - "cold_overhead": 81.94926695989462, + "cold_overhead": 1.4302990865719043, "note": "Simple quadratic; SciPy converges almost instantly", "problem_type": "NLP", "size": 5000, - "warm_overhead": 42.13192875480498 + "warm_overhead": 1.2186439631434345 }, { - "cold_overhead": 1.4299693678646468, + "cold_overhead": 1.0824487449877866, "note": "O(1) Jacobian compilation for vectorized constraints", "problem_type": "CQP", "size": 50, - "warm_overhead": 0.788050478137063 + "warm_overhead": 0.7885143099133539 }, { - "cold_overhead": 1.6394185969433936, + "cold_overhead": 1.4506026832376255, "note": "O(1) Jacobian compilation for vectorized constraints", "problem_type": "CQP", "size": 500, - "warm_overhead": 0.9872978508888796 + "warm_overhead": 1.0230633467973789 }, { - "cold_overhead": 1.0946971400391046, + "cold_overhead": 1.0556033640045543, "note": "Exact Jacobians keep constrained solves near parity", "problem_type": "CQP", "size": 5000, - "warm_overhead": 1.001488479208934 + "warm_overhead": 0.9958139221028426 }, { - "cold_overhead": 1.4566705000975524, + "cold_overhead": 1.4118825457729465, "note": "Near-parity with SciPy milp", "problem_type": "MILP", "size": 50, - "warm_overhead": 1.2941061236838522 + "warm_overhead": 1.2746938094029638 }, { - "cold_overhead": 1.9537429622573275, + "cold_overhead": 1.4160318043297646, "note": "Near-parity with SciPy milp", "problem_type": "MILP", "size": 500, - "warm_overhead": 1.2936619533279687 + "warm_overhead": 1.118445895210725 }, { - "cold_overhead": 1.0031123576531944, + "cold_overhead": 1.0858302442506533, "note": "Scales to large binary knapsack problems", "problem_type": "MILP", "size": 5000, - "warm_overhead": 1.179959153413669 + "warm_overhead": 1.1252722039991174 } ], "scaling": { @@ -154,70 +154,70 @@ "label": "CQP (Loop)", "results": [ { - "build_ms": 0.153315002535237, - "cold_overhead": 31.11061123509847, - "cold_solve_ms": 10.939440999209182, - "cold_total_ms": 11.09275600174442, + "build_ms": 0.1677440000094066, + "cold_overhead": 7.171137486656461, + "cold_solve_ms": 1.7870879996735312, + "cold_total_ms": 1.9548319996829377, "n": 10, - "scipy_ms": 0.3565586004697252, - "warm_overhead": 1.7591885287050133, - "warm_solve_ms": 0.6272537997574545, - "warm_total_ms": 0.6272537997574545 + "scipy_ms": 0.27259720000074594, + "warm_overhead": 1.4270960961430332, + "warm_solve_ms": 0.3890223999405862, + "warm_total_ms": 0.3890223999405862 }, { - "build_ms": 0.2530210003897082, - "cold_overhead": 18.974517042517437, - "cold_solve_ms": 15.85142200201517, - "cold_total_ms": 16.104443002404878, + "build_ms": 0.22640299994236557, + "cold_overhead": 11.285625998365749, + "cold_solve_ms": 5.10596499998428, + "cold_total_ms": 5.332367999926646, "n": 25, - "scipy_ms": 0.8487406012136489, - "warm_overhead": 1.3679175940132715, - "warm_solve_ms": 1.1610072011535522, - "warm_total_ms": 1.1610072011535522 + "scipy_ms": 0.4724920000626298, + "warm_overhead": 2.982929657656677, + "warm_solve_ms": 1.409410399992339, + "warm_total_ms": 1.409410399992339 }, { - "build_ms": 1.158421000582166, - "cold_overhead": 61.60524408065892, - "cold_solve_ms": 71.6935749987897, - "cold_total_ms": 72.85199599937187, + "build_ms": 0.3268699997533986, + "cold_overhead": 25.717735609804425, + "cold_solve_ms": 14.113870000073803, + "cold_total_ms": 14.440739999827201, "n": 50, - "scipy_ms": 1.1825615998532157, - "warm_overhead": 1.4221486635349372, - "warm_solve_ms": 1.681778398778988, - "warm_total_ms": 1.681778398778988 + "scipy_ms": 0.5615089998173062, + "warm_overhead": 1.7315056398723592, + "warm_solve_ms": 0.9722560000227531, + "warm_total_ms": 0.9722560000227531 }, { - "build_ms": 1.2048469980072696, - "cold_overhead": 255.63438117091056, - "cold_solve_ms": 221.54135299933841, - "cold_total_ms": 222.74619999734568, + "build_ms": 1.7901940000228933, + "cold_overhead": 41.82529263517236, + "cold_solve_ms": 71.06601100031185, + "cold_total_ms": 72.85620500033474, "n": 100, - "scipy_ms": 0.8713467999768909, - "warm_overhead": 2.100260194994894, - "warm_solve_ms": 1.8300550000276417, - "warm_total_ms": 1.8300550000276417 + "scipy_ms": 1.7419173999769555, + "warm_overhead": 1.7235244335309914, + "warm_solve_ms": 3.0022372000530595, + "warm_total_ms": 3.0022372000530595 }, { - "build_ms": 1.9555170001694933, - "cold_overhead": 396.1053364293281, - "cold_solve_ms": 762.428201000148, - "cold_total_ms": 764.3837180003175, + "build_ms": 3.3577020003576763, + "cold_overhead": 169.35686470317455, + "cold_solve_ms": 321.7535549997592, + "cold_total_ms": 325.1112570001169, "n": 200, - "scipy_ms": 1.9297485989227425, - "warm_overhead": 1.9573368272172225, - "warm_solve_ms": 3.7771679999423213, - "warm_total_ms": 3.7771679999423213 + "scipy_ms": 1.919681599974865, + "warm_overhead": 2.820367606833846, + "warm_solve_ms": 5.414207800004078, + "warm_total_ms": 5.414207800004078 }, { - "build_ms": 5.547715998545755, - "cold_overhead": 461.5214241891787, - "cold_solve_ms": 5982.5169229989115, - "cold_total_ms": 5988.064638997457, + "build_ms": 3.770271999655961, + "cold_overhead": 52.54366337163847, + "cold_solve_ms": 748.9853399997628, + "cold_total_ms": 752.7556119994188, "n": 500, - "scipy_ms": 12.974618999578524, - "warm_overhead": 2.6842881013962274, - "warm_solve_ms": 34.82761540071806, - "warm_total_ms": 34.82761540071806 + "scipy_ms": 14.326287199946819, + "warm_overhead": 2.2301639464705927, + "warm_solve_ms": 31.949969200104533, + "warm_total_ms": 31.949969200104533 } ] }, @@ -225,103 +225,103 @@ "label": "CQP (VectorVariable)", "results": [ { - "build_ms": 0.11850099690491334, - "cold_overhead": 3.027583458815483, - "cold_solve_ms": 0.8402160019613802, - "cold_total_ms": 0.9587169988662936, + "build_ms": 0.05360000022847089, + "cold_overhead": 4.225804309378625, + "cold_solve_ms": 1.396598000155791, + "cold_total_ms": 1.4501980003842618, "n": 10, - "scipy_ms": 0.316660799580859, - "warm_overhead": 1.3346085183142544, - "warm_solve_ms": 0.4226182005368173, - "warm_total_ms": 0.4226182005368173 + "scipy_ms": 0.3431768000154989, + "warm_overhead": 1.3967004760079618, + "warm_solve_ms": 0.4793151999365364, + "warm_total_ms": 0.4793151999365364 }, { - "build_ms": 0.1562409997859504, - "cold_overhead": 2.391429352767669, - "cold_solve_ms": 0.9384200020576827, - "cold_total_ms": 1.0946610018436331, + "build_ms": 0.035926999771618284, + "cold_overhead": 5.477939154587546, + "cold_solve_ms": 2.5541499999235384, + "cold_total_ms": 2.5900769996951567, "n": 25, - "scipy_ms": 0.45774339960189536, - "warm_overhead": 2.260624187010286, - "warm_solve_ms": 1.034785800584359, - "warm_total_ms": 1.034785800584359 + "scipy_ms": 0.47281959996325895, + "warm_overhead": 2.45678013369972, + "warm_solve_ms": 1.1616138000135834, + "warm_total_ms": 1.1616138000135834 }, { - "build_ms": 0.21687400294467807, - "cold_overhead": 1.4299693678646468, - "cold_solve_ms": 1.0550480001256801, - "cold_total_ms": 1.2719220030703582, + "build_ms": 0.026870000056078425, + "cold_overhead": 1.0824487449877866, + "cold_solve_ms": 1.2036980001539632, + "cold_total_ms": 1.2305680002100416, "n": 50, - "scipy_ms": 0.8894749997125473, - "warm_overhead": 0.788050478137063, - "warm_solve_ms": 0.7009511988144368, - "warm_total_ms": 0.7009511988144368 + "scipy_ms": 1.136837199828733, + "warm_overhead": 0.7885143099133539, + "warm_solve_ms": 0.896412400106783, + "warm_total_ms": 0.896412400106783 }, { - "build_ms": 0.3956479995395057, - "cold_overhead": 1.6844031244504607, - "cold_solve_ms": 1.463579999835929, - "cold_total_ms": 1.8592279993754346, + "build_ms": 0.034714999856078066, + "cold_overhead": 2.3781670145922615, + "cold_solve_ms": 2.096144999995886, + "cold_total_ms": 2.130859999851964, "n": 100, - "scipy_ms": 1.1037904005206656, - "warm_overhead": 1.1312225582223807, - "warm_solve_ms": 1.2486326006182935, - "warm_total_ms": 1.2486326006182935 + "scipy_ms": 0.8960094000030949, + "warm_overhead": 1.5813365350724404, + "warm_solve_ms": 1.4168923999932304, + "warm_total_ms": 1.4168923999932304 }, { - "build_ms": 0.7324860016524326, - "cold_overhead": 1.907498936382662, - "cold_solve_ms": 2.9472659989551175, - "cold_total_ms": 3.67975200060755, + "build_ms": 0.03521600001477054, + "cold_overhead": 1.7596890137533931, + "cold_solve_ms": 3.4471980002308555, + "cold_total_ms": 3.482414000245626, "n": 200, - "scipy_ms": 1.9290977994387504, - "warm_overhead": 1.237126703114857, - "warm_solve_ms": 2.3865384006057866, - "warm_total_ms": 2.3865384006057866 + "scipy_ms": 1.978994000091916, + "warm_overhead": 1.409494925083776, + "warm_solve_ms": 2.7893819999007974, + "warm_total_ms": 2.7893819999007974 }, { - "build_ms": 2.5035090002347715, - "cold_overhead": 1.6394185969433936, - "cold_solve_ms": 18.565805999969598, - "cold_total_ms": 21.06931500020437, + "build_ms": 0.039523999930679565, + "cold_overhead": 1.4506026832376255, + "cold_solve_ms": 18.47236100002192, + "cold_total_ms": 18.5118849999526, "n": 500, - "scipy_ms": 12.851699400926009, - "warm_overhead": 0.9872978508888796, - "warm_solve_ms": 12.68845519880415, - "warm_total_ms": 12.68845519880415 + "scipy_ms": 12.761512999986735, + "warm_overhead": 1.0230633467973789, + "warm_solve_ms": 13.055836199964688, + "warm_total_ms": 13.055836199964688 }, { - "build_ms": 13.506178998795804, - "cold_overhead": 1.340633127996738, - "cold_solve_ms": 80.1798009997583, - "cold_total_ms": 93.6859799985541, + "build_ms": 0.05229800035522203, + "cold_overhead": 1.89680755607629, + "cold_solve_ms": 168.3559900002365, + "cold_total_ms": 168.4082880005917, "n": 1000, - "scipy_ms": 69.88189240000793, - "warm_overhead": 1.3084826449294356, - "warm_solve_ms": 91.43924340023659, - "warm_total_ms": 91.43924340023659 + "scipy_ms": 88.78512080000291, + "warm_overhead": 1.1755433935272264, + "warm_solve_ms": 104.37076219996015, + "warm_total_ms": 104.37076219996015 }, { - "build_ms": 11.169469999003923, - "cold_overhead": 0.8354444607531919, - "cold_solve_ms": 471.47300299911876, - "cold_total_ms": 482.6424729981227, + "build_ms": 0.05462199987960048, + "cold_overhead": 0.9046876394019586, + "cold_solve_ms": 466.58226199997443, + "cold_total_ms": 466.63688399985404, "n": 2000, - "scipy_ms": 577.7074308003648, - "warm_overhead": 1.0372864849082362, - "warm_solve_ms": 599.2481102002785, - "warm_total_ms": 599.2481102002785 + "scipy_ms": 515.7988941999065, + "warm_overhead": 0.9817081849032054, + "warm_solve_ms": 506.3639962000707, + "warm_total_ms": 506.3639962000707 }, { - "build_ms": 49.87247100143577, - "cold_overhead": 1.0946971400391046, - "cold_solve_ms": 7601.275322998845, - "cold_total_ms": 7651.147794000281, + "build_ms": 0.055483000323874876, + "cold_overhead": 1.0556033640045543, + "cold_solve_ms": 6665.058946000045, + "cold_total_ms": 6665.114429000369, "n": 5000, - "scipy_ms": 6989.282710400585, - "warm_overhead": 1.001488479208934, - "warm_solve_ms": 6999.686112400377, - "warm_total_ms": 6999.686112400377 + "scipy_ms": 6314.032956200026, + "warm_overhead": 0.9958139221028426, + "warm_solve_ms": 6287.601922400154, + "warm_total_ms": 6287.601922400154 } ] } @@ -331,70 +331,70 @@ "label": "LP (Loop)", "results": [ { - "build_ms": 0.4565819981507957, - "cold_overhead": 11.482336147813978, - "cold_solve_ms": 20.522652001091046, - "cold_total_ms": 20.979233999241842, + "build_ms": 0.49062600010074675, + "cold_overhead": 6.0062338384742135, + "cold_solve_ms": 10.461930000019493, + "cold_total_ms": 10.95255600012024, "n": 10, - "scipy_ms": 1.8270876003953163, - "warm_overhead": 1.0815988235244256, - "warm_solve_ms": 1.9761757990636397, - "warm_total_ms": 1.9761757990636397 + "scipy_ms": 1.8235313999866776, + "warm_overhead": 1.143586998252001, + "warm_solve_ms": 2.0853667999290337, + "warm_total_ms": 2.0853667999290337 }, { - "build_ms": 1.4050399986444972, - "cold_overhead": 9.623407770861848, - "cold_solve_ms": 19.63746299952618, - "cold_total_ms": 21.042502998170676, + "build_ms": 1.58919799969226, + "cold_overhead": 7.744346214687868, + "cold_solve_ms": 13.95521499989627, + "cold_total_ms": 15.54441299958853, "n": 25, - "scipy_ms": 2.1865957984118722, - "warm_overhead": 1.1279439032452812, - "warm_solve_ms": 2.466357399680419, - "warm_total_ms": 2.466357399680419 + "scipy_ms": 2.0071949998964556, + "warm_overhead": 1.0849764970178892, + "warm_solve_ms": 2.177759399819479, + "warm_total_ms": 2.177759399819479 }, { - "build_ms": 4.869180997047806, - "cold_overhead": 32.24808044538227, - "cold_solve_ms": 51.346608001040295, - "cold_total_ms": 56.2157889980881, + "build_ms": 10.773992000395083, + "cold_overhead": 22.275961913221575, + "cold_solve_ms": 57.449309000276116, + "cold_total_ms": 68.2233010006712, "n": 50, - "scipy_ms": 1.7432289991120342, - "warm_overhead": 2.138066428056541, - "warm_solve_ms": 3.727139399416046, - "warm_total_ms": 3.727139399416046 + "scipy_ms": 3.0626421999841114, + "warm_overhead": 1.0824614772786427, + "warm_solve_ms": 3.3151922001707135, + "warm_total_ms": 3.3151922001707135 }, { - "build_ms": 10.630352997395676, - "cold_overhead": 34.299098632360746, - "cold_solve_ms": 108.15988299873425, - "cold_total_ms": 118.79023599612992, + "build_ms": 23.19088300009753, + "cold_overhead": 85.33352547430788, + "cold_solve_ms": 233.98548700015454, + "cold_total_ms": 257.17637000025206, "n": 100, - "scipy_ms": 3.4633631999895442, - "warm_overhead": 0.9625728540754156, - "warm_solve_ms": 3.3337394001137, - "warm_total_ms": 3.3337394001137 + "scipy_ms": 3.0137787999592547, + "warm_overhead": 1.1130077629128863, + "warm_solve_ms": 3.3543592000569333, + "warm_total_ms": 3.3543592000569333 }, { - "build_ms": 63.9043990013306, - "cold_overhead": 60.31374442643511, - "cold_solve_ms": 477.2999010019703, - "cold_total_ms": 541.2043000033009, + "build_ms": 68.06883299987021, + "cold_overhead": 58.58424841532407, + "cold_solve_ms": 444.50848299993595, + "cold_total_ms": 512.5773159998062, "n": 200, - "scipy_ms": 8.973150401288876, - "warm_overhead": 0.9726232159191104, - "warm_solve_ms": 8.727494400227442, - "warm_total_ms": 8.727494400227442 + "scipy_ms": 8.74940499988952, + "warm_overhead": 1.0300122122774638, + "warm_solve_ms": 9.011994000047707, + "warm_total_ms": 9.011994000047707 }, { - "build_ms": 478.78952999963076, - "cold_overhead": 91.74939438260711, - "cold_solve_ms": 4352.143078998779, - "cold_total_ms": 4830.93260899841, + "build_ms": 543.3780539997315, + "cold_overhead": 91.9948505056075, + "cold_solve_ms": 4419.6402869997655, + "cold_total_ms": 4963.018340999497, "n": 500, - "scipy_ms": 52.65356399904704, - "warm_overhead": 1.0590257100395577, - "warm_solve_ms": 55.76147800020408, - "warm_total_ms": 55.76147800020408 + "scipy_ms": 53.94887120010026, + "warm_overhead": 1.0441502546194383, + "warm_solve_ms": 56.33072760001596, + "warm_total_ms": 56.33072760001596 } ] }, @@ -402,103 +402,103 @@ "label": "LP (VectorVariable)", "results": [ { - "build_ms": 0.19218799934606068, - "cold_overhead": 1.693216922730903, - "cold_solve_ms": 1.6525819992239121, - "cold_total_ms": 1.8447699985699728, + "build_ms": 0.12826000011045835, + "cold_overhead": 1.8143836821883061, + "cold_solve_ms": 1.8964219998451881, + "cold_total_ms": 2.0246819999556465, "n": 10, - "scipy_ms": 1.0895060011534952, - "warm_overhead": 1.2623675308353952, - "warm_solve_ms": 1.375357000506483, - "warm_total_ms": 1.375357000506483 + "scipy_ms": 1.1159061999023834, + "warm_overhead": 1.365814259391491, + "warm_solve_ms": 1.524120599970047, + "warm_total_ms": 1.524120599970047 }, { - "build_ms": 0.2016159996856004, - "cold_overhead": 1.4924938118363915, - "cold_solve_ms": 1.623988999199355, - "cold_total_ms": 1.8256049988849554, + "build_ms": 0.11296100001345621, + "cold_overhead": 1.5664240835206351, + "cold_solve_ms": 1.8033380001725163, + "cold_total_ms": 1.9162990001859725, "n": 25, - "scipy_ms": 1.2231910004629754, - "warm_overhead": 1.2657148376693776, - "warm_solve_ms": 1.5482109985896386, - "warm_total_ms": 1.5482109985896386 + "scipy_ms": 1.2233589998686512, + "warm_overhead": 1.3299651207805323, + "warm_solve_ms": 1.6270248000182619, + "warm_total_ms": 1.6270248000182619 }, { - "build_ms": 0.29033099781372584, - "cold_overhead": 1.0501582337223818, - "cold_solve_ms": 2.3654819997318555, - "cold_total_ms": 2.6558129975455813, + "build_ms": 0.16892500025278423, + "cold_overhead": 1.64644252317742, + "cold_solve_ms": 2.6441889999659907, + "cold_total_ms": 2.813114000218775, "n": 50, - "scipy_ms": 2.528964600060135, - "warm_overhead": 0.812985045888142, - "warm_solve_ms": 2.0560104014293756, - "warm_total_ms": 2.0560104014293756 + "scipy_ms": 1.708601400059706, + "warm_overhead": 1.3869239483696907, + "warm_solve_ms": 2.369700199960789, + "warm_total_ms": 2.369700199960789 }, { - "build_ms": 0.9552609990350902, - "cold_overhead": 1.4109392686950275, - "cold_solve_ms": 6.260796002607094, - "cold_total_ms": 7.216057001642184, + "build_ms": 0.25551699991410715, + "cold_overhead": 1.4913857355482252, + "cold_solve_ms": 4.895423000107257, + "cold_total_ms": 5.1509400000213645, "n": 100, - "scipy_ms": 5.114363999746274, - "warm_overhead": 1.7028074262404869, - "warm_solve_ms": 8.708776999264956, - "warm_total_ms": 8.708776999264956 + "scipy_ms": 3.453794600045512, + "warm_overhead": 1.5039476290441811, + "warm_solve_ms": 5.194326199944044, + "warm_total_ms": 5.194326199944044 }, { - "build_ms": 1.8441590000293218, - "cold_overhead": 3.6727232458672763, - "cold_solve_ms": 26.99932399991667, - "cold_total_ms": 28.84348299994599, + "build_ms": 0.5499969997799781, + "cold_overhead": 0.5917800446844047, + "cold_solve_ms": 13.082112000120105, + "cold_total_ms": 13.632108999900083, "n": 200, - "scipy_ms": 7.8534321997722145, - "warm_overhead": 1.2149146968198785, - "warm_solve_ms": 9.541250199981732, - "warm_total_ms": 9.541250199981732 + "scipy_ms": 23.03576999993311, + "warm_overhead": 0.5002768303381967, + "warm_solve_ms": 11.524261999966257, + "warm_total_ms": 11.524261999966257 }, { - "build_ms": 2.4393290004809387, - "cold_overhead": 1.211487886529665, - "cold_solve_ms": 63.093166001635836, - "cold_total_ms": 65.53249500211678, + "build_ms": 2.7037199997721473, + "cold_overhead": 1.3155652933969881, + "cold_solve_ms": 67.84047700011797, + "cold_total_ms": 70.54419699989012, "n": 500, - "scipy_ms": 54.092571399814915, - "warm_overhead": 1.0357210528392964, - "warm_solve_ms": 56.024815001001116, - "warm_total_ms": 56.024815001001116 + "scipy_ms": 53.62272580005083, + "warm_overhead": 1.25864301363049, + "warm_solve_ms": 67.49186920005741, + "warm_total_ms": 67.49186920005741 }, { - "build_ms": 6.566907002707012, - "cold_overhead": 1.004822563939135, - "cold_solve_ms": 252.37838400062174, - "cold_total_ms": 258.94529100332875, + "build_ms": 2.6649579999684647, + "cold_overhead": 1.172837134151277, + "cold_solve_ms": 254.367676000129, + "cold_total_ms": 257.03263400009746, "n": 1000, - "scipy_ms": 257.7025041995512, - "warm_overhead": 1.0960677571869186, - "warm_solve_ms": 282.45940579945454, - "warm_total_ms": 282.45940579945454 + "scipy_ms": 219.1545837999911, + "warm_overhead": 1.3887398133447033, + "warm_solve_ms": 304.3486958000358, + "warm_total_ms": 304.3486958000358 }, { - "build_ms": 10.243332999380073, - "cold_overhead": 1.0615410333742983, - "cold_solve_ms": 1171.9294570029888, - "cold_total_ms": 1182.1727900023689, + "build_ms": 4.999228000087896, + "cold_overhead": 1.0581893362678234, + "cold_solve_ms": 1004.2722220000542, + "cold_total_ms": 1009.2714500001421, "n": 2000, - "scipy_ms": 1113.6383359997126, - "warm_overhead": 1.0187540921722584, - "warm_solve_ms": 1134.5236119996116, - "warm_total_ms": 1134.5236119996116 + "scipy_ms": 953.7720853999417, + "warm_overhead": 1.0921347046588157, + "warm_solve_ms": 1041.647594800088, + "warm_total_ms": 1041.647594800088 }, { - "build_ms": 24.960611001006328, - "cold_overhead": 1.1275902207716624, - "cold_solve_ms": 10199.972689999413, - "cold_total_ms": 10224.93330100042, + "build_ms": 12.240551999639138, + "cold_overhead": 1.110394944099998, + "cold_solve_ms": 9593.275089000144, + "cold_total_ms": 9605.515640999783, "n": 5000, - "scipy_ms": 9067.951382198953, - "warm_overhead": 1.0140021159852615, - "warm_solve_ms": 9194.921889201214, - "warm_total_ms": 9194.921889201214 + "scipy_ms": 8650.53978499991, + "warm_overhead": 1.0212860660463705, + "warm_solve_ms": 8834.675746200173, + "warm_total_ms": 8834.675746200173 } ] } @@ -508,70 +508,70 @@ "label": "MILP (Loop)", "results": [ { - "build_ms": 0.172080999618629, - "cold_overhead": 17.811266453769836, - "cold_solve_ms": 14.173112998832949, - "cold_total_ms": 14.345193998451577, + "build_ms": 0.1736039998831984, + "cold_overhead": 2.817920429233705, + "cold_solve_ms": 4.590033000113181, + "cold_total_ms": 4.763636999996379, "n": 10, - "scipy_ms": 0.8053999998082872, - "warm_overhead": 1.2008266692982976, - "warm_solve_ms": 0.9671457992226351, - "warm_total_ms": 0.9671457992226351 + "scipy_ms": 1.6904795999835187, + "warm_overhead": 1.4630458717368189, + "warm_solve_ms": 2.473249200011196, + "warm_total_ms": 2.473249200011196 }, { - "build_ms": 0.2327029978914652, - "cold_overhead": 2.200366322114951, - "cold_solve_ms": 1.718735002214089, - "cold_total_ms": 1.9514380001055542, + "build_ms": 0.28500200005510123, + "cold_overhead": 6.271347531956442, + "cold_solve_ms": 8.969903999968665, + "cold_total_ms": 9.254906000023766, "n": 25, - "scipy_ms": 0.8868696000718046, - "warm_overhead": 1.4006958858066936, - "warm_solve_ms": 1.2422346000676043, - "warm_total_ms": 1.2422346000676043 + "scipy_ms": 1.4757443998860253, + "warm_overhead": 1.4104500753419549, + "warm_solve_ms": 2.0814638000047125, + "warm_total_ms": 2.0814638000047125 }, { - "build_ms": 0.34325999877182767, - "cold_overhead": 2.69034763531842, - "cold_solve_ms": 2.502537001419114, - "cold_total_ms": 2.8457970001909416, + "build_ms": 0.35166699990440975, + "cold_overhead": 2.1597680450466337, + "cold_solve_ms": 2.627457000016875, + "cold_total_ms": 2.979123999921285, "n": 50, - "scipy_ms": 1.0577804008789826, - "warm_overhead": 1.3059045146807384, - "warm_solve_ms": 1.3813602010486647, - "warm_total_ms": 1.3813602010486647 + "scipy_ms": 1.3793722000627895, + "warm_overhead": 1.0561845453054601, + "warm_solve_ms": 1.4568715999303095, + "warm_total_ms": 1.4568715999303095 }, { - "build_ms": 2.207367000664817, - "cold_overhead": 4.825542619460967, - "cold_solve_ms": 4.877707000559894, - "cold_total_ms": 7.0850740012247115, + "build_ms": 1.4129090000096767, + "cold_overhead": 1.5384907550060574, + "cold_solve_ms": 9.00126199985607, + "cold_total_ms": 10.414170999865746, "n": 100, - "scipy_ms": 1.4682440007163677, - "warm_overhead": 1.215488841472715, - "warm_solve_ms": 1.784634199430002, - "warm_total_ms": 1.784634199430002 + "scipy_ms": 6.769082599930698, + "warm_overhead": 0.36921803849845786, + "warm_solve_ms": 2.4992673999804538, + "warm_total_ms": 2.4992673999804538 }, { - "build_ms": 0.9081530006369576, - "cold_overhead": 4.364352366893895, - "cold_solve_ms": 10.057105999294436, - "cold_total_ms": 10.965258999931393, + "build_ms": 2.1713350001846266, + "cold_overhead": 5.750413027043192, + "cold_solve_ms": 16.93884699989212, + "cold_total_ms": 19.110182000076747, "n": 200, - "scipy_ms": 2.512459599529393, - "warm_overhead": 1.3177663034185723, - "warm_solve_ms": 3.310834598960355, - "warm_total_ms": 3.310834598960355 + "scipy_ms": 3.3232711998607556, + "warm_overhead": 2.207551282708422, + "warm_solve_ms": 7.336291600040568, + "warm_total_ms": 7.336291600040568 }, { - "build_ms": 3.3278769988100976, - "cold_overhead": 4.835364174134148, - "cold_solve_ms": 34.82242199970642, - "cold_total_ms": 38.15029899851652, + "build_ms": 2.098379000017303, + "cold_overhead": 4.804207659501215, + "cold_solve_ms": 41.66016900035174, + "cold_total_ms": 43.758548000369046, "n": 500, - "scipy_ms": 7.889850200444926, - "warm_overhead": 1.1001823581781829, - "warm_solve_ms": 8.680273999198107, - "warm_total_ms": 8.680273999198107 + "scipy_ms": 9.108379799909017, + "warm_overhead": 1.079420162099998, + "warm_solve_ms": 9.831768800086138, + "warm_total_ms": 9.831768800086138 } ] }, @@ -579,103 +579,103 @@ "label": "MILP (VectorVariable)", "results": [ { - "build_ms": 0.11342200014041737, - "cold_overhead": 1.1751891455861652, - "cold_solve_ms": 1.0656769991328474, - "cold_total_ms": 1.1790989992732648, + "build_ms": 0.05193700008021551, + "cold_overhead": 1.323208891815784, + "cold_solve_ms": 1.2127850000069884, + "cold_total_ms": 1.264722000087204, "n": 10, - "scipy_ms": 1.003326999489218, - "warm_overhead": 0.9674415220778275, - "warm_solve_ms": 0.9706601995276287, - "warm_total_ms": 0.9706601995276287 + "scipy_ms": 0.9557991998917714, + "warm_overhead": 1.0211843658863, + "warm_solve_ms": 0.9760471998561115, + "warm_total_ms": 0.9760471998561115 }, { - "build_ms": 0.16660999972373247, - "cold_overhead": 1.1871918475053154, - "cold_solve_ms": 1.0797040013130754, - "cold_total_ms": 1.246314001036808, + "build_ms": 0.03874200001519057, + "cold_overhead": 1.201705232204657, + "cold_solve_ms": 1.2714949998553493, + "cold_total_ms": 1.3102369998705399, "n": 25, - "scipy_ms": 1.0497999996005092, - "warm_overhead": 1.0163364446826544, - "warm_solve_ms": 1.0669499992218334, - "warm_total_ms": 1.0669499992218334 + "scipy_ms": 1.0903147999670182, + "warm_overhead": 1.0041144081740991, + "warm_solve_ms": 1.0948008000923437, + "warm_total_ms": 1.0948008000923437 }, { - "build_ms": 0.20800800120923668, - "cold_overhead": 1.4566705000975524, - "cold_solve_ms": 1.331152001512237, - "cold_total_ms": 1.5391600027214736, + "build_ms": 0.03621699988798355, + "cold_overhead": 1.4118825457729465, + "cold_solve_ms": 1.5062739998938923, + "cold_total_ms": 1.5424909997818759, "n": 50, - "scipy_ms": 1.0566288001427893, - "warm_overhead": 1.2941061236838522, - "warm_solve_ms": 1.3673898007255048, - "warm_total_ms": 1.3673898007255048 + "scipy_ms": 1.0925066000709194, + "warm_overhead": 1.2746938094029638, + "warm_solve_ms": 1.3926113998422807, + "warm_total_ms": 1.3926113998422807 }, { - "build_ms": 0.32505700073670596, - "cold_overhead": 1.451575594142868, - "cold_solve_ms": 1.7858999999589287, - "cold_total_ms": 2.1109570006956346, + "build_ms": 0.035897000088880304, + "cold_overhead": 1.2351406868284072, + "cold_solve_ms": 2.039999999851716, + "cold_total_ms": 2.0758969999405963, "n": 100, - "scipy_ms": 1.4542521996190771, - "warm_overhead": 1.2799012436930013, - "warm_solve_ms": 1.8612991989357397, - "warm_total_ms": 1.8612991989357397 + "scipy_ms": 1.6806968000310007, + "warm_overhead": 1.2886237423473903, + "warm_solve_ms": 2.1657858002072317, + "warm_total_ms": 2.1657858002072317 }, { - "build_ms": 0.5490439980349038, - "cold_overhead": 1.3673208274083901, - "cold_solve_ms": 2.898244998505106, - "cold_total_ms": 3.44728899654001, + "build_ms": 0.034202999813714996, + "cold_overhead": 1.1485477488827183, + "cold_solve_ms": 3.325801999835676, + "cold_total_ms": 3.360004999649391, "n": 200, - "scipy_ms": 2.521199799957685, - "warm_overhead": 1.1874288581875057, - "warm_solve_ms": 2.993745399726322, - "warm_total_ms": 2.993745399726322 + "scipy_ms": 2.925437799967767, + "warm_overhead": 1.1965024858882178, + "warm_solve_ms": 3.5002935999727924, + "warm_total_ms": 3.5002935999727924 }, { - "build_ms": 1.1771960016631056, - "cold_overhead": 1.9537429622573275, - "cold_solve_ms": 14.222654997865902, - "cold_total_ms": 15.399850999529008, + "build_ms": 0.0386219999199966, + "cold_overhead": 1.4160318043297646, + "cold_solve_ms": 11.71721399987291, + "cold_total_ms": 11.755835999792907, "n": 500, - "scipy_ms": 7.882229800452478, - "warm_overhead": 1.2936619533279687, - "warm_solve_ms": 10.196940800233278, - "warm_total_ms": 10.196940800233278 + "scipy_ms": 8.301957599996967, + "warm_overhead": 1.118445895210725, + "warm_solve_ms": 9.28529039993009, + "warm_total_ms": 9.28529039993009 }, { - "build_ms": 4.252782000548905, - "cold_overhead": 1.3152853856338191, - "cold_solve_ms": 28.930814998602727, - "cold_total_ms": 33.18359699915163, + "build_ms": 0.04864000038651284, + "cold_overhead": 1.2384173915472392, + "cold_solve_ms": 31.82126399997287, + "cold_total_ms": 31.869904000359384, "n": 1000, - "scipy_ms": 25.229199200111907, - "warm_overhead": 1.1026000539842433, - "warm_solve_ms": 27.81771640002262, - "warm_total_ms": 27.81771640002262 + "scipy_ms": 25.7343801999923, + "warm_overhead": 1.0638225512862673, + "warm_solve_ms": 27.376814000126615, + "warm_total_ms": 27.376814000126615 }, { - "build_ms": 4.538343997410266, - "cold_overhead": 1.0930986857200522, - "cold_solve_ms": 90.94023099896731, - "cold_total_ms": 95.47857499637757, + "build_ms": 0.05470199994306313, + "cold_overhead": 1.2444617403277025, + "cold_solve_ms": 116.84635699975843, + "cold_total_ms": 116.90105899970149, "n": 2000, - "scipy_ms": 87.34671100028208, - "warm_overhead": 1.0280106162114089, - "warm_solve_ms": 89.79334619943984, - "warm_total_ms": 89.79334619943984 + "scipy_ms": 93.93704539997998, + "warm_overhead": 0.9931613752893035, + "warm_solve_ms": 93.29464520005786, + "warm_total_ms": 93.29464520005786 }, { - "build_ms": 14.063347000046633, - "cold_overhead": 1.0031123576531944, - "cold_solve_ms": 556.2133999992511, - "cold_total_ms": 570.2767469992978, + "build_ms": 0.059531999795581214, + "cold_overhead": 1.0858302442506533, + "cold_solve_ms": 600.8082369999102, + "cold_total_ms": 600.8677689997057, "n": 5000, - "scipy_ms": 568.5073488013586, - "warm_overhead": 1.179959153413669, - "warm_solve_ms": 670.8154500011005, - "warm_total_ms": 670.8154500011005 + "scipy_ms": 553.3717376000823, + "warm_overhead": 1.1252722039991174, + "warm_solve_ms": 622.6938348000658, + "warm_total_ms": 622.6938348000658 } ] } @@ -685,70 +685,70 @@ "label": "NLP (Loop)", "results": [ { - "build_ms": 0.23259400040842593, - "cold_overhead": 26.924193665611664, - "cold_solve_ms": 22.282316000200808, - "cold_total_ms": 22.514910000609234, + "build_ms": 0.15791500027262373, + "cold_overhead": 38.49230073945708, + "cold_solve_ms": 6.344420000004902, + "cold_total_ms": 6.502335000277526, "n": 10, - "scipy_ms": 0.83623339960468, - "warm_overhead": 1.526948339291722, - "warm_solve_ms": 1.2768852007866371, - "warm_total_ms": 1.2768852007866371 + "scipy_ms": 0.16892560006454005, + "warm_overhead": 3.1039191205615624, + "warm_solve_ms": 0.5243313999926613, + "warm_total_ms": 0.5243313999926613 }, { - "build_ms": 0.8333039986609947, - "cold_overhead": 140.23467756679662, - "cold_solve_ms": 23.261622001882643, - "cold_total_ms": 24.094926000543637, + "build_ms": 0.1992219999920053, + "cold_overhead": 77.49120905592983, + "cold_solve_ms": 11.969796000357746, + "cold_total_ms": 12.169018000349752, "n": 25, - "scipy_ms": 0.17181860021082684, - "warm_overhead": 2.940754958360747, - "warm_solve_ms": 0.5052764005085919, - "warm_total_ms": 0.5052764005085919 + "scipy_ms": 0.15703740009485045, + "warm_overhead": 2.1097814899675753, + "warm_solve_ms": 0.3313145999527478, + "warm_total_ms": 0.3313145999527478 }, { - "build_ms": 0.3780549996008631, - "cold_overhead": 256.11622214696143, - "cold_solve_ms": 42.20331599935889, - "cold_total_ms": 42.581370998959756, + "build_ms": 0.2698550001696276, + "cold_overhead": 234.97263543578293, + "cold_solve_ms": 37.958266000259755, + "cold_total_ms": 38.22812100042938, "n": 50, - "scipy_ms": 0.16625800053589046, - "warm_overhead": 3.638935862690727, - "warm_solve_ms": 0.6050022006093059, - "warm_total_ms": 0.6050022006093059 + "scipy_ms": 0.16269179996015737, + "warm_overhead": 2.5617173095467862, + "warm_solve_ms": 0.41677040007925825, + "warm_total_ms": 0.41677040007925825 }, { - "build_ms": 0.510792997374665, - "cold_overhead": 560.5602547995003, - "cold_solve_ms": 171.82303099980345, - "cold_total_ms": 172.33382399717811, + "build_ms": 0.5198409999138676, + "cold_overhead": 954.0348277972433, + "cold_solve_ms": 158.86942199995246, + "cold_total_ms": 159.38926299986633, "n": 100, - "scipy_ms": 0.30743140014237724, - "warm_overhead": 5.524515057550911, - "warm_solve_ms": 1.6984093992505223, - "warm_total_ms": 1.6984093992505223 + "scipy_ms": 0.16706859996702406, + "warm_overhead": 3.4220182612377377, + "warm_solve_ms": 0.5717117999665788, + "warm_total_ms": 0.5717117999665788 }, { - "build_ms": 1.5647879990865476, - "cold_overhead": 2373.6715815740263, - "cold_solve_ms": 788.4804659988731, - "cold_total_ms": 790.0452539979597, + "build_ms": 0.8625609998489381, + "cold_overhead": 2966.4781269623604, + "cold_solve_ms": 575.9430059997612, + "cold_total_ms": 576.8055669996102, "n": 200, - "scipy_ms": 0.33283680022577755, - "warm_overhead": 5.649991223581403, - "warm_solve_ms": 1.88052500016056, - "warm_total_ms": 1.88052500016056 + "scipy_ms": 0.19444120007392485, + "warm_overhead": 4.5544977073718425, + "warm_solve_ms": 0.8855819999553205, + "warm_total_ms": 0.8855819999553205 }, { - "build_ms": 2.4829899994074367, - "cold_overhead": 5156.3964225459185, - "cold_solve_ms": 5282.304603999364, - "cold_total_ms": 5284.787593998772, + "build_ms": 1.962965999609878, + "cold_overhead": 17200.780423986074, + "cold_solve_ms": 3938.2163659997786, + "cold_total_ms": 3940.1793319993885, "n": 500, - "scipy_ms": 1.024899398908019, - "warm_overhead": 7.431381273147058, - "warm_solve_ms": 7.616418199904729, - "warm_total_ms": 7.616418199904729 + "scipy_ms": 0.2290698000251723, + "warm_overhead": 9.129024427292121, + "warm_solve_ms": 2.0911837999847194, + "warm_total_ms": 2.0911837999847194 } ] }, @@ -756,103 +756,103 @@ "label": "NLP (VectorVariable)", "results": [ { - "build_ms": 0.1335380002274178, - "cold_overhead": 7.034893721008192, - "cold_solve_ms": 1.6221139994740952, - "cold_total_ms": 1.755651999701513, + "build_ms": 0.0365179998880194, + "cold_overhead": 3.709947511586566, + "cold_solve_ms": 0.5798229999527393, + "cold_total_ms": 0.6163409998407587, "n": 10, - "scipy_ms": 0.24956340057542548, - "warm_overhead": 2.187587599443851, - "warm_solve_ms": 0.5459418003738392, - "warm_total_ms": 0.5459418003738392 + "scipy_ms": 0.16613199995845207, + "warm_overhead": 1.3057761305484836, + "warm_solve_ms": 0.2169312000660284, + "warm_total_ms": 0.2169312000660284 }, { - "build_ms": 0.18729899966274388, - "cold_overhead": 8.070642423746817, - "cold_solve_ms": 3.2761719994596206, - "cold_total_ms": 3.4634709991223644, + "build_ms": 0.02652000011948985, + "cold_overhead": 3.5003577667226278, + "cold_solve_ms": 0.5487750004249392, + "cold_total_ms": 0.5752950005444291, "n": 25, - "scipy_ms": 0.4291443998226896, - "warm_overhead": 5.572431566330814, - "warm_solve_ms": 2.3913778000860475, - "warm_total_ms": 2.3913778000860475 + "scipy_ms": 0.1643532001253334, + "warm_overhead": 1.3191030051595662, + "warm_solve_ms": 0.21679880019291886, + "warm_total_ms": 0.21679880019291886 }, { - "build_ms": 0.2650540009199176, - "cold_overhead": 3.026941579482231, - "cold_solve_ms": 1.2355649996607099, - "cold_total_ms": 1.5006190005806275, + "build_ms": 0.021810999896842986, + "cold_overhead": 1.968910777419289, + "cold_solve_ms": 0.5514609997590014, + "cold_total_ms": 0.5732719996558444, "n": 50, - "scipy_ms": 0.4957541998010129, - "warm_overhead": 2.0825227513121036, - "warm_solve_ms": 1.0324194001441356, - "warm_total_ms": 1.0324194001441356 + "scipy_ms": 0.2911619999395043, + "warm_overhead": 0.8642755582338756, + "warm_solve_ms": 0.25164420003420673, + "warm_total_ms": 0.25164420003420673 }, { - "build_ms": 0.4631140000128653, - "cold_overhead": 4.760779426991482, - "cold_solve_ms": 1.846163002483081, - "cold_total_ms": 2.3092770024959464, + "build_ms": 0.021240000023681205, + "cold_overhead": 2.401388843762901, + "cold_solve_ms": 0.534547999905044, + "cold_total_ms": 0.5557879999287252, "n": 100, - "scipy_ms": 0.4850628007261548, - "warm_overhead": 4.305729478136866, - "warm_solve_ms": 2.088549199834233, - "warm_total_ms": 2.088549199834233 + "scipy_ms": 0.23144439992393018, + "warm_overhead": 1.5658836426239178, + "warm_solve_ms": 0.3624150000177906, + "warm_total_ms": 0.3624150000177906 }, { - "build_ms": 0.943560000450816, - "cold_overhead": 5.758374810789764, - "cold_solve_ms": 5.066017001809087, - "cold_total_ms": 6.009577002259903, + "build_ms": 0.01986699999179109, + "cold_overhead": 2.419829543258986, + "cold_solve_ms": 0.5734209998990991, + "cold_total_ms": 0.5932879998908902, "n": 200, - "scipy_ms": 1.0436238000693265, - "warm_overhead": 2.2984234359876172, - "warm_solve_ms": 2.3986894004337955, - "warm_total_ms": 2.3986894004337955 + "scipy_ms": 0.2451776000270911, + "warm_overhead": 1.6255587785955072, + "warm_solve_ms": 0.398550600039016, + "warm_total_ms": 0.398550600039016 }, { - "build_ms": 4.8641109970049, - "cold_overhead": 53.78946185114727, - "cold_solve_ms": 6.9507119987974875, - "cold_total_ms": 11.814822995802388, + "build_ms": 0.02139999969585915, + "cold_overhead": 1.2737089423498595, + "cold_solve_ms": 0.5200309997235308, + "cold_total_ms": 0.54143099941939, "n": 500, - "scipy_ms": 0.21964939951431006, - "warm_overhead": 8.802874053183821, - "warm_solve_ms": 1.933545999781927, - "warm_total_ms": 1.933545999781927 + "scipy_ms": 0.4250822000358312, + "warm_overhead": 0.8639030284240333, + "warm_solve_ms": 0.3672297999401053, + "warm_total_ms": 0.3672297999401053 }, { - "build_ms": 2.3693789989920333, - "cold_overhead": 36.298616780646455, - "cold_solve_ms": 6.737806001183344, - "cold_total_ms": 9.107185000175377, + "build_ms": 0.020077000044693705, + "cold_overhead": 1.6025563029421745, + "cold_solve_ms": 0.6885249999868392, + "cold_total_ms": 0.708602000031533, "n": 1000, - "scipy_ms": 0.2508961995772552, - "warm_overhead": 17.360370574977356, - "warm_solve_ms": 4.3556510005146265, - "warm_total_ms": 4.3556510005146265 + "scipy_ms": 0.442169800044212, + "warm_overhead": 1.2579434416037945, + "warm_solve_ms": 0.5562246000408777, + "warm_total_ms": 0.5562246000408777 }, { - "build_ms": 4.656695000448963, - "cold_overhead": 63.99828017713911, - "cold_solve_ms": 15.803311998752179, - "cold_total_ms": 20.46000699920114, + "build_ms": 0.02161999964300776, + "cold_overhead": 1.5557205158583143, + "cold_solve_ms": 0.8041020000746357, + "cold_total_ms": 0.8257219997176435, "n": 2000, - "scipy_ms": 0.3196962003130466, - "warm_overhead": 22.64619971147447, - "warm_solve_ms": 7.239903999288799, - "warm_total_ms": 7.239903999288799 + "scipy_ms": 0.5307650000759168, + "warm_overhead": 1.2422590034283034, + "warm_solve_ms": 0.6593476000489318, + "warm_total_ms": 0.6593476000489318 }, { - "build_ms": 19.125397000607336, - "cold_overhead": 81.94926695989462, - "cold_solve_ms": 47.659322000981774, - "cold_total_ms": 66.78471900158911, + "build_ms": 0.01873500013971352, + "cold_overhead": 1.4302990865719043, + "cold_solve_ms": 1.0973700000249664, + "cold_total_ms": 1.11610500016468, "n": 5000, - "scipy_ms": 0.8149519999278709, - "warm_overhead": 42.13192875480498, - "warm_solve_ms": 34.33549959954689, - "warm_total_ms": 34.33549959954689 + "scipy_ms": 0.7803297999998904, + "warm_overhead": 1.2186439631434345, + "warm_solve_ms": 0.9509442000307899, + "warm_total_ms": 0.9509442000307899 } ] } diff --git a/benchmarks/results/cqp_scaling_comparison.png b/benchmarks/results/cqp_scaling_comparison.png index 40a0908..af940fb 100644 Binary files a/benchmarks/results/cqp_scaling_comparison.png and b/benchmarks/results/cqp_scaling_comparison.png differ diff --git a/benchmarks/results/lp_cache_benefit.png b/benchmarks/results/lp_cache_benefit.png index 4119340..550001a 100644 Binary files a/benchmarks/results/lp_cache_benefit.png and b/benchmarks/results/lp_cache_benefit.png differ diff --git a/benchmarks/results/lp_scaling_comparison.png b/benchmarks/results/lp_scaling_comparison.png index fb7b5c7..e7505ce 100644 Binary files a/benchmarks/results/lp_scaling_comparison.png and b/benchmarks/results/lp_scaling_comparison.png differ diff --git a/benchmarks/results/milp_scaling_comparison.png b/benchmarks/results/milp_scaling_comparison.png index 3ee030f..aad5bfa 100644 Binary files a/benchmarks/results/milp_scaling_comparison.png and b/benchmarks/results/milp_scaling_comparison.png differ diff --git a/benchmarks/results/multi_problem_scaling.png b/benchmarks/results/multi_problem_scaling.png index b7f0c95..c540bc8 100644 Binary files a/benchmarks/results/multi_problem_scaling.png and b/benchmarks/results/multi_problem_scaling.png differ diff --git a/benchmarks/results/nlp_quadratic_scaling.png b/benchmarks/results/nlp_quadratic_scaling.png index 03fb244..60384a3 100644 Binary files a/benchmarks/results/nlp_quadratic_scaling.png and b/benchmarks/results/nlp_quadratic_scaling.png differ diff --git a/benchmarks/results/nlp_scaling_comparison.png b/benchmarks/results/nlp_scaling_comparison.png index ce91e22..19c06f7 100644 Binary files a/benchmarks/results/nlp_scaling_comparison.png and b/benchmarks/results/nlp_scaling_comparison.png differ diff --git a/benchmarks/results/overhead_breakdown.png b/benchmarks/results/overhead_breakdown.png index 75dbecc..8dcb452 100644 Binary files a/benchmarks/results/overhead_breakdown.png and b/benchmarks/results/overhead_breakdown.png differ diff --git a/benchmarks/results/scipy_lp_scaling.png b/benchmarks/results/scipy_lp_scaling.png index eac448e..b7a3970 100644 Binary files a/benchmarks/results/scipy_lp_scaling.png and b/benchmarks/results/scipy_lp_scaling.png differ diff --git a/benchmarks/run_benchmarks.py b/benchmarks/run_benchmarks.py index e5b5a43..96a6dc0 100644 --- a/benchmarks/run_benchmarks.py +++ b/benchmarks/run_benchmarks.py @@ -1085,7 +1085,7 @@ def _find(results: ScalingResults | None, n: int) -> BenchmarkResult | None: ax.axhline(y=2.0, color="gray", linestyle=":", alpha=0.5) ax.set_ylabel("Overhead vs SciPy (×)", fontsize=11) - ax.set_yscale("log") + # ax.set_yscale("log") ax.set_title( "Optyx End-to-End Overhead by Problem Type\n(VectorVariable, lower is better)", fontsize=12, diff --git a/docs/assets/benchmarks/benchmark_metadata.json b/docs/assets/benchmarks/benchmark_metadata.json index d69b4a7..84f3a2b 100644 --- a/docs/assets/benchmarks/benchmark_metadata.json +++ b/docs/assets/benchmarks/benchmark_metadata.json @@ -9,5 +9,5 @@ "python_implementation": "CPython", "python_version": "3.12.1", "scipy_version": "1.16.3", - "timestamp_utc": "2026-04-21T10:17:46.022288+00:00" + "timestamp_utc": "2026-05-14T14:42:29.831269+00:00" } diff --git a/docs/assets/benchmarks/benchmark_output.txt b/docs/assets/benchmarks/benchmark_output.txt index fe8eea3..2656f32 100644 --- a/docs/assets/benchmarks/benchmark_output.txt +++ b/docs/assets/benchmarks/benchmark_output.txt @@ -23,23 +23,23 @@ Measures: Build (vars + problem + constraints) + Solve Compared against: SciPy linprog (no build phase) --- Loop-based Variable (n ≤ 500, slow cold solve) --- - Loop n= 10: Build= 0.5ms, Cold= 20.5ms, Warm= 2.0ms | SciPy= 1.8ms | Cold overhead= 11.5x, Warm overhead= 1.1x - Loop n= 25: Build= 1.4ms, Cold= 19.6ms, Warm= 2.5ms | SciPy= 2.2ms | Cold overhead= 9.6x, Warm overhead= 1.1x - Loop n= 50: Build= 4.9ms, Cold= 51.3ms, Warm= 3.7ms | SciPy= 1.7ms | Cold overhead= 32.2x, Warm overhead= 2.1x - Loop n= 100: Build= 10.6ms, Cold= 108.2ms, Warm= 3.3ms | SciPy= 3.5ms | Cold overhead= 34.3x, Warm overhead= 1.0x - Loop n= 200: Build= 63.9ms, Cold= 477.3ms, Warm= 8.7ms | SciPy= 9.0ms | Cold overhead= 60.3x, Warm overhead= 1.0x - Loop n= 500: Build= 478.8ms, Cold= 4352.1ms, Warm= 55.8ms | SciPy= 52.7ms | Cold overhead= 91.7x, Warm overhead= 1.1x + Loop n= 10: Build= 0.5ms, Cold= 10.5ms, Warm= 2.1ms | SciPy= 1.8ms | Cold overhead= 6.0x, Warm overhead= 1.1x + Loop n= 25: Build= 1.6ms, Cold= 14.0ms, Warm= 2.2ms | SciPy= 2.0ms | Cold overhead= 7.7x, Warm overhead= 1.1x + Loop n= 50: Build= 10.8ms, Cold= 57.4ms, Warm= 3.3ms | SciPy= 3.1ms | Cold overhead= 22.3x, Warm overhead= 1.1x + Loop n= 100: Build= 23.2ms, Cold= 234.0ms, Warm= 3.4ms | SciPy= 3.0ms | Cold overhead= 85.3x, Warm overhead= 1.1x + Loop n= 200: Build= 68.1ms, Cold= 444.5ms, Warm= 9.0ms | SciPy= 8.7ms | Cold overhead= 58.6x, Warm overhead= 1.0x + Loop n= 500: Build= 543.4ms, Cold= 4419.6ms, Warm= 56.3ms | SciPy= 53.9ms | Cold overhead= 92.0x, Warm overhead= 1.0x --- VectorVariable (n ≤ 5,000) --- - Vec n= 10: Build= 0.2ms, Cold= 1.7ms, Warm= 1.4ms | SciPy= 1.1ms | Cold overhead= 1.7x, Warm overhead= 1.3x - Vec n= 25: Build= 0.2ms, Cold= 1.6ms, Warm= 1.5ms | SciPy= 1.2ms | Cold overhead= 1.5x, Warm overhead= 1.3x - Vec n= 50: Build= 0.3ms, Cold= 2.4ms, Warm= 2.1ms | SciPy= 2.5ms | Cold overhead= 1.1x, Warm overhead= 0.8x - Vec n= 100: Build= 1.0ms, Cold= 6.3ms, Warm= 8.7ms | SciPy= 5.1ms | Cold overhead= 1.4x, Warm overhead= 1.7x - Vec n= 200: Build= 1.8ms, Cold= 27.0ms, Warm= 9.5ms | SciPy= 7.9ms | Cold overhead= 3.7x, Warm overhead= 1.2x - Vec n= 500: Build= 2.4ms, Cold= 63.1ms, Warm= 56.0ms | SciPy= 54.1ms | Cold overhead= 1.2x, Warm overhead= 1.0x - Vec n= 1000: Build= 6.6ms, Cold= 252.4ms, Warm= 282.5ms | SciPy= 257.7ms | Cold overhead= 1.0x, Warm overhead= 1.1x - Vec n= 2000: Build= 10.2ms, Cold= 1171.9ms, Warm= 1134.5ms | SciPy= 1113.6ms | Cold overhead= 1.1x, Warm overhead= 1.0x - Vec n= 5000: Build= 25.0ms, Cold= 10200.0ms, Warm= 9194.9ms | SciPy= 9068.0ms | Cold overhead= 1.1x, Warm overhead= 1.0x + Vec n= 10: Build= 0.1ms, Cold= 1.9ms, Warm= 1.5ms | SciPy= 1.1ms | Cold overhead= 1.8x, Warm overhead= 1.4x + Vec n= 25: Build= 0.1ms, Cold= 1.8ms, Warm= 1.6ms | SciPy= 1.2ms | Cold overhead= 1.6x, Warm overhead= 1.3x + Vec n= 50: Build= 0.2ms, Cold= 2.6ms, Warm= 2.4ms | SciPy= 1.7ms | Cold overhead= 1.6x, Warm overhead= 1.4x + Vec n= 100: Build= 0.3ms, Cold= 4.9ms, Warm= 5.2ms | SciPy= 3.5ms | Cold overhead= 1.5x, Warm overhead= 1.5x + Vec n= 200: Build= 0.5ms, Cold= 13.1ms, Warm= 11.5ms | SciPy= 23.0ms | Cold overhead= 0.6x, Warm overhead= 0.5x + Vec n= 500: Build= 2.7ms, Cold= 67.8ms, Warm= 67.5ms | SciPy= 53.6ms | Cold overhead= 1.3x, Warm overhead= 1.3x + Vec n= 1000: Build= 2.7ms, Cold= 254.4ms, Warm= 304.3ms | SciPy= 219.2ms | Cold overhead= 1.2x, Warm overhead= 1.4x + Vec n= 2000: Build= 5.0ms, Cold= 1004.3ms, Warm= 1041.6ms | SciPy= 953.8ms | Cold overhead= 1.1x, Warm overhead= 1.1x + Vec n= 5000: Build= 12.2ms, Cold= 9593.3ms, Warm= 8834.7ms | SciPy= 8650.5ms | Cold overhead= 1.1x, Warm overhead= 1.0x Saved: /workspaces/optix/benchmarks/results/lp_scaling_comparison.png @@ -51,23 +51,23 @@ Objective: min Σx²ᵢ - Σxᵢ (optimal at x* = 0.5) Measures: Build + Solve (includes gradient compilation) --- Loop-based Variable (n ≤ 500, slow cold solve) --- - Loop n= 10: Build= 0.2ms, Cold= 22.3ms, Warm= 1.3ms | SciPy= 0.8ms | Cold overhead= 26.9x, Warm overhead= 1.5x - Loop n= 25: Build= 0.8ms, Cold= 23.3ms, Warm= 0.5ms | SciPy= 0.2ms | Cold overhead=140.2x, Warm overhead= 2.9x - Loop n= 50: Build= 0.4ms, Cold= 42.2ms, Warm= 0.6ms | SciPy= 0.2ms | Cold overhead=256.1x, Warm overhead= 3.6x - Loop n= 100: Build= 0.5ms, Cold= 171.8ms, Warm= 1.7ms | SciPy= 0.3ms | Cold overhead=560.6x, Warm overhead= 5.5x - Loop n= 200: Build= 1.6ms, Cold= 788.5ms, Warm= 1.9ms | SciPy= 0.3ms | Cold overhead=2373.7x, Warm overhead= 5.6x - Loop n= 500: Build= 2.5ms, Cold= 5282.3ms, Warm= 7.6ms | SciPy= 1.0ms | Cold overhead=5156.4x, Warm overhead= 7.4x + Loop n= 10: Build= 0.2ms, Cold= 6.3ms, Warm= 0.5ms | SciPy= 0.2ms | Cold overhead= 38.5x, Warm overhead= 3.1x + Loop n= 25: Build= 0.2ms, Cold= 12.0ms, Warm= 0.3ms | SciPy= 0.2ms | Cold overhead= 77.5x, Warm overhead= 2.1x + Loop n= 50: Build= 0.3ms, Cold= 38.0ms, Warm= 0.4ms | SciPy= 0.2ms | Cold overhead=235.0x, Warm overhead= 2.6x + Loop n= 100: Build= 0.5ms, Cold= 158.9ms, Warm= 0.6ms | SciPy= 0.2ms | Cold overhead=954.0x, Warm overhead= 3.4x + Loop n= 200: Build= 0.9ms, Cold= 575.9ms, Warm= 0.9ms | SciPy= 0.2ms | Cold overhead=2966.5x, Warm overhead= 4.6x + Loop n= 500: Build= 2.0ms, Cold= 3938.2ms, Warm= 2.1ms | SciPy= 0.2ms | Cold overhead=17200.8x, Warm overhead= 9.1x --- VectorVariable with x.dot(x) - x.sum() (n ≤ 5,000) --- - Vec n= 10: Build= 0.1ms, Cold= 1.6ms, Warm= 0.5ms | SciPy= 0.2ms | Cold overhead= 7.0x, Warm overhead= 2.2x - Vec n= 25: Build= 0.2ms, Cold= 3.3ms, Warm= 2.4ms | SciPy= 0.4ms | Cold overhead= 8.1x, Warm overhead= 5.6x - Vec n= 50: Build= 0.3ms, Cold= 1.2ms, Warm= 1.0ms | SciPy= 0.5ms | Cold overhead= 3.0x, Warm overhead= 2.1x - Vec n= 100: Build= 0.5ms, Cold= 1.8ms, Warm= 2.1ms | SciPy= 0.5ms | Cold overhead= 4.8x, Warm overhead= 4.3x - Vec n= 200: Build= 0.9ms, Cold= 5.1ms, Warm= 2.4ms | SciPy= 1.0ms | Cold overhead= 5.8x, Warm overhead= 2.3x - Vec n= 500: Build= 4.9ms, Cold= 7.0ms, Warm= 1.9ms | SciPy= 0.2ms | Cold overhead= 53.8x, Warm overhead= 8.8x - Vec n= 1000: Build= 2.4ms, Cold= 6.7ms, Warm= 4.4ms | SciPy= 0.3ms | Cold overhead= 36.3x, Warm overhead= 17.4x - Vec n= 2000: Build= 4.7ms, Cold= 15.8ms, Warm= 7.2ms | SciPy= 0.3ms | Cold overhead= 64.0x, Warm overhead= 22.6x - Vec n= 5000: Build= 19.1ms, Cold= 47.7ms, Warm= 34.3ms | SciPy= 0.8ms | Cold overhead= 81.9x, Warm overhead= 42.1x + Vec n= 10: Build= 0.0ms, Cold= 0.6ms, Warm= 0.2ms | SciPy= 0.2ms | Cold overhead= 3.7x, Warm overhead= 1.3x + Vec n= 25: Build= 0.0ms, Cold= 0.5ms, Warm= 0.2ms | SciPy= 0.2ms | Cold overhead= 3.5x, Warm overhead= 1.3x + Vec n= 50: Build= 0.0ms, Cold= 0.6ms, Warm= 0.3ms | SciPy= 0.3ms | Cold overhead= 2.0x, Warm overhead= 0.9x + Vec n= 100: Build= 0.0ms, Cold= 0.5ms, Warm= 0.4ms | SciPy= 0.2ms | Cold overhead= 2.4x, Warm overhead= 1.6x + Vec n= 200: Build= 0.0ms, Cold= 0.6ms, Warm= 0.4ms | SciPy= 0.2ms | Cold overhead= 2.4x, Warm overhead= 1.6x + Vec n= 500: Build= 0.0ms, Cold= 0.5ms, Warm= 0.4ms | SciPy= 0.4ms | Cold overhead= 1.3x, Warm overhead= 0.9x + Vec n= 1000: Build= 0.0ms, Cold= 0.7ms, Warm= 0.6ms | SciPy= 0.4ms | Cold overhead= 1.6x, Warm overhead= 1.3x + Vec n= 2000: Build= 0.0ms, Cold= 0.8ms, Warm= 0.7ms | SciPy= 0.5ms | Cold overhead= 1.6x, Warm overhead= 1.2x + Vec n= 5000: Build= 0.0ms, Cold= 1.1ms, Warm= 1.0ms | SciPy= 0.8ms | Cold overhead= 1.4x, Warm overhead= 1.2x Saved: /workspaces/optix/benchmarks/results/nlp_scaling_comparison.png @@ -79,23 +79,23 @@ Objective: min Σx²ᵢ s.t. Σxᵢ ≥ 1, xᵢ ≥ 0 Measures: Build + Solve (includes gradient/Jacobian compilation) --- Loop-based Variable (n ≤ 500, slow cold solve) --- - Loop n= 10: Build= 0.2ms, Cold= 10.9ms, Warm= 0.6ms | SciPy= 0.4ms | Cold overhead= 31.1x, Warm overhead= 1.8x - Loop n= 25: Build= 0.3ms, Cold= 15.9ms, Warm= 1.2ms | SciPy= 0.8ms | Cold overhead= 19.0x, Warm overhead= 1.4x - Loop n= 50: Build= 1.2ms, Cold= 71.7ms, Warm= 1.7ms | SciPy= 1.2ms | Cold overhead= 61.6x, Warm overhead= 1.4x - Loop n= 100: Build= 1.2ms, Cold= 221.5ms, Warm= 1.8ms | SciPy= 0.9ms | Cold overhead=255.6x, Warm overhead= 2.1x - Loop n= 200: Build= 2.0ms, Cold= 762.4ms, Warm= 3.8ms | SciPy= 1.9ms | Cold overhead=396.1x, Warm overhead= 2.0x - Loop n= 500: Build= 5.5ms, Cold= 5982.5ms, Warm= 34.8ms | SciPy= 13.0ms | Cold overhead=461.5x, Warm overhead= 2.7x + Loop n= 10: Build= 0.2ms, Cold= 1.8ms, Warm= 0.4ms | SciPy= 0.3ms | Cold overhead= 7.2x, Warm overhead= 1.4x + Loop n= 25: Build= 0.2ms, Cold= 5.1ms, Warm= 1.4ms | SciPy= 0.5ms | Cold overhead= 11.3x, Warm overhead= 3.0x + Loop n= 50: Build= 0.3ms, Cold= 14.1ms, Warm= 1.0ms | SciPy= 0.6ms | Cold overhead= 25.7x, Warm overhead= 1.7x + Loop n= 100: Build= 1.8ms, Cold= 71.1ms, Warm= 3.0ms | SciPy= 1.7ms | Cold overhead= 41.8x, Warm overhead= 1.7x + Loop n= 200: Build= 3.4ms, Cold= 321.8ms, Warm= 5.4ms | SciPy= 1.9ms | Cold overhead=169.4x, Warm overhead= 2.8x + Loop n= 500: Build= 3.8ms, Cold= 749.0ms, Warm= 31.9ms | SciPy= 14.3ms | Cold overhead= 52.5x, Warm overhead= 2.2x --- VectorVariable with x.dot(x), x.sum() (n ≤ 5000) --- - Vec n= 10: Build= 0.1ms, Cold= 0.8ms, Warm= 0.4ms | SciPy= 0.3ms | Cold overhead= 3.0x, Warm overhead= 1.3x - Vec n= 25: Build= 0.2ms, Cold= 0.9ms, Warm= 1.0ms | SciPy= 0.5ms | Cold overhead= 2.4x, Warm overhead= 2.3x - Vec n= 50: Build= 0.2ms, Cold= 1.1ms, Warm= 0.7ms | SciPy= 0.9ms | Cold overhead= 1.4x, Warm overhead= 0.8x - Vec n= 100: Build= 0.4ms, Cold= 1.5ms, Warm= 1.2ms | SciPy= 1.1ms | Cold overhead= 1.7x, Warm overhead= 1.1x - Vec n= 200: Build= 0.7ms, Cold= 2.9ms, Warm= 2.4ms | SciPy= 1.9ms | Cold overhead= 1.9x, Warm overhead= 1.2x - Vec n= 500: Build= 2.5ms, Cold= 18.6ms, Warm= 12.7ms | SciPy= 12.9ms | Cold overhead= 1.6x, Warm overhead= 1.0x - Vec n= 1000: Build= 13.5ms, Cold= 80.2ms, Warm= 91.4ms | SciPy= 69.9ms | Cold overhead= 1.3x, Warm overhead= 1.3x - Vec n= 2000: Build= 11.2ms, Cold= 471.5ms, Warm= 599.2ms | SciPy= 577.7ms | Cold overhead= 0.8x, Warm overhead= 1.0x - Vec n= 5000: Build= 49.9ms, Cold= 7601.3ms, Warm= 6999.7ms | SciPy= 6989.3ms | Cold overhead= 1.1x, Warm overhead= 1.0x + Vec n= 10: Build= 0.1ms, Cold= 1.4ms, Warm= 0.5ms | SciPy= 0.3ms | Cold overhead= 4.2x, Warm overhead= 1.4x + Vec n= 25: Build= 0.0ms, Cold= 2.6ms, Warm= 1.2ms | SciPy= 0.5ms | Cold overhead= 5.5x, Warm overhead= 2.5x + Vec n= 50: Build= 0.0ms, Cold= 1.2ms, Warm= 0.9ms | SciPy= 1.1ms | Cold overhead= 1.1x, Warm overhead= 0.8x + Vec n= 100: Build= 0.0ms, Cold= 2.1ms, Warm= 1.4ms | SciPy= 0.9ms | Cold overhead= 2.4x, Warm overhead= 1.6x + Vec n= 200: Build= 0.0ms, Cold= 3.4ms, Warm= 2.8ms | SciPy= 2.0ms | Cold overhead= 1.8x, Warm overhead= 1.4x + Vec n= 500: Build= 0.0ms, Cold= 18.5ms, Warm= 13.1ms | SciPy= 12.8ms | Cold overhead= 1.5x, Warm overhead= 1.0x + Vec n= 1000: Build= 0.1ms, Cold= 168.4ms, Warm= 104.4ms | SciPy= 88.8ms | Cold overhead= 1.9x, Warm overhead= 1.2x + Vec n= 2000: Build= 0.1ms, Cold= 466.6ms, Warm= 506.4ms | SciPy= 515.8ms | Cold overhead= 0.9x, Warm overhead= 1.0x + Vec n= 5000: Build= 0.1ms, Cold= 6665.1ms, Warm= 6287.6ms | SciPy= 6314.0ms | Cold overhead= 1.1x, Warm overhead= 1.0x Saved: /workspaces/optix/benchmarks/results/cqp_scaling_comparison.png @@ -108,23 +108,23 @@ Compared against: SciPy milp (no build phase) Problem: Single-constraint binary knapsack (sum(x) <= n//2) --- Loop-based Variable (n ≤ 500, slow cold solve) --- - Loop n= 10: Build= 0.2ms, Cold= 14.2ms, Warm= 1.0ms | SciPy= 0.8ms | Cold overhead= 17.8x, Warm overhead= 1.2x - Loop n= 25: Build= 0.2ms, Cold= 1.7ms, Warm= 1.2ms | SciPy= 0.9ms | Cold overhead= 2.2x, Warm overhead= 1.4x - Loop n= 50: Build= 0.3ms, Cold= 2.5ms, Warm= 1.4ms | SciPy= 1.1ms | Cold overhead= 2.7x, Warm overhead= 1.3x - Loop n= 100: Build= 2.2ms, Cold= 4.9ms, Warm= 1.8ms | SciPy= 1.5ms | Cold overhead= 4.8x, Warm overhead= 1.2x - Loop n= 200: Build= 0.9ms, Cold= 10.1ms, Warm= 3.3ms | SciPy= 2.5ms | Cold overhead= 4.4x, Warm overhead= 1.3x - Loop n= 500: Build= 3.3ms, Cold= 34.8ms, Warm= 8.7ms | SciPy= 7.9ms | Cold overhead= 4.8x, Warm overhead= 1.1x + Loop n= 10: Build= 0.2ms, Cold= 4.6ms, Warm= 2.5ms | SciPy= 1.7ms | Cold overhead= 2.8x, Warm overhead= 1.5x + Loop n= 25: Build= 0.3ms, Cold= 9.0ms, Warm= 2.1ms | SciPy= 1.5ms | Cold overhead= 6.3x, Warm overhead= 1.4x + Loop n= 50: Build= 0.4ms, Cold= 2.6ms, Warm= 1.5ms | SciPy= 1.4ms | Cold overhead= 2.2x, Warm overhead= 1.1x + Loop n= 100: Build= 1.4ms, Cold= 9.0ms, Warm= 2.5ms | SciPy= 6.8ms | Cold overhead= 1.5x, Warm overhead= 0.4x + Loop n= 200: Build= 2.2ms, Cold= 16.9ms, Warm= 7.3ms | SciPy= 3.3ms | Cold overhead= 5.8x, Warm overhead= 2.2x + Loop n= 500: Build= 2.1ms, Cold= 41.7ms, Warm= 9.8ms | SciPy= 9.1ms | Cold overhead= 4.8x, Warm overhead= 1.1x --- VectorVariable (n ≤ 5000) --- - Vec n= 10: Build= 0.1ms, Cold= 1.1ms, Warm= 1.0ms | SciPy= 1.0ms | Cold overhead= 1.2x, Warm overhead= 1.0x - Vec n= 25: Build= 0.2ms, Cold= 1.1ms, Warm= 1.1ms | SciPy= 1.0ms | Cold overhead= 1.2x, Warm overhead= 1.0x - Vec n= 50: Build= 0.2ms, Cold= 1.3ms, Warm= 1.4ms | SciPy= 1.1ms | Cold overhead= 1.5x, Warm overhead= 1.3x - Vec n= 100: Build= 0.3ms, Cold= 1.8ms, Warm= 1.9ms | SciPy= 1.5ms | Cold overhead= 1.5x, Warm overhead= 1.3x - Vec n= 200: Build= 0.5ms, Cold= 2.9ms, Warm= 3.0ms | SciPy= 2.5ms | Cold overhead= 1.4x, Warm overhead= 1.2x - Vec n= 500: Build= 1.2ms, Cold= 14.2ms, Warm= 10.2ms | SciPy= 7.9ms | Cold overhead= 2.0x, Warm overhead= 1.3x - Vec n= 1000: Build= 4.3ms, Cold= 28.9ms, Warm= 27.8ms | SciPy= 25.2ms | Cold overhead= 1.3x, Warm overhead= 1.1x - Vec n= 2000: Build= 4.5ms, Cold= 90.9ms, Warm= 89.8ms | SciPy= 87.3ms | Cold overhead= 1.1x, Warm overhead= 1.0x - Vec n= 5000: Build= 14.1ms, Cold= 556.2ms, Warm= 670.8ms | SciPy= 568.5ms | Cold overhead= 1.0x, Warm overhead= 1.2x + Vec n= 10: Build= 0.1ms, Cold= 1.2ms, Warm= 1.0ms | SciPy= 1.0ms | Cold overhead= 1.3x, Warm overhead= 1.0x + Vec n= 25: Build= 0.0ms, Cold= 1.3ms, Warm= 1.1ms | SciPy= 1.1ms | Cold overhead= 1.2x, Warm overhead= 1.0x + Vec n= 50: Build= 0.0ms, Cold= 1.5ms, Warm= 1.4ms | SciPy= 1.1ms | Cold overhead= 1.4x, Warm overhead= 1.3x + Vec n= 100: Build= 0.0ms, Cold= 2.0ms, Warm= 2.2ms | SciPy= 1.7ms | Cold overhead= 1.2x, Warm overhead= 1.3x + Vec n= 200: Build= 0.0ms, Cold= 3.3ms, Warm= 3.5ms | SciPy= 2.9ms | Cold overhead= 1.1x, Warm overhead= 1.2x + Vec n= 500: Build= 0.0ms, Cold= 11.7ms, Warm= 9.3ms | SciPy= 8.3ms | Cold overhead= 1.4x, Warm overhead= 1.1x + Vec n= 1000: Build= 0.0ms, Cold= 31.8ms, Warm= 27.4ms | SciPy= 25.7ms | Cold overhead= 1.2x, Warm overhead= 1.1x + Vec n= 2000: Build= 0.1ms, Cold= 116.8ms, Warm= 93.3ms | SciPy= 93.9ms | Cold overhead= 1.2x, Warm overhead= 1.0x + Vec n= 5000: Build= 0.1ms, Cold= 600.8ms, Warm= 622.7ms | SciPy= 553.4ms | Cold overhead= 1.1x, Warm overhead= 1.1x Saved: /workspaces/optix/benchmarks/results/milp_scaling_comparison.png Structured benchmark results saved to: /workspaces/optix/benchmarks/results/benchmark_results.json @@ -132,14 +132,14 @@ Structured benchmark results saved to: /workspaces/optix/benchmarks/results/benc ================================================================================ OVERHEAD SUMMARY BY PROBLEM TYPE ================================================================================ -LP n=50: Cold=1.1x, Warm=0.8x +LP n=50: Cold=1.6x, Warm=1.4x LP n=5000: Cold=1.1x, Warm=1.0x -NLP n=50: Cold=3.0x, Warm=2.1x -NLP n=5000: Cold=81.9x, Warm=42.1x -CQP n=50: Cold=1.4x, Warm=0.8x +NLP n=50: Cold=2.0x, Warm=0.9x +NLP n=5000: Cold=1.4x, Warm=1.2x +CQP n=50: Cold=1.1x, Warm=0.8x CQP n=5000: Cold=1.1x, Warm=1.0x -MILP n=50: Cold=1.5x, Warm=1.3x -MILP n=5000: Cold=1.0x, Warm=1.2x +MILP n=50: Cold=1.4x, Warm=1.3x +MILP n=5000: Cold=1.1x, Warm=1.1x Saved: /workspaces/optix/benchmarks/results/overhead_breakdown.png diff --git a/docs/assets/benchmarks/benchmark_results.json b/docs/assets/benchmarks/benchmark_results.json index b962dea..54a8b0f 100644 --- a/docs/assets/benchmarks/benchmark_results.json +++ b/docs/assets/benchmarks/benchmark_results.json @@ -14,138 +14,138 @@ "benchmark_suite": "run_benchmarks", "overhead_summary": [ { - "cold_overhead": 1.0501582337223818, + "cold_overhead": 1.64644252317742, "problem_type": "LP", "size": 50, - "warm_overhead": 0.812985045888142 + "warm_overhead": 1.3869239483696907 }, { - "cold_overhead": 1.1275902207716624, + "cold_overhead": 1.110394944099998, "problem_type": "LP", "size": 5000, - "warm_overhead": 1.0140021159852615 + "warm_overhead": 1.0212860660463705 }, { - "cold_overhead": 3.026941579482231, + "cold_overhead": 1.968910777419289, "problem_type": "NLP", "size": 50, - "warm_overhead": 2.0825227513121036 + "warm_overhead": 0.8642755582338756 }, { - "cold_overhead": 81.94926695989462, + "cold_overhead": 1.4302990865719043, "problem_type": "NLP", "size": 5000, - "warm_overhead": 42.13192875480498 + "warm_overhead": 1.2186439631434345 }, { - "cold_overhead": 1.4299693678646468, + "cold_overhead": 1.0824487449877866, "problem_type": "CQP", "size": 50, - "warm_overhead": 0.788050478137063 + "warm_overhead": 0.7885143099133539 }, { - "cold_overhead": 1.0946971400391046, + "cold_overhead": 1.0556033640045543, "problem_type": "CQP", "size": 5000, - "warm_overhead": 1.001488479208934 + "warm_overhead": 0.9958139221028426 }, { - "cold_overhead": 1.4566705000975524, + "cold_overhead": 1.4118825457729465, "problem_type": "MILP", "size": 50, - "warm_overhead": 1.2941061236838522 + "warm_overhead": 1.2746938094029638 }, { - "cold_overhead": 1.0031123576531944, + "cold_overhead": 1.0858302442506533, "problem_type": "MILP", "size": 5000, - "warm_overhead": 1.179959153413669 + "warm_overhead": 1.1252722039991174 } ], "performance_summary": [ { - "cold_overhead": 1.0501582337223818, + "cold_overhead": 1.64644252317742, "note": "Near-parity with SciPy linprog", "problem_type": "LP", "size": 50, - "warm_overhead": 0.812985045888142 + "warm_overhead": 1.3869239483696907 }, { - "cold_overhead": 1.211487886529665, + "cold_overhead": 1.3155652933969881, "note": "Near-parity with SciPy linprog", "problem_type": "LP", "size": 500, - "warm_overhead": 1.0357210528392964 + "warm_overhead": 1.25864301363049 }, { - "cold_overhead": 1.1275902207716624, + "cold_overhead": 1.110394944099998, "note": "Scales to large LPs while staying near parity", "problem_type": "LP", "size": 5000, - "warm_overhead": 1.0140021159852615 + "warm_overhead": 1.0212860660463705 }, { - "cold_overhead": 3.026941579482231, + "cold_overhead": 1.968910777419289, "note": "Autodiff overhead on a trivially simple objective", "problem_type": "NLP", "size": 50, - "warm_overhead": 2.0825227513121036 + "warm_overhead": 0.8642755582338756 }, { - "cold_overhead": 53.78946185114727, + "cold_overhead": 1.2737089423498595, "note": "Autodiff overhead on a trivially simple objective", "problem_type": "NLP", "size": 500, - "warm_overhead": 8.802874053183821 + "warm_overhead": 0.8639030284240333 }, { - "cold_overhead": 81.94926695989462, + "cold_overhead": 1.4302990865719043, "note": "Simple quadratic; SciPy converges almost instantly", "problem_type": "NLP", "size": 5000, - "warm_overhead": 42.13192875480498 + "warm_overhead": 1.2186439631434345 }, { - "cold_overhead": 1.4299693678646468, + "cold_overhead": 1.0824487449877866, "note": "O(1) Jacobian compilation for vectorized constraints", "problem_type": "CQP", "size": 50, - "warm_overhead": 0.788050478137063 + "warm_overhead": 0.7885143099133539 }, { - "cold_overhead": 1.6394185969433936, + "cold_overhead": 1.4506026832376255, "note": "O(1) Jacobian compilation for vectorized constraints", "problem_type": "CQP", "size": 500, - "warm_overhead": 0.9872978508888796 + "warm_overhead": 1.0230633467973789 }, { - "cold_overhead": 1.0946971400391046, + "cold_overhead": 1.0556033640045543, "note": "Exact Jacobians keep constrained solves near parity", "problem_type": "CQP", "size": 5000, - "warm_overhead": 1.001488479208934 + "warm_overhead": 0.9958139221028426 }, { - "cold_overhead": 1.4566705000975524, + "cold_overhead": 1.4118825457729465, "note": "Near-parity with SciPy milp", "problem_type": "MILP", "size": 50, - "warm_overhead": 1.2941061236838522 + "warm_overhead": 1.2746938094029638 }, { - "cold_overhead": 1.9537429622573275, + "cold_overhead": 1.4160318043297646, "note": "Near-parity with SciPy milp", "problem_type": "MILP", "size": 500, - "warm_overhead": 1.2936619533279687 + "warm_overhead": 1.118445895210725 }, { - "cold_overhead": 1.0031123576531944, + "cold_overhead": 1.0858302442506533, "note": "Scales to large binary knapsack problems", "problem_type": "MILP", "size": 5000, - "warm_overhead": 1.179959153413669 + "warm_overhead": 1.1252722039991174 } ], "scaling": { @@ -154,70 +154,70 @@ "label": "CQP (Loop)", "results": [ { - "build_ms": 0.153315002535237, - "cold_overhead": 31.11061123509847, - "cold_solve_ms": 10.939440999209182, - "cold_total_ms": 11.09275600174442, + "build_ms": 0.1677440000094066, + "cold_overhead": 7.171137486656461, + "cold_solve_ms": 1.7870879996735312, + "cold_total_ms": 1.9548319996829377, "n": 10, - "scipy_ms": 0.3565586004697252, - "warm_overhead": 1.7591885287050133, - "warm_solve_ms": 0.6272537997574545, - "warm_total_ms": 0.6272537997574545 + "scipy_ms": 0.27259720000074594, + "warm_overhead": 1.4270960961430332, + "warm_solve_ms": 0.3890223999405862, + "warm_total_ms": 0.3890223999405862 }, { - "build_ms": 0.2530210003897082, - "cold_overhead": 18.974517042517437, - "cold_solve_ms": 15.85142200201517, - "cold_total_ms": 16.104443002404878, + "build_ms": 0.22640299994236557, + "cold_overhead": 11.285625998365749, + "cold_solve_ms": 5.10596499998428, + "cold_total_ms": 5.332367999926646, "n": 25, - "scipy_ms": 0.8487406012136489, - "warm_overhead": 1.3679175940132715, - "warm_solve_ms": 1.1610072011535522, - "warm_total_ms": 1.1610072011535522 + "scipy_ms": 0.4724920000626298, + "warm_overhead": 2.982929657656677, + "warm_solve_ms": 1.409410399992339, + "warm_total_ms": 1.409410399992339 }, { - "build_ms": 1.158421000582166, - "cold_overhead": 61.60524408065892, - "cold_solve_ms": 71.6935749987897, - "cold_total_ms": 72.85199599937187, + "build_ms": 0.3268699997533986, + "cold_overhead": 25.717735609804425, + "cold_solve_ms": 14.113870000073803, + "cold_total_ms": 14.440739999827201, "n": 50, - "scipy_ms": 1.1825615998532157, - "warm_overhead": 1.4221486635349372, - "warm_solve_ms": 1.681778398778988, - "warm_total_ms": 1.681778398778988 + "scipy_ms": 0.5615089998173062, + "warm_overhead": 1.7315056398723592, + "warm_solve_ms": 0.9722560000227531, + "warm_total_ms": 0.9722560000227531 }, { - "build_ms": 1.2048469980072696, - "cold_overhead": 255.63438117091056, - "cold_solve_ms": 221.54135299933841, - "cold_total_ms": 222.74619999734568, + "build_ms": 1.7901940000228933, + "cold_overhead": 41.82529263517236, + "cold_solve_ms": 71.06601100031185, + "cold_total_ms": 72.85620500033474, "n": 100, - "scipy_ms": 0.8713467999768909, - "warm_overhead": 2.100260194994894, - "warm_solve_ms": 1.8300550000276417, - "warm_total_ms": 1.8300550000276417 + "scipy_ms": 1.7419173999769555, + "warm_overhead": 1.7235244335309914, + "warm_solve_ms": 3.0022372000530595, + "warm_total_ms": 3.0022372000530595 }, { - "build_ms": 1.9555170001694933, - "cold_overhead": 396.1053364293281, - "cold_solve_ms": 762.428201000148, - "cold_total_ms": 764.3837180003175, + "build_ms": 3.3577020003576763, + "cold_overhead": 169.35686470317455, + "cold_solve_ms": 321.7535549997592, + "cold_total_ms": 325.1112570001169, "n": 200, - "scipy_ms": 1.9297485989227425, - "warm_overhead": 1.9573368272172225, - "warm_solve_ms": 3.7771679999423213, - "warm_total_ms": 3.7771679999423213 + "scipy_ms": 1.919681599974865, + "warm_overhead": 2.820367606833846, + "warm_solve_ms": 5.414207800004078, + "warm_total_ms": 5.414207800004078 }, { - "build_ms": 5.547715998545755, - "cold_overhead": 461.5214241891787, - "cold_solve_ms": 5982.5169229989115, - "cold_total_ms": 5988.064638997457, + "build_ms": 3.770271999655961, + "cold_overhead": 52.54366337163847, + "cold_solve_ms": 748.9853399997628, + "cold_total_ms": 752.7556119994188, "n": 500, - "scipy_ms": 12.974618999578524, - "warm_overhead": 2.6842881013962274, - "warm_solve_ms": 34.82761540071806, - "warm_total_ms": 34.82761540071806 + "scipy_ms": 14.326287199946819, + "warm_overhead": 2.2301639464705927, + "warm_solve_ms": 31.949969200104533, + "warm_total_ms": 31.949969200104533 } ] }, @@ -225,103 +225,103 @@ "label": "CQP (VectorVariable)", "results": [ { - "build_ms": 0.11850099690491334, - "cold_overhead": 3.027583458815483, - "cold_solve_ms": 0.8402160019613802, - "cold_total_ms": 0.9587169988662936, + "build_ms": 0.05360000022847089, + "cold_overhead": 4.225804309378625, + "cold_solve_ms": 1.396598000155791, + "cold_total_ms": 1.4501980003842618, "n": 10, - "scipy_ms": 0.316660799580859, - "warm_overhead": 1.3346085183142544, - "warm_solve_ms": 0.4226182005368173, - "warm_total_ms": 0.4226182005368173 + "scipy_ms": 0.3431768000154989, + "warm_overhead": 1.3967004760079618, + "warm_solve_ms": 0.4793151999365364, + "warm_total_ms": 0.4793151999365364 }, { - "build_ms": 0.1562409997859504, - "cold_overhead": 2.391429352767669, - "cold_solve_ms": 0.9384200020576827, - "cold_total_ms": 1.0946610018436331, + "build_ms": 0.035926999771618284, + "cold_overhead": 5.477939154587546, + "cold_solve_ms": 2.5541499999235384, + "cold_total_ms": 2.5900769996951567, "n": 25, - "scipy_ms": 0.45774339960189536, - "warm_overhead": 2.260624187010286, - "warm_solve_ms": 1.034785800584359, - "warm_total_ms": 1.034785800584359 + "scipy_ms": 0.47281959996325895, + "warm_overhead": 2.45678013369972, + "warm_solve_ms": 1.1616138000135834, + "warm_total_ms": 1.1616138000135834 }, { - "build_ms": 0.21687400294467807, - "cold_overhead": 1.4299693678646468, - "cold_solve_ms": 1.0550480001256801, - "cold_total_ms": 1.2719220030703582, + "build_ms": 0.026870000056078425, + "cold_overhead": 1.0824487449877866, + "cold_solve_ms": 1.2036980001539632, + "cold_total_ms": 1.2305680002100416, "n": 50, - "scipy_ms": 0.8894749997125473, - "warm_overhead": 0.788050478137063, - "warm_solve_ms": 0.7009511988144368, - "warm_total_ms": 0.7009511988144368 + "scipy_ms": 1.136837199828733, + "warm_overhead": 0.7885143099133539, + "warm_solve_ms": 0.896412400106783, + "warm_total_ms": 0.896412400106783 }, { - "build_ms": 0.3956479995395057, - "cold_overhead": 1.6844031244504607, - "cold_solve_ms": 1.463579999835929, - "cold_total_ms": 1.8592279993754346, + "build_ms": 0.034714999856078066, + "cold_overhead": 2.3781670145922615, + "cold_solve_ms": 2.096144999995886, + "cold_total_ms": 2.130859999851964, "n": 100, - "scipy_ms": 1.1037904005206656, - "warm_overhead": 1.1312225582223807, - "warm_solve_ms": 1.2486326006182935, - "warm_total_ms": 1.2486326006182935 + "scipy_ms": 0.8960094000030949, + "warm_overhead": 1.5813365350724404, + "warm_solve_ms": 1.4168923999932304, + "warm_total_ms": 1.4168923999932304 }, { - "build_ms": 0.7324860016524326, - "cold_overhead": 1.907498936382662, - "cold_solve_ms": 2.9472659989551175, - "cold_total_ms": 3.67975200060755, + "build_ms": 0.03521600001477054, + "cold_overhead": 1.7596890137533931, + "cold_solve_ms": 3.4471980002308555, + "cold_total_ms": 3.482414000245626, "n": 200, - "scipy_ms": 1.9290977994387504, - "warm_overhead": 1.237126703114857, - "warm_solve_ms": 2.3865384006057866, - "warm_total_ms": 2.3865384006057866 + "scipy_ms": 1.978994000091916, + "warm_overhead": 1.409494925083776, + "warm_solve_ms": 2.7893819999007974, + "warm_total_ms": 2.7893819999007974 }, { - "build_ms": 2.5035090002347715, - "cold_overhead": 1.6394185969433936, - "cold_solve_ms": 18.565805999969598, - "cold_total_ms": 21.06931500020437, + "build_ms": 0.039523999930679565, + "cold_overhead": 1.4506026832376255, + "cold_solve_ms": 18.47236100002192, + "cold_total_ms": 18.5118849999526, "n": 500, - "scipy_ms": 12.851699400926009, - "warm_overhead": 0.9872978508888796, - "warm_solve_ms": 12.68845519880415, - "warm_total_ms": 12.68845519880415 + "scipy_ms": 12.761512999986735, + "warm_overhead": 1.0230633467973789, + "warm_solve_ms": 13.055836199964688, + "warm_total_ms": 13.055836199964688 }, { - "build_ms": 13.506178998795804, - "cold_overhead": 1.340633127996738, - "cold_solve_ms": 80.1798009997583, - "cold_total_ms": 93.6859799985541, + "build_ms": 0.05229800035522203, + "cold_overhead": 1.89680755607629, + "cold_solve_ms": 168.3559900002365, + "cold_total_ms": 168.4082880005917, "n": 1000, - "scipy_ms": 69.88189240000793, - "warm_overhead": 1.3084826449294356, - "warm_solve_ms": 91.43924340023659, - "warm_total_ms": 91.43924340023659 + "scipy_ms": 88.78512080000291, + "warm_overhead": 1.1755433935272264, + "warm_solve_ms": 104.37076219996015, + "warm_total_ms": 104.37076219996015 }, { - "build_ms": 11.169469999003923, - "cold_overhead": 0.8354444607531919, - "cold_solve_ms": 471.47300299911876, - "cold_total_ms": 482.6424729981227, + "build_ms": 0.05462199987960048, + "cold_overhead": 0.9046876394019586, + "cold_solve_ms": 466.58226199997443, + "cold_total_ms": 466.63688399985404, "n": 2000, - "scipy_ms": 577.7074308003648, - "warm_overhead": 1.0372864849082362, - "warm_solve_ms": 599.2481102002785, - "warm_total_ms": 599.2481102002785 + "scipy_ms": 515.7988941999065, + "warm_overhead": 0.9817081849032054, + "warm_solve_ms": 506.3639962000707, + "warm_total_ms": 506.3639962000707 }, { - "build_ms": 49.87247100143577, - "cold_overhead": 1.0946971400391046, - "cold_solve_ms": 7601.275322998845, - "cold_total_ms": 7651.147794000281, + "build_ms": 0.055483000323874876, + "cold_overhead": 1.0556033640045543, + "cold_solve_ms": 6665.058946000045, + "cold_total_ms": 6665.114429000369, "n": 5000, - "scipy_ms": 6989.282710400585, - "warm_overhead": 1.001488479208934, - "warm_solve_ms": 6999.686112400377, - "warm_total_ms": 6999.686112400377 + "scipy_ms": 6314.032956200026, + "warm_overhead": 0.9958139221028426, + "warm_solve_ms": 6287.601922400154, + "warm_total_ms": 6287.601922400154 } ] } @@ -331,70 +331,70 @@ "label": "LP (Loop)", "results": [ { - "build_ms": 0.4565819981507957, - "cold_overhead": 11.482336147813978, - "cold_solve_ms": 20.522652001091046, - "cold_total_ms": 20.979233999241842, + "build_ms": 0.49062600010074675, + "cold_overhead": 6.0062338384742135, + "cold_solve_ms": 10.461930000019493, + "cold_total_ms": 10.95255600012024, "n": 10, - "scipy_ms": 1.8270876003953163, - "warm_overhead": 1.0815988235244256, - "warm_solve_ms": 1.9761757990636397, - "warm_total_ms": 1.9761757990636397 + "scipy_ms": 1.8235313999866776, + "warm_overhead": 1.143586998252001, + "warm_solve_ms": 2.0853667999290337, + "warm_total_ms": 2.0853667999290337 }, { - "build_ms": 1.4050399986444972, - "cold_overhead": 9.623407770861848, - "cold_solve_ms": 19.63746299952618, - "cold_total_ms": 21.042502998170676, + "build_ms": 1.58919799969226, + "cold_overhead": 7.744346214687868, + "cold_solve_ms": 13.95521499989627, + "cold_total_ms": 15.54441299958853, "n": 25, - "scipy_ms": 2.1865957984118722, - "warm_overhead": 1.1279439032452812, - "warm_solve_ms": 2.466357399680419, - "warm_total_ms": 2.466357399680419 + "scipy_ms": 2.0071949998964556, + "warm_overhead": 1.0849764970178892, + "warm_solve_ms": 2.177759399819479, + "warm_total_ms": 2.177759399819479 }, { - "build_ms": 4.869180997047806, - "cold_overhead": 32.24808044538227, - "cold_solve_ms": 51.346608001040295, - "cold_total_ms": 56.2157889980881, + "build_ms": 10.773992000395083, + "cold_overhead": 22.275961913221575, + "cold_solve_ms": 57.449309000276116, + "cold_total_ms": 68.2233010006712, "n": 50, - "scipy_ms": 1.7432289991120342, - "warm_overhead": 2.138066428056541, - "warm_solve_ms": 3.727139399416046, - "warm_total_ms": 3.727139399416046 + "scipy_ms": 3.0626421999841114, + "warm_overhead": 1.0824614772786427, + "warm_solve_ms": 3.3151922001707135, + "warm_total_ms": 3.3151922001707135 }, { - "build_ms": 10.630352997395676, - "cold_overhead": 34.299098632360746, - "cold_solve_ms": 108.15988299873425, - "cold_total_ms": 118.79023599612992, + "build_ms": 23.19088300009753, + "cold_overhead": 85.33352547430788, + "cold_solve_ms": 233.98548700015454, + "cold_total_ms": 257.17637000025206, "n": 100, - "scipy_ms": 3.4633631999895442, - "warm_overhead": 0.9625728540754156, - "warm_solve_ms": 3.3337394001137, - "warm_total_ms": 3.3337394001137 + "scipy_ms": 3.0137787999592547, + "warm_overhead": 1.1130077629128863, + "warm_solve_ms": 3.3543592000569333, + "warm_total_ms": 3.3543592000569333 }, { - "build_ms": 63.9043990013306, - "cold_overhead": 60.31374442643511, - "cold_solve_ms": 477.2999010019703, - "cold_total_ms": 541.2043000033009, + "build_ms": 68.06883299987021, + "cold_overhead": 58.58424841532407, + "cold_solve_ms": 444.50848299993595, + "cold_total_ms": 512.5773159998062, "n": 200, - "scipy_ms": 8.973150401288876, - "warm_overhead": 0.9726232159191104, - "warm_solve_ms": 8.727494400227442, - "warm_total_ms": 8.727494400227442 + "scipy_ms": 8.74940499988952, + "warm_overhead": 1.0300122122774638, + "warm_solve_ms": 9.011994000047707, + "warm_total_ms": 9.011994000047707 }, { - "build_ms": 478.78952999963076, - "cold_overhead": 91.74939438260711, - "cold_solve_ms": 4352.143078998779, - "cold_total_ms": 4830.93260899841, + "build_ms": 543.3780539997315, + "cold_overhead": 91.9948505056075, + "cold_solve_ms": 4419.6402869997655, + "cold_total_ms": 4963.018340999497, "n": 500, - "scipy_ms": 52.65356399904704, - "warm_overhead": 1.0590257100395577, - "warm_solve_ms": 55.76147800020408, - "warm_total_ms": 55.76147800020408 + "scipy_ms": 53.94887120010026, + "warm_overhead": 1.0441502546194383, + "warm_solve_ms": 56.33072760001596, + "warm_total_ms": 56.33072760001596 } ] }, @@ -402,103 +402,103 @@ "label": "LP (VectorVariable)", "results": [ { - "build_ms": 0.19218799934606068, - "cold_overhead": 1.693216922730903, - "cold_solve_ms": 1.6525819992239121, - "cold_total_ms": 1.8447699985699728, + "build_ms": 0.12826000011045835, + "cold_overhead": 1.8143836821883061, + "cold_solve_ms": 1.8964219998451881, + "cold_total_ms": 2.0246819999556465, "n": 10, - "scipy_ms": 1.0895060011534952, - "warm_overhead": 1.2623675308353952, - "warm_solve_ms": 1.375357000506483, - "warm_total_ms": 1.375357000506483 + "scipy_ms": 1.1159061999023834, + "warm_overhead": 1.365814259391491, + "warm_solve_ms": 1.524120599970047, + "warm_total_ms": 1.524120599970047 }, { - "build_ms": 0.2016159996856004, - "cold_overhead": 1.4924938118363915, - "cold_solve_ms": 1.623988999199355, - "cold_total_ms": 1.8256049988849554, + "build_ms": 0.11296100001345621, + "cold_overhead": 1.5664240835206351, + "cold_solve_ms": 1.8033380001725163, + "cold_total_ms": 1.9162990001859725, "n": 25, - "scipy_ms": 1.2231910004629754, - "warm_overhead": 1.2657148376693776, - "warm_solve_ms": 1.5482109985896386, - "warm_total_ms": 1.5482109985896386 + "scipy_ms": 1.2233589998686512, + "warm_overhead": 1.3299651207805323, + "warm_solve_ms": 1.6270248000182619, + "warm_total_ms": 1.6270248000182619 }, { - "build_ms": 0.29033099781372584, - "cold_overhead": 1.0501582337223818, - "cold_solve_ms": 2.3654819997318555, - "cold_total_ms": 2.6558129975455813, + "build_ms": 0.16892500025278423, + "cold_overhead": 1.64644252317742, + "cold_solve_ms": 2.6441889999659907, + "cold_total_ms": 2.813114000218775, "n": 50, - "scipy_ms": 2.528964600060135, - "warm_overhead": 0.812985045888142, - "warm_solve_ms": 2.0560104014293756, - "warm_total_ms": 2.0560104014293756 + "scipy_ms": 1.708601400059706, + "warm_overhead": 1.3869239483696907, + "warm_solve_ms": 2.369700199960789, + "warm_total_ms": 2.369700199960789 }, { - "build_ms": 0.9552609990350902, - "cold_overhead": 1.4109392686950275, - "cold_solve_ms": 6.260796002607094, - "cold_total_ms": 7.216057001642184, + "build_ms": 0.25551699991410715, + "cold_overhead": 1.4913857355482252, + "cold_solve_ms": 4.895423000107257, + "cold_total_ms": 5.1509400000213645, "n": 100, - "scipy_ms": 5.114363999746274, - "warm_overhead": 1.7028074262404869, - "warm_solve_ms": 8.708776999264956, - "warm_total_ms": 8.708776999264956 + "scipy_ms": 3.453794600045512, + "warm_overhead": 1.5039476290441811, + "warm_solve_ms": 5.194326199944044, + "warm_total_ms": 5.194326199944044 }, { - "build_ms": 1.8441590000293218, - "cold_overhead": 3.6727232458672763, - "cold_solve_ms": 26.99932399991667, - "cold_total_ms": 28.84348299994599, + "build_ms": 0.5499969997799781, + "cold_overhead": 0.5917800446844047, + "cold_solve_ms": 13.082112000120105, + "cold_total_ms": 13.632108999900083, "n": 200, - "scipy_ms": 7.8534321997722145, - "warm_overhead": 1.2149146968198785, - "warm_solve_ms": 9.541250199981732, - "warm_total_ms": 9.541250199981732 + "scipy_ms": 23.03576999993311, + "warm_overhead": 0.5002768303381967, + "warm_solve_ms": 11.524261999966257, + "warm_total_ms": 11.524261999966257 }, { - "build_ms": 2.4393290004809387, - "cold_overhead": 1.211487886529665, - "cold_solve_ms": 63.093166001635836, - "cold_total_ms": 65.53249500211678, + "build_ms": 2.7037199997721473, + "cold_overhead": 1.3155652933969881, + "cold_solve_ms": 67.84047700011797, + "cold_total_ms": 70.54419699989012, "n": 500, - "scipy_ms": 54.092571399814915, - "warm_overhead": 1.0357210528392964, - "warm_solve_ms": 56.024815001001116, - "warm_total_ms": 56.024815001001116 + "scipy_ms": 53.62272580005083, + "warm_overhead": 1.25864301363049, + "warm_solve_ms": 67.49186920005741, + "warm_total_ms": 67.49186920005741 }, { - "build_ms": 6.566907002707012, - "cold_overhead": 1.004822563939135, - "cold_solve_ms": 252.37838400062174, - "cold_total_ms": 258.94529100332875, + "build_ms": 2.6649579999684647, + "cold_overhead": 1.172837134151277, + "cold_solve_ms": 254.367676000129, + "cold_total_ms": 257.03263400009746, "n": 1000, - "scipy_ms": 257.7025041995512, - "warm_overhead": 1.0960677571869186, - "warm_solve_ms": 282.45940579945454, - "warm_total_ms": 282.45940579945454 + "scipy_ms": 219.1545837999911, + "warm_overhead": 1.3887398133447033, + "warm_solve_ms": 304.3486958000358, + "warm_total_ms": 304.3486958000358 }, { - "build_ms": 10.243332999380073, - "cold_overhead": 1.0615410333742983, - "cold_solve_ms": 1171.9294570029888, - "cold_total_ms": 1182.1727900023689, + "build_ms": 4.999228000087896, + "cold_overhead": 1.0581893362678234, + "cold_solve_ms": 1004.2722220000542, + "cold_total_ms": 1009.2714500001421, "n": 2000, - "scipy_ms": 1113.6383359997126, - "warm_overhead": 1.0187540921722584, - "warm_solve_ms": 1134.5236119996116, - "warm_total_ms": 1134.5236119996116 + "scipy_ms": 953.7720853999417, + "warm_overhead": 1.0921347046588157, + "warm_solve_ms": 1041.647594800088, + "warm_total_ms": 1041.647594800088 }, { - "build_ms": 24.960611001006328, - "cold_overhead": 1.1275902207716624, - "cold_solve_ms": 10199.972689999413, - "cold_total_ms": 10224.93330100042, + "build_ms": 12.240551999639138, + "cold_overhead": 1.110394944099998, + "cold_solve_ms": 9593.275089000144, + "cold_total_ms": 9605.515640999783, "n": 5000, - "scipy_ms": 9067.951382198953, - "warm_overhead": 1.0140021159852615, - "warm_solve_ms": 9194.921889201214, - "warm_total_ms": 9194.921889201214 + "scipy_ms": 8650.53978499991, + "warm_overhead": 1.0212860660463705, + "warm_solve_ms": 8834.675746200173, + "warm_total_ms": 8834.675746200173 } ] } @@ -508,70 +508,70 @@ "label": "MILP (Loop)", "results": [ { - "build_ms": 0.172080999618629, - "cold_overhead": 17.811266453769836, - "cold_solve_ms": 14.173112998832949, - "cold_total_ms": 14.345193998451577, + "build_ms": 0.1736039998831984, + "cold_overhead": 2.817920429233705, + "cold_solve_ms": 4.590033000113181, + "cold_total_ms": 4.763636999996379, "n": 10, - "scipy_ms": 0.8053999998082872, - "warm_overhead": 1.2008266692982976, - "warm_solve_ms": 0.9671457992226351, - "warm_total_ms": 0.9671457992226351 + "scipy_ms": 1.6904795999835187, + "warm_overhead": 1.4630458717368189, + "warm_solve_ms": 2.473249200011196, + "warm_total_ms": 2.473249200011196 }, { - "build_ms": 0.2327029978914652, - "cold_overhead": 2.200366322114951, - "cold_solve_ms": 1.718735002214089, - "cold_total_ms": 1.9514380001055542, + "build_ms": 0.28500200005510123, + "cold_overhead": 6.271347531956442, + "cold_solve_ms": 8.969903999968665, + "cold_total_ms": 9.254906000023766, "n": 25, - "scipy_ms": 0.8868696000718046, - "warm_overhead": 1.4006958858066936, - "warm_solve_ms": 1.2422346000676043, - "warm_total_ms": 1.2422346000676043 + "scipy_ms": 1.4757443998860253, + "warm_overhead": 1.4104500753419549, + "warm_solve_ms": 2.0814638000047125, + "warm_total_ms": 2.0814638000047125 }, { - "build_ms": 0.34325999877182767, - "cold_overhead": 2.69034763531842, - "cold_solve_ms": 2.502537001419114, - "cold_total_ms": 2.8457970001909416, + "build_ms": 0.35166699990440975, + "cold_overhead": 2.1597680450466337, + "cold_solve_ms": 2.627457000016875, + "cold_total_ms": 2.979123999921285, "n": 50, - "scipy_ms": 1.0577804008789826, - "warm_overhead": 1.3059045146807384, - "warm_solve_ms": 1.3813602010486647, - "warm_total_ms": 1.3813602010486647 + "scipy_ms": 1.3793722000627895, + "warm_overhead": 1.0561845453054601, + "warm_solve_ms": 1.4568715999303095, + "warm_total_ms": 1.4568715999303095 }, { - "build_ms": 2.207367000664817, - "cold_overhead": 4.825542619460967, - "cold_solve_ms": 4.877707000559894, - "cold_total_ms": 7.0850740012247115, + "build_ms": 1.4129090000096767, + "cold_overhead": 1.5384907550060574, + "cold_solve_ms": 9.00126199985607, + "cold_total_ms": 10.414170999865746, "n": 100, - "scipy_ms": 1.4682440007163677, - "warm_overhead": 1.215488841472715, - "warm_solve_ms": 1.784634199430002, - "warm_total_ms": 1.784634199430002 + "scipy_ms": 6.769082599930698, + "warm_overhead": 0.36921803849845786, + "warm_solve_ms": 2.4992673999804538, + "warm_total_ms": 2.4992673999804538 }, { - "build_ms": 0.9081530006369576, - "cold_overhead": 4.364352366893895, - "cold_solve_ms": 10.057105999294436, - "cold_total_ms": 10.965258999931393, + "build_ms": 2.1713350001846266, + "cold_overhead": 5.750413027043192, + "cold_solve_ms": 16.93884699989212, + "cold_total_ms": 19.110182000076747, "n": 200, - "scipy_ms": 2.512459599529393, - "warm_overhead": 1.3177663034185723, - "warm_solve_ms": 3.310834598960355, - "warm_total_ms": 3.310834598960355 + "scipy_ms": 3.3232711998607556, + "warm_overhead": 2.207551282708422, + "warm_solve_ms": 7.336291600040568, + "warm_total_ms": 7.336291600040568 }, { - "build_ms": 3.3278769988100976, - "cold_overhead": 4.835364174134148, - "cold_solve_ms": 34.82242199970642, - "cold_total_ms": 38.15029899851652, + "build_ms": 2.098379000017303, + "cold_overhead": 4.804207659501215, + "cold_solve_ms": 41.66016900035174, + "cold_total_ms": 43.758548000369046, "n": 500, - "scipy_ms": 7.889850200444926, - "warm_overhead": 1.1001823581781829, - "warm_solve_ms": 8.680273999198107, - "warm_total_ms": 8.680273999198107 + "scipy_ms": 9.108379799909017, + "warm_overhead": 1.079420162099998, + "warm_solve_ms": 9.831768800086138, + "warm_total_ms": 9.831768800086138 } ] }, @@ -579,103 +579,103 @@ "label": "MILP (VectorVariable)", "results": [ { - "build_ms": 0.11342200014041737, - "cold_overhead": 1.1751891455861652, - "cold_solve_ms": 1.0656769991328474, - "cold_total_ms": 1.1790989992732648, + "build_ms": 0.05193700008021551, + "cold_overhead": 1.323208891815784, + "cold_solve_ms": 1.2127850000069884, + "cold_total_ms": 1.264722000087204, "n": 10, - "scipy_ms": 1.003326999489218, - "warm_overhead": 0.9674415220778275, - "warm_solve_ms": 0.9706601995276287, - "warm_total_ms": 0.9706601995276287 + "scipy_ms": 0.9557991998917714, + "warm_overhead": 1.0211843658863, + "warm_solve_ms": 0.9760471998561115, + "warm_total_ms": 0.9760471998561115 }, { - "build_ms": 0.16660999972373247, - "cold_overhead": 1.1871918475053154, - "cold_solve_ms": 1.0797040013130754, - "cold_total_ms": 1.246314001036808, + "build_ms": 0.03874200001519057, + "cold_overhead": 1.201705232204657, + "cold_solve_ms": 1.2714949998553493, + "cold_total_ms": 1.3102369998705399, "n": 25, - "scipy_ms": 1.0497999996005092, - "warm_overhead": 1.0163364446826544, - "warm_solve_ms": 1.0669499992218334, - "warm_total_ms": 1.0669499992218334 + "scipy_ms": 1.0903147999670182, + "warm_overhead": 1.0041144081740991, + "warm_solve_ms": 1.0948008000923437, + "warm_total_ms": 1.0948008000923437 }, { - "build_ms": 0.20800800120923668, - "cold_overhead": 1.4566705000975524, - "cold_solve_ms": 1.331152001512237, - "cold_total_ms": 1.5391600027214736, + "build_ms": 0.03621699988798355, + "cold_overhead": 1.4118825457729465, + "cold_solve_ms": 1.5062739998938923, + "cold_total_ms": 1.5424909997818759, "n": 50, - "scipy_ms": 1.0566288001427893, - "warm_overhead": 1.2941061236838522, - "warm_solve_ms": 1.3673898007255048, - "warm_total_ms": 1.3673898007255048 + "scipy_ms": 1.0925066000709194, + "warm_overhead": 1.2746938094029638, + "warm_solve_ms": 1.3926113998422807, + "warm_total_ms": 1.3926113998422807 }, { - "build_ms": 0.32505700073670596, - "cold_overhead": 1.451575594142868, - "cold_solve_ms": 1.7858999999589287, - "cold_total_ms": 2.1109570006956346, + "build_ms": 0.035897000088880304, + "cold_overhead": 1.2351406868284072, + "cold_solve_ms": 2.039999999851716, + "cold_total_ms": 2.0758969999405963, "n": 100, - "scipy_ms": 1.4542521996190771, - "warm_overhead": 1.2799012436930013, - "warm_solve_ms": 1.8612991989357397, - "warm_total_ms": 1.8612991989357397 + "scipy_ms": 1.6806968000310007, + "warm_overhead": 1.2886237423473903, + "warm_solve_ms": 2.1657858002072317, + "warm_total_ms": 2.1657858002072317 }, { - "build_ms": 0.5490439980349038, - "cold_overhead": 1.3673208274083901, - "cold_solve_ms": 2.898244998505106, - "cold_total_ms": 3.44728899654001, + "build_ms": 0.034202999813714996, + "cold_overhead": 1.1485477488827183, + "cold_solve_ms": 3.325801999835676, + "cold_total_ms": 3.360004999649391, "n": 200, - "scipy_ms": 2.521199799957685, - "warm_overhead": 1.1874288581875057, - "warm_solve_ms": 2.993745399726322, - "warm_total_ms": 2.993745399726322 + "scipy_ms": 2.925437799967767, + "warm_overhead": 1.1965024858882178, + "warm_solve_ms": 3.5002935999727924, + "warm_total_ms": 3.5002935999727924 }, { - "build_ms": 1.1771960016631056, - "cold_overhead": 1.9537429622573275, - "cold_solve_ms": 14.222654997865902, - "cold_total_ms": 15.399850999529008, + "build_ms": 0.0386219999199966, + "cold_overhead": 1.4160318043297646, + "cold_solve_ms": 11.71721399987291, + "cold_total_ms": 11.755835999792907, "n": 500, - "scipy_ms": 7.882229800452478, - "warm_overhead": 1.2936619533279687, - "warm_solve_ms": 10.196940800233278, - "warm_total_ms": 10.196940800233278 + "scipy_ms": 8.301957599996967, + "warm_overhead": 1.118445895210725, + "warm_solve_ms": 9.28529039993009, + "warm_total_ms": 9.28529039993009 }, { - "build_ms": 4.252782000548905, - "cold_overhead": 1.3152853856338191, - "cold_solve_ms": 28.930814998602727, - "cold_total_ms": 33.18359699915163, + "build_ms": 0.04864000038651284, + "cold_overhead": 1.2384173915472392, + "cold_solve_ms": 31.82126399997287, + "cold_total_ms": 31.869904000359384, "n": 1000, - "scipy_ms": 25.229199200111907, - "warm_overhead": 1.1026000539842433, - "warm_solve_ms": 27.81771640002262, - "warm_total_ms": 27.81771640002262 + "scipy_ms": 25.7343801999923, + "warm_overhead": 1.0638225512862673, + "warm_solve_ms": 27.376814000126615, + "warm_total_ms": 27.376814000126615 }, { - "build_ms": 4.538343997410266, - "cold_overhead": 1.0930986857200522, - "cold_solve_ms": 90.94023099896731, - "cold_total_ms": 95.47857499637757, + "build_ms": 0.05470199994306313, + "cold_overhead": 1.2444617403277025, + "cold_solve_ms": 116.84635699975843, + "cold_total_ms": 116.90105899970149, "n": 2000, - "scipy_ms": 87.34671100028208, - "warm_overhead": 1.0280106162114089, - "warm_solve_ms": 89.79334619943984, - "warm_total_ms": 89.79334619943984 + "scipy_ms": 93.93704539997998, + "warm_overhead": 0.9931613752893035, + "warm_solve_ms": 93.29464520005786, + "warm_total_ms": 93.29464520005786 }, { - "build_ms": 14.063347000046633, - "cold_overhead": 1.0031123576531944, - "cold_solve_ms": 556.2133999992511, - "cold_total_ms": 570.2767469992978, + "build_ms": 0.059531999795581214, + "cold_overhead": 1.0858302442506533, + "cold_solve_ms": 600.8082369999102, + "cold_total_ms": 600.8677689997057, "n": 5000, - "scipy_ms": 568.5073488013586, - "warm_overhead": 1.179959153413669, - "warm_solve_ms": 670.8154500011005, - "warm_total_ms": 670.8154500011005 + "scipy_ms": 553.3717376000823, + "warm_overhead": 1.1252722039991174, + "warm_solve_ms": 622.6938348000658, + "warm_total_ms": 622.6938348000658 } ] } @@ -685,70 +685,70 @@ "label": "NLP (Loop)", "results": [ { - "build_ms": 0.23259400040842593, - "cold_overhead": 26.924193665611664, - "cold_solve_ms": 22.282316000200808, - "cold_total_ms": 22.514910000609234, + "build_ms": 0.15791500027262373, + "cold_overhead": 38.49230073945708, + "cold_solve_ms": 6.344420000004902, + "cold_total_ms": 6.502335000277526, "n": 10, - "scipy_ms": 0.83623339960468, - "warm_overhead": 1.526948339291722, - "warm_solve_ms": 1.2768852007866371, - "warm_total_ms": 1.2768852007866371 + "scipy_ms": 0.16892560006454005, + "warm_overhead": 3.1039191205615624, + "warm_solve_ms": 0.5243313999926613, + "warm_total_ms": 0.5243313999926613 }, { - "build_ms": 0.8333039986609947, - "cold_overhead": 140.23467756679662, - "cold_solve_ms": 23.261622001882643, - "cold_total_ms": 24.094926000543637, + "build_ms": 0.1992219999920053, + "cold_overhead": 77.49120905592983, + "cold_solve_ms": 11.969796000357746, + "cold_total_ms": 12.169018000349752, "n": 25, - "scipy_ms": 0.17181860021082684, - "warm_overhead": 2.940754958360747, - "warm_solve_ms": 0.5052764005085919, - "warm_total_ms": 0.5052764005085919 + "scipy_ms": 0.15703740009485045, + "warm_overhead": 2.1097814899675753, + "warm_solve_ms": 0.3313145999527478, + "warm_total_ms": 0.3313145999527478 }, { - "build_ms": 0.3780549996008631, - "cold_overhead": 256.11622214696143, - "cold_solve_ms": 42.20331599935889, - "cold_total_ms": 42.581370998959756, + "build_ms": 0.2698550001696276, + "cold_overhead": 234.97263543578293, + "cold_solve_ms": 37.958266000259755, + "cold_total_ms": 38.22812100042938, "n": 50, - "scipy_ms": 0.16625800053589046, - "warm_overhead": 3.638935862690727, - "warm_solve_ms": 0.6050022006093059, - "warm_total_ms": 0.6050022006093059 + "scipy_ms": 0.16269179996015737, + "warm_overhead": 2.5617173095467862, + "warm_solve_ms": 0.41677040007925825, + "warm_total_ms": 0.41677040007925825 }, { - "build_ms": 0.510792997374665, - "cold_overhead": 560.5602547995003, - "cold_solve_ms": 171.82303099980345, - "cold_total_ms": 172.33382399717811, + "build_ms": 0.5198409999138676, + "cold_overhead": 954.0348277972433, + "cold_solve_ms": 158.86942199995246, + "cold_total_ms": 159.38926299986633, "n": 100, - "scipy_ms": 0.30743140014237724, - "warm_overhead": 5.524515057550911, - "warm_solve_ms": 1.6984093992505223, - "warm_total_ms": 1.6984093992505223 + "scipy_ms": 0.16706859996702406, + "warm_overhead": 3.4220182612377377, + "warm_solve_ms": 0.5717117999665788, + "warm_total_ms": 0.5717117999665788 }, { - "build_ms": 1.5647879990865476, - "cold_overhead": 2373.6715815740263, - "cold_solve_ms": 788.4804659988731, - "cold_total_ms": 790.0452539979597, + "build_ms": 0.8625609998489381, + "cold_overhead": 2966.4781269623604, + "cold_solve_ms": 575.9430059997612, + "cold_total_ms": 576.8055669996102, "n": 200, - "scipy_ms": 0.33283680022577755, - "warm_overhead": 5.649991223581403, - "warm_solve_ms": 1.88052500016056, - "warm_total_ms": 1.88052500016056 + "scipy_ms": 0.19444120007392485, + "warm_overhead": 4.5544977073718425, + "warm_solve_ms": 0.8855819999553205, + "warm_total_ms": 0.8855819999553205 }, { - "build_ms": 2.4829899994074367, - "cold_overhead": 5156.3964225459185, - "cold_solve_ms": 5282.304603999364, - "cold_total_ms": 5284.787593998772, + "build_ms": 1.962965999609878, + "cold_overhead": 17200.780423986074, + "cold_solve_ms": 3938.2163659997786, + "cold_total_ms": 3940.1793319993885, "n": 500, - "scipy_ms": 1.024899398908019, - "warm_overhead": 7.431381273147058, - "warm_solve_ms": 7.616418199904729, - "warm_total_ms": 7.616418199904729 + "scipy_ms": 0.2290698000251723, + "warm_overhead": 9.129024427292121, + "warm_solve_ms": 2.0911837999847194, + "warm_total_ms": 2.0911837999847194 } ] }, @@ -756,103 +756,103 @@ "label": "NLP (VectorVariable)", "results": [ { - "build_ms": 0.1335380002274178, - "cold_overhead": 7.034893721008192, - "cold_solve_ms": 1.6221139994740952, - "cold_total_ms": 1.755651999701513, + "build_ms": 0.0365179998880194, + "cold_overhead": 3.709947511586566, + "cold_solve_ms": 0.5798229999527393, + "cold_total_ms": 0.6163409998407587, "n": 10, - "scipy_ms": 0.24956340057542548, - "warm_overhead": 2.187587599443851, - "warm_solve_ms": 0.5459418003738392, - "warm_total_ms": 0.5459418003738392 + "scipy_ms": 0.16613199995845207, + "warm_overhead": 1.3057761305484836, + "warm_solve_ms": 0.2169312000660284, + "warm_total_ms": 0.2169312000660284 }, { - "build_ms": 0.18729899966274388, - "cold_overhead": 8.070642423746817, - "cold_solve_ms": 3.2761719994596206, - "cold_total_ms": 3.4634709991223644, + "build_ms": 0.02652000011948985, + "cold_overhead": 3.5003577667226278, + "cold_solve_ms": 0.5487750004249392, + "cold_total_ms": 0.5752950005444291, "n": 25, - "scipy_ms": 0.4291443998226896, - "warm_overhead": 5.572431566330814, - "warm_solve_ms": 2.3913778000860475, - "warm_total_ms": 2.3913778000860475 + "scipy_ms": 0.1643532001253334, + "warm_overhead": 1.3191030051595662, + "warm_solve_ms": 0.21679880019291886, + "warm_total_ms": 0.21679880019291886 }, { - "build_ms": 0.2650540009199176, - "cold_overhead": 3.026941579482231, - "cold_solve_ms": 1.2355649996607099, - "cold_total_ms": 1.5006190005806275, + "build_ms": 0.021810999896842986, + "cold_overhead": 1.968910777419289, + "cold_solve_ms": 0.5514609997590014, + "cold_total_ms": 0.5732719996558444, "n": 50, - "scipy_ms": 0.4957541998010129, - "warm_overhead": 2.0825227513121036, - "warm_solve_ms": 1.0324194001441356, - "warm_total_ms": 1.0324194001441356 + "scipy_ms": 0.2911619999395043, + "warm_overhead": 0.8642755582338756, + "warm_solve_ms": 0.25164420003420673, + "warm_total_ms": 0.25164420003420673 }, { - "build_ms": 0.4631140000128653, - "cold_overhead": 4.760779426991482, - "cold_solve_ms": 1.846163002483081, - "cold_total_ms": 2.3092770024959464, + "build_ms": 0.021240000023681205, + "cold_overhead": 2.401388843762901, + "cold_solve_ms": 0.534547999905044, + "cold_total_ms": 0.5557879999287252, "n": 100, - "scipy_ms": 0.4850628007261548, - "warm_overhead": 4.305729478136866, - "warm_solve_ms": 2.088549199834233, - "warm_total_ms": 2.088549199834233 + "scipy_ms": 0.23144439992393018, + "warm_overhead": 1.5658836426239178, + "warm_solve_ms": 0.3624150000177906, + "warm_total_ms": 0.3624150000177906 }, { - "build_ms": 0.943560000450816, - "cold_overhead": 5.758374810789764, - "cold_solve_ms": 5.066017001809087, - "cold_total_ms": 6.009577002259903, + "build_ms": 0.01986699999179109, + "cold_overhead": 2.419829543258986, + "cold_solve_ms": 0.5734209998990991, + "cold_total_ms": 0.5932879998908902, "n": 200, - "scipy_ms": 1.0436238000693265, - "warm_overhead": 2.2984234359876172, - "warm_solve_ms": 2.3986894004337955, - "warm_total_ms": 2.3986894004337955 + "scipy_ms": 0.2451776000270911, + "warm_overhead": 1.6255587785955072, + "warm_solve_ms": 0.398550600039016, + "warm_total_ms": 0.398550600039016 }, { - "build_ms": 4.8641109970049, - "cold_overhead": 53.78946185114727, - "cold_solve_ms": 6.9507119987974875, - "cold_total_ms": 11.814822995802388, + "build_ms": 0.02139999969585915, + "cold_overhead": 1.2737089423498595, + "cold_solve_ms": 0.5200309997235308, + "cold_total_ms": 0.54143099941939, "n": 500, - "scipy_ms": 0.21964939951431006, - "warm_overhead": 8.802874053183821, - "warm_solve_ms": 1.933545999781927, - "warm_total_ms": 1.933545999781927 + "scipy_ms": 0.4250822000358312, + "warm_overhead": 0.8639030284240333, + "warm_solve_ms": 0.3672297999401053, + "warm_total_ms": 0.3672297999401053 }, { - "build_ms": 2.3693789989920333, - "cold_overhead": 36.298616780646455, - "cold_solve_ms": 6.737806001183344, - "cold_total_ms": 9.107185000175377, + "build_ms": 0.020077000044693705, + "cold_overhead": 1.6025563029421745, + "cold_solve_ms": 0.6885249999868392, + "cold_total_ms": 0.708602000031533, "n": 1000, - "scipy_ms": 0.2508961995772552, - "warm_overhead": 17.360370574977356, - "warm_solve_ms": 4.3556510005146265, - "warm_total_ms": 4.3556510005146265 + "scipy_ms": 0.442169800044212, + "warm_overhead": 1.2579434416037945, + "warm_solve_ms": 0.5562246000408777, + "warm_total_ms": 0.5562246000408777 }, { - "build_ms": 4.656695000448963, - "cold_overhead": 63.99828017713911, - "cold_solve_ms": 15.803311998752179, - "cold_total_ms": 20.46000699920114, + "build_ms": 0.02161999964300776, + "cold_overhead": 1.5557205158583143, + "cold_solve_ms": 0.8041020000746357, + "cold_total_ms": 0.8257219997176435, "n": 2000, - "scipy_ms": 0.3196962003130466, - "warm_overhead": 22.64619971147447, - "warm_solve_ms": 7.239903999288799, - "warm_total_ms": 7.239903999288799 + "scipy_ms": 0.5307650000759168, + "warm_overhead": 1.2422590034283034, + "warm_solve_ms": 0.6593476000489318, + "warm_total_ms": 0.6593476000489318 }, { - "build_ms": 19.125397000607336, - "cold_overhead": 81.94926695989462, - "cold_solve_ms": 47.659322000981774, - "cold_total_ms": 66.78471900158911, + "build_ms": 0.01873500013971352, + "cold_overhead": 1.4302990865719043, + "cold_solve_ms": 1.0973700000249664, + "cold_total_ms": 1.11610500016468, "n": 5000, - "scipy_ms": 0.8149519999278709, - "warm_overhead": 42.13192875480498, - "warm_solve_ms": 34.33549959954689, - "warm_total_ms": 34.33549959954689 + "scipy_ms": 0.7803297999998904, + "warm_overhead": 1.2186439631434345, + "warm_solve_ms": 0.9509442000307899, + "warm_total_ms": 0.9509442000307899 } ] } diff --git a/docs/assets/benchmarks/cqp_scaling_comparison.png b/docs/assets/benchmarks/cqp_scaling_comparison.png index 40a0908..af940fb 100644 Binary files a/docs/assets/benchmarks/cqp_scaling_comparison.png and b/docs/assets/benchmarks/cqp_scaling_comparison.png differ diff --git a/docs/assets/benchmarks/lp_scaling_comparison.png b/docs/assets/benchmarks/lp_scaling_comparison.png index fb7b5c7..2fa55d9 100644 Binary files a/docs/assets/benchmarks/lp_scaling_comparison.png and b/docs/assets/benchmarks/lp_scaling_comparison.png differ diff --git a/docs/assets/benchmarks/milp_scaling_comparison.png b/docs/assets/benchmarks/milp_scaling_comparison.png index 3ee030f..aad5bfa 100644 Binary files a/docs/assets/benchmarks/milp_scaling_comparison.png and b/docs/assets/benchmarks/milp_scaling_comparison.png differ diff --git a/docs/assets/benchmarks/nlp_scaling_comparison.png b/docs/assets/benchmarks/nlp_scaling_comparison.png index ce91e22..19c06f7 100644 Binary files a/docs/assets/benchmarks/nlp_scaling_comparison.png and b/docs/assets/benchmarks/nlp_scaling_comparison.png differ diff --git a/docs/assets/benchmarks/overhead_breakdown.png b/docs/assets/benchmarks/overhead_breakdown.png index 75dbecc..8dcb452 100644 Binary files a/docs/assets/benchmarks/overhead_breakdown.png and b/docs/assets/benchmarks/overhead_breakdown.png differ diff --git a/src/optyx/analysis.py b/src/optyx/analysis.py index 0714b50..04447b8 100644 --- a/src/optyx/analysis.py +++ b/src/optyx/analysis.py @@ -252,6 +252,7 @@ def _compute_degree_impl(expr: Expression) -> Optional[int]: DotProduct, LinearCombination, VectorSum, + VectorVariable, VectorPowerSum, VectorUnarySum, ElementwisePower, @@ -267,7 +268,7 @@ def _compute_degree_impl(expr: Expression) -> Optional[int]: # Vector expressions if isinstance(expr, LinearCombination): # Check if vector contains variables (degree 1) or expressions - if hasattr(expr.vector, "_variables"): + if isinstance(expr.vector, VectorVariable): return 1 # Check expressions in vector (VectorExpression case) if hasattr(expr.vector, "_expressions"): @@ -281,7 +282,7 @@ def _compute_degree_impl(expr: Expression) -> Optional[int]: return 1 # Default for unknown vector types if isinstance(expr, VectorSum): - if hasattr(expr.vector, "_variables"): + if isinstance(expr.vector, VectorVariable): return 1 if hasattr(expr.vector, "_expressions"): max_deg = 0 @@ -766,21 +767,21 @@ def extract_all_linear_coefficients( # Fast path: VectorSum over VectorVariable covering all variables # This is O(1) using numpy instead of O(n) Python loop if isinstance(expr, VectorSum) and isinstance(expr.vector, VectorVariable): - vec_n = len(expr.vector._variables) + vec_n = expr.vector.size if vec_n == n: # Check if variables are in order (common case) - first_var = expr.vector._variables[0] - first_idx = var_index.get(first_var.name, -1) + first_name = expr.vector._name_at(0) + first_idx = var_index.get(first_name, -1) if first_idx == 0: # All variables in order, return ones directly return np.ones(n, dtype=np.float64) # Fast path: LinearCombination over VectorVariable covering all variables if isinstance(expr, LinearCombination) and isinstance(expr.vector, VectorVariable): - vec_n = len(expr.vector._variables) + vec_n = expr.vector.size if vec_n == n: - first_var = expr.vector._variables[0] - first_idx = var_index.get(first_var.name, -1) + first_name = expr.vector._name_at(0) + first_idx = var_index.get(first_name, -1) if first_idx == 0: # Variables in order, return coefficients directly return np.asarray(expr.coefficients, dtype=np.float64).copy() @@ -814,10 +815,10 @@ def _try_extract_fast_binop( if isinstance(expr.left, VectorSum) and isinstance( expr.left.vector, VectorVariable ): - vec_n = len(expr.left.vector._variables) + vec_n = expr.left.vector.size if vec_n == n: - first_var = expr.left.vector._variables[0] - first_idx = var_index.get(first_var.name, -1) + first_name = expr.left.vector._name_at(0) + first_idx = var_index.get(first_name, -1) if first_idx == 0 and isinstance(expr.right, (Constant, int, float)): return np.ones(n, dtype=np.float64) @@ -825,10 +826,10 @@ def _try_extract_fast_binop( if isinstance(expr.left, LinearCombination) and isinstance( expr.left.vector, VectorVariable ): - vec_n = len(expr.left.vector._variables) + vec_n = expr.left.vector.size if vec_n == n: - first_var = expr.left.vector._variables[0] - first_idx = var_index.get(first_var.name, -1) + first_name = expr.left.vector._name_at(0) + first_idx = var_index.get(first_name, -1) if first_idx == 0 and isinstance(expr.right, (Constant, int, float)): return np.asarray(expr.left.coefficients, dtype=np.float64).copy() @@ -838,10 +839,10 @@ def _try_extract_fast_binop( if isinstance(expr.right, VectorSum) and isinstance( expr.right.vector, VectorVariable ): - vec_n = len(expr.right.vector._variables) + vec_n = expr.right.vector.size if vec_n == n: - first_var = expr.right.vector._variables[0] - first_idx = var_index.get(first_var.name, -1) + first_name = expr.right.vector._name_at(0) + first_idx = var_index.get(first_name, -1) if first_idx == 0: return np.full(n, float(expr.left.value), dtype=np.float64) @@ -849,10 +850,10 @@ def _try_extract_fast_binop( if isinstance(expr.left, VectorSum) and isinstance( expr.left.vector, VectorVariable ): - vec_n = len(expr.left.vector._variables) + vec_n = expr.left.vector.size if vec_n == n: - first_var = expr.left.vector._variables[0] - first_idx = var_index.get(first_var.name, -1) + first_name = expr.left.vector._name_at(0) + first_idx = var_index.get(first_name, -1) if first_idx == 0: return np.full(n, float(expr.right.value), dtype=np.float64) @@ -888,8 +889,8 @@ def _extract_all_coefficients_impl( # VectorSum: sum(x) - each variable has coefficient 1 * multiplier if isinstance(expr, VectorSum): - for var in expr.vector._variables: - idx = var_index.get(var.name) + for name in expr.vector._iter_variable_names(): + idx = var_index.get(name) if idx is not None: result[idx] += multiplier return @@ -897,8 +898,8 @@ def _extract_all_coefficients_impl( # LinearCombination: c @ x - coefficient is c[i] * multiplier if isinstance(expr, LinearCombination): if isinstance(expr.vector, VectorVariable): - for i, var in enumerate(expr.vector._variables): - idx = var_index.get(var.name) + for i, name in enumerate(expr.vector._iter_variable_names()): + idx = var_index.get(name) if idx is not None: result[idx] += float(expr.coefficients[i]) * multiplier else: @@ -1207,6 +1208,70 @@ def extract(self, problem: Problem) -> LPData: Raises: ValueError: If problem is not a valid LP. """ + source_vector = problem._single_vector_source() + if source_vector is not None: + n = source_vector.size + names: list[str] = [] + bounds: list[tuple[float | None, float | None]] = [] + obj_terms = np.zeros(n, dtype=np.float64) + var_index: dict[str, int] = {} + + for index, (name, bound_pair, _, obj_coeff) in enumerate( + source_vector._iter_lp_metadata() + ): + names.append(name) + bounds.append(bound_pair) + var_index[name] = index + if obj_coeff != 0.0: + obj_terms[index] = obj_coeff + + assert problem.objective is not None + c = extract_all_linear_coefficients(problem.objective, var_index, n) + if np.any(obj_terms): + c = c + obj_terms + + ub_rows: list[NDArray[np.floating]] = [] + ub_rhs: list[float] = [] + eq_rows: list[NDArray[np.floating]] = [] + eq_rhs: list[float] = [] + + for constraint in problem.constraints: + if not is_linear(constraint.expr): + raise NonLinearError( + expression=repr(constraint.expr)[:100], + context="LP constraint extraction", + suggestion="All constraints must be linear for LP solvers.", + ) + + row = extract_all_linear_coefficients(constraint.expr, var_index, n) + rhs = -extract_constant_term(constraint.expr) + + if constraint.sense == "==": + eq_rows.append(row) + eq_rhs.append(rhs) + elif constraint.sense == "<=": + ub_rows.append(row) + ub_rhs.append(rhs) + elif constraint.sense == ">=": + ub_rows.append(-row) + ub_rhs.append(-rhs) + + A_ub = np.array(ub_rows, dtype=np.float64) if ub_rows else None + b_ub = np.array(ub_rhs, dtype=np.float64) if ub_rhs else None + A_eq = np.array(eq_rows, dtype=np.float64) if eq_rows else None + b_eq = np.array(eq_rhs, dtype=np.float64) if eq_rhs else None + + return LPData( + c=c, + sense="min" if problem.sense == "minimize" else "max", + A_ub=A_ub, + b_ub=b_ub, + A_eq=A_eq, + b_eq=b_eq, + bounds=bounds, + variables=names, + ) + c, sense, variables = self.extract_objective(problem) A_ub, b_ub, A_eq, b_eq = self.extract_constraints(problem, variables) bounds = self.extract_bounds(variables) diff --git a/src/optyx/core/autodiff.py b/src/optyx/core/autodiff.py index fd027d8..218e7e6 100644 --- a/src/optyx/core/autodiff.py +++ b/src/optyx/core/autodiff.py @@ -1573,16 +1573,13 @@ def gradient_quadratic_form(expr: QuadraticForm, wrt: Variable) -> Expression: vec = expr.vector Q = expr.matrix - # Compute Q + Q' (symmetric part times 2) - Q_sym = Q + Q.T - # Find if wrt is in the vector if isinstance(vec, VectorVariable): for i, var in enumerate(vec._variables): if var.name == wrt.name: # ∂(x'Qx)/∂x_i = [(Q + Q')x]_i = Σ_j (Q + Q')_{ij} * x_j - # This is a LinearCombination with coefficients from row i of Q_sym - row_coeffs = Q_sym[i, :] + # This is a LinearCombination with coefficients from row i of Q + Q' + row_coeffs = Q[i, :] + Q[:, i] return LinearCombination(row_coeffs, vec) return Constant(0.0) else: @@ -1592,10 +1589,11 @@ def gradient_quadratic_form(expr: QuadraticForm, wrt: Variable) -> Expression: elems = list(vec._expressions) for i, elem in enumerate(elems): d_elem = gradient(elem, wrt) - # [(Q + Q')f]_i = Σ_j Q_sym[i,j] * f_j + coeffs = Q[i, :] + Q[:, i] + # [(Q + Q')f]_i = Σ_j (Q[i,j] + Q[j,i]) * f_j qf_i: Expression = Constant(0.0) for j, elem_j in enumerate(elems): - coeff = Q_sym[i, j] + coeff = coeffs[j] if coeff != 0: qf_i = _simplify_add( qf_i, _simplify_mul(Constant(coeff), elem_j) @@ -2039,17 +2037,16 @@ def compile_jacobian( compile_vector_gradient, _compile_vectorized_power_gradient, _compile_vectorized_unary_gradient, + _compile_nary_sum_gradient_fast, ) - from optyx.core.expressions import Constant + from optyx.core.expressions import Constant, NarySum from optyx.core.vectors import VectorPowerSum, VectorUnarySum + from optyx.core.optimizer import flatten_expression m = len(exprs) n = len(variables) row_fns = [] - for i in range(m): - expr = exprs[i] - # Check if ALL rows are constant, for global optimization # This restores the optimization tested by test_constant_jacobian_returns_same_object # We detect this during row construction @@ -2058,7 +2055,14 @@ def compile_jacobian( processed_rows = [] for i in range(m): - expr = exprs[i] + expr = flatten_expression(exprs[i]) + + if isinstance(expr, NarySum): + grad_fn = _compile_nary_sum_gradient_fast(expr, variables) + if grad_fn is not None: + row_fns.append(grad_fn) + processed_rows.append(None) + continue # 1. Try vector gradient pattern (linear/quadratic => O(1) compile) pattern_fn = compile_vector_gradient(expr, variables) diff --git a/src/optyx/core/compiler.py b/src/optyx/core/compiler.py index 73a7cfa..225f264 100644 --- a/src/optyx/core/compiler.py +++ b/src/optyx/core/compiler.py @@ -11,6 +11,7 @@ from __future__ import annotations +from collections.abc import Sequence from functools import lru_cache from typing import TYPE_CHECKING, Any, Callable @@ -59,9 +60,154 @@ def _sanitize_derivatives(arr: np.ndarray) -> np.ndarray: from optyx.core.vectors import VectorPowerSum, VectorUnarySum +class _ContiguousVectorCompileLayout: + """Index resolver for a single contiguous VectorVariable layout.""" + + __slots__ = ("vector_name", "vector_size", "_prefix", "_full_indices") + + def __init__(self, vector_name: str, vector_size: int) -> None: + self.vector_name = vector_name + self.vector_size = vector_size + self._prefix = f"{vector_name}[" + self._full_indices = np.arange(vector_size, dtype=np.intp) + + def _parse_name_index(self, name: str) -> int | None: + if not name.startswith(self._prefix) or not name.endswith("]"): + return None + + try: + index = int(name[len(self._prefix) : -1]) + except ValueError: + return None + + if 0 <= index < self.vector_size: + return index + return None + + def contains_name(self, name: str) -> bool: + return self._parse_name_index(name) is not None + + def index_of_name(self, name: str) -> int: + index = self._parse_name_index(name) + if index is None: + raise KeyError(name) + return index + + def matches_vector(self, vec: Any) -> bool: + return ( + getattr(vec, "name", None) == self.vector_name + and getattr(vec, "size", None) == self.vector_size + ) + + def vector_indices(self, vec: Any, *, allow_subset: bool = False) -> np.ndarray: + if self.matches_vector(vec): + return self._full_indices + + if allow_subset: + return np.array( + [ + index + for name in vec._iter_variable_names() + if (index := self._parse_name_index(name)) is not None + ], + dtype=np.intp, + ) + + return np.fromiter( + (self.index_of_name(name) for name in vec._iter_variable_names()), + dtype=np.intp, + count=vec.size, + ) + + +class ContiguousVectorVariables(Sequence[Any]): + """Vector-backed variable sequence for single-vector compile fast paths.""" + + __slots__ = ("source_vector", "_compile_layout") + + def __init__(self, source_vector: Any) -> None: + self.source_vector = source_vector + self._compile_layout = _ContiguousVectorCompileLayout( + source_vector.name, + source_vector.size, + ) + + def __len__(self) -> int: + return self.source_vector.size + + def __getitem__(self, index: int | slice) -> Any: + if isinstance(index, slice): + return [ + self.source_vector._get_variable(i) + for i in range(*index.indices(len(self))) + ] + + if index < 0: + index += len(self) + if index < 0 or index >= len(self): + raise IndexError(index) + return self.source_vector._get_variable(index) + + def __iter__(self): + for index in range(len(self)): + yield self.source_vector._get_variable(index) + + def materialize(self) -> list[Any]: + return list(self.source_vector._variables) + + +def _contiguous_compile_layout( + variables: Any, +) -> _ContiguousVectorCompileLayout | None: + if isinstance(variables, ContiguousVectorVariables): + return variables._compile_layout + return None + + +def _build_var_index_data( + variables: Any, +) -> dict[str, int] | _ContiguousVectorCompileLayout: + layout = _contiguous_compile_layout(variables) + if layout is not None: + return layout + return {var.name: i for i, var in enumerate(variables)} + + +def _lookup_var_index( + var_indices: dict[str, int] | _ContiguousVectorCompileLayout, + name: str, +) -> int: + if isinstance(var_indices, _ContiguousVectorCompileLayout): + return var_indices.index_of_name(name) + return var_indices[name] + + +def _has_var_name( + var_indices: dict[str, int] | _ContiguousVectorCompileLayout, + name: str, +) -> bool: + if isinstance(var_indices, _ContiguousVectorCompileLayout): + return var_indices.contains_name(name) + return name in var_indices + + +def _iter_index_names(vec: Any): + """Yield stable variable names for indexing without forcing materialization.""" + cache = getattr(vec, "_variable_cache", None) + if cache is not None: + for index, variable in enumerate(cache): + if variable is not None: + yield variable.name + else: + yield vec._name_at(index) + return + + yield from vec._iter_variable_names() + + def compile_expression( expr: Expression, - variables: list[Variable], + variables: Any, ) -> Callable[[NDArray[np.floating]], NDArray[np.floating] | np.floating | float]: """Compile an expression tree into a fast callable. @@ -83,6 +229,14 @@ def compile_expression( >>> f = compile_expression(expr, [x, y]) >>> f(np.array([3.0, 4.0])) # Returns 25.0 """ + layout = _contiguous_compile_layout(variables) + if layout is not None: + return _compile_contiguous_vector_cached( + expr, + layout.vector_name, + layout.vector_size, + ) + # Create mapping from variable name to array index var_indices = {var.name: i for i, var in enumerate(variables)} @@ -103,14 +257,37 @@ def _compile_cached( Uses LRU cache to avoid recompiling the same expression. Switches to iterative compilation for deep expression trees. """ + from optyx.core.optimizer import flatten_expression + var_indices = dict(var_indices_items) + optimized_expr = flatten_expression(expr) # Check tree depth - depth = _estimate_tree_depth(expr) + depth = _estimate_tree_depth(optimized_expr) + if depth >= _RECURSION_THRESHOLD: + eval_func = _build_evaluator_iterative(optimized_expr, var_indices) + else: + eval_func = _build_evaluator(optimized_expr, var_indices) + return eval_func + + +@lru_cache(maxsize=1024) +def _compile_contiguous_vector_cached( + expr: Expression, + vector_name: str, + vector_size: int, +) -> Callable[[NDArray[np.floating]], NDArray[np.floating] | np.floating | float]: + """Cached compilation for a single contiguous VectorVariable layout.""" + from optyx.core.optimizer import flatten_expression + + layout = _ContiguousVectorCompileLayout(vector_name, vector_size) + optimized_expr = flatten_expression(expr) + + depth = _estimate_tree_depth(optimized_expr) if depth >= _RECURSION_THRESHOLD: - eval_func = _build_evaluator_iterative(expr, var_indices) + eval_func = _build_evaluator_iterative(optimized_expr, layout) else: - eval_func = _build_evaluator(expr, var_indices) + eval_func = _build_evaluator(optimized_expr, layout) return eval_func @@ -145,9 +322,116 @@ def _estimate_tree_depth(expr: Expression) -> int: return depth +def _vector_indices( + vec: Any, + var_indices: dict[str, int] | _ContiguousVectorCompileLayout, + *, + allow_subset: bool = False, +) -> np.ndarray: + """Resolve VectorVariable element indices without materializing scalars.""" + if isinstance(var_indices, _ContiguousVectorCompileLayout): + return var_indices.vector_indices(vec, allow_subset=allow_subset) + + if allow_subset: + return np.array( + [ + _lookup_var_index(var_indices, name) + for name in _iter_index_names(vec) + if _has_var_name(var_indices, name) + ], + dtype=np.intp, + ) + + return np.fromiter( + (_lookup_var_index(var_indices, name) for name in _iter_index_names(vec)), + dtype=np.intp, + count=vec.size, + ) + + +def _variables_match_vector_names(variables: Any, vec: Any) -> bool: + """Check whether the variable ordering matches a VectorVariable exactly.""" + layout = _contiguous_compile_layout(variables) + if layout is not None: + return layout.matches_vector(vec) + + if len(variables) != vec.size: + return False + + for variable, name in zip(variables, _iter_index_names(vec)): + if variable.name != name: + return False + return True + + +def _try_build_nary_sum_fast_evaluator( + expr: NarySum, + var_indices: dict[str, int] | _ContiguousVectorCompileLayout, +) -> Callable[[NDArray[np.floating]], float] | None: + """Build a vectorized evaluator for common loop-built sum patterns.""" + from optyx.core.expressions import BinaryOp, Constant, UnaryOp, Variable + + terms = expr.terms + if not terms: + return lambda x: 0.0 + + if all(isinstance(term, Variable) for term in terms): + indices = np.array( + [_lookup_var_index(var_indices, term.name) for term in terms], + dtype=np.intp, + ) + return lambda x, idx=indices: float(np.sum(x[idx])) + + power_indices: list[int] = [] + power_value: float | None = None + for term in terms: + if not isinstance(term, BinaryOp) or term.op != "**": + power_indices = [] + break + if not isinstance(term.left, Variable) or not isinstance(term.right, Constant): + power_indices = [] + break + exponent = term.right.value + if isinstance(exponent, np.ndarray): + power_indices = [] + break + exponent_value = float(exponent) + if power_value is None: + power_value = exponent_value + elif exponent_value != power_value: + power_indices = [] + break + power_indices.append(_lookup_var_index(var_indices, term.left.name)) + + if power_indices and power_value is not None: + indices = np.array(power_indices, dtype=np.intp) + return lambda x, idx=indices, power=power_value: float(np.sum(x[idx] ** power)) + + unary_indices: list[int] = [] + unary_op: str | None = None + numpy_func: np.ufunc | None = None + for term in terms: + if not isinstance(term, UnaryOp) or not isinstance(term.operand, Variable): + unary_indices = [] + break + if unary_op is None: + unary_op = term.op + numpy_func = term._numpy_func + elif term.op != unary_op: + unary_indices = [] + break + unary_indices.append(_lookup_var_index(var_indices, term.operand.name)) + + if unary_indices and numpy_func is not None: + indices = np.array(unary_indices, dtype=np.intp) + return lambda x, idx=indices, np_f=numpy_func: float(np.sum(np_f(x[idx]))) + + return None + + def _build_evaluator( expr: Expression, - var_indices: dict[str, int], + var_indices: dict[str, int] | _ContiguousVectorCompileLayout, ) -> Callable[[NDArray[np.floating]], NDArray[np.floating] | np.floating | float]: """Recursively build an evaluator function for an expression. @@ -182,14 +466,14 @@ def _build_evaluator( return lambda x, p=param: p.value elif isinstance(expr, Variable): - idx = var_indices[expr.name] + idx = _lookup_var_index(var_indices, expr.name) return lambda x, i=idx: x[i] elif isinstance(expr, LinearCombination): # c @ x = c[0]*x[0] + c[1]*x[1] + ... - efficient numpy implementation coeffs = np.asarray(expr.coefficients) if isinstance(expr.vector, VectorVariable): - indices = np.array([var_indices[v.name] for v in expr.vector._variables]) + indices = _vector_indices(expr.vector, var_indices) return lambda x, c=coeffs, idx=indices: np.dot(c, x[idx]) else: # VectorExpression/VectorBinaryOp - use vector evaluator @@ -198,7 +482,7 @@ def _build_evaluator( elif isinstance(expr, VectorSum): # sum(x) = x[0] + x[1] + ... - efficient numpy implementation - indices = np.array([var_indices[v.name] for v in expr.vector._variables]) + indices = _vector_indices(expr.vector, var_indices) return lambda x, idx=indices: np.sum(x[idx]) elif isinstance(expr, VectorExpressionSum): @@ -238,26 +522,26 @@ def _build_evaluator( elif isinstance(expr, VectorPowerSum): # sum(x ** k) - efficient numpy implementation - indices = np.array([var_indices[v.name] for v in expr.vector._variables]) + indices = _vector_indices(expr.vector, var_indices) power = expr.power return lambda x, idx=indices, k=power: float(np.sum(x[idx] ** k)) elif isinstance(expr, VectorUnarySum): # sum(f(x)) - efficient numpy implementation - indices = np.array([var_indices[v.name] for v in expr.vector._variables]) + indices = _vector_indices(expr.vector, var_indices) op = expr.op numpy_func = VectorUnarySum._NUMPY_FUNCS[op] return lambda x, idx=indices, f=numpy_func: float(np.sum(f(x[idx]))) elif isinstance(expr, ElementwisePower): # x ** k element-wise - returns array - indices = np.array([var_indices[v.name] for v in expr.vector._variables]) + indices = _vector_indices(expr.vector, var_indices) power = expr.power return lambda x, idx=indices, k=power: x[idx] ** k elif isinstance(expr, ElementwiseUnary): # f(x) element-wise - returns array - indices = np.array([var_indices[v.name] for v in expr.vector._variables]) + indices = _vector_indices(expr.vector, var_indices) op = expr.op numpy_func = ElementwiseUnary._NUMPY_FUNCS[op] return lambda x, idx=indices, f=numpy_func: f(x[idx]) @@ -289,6 +573,9 @@ def _build_evaluator( return lambda x, f=operand_fn, np_f=numpy_func: np_f(f(x)) elif isinstance(expr, NarySum): + fast_sum = _try_build_nary_sum_fast_evaluator(expr, var_indices) + if fast_sum is not None: + return fast_sum term_fns = tuple(_build_evaluator(t, var_indices) for t in expr.terms) return lambda x, fns=term_fns: sum(fn(x) for fn in fns) @@ -313,7 +600,7 @@ def _eval_product(x: NDArray, fns: tuple = factor_fns) -> float: def _build_vector_evaluator( vec: Any, - var_indices: dict[str, int], + var_indices: dict[str, int] | _ContiguousVectorCompileLayout, ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]]: """Build an evaluator for a vector (returns array of values).""" from optyx.core.vectors import ( @@ -324,7 +611,7 @@ def _build_vector_evaluator( ) if isinstance(vec, VectorVariable): - indices = np.array([var_indices[v.name] for v in vec._variables]) + indices = _vector_indices(vec, var_indices) return lambda x, idx=indices: x[idx] elif isinstance(vec, VectorBinaryOp): # Single numpy op instead of N per-element evaluations @@ -362,7 +649,7 @@ def vector_expr_eval( def _build_evaluator_iterative( expr: Expression, - var_indices: dict[str, int], + var_indices: dict[str, int] | _ContiguousVectorCompileLayout, ) -> Callable[[NDArray[np.floating]], NDArray[np.floating] | np.floating | float]: """Build evaluator using iterative post-order traversal. @@ -409,7 +696,7 @@ def _build_evaluator_iterative( continue if isinstance(node, Variable): - idx = var_indices[node.name] + idx = _lookup_var_index(var_indices, node.name) result_stack.append(lambda x, i=idx: x[i]) continue @@ -417,9 +704,7 @@ def _build_evaluator_iterative( if isinstance(node, LinearCombination): coeffs = np.asarray(node.coefficients) if isinstance(node.vector, VectorVariable): - indices = np.array( - [var_indices[v.name] for v in node.vector._variables] - ) + indices = _vector_indices(node.vector, var_indices) result_stack.append(lambda x, c=coeffs, idx=indices: np.dot(c, x[idx])) else: # VectorExpression/VectorBinaryOp - use vector evaluator @@ -428,7 +713,7 @@ def _build_evaluator_iterative( continue if isinstance(node, VectorSum): - indices = np.array([var_indices[v.name] for v in node.vector._variables]) + indices = _vector_indices(node.vector, var_indices) result_stack.append(lambda x, idx=indices: np.sum(x[idx])) continue @@ -444,7 +729,7 @@ def _build_evaluator_iterative( elem_fns = [] for e in node.expression._expressions: if isinstance(e, Variable): - idx = var_indices[e.name] + idx = _lookup_var_index(var_indices, e.name) elem_fns.append(lambda x, i=idx: x[i]) elif isinstance(e, Constant): val = e.value @@ -597,7 +882,7 @@ def dict_fn( def compile_vector_gradient( expr: Expression, - variables: list[Variable], + variables: Any, ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]] | None: """Attempt to compile a fast vector gradient O(1).""" from optyx.core.autodiff import detect_affine_gradient_pattern @@ -607,17 +892,9 @@ def compile_vector_gradient( return None # Check if variables match exactly Pattern.vector - vec_vars = pattern.vector._variables - if len(variables) != len(vec_vars): + if not _variables_match_vector_names(variables, pattern.vector): return None - # Fast check: are they the same objects? - if variables != vec_vars: - # Check names - for v1, v2 in zip(variables, vec_vars): - if v1.name != v2.name: - return None - b = pattern.constant_term lt = pattern.linear_type @@ -689,7 +966,7 @@ def grad_Ax_b(x: NDArray[np.floating]) -> NDArray[np.floating]: def compile_gradient( expr: Expression, - variables: list[Variable], + variables: Any, ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]]: """Compile the gradient of an expression using symbolic differentiation. @@ -714,13 +991,23 @@ def compile_gradient( >>> grad_fn = compile_gradient(expr, [x, y]) >>> grad_fn(np.array([3.0, 4.0])) # Returns [6.0, 8.0] """ + from optyx.core.optimizer import flatten_expression + from optyx.core.expressions import NarySum # noqa: F811 + + expr = flatten_expression(expr) + is_contiguous_single_vector = isinstance(variables, ContiguousVectorVariables) + + if isinstance(expr, NarySum) and not is_contiguous_single_vector: + result = _compile_nary_sum_gradient_fast(expr, variables) + if result is not None: + return result + # Fast path: Vector Gradient Pattern (Linear/Quadratic forms) vec_grad = compile_vector_gradient(expr, variables) if vec_grad is not None: return vec_grad from optyx.core.vectors import VectorPowerSum, VectorUnarySum, VectorBinaryOp - from optyx.core.expressions import NarySum # noqa: F811 # Fast path for VectorPowerSum: gradient is k * x^(k-1), vectorized if isinstance(expr, VectorPowerSum): @@ -740,11 +1027,8 @@ def compile_gradient( if result is not None: return result - # Fast path for NarySum containing VectorExpressionSum(VectorBinaryOp) terms - if isinstance(expr, NarySum): - result = _compile_nary_sum_gradient_fast(expr, variables) - if result is not None: - return result + if is_contiguous_single_vector: + return compile_gradient(expr, variables.materialize()) # General path: symbolic differentiation from optyx.core.autodiff import gradient @@ -894,7 +1178,7 @@ def compile_gradient_with_sparsity( def _compile_vectorized_power_gradient( expr: "VectorPowerSum", - variables: list["Variable"], + variables: Any, ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]]: """Compile O(1) gradient for VectorPowerSum. @@ -905,9 +1189,8 @@ def _compile_vectorized_power_gradient( n = len(variables) # Build index mapping: which positions in the gradient correspond to vector vars - var_name_to_idx = {v.name: i for i, v in enumerate(variables)} - vector_vars = expr.vector._variables - indices = np.array([var_name_to_idx[v.name] for v in vector_vars], dtype=np.intp) + var_name_to_idx = _build_var_index_data(variables) + indices = _vector_indices(expr.vector, var_name_to_idx) # Check if vector variables form a contiguous block starting at 0 if len(indices) == n and np.array_equal(indices, np.arange(n)): @@ -944,7 +1227,7 @@ def grad_power_sparse(x: NDArray[np.floating]) -> NDArray[np.floating]: def _compile_vectorized_unary_gradient( expr: "VectorUnarySum", - variables: list["Variable"], + variables: Any, ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]]: """Compile O(1) gradient for VectorUnarySum. @@ -955,9 +1238,8 @@ def _compile_vectorized_unary_gradient( n = len(variables) # Build index mapping - var_name_to_idx = {v.name: i for i, v in enumerate(variables)} - vector_vars = expr.vector._variables - indices = np.array([var_name_to_idx[v.name] for v in vector_vars], dtype=np.intp) + var_name_to_idx = _build_var_index_data(variables) + indices = _vector_indices(expr.vector, var_name_to_idx) # Check if all variables are in the vector is_full = len(indices) == n and np.array_equal(indices, np.arange(n)) @@ -1155,7 +1437,7 @@ def fallback_gradient(x: NDArray[np.floating]) -> NDArray[np.floating]: def _compile_vectorized_binary_op_sum_gradient( vbo: Any, - variables: list["Variable"], + variables: Any, ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]] | None: """Compile O(1) gradient for sum(VectorBinaryOp). @@ -1178,21 +1460,14 @@ def _compile_vectorized_binary_op_sum_gradient( left = vbo.left right = vbo.right n = len(variables) - var_name_to_idx = {v.name: i for i, v in enumerate(variables)} + var_name_to_idx = _build_var_index_data(variables) def _get_indices( vec: Any, ) -> np.ndarray | None: """Get variable indices for a vector operand, or None if scalar/const.""" if isinstance(vec, VectorVariable): - return np.array( - [ - var_name_to_idx[v.name] - for v in vec._variables - if v.name in var_name_to_idx - ], - dtype=np.intp, - ) + return _vector_indices(vec, var_name_to_idx, allow_subset=True) return None left_idx = _get_indices(left) @@ -1287,14 +1562,148 @@ def _compile_nary_sum_gradient_fast( expr: Any, variables: list["Variable"], ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]] | None: - """Try to compile a fast gradient for NarySum with VectorBinaryOp terms. + """Try to compile a fast gradient for common NarySum patterns. - If some terms of the NarySum are VectorExpressionSum(VectorBinaryOp), - compile those with the fast path and use symbolic for the rest. - Only activates if at least one term benefits from the fast path. + Handles common loop-built scalar patterns directly, and falls back to the + existing mixed VectorBinaryOp strategy when only some terms are vectorized. """ + from optyx.core.expressions import BinaryOp, Constant, UnaryOp, Variable from optyx.core.vectors import VectorExpressionSum, VectorBinaryOp + n = len(variables) + var_name_to_idx = {v.name: i for i, v in enumerate(variables)} + terms = expr.terms + + if terms and all(isinstance(term, Variable) for term in terms): + indices = np.array( + [var_name_to_idx[term.name] for term in terms], dtype=np.intp + ) + if len(indices) == n and np.array_equal(indices, np.arange(n)): + ones = np.ones(n) + return lambda x, values=ones: values + + def grad_variable_sum(x: NDArray[np.floating]) -> NDArray[np.floating]: + result = np.zeros(n) + np.add.at(result, indices, 1.0) + return result + + return grad_variable_sum + + if terms: + power_indices: list[int] = [] + power_value: float | None = None + for term in terms: + if not isinstance(term, BinaryOp) or term.op != "**": + power_indices = [] + break + if not isinstance(term.left, Variable) or not isinstance( + term.right, Constant + ): + power_indices = [] + break + exponent = term.right.value + if isinstance(exponent, np.ndarray): + power_indices = [] + break + exponent_value = float(exponent) + if power_value is None: + power_value = exponent_value + elif exponent_value != power_value: + power_indices = [] + break + power_indices.append(var_name_to_idx[term.left.name]) + + if power_indices and power_value is not None: + indices = np.array(power_indices, dtype=np.intp) + if len(indices) == n and np.array_equal(indices, np.arange(n)): + if power_value == 1.0: + ones = np.ones(n) + return lambda x, values=ones: values + if power_value == 2.0: + return lambda x: 2.0 * x + + def grad_full_power_sum( + x: NDArray[np.floating], + ) -> NDArray[np.floating]: + raw = power_value * np.power(x, power_value - 1.0) + return _sanitize_derivatives(raw) + + return grad_full_power_sum + + def grad_sparse_power_sum(x: NDArray[np.floating]) -> NDArray[np.floating]: + result = np.zeros(n) + np.add.at( + result, + indices, + power_value * np.power(x[indices], power_value - 1.0), + ) + return _sanitize_derivatives(result) + + return grad_sparse_power_sum + + if terms: + unary_indices: list[int] = [] + unary_op: str | None = None + for term in terms: + if not isinstance(term, UnaryOp) or not isinstance(term.operand, Variable): + unary_indices = [] + break + if unary_op is None: + unary_op = term.op + elif term.op != unary_op: + unary_indices = [] + break + unary_indices.append(var_name_to_idx[term.operand.name]) + + if unary_indices and unary_op is not None: + indices = np.array(unary_indices, dtype=np.intp) + + def _eval_unary_derivative( + values: NDArray[np.floating], + ) -> NDArray[np.floating] | None: + if unary_op == "sin": + return np.cos(values) + if unary_op == "cos": + return -np.sin(values) + if unary_op == "exp": + return np.exp(values) + if unary_op == "log": + return 1.0 / values + if unary_op == "sqrt": + return 1.0 / (2.0 * np.sqrt(values)) + if unary_op == "sinh": + return np.cosh(values) + if unary_op == "cosh": + return np.sinh(values) + if unary_op == "tanh": + return 1.0 - np.tanh(values) ** 2 + return None + + if len(indices) == n and np.array_equal(indices, np.arange(n)): + + def grad_full_unary_sum( + x: NDArray[np.floating], + ) -> NDArray[np.floating]: + raw = _eval_unary_derivative(x) + if raw is None: + raise RuntimeError("unsupported unary sum gradient fast path") + return _sanitize_derivatives(raw) + + if _eval_unary_derivative(np.ones(1)) is not None: + return grad_full_unary_sum + elif _eval_unary_derivative(np.ones(1)) is not None: + + def grad_sparse_unary_sum( + x: NDArray[np.floating], + ) -> NDArray[np.floating]: + raw = _eval_unary_derivative(x[indices]) + assert raw is not None + result = np.zeros(n) + np.add.at(result, indices, raw) + return _sanitize_derivatives(result) + + return grad_sparse_unary_sum + fast_grads: list[Callable] = [] slow_terms: list[Any] = [] @@ -1320,8 +1729,6 @@ def _compile_nary_sum_gradient_fast( compiled = [compile_expression(g, variables) for g in grad_exprs] slow_grad_fns.append(compiled) - n = len(variables) - def nary_gradient(x: NDArray[np.floating]) -> NDArray[np.floating]: result = np.zeros(n) # Fast vectorized contributions diff --git a/src/optyx/core/expressions.py b/src/optyx/core/expressions.py index a76fd08..8755887 100644 --- a/src/optyx/core/expressions.py +++ b/src/optyx/core/expressions.py @@ -3,11 +3,10 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Callable, Literal from typing import Mapping import numpy as np -import re from optyx.core.errors import MissingValueError, UnknownOperatorError @@ -16,8 +15,36 @@ from optyx.constraints import Constraint -# Pre-compiled regex for natural sorting of variable names -_NUMBER_SPLIT_RE = re.compile(r"(\d+)") +def _compute_name_sort_key(name: str) -> tuple[object, ...]: + """Split a variable name into text and integer runs for natural sorting. + + This mirrors the old ``re.split(r"(\\d+)", name)`` behavior but avoids + regex dispatch and intermediate string digit checks on the hot path. + """ + parts: list[object] = [] + text_start = 0 + idx = 0 + name_len = len(name) + + while idx < name_len: + if name[idx].isdigit(): + parts.append(name[text_start:idx]) + digit_start = idx + idx += 1 + while idx < name_len and name[idx].isdigit(): + idx += 1 + parts.append(int(name[digit_start:idx])) + text_start = idx + continue + idx += 1 + + parts.append(name[text_start:]) + return tuple(parts) + + +def _vector_name_sort_key(prefix: str, index: int) -> tuple[object, ...]: + """Build the natural sort key for a VectorVariable element name.""" + return (f"{prefix}[", index, "]") class Expression(ABC): @@ -245,7 +272,15 @@ class Variable(Expression): 5.0 """ - __slots__ = ("name", "lb", "ub", "domain", "_sort_key", "obj") + __slots__ = ( + "name", + "_lb", + "_ub", + "domain", + "_sort_key", + "obj", + "_metadata_callback", + ) def __init__( self, @@ -254,14 +289,17 @@ def __init__( ub: float | None = None, domain: Literal["continuous", "integer", "binary"] = "continuous", obj: float | int = 0.0, + sort_key: tuple[object, ...] | None = None, + metadata_callback: Callable[[], None] | None = None, ) -> None: self._hash = None self._degree = None self.name = name - self.lb = lb - self.ub = ub + self._lb = float(lb) if lb is not None else None + self._ub = float(ub) if ub is not None else None self.domain = domain self.obj = float(obj) # Linear objective coefficient + self._metadata_callback = None # Validate domain if domain not in ("continuous", "integer", "binary"): @@ -270,8 +308,9 @@ def __init__( ) # Pre-compute sort key for consistent ordering - parts = _NUMBER_SPLIT_RE.split(name) - self._sort_key = tuple(int(p) if p.isdigit() else p for p in parts) + self._sort_key = ( + sort_key if sort_key is not None else _compute_name_sort_key(name) + ) # Binary variables have implicit bounds if domain == "binary": @@ -279,8 +318,72 @@ def __init__( raise ValueError(f"Binary variable must have lb=0, got {lb!r}") if ub is not None and float(ub) != 1.0: raise ValueError(f"Binary variable must have ub=1, got {ub!r}") - self.lb = 0.0 - self.ub = 1.0 + self._lb = 0.0 + self._ub = 1.0 + + self._metadata_callback = metadata_callback + + @classmethod + def _from_vector_element( + cls, + name: str, + *, + lb: float | None, + ub: float | None, + domain: Literal["continuous", "integer", "binary"], + sort_key: tuple[object, ...], + metadata_callback: Callable[[], None] | None, + ) -> Variable: + """Construct a vector-backed scalar variable with minimal overhead.""" + variable = cls.__new__(cls) + variable._hash = None + variable._degree = None + variable.name = name + variable.domain = domain + variable.obj = 0.0 + variable._sort_key = sort_key + variable._metadata_callback = metadata_callback + + if domain == "binary": + variable._lb = 0.0 + variable._ub = 1.0 + else: + variable._lb = lb + variable._ub = ub + + return variable + + @property + def lb(self) -> float | None: + return self._lb + + @lb.setter + def lb(self, value: float | None) -> None: + if self.domain == "binary" and value is not None and float(value) != 0.0: + raise ValueError(f"Binary variable must have lb=0, got {value!r}") + self._lb = ( + 0.0 + if self.domain == "binary" + else (float(value) if value is not None else None) + ) + if self._metadata_callback is not None: + self._metadata_callback() + + @property + def ub(self) -> float | None: + return self._ub + + @ub.setter + def ub(self, value: float | None) -> None: + if self.domain == "binary" and value is not None and float(value) != 1.0: + raise ValueError(f"Binary variable must have ub=1, got {value!r}") + self._ub = ( + 1.0 + if self.domain == "binary" + else (float(value) if value is not None else None) + ) + if self._metadata_callback is not None: + self._metadata_callback() def evaluate( self, values: Mapping[str, ArrayLike | float] diff --git a/src/optyx/core/vectors.py b/src/optyx/core/vectors.py index 7d09f7b..2f5742d 100644 --- a/src/optyx/core/vectors.py +++ b/src/optyx/core/vectors.py @@ -16,6 +16,7 @@ Variable, Constant, BinaryOp, + _vector_name_sort_key, ) from optyx.core.errors import ( DimensionMismatchError, @@ -1225,7 +1226,15 @@ class VectorVariable: >>> for v in x: print(v.name) # x[0], x[1], ..., x[4] """ - __slots__ = ("name", "size", "lb", "ub", "domain", "_variables") + __slots__ = ( + "name", + "size", + "lb", + "ub", + "domain", + "_variable_cache", + "_has_bound_overrides", + ) # Tell NumPy to defer to Python's operators (enables numpy_array @ vector) __array_ufunc__ = None @@ -1236,7 +1245,8 @@ class VectorVariable: lb: float | Sequence[float] | NDArray | None ub: float | Sequence[float] | NDArray | None domain: DomainType - _variables: list[Variable] + _variable_cache: list[Variable | None] | None + _has_bound_overrides: bool def __init__( self, @@ -1259,35 +1269,150 @@ def __init__( self.ub = ub self.domain = domain - # Helper to get bound for index i - def get_bound( - b: float | Sequence[float] | NDArray | None, i: int, param_name: str - ) -> float | None: - if b is None: - return None - if isinstance(b, (int, float, np.number)): - return float(b) - if hasattr(b, "__len__") and hasattr(b, "__getitem__"): - if len(b) != size: - raise InvalidSizeError( - entity=f"{param_name} for {name}", - size=len(b), - reason=f"must match vector size {size}", - ) - return float(b[i]) - # Fallback - if hasattr(b, "__float__"): - return float(b) # type: ignore + self._validate_bound_length(lb, "lb") + self._validate_bound_length(ub, "ub") + self._variable_cache = None + self._has_bound_overrides = False + + def _validate_bound_length( + self, + bound: float | Sequence[float] | NDArray | None, + param_name: str, + ) -> None: + if bound is None or isinstance(bound, (int, float, np.number)): + return + if hasattr(bound, "__len__") and hasattr(bound, "__getitem__"): + if len(bound) != self.size: + raise InvalidSizeError( + entity=f"{param_name} for {self.name}", + size=len(bound), + reason=f"must match vector size {self.size}", + ) + + def _bound_at( + self, + bound: float | Sequence[float] | NDArray | None, + index: int, + ) -> float | None: + if bound is None: return None + if isinstance(bound, (int, float, np.number)): + return float(bound) + if hasattr(bound, "__getitem__"): + return float(bound[index]) + if hasattr(bound, "__float__"): + return float(bound) # type: ignore[arg-type] + return None + + def _name_at(self, index: int) -> str: + return f"{self.name}[{index}]" + + def _mark_bound_override(self) -> None: + self._has_bound_overrides = True + + def _matches_default_bounds(self, variable: Variable, index: int) -> bool: + return variable.lb == self._bound_at( + self.lb, index + ) and variable.ub == self._bound_at(self.ub, index) + + def _iter_variable_names(self) -> Iterator[str]: + for index in range(self.size): + yield self._name_at(index) + + def _can_skip_scipy_bounds(self) -> bool: + return ( + self.domain == "continuous" + and self.lb is None + and self.ub is None + and not self._has_bound_overrides + ) - # Create individual variables - self._variables: list[Variable] = [] - for i in range(size): - val_l = get_bound(lb, i, "lb") - val_u = get_bound(ub, i, "ub") - self._variables.append( - Variable(f"{name}[{i}]", lb=val_l, ub=val_u, domain=domain) - ) + def _lp_metadata_at( + self, index: int + ) -> tuple[str, tuple[float | None, float | None], DomainType, float]: + cache = self._variable_cache + if cache is not None: + variable = cache[index] + if variable is not None: + return ( + variable.name, + (variable.lb, variable.ub), + cast(DomainType, variable.domain), + variable.obj, + ) + + return ( + self._name_at(index), + (self._bound_at(self.lb, index), self._bound_at(self.ub, index)), + self.domain, + 0.0, + ) + + def _iter_lp_metadata( + self, + ) -> Iterator[tuple[str, tuple[float | None, float | None], DomainType, float]]: + for index in range(self.size): + yield self._lp_metadata_at(index) + + def _has_non_continuous_domain(self) -> bool: + if self.domain != "continuous": + return True + + cache = self._variable_cache + if cache is None: + return False + + return any( + variable is not None and variable.domain != "continuous" + for variable in cache + ) + + def _create_variable(self, index: int) -> Variable: + return Variable._from_vector_element( + self._name_at(index), + lb=self._bound_at(self.lb, index), + ub=self._bound_at(self.ub, index), + domain=self.domain, + sort_key=_vector_name_sort_key(self.name, index), + metadata_callback=self._mark_bound_override, + ) + + def _get_variable(self, index: int) -> Variable: + cache = self._variable_cache + if cache is None: + cache = [None] * self.size + self._variable_cache = cache + + variable = cache[index] + if variable is None: + variable = self._create_variable(index) + cache[index] = variable + return variable + + @property + def _variables(self) -> list[Variable]: + cache = self._variable_cache + if cache is None: + variables = [self._create_variable(index) for index in range(self.size)] + self._variable_cache = variables + return variables + + for index, variable in enumerate(cache): + if variable is None: + cache[index] = self._create_variable(index) + + return cast(list[Variable], cache) + + @_variables.setter + def _variables(self, value: list[Variable]) -> None: + cache = list(value) + self._variable_cache = cache + self._has_bound_overrides = False + + for index, variable in enumerate(cache): + variable._metadata_callback = self._mark_bound_override + if not self._matches_default_bounds(variable, index): + self._has_bound_overrides = True @overload def __getitem__(self, key: int) -> Variable: ... @@ -1321,11 +1446,12 @@ def __getitem__( raise IndexError( f"Index {key} out of range for VectorVariable of size {self.size}" ) - return self._variables[key] + return self._get_variable(key) elif isinstance(key, slice): # Get the sliced variables - sliced_vars = self._variables[key] + indices = range(*key.indices(self.size)) + sliced_vars = [self._get_variable(i) for i in indices] if len(sliced_vars) == 0: raise IndexError("Slice results in empty VectorVariable") @@ -1359,7 +1485,7 @@ def __getitem__( i = self.size + i if i < 0 or i >= self.size: raise IndexError(f"Index {i} out of range") - selected_vars.append(self._variables[i]) + selected_vars.append(self._get_variable(i)) if len(selected_vars) == 0: raise IndexError("Fancy indexing results in empty VectorVariable") @@ -1410,7 +1536,8 @@ def __len__(self) -> int: def __iter__(self) -> Iterator[Variable]: """Iterate over all variables in the vector.""" - return iter(self._variables) + for i in range(self.size): + yield self._get_variable(i) def get_variables(self) -> list[Variable]: """Return all variables in this vector. @@ -1418,7 +1545,7 @@ def get_variables(self) -> list[Variable]: Returns: List of Variable instances in order. """ - return list(self._variables) + return [self._get_variable(i) for i in range(self.size)] def __repr__(self) -> str: bounds = "" diff --git a/src/optyx/problem.py b/src/optyx/problem.py index 5843c58..7040b83 100644 --- a/src/optyx/problem.py +++ b/src/optyx/problem.py @@ -10,7 +10,6 @@ from __future__ import annotations -import re from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Callable, Literal, Iterable from types import TracebackType @@ -23,6 +22,7 @@ NoObjectiveError, UnsupportedOperationError, ) +from optyx.core.expressions import _compute_name_sort_key if TYPE_CHECKING: from numpy.typing import NDArray @@ -55,9 +55,6 @@ class _MatrixConstraint: SPARSE_NLP_DENSITY_THRESHOLD = 0.15 SPARSE_MATRIX_ENTRY_THRESHOLD = 4096 -# Pre-compiled regex for natural sorting of variable names -_NUMBER_SPLIT_RE = re.compile(r"(\d+)") - def _natural_sort_key(var: Variable) -> tuple: """Generate a sort key for natural ordering of variable names. @@ -75,11 +72,7 @@ def _natural_sort_key(var: Variable) -> tuple: if hasattr(var, "_sort_key"): return var._sort_key - name = var.name - # Split into text and number parts - parts = _NUMBER_SPLIT_RE.split(name) - # Convert number parts to integers for proper numeric sorting - return tuple(int(p) if p.isdigit() else p for p in parts) + return _compute_name_sort_key(var.name) def _try_get_single_vector_source(expr: "Expression") -> "VectorVariable | None": @@ -554,6 +547,27 @@ def constraints(self) -> list[Constraint]: """List of constraints.""" return self._constraints.copy() + def _single_vector_source(self) -> VectorVariable | None: + """Return the sole VectorVariable driving the problem, if any. + + This fast path only applies when the objective and all scalar constraints + depend on the same VectorVariable and there are no structured matrix + constraint blocks that could reorder or subset the columns. + """ + if self._objective is None or self._matrix_constraints: + return None + + source_vector = _try_get_single_vector_source(self._objective) + if source_vector is None: + return None + + for constraint in self._constraints: + constraint_source = _try_get_single_vector_source(constraint.expr) + if constraint_source is None or constraint_source is not source_vector: + return None + + return source_vector + @property def variables(self) -> list[Variable]: """All decision variables in the problem. @@ -573,26 +587,11 @@ def variables(self) -> list[Variable]: from optyx.core.expressions import get_all_variables - # Fast path: check if objective is based on a single VectorVariable - # In this case, we can skip the expensive set operations and sorting - if self._objective is not None: - source_vector = _try_get_single_vector_source(self._objective) - if source_vector is not None: - # Check if all constraints use the same VectorVariable - all_same = True - for constraint in self._constraints: - constraint_source = _try_get_single_vector_source(constraint.expr) - if ( - constraint_source is None - or constraint_source is not source_vector - ): - all_same = False - break - - if all_same: - # All variables from one VectorVariable - already in order! - self._variables = list(source_vector._variables) - return self._variables + source_vector = self._single_vector_source() + if source_vector is not None: + # All variables from one VectorVariable - already in order! + self._variables = list(source_vector._variables) + return self._variables # General case: collect from all expressions and sort all_vars: set[Variable] = set() @@ -628,6 +627,10 @@ def get_bounds(self) -> list[tuple[float | None, float | None]]: Returns: List of bounds in variable order. """ + source_vector = self._single_vector_source() + if source_vector is not None: + return [bounds for _, bounds, _, _ in source_vector._iter_lp_metadata()] + return [(v.lb, v.ub) for v in self.variables] def _is_linear_problem(self) -> bool: @@ -884,9 +887,20 @@ def solve( "trust-krylov", } if method in _NLP_METHODS: - discrete_names = [ - v.name for v in self.variables if v.domain in ("integer", "binary") - ] + source_vector = self._single_vector_source() + if source_vector is not None: + if source_vector._has_non_continuous_domain(): + discrete_names = [ + name + for name, _, domain, _ in source_vector._iter_lp_metadata() + if domain in ("integer", "binary") + ] + else: + discrete_names = [] + else: + discrete_names = [ + v.name for v in self.variables if v.domain in ("integer", "binary") + ] if discrete_names and not self._is_linear_problem(): raise UnsupportedOperationError( "MIQP/MINLP solve", @@ -942,9 +956,20 @@ def solve( def _store_solution(self, solution: Solution) -> None: """Store solution values for warm starting subsequent solves.""" + if solution._raw_x is not None: + self._last_solution = np.asarray(solution._raw_x, dtype=np.float64).copy() + return + if solution.values: - variables = self.variables - x = np.array([solution.values.get(v.name, 0.0) for v in variables]) + names: list[str] | None = None + if self._lp_cache is not None and all( + name in solution.values for name in self._lp_cache.variables + ): + names = self._lp_cache.variables + else: + names = [v.name for v in self.variables] + + x = np.array([solution.values.get(name, 0.0) for name in names]) self._last_solution = x def __repr__(self) -> str: diff --git a/src/optyx/solution.py b/src/optyx/solution.py index 9997c59..ef89733 100644 --- a/src/optyx/solution.py +++ b/src/optyx/solution.py @@ -8,7 +8,7 @@ from dataclasses import dataclass, field from enum import Enum -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable import json import os @@ -37,6 +37,78 @@ class SolverStatus(Enum): NOT_SOLVED = "not_solved" +class LazyValuesDict(dict[str, float]): + """Dictionary that materializes solution values on first access.""" + + __slots__ = ("_loader", "_loaded") + + def __init__( + self, + initial: dict[str, float] | None = None, + loader: Callable[[], dict[str, float]] | None = None, + ) -> None: + super().__init__(initial or {}) + self._loader = loader + self._loaded = initial is not None or loader is None + + def _ensure_loaded(self) -> None: + if self._loaded: + return + + assert self._loader is not None + super().update(self._loader()) + self._loader = None + self._loaded = True + + def __getitem__(self, key: str) -> float: + self._ensure_loaded() + return super().__getitem__(key) + + def __contains__(self, key: object) -> bool: + self._ensure_loaded() + return super().__contains__(key) + + def __iter__(self): + self._ensure_loaded() + return super().__iter__() + + def __len__(self) -> int: + self._ensure_loaded() + return super().__len__() + + def __bool__(self) -> bool: + self._ensure_loaded() + return super().__len__() > 0 + + def __repr__(self) -> str: + self._ensure_loaded() + return super().__repr__() + + def __eq__(self, other: object) -> bool: + self._ensure_loaded() + return super().__eq__(other) + + def get(self, key: str, default: float | None = None) -> float | None: + self._ensure_loaded() + return super().get(key, default) + + def items(self): + self._ensure_loaded() + return super().items() + + def keys(self): + self._ensure_loaded() + return super().keys() + + def values(self): + self._ensure_loaded() + return super().values() + + def copy(self) -> dict[str, float]: + self._ensure_loaded() + return dict(self) + + @dataclass class SolverProgress: """Snapshot of solver state passed to user callbacks during optimization. @@ -85,6 +157,7 @@ class Solution: solve_time: float | None = None mip_gap: float | None = None best_bound: float | None = None + _raw_x: NDArray[np.floating] | None = field(default=None, repr=False, compare=False) @property def is_optimal(self) -> bool: @@ -105,7 +178,7 @@ def to_dict(self) -> dict: return { "status": self.status.value, "objective_value": self.objective_value, - "values": self.values, + "values": dict(self.values), "multipliers": self.multipliers, "iterations": self.iterations, "message": self.message, diff --git a/src/optyx/solvers/lp_solver.py b/src/optyx/solvers/lp_solver.py index 8f436f1..205288c 100644 --- a/src/optyx/solvers/lp_solver.py +++ b/src/optyx/solvers/lp_solver.py @@ -91,8 +91,17 @@ def solve_lp( ) # Check for non-continuous domains — route to MILP solver - variables = problem.variables - non_continuous = [v for v in variables if v.domain != "continuous"] + source_vector = problem._single_vector_source() + variables = None + non_continuous = [] + if source_vector is not None: + if source_vector._has_non_continuous_domain(): + variables = problem.variables + non_continuous = [v for v in variables if v.domain != "continuous"] + else: + variables = problem.variables + non_continuous = [v for v in variables if v.domain != "continuous"] + if non_continuous: from optyx.solvers.milp_solver import solve_milp @@ -110,6 +119,7 @@ def solve_lp( solver_name="milp", ) from e + assert variables is not None return solve_milp(lp_data, variables, **kwargs) # Check SciPy version and select method @@ -161,7 +171,7 @@ def solve_lp( # Always re-extract bounds from live variable properties to ensure # updates to v.lb/v.ub are respected even when LP data is cached. - fresh_bounds = [(v.lb, v.ub) for v in variables] + fresh_bounds = problem.get_bounds() if fresh_bounds: linprog_kwargs["bounds"] = fresh_bounds diff --git a/src/optyx/solvers/scipy_solver.py b/src/optyx/solvers/scipy_solver.py index 644576c..54f0dbb 100644 --- a/src/optyx/solvers/scipy_solver.py +++ b/src/optyx/solvers/scipy_solver.py @@ -32,6 +32,111 @@ def __init__(self, x: np.ndarray, iteration: int, message: str) -> None: self.message = message +def _build_single_vector_bounds(source_vector: Any) -> Bounds | None: + """Build SciPy bounds directly from VectorVariable metadata.""" + lb_arr = np.empty(source_vector.size) + ub_arr = np.empty(source_vector.size) + has_finite_bound = False + + for i, (_, bounds, _, _) in enumerate(source_vector._iter_lp_metadata()): + lb = bounds[0] if bounds[0] is not None else -np.inf + ub = bounds[1] if bounds[1] is not None else np.inf + lb_arr[i] = lb + ub_arr[i] = ub + if not has_finite_bound and (np.isfinite(lb) or np.isfinite(ub)): + has_finite_bound = True + + if not has_finite_bound: + return None + + return Bounds(lb=lb_arr, ub=ub_arr) # type: ignore[arg-type] + + +def _compute_single_vector_initial_point(source_vector: Any) -> np.ndarray: + """Compute an initial point directly from VectorVariable metadata.""" + x0 = np.zeros(source_vector.size) + + _INTERIOR_EPSILON = 1e-4 + _INTERIOR_FRACTION = 0.01 + + for i, (_, bounds, _, _) in enumerate(source_vector._iter_lp_metadata()): + lb = bounds[0] if bounds[0] is not None else -np.inf + ub = bounds[1] if bounds[1] is not None else np.inf + + if np.isfinite(lb) and np.isfinite(ub): + range_size = ub - lb + epsilon = max(_INTERIOR_EPSILON, _INTERIOR_FRACTION * range_size) + x0[i] = min(lb + epsilon, (lb + ub) / 2) + elif np.isfinite(lb): + x0[i] = lb + _INTERIOR_EPSILON + elif np.isfinite(ub): + x0[i] = ub - 1.0 + else: + x0[i] = 0.0 + + return x0 + + +def _single_vector_values_loader( + source_vector: Any, + raw_x: np.ndarray, +) -> dict[str, float]: + """Build a solution values dict using metadata-backed variable names.""" + return { + name: float(raw_x[i]) + for i, (name, _, _, _) in enumerate(source_vector._iter_lp_metadata()) + } + + +def _variable_values_loader( + variables: list[Any], + raw_x: np.ndarray, +) -> dict[str, float]: + """Build a solution values dict from an explicit variable list.""" + return {v.name: float(raw_x[i]) for i, v in enumerate(variables)} + + +def _make_single_vector_loader( + source_vector: Any, + raw_x: np.ndarray, +) -> Callable[[], dict[str, float]]: + def load() -> dict[str, float]: + return _single_vector_values_loader(source_vector, raw_x) + + return load + + +def _make_variable_loader( + variables: list[Any], + raw_x: np.ndarray, +) -> Callable[[], dict[str, float]]: + def load() -> dict[str, float]: + return _variable_values_loader(variables, raw_x) + + return load + + +def _build_bounds(variables: list) -> Bounds | None: + """Build SciPy bounds, skipping all-infinite no-op bounds.""" + lb_arr = np.empty(len(variables)) + ub_arr = np.empty(len(variables)) + has_finite_bound = False + + for i, v in enumerate(variables): + lb = v.lb if v.lb is not None else -np.inf + ub = v.ub if v.ub is not None else np.inf + lb_arr[i] = lb + ub_arr[i] = ub + + if not has_finite_bound and (np.isfinite(lb) or np.isfinite(ub)): + has_finite_bound = True + + if not has_finite_bound: + return None + + return Bounds(lb=lb_arr, ub=ub_arr) # type: ignore[arg-type] + + def solve_scipy( problem: Problem, method: str = "SLSQP", @@ -81,7 +186,7 @@ def solve_scipy( reaches this solver directly. """ from optyx.core.autodiff import compile_hessian - from optyx.solution import Solution, SolverStatus + from optyx.solution import LazyValuesDict, Solution, SolverStatus # Methods that support Hessian HESSIAN_METHODS = { @@ -109,8 +214,20 @@ def solve_scipy( "Nelder-Mead", } - variables = problem.variables - n = len(variables) + source_vector = problem._single_vector_source() + use_lazy_single_vector = ( + source_vector is not None + and not problem._has_general_constraints() + and method not in HESSIAN_METHODS + ) + + variables: list[Any] | None = None + if use_lazy_single_vector: + assert source_vector is not None + n = source_vector.size + else: + variables = problem.variables + n = len(variables) if n == 0: return Solution( @@ -121,12 +238,24 @@ def solve_scipy( # Check for non-continuous domains — always raise for MINLP. # The caller (Problem.solve) should have caught this already, but # guard here as a safety net. - non_continuous = [v for v in variables if v.domain != "continuous"] + if use_lazy_single_vector: + assert source_vector is not None + if source_vector._has_non_continuous_domain(): + non_continuous = [ + name + for name, _, domain, _ in source_vector._iter_lp_metadata() + if domain != "continuous" + ] + else: + non_continuous = [] + else: + assert variables is not None + non_continuous = [v.name for v in variables if v.domain != "continuous"] if non_continuous: if problem._is_linear_problem(): raise IntegerVariableError( solver_name="SciPy", - variable_names=[v.name for v in non_continuous], + variable_names=non_continuous, ) raise UnsupportedOperationError( @@ -134,7 +263,7 @@ def solve_scipy( solver_name="SciPy", problem_feature=( "nonlinear objective or constraints with integer/binary " - f"variables {[v.name for v in non_continuous]}" + f"variables {non_continuous}" ), suggestion=( "Use the MILP path for linear discrete models, or switch to a " @@ -145,10 +274,16 @@ def solve_scipy( # Check for cached compiled callables cache = problem._solver_cache if cache is None: - cache = _build_solver_cache(problem, variables) + if use_lazy_single_vector: + assert source_vector is not None + cache = _build_single_vector_solver_cache(problem, source_vector) + else: + assert variables is not None + cache = _build_solver_cache(problem, variables) problem._solver_cache = cache - elif "scipy_constraints" not in cache: + elif "scipy_constraints" not in cache and not use_lazy_single_vector: # Selective invalidation: objective cache preserved, rebuild constraints only + assert variables is not None _rebuild_constraint_cache(cache, problem, variables) # Extract cached callables @@ -159,13 +294,18 @@ def solve_scipy( list[LinearConstraint], cache.get("linear_constraints", []) ) - # Recompute bounds each time to ensure updates to variable properties are respected - lb_arr = np.empty(n) - ub_arr = np.empty(n) - for i, v in enumerate(variables): - lb_arr[i] = v.lb if v.lb is not None else -np.inf - ub_arr[i] = v.ub if v.ub is not None else np.inf - bounds = Bounds(lb=lb_arr, ub=ub_arr) # type: ignore[arg-type] # scipy stubs are wrong, Bounds accepts arrays + # Recompute bounds each time to ensure updates to variable properties are + # respected, but skip the scan entirely for the common single-vector case + # where the vector remains fully unbounded. + bounds = None + if method in BOUNDS_METHODS: + if not (source_vector is not None and source_vector._can_skip_scipy_bounds()): + if use_lazy_single_vector: + assert source_vector is not None + bounds = _build_single_vector_bounds(source_vector) + else: + assert variables is not None + bounds = _build_bounds(variables) def objective(x: np.ndarray) -> float: return float(obj_fn(x)) @@ -204,7 +344,12 @@ def _hess_fn(x: np.ndarray) -> np.ndarray: ): x0 = problem._last_solution.copy() else: - x0 = _compute_initial_point(variables) + if use_lazy_single_vector: + assert source_vector is not None + x0 = _compute_single_vector_initial_point(source_vector) + else: + assert variables is not None + x0 = _compute_initial_point(variables) # Solver options options: dict[str, Any] = {} @@ -278,13 +423,21 @@ def warning_handler(message, category, filename, lineno, file=None, line=None): obj_value = float(obj_fn(et.x)) if problem.sense == "maximize": obj_value = -obj_value + raw_x = np.asarray(et.x, dtype=np.float64).copy() + if use_lazy_single_vector: + assert source_vector is not None + loader = _make_single_vector_loader(source_vector, raw_x) + else: + assert variables is not None + loader = _make_variable_loader(variables, raw_x) return Solution( status=SolverStatus.TERMINATED, objective_value=obj_value, - values={v.name: float(et.x[i]) for i, v in enumerate(variables)}, + values=LazyValuesDict(loader=loader), iterations=et.iteration, message=et.message, solve_time=solve_time, + _raw_x=raw_x, ) except Exception as e: warnings.showwarning = old_showwarning @@ -360,13 +513,22 @@ def warning_handler(message, category, filename, lineno, file=None, line=None): if linear_problem_detected: message = f"{message} (Note: problem appears linear)" + raw_x = np.asarray(result.x, dtype=np.float64).copy() + if use_lazy_single_vector: + assert source_vector is not None + loader = _make_single_vector_loader(source_vector, raw_x) + else: + assert variables is not None + loader = _make_variable_loader(variables, raw_x) + return Solution( status=status, objective_value=obj_value, - values={v.name: float(result.x[i]) for i, v in enumerate(variables)}, + values=LazyValuesDict(loader=loader), iterations=result.nit if hasattr(result, "nit") else None, message=message, solve_time=solve_time, + _raw_x=raw_x, ) @@ -528,6 +690,66 @@ def _build_solver_cache(problem: Problem, variables: list) -> dict[str, Any]: return cache +def _build_single_vector_solver_cache( + problem: Problem, source_vector: Any +) -> dict[str, Any]: + """Build cache for unconstrained single-vector NLPs without Variable materialization.""" + from optyx.core.compiler import ( + ContiguousVectorVariables, + _sanitize_derivatives, + compile_expression, + compile_gradient, + ) + from optyx.core.optimizer import flatten_expression + + cache: dict[str, Any] = {} + variables = ContiguousVectorVariables(source_vector) + + obj_expr = problem.objective + if obj_expr is None: + raise NoObjectiveError( + suggestion="Call minimize() or maximize() on the problem first.", + ) + + sign = -1.0 if problem.sense == "maximize" else 1.0 + vector_cache = source_vector._variable_cache + if vector_cache is None: + obj_linear = None + else: + obj_linear = sign * np.fromiter( + ( + float(variable.obj) if variable is not None else 0.0 + for variable in vector_cache + ), + dtype=np.float64, + count=source_vector.size, + ) + if problem.sense == "maximize": + obj_expr = -obj_expr # type: ignore[operator] + + obj_expr = flatten_expression(obj_expr) + + compiled_obj = compile_expression(obj_expr, variables) + compiled_grad = compile_gradient(obj_expr, variables) + + if obj_linear is not None and np.any(obj_linear): + cache["obj_fn"] = lambda x, fn=compiled_obj, c=obj_linear: float(fn(x)) + float( + np.dot(c, x) + ) + cache["grad_fn"] = ( + lambda x, fn=compiled_grad, c=obj_linear: _sanitize_derivatives( + np.asarray(fn(x), dtype=np.float64).ravel() + c + ) + ) + else: + cache["obj_fn"] = compiled_obj + cache["grad_fn"] = compiled_grad + + cache["scipy_constraints"] = [] + cache["linear_constraints"] = [] + return cache + + def _rebuild_constraint_cache( cache: dict[str, Any], problem: Problem, variables: list ) -> None: diff --git a/tests/test_autodiff.py b/tests/test_autodiff.py index 3ff0b62..8cd7d22 100644 --- a/tests/test_autodiff.py +++ b/tests/test_autodiff.py @@ -3,7 +3,18 @@ import numpy as np import pytest -from optyx import Variable, Constant, sin, cos, exp, log, sqrt, tanh, abs_ +from optyx import ( + Variable, + VectorVariable, + Constant, + sin, + cos, + exp, + log, + sqrt, + tanh, + abs_, +) from optyx.core.autodiff import ( gradient, compute_jacobian, @@ -380,6 +391,19 @@ def test_compiled_jacobian(self): expected = np.array([[4.0, 1.0], [3.0, 2.0]]) np.testing.assert_array_almost_equal(result, expected) + def test_compiled_jacobian_loop_built_power_sum(self): + x = VectorVariable("x", 5) + expr = x[0] ** 3 + for i in range(1, 5): + expr = expr + x[i] ** 3 + + jac_fn = compile_jacobian([expr], list(x)) + values = np.arange(5, dtype=np.float64) + + np.testing.assert_array_almost_equal( + jac_fn(values), np.array([3.0 * values**2]) + ) + class TestHessian: """Tests for Hessian computation.""" diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 9ce9582..81527e5 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -4,8 +4,9 @@ import numpy as np -from optyx import Variable, sin, cos, exp, log, sqrt +from optyx import Variable, VectorVariable, sin, cos, exp, log, sqrt from optyx.core.compiler import ( + ContiguousVectorVariables, compile_expression, compile_to_dict_function, compile_gradient, @@ -78,6 +79,34 @@ def test_single_variable(self): f = compile_expression(expr, [x]) assert f(np.array([4.0])) == 16.0 + def test_loop_built_power_sum(self): + x = VectorVariable("x", 5) + expr = x[0] ** 3 + for i in range(1, 5): + expr = expr + x[i] ** 3 + + f = compile_expression(expr, list(x)) + values = np.arange(5, dtype=np.float64) + + assert f(values) == np.sum(values**3) + + def test_contiguous_single_vector_compile_path_keeps_lazy_cache(self): + x = VectorVariable("x", 4) + variables = ContiguousVectorVariables(x) + expr = x.dot(x) - x.sum() + values = np.array([1.0, 2.0, 3.0, 4.0]) + + assert x._variable_cache is None + + value_fn = compile_expression(expr, variables) + grad_fn = compile_gradient(expr, variables) + + np.testing.assert_allclose( + value_fn(values), np.dot(values, values) - np.sum(values) + ) + np.testing.assert_allclose(grad_fn(values), 2.0 * values - 1.0) + assert x._variable_cache is None + class TestCompileToDictFunction: """Tests for compile_to_dict_function.""" diff --git a/tests/test_solver_integration.py b/tests/test_solver_integration.py index 8508499..e8113fe 100644 --- a/tests/test_solver_integration.py +++ b/tests/test_solver_integration.py @@ -19,6 +19,47 @@ class TestVectorVariableSolverIntegration: """Tests for VectorVariable with Problem.solve().""" + def test_single_vector_lp_solve_keeps_lazy_cache(self): + """Pure vector LP solves should not force full Variable materialization.""" + n = 200 + x = VectorVariable("x", n, lb=0, ub=1) + coeffs = np.ones(n) + + prob = Problem().maximize(coeffs @ x).subject_to(x.sum() <= 1) + assert x._variable_cache is None + + sol = prob.solve() + + assert sol.is_optimal + assert x._variable_cache is None + + def test_single_vector_lp_respects_cached_bound_override(self): + """Bounds overridden on cached elements are preserved by the LP fast path.""" + x = VectorVariable("x", 3, lb=0, ub=1) + x[1].ub = 0.0 + + prob = Problem().maximize(np.ones(3) @ x) + sol = prob.solve() + + assert sol.is_optimal + assert sol["x[1]"] == pytest.approx(0.0, abs=1e-8) + assert sol["x[0]"] == pytest.approx(1.0, abs=1e-8) + assert sol["x[2]"] == pytest.approx(1.0, abs=1e-8) + + def test_single_vector_unconstrained_nlp_solve_keeps_lazy_cache(self): + """Unconstrained vector NLP solves should avoid first-time scalar materialization.""" + n = 200 + x = VectorVariable("x", n) + prob = Problem().minimize(x.dot(x) - x.sum()) + + assert x._variable_cache is None + + sol = prob.solve(x0=np.zeros(n)) + + assert sol.is_optimal + assert x._variable_cache is None + assert sol["x[0]"] == pytest.approx(0.5, abs=1e-6) + def test_vector_minimize_sum_of_squares(self): """Minimize sum of squares with vector variable.""" diff --git a/tests/test_solvers.py b/tests/test_solvers.py index e79436a..6da6945 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -3,10 +3,12 @@ import warnings import pytest import numpy as np +from scipy.optimize import Bounds, OptimizeResult -from optyx import Variable +from optyx import Variable, VectorVariable from optyx.core.errors import UnsupportedOperationError from optyx.problem import Problem +from optyx.solvers import scipy_solver class TestIntegerBinaryWarning: @@ -331,6 +333,112 @@ def test_lbfgsb_bounds_only(self): assert sol.is_optimal assert abs(sol["x"] - 1.0) < 1e-4 + def test_lbfgsb_skips_all_infinite_bounds(self, monkeypatch): + """Unbounded L-BFGS-B problems should not pass no-op bounds to SciPy.""" + captured: dict[str, object] = {} + + def fake_minimize(*args, **kwargs): + captured["bounds"] = kwargs.get("bounds") + x0 = np.asarray(kwargs["x0"], dtype=float) + return OptimizeResult( + x=x0, + fun=float(kwargs["fun"](x0)), + success=True, + message="ok", + nit=0, + ) + + monkeypatch.setattr(scipy_solver, "minimize", fake_minimize) + + x = Variable("x") + prob = Problem().minimize(x**2) + sol = prob.solve(method="L-BFGS-B", x0=np.array([0.0])) + + assert sol.is_optimal + assert captured["bounds"] is None + + def test_lbfgsb_preserves_finite_bounds(self, monkeypatch): + """Finite bounds should still be forwarded to SciPy.""" + captured: dict[str, object] = {} + + def fake_minimize(*args, **kwargs): + captured["bounds"] = kwargs.get("bounds") + x0 = np.asarray(kwargs["x0"], dtype=float) + return OptimizeResult( + x=x0, + fun=float(kwargs["fun"](x0)), + success=True, + message="ok", + nit=0, + ) + + monkeypatch.setattr(scipy_solver, "minimize", fake_minimize) + + x = Variable("x", lb=1.0, ub=2.0) + prob = Problem().minimize(x**2) + sol = prob.solve(method="L-BFGS-B", x0=np.array([1.5])) + + assert sol.is_optimal + assert isinstance(captured["bounds"], Bounds) + + def test_lbfgsb_skips_unbounded_single_vector_bounds(self, monkeypatch): + """Unbounded single-vector problems should bypass bounds scanning.""" + captured: dict[str, object] = {} + + def fake_minimize(*args, **kwargs): + captured["bounds"] = kwargs.get("bounds") + x0 = np.asarray(kwargs["x0"], dtype=float) + return OptimizeResult( + x=x0, + fun=float(kwargs["fun"](x0)), + success=True, + message="ok", + nit=0, + ) + + def fail_build_bounds(_variables): + raise AssertionError( + "_build_bounds should not run for unbounded vector problems" + ) + + monkeypatch.setattr(scipy_solver, "minimize", fake_minimize) + monkeypatch.setattr(scipy_solver, "_build_bounds", fail_build_bounds) + + x = VectorVariable("x", 3) + prob = Problem().minimize(x.dot(x) - x.sum()) + sol = prob.solve(method="L-BFGS-B", x0=np.zeros(3)) + + assert sol.is_optimal + assert captured["bounds"] is None + + def test_lbfgsb_vector_bound_override_preserves_bounds(self, monkeypatch): + """Per-element bound overrides must still flow through to SciPy.""" + captured: dict[str, object] = {} + + def fake_minimize(*args, **kwargs): + captured["bounds"] = kwargs.get("bounds") + x0 = np.asarray(kwargs["x0"], dtype=float) + return OptimizeResult( + x=x0, + fun=float(kwargs["fun"](x0)), + success=True, + message="ok", + nit=0, + ) + + monkeypatch.setattr(scipy_solver, "minimize", fake_minimize) + + x = VectorVariable("x", 3) + x[1].lb = 2.0 + prob = Problem().minimize(x.dot(x) - x.sum()) + sol = prob.solve(method="L-BFGS-B", x0=np.zeros(3)) + + assert sol.is_optimal + assert isinstance(captured["bounds"], Bounds) + bounds = captured["bounds"] + assert isinstance(bounds, Bounds) + assert bounds.lb[1] == pytest.approx(2.0) + class TestEdgeCases: """Tests for edge cases and error handling.""" diff --git a/tests/test_vectors.py b/tests/test_vectors.py index e4236a4..a11dcae 100644 --- a/tests/test_vectors.py +++ b/tests/test_vectors.py @@ -134,6 +134,11 @@ def test_index_returns_variable(self): x = VectorVariable("x", 5) assert isinstance(x[0], Variable) + def test_repeated_index_returns_same_variable(self): + """Repeated indexing returns the same cached Variable object.""" + x = VectorVariable("x", 5) + assert x[0] is x[0] + def test_index_out_of_range(self): """Out of range index raises IndexError.""" x = VectorVariable("x", 5)