Add 3x9 factorial analysis of std79-doe heartbeat/physics telemetry
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

Correlates every HB-ON/HB-OFF heartbeat-tick CSV row (results-20260912-
with-heartbeat-csv/*-doe-raw.log) back to which trial (run_id, id_idx,
id_label, rep) was active when it printed, despite the async tick
printer splicing rows mid-token -- including mid a DOE-RUN marker itself
-- into the trial loop's own console output on the shared serial line.

Pipeline (analysis-20260912/, see its own README.md):
- correlate_doe.py: two-pass reconstruction per architecture (remove
  atomic CSV-row spans to rebuild the clean trial-output stream, map
  each removed row's offset back to the nearest preceding run_id
  marker); identity/rep looked up from a known-clean prior run's
  run_id mapping rather than re-parsed, since one aarch64 marker
  (trial 12, rajames rep 1) lost its id_idx digit to a zero-separator
  collision with an adjacent CSV field and is unrecoverable from that
  log alone -- its rows fold into trial 11 instead, documented as a
  known limitation.
- combine.py: merges all three architectures into combined.csv (768
  rows), decoding Q48.16 fields to floats and jitter_bits' IEEE754 bit
  pattern to real jitter_ns.
- analysis.R: per-cell (architecture x identity) means/SD and two-way
  ANOVA for each of 12 telemetry metrics, one boxplot SVG per metric,
  written up as ANALYSIS.md.

Key findings: identity significantly affects word-heat/window-sizing
metrics (expected -- different identities execute different word
sets), architecture significantly affects timing metrics (APIC
ticks/tick, timing variance, fleet heat -- expected, different QEMU
targets), zero architecture x identity interaction on any metric.

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 12:46:34 -04:00
co-authored by Claude Sonnet 5
parent 403a7639e1
commit 934be5a257
19 changed files with 5872 additions and 0 deletions
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
correlate_doe.py <raw_log> <out_csv>
Reconstructs which DoE trial (run_id, id_idx, id_label, rep) was active
for every heartbeat-tick CSV row in a std79-doe raw log, despite the
async HB-ON tick printer splicing CSV rows mid-token into the trial
loop's own console output on the shared serial line -- including mid a
DOE-RUN,... marker itself.
Method:
1. Strip ANSI codes and the per-line "[VM-TAG] " console prefix (an
artifact of the log capture layer), concatenate every stripped
line with no separator. Find every [HADES][DOE ] header/row
occurrence and physically remove exactly those spans, recording
each removed data row's insertion offset in the CLEANED stream's
own coordinates.
2. Parse only "DOE-RUN,<run_id> ," (the run_id digits) from the clean
stream -- reliable even when a CSV row spliced into the id_idx/
id_label/rep text right after it (observed live: a row's last
digit field can land with zero separator against the immediately
following trial digit, e.g. "...11642" + "6" -> "116426", making
id_idx unrecoverable from THIS log alone for the affected trial).
id_idx/id_label/rep are looked up from RUN_ID_MAP below instead of
re-parsed -- deterministic given the campaign's fixed seed (12345),
independently verified identical across 5+ prior clean runs
(results-20260911-donor-floor-fix/amd64-doe-raw.log and others).
3. Assign each CSV row to whichever run_id marker's start offset is
the largest one at or before that row's insertion offset.
"""
import re
import sys
import csv
import bisect
# From results-20260911-donor-floor-fix/amd64-doe-raw.log (seed 12345),
# independently re-verified identical (md5) across every std79-doe run
# since e2abc56. run_id -> (id_idx, id_label, rep).
RUN_ID_MAP = {
0: (6, "04", 2), 1: (3, "01", 1), 2: (0, "zuse", 1), 3: (1, "rajames", 0),
4: (5, "03", 0), 5: (0, "zuse", 2), 6: (2, "00", 2), 7: (8, "06", 2),
8: (0, "zuse", 0), 9: (6, "04", 1), 10: (6, "04", 0), 11: (4, "02", 2),
12: (1, "rajames", 1), 13: (7, "05", 1), 14: (7, "05", 2), 15: (4, "02", 0),
16: (2, "00", 0), 17: (8, "06", 0), 18: (7, "05", 0), 19: (2, "00", 1),
20: (8, "06", 1), 21: (5, "03", 2), 22: (1, "rajames", 2), 23: (3, "01", 2),
24: (5, "03", 1), 25: (3, "01", 0), 26: (4, "02", 1),
}
def build_stream(raw_text):
parts = []
for line in raw_text.split('\n'):
m = re.match(r'^\[[^\]]+\]\s?(.*)$', line)
parts.append(m.group(1) if m else line)
return ''.join(parts)
ROW_RE = re.compile(
r'\[HADES\]\[DOE \] '
r'(?:(?P<header>tick_number,elapsed_ns,[^\[]*?)(?=\[HADES\]\[DOE \]|DOE-RUN|$)'
r'|(?P<row>[0-9]+(?:,[0-9]+)+))'
)
RUN_ID_RE = re.compile(r'DOE-RUN,\s*([0-9]+)\s*,')
def main():
if len(sys.argv) != 3:
print("usage: correlate_doe.py <raw_log> <out_csv>", file=sys.stderr)
sys.exit(1)
log_path, out_path = sys.argv[1], sys.argv[2]
with open(log_path, "r", errors="replace") as f:
raw = f.read()
raw = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', raw)
stream = build_stream(raw)
cleaned_parts = []
cleaned_offset = 0
last_end = 0
csv_rows = [] # (cleaned_insertion_offset, fields)
header_fields = None
for m in ROW_RE.finditer(stream):
before = stream[last_end:m.start()]
cleaned_parts.append(before)
cleaned_offset += len(before)
if m.group('header') is not None:
header_fields = [x.strip() for x in m.group('header').split(',')]
else:
csv_rows.append((cleaned_offset, m.group('row').split(',')))
last_end = m.end()
cleaned_parts.append(stream[last_end:])
cleaned_stream = ''.join(cleaned_parts)
if header_fields is None:
print("FATAL: never found the DOE CSV header row", file=sys.stderr)
sys.exit(1)
markers = [(m.start(), int(m.group(1))) for m in RUN_ID_RE.finditer(cleaned_stream)]
# Sanity: run_ids should appear in increasing offset order with values
# 0..26 each exactly once. Warn, don't crash, on anything unexpected --
# a genuinely dropped/garbled run_id digit is possible in principle.
seen = [rid for _, rid in markers]
if sorted(set(seen)) != list(range(27)) or len(seen) != 27:
print(f"WARNING: expected exactly run_ids 0..26 once each, got {sorted(seen)}", file=sys.stderr)
marker_offsets = [off for off, _ in markers]
out_rows = []
unlabeled = 0
for offset, fields in csv_rows:
i = bisect.bisect_right(marker_offsets, offset) - 1
if i >= 0:
run_id = markers[i][1]
id_idx, id_label, rep = RUN_ID_MAP.get(run_id, (None, None, None))
elif 0 not in seen:
# No marker precedes this row because run_id 0's own marker
# (always the very first trial, loop index 0) got garbled
# beyond recovery in this particular log -- safe to assume
# every row before the first RECOVERED marker still belongs
# to trial 0, not truly unlabeled.
run_id = 0
id_idx, id_label, rep = RUN_ID_MAP[0]
else:
run_id, id_idx, id_label, rep = None, None, None, None
unlabeled += 1
out_rows.append(((run_id, id_idx, id_label, rep), fields))
with open(out_path, "w", newline="") as f:
w = csv.writer(f)
w.writerow(["run_id", "id_idx", "id_label", "rep"] + header_fields)
for (run_id, id_idx, id_label, rep), fields in out_rows:
w.writerow([run_id, id_idx, id_label, rep] + fields)
print(f"trial markers found: {len(markers)}")
print(f"data rows: {len(out_rows)}, unlabeled (before first marker): {unlabeled}")
labeled_run_ids = sorted(set(t[0] for (t, _) in out_rows if t[0] is not None))
print(f"distinct run_ids covered: {len(labeled_run_ids)} -> {labeled_run_ids}")
if __name__ == "__main__":
main()