scs 0.5.6 → 0.6.0

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +1 -1
  4. data/lib/scs/ffi.rb +18 -0
  5. data/lib/scs/version.rb +1 -1
  6. data/vendor/scs/CITATION.cff +2 -3
  7. data/vendor/scs/CLAUDE.md +115 -0
  8. data/vendor/scs/CMakeLists.txt +330 -201
  9. data/vendor/scs/CONTRIBUTING.md +49 -0
  10. data/vendor/scs/Makefile +90 -43
  11. data/vendor/scs/README.md +96 -2
  12. data/vendor/scs/include/aa.h +59 -11
  13. data/vendor/scs/include/aa_stats.h +47 -0
  14. data/vendor/scs/include/cones.h +16 -7
  15. data/vendor/scs/include/glbopts.h +113 -26
  16. data/vendor/scs/include/linalg.h +5 -1
  17. data/vendor/scs/include/linsys.h +1 -2
  18. data/vendor/scs/include/normalize.h +6 -2
  19. data/vendor/scs/include/rw.h +9 -3
  20. data/vendor/scs/include/scs.h +17 -1
  21. data/vendor/scs/include/scs_blas.h +8 -0
  22. data/vendor/scs/include/scs_types.h +2 -2
  23. data/vendor/scs/include/scs_work.h +22 -1
  24. data/vendor/scs/include/util.h +3 -6
  25. data/vendor/scs/include/util_spectral_cones.h +3 -3
  26. data/vendor/scs/linsys/accelerate/direct/private.c +126 -0
  27. data/vendor/scs/linsys/accelerate/direct/private.h +34 -0
  28. data/vendor/scs/linsys/cpu/dense/private.c +250 -0
  29. data/vendor/scs/linsys/cpu/dense/private.h +31 -0
  30. data/vendor/scs/linsys/cpu/direct/private.c +86 -44
  31. data/vendor/scs/linsys/cpu/indirect/private.c +647 -113
  32. data/vendor/scs/linsys/cpu/indirect/private.h +28 -0
  33. data/vendor/scs/linsys/csparse.c +9 -3
  34. data/vendor/scs/linsys/csparse.h +4 -2
  35. data/vendor/scs/linsys/cudss/direct/private.c +77 -43
  36. data/vendor/scs/linsys/cudss/direct/private.h +33 -7
  37. data/vendor/scs/linsys/gpu/indirect/private.c +6 -2
  38. data/vendor/scs/linsys/mkl/direct/private.c +63 -31
  39. data/vendor/scs/linsys/mkl/direct/private.h +0 -1
  40. data/vendor/scs/linsys/scs_matrix.c +291 -165
  41. data/vendor/scs/linsys/scs_matrix.h +7 -9
  42. data/vendor/scs/scs.mk +35 -13
  43. data/vendor/scs/src/aa.c +673 -166
  44. data/vendor/scs/src/cones.c +545 -223
  45. data/vendor/scs/src/ctrlc.c +59 -16
  46. data/vendor/scs/src/exp_cone.c +70 -50
  47. data/vendor/scs/src/linalg.c +21 -2
  48. data/vendor/scs/src/normalize.c +24 -26
  49. data/vendor/scs/src/rw.c +596 -124
  50. data/vendor/scs/src/scs.c +990 -513
  51. data/vendor/scs/src/spectral_cones/logdeterminant/log_cone_IPM.c +240 -187
  52. data/vendor/scs/src/spectral_cones/logdeterminant/log_cone_Newton.c +108 -85
  53. data/vendor/scs/src/spectral_cones/logdeterminant/log_cone_wrapper.c +62 -63
  54. data/vendor/scs/src/spectral_cones/logdeterminant/logdet_cone.c +85 -78
  55. data/vendor/scs/src/spectral_cones/nuclear/ell1_cone.c +92 -97
  56. data/vendor/scs/src/spectral_cones/nuclear/nuclear_cone.c +39 -28
  57. data/vendor/scs/src/spectral_cones/sum-largest/sum_largest_cone.c +59 -40
  58. data/vendor/scs/src/spectral_cones/sum-largest/sum_largest_eval_cone.c +37 -29
  59. data/vendor/scs/src/spectral_cones/util_spectral_cones.c +12 -6
  60. data/vendor/scs/src/util.c +37 -7
  61. data/vendor/scs/test/mkl_interface_mismatch.c +97 -0
  62. data/vendor/scs/test/packaging/CMakeLists.txt +21 -0
  63. data/vendor/scs/test/packaging/consume.c +50 -0
  64. data/vendor/scs/test/problem_utils.h +9 -2
  65. data/vendor/scs/test/problems/dense_qp.h +64 -0
  66. data/vendor/scs/test/problems/hs21_tiny_qp.h +6 -2
  67. data/vendor/scs/test/problems/hs21_tiny_qp_rw.h +10 -2
  68. data/vendor/scs/test/problems/infeasible_lp.h +66 -0
  69. data/vendor/scs/test/problems/infeasible_socp.h +75 -0
  70. data/vendor/scs/test/problems/lp_update.h +110 -0
  71. data/vendor/scs/test/problems/qafiro_tiny_qp.h +3 -1
  72. data/vendor/scs/test/problems/rob_gauss_cov_est.h +6 -3
  73. data/vendor/scs/test/problems/small_qp.h +5 -0
  74. data/vendor/scs/test/problems/test_box_cone.h +92 -0
  75. data/vendor/scs/test/problems/test_dual_exp_cone.h +96 -0
  76. data/vendor/scs/test/problems/test_inaccurate.h +212 -0
  77. data/vendor/scs/test/problems/test_mixed_cones.h +113 -0
  78. data/vendor/scs/test/problems/test_normalize_roundtrip.h +279 -0
  79. data/vendor/scs/test/problems/test_power_cone.h +414 -0
  80. data/vendor/scs/test/problems/test_psd_metric.h +109 -0
  81. data/vendor/scs/test/problems/test_psd_n1.h +84 -0
  82. data/vendor/scs/test/problems/test_root_plus.h +191 -0
  83. data/vendor/scs/test/problems/test_rw_settings.h +112 -0
  84. data/vendor/scs/test/problems/test_soc_sizes.h +393 -0
  85. data/vendor/scs/test/problems/test_solver_options.h +558 -0
  86. data/vendor/scs/test/problems/test_validation.h +263 -24
  87. data/vendor/scs/test/problems/test_zero_cone.h +81 -0
  88. data/vendor/scs/test/problems/unbounded_lp.h +66 -0
  89. data/vendor/scs/test/problems/unbounded_socp.h +71 -0
  90. data/vendor/scs/test/run_from_file.c +6 -0
  91. data/vendor/scs/test/run_tests.c +71 -0
  92. data/vendor/scs/test/rw_settings.c +7 -0
  93. data/vendor/scs/test/spectral_cones_problems/test_ell1_and_nuc.h +128 -0
  94. data/vendor/scs/test/spectral_cones_problems/test_ell1_cone.h +115 -0
  95. metadata +35 -3
data/vendor/scs/src/aa.c CHANGED
@@ -20,22 +20,40 @@
20
20
  * Type-II:
21
21
  * return f = f - (S - Y) * ( Y'Y + r I)^{-1} ( Y'g )
22
22
  *
