1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
//! gh#616 — the corpus cost of the gh#605 least-square-init safeguard,
//! pinned so it is visible to the suite instead of living in a PR body.
//!
//! Under `least_square_init_primal=yes` (off by default), gh#605's
//! safeguard is a broad win — ten fixtures reach the same answer in
//! fewer iterations, several dramatically — and it cost two tolerance
//! downgrades when gh#616 measured it: `csfi2` and `eigenb2` finished
//! at `SolvedToAcceptableLevel` where the unsafeguarded step reached
//! `SolveSucceeded`.
//!
//! **For one release, one of those two was gone, and nobody went looking
//! for it** (gh#681). gh#588's Q4 evaluates a recognized degree-≤2 row
//! from its constant matrix instead of rebuilding an AD tape every
//! iteration, which reassociates the sums in `eval_g` and `eval_jac_g` —
//! `quad_evaluator_differential.rs` declares those two comparisons
//! non-bitwise in advance, for exactly this reason. `eigenb2` sat close
//! enough to the accept band for the reassociation to carry it across,
//! reaching `SolveSucceeded` in 54 iterations where the tape stalled at
//! `SolvedToAcceptableLevel` in 57. `csfi2` does not move at all, to the
//! bit — the safeguard declines there, so no evaluator change can reach
//! it.
//!
//! gh#702 then compensated that same sum, because the uncompensated
//! association was costing a 1500-variable QCQP 28 extra iterations, and
//! `eigenb2` came back to `SolvedToAcceptableLevel`. So the count is two
//! again, as gh#616 measured it. The lesson is not that either verdict is
//! right: it is that on these two models the status is **downstream
//! round-off**, and the tests below are written to make that visible
//! rather than to defend a particular one.
//!
//! That is a trajectory change and it is pinned as one: the tests below
//! assert both legs, so the fast path's verdict and the tape's are each
//! held in place and a future move is attributable to one of them.
//!
//! gh#616 decided that cost is acceptable and that the accept test must
//! **not** be tightened to chase it. The reasoning is in
//! `docs/src/initialization.md` and the algebra is pinned in
//! `pounce-algorithm/tests/issue_616_ls_init_accept_test.rs`. What this
//! file pins is the measurement, because the failure mode CLAUDE.md
//! warns about is a recorded cost that nothing re-reads: gh#544's
//! `pooling_rt2stp` 206 → 812 sat in a commit message through a
//! release. If someone later retunes the accept test and these statuses
//! move, that is a decision being reversed and it should have to be
//! noticed.
//!
//! Every assertion here is on **status and objective**, plus three
//! iteration-count *relations* (two `==`, one `!=`) that each carry a
//! mechanism claim about which arm of the safeguard ran. No absolute
//! iteration count is asserted: those are the most platform-sensitive
//! numbers in a sweep, and none of gh#616's conclusions rests on a
//! particular one.
//!
//! The measured counts, for the record, under
//! `least_square_init_primal=yes` against `=no` on the parent commit
//! `a44f4e8b`: `csfi2` 35/35, `eigena2` 65/26, `eigenb2` 57/67, `deb7`
//! 202/154, `pooling_rt2stp` 81/298, `unbounded_cubic` 290/290.
//!
//! ## gh#693: the safeguard is unchanged; two of these models stop being
//! ## coin flips
//!
//! gh#693 removed the Tikhonov `δ` from the least-square multiplier
//! initializer. It does **not** change what this safeguard decides. The
//! attribution line is bit-identical across the change on all three
//! models this file drives:
//!
//! ```text
//! csfi2 violation 1508.554… → 1508.554… alpha=0 trials=4 declined
//! eigenb2 violation 1.0 → 0.25000000624999996 alpha=0.5 trials=1 accepted
//! pooling_rt2stp violation 4.93000007 → 4.93000007 alpha=0 trials=4 declined
//! ```
//!
//! Same numbers, same arm, same verdict. What moves is the state the
//! initializer's augmented-system solve leaves behind, and it moves the
//! two models that were sitting on the accept band:
//!
//! ```text
//! main (0.10.0) with gh#693
//! eigenb2 =yes SolvedToAcceptableLevel 48 SolveSucceeded 17
//! eigenb2 =no SolveSucceeded 67 SolveSucceeded 21
//! eigena2 =yes SolvedToAcceptableLevel 127 SolveSucceeded 17
//! csfi2 =yes SolvedToAcceptableLevel 35 SolvedToAcceptableLevel 35
//! ```
//!
//! The header above warned that a `SolveSucceeded` on `eigenb2` "means
//! something reassociated `eval_g` again and landed on the lucky side of
//! the accept band ... and it is not a fix". That was the right warning
//! and it is worth being precise about why it does not apply here.
//!
//! Each model was re-run at 17 values of `mu_init` at `0.1·(1 ± k·1e-12)`
//! — round-off scale, where a model sitting on a tolerance band scatters
//! and one that is clear of it does not:
//!
//! ```text
//! main gh#693
//! eigenb2 =yes 14 Succeeded / 3 Acceptable 17 Succeeded
//! eigena2 =yes 11 Succeeded / 6 Acceptable 17 Succeeded
//! csfi2 =yes 17 Acceptable 17 Acceptable
//! ```
//!
//! So on `main` these two statuses were never stable facts: the values
//! this file pinned were a 3-point island around the default draw, and
//! the majority outcome on `main` itself was already the other one.
//! gh#693 does not carry them across the band, it moves them off it —
//! and `csfi2`, which is genuinely clear of the band, does not move at
//! all, to the bit, in either build. The pins below are updated to the
//! measured outcome, and the round-off screen is the reason they are now
//! stronger than what they replace rather than weaker.
//!
//! This also amends gh#706, which recorded `eigena2`'s status as
//! *platform*-dependent. On `main` it is round-off-dependent on a single
//! platform — 11/6 across a `1e-12` perturbation — which is a simpler
//! and worse explanation. It is deterministic here.
//!
//! `pooling_rt2stp` is the one model in this file that gh#693 does not
//! settle, and the assertion it used to carry was never true in the way
//! the file believed. See the test itself.
use BTreeMap;
use PathBuf;
use Command;
use SolveReport;
/// Solve `model` and return its report. `ls_init` picks the route:
/// `true` sets `least_square_init_primal=yes`, `false` leaves the
/// default (`no`).
///
/// The exit status is deliberately not asserted: `unbounded_cubic` is
/// meant to end at `DivergingIterates`, which the CLI reports with a
/// nonzero exit, and that verdict is exactly what one of the tests
/// below is checking. The report file being written and parseable is
/// the success condition here.
/// `solve`, plus environment for the child process. The only caller
/// that needs it sets `POUNCE_DBG_NO_QUAD=1` to force the AD tape,
/// which is how a fixture that moved under gh#588's Q4 is made to say
/// so instead of being asserted around.
/// `solve_with_env`, plus extra CLI options appended verbatim. Used by
/// the barrier-independence screen below, which is the same run at two
/// values of `mu_init`.
/// The attribution channel gh#616 added, because taking this
/// measurement without it meant editing `init/default.rs` to print the
/// report and rebuilding the workspace — for every hypothesis. The
/// accessor `IpoptApplication::least_square_init_report` is not
/// reachable from the CLI, and the fixture sweep runs the CLI.
///
/// One line per solve on the `pounce::algorithm` target at `debug`,
/// carrying the same fields as the accessor. Silent at the default log
/// level: this must not become per-iteration noise on a normal run.
/// Strip ANSI SGR escapes. `tracing_subscriber`'s fmt layer colours
/// its field names whether or not stderr is a terminal, so a captured
/// attribution line holds `\x1b[3mviolation_initial\x1b[0m\x1b[2m=\x1b[0m1.0`
/// where the eye reads `violation_initial=1.0`.
/// The safeguard's attribution line for `model` under
/// `least_square_init_primal=yes`, parsed to field -> value.
///
/// This is the channel gh#616 added, used for the thing it was added
/// for: comparing two models' safeguard decisions without editing
/// `init/default.rs` and rebuilding.
/// What the safeguard costs, measured on the route users actually get.
///
/// gh#616 recorded two downgrades. On this branch there is one.
///
/// `csfi2` still reaches 55.0176045 at `SolvedToAcceptableLevel`, bit
/// for bit what `=no` reaches, because the safeguard declines every
/// trial there — a decline is not a step, so there is no trajectory for
/// an evaluator change to perturb.
///
/// `eigenb2` downgrades too, which is what gh#616 measured in the first
/// place. It spent one release not doing so: gh#588's Q4 reassociated
/// `eval_g` and nudged this model — which sat close enough to the accept
/// band to be nudged — across it. That was never a better safeguard, and
/// the version of this test written against it said so.
///
/// gh#702 compensated the same sum and put `eigenb2` back where gh#616
/// found it. Three associations of one dot product, three answers:
///
/// | route | status | iters | objective |
/// |-----------------------------|---------------------------|-------|--------------------|
/// | tape (`POUNCE_DBG_NO_QUAD`) | `SolvedToAcceptableLevel` | 57 | 1.59999999134715 |
/// | Q4, uncompensated | `SolveSucceeded` | 54 | 1.599999999992518 |
/// | Q4, compensated (gh#702) | `SolvedToAcceptableLevel` | 48 | 1.599999996403372 |
///
/// Two of the three agree with gh#616, and the odd one out is the one
/// nobody designed. Do not read the recovery as progress if it comes
/// back — check which association produced it.
///
/// The `POUNCE_DBG_NO_QUAD=1` leg no longer discriminates, since both
/// evaluator routes agree. It stays as a cross-check: the two routes
/// agreeing on this model is the normal state, and Q4's window was the
/// exception.
///
/// gh#693 adds a fourth row to that table, and it is the one that
/// settles it — `SolveSucceeded` in 17 iterations on both evaluator
/// routes, and 17/17 under the round-off screen in the header. The three
/// rows above disagree because all three sat on the accept band; this
/// one is off it. The test name changed with it: the safeguard's
/// measured cost is now `csfi2` alone.
/// The fact that decides gh#616: `eigena2` and `eigenb2` hand the
/// safeguard **the same numbers**, and it takes the same decision on
/// both — `theta_0 = 1.0`, one rejected trial, accepted at
/// `alpha = 0.5` on a step of norm 3.2596011939729705.
///
/// gh#616 read that fact off a status *disagreement*: identical
/// decisions, different outcomes, therefore no criterion computed from
/// the safeguard's own inputs separates them, therefore tightening the
/// accept test could not rescue `eigenb2` without also reaching
/// `eigena2`, which needed no rescuing. That disagreement has since
/// opened and closed twice, so this test asserts the premise
/// **directly**, off the attribution channel, instead of inferring it
/// from the outcomes.
///
/// The conclusion now rests on three independent reassociations of one
/// dot product rather than one. Each moves the `violation_final` the
/// safeguard reports by a few ulps, each moves **both models by the
/// same amount**, and nothing else the accept test reads changes at all:
///
/// | route | `violation_final` | `eigena2` | `eigenb2` |
/// |--------------------------|----------------------|------------|-----------|
/// | tape | 0.2500000062500001 | succeeded | acceptable|
/// | Q4, uncompensated | 0.2500000062500003 | succeeded | succeeded |
/// | Q4, compensated (gh#702) | 0.25000000624999996 | *platform* | acceptable|
/// | …plus gh#693 (`main`) | 0.25000000624999996 | succeeded | succeeded |
///
/// The last row is the current one, and it is bit-identical to the row
/// above it in every field the safeguard reads — gh#693 changes nothing
/// this test asserts about the decision, only where the iteration after
/// it lands. That is the fourth reassociation and the third verdict on
/// the pair, which is the point: the accept test never decided either.
///
/// Three associations, three verdicts on the pair, off inputs that stay
/// bit-for-bit identical between the two models every time. So neither
/// model's status was ever a property of the accept test: both are
/// decided downstream, by where the iteration after the safeguard lands
/// relative to the acceptable band, and one reassociated sum in `eval_g`
/// is enough to move either. An accept test tightened to chase `eigenb2`
/// would have been tuned against round-off — gh#616's conclusion, now
/// re-derived twice.
///
/// **`eigena2` is why that third row says *platform*.** Under gh#702's
/// compensated sum it reached `SolveSucceeded` on Linux and
/// `SolvedToAcceptableLevel` in 127 iterations on macOS, for an objective
/// correct to 82.50000000000348 either way. That was not a status this
/// file could pin, and the attempt to pin it is what caught the fact: the
/// first version of this assertion asserted the macOS reading and failed
/// on CI. It was filed as gh#706, whose question was not how to get 51
/// iterations back — that number was luck — but why a model this
/// well-behaved sat close enough to the band that libm decided it.
///
/// gh#693 answered that, and not by carrying the model across the band:
/// it took the barrier parameter out of this model's steering
/// altogether. Measured on gh#693's parent (`fe631b0c^`) against `main`,
/// same machine, same fixture, `least_square_init_primal=yes`:
///
/// ```text
/// mu_init screen at 1e-12 mu_init 0.1 -> 100 (1000x)
/// fe631b0c^ 11 succeeded / 6 acceptable n/a: the 1e-12 screen
/// iterations 61 … 127 already moves it 2x
/// main 17 succeeded, 17 iterations every iterate bit-identical
/// every time — objective, inf_pr, ‖d‖,
/// both alphas; only the
/// printed lg(mu) column moves
/// ```
///
/// A trajectory that swings 61 → 127 iterations on a last-ulp change in
/// `mu_init` is what "sitting on the band" meant here. One that does not
/// move at all when `mu_init` moves three decades is not near one, and
/// the cause was the Tikhonov `δ` in the multiplier initializer rather
/// than gh#702's compensated sum. The tape route now agrees with the
/// compensated one bit-for-bit on the objective, where gh#706 recorded
/// three different values across the three routes above.
///
/// So the assertion below is single-valued again. If it starts failing on
/// one platform's CI leg only, that is gh#706 returning and the two-valued
/// pin was load-bearing after all — say so in the issue rather than
/// widening the `matches!` back out, which is how a defect becomes a
/// shrug.
///
/// The absolute values pinned below are the ones the accept test reads
/// (`violation_initial`, `alpha`, `rejected_trials`, `termination`).
/// `violation_final` and `step_norm` are compared **between** the two
/// models only, never against a literal: cross-model equality is the
/// durable fact, and the last two digits of a reported violation are
/// the kind of number Q4 has already been shown to move.
/// The measurement that makes the single-valued pin above safe, kept as
/// an assertion instead of as a paragraph: `eigena2`'s trajectory no
/// longer depends on the barrier parameter at all.
///
/// `mu_init` is what gh#706's round-off screen perturbed, and on gh#693's
/// parent a change of one part in `1e12` swung this model between 61 and
/// 127 iterations and between both statuses. Here it moves by three
/// decades — a thousandfold, not an ulp — and the run does not notice:
/// same iteration count, same objective to the bit. Only the printed
/// `lg(mu)` column differs.
///
/// A model whose steps are steered by the barrier parameter cannot
/// produce that. So a failure here says the pin above has stopped being
/// safe for the reason it was tightened — the status is a coin flip
/// again, whether or not it has yet landed on the wrong side of the band
/// on this platform.
/// `csfi2` is not a case the accept test could ever have rescued: the
/// safeguard **declines** there — all four trials are worse than
/// `theta_0 = 1508.554...` — so `least_square_init_primal=yes` gives
/// back exactly what `=no` gives, to the bit.
///
/// That is the whole reason no tightening reaches `csfi2`: a tighter
/// test still declines, and the only thing that reaches its old
/// `SolveSucceeded` is accepting a step that makes the violation worse,
/// which is what gh#605 exists to prevent.
/// But "declined" is **not** "never asked", and that surprises people.
///
/// Declining restores the user's `x` exactly. It does not restore the
/// solver's state: computing the direction already drove the first
/// factorization through the augmented-system solver, on the `W = 0`
/// least-square matrix rather than on the first real KKT matrix.
/// gh#616 isolated this by forcing a decline on either side of that
/// call — declining *before* it is bit-identical to
/// `least_square_init_primal=no` everywhere, declining *after* it is
/// bit-identical to the real safeguard.
///
/// `pooling_rt2stp` is where it shows, as a large iteration difference
/// in the *declined* route's favour. This test pins that the two routes
/// differ, not by how much — the direction is the mechanism claim, the
/// magnitude is a platform detail.
///
/// **This test used to also assert the two routes reach the same local
/// optimum, and that was never a property of this model.** gh#693 made
/// it fail — on the default draw the declined route reaches −4391.826
/// and the `=no` route −3273.955 — which prompted measuring it properly
/// rather than re-pinning it. Across 17 values of `mu_init` at
/// `0.1·(1 ± k·1e-12)` the two routes agree on the optimum at **10 of 17
/// points on `main`** and 8 of 17 here. It was a coin flip that happened
/// to land heads at one draw.
///
/// The repository had in fact already recorded both sides of that flip
/// as fact in different places: `docs/src/initialization.md` still
/// carried gh#616's original measurement, where the two routes reached
/// *different* optima (−4391.826 against −3273.955), while this test
/// asserted they reached the same one. Both were written from a single
/// draw of a bistable nonconvex model, and neither noticed the other.
/// The doc now says so too.
///
/// So the same-optimum assertion is gone rather than inverted, and what
/// is left is the part that survives the screen: the iteration counts
/// differ at **17 of 17 points on both builds**. That is the mechanism
/// claim, and it is the one this test was written to make.
/// `unbounded_cubic` starts feasible (`theta_0 = 0`), so the safeguard
/// short-circuits before computing any direction: no violation can be
/// improved on zero. The unsafeguarded path took a step anyway and
/// diverged in 91 iterations against 290 here.
///
/// This is the third, independent arm of the safeguard — neither
/// `csfi2`'s decline nor `eigenb2`'s backtrack — and the issue asked
/// specifically whether the three share a mechanism. They do not. The
/// status is `DivergingIterates` either way: the model is unbounded and
/// the verdict is right on both routes.
/// The headline that keeps the decision honest: turning the option on
/// does not lose a model. The solved-or-acceptable set is the same size
/// on both routes — gh#605 moved two fixtures between the two solved
/// statuses, it did not drop any.
/// `deb7` is the one fixture that cannot carry the invariant above, and
/// the reason is not a POUNCE defect: **reference Ipopt does not satisfy
/// it either.**
///
/// Ipopt 3.14.19, `deb7.nl`, one innocuous option perturbed at a time,
/// solved-or-acceptable on each route:
///
/// | perturbation | `least_square_init_primal=yes` | `=no` |
/// |---|---|---|
/// | `max_soc=0` | Solved To Acceptable Level | Error in step computation |
/// | `tol=1e-6` | Error in step computation | Optimal Solution Found |
///
/// Two of eight sampled perturbations flip Ipopt's own verdict with the
/// option. `deb7` sits on a knife edge here, and which side a binary
/// lands on is set by last-bit arithmetic rather than by anything the
/// option means. POUNCE reproduces that sensitivity: on x86-64, with
/// `feral_refine` off (gh#735 measured it that way; it is not the
/// exact-Hessian default, which is `yes` — gh#909), the `yes` route reaches
/// `ErrorInStepComputation` at it=358 where `no` succeeds at it=131 —
/// while on aarch64 both succeed. `main` is not immune, it is merely on
/// the other side: perturbing `obj_scaling_factor` or `max_soc` flips
/// `main`'s verdict on this fixture and leaves gh#735's unchanged. MA57
/// flips on the same two.
///
/// So this test pins what `deb7` actually guarantees — it solves on the
/// default route — instead of an invariant no implementation holds.