Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions R/adaptive_btl_refit.R
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,8 @@
hub_theta,
spoke_theta,
transform_mode) {
# Linking transform parameters are estimated via MAP optimization with
# Hessian-based (Laplace-style) uncertainty approximation.
use_scale <- identical(transform_mode, "shift_scale")
edge_attrs <- attributes(cross_edges)
refit_contract_ctx <- edge_attrs$refit_contract %||% list()
Expand Down Expand Up @@ -967,6 +969,8 @@
)
empty$fit_contract <- list(
contract_type = "link_refit",
estimation_method = "map_optim",
uncertainty_approximation = "hessian_laplace",
link_refit_mode = as.character(link_refit_mode),
link_transform_mode = as.character(transform_mode),
parameters = if (isTRUE(use_scale)) c("delta_s", "log_alpha_s") else c("delta_s"),
Expand Down Expand Up @@ -1191,6 +1195,8 @@

fit_contract <- list(
contract_type = "link_refit",
estimation_method = "map_optim",
uncertainty_approximation = "hessian_laplace",
link_refit_mode = as.character(link_refit_mode),
link_transform_mode = as.character(transform_mode),
parameters = if (isTRUE(joint_used)) {
Expand Down
22 changes: 16 additions & 6 deletions R/adaptive_round_candidates.R
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,8 @@ generate_stage_candidates_from_state <- function(state,

#' @keywords internal
#' @noRd
.adaptive_linking_selection_order <- function(candidates) {
.adaptive_linking_selection_order <- function(candidates,
utility_mode = "linking_cross_set_p_times_1_minus_p") {
cand <- tibble::as_tibble(candidates)
if (nrow(cand) == 0L) {
return(integer())
Expand All @@ -640,12 +641,21 @@ generate_stage_candidates_from_state <- function(state,
idx <- coverage_idx
}
}
# Linking ordering priority is predictive cross-set utility; candidate
# generation/filtering invariants remain upstream in the canonical pipeline.
utility <- if ("link_u" %in% names(cand)) {
as.double(cand$link_u[idx])
# Linking ordering priority is resolver-selected utility. If all values are
# non-finite, fall back deterministically to U0, then lexical tie-break.
utility_col <- .adaptive_resolve_selection_column(utility_mode)
utility <- if (!is.na(utility_col) && utility_col %in% names(cand)) {
as.double(cand[[utility_col]][idx])
} else {
as.double(cand$u0[idx])
rep_len(NA_real_, length(idx))
}
if (!any(is.finite(utility))) {
fallback <- if ("u0" %in% names(cand)) as.double(cand$u0[idx]) else rep_len(NA_real_, length(idx))
if (!any(is.finite(fallback))) {
return(idx[order(cand$i[idx], cand$j[idx])])
}
fallback[!is.finite(fallback)] <- -Inf
return(idx[order(-fallback, cand$i[idx], cand$j[idx])])
}
utility[!is.finite(utility)] <- -Inf
idx[order(-utility, cand$i[idx], cand$j[idx])]
Expand Down
17 changes: 10 additions & 7 deletions R/adaptive_run.R
Original file line number Diff line number Diff line change
Expand Up @@ -548,9 +548,10 @@
#' Within-set routing uses TrueSkill base utility
#' \deqn{U_0 = p_{ij}(1 - p_{ij})} where \eqn{p_{ij}} is the current TrueSkill
#' win probability for pair \eqn{\{i, j\}}.
#' In linking Phase B, pair choice remains TrueSkill-based and never uses BTL
#' posterior quantities. Model-implied predictive probabilities/utility are
#' logged for diagnostics only and do not affect selection.
#' In linking Phase B, eligible cross-set candidates are ranked by
#' \eqn{p_{hx}(1-p_{hx})} under the current linking transform and judge
#' parameters. Linking inference parameters are used for
#' inference/diagnostics/stopping, not as direct selection objectives.
#' When \code{judge_param_mode = "phase_specific"}, the first Phase B startup
#' step may use deterministic fallback from available within/shared judge
#' estimates if link-specific estimates are not yet available; once link-specific
Expand Down Expand Up @@ -597,7 +598,7 @@
#' `link_transform_escalation_is_one_way`,
#' `spoke_quantile_coverage_bins`,
#' `spoke_quantile_coverage_min_per_bin_per_refit`, `multi_spoke_mode`,
#' `min_cross_set_pairs_per_spoke_per_refit`, `cross_set_utility`,
#' `min_cross_set_pairs_per_spoke_per_refit`,
#' `phase_a_mode`, `phase_a_import_failure_policy`,
#' `phase_a_required_reliability_min`, `phase_a_compatible_model_ids`,
#' `phase_a_compatible_config_hashes`, `phase_a_artifacts`,
Expand Down Expand Up @@ -691,8 +692,10 @@ adaptive_rank_start <- function(items,
#' Pair selection does not use BTL posterior draws.
#' Within-set routing is TrueSkill-based with utility
#' \deqn{U_0 = p_{ij}(1 - p_{ij})}.
#' Linking Phase B cross-set routing is also TrueSkill-based; model-implied
#' predictive probabilities/utility are recorded for diagnostics only.
#' Linking Phase B routing ranks eligible cross-set candidates by
#' \eqn{p_{hx}(1-p_{hx})} under the current linking transform and judge
#' parameters. Linking inference parameters remain inference-only
#' (diagnostics and stopping) and are not direct pair-selection objectives.
#' When \code{judge_param_mode = "phase_specific"}, startup can use deterministic
#' fallback from within/shared judge estimates only until link-specific estimates
#' are expected, after which malformed link estimates abort.
Expand Down Expand Up @@ -758,7 +761,7 @@ adaptive_rank_start <- function(items,
#' `link_transform_escalation_is_one_way`,
#' `spoke_quantile_coverage_bins`,
#' `spoke_quantile_coverage_min_per_bin_per_refit`, `multi_spoke_mode`,
#' `min_cross_set_pairs_per_spoke_per_refit`, `cross_set_utility`,
#' `min_cross_set_pairs_per_spoke_per_refit`,
#' `phase_a_mode`, `phase_a_import_failure_policy`,
#' `phase_a_required_reliability_min`, `phase_a_compatible_model_ids`,
#' `phase_a_compatible_config_hashes`, `phase_a_artifacts`, and
Expand Down
91 changes: 88 additions & 3 deletions R/adaptive_select.R
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,35 @@ adaptive_defaults <- function(N) {
)
}

.adaptive_selection_mode_is_linking <- function(run_mode, is_cross_set = FALSE) {
as.character(run_mode %||% "within_set") %in% c("link_one_spoke", "link_multi_spoke") &&
isTRUE(is_cross_set)
}

.adaptive_selection_utility_mode <- function(run_mode, has_regularization = FALSE, is_cross_set = FALSE) {
if (.adaptive_selection_mode_is_linking(run_mode = run_mode, is_cross_set = is_cross_set)) {
return("linking_cross_set_p_times_1_minus_p")
}
if (isTRUE(has_regularization)) {
return("pairing_trueskill_u")
}
"pairing_trueskill_u0"
}

.adaptive_resolve_selection_column <- function(utility_mode) {
mode <- as.character(utility_mode %||% NA_character_)
if (identical(mode, "pairing_trueskill_u0")) {
return("u0")
}
if (identical(mode, "pairing_trueskill_u")) {
return("u")
}
if (identical(mode, "linking_cross_set_p_times_1_minus_p")) {
return("link_u")
}
NA_character_
}

.adaptive_local_priority_select <- function(cand, state, round, stage_committed_so_far, stage_quota, defaults) {
if (nrow(cand) == 0L) {
return(list(candidates = cand, mode = "standard"))
Expand Down Expand Up @@ -1143,13 +1172,46 @@ select_next_pair <- function(state, step_id = NULL, candidates = NULL) {
} else {
stage_local_priority_mode <- NA_character_
}
if (isTRUE(is_link_mode)) {
has_regularized_utility <- "u" %in% names(cand) &&
"u0" %in% names(cand) &&
any(
is.finite(as.double(cand$u)) &
is.finite(as.double(cand$u0)) &
abs(as.double(cand$u) - as.double(cand$u0)) > sqrt(.Machine$double.eps),
na.rm = TRUE
)
selected_utility_mode <- .adaptive_selection_utility_mode(
run_mode = controller$run_mode,
has_regularization = isTRUE(has_regularized_utility),
is_cross_set = isTRUE(is_link_mode) && isTRUE(link_phase_b)
)
if (isTRUE(is_link_mode) && isTRUE(link_phase_b)) {
# Linking mode keeps canonical candidate generation/filtering via
# TrueSkill and hard invariants; this call only applies the
# linking-specific final ordering priority.
order_idx <- .adaptive_linking_selection_order(cand)
order_idx <- .adaptive_linking_selection_order(
cand,
utility_mode = selected_utility_mode
)
} else {
order_idx <- order(-cand$u0, cand$i, cand$j)
utility_col <- .adaptive_resolve_selection_column(selected_utility_mode)
utility <- if (!is.na(utility_col) && utility_col %in% names(cand)) {
as.double(cand[[utility_col]])
} else {
rep_len(NA_real_, nrow(cand))
}
if (!any(is.finite(utility))) {
tie_utility <- if ("u0" %in% names(cand)) as.double(cand$u0) else rep_len(NA_real_, nrow(cand))
if (any(is.finite(tie_utility))) {
tie_utility[!is.finite(tie_utility)] <- -Inf
order_idx <- order(-tie_utility, cand$i, cand$j)
} else {
order_idx <- order(cand$i, cand$j)
}
} else {
utility[!is.finite(utility)] <- -Inf
order_idx <- order(-utility, cand$i, cand$j)
}
}
selected_pair <- cand[order_idx[[1L]], , drop = FALSE]
}
Expand Down Expand Up @@ -1263,10 +1325,32 @@ select_next_pair <- function(state, step_id = NULL, candidates = NULL) {
if (is.na(selected_spoke_id) && !is.na(selected_link_spoke_attempt)) {
selected_spoke_id <- as.integer(selected_link_spoke_attempt)
}
set_map <- stats::setNames(as.integer(state$items$set_id), as.character(state$items$item_id))
set_i_selected <- as.integer(set_map[[i_id]] %||% NA_integer_)
set_j_selected <- as.integer(set_map[[j_id]] %||% NA_integer_)
selected_is_cross_set <- !is.na(set_i_selected) && !is.na(set_j_selected) && set_i_selected != set_j_selected
if (isTRUE(selected_is_cross_set) && is.na(selected_spoke_id) && isTRUE(is_link_mode)) {
hub_id <- as.integer(link_controller$hub_id %||% 1L)
if (identical(set_i_selected, hub_id)) {
selected_spoke_id <- set_j_selected
} else if (identical(set_j_selected, hub_id)) {
selected_spoke_id <- set_i_selected
}
}
A_id <- as.character(order_vals[["A_id"]] %||% NA_character_)
B_id <- as.character(order_vals[["B_id"]] %||% NA_character_)
p_ij_ts <- trueskill_win_probability(A_id, B_id, state$trueskill_state)
p_ij <- as.double(p_ij_ts)
has_regularized_utility <- "u" %in% names(selected_pair) &&
"u0" %in% names(selected_pair) &&
is.finite(as.double(selected_pair$u[[1L]])) &&
is.finite(as.double(selected_pair$u0[[1L]])) &&
abs(as.double(selected_pair$u[[1L]]) - as.double(selected_pair$u0[[1L]])) > sqrt(.Machine$double.eps)
utility_mode <- .adaptive_selection_utility_mode(
run_mode = controller$run_mode,
has_regularization = isTRUE(has_regularized_utility),
is_cross_set = isTRUE(selected_is_cross_set)
)
if (isTRUE(is_link_mode) && !is.na(selected_spoke_id)) {
p_link_oriented <- .adaptive_link_predictive_prob_oriented(
state = state,
Expand Down Expand Up @@ -1330,6 +1414,7 @@ select_next_pair <- function(state, step_id = NULL, candidates = NULL) {
sigma_j = as.double(sigma_vals[[j_id]]),
p_ij = as.double(p_ij),
U0_ij = as.double(u0_ij),
utility_mode = as.character(utility_mode),
star_cap_rejects = as.integer(last_star_caps$rejects %||% 0L),
star_cap_reject_items = as.integer(last_star_caps$reject_items_count %||% 0L)
)
Expand Down
10 changes: 8 additions & 2 deletions R/adaptive_state.R
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
spoke_quantile_coverage_min_per_bin_per_refit = 1L,
multi_spoke_mode = "independent",
min_cross_set_pairs_per_spoke_per_refit = 5L,
cross_set_utility = "p_times_1_minus_p",
cross_set_utility = "linking_cross_set_p_times_1_minus_p",
phase_a_mode = "run",
phase_a_import_failure_policy = "fail_fast",
phase_a_required_reliability_min = 0.80,
Expand Down Expand Up @@ -328,7 +328,13 @@
1L,
Inf
)
out$cross_set_utility <- read_choice("cross_set_utility", "p_times_1_minus_p")
out$cross_set_utility <- read_choice(
"cross_set_utility",
c("linking_cross_set_p_times_1_minus_p", "p_times_1_minus_p")
)
if (identical(out$cross_set_utility, "p_times_1_minus_p")) {
out$cross_set_utility <- "linking_cross_set_p_times_1_minus_p"
}
out$phase_a_mode <- read_choice("phase_a_mode", c("run", "import", "mixed"))
out$phase_a_import_failure_policy <- read_choice(
"phase_a_import_failure_policy",
Expand Down
68 changes: 58 additions & 10 deletions R/adaptive_step.R
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ validate_judge_result <- function(result, A_id, B_id) {
idx_map <- state$item_index %||% stats::setNames(seq_along(state$item_ids), state$item_ids)
recent_deg <- .adaptive_recent_deg(history, state$item_ids, adaptive_defaults(length(state$item_ids))$W_cap)
defaults <- adaptive_defaults(length(state$item_ids))
controller <- .adaptive_controller_resolve(state)
run_mode <- as.character(controller$run_mode %||% "within_set")
set_i <- as.integer(state$items$set_id[[idx_map[[i_id]]]])
set_j <- as.integer(state$items$set_id[[idx_map[[j_id]]]])
is_cross_set <- !is.na(set_i) && !is.na(set_j) && set_i != set_j
utility_mode <- .adaptive_selection_utility_mode(
run_mode = run_mode,
has_regularization = FALSE,
is_cross_set = isTRUE(is_cross_set)
)

list(
i = as.integer(idx_map[[i_id]]),
Expand Down Expand Up @@ -149,6 +159,7 @@ validate_judge_result <- function(result, A_id, B_id) {
sigma_j = as.double(sigma_vals[[j_id]]),
p_ij = as.double(p_ij),
U0_ij = as.double(u0_ij),
utility_mode = as.character(utility_mode),
star_cap_rejects = 0L,
star_cap_reject_items = 0L
)
Expand Down Expand Up @@ -214,12 +225,34 @@ validate_judge_result <- function(result, A_id, B_id) {
"."
))
}
valid_utility_modes <- c(
"pairing_trueskill_u0",
"pairing_trueskill_u",
"linking_cross_set_p_times_1_minus_p"
)
utility_mode <- if ("utility_mode" %in% names(row)) {
as.character(row$utility_mode[[1L]] %||% NA_character_)
} else {
NA_character_
}
if (!is.na(utility_mode) && !utility_mode %in% valid_utility_modes) {
rlang::abort(
paste0(
"step_log append completeness failure: `utility_mode` must be one of: ",
paste(valid_utility_modes, collapse = ", "),
", or NA."
)
)
}
run_mode <- as.character(row$run_mode[[1L]] %||% "within_set")
is_link_run_mode <- run_mode %in% c("link_one_spoke", "link_multi_spoke")

is_cross <- row$is_cross_set[[1L]]
if (isTRUE(is_cross)) {
required_cross <- c(
"set_i", "set_j", "link_spoke_id", "run_mode", "posterior_win_prob_pre", "cross_set_utility_pre"
)
required_cross <- c("set_i", "set_j", "link_spoke_id", "run_mode", "posterior_win_prob_pre")
if (isTRUE(is_link_run_mode)) {
required_cross <- c(required_cross, "cross_set_utility_pre")
}
bad <- required_cross[vapply(required_cross, function(col) is.na(row[[col]][[1L]]), logical(1L))]
if (length(bad) > 0L) {
rlang::abort(paste0(
Expand All @@ -234,6 +267,14 @@ validate_judge_result <- function(result, A_id, B_id) {
"step_log append completeness failure for cross-set row: `link_stage` must be populated for stage-routed steps."
)
}
if (isTRUE(is_link_run_mode) && !identical(utility_mode, "linking_cross_set_p_times_1_minus_p")) {
rlang::abort(
paste0(
"step_log append completeness failure for cross-set row: ",
"`utility_mode` must be linking_cross_set_p_times_1_minus_p."
)
)
}
} else if (isFALSE(is_cross)) {
if (!is.na(row$link_spoke_id[[1L]])) {
rlang::abort(
Expand All @@ -247,7 +288,6 @@ validate_judge_result <- function(result, A_id, B_id) {
"posterior_win_prob_pre",
"link_transform_mode",
"cross_set_utility_pre",
"utility_mode",
"log_alpha_spoke_estimate_pre",
"log_alpha_spoke_sd_pre",
"hub_lock_mode",
Expand All @@ -261,6 +301,13 @@ validate_judge_result <- function(result, A_id, B_id) {
"."
))
}
if (isTRUE(is_link_run_mode) &&
!is.na(utility_mode) &&
!utility_mode %in% c("pairing_trueskill_u0", "pairing_trueskill_u")) {
rlang::abort(
"step_log append completeness failure: non-cross-set rows in linking runs must use pairing utility mode or NA."
)
}
}

invisible(TRUE)
Expand Down Expand Up @@ -376,7 +423,7 @@ run_one_step <- function(state, judge, ...) {
run_mode <- as.character(controller$run_mode %||% "within_set")
hub_id <- as.integer(controller$hub_id %||% 1L)
link_transform_mode <- as.character(controller$link_transform_mode %||% NA_character_)
utility_mode <- as.character(controller$cross_set_utility %||% NA_character_)
utility_mode <- as.character(selection$utility_mode %||% NA_character_)
hub_lock_mode <- as.character(controller$hub_lock_mode %||% NA_character_)
hub_lock_kappa <- as.double(controller$hub_lock_kappa %||% NA_real_)
set_i <- if (!is.na(selection$i)) {
Expand Down Expand Up @@ -418,7 +465,10 @@ run_one_step <- function(state, judge, ...) {
} else {
NA_character_
}
cross_set_utility_pre <- if (isTRUE(is_cross_set)) {
is_link_run_mode <- run_mode %in% c("link_one_spoke", "link_multi_spoke")
cross_set_utility_pre <- if (isTRUE(is_cross_set) &&
isTRUE(is_link_run_mode) &&
identical(utility_mode, "linking_cross_set_p_times_1_minus_p")) {
as.double(selection$U0_ij %||% NA_real_)
} else {
NA_real_
Expand All @@ -443,10 +493,8 @@ run_one_step <- function(state, judge, ...) {
} else {
NA_character_
}
utility_mode <- if (isTRUE(is_cross_set)) {
utility_mode
} else {
NA_character_
if (!is.character(utility_mode) || length(utility_mode) != 1L || is.na(utility_mode) || utility_mode == "") {
utility_mode <- NA_character_
}
log_alpha_spoke_estimate_pre <- if (isTRUE(is_cross_set)) {
as.double(spoke_stats$log_alpha_spoke_mean %||% NA_real_)
Expand Down
Loading