primalsolver 0.1.0__tar.gz → 0.1.2__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 (61) hide show
  1. {primalsolver-0.1.0/src/primalsolver.egg-info → primalsolver-0.1.2}/PKG-INFO +8 -7
  2. {primalsolver-0.1.0 → primalsolver-0.1.2}/README.md +8 -7
  3. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/mpsio.c +143 -16
  4. {primalsolver-0.1.0 → primalsolver-0.1.2}/pyproject.toml +4 -1
  5. {primalsolver-0.1.0 → primalsolver-0.1.2}/setup.py +6 -2
  6. {primalsolver-0.1.0 → primalsolver-0.1.2}/src/primalsolver/__init__.py +1 -1
  7. {primalsolver-0.1.0 → primalsolver-0.1.2/src/primalsolver.egg-info}/PKG-INFO +8 -7
  8. {primalsolver-0.1.0 → primalsolver-0.1.2}/LICENSE +0 -0
  9. {primalsolver-0.1.0 → primalsolver-0.1.2}/MANIFEST.in +0 -0
  10. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/cbf.c +0 -0
  11. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/cbf.h +0 -0
  12. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/expcone.c +0 -0
  13. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/expcone.h +0 -0
  14. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/ipm.c +0 -0
  15. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/ipm.h +0 -0
  16. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/linalg.c +0 -0
  17. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/linalg.h +0 -0
  18. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/mpsio.h +0 -0
  19. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/presolve.c +0 -0
  20. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/presolve.h +0 -0
  21. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal.h +0 -0
  22. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_afe.c +0 -0
  23. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_bar.c +0 -0
  24. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_conicopt.c +0 -0
  25. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_core.c +0 -0
  26. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_djc.c +0 -0
  27. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_get.c +0 -0
  28. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_info.c +0 -0
  29. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_meta.c +0 -0
  30. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_mip.c +0 -0
  31. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_mip_opt.c +0 -0
  32. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_misc.c +0 -0
  33. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_names.c +0 -0
  34. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_optimize.c +0 -0
  35. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_priv.h +0 -0
  36. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_put.c +0 -0
  37. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_qcon.c +0 -0
  38. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_quad.c +0 -0
  39. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_sdptask.c +0 -0
  40. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_solio.c +0 -0
  41. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_solution.c +0 -0
  42. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_std.c +0 -0
  43. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_vartype.c +0 -0
  44. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/primal_verdict.c +0 -0
  45. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/scaling.c +0 -0
  46. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/scaling.h +0 -0
  47. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/sdp.c +0 -0
  48. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/sdp.h +0 -0
  49. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/simplex.c +0 -0
  50. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/simplex.h +0 -0
  51. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/socp.c +0 -0
  52. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/socp.h +0 -0
  53. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/stdform.c +0 -0
  54. {primalsolver-0.1.0 → primalsolver-0.1.2}/csrc/stdform.h +0 -0
  55. {primalsolver-0.1.0 → primalsolver-0.1.2}/setup.cfg +0 -0
  56. {primalsolver-0.1.0 → primalsolver-0.1.2}/src/primalsolver/_bindings.py +0 -0
  57. {primalsolver-0.1.0 → primalsolver-0.1.2}/src/primalsolver/_native.py +0 -0
  58. {primalsolver-0.1.0 → primalsolver-0.1.2}/src/primalsolver.egg-info/SOURCES.txt +0 -0
  59. {primalsolver-0.1.0 → primalsolver-0.1.2}/src/primalsolver.egg-info/dependency_links.txt +0 -0
  60. {primalsolver-0.1.0 → primalsolver-0.1.2}/src/primalsolver.egg-info/top_level.txt +0 -0
  61. {primalsolver-0.1.0 → primalsolver-0.1.2}/sync_csrc.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: primalsolver
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: PrimalSolver: dependency-free convex optimization (LP/QP/SOCP/SDP/exp-power/MIP) in C99, callable from Python via ctypes.
5
5
  Author: Gaetano Minardi
6
6
  License: Apache-2.0
@@ -208,13 +208,14 @@ Notes:
208
208
  git push origin main --tags
