Log fleet_k_q48/fleet_conserved; K holds exactly, 775/775 ticks (FABRIC-3.md §XVIII)
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

doe_log.c's per-heartbeat-tick CSV gains two columns: fleet_k_q48
(vm_physics_fleet_heat_sum() over ALL live VMs -- the genuine fleet-wide
conservation invariant K, not reconstructable from the 3 named-Tripod-
member heat columns already logged, which omit every identity VM's own
heat) and fleet_conserved (vm_physics_conserved() as 0/1). Requested
explicitly after the first heartbeat-telemetry analysis pass
(analysis-20260912/) omitted K entirely.

Kernel rebuilt on all three architectures, full 3x9x3 campaign rerun
(results-20260912-with-k/). K = 1.0000000000 (Q48.16 raw 65536) on every
one of 775 heartbeat-tick observations, sd(K) = 0, 100% fleet_conserved,
across amd64/aarch64/riscv64, nine identities, three replicates -- zero
deviation. Also a free regression check on both recent Stadium fixes
(§XVI/§XVII): neither disturbed the reservoir-transfer accounting K
depends on.

Found and fixed a tooling wrinkle along the way: fleet_conserved, being
the CSV row's very last field with nothing after it to bound a regex
match, can have a resumed trial digit merge into it with zero separator
on the wire -- combine.py now derives it from fleet_k_q48 directly (same
epsilon vm_physics_conserved() uses) instead of trusting the raw field.
fleet_k_q48 itself is unaffected either way.

