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.
Files changed (26) hide show
  1. {jaddle-0.2.2 → jaddle-0.2.3}/PKG-INFO +203 -59
  2. {jaddle-0.2.2 → jaddle-0.2.3}/README.md +202 -58
  3. jaddle-0.2.3/jaddle/__init__.py +1 -0
  4. jaddle-0.2.3/jaddle/cones.py +534 -0
  5. {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/jaddle_basic_types.py +51 -5
  6. {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/jaddle_convex.py +259 -187
  7. {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/jaddle_cvxpy.py +507 -41
  8. {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/jaddle_linear.py +1062 -392
  9. {jaddle-0.2.2 → jaddle-0.2.3}/jaddle.egg-info/PKG-INFO +203 -59
  10. {jaddle-0.2.2 → jaddle-0.2.3}/jaddle.egg-info/SOURCES.txt +2 -0
  11. {jaddle-0.2.2 → jaddle-0.2.3}/pyproject.toml +1 -1
  12. jaddle-0.2.3/tests/test_conic.py +533 -0
  13. {jaddle-0.2.2 → jaddle-0.2.3}/tests/test_convex.py +92 -21
  14. {jaddle-0.2.2 → jaddle-0.2.3}/tests/test_cvxpy.py +111 -6
  15. {jaddle-0.2.2 → jaddle-0.2.3}/tests/test_examples.py +5 -0
  16. {jaddle-0.2.2 → jaddle-0.2.3}/tests/test_linear.py +325 -48
  17. jaddle-0.2.2/jaddle/__init__.py +0 -1
  18. {jaddle-0.2.2 → jaddle-0.2.3}/LICENSE.txt +0 -0
  19. {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/highs_helpers.py +0 -0
  20. {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/jaddle_optimisers.py +0 -0
  21. {jaddle-0.2.2 → jaddle-0.2.3}/jaddle/presolve.py +0 -0
  22. {jaddle-0.2.2 → jaddle-0.2.3}/jaddle.egg-info/dependency_links.txt +0 -0
  23. {jaddle-0.2.2 → jaddle-0.2.3}/jaddle.egg-info/requires.txt +0 -0
  24. {jaddle-0.2.2 → jaddle-0.2.3}/jaddle.egg-info/top_level.txt +0 -0
  25. {jaddle-0.2.2 → jaddle-0.2.3}/setup.cfg +0 -0
  26. {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.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 in `alternating` mode it accepts any Optax gradient
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
- `jl.make_solver(lp, **options)` turns `solve()` into a pure JAX function of
352
- the LP's numbers. It takes the same options and reproduces `solve()` exactly,
353
- but it has no logging, time limit or `vertex_bias`. The numbers travel as a
354
- `jl.LPValues` pytree: the cost, the stored nonzeros of `A_eq` and `A_ineq` on
355
- `lp`'s sparsity pattern, the right-hand sides and the bounds
356
- (`jaddle_lp.values()` extracts them, `jaddle_lp.with_values(v)` puts them
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
- values = jl.to_jaddle_sparse(lp).values()
361
- solve_fn = jl.make_solver(lp, max_epochs=1000)
362
- result = jax.jit(solve_fn)(values) # SolveCoreResult: solution, converged, ...
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
- costs = values.c * (1 + 0.1 * jax.random.normal(key, (32,) + values.c.shape))
365
- batch = jl.solve_batch(lp, values._replace(c=costs)) # 32 solves at once
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
- In a batch the cost, right-hand sides, bounds and matrix values can all vary;
369
- any field without a batch axis is shared. Each member is scaled as `solve()`
370
- would scale it, and finished members wait for the slowest. Members are not
371
- bit-identical to separate solves, because XLA rounds batched reductions
372
- slightly differently, but each certifies to the requested tolerance.
373
-
374
- Three functions differentiate through a solve:
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. The update rule is a user-supplied Optax optimiser. |
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
- | `"forward_reflected"` | 1 | Malitsky–Tam forward-reflected-backward: one gradient per iteration plus a reflection term built from the previous gradient. Requires the adaptive step. |
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="auto"` (the default), the
475
- extragradient and forward-reflected schemes use a Malitsky–Tam line search
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. The step is accepted while it remains within the
479
- admissible bound, and is rejected and shrunk otherwise. The learned step is
480
- carried across restarts.
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 = 10`).
486
-
487
- **Custom optimisers.** In `alternating` mode, `optimiser` accepts any Optax
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 available but off by default.
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` | `"auto"` | Adaptive line-searched step. Pass a float to set the seed, or `None` to use the optimiser's fixed learning rate. |
515
- | `optimiser` | `None` | Optax optimiser for `alternating` mode. When `None`, gradient descent with learning rate 0.5 is used. |
516
- | `iterations_per_epoch` | `1000` | Iterations per compiled 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` | `10.0`, `0.5`, `None` | Primal weight controls. `k_scale=None` disables the primal weight. |
519
- | `restarts`, `epochs_per_restart` | `False`, `None` | Adaptive restarts (`epochs_per_restart=None` = no cycle cap). |
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 two routes, and
615
- both return results the CVXPY way: `problem.status`, `problem.value`,
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 (exponential, second-order, PSD), and Jaddle has no
645
- cone projections. The cone form is also a poor fit for a first-order method:
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
- Atoms with a restricted domain (`log`, `sqrt`, `entr`, ...) and second-order
665
- or PSD constraints raise `NotImplementedError`: Jaddle keeps its iterates
666
- inside the box only, so they would leave the domain. A problem that CVXPY can
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 600-second limit per instance, on one NVIDIA GeForce RTX 5070 Ti
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 | **380 / 383** (one settled by presolve itself) |
704
- | Certified optimal on the original LP, after HiGHS postsolve | **291 / 383** |
705
- | Time limit reached | 3 |
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` and
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 600-second limit, as in the PDLP papers):
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`) | 7.04 s |
720
- | Iteration loop with the compile amortised out (`corrected_seconds`) | 4.62 s |
721
- | Wall clock, including presolve, scaling and setup | 9.81 s |
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 |