Skip to content

Commit 270970a

Browse files
committed
fix: handle Stan tuple and complex types
1 parent c0e7c35 commit 270970a

7 files changed

Lines changed: 694 additions & 29 deletions

File tree

R/args.R

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,37 +1096,77 @@ process_init.draws <- function(init, num_procs, model_variables = NULL,
10961096
method ="simple_no_replace")
10971097
}
10981098
draws_rvar = posterior::as_draws_rvars(draws)
1099-
variable_names <- variable_names[variable_names %in% names(draws_rvar)]
1100-
draws_rvar <- posterior::subset_draws(draws_rvar, variable = variable_names)
1099+
1100+
# Separate tuple and non-tuple parameters. Tuple parameters use leaf names
1101+
# in draws (e.g., "b_tuple:1:1") rather than the Stan-level name ("b_tuple"),
1102+
# so they need special handling via build_tuple_init_value().
1103+
is_tuple <- if (!is.null(model_variables)) {
1104+
vapply(variable_names, function(nm) {
1105+
is_tuple_type(model_variables$parameters[[nm]])
1106+
}, logical(1))
1107+
} else {
1108+
rep(FALSE, length(variable_names))
1109+
}
1110+
tuple_names <- variable_names[is_tuple]
1111+
scalar_names <- variable_names[!is_tuple]
1112+
1113+
# Filter non-tuple names to those present in draws
1114+
scalar_names <- scalar_names[scalar_names %in% names(draws_rvar)]
1115+
1116+
# For tuple names, check that their leaf draws exist
1117+
rvar_names <- names(draws_rvar)
1118+
tuple_names <- tuple_names[vapply(tuple_names, function(nm) {
1119+
any(startsWith(rvar_names, paste0(nm, ":")))
1120+
}, logical(1))]
1121+
1122+
all_names <- c(scalar_names, tuple_names)
1123+
1124+
if (length(scalar_names) > 0) {
1125+
draws_rvar <- posterior::subset_draws(
1126+
draws_rvar,
1127+
variable = expand_stan_params_to_leaves(all_names, rvar_names)
1128+
)
1129+
}
1130+
11011131
inits = lapply(1:num_procs, function(draw_iter) {
1102-
init_i = lapply(variable_names, function(var_name) {
1103-
x = .remove_leftmost_dim(posterior::draws_of(
1104-
posterior::subset_draws(draws_rvar[[var_name]], draw=draw_iter)))
1132+
bad_names <- character(0)
1133+
1134+
# Extract non-tuple parameters
1135+
init_i = lapply(scalar_names, function(var_name) {
1136+
x = .extract_draw_value(var_name, draws_rvar, draw_iter)
1137+
if (any(is.infinite(x)) || any(is.na(x))) {
1138+
bad_names[[length(bad_names) + 1L]] <<- var_name
1139+
}
11051140
if (model_variables$parameters[[var_name]]$dimensions == 0) {
11061141
return(as.double(x))
11071142
} else {
11081143
return(x)
11091144
}
11101145
})
1111-
bad_names = unlist(lapply(variable_names, function(var_name) {
1112-
x = drop(posterior::draws_of(drop(
1113-
posterior::subset_draws(draws_rvar[[var_name]], draw=draw_iter))))
1114-
if (any(is.infinite(x)) || any(is.na(x))) {
1115-
return(var_name)
1146+
names(init_i) <- scalar_names
1147+
1148+
# Extract tuple parameters (build_tuple_init_value also validates)
1149+
for (var_name in tuple_names) {
1150+
tuple_result <- build_tuple_init_value(
1151+
var_name, model_variables$parameters[[var_name]],
1152+
draws_rvar, draw_iter
1153+
)
1154+
init_i[[var_name]] <- tuple_result$value
1155+
if (length(tuple_result$bad_leaves) > 0) {
1156+
bad_names <- c(bad_names, var_name)
11161157
}
1117-
return("")
1118-
}))
1119-
any_na_or_inf = bad_names != ""
1120-
if (any(any_na_or_inf)) {
1121-
err_msg = paste0(paste(bad_names[any_na_or_inf], collapse = ", "), " contains NA or Inf values!")
1122-
if (length(any_na_or_inf) > 1) {
1158+
}
1159+
1160+
if (length(bad_names) > 0) {
1161+
err_msg = paste0(paste(bad_names, collapse = ", "), " contains NA or Inf values!")
1162+
if (length(bad_names) > 1) {
11231163
err_msg = paste0("Variables: ", err_msg)
11241164
} else {
11251165
err_msg = paste0("Variable: ", err_msg)
11261166
}
11271167
stop(err_msg)
11281168
}
1129-
names(init_i) = variable_names
1169+
11301170
return(init_i)
11311171
})
11321172
return(process_init(inits, num_procs, model_variables, warn_partial))
@@ -1245,7 +1285,7 @@ validate_fit_init = function(init, model_variables) {
12451285
# Convert from data.table to data.frame
12461286
if (all(init$return_codes() == 1)) {
12471287
stop("We are unable to create initial values from a model with no samples. Please check the results of the model used for inits before continuing.")
1248-
} else if (!is.null(model_variables) &&!any(names(model_variables$parameters) %in% init$metadata()$stan_variables)) {
1288+
} else if (!is.null(model_variables) && !any(stan_param_has_leaf(names(model_variables$parameters), init$metadata()$stan_variables))) {
12491289
stop("None of the names of the parameters for the model used for initial values match the names of parameters from the model currently running.")
12501290
}
12511291
}

R/csv.R

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -923,11 +923,36 @@ check_csv_metadata_matches <- function(csv_metadata) {
923923
}
924924

925925
# convert names like beta.1.1 to beta[1,1]
926+
# also handles complex suffixes (.real/.imag) and tuple separators (:)
926927
repair_variable_names <- function(names) {
928+
# 1. Detect and strip .real/.imag suffix before dot conversion
929+
complex_suffix <- ifelse(
930+
grepl("\\.real$", names), ",real",
931+
ifelse(grepl("\\.imag$", names), ",imag", "")
932+
)
933+
names <- sub("\\.(real|imag)$", "", names)
934+
935+
# 2. Standard dot-to-bracket conversion (remaining dots are numeric indices)
927936
names <- sub("\\.", "[", names)
928937
names <- gsub("\\.", ",", names)
929-
names[grep("\\[", names)] <-
930-
paste0(names[grep("\\[", names)], "]")
938+
has_bracket <- grepl("\\[", names)
939+
names[has_bracket] <- paste0(names[has_bracket], "]")
940+
941+
# 3. Re-attach complex suffix
942+
has_complex <- nzchar(complex_suffix)
943+
has_both <- has_complex & has_bracket
944+
has_complex_only <- has_complex & !has_bracket
945+
# Had numeric indices: insert complex suffix before closing ]
946+
names[has_both] <- paste0(
947+
sub("\\]$", "", names[has_both]),
948+
complex_suffix[has_both], "]"
949+
)
950+
# No numeric indices: wrap in brackets
951+
names[has_complex_only] <- paste0(
952+
names[has_complex_only], "[",
953+
sub("^,", "", complex_suffix[has_complex_only]), "]"
954+
)
955+
931956
names
932957
}
933958

@@ -994,8 +1019,25 @@ variable_dims <- function(variable_names = NULL) {
9941019
var_indices <- var_names[grep(pattern, var_names)]
9951020
var_indices <- gsub(pattern, "", var_indices)
9961021
if (length(var_indices)) {
997-
var_indices <- strsplit(var_indices[length(var_indices)], ",")[[1]]
998-
dims[[var]] <- as.numeric(var_indices)
1022+
# Split the last index entry by comma to determine number of dimensions
1023+
last_indices <- strsplit(var_indices[length(var_indices)], ",")[[1]]
1024+
ndims <- length(last_indices)
1025+
dim_sizes <- integer(ndims)
1026+
for (d in seq_len(ndims)) {
1027+
num_idx <- suppressWarnings(as.integer(last_indices[d]))
1028+
if (!is.na(num_idx)) {
1029+
# Numeric index: the maximum value is the dimension size
1030+
dim_sizes[d] <- num_idx
1031+
} else {
1032+
# Non-numeric index (e.g., "real"/"imag" for complex, or
1033+
# tuple indices like "1:2"): count unique values across all
1034+
# entries for this dimension position
1035+
all_indices <- strsplit(var_indices, ",")
1036+
unique_vals <- unique(vapply(all_indices, `[`, character(1), d))
1037+
dim_sizes[d] <- length(unique_vals)
1038+
}
1039+
}
1040+
dims[[var]] <- dim_sizes
9991041
} else {
10001042
dims[[var]] <- 1
10011043
}

R/data.R

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ write_stan_json <- function(data, file, always_decimal = FALSE) {
9292
} else if (is.data.frame(var)) {
9393
var <- data.matrix(var)
9494
} else if (is.list(var)) {
95-
var <- list_to_array(var, var_name)
95+
if (is_tuple_list(var)) {
96+
var <- prepare_tuple_for_json(var)
97+
} else {
98+
var <- list_to_array(var, var_name)
99+
}
96100
}
97101
data[[var_name]] <- var
98102
}
@@ -110,6 +114,38 @@ write_stan_json <- function(data, file, always_decimal = FALSE) {
110114
}
111115

112116

117+
# Detect whether a list represents a Stan tuple value.
118+
# Tuple lists are named lists with string-integer keys ("1", "2", ...)
119+
# corresponding to the tuple element positions.
120+
is_tuple_list <- function(x) {
121+
nms <- names(x)
122+
if (is.null(nms) || length(nms) == 0) {
123+
return(FALSE)
124+
}
125+
expected <- as.character(seq_along(x))
126+
identical(nms, expected)
127+
}
128+
129+
# Recursively prepare a tuple value for JSON serialization.
130+
# Processes sub-elements: nested tuple lists are recursed into,
131+
# array-style lists (unnamed, homogeneous) are converted via list_to_array,
132+
# and numeric/logical values are left as-is.
133+
prepare_tuple_for_json <- function(x) {
134+
for (i in seq_along(x)) {
135+
val <- x[[i]]
136+
if (is.list(val)) {
137+
if (is_tuple_list(val)) {
138+
x[[i]] <- prepare_tuple_for_json(val)
139+
} else {
140+
x[[i]] <- list_to_array(val)
141+
}
142+
} else if (is.logical(val)) {
143+
mode(x[[i]]) <- "integer"
144+
}
145+
}
146+
x
147+
}
148+
113149
list_to_array <- function(x, name = NULL) {
114150
list_length <- length(x)
115151
if (list_length == 0) {

R/fit.R

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,12 @@ unconstrain_variables <- function(variables) {
499499
model_variables <- self$runset$args$model_variables
500500

501501
# If zero-length parameters are present, they will be listed in model_variables
502-
# but not in metadata()$variables
503-
nonzero_length_params <- names(model_variables$parameters) %in% model_par_names
502+
# but not in metadata()$variables. For tuple parameters, model_variables uses
503+
# the Stan-level name (e.g., "b_tuple") while model_par_names uses leaf names
504+
# with ":" separators (e.g., "b_tuple:1:1"), so we use prefix matching.
505+
nonzero_length_params <- stan_param_has_leaf(
506+
names(model_variables$parameters), model_par_names
507+
)
504508
model_par_names <- names(model_variables$parameters[nonzero_length_params])
505509

506510
model_pars_not_prov <- which(!(model_par_names %in% prov_par_names))
@@ -589,14 +593,23 @@ unconstrain_draws <- function(files = NULL, draws = NULL,
589593
model_variables <- self$runset$args$model_variables
590594

591595
# If zero-length parameters are present, they will be listed in model_variables
592-
# but not in metadata()$variables
593-
nonzero_length_params <- names(model_variables$parameters) %in% model_par_names
596+
# but not in metadata()$variables. For tuple parameters, model_variables uses
597+
# the Stan-level name (e.g., "b_tuple") while model_par_names uses leaf names
598+
# with ":" separators (e.g., "b_tuple:1:1"), so we use prefix matching.
599+
nonzero_length_params <- stan_param_has_leaf(
600+
names(model_variables$parameters), model_par_names
601+
)
594602

595603
# Remove zero-length parameters from model_variables, otherwise process_init
596604
# warns about missing inputs
597605
pars <- names(model_variables$parameters[nonzero_length_params])
598606

599-
draws <- posterior::subset_draws(draws, variable = pars)
607+
# For subset_draws, we need to use the leaf-level names from stan_variables
608+
# (e.g., "b_tuple:1:1") rather than Stan-level names (e.g., "b_tuple"),
609+
# because posterior doesn't recognize Stan-level tuple names.
610+
pars_for_draws <- expand_stan_params_to_leaves(pars, model_par_names)
611+
612+
draws <- posterior::subset_draws(draws, variable = pars_for_draws)
600613
unconstrained <- private$model_methods_env_$unconstrain_draws(private$model_methods_env_$model_ptr_, draws)
601614
uncon_names <- private$model_methods_env_$unconstrained_param_names(private$model_methods_env_$model_ptr_, FALSE, FALSE)
602615
names(unconstrained) <- repair_variable_names(uncon_names)

R/utils.R

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,94 @@ initialize_model_pointer <- function(env, datafile_path, seed = 0) {
867867
invisible(NULL)
868868
}
869869

870+
# Check if Stan-level parameter names (which may include tuple names like
871+
# "b_tuple") have a match among leaf-level variable names (which use ":"
872+
# to separate tuple elements, e.g., "b_tuple:1:1", "b_tuple:1:2").
873+
# A Stan-level name matches if it appears directly in leaf_names, or if
874+
# any leaf name starts with "<name>:" (tuple expansion).
875+
stan_param_has_leaf <- function(stan_names, leaf_names) {
876+
vapply(stan_names, function(nm) {
877+
nm %in% leaf_names || any(startsWith(leaf_names, paste0(nm, ":")))
878+
}, logical(1), USE.NAMES = FALSE)
879+
}
880+
881+
# Check if a parameter's type info represents a tuple.
882+
# Tuples have $type as a list; non-tuples have $type as a string.
883+
is_tuple_type <- function(var_info) {
884+
is.list(var_info$type)
885+
}
886+
887+
# Reconstruct a tuple init value as a nested named list from flat leaf draws.
888+
# Also validates that no leaf values contain NA or Inf.
889+
#
890+
# @param path The accumulated `:` path (e.g., "b_tuple", "b_tuple:1")
891+
# @param var_info The type info at this level (from model_variables)
892+
# @param draws_rvar The draws_rvars object containing leaf entries
893+
# @param draw_iter Which draw iteration to extract
894+
# @return A list with two elements:
895+
# - `value`: nested named list suitable for CmdStan JSON
896+
# - `bad_leaves`: character vector of leaf names with NA/Inf values
897+
build_tuple_init_value <- function(path, var_info, draws_rvar, draw_iter) {
898+
components <- var_info$type
899+
result <- vector("list", length(components))
900+
names(result) <- as.character(seq_along(components))
901+
bad_leaves <- character(0)
902+
for (i in seq_along(components)) {
903+
child_path <- paste0(path, ":", i)
904+
child_info <- components[[i]]
905+
if (is_tuple_type(child_info)) {
906+
child <- build_tuple_init_value(
907+
child_path, child_info, draws_rvar, draw_iter
908+
)
909+
result[[i]] <- child$value
910+
bad_leaves <- c(bad_leaves, child$bad_leaves)
911+
} else {
912+
x <- .extract_draw_value(child_path, draws_rvar, draw_iter)
913+
if (any(is.infinite(x)) || any(is.na(x))) {
914+
bad_leaves <- c(bad_leaves, child_path)
915+
}
916+
if (child_info$dimensions == 0) {
917+
result[[i]] <- as.double(x)
918+
} else {
919+
result[[i]] <- x
920+
}
921+
}
922+
}
923+
list(value = result, bad_leaves = bad_leaves)
924+
}
925+
926+
# Extract a single draw value from draws_rvar for a given variable name.
927+
# Handles the subset → draws_of → remove_leftmost_dim pipeline.
928+
.extract_draw_value <- function(var_name, draws_rvar, draw_iter) {
929+
.remove_leftmost_dim(posterior::draws_of(
930+
posterior::subset_draws(draws_rvar[[var_name]], draw = draw_iter)
931+
))
932+
}
933+
934+
# Expand Stan-level parameter names to their leaf-level equivalents in
935+
# stan_variables. Non-tuple names pass through unchanged. Tuple names
936+
# (e.g., "b_tuple") are expanded to all matching leaf names
937+
# (e.g., "b_tuple:1:1", "b_tuple:1:2", "b_tuple:2").
938+
expand_stan_params_to_leaves <- function(stan_params, leaf_names) {
939+
result <- character(0)
940+
for (param in stan_params) {
941+
if (param %in% leaf_names) {
942+
result <- c(result, param)
943+
} else {
944+
# Find leaf-level names for this tuple parameter
945+
prefix <- paste0(param, ":")
946+
leaves <- leaf_names[startsWith(leaf_names, prefix)]
947+
if (length(leaves) > 0) {
948+
result <- c(result, leaves)
949+
} else {
950+
# No match found, include as-is (will be caught by subset_draws)
951+
result <- c(result, param)
952+
}
953+
}
954+
}
955+
result
956+
}
957+
870958
create_skeleton <- function(param_metadata, model_variables,
871959
transformed_parameters, generated_quantities) {
872960
target_params <- names(model_variables$parameters)
@@ -878,7 +966,25 @@ create_skeleton <- function(param_metadata, model_variables,
878966
target_params <- c(target_params,
879967
names(model_variables$generated_quantities))
880968
}
881-
lapply(param_metadata[target_params], function(par_dims) {
969+
# Expand target_params to match param_metadata leaf names.
970+
# For tuple parameters, the Stan-level name (e.g., "b_tuple") maps to
971+
# multiple leaf entries in param_metadata (e.g., "b_tuple.1.1",
972+
# "b_tuple.1.2", "b_tuple.2"). We expand by matching the prefix.
973+
meta_names <- names(param_metadata)
974+
expanded_params <- character(0)
975+
for (param in target_params) {
976+
if (param %in% meta_names) {
977+
expanded_params <- c(expanded_params, param)
978+
} else {
979+
# Find leaf entries with this prefix (tuple expansion)
980+
prefix <- paste0(param, ".")
981+
leaves <- meta_names[startsWith(meta_names, prefix)]
982+
if (length(leaves) > 0) {
983+
expanded_params <- c(expanded_params, leaves)
984+
}
985+
}
986+
}
987+
lapply(param_metadata[expanded_params], function(par_dims) {
882988
if ((length(par_dims) == 0)) {
883989
array(0, dim = 1)
884990
} else {

0 commit comments

Comments
 (0)