Full analysis, discussion, and light/dark SVG->PDF figures written up as
a proper LaTeX report (report-20260912/report/std79_doe_report.pdf),
following experiments/bare_metal/analysis/report/bare_metal_doe_report.tex's
established style -- supersedes analysis-20260912/'s markdown-only first
pass as the primary deliverable for this dataset (kept, not discarded).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo
This commit is contained in:
Robert Allan James
2026-09-12 13:25:09 -04:00
co-authored by Claude Sonnet 5
parent 6573a6d1d5
commit 66ba21adb4
88 changed files with 82210 additions and 788 deletions
@@ -0,0 +1,171 @@
#!/usr/bin/env Rscript
# analysis.R -- std79-doe 3x9 factorial analysis, K-conservation centerpiece.
# Run from this directory (contains combined.csv). Produces charts/*.svg
# (light+dark pairs, matching experiments/bare_metal/analysis's own
# convention) and tables/*.csv for the LaTeX report in report/.
suppressMessages({
library(dplyr)
library(tidyr)
library(ggplot2)
library(svglite)
})
dir.create("charts", showWarnings = FALSE)
dir.create("tables", showWarnings = FALSE)
df <- read.csv("combined.csv", stringsAsFactors = FALSE)
id_levels <- c("zuse", "rajames", "00", "01", "02", "03", "04", "05", "06")
arch_levels <- c("amd64", "aarch64", "riscv64")
df$id_label <- factor(df$id_label, levels = id_levels)
df$arch <- factor(df$arch, levels = arch_levels)
df <- df %>%
arrange(arch, run_id, tick_number) %>%
group_by(arch, run_id) %>%
mutate(apic_delta = apic_ticks - lag(apic_ticks)) %>%
ungroup()
# ── shared theme, matches experiments/bare_metal/analysis/analyse_bare_metal.R ──
theme_light_report <- function() {
theme_minimal(base_size = 11) %+replace%
theme(
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey88"),
plot.title = element_text(face = "bold", size = 12),
axis.text.x = element_text(angle = 45, hjust = 1)
)
}
theme_dark_report <- function() {
theme_minimal(base_size = 11) %+replace%
theme(
plot.background = element_rect(fill = "#0d0d0d", colour = NA),
panel.background = element_rect(fill = "#0d0d0d", colour = NA),
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "#1a1a2e"),
text = element_text(colour = "#aaaaaa"),
axis.text = element_text(colour = "#aaaaaa"),
plot.title = element_text(face = "bold", size = 12, colour = "white"),
axis.text.x = element_text(angle = 45, hjust = 1, colour = "#aaaaaa"),
legend.text = element_text(colour = "#aaaaaa"),
legend.background = element_rect(fill = "#0d0d0d")
)
}
save_svg <- function(p, name, w = 8, h = 6) {
ggsave(file.path("charts", paste0(name, ".svg")), p, width = w, height = h, device = svglite)
}
# ── K / conservation centerpiece: fleet_k across every tick, every arch ──
cat(sprintf("fleet_k range: [%.10f, %.10f], distinct values: %d\n",
min(df$fleet_k), max(df$fleet_k), length(unique(df$fleet_k))))
cat(sprintf("fleet_conserved: %d/%d rows TRUE\n", sum(df$fleet_conserved == 1), nrow(df)))
make_k_plot <- function(dark = FALSE) {
bg <- if (dark) "#0d0d0d" else "white"
line_col <- if (dark) "#00e5ff" else "#1f77b4"
ref_col <- if (dark) "#888888" else "grey50"
p <- ggplot(df, aes(x = tick_number, y = fleet_k, colour = arch)) +
geom_hline(yintercept = 1.0, linetype = "dashed", colour = ref_col, linewidth = 0.4) +
geom_point(size = 0.5, alpha = 0.6) +
facet_wrap(~arch, ncol = 1, scales = "free_x") +
scale_y_continuous(limits = c(0.9, 1.1)) +
labs(title = "Fleet conservation invariant K over the whole campaign",
subtitle = "K = sum(execution_heat_q48) over all live VMs; dashed line = Q48_ONE (perfect conservation)",
x = "Heartbeat tick number", y = "K") +
theme(legend.position = "none")
if (dark) p <- p + theme_dark_report() + theme(legend.position = "none")
else p <- p + theme_light_report() + theme(legend.position = "none")
p
}
save_svg(make_k_plot(FALSE), "fleet_k_light", w = 9, h = 8)
save_svg(make_k_plot(TRUE), "fleet_k_dark", w = 9, h = 8)
# ── per-cell (arch x id_label) summary + ANOVA for every other metric ──
metrics <- c(
"hot_word_count", "avg_word_heat", "window_width", "actual_window_size",
"jitter_ns", "apic_delta", "time_trust", "variance", "vm_call_depth_max",
"hera_heat", "hermes_heat", "artemis_heat"
)
metric_labels <- c(
hot_word_count = "Hot word count",
avg_word_heat = "Mean word execution heat",
window_width = "Rolling window width",
actual_window_size = "Actual analysis window size",
jitter_ns = "Estimated timer jitter (ns)",
apic_delta = "APIC ticks per heartbeat tick",
time_trust = "TIME-TRUST",
variance = "Timing variance",
vm_call_depth_max = "Max VM call depth",
hera_heat = "Hera fleet heat",
hermes_heat = "Hermes fleet heat",
artemis_heat = "Artemis fleet heat"
)
cell_summary <- function(metric) {
df %>%
filter(!is.na(.data[[metric]])) %>%
group_by(arch, id_label) %>%
summarise(n = n(), mean = mean(.data[[metric]]), sd = sd(.data[[metric]]),
cv_pct = ifelse(mean != 0, 100 * sd / abs(mean), NA_real_), .groups = "drop")
}
anova_table <- function(metric) {
d <- df %>% filter(!is.na(.data[[metric]]))
if (length(unique(d[[metric]])) <= 1) return(NULL)
fit <- tryCatch(aov(as.formula(paste0(metric, " ~ arch * id_label")), data = d),
error = function(e) NULL)
if (is.null(fit)) return(NULL)
summary(fit)[[1]]
}
make_plot <- function(metric) {
d <- df %>% filter(!is.na(.data[[metric]]))
if (length(unique(d[[metric]])) <= 1) return(NULL)
p <- ggplot(d, aes(x = id_label, y = .data[[metric]], fill = id_label)) +
geom_boxplot(outlier.size = 0.6, alpha = 0.85) +
facet_wrap(~arch, ncol = 1) +
labs(title = metric_labels[[metric]], x = "Identity", y = metric_labels[[metric]]) +
theme(legend.position = "none")
p_light <- p + theme_light_report() + theme(legend.position = "none")
p_dark <- p + theme_dark_report() + theme(legend.position = "none")
save_svg(p_light, paste0(metric, "_light"), w = 7, h = 8)
save_svg(p_dark, paste0(metric, "_dark"), w = 7, h = 8)
TRUE
}
anova_rows <- list()
cell_rows <- list()
for (metric in metrics) {
cs <- cell_summary(metric)
cs$metric <- metric
cell_rows[[metric]] <- cs
at <- anova_table(metric)
if (!is.null(at)) {
at_df <- as.data.frame(at)
at_df$term <- trimws(rownames(at))
at_df$metric <- metric
anova_rows[[metric]] <- at_df
}
make_plot(metric)
}
write.csv(bind_rows(cell_rows), "tables/cell_summary.csv", row.names = FALSE)
write.csv(bind_rows(anova_rows), "tables/anova.csv", row.names = FALSE)
# ── overall dataset summary table (per architecture) ──
overall <- df %>%
group_by(arch) %>%
summarise(
n_rows = n(),
n_trials = n_distinct(run_id),
elapsed_s = max(elapsed_ns) / 1e9,
mean_hot_words = mean(hot_word_count),
mean_fleet_k = mean(fleet_k),
sd_fleet_k = sd(fleet_k),
pct_conserved = 100 * mean(fleet_conserved == 1),
.groups = "drop"
)
write.csv(overall, "tables/overall_summary.csv", row.names = FALSE)
cat("\nDone. charts/, tables/ written.\n")
print(as.data.frame(overall))