Skip to content

Commit 37950c7

Browse files
authored
Merge pull request #2 from yo5uke:perf/fista-adaptive-restart
perf(scm): add FISTA adaptive restart to simplex QP solvers
2 parents ff379c2 + 0005b49 commit 37950c7

2 files changed

Lines changed: 30 additions & 8 deletions

File tree

src/optim.cpp

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,31 @@ arma::vec solve_simplex_qp(const arma::mat& Q, const arma::vec& c, int max_iter
4949

5050
arma::vec y = x;
5151
double t_acc = 1.0;
52-
52+
5353
for(int iter = 0; iter < max_iter; iter++) {
5454
arma::vec x_prev = x;
55-
55+
5656
// Gradient step
5757
arma::vec grad = Q * y - c;
58-
58+
5959
// Projection step
6060
x = proj_simplex(y - t * grad);
61-
61+
62+
// Adaptive restart (O'Donoghue & Candes 2015, gradient scheme): when the
63+
// momentum direction disagrees with the descent direction, the
64+
// acceleration is overshooting and slowing convergence on ill-conditioned
65+
// Q (the dominant cost in SCM, where Q = X0'VX0 is highly collinear).
66+
// Resetting t_acc removes the stale momentum. This only changes the path
67+
// to the optimum, not the optimum itself.
68+
if (arma::dot(grad, x - x_prev) > 0.0) {
69+
t_acc = 1.0;
70+
}
71+
6272
// FISTA acceleration
6373
double t_acc_next = (1.0 + std::sqrt(1.0 + 4.0 * t_acc * t_acc)) / 2.0;
6474
y = x + ((t_acc - 1.0) / t_acc_next) * (x - x_prev);
6575
t_acc = t_acc_next;
66-
76+
6777
if(arma::norm(x - x_prev, 2) < tol) {
6878
break;
6979
}
@@ -97,6 +107,10 @@ arma::vec solve_simplex_qp_lr(const arma::mat& B, const arma::vec& b,
97107
arma::vec x_prev = x;
98108
arma::vec grad = B.t() * (B * y - b); // O(k * N_co)
99109
x = proj_simplex(y - t * grad);
110+
// Adaptive restart (gradient scheme) -- see solve_simplex_qp for rationale.
111+
if (arma::dot(grad, x - x_prev) > 0.0) {
112+
t_acc = 1.0;
113+
}
100114
double t_acc_next = (1.0 + std::sqrt(1.0 + 4.0 * t_acc * t_acc)) / 2.0;
101115
y = x + ((t_acc - 1.0) / t_acc_next) * (x - x_prev);
102116
t_acc = t_acc_next;

tests/testthat/test-integration.R

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,9 +1452,17 @@ test_that("Phase 13a: SDID without covariates — beta_hat is length-0", {
14521452
})
14531453

14541454
test_that("Phase 13a: covariate partial-out changes the ATT estimate", {
1455-
fit_no <- scm_fit(y ~ d | id + time, data = panel_cov, method = "sdid")
1456-
fit_cov <- scm_fit(y ~ d | id + time, data = panel_cov, method = "sdid",
1457-
covariates = "cov1")
1455+
# cov1/cov2 in panel_cov are time-invariant, so they are absorbed by the
1456+
# two-way fixed effects and (correctly) leave the SDID ATT essentially
1457+
# unchanged. A meaningful partial-out test needs a genuinely time-varying
1458+
# covariate that drives the outcome.
1459+
set.seed(99)
1460+
pc <- panel_cov
1461+
pc$covtv <- rnorm(nrow(pc), 0, 1) + 0.5 * pc$time
1462+
pc$y <- pc$y + 0.8 * pc$covtv
1463+
fit_no <- scm_fit(y ~ d | id + time, data = pc, method = "sdid")
1464+
fit_cov <- scm_fit(y ~ d | id + time, data = pc, method = "sdid",
1465+
covariates = "covtv")
14581466
expect_false(isTRUE(all.equal(fit_no$estimate, fit_cov$estimate, tol = 1e-6)))
14591467
})
14601468

0 commit comments

Comments
 (0)