gsimplex 0.0.2__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.
- gsimplex/__init__.py +1 -0
- gsimplex/benchmarks/downloader.py +76 -0
- gsimplex/benchmarks/netlib.py +78 -0
- gsimplex/benchmarks/netlib_emps.py +805 -0
- gsimplex/benchmarks/plato.py +55 -0
- gsimplex/demo.py +54 -0
- gsimplex/main.py +37 -0
- gsimplex/problem.py +46 -0
- gsimplex/solution.py +25 -0
- gsimplex/solvers/__init__.py +1 -0
- gsimplex/solvers/criss_cross.py +13 -0
- gsimplex/solvers/dual_simplex.py +104 -0
- gsimplex/solvers/gap_simplex.py +74 -0
- gsimplex/solvers/iterative_solver.py +20 -0
- gsimplex/solvers/primal_simplex.py +136 -0
- gsimplex/solvers/simplex_interface.py +15 -0
- gsimplex/solvers/solver_interface.py +27 -0
- gsimplex/tools/extractor.py +37 -0
- gsimplex/tools/parser.py +45 -0
- gsimplex/vertex.py +96 -0
- gsimplex-0.0.2.dist-info/METADATA +25 -0
- gsimplex-0.0.2.dist-info/RECORD +25 -0
- gsimplex-0.0.2.dist-info/WHEEL +5 -0
- gsimplex-0.0.2.dist-info/entry_points.txt +6 -0
- gsimplex-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
"""
|
|
2
|
+
netlib_emps.py - Expand compressed LP programs (netlib format) to MPS format.
|
|
3
|
+
|
|
4
|
+
Python port of emps.c by David M. Gay (AT&T Bell Laboratories).
|
|
5
|
+
|
|
6
|
+
See the original c version at https://www.netlib.org/lp/data/emps.c
|
|
7
|
+
|
|
8
|
+
The netlib LP archive stores problems in a compressed ASCII encoding.
|
|
9
|
+
This module decodes that format and writes standard MPS output.
|
|
10
|
+
|
|
11
|
+
Public API
|
|
12
|
+
----------
|
|
13
|
+
expand_mps(input_file, output_file=None, *, keepmyst, blanksubst, just1)
|
|
14
|
+
Convert one compressed LP file. Returns MPS text when output_file
|
|
15
|
+
is None; otherwise writes to output_file and returns None.
|
|
16
|
+
|
|
17
|
+
expand_mps_string(text, ...)
|
|
18
|
+
Accepts the compressed text as a str; returns MPS text as a str.
|
|
19
|
+
|
|
20
|
+
Command line usage
|
|
21
|
+
------------------
|
|
22
|
+
This file can be used as a command-line tool as well. The options mirror the original C program.
|
|
23
|
+
See also `gsimplex-emps --help` for usage information.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import io
|
|
29
|
+
import sys
|
|
30
|
+
import argparse
|
|
31
|
+
from typing import List, Optional, Tuple, TextIO
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Translation table (identical to the C original)
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
TRTAB: str = (
|
|
37
|
+
"!\"#$%&'()*+,-./0123456789;<=>?@"
|
|
38
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`"
|
|
39
|
+
"abcdefghijklmnopqrstuvwxyz{|}~"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_INVTRTAB: List[int] = [92] * 256
|
|
43
|
+
for _i, _ch in enumerate(TRTAB):
|
|
44
|
+
_INVTRTAB[ord(_ch)] = _i
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _tr(c: str) -> int:
|
|
48
|
+
"""Return the inverse-translation-table value for a character."""
|
|
49
|
+
|
|
50
|
+
return _INVTRTAB[ord(c) & 0xFF]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Low-level decoders (stateless)
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
def _exindx(z: str, zi: int) -> Tuple[int, int]:
|
|
58
|
+
"""Decode one variable-length supersparse index.
|
|
59
|
+
|
|
60
|
+
Mirrors exindx() in the C source. Characters with tr-value 23-45
|
|
61
|
+
encode a single-character terminal (value = k-23). Characters with
|
|
62
|
+
tr-value 0-22 start a multi-character base-46 sequence that ends when
|
|
63
|
+
a character with tr-value >= 46 is encountered (the terminal 'bit').
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
z : current data string (one stripped line from the compressed file)
|
|
68
|
+
zi : current read offset within *z*
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
(index_value, new_zi)
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
k = _tr(z[zi]); zi += 1
|
|
76
|
+
if k >= 46:
|
|
77
|
+
raise ValueError(f"exindx: bad encoded byte at offset {zi - 1}")
|
|
78
|
+
if k >= 23:
|
|
79
|
+
return k - 23, zi
|
|
80
|
+
|
|
81
|
+
# Multi-character base-46 variable-length encoding
|
|
82
|
+
x = k
|
|
83
|
+
while True:
|
|
84
|
+
k = _tr(z[zi]); zi += 1
|
|
85
|
+
x = x * 46 + k
|
|
86
|
+
if k >= 46:
|
|
87
|
+
x -= 46
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
return x, zi
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _exform(
|
|
94
|
+
z: str, zi: int, ss: List[str], kmax: int
|
|
95
|
+
) -> Tuple[str, int]:
|
|
96
|
+
"""Decode one compressed floating-point number.
|
|
97
|
+
|
|
98
|
+
Mirrors exform() in the C source.
|
|
99
|
+
|
|
100
|
+
When the first encoded character has tr-value < 46 the number is a
|
|
101
|
+
supersparse reference into the pre-computed table *ss*. Otherwise the
|
|
102
|
+
number is encoded inline as either integer-float (k >= 11 after sign
|
|
103
|
+
adjustment) or general float.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
z : current data string
|
|
108
|
+
zi : current read offset within *z*
|
|
109
|
+
ss : pre-computed number table, **1-based** (ss[0] unused)
|
|
110
|
+
kmax : number of valid entries in *ss* (== len(ss) - 1)
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
(formatted_string, new_zi)
|
|
115
|
+
The string is right-justified in a field of at least 12 characters,
|
|
116
|
+
matching the C padding logic ``while(k++ < 12) *s++ = ' '``.
|
|
117
|
+
"""
|
|
118
|
+
y: int = 0
|
|
119
|
+
k: int = _tr(z[zi]); zi += 1
|
|
120
|
+
|
|
121
|
+
# Supersparse table reference
|
|
122
|
+
if k < 46:
|
|
123
|
+
# The index encoding starts at the character we already read;
|
|
124
|
+
# back up one so _exindx can re-consume the full encoding.
|
|
125
|
+
idx, zi = _exindx(z, zi - 1)
|
|
126
|
+
if idx > kmax:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"exform: table reference {idx} exceeds kmax {kmax}"
|
|
129
|
+
)
|
|
130
|
+
return ss[idx], zi
|
|
131
|
+
|
|
132
|
+
# Inline encoded number
|
|
133
|
+
k -= 46
|
|
134
|
+
neg = k >= 23
|
|
135
|
+
if neg:
|
|
136
|
+
k -= 23
|
|
137
|
+
nelim = 11
|
|
138
|
+
else:
|
|
139
|
+
nelim = 12
|
|
140
|
+
|
|
141
|
+
# d[] holds decimal digit characters, least-significant first
|
|
142
|
+
# (mirrors the db[] buffer + d pointer pattern in the C source).
|
|
143
|
+
d: List[str] = []
|
|
144
|
+
|
|
145
|
+
if k >= 11:
|
|
146
|
+
# Integer floating-point
|
|
147
|
+
# '.' is stored first so that reversing d places it after the
|
|
148
|
+
# digits, yielding output like "123." (integer with trailing point).
|
|
149
|
+
k -= 11
|
|
150
|
+
d.append('.')
|
|
151
|
+
if k >= 6:
|
|
152
|
+
x: int = k - 6
|
|
153
|
+
else:
|
|
154
|
+
x = k
|
|
155
|
+
while True:
|
|
156
|
+
k = _tr(z[zi]); zi += 1
|
|
157
|
+
x = x * 46 + k
|
|
158
|
+
if k >= 46:
|
|
159
|
+
x -= 46
|
|
160
|
+
break
|
|
161
|
+
if x == 0:
|
|
162
|
+
d.append('0')
|
|
163
|
+
else:
|
|
164
|
+
while x:
|
|
165
|
+
d.append(str(x % 10))
|
|
166
|
+
x //= 10
|
|
167
|
+
sbuf = list(reversed(d)) # MSB-first digits then '.'
|
|
168
|
+
|
|
169
|
+
else:
|
|
170
|
+
# General floating-point
|
|
171
|
+
ex: int = _tr(z[zi]) - 50; zi += 1 # decimal exponent
|
|
172
|
+
x = _tr(z[zi]); zi += 1 # start of base-92 mantissa
|
|
173
|
+
kk = k
|
|
174
|
+
while kk > 0:
|
|
175
|
+
kk -= 1
|
|
176
|
+
if x >= 100_000_000:
|
|
177
|
+
# High-part overflow: save x, restart x from new character
|
|
178
|
+
y = x
|
|
179
|
+
x = _tr(z[zi]); zi += 1
|
|
180
|
+
else:
|
|
181
|
+
x = x * 92 + _tr(z[zi]); zi += 1
|
|
182
|
+
|
|
183
|
+
# Collect decimal digits of the mantissa (LSB first)
|
|
184
|
+
if y:
|
|
185
|
+
# y holds the high part; x holds the low extension.
|
|
186
|
+
# C loop: while(x > 1) drain x digits, then drain y digits.
|
|
187
|
+
while x > 1:
|
|
188
|
+
d.append(str(x % 10))
|
|
189
|
+
x //= 10
|
|
190
|
+
yy = y
|
|
191
|
+
while True:
|
|
192
|
+
d.append(str(yy % 10))
|
|
193
|
+
if yy < 10:
|
|
194
|
+
break
|
|
195
|
+
yy //= 10
|
|
196
|
+
elif x:
|
|
197
|
+
xx = x
|
|
198
|
+
while True:
|
|
199
|
+
d.append(str(xx % 10))
|
|
200
|
+
if xx < 10:
|
|
201
|
+
break
|
|
202
|
+
xx //= 10
|
|
203
|
+
else:
|
|
204
|
+
d.append('0')
|
|
205
|
+
|
|
206
|
+
# nd = number of digits that belong before the decimal point
|
|
207
|
+
nd = len(d) + ex
|
|
208
|
+
sbuf = []
|
|
209
|
+
eout = False
|
|
210
|
+
|
|
211
|
+
if ex > 0:
|
|
212
|
+
if nd < nelim or ex < 3:
|
|
213
|
+
# All mantissa digits, trailing zeros, then '.'
|
|
214
|
+
sbuf.extend(reversed(d))
|
|
215
|
+
sbuf.extend(['0'] * ex)
|
|
216
|
+
sbuf.append('.')
|
|
217
|
+
else:
|
|
218
|
+
eout = True
|
|
219
|
+
elif nd >= 0:
|
|
220
|
+
rev = list(reversed(d))
|
|
221
|
+
sbuf.extend(rev[:nd])
|
|
222
|
+
sbuf.append('.')
|
|
223
|
+
sbuf.extend(rev[nd:])
|
|
224
|
+
elif ex > -nelim:
|
|
225
|
+
sbuf.append('.')
|
|
226
|
+
sbuf.extend(['0'] * (-nd))
|
|
227
|
+
sbuf.extend(reversed(d))
|
|
228
|
+
else:
|
|
229
|
+
eout = True
|
|
230
|
+
|
|
231
|
+
if eout:
|
|
232
|
+
# Scientific notation (mirrors the Eout: label in the C source)
|
|
233
|
+
ex += len(d) - 1
|
|
234
|
+
rev = list(reversed(d))
|
|
235
|
+
ridx = 0
|
|
236
|
+
if ex == -10:
|
|
237
|
+
# Special case: avoid a 3-digit exponent; accept slight
|
|
238
|
+
# rounding error (mirrors the C original).
|
|
239
|
+
ex = -9
|
|
240
|
+
# No digits before '.'; fall through to the '.' append below
|
|
241
|
+
else:
|
|
242
|
+
# Optionally emit leading digits to reduce the exponent
|
|
243
|
+
if ex > 9 and ex <= len(d) + 8:
|
|
244
|
+
while ex > 9:
|
|
245
|
+
sbuf.append(rev[ridx]); ridx += 1
|
|
246
|
+
ex -= 1
|
|
247
|
+
sbuf.append(rev[ridx]); ridx += 1
|
|
248
|
+
sbuf.append('.')
|
|
249
|
+
sbuf.extend(rev[ridx:])
|
|
250
|
+
sbuf.append('E')
|
|
251
|
+
if ex < 0:
|
|
252
|
+
sbuf.append('-')
|
|
253
|
+
ex = -ex
|
|
254
|
+
ed: List[str] = []
|
|
255
|
+
while ex:
|
|
256
|
+
ed.append(str(ex % 10))
|
|
257
|
+
ex //= 10
|
|
258
|
+
sbuf.extend(reversed(ed))
|
|
259
|
+
|
|
260
|
+
result = ('-' if neg else '') + ''.join(sbuf)
|
|
261
|
+
# Right-justify in a field of at least 12 characters
|
|
262
|
+
# (matches C: while(k++ < 12) *s++ = ' '; strcpy(s, sbuf);)
|
|
263
|
+
return result.rjust(12), zi
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# Converter class
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
class EMPSConverter:
|
|
271
|
+
"""Expand one or more compressed netlib LP problems to MPS format."""
|
|
272
|
+
|
|
273
|
+
_BOUND_TYPES = ("UP", "LO", "FX", "FR", "MI", "PL")
|
|
274
|
+
|
|
275
|
+
def __init__(
|
|
276
|
+
self,
|
|
277
|
+
*,
|
|
278
|
+
keepmyst: bool = True,
|
|
279
|
+
blanksubst: Optional[str] = None,
|
|
280
|
+
just1: bool = False,
|
|
281
|
+
) -> None:
|
|
282
|
+
"""
|
|
283
|
+
Parameters
|
|
284
|
+
----------
|
|
285
|
+
keepmyst
|
|
286
|
+
When True (default), "mystery lines" (lines starting with ':'
|
|
287
|
+
in the source) are included in the output.
|
|
288
|
+
blanksubst
|
|
289
|
+
If given (e.g. '_'), blanks within names are replaced with
|
|
290
|
+
this character.
|
|
291
|
+
just1
|
|
292
|
+
If True, emit only one nonzero per output line.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
self.keepmyst = keepmyst
|
|
296
|
+
self.blanksubst = blanksubst
|
|
297
|
+
self.just1 = just1
|
|
298
|
+
|
|
299
|
+
# Public interface
|
|
300
|
+
|
|
301
|
+
def convert(
|
|
302
|
+
self,
|
|
303
|
+
input_file: "str | TextIO",
|
|
304
|
+
output_file: "str | TextIO | None" = None,
|
|
305
|
+
) -> Optional[str]:
|
|
306
|
+
"""Expand *input_file* and write the MPS result to *output_file*.
|
|
307
|
+
|
|
308
|
+
Parameters
|
|
309
|
+
----------
|
|
310
|
+
input_file
|
|
311
|
+
A filesystem path or an already-opened readable text file.
|
|
312
|
+
output_file
|
|
313
|
+
A filesystem path, a writable text file, or None.
|
|
314
|
+
When None the MPS text is returned as a string.
|
|
315
|
+
|
|
316
|
+
Returns
|
|
317
|
+
-------
|
|
318
|
+
MPS text (str) when output_file is None; otherwise None.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
return_str = output_file is None
|
|
322
|
+
|
|
323
|
+
if isinstance(input_file, str):
|
|
324
|
+
with open(input_file) as fh:
|
|
325
|
+
result = self._run(fh, input_file)
|
|
326
|
+
else:
|
|
327
|
+
name = getattr(input_file, 'name', '<stream>')
|
|
328
|
+
result = self._run(input_file, name)
|
|
329
|
+
|
|
330
|
+
if return_str:
|
|
331
|
+
return result
|
|
332
|
+
if isinstance(output_file, str):
|
|
333
|
+
with open(output_file, 'w') as fh:
|
|
334
|
+
fh.write(result)
|
|
335
|
+
else:
|
|
336
|
+
output_file.write(result)
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
# Internal driver
|
|
340
|
+
|
|
341
|
+
def _run(self, inf: TextIO, infile_name: str) -> str:
|
|
342
|
+
"""Core loop; returns the complete MPS text as a single string."""
|
|
343
|
+
# Per-run state
|
|
344
|
+
self._inf = inf
|
|
345
|
+
self._infile = infile_name
|
|
346
|
+
self._nline = 0
|
|
347
|
+
self._ncs = 1 # next slot in the rolling checksum buffer
|
|
348
|
+
self._canend = False # True -> EOF is acceptable
|
|
349
|
+
self._kmax = -1 # entries currently in the number table
|
|
350
|
+
self._cn = 0 # column name counter
|
|
351
|
+
self._nrow = 0
|
|
352
|
+
self._ss: List[str] = [] # pre-computed number table (1-based)
|
|
353
|
+
self._bs: List[str] = [] # name store (1-based via index-1)
|
|
354
|
+
self._bsz = 0 # capacity of _bs
|
|
355
|
+
self._lastl = ''
|
|
356
|
+
# Checksum buffer: _chkbuf[0]=' ', checksum chars go into [1..71],
|
|
357
|
+
# a '\n' is appended when reading the checksum verification line.
|
|
358
|
+
self._chkbuf: List[str] = [' '] + ['\x00'] * 75
|
|
359
|
+
self._cfn: List[str] = [''] * 72 # filename per checksum slot
|
|
360
|
+
self._cfl: List[int] = [0] * 72 # line no. per checksum slot
|
|
361
|
+
self._out: List[str] = []
|
|
362
|
+
|
|
363
|
+
buf = self._rdline()
|
|
364
|
+
while True:
|
|
365
|
+
self._kmax = -1
|
|
366
|
+
self._ncs = 1
|
|
367
|
+
|
|
368
|
+
# Skip lines until we find "NAME"
|
|
369
|
+
while buf is not None and not buf.startswith('NAME'):
|
|
370
|
+
buf = self._rdline()
|
|
371
|
+
if buf is None:
|
|
372
|
+
break
|
|
373
|
+
|
|
374
|
+
self._canend = False
|
|
375
|
+
self._emit(buf)
|
|
376
|
+
self._ncs = 1
|
|
377
|
+
|
|
378
|
+
# Problem statistics: two encoded lines
|
|
379
|
+
s1 = (self._rdline() or '').split()
|
|
380
|
+
s2 = (self._rdline() or '').split()
|
|
381
|
+
try:
|
|
382
|
+
nrow = int(s1[0]); ncol = int(s1[1])
|
|
383
|
+
# s1[2] = colmx (unused)
|
|
384
|
+
nz = int(s1[3])
|
|
385
|
+
# s1[4] = nrhs (unused)
|
|
386
|
+
rhsnz = int(s1[5])
|
|
387
|
+
# s1[6] = nran (unused)
|
|
388
|
+
ranz = int(s1[7])
|
|
389
|
+
# s2[0] = nbd (unused)
|
|
390
|
+
bdnz = int(s2[1])
|
|
391
|
+
ns = int(s2[2])
|
|
392
|
+
except (IndexError, ValueError) as exc:
|
|
393
|
+
raise RuntimeError(
|
|
394
|
+
f"Bad statistics lines in {self._infile}"
|
|
395
|
+
) from exc
|
|
396
|
+
|
|
397
|
+
self._nrow = nrow
|
|
398
|
+
self._ncs = 1
|
|
399
|
+
self._cn = nrow # columns will be stored after rows
|
|
400
|
+
self._bsz = nrow + ncol
|
|
401
|
+
self._bs = [' '] * self._bsz
|
|
402
|
+
|
|
403
|
+
# Number table: ns pre-formatted floating-point strings
|
|
404
|
+
# Indexed 1-based; ss[0] is a placeholder that is never read.
|
|
405
|
+
self._ss = [''] * (ns + 1)
|
|
406
|
+
z, zi = '', 0
|
|
407
|
+
for i in range(1, ns + 1):
|
|
408
|
+
if zi >= len(z):
|
|
409
|
+
b = self._rdline()
|
|
410
|
+
z = b if b is not None else ''
|
|
411
|
+
zi = 0
|
|
412
|
+
val, zi = _exform(z, zi, self._ss, self._kmax)
|
|
413
|
+
self._ss[i] = val
|
|
414
|
+
self._kmax = ns
|
|
415
|
+
|
|
416
|
+
# ROWS section
|
|
417
|
+
for i in range(1, nrow + 1):
|
|
418
|
+
buf = self._rdline() or ''
|
|
419
|
+
if i == 1:
|
|
420
|
+
self._emit('ROWS')
|
|
421
|
+
row_type = buf[0] if buf else '?'
|
|
422
|
+
row_name = buf[1:] if len(buf) > 1 else ''
|
|
423
|
+
if self.blanksubst:
|
|
424
|
+
row_name = row_name.replace(' ', self.blanksubst)
|
|
425
|
+
self._emit(f' {row_type} {row_name}')
|
|
426
|
+
self._namstore(i, row_name)
|
|
427
|
+
|
|
428
|
+
# Data sections
|
|
429
|
+
self._colout('COLUMNS', nz, 1)
|
|
430
|
+
self._colout('RHS', rhsnz, 2)
|
|
431
|
+
self._colout('RANGES', ranz, 3)
|
|
432
|
+
self._colout('BOUNDS', bdnz, 4)
|
|
433
|
+
|
|
434
|
+
if self._ncs > 1:
|
|
435
|
+
self._checkline()
|
|
436
|
+
|
|
437
|
+
self._emit('ENDATA')
|
|
438
|
+
|
|
439
|
+
# Look for another LP in the same file
|
|
440
|
+
self._canend = True
|
|
441
|
+
self._ncs = 1
|
|
442
|
+
buf = self._rdline()
|
|
443
|
+
if buf is None:
|
|
444
|
+
break
|
|
445
|
+
|
|
446
|
+
return ''.join(self._out)
|
|
447
|
+
|
|
448
|
+
def _emit(self, s: str) -> None:
|
|
449
|
+
self._out.append(s if s.endswith('\n') else s + '\n')
|
|
450
|
+
|
|
451
|
+
def _checkchar(self, s: str) -> None:
|
|
452
|
+
"""Update the rolling checksum with all characters of line *s*.
|
|
453
|
+
|
|
454
|
+
Mirrors checkchar() in the C source. The hash accumulates one
|
|
455
|
+
character at a time; the final hash character (mod 92) is stored
|
|
456
|
+
in _chkbuf[_ncs].
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
x = 0
|
|
460
|
+
for c in s:
|
|
461
|
+
if c == '\n':
|
|
462
|
+
break
|
|
463
|
+
cv = _tr(c)
|
|
464
|
+
if x & 1:
|
|
465
|
+
x = (x >> 1) + cv + 16384
|
|
466
|
+
else:
|
|
467
|
+
x = (x >> 1) + cv
|
|
468
|
+
self._cfn[self._ncs] = self._infile
|
|
469
|
+
self._cfl[self._ncs] = self._nline
|
|
470
|
+
self._chkbuf[self._ncs] = TRTAB[x % 92]
|
|
471
|
+
self._ncs += 1
|
|
472
|
+
|
|
473
|
+
def _checkline(self) -> None:
|
|
474
|
+
"""Read and verify the next checksum line from the input.
|
|
475
|
+
|
|
476
|
+
Mirrors checkline() in the C source. The expected checksum line
|
|
477
|
+
is _chkbuf[0.._ncs] = ' ' + (ncs-1 hash chars) + '\\n'.
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
self._canend = False
|
|
481
|
+
while True:
|
|
482
|
+
self._chkbuf[self._ncs] = '\n'
|
|
483
|
+
expected = ''.join(self._chkbuf[:self._ncs + 1])
|
|
484
|
+
self._nline += 1
|
|
485
|
+
raw = self._inf.readline()
|
|
486
|
+
if not raw:
|
|
487
|
+
self._early_eof()
|
|
488
|
+
if raw != expected:
|
|
489
|
+
if raw.startswith(':') and self._ncs <= 72:
|
|
490
|
+
|
|
491
|
+
# Mystery line embedded at a checksum position
|
|
492
|
+
self._ncs -= 1
|
|
493
|
+
self._checkchar(raw)
|
|
494
|
+
if self.keepmyst:
|
|
495
|
+
self._emit(raw[1:].rstrip('\n'))
|
|
496
|
+
continue
|
|
497
|
+
self._badchk(raw)
|
|
498
|
+
break
|
|
499
|
+
self._ncs = 1
|
|
500
|
+
|
|
501
|
+
def _early_eof(self) -> None:
|
|
502
|
+
self._lastl = ''
|
|
503
|
+
raise RuntimeError(
|
|
504
|
+
f"Premature end of file at line {self._nline} of {self._infile}"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def _badchk(self, got: str) -> None:
|
|
508
|
+
expected = ''.join(self._chkbuf[:self._ncs + 1])
|
|
509
|
+
# Identify the first mismatching slot to report the corrupt data line
|
|
510
|
+
for i in range(1, self._ncs + 1):
|
|
511
|
+
if i >= len(got) or got[i] != expected[i]:
|
|
512
|
+
raise RuntimeError(
|
|
513
|
+
f"Checksum error: data line {self._cfl[i]} of "
|
|
514
|
+
f"{self._cfn[i]} appears corrupt\n"
|
|
515
|
+
f" expected checksum line: {expected!r}\n"
|
|
516
|
+
f" got: {got!r}"
|
|
517
|
+
)
|
|
518
|
+
raise RuntimeError(
|
|
519
|
+
f"Checksum error:\n expected: {expected!r}\n got: {got!r}"
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
def _rdline(self) -> Optional[str]:
|
|
523
|
+
"""Read one data line; return None at EOF when _canend is True."""
|
|
524
|
+
|
|
525
|
+
self._nline += 1
|
|
526
|
+
raw = self._inf.readline()
|
|
527
|
+
if not raw:
|
|
528
|
+
if self._canend:
|
|
529
|
+
return None
|
|
530
|
+
self._early_eof()
|
|
531
|
+
|
|
532
|
+
# Strip newline for processing (checkchar handles chars up to '\n')
|
|
533
|
+
s = raw.rstrip('\n')
|
|
534
|
+
self._checkchar(s)
|
|
535
|
+
if self._ncs >= 72:
|
|
536
|
+
self._checkline()
|
|
537
|
+
|
|
538
|
+
# Mystery line: starts with ':'
|
|
539
|
+
if s.startswith(':'):
|
|
540
|
+
if self.keepmyst:
|
|
541
|
+
self._emit(s[1:])
|
|
542
|
+
return self._rdline()
|
|
543
|
+
|
|
544
|
+
self._lastl = s
|
|
545
|
+
return s
|
|
546
|
+
|
|
547
|
+
def _namstore(self, i: int, s: str) -> None:
|
|
548
|
+
"""Store up to 8 characters of *s* at position *i* (1-based)."""
|
|
549
|
+
|
|
550
|
+
if not (1 <= i <= self._bsz):
|
|
551
|
+
raise RuntimeError(
|
|
552
|
+
f"namstore: index {i} out of range [1, {self._bsz}]"
|
|
553
|
+
)
|
|
554
|
+
self._bs[i - 1] = (s + ' ')[:8]
|
|
555
|
+
|
|
556
|
+
def _namfetch(self, i: int) -> str:
|
|
557
|
+
"""Retrieve the 8-character name stored at position *i* (1-based)."""
|
|
558
|
+
|
|
559
|
+
if not (1 <= i <= self._bsz):
|
|
560
|
+
raise RuntimeError(
|
|
561
|
+
f"namfetch: index {i} out of range [1, {self._bsz}]"
|
|
562
|
+
)
|
|
563
|
+
return self._bs[i - 1]
|
|
564
|
+
|
|
565
|
+
# Section output (COLUMNS / RHS / RANGES / BOUNDS)
|
|
566
|
+
|
|
567
|
+
def _colout(self, head: str, nz: int, what: int) -> None:
|
|
568
|
+
"""Output one MPS data section.
|
|
569
|
+
|
|
570
|
+
Parameters
|
|
571
|
+
----------
|
|
572
|
+
head : section header keyword
|
|
573
|
+
nz : number of nonzero/bound entries to process
|
|
574
|
+
what : 1=COLUMNS, 2=RHS, 3=RANGES, 4=BOUNDS
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
BT = self._BOUND_TYPES
|
|
578
|
+
|
|
579
|
+
if not nz:
|
|
580
|
+
# Empty section: COLUMNS and RHS still print the header line
|
|
581
|
+
if what <= 2:
|
|
582
|
+
self._emit(head)
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
first = True
|
|
586
|
+
k = 0 # pending nonzero counter (0 or 1)
|
|
587
|
+
z, zi = '', 0 # current data line and read position
|
|
588
|
+
curcol = ''
|
|
589
|
+
rc1 = rc2 = ''
|
|
590
|
+
rownm = ['', '']
|
|
591
|
+
|
|
592
|
+
while nz > 0:
|
|
593
|
+
nz -= 1
|
|
594
|
+
|
|
595
|
+
# Refill the line buffer when exhausted
|
|
596
|
+
if zi >= len(z):
|
|
597
|
+
b = self._rdline()
|
|
598
|
+
z = b if b is not None else ''
|
|
599
|
+
zi = 0
|
|
600
|
+
|
|
601
|
+
if first:
|
|
602
|
+
self._emit(head)
|
|
603
|
+
first = False
|
|
604
|
+
|
|
605
|
+
# Consume new-column markers (index == 0)
|
|
606
|
+
# A zero index signals that a column name follows inline in z.
|
|
607
|
+
n, zi = _exindx(z, zi)
|
|
608
|
+
while n == 0:
|
|
609
|
+
# Flush any pending single-nonzero output
|
|
610
|
+
if k:
|
|
611
|
+
self._emit(
|
|
612
|
+
f' {curcol:<8.8} {rownm[0]:<8.8} {rc1:.15}'
|
|
613
|
+
)
|
|
614
|
+
k = 0
|
|
615
|
+
# The column name is the remaining data in z after the index
|
|
616
|
+
seg = z[zi:] if zi < len(z) else head
|
|
617
|
+
if self.blanksubst:
|
|
618
|
+
seg = seg.replace(' ', self.blanksubst)
|
|
619
|
+
curcol = (seg + ' ')[:8]
|
|
620
|
+
if what == 1:
|
|
621
|
+
self._cn += 1
|
|
622
|
+
self._namstore(self._cn, seg)
|
|
623
|
+
# Advance to the next line for the nonzero data
|
|
624
|
+
b = self._rdline()
|
|
625
|
+
z = b if b is not None else ''
|
|
626
|
+
zi = 0
|
|
627
|
+
n, zi = _exindx(z, zi)
|
|
628
|
+
|
|
629
|
+
# Process the entry
|
|
630
|
+
if what >= 4:
|
|
631
|
+
# BOUNDS section: n is the bound-type index (1-based, 1..6)
|
|
632
|
+
if n >= 7:
|
|
633
|
+
raise RuntimeError(f"bad bound type index {n}")
|
|
634
|
+
if zi >= len(z):
|
|
635
|
+
b = self._rdline()
|
|
636
|
+
z = b if b is not None else ''
|
|
637
|
+
zi = 0
|
|
638
|
+
col_idx, zi = _exindx(z, zi)
|
|
639
|
+
rownm[0] = self._namfetch(self._nrow + col_idx)
|
|
640
|
+
if n >= 4:
|
|
641
|
+
# FR / MI / PL: no numeric value needed
|
|
642
|
+
# C: n-- >= 4 -> post-decrement; use n-1 as BT index
|
|
643
|
+
self._emit(
|
|
644
|
+
f' {BT[n - 1]} {curcol:<8.8} {rownm[0]:.8}'
|
|
645
|
+
)
|
|
646
|
+
continue
|
|
647
|
+
# UP / LO / FX: n decremented to 0-based BT index
|
|
648
|
+
n -= 1
|
|
649
|
+
else:
|
|
650
|
+
# COLUMNS / RHS / RANGES: n is the row index
|
|
651
|
+
rownm[k] = self._namfetch(n)
|
|
652
|
+
|
|
653
|
+
# Read the numeric value
|
|
654
|
+
if zi >= len(z):
|
|
655
|
+
b = self._rdline()
|
|
656
|
+
z = b if b is not None else ''
|
|
657
|
+
zi = 0
|
|
658
|
+
if k:
|
|
659
|
+
rc2, zi = _exform(z, zi, self._ss, self._kmax)
|
|
660
|
+
else:
|
|
661
|
+
rc1, zi = _exform(z, zi, self._ss, self._kmax)
|
|
662
|
+
|
|
663
|
+
# Emit the output line(s)
|
|
664
|
+
if what <= 3:
|
|
665
|
+
if self.just1:
|
|
666
|
+
self._emit(
|
|
667
|
+
f' {curcol:<8.8} {rownm[0]:<8.8} {rc1:.15}'
|
|
668
|
+
)
|
|
669
|
+
else:
|
|
670
|
+
k += 1
|
|
671
|
+
if k == 1:
|
|
672
|
+
continue # wait for a second nonzero on the same line
|
|
673
|
+
# Two nonzeros on one line (standard MPS compact format)
|
|
674
|
+
self._emit(
|
|
675
|
+
f' {curcol:<8.8} {rownm[0]:<8.8} '
|
|
676
|
+
f'{rc1:<15.15}{rownm[1]:<8.8} {rc2:.15}'
|
|
677
|
+
)
|
|
678
|
+
k = 0
|
|
679
|
+
else:
|
|
680
|
+
# BOUNDS with a value (UP / LO / FX)
|
|
681
|
+
self._emit(
|
|
682
|
+
f' {BT[n]} {curcol:<8.8} {rownm[0]:<8.8} {rc1:.15}'
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# Flush any unpaired pending nonzero
|
|
686
|
+
if k:
|
|
687
|
+
self._emit(
|
|
688
|
+
f' {curcol:<8.8} {rownm[0]:<8.8} {rc1:.15}'
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
# ---------------------------------------------------------------------------
|
|
693
|
+
# Convenience wrappers
|
|
694
|
+
# ---------------------------------------------------------------------------
|
|
695
|
+
|
|
696
|
+
def expand_mps(
|
|
697
|
+
input_file: "str | TextIO",
|
|
698
|
+
output_file: "str | TextIO | None" = None,
|
|
699
|
+
*,
|
|
700
|
+
keepmyst: bool = True,
|
|
701
|
+
blanksubst: Optional[str] = None,
|
|
702
|
+
just1: bool = False,
|
|
703
|
+
) -> Optional[str]:
|
|
704
|
+
"""Expand a compressed netlib LP file to MPS format.
|
|
705
|
+
|
|
706
|
+
Parameters
|
|
707
|
+
----------
|
|
708
|
+
input_file
|
|
709
|
+
Filesystem path (str) or readable file object.
|
|
710
|
+
output_file
|
|
711
|
+
Filesystem path, writable file object, or None.
|
|
712
|
+
When None the MPS text is returned as a string.
|
|
713
|
+
keepmyst
|
|
714
|
+
Include mystery-line extensions in output (default True).
|
|
715
|
+
blanksubst
|
|
716
|
+
Replace blanks in names with this character (e.g. '_').
|
|
717
|
+
just1
|
|
718
|
+
Output at most one nonzero per line.
|
|
719
|
+
|
|
720
|
+
Returns
|
|
721
|
+
-------
|
|
722
|
+
MPS text string when output_file is None; otherwise None.
|
|
723
|
+
|
|
724
|
+
Examples
|
|
725
|
+
--------
|
|
726
|
+
>>> mps = expand_mps("afiro.mps.netlib") # returns str
|
|
727
|
+
>>> expand_mps("afiro.mps.netlib", "afiro.mps") # writes file
|
|
728
|
+
>>> with open("afiro.mps.netlib") as f:
|
|
729
|
+
... mps = expand_mps(f)
|
|
730
|
+
"""
|
|
731
|
+
|
|
732
|
+
return EMPSConverter(
|
|
733
|
+
keepmyst=keepmyst,
|
|
734
|
+
blanksubst=blanksubst,
|
|
735
|
+
just1=just1,
|
|
736
|
+
).convert(input_file, output_file)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def expand_mps_string(
|
|
740
|
+
compressed_text: str,
|
|
741
|
+
*,
|
|
742
|
+
keepmyst: bool = True,
|
|
743
|
+
blanksubst: Optional[str] = None,
|
|
744
|
+
just1: bool = False,
|
|
745
|
+
) -> str:
|
|
746
|
+
"""Expand compressed LP text supplied as a string; return MPS text.
|
|
747
|
+
|
|
748
|
+
Examples
|
|
749
|
+
--------
|
|
750
|
+
>>> with open("afiro.mps.netlib") as f:
|
|
751
|
+
... raw = f.read()
|
|
752
|
+
>>> mps = expand_mps_string(raw)
|
|
753
|
+
"""
|
|
754
|
+
|
|
755
|
+
result = expand_mps(
|
|
756
|
+
io.StringIO(compressed_text),
|
|
757
|
+
keepmyst=keepmyst,
|
|
758
|
+
blanksubst=blanksubst,
|
|
759
|
+
just1=just1,
|
|
760
|
+
)
|
|
761
|
+
assert result is not None
|
|
762
|
+
return result
|
|
763
|
+
|
|
764
|
+
def __main():
|
|
765
|
+
|
|
766
|
+
parser = argparse.ArgumentParser(
|
|
767
|
+
prog='gsimplex-emps',
|
|
768
|
+
description='Expand compressed netlib LP files to MPS format. See also: https://www.netlib.org/lp/data',
|
|
769
|
+
)
|
|
770
|
+
parser.add_argument(
|
|
771
|
+
'files', nargs='*', metavar='FILE',
|
|
772
|
+
help='Compressed input file(s); reads stdin when none given.',
|
|
773
|
+
)
|
|
774
|
+
parser.add_argument(
|
|
775
|
+
'-1', dest='just1', action='store_true',
|
|
776
|
+
help='Output only one nonzero per line.',
|
|
777
|
+
)
|
|
778
|
+
parser.add_argument(
|
|
779
|
+
'-b', dest='blanksubst', action='store_const', const='_', default=None,
|
|
780
|
+
help="Replace blanks within names with '_'.",
|
|
781
|
+
)
|
|
782
|
+
parser.add_argument(
|
|
783
|
+
'-m', dest='keepmyst', action='store_false',
|
|
784
|
+
help='Skip mystery lines.',
|
|
785
|
+
)
|
|
786
|
+
args = parser.parse_args()
|
|
787
|
+
|
|
788
|
+
conv = EMPSConverter(
|
|
789
|
+
keepmyst=args.keepmyst,
|
|
790
|
+
blanksubst=args.blanksubst,
|
|
791
|
+
just1=args.just1,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
if not args.files:
|
|
795
|
+
sys.stdout.write(conv.convert(sys.stdin)) # type: ignore[arg-type]
|
|
796
|
+
else:
|
|
797
|
+
for path in args.files:
|
|
798
|
+
result = conv.convert(path)
|
|
799
|
+
assert result is not None
|
|
800
|
+
sys.stdout.write(result)
|
|
801
|
+
|
|
802
|
+
return 0
|
|
803
|
+
|
|
804
|
+
if __name__ == '__main__':
|
|
805
|
+
sys.exit(__main())
|