primalsolver 0.1.1__py3-none-win_amd64.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.
@@ -0,0 +1,373 @@
1
+ """PrimalSolver from Python — a thin, convenience wrapper over the C99 library.
2
+
3
+ from primalsolver import Model, BK, SOL_ITR
4
+
5
+ with Model(maxcon=2, maxvar=2) as m:
6
+ m.obj([0, 1], [-3.0, -2.0])
7
+ m.a_ij(0, 0, 1.0); m.a_ij(0, 1, 1.0) # x0 + x1 <= 4
8
+ m.a_ij(1, 0, 1.0); m.a_ij(1, 1, 3.0) # x0 + 3x1 <= 6
9
+ m.con_bounds(0, BK.UP, up=4.0)
10
+ m.con_bounds(1, BK.UP, up=6.0)
11
+ m.var_bounds(0, BK.RA, 0.0, 3.0)
12
+ m.var_bounds(1, BK.RA, 0.0, 3.0)
13
+ r = m.solve()
14
+ print(r.x, r.objective, r.solsta) # [3.0, 1.0] -11.0 1
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import ctypes as C
20
+ import types
21
+ from collections import namedtuple
22
+
23
+ from . import _native as _n
24
+
25
+ __version__ = "0.1.1"
26
+
27
+ LIB = _n.LIB
28
+ c_void_p = C.c_void_p
29
+ byref = C.byref
30
+
31
+ # The full generated binding is re-exported at package level:
32
+ # primalsolver.PRIMAL_optimize(...), primalsolver.FUNCTIONS, primalsolver.MISSING
33
+ FUNCTIONS = _n.FUNCTIONS
34
+ MISSING = _n.MISSING
35
+ globals().update(FUNCTIONS)
36
+
37
+
38
+ class SolverError(RuntimeError):
39
+ """A PrimalSolver call returned a non-OK resource code (or a wrapper bug)."""
40
+
41
+
42
+ # --- enums, numbered as the C header (and as the vendor reference) -----------
43
+ class BK:
44
+ LO, UP, FX, FR, RA = 0, 1, 2, 3, 4 # PRIMALboundkeye
45
+
46
+
47
+ class SENSE:
48
+ MIN, MAX = 0, 1 # PRIMALobjsensee
49
+
50
+
51
+ class CT:
52
+ QUAD, RQUAD, PEXP, DEXP, PPOW, RPOW = 0, 1, 2, 3, 4, 7 # PRIMALconetypee
53
+
54
+
55
+ class VAR_TYPE:
56
+ CONT, INT, INT_BIN, SEMI_CONT, SEMI_INT = 0, 1, 2, 3, 4 # PRIMALvariabletypee
57
+
58
+
59
+ class SK: # PRIMALstakeye (basis status)
60
+ LOW, UPR, FIX, BAS, SUP, UNDEF = 0, 1, 2, 3, 4, 5
61
+
62
+
63
+ class APPROX: # PRIMALdomaintypee (linear domains)
64
+ R, RZERO, RPLUS, RMINUS = 0, 1, 2, 3
65
+
66
+
67
+ class SOL:
68
+ ITR, BAS, ITG = 0, 1, 2 # PRIMALsolt
69
+
70
+
71
+ class SOLSTA:
72
+ UNKNOWN = 0
73
+ OPTIMAL = 1
74
+ PRIM_FEAS = 2
75
+ PRIM_INFEAS_CER = 5
76
+ DUAL_INFEAS_CER = 6
77
+ INTEGER_OPTIMAL = 9
78
+
79
+
80
+ SOL_ITR = SOL.ITR
81
+
82
+ Result = namedtuple("Result", "x y objective dual solsta prosta pinf rc")
83
+
84
+
85
+ def version() -> tuple[int, int, int]:
86
+ a, b, c = (C.c_int(), C.c_int(), C.c_int())
87
+ _n.getversion(byref(a), byref(b), byref(c))
88
+ return a.value, b.value, c.value
89
+
90
+
91
+ class Model:
92
+ """A task builder + solver. Use as a context manager to free the C handles."""
93
+
94
+ def __init__(self, maxcon: int = 0, maxvar: int = 0):
95
+ self.env = c_void_p()
96
+ self.task = c_void_p()
97
+ self._check(_n.makeenv(byref(self.env), None), "makeenv")
98
+ self._check(_n.maketask(self.env, int(maxcon), int(maxvar), byref(self.task)), "maketask")
99
+
100
+ # -- helpers -------------------------------------------------------------
101
+ @staticmethod
102
+ def _check(rc: int, what: str = "") -> None:
103
+ if rc != 0:
104
+ raise SolverError(f"PRIMAL {what} failed (rc={rc})")
105
+
106
+ @staticmethod
107
+ def _ivec(seq):
108
+ return (C.c_int * len(seq))(*seq)
109
+
110
+ @staticmethod
111
+ def _dvec(seq):
112
+ return (C.c_double * len(seq))(*[float(v) for v in seq])
113
+
114
+ def _numcon(self) -> int:
115
+ n = C.c_int(); self._check(_n.getnumcon(self.task, byref(n))); return n.value
116
+
117
+ def _numvar(self) -> int:
118
+ n = C.c_int(); self._check(_n.getnumvar(self.task, byref(n))); return n.value
119
+
120
+ # -- model building ------------------------------------------------------
121
+ def add_cons(self, n: int) -> None:
122
+ self._check(_n.appendcons(self.task, int(n)), "appendcons")
123
+
124
+ def add_vars(self, n: int) -> None:
125
+ self._check(_n.appendvars(self.task, int(n)), "appendvars")
126
+
127
+ def obj(self, idx, vals) -> None:
128
+ """Set objective coefficients c[idx] = vals (replaces those entries)."""
129
+ idx = list(idx); vals = list(vals)
130
+ self._check(_n.putclist(self.task, len(idx), self._ivec(idx), self._dvec(vals)), "putclist")
131
+
132
+ def a_ij(self, i: int, j: int, value: float) -> None:
133
+ self._check(_n.putaij(self.task, int(i), int(j), float(value)), "putaij")
134
+
135
+ def a_row(self, i: int, cols, vals) -> None:
136
+ """Replace row i with the sparse coefficients (cols, vals)."""
137
+ cols = list(cols); vals = list(vals)
138
+ self._check(_n.putarow(self.task, int(i), len(cols), self._ivec(cols), self._dvec(vals)), "putarow")
139
+
140
+ def cj(self, j: int, value: float) -> None:
141
+ """Set a single objective coefficient."""
142
+ self._check(_n.putcj(self.task, int(j), float(value)), "putcj")
143
+
144
+ def qobj(self, terms) -> None:
145
+ """Set the quadratic objective from (i, j, value) terms (upper triangle)."""
146
+ terms = list(terms)
147
+ qi = self._ivec([int(t[0]) for t in terms])
148
+ qj = self._ivec([int(t[1]) for t in terms])
149
+ qv = self._dvec([float(t[2]) for t in terms])
150
+ self._check(_n.putqobj(self.task, len(terms), qi, qj, qv), "putqobj")
151
+
152
+ def cone(self, ctype: int, members, param: float = 0.0) -> None:
153
+ """Append a cone whose members are the given variable indices."""
154
+ members = list(members)
155
+ self._check(_n.appendcone(self.task, int(ctype), float(param), len(members), self._ivec(members)), "appendcone")
156
+
157
+ def a_col(self, j: int, rows, vals) -> None:
158
+ rows = list(rows); vals = list(vals)
159
+ self._check(_n.putacol(self.task, int(j), len(rows), self._ivec(rows), self._dvec(vals)), "putacol")
160
+
161
+ def qconk(self, k: int, terms) -> None:
162
+ """Quadratic terms of constraint k: iterable of (i, j, value), i<=j."""
163
+ terms = list(terms)
164
+ self._check(_n.putqconk(self.task, int(k), len(terms),
165
+ self._ivec([int(t[0]) for t in terms]),
166
+ self._ivec([int(t[1]) for t in terms]),
167
+ self._dvec([float(t[2]) for t in terms])), "putqconk")
168
+
169
+ def qobj_ij(self, i: int, j: int, value: float) -> None:
170
+ self._check(_n.putqobjij(self.task, int(i), int(j), float(value)), "putqobjij")
171
+
172
+ def var_type(self, j: int, vt: int) -> None:
173
+ self._check(_n.putvartype(self.task, int(j), int(vt)), "putvartype")
174
+
175
+ def var_types(self, cols, types) -> None:
176
+ types = list(types)
177
+ self._check(_n.putvartypelist(self.task, len(cols), self._ivec(list(cols)),
178
+ (C.c_int * len(types))(*types)), "putvartypelist")
179
+
180
+ def cfix(self, value: float) -> None:
181
+ self._check(_n.putcfix(self.task, float(value)), "putcfix")
182
+
183
+ def int_param(self, param: int, value: int) -> None:
184
+ self._check(_n.putintparam(self.task, int(param), int(value)), "putintparam")
185
+
186
+ def dou_param(self, param: int, value: float) -> None:
187
+ self._check(_n.putdouparam(self.task, int(param), float(value)), "putdouparam")
188
+
189
+ def warm_start(self, x=None, y=None) -> None:
190
+ if x is not None:
191
+ self._check(_n.putxx(self.task, SOL.ITR, self._dvec(list(x))), "putxx")
192
+ if y is not None:
193
+ self._check(_n.puty(self.task, SOL.ITR, self._dvec(list(y))), "puty")
194
+
195
+ def sos(self, kind: int, members, weights=None) -> None:
196
+ members = list(members)
197
+ w = self._dvec(list(weights) if weights is not None else [1.0] * len(members))
198
+ fn = _n.PRIMAL_appendsos1 if int(kind) == 1 else _n.PRIMAL_appendsos2
199
+ self._check(fn(self.task, len(members), self._ivec(members), w), "appendsos")
200
+
201
+ # -- SDP (semi-definite bar variables) -----------------------------------
202
+ def barvars(self, dims) -> None:
203
+ dims = list(dims)
204
+ self._check(_n.appendbarvars(self.task, len(dims), self._ivec(dims)), "appendbarvars")
205
+
206
+ def sparsesymmat(self, dim: int, terms) -> int:
207
+ """Store a sparse symmetric matrix; returns its id (for baraij/barcj)."""
208
+ terms = list(terms)
209
+ idx = C.c_int()
210
+ self._check(_n.appendsparsesymmat(self.task, int(dim), len(terms),
211
+ self._ivec([int(t[0]) for t in terms]),
212
+ self._ivec([int(t[1]) for t in terms]),
213
+ self._dvec([float(t[2]) for t in terms]), byref(idx)), "appendsparsesymmat")
214
+ return idx.value
215
+
216
+ def baraij(self, i: int, j: int, terms) -> None:
217
+ terms = list(terms) # (symmat_id, coefficient)
218
+ self._check(_n.putbaraij(self.task, int(i), int(j), len(terms),
219
+ self._ivec([int(t[0]) for t in terms]),
220
+ self._dvec([float(t[1]) for t in terms])), "putbaraij")
221
+
222
+ def barcj(self, j: int, terms) -> None:
223
+ terms = list(terms)
224
+ self._check(_n.putbarcj(self.task, int(j), len(terms),
225
+ self._ivec([int(t[0]) for t in terms]),
226
+ self._dvec([float(t[1]) for t in terms])), "putbarcj")
227
+
228
+ # -- affine expressions / conic + disjunctive constraints ----------------
229
+ def afes(self, num: int) -> None:
230
+ self._check(_n.appendafes(self.task, int(num)), "appendafes")
231
+
232
+ def afe_entry(self, i: int, j: int, value: float) -> None:
233
+ self._check(_n.putafefentry(self.task, int(i), int(j), float(value)), "putafefentry")
234
+
235
+ def afe_g(self, i: int, value: float) -> None:
236
+ self._check(_n.putafeg(self.task, int(i), float(value)), "putafeg")
237
+
238
+ def domain(self, kind: int, n: int = 1) -> int:
239
+ fn = {0: _n.PRIMAL_appendrdomain, 1: _n.PRIMAL_appendrzerodomain,
240
+ 2: _n.PRIMAL_appendrplusdomain, 3: _n.PRIMAL_appendrminusdomain}[int(kind)]
241
+ idx = C.c_longlong()
242
+ self._check(fn(self.task, int(n), byref(idx)), "appenddomain")
243
+ return idx.value
244
+
245
+ def acc(self, domidx: int, afeidx, b=None) -> None:
246
+ afeidx = list(afeidx)
247
+ bb = self._dvec(list(b) if b is not None else [0.0] * len(afeidx))
248
+ a64 = (C.c_longlong * len(afeidx))(*[int(a) for a in afeidx])
249
+ self._check(_n.appendacc(self.task, int(domidx), len(afeidx), a64, bb), "appendacc")
250
+
251
+ def djcs(self, num: int) -> None:
252
+ self._check(_n.appenddjcs(self.task, int(num)), "appenddjcs")
253
+
254
+ def djc(self, idx: int, domidxlist, afeidxlist, b, termsizelist) -> None:
255
+ dom = (C.c_longlong * len(domidxlist))(*[int(d) for d in domidxlist])
256
+ afe = (C.c_longlong * len(afeidxlist))(*[int(a) for a in afeidxlist])
257
+ bv = self._dvec(list(b))
258
+ ts = (C.c_longlong * len(termsizelist))(*[int(t) for t in termsizelist])
259
+ self._check(_n.putdjc(self.task, int(idx), len(domidxlist), dom,
260
+ len(afeidxlist), afe, bv, len(termsizelist), ts), "putdjc")
261
+
262
+ # -- data getters (for independent verification) -------------------------
263
+ def getcj(self, j: int) -> float:
264
+ v = C.c_double(); self._check(_n.getcj(self.task, int(j), byref(v))); return v.value
265
+
266
+ def getaij(self, i: int, j: int) -> float:
267
+ v = C.c_double(); self._check(_n.getaij(self.task, int(i), int(j), byref(v))); return v.value
268
+
269
+ def getvarbound(self, j: int):
270
+ bk = C.c_int(); bl = C.c_double(); bu = C.c_double()
271
+ self._check(_n.getvarbound(self.task, int(j), byref(bk), byref(bl), byref(bu)))
272
+ return bk.value, bl.value, bu.value
273
+
274
+ def getconbound(self, i: int):
275
+ bk = C.c_int(); bl = C.c_double(); bu = C.c_double()
276
+ self._check(_n.getconbound(self.task, int(i), byref(bk), byref(bl), byref(bu)))
277
+ return bk.value, bl.value, bu.value
278
+
279
+ def getcfix(self) -> float:
280
+ v = C.c_double(); self._check(_n.getcfix(self.task, byref(v))); return v.value
281
+
282
+ def getdualinfeas(self) -> float:
283
+ v = C.c_double(); self._check(_n.getdualinfeas(self.task, SOL.ITR, byref(v))); return v.value
284
+
285
+ def getreducedcosts(self, first: int = 0, last: int = None) -> list:
286
+ if last is None:
287
+ last = self._numvar()
288
+ buf = (C.c_double * (last - first))()
289
+ self._check(_n.getreducedcosts(self.task, SOL.ITR, int(first), int(last), buf))
290
+ return list(buf)
291
+
292
+ def getintparam(self, param: int) -> int:
293
+ v = C.c_int(); self._check(_n.getintparam(self.task, int(param), byref(v))); return v.value
294
+
295
+ def getdouparam(self, param: int) -> float:
296
+ v = C.c_double(); self._check(_n.getdouparam(self.task, int(param), byref(v))); return v.value
297
+
298
+ def numvar(self) -> int:
299
+ return self._numvar()
300
+
301
+ def numcon(self) -> int:
302
+ return self._numcon()
303
+
304
+ def getbarxj(self, j: int = 0) -> list:
305
+ """Bar variable j as a full dim x dim row-major matrix (dim*dim values)."""
306
+ dim = C.c_int(); self._check(_n.getdimbarvarj(self.task, int(j), byref(dim)))
307
+ buf = (C.c_double * (dim.value * dim.value))()
308
+ self._check(_n.getbarxj(self.task, SOL.ITR, int(j), buf))
309
+ return list(buf)
310
+
311
+ def getbarsj(self, j: int = 0) -> list:
312
+ dim = C.c_int(); self._check(_n.getdimbarvarj(self.task, int(j), byref(dim)))
313
+ buf = (C.c_double * (dim.value * dim.value))()
314
+ self._check(_n.getbarsj(self.task, SOL.ITR, int(j), buf))
315
+ return list(buf)
316
+
317
+ def var_bounds(self, j: int, bk: int, lo: float = 0.0, up: float = 0.0) -> None:
318
+ self._check(_n.putvarbound(self.task, int(j), int(bk), float(lo), float(up)), "putvarbound")
319
+
320
+ def con_bounds(self, i: int, bk: int, lo: float = 0.0, up: float = 0.0) -> None:
321
+ self._check(_n.putconbound(self.task, int(i), int(bk), float(lo), float(up)), "putconbound")
322
+
323
+ def sense(self, sense: int) -> None:
324
+ self._check(_n.putobjsense(self.task, int(sense)), "putobjsense")
325
+
326
+ # -- file I/O ------------------------------------------------------------
327
+ def read(self, path: str) -> None:
328
+ self._check(_n.readdataautoformat(self.task, str(path).encode()), "readdataautoformat")
329
+
330
+ def write(self, path: str) -> None:
331
+ self._check(_n.writedata(self.task, str(path).encode()), "writedata")
332
+
333
+ # -- solve ---------------------------------------------------------------
334
+ def solve(self, which: int = SOL.ITR) -> Result:
335
+ rc = _n.optimize(self.task)
336
+ nv, nc = self._numvar(), self._numcon()
337
+ x = (C.c_double * nv)() if nv else (C.c_double * 0)()
338
+ y = (C.c_double * nc)() if nc else (C.c_double * 0)()
339
+ obj = C.c_double()
340
+ dual = C.c_double()
341
+ pinf = C.c_double()
342
+ sta = C.c_int()
343
+ pro = C.c_int()
344
+ # A non-OK rc can still carry a verdict (e.g. infeasible/unbounded): read
345
+ # the status regardless and let the caller decide.
346
+ if nv:
347
+ _n.getxx(self.task, int(which), x)
348
+ if nc:
349
+ _n.gety(self.task, int(which), y)
350
+ _n.getprimalobj(self.task, int(which), byref(obj))
351
+ _n.getdualobj(self.task, int(which), byref(dual))
352
+ _n.getprimalinfeas(self.task, int(which), byref(pinf))
353
+ _n.getsolsta(self.task, int(which), byref(sta))
354
+ _n.getprosta(self.task, int(which), byref(pro))
355
+ return Result(list(x), list(y), obj.value, dual.value, sta.value, pro.value, pinf.value, rc)
356
+
357
+ def close(self) -> None:
358
+ if self.task:
359
+ _n.deletetask(byref(self.task))
360
+ self.task = c_void_p()
361
+ if self.env:
362
+ _n.deleteenv(byref(self.env))
363
+ self.env = c_void_p()
364
+
365
+ def __enter__(self) -> "Model":
366
+ return self
367
+
368
+ def __exit__(self, *exc) -> None:
369
+ self.close()
370
+
371
+
372
+ __all__ = ["Model", "Result", "SolverError", "version", "BK", "SENSE", "SOL", "SOLSTA", "CT",
373
+ "SOL_ITR", "FUNCTIONS", "MISSING"]