23
+ * Both types reduce to the same regularized least-squares augmentation
24
+ * (A'B + r I) γ = A' g
25
+ * ⇔ [A; √r I]' [B; √r I] γ = [A; √r I]' [g; 0],
26
+ * where A = S (type-I) or A = Y (type-II), and B = Y. We solve via a thin
27
+ * QR factorization of the augmented A, which keeps the conditioning at
28
+ * κ(A_aug) rather than the κ(A_aug)² that a normal-equations solve would
29
+ * incur — critical near the optimum where Y rows are tiny and the Gram
30
+ * matrix becomes numerically singular.
23
31
  */
24
32
 
33
+ #include <float.h>
34
+ #include <math.h>
35
+ #include <string.h>
36
+
25
37
  #include "aa.h"
26
38
  #include "scs_blas.h"
27
39
 
40
+ #ifndef SFLOAT
41
+ #define AA_EPS DBL_EPSILON
42
+ #else
43
+ #define AA_EPS FLT_EPSILON
44
+ #endif
45
+
28
46
  #define MAX(a, b) (((a) > (b)) ? (a) : (b))
29
47
  #define MIN(a, b) (((a) < (b)) ? (a) : (b))
30
- #define FILL_MEMORY_BEFORE_SOLVE (1)
31
48
 
32
49
  #ifndef USE_LAPACK
33
50
 
34
51
  typedef void *ACCEL_WORK;
35
52
 
36
- AaWork *aa_init(aa_int dim, aa_int mem, aa_int type1, aa_float regularization,
37
- aa_float relaxation, aa_float safeguard_factor,
38
- aa_float max_weight_norm, aa_int verbosity) {
53
+ AaWork *aa_init(aa_int dim, aa_int mem, aa_int min_len, aa_int type1,
54
+ aa_float regularization, aa_float relaxation,
55
+ aa_float safeguard_factor, aa_float max_weight_norm,
56
+ aa_int ir_max_steps, aa_int verbosity) {
39
57
  return SCS_NULL;
40
58
  }
41
59
  aa_float aa_apply(aa_float *f, const aa_float *x, AaWork *a) {
@@ -48,6 +66,12 @@ void aa_finish(AaWork *a) {
48
66
  }
49
67
  void aa_reset(AaWork *a) {
50
68
  }
69
+ AaStats aa_get_stats(const AaWork *a) {
70
+ AaStats s;
71
+ memset(&s, 0, sizeof(AaStats));
72
+ s.last_aa_norm = NAN;
73
+ return s;
74
+ }
51
75
 
52
76
  #else
53
77
 
@@ -58,6 +82,25 @@ void aa_reset(AaWork *a) {
58
82
  tic(&__t);
59
83
  #define TIME_TOC toc(__func__, &__t);
60
84
 
85
+ #ifdef _WIN32
86
+ #include <windows.h>
87
+ typedef struct timer {
88
+ LARGE_INTEGER tic;
89
+ LARGE_INTEGER toc;
90
+ } timer;
91
+
92
+ void tic(timer *t) {
93
+ QueryPerformanceCounter(&t->tic);
94
+ }
95
+
96
+ aa_float tocq(timer *t) {
97
+ LARGE_INTEGER freq;
98
+ QueryPerformanceFrequency(&freq);
99
+ QueryPerformanceCounter(&t->toc);
100
+ return (aa_float)(t->toc.QuadPart - t->tic.QuadPart) /
101
+ (aa_float)freq.QuadPart * 1e3;
102
+ }
103
+ #else
61
104
  #include <time.h>
62
105
  typedef struct timer {
63
106
  struct timespec tic;
@@ -82,10 +125,11 @@ aa_float tocq(timer *t) {
82
125
  }
83
126
  return (aa_float)temp.tv_sec * 1e3 + (aa_float)temp.tv_nsec / 1e6;
84
127
  }
128
+ #endif
85
129
 
86
130
  aa_float toc(const char *str, timer *t) {
87
131
  aa_float time = tocq(t);
88
- printf("%s - time: %8.4f milli-seconds.\n", str, time);
132
+ scs_printf("%s - time: %8.4f milli-seconds.\n", str, time);
89
133
  return time;
90
134
  }
91
135
 
@@ -100,7 +144,7 @@ aa_float toc(const char *str, timer *t) {
100
144
  extern "C" {
101
145
  #endif
102
146
 
103
- /* BLAS functions used */
147
+ /* BLAS / LAPACK functions used */
104
148
  aa_float BLAS(nrm2)(blas_int *n, aa_float *x, blas_int *incx);
105
149
  void BLAS(axpy)(blas_int *n, aa_float *a, const aa_float *x, blas_int *incx,
106
150
  aa_float *y, blas_int *incy);
@@ -110,12 +154,25 @@ void BLAS(gemv)(const char *trans, const blas_int *m, const blas_int *n,
110
154
  aa_float *y, const blas_int *incy);
111
155
  void BLAS(gesv)(blas_int *n, blas_int *nrhs, aa_float *a, blas_int *lda,
112
156
  blas_int *ipiv, aa_float *b, blas_int *ldb, blas_int *info);
113
- void BLAS(gemm)(const char *transa, const char *transb, blas_int *m,
114
- blas_int *n, blas_int *k, aa_float *alpha, aa_float *a,
115
- blas_int *lda, aa_float *b, blas_int *ldb, aa_float *beta,
116
- aa_float *c, blas_int *ldc);
157
+ void BLAS(getrs)(const char *trans, const blas_int *n, const blas_int *nrhs,
158
+ const aa_float *a, const blas_int *lda, const blas_int *ipiv,
159
+ aa_float *b, const blas_int *ldb, blas_int *info);
117
160
  void BLAS(scal)(const blas_int *n, const aa_float *a, aa_float *x,
118
161
  const blas_int *incx);
162
+ void BLAS(trsv)(const char *uplo, const char *trans, const char *diag,
163
+ const blas_int *n, const aa_float *a, const blas_int *lda,
164
+ aa_float *x, const blas_int *incx);
165
+ void BLAS(trmv)(const char *uplo, const char *trans, const char *diag,
166
+ const blas_int *n, const aa_float *a, const blas_int *lda,
167
+ aa_float *x, const blas_int *incx);
168
+ void BLAS(geqp3)(const blas_int *m, const blas_int *n, aa_float *a,
169
+ const blas_int *lda, blas_int *jpvt, aa_float *tau,
170
+ aa_float *work, const blas_int *lwork, blas_int *info);
171
+ void BLAS(ormqr)(const char *side, const char *trans, const blas_int *m,
172
+ const blas_int *n, const blas_int *k, const aa_float *a,
173
+ const blas_int *lda, const aa_float *tau, aa_float *c,
174
+ const blas_int *ldc, aa_float *work, const blas_int *lwork,
175
+ blas_int *info);
119
176
 
120
177
  #ifdef __cplusplus
121
178
  }
@@ -123,18 +180,21 @@ void BLAS(scal)(const blas_int *n, const aa_float *a, aa_float *x,
123
180
 
124
181
  /* This file uses Anderson acceleration to improve the convergence of
125
182
  * a fixed point mapping.
126
- * At each iteration we need to solve a (small) linear system, we
127
- * do this using LAPACK ?gesv.
183
+ * At each iteration we solve a (small) regularized least-squares
184
+ * problem via a pivoted QR factorization of an augmented matrix,
185
+ * followed by iterative refinement on the reduced system.
128
186
  */
129
187
 
130
188
  /* contains the necessary parameters to perform aa at each step */
131
189
  struct ACCEL_WORK {
132
- aa_int type1; /* bool, if true type 1 aa otherwise type 2 */
133
- aa_int mem; /* aa memory */
134
- aa_int dim; /* variable dimension */
135
- aa_int iter; /* current iteration */
136
- aa_int verbosity; /* verbosity level, 0 is no printing */
137
- aa_int success; /* was the last AA step successful or not */
190
+ aa_int type1; /* bool, if true type 1 aa otherwise type 2 */
191
+ aa_int mem; /* aa memory */
192
+ aa_int min_len; /* min iterates before solve starts (1..mem) */
193
+ aa_int dim; /* variable dimension */
194
+ aa_int iter; /* current iteration */
195
+ aa_int verbosity; /* verbosity level, 0 is no printing */
196
+ aa_int success; /* was the last AA step successful or not */
197
+ aa_int ir_max_steps; /* max iterative refinement passes, 0 disables */
138
198
 
139
199
  aa_float relaxation; /* relaxation x and f, beta in some papers */
140
200
  aa_float regularization; /* regularization */
@@ -149,55 +209,121 @@ struct ACCEL_WORK {
149
209
  /* from previous iteration */
150
210
  aa_float *g_prev; /* x_prev - f(x_prev) */
151
211
 
152
- aa_float *y; /* g - g_prev */
153
- aa_float *s; /* x - x_prev */
154
- aa_float *d; /* f - f_prev */
155
-
156
212
  aa_float *Y; /* matrix of stacked y values */
157
213
  aa_float *S; /* matrix of stacked s values */
158
214
  aa_float *D; /* matrix of stacked d values = (S-Y) */
159
- aa_float *M; /* S'Y or Y'Y depending on type of aa */
160
215
 
161
- /* workspace variables */
162
- aa_float *work; /* scratch space */
163
- blas_int *ipiv; /* permutation variable, not used after solve */
216
+ /* Per-column cached L2 norms of S and Y (length mem). Each slot is
217
+ * rewritten when its column is rewritten (update_accel_params), and
218
+ * compute_regularization reduces these with a max-scaled sum to form
219
+ * ||S||_F / ||Y||_F in O(mem) — versus the original O(dim·mem) nrm2
220
+ * over the whole matrix. Storing per-column (not incremental "subtract
221
+ * old / add new") avoids drift: near convergence Y columns can shrink
222
+ * by many orders of magnitude and add/subtract updates fall below the
223
+ * running sum's rounding floor, pegging the total above the true
224
+ * Frobenius norm (observed on the κ=1e10 stress test). Storing the
225
+ * norm (not its square) preserves nrm2's overflow/underflow safety —
226
+ * squaring a 1e200 or 1e-200 column norm would hit ±inf / 0 even
227
+ * though nrm2 on the whole matrix would produce a finite answer. */
228
+ aa_float *nrm_s_col;
229
+ aa_float *nrm_y_col;
230
+
231
+ /* QR workspaces, sized for the augmented problem. */
232
+ aa_float *A_aug; /* (dim + mem) x mem -- [A; √r I]; factored in place */
233
+ aa_float *B_aug; /* (dim + mem) x mem -- [Y; √r I] (type-I only) */
234
+ aa_float *c_aug; /* (dim + mem) -- [g; 0], overwritten by Q' c */
235
+ aa_float *tau; /* mem -- Householder scalars */
236
+ aa_float *qr_work; /* lwork -- LAPACK scratch for geqp3/ormqr */
237
+ blas_int qr_lwork; /* size of qr_work, chosen via workspace query at init */
238
+ blas_int *jpvt; /* mem -- column permutation from geqp3 */
239
+
240
+ aa_float *W; /* mem x mem scratch: Q' B_aug top block (type-I gesv) */
241
+ aa_float *W_orig; /* mem x mem: W copy preserved for iterative refinement */
242
+ blas_int *ipiv; /* gesv permutation (type-I) */
243
+
244
+ /* Iterative refinement scratches (size mem each). Separate from `work`
245
+ * so the IR dance doesn't clobber γ or the safeguard scratch. */
246
+ aa_float *gamma_red; /* permuted/truncated γ (length `rank`) */
247
+ aa_float *c_top_save; /* original RHS preserved across the solve */
248
+ aa_float *ir_res; /* residual/correction vector */
249
+
250
+ /* Dual-use scratch: dim-sized buffer for aa_safeguard's x_new - f_new
251
+ * diff, and also the natural-order γ home (len ≤ mem entries) inside
252
+ * solve(). Allocated as max(mem, dim) so both uses fit. */
253
+ aa_float *work;
164
254
 
165
255
  aa_float *x_work; /* workspace (= x) for when relaxation != 1.0 */
256
+
257
+ /* Lifetime diagnostics (see AaStats in aa_stats.h). NOT cleared by
258
+ * aa_reset — the internal reset path fires on safeguard rejection,
259
+ * and you want the rejection to stay visible in the counters. */
260
+ aa_int n_accept;
261
+ aa_int n_reject_lapack;
262
+ aa_int n_reject_rank0;
263
+ aa_int n_reject_nonfinite;
264
+ aa_int n_reject_weight_cap;
265
+ aa_int n_safeguard_reject;
266
+ aa_int last_rank;
267
+ aa_float last_aa_norm;
268
+ aa_float last_regularization;
166
269
  };
167
270
 
168
- /* add regularization dependent on Y and S matrices */
169
- static aa_float compute_regularization(AaWork *a, aa_int len) {
170
- /* typically type-I does better with higher regularization than type-II */
271
+ /* Reduce a length-`mem` vector of nonnegative column norms to a single
272
+ * Frobenius norm ||A||_F = sqrt(Σ nrm_col_i²), using the classic
273
+ * max-scaled sum of squares so we don't square-then-overflow on a
274
+ * large column or square-then-underflow on a tiny one. This mirrors
275
+ * what nrm2 does internally across elements, but here across column
276
+ * norms — each nrm_col_i is already nrm2-safe per column. */
277
+ static aa_float frob_from_col_norms(const aa_float *nrm_col, aa_int mem) {
278
+ aa_int i;
279
+ aa_float m = 0;
280
+ for (i = 0; i < mem; ++i) {
281
+ if (nrm_col[i] > m) m = nrm_col[i];
282
+ }
283
+ if (m == 0) return 0;
284
+ aa_float sumsq = 0;
285
+ for (i = 0; i < mem; ++i) {
286
+ aa_float t = nrm_col[i] / m;
287
+ sumsq += t * t;
288
+ }
289
+ return m * sqrt(sumsq);
290
+ }
291
+
292
+ /* Tikhonov regularization scaled with the problem. Matches the prior
293
+ * behavior's intent (r grows with the magnitude of A'B so `regularization`
294
+ * stays unitless), but uses the cheap Frobenius-norm upper bound
295
+ * ||A'B||_F ≤ ||A||_F · ||B||_F
296
+ * instead of maintaining a Gram matrix. For type-II A == B so this is
297
+ * ||Y||_F², the same scale as the previous ||Y'Y||_F up to a factor
298
+ * ≤ √mem. Reduces the per-column cached norms (O(mem)) rather than a
299
+ * fresh nrm2 over dim·mem entries. */
300
+ static aa_float compute_regularization(AaWork *a) {
171
301
  TIME_TIC
172
- aa_float r, nrm_m;
173
- blas_int btotal = (blas_int)(len * len), one = 1;
174
- nrm_m = BLAS(nrm2)(&btotal, a->M, &one);
175
- r = a->regularization * nrm_m;
302
+ aa_float nrm_y = frob_from_col_norms(a->nrm_y_col, a->mem);
303
+ aa_float nrm_a = a->type1 ? frob_from_col_norms(a->nrm_s_col, a->mem) : nrm_y;
304
+ aa_float r = a->regularization * nrm_a * nrm_y;
176
305
  if (a->verbosity > 2) {
177
- printf("iter: %i, norm: M %.2e, r: %.2e\n", (int)a->iter, nrm_m, r);
306
+ scs_printf("iter: %i, ||A||_F %.2e, ||Y||_F %.2e, r: %.2e\n",
307
+ (int)a->iter, nrm_a, nrm_y, r);
178
308
  }
179
309
  TIME_TOC
180
310
  return r;
181
311
  }
182
312
 
183
- /* sets a->M to S'Y or Y'Y depending on type of aa used */
184
- /* M is len x len after this */
185
- static void set_m(AaWork *a, aa_int len) {
186
- TIME_TIC
313
+ /* Build [M; √r I_len] column-major into `dst` with fixed leading dim
314
+ * (dim + mem). When len < mem we zero-pad the unused trailing rows so
315
+ * the QR factorization still operates on a well-defined (dim+mem) x len
316
+ * block; the zero rows don't change the solve. */
317
+ static void build_augmented(aa_float *dst, const aa_float *src, aa_int dim,
318
+ aa_int mem, aa_int len, aa_float sqrt_r) {
187
319
  aa_int i;
188
- blas_int bdim = (blas_int)(a->dim);
189
- blas_int blen = (blas_int)len;
190
- aa_float onef = 1.0, zerof = 0.0, r;
191
- /* if len < mem this only uses len cols */
192
- BLAS(gemm)("Trans", "No", &blen, &blen, &bdim, &onef, a->type1 ? a->S : a->Y,
193
- &bdim, a->Y, &bdim, &zerof, a->M, &blen);
194
- if (a->regularization > 0) {
195
- r = compute_regularization(a, len);
196
- for (i = 0; i < len; ++i) {
197
- a->M[i + len * i] += r;
198
- }
320
+ aa_int aug_rows = dim + mem;
321
+ for (i = 0; i < len; ++i) {
322
+ aa_float *col = &dst[i * aug_rows];
323
+ memcpy(col, &src[i * dim], dim * sizeof(aa_float));
324
+ memset(&col[dim], 0, mem * sizeof(aa_float));
325
+ col[dim + i] = sqrt_r;
199
326
  }
200
- TIME_TOC
201
327
  }
202
328
 
203
329
  /* initialize accel params, in particular x_prev, f_prev, g_prev */
@@ -217,62 +343,72 @@ static void init_accel_params(const aa_float *x, const aa_float *f, AaWork *a) {
217
343
  TIME_TOC
218
344
  }
219
345
 
220
- /* updates the workspace parameters for aa for this iteration */
346
+ /* updates the workspace parameters for aa for this iteration
347
+ *
348
+ * Writes this iteration's s, d, y columns directly into S, D, Y at slot
349
+ * `idx` — no intermediate scratch. Numerically sensitive because:
350
+ *
351
+ * - y is computed as g - g_prev (ONE rounding into a cancellation-prone
352
+ * quantity). Deriving y from s - d would add two extra roundings and
353
+ * make y noticeably worse near convergence where g and g_prev are
354
+ * tiny and nearly equal.
355
+ *
356
+ * - The reads of a->x, a->f, a->g_prev all require the PREVIOUS
357
+ * iteration's values, so state advance (x_prev <- x, f_prev <- f,
358
+ * g_prev <- g) must happen AFTER everything that reads them. s uses
359
+ * old a->x; d uses old a->f; y uses old a->g_prev. */
221
360
  static void update_accel_params(const aa_float *x, const aa_float *f, AaWork *a,
222
361
  aa_int len) {
223
- /* at the start a->x = x_prev and a->f = f_prev */
362
+ /* Entry invariant: a->x == x_prev, a->f == f_prev, a->g_prev == g_prev. */
224
363
  TIME_TIC
225
364
  aa_int idx = (a->iter - 1) % a->mem;
226
365
  blas_int one = 1;
227
366
  blas_int bdim = (blas_int)a->dim;
228
367
  aa_float neg_onef = -1.0;
368
+ aa_float *s_col = &(a->S[idx * a->dim]);
369
+ aa_float *d_col = &(a->D[idx * a->dim]);
370
+ aa_float *y_col = &(a->Y[idx * a->dim]);
229
371
 
230
- /* g = x */
231
- memcpy(a->g, x, sizeof(aa_float) * a->dim);
232
- /* s = x */
233
- memcpy(a->s, x, sizeof(aa_float) * a->dim);
234
- /* d = f */
235
- memcpy(a->d, f, sizeof(aa_float) * a->dim);
236
- /* g = x - f */
237
- BLAS(axpy)(&bdim, &neg_onef, f, &one, a->g, &one);
238
- /* s = x - x_prev */
239
- BLAS(axpy)(&bdim, &neg_onef, a->x, &one, a->s, &one);
240
- /* d = f - f_prev */
241
- BLAS(axpy)(&bdim, &neg_onef, a->f, &one, a->d, &one);
242
-
243
- /* g, s, d correct here */
372
+ /* S[:, idx] = x - x_prev (reads old a->x). */
373
+ memcpy(s_col, x, sizeof(aa_float) * a->dim);
374
+ BLAS(axpy)(&bdim, &neg_onef, a->x, &one, s_col, &one);
244
375
 
245
- /* y = g */
246
- memcpy(a->y, a->g, sizeof(aa_float) * a->dim);
247
- /* y = g - g_prev */
248
- BLAS(axpy)(&bdim, &neg_onef, a->g_prev, &one, a->y, &one);
376
+ /* D[:, idx] = f - f_prev (reads old a->f). */
377
+ memcpy(d_col, f, sizeof(aa_float) * a->dim);
378
+ BLAS(axpy)(&bdim, &neg_onef, a->f, &one, d_col, &one);
249
379
 
250
- /* y correct here */
380
+ /* g = x - f (this iteration's residual; needed for the solve RHS). */
381
+ memcpy(a->g, x, sizeof(aa_float) * a->dim);
382
+ BLAS(axpy)(&bdim, &neg_onef, f, &one, a->g, &one);
251
383
 
252
- /* copy y into idx col of Y */
253
- memcpy(&(a->Y[idx * a->dim]), a->y, sizeof(aa_float) * a->dim);
254
- /* copy s into idx col of S */
255
- memcpy(&(a->S[idx * a->dim]), a->s, sizeof(aa_float) * a->dim);
256
- /* copy d into idx col of D */
257
- memcpy(&(a->D[idx * a->dim]), a->d, sizeof(aa_float) * a->dim);
384
+ /* Y[:, idx] = g - g_prev (reads old a->g_prev; single-rounding y). */
385
+ memcpy(y_col, a->g, sizeof(aa_float) * a->dim);
386
+ BLAS(axpy)(&bdim, &neg_onef, a->g_prev, &one, y_col, &one);
258
387
 
259
- /* Y, S, D correct here */
388
+ /* Update the per-column cached norms only when the scaled
389
+ * regularization path will read them. Pinned/no regularization skips
390
+ * compute_regularization entirely, and Type-II uses Y for both A and B,
391
+ * so S norms are unused there. Store the norm itself (not its square) to
392
+ * preserve nrm2's overflow/underflow safety. */
393
+ if (a->regularization > 0) {
394
+ if (a->type1) {
395
+ a->nrm_s_col[idx] = BLAS(nrm2)(&bdim, s_col, &one);
396
+ }
397
+ a->nrm_y_col[idx] = BLAS(nrm2)(&bdim, y_col, &one);
398
+ }
260
399
 
261
- /* set a->f and a->x for next iter (x_prev and f_prev) */
262
- memcpy(a->f, f, sizeof(aa_float) * a->dim);
400
+ /* State advance for next iter: (x_prev, f_prev, g_prev) <- (x, f, g).
401
+ * Must follow all the reads above. */
263
402
  memcpy(a->x, x, sizeof(aa_float) * a->dim);
403
+ memcpy(a->f, f, sizeof(aa_float) * a->dim);
404
+ memcpy(a->g_prev, a->g, sizeof(aa_float) * a->dim);
264
405
 
265
- /* workspace for when relaxation != 1.0 */
406
+ /* Relaxation scratch (mirror of x); only present when relaxation != 1.0. */
266
407
  if (a->x_work) {
267
408
  memcpy(a->x_work, x, sizeof(aa_float) * a->dim);
268
409
  }
269
410
 
270
- /* x, f correct here */
271
-
272
- memcpy(a->g_prev, a->g, sizeof(aa_float) * a->dim);
273
- /* g_prev set for next iter here */
274
-
275
- /* compute ||g|| = ||f - x|| */
411
+ /* ||g|| = ||x - f|| (current residual norm, used by the safeguard). */
276
412
  a->norm_g = BLAS(nrm2)(&bdim, a->g, &one);
277
413
 
278
414
  TIME_TOC
@@ -286,8 +422,9 @@ static void relax(aa_float *f, AaWork *a, aa_int len) {
286
422
  aa_float onef = 1.0, neg_onef = -1.0;
287
423
  aa_float one_m_relaxation = 1. - a->relaxation;
288
424
  /* x_work = x - S * work */
289
- BLAS(gemv)("NoTrans", &bdim, &blen, &neg_onef, a->S, &bdim, a->work, &one,
290
- &onef, a->x_work, &one);
425
+ BLAS(gemv)
426
+ ("NoTrans", &bdim, &blen, &neg_onef, a->S, &bdim, a->work, &one, &onef,
427
+ a->x_work, &one);
291
428
  /* f = relaxation * f */
292
429
  BLAS(scal)(&bdim, &a->relaxation, f, &one);
293
430
  /* f += (1 - relaxation) * x_work */
@@ -295,52 +432,246 @@ static void relax(aa_float *f, AaWork *a, aa_int len) {
295
432
  TIME_TOC
296
433
  }
297
434
 
298
- /* solves the system of equations to perform the AA update
299
- * at the end f contains the next iterate to be returned
300
- */
435
+ /* Solve the regularized normal equations (A'B + rI) γ = A'g via a
436
+ * pivoted QR (geqp3) of the augmented matrix [A; √r I]. Column pivoting
437
+ * exposes the numerical rank directly in the diagonal of R: we truncate
438
+ * at the first diagonal whose magnitude falls below len·ε·|R_11|
439
+ * (dim-independent: the inner LS is a len-column problem, so its noise
440
+ * floor scales with column count, not the caller's state dim) and solve
441
+ * the smaller, well-conditioned system (graceful degradation instead of
442
+ * hard reset on near-rank-deficiency). Iterative refinement on the
443
+ * reduced system recovers digits lost to gesv/trsv rounding; the loop
444
+ * auto-stops when the correction no longer contracts and is capped at
445
+ * ir_max_steps (see aa_init). γ is then validated against
446
+ * max_weight_norm in the L2 sense. */
301
447
  static aa_float solve(aa_float *f, AaWork *a, aa_int len) {
302
448
  TIME_TIC
303
449
  blas_int info = -1, bdim = (blas_int)(a->dim), one = 1, blen = (blas_int)len;
304
- aa_float onef = 1.0, zerof = 0.0, neg_onef = -1.0, aa_norm;
450
+ /* Leading dim is fixed to dim+mem regardless of len so the buffers
451
+ * allocated in aa_init match the strides used here. Unused trailing
452
+ * rows are kept zero by build_augmented. */
453
+ blas_int aug_rows = bdim + (blas_int)a->mem;
454
+ blas_int bmem = (blas_int)a->mem;
455
+ aa_float onef = 1.0, neg_onef = -1.0, aa_norm;
456
+ aa_float *A_src = a->type1 ? a->S : a->Y;
457
+ aa_float *gamma = a->work; /* natural-order γ, len entries used by gemv below */
458
+ aa_int i;
459
+ aa_int rank = 0;
460
+ blas_int brank;
461
+
462
+ /* Three regularization modes:
463
+ * regularization > 0 : problem-scaled r = regularization * ||A||_F ||Y||_F
464
+ * regularization < 0 : pinned absolute r = -regularization (Frobenius skipped)
465
+ * regularization == 0 : unregularized r = 0
466
+ * Pinned mode gives a knob for applications where the problem scale is
467
+ * known and the caller wants a stable, scale-free regularizer. */
468
+ aa_float r;
469
+ if (a->regularization > 0) {
470
+ r = compute_regularization(a);
471
+ } else if (a->regularization < 0) {
472
+ r = -a->regularization;
473
+ } else {
474
+ r = 0.0;
475
+ }
476
+ aa_float sqrt_r = (r > 0) ? sqrt(r) : 0.0;
477
+
478
+ /* 1. Build A_aug = [A; √r I_len]; factor with column pivoting. geqp3
479
+ * requires jpvt zeroed on entry so it is free to choose the pivot
480
+ * order (nonzero entries would be treated as user-forced pivots). */
481
+ build_augmented(a->A_aug, A_src, a->dim, a->mem, len, sqrt_r);
482
+ for (i = 0; i < len; ++i) a->jpvt[i] = 0;
483
+ BLAS(geqp3)(&aug_rows, &blen, a->A_aug, &aug_rows, a->jpvt, a->tau,
484
+ a->qr_work, &a->qr_lwork, &info);
485
+ /* Capture geqp3's info before the rank-0 path below overwrites it; the
486
+ * reject-cause attribution needs to distinguish a genuine LAPACK failure
487
+ * from "the matrix went numerically to zero." */
488
+ blas_int lapack_info = info;
489
+
490
+ /* 2. Rank estimation. geqp3 sorts |R_ii| non-increasingly; find the
491
+ * largest `rank` with |R_rank-1,rank-1| ≥ tol. A rank of zero means
492
+ * the whole à is numerically zero — hand off to aa_reset below. */
493
+ if (info == 0) {
494
+ aa_float r11 = fabs(a->A_aug[0]);
495
+ if (r11 > 0) {
496
+ /* Column-count-based rank tolerance: the effective LS problem has
497
+ * `len` columns, so the rounding floor for rank determination
498
+ * scales with `len`, not (dim + mem). Decoupling from `dim` avoids
499
+ * falsely dropping columns that are healthy relative to the
500
+ * regularizer at large state dimensions. */
501
+ aa_float tol = r11 * (aa_float)len * AA_EPS;
502
+ for (rank = 0; rank < len; ++rank) {
503
+ if (fabs(a->A_aug[rank * aug_rows + rank]) < tol) break;
504
+ }
505
+ }
506
+ if (rank == 0) info = 1;
507
+ }
508
+ brank = (blas_int)rank;
509
+
510
+ /* 3. c_aug = [g; 0]; overwrite with Q' c_aug. We only need the first
511
+ * `rank` entries (= Q_rank' c̃); pass `rank` to ormqr so it applies
512
+ * only the reflectors we care about. */
513
+ if (info == 0) {
514
+ memcpy(a->c_aug, a->g, a->dim * sizeof(aa_float));
515
+ memset(&a->c_aug[a->dim], 0, a->mem * sizeof(aa_float));
516
+ BLAS(ormqr)
517
+ ("Left", "Trans", &aug_rows, &one, &brank, a->A_aug, &aug_rows, a->tau,
518
+ a->c_aug, &aug_rows, a->qr_work, &a->qr_lwork, &info);
519
+ }
305
520
 
306
- /* work = S'g or Y'g */
307
- BLAS(gemv)("Trans", &bdim, &blen, &onef, a->type1 ? a->S : a->Y, &bdim, a->g,
308
- &one, &zerof, a->work, &one);
521
+ /* 4. Solve the reduced rank×rank system for γ_red (pivoted order),
522
+ * then un-permute into the natural-order γ consumed by `f -= D γ`. */
523
+ if (info == 0) {
524
+ /* Preserve the RHS for iterative refinement below. */
525
+ memcpy(a->c_top_save, a->c_aug, rank * sizeof(aa_float));
526
+
527
+ if (a->type1) {
528
+ /* Type-I: build B_aug with the pivoted Y columns (first `rank` only),
529
+ * apply Q', extract top-left rank×rank block into W, solve
530
+ * W γ_red = c_top. The √rI block is reshuffled too — column i of
531
+ * the permuted B̃ has √r at row (jpvt[i]-1). */
532
+ for (i = 0; i < rank; ++i) {
533
+ aa_int piv = a->jpvt[i] - 1; /* LAPACK jpvt is 1-indexed */
534
+ aa_float *col = &a->B_aug[i * aug_rows];
535
+ memcpy(col, &a->Y[piv * a->dim], a->dim * sizeof(aa_float));
536
+ memset(&col[a->dim], 0, a->mem * sizeof(aa_float));
537
+ col[a->dim + piv] = sqrt_r;
538
+ }
539
+ BLAS(ormqr)
540
+ ("Left", "Trans", &aug_rows, &brank, &brank, a->A_aug, &aug_rows,
541
+ a->tau, a->B_aug, &aug_rows, a->qr_work, &a->qr_lwork, &info);
542
+ if (info == 0) {
543
+ /* W (mem×mem, LDA=mem) holds the rank×rank top-left block. */
544
+ for (i = 0; i < rank; ++i) {
545
+ memcpy(&a->W[i * a->mem], &a->B_aug[i * aug_rows],
546
+ rank * sizeof(aa_float));
547
+ memcpy(&a->W_orig[i * a->mem], &a->W[i * a->mem],
548
+ rank * sizeof(aa_float));
549
+ }
550
+ memcpy(a->gamma_red, a->c_top_save, rank * sizeof(aa_float));
551
+ BLAS(gesv)
552
+ (&brank, &one, a->W, &bmem, a->ipiv, a->gamma_red, &brank, &info);
553
+ if (info == 0) {
554
+ /* Iterative refinement: repeat while δ is still contracting,
555
+ * capped at ir_max_steps. Each step: ρ = c_top - W_orig γ_red,
556
+ * solve W δ = ρ with the LU already in W, γ_red += δ. Stop when
557
+ * ‖δ_k‖ ≥ 0.5·‖δ_{k-1}‖ (we've hit the working-precision floor
558
+ * and further steps won't help). */
559
+ aa_float prev_dnorm = 0.0;
560
+ aa_int k;
561
+ for (k = 0; k < a->ir_max_steps; ++k) {
562
+ aa_float dnorm;
563
+ memcpy(a->ir_res, a->c_top_save, rank * sizeof(aa_float));
564
+ BLAS(gemv)
565
+ ("NoTrans", &brank, &brank, &neg_onef, a->W_orig, &bmem,
566
+ a->gamma_red, &one, &onef, a->ir_res, &one);
567
+ BLAS(getrs)
568
+ ("NoTrans", &brank, &one, a->W, &bmem, a->ipiv, a->ir_res,
569
+ &brank, &info);
570
+ if (info != 0) break;
571
+ dnorm = BLAS(nrm2)(&brank, a->ir_res, &one);
572
+ BLAS(axpy)(&brank, &onef, a->ir_res, &one, a->gamma_red, &one);
573
+ if (k > 0 && dnorm >= 0.5 * prev_dnorm) break;
574
+ prev_dnorm = dnorm;
575
+ }
576
+ }
577
+ }
578
+ } else {
579
+ /* Type-II: B̃ = Ã, so Q' B̃ = R. Solve R u = c_top for the permuted
580
+ * solution u; the rank×rank leading block of R lives in the upper
581
+ * triangle of A_aug. Rank truncation above already guaranteed the
582
+ * diagonal is nonzero through index rank-1. */
583
+ memcpy(a->gamma_red, a->c_top_save, rank * sizeof(aa_float));
584
+ BLAS(trsv)("Upper", "NoTrans", "NonUnit", &brank, a->A_aug,
585
+ &aug_rows, a->gamma_red, &one);
586
+ /* Iterative refinement, capped at ir_max_steps with early stop when
587
+ * δ stops contracting. ρ = c_top - R u via trmv + subtract; solve
588
+ * R δ = ρ via trsv; u += δ. */
589
+ {
590
+ aa_float prev_dnorm = 0.0;
591
+ aa_int k;
592
+ for (k = 0; k < a->ir_max_steps; ++k) {
593
+ aa_float dnorm;
594
+ memcpy(a->ir_res, a->gamma_red, rank * sizeof(aa_float));
595
+ BLAS(trmv)("Upper", "NoTrans", "NonUnit", &brank, a->A_aug,
596
+ &aug_rows, a->ir_res, &one);
597
+ for (i = 0; i < rank; ++i) {
598
+ a->ir_res[i] = a->c_top_save[i] - a->ir_res[i];
599
+ }
600
+ BLAS(trsv)("Upper", "NoTrans", "NonUnit", &brank, a->A_aug,
601
+ &aug_rows, a->ir_res, &one);
602
+ dnorm = BLAS(nrm2)(&brank, a->ir_res, &one);
603
+ BLAS(axpy)(&brank, &onef, a->ir_res, &one, a->gamma_red, &one);
604
+ if (k > 0 && dnorm >= 0.5 * prev_dnorm) break;
605
+ prev_dnorm = dnorm;
606
+ }
607
+ }
608
+ }
609
+
610
+ /* Un-permute γ_red into γ (natural column order); zero the rest so
611
+ * the `f -= D γ` gemv below sees a well-defined full-length vector. */
612
+ if (info == 0) {
613
+ memset(gamma, 0, len * sizeof(aa_float));
614
+ for (i = 0; i < rank; ++i) {
615
+ gamma[a->jpvt[i] - 1] = a->gamma_red[i];
616
+ }
617
+ }
618
+ }
619
+
620
+ /* 5. Validate γ via ‖γ‖₂ against max_weight_norm. */
621
+ aa_norm = (info == 0) ? BLAS(nrm2)(&blen, gamma, &one) : -1.0;
622
+
623
+ /* Record diagnostics for this solve, regardless of accept/reject.
624
+ * NaN last_aa_norm signals "no valid norm this solve" — distinguishing
625
+ * the genuine-zero case (rank collapse gives aa_norm = 0 legitimately)
626
+ * from a failed/rejected solve. */
627
+ a->last_rank = rank;
628
+ a->last_regularization = r;
629
+ a->last_aa_norm = (info == 0 && isfinite(aa_norm)) ? aa_norm : NAN;
309
630
 
310
- /* work = M \ work, where update_accel_params has set M = S'Y or M = Y'Y */
311
- BLAS(gesv)(&blen, &one, a->M, &blen, a->ipiv, a->work, &blen, &info);
312
- aa_norm = BLAS(nrm2)(&blen, a->work, &one);
313
631
  if (a->verbosity > 1) {
314
- printf("AA type %i, iter: %i, len %i, info: %i, aa_norm %.2e\n",
315
- a->type1 ? 1 : 2, (int)a->iter, (int)len, (int)info, aa_norm);
632
+ scs_printf("AA type %i, iter: %i, len %i, rank %i, info: %i, aa_norm %.2e\n",
633
+ a->type1 ? 1 : 2, (int)a->iter, (int)len, (int)rank, (int)info,
634
+ aa_norm);
316
635
  }
317
636
 
318
- /* info < 0 input error, input > 0 matrix is singular */
319
- if (info != 0 || aa_norm >= a->max_weight_norm) {
637
+ if (info != 0 || !isfinite(aa_norm) || aa_norm >= a->max_weight_norm) {
320
638
  if (a->verbosity > 0) {
321
- printf("Error in AA type %i, iter: %i, len %i, info: %i, aa_norm %.2e\n",
322
- a->type1 ? 1 : 2, (int)a->iter, (int)len, (int)info, aa_norm);
639
+ scs_printf("Error in AA type %i, iter: %i, len %i, rank %i, info: %i, "
640
+ "aa_norm %.2e\n",
641
+ a->type1 ? 1 : 2, (int)a->iter, (int)len, (int)rank, (int)info,
642
+ aa_norm);
643
+ }
644
+ /* Attribute the rejection to exactly one cause, in priority order.
645
+ * lapack_info is the original geqp3 return; the rank-0 path above may
646
+ * have set info=1 but that is the bookkeeping trick, not a LAPACK
647
+ * failure. Without this ordering, rank-0 would be miscounted as
648
+ * "lapack" via info. */
649
+ if (lapack_info != 0) {
650
+ a->n_reject_lapack++;
651
+ } else if (rank == 0) {
652
+ a->n_reject_rank0++;
653
+ } else if (!isfinite(aa_norm)) {
654
+ a->n_reject_nonfinite++;
655
+ } else {
656
+ a->n_reject_weight_cap++;
323
657
  }
324
658
  a->success = 0;
325
- /* reset aa for stability */
326
659
  aa_reset(a);
327
660
  TIME_TOC
328
- return -aa_norm;
661
+ if (!isfinite(aa_norm)) aa_norm = -1.0;
662
+ return (aa_norm < 0) ? aa_norm : -aa_norm;
329
663
  }
330
664
 
331
- /* here work = gamma, ie, the correct AA shifted weights */
332
- /* if solve was successful compute new point */
333
-
334
- /* first set f -= D * work */
335
- BLAS(gemv)("NoTrans", &bdim, &blen, &neg_onef, a->D, &bdim, a->work, &one,
336
- &onef, f, &one);
665
+ /* f -= D γ */
666
+ BLAS(gemv)
667
+ ("NoTrans", &bdim, &blen, &neg_onef, a->D, &bdim, gamma, &one, &onef, f,
668
+ &one);
337
669
 
338
- /* if relaxation is not 1 then need to incorporate */
339
670
  if (a->relaxation != 1.0) {
340
671
  relax(f, a, len);
341
672
  }
342
673
 
343
- a->success = 1; /* this should be the only place we set success = 1 */
674
+ a->success = 1;
344
675
  TIME_TOC
345
676
  return aa_norm;
346
677
  }
@@ -348,51 +679,167 @@ static aa_float solve(aa_float *f, AaWork *a, aa_int len) {
348
679
  /*
349
680
  * API functions below this line, see aa.h for descriptions.
350
681
  */
351
- AaWork *aa_init(aa_int dim, aa_int mem, aa_int type1, aa_float regularization,
352
- aa_float relaxation, aa_float safeguard_factor,
353
- aa_float max_weight_norm, aa_int verbosity) {
682
+ AaWork *aa_init(aa_int dim, aa_int mem, aa_int min_len, aa_int type1,
683
+ aa_float regularization, aa_float relaxation,
684
+ aa_float safeguard_factor, aa_float max_weight_norm,
685
+ aa_int ir_max_steps, aa_int verbosity) {
354
686
  TIME_TIC
355
- AaWork *a = (AaWork *)calloc(1, sizeof(AaWork));
687
+ AaWork *a;
688
+ aa_int mem_clamped = MIN(mem, dim);
689
+ /* `regularization` is accepted with either sign: positive = scaled by
690
+ * ||A||_F ||Y||_F; negative = pinned absolute |regularization|; zero = off.
691
+ * Only NaN / non-finite values are rejected (via the !isfinite check).
692
+ * min_len < 1 is rejected when mem > 0; min_len > mem_clamped is
693
+ * silently clamped down — same treatment the `mem` argument already
694
+ * gets against `dim`, so callers can pass `min_len = mem` without
695
+ * caring whether mem exceeded dim. When mem == 0 (AA off), min_len
696
+ * is ignored entirely. */
697
+ if (dim <= 0 || mem < 0 || !isfinite(regularization) ||
698
+ !isfinite(relaxation) || relaxation < 0 || relaxation > 2 ||
699
+ !isfinite(safeguard_factor) || safeguard_factor < 0 ||
700
+ !isfinite(max_weight_norm) || max_weight_norm <= 0 ||
701
+ ir_max_steps < 0 ||
702
+ (mem_clamped > 0 && min_len < 1)) {
703
+ scs_printf("Invalid AA parameters.\n");
704
+ return SCS_NULL;
705
+ }
706
+ a = (AaWork *)scs_calloc(1, sizeof(AaWork));
356
707
  if (!a) {
357
- printf("Failed to allocate memory for AA.\n");
358
- return (AaWork *)0;
708
+ scs_printf("Failed to allocate memory for AA.\n");
709
+ return SCS_NULL;
359
710
  }
360
711
  a->type1 = type1;
361
712
  a->iter = 0;
362
713
  a->dim = dim;
363
- a->mem = MIN(mem, dim); /* for rank stability */
714
+ a->mem = mem_clamped; /* clamped to dim for rank stability */
715
+ if (mem > dim && verbosity > 0) {
716
+ scs_printf("AA: mem (%d) > dim (%d); clamping mem to dim.\n",
717
+ (int)mem, (int)dim);
718
+ }
719
+ a->min_len = mem_clamped > 0 ? MIN(min_len, mem_clamped) : 0;
364
720
  a->regularization = regularization;
365
721
  a->relaxation = relaxation;
366
722
  a->safeguard_factor = safeguard_factor;
367
723
  a->max_weight_norm = max_weight_norm;
724
+ a->ir_max_steps = ir_max_steps;
368
725
  a->success = 0;
369
726
  a->verbosity = verbosity;
727
+ /* Counters are already zero from calloc; only last_aa_norm needs an
728
+ * explicit sentinel so callers can distinguish "never solved" from a
729
+ * legitimate zero norm (which never happens on a successful solve, but
730
+ * 0 is a bad signal either way). */
731
+ a->last_aa_norm = NAN;
370
732
  if (a->mem <= 0) {
371
733
  return a;
372
734
  }
373
735
 
374
- a->x = (aa_float *)calloc(a->dim, sizeof(aa_float));
375
- a->f = (aa_float *)calloc(a->dim, sizeof(aa_float));
376
- a->g = (aa_float *)calloc(a->dim, sizeof(aa_float));
377
-
378
- a->g_prev = (aa_float *)calloc(a->dim, sizeof(aa_float));
379
-
380
- a->y = (aa_float *)calloc(a->dim, sizeof(aa_float));
381
- a->s = (aa_float *)calloc(a->dim, sizeof(aa_float));
382
- a->d = (aa_float *)calloc(a->dim, sizeof(aa_float));
736
+ a->x = (aa_float *)scs_calloc(a->dim, sizeof(aa_float));
737
+ a->f = (aa_float *)scs_calloc(a->dim, sizeof(aa_float));
738
+ a->g = (aa_float *)scs_calloc(a->dim, sizeof(aa_float));
739
+
740
+ a->g_prev = (aa_float *)scs_calloc(a->dim, sizeof(aa_float));
741
+
742
+ a->Y = (aa_float *)scs_calloc(a->dim * a->mem, sizeof(aa_float));
743
+ a->S = (aa_float *)scs_calloc(a->dim * a->mem, sizeof(aa_float));
744
+ a->D = (aa_float *)scs_calloc(a->dim * a->mem, sizeof(aa_float));
745
+
746
+ {
747
+ aa_int aug_rows = a->dim + a->mem;
748
+ a->A_aug = (aa_float *)scs_calloc((size_t)aug_rows * a->mem, sizeof(aa_float));
749
+ a->c_aug = (aa_float *)scs_calloc((size_t)aug_rows, sizeof(aa_float));
750
+ a->tau = (aa_float *)scs_calloc(a->mem, sizeof(aa_float));
751
+ a->jpvt = (blas_int *)scs_calloc(a->mem, sizeof(blas_int));
752
+
753
+ /* Scratches for iterative refinement (both types). */
754
+ a->gamma_red = (aa_float *)scs_calloc(a->mem, sizeof(aa_float));
755
+ a->c_top_save = (aa_float *)scs_calloc(a->mem, sizeof(aa_float));
756
+ a->ir_res = (aa_float *)scs_calloc(a->mem, sizeof(aa_float));
757
+
758
+ /* Per-column cached norms used by compute_regularization. */
759
+ a->nrm_s_col = (aa_float *)scs_calloc(a->mem, sizeof(aa_float));
760
+ a->nrm_y_col = (aa_float *)scs_calloc(a->mem, sizeof(aa_float));
761
+
762
+ /* type-I needs a second augmented buffer and mem×mem gesv scratches;
763
+ * W_orig preserves W across gesv so iterative refinement can form
764
+ * the residual c_top − W γ. */
765
+ if (type1) {
766
+ a->B_aug = (aa_float *)scs_calloc((size_t)aug_rows * a->mem, sizeof(aa_float));
767
+ a->W = (aa_float *)scs_calloc((size_t)a->mem * a->mem, sizeof(aa_float));
768
+ a->W_orig = (aa_float *)scs_calloc((size_t)a->mem * a->mem, sizeof(aa_float));
769
+ a->ipiv = (blas_int *)scs_calloc(a->mem, sizeof(blas_int));
770
+ } else {
771
+ a->B_aug = SCS_NULL;
772
+ a->W = SCS_NULL;
773
+ a->W_orig = SCS_NULL;
774
+ a->ipiv = SCS_NULL;
775
+ }
383
776
 
384
- a->Y = (aa_float *)calloc(a->dim * a->mem, sizeof(aa_float));
385
- a->S = (aa_float *)calloc(a->dim * a->mem, sizeof(aa_float));
386
- a->D = (aa_float *)calloc(a->dim * a->mem, sizeof(aa_float));
777
+ a->work = (aa_float *)scs_calloc(MAX(a->mem, a->dim), sizeof(aa_float));
778
+ if (relaxation != 1.0) {
779
+ a->x_work = (aa_float *)scs_calloc(a->dim, sizeof(aa_float));
780
+ } else {
781
+ a->x_work = SCS_NULL;
782
+ }
387
783
 
388
- a->M = (aa_float *)calloc(a->mem * a->mem, sizeof(aa_float));
389
- a->work = (aa_float *)calloc(MAX(a->mem, a->dim), sizeof(aa_float));
390
- a->ipiv = (blas_int *)calloc(a->mem, sizeof(blas_int));
784
+ /* Check every allocation before the LAPACK workspace query below. The
785
+ * query passes A_aug/jpvt/tau/c_aug/B_aug into geqp3/ormqr; if any of
786
+ * them is NULL we'd dereference inside LAPACK instead of returning
787
+ * cleanly. qr_work is still NULL here — it's allocated after the query. */
788
+ if (!a->x || !a->f || !a->g || !a->g_prev ||
789
+ !a->Y || !a->S || !a->D ||
790
+ !a->A_aug || !a->c_aug || !a->tau || !a->jpvt ||
791
+ !a->gamma_red || !a->c_top_save || !a->ir_res ||
792
+ !a->nrm_s_col || !a->nrm_y_col ||
793
+ (type1 && (!a->B_aug || !a->W || !a->W_orig || !a->ipiv)) ||
794
+ !a->work ||
795
+ (relaxation != 1.0 && !a->x_work)) {
796
+ scs_printf("Failed to allocate memory for AA.\n");
797
+ aa_finish(a);
798
+ return SCS_NULL;
799
+ }
391
800
 
392
- if (relaxation != 1.0) {
393
- a->x_work = (aa_float *)calloc(a->dim, sizeof(aa_float));
394
- } else {
395
- a->x_work = 0;
801
+ /* LAPACK workspace query: ask geqp3 and ormqr for their preferred lwork,
802
+ * then take the max. lwork = -1 makes the routine write the optimal
803
+ * size into work[0] without doing any factoring. geqp3 typically wants
804
+ * more scratch than geqrf because it also maintains column norms. The
805
+ * optimal size is returned in an aa_float slot; round up with ceil
806
+ * before casting so a value like 255.9999 doesn't truncate to 255 and
807
+ * under-allocate. */
808
+ {
809
+ blas_int b_aug = (blas_int)aug_rows;
810
+ blas_int b_mem = (blas_int)a->mem;
811
+ blas_int b_neg_one = -1;
812
+ blas_int info_q = 0;
813
+ aa_float q_geqp3 = 0.0, q_ormqr_c = 0.0, q_ormqr_b = 0.0;
814
+ BLAS(geqp3)(&b_aug, &b_mem, a->A_aug, &b_aug, a->jpvt, a->tau,
815
+ &q_geqp3, &b_neg_one, &info_q);
816
+ {
817
+ blas_int b_one = 1;
818
+ BLAS(ormqr)
819
+ ("Left", "Trans", &b_aug, &b_one, &b_mem, a->A_aug, &b_aug, a->tau,
820
+ a->c_aug, &b_aug, &q_ormqr_c, &b_neg_one, &info_q);
821
+ }
822
+ if (type1) {
823
+ BLAS(ormqr)
824
+ ("Left", "Trans", &b_aug, &b_mem, &b_mem, a->A_aug, &b_aug, a->tau,
825
+ a->B_aug, &b_aug, &q_ormqr_b, &b_neg_one, &info_q);
826
+ }
827
+ {
828
+ aa_float lwork_f = q_geqp3;
829
+ if (q_ormqr_c > lwork_f) lwork_f = q_ormqr_c;
830
+ if (q_ormqr_b > lwork_f) lwork_f = q_ormqr_b;
831
+ /* Floor at mem — some LAPACK builds return modest sizes; keep
832
+ * a sane minimum. calloc of zero is implementation-defined. */
833
+ if (lwork_f < (aa_float)a->mem) lwork_f = (aa_float)a->mem;
834
+ a->qr_lwork = (blas_int)ceil(lwork_f);
835
+ a->qr_work = (aa_float *)scs_calloc((size_t)a->qr_lwork, sizeof(aa_float));
836
+ }
837
+ }
838
+ if (!a->qr_work) {
839
+ scs_printf("Failed to allocate memory for AA.\n");
840
+ aa_finish(a);
841
+ return SCS_NULL;
842
+ }
396
843
  }
397
844
  TIME_TOC
398
845
  return a;
@@ -417,12 +864,15 @@ aa_float aa_apply(aa_float *f, const aa_float *x, AaWork *a) {
417
864
  /* set various accel quantities */
418
865
  update_accel_params(x, f, a, len);
419
866
 
420
- /* only perform solve steps when the memory is full */
421
- if (!FILL_MEMORY_BEFORE_SOLVE || a->iter >= a->mem) {
422
- /* set M = S'Y or Y'Y depending on type of aa used */
423
- set_m(a, len);
424
- /* solve linear system, new point overwrites f if successful */
867
+ /* Hold off the solve until we have min_len residual pairs buffered. */
868
+ if (a->iter >= a->min_len) {
869
+ /* solve linear system, new point overwrites f if successful.
870
+ * Rejection causes are counted inside solve() where the specific
871
+ * failure mode is known; here we only count acceptances. */
425
872
  aa_norm = solve(f, a, len);
873
+ if (aa_norm > 0) {
874
+ a->n_accept++;
875
+ }
426
876
  }
427
877
  a->iter++;
428
878
  TIME_TOC
@@ -435,6 +885,11 @@ aa_int aa_safeguard(aa_float *f_new, aa_float *x_new, AaWork *a) {
435
885
  blas_int one = 1;
436
886
  aa_float neg_onef = -1.0;
437
887
  aa_float norm_diff;
888
+ if (a->mem <= 0) {
889
+ /* degenerate workspace, nothing to safeguard against */
890
+ TIME_TOC
891
+ return 0;
892
+ }
438
893
  if (!a->success) {
439
894
  /* last AA update was not successful, no need for safeguarding */
440
895
  TIME_TOC
@@ -444,6 +899,9 @@ aa_int aa_safeguard(aa_float *f_new, aa_float *x_new, AaWork *a) {
444
899
  /* reset success indicator in case safeguarding called multiple times */
445
900
  a->success = 0;
446
901
 
902
+ /* NB: a->work is used here as a dim-sized scratch, but elsewhere (in solve)
903
+ * only as a len-sized (<=mem) scratch. This is why it is allocated with
904
+ * MAX(mem, dim) in aa_init — do not shrink it to mem. */
447
905
  /* work = x_new */
448
906
  memcpy(a->work, x_new, a->dim * sizeof(aa_float));
449
907
  /* work = x_new - f_new */
@@ -456,9 +914,10 @@ aa_int aa_safeguard(aa_float *f_new, aa_float *x_new, AaWork *a) {
456
914
  memcpy(f_new, a->f, a->dim * sizeof(aa_float));
457
915
  memcpy(x_new, a->x, a->dim * sizeof(aa_float));
458
916
  if (a->verbosity > 0) {
459
- printf("AA rejection, iter: %i, norm_diff %.4e, prev_norm_diff %.4e\n",
460
- (int)a->iter, norm_diff, a->norm_g);
917
+ scs_printf("AA rejection, iter: %i, norm_diff %.4e, prev_norm_diff %.4e\n",
918
+ (int)a->iter, norm_diff, a->norm_g);
461
919
  }
920
+ a->n_safeguard_reject++;
462
921
  aa_reset(a);
463
922
  TIME_TOC
464
923
  return -1;
@@ -469,32 +928,80 @@ aa_int aa_safeguard(aa_float *f_new, aa_float *x_new, AaWork *a) {
469
928
 
470
929
  void aa_finish(AaWork *a) {
471
930
  if (a) {
472
- free(a->x);
473
- free(a->f);
474
- free(a->g);
475
- free(a->g_prev);
476
- free(a->y);
477
- free(a->s);
478
- free(a->d);
479
- free(a->Y);
480
- free(a->S);
481
- free(a->D);
482
- free(a->M);
483
- free(a->work);
484
- free(a->ipiv);
931
+ scs_free(a->x);
932
+ scs_free(a->f);
933
+ scs_free(a->g);
934
+ scs_free(a->g_prev);
935
+ scs_free(a->Y);
936
+ scs_free(a->S);
937
+ scs_free(a->D);
938
+ scs_free(a->A_aug);
939
+ scs_free(a->B_aug);
940
+ scs_free(a->c_aug);
941
+ scs_free(a->tau);
942
+ scs_free(a->qr_work);
943
+ scs_free(a->jpvt);
944
+ scs_free(a->W);
945
+ scs_free(a->W_orig);
946
+ scs_free(a->ipiv);
947
+ scs_free(a->gamma_red);
948
+ scs_free(a->c_top_save);
949
+ scs_free(a->ir_res);
950
+ scs_free(a->nrm_s_col);
951
+ scs_free(a->nrm_y_col);
952
+ scs_free(a->work);
485
953
  if (a->x_work) {
486
- free(a->x_work);
954
+ scs_free(a->x_work);
487
955
  }
488
- free(a);
956
+ scs_free(a);
489
957
  }
490
958
  }
491
959
 
492
960
  void aa_reset(AaWork *a) {
493
- /* to reset we simply set a->iter = 0 */
961
+ /* Restore the logical state of a freshly calloc'd workspace.
962
+ *
963
+ * Most internal buffers are fully overwritten before they are read:
964
+ * - x, f, g_prev are re-seeded by init_accel_params on the next
965
+ * aa_apply call (which runs when iter == 0).
966
+ * - g, S/Y/D columns, A_aug/B_aug/c_aug, tau, jpvt, qr_work, W/W_orig,
967
+ * gamma_red, c_top_save, ir_res, work, ipiv, x_work are all
968
+ * rewritten inside update_accel_params / solve / relax each
969
+ * iteration before any read.
970
+ *
971
+ * The only buffers that require zeroing are nrm_{s,y}_col: they are
972
+ * reduced over all `mem` slots in compute_regularization, and stale
973
+ * entries from an earlier run would contaminate the Frobenius-norm
974
+ * scale until every slot has been rewritten. */
975
+ if (!a) {
976
+ return;
977
+ }
494
978
  if (a->verbosity > 0) {
495
- printf("AA reset.\n");
979
+ scs_printf("AA reset.\n");
496
980
  }
497
981
  a->iter = 0;
982
+ a->success = 0;
983
+ a->norm_g = 0;
984
+ if (a->nrm_s_col) {
985
+ memset(a->nrm_s_col, 0, sizeof(aa_float) * a->mem);
986
+ }
987
+ if (a->nrm_y_col) {
988
+ memset(a->nrm_y_col, 0, sizeof(aa_float) * a->mem);
989
+ }
498
990
  }
499
991
 
500
- #endif
992
+ AaStats aa_get_stats(const AaWork *a) {
993
+ AaStats s;
994
+ s.iter = a->iter;
995
+ s.n_accept = a->n_accept;
996
+ s.n_reject_lapack = a->n_reject_lapack;
997
+ s.n_reject_rank0 = a->n_reject_rank0;
998
+ s.n_reject_nonfinite = a->n_reject_nonfinite;
999
+ s.n_reject_weight_cap = a->n_reject_weight_cap;
1000
+ s.n_safeguard_reject = a->n_safeguard_reject;
1001
+ s.last_rank = a->last_rank;
1002
+ s.last_aa_norm = a->last_aa_norm;
1003
+ s.last_regularization = a->last_regularization;
1004
+ return s;
1005
+ }
1006
+
1007
+ #endif /* USE_LAPACK */