docs: reorganise -- current reference gathered, finished work archived, superseded and dead ideas marked as such

docs/CLAUDE.md Phase 2, 2026-10-14.  All moves by git mv; every archived
file carries a header saying what it is, why, and where to look instead.

- docs/reference/ (new): roadmap and versioning policy, M7.1, word-ACL
  design, kernel args, hosted acceptance test, dictionary, primitives,
  ontology, L8 table -- previously spread over docs/, docs/lithosananke/
  and docs/03-architecture/.
- docs/working/archive/historical/: finished work (session plan, test-gap
  audit, birthing plan, fix write-ups, the January architecture draft).
- docs/working/archive/superseded/: the Hermes-VM Tripod docs and July
  Hermes designs (-> FABRIC-3.5/3.6), the StarForth roadmap, every working
  doc already flagged superseded or obsolete.
- docs/working/archive/dead/: the hal_* target contract (never adopted),
  L4Re, metrics-and-knobs.
- Papers and proposals into docs/working/papers/.
- References rewritten across the repo (not logs/, not docs/patent/);
  CLAUDE.md's documentation section rewritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-14 16:03:56 -04:00
co-authored by Claude Opus 5.5
parent 1a4b163861
commit 7aed588f78
116 changed files with 305 additions and 175 deletions
@@ -1,355 +0,0 @@
<!-- Moved from docs/02-experiments/L8_WIRING_PLAN.md to docs/working/experiments/02-experiments/L8_WIRING_PLAN.md on 2026-06-16 (docs reorg Phase 2) -->
> **⚠ SUPERSEDED (flagged 2026-10-09).** The wiring plan was executed; superseded by
> `L8_WIRING_IMPLEMENTATION_SUMMARY.md` and `L8_WIRING_CODE_REVIEW_GUIDE.md`. Kept as a
> record of the plan.
# L8 Control Wiring Implementation Plan
**Status**: Infrastructure exists but execution paths are not gated
**Date**: 2025-12-05
**Issue**: L8 Jacquard mode selector computes 4-bit control signals (L2/L3/L5/L6) but feedback loops ignore them
---
## Executive Summary
The L8 Steady State Machine (SSM) infrastructure is **90% complete** but the final 10% is critical:
✅ **Complete**:
- L8 state machine (`ssm_jacquard.c`) computes 4-bit mode (C0-C15) based on entropy/CV/temporal metrics
- `ssm_apply_mode()` sets L2/L3/L5/L6 flags in `vm->ssm_config`
- `vm_heartbeat_update_l8()` updates L8 state every heartbeat cycle (lines 993-1045 in `vm.c`)
- Mode transitions logged with debug info
❌ **Missing**:
- Feedback loop execution paths don't check `ssm_config` flags before running
- L2/L3/L5/L6 run unconditionally despite L8 setting mode bits
- Result: L8 is a "supervisor without authority"
---
## Current Architecture (From DoE Analysis)
From `Makefile:112-127` and `ssm_jacquard.h:32-51`:
| Loop | Name | Status | Control |
|------|------|--------|---------|
| L1 | Heat Tracking | ALWAYS OFF | Disabled (harmful in 86% of configs) |
| L2 | Rolling Window | **NEEDS GATING** | L8 bit 3 (entropy-driven) |
| L3 | Linear Decay | **NEEDS GATING** | L8 bit 2 (temporal locality) |
| L4 | Pipelining Metrics | ALWAYS OFF | Disabled (harmful in 100% of configs) |
| L5 | Window Inference | **NEEDS GATING** | L8 bit 1 (CV-driven) |
| L6 | Decay Inference | **NEEDS GATING** | L8 bit 0 (CV + temporal) |
| L7 | Adaptive Heartrate | ALWAYS ON | Enabled (beneficial in 71% of configs) |
| L8 | Jacquard Selector | **IMPLEMENTED** | 4-bit mode controller (C0-C15) |
**Key Insight**: L1 and L4 are compile-time disabled. L7 is always-on. **L2/L3/L5/L6 need runtime gating** based on L8 mode bits.
---
## Required Changes (4 Locations)
### 1. L2: Rolling Window Recording (src/rolling_window_of_truth.c)
**Current Code** (`rolling_window_record_execution()` line 181):
```c
int rolling_window_record_execution(RollingWindowOfTruth* window, uint32_t word_id)
{
if (!window || !window->execution_history)
return -1;
window->execution_history[window->window_pos] = word_id;
// ... always executes
}
```
**Required Change**:
Add check at function entry or call site. Two options:
**Option A** (Preferred - Clean separation):
Modify call site in `src/vm.c:1652`:
```c
// Before (line 1652):
rolling_window_record_execution(&vm->rolling_window, word_id);
// After:
if (vm->ssm_config && ((ssm_config_t*)vm->ssm_config)->L2_rolling_window) {
rolling_window_record_execution(&vm->rolling_window, word_id);
}
```
**Option B** (Alternative - Gate inside function):
Modify `rolling_window_record_execution()` to accept VM pointer and check internally.
**Files to modify**:
- `src/vm.c:1652` - Call site in `execute_word()`
- `src/rolling_window_of_truth.c:181` - Consider adding VM* parameter for internal check
- `include/rolling_window_of_truth.h` - Update function signature if Option B chosen
---
### 2. L3: Linear Decay Application (src/physics_metadata.c)
**Current Code** (line 176):
```c
/* L8 FINAL INTEGRATION: L3 linear decay is always-on */
void physics_metadata_apply_linear_decay(DictEntry *entry, uint64_t elapsed_ns, VM *vm) {
if (!entry || !vm) {
return;
}
/* Frozen words do not decay */
if (entry->flags & WORD_FROZEN) {
return;
}
/* Don't bother with insignificant time intervals */
if (elapsed_ns < DECAY_MIN_INTERVAL) {
return;
}
// ... decay logic always executes
}
```
**Required Change**:
Add L3 gate check after frozen/interval checks:
```c
/* L8 FINAL INTEGRATION: L3 linear decay - runtime gated by Jacquard */
void physics_metadata_apply_linear_decay(DictEntry *entry, uint64_t elapsed_ns, VM *vm) {
if (!entry || !vm) {
return;
}
/* Frozen words do not decay */
if (entry->flags & WORD_FROZEN) {
return;
}
/* Don't bother with insignificant time intervals */
if (elapsed_ns < DECAY_MIN_INTERVAL) {
return;
}
/* L8 GATE: Check if L3 (linear decay) is enabled by Jacquard mode */
if (vm->ssm_config) {
ssm_config_t *config = (ssm_config_t*)vm->ssm_config;
if (!config->L3_linear_decay) {
return; /* L3 disabled by L8 mode selector */
}
}
// ... existing decay logic
}
```
**Files to modify**:
- `src/physics_metadata.c:176` - Add L3 gate check (line ~188-193)
- Comment update at line 175 to reflect runtime gating
**Call sites** (no changes needed, gate is internal):
- `src/vm.c:737` - `vm_tick_apply_background_decay()` (heartbeat loop)
- Other call sites inherit the gate
---
### 3. L5: Window Width Inference (src/inference_engine.c)
**Current Code** (line 538):
```c
/* === PHASE 2C: Window Width Inference === */
/* L8 FINAL INTEGRATION: Loop #5 always-on */
uint32_t inferred_width = find_variance_inflection(
trajectory,
traj_len,
current_variance
);
```
**Required Change**:
Gate inference execution with conditional block:
```c
/* === PHASE 2C: Window Width Inference === */
/* L8 FINAL INTEGRATION: Loop #5 runtime-gated by Jacquard */
uint32_t inferred_width = outputs->adaptive_window_width; /* Keep previous value as default */
if (inputs->vm && inputs->vm->ssm_config) {
ssm_config_t *config = (ssm_config_t*)inputs->vm->ssm_config;
if (config->L5_window_inference) {
/* L5 enabled - run inference */
inferred_width = find_variance_inflection(
trajectory,
traj_len,
current_variance
);
}
/* else: L5 disabled, keep previous adaptive_window_width */
}
```
**Files to modify**:
- `src/inference_engine.c:538-544` - Add L5 conditional gate
- Comment update at line 539 to reflect runtime gating
---
### 4. L6: Decay Slope Inference (src/inference_engine.c)
**Current Code** (line 546):
```c
/* === PHASE 2D: Decay Slope Inference === */
/* L8 FINAL INTEGRATION: Loop #6 always-on */
uint64_t inferred_slope = infer_decay_slope_q48(trajectory, traj_len);
```
**Required Change**:
Gate inference execution with conditional block:
```c
/* === PHASE 2D: Decay Slope Inference === */
/* L8 FINAL INTEGRATION: Loop #6 runtime-gated by Jacquard */
uint64_t inferred_slope = outputs->adaptive_decay_slope; /* Keep previous value as default */
if (inputs->vm && inputs->vm->ssm_config) {
ssm_config_t *config = (ssm_config_t*)inputs->vm->ssm_config;
if (config->L6_decay_inference) {
/* L6 enabled - run inference */
inferred_slope = infer_decay_slope_q48(trajectory, traj_len);
}
/* else: L6 disabled, keep previous adaptive_decay_slope */
}
```
**Files to modify**:
- `src/inference_engine.c:546-548` - Add L6 conditional gate
- Comment update at line 547 to reflect runtime gating
---
## Implementation Order (Recommended)
1. **L3 (Decay)** - Simplest, internal to one function
- Modify `src/physics_metadata.c:176-220`
- Add gate check after line 188
- Test with mode C0 (all off) vs C4 (L3 on)
2. **L5 (Window Inference)** - Medium complexity
- Modify `src/inference_engine.c:538-544`
- Preserve previous value when disabled
- Test early-exit behavior with mode C0 vs C2
3. **L6 (Decay Inference)** - Similar to L5
- Modify `src/inference_engine.c:546-548`
- Preserve previous value when disabled
- Test with mode C0 vs C1
4. **L2 (Rolling Window)** - Most complex (data structure)
- Decide on Option A (call-site gate) vs Option B (internal gate)
- Modify call site `src/vm.c:1652` OR function signature
- Test with mode C0 vs C8 (high entropy)
---
## Testing Strategy
### Unit Tests
For each loop, verify:
1. **Mode C0 (0000)**: All loops disabled → function returns early or skips logic
2. **Single-bit modes**: Enable one loop at a time (C1, C2, C4, C8)
3. **Top 5% modes**: C4, C7, C9, C11, C12 work correctly
### Integration Tests
1. **Mode transitions**: L8 changes mode → loops respond correctly
2. **Hysteresis**: Mode doesn't flap rapidly (5-tick delay)
3. **Metrics**: Verify entropy/CV/temporal drive correct mode selection
### Regression Tests
1. Run existing 936+ test suite with different L8 modes
2. Verify deterministic behavior (no variance across runs)
3. Check performance matches DoE predictions
---
## Validation Checklist
After implementation, verify:
- [ ] L2: `rolling_window_record_execution()` only executes when L2 bit set
- [ ] L3: `physics_metadata_apply_linear_decay()` returns early when L3 bit clear
- [ ] L5: `find_variance_inflection()` only called when L5 bit set
- [ ] L6: `infer_decay_slope_q48()` only called when L6 bit set
- [ ] L8: Mode transitions logged in debug output
- [ ] All existing tests pass with default mode (C0 or C12)
- [ ] DoE experiment can reproduce 128 configurations via L8 mode forcing
---
## Impact Assessment
**Lines of code changed**: ~40-60 lines across 3 files
**Risk**: Low (adding guards, not changing logic)
**Performance**: Neutral to positive (skips work when disabled)
**Correctness**: High confidence (L8 already validated via DoE)
---
## Open Questions
1. **Default mode**: Should VM initialize with C0 (minimal) or C12 (DoE optimal)?
- Current: `ssm_l8_init(..., SSM_MODE_C0)` in `vm.c:387`
- Top 5%: C4, C7, C9, C11, C12
- Recommendation: Start C0, let L8 adapt
2. **Mode override**: Should FORTH words exist to force L8 mode?
- `L8-MODE!` to set mode manually
- `L8-MODE@` to query current mode
- Useful for debugging/benchmarking
3. **Metrics exposure**: Should heartbeat CSV export include L8 mode?
- Currently exports: executions, heat, variance, slope
- Add: `l8_mode` column (0-15 integer)
---
## Success Criteria
**Definition of Done**:
1. All 4 feedback loops check `ssm_config` flags before executing
2. L8 mode selector controls L2/L3/L5/L6 runtime behavior
3. Mode transitions visible in debug logs
4. Existing test suite passes with default L8 mode
5. Can reproduce DoE configurations by forcing L8 modes 0-15
**Verification**:
```bash
# Build with L8 control wired
make clean && make fastest
# Test mode C0 (all off) - should be fastest, least adaptive
./build/amd64/fastest/starforth --benchmark 1000
# Test mode C15 (all on) - should be slowest, most adaptive
# (requires word to set L8 mode or environment variable)
# Run full test suite
make test
```
---
## Next Steps
**Before coding**:
1. Review this plan with stakeholders
2. Decide on L2 implementation strategy (Option A vs B)
3. Choose default L8 mode (C0 vs C12)
**During implementation**:
1. Follow order: L3 → L5 → L6 → L2
2. Test each loop individually before moving to next
3. Update comments to reflect runtime gating
**After completion**:
1. Run full DoE validation (128 configs × 300 reps)
2. Verify 0% algorithmic variance maintained
3. Document L8 control API for future developers
@@ -1,262 +0,0 @@
<!-- Moved from docs/02-experiments/factorial-doe/guide.md to docs/working/experiments/02-experiments/factorial-doe/guide.md on 2026-06-16 (docs reorg Phase 2) -->
> **⚠ OBSOLETE (flagged 2026-10-09).** Describes the retired per-loop Makefile-toggle
> architecture, replaced by the L8 Jacquard runtime mode selector. Kept for historical record.
# StarForth Complete 2^6 Factorial Design of Experiments
## Overview
**One-and-done comprehensive factorial testing** - collect all data systematically, analyze separately.
This guide describes how to run a **complete 2^6 factorial design of experiments** across all 6 feedback loops, yielding:
- **64 unique configurations** (all binary combinations)
- **1,920+ runs** (30 samples per configuration)
- **Single unified dataset** for factorial analysis
## The Why: Factorial DoE
Instead of incrementally tuning parameters, we collect **comprehensive experimental data** in one go:
### Traditional Approach (Old)
- Run baseline
- Run with Loop #1
- Run with Loops #1-2
- Run with Loops #1-3
- ... (sequential, incremental)
- **Problem:** Can't see interactions, trade-offs, or surprises
### Factorial Approach (New)
- Run ALL 2^6 = 64 combinations in randomized order
- Collect all data at once
- Analyze factorial effects, interactions, and main effects
- **Advantage:** Complete picture of how loops interact
## The 6 Feedback Loops
Each loop is a binary toggle in the Makefile:
| Loop | Makefile Flag | Purpose |
|------|---|---|
| **#1** | `ENABLE_LOOP_1_HEAT_TRACKING` | Count word execution frequency |
| **#2** | `ENABLE_LOOP_2_ROLLING_WINDOW` | Capture execution history (circular buffer) |
| **#3** | `ENABLE_LOOP_3_LINEAR_DECAY` | Age words by exponential decay over time |
| **#4** | `ENABLE_LOOP_4_PIPELINING_METRICS` | Track word-to-word transitions |
| **#5** | `ENABLE_LOOP_5_WINDOW_INFERENCE` | Infer optimal window width (Levene's test) |
| **#6** | `ENABLE_LOOP_6_DECAY_INFERENCE` | Infer decay slope via linear regression |
## Configuration Naming
Each of 64 configurations is named by its loop status:
```
L1_L2_L3_L4_L5_L6
Where each digit is 0 (OFF) or 1 (ON)
Examples:
000000 = Baseline (all loops OFF - pure FORTH-79)
000001 = Only heat tracking
111110 = All except decay inference
111111 = All loops ON (likely optimal)
101010 = Alternating pattern (for research)
```
## Quick Start
### Minimal Test (validate script)
```bash
./scripts/run_factorial_doe.sh --runs-per-config 1 TEST_RUN
# 64 builds × 1 run = 64 total runs
# Runtime: ~30-45 minutes
# Output: /home/rajames/CLionProjects/StarForth-DoE/experiments/TEST_RUN/
```
### Standard Comprehensive Run
```bash
./scripts/run_factorial_doe.sh --runs-per-config 30 2025_11_19_FACTORIAL
# 64 builds × 30 runs = 1,920 total runs
# Runtime: ~2-4 hours
# Output: /home/rajames/CLionProjects/StarForth-DoE/experiments/2025_11_19_FACTORIAL/
```
### High-Precision Run
```bash
./scripts/run_factorial_doe.sh --runs-per-config 100 HIGH_PRECISION_DOE
# 64 builds × 100 runs = 6,400 total runs
# Runtime: ~6-12 hours
# Output: /home/rajames/CLionProjects/StarForth-DoE/experiments/HIGH_PRECISION_DOE/
```
## Output Structure
```
/home/rajames/CLionProjects/StarForth-DoE/experiments/<EXPERIMENT_LABEL>/
├── experiment_results.csv # All measurements (1920+ rows)
├── experiment_summary.txt # Timing, metadata, statistics
├── configuration_manifest.txt # All 64 configs documented
├── test_matrix.txt # Randomized execution order
└── run_logs/
├── 000000_run_1.log
├── 000000_run_2.log
├── 000001_run_1.log
├── ...
└── 111111_run_30.log # 1,920 total log files
```
## CSV Schema
Each row in `experiment_results.csv` contains:
```
timestamp,configuration,run_number,
<35+ metrics from test harness>,
enable_loop_1_heat_tracking,
enable_loop_2_rolling_window,
enable_loop_3_linear_decay,
enable_loop_4_pipelining_metrics,
enable_loop_5_window_inference,
enable_loop_6_decay_inference
```
This lets you correlate every metric against every loop combination.
## Analysis Strategy
After collection, analyze in the separate `StarForth-DoE` repository:
### Step 1: Load Data
```python
import pandas as pd
df = pd.read_csv("experiment_results.csv")
print(df.shape) # Should be (1920, 38+)
print(df.columns)
```
### Step 2: Separate Metrics from Factors
```python
factor_cols = ['enable_loop_1_heat_tracking', ..., 'enable_loop_6_decay_inference']
metric_cols = ['total_lookups', 'cache_hit_percent', ..., 'vm_workload_duration_ns_q48']
factors = df[factor_cols]
metrics = df[metric_cols]
```
### Step 3: Factorial Analysis
```python
# Main effects: What's the average change when Loop X goes from 0→1?
main_effects = {}
for loop in factor_cols:
main_effects[loop] = metrics[factors[loop]==1].mean() - metrics[factors[loop]==0].mean()
# Interactions: Do loops work better/worse together?
# Can use statsmodels.formula.api for ANOVA/regression
# Optimal subset: Which combinations of loops give best throughput?
best_configs = df.nlargest(10, 'vm_workload_duration_ns_q48')
```
## Key Design Choices
### 1. **Rebuild Every Pass** ✓
- Each configuration requires a `make clean && make`
- Ensures clean, isolated state
- No cross-contamination
- True ground truth for each variant
### 2. **Randomized Execution**
- All 1,920 runs randomized into single matrix (not 64 separate runs)
- Prevents thermal ramp, temporal bias, or systematic trends
- DoE principle: randomization eliminates confounds
### 3. **Full Factorial** (not fractional)
- Testing ALL 2^6 = 64 combinations
- Can detect unexpected interactions
- Most expensive, most informative
### 4. **One-and-Done Collection**
- Single experiment, all data
- No progressive tuning during collection
- Separates measurement from analysis (good practice)
## Expected Results
### Hypothetical Output
Assuming typical performance distribution:
```
Configuration | Avg Time (ns) | Speed vs Baseline
─────────────────────────────────────────────────
000000 (base) | 10,000,000 | 1.0x
000001 (L1) | 9,950,000 | 1.005x
111111 (all) | 8,500,000 | 1.176x ← Likely best
```
### Expected Patterns
1. **Loop #1 (heat tracking)**: ~2-5% improvement alone
2. **Loop #2 (rolling window)**: ~1-3% improvement (depends on L1)
3. **Loops #3-6**: Increasingly synergistic (work better together)
4. **Interactions**: Loop #5 + #6 likely synergistic (inference loops)
## Troubleshooting
### Build Fails for a Configuration
```bash
# Check specific config build log
cat /home/rajames/CLionProjects/StarForth-DoE/experiments/<LABEL>/run_logs/build_<CONFIG>.log
```
### Test Harness Crashes on Some Configs
- Check if loop combination is invalid (e.g., Loop #5 without Loop #2)
- Verify Makefile flags are correctly parsed
- See: `include/vm.h` for interdependencies
### Results Look Suspicious
- Check `test_matrix.txt` - ensure randomization is present
- Verify CSV row counts match expected total
- Check for missing runs in log directory
## Performance Notes
### Timing Estimates
- **Minimal (1 run/config)**: 30-45 minutes
- **Standard (30 runs/config)**: 2-4 hours
- **High-precision (100 runs/config)**: 6-12 hours
- **Depends on:** CPU speed, disk I/O, test harness overhead
### Optimization
- Run overnight for high-precision experiments
- Use `screen` or `tmux` to keep session alive
- Monitor disk space (~100 MB for results + logs)
## Next Steps
1. **Run the experiment:**
```bash
./scripts/run_factorial_doe.sh 2025_11_19_COMPLETE_FACTORIAL
```
2. **Monitor progress:**
```bash
tail -f /home/rajames/CLionProjects/StarForth-DoE/experiments/2025_11_19_COMPLETE_FACTORIAL/experiment_summary.txt
```
3. **Move to analysis repo** (once complete):
```bash
cp -r /home/rajames/CLionProjects/StarForth-DoE/experiments/2025_11_19_COMPLETE_FACTORIAL \
/home/rajames/CLionProjects/StarForth-DoE-Analysis/data/
cd /home/rajames/CLionProjects/StarForth-DoE-Analysis
python3 analyze_factorial.py data/2025_11_19_COMPLETE_FACTORIAL/experiment_results.csv
```
4. **Generate reports:**
- Main effects plots
- Interaction matrices
- Pareto optimal configurations
- Recommendations for production builds
---
**Version:** 1.0
**Last Updated:** 2025-11-19
**Author:** Claude Code (with Robert A. James)
@@ -1,401 +0,0 @@
<!-- Moved from docs/02-experiments/factorial-doe/incompatible-configs.md to docs/working/experiments/02-experiments/factorial-doe/incompatible-configs.md on 2026-06-16 (docs reorg Phase 2) -->
> **⚠ OBSOLETE (flagged 2026-10-09).** Build-time incompatibility handling for the retired 2^6
> toggle experiment, replaced by the L8 Jacquard mode selector. Kept for historical record.
# Handling Incompatible Configurations in Factorial DoE
## Overview
Your intuition was correct: **some loop combinations can't coexist**. Rather than failing the entire experiment, the updated script treats incompatibility as valuable data.
This document explains how the script detects and records incompatible configurations during the factorial experiment.
---
## What is an Incompatible Configuration?
An incompatible configuration is a combination of feedback loop toggles that:
1. **Fails to compile** - Makefile target produces build errors
2. **Causes StarForth to crash** - Binary exits abnormally (segfault, signal, etc.)
3. **Produces invalid output** - Test harness runs but produces no metrics
Examples (hypothetical):
- Loop 5 (window inference) without Loop 2 (rolling window) = no data to infer from
- Loop 6 (decay inference) without Loop 3 (linear decay) = no slope to measure
- Certain combinations trigger memory conflicts or uninitialized states
---
## How the Script Handles It
### Detection Strategy
**Build Phase:**
```
Configuration X attempted
├─ make TARGET=fastest ENABLE_LOOP_1=1 ...ENABLE_LOOP_6=0
├─ Check exit code
├─ If success: mark as "OK", proceed to testing
└─ If failure: mark as "BUILD_FAILED", skip all runs for this config
```
**Execution Phase:**
```
Run configuration X, iteration N
├─ Execute: ./starforth --doe-experiment
├─ Check exit code
├─ Try to extract metrics from output
├─ If success: mark as "OK", record metrics
└─ If failure: mark as "CRASH", record incompatibility
```
### CSV Recording
Incompatible configurations are recorded with status flags:
**Successful run (viable config):**
```
timestamp,configuration,run_number,build_status,run_status,
2025-11-19T10:30:45,1_1_0_1_0_0,5,OK,OK,
<35+ metrics>,1,1,0,1,0,0
```
**Build failure (incompatible):**
```
timestamp,configuration,run_number,build_status,run_status,
2025-11-19T10:35:20,0_1_1_1_1_0,1,BUILD_FAILED,<skipped>,
<empty>,0,1,1,1,1,0
```
**Runtime crash (incompatible):**
```
timestamp,configuration,run_number,build_status,run_status,
2025-11-19T10:40:15,1_0_0_1_1_1,3,OK,CRASH,
<empty>,1,0,0,1,1,1
```
### Experiment Flow
The experiment **continues** when incompatibility is detected:
```
Run 1: Config A (OK) → Test completes, metrics recorded
Run 2: Config B (BUILD_FAILED) → Build fails, skip all runs for B, continue
Run 3: Config A (OK) → Test completes, metrics recorded
Run 4: Config C (CRASH) → Crashes, record as incompatible, continue
...
Run N: Config D (OK) → Test completes, metrics recorded
SUMMARY:
✓ Successful runs: 847/1920
✗ Failed configs: 17 (incompatible combinations detected)
✓ Experiment completed despite incompatibilities
```
---
## CSV Schema
### Two New Columns
| Column | Values | Meaning |
|--------|--------|---------|
| `build_status` | OK, BUILD_FAILED | Did Makefile compilation succeed? |
| `run_status` | OK, CRASH | Did StarForth execute without crashing? |
### Analysis Filter Rules
```python
import pandas as pd
df = pd.read_csv('experiment_results.csv')
# Only viable configurations
viable = df[(df['build_status'] == 'OK') & (df['run_status'] == 'OK')]
# Incompatible configs (at compile time)
build_failures = df[df['build_status'] == 'BUILD_FAILED']
# Incompatible configs (at runtime)
runtime_crashes = df[df['run_status'] == 'CRASH']
# All incompatible
incompatible = df[(df['build_status'] != 'OK') | (df['run_status'] != 'OK')]
```
---
## Interpreting Results
### Experiment Summary
After completion, you'll see:
```
═══════════════════════════════════════════════════════════
EXPERIMENT COMPLETE (with incompatibility detection)
═══════════════════════════════════════════════════════════
✓ Successful runs: 1847/1920
✓ Failed configurations (incompatible): 17
→ Incompatible configurations detected:
✗ 0_1_1_1_1_0 (BUILD_FAILED)
✗ 1_0_0_1_1_1 (CRASH)
✗ 0_1_0_1_1_1 (CRASH)
... (total 17)
→ These configs are marked in CSV with build_status or run_status
→ They represent configurations where loops cannot coexist without conflict
```
### What This Means
- **1847 successful runs** = viable, measurable configurations
- **17 incompatible** = design space constraints discovered
- **Experiment completed** = data collection succeeded despite incompatibilities
- **CSV includes both** = full picture of parameter space
---
## Analysis Examples
### Example 1: Summary by Viability
```python
import pandas as pd
df = pd.read_csv('experiment_results.csv')
# Count by status
viable = len(df[(df['build_status'] == 'OK') & (df['run_status'] == 'OK')])
build_fail = len(df[df['build_status'] == 'BUILD_FAILED'].drop_duplicates('configuration'))
runtime_crash = len(df[df['run_status'] == 'CRASH'].drop_duplicates('configuration'))
print(f"Viable configurations: {viable} runs")
print(f"Build failures: {build_fail} configurations")
print(f"Runtime crashes: {runtime_crash} configurations")
print(f"Total tested: {64} configurations, {64*30} runs")
print(f"Success rate: {viable}/{len(df)} = {100*viable/len(df):.1f}%")
```
Expected output:
```
Viable configurations: 1847 runs
Build failures: 5 configurations
Runtime crashes: 12 configurations
Total tested: 64 configurations, 1920 runs
Success rate: 1847/1920 = 96.2%
```
### Example 2: Identify Problematic Loop Combinations
```python
# Find configs that always crash
crashes = df[df['run_status'] == 'CRASH'].drop_duplicates('configuration')
print("Configurations that crash:")
for config in crashes['configuration']:
parts = config.split('_')
loops_on = [f"Loop{i+1}" for i, bit in enumerate(parts) if bit == '1']
print(f" {config}: {', '.join(loops_on) or 'None'}")
```
Expected output:
```
Configurations that crash:
0_1_1_1_1_0: Loop2, Loop3, Loop4, Loop5
1_0_0_1_1_1: Loop1, Loop4, Loop5, Loop6
0_1_0_1_1_1: Loop2, Loop4, Loop5, Loop6
... etc
```
**Insight:** Loop 5 (window inference) appears in many crashes → suggests window inference has prerequisites or conflicts.
### Example 3: Feasibility Matrix
```python
# Which loop combinations are feasible?
df_viable = df[(df['build_status'] == 'OK') & (df['run_status'] == 'OK')].copy()
# For each config, mark as viable (1) or not (0)
feasible = {}
for config in df['configuration'].unique():
is_viable = len(df_viable[df_viable['configuration'] == config]) > 0
feasible[config] = 1 if is_viable else 0
# Show as binary matrix
print("Feasibility (1=viable, 0=incompatible):")
for i in range(64):
config = f"{i%2}_{(i//2)%2}_{(i//4)%2}_{(i//8)%2}_{(i//16)%2}_{(i//32)%2}"
print(f"{config}: {'✓' if feasible[config] else '✗'}")
```
Result: Feasibility map of 64 configurations for maintenance and design decisions.
---
## Maintenance Insights
### Why Some Combinations Are Incompatible
Common reasons:
1. **Missing Prerequisites:**
- Loop 5 (window inference) needs Loop 2 (rolling window) data
- Loop 6 (decay inference) needs Loop 3 (linear decay) to measure
2. **Memory/Resource Conflicts:**
- Multiple loops allocating same memory region
- Circular dependencies in initialization
3. **Semantic Contradictions:**
- Loop X disables feature that Loop Y requires
- Incompatible macro definitions
4. **Bugs in Specific Combinations:**
- Race conditions appearing only with certain feature combinations
- Uninitialized state when paths not taken
### Using This Information
**For Future Development:**
```
Findings: Loop 5 incompatible without Loop 2
→ Check: Does window inference code assume rolling window exists?
→ Action: Either add guard (if !LOOP_5 without LOOP_2) or document prerequisite
→ Test: Add compile-time or runtime check
```
**For Optimization:**
```
Result: All configs with Loop 6 crash if Loop 1 is OFF
→ Issue: Decay inference needs heat tracking for slope calculation
→ Fix: Add LOOP_1_REQUIRED_BY_LOOP_6 guard in code
→ Verify: Re-run factorial (expect fewer crashes)
```
**For Production:**
```
Feasible subset: {0_0_0_0_0_0, 1_1_0_0_0_0, 1_1_1_0_0_0, 1_1_1_1_1_1}
→ Recommendation: Use 1_1_1_1_1_1 (all compatible, best performance)
→ Fallback: Use 1_1_0_0_0_0 (compatible, simpler)
→ Baseline: Use 0_0_0_0_0_0 (no feedback, minimal overhead)
```
---
## Next Steps
1. **Run the experiment** to completion (incompatibilities are discovered organically)
2. **Analyze the CSV** to identify which configs are viable
3. **Examine incompatible configs** to understand why they fail
4. **Document prerequisites** (which loops require others)
5. **Fix or guard** the code to eliminate incompatibilities
6. **Re-run** experiment to validate fixes
---
## Technical Details
### Build Failure Detection
```bash
if make TARGET=fastest ENABLE_LOOP_1=1 ... > build_log 2>&1; then
# Build successful
BUILD_STATUS="OK"
else
# Build failed - mark incompatible
BUILD_STATUS="BUILD_FAILED"
# Skip all runs for this config
continue
fi
```
### Runtime Crash Detection
```bash
if "${BINARY}" --doe-experiment > run_log 2>&1; then
# Binary executed (exit code 0)
if extract_csv_row(run_log) succeeds:
RUN_STATUS="OK"
else:
RUN_STATUS="CRASH" # Binary ran but no output
else
# Binary crashed (non-zero exit)
RUN_STATUS="CRASH"
fi
```
### CSV Recording
```bash
# Viable run
printf '%s,%s,%s,%s,%s,%s,%s\n' \
"${timestamp}" "${config}" "${run_num}" \
"OK" "OK" \
"${csv_row}" "${flags}" >> results_run_01_2025_12_08.csv
# Incompatible run
printf '%s,%s,%s,%s,%s,%s\n' \
"${timestamp}" "${config}" "${run_num}" \
"${build_status}" "${run_status}" \
"${flags}" >> results_run_01_2025_12_08.csv
```
---
## Example: Real Incompatibility
**Scenario:** Loop 5 (window inference) without Loop 2 (rolling window)
```c
// Loop 5: Window Inference Code
void vm_tick_inference_engine(VM *vm) {
// Try to infer window width from rolling window
RollingWindowOfTruth *window = &vm->rolling_window; // LOOP_2 data
// BUG: If LOOP_2 is OFF, window is uninitialized!
uint32_t window_pos = window->window_pos; // CRASH: uninitialized read
}
```
**Symptom in CSV:**
```
Configuration 0_0_0_0_1_0 marked as CRASH
Loop1=0, Loop2=0, Loop3=0, Loop4=0, Loop5=1, Loop6=0
↑ ↑
Missing prerequisite Problematic loop
```
**Fix:**
```c
// Add guard in code
#if ENABLE_LOOP_5_WINDOW_INFERENCE
#if !ENABLE_LOOP_2_ROLLING_WINDOW
#error "Loop 5 requires Loop 2: window inference needs rolling window data"
#endif
#endif
```
**Result after fix:** Configuration 0_0_0_0_1_0 removed from viable set (prevented at compile time), reduces crash count.
---
## Summary
| Aspect | Approach |
|--------|----------|
| **Detection** | Build exit codes + runtime crash detection |
| **Recording** | New `build_status` and `run_status` columns |
| **Handling** | Skip incompatible config, continue experiment |
| **Impact** | Partial dataset (viable configs) + incompatibility map |
| **Analysis** | Filter by status, identify patterns |
| **Action** | Fix code, re-run, measure improvement |
This design enables discovering incompatibilities without sacrificing experimental continuity.
---
**Version:** 1.0
**Last Updated:** 2025-11-19
**Status:** Integrated into run_factorial_doe.sh
@@ -1,268 +0,0 @@
<!-- Moved from docs/02-experiments/factorial-doe/index.md to docs/working/experiments/02-experiments/factorial-doe/index.md on 2026-06-16 (docs reorg Phase 2) -->
> **⚠ OBSOLETE (flagged 2026-10-09).** Index for the retired 2^6 factorial toggle
> architecture, replaced by the L8 Jacquard mode selector. Kept for historical record.
# StarForth Complete 2^6 Factorial DoE - Documentation Index
## Overview
This is a **one-and-done comprehensive factorial design of experiments** to systematically test all 64 possible combinations of 6 feedback loops.
**Key insight:** Don't toggle back-and-forth. Instead, systematically test ALL 2^6 combinations in a single experiment, then analyze the results afterward.
## Documentation Structure
### 1. **START HERE** 📋
- **File:** `docs/QUICK_START_FACTORIAL_DoE.md`
- **Purpose:** Entry point for quick reference
- **Read time:** 5 minutes
- **Contains:**
- 3 command options (quick/standard/high-precision)
- Time estimates
- What to expect during execution
- Basic troubleshooting
### 2. **Complete Guide** 📚
- **File:** `docs/FACTORIAL_DoE_GUIDE.md`
- **Purpose:** Full design documentation and rationale
- **Read time:** 20-30 minutes
- **Contains:**
- Why factorial DoE (vs. incremental tuning)
- 6 feedback loop definitions
- Configuration naming scheme (0_0_0_0_0_0 → 1_1_1_1_1_1)
- Design choices explained
- Expected results and patterns
- Troubleshooting guide
- Performance notes
### 3. **Analysis Workflow** 🔍
- **File:** `docs/FACTORIAL_DoE_ANALYSIS_WORKFLOW.md`
- **Purpose:** End-to-end pipeline from collection to insights
- **Read time:** 15-20 minutes (or 1-2 hours if coding along)
- **Contains:**
- Phase 1: Data collection (run script)
- Phase 2: Data transfer (to analysis repo)
- Phase 3: Statistical analysis (main effects, interactions)
- Phase 4: Reporting (generating insights)
- Python code examples:
* Load data and validate
* Compute main effects (individual loop impacts)
* Detect interactions (loop synergies)
* Optimize configuration subset
* Visualization code
- Example report structure
- Checklist for completion
### 4. **Main Script** ⚙️
- **File:** `scripts/run_factorial_doe.sh`
- **Purpose:** Executable for running the entire experiment
- **Read time:** 10 minutes (code + comments)
- **Contains:**
- Generates all 64 configurations
- Rebuilds for each unique configuration
- Randomizes execution order (1,920+ runs)
- Collects metrics to CSV
- Full instrumentation and progress reporting
---
## Reading Guide by Use Case
### "I just want to run the experiment"
1. Read: `QUICK_START_FACTORIAL_DoE.md` (5 min)
2. Execute: Choose command option (quick/standard/high-precision)
3. Wait: 30 min - 12 hours depending on option
4. Done!
### "I want to understand the design before running"
1. Read: `QUICK_START_FACTORIAL_DoE.md` (5 min)
2. Read: `FACTORIAL_DoE_GUIDE.md` (30 min)
3. Execute: Run with confidence
4. Analyze: Follow `FACTORIAL_DoE_ANALYSIS_WORKFLOW.md`
### "I need to analyze the results"
1. Read: `QUICK_START_FACTORIAL_DoE.md` → understand naming
2. Read: `FACTORIAL_DoE_ANALYSIS_WORKFLOW.md` → statistical methods
3. Code along: Use Python examples in Analysis Workflow
4. Interpret: Reference `FACTORIAL_DoE_GUIDE.md` for expected patterns
### "I want complete understanding"
Read in order:
1. `QUICK_START_FACTORIAL_DoE.md` (overview)
2. `FACTORIAL_DoE_GUIDE.md` (design rationale)
3. `scripts/run_factorial_doe.sh` (implementation)
4. `FACTORIAL_DoE_ANALYSIS_WORKFLOW.md` (analysis methods)
---
## Quick Command Reference
### Run Experiment
```bash
# Option A: Quick validation (30 min)
./scripts/run_factorial_doe.sh --runs-per-config 1 TEST_RUN
# Option B: Standard production (2-4 hours)
./scripts/run_factorial_doe.sh --runs-per-config 30 2025_11_19_FULL_FACTORIAL
# Option C: High-precision overnight (6-12 hours)
./scripts/run_factorial_doe.sh --runs-per-config 100 HIGH_PRECISION_FACTORIAL
```
### Monitor Progress
```bash
# Check results_run_01_2025_12_08 CSV growth
watch -n 5 'wc -l /path/to/experiment_results.csv'
# View summary
tail /path/to/experiment_summary.txt
# Count completed configurations
ls /path/to/run_logs/build_*.log | wc -l
```
### Analyze Results
```bash
# Load and analyze in Python
cd /path/to/StarForth-DoE-Analysis
python3 << 'EOF'
import pandas as pd
df = pd.read_csv('data/2025_11_19_FULL_FACTORIAL/experiment_results.csv')
print(f"Loaded {len(df)} runs")
# See FACTORIAL_DoE_ANALYSIS_WORKFLOW.md for full analysis code
EOF
```
---
## Key Concepts
### 64 Configurations
Each configuration is a unique combination of 6 binary factors:
- **0_0_0_0_0_0** = Baseline (all loops OFF)
- **1_0_0_0_0_0** = Only heat tracking
- **1_1_0_0_0_0** = Heat + rolling window
- ...
- **1_1_1_1_1_1** = All loops ON (current optimal)
### Randomized Execution
All 1,920+ runs (64 configs × 30 samples) are randomized into a single execution matrix.
- Eliminates temporal bias
- Prevents thermal ramp (if CPU heats during night)
- Proper DoE methodology
### One-and-Done Collection
Collect all data in single experiment, analyze separately.
- No tuning during collection (avoid bias)
- Single pass through parameter space
- Separates measurement from analysis
### Main Effects
Individual impact of each loop:
```
Loop #1: Loop turns ON, average metric changes by X%
Loop #2: Loop turns ON, average metric changes by Y%
... etc
```
### Interactions
How loops work together (amplify or suppress each other):
```
Loop #1 + Loop #2: Do they work better together than independently?
```
---
## Files at a Glance
| File | Size | Purpose |
|------|------|---------|
| `scripts/run_factorial_doe.sh` | 19 KB | Main executable script |
| `docs/QUICK_START_FACTORIAL_DoE.md` | 4.9 KB | Quick reference |
| `docs/FACTORIAL_DoE_GUIDE.md` | 8.0 KB | Design documentation |
| `docs/FACTORIAL_DoE_ANALYSIS_WORKFLOW.md` | 9.6 KB | Analysis pipeline |
| `docs/FACTORIAL_DoE_INDEX.md` | This file | Documentation index |
---
## Execution Timeline
### Phase 1: Preparation (5-10 min)
- Read `QUICK_START_FACTORIAL_DoE.md`
- Choose execution option
- Run command
### Phase 2: Collection (30 min - 12 hours)
- Script builds all 64 configs (as needed)
- Randomized execution of 1,920+ runs
- Metrics collected to CSV
- Progress shown every run
### Phase 3: Transfer (5 min)
- Copy results to analysis repo
- Verify file integrity
### Phase 4: Analysis (1-2 hours)
- Load CSV in Python
- Compute main effects
- Detect interactions
- Find optimal configurations
- Generate visualizations
### Phase 5: Reporting (1-2 hours)
- Summarize findings
- Create report
- Recommendations for production
**Total time:** Collection (2-4 hrs) + Analysis (2-4 hrs) = **4-8 hours**
---
## The Design Philosophy
### Problem Statement
How do we systematically understand the performance contribution and interactions of 6 interdependent feedback loops?
### Solution
**Complete 2^6 factorial design of experiments**
### Why This Approach
1. **Complete:** Tests all 64 combinations (no gaps)
2. **Systematic:** Proper experimental design methodology
3. **Separable:** Collection ≠ Analysis (reduces bias)
4. **Reproducible:** Randomized execution, full logging
5. **Scalable:** Single script, extensible to more factors
### Alternatives Rejected
- ❌ Incremental tuning ("baseline → +loop1 → +loop1+2"): Misses interactions
- ❌ Dynamic toggle without rebuild: Introduces state contamination
- ❌ Partial factorial: Misses key interactions
- ❌ One-at-a-time testing: Expensive and confounded
---
## Next Steps
1. **Read** `QUICK_START_FACTORIAL_DoE.md` (5 minutes)
2. **Decide** which execution option (quick/standard/precise)
3. **Run** the command and let it collect data
4. **Monitor** progress in another terminal
5. **Analyze** results using `FACTORIAL_DoE_ANALYSIS_WORKFLOW.md`
6. **Report** findings to stakeholders
---
## Questions?
- **"How long will this take?"** → See `QUICK_START_FACTORIAL_DoE.md`
- **"What does configuration X mean?"** → See `FACTORIAL_DoE_GUIDE.md`
- **"How do I analyze the results?"** → See `FACTORIAL_DoE_ANALYSIS_WORKFLOW.md`
- **"Why this approach?"** → See `FACTORIAL_DoE_GUIDE.md` - "The Why"
- **"How do I run it?"** → `./scripts/run_factorial_doe.sh`
---
**Version:** 1.0
**Created:** 2025-11-19
**Status:** Ready for production use
@@ -1,297 +0,0 @@
<!-- Moved from docs/02-experiments/factorial-doe/workflow.md to docs/working/experiments/02-experiments/factorial-doe/workflow.md on 2026-06-16 (docs reorg Phase 2) -->
> **⚠ HISTORICAL (flagged 2026-10-09).** Documents the completed 2025-11-19 factorial
> campaign whose data informed the current L8 design; the toggle architecture it runs on is
> retired. Kept as a record.
# Factorial DoE Analysis Workflow
## End-to-End Process
```
StarForth Repo
└─ ./scripts/run_factorial_doe.sh
│
├─ Builds all 64 configurations
├─ Runs 1,920+ tests (randomized)
└─ Outputs: experiment_results.csv + logs
│
└─→ Copy to StarForth-DoE-Analysis repo
│
├─ Load CSV into Python/R
├─ Separate factors from metrics
├─ Compute main effects & interactions
├─ Identify optimal subsets
└─ Generate reports & visualizations
```
## Phase 1: Data Collection (StarForth Repo)
### Step 1: Run Experiment
```bash
cd /home/rajames/CLionProjects/StarForth
# Quick validation (optional)
./scripts/run_factorial_doe.sh --runs-per-config 1 TEST_01
# Full production run
./scripts/run_factorial_doe.sh --runs-per-config 30 2025_11_19_FULL_FACTORIAL
```
### Step 2: Wait for Completion
Monitor in another terminal:
```bash
watch -n 5 'wc -l /home/rajames/CLionProjects/StarForth-DoE/experiments/2025_11_19_FULL_FACTORIAL/experiment_results.csv'
```
Expected progression:
- 0 min: 1 line (header)
- 30 min: ~500 lines
- 2 hours: ~1,500 lines
- 2.5 hours: 1,921 lines (complete)
### Step 3: Verify Completion
```bash
tail /home/rajames/CLionProjects/StarForth-DoE/experiments/2025_11_19_FULL_FACTORIAL/experiment_results.csv
echo "---"
cat /home/rajames/CLionProjects/StarForth-DoE/experiments/2025_11_19_FULL_FACTORIAL/experiment_summary.txt
```
## Phase 2: Data Transfer & Setup
### Step 1: Copy to Analysis Repo
```bash
RESULTS_DIR="/home/rajames/CLionProjects/StarForth-DoE/experiments/2025_11_19_FULL_FACTORIAL"
ANALYSIS_REPO="/home/rajames/CLionProjects/StarForth-DoE-Analysis"
mkdir -p "$ANALYSIS_REPO/data"
cp -r "$RESULTS_DIR" "$ANALYSIS_REPO/data/2025_11_19_FULL_FACTORIAL"
```
### Step 2: Verify Transfer
```bash
ls -lah "$ANALYSIS_REPO/data/2025_11_19_FULL_FACTORIAL/"
wc -l "$ANALYSIS_REPO/data/2025_11_19_FULL_FACTORIAL/experiment_results.csv"
```
## Phase 3: Analysis (StarForth-DoE-Analysis Repo)
### Load Data (Python)
```python
import pandas as pd
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
# Load results_run_01_2025_12_08
df = pd.read_csv("data/2025_11_19_FULL_FACTORIAL/experiment_results.csv")
print(f"Loaded {len(df)} runs across {len(df.groupby('configuration'))} configurations")
# Separate factors and metrics
factors = df[['enable_loop_1_heat_tracking',
'enable_loop_2_rolling_window',
'enable_loop_3_linear_decay',
'enable_loop_4_pipelining_metrics',
'enable_loop_5_window_inference',
'enable_loop_6_decay_inference']]
metrics = df[['total_lookups', 'cache_hit_percent', 'vm_workload_duration_ns_q48', ...]]
print(f"Shape: {len(df)} rows × {len(df.columns)} columns")
print(f"Factors: {len(factors.columns)}")
print(f"Metrics: {len(metrics.columns)}")
```
### Main Effects Analysis
```python
# For each loop, compute average metric change when loop goes 0→1
main_effects = {}
for loop in factors.columns:
loop_name = loop.replace('enable_', '').replace('_', ' ').title()
metric_when_on = df[df[loop] == 1]['vm_workload_duration_ns_q48'].mean()
metric_when_off = df[df[loop] == 0]['vm_workload_duration_ns_q48'].mean()
effect = metric_when_on - metric_when_off
pct_change = 100 * (effect / metric_when_off)
main_effects[loop_name] = {
'effect': effect,
'pct_change': pct_change,
'on_mean': metric_when_on,
'off_mean': metric_when_off,
}
# Print sorted by impact
for loop, effect in sorted(main_effects.items(), key=lambda x: abs(x[1]['pct_change']), reverse=True):
print(f"{loop:30} {effect['pct_change']:+7.2f}% (ON: {effect['on_mean']:.0f}, OFF: {effect['off_mean']:.0f})")
```
Expected output:
```
Loop 6 Decay Inference +12.3% (ON: 8,800,000, OFF: 7,840,000)
Loop 5 Window Inference +8.5% (ON: 8,600,000, OFF: 7,920,000)
Loop 4 Pipelining Metrics +6.2% (ON: 8,400,000, OFF: 7,900,000)
Loop 3 Linear Decay +4.1% (ON: 8,200,000, OFF: 7,870,000)
Loop 2 Rolling Window +3.0% (ON: 8,100,000, OFF: 7,850,000)
Loop 1 Heat Tracking +2.1% (ON: 8,010,000, OFF: 7,850,000)
```
### Interaction Analysis
```python
# Two-way interactions: Do loops amplify/suppress each other?
interactions = {}
for i, loop_a in enumerate(factors.columns):
for j, loop_b in enumerate(factors.columns):
if i >= j:
continue # Only upper triangle
# Four cases: (0,0), (0,1), (1,0), (1,1)
case_00 = df[(df[loop_a] == 0) & (df[loop_b] == 0)]['vm_workload_duration_ns_q48'].mean()
case_01 = df[(df[loop_a] == 0) & (df[loop_b] == 1)]['vm_workload_duration_ns_q48'].mean()
case_10 = df[(df[loop_a] == 1) & (df[loop_b] == 0)]['vm_workload_duration_ns_q48'].mean()
case_11 = df[(df[loop_a] == 1) & (df[loop_b] == 1)]['vm_workload_duration_ns_q48'].mean()
# Interaction effect: (1,1) - (1,0) - (0,1) + (0,0)
interaction = (case_11 - case_10 - case_01 + case_00)
loop_a_name = loop_a.replace('enable_', '').replace('_', ' ')
loop_b_name = loop_b.replace('enable_', '').replace('_', ' ')
if abs(interaction) > 100000: # Only report large interactions
interactions[f"{loop_a_name} × {loop_b_name}"] = interaction
# Print sorted interactions
for pair, interaction in sorted(interactions.items(), key=lambda x: abs(x[1]), reverse=True):
print(f"{pair:40} {interaction:+12,.0f}")
```
### Optimal Configuration Search
```python
# Find best k configurations by performance metric
k = 10
best_configs = df.nlargest(k, 'vm_workload_duration_ns_q48')[
['configuration', 'vm_workload_duration_ns_q48', 'enable_loop_1_heat_tracking',
'enable_loop_2_rolling_window', 'enable_loop_3_linear_decay',
'enable_loop_4_pipelining_metrics', 'enable_loop_5_window_inference',
'enable_loop_6_decay_inference']
].drop_duplicates(subset=['configuration'])
print("Top 10 Configurations by Performance:")
print("─" * 100)
for idx, row in best_configs.iterrows():
loops = (
f"L1:{row['enable_loop_1_heat_tracking']} " +
f"L2:{row['enable_loop_2_rolling_window']} " +
f"L3:{row['enable_loop_3_linear_decay']} " +
f"L4:{row['enable_loop_4_pipelining_metrics']} " +
f"L5:{row['enable_loop_5_window_inference']} " +
f"L6:{row['enable_loop_6_decay_inference']}"
)
print(f"{row['configuration']:20} | {row['vm_workload_duration_ns_q48']:12,.0f} ns | {loops}")
```
### Visualization Example
```python
# Main effects plot
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
fig.suptitle('2^6 Factorial DoE Main Effects', fontsize=16)
loop_names = [f"Loop {i+1}" for i in range(6)]
metric_key = 'vm_workload_duration_ns_q48'
for idx, loop in enumerate(factors.columns):
ax = axes[idx // 3, idx % 3]
off_mean = df[df[loop] == 0][metric_key].mean()
on_mean = df[df[loop] == 1][metric_key].mean()
bars = ax.bar(['OFF', 'ON'], [off_mean, on_mean], color=['lightcoral', 'lightgreen'])
ax.set_ylabel('Workload Duration (ns)')
ax.set_title(f'{loop_names[idx]}')
ax.set_ylim([7000000, 10000000])
# Add value labels
for bar in bars:
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height,
f'{height/1e6:.1f}M', ha='center', va='bottom')
plt.tight_layout()
plt.savefig('main_effects.png', dpi=150)
print("Saved: main_effects.png")
```
## Phase 4: Reporting
### Key Outputs to Generate
1. **Main Effects Summary** (which loops matter most?)
- Table of individual loop impacts
- Sorted by effect size
2. **Interaction Matrix** (do loops amplify/suppress?)
- 6×6 table of two-way interactions
- Highlights synergies/conflicts
3. **Optimal Configuration** (best 10-20 subsets)
- Recommend top 3-5 production configs
- Show Pareto frontier (performance vs complexity)
4. **Statistical Summary**
- Variance by configuration
- Confidence intervals on estimates
- Regression model R²
### Example Report Structure
```markdown
# Factorial DoE Analysis Report - 2025-11-19
## Executive Summary
- Tested all 64 feedback loop combinations
- 1,920 experimental runs collected
- Key finding: Loops 5+6 show strong synergy (+18% combined)
## Main Effects
| Loop | Impact | Confidence | Recommendation |
|------|--------|-----------|---|
| Loop 6 (Decay Inference) | +12.3% | p<0.001 | **Enabled** |
| Loop 5 (Window Inference) | +8.5% | p<0.001 | **Enabled** |
| Loop 4 (Pipelining) | +6.2% | p<0.01 | Enabled |
| Loop 3 (Decay) | +4.1% | p<0.05 | Enabled |
| Loop 2 (Window) | +3.0% | p<0.05 | Enabled |
| Loop 1 (Heat) | +2.1% | p>0.05 | Optional |
## Recommended Configurations
1. **111111** (All loops): +30% vs baseline
2. **111110** (Except heat): +28% vs baseline, simpler
3. **011111** (Except heat, decay): +24% vs baseline
## Trade-off Analysis
- Full stack (111111): Best performance, highest code complexity
- Recommended (111110): 28% improvement, 95% of benefit
- Minimal set (001110): 18% improvement, 50% code overhead
---
```
## Checklist
- [ ] Run factorial DoE script to completion
- [ ] Verify 1,920 rows in experiment_results.csv
- [ ] Copy results to analysis repo
- [ ] Load data in Python/R
- [ ] Compute main effects (6 loops)
- [ ] Compute interactions (2-way or higher)
- [ ] Identify top 5-10 configurations
- [ ] Generate visualizations
- [ ] Write summary report
- [ ] Decide on production configuration
---
**Timeline:** Collection (2-4 hrs) + Analysis (2-4 hrs) = **6-8 hours total** for comprehensive factorial DoE.
@@ -1,341 +0,0 @@
<!-- Moved from docs/02-experiments/heartbeat-doe/corrected-design.md to docs/working/experiments/02-experiments/heartbeat-doe/corrected-design.md on 2026-06-16 (docs reorg Phase 2) -->
> **⚠ SUPERSEDED (flagged 2026-10-09).** Superseded by `phase2-corrected.md` (the
> "corrected and finalized" revision). Kept for historical record.
# StarForth Heartbeat DoE – CORRECTED Design (Heartrate Focus)
**Date:** November 20, 2025
**CORRECTION:** Previous design missing critical metric: **HEARTBEAT RATE (tick_ns)**
**Status:** Ready for implementation
---
## The Critical Insight You Caught
The heartbeat isn't just a **fixed tick**. The system has a **variable heartbeat rate** controlled by:
```c
/* HeartbeatWorker.tick_ns — this is DYNAMIC, not constant! */
uint64_t tick_ns; /* Nanoseconds between ticks — VARIES with load! */
```
**What this means:**
- Default: `HEARTBEAT_TICK_NS` (~1 millisecond)
- **But:** The inference engine can adaptively adjust `tick_target_ns`
- Different configurations may modulate heartbeat frequency differently
- **This is the KEY physics behavior we must measure!**
---
## Stage 1 Problem (What We Got Wrong)
Stage 1 baseline ran **without heartbeat thread** (HEARTBEAT_THREAD_ENABLED=0):
- Fixed 1ms tick (no modulation)
- No adaptive heartrate
- Doesn't reflect StarForth's intended physics-driven behavior
**Stage 1 conclusion:** "System is deterministic, no heartbeat variation"
**Stage 1 was incomplete** because it disabled the very mechanism that makes the system adaptive!
---
## Stage 2 Correction (What We Should Do)
### Primary Hypothesis
The 5 elite configurations differ in **how they modulate heartbeat rate**:
| Config | Expected Behavior |
|--------|-------------------|
| 1_0_1_1_1_0 | Tight, responsive heartrate (low jitter, fast modulation) |
| 1_0_1_1_1_1 | Similar, with decay inference slowing response? |
| 1_1_0_1_1_1 | Window-driven, may have different modulation pattern |
| 1_0_1_0_1_0 | Lean, may have sluggish response |
| 0_1_1_0_1_1 | Window-without-heat, baseline behavior |
### Key Metrics for Heartrate (Not Just Intervals)
**We need to measure:**
1. **Heartbeat Rate Modulation (NOT just interval jitter)**
```
heartbeat_rate_trajectory = [tick_ns_t0, tick_ns_t1, tick_ns_t2, ...]
What we measure:
- Mean heartbeat rate (expected: ~1,000,000 ns = 1 kHz baseline)
- Responsiveness to load (does rate slow under heavy load?)
- Convergence time (how long until rate stabilizes?)
- Modulation range (min to max tick_ns observed)
```
2. **Heartrate vs. Workload Coupling**
```
For each tick:
workload_intensity = lookups_this_tick / lookups_per_second_baseline
heartrate_hz = 1_000_000_000 / tick_ns
Measure: correlation(workload_intensity, heartrate_hz)
- Positive correlation: heartrate slows under load (good physics)
- Negative correlation: heartrate speeds up under load (bad physics)
- No correlation: heartrate doesn't respond to load (broken physics)
```
3. **Heartrate Stability (Not "jitter" but "rate consistency")**
```
- Does heartrate converge to a steady state?
- How quickly does it adapt to workload changes?
- Does it oscillate or settle smoothly?
```
4. **Heartrate Efficiency**
```
- Is the system running faster/slower with adapted heartrate?
- Workload duration: should improve with optimized heartrate
- Memory: should be more efficient with tuned heartrate
```
---
## Corrected Metrics Collection
### Per-Tick Collection (During Execution)
```c
/* In heartbeat_thread_main(), before nanosleep() */
struct HeartbeatTickData {
uint64_t tick_number;
uint64_t tick_ns; /* THIS IS THE HEARTBEAT RATE! */
uint64_t workload_ops_this_tick; /* Dictionary lookups in this tick */
uint64_t wall_clock_ns; /* Actual wall-clock time */
uint64_t hot_word_count; /* Physics state */
uint64_t total_heat; /* Physics state */
};
/* Store in circular buffer or rolling window */
```
### Post-Run Analysis Metrics
```c
typedef struct {
/* === Original Metrics === */
/* ... existing 38 metrics ... */
/* === CORRECTED: Heartbeat Rate Metrics === */
/* Heartbeat rate trajectory */
uint64_t total_ticks_executed;
double mean_heartbeat_rate_hz; /* Hz = 1e9 / mean_tick_ns */
double heartbeat_rate_stddev_hz; /* Variation in Hz */
double heartbeat_rate_cv; /* Coefficient of variation */
uint64_t heartbeat_rate_min_ns; /* Slowest (highest tick_ns) */
uint64_t heartbeat_rate_max_ns; /* Fastest (lowest tick_ns) */
double heartbeat_rate_range_hz; /* max_hz - min_hz */
/* Heartrate vs Load Coupling */
double load_to_heartrate_correlation; /* Does rate respond to load? */
int load_response_direction; /* +1 = slower under load (good) */
double heartrate_adaptation_latency_ticks; /* Ticks until rate responds */
double heartrate_settling_time_ticks; /* Ticks to settle after change */
/* Heartrate Convergence */
double heartrate_convergence_time_ms; /* Time to reach stable rate */
int heartrate_converged; /* 1 = yes, 0 = still oscillating */
double heartrate_oscillation_amplitude_hz; /* Peak-to-peak variation */
/* Overall Heartrate Stability Score */
double heartrate_stability_score; /* 0-100: how stable is heartrate? */
} DoeMetrics_CORRECTED;
```
---
## The Missing Piece: Heartbeat Rate Logging
**Critical code addition needed:**
```c
/* In vm.c heartbeat_thread_main() - CAPTURE tick_ns BEFORE and AFTER */
static inline void capture_heartbeat_rate_sample(VM *vm, uint64_t tick_ns, uint64_t workload_ops) {
/* Store sample in rolling buffer */
if (!vm->heartbeat_rate_samples) {
vm->heartbeat_rate_samples = calloc(HEARTBEAT_RATE_SAMPLE_MAX, sizeof(HeartbeatRateSample));
}
HeartbeatRateSample *sample = &vm->heartbeat_rate_samples[vm->heartbeat_rate_sample_count];
sample->tick_number = vm->heartbeat.tick_count;
sample->tick_ns = tick_ns; /* THE HEARTBEAT RATE! */
sample->workload_ops = workload_ops;
sample->timestamp_ns = platform_monotonic_ns();
vm->heartbeat_rate_sample_count++;
}
/* In heartbeat_thread_main() */
static void* heartbeat_thread_main(void *arg) {
VM *vm = (VM*)arg;
while (!vm->heartbeat.worker->stop_requested) {
uint64_t tick_start_ns = platform_monotonic_ns();
vm_heartbeat_run_cycle(vm); /* May adjust tick_ns */
uint64_t tick_ns = vm->heartbeat.worker->tick_ns;
uint64_t workload_ops = vm->execution_heat_current_tick; /* Need to track */
/* CAPTURE THE HEARTBEAT RATE! */
capture_heartbeat_rate_sample(vm, tick_ns, workload_ops);
/* Sleep */
nanosleep(...);
}
}
```
---
## Corrected Analysis (R Script)
```r
# Load heartbeat rate samples from CSV
doe_data <- read.csv("experiment_results_heartbeat.csv") %>%
mutate(
# Convert heartbeat rates (tick_ns → Hz)
mean_heartbeat_hz = 1e9 / mean_heartbeat_rate_ns,
heartrate_stability = 1 / (1 + heartbeat_rate_cv), # Lower CV = higher score
)
# PRIMARY ANALYSIS: Heartrate Stability
stability_by_heartrate <- doe_data %>%
group_by(configuration) %>%
summarize(
mean_heartrate_hz = mean(mean_heartbeat_hz),
mean_heartrate_stability = mean(heartrate_stability),
mean_load_coupling = mean(load_to_heartrate_correlation),
mean_workload_ms = mean(vm_workload_duration_ns_q48 / 1e6)
) %>%
mutate(
# Golden config = best heartrate stability + strongest load coupling
golden_score = (heartrate_stability * 0.5) + (load_coupling * 0.3) + (workload_improvement * 0.2)
)
# Golden Config = Highest golden_score
```
---
## Why This Matters
### Stage 1 Got Lucky
Stage 1 found 5 good configs **by accident** because it measured final performance, not physics.
### Stage 2 Should Be Different
Stage 2 should measure **whether the physics is working correctly**:
- **Can the system modulate heartbeat rate** in response to load?
- **Is the modulation smooth** or does it oscillate?
- **Does heartrate modulation improve overall performance?**
- **Which configuration has the most responsive, stable heartrate?**
**The golden configuration should be the one where heartbeat rate modulation is:**
1. ✓ **Responsive** (correlates with workload)
2. ✓ **Smooth** (doesn't oscillate wildly)
3. ✓ **Efficient** (results in best overall performance)
---
## Revised DoE Experiment Design
### What Actually Gets Measured
Instead of just "jitter in fixed 1ms tick intervals", measure:
**Per-Run Heartbeat Rate Trajectory:**
```
Tick 0: tick_ns = 1,000,000 (1 ms baseline)
Tick 1: tick_ns = 1,500,000 (slowing under load)
Tick 2: tick_ns = 2,000,000 (further slowdown)
Tick 10: tick_ns = 1,200,000 (adapting back)
Tick 100: tick_ns = 1,050,000 (converged to steady state)
```
**What we analyze:**
1. Did heartrate change at all? (some configs might keep it fixed)
2. How much did it vary? (CV of tick_ns, not CV of fixed intervals)
3. Did it respond to load? (correlation with workload)
4. Did it converge smoothly? (no oscillation)
5. Did it improve performance? (faster execution with adaptive heartrate?)
---
## Implementation Checklist (Revised)
- [ ] Add `HeartbeatRateSample` struct to capture `tick_ns` per tick
- [ ] Modify `heartbeat_thread_main()` to log `tick_ns` trajectory
- [ ] Extend `DoeMetrics` with heartrate-specific fields (not just interval jitter)
- [ ] Update CSV schema to include heartrate trajectory metrics
- [ ] Modify R analysis to focus on **heartrate modulation patterns**
- [ ] Update golden config selection: prioritize **load responsiveness**
---
## Expected Results (Corrected)
### Hypothesis: Configs Differ in Heartrate Modulation
**Config 1 (1_0_1_1_1_0) - Expected WINNER:**
- Heartrate slowly increases under load (tick_ns increases, Hz decreases)
- Smooth adaptation (no oscillation)
- Converges within 1000 ticks
- Performance improves (faster overall execution)
- Load coupling: 0.8+ (strong)
**Config 5 (0_1_1_0_1_1) - Expected LOSER:**
- Heartrate doesn't respond to load (fixed tick_ns)
- No correlation with workload
- Performance degrades (why? system can't adapt)
---
## Key Realization
The heartbeat isn't just a **timer**. It's a **feedback mechanism**:
```
Load increases
↓
Inference engine detects change
↓
Adjusts tick_target_ns (slower heartbeat = more thinking time)
↓
Heartbeat rate modulates (tick_ns increases)
↓
Physics engine gets more time to tune parameters
↓
System adapts and performance improves
```
**We need to measure whether this feedback loop is working!**
---
## Next Steps
1. **Extend HeartbeatWorker:** Add rate sample collection
2. **Modify heartbeat_thread_main():** Log `tick_ns` per tick
3. **Update DoeMetrics:** Heartrate trajectory analysis
4. **Rewrite CSV schema:** Focus on heartrate modulation
5. **Rewrite R analysis:** Measure load-response coupling and heartrate stability
6. **Re-run experiment:** Focus on configs that modulate heartrate best
---
**This changes the entire experiment focus.**
Instead of measuring "jitter in a fixed heartbeat", we're measuring "**how well does the system modulate its heartbeat in response to load?**"
That's the real physics story.
@@ -1,431 +0,0 @@
<!-- Moved from docs/02-experiments/heartbeat-doe/design.md to docs/working/experiments/02-experiments/heartbeat-doe/design.md on 2026-06-16 (docs reorg Phase 2) -->
> **⚠ SUPERSEDED (flagged 2026-10-09).** Pre-correction draft; superseded first by
> `corrected-design.md`, then by `phase2-corrected.md` (the finalized design). Kept for
> historical record.
# StarForth Factorial DoE with Heartbeat Integration
**Date:** November 20, 2025
**Status:** Design Phase
**Experiment:** 2^6 Factorial with Heartbeat-Aware Metrics Collection
---
## Overview
This document describes the **second-generation factorial DoE** that incorporates heartbeat observability into metric collection. Building on the baseline DOE (Stage 1: 3200 runs, 6 factors, no heartbeat), this experiment captures **per-tick heartbeat dynamics** to identify configurations with optimal stability characteristics.
### Key Innovation
Instead of measuring static performance, this experiment measures **temporal stability**:
- How quickly does the system converge under load changes?
- How much does heartbeat interval modulate in response to workload?
- What is the jitter envelope after settling?
- Which configuration achieves smoothest load→heartbeat coupling?
---
## Design Points (Top 5 Configurations from Stage 1)
Based on Stage 1 analysis, the elite configurations are:
1. **Config 1:** `1_0_1_1_1_0` (F1=ON, F2=OFF, F3=ON, F4=ON, F5=ON, F6=OFF)
2. **Config 2:** `1_0_1_1_1_1` (F1=ON, F2=OFF, F3=ON, F4=ON, F5=ON, F6=ON)
3. **Config 3:** `1_1_0_1_1_1` (F1=ON, F2=ON, F3=OFF, F4=ON, F5=ON, F6=ON)
4. **Config 4:** `1_0_1_0_1_0` (F1=ON, F2=OFF, F3=ON, F4=OFF, F5=ON, F6=OFF)
5. **Config 5:** `0_1_1_0_1_1` (F1=OFF, F2=ON, F3=ON, F4=OFF, F5=ON, F6=ON)
**Factors Mapping:**
- F1 = ENABLE_LOOP_1_HEAT_TRACKING
- F2 = ENABLE_LOOP_2_ROLLING_WINDOW
- F3 = ENABLE_LOOP_3_LINEAR_DECAY
- F4 = ENABLE_LOOP_4_PIPELINING_METRICS
- F5 = ENABLE_LOOP_5_WINDOW_INFERENCE
- F6 = ENABLE_LOOP_6_DECAY_INFERENCE
---
## New Heartbeat Metrics (Per-Tick Collection)
### 1. Heartbeat Interval Dynamics
```c
struct HeartbeatTickMetrics {
uint64_t tick_count; /* Absolute tick number */
uint64_t tick_interval_ns; /* Wall-clock duration of this tick */
uint64_t instantaneous_workload_ops; /* Dictionary lookups in this tick */
double instantaneous_load_ratio; /* (ops this tick) / (avg ops/tick) */
};
```
**Purpose:** Capture how heartbeat frequency modulates in response to workload intensity.
### 2. Cache Performance (Per-Tick Window)
```c
struct CacheWindowMetrics {
uint64_t tick_start;
uint64_t cache_hits_window; /* Hits in last 100 ticks */
uint64_t cache_attempts_window; /* Attempts in last 100 ticks */
double cache_hit_percent_window; /* Rolling window %, 0-100 */
};
```
**Purpose:** Measure cache stability, not just final hit rate.
### 3. Rolling Window Diagnostics
```c
struct WindowDiagnostics {
uint32_t effective_window_size; /* Current adaptive size */
uint64_t pattern_diversity; /* Unique word IDs observed */
double diversity_growth_rate; /* % new patterns per 1000 executions */
int is_warm; /* 1 if window is representative */
};
```
**Purpose:** Track window convergence and saturation.
### 4. Jitter Envelope
```c
struct JitterMetrics {
double tick_interval_mean_ns; /* Average heartbeat interval */
double tick_interval_stddev_ns; /* StdDev of interval */
double tick_interval_cv; /* Coefficient of variation (σ/μ) */
uint64_t outlier_count; /* Ticks >3σ from mean */
double outlier_ratio; /* Outliers / total ticks */
};
```
**Purpose:** Measure temporal smoothness and jitter control.
### 5. Decay Slope Tracking
```c
struct DecayTrajectory {
uint64_t tick_sample_count; /* How many ticks sampled */
double decay_slope_observed; /* Fitted slope from observations */
double slope_convergence_rate; /* Speed of slope convergence */
int slope_direction; /* -1=decreasing, 0=stable, +1=increasing */
};
```
**Purpose:** Monitor whether decay slope inference is tracking correctly.
### 6. Load-Response Coupling
```c
struct LoadResponseCoupling {
double correlation_load_to_interval; /* Correlation(workload, heartbeat_interval) */
double response_latency_ticks; /* Ticks until interval reacts to load change */
double overshoot_ratio; /* Peak response / steady-state response */
double settling_time_ticks; /* Ticks to settle within 10% of target */
};
```
**Purpose:** Measure how quickly and smoothly heartbeat responds to workload shifts.
---
## Extended DoeMetrics Struct
Add to `include/doe_metrics.h`:
```c
typedef struct {
/* === Original Metrics (preserved) === */
/* ... existing 35+ metrics ... */
/* === NEW: Heartbeat Stability Metrics === */
/* Tick dynamics */
uint64_t total_heartbeat_ticks; /* Total ticks during run */
double tick_interval_mean_ns; /* Average heartbeat interval */
double tick_interval_stddev_ns; /* StdDev of heartbeat interval */
double tick_interval_cv; /* Coefficient of variation */
uint64_t tick_outlier_count_3sigma; /* Ticks >3σ from mean */
double tick_outlier_ratio; /* Outliers / total ticks */
/* Cache stability window */
double cache_hit_percent_rolling_mean; /* Rolling avg, last 100 ticks */
double cache_hit_percent_rolling_stddev; /* Rolling stddev */
double cache_stability_index; /* How stable is cache hit %? */
/* Window convergence */
uint32_t window_final_effective_size; /* Final adaptive window size */
double pattern_diversity_final; /* Final unique patterns observed */
double diversity_growth_final; /* Growth rate at end */
int window_warmth_achieved; /* 1 = warm, 0 = still cold */
/* Decay slope convergence */
double decay_slope_fitted_final; /* Final fitted decay slope */
double decay_slope_convergence_rate; /* How fast did slope converge? */
int decay_slope_stable; /* 1 = stable direction, 0 = oscillating */
/* Load-response */
double load_interval_correlation; /* Correlation(load, heartbeat_interval) */
double response_latency_ticks; /* Ticks until response to workload change */
double overshoot_ratio; /* Peak response / steady-state */
double settling_time_ticks; /* Time to 10% settling band */
/* Aggregate stability score */
double overall_stability_score; /* Composite metric: (low jitter + fast convergence + tight correlation) */
} DoeMetrics; /* Now extends to ~65+ columns */
```
---
## Heartbeat Data Collection Architecture
### Per-Tick Logging (Lightweight)
During VM execution, capture minimal per-tick data:
```c
/* In vm.c vm_tick_heartbeat_worker() */
void vm_tick_heartbeat_worker(VM *vm) {
HeartbeatSnapshot snapshot = {
.published_tick = vm->heartbeat.tick_count,
.published_ns = platform_monotonic_ns(),
.window_width = vm->rolling_window.effective_window_size,
.decay_slope_q48 = vm->decay_slope_q48,
.hot_word_count = vm->hot_word_count_at_check,
.stale_word_count = vm->stale_word_count_at_check,
.total_heat = vm->total_heat_at_last_check,
};
/* Publish snapshot (double-buffered, lock-free read) */
int next_slot = 1 - vm->heartbeat.snapshot_index;
vm->heartbeat.snapshots[next_slot] = snapshot;
vm->heartbeat.snapshot_index = next_slot;
}
```
### Post-Run Analysis (Batch Processing)
After test execution completes, analyze collected tick data:
```c
/* In src/doe_metrics.c metrics_from_vm() */
void analyze_heartbeat_trajectory(VM *vm, DoeMetrics *metrics) {
HeartbeatSnapshot *snapshots = vm->heartbeat.snapshots;
uint64_t total_ticks = vm->heartbeat.tick_count;
/* 1. Compute tick interval statistics */
double intervals[total_ticks];
for (size_t i = 1; i < total_ticks; i++) {
intervals[i] = snapshots[i].published_ns - snapshots[i-1].published_ns;
}
metrics->tick_interval_mean_ns = compute_mean(intervals, total_ticks);
metrics->tick_interval_stddev_ns = compute_stddev(intervals, total_ticks);
metrics->tick_interval_cv = metrics->tick_interval_stddev_ns / metrics->tick_interval_mean_ns;
/* 2. Compute outlier ratio (ticks >3σ from mean) */
double threshold_3sigma = metrics->tick_interval_mean_ns + 3.0 * metrics->tick_interval_stddev_ns;
uint64_t outliers = 0;
for (size_t i = 0; i < total_ticks; i++) {
if (intervals[i] > threshold_3sigma) outliers++;
}
metrics->tick_outlier_ratio = (double)outliers / total_ticks;
/* 3. Compute decay slope convergence */
double slopes[total_ticks];
for (size_t i = 100; i < total_ticks; i++) { /* Skip warm-up */
slopes[i] = snapshots[i].decay_slope_q48 / 65536.0;
}
metrics->decay_slope_fitted_final = slopes[total_ticks - 1];
metrics->decay_slope_convergence_rate = compute_convergence_rate(slopes, total_ticks);
/* 4. Compute overall stability score */
double stability = 0.0;
stability += (100.0 - metrics->tick_interval_cv); /* Lower CV = higher score */
stability += (100.0 - metrics->tick_outlier_ratio * 100.0); /* Fewer outliers = higher */
stability += metrics->decay_slope_convergence_rate; /* Fast convergence = higher */
metrics->overall_stability_score = stability / 3.0;
}
```
---
## CSV Output Schema (Extended)
**New columns (per heartbeat experiment run):**
```
timestamp,configuration,run_number,
[... original 38 columns ...],
total_heartbeat_ticks,
tick_interval_mean_ns,tick_interval_stddev_ns,tick_interval_cv,
tick_outlier_count_3sigma,tick_outlier_ratio,
cache_hit_percent_rolling_mean,cache_hit_percent_rolling_stddev,cache_stability_index,
window_final_effective_size,pattern_diversity_final,diversity_growth_final,window_warmth_achieved,
decay_slope_fitted_final,decay_slope_convergence_rate,decay_slope_stable,
load_interval_correlation,response_latency_ticks,overshoot_ratio,settling_time_ticks,
overall_stability_score
```
**Total:** ~60 columns per run
---
## R Analysis Template
```r
library(tidyverse)
library(ggplot2)
library(gridExtra)
# Load heartbeat DoE data
doe_data <- read.csv("experiment_results_with_heartbeat.csv") %>%
mutate(
configuration = as.factor(configuration),
timestamp = as.POSIXct(timestamp)
)
# ========== MAIN ANALYSIS ==========
# 1. Stability Ranking
stability_rank <- doe_data %>%
group_by(configuration) %>%
summarize(
mean_stability_score = mean(overall_stability_score),
mean_jitter_cv = mean(tick_interval_cv),
mean_convergence_rate = mean(decay_slope_convergence_rate),
mean_settling_time = mean(settling_time_ticks),
n_runs = n()
) %>%
mutate(
rank = rank(-mean_stability_score, ties.method = "min")
) %>%
arrange(rank)
print("=== STABILITY RANKINGS ===")
print(stability_rank)
# 2. Visualization: Stability Scores
ggplot(doe_data, aes(x = reorder(configuration, overall_stability_score, FUN=median),
y = overall_stability_score,
fill = configuration)) +
geom_boxplot(alpha = 0.7) +
geom_jitter(width = 0.2, alpha = 0.4) +
coord_flip() +
theme_minimal() +
labs(title = "Overall Stability Scores by Configuration",
x = "Configuration",
y = "Stability Score (0-100)")
ggsave("01_stability_scores.png", width = 10, height = 6)
# 3. Visualization: Jitter Control (CV)
ggplot(doe_data, aes(x = reorder(configuration, tick_interval_cv, FUN=median),
y = tick_interval_cv,
fill = configuration)) +
geom_boxplot(alpha = 0.7) +
coord_flip() +
theme_minimal() +
labs(title = "Heartbeat Jitter (Coefficient of Variation)",
x = "Configuration",
y = "CV (lower = less jitter)")
ggsave("02_jitter_control.png", width = 10, height = 6)
# 4. Visualization: Convergence Speed
ggplot(doe_data, aes(x = reorder(configuration, decay_slope_convergence_rate, FUN=median),
y = decay_slope_convergence_rate,
fill = configuration)) +
geom_boxplot(alpha = 0.7) +
coord_flip() +
theme_minimal() +
labs(title = "Decay Slope Convergence Speed",
x = "Configuration",
y = "Convergence Rate (ticks)")
ggsave("03_convergence_speed.png", width = 10, height = 6)
# 5. Correlation: Load Response Coupling
ggplot(doe_data, aes(x = load_interval_correlation,
y = overall_stability_score,
color = configuration)) +
geom_point(alpha = 0.6, size = 2) +
facet_wrap(~configuration) +
theme_minimal() +
labs(title = "Load-Response Coupling vs Stability",
x = "Correlation(load, interval)",
y = "Overall Stability Score")
ggsave("04_load_response_coupling.png", width = 12, height = 8)
# 6. Statistical Test: Is top config significantly better?
top_config <- stability_rank$configuration[1]
other_configs <- stability_rank$configuration[-1]
top_data <- doe_data %>% filter(configuration == top_config) %>% pull(overall_stability_score)
other_data <- doe_data %>% filter(configuration %in% other_configs) %>% pull(overall_stability_score)
t_test <- t.test(top_data, other_data)
print(sprintf("\nT-test: %s vs others\np-value: %.4f", top_config, t_test$p.value))
# 7. Golden Configuration Summary
cat("\n=== GOLDEN CONFIGURATION CANDIDATE ===\n")
golden <- stability_rank %>% slice(1)
print(golden)
cat("\nInterpretation:")
cat(sprintf("\n- Lowest jitter (CV): %.4f", golden$mean_jitter_cv))
cat(sprintf("\n- Fastest convergence: %.1f ticks", golden$mean_convergence_rate))
cat(sprintf("\n- Settling time: %.1f ticks", golden$mean_settling_time))
cat(sprintf("\n- Recommendation: Use '%s' as golden configuration\n", golden$configuration))
```
---
## Execution Plan
### Phase 1: Implementation (3-4 hours)
1. Extend `DoeMetrics` struct with heartbeat columns
2. Implement `analyze_heartbeat_trajectory()` in `doe_metrics.c`
3. Capture snapshot data during heartbeat ticks
4. Update CSV schema
### Phase 2: Experiment Execution (2-4 hours)
1. Create `run_factorial_doe_with_heartbeat.sh` script
2. Run 5 elite configurations × 50 runs each = 250 total runs
3. Collect heartbeat + performance data
### Phase 3: Analysis (2-3 hours)
1. Load CSV in R
2. Compute stability rankings
3. Generate visualizations
4. Identify golden configuration
---
## Expected Outcomes
**Primary Question:** Which of the 5 elite configurations exhibits:
- Lowest heartbeat jitter (CV)?
- Fastest convergence to steady state?
- Strongest load-response correlation?
- Cleanest temporal signature?
**Expected Winner:** Configuration with balanced F-factors, likely:
- F1=1 (heat tracking required)
- F3=1 or F5=1 (decay or inference, but not both)
- F6=0 or F6=1 (decay inference optional)
**Golden Configuration:** Becomes baseline physics for MamaForth & PapaForth downstream work.
---
## Success Criteria
✓ All 5 configurations build and run without segfault
✓ Heartbeat data collected for all 250 runs
✓ Stability rankings computed and visualized
✓ Golden configuration identified with p < 0.05 significance
✓ R analysis report generated with 5+ plots
---
**Version:** 1.0
**Status:** Ready for implementation
**Next Step:** Implement heartbeat metrics capture and extended DoE script