209
209
  ```
210
210
 
211
- 6. **Publish**: a Git **tag** triggers `.github/workflows/wheels.yml`, which
212
- builds the wheels + sdist and uploads them as artifacts. Publishing to PyPI
213
- runs only when a **GitHub Release** is published for that tag, using
214
- **trusted publishing** (OIDC, no API token stored).
211
+ 6. **Publish**: pushing a **tag** `vX.Y.Z` triggers
212
+ `.github/workflows/wheels.yml`, which builds the wheels + sdist and uploads
213
+ them as artifacts. Publishing to PyPI runs when a **GitHub Release** is
214
+ published for that tag, using the repository secret `PYPI_API_TOKEN` (token
215
+ auth).
215
216
 
216
- Prerequisites (one-time): register a PyPI *trusted publisher* for this
217
- repository and create a `pypi` environment in the GitHub repo settings.
217
+ Prerequisites (one-time): add `PYPI_API_TOKEN` in the repo (Settings →
218
+ Secrets and variables → Actions) with the PyPI API token.
218
219
 
219
220
  Manual/fallback publish (e.g. TestPyPI):
220
221
 
@@ -191,13 +191,14 @@ Notes:
191
191
  git push origin main --tags
192
192
  ```
193
193
 
194
- 6. **Publish**: a Git **tag** triggers `.github/workflows/wheels.yml`, which
195
- builds the wheels + sdist and uploads them as artifacts. Publishing to PyPI
196
- runs only when a **GitHub Release** is published for that tag, using
197
- **trusted publishing** (OIDC, no API token stored).
198
-
199
- Prerequisites (one-time): register a PyPI *trusted publisher* for this
200
- repository and create a `pypi` environment in the GitHub repo settings.
194
+ 6. **Publish**: pushing a **tag** `vX.Y.Z` triggers
195
+ `.github/workflows/wheels.yml`, which builds the wheels + sdist and uploads
196
+ them as artifacts. Publishing to PyPI runs when a **GitHub Release** is
197
+ published for that tag, using the repository secret `PYPI_API_TOKEN` (token
198
+ auth).
199
+
200
+ Prerequisites (one-time): add `PYPI_API_TOKEN` in the repo (Settings →
201
+ Secrets and variables → Actions) with the PyPI API token.
201
202
 
202
203
  Manual/fallback publish (e.g. TestPyPI):
203
204
 
