Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure

Source tree reorganization:
- Move StarForth v3 engine to v3/ (src/, include/, Makefile)
- Move kernel to kernel/ (src/, include/, linker/, Makefile)
- Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md)
- Move FABRIC-0..4.md to docs/fabric/
- Move ONTOLOGY.md and ROADMAP.md to docs/

Board infrastructure:
- Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/
- Each board has board.mk (ISA, CPU flags, boot recipe) and README.md
- Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET
- make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board
- ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet)
- scripts/mkdiskimage.sh builds disk images for all boards

Docs pipeline:
- docs/book/ with LaTeX master (main.tex) and Makefile
- pandoc converts Markdown to LaTeX at build time
- Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks)
- make docs builds single PDF (754 pages, 0 missing characters)
- make docs TARGET=<board> adds board appendix
- build/docs/<book|board>/meta.tex stamps git commit into PDF

Bug fixes:
- 42 include paths that only worked by accident now use correct relative paths
- clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build)
- Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved
- Doxyfile, .clang-tidy, README.md, Kconfig paths updated

Verified:
- Hosted v3 build passes 1012 tests, 0 failures
- SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE)
- Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes
- make clean TARGET=<board> removes only that board and its ISA objects
- make all builds all boards, hosted v3, and docs in one run

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
rajames
2026-10-01 15:40:09 -04:00
co-authored by Junie
parent 3c709c115b
commit a8b70e88d3
630 changed files with 2646 additions and 2036 deletions
+832
View File
@@ -0,0 +1,832 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>
#include <assert.h>
#include "inference_engine.h"
#include "q48_16.h"
#include "vm.h"
#include "rolling_window_of_truth.h"
#include "ssm_jacquard.h"
/* ============================================================================
* Phase 2A: ANOVA Early-Exit Check
* ============================================================================
*
* Purpose: Skip full inference if variance hasn't changed significantly
* Threshold: 5% variance change (VARIANCE_SIGNIFICANCE_THRESHOLD in vm.h)
* Cost if stable: ~100 CPU cycles
* Cost if unstable: Full inference run (~5-10k cycles)
*/
/**
* @brief ANOVA early-exit check: test whether variance has stabilised.
*
* Computes |current - last| / last. If the relative change is <= 5%
* (threshold = 3276 in Q48.16), the variance is considered stable and
* full inference can be skipped (~100 cycles vs ~5–10k for a full run).
* Always returns 0 (unstable) on the first call when @c last_variance == 0.
*
* @param current_variance Variance from the current snapshot (Q48.16)
* @param last_variance Variance from the previous inference run (Q48.16)
* @return 1 if variance is stable (skip inference), 0 if full run needed
*/
static int has_variance_stabilized(
q48_16_t current_variance,
q48_16_t last_variance
)
{
if (last_variance == 0) {
/* First run, always do full inference */
return 0;
}
/* Calculate variance delta as ratio */
q48_16_t delta = (current_variance > last_variance)
? (current_variance - last_variance)
: (last_variance - current_variance);
/* Compute delta / last_variance in Q48.16 */
q48_16_t ratio = q48_div(delta, last_variance);
/* Threshold: 5% = 0.05 in Q48.16 = 0.05 * 65536 = 3276 */
q48_16_t threshold = 3276;
if (ratio <= threshold) {
/* Variance is stable, skip full inference */
return 1;
}
/* Variance changed significantly, run full inference */
return 0;
}
/* ============================================================================
* Phase 2B: Heat Trajectory Extraction
* ============================================================================
*
* Purpose: Fresh snapshot of execution_heat from dictionary
* Strategy: Iterate vm->latest backwards, collect heat values
* Timing: O(dictionary_entries), called every HEARTBEAT_INFERENCE_FREQUENCY ticks
*/
/*
* Build a heat trajectory for inference using a consistent rolling window snapshot.
*
* We linearize the rolling window into a temporary ID buffer (via the public export API)
* and convert the most recent entries into execution_heat samples by consulting the
* stable word-id map protected by dict_lock. This keeps the inference engine fully
* thread-safe while still operating on real heat values instead of raw IDs.
*/
/**
* @brief Build a heat trajectory array from the rolling window of truth.
*
* Linearises the rolling window via @c rolling_window_export_execution_history(),
* then translates each word-id slot to its current @c execution_heat by
* looking up the entry under @c vm->dict_lock. The result is a freshly
* allocated array of @c *out_length uint64_t values in chronological order.
* The caller is responsible for freeing the returned pointer. Returns NULL
* (and sets @c *out_length = 0) on allocation failure or empty window.
*
* @param window Rolling window to export from
* @param vm VM whose dictionary is consulted for heat values
* @param out_length Output: number of elements in the returned array
* @return Heap-allocated trajectory array, or NULL on failure
*/
static uint64_t* extract_heat_trajectory(
RollingWindowOfTruth *window,
VM *vm,
uint64_t *out_length
)
{
if (!window || !vm || !out_length) {
return NULL;
}
uint32_t *word_ids = (uint32_t*)malloc(ROLLING_WINDOW_SIZE * sizeof(uint32_t));
if (!word_ids) {
*out_length = 0;
return NULL;
}
uint64_t exported = rolling_window_export_execution_history(window,
word_ids,
ROLLING_WINDOW_SIZE);
if (exported == 0) {
free(word_ids);
*out_length = 0;
return NULL;
}
uint64_t span = window->is_warm
? (uint64_t)window->effective_window_size
: exported;
if (span > exported) span = exported;
if (span == 0) {
free(word_ids);
*out_length = 0;
return NULL;
}
uint64_t *trajectory = (uint64_t*)malloc(span * sizeof(uint64_t));
if (!trajectory) {
free(word_ids);
*out_length = 0;
return NULL;
}
uint64_t start = exported - span;
sf_mutex_lock(&vm->dict_lock);
for (uint64_t i = 0; i < span; i++) {
uint32_t word_id = word_ids[start + i];
uint64_t heat = 0;
if (word_id < DICTIONARY_SIZE) {
DictEntry *entry = vm_dictionary_lookup_by_word_id(vm, word_id);
if (entry) {
heat = (uint64_t)entry->execution_heat;
}
}
trajectory[i] = heat;
}
sf_mutex_unlock(&vm->dict_lock);
free(word_ids);
*out_length = span;
return trajectory;
}
/* ============================================================================
* Phase 2C: Window Width Inference (Variance Inflection)
* ============================================================================
*
* Purpose: Find statistical point where adding more data stops refining understanding
*
* Algorithm:
* 1. For each sub-window size from MIN to full:
* - Compute variance_q48 of heat in that window
* 2. Detect inflection: where d(variance)/d(size) → 0
* - When |variance[i+1] - variance[i]| < 1% of current variance
* - Return that size as inferred_window_width
* 3. Clamp to [ADAPTIVE_MIN_WINDOW_SIZE, ROLLING_WINDOW_SIZE]
*/
/**
* @brief Compute the variance of a heat trajectory in Q48.16 fixed-point.
*
* Computes the population variance: Σ(heat[i] - mean)² / N.
* All intermediate calculations use Q48.16 arithmetic. Returns 0 for
* an empty array.
*
* @param heat_data Array of execution heat values (raw uint64_t counts)
* @param length Number of elements in @c heat_data
* @return Population variance in Q48.16 format
*/
q48_16_t compute_variance_q48(
const uint64_t *heat_data,
uint64_t length
)
{
if (length == 0) return 0;
/* Compute mean in Q48.16 */
uint64_t sum = 0;
for (uint64_t i = 0; i < length; i++) {
sum += heat_data[i];
}
q48_16_t mean = q48_div(q48_from_u64(sum), q48_from_u64(length));
/* Compute sum of squared deviations */
uint64_t sum_sq_diff = 0;
for (uint64_t i = 0; i < length; i++) {
q48_16_t heat_q48 = q48_from_u64(heat_data[i]);
q48_16_t diff = (heat_q48 > mean) ? (heat_q48 - mean) : (mean - heat_q48);
q48_16_t sq_diff = q48_mul(diff, diff);
sum_sq_diff += q48_to_u64(sq_diff);
}
/* Variance = sum_sq_diff / length */
q48_16_t variance = q48_div(q48_from_u64(sum_sq_diff), q48_from_u64(length));
return variance;
}
/* ============================================================================
* Helper: Compute Median (for Levene's Test)
* ============================================================================
*
* Purpose: Find median of array for robust central tendency
* Note: Uses simple selection algorithm (O(n) expected, O(n²) worst case)
*/
/**
* @brief Compute the median of a Q48.16 array.
*
* Makes a temporary copy, sorts it with bubble sort (acceptable for the
* small chunk counts used in Levene's test, typically K <= 16), and
* returns the middle element. Returns 0 for an empty array.
*
* @param data Array of Q48.16 values
* @param length Number of elements
* @return Median value in Q48.16 format
*/
static q48_16_t compute_median_q48(
const q48_16_t *data,
uint32_t length
)
{
if (length == 0) return 0;
if (length == 1) return data[0];
/* Make a copy and sort (bubble sort for small arrays) */
q48_16_t *sorted = (q48_16_t *)malloc(length * sizeof(q48_16_t));
if (!sorted) return 0;
memcpy(sorted, data, length * sizeof(q48_16_t));
/* Simple bubble sort */
for (uint32_t i = 0; i < length - 1; i++) {
for (uint32_t j = 0; j < length - i - 1; j++) {
if (sorted[j] > sorted[j + 1]) {
q48_16_t tmp = sorted[j];
sorted[j] = sorted[j + 1];
sorted[j + 1] = tmp;
}
}
}
q48_16_t median = sorted[length / 2];
free(sorted);
return median;
}
/* ============================================================================
* Helper: Compute Mean in Q48.16
* ============================================================================
*
* Purpose: Calculate arithmetic mean of Q48.16 values
*/
/**
* @brief Compute the arithmetic mean of a Q48.16 array.
*
* Sums the integer parts of each element (>> 16) and returns the result
* converted back to Q48.16. Returns 0 for an empty array.
*
* @param data Array of Q48.16 values
* @param length Number of elements
* @return Arithmetic mean in Q48.16 format
*/
static q48_16_t compute_mean_q48(
const q48_16_t *data,
uint32_t length
)
{
if (length == 0) return 0;
uint64_t sum = 0;
for (uint32_t i = 0; i < length; i++) {
sum += data[i] >> 16; /* Convert to integer part */
}
return q48_from_u64(sum / length);
}
/* ============================================================================
* Levene's Test for Equality of Variance (Statistically Valid)
* ============================================================================
*
* Purpose: Test if multiple samples have equal variance
* Reference: Levene, H. (1960). "Robust tests for equality of variances"
*
* Null Hypothesis H₀: All chunk variances are equal
* Test Statistic W: Ratio of variance of deviations to overall deviation
*
* If W > critical_value (≈6.5 for α=0.05): REJECT H₀ (variances differ)
* If W ≤ critical_value: FAIL TO REJECT H₀ (variances are similar)
*
* Input:
* - chunk_variances: Array of K variance values (one per chunk)
* - num_chunks: K (number of chunks)
* - chunk_size: N (size of each chunk, all equal)
*
* Output:
* - W statistic in Q48.16 format
* - Compare result to LEVENE_CRITICAL_VALUE_Q48
*/
/**
* @brief Compute Levene's W statistic for equality of variance.
*
* Tests the null hypothesis H₀: all chunk variances are equal.
* Uses the median-based variant for robustness: z_i = |var_i - median_var|,
* then W = (K-1)*N*Σ(z_i - z_bar)² / (K*N * var(z)).
* Returns 0 for fewer than 2 chunks. Compare result to
* @c LEVENE_CRITICAL_VALUE_Q48 (≈ 6.5 at α=0.05): W > critical means
* variances differ significantly.
*
* @param chunk_variances Array of K per-chunk variance values (Q48.16)
* @param num_chunks Number of chunks K (must be >= 2)
* @param chunk_size Number of samples N per chunk
* @return Levene's W statistic in Q48.16 format
*/
static q48_16_t compute_levene_statistic(
const q48_16_t *chunk_variances,
uint32_t num_chunks,
uint32_t chunk_size
)
{
if (num_chunks < 2) return 0;
/* Step 1: Compute median variance */
q48_16_t median_var = compute_median_q48(chunk_variances, num_chunks);
/* Step 2: Compute z_i = |variance_i - median_var| */
q48_16_t *z = (q48_16_t *)malloc(num_chunks * sizeof(q48_16_t));
if (!z) return 0;
for (uint32_t i = 0; i < num_chunks; i++) {
z[i] = (chunk_variances[i] > median_var)
? (chunk_variances[i] - median_var)
: (median_var - chunk_variances[i]);
}
/* Step 3: Compute z_bar = mean(z) */
q48_16_t z_bar = compute_mean_q48(z, num_chunks);
/* Step 4: Compute numerator = (K-1) * N * Σ(z_i - z_bar)² */
q48_16_t sum_sq_diff = 0;
for (uint32_t i = 0; i < num_chunks; i++) {
q48_16_t diff = (z[i] > z_bar) ? (z[i] - z_bar) : (z_bar - z[i]);
q48_16_t sq = q48_mul(diff, diff);
sum_sq_diff = q48_add(sum_sq_diff, sq);
}
q48_16_t numerator = q48_mul(
q48_from_u64(num_chunks - 1),
q48_mul(q48_from_u64(chunk_size), sum_sq_diff)
);
/* Step 5: Compute denominator = Σ_i Σ_j (z_ij - z_i_mean)² */
/* Approximation: Use variance of z values */
q48_16_t z_variance = compute_variance_q48((const uint64_t *)z, num_chunks);
q48_16_t denominator = q48_mul(q48_from_u64(num_chunks), z_variance);
/* Step 6: W = numerator / denominator */
q48_16_t W = (denominator > 0) ? q48_div(numerator, denominator) : 0;
free(z);
return W;
}
/**
* @brief Find the minimum window size where heat variance is statistically stable.
*
* Implements Loop #5 (window width inference). Scans candidate window sizes from
* @c ADAPTIVE_MIN_WINDOW_SIZE to the full trajectory in steps of 64, divides the
* trajectory into disjoint chunks of that size, and applies Levene's test. Returns
* the first size where W <= 6.5 (α=0.05 critical value). Falls back to the full
* trajectory length if no size passes. @c full_variance is unused (retained for
* API compatibility with callers that pre-compute it).
*
* @param heat_data Heat trajectory array
* @param trajectory_length Number of elements in @c heat_data
* @param full_variance Unused; retained for API compatibility
* @return Minimum sufficient window width in [ADAPTIVE_MIN_WINDOW_SIZE, ROLLING_WINDOW_SIZE]
*/
uint32_t find_variance_inflection(
const uint64_t *heat_data,
uint64_t trajectory_length,
q48_16_t full_variance /* Unused in new algorithm */
)
{
(void)full_variance;
/* ========================================================================
* REDESIGNED: Levene's Test for Statistical Validity (2025-11-19)
* ========================================================================
*
* OLD ALGORITHM (FLAWED):
* - Computed prefix variance: var[0..N], var[0..2N], var[0..3N], ...
* - Violated statistical independence
* - Confounded "enough data" with "variance decay"
* - Used magic 1% threshold with no statistical justification
*
* NEW ALGORITHM (VALID):
* - Divides trajectory into K disjoint chunks of size N
* - Computes variance of each chunk independently
* - Uses Levene's test for equality of variance
* - Statistically sound hypothesis test (α=0.05)
* - Finds MINIMUM window size where variance is stable
*
* Reference: Levene, H. (1960). "Robust tests for equality of variances"
* In: Contributions to Probability and Statistics
*/
/* Use constants from vm.h (defined as macros) */
#ifndef ADAPTIVE_MIN_WINDOW_SIZE
#define ADAPTIVE_MIN_WINDOW_SIZE 256
#endif
#ifndef ROLLING_WINDOW_SIZE
#define ROLLING_WINDOW_SIZE 4096
#endif
if (trajectory_length == 0) {
return ROLLING_WINDOW_SIZE / 2; /* Default */
}
uint32_t min_size = ADAPTIVE_MIN_WINDOW_SIZE;
uint32_t max_size = (trajectory_length < ROLLING_WINDOW_SIZE)
? (uint32_t)trajectory_length
: ROLLING_WINDOW_SIZE;
/* Levene's critical value for α=0.05 with K≥3 degrees of freedom */
/* Theoretical value ≈ 5.88, conservative estimate ≈ 6.5 */
q48_16_t levene_critical = q48_from_double(6.5);
/* Scan for minimum window size where variance is statistically stable */
for (uint32_t size = min_size; size <= max_size; size += 64) {
uint32_t num_chunks = (uint32_t)(trajectory_length / size);
/* Need at least 3 chunks for reliable statistical test */
if (num_chunks < 3) {
continue; /* Too few chunks, try larger size */
}
/* Allocate and compute variance for each disjoint chunk */
q48_16_t *chunk_vars = (q48_16_t *)malloc(num_chunks * sizeof(q48_16_t));
if (!chunk_vars) {
continue; /* Allocation failed, skip this size */
}
for (uint32_t i = 0; i < num_chunks; i++) {
uint64_t chunk_start = (uint64_t)i * (uint64_t)size;
chunk_vars[i] = compute_variance_q48(
&heat_data[chunk_start],
size
);
}
/* Apply Levene's test for equality of variance */
q48_16_t W = compute_levene_statistic(chunk_vars, num_chunks, size);
free(chunk_vars);
/* If test passes: variances are statistically similar */
/* This window size is SUFFICIENT for capturing the pattern */
if (W <= levene_critical) {
return size; /* Found minimum sufficient window */
}
}
/* If no size passed test, use maximum available */
return max_size;
}
/* ============================================================================
* Phase 2D: Decay Slope Inference (Closed-Form Linear Regression)
* ============================================================================
*
* Purpose: Extract decay_slope from heat trajectory via exponential fitting
*
* Model: ln(heat[t]) = ln(h0) - slope*t
* (Exponential decay: heat(t) = h0 * e^(-slope*t))
*
* Algorithm (Integer-only, Q48.16):
* 1. Transform trajectory to log space
* 2. Linear regression on log_heat = a - slope*t
* 3. Extract slope coefficient
*
* Closed-form solution:
* numerator = n * Σ(t*ln(heat)) - Σt * Σln(heat)
* denominator = n * Σ(t²) - (Σt)²
* slope = numerator / denominator
*/
/**
* @brief Infer the decay slope from a heat trajectory via log-linear regression.
*
* Implements Loop #6 (decay slope inference). Fits the model
* ln(heat[t]) = ln(h₀) - slope*t using the closed-form OLS solution:
* slope = (n*Σ(t·ln(h)) - Σt·Σln(h)) / (n·Σt² - (Σt)²).
* The sign of the numerator is taken as absolute value since decay rate
* is always positive. Zero heat values are skipped. Returns 0 if fewer
* than 2 samples are available.
*
* @param heat_data Array of execution heat values (raw counts)
* @param length Number of elements in @c heat_data
* @return Inferred decay slope in Q48.16 format (0 = insufficient data)
*/
uint64_t infer_decay_slope_q48(
const uint64_t *heat_data,
uint64_t length
)
{
if (length < 2) {
return 0;
}
/* Compute sums for linear regression */
uint64_t n = length;
uint64_t sum_t = (n * (n - 1)) / 2; /* 0+1+2+...+(n-1) */
uint64_t sum_t_sq = (n * (n - 1) * (2 * n - 1)) / 6; /* 0²+1²+...+(n-1)² */
q48_16_t sum_log_heat = 0;
q48_16_t sum_t_log_heat = 0;
for (uint64_t t = 0; t < length; t++) {
if (heat_data[t] == 0) continue; /* Skip zero heat values */
/* Compute ln(heat[t]) in Q48.16 */
q48_16_t log_heat = q48_log_approx(heat_data[t]);
sum_log_heat = q48_add(sum_log_heat, log_heat);
/* Compute t * ln(heat[t]) */
q48_16_t t_log = q48_mul(q48_from_u64(t), log_heat);
sum_t_log_heat = q48_add(sum_t_log_heat, t_log);
}
/* Compute slope = (n*Σ(t*ln) - Σt*Σln) / (n*Σ(t²) - (Σt)²) */
/* Note: For decay, numerator may be negative, so use signed arithmetic */
int64_t n_times_sum_t_log = (int64_t)q48_mul(q48_from_u64(n), sum_t_log_heat);
int64_t sum_t_times_sum_log = (int64_t)q48_mul(q48_from_u64(sum_t), sum_log_heat);
int64_t numerator_signed = n_times_sum_t_log - sum_t_times_sum_log;
/* Take absolute value (decay rate is always positive) */
uint64_t numerator = (numerator_signed < 0) ? (uint64_t)(-numerator_signed) : (uint64_t)numerator_signed;
uint64_t denominator_raw = (n * sum_t_sq) - (sum_t * sum_t);
if (denominator_raw == 0) {
denominator_raw = 1; /* Avoid division by zero */
}
/* Divide: numerator is Q48.16, denominator is raw */
/* slope = numerator_Q48 / denominator_raw preserves Q48.16 scaling */
uint64_t slope = numerator / denominator_raw;
return slope;
}
/* ============================================================================
* Phase 2E: Fit Quality Assessment
* ============================================================================
*
* Purpose: Compute R² or residual metric for diagnostics
* Simplified: Use residual sum of squares / total sum of squares
*/
/**
* @brief Compute R²-style fit quality for the inferred decay slope.
*
* Currently returns a fixed placeholder of 0.8 in Q48.16 (52429). A full
* residual-sum-of-squares implementation is planned but deferred — the
* placeholder is sufficient for the DoE convergence signal.
*
* @param heat_data Array of heat values (unused in current implementation)
* @param length Array length (unused in current implementation)
* @param slope_q48 Inferred slope in Q48.16 (unused in current implementation)
* @return Fit quality in Q48.16; currently always 0.8 (52429)
*/
static uint64_t compute_fit_quality(
const uint64_t *heat_data,
uint64_t length,
uint64_t slope_q48
)
{
(void)heat_data;
(void)slope_q48;
if (length < 2) {
return q48_from_u64(1); /* Perfect fit if no data */
}
/* Simplified: Return ratio of predicted-to-actual variance */
/* For now: return 0.8 in Q48.16 as placeholder */
return (q48_16_t)52429ULL; /* 0.8 * 65536 ≈ 52429 in Q48.16 */
}
/* ============================================================================
* Main API: inference_engine_run()
* ============================================================================
*
* High-level orchestrator that coordinates all inference phases
*/
/*
* @brief Run the full inference engine pipeline.
*
* High-level orchestrator for Loops #5 and #6. In order:
* 1. Extract the heat trajectory from the rolling window (Phase 2B).
* 2. ANOVA early-exit check — if variance is stable, skip and set
* @c outputs->early_exited = 1 (Phase 2A).
* 3. Window width inference via Levene's test — gated by L5 flag in
* @c vm->ssm_config (Phase 2C).
* 4. Decay slope inference via log-linear regression — gated by L6 flag
* (Phase 2D).
* 5. Fit quality assessment (Phase 2E).
* Writes all results into @c outputs and frees the trajectory. No-op if
* any required pointer is NULL.
*
* @param inputs Input pointers: rolling window and VM reference
* @param outputs Output struct updated with window, slope, variance, fit quality
*/
void inference_engine_run(InferenceInputs *inputs, InferenceOutputs *outputs)
{
if (!inputs || !outputs || !inputs->window || !inputs->vm) {
return;
}
/* === PHASE 2B: Extract Fresh Heat Trajectory === */
uint64_t traj_len = 0;
uint64_t *trajectory = extract_heat_trajectory(inputs->window, inputs->vm, &traj_len);
if (!trajectory || traj_len < 2) {
outputs->early_exited = 1;
if (trajectory) free(trajectory);
return;
}
/* === PHASE 2A: ANOVA Early-Exit Check (using actual heat samples) === */
q48_16_t current_variance = compute_variance_q48(trajectory, traj_len);
if (has_variance_stabilized(current_variance, outputs->window_variance_q48)) {
outputs->early_exited = 1;
free(trajectory);
return;
}
/* === PHASE 2C: Window Width Inference === */
/* L8 FINAL INTEGRATION: Loop #5 runtime-gated by Jacquard mode selector */
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 */
} else {
/* No L8 config available - run inference (legacy mode) */
inferred_width = find_variance_inflection(
trajectory,
traj_len,
current_variance
);
}
/* === PHASE 2D: Decay Slope Inference === */
/* L8 FINAL INTEGRATION: Loop #6 runtime-gated by Jacquard mode selector */
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 */
} else {
/* No L8 config available - run inference (legacy mode) */
inferred_slope = infer_decay_slope_q48(trajectory, traj_len);
}
/* === PHASE 2E: Diagnostics === */
uint64_t fit_quality = compute_fit_quality(trajectory, traj_len, inferred_slope);
/* === Update Outputs === */
outputs->adaptive_window_width = inferred_width;
outputs->adaptive_decay_slope = inferred_slope;
outputs->window_variance_q48 = current_variance;
outputs->slope_fit_quality_q48 = fit_quality;
outputs->early_exited = 0;
/* === Cleanup === */
free(trajectory);
}
/* ============================================================================
* Helper Functions: Logging & Validation
* ============================================================================
*/
/**
* @brief Format inference outputs as a human-readable string.
*
* Writes into a static 256-byte buffer — not thread-safe, for diagnostics only.
* Format: @c "window=N var=F slope=F quality=F (cached|full)".
*
* @param outputs Inference outputs to format (NULL produces @c "(null)")
* @return Pointer to a static string buffer
*/
const char* inference_outputs_to_string(const InferenceOutputs *outputs)
{
static char buf[256];
if (!outputs) {
snprintf(buf, sizeof(buf), "(null)");
return buf;
}
double var_dbl = q48_to_double(outputs->window_variance_q48);
double slope_dbl = q48_to_double(outputs->adaptive_decay_slope);
double quality_dbl = q48_to_double(outputs->slope_fit_quality_q48);
snprintf(buf, sizeof(buf),
"window=%u var=%.6f slope=%.6f quality=%.6f %s",
outputs->adaptive_window_width,
var_dbl,
slope_dbl,
quality_dbl,
outputs->early_exited ? "(cached)" : "(full)");
return buf;
}
/**
* @brief Validate inference outputs for sanity.
*
* Checks that @c adaptive_window_width is in
* [@c ADAPTIVE_MIN_WINDOW_SIZE, @c ROLLING_WINDOW_SIZE], that
* @c adaptive_decay_slope is non-zero and <= 100.0 in Q48.16, and that
* @c slope_fit_quality_q48 is in [0.0, 1.0].
*
* @param outputs Inference outputs to validate (NULL returns 0)
* @return 1 if all fields are within acceptable ranges, 0 otherwise
*/
int inference_outputs_validate(const InferenceOutputs *outputs)
{
if (!outputs) {
return 0;
}
/* Check window width is reasonable */
#ifndef ADAPTIVE_MIN_WINDOW_SIZE
#define ADAPTIVE_MIN_WINDOW_SIZE 256
#endif
#ifndef ROLLING_WINDOW_SIZE
#define ROLLING_WINDOW_SIZE 4096
#endif
if (outputs->adaptive_window_width < ADAPTIVE_MIN_WINDOW_SIZE ||
outputs->adaptive_window_width > ROLLING_WINDOW_SIZE) {
return 0;
}
/* Check slope is positive and reasonable */
/* Typical range: 0.001 to 100.0 in Q48.16 */
if (outputs->adaptive_decay_slope == 0 ||
outputs->adaptive_decay_slope > q48_from_u64(100)) {
return 0;
}
/* Check fit quality is between 0.0 and 1.0 */
if (outputs->slope_fit_quality_q48 > q48_from_u64(1)) {
return 0;
}
return 1;
}