503 lines
24 KiB
R
503 lines
24 KiB
R
#!/usr/bin/env Rscript
|
|
# analyse_bare_metal.R
|
|
# StarForth LithosAnanke — Multi-Architecture Bare-Metal DoE Analysis
|
|
#
|
|
# Key insight from data exploration:
|
|
# - tick_interval_ns is constant at 10000 (QEMU deterministic 10µs timer).
|
|
# HR-based phase portraits (as in l8_attractor_map) do not apply here.
|
|
# - The interesting dynamics are in:
|
|
# window_width — adaptive rolling window BREATHES 256→512→1024→2048→4096→...
|
|
# avg_word_heat_q48 — monotonically rising thermal accumulation
|
|
# hot_word_count — convergent excited-state count
|
|
# time_trust_q48 / variance_q48 — arch-specific timer quality metrics
|
|
# - Rows 1~26251 are IDENTICAL across all three ISAs (deterministic boot/POST).
|
|
# - Divergence starts at row ~26252 in arch-specific timer metrics.
|
|
#
|
|
# The centrepiece chart: window_width[n] vs window_width[n+1] (Poincaré map).
|
|
# The adaptive window orbits a fixed cycle through powers of 2 — this IS the
|
|
# compudynamics attractor for bare-metal execution.
|
|
|
|
suppressPackageStartupMessages({
|
|
library(ggplot2)
|
|
library(svglite)
|
|
library(dplyr)
|
|
library(tidyr)
|
|
library(scales)
|
|
library(patchwork)
|
|
library(viridis)
|
|
})
|
|
|
|
# ── paths ─────────────────────────────────────────────────────────────────────
|
|
SCRIPT_DIR <- tryCatch(
|
|
dirname(normalizePath(sys.frames()[[1]]$ofile)),
|
|
error = function(e) getwd()
|
|
)
|
|
BASE_DIR <- normalizePath(file.path(SCRIPT_DIR, ".."))
|
|
DATA_DIR <- file.path(BASE_DIR, "latest")
|
|
OUT_CHARTS <- file.path(SCRIPT_DIR, "charts")
|
|
OUT_TABLES <- file.path(SCRIPT_DIR, "tables")
|
|
dir.create(OUT_CHARTS, showWarnings = FALSE, recursive = TRUE)
|
|
dir.create(OUT_TABLES, showWarnings = FALSE, recursive = TRUE)
|
|
|
|
cat("══════════════════════════════════════════════════════════════════\n")
|
|
cat(" StarForth LithosAnanke — Bare-Metal DoE Analysis\n")
|
|
cat("══════════════════════════════════════════════════════════════════\n\n")
|
|
|
|
Q48 <- 65536.0
|
|
|
|
load_arch <- function(arch) {
|
|
path <- file.path(DATA_DIR, paste0(arch, ".csv"))
|
|
cat(sprintf(" Loading %s ... ", arch))
|
|
df <- read.csv(path, stringsAsFactors = FALSE)
|
|
cat(sprintf("%d rows\n", nrow(df)))
|
|
df$arch <- arch
|
|
|
|
# Guard: remove degenerate rows
|
|
df <- df[!is.na(df$tick_interval_ns) & df$tick_interval_ns > 0, ]
|
|
|
|
# Q48.16 decode
|
|
df$heat <- df$avg_word_heat_q48 / Q48
|
|
df$variance <- df$variance_q48 / Q48
|
|
df$time_trust <- df$time_trust_q48 / Q48
|
|
|
|
# Elapsed in seconds
|
|
df$elapsed_s <- df$elapsed_ns / 1e9
|
|
|
|
# Lag-1 window step for Poincaré map
|
|
df$win_next <- c(df$window_width[-1], NA_real_)
|
|
df$delta_win <- c(diff(df$window_width), NA_real_)
|
|
df$delta_heat <- c(diff(df$heat), NA_real_)
|
|
|
|
df
|
|
}
|
|
|
|
archs <- c("amd64", "aarch64", "riscv64")
|
|
all_data <- lapply(archs, load_arch)
|
|
names(all_data) <- archs
|
|
df_all <- bind_rows(all_data)
|
|
df_all$arch <- factor(df_all$arch, levels = archs)
|
|
|
|
cat(sprintf("\n Total rows: %d\n\n", nrow(df_all)))
|
|
|
|
# ── summary table ─────────────────────────────────────────────────────────────
|
|
cat("Computing per-arch summary...\n")
|
|
|
|
summary_tbl <- df_all %>%
|
|
group_by(arch) %>%
|
|
summarise(
|
|
n_ticks = n(),
|
|
elapsed_s = max(elapsed_s, na.rm = TRUE),
|
|
mean_exec_delta = mean(word_executions_delta, na.rm = TRUE),
|
|
sd_exec_delta = sd(word_executions_delta, na.rm = TRUE),
|
|
mean_hot_words = mean(hot_word_count, na.rm = TRUE),
|
|
mean_heat = mean(heat, na.rm = TRUE),
|
|
max_heat = max(heat, na.rm = TRUE),
|
|
mean_win = mean(window_width, na.rm = TRUE),
|
|
mean_trust = mean(time_trust, na.rm = TRUE),
|
|
mean_variance = mean(variance, na.rm = TRUE),
|
|
.groups = "drop"
|
|
)
|
|
|
|
write.csv(summary_tbl, file.path(OUT_TABLES, "arch_summary.csv"), row.names = FALSE)
|
|
cat(" Saved: arch_summary.csv\n")
|
|
print(as.data.frame(summary_tbl))
|
|
|
|
# Statistical tests on the divergent region (rows after ~26251)
|
|
# Use time_trust and variance which differ per arch.
|
|
# If run length == 26251 (3-rep standard sweep) df_tail will be empty;
|
|
# fall back to the last 10% of each arch's data.
|
|
df_tail <- df_all %>%
|
|
group_by(arch) %>%
|
|
filter(row_number() > 26251) %>%
|
|
ungroup()
|
|
|
|
if (nrow(df_tail) == 0) {
|
|
cat(" NOTE: No rows beyond 26251 — using last 10% per arch as divergent region\n")
|
|
df_tail <- df_all %>%
|
|
group_by(arch) %>%
|
|
slice_tail(prop = 0.10) %>%
|
|
ungroup()
|
|
}
|
|
|
|
n_groups_trust <- df_tail %>% pull(time_trust) %>% unique() %>% length()
|
|
n_groups_var <- df_tail %>% pull(variance) %>% unique() %>% length()
|
|
|
|
kw_note <- ""
|
|
if (n_groups_trust < 2) {
|
|
kw_note <- "SKIP — all time_trust values identical across architectures (algorithmic variance = 0)"
|
|
cat(sprintf("\nKruskal-Wallis: time_trust_q48 ~ architecture: %s\n", kw_note))
|
|
kw_trust <- list(statistic = 0, parameter = 0, p.value = 1)
|
|
} else {
|
|
cat("\nKruskal-Wallis: time_trust_q48 ~ architecture (divergent region)\n")
|
|
kw_trust <- kruskal.test(time_trust ~ arch, data = df_tail)
|
|
cat(sprintf(" H=%.4f df=%d p=%.4e\n", kw_trust$statistic, kw_trust$parameter, kw_trust$p.value))
|
|
}
|
|
|
|
if (n_groups_var < 2) {
|
|
kw_var_note <- "SKIP — all variance values identical across architectures (algorithmic variance = 0)"
|
|
cat(sprintf("Kruskal-Wallis: variance_q48 ~ architecture: %s\n\n", kw_var_note))
|
|
kw_var <- list(statistic = 0, parameter = 0, p.value = 1)
|
|
} else {
|
|
cat("Kruskal-Wallis: variance_q48 ~ architecture (divergent region)\n")
|
|
kw_var <- kruskal.test(variance ~ arch, data = df_tail)
|
|
cat(sprintf(" H=%.4f df=%d p=%.4e\n\n", kw_var$statistic, kw_var$parameter, kw_var$p.value))
|
|
}
|
|
|
|
sink(file.path(OUT_TABLES, "kw_results.txt"))
|
|
cat("Kruskal-Wallis tests (divergent region)\n")
|
|
cat("════════════════════════════════════════\n\n")
|
|
if (nzchar(kw_note)) {
|
|
cat(sprintf("time_trust_q48 ~ architecture: %s\n", kw_note))
|
|
} else {
|
|
cat("time_trust_q48 ~ architecture\n"); print(kw_trust)
|
|
}
|
|
cat("\n")
|
|
if (nzchar(kw_note)) {
|
|
cat(sprintf("variance_q48 ~ architecture: %s\n", kw_note))
|
|
} else {
|
|
cat("variance_q48 ~ architecture\n"); print(kw_var)
|
|
}
|
|
sink()
|
|
cat(" Saved: kw_results.txt\n\n")
|
|
|
|
# ── colour themes ─────────────────────────────────────────────────────────────
|
|
arch_colours <- c(amd64 = "#E07B39",
|
|
aarch64 = "#4A90D9",
|
|
riscv64 = "#50C878")
|
|
|
|
theme_light_report <- function() {
|
|
theme_minimal(base_size = 11) %+replace%
|
|
theme(
|
|
panel.grid.minor = element_blank(),
|
|
panel.grid.major = element_line(colour = "grey90"),
|
|
strip.text = element_text(face = "bold"),
|
|
plot.title = element_text(face = "bold", size = 12),
|
|
plot.subtitle = element_text(colour = "grey40", size = 9),
|
|
legend.position = "bottom"
|
|
)
|
|
}
|
|
|
|
theme_dark_report <- function() {
|
|
theme_minimal(base_size = 11) %+replace%
|
|
theme(
|
|
panel.background = element_rect(fill = "#0d0d0d", colour = NA),
|
|
plot.background = element_rect(fill = "#0d0d0d", colour = NA),
|
|
panel.grid.major = element_line(colour = "#1e1e1e"),
|
|
panel.grid.minor = element_blank(),
|
|
axis.text = element_text(colour = "#aaaaaa"),
|
|
axis.title = element_text(colour = "#cccccc"),
|
|
strip.text = element_text(colour = "white", face = "bold"),
|
|
plot.title = element_text(colour = "white", face = "bold", size = 12),
|
|
plot.subtitle = element_text(colour = "#666666", size = 9),
|
|
legend.text = element_text(colour = "#aaaaaa"),
|
|
legend.title = element_text(colour = "#cccccc"),
|
|
legend.background = element_rect(fill = "#0d0d0d", colour = NA),
|
|
legend.position = "bottom"
|
|
)
|
|
}
|
|
|
|
save_svg <- function(plot, name, w = 12, h = 7) {
|
|
path <- file.path(OUT_CHARTS, paste0(name, ".svg"))
|
|
svglite(path, width = w, height = h)
|
|
print(plot)
|
|
dev.off()
|
|
cat(sprintf(" Saved: %s.svg\n", name))
|
|
invisible(path)
|
|
}
|
|
|
|
cat("Generating charts...\n\n")
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# CHART 1+2: Window Poincaré Map — THE CENTREPIECE
|
|
# window_width[n] vs window_width[n+1], density-coloured
|
|
# Shows the adaptive window's fixed-cycle attractor
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
cat("[1+2] Window Poincaré map (centrepiece — light + dark)...\n")
|
|
|
|
df_poin <- df_all %>%
|
|
filter(!is.na(win_next), window_width > 0, win_next > 0)
|
|
|
|
NBINS <- 60 # coarser bins since window_width has only 5 discrete levels
|
|
|
|
make_poincare <- function(dark = FALSE) {
|
|
bg <- if (dark) "#0d0d0d" else "white"
|
|
lo <- if (dark) "#0d0d0d" else "white"
|
|
mid <- if (dark) "#003366" else "#ffcc00"
|
|
hi <- if (dark) "#00e5ff" else "#cc0000"
|
|
vhigh <- if (dark) "white" else "#1a0000"
|
|
grid_col <- if (dark) "#1a1a2e" else "grey88"
|
|
txt_col <- if (dark) "#aaaaaa" else "grey30"
|
|
ttl_col <- if (dark) "white" else "black"
|
|
|
|
# Log-scale window for better visual separation of powers-of-2
|
|
ggplot(df_poin, aes(x = log2(window_width), y = log2(win_next))) +
|
|
stat_bin_2d(bins = NBINS, aes(fill = after_stat(count))) +
|
|
scale_fill_gradientn(
|
|
colours = c(lo, mid, hi, vhigh),
|
|
values = scales::rescale(c(0, 0.05, 0.3, 1)),
|
|
name = "Density",
|
|
trans = "sqrt",
|
|
labels = scales::comma
|
|
) +
|
|
geom_abline(slope = 1, intercept = 0,
|
|
colour = if (dark) "#334455" else "grey75",
|
|
linewidth = 0.5, linetype = "dashed") +
|
|
scale_x_continuous(
|
|
breaks = log2(c(256, 512, 1024, 2048, 4096)),
|
|
labels = c("256","512","1024","2048","4096")
|
|
) +
|
|
scale_y_continuous(
|
|
breaks = log2(c(256, 512, 1024, 2048, 4096)),
|
|
labels = c("256","512","1024","2048","4096")
|
|
) +
|
|
facet_wrap(~ arch, ncol = 3) +
|
|
labs(
|
|
title = "Adaptive Window Poincaré Map — Bare-Metal Multi-Architecture",
|
|
subtitle = "window_width[n] vs window_width[n+1]; diagonal = identity; orbit = adaptive breathing cycle",
|
|
x = expression(W[n]~"(words, log"[2]*" scale)"),
|
|
y = expression(W[n+1]~"(words, log"[2]*" scale)")
|
|
) +
|
|
theme_minimal(base_size = 11) %+replace%
|
|
theme(
|
|
panel.background = element_rect(fill = bg, colour = NA),
|
|
plot.background = element_rect(fill = bg, colour = NA),
|
|
panel.grid.major = element_line(colour = grid_col),
|
|
panel.grid.minor = element_blank(),
|
|
axis.text = element_text(colour = txt_col, size = 8),
|
|
axis.title = element_text(colour = txt_col),
|
|
strip.text = element_text(colour = ttl_col, face = "bold"),
|
|
plot.title = element_text(colour = ttl_col, face = "bold", size = 13),
|
|
plot.subtitle = element_text(colour = if(dark) "#888888" else "grey40", size = 9),
|
|
legend.position = "right",
|
|
legend.background = element_rect(fill = bg, colour = NA),
|
|
legend.text = element_text(colour = txt_col),
|
|
legend.title = element_text(colour = txt_col)
|
|
)
|
|
}
|
|
|
|
save_svg(make_poincare(dark = FALSE), "poincare_light", w = 14, h = 6)
|
|
save_svg(make_poincare(dark = TRUE), "poincare_dark", w = 14, h = 6)
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# CHART 3+4: Window width time series — shows the breathing oscillation
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
cat("[3+4] Window width time series (light + dark)...\n")
|
|
|
|
df_win_ts <- df_all %>%
|
|
group_by(arch) %>%
|
|
slice(seq(1, n(), by = 3)) %>%
|
|
ungroup()
|
|
|
|
make_window_ts <- function(dark = FALSE) {
|
|
base <- ggplot(df_win_ts, aes(x = elapsed_s, y = window_width, colour = arch)) +
|
|
geom_line(alpha = 0.65, linewidth = 0.35) +
|
|
scale_colour_manual(values = arch_colours) +
|
|
scale_y_log10(
|
|
breaks = c(256, 512, 1024, 2048, 4096),
|
|
labels = c("256","512","1024","2048","4096")
|
|
) +
|
|
scale_x_continuous(labels = scales::comma) +
|
|
facet_wrap(~ arch, ncol = 1, scales = "free_x") +
|
|
labs(
|
|
title = "Adaptive Window Width Over Session",
|
|
subtitle = "Log₂ scale; breathing cycle: 256→4096→256 repeating — the adaptive attractor",
|
|
x = "Elapsed time (s)",
|
|
y = "Window width (words)"
|
|
)
|
|
if (dark) base + theme_dark_report() + theme(legend.position = "none")
|
|
else base + theme_light_report() + theme(legend.position = "none")
|
|
}
|
|
|
|
save_svg(make_window_ts(FALSE), "window_ts_light", w = 12, h = 9)
|
|
save_svg(make_window_ts(TRUE), "window_ts_dark", w = 12, h = 9)
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# CHART 5+6: Thermal accumulation — avg_word_heat time series
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
cat("[5+6] Thermal accumulation time series (light + dark)...\n")
|
|
|
|
df_heat_ts <- df_all %>%
|
|
group_by(arch) %>%
|
|
slice(seq(1, n(), by = 5)) %>%
|
|
ungroup()
|
|
|
|
make_heat_ts <- function(dark = FALSE) {
|
|
base <- ggplot(df_heat_ts, aes(x = elapsed_s, y = heat, colour = arch)) +
|
|
geom_line(alpha = 0.6, linewidth = 0.35) +
|
|
scale_colour_manual(values = arch_colours) +
|
|
scale_x_continuous(labels = scales::comma) +
|
|
facet_wrap(~ arch, ncol = 1, scales = "free_x") +
|
|
labs(
|
|
title = "Average Word Heat Accumulation Over Session",
|
|
subtitle = "Q48.16 decoded; monotonic rise during DOE-WORK; arch-specific divergence in later phase",
|
|
x = "Elapsed time (s)",
|
|
y = "Avg word heat"
|
|
)
|
|
if (dark) base + theme_dark_report() + theme(legend.position = "none")
|
|
else base + theme_light_report() + theme(legend.position = "none")
|
|
}
|
|
|
|
save_svg(make_heat_ts(FALSE), "heat_ts_light", w = 12, h = 9)
|
|
save_svg(make_heat_ts(TRUE), "heat_ts_dark", w = 12, h = 9)
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# CHART 7+8: Execution rate distribution (word_executions_delta)
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
cat("[7+8] Execution rate distribution (light + dark)...\n")
|
|
|
|
df_exec <- df_all %>% filter(word_executions_delta > 0)
|
|
|
|
make_exec_hist <- function(dark = FALSE) {
|
|
base <- ggplot(df_exec, aes(x = word_executions_delta, fill = arch)) +
|
|
geom_histogram(binwidth = 8, alpha = 0.75, position = "identity") +
|
|
scale_fill_manual(values = arch_colours) +
|
|
scale_y_continuous(labels = scales::comma) +
|
|
facet_wrap(~ arch, ncol = 1) +
|
|
labs(
|
|
title = "FORTH Execution Rate Distribution",
|
|
subtitle = "Words executed per 10 µs tick; ISA-independent rate convergence expected",
|
|
x = "Words per tick",
|
|
y = "Count"
|
|
)
|
|
if (dark) base + theme_dark_report() + theme(legend.position = "none")
|
|
else base + theme_light_report() + theme(legend.position = "none")
|
|
}
|
|
|
|
save_svg(make_exec_hist(FALSE), "exec_rate_light", w = 10, h = 9)
|
|
save_svg(make_exec_hist(TRUE), "exec_rate_dark", w = 10, h = 9)
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# CHART 9+10: time_trust & variance by architecture (divergent region)
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
cat("[9+10] Time-trust & variance density (light + dark)...\n")
|
|
|
|
df_trust <- df_tail %>% filter(time_trust < 1.01, time_trust > 0.9)
|
|
|
|
make_trust <- function(dark = FALSE) {
|
|
p_trust <- ggplot(df_trust, aes(x = time_trust, fill = arch, colour = arch)) +
|
|
geom_density(alpha = 0.4, linewidth = 0.6) +
|
|
scale_fill_manual(values = arch_colours) +
|
|
scale_colour_manual(values = arch_colours) +
|
|
labs(title = "Time-Trust Distribution (divergent region)",
|
|
subtitle = "Q48.16 decoded; timer quality diverges per ISA after boot phase",
|
|
x = "Time trust", y = "Density")
|
|
|
|
df_vq <- df_tail %>% filter(variance > 0, variance < quantile(variance, 0.99, na.rm=TRUE))
|
|
p_var <- ggplot(df_vq, aes(x = variance, fill = arch, colour = arch)) +
|
|
geom_density(alpha = 0.4, linewidth = 0.6) +
|
|
scale_fill_manual(values = arch_colours) +
|
|
scale_colour_manual(values = arch_colours) +
|
|
labs(title = "Timing Variance Distribution (divergent region)",
|
|
subtitle = "Q48.16 decoded; lower variance = more stable adaptive timing",
|
|
x = "Variance", y = "Density")
|
|
|
|
if (dark) {
|
|
p_trust <- p_trust + theme_dark_report()
|
|
p_var <- p_var + theme_dark_report()
|
|
} else {
|
|
p_trust <- p_trust + theme_light_report()
|
|
p_var <- p_var + theme_light_report()
|
|
}
|
|
p_trust / p_var
|
|
}
|
|
|
|
save_svg(make_trust(FALSE), "trust_variance_light", w = 10, h = 10)
|
|
save_svg(make_trust(TRUE), "trust_variance_dark", w = 10, h = 10)
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# CHART 11+12: Hot word count evolution
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
cat("[11+12] Hot word count evolution (light + dark)...\n")
|
|
|
|
df_hot_ts <- df_all %>%
|
|
group_by(arch) %>%
|
|
slice(seq(1, n(), by = 5)) %>%
|
|
ungroup()
|
|
|
|
make_hot_ts <- function(dark = FALSE) {
|
|
base <- ggplot(df_hot_ts, aes(x = elapsed_s, y = hot_word_count, colour = arch)) +
|
|
geom_line(alpha = 0.55, linewidth = 0.35) +
|
|
scale_colour_manual(values = arch_colours) +
|
|
scale_x_continuous(labels = scales::comma) +
|
|
facet_wrap(~ arch, ncol = 1, scales = "free_x") +
|
|
labs(
|
|
title = "Hot Word Count Over Session",
|
|
subtitle = "Words above heat threshold; converges then differentiates per ISA in DoE phase",
|
|
x = "Elapsed time (s)",
|
|
y = "Hot word count"
|
|
)
|
|
if (dark) base + theme_dark_report() + theme(legend.position = "none")
|
|
else base + theme_light_report() + theme(legend.position = "none")
|
|
}
|
|
|
|
save_svg(make_hot_ts(FALSE), "hot_words_ts_light", w = 12, h = 9)
|
|
save_svg(make_hot_ts(TRUE), "hot_words_ts_dark", w = 12, h = 9)
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# CHART 13+14: Execution phase portrait (heat[n] vs delta_heat[n])
|
|
# Thermal dynamics — analogous to HR vs ΔHR in l8_attractor_map
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
cat("[13+14] Thermal phase portrait (light + dark)...\n")
|
|
|
|
df_thermal <- df_all %>%
|
|
filter(!is.na(delta_heat)) %>%
|
|
filter(abs(delta_heat) < quantile(abs(delta_heat), 0.995, na.rm=TRUE))
|
|
|
|
make_thermal_portrait <- function(dark = FALSE) {
|
|
bg <- if (dark) "#0d0d0d" else "white"
|
|
lo <- if (dark) "#0d0d0d" else "white"
|
|
mid <- if (dark) "#003366" else "#ffcc00"
|
|
hi <- if (dark) "#00e5ff" else "#cc0000"
|
|
vhigh <- if (dark) "white" else "#1a0000"
|
|
grid_col <- if (dark) "#1a1a2e" else "grey88"
|
|
txt_col <- if (dark) "#aaaaaa" else "grey30"
|
|
ttl_col <- if (dark) "white" else "black"
|
|
|
|
ggplot(df_thermal, aes(x = heat, y = delta_heat)) +
|
|
stat_bin_2d(bins = 200, aes(fill = after_stat(count))) +
|
|
scale_fill_gradientn(
|
|
colours = c(lo, mid, hi, vhigh),
|
|
values = scales::rescale(c(0, 0.05, 0.3, 1)),
|
|
name = "Density",
|
|
trans = "sqrt",
|
|
labels = scales::comma
|
|
) +
|
|
geom_hline(yintercept = 0,
|
|
colour = if (dark) "#334455" else "grey75",
|
|
linewidth = 0.4, linetype = "dashed") +
|
|
facet_wrap(~ arch, ncol = 3) +
|
|
labs(
|
|
title = "Thermal Phase Portrait — Heat[n] vs ΔHeat[n]",
|
|
subtitle = "Thermal analogue of the l8 HR phase portrait; attractor at ΔHeat≈0 as system reaches steady state",
|
|
x = "Avg word heat (Q48.16 decoded)",
|
|
y = "ΔHeat per tick"
|
|
) +
|
|
theme_minimal(base_size = 11) %+replace%
|
|
theme(
|
|
panel.background = element_rect(fill = bg, colour = NA),
|
|
plot.background = element_rect(fill = bg, colour = NA),
|
|
panel.grid.major = element_line(colour = grid_col),
|
|
panel.grid.minor = element_blank(),
|
|
axis.text = element_text(colour = txt_col, size = 8),
|
|
axis.title = element_text(colour = txt_col),
|
|
strip.text = element_text(colour = ttl_col, face = "bold"),
|
|
plot.title = element_text(colour = ttl_col, face = "bold", size = 13),
|
|
plot.subtitle = element_text(colour = if(dark) "#888888" else "grey40", size = 9),
|
|
legend.position = "right",
|
|
legend.background = element_rect(fill = bg, colour = NA),
|
|
legend.text = element_text(colour = txt_col),
|
|
legend.title = element_text(colour = txt_col)
|
|
)
|
|
}
|
|
|
|
save_svg(make_thermal_portrait(FALSE), "thermal_portrait_light", w = 14, h = 6)
|
|
save_svg(make_thermal_portrait(TRUE), "thermal_portrait_dark", w = 14, h = 6)
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# done
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
svg_files <- list.files(OUT_CHARTS, pattern = "\\.svg$")
|
|
cat(sprintf("\n══════════════════════════════════════════════════════════════════\n"))
|
|
cat(sprintf(" Analysis complete. %d SVG charts in %s\n", length(svg_files), OUT_CHARTS))
|
|
cat(sprintf("══════════════════════════════════════════════════════════════════\n\n"))
|