@@ -83,21 +83,73 @@ static void toks_push(Toks *T, const char *s, int len) {
83
83
  * Grammar: a '*' is a comment only in column 1 (MPS), a '\' truncates the rest
84
84
  * of the line even inside MPS text (LP), and the characters : < > = are tokens
85
85
  * of their own -- with a following '=' glued to them -- so a reader can
86
- * recognise a relation without looking ahead. */
87
- static int toks_load(const char *filename, Toks *T) {
86
+ * recognise a relation without looking ahead. `lp` selects the LP scan, where
87
+ * '+' '-' '*' are separators too and a number is scanned whole, so a term may
88
+ * be written "x0+x1", "2x0" or "2*x" with no spaces; the MPS scan keeps
89
+ * whitespace-delimited names, where those characters may be part of a name. */
90
+ static int toks_load(const char *filename, Toks *T, int lp) {
88
91
  FILE *f = fopen(filename, "r");
89
92
  if (!f) return 0;
90
93
  char line[8192];
91
94
  while (fgets(line, sizeof line, f)) {
92
95
  T->curline++;
93
- char *p = line;
94
- if (*p == '*') continue; /* MPS comment line */
96
+ if (line[0] == '*') continue; /* comment line (MPS; also seen in LP) */
95
97
  char *q = strchr(line, '\\');
96
98
  if (q) *q = 0; /* LP comment */
97
- p = line;
99
+ char *p = line;
100
+ if (lp) {
101
+ /* LP grammar: '+' '-' '*' are separators and are routinely glued
102
+ * to what they act on ("x0+x1", "2x0", "-x", "2*x"), so a name
103
+ * never contains one, a sign is always its own token, and a number
104
+ * is scanned whole (exponent sign included). Without this every
105
+ * glued term coined a variable named after the whole text -- the
106
+ * phantom "+COLONE" of issue #27 was the sign case. */
107
+ while (*p) {
108
+ while (*p && isspace((unsigned char)*p)) p++;
109
+ if (!*p) break;
110
+ unsigned char ch = (unsigned char)*p;
111
+ if (ch == ':' || ch == '[' || ch == ']') {
112
+ toks_push(T, p, 1); p++; continue;
113
+ }
114
+ if (ch == '<' || ch == '>' || ch == '=') {
115
+ int len = (p[1] == '=') ? 2 : 1;
116
+ toks_push(T, p, len); p += len; continue;
117
+ }
118
+ if (ch == '+' || ch == '-') { toks_push(T, p, 1); p++; continue; }
119
+ if (ch == '*') { p++; continue; } /* multiplication: a separator */
120
+ if (isdigit(ch) || (ch == '.' && isdigit((unsigned char)p[1]))) {
121
+ char *st = p;
122
+ while (isdigit((unsigned char)*p)) p++;
123
+ if (*p == '.') { p++; while (isdigit((unsigned char)*p)) p++; }
124
+ if (*p == 'e' || *p == 'E') {
125
+ char *sv = p; p++;
126
+ if (*p == '+' || *p == '-') p++;
127
+ if (isdigit((unsigned char)*p))
128
+ while (isdigit((unsigned char)*p)) p++;
129
+ else p = sv; /* not an exponent after all */
130
+ }
131
+ toks_push(T, st, (int)(p - st)); continue;
132
+ }
133
+ if (isalpha(ch) || ch == '_') {
134
+ char *st = p; p++;
135
+ while (isalnum((unsigned char)*p) || *p == '_' || *p == '.' ||
136
+ *p == '$' || *p == '~') p++;
137
+ toks_push(T, st, (int)(p - st)); continue;
138
+ }
139
+ toks_push(T, p, 1); p++; /* unknown char: never stall */
140
+ }
141
+ continue;
142
+ }
143
+ /* MPS / free MPS: names are whitespace-delimited with no operators, so a
144
+ * sign glued to a name ("+COLONE") is two tokens while a sign glued to a
145
+ * number ("-3.5") stays one (parse_num consumes it whole). */
98
146
  while (*p) {
99
147
  while (*p && isspace((unsigned char)*p)) p++;
100
148
  if (!*p) break;
149
+ if ((*p == '+' || *p == '-') &&
150
+ (isalpha((unsigned char)p[1]) || p[1] == '_')) {
151
+ toks_push(T, p, 1); p++; continue;
152
+ }
101
153
  if (*p == ':') { toks_push(T, p, 1); p++; continue; }
102
154
  if (*p == '<' || *p == '>') {
103
155
  int len = (p[1] == '=') ? 2 : 1;
@@ -178,6 +230,19 @@ static char rel_char(const char *s) {
178
230
  return 'E';
179
231
  }
180
232
 
233
+ /* True if token i may continue the statement whose previous token is i-1. An
234
+ * LP expression wraps onto the next line only when a '+'/'-' sits at one end of
235
+ * the break; without that a new line is a new statement. This is what tells
236
+ * "x <= 4\n x0 <= 3" (two constraints) apart from "x <=\n 4 + y" (one wrapped
237
+ * constraint), and it is why the readers know where a constraint ends. */
238
+ static int lp_cont(const Toks *T, int i) {
239
+ if (i <= 0) return 1;
240
+ if (T->line[i] == T->line[i - 1]) return 1;
241
+ if (ieq(T->tok[i], "+") || ieq(T->tok[i], "-")) return 1;
242
+ if (ieq(T->tok[i - 1], "+") || ieq(T->tok[i - 1], "-")) return 1;
243
+ return 0;
244
+ }
245
+
181
246
  /* Same out-of-memory rule as toks_push: allocate or exit. */
182
247
  static char *xstrdup(const char *s) {
183
248
  size_t n = strlen(s) + 1;
@@ -267,6 +332,15 @@ static int parse_num(const char *s, double *v) {
267
332
  return 1;
268
333
  }
269
334
 
335
+ /* An LP bound may be written "inf"/"infinity" (optionally signed) instead of a
336
+ * number; the LP scanner emits the sign separately, so this is never asked for
337
+ * "-inf" as one token. Recognising it keeps "x <= +inf" from coining a
338
+ * variable named "+inf". */
339
+ static int parse_inf(const char *s, double *v) {
340
+ if (ieq(s, "inf") || ieq(s, "infinity")) { *v = INF; return 1; }
341
+ return 0;
342
+ }
343
+
270
344
  /* ================= MPS writer ================= */
271
345
  /* The writer names rows and columns as the TASK calls them when the name is
272
346
  * non-empty and free of whitespace (MPS/LP names are whitespace-delimited), and
@@ -1169,7 +1243,9 @@ static PRIMALrescodee lp_read(PRIMALtask_t t, const Toks *T) {
1169
1243
  double v;
1170
1244
  if (parse_num(tk, &v)) {
1171
1245
  if (i + 1 < T->n && !is_lp_section(T->tok[i + 1]) &&
1172
- !tok_is(T, i + 1, ":")) {
1246
+ !tok_is(T, i + 1, ":") && !ieq(T->tok[i + 1], "+") &&
1247
+ !ieq(T->tok[i + 1], "-") &&
1248
+ T->line[i + 1] == T->line[i]) {
1173
1249
  int c = names_get(&cols, T->tok[i + 1], 1);
1174
1250
  ENSURE_CJ(c);
1175
1251
  cj[c] += sign * v;
@@ -1213,9 +1289,11 @@ static PRIMALrescodee lp_read(PRIMALtask_t t, const Toks *T) {
1213
1289
  }
1214
1290
  i += 2;
1215
1291
  }
1292
+ int cstart = i;
1216
1293
  double lsign = 1.0, lconst = 0.0;
1217
1294
  while (i < T->n && !is_rel(T->tok[i]) && !is_lp_section(T->tok[i]) &&
1218
- !tok_is(T, i, ":") && !tok_is(T, i + 1, ":")) {
1295
+ !tok_is(T, i, ":") && !tok_is(T, i + 1, ":") &&
1296
+ (i == cstart || lp_cont(T, i))) {
1219
1297
  if (T->tok[i][0] == '[' || T->tok[i][0] == ']') { rc = PRIMAL_RES_ERR_FILE; goto done; }
1220
1298
  if (ieq(T->tok[i], "+")) { lsign = 1.0; i++; continue; }
1221
1299
  if (ieq(T->tok[i], "-")) { lsign = -1.0; i++; continue; }
@@ -1223,7 +1301,8 @@ static PRIMALrescodee lp_read(PRIMALtask_t t, const Toks *T) {
1223
1301
  if (parse_num(T->tok[i], &v)) {
1224
1302
  if (i + 2 < T->n && !is_rel(T->tok[i + 1]) &&
1225
1303
  !is_lp_section(T->tok[i + 1]) && !tok_is(T, i + 1, ":") &&
1226
- !tok_is(T, i + 2, ":")) {
1304
+ !tok_is(T, i + 2, ":") && !ieq(T->tok[i + 1], "+") &&
1305
+ !ieq(T->tok[i + 1], "-") && T->line[i + 1] == T->line[i]) {
1227
1306
  int c = names_get(&cols, T->tok[i + 1], 1);
1228
1307
  ipush(&C->sub, c); dpush(&C->val, lsign * v);
1229
1308
  i += 2;
@@ -1237,9 +1316,11 @@ static PRIMALrescodee lp_read(PRIMALtask_t t, const Toks *T) {
1237
1316
  }
1238
1317
  if (i >= T->n || !is_rel(T->tok[i])) { rc = PRIMAL_RES_ERR_FILE; goto done; }
1239
1318
  char r2 = rel_char(T->tok[i++]);
1319
+ int rstart = i;
1240
1320
  double rsign = 1.0, rconst = 0.0;
1241
1321
  while (i < T->n && !is_lp_section(T->tok[i]) && !is_rel(T->tok[i]) &&
1242
- !tok_is(T, i, ":") && !tok_is(T, i + 1, ":")) {
1322
+ !tok_is(T, i, ":") && !tok_is(T, i + 1, ":") &&
1323
+ (i == rstart || lp_cont(T, i))) {
1243
1324
  if (T->tok[i][0] == '[' || T->tok[i][0] == ']') { rc = PRIMAL_RES_ERR_FILE; goto done; }
1244
1325
  if (ieq(T->tok[i], "+")) { rsign = 1.0; i++; continue; }
1245
1326
  if (ieq(T->tok[i], "-")) { rsign = -1.0; i++; continue; }
@@ -1247,7 +1328,8 @@ static PRIMALrescodee lp_read(PRIMALtask_t t, const Toks *T) {
1247
1328
  if (parse_num(T->tok[i], &v)) {
1248
1329
  if (i + 2 < T->n && !is_lp_section(T->tok[i + 1]) &&
1249
1330
  !is_rel(T->tok[i + 1]) && !tok_is(T, i + 1, ":") &&
1250
- !tok_is(T, i + 2, ":")) {
1331
+ !tok_is(T, i + 2, ":") && !ieq(T->tok[i + 1], "+") &&
1332
+ !ieq(T->tok[i + 1], "-") && T->line[i + 1] == T->line[i]) {
1251
1333
  /* coef var on the right: move left negated */
1252
1334
  int c = names_get(&cols, T->tok[i + 1], 1);
1253
1335
  ipush(&C->sub, c); dpush(&C->val, -rsign * v);
@@ -1263,6 +1345,50 @@ static PRIMALrescodee lp_read(PRIMALtask_t t, const Toks *T) {
1263
1345
  C->rel = r2;
1264
1346
  C->rhs = rconst - lconst;
1265
1347
  ncon++;
1348
+ /* A ranged row "l rel1 expr rel2 r": the row above is "l - expr
1349
+ * rel1 0"; the second relation makes "expr rel2 r" a row of its
1350
+ * own, so the other bound is not silently dropped. The middle
1351
+ * expression is -(C), since C holds l - expr. */
1352
+ if (i < T->n && is_rel(T->tok[i])) {
1353
+ if (ncon == con_cap) {
1354
+ con_cap = con_cap ? con_cap * 2 : 8;
1355
+ LpCon *nc2 = (LpCon *)realloc(cons, (size_t)con_cap * sizeof(LpCon));
1356
+ if (!nc2) { rc = PRIMAL_RES_ERR_ALLOC; goto done; }
1357
+ cons = nc2;
1358
+ }
1359
+ C = &cons[ncon - 1]; /* realloc may have moved the array */
1360
+ LpCon *D = &cons[ncon];
1361
+ memset(D, 0, sizeof *D);
1362
+ D->rel = rel_char(T->tok[i++]);
1363
+ for (int z = 0; z < C->sub.n; z++) {
1364
+ ipush(&D->sub, C->sub.a[z]);
1365
+ dpush(&D->val, -C->val.a[z]);
1366
+ }
1367
+ double dsign = 1.0, dconst = 0.0;
1368
+ while (i < T->n && !is_lp_section(T->tok[i]) && !is_rel(T->tok[i]) &&
1369
+ !tok_is(T, i, ":") && !tok_is(T, i + 1, ":")) {
1370
+ if (ieq(T->tok[i], "+")) { dsign = 1.0; i++; continue; }
1371
+ if (ieq(T->tok[i], "-")) { dsign = -1.0; i++; continue; }
1372
+ double v2;
1373
+ if (parse_num(T->tok[i], &v2) || parse_inf(T->tok[i], &v2)) {
1374
+ if (i + 2 < T->n && !is_rel(T->tok[i + 1]) &&
1375
+ !is_lp_section(T->tok[i + 1]) && !tok_is(T, i + 1, ":") &&
1376
+ !tok_is(T, i + 2, ":") && !ieq(T->tok[i + 1], "+") &&
1377
+ !ieq(T->tok[i + 1], "-") && T->line[i + 1] == T->line[i]) {
1378
+ int c = names_get(&cols, T->tok[i + 1], 1);
1379
+ ipush(&D->sub, c); dpush(&D->val, -dsign * v2);
1380
+ i += 2;
1381
+ } else { dconst += dsign * v2; i++; }
1382
+ } else {
1383
+ int c = names_get(&cols, T->tok[i], 1);
1384
+ ipush(&D->sub, c); dpush(&D->val, -dsign);
1385
+ i++;
1386
+ }
1387
+ dsign = 1.0;
1388
+ }
1389
+ D->rhs = dconst;
1390
+ ncon++;
1391
+ }
1266
1392
  }
1267
1393
  continue;
1268
1394
  }
@@ -1274,7 +1400,7 @@ static PRIMALrescodee lp_read(PRIMALtask_t t, const Toks *T) {
1274
1400
  if (ieq(T->tok[k], "-")) { sgn = -1.0; k++; }
1275
1401
  else if (ieq(T->tok[k], "+")) k++;
1276
1402
  double v;
1277
- if (parse_num(T->tok[k], &v)) {
1403
+ if (parse_num(T->tok[k], &v) || parse_inf(T->tok[k], &v)) {
1278
1404
  v *= sgn;
1279
1405
  i = k + 1;
1280
1406
  if (i >= T->n || !is_rel(T->tok[i])) { rc = PRIMAL_RES_ERR_FILE; goto done; }
@@ -1289,7 +1415,7 @@ static PRIMALrescodee lp_read(PRIMALtask_t t, const Toks *T) {
1289
1415
  if (ieq(T->tok[i], "-")) { s2 = -1.0; i++; }
1290
1416
  else if (ieq(T->tok[i], "+")) i++;
1291
1417
  double v2;
1292
- if (i >= T->n || !parse_num(T->tok[i], &v2)) { rc = PRIMAL_RES_ERR_FILE; goto done; }
1418
+ if (i >= T->n || (!parse_num(T->tok[i], &v2) && !parse_inf(T->tok[i], &v2))) { rc = PRIMAL_RES_ERR_FILE; goto done; }
1293
1419
  upfin[c] = 1; bup[c] = v2 * s2;
1294
1420
  i++;
1295
1421
  }
@@ -1306,7 +1432,7 @@ static PRIMALrescodee lp_read(PRIMALtask_t t, const Toks *T) {
1306
1432
  if (ieq(T->tok[i], "-")) { s2 = -1.0; i++; }
1307
1433
  else if (ieq(T->tok[i], "+")) i++;
1308
1434
  double v2;
1309
- if (i >= T->n || !parse_num(T->tok[i], &v2)) { rc = PRIMAL_RES_ERR_FILE; goto done; }
1435
+ if (i >= T->n || (!parse_num(T->tok[i], &v2) && !parse_inf(T->tok[i], &v2))) { rc = PRIMAL_RES_ERR_FILE; goto done; }
1310
1436
  v2 *= s2;
1311
1437
  if (rc2 == 'L') { upfin[c] = 1; bup[c] = v2; }
1312
1438
  else if (rc2 == 'G') { lofin[c] = 1; blo[c] = v2; }
@@ -2171,8 +2297,9 @@ PRIMALrescodee primalio_read(PRIMALtask_t t, const char *filename) {
2171
2297
  return rc;
2172
2298
  }
2173
2299
  Toks T = {0};
2174
- if (!toks_load(filename, &T)) return PRIMAL_RES_ERR_FILE;
2175
- PRIMALrescodee rc = has_ext(filename, ".lp") ? lp_read(t, &T) : mps_read(t, &T);
2300
+ int islp = has_ext(filename, ".lp");
2301
+ if (!toks_load(filename, &T, islp)) return PRIMAL_RES_ERR_FILE;
2302
+ PRIMALrescodee rc = islp ? lp_read(t, &T) : mps_read(t, &T);
2176
2303
  toks_free(&T);
2177
2304
  return rc;
2178
2305
  }
@@ -2217,7 +2344,7 @@ PRIMALrescodee primalio_read_format(PRIMALtask_t t, const char *filename, int fo
2217
2344
  if (format == PRIMAL_DATA_FORMAT_MPS || format == PRIMAL_DATA_FORMAT_LP ||
2218
2345
  format == PRIMAL_DATA_FORMAT_FREE_MPS) {
2219
2346
  Toks T = {0};
2220
- if (!toks_load(filename, &T)) return PRIMAL_RES_ERR_FILE;
2347
+ if (!toks_load(filename, &T, format == PRIMAL_DATA_FORMAT_LP)) return PRIMAL_RES_ERR_FILE;
2221
2348
  PRIMALrescodee rc = (format == PRIMAL_DATA_FORMAT_LP) ? lp_read(t, &T) : mps_read(t, &T);
2222
2349
  toks_free(&T);
2223
2350
  return rc;
@@ -43,7 +43,10 @@ test-command = "python -c \"import primalsolver as ps; print(ps.version())\""
43
43
  # csrc/ is vendored in the repo (run tools/../sync_csrc.py before tagging).
44
44
 
45
45
  [tool.cibuildwheel.linux]
46
- archs = ["x86_64", "aarch64"]
46
+ # x86_64 only: aarch64 requires QEMU emulation on an x86_64 runner, which is
47
+ # very slow (or hangs) building the C library. Enable it with a native arm64
48
+ # runner (runs-on: ubuntu-24.04-arm) if needed.
49
+ archs = ["x86_64"]
47
50
 
48
51
  [tool.cibuildwheel.macos]
49
52
  archs = ["x86_64", "arm64"]
@@ -63,8 +63,12 @@ def _build_shared(dest):
63
63
  base = cc + ["-std=c99", "-O2", "-fPIC", "-pthread", "-I", srcdir]
64
64
  if sys.platform == "darwin":
65
65
  out = os.path.join(dest, "libprimal.dylib")
66
- cmd = base + ["-dynamiclib", "-install_name", "@rpath/libprimal.dylib",
67
- "-o", out, *srcs, "-lm"]
66
+ # Build a universal2 dylib by default: cibuildwheel builds the x86_64 and
67
+ # arm64 wheels on one runner, and each needs its slice present in the
68
+ # library (delocate --require-archs). Skip if CC already sets -arch.
69
+ arch = [] if "-arch" in cc else ["-arch", "arm64", "-arch", "x86_64"]
70
+ cmd = base + arch + ["-dynamiclib", "-install_name", "@rpath/libprimal.dylib",
71
+ "-o", out, *srcs, "-lm"]
68
72
  elif sys.platform.startswith("linux"):
69
73
  out = os.path.join(dest, "libprimal.so")
70
74
  cmd = base + ["-shared", "-Wl,-soname,libprimal.so", "-o", out, *srcs, "-lm"]
@@ -22,7 +22,7 @@ from collections import namedtuple
22
22
 
23
23
  from . import _native as _n
24
24
 
25
- __version__ = "0.1.0"
25
+ __version__ = "0.1.2"
26
26
 
27
27
  LIB = _n.LIB
28
28
  c_void_p = C.c_void_p
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: primalsolver
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: PrimalSolver: dependency-free convex optimization (LP/QP/SOCP/SDP/exp-power/MIP) in C99, callable from Python via ctypes.
5
5
  Author: Gaetano Minardi
6
6
  License: Apache-2.0
@@ -208,13 +208,14 @@ Notes:
208
208
  git push origin main --tags
209
209
  ```
210
210
 
211
- 6. **Publish**: a Git **tag** triggers `.github/workflows/wheels.yml`, which
212
- builds the wheels + sdist and uploads them as artifacts. Publishing to PyPI
213
- runs only when a **GitHub Release** is published for that tag, using
214
- **trusted publishing** (OIDC, no API token stored).
211
+ 6. **Publish**: pushing a **tag** `vX.Y.Z` triggers
212
+ `.github/workflows/wheels.yml`, which builds the wheels + sdist and uploads
213
+ them as artifacts. Publishing to PyPI runs when a **GitHub Release** is
214
+ published for that tag, using the repository secret `PYPI_API_TOKEN` (token
215
+ auth).
215
216
 
216
- Prerequisites (one-time): register a PyPI *trusted publisher* for this
217
- repository and create a `pypi` environment in the GitHub repo settings.
217
+ Prerequisites (one-time): add `PYPI_API_TOKEN` in the repo (Settings →
218
+ Secrets and variables → Actions) with the PyPI API token.
218
219
 
219
220
  Manual/fallback publish (e.g. TestPyPI):
220
221
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes