jaddle 0.1.0__py3-none-any.whl
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/__init__.py +1 -0
- jaddle/glop_helpers.py +112 -0
- jaddle/highs_helpers.py +90 -0
- jaddle/jaddle_basic_types.py +272 -0
- jaddle/jaddle_convex.py +1340 -0
- jaddle/jaddle_linear.py +3224 -0
- jaddle/jaddle_optimisers.py +223 -0
- jaddle/presolve.py +242 -0
- jaddle/sciopt_helpers.py +176 -0
- jaddle-0.1.0.dist-info/METADATA +464 -0
- jaddle-0.1.0.dist-info/RECORD +14 -0
- jaddle-0.1.0.dist-info/WHEEL +5 -0
- jaddle-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
- jaddle-0.1.0.dist-info/top_level.txt +1 -0
jaddle/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
jaddle/glop_helpers.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import scipy.sparse as sp
|
|
9
|
+
from ortools.linear_solver import linear_solver_pb2
|
|
10
|
+
|
|
11
|
+
from jaddle.highs_helpers import rows_to_standard_form
|
|
12
|
+
|
|
13
|
+
# The OR-Tools Python bindings don't expose glop's presolver, so it runs in a
|
|
14
|
+
# small C++ helper (tools/glop_presolve, built with tools/glop_presolve/build.sh).
|
|
15
|
+
# Override its location with the JADDLE_GLOP_PRESOLVE environment variable.
|
|
16
|
+
GLOP_PRESOLVE_BINARY = os.environ.get(
|
|
17
|
+
"JADDLE_GLOP_PRESOLVE",
|
|
18
|
+
os.path.join(
|
|
19
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
20
|
+
"tools",
|
|
21
|
+
"glop_presolve",
|
|
22
|
+
"build",
|
|
23
|
+
"glop_presolve",
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def glop_presolve(path: str, verbose: bool = False):
|
|
29
|
+
"""
|
|
30
|
+
Reads an MPS file, relaxes integrality and presolves the LP relaxation with
|
|
31
|
+
OR-Tools' glop presolver (MainLpPreprocessor, with PDLP's settings: no
|
|
32
|
+
dualization, no implied-free preprocessor, no scaling).
|
|
33
|
+
|
|
34
|
+
Returns: (LP, offset, info) where ``offset`` is the constant objective term
|
|
35
|
+
outside ``c`` in minimisation form (min c^T x + offset; for a maximisation
|
|
36
|
+
model the original objective is ``-(c^T x + offset)``, as in
|
|
37
|
+
``sciopt_helpers.scip_to_standard_form_sparse``) and ``info`` is the
|
|
38
|
+
helper's report (status, original/presolved sizes, read/presolve seconds).
|
|
39
|
+
|
|
40
|
+
Raises RuntimeError if glop's presolve already decided the problem (status
|
|
41
|
+
other than INIT, e.g. PRIMAL_INFEASIBLE), since no LP is left to solve.
|
|
42
|
+
"""
|
|
43
|
+
if not os.path.exists(GLOP_PRESOLVE_BINARY):
|
|
44
|
+
raise FileNotFoundError(
|
|
45
|
+
f"glop_presolve helper not found at {GLOP_PRESOLVE_BINARY}; "
|
|
46
|
+
"build it with tools/glop_presolve/build.sh"
|
|
47
|
+
)
|
|
48
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
49
|
+
out_path = os.path.join(tmp, "presolved.pb")
|
|
50
|
+
cmd = [GLOP_PRESOLVE_BINARY, path, out_path]
|
|
51
|
+
if verbose:
|
|
52
|
+
cmd.insert(1, "--verbose")
|
|
53
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
54
|
+
if proc.returncode != 0:
|
|
55
|
+
raise RuntimeError(
|
|
56
|
+
f"glop_presolve failed on {path} (exit {proc.returncode}): "
|
|
57
|
+
f"{proc.stderr.strip()}"
|
|
58
|
+
)
|
|
59
|
+
lines = proc.stdout.strip().splitlines()
|
|
60
|
+
if verbose:
|
|
61
|
+
print("\n".join(lines[:-1]))
|
|
62
|
+
sys.stdout.flush()
|
|
63
|
+
info = json.loads(lines[-1])
|
|
64
|
+
if info["status"] != "INIT":
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
f"glop presolve decided {path} outright (status {info['status']})"
|
|
67
|
+
)
|
|
68
|
+
model = linear_solver_pb2.MPModelProto()
|
|
69
|
+
with open(out_path, "rb") as f:
|
|
70
|
+
model.ParseFromString(f.read())
|
|
71
|
+
|
|
72
|
+
lp, offset = mpmodel_to_standard_form_sparse(
|
|
73
|
+
model, scaling_factor=info["objective_scaling_factor"]
|
|
74
|
+
)
|
|
75
|
+
return lp, offset, info
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def mpmodel_to_standard_form_sparse(model, scaling_factor: float = 1.0):
|
|
79
|
+
"""
|
|
80
|
+
Converts an MPModelProto (linear constraints only) to Jaddle's standard
|
|
81
|
+
form. Maximisation is negated into minimisation. ``scaling_factor`` is the
|
|
82
|
+
glop objective scaling factor (true objective = factor * (c^T x + offset)),
|
|
83
|
+
which MPModelProto can't store; it is folded into ``c`` and the offset.
|
|
84
|
+
|
|
85
|
+
Returns: (LP, offset).
|
|
86
|
+
"""
|
|
87
|
+
if len(model.general_constraint) or model.HasField("quadratic_objective"):
|
|
88
|
+
raise ValueError("model has non-linear parts; cannot export to a Jaddle LP")
|
|
89
|
+
|
|
90
|
+
sense = -1.0 if model.maximize else 1.0
|
|
91
|
+
scale = sense * scaling_factor
|
|
92
|
+
variables = model.variable
|
|
93
|
+
c = scale * np.array([v.objective_coefficient for v in variables], dtype=np.float64)
|
|
94
|
+
lower_bounds = np.array([v.lower_bound for v in variables], dtype=np.float64)
|
|
95
|
+
upper_bounds = np.array([v.upper_bound for v in variables], dtype=np.float64)
|
|
96
|
+
offset = scale * model.objective_offset
|
|
97
|
+
|
|
98
|
+
cons = model.constraint
|
|
99
|
+
row_nnz = np.array([len(k.var_index) for k in cons], dtype=np.int64)
|
|
100
|
+
indptr = np.concatenate([[0], np.cumsum(row_nnz)])
|
|
101
|
+
indices = np.fromiter(
|
|
102
|
+
(j for k in cons for j in k.var_index), dtype=np.int64, count=indptr[-1]
|
|
103
|
+
)
|
|
104
|
+
data = np.fromiter(
|
|
105
|
+
(a for k in cons for a in k.coefficient), dtype=np.float64, count=indptr[-1]
|
|
106
|
+
)
|
|
107
|
+
A = sp.csr_matrix((data, indices, indptr), shape=(len(cons), len(variables)))
|
|
108
|
+
row_lower = np.array([k.lower_bound for k in cons], dtype=np.float64)
|
|
109
|
+
row_upper = np.array([k.upper_bound for k in cons], dtype=np.float64)
|
|
110
|
+
|
|
111
|
+
lp = rows_to_standard_form(c, A, row_lower, row_upper, lower_bounds, upper_bounds)
|
|
112
|
+
return lp, float(offset)
|
jaddle/highs_helpers.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.sparse as sp
|
|
3
|
+
import highspy as hspy
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
import jax.experimental.sparse as jsp
|
|
6
|
+
from jaddle.jaddle_basic_types import JaddleLP, LP
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def highs_to_standard_form_sparse(lp: hspy.HighsLp):
|
|
10
|
+
"""
|
|
11
|
+
Converts a HighsLp object to standard form matrices:
|
|
12
|
+
min c^T x
|
|
13
|
+
s.t. A_eq x = b_eq
|
|
14
|
+
A_ineq x <= b_ineq
|
|
15
|
+
x >= lower_bounds
|
|
16
|
+
x <= upper_bounds
|
|
17
|
+
Returns: LP (JAX-native; device-side sparse). Stages that need a scipy
|
|
18
|
+
view (scaling, polish, crossover) call ``LP.to_scipy()``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
c = np.array(lp.col_cost_, dtype=np.float64)
|
|
22
|
+
lower_bounds = np.array(lp.col_lower_, dtype=np.float64)
|
|
23
|
+
upper_bounds = np.array(lp.col_upper_, dtype=np.float64)
|
|
24
|
+
|
|
25
|
+
# Build A matrix from sparse representation
|
|
26
|
+
num_row = lp.a_matrix_.num_row_
|
|
27
|
+
num_col = lp.a_matrix_.num_col_
|
|
28
|
+
A = sp.csc_matrix(
|
|
29
|
+
(lp.a_matrix_.value_, lp.a_matrix_.index_, lp.a_matrix_.start_),
|
|
30
|
+
shape=(num_row, num_col),
|
|
31
|
+
dtype=np.float64,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
row_lower = np.array(lp.row_lower_, dtype=np.float64)
|
|
35
|
+
row_upper = np.array(lp.row_upper_, dtype=np.float64)
|
|
36
|
+
return rows_to_standard_form(c, A, row_lower, row_upper, lower_bounds, upper_bounds)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def rows_to_standard_form(c, A, row_lower, row_upper, lower_bounds, upper_bounds):
|
|
40
|
+
"""
|
|
41
|
+
Converts a row-bounded LP
|
|
42
|
+
min c^T x s.t. row_lower <= A x <= row_upper, lower <= x <= upper
|
|
43
|
+
(infinite bounds as +-inf) to the standard form of
|
|
44
|
+
``highs_to_standard_form_sparse``: rows with equal finite bounds become
|
|
45
|
+
A_eq x = b_eq, every other finite row bound becomes a row of
|
|
46
|
+
A_ineq x <= b_ineq. Returns: LP.
|
|
47
|
+
"""
|
|
48
|
+
num_col = A.shape[1]
|
|
49
|
+
|
|
50
|
+
# Equality constraints: use a numeric tolerance and require finite bounds
|
|
51
|
+
eps = 1e-8
|
|
52
|
+
finite_lower_all = np.isfinite(row_lower)
|
|
53
|
+
finite_upper_all = np.isfinite(row_upper)
|
|
54
|
+
eq_mask = (
|
|
55
|
+
finite_lower_all & finite_upper_all & (np.abs(row_lower - row_upper) <= eps)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
A_eq = A[eq_mask, :].tocsc().astype(np.float64)
|
|
59
|
+
b_eq = row_lower[eq_mask].astype(np.float64)
|
|
60
|
+
|
|
61
|
+
# Inequality constraints (rows not treated as equalities)
|
|
62
|
+
ineq_mask = ~eq_mask
|
|
63
|
+
A_ineq_rows = A[ineq_mask, :]
|
|
64
|
+
row_lower_ineq = row_lower[ineq_mask]
|
|
65
|
+
row_upper_ineq = row_upper[ineq_mask]
|
|
66
|
+
|
|
67
|
+
finite_upper = np.isfinite(row_upper_ineq)
|
|
68
|
+
finite_lower = np.isfinite(row_lower_ineq)
|
|
69
|
+
|
|
70
|
+
# Build inequality matrices more efficiently
|
|
71
|
+
matrices = []
|
|
72
|
+
vectors = []
|
|
73
|
+
|
|
74
|
+
if finite_upper.any():
|
|
75
|
+
matrices.append(A_ineq_rows[finite_upper].tocsc().astype(np.float64))
|
|
76
|
+
vectors.append(row_upper_ineq[finite_upper].astype(np.float64))
|
|
77
|
+
|
|
78
|
+
if finite_lower.any():
|
|
79
|
+
matrices.append((-A_ineq_rows[finite_lower]).tocsc().astype(np.float64))
|
|
80
|
+
vectors.append((-row_lower_ineq[finite_lower]).astype(np.float64))
|
|
81
|
+
|
|
82
|
+
if len(matrices) > 0:
|
|
83
|
+
A_ineq = sp.vstack(matrices, format="csc")
|
|
84
|
+
b_ineq = np.concatenate(vectors)
|
|
85
|
+
else:
|
|
86
|
+
# No inequality rows: return empty (0 x num_col) matrix and empty RHS
|
|
87
|
+
A_ineq = sp.csc_matrix((0, num_col), dtype=np.float64)
|
|
88
|
+
b_ineq = np.empty(0, dtype=np.float64)
|
|
89
|
+
|
|
90
|
+
return LP(c, A_eq, b_eq, A_ineq, b_ineq, lower_bounds, upper_bounds)
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import jax.numpy as jnp
|
|
2
|
+
from typing import NamedTuple
|
|
3
|
+
import optax
|
|
4
|
+
from typing import Any, Callable, NamedTuple, Sequence, Union
|
|
5
|
+
import jax
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
ScheduleLike = Union[float, Callable[[jnp.ndarray], jnp.ndarray]]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# %%
|
|
12
|
+
# Basic Types
|
|
13
|
+
class SaddleState(NamedTuple):
|
|
14
|
+
primal: jnp.ndarray
|
|
15
|
+
dual_ineq: jnp.ndarray
|
|
16
|
+
dual_eq: jnp.ndarray
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class JaddleCP:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
num_variables,
|
|
23
|
+
objective,
|
|
24
|
+
constraints_eq,
|
|
25
|
+
constraints_ineq,
|
|
26
|
+
lower_bounds,
|
|
27
|
+
upper_bounds,
|
|
28
|
+
dual_bound=None,
|
|
29
|
+
):
|
|
30
|
+
self.num_variables = num_variables
|
|
31
|
+
self.objective = objective
|
|
32
|
+
self.constraints_eq = constraints_eq
|
|
33
|
+
self.constraints_ineq = constraints_ineq
|
|
34
|
+
self.lower_bounds = lower_bounds
|
|
35
|
+
self.upper_bounds = upper_bounds
|
|
36
|
+
self.dual_bound = dual_bound
|
|
37
|
+
|
|
38
|
+
def initial_primal_solution(self):
|
|
39
|
+
return jnp.zeros(self.num_variables)
|
|
40
|
+
|
|
41
|
+
def num_eq_constraints(self):
|
|
42
|
+
return len(self.constraints_eq(self.initial_primal_solution()))
|
|
43
|
+
|
|
44
|
+
def num_ineq_constraints(self):
|
|
45
|
+
return len(self.constraints_ineq(self.initial_primal_solution()))
|
|
46
|
+
|
|
47
|
+
def num_constraints(self):
|
|
48
|
+
return self.num_eq_constraints() + self.num_ineq_constraints()
|
|
49
|
+
|
|
50
|
+
def ineq_slack(self, x):
|
|
51
|
+
return jnp.max(jnp.maximum(self.constraints_ineq(x), 0.0))
|
|
52
|
+
|
|
53
|
+
def eq_slack(self, x):
|
|
54
|
+
return jnp.max(jnp.abs(self.constraints_eq(x)))
|
|
55
|
+
|
|
56
|
+
def complementarity_slack(self, x, dual_ineq):
|
|
57
|
+
return dual_ineq * (self.constraints_ineq(x))
|
|
58
|
+
|
|
59
|
+
def initial_solution(self):
|
|
60
|
+
return SaddleState(
|
|
61
|
+
primal=jnp.zeros(self.num_variables),
|
|
62
|
+
dual_ineq=jnp.zeros(self.num_ineq_constraints()),
|
|
63
|
+
dual_eq=jnp.zeros(self.num_eq_constraints()),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class LP:
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
c,
|
|
71
|
+
A_eq,
|
|
72
|
+
b_eq,
|
|
73
|
+
A_ineq,
|
|
74
|
+
b_ineq,
|
|
75
|
+
lower_bounds,
|
|
76
|
+
upper_bounds,
|
|
77
|
+
):
|
|
78
|
+
self.c = c
|
|
79
|
+
self.A_eq = A_eq
|
|
80
|
+
self.b_eq = b_eq
|
|
81
|
+
self.A_ineq = A_ineq
|
|
82
|
+
self.b_ineq = b_ineq
|
|
83
|
+
self.lower_bounds = lower_bounds
|
|
84
|
+
self.upper_bounds = upper_bounds
|
|
85
|
+
|
|
86
|
+
def objective(self, x):
|
|
87
|
+
return self.c @ x
|
|
88
|
+
|
|
89
|
+
def num_variables(self):
|
|
90
|
+
return len(self.c)
|
|
91
|
+
|
|
92
|
+
def num_eq_constraints(self):
|
|
93
|
+
return self.A_eq.shape[0]
|
|
94
|
+
|
|
95
|
+
def num_ineq_constraints(self):
|
|
96
|
+
return self.A_ineq.shape[0]
|
|
97
|
+
|
|
98
|
+
def num_constraints(self):
|
|
99
|
+
return self.A_eq.shape[0] + self.A_ineq.shape[0]
|
|
100
|
+
|
|
101
|
+
def ineq_slack(self, x):
|
|
102
|
+
return jnp.max(jnp.maximum(self.A_ineq @ x - self.b_ineq, 0.0), initial=0.0)
|
|
103
|
+
|
|
104
|
+
def eq_slack(self, x):
|
|
105
|
+
return jnp.max(jnp.abs(self.A_eq @ x - self.b_eq), initial=0.0)
|
|
106
|
+
|
|
107
|
+
def diff_eq_slack(self, x):
|
|
108
|
+
return self.A_eq @ x - self.b_eq
|
|
109
|
+
|
|
110
|
+
def complementarity_slack(self, x, dual_ineq):
|
|
111
|
+
return (dual_ineq * (self.A_ineq @ x - self.b_ineq)).sum()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class JaddleLP:
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
c,
|
|
118
|
+
A_eq,
|
|
119
|
+
b_eq,
|
|
120
|
+
A_ineq,
|
|
121
|
+
b_ineq,
|
|
122
|
+
lower_bounds,
|
|
123
|
+
upper_bounds,
|
|
124
|
+
):
|
|
125
|
+
self.c = c
|
|
126
|
+
self.A_eq = A_eq
|
|
127
|
+
self.b_eq = b_eq
|
|
128
|
+
self.A_ineq = A_ineq
|
|
129
|
+
self.b_ineq = b_ineq
|
|
130
|
+
self.lower_bounds = lower_bounds
|
|
131
|
+
self.upper_bounds = upper_bounds
|
|
132
|
+
# Fused [A_eq; A_ineq] for 2-matvec gradient computation.
|
|
133
|
+
#
|
|
134
|
+
# Both A and Aᵀ are kept as BCOO. BCSR was benchmarked here (Aᵀ
|
|
135
|
+
# materialised as its own row-major CSR, not a lazy `.T`) and was MUCH
|
|
136
|
+
# slower than BCOO on this workload — do not switch back to BCSR without
|
|
137
|
+
# re-benchmarking; BCOO's matvec wins for these matrices/precision/device.
|
|
138
|
+
#
|
|
139
|
+
# Aᵀ is stored as an explicit transposed BCOO (column-swapped indices)
|
|
140
|
+
# rather than the lazy `self.A.T`, so `Aᵀ @ y` runs its own matvec
|
|
141
|
+
# instead of the transposed-operand code path.
|
|
142
|
+
import jax.experimental.sparse as _jsp
|
|
143
|
+
|
|
144
|
+
self.n_eq = A_eq.shape[0]
|
|
145
|
+
A_bcoo = _jsp.bcoo_concatenate([A_eq, A_ineq], dimension=0)
|
|
146
|
+
A_T_bcoo = _jsp.BCOO(
|
|
147
|
+
(A_bcoo.data, A_bcoo.indices[:, ::-1]),
|
|
148
|
+
shape=A_bcoo.shape[::-1],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Column-swapping A's indices destroys row-major sort order, leaving A_T
|
|
152
|
+
# with indices_sorted=False/unique_indices=False. JAX's BCOO matvec has a
|
|
153
|
+
# faster kernel for sorted+unique indices (and the unsorted path can't
|
|
154
|
+
# skip duplicate accumulation), so sort A_T once at build time. The LP
|
|
155
|
+
# has no duplicate (row, col) entries, so unique_indices is safe to set.
|
|
156
|
+
A_bcoo = A_bcoo.sort_indices()
|
|
157
|
+
A_bcoo.unique_indices = True
|
|
158
|
+
|
|
159
|
+
A_T_bcoo = A_T_bcoo.sort_indices()
|
|
160
|
+
A_T_bcoo.unique_indices = True
|
|
161
|
+
|
|
162
|
+
self.A = A_bcoo
|
|
163
|
+
self.A_T = A_T_bcoo
|
|
164
|
+
self.b = jnp.concatenate([b_eq, b_ineq])
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def from_scipy(cls, c, A_eq, b_eq, A_ineq, b_ineq, lower_bounds, upper_bounds):
|
|
168
|
+
"""Build a ``JaddleLP`` directly from scipy sparse blocks + numpy vectors.
|
|
169
|
+
|
|
170
|
+
Mirrors ``jaddle_linear.to_jaddle_sparse``: resolves the active precision
|
|
171
|
+
profile (float64 under x64, else float32; float16 lives only in JAX since
|
|
172
|
+
scipy cannot hold it), converts each constraint block to a sorted,
|
|
173
|
+
deduplicated BCOO, and casts the data array to the profile dtype. This is
|
|
174
|
+
the JAX-native entry point — callers (e.g. ``highs_to_standard_form_sparse``)
|
|
175
|
+
no longer route through a scipy ``LP``.
|
|
176
|
+
"""
|
|
177
|
+
import jax.experimental.sparse as _jsp
|
|
178
|
+
from jaddle.jaddle_optimisers import jaddle_dtype
|
|
179
|
+
|
|
180
|
+
float_dtype = jaddle_dtype()
|
|
181
|
+
if float_dtype == jnp.float64 and not jax.config.jax_enable_x64:
|
|
182
|
+
float_dtype = jnp.float32
|
|
183
|
+
np_float = np.float64 if float_dtype == jnp.float64 else np.float32
|
|
184
|
+
|
|
185
|
+
def _to_bcoo(A):
|
|
186
|
+
A = A.astype(np_float).sorted_indices()
|
|
187
|
+
A.sum_duplicates()
|
|
188
|
+
bcoo = _jsp.BCOO.from_scipy_sparse(A.tocoo()).sort_indices()
|
|
189
|
+
return _jsp.BCOO(
|
|
190
|
+
(bcoo.data.astype(float_dtype), bcoo.indices), shape=bcoo.shape
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return cls(
|
|
194
|
+
jnp.asarray(c, dtype=float_dtype),
|
|
195
|
+
_to_bcoo(A_eq),
|
|
196
|
+
jnp.asarray(b_eq, dtype=float_dtype),
|
|
197
|
+
_to_bcoo(A_ineq),
|
|
198
|
+
jnp.asarray(b_ineq, dtype=float_dtype),
|
|
199
|
+
jnp.asarray(lower_bounds, dtype=float_dtype),
|
|
200
|
+
jnp.asarray(upper_bounds, dtype=float_dtype),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def objective(self, x):
|
|
204
|
+
return self.c @ x
|
|
205
|
+
|
|
206
|
+
def num_variables(self):
|
|
207
|
+
return len(self.c)
|
|
208
|
+
|
|
209
|
+
def num_eq_constraints(self):
|
|
210
|
+
return self.A_eq.shape[0]
|
|
211
|
+
|
|
212
|
+
def num_ineq_constraints(self):
|
|
213
|
+
return self.A_ineq.shape[0]
|
|
214
|
+
|
|
215
|
+
def num_constraints(self):
|
|
216
|
+
return self.A_eq.shape[0] + self.A_ineq.shape[0]
|
|
217
|
+
|
|
218
|
+
def ineq_slack(self, x):
|
|
219
|
+
return jnp.max(jnp.maximum(self.A_ineq @ x - self.b_ineq, 0.0), initial=0.0)
|
|
220
|
+
|
|
221
|
+
def eq_slack(self, x):
|
|
222
|
+
return jnp.max(jnp.abs(self.A_eq @ x - self.b_eq), initial=0.0)
|
|
223
|
+
|
|
224
|
+
def diff_eq_slack(self, x):
|
|
225
|
+
return self.A_eq @ x - self.b_eq
|
|
226
|
+
|
|
227
|
+
def complementarity_slack(self, x, dual_ineq):
|
|
228
|
+
return (dual_ineq * (self.A_ineq @ x - self.b_ineq)).sum()
|
|
229
|
+
|
|
230
|
+
def initial_solution(self):
|
|
231
|
+
"""Default start point: the box-projected zero primal with zero duals
|
|
232
|
+
(the PDLP default)."""
|
|
233
|
+
return SaddleState(
|
|
234
|
+
primal=optax.projections.projection_box(
|
|
235
|
+
jnp.zeros(self.num_variables()), self.lower_bounds, self.upper_bounds
|
|
236
|
+
),
|
|
237
|
+
dual_ineq=jnp.zeros(self.num_ineq_constraints()),
|
|
238
|
+
dual_eq=jnp.zeros(self.num_eq_constraints()),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def to_scipy(self) -> "LP":
|
|
242
|
+
"""Materialise a scipy-backed ``LP`` (CSC matrices, numpy vectors).
|
|
243
|
+
|
|
244
|
+
``JaddleLP`` is the device-side representation the saddle solver iterates
|
|
245
|
+
on; the scaling, primal/dual polish, crossover and eq-projection LU stages
|
|
246
|
+
run host-side on scipy sparse (sparse direct solve, ``lsmr``, boolean
|
|
247
|
+
row-slicing — none of which JAX provides). Those stages call this to get
|
|
248
|
+
the scipy view they need; the solver never does.
|
|
249
|
+
"""
|
|
250
|
+
import scipy.sparse as sp
|
|
251
|
+
|
|
252
|
+
def _to_csc(bcoo):
|
|
253
|
+
data = np.asarray(bcoo.data)
|
|
254
|
+
idx = np.asarray(bcoo.indices)
|
|
255
|
+
return sp.csc_matrix(
|
|
256
|
+
(data, (idx[:, 0], idx[:, 1])),
|
|
257
|
+
shape=bcoo.shape,
|
|
258
|
+
dtype=np.float64,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return LP(
|
|
262
|
+
np.asarray(self.c, dtype=np.float64),
|
|
263
|
+
_to_csc(self.A_eq),
|
|
264
|
+
np.asarray(self.b_eq, dtype=np.float64),
|
|
265
|
+
_to_csc(self.A_ineq),
|
|
266
|
+
np.asarray(self.b_ineq, dtype=np.float64),
|
|
267
|
+
np.asarray(self.lower_bounds, dtype=np.float64),
|
|
268
|
+
np.asarray(self.upper_bounds, dtype=np.float64),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# %%
|