jaddle 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jaddle-0.2.2 → jaddle-0.2.3}/PKG-INFO +203 -59
- {jaddle-0.2.2 → jaddle-0.2.3}/README.md +202 -58
- jaddle-0.2.3/jaddle/__init__.py +1 -0
- jaddle-0.2.3/jaddle/cones.py +534 -0
- {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/jaddle_basic_types.py +51 -5
- {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/jaddle_convex.py +259 -187
- {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/jaddle_cvxpy.py +507 -41
- {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/jaddle_linear.py +1062 -392
- {jaddle-0.2.2 → jaddle-0.2.3}/jaddle.egg-info/PKG-INFO +203 -59
- {jaddle-0.2.2 → jaddle-0.2.3}/jaddle.egg-info/SOURCES.txt +2 -0
- {jaddle-0.2.2 → jaddle-0.2.3}/pyproject.toml +1 -1
- jaddle-0.2.3/tests/test_conic.py +533 -0
- {jaddle-0.2.2 → jaddle-0.2.3}/tests/test_convex.py +92 -21
- {jaddle-0.2.2 → jaddle-0.2.3}/tests/test_cvxpy.py +111 -6
- {jaddle-0.2.2 → jaddle-0.2.3}/tests/test_examples.py +5 -0
- {jaddle-0.2.2 → jaddle-0.2.3}/tests/test_linear.py +325 -48
- jaddle-0.2.2/jaddle/__init__.py +0 -1
- {jaddle-0.2.2 → jaddle-0.2.3}/LICENSE.txt +0 -0
- {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/highs_helpers.py +0 -0
- {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/jaddle_optimisers.py +0 -0
- {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/presolve.py +0 -0
- {jaddle-0.2.2 → jaddle-0.2.3}/jaddle.egg-info/dependency_links.txt +0 -0
- {jaddle-0.2.2 → jaddle-0.2.3}/jaddle.egg-info/requires.txt +0 -0
- {jaddle-0.2.2 → jaddle-0.2.3}/jaddle.egg-info/top_level.txt +0 -0
- {jaddle-0.2.2 → jaddle-0.2.3}/setup.cfg +0 -0
- {jaddle-0.2.2 → jaddle-0.2.3}/tests/test_presolve.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jaddle
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: First-order primal-dual solvers for large-scale linear and convex programs, written in JAX
|
|
5
5
|
Author: Brendan van Rooyen
|
|
6
6
|
License-Expression: MIT
|
|
@@ -115,7 +115,7 @@ differentiated with respect to the LP's data, and solves batch with `vmap`
|
|
|
115
115
|
|
|
116
116
|
**Composable optimisation primitives.** The convex solver builds on
|
|
117
117
|
[Optax](https://github.com/google-deepmind/optax). It uses Optax's projection
|
|
118
|
-
operators, and
|
|
118
|
+
operators, and with `adaptive_eta=False` it accepts any Optax gradient
|
|
119
119
|
transformation as the update rule. A new primal–dual variant is often only a
|
|
120
120
|
few lines of code: for example, giving the primal player plain gradient descent
|
|
121
121
|
and the dual player Nesterov momentum.
|
|
@@ -274,7 +274,9 @@ some refinements of our own.
|
|
|
274
274
|
iteration the step `η` is checked against a local estimate of the interaction
|
|
275
275
|
term `(Δy)ᵀ A (Δx)`. A step that overshoots is rejected and shrunk. An accepted
|
|
276
276
|
step is advanced conservatively. The step size learned during a solve is
|
|
277
|
-
carried across restarts.
|
|
277
|
+
carried across restarts. `adaptive_eta=False` turns the line search off and
|
|
278
|
+
holds `η` at `step_size` (plain PDHG; `step_size="auto"` picks a step that is
|
|
279
|
+
stable for the scaled problem).
|
|
278
280
|
|
|
279
281
|
**Primal weight.** The primal and dual step sizes are `η / k` and `η · k`. The
|
|
280
282
|
primal weight `k` is initialised from the ratio `‖c‖ / ‖b‖`. At each restart it
|
|
@@ -326,6 +328,7 @@ the dual objective as in PDLP, so far-away finite bounds do not swamp the gap.
|
|
|
326
328
|
| `restarts`, `epochs_per_restart` | `True`, `None` | Adaptive restarts and the length cap of a restart cycle (`None` = no cap). |
|
|
327
329
|
| `restart_decay`, `necessary_decay` | `0.2`, `0.8` | Thresholds for the sufficient-progress and stalling restarts. |
|
|
328
330
|
| `k_scale`, `k_theta`, `k_init` | `1e8`, `0.5`, `None` | Clamp band, smoothing and initial value for the primal weight. |
|
|
331
|
+
| `adaptive_eta`, `step_size` | `True`, `1.0` | Line search on the step `η` (off: `η` stays fixed), and the starting `η` (`"auto"` = `0.99/‖[[A, b], [cᵀ, 0]]‖₂`). |
|
|
329
332
|
| `diag_adapt`, `diag_theta`, `diag_clamp` | `True`, `0.5`, `10.0` | Per-coordinate diagonal step metric on top of the scaling, adapted at each restart (larger steps for steadily drifting coordinates, smaller for oscillating ones). |
|
|
330
333
|
| `ruiz_iterations`, `pc_iterations` | `10`, `1` | Number of preconditioning sweeps. |
|
|
331
334
|
| `initial_solution`, `initial_opt_state` | `None` | Warm start from a previous solve's `solution` and `opt_state`. |
|
|
@@ -348,36 +351,48 @@ the dual objective as in PDLP, so far-away finite bounds do not swamp the gap.
|
|
|
348
351
|
|
|
349
352
|
### Batched and differentiable solves
|
|
350
353
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
`
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
back). The function can be jitted, batched with `jax.vmap` and differentiated:
|
|
354
|
+
As in the convex front end, write a function `build_lp(params)` that returns
|
|
355
|
+
an ordinary `jl.LP` (or `JaddleLP`) whose numbers depend on `params`, any
|
|
356
|
+
pytree. `jl.make_solver(build_lp, **options)` turns `solve()` into a pure JAX
|
|
357
|
+
function of `params`. It takes the same options and reproduces `solve()`
|
|
358
|
+
exactly, but it has no logging, time limit or `vertex_bias`. The function
|
|
359
|
+
can be jitted, batched with `jax.vmap` and differentiated:
|
|
358
360
|
|
|
359
361
|
```python
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
362
|
+
def build_lp(price):
|
|
363
|
+
return jl.LP(c=price, A_eq=A_eq, b_eq=b_eq, A_ineq=A_ineq, b_ineq=b_ineq,
|
|
364
|
+
lower_bounds=lb, upper_bounds=ub)
|
|
363
365
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
+
solve_fn = jax.jit(jl.make_solver(build_lp, max_epochs=1000))
|
|
367
|
+
result = solve_fn(price) # SolveCoreResult: solution, converged, ...
|
|
368
|
+
batch = jax.vmap(solve_fn)(prices) # many LPs at once
|
|
366
369
|
```
|
|
367
370
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
The LPs of one function share a sparsity pattern, which is read from whatever
|
|
372
|
+
does not depend on `params` (`build_lp` runs under
|
|
373
|
+
`jax.ensure_compile_time_eval`, so this holds under `jit` too). SciPy matrices
|
|
374
|
+
and dense matrices that don't depend on `params` keep their nonzeros. A dense
|
|
375
|
+
matrix that does depend on them stores every entry. To reuse a `JaddleLP`'s
|
|
376
|
+
pattern with new numbers, `jl.make_solver(jaddle_lp.with_values)` takes a
|
|
377
|
+
`jl.LPValues` pytree as `params`: the cost, the stored nonzeros of `A_eq` and
|
|
378
|
+
`A_ineq`, the right-hand sides and the bounds (`jaddle_lp.values()` extracts
|
|
379
|
+
them).
|
|
380
|
+
|
|
381
|
+
In a batch the cost, right-hand sides, bounds and matrix values can all vary.
|
|
382
|
+
`jl.solve_batch(build_lp, params, in_axes=0)` is the jitted `vmap`. Each
|
|
383
|
+
member is scaled as `solve()` would scale it, and finished members wait for
|
|
384
|
+
the slowest. Members are not bit-identical to separate solves, because XLA
|
|
385
|
+
rounds batched reductions slightly differently, but each certifies to the
|
|
386
|
+
requested tolerance.
|
|
387
|
+
|
|
388
|
+
Three functions differentiate through a solve. Each takes `build_lp` and
|
|
389
|
+
carries its gradient back through it to `params`:
|
|
375
390
|
|
|
376
391
|
| Function | Differentiates | How |
|
|
377
392
|
|---|---|---|
|
|
378
393
|
| `jl.make_optimal_value` | the optimal value `z*` w.r.t. every number | envelope theorem: `∂z*/∂c = x*`, `∂z*/∂b = −y*`, `∂z*/∂A_ij = y*_i x*_j`, and the reduced costs for the bounds |
|
|
379
394
|
| `jl.make_solution` | `x*` w.r.t. `b`, `A` and the bounds | implicit differentiation of the active constraints at a nondegenerate vertex (raises an error elsewhere) |
|
|
380
|
-
| `jl.make_perturbed_solution` | a smoothed `x*` w.r.t. `c` | perturbed optimizer: averages solves at `c + σZ`, batched |
|
|
395
|
+
| `jl.make_perturbed_solution` | a smoothed `x*` w.r.t. `c` | perturbed optimizer: averages solves at `c + σZ`, batched; `x_fn(params, key)` |
|
|
381
396
|
|
|
382
397
|
An LP's solution is piecewise constant in its cost, so `dx*/dc` is zero
|
|
383
398
|
almost everywhere. That's why the smoothed, perturbed version exists for
|
|
@@ -431,6 +446,16 @@ tests.
|
|
|
431
446
|
dual_gap_tolerance=1e-4`: on stp3d (float64) that certifies in 24 s, where a
|
|
432
447
|
plain solve is still at PFR 5e-6 after 150 s.
|
|
433
448
|
`jl.solve_with_presolve(model, polish=True)` uses it for the reduced LP.
|
|
449
|
+
- **Iterative refinement.** `jl.solve_with_refinement` is `jl.solve` with
|
|
450
|
+
iterative refinement (Gleixner, Steffy & Wolter, 2016). A loose first solve
|
|
451
|
+
is followed by rounds of correction LPs: the same problem shifted to the
|
|
452
|
+
current point, with the residuals scaled up to O(1). Each correction
|
|
453
|
+
restarts the solver on a well-scaled problem in place of a long tail. Unlike
|
|
454
|
+
polishing, it tightens the gap along with feasibility: on stp3d (float64,
|
|
455
|
+
`tol=1e-8, dual_gap_tolerance=1e-4`) one round certified in about 94 s,
|
|
456
|
+
with the gap at 9.7e-9. `result["refinement"]` records the rounds. For
|
|
457
|
+
presolved models use
|
|
458
|
+
`jl.solve_with_presolve(model, reduced_solver=jl.solve_with_refinement)`.
|
|
434
459
|
- **Infeasibility detection.** `jl.detect_infeasibility` classifies an LP as
|
|
435
460
|
`"primal_infeasible"`, `"dual_infeasible"` (unbounded if feasible),
|
|
436
461
|
`"optimal"` or `"undetermined"` the PDLP way: diverging iterates give a
|
|
@@ -440,6 +465,67 @@ tests.
|
|
|
440
465
|
coefficient ranges, which is the first thing to check when an instance
|
|
441
466
|
misbehaves.
|
|
442
467
|
|
|
468
|
+
### Conic programs
|
|
469
|
+
|
|
470
|
+
The same solver handles second-order, positive semidefinite and exponential
|
|
471
|
+
cones. Pass `cones=jl.Cones(soc=..., psd=..., exp=...)` to `jl.LP` and the
|
|
472
|
+
last variables of `x` are constrained to cones, in the style of SCS's cone
|
|
473
|
+
dimensions:
|
|
474
|
+
|
|
475
|
+
```
|
|
476
|
+
minimise cᵀx
|
|
477
|
+
subject to A_eq x = b_eq, A_ineq x <= b_ineq, l <= x <= u (linear variables)
|
|
478
|
+
x[K₁] ∈ K₁, x[K₂] ∈ K₂, ... (the tail of x)
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
With `n` variables and `jl.Cones(soc=(3, 5), psd=(4,), exp=2)`, the first
|
|
482
|
+
`n − 24` are ordinary linear variables, the next 3 lie in a second-order cone
|
|
483
|
+
`{(t, z) : ‖z‖ ≤ t}`, the next 5 in another, the next 10 hold a 4 × 4 PSD
|
|
484
|
+
matrix in `svec` form (its upper triangle row by row, off-diagonals times √2;
|
|
485
|
+
`jaddle.cones.svec` and `smat` convert), and the last 6 are two exponential
|
|
486
|
+
cones `closure{(x, y, z) : y·exp(x/y) ≤ z, y > 0}`. Cone variables must be
|
|
487
|
+
unbounded. A conic constraint `b − Ax ∈ K` becomes `Ax + s = b` with slack
|
|
488
|
+
variables `s` in the cone.
|
|
489
|
+
|
|
490
|
+
```python
|
|
491
|
+
import numpy as np
|
|
492
|
+
import scipy.sparse as sp
|
|
493
|
+
import jaddle.jaddle_linear as jl
|
|
494
|
+
|
|
495
|
+
# minimise cᵀz subject to ‖z‖ <= 1, as (t, z) in a second-order cone, t = 1
|
|
496
|
+
c = np.array([1.0, -2.0, 2.0])
|
|
497
|
+
n = 1 + c.size
|
|
498
|
+
lp = jl.LP(
|
|
499
|
+
c=np.r_[0.0, c],
|
|
500
|
+
A_eq=sp.csc_matrix(np.eye(1, n)), b_eq=np.ones(1),
|
|
501
|
+
A_ineq=sp.csc_matrix((0, n)), b_ineq=np.zeros(0),
|
|
502
|
+
lower_bounds=np.full(n, -np.inf), upper_bounds=np.full(n, np.inf),
|
|
503
|
+
cones=jl.Cones(soc=(n,)),
|
|
504
|
+
)
|
|
505
|
+
z = jl.solve(lp)["solution"].primal[1:] # -c / ‖c‖
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Each step projects the cone variables onto their cones instead of onto a box:
|
|
509
|
+
a closed form for second-order cones, one batched eigendecomposition per PSD
|
|
510
|
+
size, and for exponential cones Friberg's univariate root search (as in SCS
|
|
511
|
+
and Clarabel). The dual condition on a cone block is that its reduced cost
|
|
512
|
+
lies in the dual cone `K*`, and the certificate measures dual infeasibility as
|
|
513
|
+
the reduced cost's distance to `K*`, computed from the projection onto `K` as
|
|
514
|
+
`−Π_K(−r)`. (The second-order and PSD cones are self-dual; the exponential
|
|
515
|
+
cone is not.) The exponential cone's projection is built for float64. It is
|
|
516
|
+
also the expensive part of an exponential-cone solve: on GPU it runs as a
|
|
517
|
+
sequence of small kernels each iteration, so such problems are slower per
|
|
518
|
+
iteration than SOC or PSD ones. Scaling, the
|
|
519
|
+
diagonal metric and restarts work as for an LP, with column scales tied to one
|
|
520
|
+
value per cone block. `jl.solve`, `make_solver`, `solve_batch` and
|
|
521
|
+
`make_optimal_value` (differentiable optimal values) all take conic problems;
|
|
522
|
+
presolve, crossover, polishing, iterative refinement, infeasibility
|
|
523
|
+
detection and `make_solution` do not yet, and raise `NotImplementedError`.
|
|
524
|
+
CVXPY problems with
|
|
525
|
+
second-order, PSD and exponential constraints go through
|
|
526
|
+
`jcv.JaddleConicSolver` (see
|
|
527
|
+
[Modelling with CVXPY](#modelling-with-cvxpy)).
|
|
528
|
+
|
|
443
529
|
## The convex programming solver
|
|
444
530
|
|
|
445
531
|
### Problem form
|
|
@@ -467,24 +553,44 @@ automatic differentiation. No derivatives are required from the user.
|
|
|
467
553
|
|
|
468
554
|
| Mode | Gradient evaluations per iteration | Description |
|
|
469
555
|
|---|---|---|
|
|
470
|
-
| `"alternating"` (default) | 1 | Primal step, then a dual step at the new primal point.
|
|
556
|
+
| `"alternating"` (default) | 1 | Primal step, then a dual step at the new primal point: PDHG without the extrapolation. With `adaptive_eta=False` the update rule is a user-supplied Optax optimiser. Best used with restarts. |
|
|
471
557
|
| `"extragradient"` | 2 | Korpelevich extragradient: a look-ahead step followed by a corrector step. Contractive for monotone problems. |
|
|
472
|
-
| `"
|
|
558
|
+
| `"pdhg"` | 1 | PDHG as in the LP solver: a primal step, then a dual step that reads the extrapolated constraint values `2g(x⁺) − g(x)`. One gradient plus one constraint evaluation per iteration. Requires the adaptive step. |
|
|
473
559
|
|
|
474
|
-
**Adaptive step size.** With `adaptive_eta=
|
|
475
|
-
|
|
476
|
-
based on a local Lipschitz estimate. The two gradient evaluations that
|
|
560
|
+
**Adaptive step size.** With `adaptive_eta=True` (the default, as in the LP
|
|
561
|
+
solver), every scheme uses a Malitsky–Tam line search, started from
|
|
562
|
+
`step_size` and based on a local Lipschitz estimate. The two gradient evaluations that
|
|
477
563
|
extragradient already performs give an estimate of the local Lipschitz
|
|
478
|
-
constant at no extra cost.
|
|
479
|
-
|
|
480
|
-
|
|
564
|
+
constant at no extra cost. PDHG and alternating get their estimate from
|
|
565
|
+
consecutive iterations, so they check each step one iteration late and undo
|
|
566
|
+
it if it overshot. The
|
|
567
|
+
step is accepted while it remains within the admissible bound, and is rejected
|
|
568
|
+
and shrunk otherwise. The learned step is carried across restarts.
|
|
481
569
|
|
|
482
570
|
**Primal weight.** As in the LP solver, a primal weight `k` balances the primal
|
|
483
571
|
and dual step sizes. It is initialised from the gradient of the objective and
|
|
484
572
|
the constraint values at the origin, rebalanced at each restart, and clamped
|
|
485
|
-
to `[1/k_scale, k_scale]` (default `k_scale =
|
|
486
|
-
|
|
487
|
-
|
|
573
|
+
to `[1/k_scale, k_scale]` (default `k_scale = 100`; the LP solver's `1e8`
|
|
574
|
+
let `k` swing far enough to collapse the adaptive step on some problems). It
|
|
575
|
+
starts at 1 when the objective gradient or the constraint values at the origin
|
|
576
|
+
are zero.
|
|
577
|
+
|
|
578
|
+
**Diagonal metric.** As in the LP solver, `diag_adapt=True` (the default)
|
|
579
|
+
adapts a per-coordinate step metric at each restart: larger steps for
|
|
580
|
+
steadily drifting coordinates, smaller for oscillating ones. It applies to all
|
|
581
|
+
three schemes with the adaptive step, and has no effect without restarts or
|
|
582
|
+
with a custom optimiser.
|
|
583
|
+
|
|
584
|
+
**Gradient scaling.** The solver sees a problem only through its functions,
|
|
585
|
+
so it cannot equilibrate a matrix. Instead, `gradient_scaling=True` (the
|
|
586
|
+
default) rescales the objective and each constraint row by the size of its
|
|
587
|
+
gradient at the start point, as IPOPT does. This fixes objectives and rows in
|
|
588
|
+
mismatched units, but not variables in mismatched units. The convergence test
|
|
589
|
+
and the returned duals stay in the problem's own units.
|
|
590
|
+
`jc.quadratic_program` scales its matrices itself, so it turns gradient
|
|
591
|
+
scaling off unless asked.
|
|
592
|
+
|
|
593
|
+
**Custom optimisers.** With `adaptive_eta=False`, `optimiser` accepts any Optax
|
|
488
594
|
`GradientTransformation`. It is applied separately to the primal and dual
|
|
489
595
|
players through `jaddle_optimisers.create_saddle_optimiser`. Ready-made
|
|
490
596
|
choices include `jo.gd`, `jo.optimistic_gd`, `jo.optimistic_adadelta` and
|
|
@@ -495,7 +601,7 @@ optional `weight_function`) is on by default. As in the LP solver,
|
|
|
495
601
|
`report_best=True` (the default) tests convergence on, and returns, whichever
|
|
496
602
|
of the average and the last iterate has the lower KKT merit each epoch, so
|
|
497
603
|
averaging never holds back a last iterate that is already converging. Adaptive
|
|
498
|
-
restarts (`restarts=True`) are
|
|
604
|
+
restarts (`restarts=True`) are on by default, as in the LP solver.
|
|
499
605
|
|
|
500
606
|
**Termination.** The solver stops when all three of the following fall below
|
|
501
607
|
their tolerances:
|
|
@@ -511,12 +617,14 @@ their tolerances:
|
|
|
511
617
|
| Argument | Default | Purpose |
|
|
512
618
|
|---|---|---|
|
|
513
619
|
| `update_mode` | `"alternating"` | Iteration scheme (see above). |
|
|
514
|
-
| `adaptive_eta` | `
|
|
515
|
-
| `optimiser` | `None` | Optax optimiser for `
|
|
516
|
-
| `iterations_per_epoch` | `
|
|
620
|
+
| `adaptive_eta`, `step_size` | `True`, `1.0` | Line search on the step `η`, and the starting `η`. With `adaptive_eta=False` the optimiser's fixed step is used instead. |
|
|
621
|
+
| `optimiser` | `None` | Optax optimiser for the fixed step (`adaptive_eta=False`). When `None`, gradient descent with learning rate `step_size` is used. |
|
|
622
|
+
| `iterations_per_epoch` | `256` | Iterations per compiled epoch. |
|
|
517
623
|
| `max_epochs`, `max_seconds` | `None` | Iteration and wall-clock budgets. |
|
|
518
|
-
| `k_scale`, `k_theta`, `k_init` | `
|
|
519
|
-
| `restarts`, `epochs_per_restart` | `
|
|
624
|
+
| `k_scale`, `k_theta`, `k_init` | `100.0`, `0.5`, `None` | Primal weight controls. `k_scale=None` disables the primal weight. |
|
|
625
|
+
| `restarts`, `epochs_per_restart` | `True`, `None` | Adaptive restarts (`epochs_per_restart=None` = no cycle cap). |
|
|
626
|
+
| `diag_adapt`, `diag_theta`, `diag_clamp` | `True`, `0.5`, `10.0` | Per-coordinate diagonal step metric, adapted at each restart. |
|
|
627
|
+
| `gradient_scaling` | `True` | Rescale the objective and constraint rows by their gradients at the start point. |
|
|
520
628
|
| `initial_solution`, `initial_opt_state` | `None` | Warm start. |
|
|
521
629
|
|
|
522
630
|
`solve` returns a dictionary containing `solution` (a `SaddleState`),
|
|
@@ -611,8 +719,8 @@ argument, a function of the solver state that returns the three quantities
|
|
|
611
719
|
## Modelling with CVXPY
|
|
612
720
|
|
|
613
721
|
[CVXPY](https://www.cvxpy.org/) can serve as the modelling language
|
|
614
|
-
(`pip install "jaddle[cvxpy]"`). `jaddle.jaddle_cvxpy` offers
|
|
615
|
-
|
|
722
|
+
(`pip install "jaddle[cvxpy]"`). `jaddle.jaddle_cvxpy` offers three routes, and
|
|
723
|
+
all return results the CVXPY way: `problem.status`, `problem.value`,
|
|
616
724
|
`var.value`, each constraint's `dual_value`, and Jaddle's full result in
|
|
617
725
|
`problem.solver_stats.extra_stats`.
|
|
618
726
|
|
|
@@ -626,6 +734,10 @@ w = cp.Variable(p)
|
|
|
626
734
|
lasso = cp.Problem(cp.Minimize(cp.sum_squares(X @ w - y) + cp.norm1(w)))
|
|
627
735
|
lasso.solve(solver=jcv.JaddleSolver(), tol=1e-6)
|
|
628
736
|
|
|
737
|
+
# Second-order and PSD cones: Jaddle as a CVXPY conic solver.
|
|
738
|
+
risk_limited = cp.Problem(cp.Maximize(mu @ w), [cp.sum(w) == 1, cp.norm(F.T @ w) <= 0.2])
|
|
739
|
+
risk_limited.solve(solver=jcv.JaddleConicSolver(), tol=1e-6)
|
|
740
|
+
|
|
629
741
|
# Any other DCP problem: compiled to a JaddleCP.
|
|
630
742
|
logistic = cp.Problem(cp.Minimize(cp.sum(cp.logistic(-cp.multiply(labels, X @ w)))))
|
|
631
743
|
jcv.solve(logistic, tol=1e-6)
|
|
@@ -640,18 +752,46 @@ tolerance on the original problem is reported as `"optimal_inaccurate"`.
|
|
|
640
752
|
[`examples/lp/cvxpy_transport.py`](https://github.com/BrendanVR/Jaddle/blob/main/examples/lp/cvxpy_transport.py)
|
|
641
753
|
is a small worked LP.
|
|
642
754
|
|
|
755
|
+
**`JaddleConicSolver`** is a CVXPY conic solver for problems CVXPY can put in
|
|
756
|
+
the form `min cᵀx s.t. Ax + s = b`, with `s` in a product of zero,
|
|
757
|
+
nonnegative, second-order, PSD and exponential cones. It solves them with
|
|
758
|
+
`jl.solve` on cone-constrained variables (see [Conic programs](#conic-programs));
|
|
759
|
+
there is no presolve. When a cone constraint is on a variable itself, such
|
|
760
|
+
as `X >> 0` for a variable `X`, the cone goes on that variable directly
|
|
761
|
+
rather than on a copy tied to it by equality rows: on a 200-node Max-Cut SDP
|
|
762
|
+
that cut the iterations threefold (`cone_substitution=False` turns it off).
|
|
763
|
+
Power cones are not supported. Exponential cones bring
|
|
764
|
+
in `log`, `log_det`, `log1p`, `rel_entr` and `kl_div`, which the compiled
|
|
765
|
+
route below cannot take, alongside `exp`, `log_sum_exp`, `logistic` and
|
|
766
|
+
`entr`.
|
|
767
|
+
[`examples/conic/cvxpy_portfolio.py`](https://github.com/BrendanVR/Jaddle/blob/main/examples/conic/cvxpy_portfolio.py)
|
|
768
|
+
traces a risk-limited portfolio's efficient frontier, and
|
|
769
|
+
[`examples/conic/cvxpy_maxcut.py`](https://github.com/BrendanVR/Jaddle/blob/main/examples/conic/cvxpy_maxcut.py)
|
|
770
|
+
solves the Goemans–Williamson Max-Cut SDP and rounds it to a cut, and
|
|
771
|
+
[`examples/conic/cvxpy_kelly.py`](https://github.com/BrendanVR/Jaddle/blob/main/examples/conic/cvxpy_kelly.py)
|
|
772
|
+
finds log-optimal (Kelly) portfolios, one exponential cone per scenario.
|
|
773
|
+
|
|
643
774
|
**`jcv.solve`** handles the rest. For a general convex problem CVXPY's own
|
|
644
|
-
reductions produce cones
|
|
645
|
-
|
|
775
|
+
reductions produce cones, and the cone form is often a poor fit for a
|
|
776
|
+
first-order method:
|
|
646
777
|
on the L1-logistic regression in
|
|
647
778
|
[`examples/cp/cvxpy_lasso.py`](https://github.com/BrendanVR/Jaddle/blob/main/examples/cp/cvxpy_lasso.py) it has 701
|
|
648
779
|
variables, 1500 rows and 400 exponential cones. `jcv.compile_problem` instead
|
|
649
780
|
walks the expression tree and builds a `JaddleCP` of JAX functions, with 101
|
|
650
|
-
variables and 100 linear rows for that problem
|
|
781
|
+
variables and 100 linear rows for that problem.
|
|
782
|
+
[`examples/cp/cvxpy_robust_kalman.py`](https://github.com/BrendanVR/Jaddle/blob/main/examples/cp/cvxpy_robust_kalman.py)
|
|
783
|
+
compiles another such problem: a Kalman smoother with `huber(norm(v[t]))` on the measurement
|
|
784
|
+
noise, which CVXPY would turn into second-order cones. The compiled route
|
|
785
|
+
handles these atoms as follows:
|
|
651
786
|
|
|
652
787
|
- smooth atoms stay smooth: `logistic`, `log_sum_exp`, `exp`, `huber`,
|
|
653
788
|
`sum_squares`, `quad_form`, `quad_over_lin` with a constant denominator,
|
|
654
789
|
even powers, and p-norms with p ≥ 1;
|
|
790
|
+
- `entr` is continued below `jcv.ENTR_FLOOR` (1e-8) by its tangent line, so
|
|
791
|
+
iterates outside its domain see a finite value and gradient. This is exact
|
|
792
|
+
when the solution keeps the argument above the floor.
|
|
793
|
+
[`examples/cp/cvxpy_channel_capacity.py`](https://github.com/BrendanVR/Jaddle/blob/main/examples/cp/cvxpy_channel_capacity.py)
|
|
794
|
+
uses it to compute a channel's capacity;
|
|
655
795
|
- piecewise-linear atoms (`abs`, `maximum`, `max`, `norm1`, `norm_inf`, and
|
|
656
796
|
their concave mirrors `minimum` and `min`; `pos` and `neg` are written with
|
|
657
797
|
these) become epigraph variables with one linear row per piece. This is
|
|
@@ -661,9 +801,10 @@ variables and 100 linear rows for that problem:
|
|
|
661
801
|
costs no dual. The bound constraints' duals are recovered from the reduced
|
|
662
802
|
gradient.
|
|
663
803
|
|
|
664
|
-
|
|
665
|
-
or PSD constraints raise `NotImplementedError
|
|
666
|
-
inside the box only, so they would leave the domain.
|
|
804
|
+
Other atoms with a restricted domain (`log`, `sqrt`, ...) and second-order
|
|
805
|
+
or PSD constraints raise `NotImplementedError` on this route: the convex
|
|
806
|
+
solver keeps its iterates inside the box only, so they would leave the domain.
|
|
807
|
+
Second-order and PSD constraints go to `JaddleConicSolver` instead. A problem that CVXPY can
|
|
667
808
|
put in QP form goes to `JaddleSolver` unless `qp_route=False`, so it gets
|
|
668
809
|
presolve or KKT scaling. The compiled problem is solved by `jc.solve` with
|
|
669
810
|
adaptive extragradient, restarts and gradient scaling (`jcv.COMPILED_DEFAULTS`).
|
|
@@ -695,30 +836,32 @@ recompilation.
|
|
|
695
836
|
Jaddle's LP solver is evaluated on the LP relaxations of the 383 MIPLIB 2017
|
|
696
837
|
instances used in the PDLP papers (100k–10M nonzeros). The run below used
|
|
697
838
|
default solver settings in float64, HiGHS presolve, a relative tolerance of
|
|
698
|
-
`1e-4` and a
|
|
839
|
+
`1e-4` and a 3600-second limit per instance, on one NVIDIA GeForce RTX 5070 Ti
|
|
699
840
|
Laptop GPU.
|
|
700
841
|
|
|
701
842
|
| Outcome | Instances |
|
|
702
843
|
|---|---|
|
|
703
|
-
| Certified optimal on the HiGHS-presolved LP | **
|
|
704
|
-
| Certified optimal on the original LP, after HiGHS postsolve | **
|
|
705
|
-
| Time limit reached |
|
|
844
|
+
| Certified optimal on the HiGHS-presolved LP | **382 / 383** (one settled by presolve itself) |
|
|
845
|
+
| Certified optimal on the original LP, after HiGHS postsolve | **297 / 383** |
|
|
846
|
+
| Time limit reached | 1 (hgms62) |
|
|
706
847
|
|
|
707
848
|
The two certificates differ because postsolve can enlarge a first-order
|
|
708
849
|
solution's small dual errors, and because presolve can rescale a problem
|
|
709
850
|
drastically, which changes what a relative tolerance means (see
|
|
710
|
-
*Presolve and postsolve* above). `jl.solve_with_polishing
|
|
711
|
-
`jl.crossover` can tighten a postsolved point when the original certificate
|
|
851
|
+
*Presolve and postsolve* above). `jl.solve_with_polishing`,
|
|
852
|
+
`jl.solve_with_refinement` and `jl.crossover` can tighten a postsolved point when the original certificate
|
|
712
853
|
matters.
|
|
713
854
|
|
|
855
|
+
Convergence on hgms62 can be recovered by altering the solvers default settings, in particular greatly increasing `diag_clamp` which alters the maximum amount of adaptive diagonal scaling used by the solver.
|
|
856
|
+
|
|
714
857
|
Shifted geometric means of the solve time (shift 10 s, unsolved instances
|
|
715
|
-
counted at the
|
|
858
|
+
counted at the 3600-second limit, as in the PDLP papers):
|
|
716
859
|
|
|
717
860
|
| Time measured | SGM10 |
|
|
718
861
|
|---|---|
|
|
719
|
-
| Iteration loop, including the first-epoch compile (`solve_seconds`) |
|
|
720
|
-
| Iteration loop with the compile amortised out (`corrected_seconds`) | 4.
|
|
721
|
-
| Wall clock, including presolve, scaling and setup | 9.
|
|
862
|
+
| Iteration loop, including the first-epoch compile (`solve_seconds`) | 6.86 s |
|
|
863
|
+
| Iteration loop with the compile amortised out (`corrected_seconds`) | 4.72 s |
|
|
864
|
+
| Wall clock, including presolve, scaling and setup | 9.56 s |
|
|
722
865
|
|
|
723
866
|
The benchmark harnesses, instance diagnostics and instructions for reproducing
|
|
724
867
|
these results are described in [`benchmarks/README.md`](https://github.com/BrendanVR/Jaddle/blob/main/benchmarks/README.md).
|
|
@@ -729,12 +872,13 @@ these results are described in [`benchmarks/README.md`](https://github.com/Brend
|
|
|
729
872
|
|---|---|
|
|
730
873
|
| [`jaddle/jaddle_linear.py`](https://github.com/BrendanVR/Jaddle/blob/main/jaddle/jaddle_linear.py) | LP solver, scaling, certification and polishing |
|
|
731
874
|
| [`jaddle/jaddle_convex.py`](https://github.com/BrendanVR/Jaddle/blob/main/jaddle/jaddle_convex.py) | Convex solver |
|
|
875
|
+
| [`jaddle/cones.py`](https://github.com/BrendanVR/Jaddle/blob/main/jaddle/cones.py) | Second-order and PSD cones for the LP solver |
|
|
732
876
|
| [`jaddle/jaddle_cvxpy.py`](https://github.com/BrendanVR/Jaddle/blob/main/jaddle/jaddle_cvxpy.py) | CVXPY front end |
|
|
733
877
|
| [`jaddle/jaddle_basic_types.py`](https://github.com/BrendanVR/Jaddle/blob/main/jaddle/jaddle_basic_types.py) | `SaddleState`, `JaddleCP`, `LP` and `JaddleLP` |
|
|
734
878
|
| [`jaddle/jaddle_optimisers.py`](https://github.com/BrendanVR/Jaddle/blob/main/jaddle/jaddle_optimisers.py) | Precision profiles and Optax saddle-point optimisers |
|
|
735
879
|
| [`jaddle/presolve.py`](https://github.com/BrendanVR/Jaddle/blob/main/jaddle/presolve.py) | Defined-variable elimination and postsolve |
|
|
736
880
|
| [`jaddle/highs_helpers.py`](https://github.com/BrendanVR/Jaddle/blob/main/jaddle/highs_helpers.py) | HiGHS converters and presolve interface |
|
|
737
|
-
| [`examples/`](https://github.com/BrendanVR/Jaddle/blob/main/examples/README.md) | Worked LP and convex examples |
|
|
881
|
+
| [`examples/`](https://github.com/BrendanVR/Jaddle/blob/main/examples/README.md) | Worked LP, conic and convex examples |
|
|
738
882
|
| [`benchmarks/`](https://github.com/BrendanVR/Jaddle/blob/main/benchmarks/README.md) | MIPLIB and convex benchmark harnesses |
|
|
739
883
|
| [`tests/`](https://github.com/BrendanVR/Jaddle/tree/main/tests) | Test suite (`pytest`) |
|
|
740
884
|
| [`CHANGELOG.md`](https://github.com/BrendanVR/Jaddle/blob/main/CHANGELOG.md) | Changes in each release |
|