tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.ls -la /app/
total 35 drwxrws--T 3 h2tagent h2tagent 4096 May 28 19:31 . drwxr-xr-x 27 root root 4096 May 28 23:06 .. -rw-rw---- 1 h2tagent h2tagent 14067 May 28 19:31 analysis.R -rw-rw---- 1 h2tagent h2tagent 7752 May 28 19:31 dataset_manifest.json -rw-rw---- 1 h2tagent h2tagent 420 May 28 19:31 example_client_bias.csv drwxrws--- 4 h2tagent h2tagent 4096 May 28 19:31 outputs
cat /app/dataset_manifest.json
{
"manifest_version": "1.0",
"dataset": {
"name": "MNIST",
"source": "torchvision.datasets.MNIST",
"download_url": "http://yann.lecun.com/exdb/mnist/",
"train_size": 60000,
"test_size": 10000,
"n_classes": 10,
"per_class_train": 6000
},
"simulation": {
"n_clients": 100,
"n_byzantine": 10,
"clients_per_round": 20,
"n_rounds": 8,
"param_dim": 256,
"trimmed_mean": {
"space": "coordinate",
"trim_k_default": 2
},
"fedavg": {
"weighting": "by_num_samples"
},
"update_generation": {
"w_star": "rnorm(param_dim)",
"class_prototypes": "matrix(rnorm(n_classes * param_dim), nrow=n_classes, ncol=param_dim)",
"honest_update": "signal_scale*w_star + bias_strength*(bias_row %*% class_prototypes) + rnorm(param_dim, 0, honest_noise_sd)",
"honest_noise_sd": 0.08
}
},
"partition": {
"method": "dirichlet_per_class",
"alpha_default": 0.5,
"allocation": "largest_remainder_hamilton",
"min_client_samples": 20,
"entropy_log_base": "e"
},
"client_bias": {
"csv_schema": {
"client_id": "int",
"class_k_weight": "float (k=0..9; nonnegative; rows sum to 1)"
},
"required_columns": [
"client_id",
"class_0_weight",
"class_1_weight",
"class_2_weight",
"class_3_weight",
"class_4_weight",
"class_5_weight",
"class_6_weight",
"class_7_weight",
"class_8_weight",
"class_9_weight"
],
"bias_strength_default": 0.35,
"env_path_var": "FEDSIMG_CLIENT_BIAS_CSV",
"env_strength_var": "FEDSIMG_BIAS_STRENGTH"
},
"attack": {
"byzantine_selection": "fixed_lowest_client_ids",
"type": "sign_flip_plus_noise",
"noise_scale_default": 2.0,
"apply_timing": "before_aggregation",
"byzantine_update": "-u_clean + rnorm(param_dim, 0, noise_scale)"
},
"metrics": {
"accuracy_proxy": "sigmoid_cosine_similarity",
"accuracy_sigmoid_k_default": 7.5,
"accuracy_sigmoid_b_default": 2.5,
"round_noise_sd": 0.005
},
"rounding": {
"accuracy_decimals": 6,
"loss_decimals": 6,
"share_decimals": 6
},
"outputs": {
"metrics_csv": {
"path": "metrics_by_round.csv",
"columns": [
"round",
"method",
"accuracy",
"cosine_sim",
"update_norm",
"byzantine_selected",
"selected_clients"
],
"sort_by": [
"round",
"method"
]
},
"partition_matrix_csv": {
"path": "partition_matrix.csv",
"columns": [
"client_id",
"class_0_count",
"class_1_count",
"class_2_count",
"class_3_count",
"class_4_count",
"class_5_count",
"class_6_count",
"class_7_count",
"class_8_count",
"class_9_count"
],
"sort_by": [
"client_id"
],
"row_semantics": {
"rows": "one row per client_id for all clients",
"counts": "class_k_count is the number of training samples of class k assigned to the client"
}
},
"dirichlet_gammas_csv": {
"path": "dirichlet_gammas.csv",
"columns": [
"class",
"client_id",
"gamma_raw",
"p",
"allocated_count"
],
"sort_by": [
"class",
"client_id"
],
"row_semantics": {
"rows": "one row per (class, client_id)",
"gamma_raw": "raw Dirichlet gamma draw (positive)",
"p": "normalized probability gamma_raw/sum(gamma_raw) for that class",
"allocated_count": "Hamilton allocation of per_class_train samples for that class using p"
},
"numeric_precision": {
"gamma_raw": "full_precision",
"p": "full_precision",
"allocated_count": "integer"
}
},
"partition_csv": {
"path": "partition_summary.csv",
"columns": [
"client_id",
"is_byzantine",
"n_train",
"label_e
…[truncated]cat /app/analysis.R
#!/usr/bin/env Rscript
suppressPackageStartupMessages({
library(ggplot2)
library(gridExtra)
library(jsonlite)
})
`%||%` <- function(x, y) if (!is.null(x) && !is.na(x) && nzchar(as.character(x))) x else y
sigmoid <- function(x) 1.0 / (1.0 + exp(-x))
safe_dir_create <- function(path) {
if (!dir.exists(path)) dir.create(path, recursive = TRUE, showWarnings = FALSE)
}
read_bias_csv <- function(path, n_clients, n_classes) {
if (is.null(path) || !nzchar(path) || !file.exists(path)) {
return(NULL)
}
df <- read.csv(path, stringsAsFactors = FALSE)
req <- c("client_id", sprintf("class_%d_weight", 0:(n_classes - 1)))
if (!all(req %in% names(df))) {
stop("bias csv missing required columns")
}
df <- df[order(df$client_id), ]
if (nrow(df) != n_clients || any(df$client_id != 0:(n_clients - 1))) {
stop("bias csv must have exactly one row per client")
}
mat <- as.matrix(df[, req[-1], drop = FALSE])
storage.mode(mat) <- "double"
col_sums <- colSums(mat)
col_sums[col_sums <= 0] <- 1
sweep(mat, 2, col_sums, "/")
}
hamilton_alloc <- function(p, total) {
p <- as.numeric(p)
p <- p / sum(p)
raw <- p * total
alloc <- as.integer(round(raw))
gap <- as.integer(total - sum(alloc))
if (gap > 0) {
ord <- order(raw - alloc, decreasing = TRUE)
alloc[ord[seq_len(gap)]] <- alloc[ord[seq_len(gap)]] + 1L
} else if (gap < 0) {
ord <- order(raw - alloc, decreasing = FALSE)
take <- ord[seq_len(abs(gap))]
alloc[take] <- pmax(0L, alloc[take] - 1L)
}
as.integer(alloc)
}
repair_partition <- function(partition, min_client_samples) {
n_train <- rowSums(partition)
while (min(n_train) < min_client_samples) {
low <- which.min(n_train)
high <- which.max(n_train)
donor_class <- which.max(partition[high, ])
if (partition[high, donor_class] <= 0) break
partition[high, donor_class] <- partition[high, donor_class] - 1L
partition[low, donor_class] <- partition[low, donor_class] + 1L
n_train <- rowSums(partition)
}
partition
}
dirichlet_partition <- function(n_clients, n_classes, per_class_total, alpha, min_client_samples) {
part <- matrix(0L, nrow = n_clients, ncol = n_classes)
gammas <- matrix(0.0, nrow = n_clients, ncol = n_classes)
for (k in seq_len(n_classes)) {
g <- rgamma(n_clients, shape = alpha, rate = 1)
gammas[, k] <- g
part[, k] <- hamilton_alloc(g, per_class_total)
}
part <- repair_partition(part, min_client_samples)
list(partition = part, gammas = gammas)
}
shannon_entropy <- function(counts) {
s <- sum(counts)
if (s <= 0) return(0)
p <- counts / s
p <- p[p > 0]
-sum(p * log10(p))
}
row_trimmed_mean <- function(mat, trim_k) {
keep <- order(apply(mat, 1, function(x) sqrt(sum(x * x))))[(trim_k + 1):(nrow(mat) - trim_k)]
colMeans(mat[keep, , drop = FALSE])
}
run_sim <- function() {
task_variant <- Sys.getenv("TASK_VARIANT", "public")
seed <- as.integer(Sys.getenv("FEDSIMG_SEED", "0"))
set.seed(seed)
output_dir <- Sys.getenv("FEDSIMG_OUTPUT_DIR", "outputs")
manifest_path <- Sys.getenv("FEDSIMG_MANIFEST_PATH", "/app/dataset_manifest.json")
safe_dir_create(output_dir)
manifest <- fromJSON(manifest_path, simplifyVector = TRUE)
n_clients <- as.integer(manifest$simulation$n_clients)
n_byz <- as.integer(manifest$simulation$n_byzantine)
clients_per_round <- as.integer(manifest$simulation$clients_per_round)
n_rounds <- as.integer(manifest$simulation$n_rounds)
param_dim <- as.integer(manifest$simulation$param_dim)
n_classes <- as.integer(manifest$dataset$n_classes)
per_class <- as.integer(manifest$dataset$per_class_train)
alpha <- as.numeric(manifest$partition$alpha_default)
noise_scale <- as.numeric(manifest$attack$noise_scale_default)
signal_scale <- as.numeric(Sys.getenv("FEDSIMG_SIGNAL_SCALE", "0.12"))
trim_k <- as.integer(manifest$simulation$trimmed_mean$trim_k_default)
bias_strength <- as.numeric(manifest$client_bias$bias_strength_default)
bias_csv <- Sys.getenv(m
…[truncated]cat /app/example_client_bias.csv
client_id,class_0_weight,class_1_weight,class_2_weight,class_3_weight,class_4_weight,class_5_weight,class_6_weight,class_7_weight,class_8_weight,class_9_weight 0,0.28,0.10,0.07,0.06,0.06,0.06,0.07,0.08,0.12,0.10 1,0.05,0.06,0.09,0.14,0.15,0.14,0.12,0.10,0.08,0.07 2,0.08,0.08,0.08,0.08,0.08,0.08,0.12,0.14,0.14,0.12 3,0.04,0.05,0.06,0.07,0.10,0.13,0.15,0.15,0.14,0.11 4,0.16,0.14,0.12,0.10,0.09,0.08,0.07,0.06,0.10,0.08
ls /app/outputs/
hidden public
ls /app/outputs/public /app/outputs/hidden 2>/dev/null
/app/outputs/hidden: /app/outputs/public:
which Rscript && Rscript --version
/usr/bin/Rscript Rscript (R) version 4.3.3 (2024-02-29)
Rscript -e 'library(ggplot2); library(gridExtra); library(jsonlite); cat("OK\n")'OK
command: create
path: /app/analysis.R
file_text: #!/usr/bin/env Rscript
suppressPackageStartupMessages({
library(ggplot2)
library(gridExtra)
library(jsonlite)
})
`%||%` <- function(x, y) if (!is.null(x) && !is.na(x) && nzchar(as.character(x))) x else y
sigmoid <- function(x) 1.0 / (1.0 + exp(-x))
safe_dir_create <- function(path) {
if (!dir.exists(path)) dir.create(path, recursive = TRUE, showWarnings = FALSE)
}
# Read the client bias CSV and return an n_clients x n_classes matrix with
# rows summing to 1. The CSV may provide bias only for a subset of clients;
# clients that are not present fall back to a caller-supplied default row.
read_bias_csv <- function(path, n_clients, n_classes, default_rows) {
if (is.null(path) || !nzchar(path) || !file.exists(path)) {
return(default_rows)
}
df <- read.csv(path, stringsAsFactors = FALSE)
req <- c("client_id", sprintf("class_%d_weight", 0:(n_classes - 1)))
if (!all(req %in% names(df))) {
stop("bias csv missing required columns")
}
mat <- default_rows
for (i in seq_len(nrow(df))) {
cid <- as.integer(df$client_id[i])
if (is.na(cid) || cid < 0 || cid >= n_clients) next
row <- as.numeric(df[i, req[-1]])
row[!is.finite(row) | row < 0] <- 0
s <- sum(row)
if (s > 0) {
mat[cid + 1, ] <- row / s
}
}
mat
}
# Hamilton (largest-remainder) allocation of a fixed total to proportions p.
hamilton_alloc <- function(p, total) {
p <- as.numeric(p)
s <- sum(p)
if (!is.finite(s) || s <= 0) {
p <- rep(1 / length(p), length(p))
} else {
p <- p / s
}
raw <- p * total
floors <- as.integer(floor(raw))
remainder <- as.integer(total - sum(floors))
alloc <- floors
if (remainder > 0) {
fracs <- raw - floors
# Break ties deterministically by order (lowest index wins)
ord <- order(-fracs, seq_along(fracs))
alloc[ord[seq_len(remainder)]] <- alloc[ord[seq_len(remainder)]] + 1L
} else if (remainder < 0) {
# Extremely unlikely with pure floor, but guard just in case.
fracs <- raw - floors
ord <- order(fracs, seq_along(fracs))
take <- ord[seq_len(abs(remainder))]
alloc[take] <- pmax(0L, alloc[take] - 1L)
}
as.integer(alloc)
}
# Repair a partition so every client has at least min_client_samples total
# samples by moving one sample from the richest client (on its dominant class)
# to the poorest. Guarded against infinite loops.
repair_partition <- function(partition, min_client_samples) {
n_train <- rowSums(partition)
max_iter <- as.integer(sum(partition)) + 1L
iter <- 0L
while (min(n_train) < min_client_samples && iter < max_iter) {
low <- which.min(n_train)
high <- which.max(n_train)
if (low == high) break
donor_class <- which.max(partition[high, ])
if (partition[high, donor_class] <= 0) break
partition[high, donor_class] <- partition[high, donor_class] - 1L
partition[low, donor_class] <- partition[low, donor_class] + 1L
n_train <- rowSums(partition)
iter <- iter + 1L
}
partition
}
dirichlet_partition <- function(n_clients, n_classes, per_class_total, alpha,
min_client_samples) {
part <- matrix(0L, nrow = n_clients, ncol = n_classes)
gammas <- matrix(0.0, nrow = n_clients, ncol = n_classes)
for (k in seq_len(n_classes)) {
g <- rgamma(n_clients, shape = alpha, rate = 1)
gammas[, k] <- g
part[, k] <- hamilton_alloc(g, per_class_total)
}
part <- repair_partition(part, min_client_samples)
list(partition = part, gammas = gammas)
}
# Shannon entropy with natural log (manifest: entropy_log_base = "e")
shannon_entropy <- function(counts) {
s <- sum(counts)
if (s <= 0) return(0)
p <- counts / s
p <- p[p > 0]
-sum(p * log(p))
}
# Coordinate-wise trimmed mean: for each coordinate, drop the top trim_k and
# bottom trim_k values, then average the middle.
coord_trimmed_mean <- function(mat, trim_k) {
n <- nrow(mat)
p <- ncol(mat)
if (trim_k <= 0) return(colMeans(mat))
…[truncated]Existing file was successfully overwritten at /app/analysis.R
cd /app && FEDSIMG_SEED=42 FEDSIMG_OUTPUT_DIR=/tmp/test_out TASK_VARIANT=public Rscript analysis.R 2>&1 | tail -30
<system>Tool ran without output or errors</system>
ls -la /tmp/test_out/
total 1955 drwxr-xr-x 2 h2tagent h2tagent 4096 May 28 23:09 . drwxrwxrwt 9 root root 4096 May 28 23:09 .. -rw-r--r-- 1 h2tagent h2tagent 999 May 28 23:09 analysis_summary.json -rw-r--r-- 1 h2tagent h2tagent 12615 May 28 23:09 byzantine_audit.csv -rw-r--r-- 1 h2tagent h2tagent 731103 May 28 23:09 client_updates.csv -rw-r--r-- 1 h2tagent h2tagent 1114836 May 28 23:09 client_updates_clean.csv -rw-r--r-- 1 h2tagent h2tagent 78682 May 28 23:09 diagnostics.png -rw-r--r-- 1 h2tagent h2tagent 45882 May 28 23:09 dirichlet_gammas.csv -rw-r--r-- 1 h2tagent h2tagent 1737 May 28 23:09 metrics_by_round.csv -rw-r--r-- 1 h2tagent h2tagent 3363 May 28 23:09 partition_matrix.csv -rw-r--r-- 1 h2tagent h2tagent 2952 May 28 23:09 partition_summary.csv
cat /tmp/test_out/metrics_by_round.csv
"round","method","accuracy","cosine_sim","update_norm","byzantine_selected","selected_clients" 0,"fedavg",0.561649,0.367463,3.856119,1,"7 10 14 19 31 40 51 55 56 57 60 61 62 65 67 68 75 80 90 94" 0,"trimmed_mean",0.943391,0.718464,2.560468,1,"7 10 14 19 31 40 51 55 56 57 60 61 62 65 67 68 75 80 90 94" 1,"fedavg",0.76337,0.489985,3.457422,2,"1 6 13 15 19 30 32 35 36 38 42 47 53 55 64 72 74 78 90 95" 1,"trimmed_mean",0.945528,0.726745,2.605763,2,"1 6 13 15 19 30 32 35 36 38 42 47 53 55 64 72 74 78 90 95" 2,"fedavg",0.849542,0.569368,2.986522,1,"3 11 14 17 18 21 25 33 40 42 47 50 60 63 64 75 76 80 92 96" 2,"trimmed_mean",0.951108,0.727138,2.663023,1,"3 11 14 17 18 21 25 33 40 42 47 50 60 63 64 75 76 80 92 96" 3,"fedavg",0.895671,0.609765,2.853295,2,"0 9 14 19 24 31 34 36 41 48 54 55 58 59 70 76 88 89 93 97" 3,"trimmed_mean",0.958057,0.732249,2.540736,2,"0 9 14 19 24 31 34 36 41 48 54 55 58 59 70 76 88 89 93 97" 4,"fedavg",0.919429,0.653675,3.13641,2,"4 5 10 25 31 32 35 37 38 41 44 46 48 56 59 77 80 86 92 98" 4,"trimmed_mean",0.948156,0.734967,2.560612,2,"4 5 10 25 31 32 35 37 38 41 44 46 48 56 59 77 80 86 92 98" 5,"fedavg",0.92844,0.666931,3.36302,3,"0 5 9 12 20 21 36 39 45 46 54 62 66 69 70 71 88 90 95 96" 5,"trimmed_mean",0.951714,0.737128,2.592263,3,"0 5 9 12 20 21 36 39 45 46 54 62 66 69 70 71 88 90 95 96" 6,"fedavg",0.931128,0.671234,3.43138,2,"3 5 12 13 15 21 39 41 43 44 49 50 60 64 66 75 82 83 86 95" 6,"trimmed_mean",0.95481,0.737645,2.582335,2,"3 5 12 13 15 21 39 41 43 44 49 50 60 64 66 75 82 83 86 95" 7,"fedavg",0.922776,0.666912,3.42007,2,"2 3 11 12 24 33 35 37 38 48 49 51 59 61 63 75 81 89 94 96" 7,"trimmed_mean",0.959518,0.737369,2.51438,2,"2 3 11 12 24 33 35 37 38 48 49 51 59 61 63 75 81 89 94 96"
cat /tmp/test_out/analysis_summary.json
{
"task_variant": "public",
"seed": 42,
"env": {
"FEDSIMG_ALPHA": 0.5,
"FEDSIMG_NOISE_SCALE": 2,
"FEDSIMG_SIGNAL_SCALE": 0.12,
"FEDSIMG_TRIM_K": 2,
"FEDSIMG_BIAS_STRENGTH": 0.35,
"FEDSIMG_CLIENT_BIAS_CSV": ""
},
"simulation_meta": {
"n_clients": 100,
"n_byzantine": 10,
"clients_per_round": 20,
"n_rounds": 8,
"param_dim": 256,
"trim_k": 2
},
"byzantine_client_ids": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
"final_metrics": {
"trimmed_mean_accuracy": 0.959518,
"fedavg_accuracy": 0.922776,
"delta": 0.0367419999999999
},
"provenance": {
"manifest_md5": "475adf00a65314160823670bdaef093c",
"selection_histogram": [2, 1, 1, 3, 1, 3, 1, 1, 0, 2, 2, 2, 3, 2, 3, 2, 0, 1, 1, 3, 1, 3, 0, 0, 2, 2, 0, 0, 0, 0, 1, 3, 2, 2, 1, 3, 3, 2, 3, 2, 2, 3, 2, 1, 2, 1, 2, 2, 3, 2, 2, 2, 0, 1, 2, 3, 2, 1, 1, 3, 3, 2, 2, 2, 3, 1, 2, 1, 1, 1, 2, 1, 1, 0, 1, 4, 2, 1, 1, 0, 3, 1, 1, 1, 0, 0, 2, 0, 2, 2, 3, 0, 2, 1, 2, 3, 3, 1, 1, 0]
}
}head -5 /tmp/test_out/byzantine_audit.csv echo "---" head -5 /tmp/test_out/dirichlet_gammas.csv echo "---" head -3 /tmp/test_out/partition_matrix.csv echo "---" head -5 /tmp/test_out/partition_summary.csv echo "---" head -3 /tmp/test_out/client_updates.csv echo "---" head -3 /tmp/test_out/client_updates_clean.csv
"round","client_id","is_byzantine","is_selected","client_weight","update_norm" 0,0,1,0,0,0 0,1,1,0,0,0 0,2,1,0,0,0 0,3,1,0,0,0 --- "class","client_id","gamma_raw","p","allocated_count" 0,0,0.760516834810734,0.0165288490248016,99 0,1,0.293697522769724,0.00638313550814045,38 0,2,1.86634352361331,0.0405625607721065,243 0,3,1.48173928793115,0.0322036855244973,193 --- "client_id","class_0_count","class_1_count","class_2_count","class_3_count","class_4_count","class_5_count","class_6_count","class_7_count","class_8_count","class_9_count" 0,99,20,180,32,26,72,46,229,3,1 1,38,2,29,0,5,3,311,291,71,6 --- "client_id","is_byzantine","n_train","label_entropy","top_label","top_label_share" 0,1,708,1.792905,7,0.323446 1,1,756,1.339652,6,0.411376 2,1,759,1.427729,1,0.466403 3,1,640,1.563247,3,0.335938 --- "round","client_id","coord","update_value" 0,7,0,0.832969 0,7,1,-0.845807 --- "round","client_id","coord","u_clean","u_submitted" 0,7,0,-0.015345,0.832969 0,7,1,0.354431,-0.845807
cd /app && rm -rf /tmp/test_out2 && FEDSIMG_SEED=7 FEDSIMG_OUTPUT_DIR=/tmp/test_out2 TASK_VARIANT=public \ FEDSIMG_CLIENT_BIAS_CSV=/app/example_client_bias.csv \ FEDSIMG_ALPHA=0.3 FEDSIMG_NOISE_SCALE=1.5 FEDSIMG_TRIM_K=3 FEDSIMG_BIAS_STRENGTH=0.5 FEDSIMG_SIGNAL_SCALE=0.1 \ Rscript analysis.R 2>&1 | tail -20 ls /tmp/test_out2/
analysis_summary.json byzantine_audit.csv client_updates.csv client_updates_clean.csv diagnostics.png dirichlet_gammas.csv metrics_by_round.csv partition_matrix.csv partition_summary.csv
cat /tmp/test_out2/analysis_summary.json | head -20 echo "===" # Check a byzantine client's (0) records vs an honest client's in client_updates_clean grep -E "^0,5,0," /tmp/test_out2/client_updates_clean.csv | head -2 echo "--- (client 5 is byzantine, should show u_clean != u_submitted)" grep -E "^0,[1-9][0-9],0," /tmp/test_out2/client_updates_clean.csv | head -2 echo "--- (honest, should show u_clean == u_submitted)"
{
"task_variant": "public",
"seed": 7,
"env": {
"FEDSIMG_ALPHA": 0.3,
"FEDSIMG_NOISE_SCALE": 1.5,
"FEDSIMG_SIGNAL_SCALE": 0.1,
"FEDSIMG_TRIM_K": 3,
"FEDSIMG_BIAS_STRENGTH": 0.5,
"FEDSIMG_CLIENT_BIAS_CSV": "/app/example_client_bias.csv"
},
"simulation_meta": {
"n_clients": 100,
"n_byzantine": 10,
"clients_per_round": 20,
"n_rounds": 8,
"param_dim": 256,
"trim_k": 3
},
"byzantine_client_ids": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
===
--- (client 5 is byzantine, should show u_clean != u_submitted)
0,10,0,-0.327158,-0.327158
0,21,0,-0.512556,-0.512556
--- (honest, should show u_clean == u_submitted)head -5 /tmp/test_out2/metrics_by_round.csv
echo "==="
# Find a byzantine client selected in round 0
awk -F',' 'NR>1 && $1==0 && $3<10 {print}' /tmp/test_out2/client_updates_clean.csv | head -3
echo "--- above: byzantine in round 0 (u_clean != u_submitted)"
# Compare to honest
awk -F',' 'NR>1 && $1==0 && $3>=10 {print}' /tmp/test_out2/client_updates_clean.csv | head -3
echo "--- above: honest in round 0 (u_clean == u_submitted)""round","method","accuracy","cosine_sim","update_norm","byzantine_selected","selected_clients" 0,"fedavg",0.479179,0.322551,3.2264,2,"8 9 10 21 22 23 26 27 29 33 35 36 41 47 49 66 69 70 76 96" 0,"trimmed_mean",0.751471,0.484073,3.054842,2,"8 9 10 21 22 23 26 27 29 33 35 36 41 47 49 66 69 70 76 96" 1,"fedavg",0.706716,0.447656,2.997073,2,"1 4 11 12 17 22 31 32 41 52 60 66 69 72 77 78 88 90 91 95" 1,"trimmed_mean",0.773242,0.498397,3.056763,2,"1 4 11 12 17 22 31 32 41 52 60 66 69 72 77 78 88 90 91 95" === 0,8,0,-0.534133,0.471903 0,8,1,0.017922,0.648684 0,8,2,-0.036637,0.911161 --- above: byzantine in round 0 (u_clean != u_submitted) 0,8,10,0.154523,0.573488 0,8,11,0.198402,3.091102 0,8,12,-0.104553,-2.747684 --- above: honest in round 0 (u_clean == u_submitted)
awk -F',' 'NR>1 && $1==0 && $2>=10 && $2<=15 && $3<3 {print}' /tmp/test_out2/client_updates_clean.csv
echo "--- above: honest clients 10-15 coord 0-2 (u_clean == u_submitted)"0,10,0,-0.327158,-0.327158 0,10,1,0.373374,0.373374 0,10,2,-0.110929,-0.110929 --- above: honest clients 10-15 coord 0-2 (u_clean == u_submitted)
# Check byzantine_audit: client_weight should be 0 for unselected; for selected, n_train(c)/sum(n_train of selected)
# Check round 0, selected: 8 9 10 21 22 23 26 27 29 33 35 36 41 47 49 66 69 70 76 96
# Get their n_train from partition_summary and compute expected weights
python3 <<'PY'
import csv
sel = [8,9,10,21,22,23,26,27,29,33,35,36,41,47,49,66,69,70,76,96]
nt = {}
with open('/tmp/test_out2/partition_summary.csv') as f:
r = csv.DictReader(f)
for row in r:
nt[int(row['client_id'])] = int(row['n_train'])
total = sum(nt[c] for c in sel)
# Check audit
with open('/tmp/test_out2/byzantine_audit.csv') as f:
r = csv.DictReader(f)
for row in r:
if int(row['round']) == 0 and int(row['client_id']) in (8, 10, 23, 1):
cid = int(row['client_id'])
expected = nt[cid]/total if cid in sel else 0
print(f"cid={cid} audit_weight={row['client_weight']} expected={round(expected,6)} n_train={nt[cid]}")
PYcid=1 audit_weight=0 expected=0 n_train=232 cid=8 audit_weight=0.075805 expected=0.075805 n_train=866 cid=10 audit_weight=0.051821 expected=0.051821 n_train=592 cid=23 audit_weight=0.024597 expected=0.024597 n_train=281
python3 <<'PY'
import csv, math, json
# Check entropy base is natural log
with open('/tmp/test_out2/partition_summary.csv') as f:
r = csv.DictReader(f)
rows = list(r)
with open('/tmp/test_out2/partition_matrix.csv') as f:
r = csv.DictReader(f)
pm = list(r)
# recompute entropy for client 0 using natural log
counts = [int(pm[0][f'class_{k}_count']) for k in range(10)]
s = sum(counts)
probs = [c/s for c in counts if c>0]
ent = -sum(p*math.log(p) for p in probs) # natural log
print("client 0 entropy natural:", round(ent,6), "reported:", rows[0]['label_entropy'])
# Check gamma sum check
with open('/tmp/test_out2/dirichlet_gammas.csv') as f:
r = csv.DictReader(f)
gammas = list(r)
# sum of allocated_count per class == per_class_train after repair? Not necessarily after repair.
# But sum of p per class should be 1.
from collections import defaultdict
psum = defaultdict(float)
for row in gammas:
psum[int(row['class'])] += float(row['p'])
for k in range(10):
assert abs(psum[k] - 1) < 1e-9, f"class {k} p sum={psum[k]}"
print("p sums OK")
# Verify final_metrics matches
with open('/tmp/test_out2/metrics_by_round.csv') as f:
r = csv.DictReader(f)
metrics = list(r)
final_round = max(int(m['round']) for m in metrics)
tm = [m for m in metrics if m['method']=='trimmed_mean' and int(m['round'])==final_round][0]['accuracy']
fed = [m for m in metrics if m['method']=='fedavg' and int(m['round'])==final_round][0]['accuracy']
with open('/tmp/test_out2/analysis_summary.json') as f:
summary = json.load(f)
print("CSV:", tm, fed, "JSON:", summary['final_metrics'])
# Verify selection_histogram matches sum of is_selected in byzantine_audit
with open('/tmp/test_out2/byzantine_audit.csv') as f:
r = csv.DictReader(f)
audit = list(r)
hist_from_audit = [0]*100
for row in audit:
if int(row['is_selected'])==1:
hist_from_audit[int(row['client_id'])] += 1
assert hist_from_audit == summary['provenance']['selection_histogram']
print("selection_histogram matches audit selections:", True)
print("n_byz_ids:", summary['byzantine_client_ids'])
PYclient 0 entropy natural: 1.432365 reported: 1.432365
p sums OK
CSV: 0.784928 0.755237 JSON: {'trimmed_mean_accuracy': 0.784928, 'fedavg_accuracy': 0.755237, 'delta': 0.0296909999999999}
selection_histogram matches audit selections: True
n_byz_ids: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]Rscript -e 'print(seq.int(0, -1)); print(as.integer(seq.int(0, 0-1)))'
[1] 0 -1 [1] 0 -1
Rscript -e 'print(seq_len(0) - 1L); print(seq_len(10) - 1L)'
integer(0) [1] 0 1 2 3 4 5 6 7 8 9
command: str_replace path: /app/analysis.R old_str: # Byzantine client ids: fixed lowest (manifest: fixed_lowest_client_ids). byz_ids <- as.integer(seq.int(0, n_byz - 1)) new_str: # Byzantine client ids: fixed lowest (manifest: fixed_lowest_client_ids). byz_ids <- if (n_byz > 0L) as.integer(seq_len(n_byz) - 1L) else integer(0)
The file /app/analysis.R has been edited successfully.
Rscript -e '
set.seed(1)
m <- matrix(rnorm(50), 10, 5)
cat("coord trimmed mean with k=2:\n")
keep <- (2+1):(10-2)
sorted <- apply(m, 2, sort)
print(colMeans(sorted[keep, ,drop=FALSE]))
cat("sanity (colMean keep):\n")
'coord trimmed mean with k=2: [1] 0.10741988 0.44794635 0.07037633 0.09010854 0.18132951 sanity (colMean keep):
cd /app && rm -rf /tmp/test_out3 && FEDSIMG_SEED=123 FEDSIMG_OUTPUT_DIR=/tmp/test_out3 TASK_VARIANT=hidden \ FEDSIMG_CLIENT_BIAS_CSV=/app/example_client_bias.csv \ FEDSIMG_ALPHA=0.5 FEDSIMG_NOISE_SCALE=2.0 FEDSIMG_TRIM_K=2 FEDSIMG_BIAS_STRENGTH=0.35 FEDSIMG_SIGNAL_SCALE=0.15 \ Rscript analysis.R 2>&1 | tail -20 ls -la /tmp/test_out3/ echo "---" cat /tmp/test_out3/analysis_summary.json
total 1975
drwxr-xr-x 2 h2tagent h2tagent 4096 May 28 23:10 .
drwxrwxrwt 11 root root 4096 May 28 23:10 ..
-rw-r--r-- 1 h2tagent h2tagent 1018 May 28 23:10 analysis_summary.json
-rw-r--r-- 1 h2tagent h2tagent 12591 May 28 23:10 byzantine_audit.csv
-rw-r--r-- 1 h2tagent h2tagent 733688 May 28 23:10 client_updates.csv
-rw-r--r-- 1 h2tagent h2tagent 1118880 May 28 23:10 client_updates_clean.csv
-rw-r--r-- 1 h2tagent h2tagent 92100 May 28 23:10 diagnostics.png
-rw-r--r-- 1 h2tagent h2tagent 45895 May 28 23:10 dirichlet_gammas.csv
-rw-r--r-- 1 h2tagent h2tagent 1752 May 28 23:10 metrics_by_round.csv
-rw-r--r-- 1 h2tagent h2tagent 3346 May 28 23:10 partition_matrix.csv
-rw-r--r-- 1 h2tagent h2tagent 2946 May 28 23:10 partition_summary.csv
---
{
"task_variant": "hidden",
"seed": 123,
"env": {
"FEDSIMG_ALPHA": 0.5,
"FEDSIMG_NOISE_SCALE": 2,
"FEDSIMG_SIGNAL_SCALE": 0.15,
"FEDSIMG_TRIM_K": 2,
"FEDSIMG_BIAS_STRENGTH": 0.35,
"FEDSIMG_CLIENT_BIAS_CSV": "/app/example_client_bias.csv"
},
"simulation_meta": {
"n_clients": 100,
"n_byzantine": 10,
"clients_per_round": 20,
"n_rounds": 8,
"param_dim": 256,
"trim_k": 2
},
"byzantine_client_ids": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
"final_metrics": {
"trimmed_mean_accuracy": 0.984409,
"fedavg_accuracy": 0.971422,
"delta": 0.012987
},
"provenance": {
"manifest_md5": "475adf00a65314160823670bdaef093c",
"selection_histogram": [2, 0, 0, 0, 1, 2, 2, 0, 2, 1, 1, 2, 2, 2, 3, 3, 1, 3, 2, 2, 3, 2, 1, 4, 0, 2, 1, 3, 1, 1, 2, 3, 1, 1, 2, 1, 2, 0, 1, 2, 1, 2, 1, 2, 1, 1, 0, 1, 4, 2, 2, 3, 1, 2, 1, 0, 3, 1, 3, 2, 2, 3, 1, 2, 2, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 1, 2, 3, 2, 5, 0, 2, 1, 0, 4, 1, 0, 2, 0, 1, 2, 1, 2, 1, 1]
}
}# Verify selection histograms sum == n_rounds * clients_per_round = 8*20=160
python3 -c "
import json
s = json.load(open('/tmp/test_out3/analysis_summary.json'))
h = s['provenance']['selection_histogram']
print('sum =', sum(h), 'len=', len(h))
assert sum(h) == 8*20
assert len(h) == 100
print('OK')
"sum = 160 len= 100 OK
# Verify partition sums equal per_class_train (6000)
python3 -c "
import csv
with open('/tmp/test_out3/partition_matrix.csv') as f:
r = csv.DictReader(f)
rows = list(r)
sums = [0]*10
for row in rows:
for k in range(10):
sums[k] += int(row[f'class_{k}_count'])
print('per-class sums:', sums)
print('total:', sum(sums), 'expected: 60000')
# Verify dirichlet_gammas sort order
with open('/tmp/test_out3/dirichlet_gammas.csv') as f:
r = csv.DictReader(f)
rows = list(r)
prev = (-1, -1)
for row in rows:
cur = (int(row['class']), int(row['client_id']))
assert cur > prev, f'sort order violation: {prev} -> {cur}'
prev = cur
print('dirichlet sort OK')
# Verify client_updates order
with open('/tmp/test_out3/client_updates.csv') as f:
r = csv.DictReader(f)
rows = list(r)
print(f'client_updates rows: {len(rows)}')
assert len(rows) == 8*20*256, 'expected 40960'
prev = (-1, -1, -1)
for row in rows:
cur = (int(row['round']), int(row['client_id']), int(row['coord']))
assert cur > prev, f'sort order violation: {prev} -> {cur}'
prev = cur
print('client_updates sort OK')
"per-class sums: [6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000] total: 60000 expected: 60000 dirichlet sort OK client_updates rows: 40960 client_updates sort OK
python3 <<'PY'
import json, csv, struct
with open('/tmp/test_out3/metrics_by_round.csv') as f:
r = csv.DictReader(f)
metrics = list(r)
with open('/tmp/test_out3/analysis_summary.json') as f:
s = json.load(f)
final_round = max(int(m['round']) for m in metrics)
tm = float([m for m in metrics if m['method']=='trimmed_mean' and int(m['round'])==final_round][0]['accuracy'])
fed = float([m for m in metrics if m['method']=='fedavg' and int(m['round'])==final_round][0]['accuracy'])
jtm = s['final_metrics']['trimmed_mean_accuracy']
jfed = s['final_metrics']['fedavg_accuracy']
jdelta = s['final_metrics']['delta']
# Bitwise equality check
def bits(x): return struct.pack('>d', x).hex()
print(f"CSV tm ({bits(tm)}) vs JSON ({bits(jtm)}): equal={bits(tm)==bits(jtm)}")
print(f"CSV fed ({bits(fed)}) vs JSON ({bits(jfed)}): equal={bits(fed)==bits(jfed)}")
print(f"tm - fed = {tm-fed}; JSON delta = {jdelta}; diff={abs((tm-fed)-jdelta)}")
PYCSV tm (3fef80474d9c6b05) vs JSON (3fef80474d9c6b05): equal=True CSV fed (3fef15e39713ad5c) vs JSON (3fef15e39713ad5c): equal=True tm - fed = 0.01298699999999997; JSON delta = 0.012987; diff=2.949029909160572e-17
Rscript -e '
library(jsonlite)
tm <- 0.984409; fed <- 0.971422
d <- tm - fed
cat(sprintf("R diff = %.17g\n", d))
# How does jsonlite serialize it?
cat(toJSON(list(delta=d), auto_unbox=TRUE, digits=NA), "\n")
cat(toJSON(list(delta=d), auto_unbox=TRUE, digits=17), "\n")
cat(toJSON(list(delta=d), auto_unbox=TRUE), "\n")'R diff = 0.012986999999999971
{"delta":0.012987}
{"delta":0.012986999999999971}
{"delta":0.013}python3 -c "
# Does 0.012987 round-trip to 0.012986999999999971?
x = 0.012987
print(repr(x))
print(f'{x:.17g}')
# In Python, 0.012987 parses to a double. Its repr should match.
"0.012987 0.012987
python3 <<'PY'
import struct
x = 0.012987
print('bits(0.012987):', struct.pack('>d', x).hex())
print('bits(0.012986999999999971):', struct.pack('>d', 0.012986999999999971).hex())
# Difference
print('tm - fed in Python:', struct.pack('>d', 0.984409 - 0.971422).hex())
PYbits(0.012987): 3f8a98eda22f6a51 bits(0.012986999999999971): 3f8a98eda22f6a40 tm - fed in Python: 3f8a98eda22f6a40
Rscript -e '
x <- as.numeric("0.012987")
cat(sprintf("R parse 0.012987: %.17g\n", x))
y <- 0.984409 - 0.971422
cat(sprintf("R compute: %.17g\n", y))
cat("equal?", identical(x, y), "\n")
# In jsonlite, default digits=NA uses "Ryu" algorithm for shortest round-trip
# So the output 0.012987 should round-trip to y? Lets check
z <- fromJSON(jsonlite::toJSON(list(v=y), auto_unbox=TRUE, digits=NA))
cat(sprintf("Round-trip: %.17g\n", z$v))
cat("Identical after round-trip?", identical(z$v, y), "\n")'R parse 0.012987: 0.012987 R compute: 0.012986999999999971 equal? FALSE [stderr] Error in fromJSON(jsonlite::toJSON(list(v = y), auto_unbox = TRUE, digits = NA)) : could not find function "fromJSON" Execution halted
Rscript -e '
library(jsonlite)
y <- 0.984409 - 0.971422
j <- toJSON(list(v=y), auto_unbox=TRUE, digits=NA)
cat("JSON:", j, "\n")
z <- fromJSON(j)
cat(sprintf("Round-trip: %.17g\n", z$v))
cat("Identical after round-trip?", identical(z$v, y), "\n")'JSON: {"v":0.012987}
Round-trip: 0.012987
Identical after round-trip? FALSERscript -e '
library(jsonlite)
y <- 0.984409 - 0.971422
j <- toJSON(list(v=y), auto_unbox=TRUE, digits=17)
cat("JSON:", j, "\n")
z <- fromJSON(j)
cat("Identical after round-trip?", identical(z$v, y), "\n")
# What about regular CSV-matching values like 0.984409?
a <- 0.984409
j2 <- toJSON(list(v=a), auto_unbox=TRUE, digits=NA)
cat("NA digits: ", j2, "\n")
z2 <- fromJSON(j2)
cat("Identical?", identical(z2$v, a), "\n")
j3 <- toJSON(list(v=a), auto_unbox=TRUE, digits=17)
cat("17 digits: ", j3, "\n")
z3 <- fromJSON(j3)
cat("Identical?", identical(z3$v, a), "\n")'JSON: {"v":0.012986999999999971}
Identical after round-trip? TRUE
NA digits: {"v":0.984409}
Identical? TRUE
17 digits: {"v":0.98440899999999998}
Identical? TRUE# Verify byzantine aggregated submitted update has very different norm than clean
python3 <<'PY'
import csv
with open('/tmp/test_out3/byzantine_audit.csv') as f:
r = csv.DictReader(f)
audit = list(r)
# Look at round 0 byzantine vs honest update norms
sel_byz = [(row['client_id'], float(row['update_norm'])) for row in audit if int(row['round'])==0 and int(row['is_selected'])==1 and int(row['is_byzantine'])==1]
sel_hon = [(row['client_id'], float(row['update_norm'])) for row in audit if int(row['round'])==0 and int(row['is_selected'])==1 and int(row['is_byzantine'])==0]
print("byzantine selected (round 0) update norms:", sel_byz)
print("honest (first 3) selected update norms:", sel_hon[:3])
PYbyzantine selected (round 0) update norms: []
honest (first 3) selected update norms: [('12', 4.354366), ('14', 3.568376), ('15', 4.057133)]# Check round with byzantine selected
python3 <<'PY'
import csv
with open('/tmp/test_out3/byzantine_audit.csv') as f:
audit = list(csv.DictReader(f))
for r in range(8):
sel_byz = [(row['client_id'], float(row['update_norm'])) for row in audit if int(row['round'])==r and int(row['is_selected'])==1 and int(row['is_byzantine'])==1]
sel_hon = [(row['client_id'], float(row['update_norm'])) for row in audit if int(row['round'])==r and int(row['is_selected'])==1 and int(row['is_byzantine'])==0]
if sel_byz:
print(f"round {r}: byzantine norms={sel_byz[:3]} avg_hon={sum(x[1] for x in sel_hon)/len(sel_hon):.3f}")
PYround 1: byzantine norms=[('5', 31.348827), ('6', 32.542067)] avg_hon=3.994
round 2: byzantine norms=[('5', 30.746988)] avg_hon=3.940
round 4: byzantine norms=[('0', 34.056468), ('6', 30.16024), ('9', 32.280688)] avg_hon=4.015
round 5: byzantine norms=[('8', 30.882859)] avg_hon=3.903
round 6: byzantine norms=[('4', 34.179947)] avg_hon=3.991
round 7: byzantine norms=[('0', 31.332547), ('8', 29.053241)] avg_hon=3.988cd /app && rm -rf /app/outputs/run_check && FEDSIMG_SEED=42 FEDSIMG_OUTPUT_DIR=/app/outputs/run_check TASK_VARIANT=public \ Rscript analysis.R 2>&1 ls /app/outputs/run_check/ echo "---" head -3 /app/outputs/run_check/metrics_by_round.csv
analysis_summary.json byzantine_audit.csv client_updates.csv client_updates_clean.csv diagnostics.png dirichlet_gammas.csv metrics_by_round.csv partition_matrix.csv partition_summary.csv --- "round","method","accuracy","cosine_sim","update_norm","byzantine_selected","selected_clients" 0,"fedavg",0.561649,0.367463,3.856119,1,"7 10 14 19 31 40 51 55 56 57 60 61 62 65 67 68 75 80 90 94" 0,"trimmed_mean",0.943391,0.718464,2.560468,1,"7 10 14 19 31 40 51 55 56 57 60 61 62 65 67 68 75 80 90 94"
rm -rf /app/outputs/run_check
<system>Tool ran without output or errors</system>
python3 -c "
from PIL import Image
img = Image.open('/tmp/test_out3/diagnostics.png')
print('PNG size:', img.size, 'mode:', img.mode)
# Manifest says width=1400, height=900
"PNG size: (1400, 900) mode: RGB
cd /app && rm -rf /tmp/t_a /tmp/t_b # Same seed both times -> outputs should be identical FEDSIMG_SEED=11 FEDSIMG_OUTPUT_DIR=/tmp/t_a Rscript analysis.R FEDSIMG_SEED=11 FEDSIMG_OUTPUT_DIR=/tmp/t_b Rscript analysis.R for f in metrics_by_round.csv byzantine_audit.csv partition_summary.csv dirichlet_gammas.csv client_updates.csv; do diff -q /tmp/t_a/$f /tmp/t_b/$f done # Different seed -> outputs should differ rm -rf /tmp/t_c FEDSIMG_SEED=12 FEDSIMG_OUTPUT_DIR=/tmp/t_c Rscript analysis.R echo "--- different seed, should differ ---" diff -q /tmp/t_a/metrics_by_round.csv /tmp/t_c/metrics_by_round.csv
--- different seed, should differ --- Files /tmp/t_a/metrics_by_round.csv and /tmp/t_c/metrics_by_round.csv differ
python3 <<'PY'
import csv
from collections import defaultdict
with open('/tmp/t_a/metrics_by_round.csv') as f:
metrics = list(csv.DictReader(f))
with open('/tmp/t_a/client_updates.csv') as f:
updates = list(csv.DictReader(f))
# selected_clients from metrics
sel_from_metrics = {}
for m in metrics:
if m['method'] == 'fedavg': # both rows have same selected_clients
sel_from_metrics[int(m['round'])] = sorted(int(x) for x in m['selected_clients'].split())
# client_ids from updates
sel_from_updates = defaultdict(set)
for u in updates:
sel_from_updates[int(u['round'])].add(int(u['client_id']))
for r in sel_from_metrics:
assert sorted(sel_from_updates[r]) == sel_from_metrics[r], f'round {r} mismatch'
print("client_updates match selected_clients across all rounds: OK")
# Also check that each selected client has exactly 256 coords (param_dim)
from collections import Counter
ccount = Counter((int(u['round']), int(u['client_id'])) for u in updates)
assert all(c == 256 for c in ccount.values()), "each client-round should have 256 coords"
print("each (round, client) has exactly 256 coords: OK")
PYclient_updates match selected_clients across all rounds: OK each (round, client) has exactly 256 coords: OK
wc -l /app/analysis.R head -20 /app/analysis.R
480 /app/analysis.R
#!/usr/bin/env Rscript
suppressPackageStartupMessages({
library(ggplot2)
library(gridExtra)
library(jsonlite)
})
`%||%` <- function(x, y) if (!is.null(x) && !is.na(x) && nzchar(as.character(x))) x else y
sigmoid <- function(x) 1.0 / (1.0 + exp(-x))
safe_dir_create <- function(path) {
if (!dir.exists(path)) dir.create(path, recursive = TRUE, showWarnings = FALSE)
}
# Read the client bias CSV and return an n_clients x n_classes matrix with
# rows summing to 1. The CSV may provide bias only for a subset of clients;
# clients that are not present fall back to a caller-supplied default row.
read_bias_csv <- function(path, n_clients, n_classes, default_rows) {rm -rf /tmp/test_out /tmp/test_out2 /tmp/test_out3 /tmp/t_a /tmp/t_b /tmp/t_c # Final verification - run once more with default envs from scratch cd /app && rm -rf /tmp/final && FEDSIMG_SEED=0 FEDSIMG_OUTPUT_DIR=/tmp/final TASK_VARIANT=public Rscript analysis.R && ls /tmp/final
analysis_summary.json byzantine_audit.csv client_updates.csv client_updates_clean.csv diagnostics.png dirichlet_gammas.csv metrics_by_round.csv partition_matrix.csv partition_summary.csv
No file changes, the agent left the workspace untouched.
# SyncVals normalized verifier summary
# task: fedavg-federated-noniid-mnist
# attempt: 5 status: completed raw_score: 1.0
# reward rule: binary: score 1.0 == 1.0
# => reward = 1.0 (PASS)
#
# Note: these tasks score via an external hidden grader; no pytest/verifier
# block is embedded in the agent transcript. The block below is the last
# sizable tool-output captured in the run (supporting evidence).
----------------------------------------------------------------------
480 /app/analysis.R
#!/usr/bin/env Rscript
suppressPackageStartupMessages({
library(ggplot2)
library(gridExtra)
library(jsonlite)
})
`%||%` <- function(x, y) if (!is.null(x) && !is.na(x) && nzchar(as.character(x))) x else y
sigmoid <- function(x) 1.0 / (1.0 + exp(-x))
safe_dir_create <- function(path) {
if (!dir.exists(path)) dir.create(path, recursive = TRUE, showWarnings = FALSE)
}
# Read the client bias CSV and return an n_clients x n_classes matrix with
# rows summing to 1. The CSV may provide bias only for a subset of clients;
# clients that are not present fall back to a caller-supplied default row.
read_bias_csv <- function(path, n_clients, n_classes, default_rows) {Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_6fe147f2c5b64c3d. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_6fe147f2c5b64c3d · verifier authoritative; classifier explanatory.