Skip to content

Commit 931a7f8

Browse files
authored
Merge pull request #204 from shmercer:PR-34/spec-gaps
fix gaps
2 parents 04f4012 + 4ac1a7f commit 931a7f8

10 files changed

+266
-33
lines changed

R/adaptive_round_candidates.R

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,31 @@
22
# Adaptive stage candidates: anchors, strata, and per-stage generation.
33
# -------------------------------------------------------------------------
44

5-
.adaptive_rank_proxy <- function(state) {
5+
.adaptive_rank_proxy <- function(state, prefer_btl = FALSE) {
66
ids <- as.character(state$trueskill_state$items$item_id)
77
mu <- as.double(state$trueskill_state$items$mu)
88
names(mu) <- ids
99
refit_id <- as.integer(state$refit_meta$last_refit_round_id %||% 0L)
10+
if (isTRUE(prefer_btl)) {
11+
theta_mean <- tryCatch(
12+
.adaptive_btl_fit_theta_mean(state$btl_fit %||% list()),
13+
error = function(e) NULL
14+
)
15+
16+
if (is.numeric(theta_mean) && length(theta_mean) > 0L && !is.null(names(theta_mean))) {
17+
theta <- as.double(theta_mean)
18+
names(theta) <- as.character(names(theta_mean))
19+
theta_scores <- as.double(theta[ids])
20+
names(theta_scores) <- ids
21+
if (length(theta_scores) == length(ids) && all(is.finite(theta_scores))) {
22+
return(list(
23+
scores = theta_scores,
24+
source = "btl_theta_eap",
25+
refit_id = refit_id
26+
))
27+
}
28+
}
29+
}
1030

1131
list(
1232
scores = mu[ids],
@@ -147,7 +167,7 @@
147167
return(out)
148168
}
149169

150-
proxy <- .adaptive_rank_proxy(out)
170+
proxy <- .adaptive_rank_proxy(out, prefer_btl = TRUE)
151171
anchors <- .adaptive_select_rolling_anchors(proxy$scores, defaults)
152172
out$round$anchor_ids <- as.character(anchors)
153173
out$round$anchor_refresh_source <- as.character(proxy$source)

R/adaptive_run.R

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,31 @@
281281
#' @keywords internal
282282
#' @noRd
283283
.adaptive_link_all_spokes_stopped <- function(state) {
284-
FALSE
284+
controller <- .adaptive_controller_resolve(state)
285+
if (!.adaptive_link_mode_active(controller)) {
286+
return(FALSE)
287+
}
288+
phase_ctx <- .adaptive_link_phase_context(state, controller = controller)
289+
if (!identical(as.character(phase_ctx$phase %||% "phase_a"), "phase_b")) {
290+
return(FALSE)
291+
}
292+
spoke_ids <- as.integer(phase_ctx$active_spokes %||% phase_ctx$ready_spokes %||% integer())
293+
spoke_ids <- sort(unique(spoke_ids[!is.na(spoke_ids)]))
294+
if (length(spoke_ids) < 1L) {
295+
return(FALSE)
296+
}
297+
stopped_map <- controller$link_stopped_by_spoke %||% list()
298+
all_stopped <- all(vapply(as.character(spoke_ids), function(key) isTRUE(stopped_map[[key]]), logical(1L)))
299+
if (!isTRUE(all_stopped)) {
300+
return(FALSE)
301+
}
302+
303+
probe_cap <- as.integer(controller$probe_pairs_per_refit_per_spoke %||% 2L)
304+
probe_cap <- max(0L, probe_cap)
305+
if (probe_cap > 0L) {
306+
return(FALSE)
307+
}
308+
TRUE
285309
}
286310

287311
#' @keywords internal

R/adaptive_select.R

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,7 @@ select_next_pair <- function(state, step_id = NULL, candidates = NULL) {
10571057
link_phase_b <- .adaptive_link_mode_active(controller) && identical(phase_ctx$phase, "phase_b")
10581058
link_phase_b_concurrent <- isTRUE(link_phase_b) &&
10591059
identical(as.character(controller$multi_spoke_mode %||% "independent"), "concurrent")
1060+
frozen_map <- link_controller$link_transform_frozen_by_spoke %||% list()
10601061
active_link_spoke <- as.integer(NA_integer_)
10611062
ranked_link_spokes <- integer()
10621063
link_progress <- NULL
@@ -1177,6 +1178,7 @@ select_next_pair <- function(state, step_id = NULL, candidates = NULL) {
11771178
local_priority_mode <- NA_character_
11781179
is_explore_step <- FALSE
11791180
selected_link_spoke_attempt <- as.integer(NA_integer_)
1181+
selected_is_probe_ordering <- FALSE
11801182
selected_round_stage <- as.character(round_stage)
11811183
selected_stage_quota <- as.integer(stage_quota)
11821184
selected_stage_committed_so_far <- as.integer(stage_committed_so_far)
@@ -1258,18 +1260,24 @@ select_next_pair <- function(state, step_id = NULL, candidates = NULL) {
12581260
if (is.null(cand) || nrow(cand) == 0L) {
12591261
next
12601262
}
1263+
spoke_for_utility <- as.integer(NA_integer_)
1264+
stage_is_probe_ordering <- FALSE
12611265
if (isTRUE(is_link_mode) && isTRUE(link_phase_b)) {
12621266
spoke_for_utility <- if ("link_spoke_id" %in% names(cand)) {
12631267
as.integer(unique(stats::na.omit(as.integer(cand$link_spoke_id)))[1L] %||% NA_integer_)
12641268
} else {
12651269
as.integer(spoke_attempt %||% NA_integer_)
12661270
}
1267-
cand <- .adaptive_link_attach_predictive_utility(
1268-
candidates = cand,
1269-
state = state,
1270-
controller = link_controller,
1271-
spoke_id = as.integer(spoke_for_utility)
1272-
)
1271+
stage_is_probe_ordering <- !is.na(spoke_for_utility) &&
1272+
isTRUE(frozen_map[[as.character(spoke_for_utility)]])
1273+
if (!isTRUE(stage_is_probe_ordering)) {
1274+
cand <- .adaptive_link_attach_predictive_utility(
1275+
candidates = cand,
1276+
state = state,
1277+
controller = link_controller,
1278+
spoke_id = as.integer(spoke_for_utility)
1279+
)
1280+
}
12731281
}
12741282

12751283
explore_rate <- defaults$explore_rate
@@ -1379,9 +1387,9 @@ select_next_pair <- function(state, step_id = NULL, candidates = NULL) {
13791387
na.rm = TRUE
13801388
)
13811389
selected_utility_mode <- .adaptive_selection_utility_mode(
1382-
run_mode = controller$run_mode,
1390+
run_mode = if (isTRUE(stage_is_probe_ordering)) "within_set" else controller$run_mode,
13831391
has_regularization = isTRUE(has_regularized_utility),
1384-
is_cross_set = isTRUE(is_link_mode) && isTRUE(link_phase_b)
1392+
is_cross_set = isTRUE(is_link_mode) && isTRUE(link_phase_b) && !isTRUE(stage_is_probe_ordering)
13851393
)
13861394
if (isTRUE(is_link_mode) && isTRUE(link_phase_b)) {
13871395
# Linking mode keeps canonical candidate generation/filtering via
@@ -1420,6 +1428,7 @@ select_next_pair <- function(state, step_id = NULL, candidates = NULL) {
14201428
local_priority_mode <- stage_local_priority_mode
14211429
selected_stage <- stage
14221430
selected_link_spoke_attempt <- as.integer(spoke_attempt %||% NA_integer_)
1431+
selected_is_probe_ordering <- isTRUE(stage_is_probe_ordering)
14231432
selected_round_stage <- as.character(attempt_round_stage)
14241433
selected_stage_quota <- as.integer(attempt_stage_quota)
14251434
selected_stage_committed_so_far <- as.integer(attempt_stage_committed_so_far)
@@ -1545,16 +1554,20 @@ select_next_pair <- function(state, step_id = NULL, candidates = NULL) {
15451554
B_id <- as.character(order_vals[["B_id"]] %||% NA_character_)
15461555
p_ij_ts <- trueskill_win_probability(A_id, B_id, state$trueskill_state)
15471556
p_ij <- as.double(p_ij_ts)
1548-
has_regularized_utility <- "u" %in% names(selected_pair) &&
1549-
"u0" %in% names(selected_pair) &&
1550-
is.finite(as.double(selected_pair$u[[1L]])) &&
1551-
is.finite(as.double(selected_pair$u0[[1L]])) &&
1552-
abs(as.double(selected_pair$u[[1L]]) - as.double(selected_pair$u0[[1L]])) > sqrt(.Machine$double.eps)
1553-
utility_mode <- .adaptive_selection_utility_mode(
1554-
run_mode = controller$run_mode,
1555-
has_regularization = isTRUE(has_regularized_utility),
1556-
is_cross_set = isTRUE(selected_is_cross_set)
1557-
)
1557+
if (isTRUE(selected_is_cross_set) && isTRUE(selected_is_probe_ordering)) {
1558+
utility_mode <- NA_character_
1559+
} else {
1560+
has_regularized_utility <- "u" %in% names(selected_pair) &&
1561+
"u0" %in% names(selected_pair) &&
1562+
is.finite(as.double(selected_pair$u[[1L]])) &&
1563+
is.finite(as.double(selected_pair$u0[[1L]])) &&
1564+
abs(as.double(selected_pair$u[[1L]]) - as.double(selected_pair$u0[[1L]])) > sqrt(.Machine$double.eps)
1565+
utility_mode <- .adaptive_selection_utility_mode(
1566+
run_mode = controller$run_mode,
1567+
has_regularization = isTRUE(has_regularized_utility),
1568+
is_cross_set = isTRUE(selected_is_cross_set)
1569+
)
1570+
}
15581571
if (isTRUE(is_link_mode) && !is.na(selected_spoke_id)) {
15591572
p_link_oriented <- .adaptive_link_predictive_prob_oriented(
15601573
state = state,

R/adaptive_step.R

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,12 @@ validate_judge_result <- function(result, A_id, B_id) {
264264
}
265265
run_mode <- as.character(row$run_mode[[1L]] %||% "within_set")
266266
is_link_run_mode <- run_mode %in% c("link_one_spoke", "link_multi_spoke", "link_probe")
267+
is_probe_run_mode <- identical(run_mode, "link_probe")
267268

268269
is_cross <- row$is_cross_set[[1L]]
269270
if (isTRUE(is_cross)) {
270271
required_cross <- c("set_i", "set_j", "link_spoke_id", "run_mode", "posterior_win_prob_pre")
271-
if (isTRUE(is_link_run_mode)) {
272+
if (isTRUE(is_link_run_mode) && !isTRUE(is_probe_run_mode)) {
272273
required_cross <- c(required_cross, "cross_set_utility_pre")
273274
}
274275
bad <- required_cross[vapply(required_cross, function(col) is.na(row[[col]][[1L]]), logical(1L))]
@@ -285,14 +286,34 @@ validate_judge_result <- function(result, A_id, B_id) {
285286
"step_log append completeness failure for cross-set row: `link_stage` must be populated for stage-routed steps."
286287
)
287288
}
288-
if (isTRUE(is_link_run_mode) && !identical(utility_mode, "linking_d_optimal")) {
289+
if (isTRUE(is_link_run_mode) &&
290+
!isTRUE(is_probe_run_mode) &&
291+
!identical(utility_mode, "linking_d_optimal")) {
289292
rlang::abort(
290293
paste0(
291294
"step_log append completeness failure for cross-set row: ",
292295
"`utility_mode` must be linking_d_optimal."
293296
)
294297
)
295298
}
299+
if (isTRUE(is_probe_run_mode) &&
300+
identical(utility_mode, "linking_d_optimal")) {
301+
rlang::abort(
302+
paste0(
303+
"step_log append completeness failure for cross-set probe row: ",
304+
"`utility_mode` must not be linking_d_optimal."
305+
)
306+
)
307+
}
308+
if (isTRUE(is_probe_run_mode) &&
309+
!is.na(row$cross_set_utility_pre[[1L]])) {
310+
rlang::abort(
311+
paste0(
312+
"step_log append completeness failure for cross-set probe row: ",
313+
"`cross_set_utility_pre` must be NA."
314+
)
315+
)
316+
}
296317
posterior_pre <- as.double(row$posterior_win_prob_pre[[1L]] %||% NA_real_)
297318
if (!is.finite(posterior_pre) || posterior_pre < 0 || posterior_pre > 1) {
298319
rlang::abort(
@@ -621,6 +642,7 @@ run_one_step <- function(state, judge, ...) {
621642
is_probe_step <- if (isTRUE(is_cross_set) && identical(run_mode, "link_probe")) TRUE else FALSE
622643
cross_set_utility_pre <- if (isTRUE(is_cross_set) &&
623644
isTRUE(is_link_run_mode) &&
645+
!isTRUE(is_probe_step) &&
624646
identical(utility_mode, "linking_d_optimal")) {
625647
as.double(
626648
if (is.finite(as.double(selection$link_d_opt_gain %||% NA_real_))) {
1 Byte
Binary file not shown.

tests/testthat/test-5006-candidates.R

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,12 @@ test_that("rolling anchors refresh deterministically from trueskill", {
133133
expect_equal(state_1$round$anchor_refresh_source, "trueskill_mu")
134134

135135
state_1$btl_fit <- list(
136-
theta_mean = stats::setNames(seq(1, 10), as.character(items$item_id))
136+
theta_mean = stats::setNames(seq(10, 1), as.character(items$item_id))
137137
)
138138
state_1$refit_meta$last_refit_round_id <- 1L
139139
state_2 <- pairwiseLLM:::.adaptive_refresh_round_anchors(state_1)
140140

141-
expect_equal(state_2$round$anchor_refresh_source, "trueskill_mu")
141+
expect_equal(state_2$round$anchor_refresh_source, "btl_theta_eap")
142142
expect_identical(state_2$round$anchor_ids, anchors_1)
143143
expect_equal(state_2$round$anchor_refit_round_id, 1L)
144144

@@ -147,6 +147,20 @@ test_that("rolling anchors refresh deterministically from trueskill", {
147147
expect_equal(state_3$round$anchor_refit_round_id, 1L)
148148
})
149149

150+
test_that("rank proxy falls back to trueskill when BTL theta is incomplete", {
151+
items <- make_test_items(5)
152+
trueskill_state <- make_test_trueskill_state(items, mu = seq(5, 1))
153+
state <- make_test_state(items, trueskill_state)
154+
state$btl_fit <- list(
155+
theta_mean = stats::setNames(c(1, 2, 3, 4), as.character(1:4))
156+
)
157+
158+
proxy <- pairwiseLLM:::.adaptive_rank_proxy(state)
159+
160+
expect_identical(proxy$source, "trueskill_mu")
161+
expect_equal(unname(proxy$scores), as.double(trueskill_state$items$mu))
162+
})
163+
150164
test_that("rolling anchor count follows clamped default", {
151165
scores_small <- stats::setNames(seq(9, 1), as.character(1:9))
152166
scores_mid <- stats::setNames(seq(30, 1), as.character(1:30))

tests/testthat/test-5011-run-one-step-transactional.R

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,71 @@ test_that("run_one_step logs linking pre-step transform estimates when available
176176
expect_equal(row$log_alpha_spoke_sd_pre[[1L]], 0.02, tolerance = 1e-12)
177177
})
178178

179+
test_that("run_one_step logs probe rows without linking d-opt utility fields", {
180+
items <- tibble::tibble(
181+
item_id = c("h1", "h2", "s21", "s22"),
182+
set_id = c(1L, 1L, 2L, 2L),
183+
global_item_id = c("gh1", "gh2", "gs21", "gs22")
184+
)
185+
state <- adaptive_rank_start(
186+
items,
187+
seed = 37L,
188+
adaptive_config = list(run_mode = "link_one_spoke", hub_id = 1L)
189+
)
190+
state$warm_start_done <- TRUE
191+
state$linking$phase_a <- list(
192+
set_status = tibble::tibble(
193+
set_id = c(1L, 2L),
194+
source = c("run", "run"),
195+
status = c("ready", "ready"),
196+
validation_message = c("ok", "ok"),
197+
artifact_path = c(NA_character_, NA_character_)
198+
),
199+
artifacts = list(
200+
`1` = list(
201+
items = tibble::tibble(
202+
global_item_id = c("gh1", "gh2"),
203+
theta_raw_mean = c(0.2, -0.2),
204+
theta_raw_sd = c(0.1, 0.1),
205+
rank_mu_raw = c(1, 2)
206+
)
207+
),
208+
`2` = list(
209+
items = tibble::tibble(
210+
global_item_id = c("gs21", "gs22"),
211+
theta_raw_mean = c(0.1, -0.1),
212+
theta_raw_sd = c(0.1, 0.1),
213+
rank_mu_raw = c(1, 2)
214+
)
215+
)
216+
),
217+
ready_for_phase_b = TRUE,
218+
strict_ready_for_phase_b = TRUE,
219+
required_sets = c(1L, 2L),
220+
set_stop_pass_by_set = list(`1` = TRUE, `2` = TRUE),
221+
phase = "phase_b",
222+
ready_spokes = 2L,
223+
active_phase_a_set = NA_integer_,
224+
phase_b_started_at_step = 1L
225+
)
226+
state$controller$link_transform_frozen_by_spoke <- list(`2` = TRUE)
227+
state$controller$link_transform_frozen_delta_by_spoke <- list(`2` = 0)
228+
state$controller$link_transform_mode_by_spoke <- list(`2` = "shift_only")
229+
state$controller$link_refit_stats_by_spoke <- list(`2` = list(
230+
link_transform_mode = "shift_only",
231+
delta_spoke_mean = 0,
232+
delta_spoke_sd = 0.1
233+
))
234+
235+
out <- pairwiseLLM:::run_one_step(state, make_deterministic_judge("i_wins"))
236+
row <- out$step_log[nrow(out$step_log), , drop = FALSE]
237+
238+
expect_identical(as.character(row$run_mode[[1L]]), "link_probe")
239+
expect_true(isTRUE(row$is_probe_step[[1L]]))
240+
expect_true(is.na(row$utility_mode[[1L]]))
241+
expect_true(is.na(row$cross_set_utility_pre[[1L]]))
242+
})
243+
179244
test_that("invalid linking step does not mutate controller link routing state", {
180245
items <- tibble::tibble(
181246
item_id = c("a", "b"),

tests/testthat/test-5049-linking-candidates-round-routing.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,8 @@ test_that("frozen spoke cross-set commits are tagged as probe steps", {
11871187
expect_true(isTRUE(row$is_cross_set[[1L]]))
11881188
expect_identical(as.character(row$run_mode[[1L]]), "link_probe")
11891189
expect_true(isTRUE(row$is_probe_step[[1L]]))
1190+
expect_true(is.na(row$cross_set_utility_pre[[1L]]))
1191+
expect_true(is.na(row$utility_mode[[1L]]))
11901192
})
11911193

11921194
test_that("linking predictive utility applies signed position bias by (A,B) orientation", {

0 commit comments

Comments
 (0)