Skip to content

Commit 897a3ec

Browse files
committed
Experiment 8: head-to-head benchmark of three search algorithms
Settles the PHI_PI_FIB_ALGORITHM.md vs TIER_4_HONEST_REVISION.md contradiction empirically by running all three algorithms under the same OMC harness. New surface in phi_pi_fib.rs: phi_pi_fib_search_v2(arr, target) - the F(k)/phi^(pi*k) split-point algorithm from PHI_PI_FIB_ALGORITHM.md. Falls back to binary search when the offset rounds to 0. log_phi_pi_fibonacci(n) - the theoretical bound ln(n) / (pi * ln(phi)) ~= 0.459 * log2(n). log_phi(n) - kept, marked #[deprecated] in favor of log_phi_pi_fibonacci. Four new OMC builtins so experiments can call any algorithm: phi_pi_fib_search_v2(arr, target) phi_pi_fib_nearest_v2(arr, target) phi_pi_bin_search(arr, target) log_phi_pi_fibonacci(n) Head-to-head bench (500 queries per N, random integer targets): N | fib-step | phi-pi-fib-v2 | binary | log2(N) | log_phi_pi_fib(N) -------+----------+---------------+--------+---------+------------------ 16 | 5.47 | 4.15 | 3.82 | 4.0 | 1.83 64 | 8.18 | 7.03 | 5.61 | 6.0 | 2.75 256 | 10.45 | 9.83 | 7.58 | 8.0 | 3.67 1024 | 12.75 | 13.76 | 9.49 | 10.0 | 4.58 4096 | 13.85 | 16.69 | 11.45 | 12.0 | 5.50 16384 | 16.32 | 19.75 | 13.49 | 14.0 | 6.42 65536 | 18.28 | 23.86 | 15.54 | 16.0 | 7.34 Race over N=65536 with 2000 fixed-seed queries each: fib-step: 36,875 compares phi-pi-fib-v2: 47,458 compares (28.7% more than fib-step) binary search: 31,058 compares (the winner) Pairwise ratios: v2 / binary = 1.528 fib / binary = 1.187 v2 / fib = 1.287 Binary search wins decisively at every N. v2's unbalanced probe positions waste comparison information; it actually does WORSE than fib-step at N >= 1024. The theoretical log_phi_pi_fibonacci(N) ~= 0.46 * log2(N) is BELOW the information-theoretic minimum log2(N), so it was never achievable in single-comparison sequential code. TIER_4_HONEST_REVISION.md was empirically correct. What this means for the substrate refactor: phi_pi_fibonacci is the right ARCHITECTURAL FRAMING (attractor sets, naming, conceptual unit), but the LOOKUP PRIMITIVE in the unified substrate should be binary search. Holding off on the refactor for your review. Both engines audit byte-identical (1759 bytes). 148/148 tests pass after fixing two new rustdoc tests that mis-parsed indented math blocks as Rust code.
1 parent a9232e0 commit 897a3ec

3 files changed

Lines changed: 394 additions & 10 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# =============================================================================
2+
# Experiment 8 — Head-to-head benchmark of three search algorithms.
3+
#
4+
# The repo has two contradictory docs:
5+
#
6+
# PHI_PI_FIB_ALGORITHM.md claims an F(k)/φ^(π·k) split-point
7+
# search achieves O(log_φ_π_fibonacci n)
8+
# ≈ 0.459 · log₂(n) comparisons.
9+
#
10+
# TIER_4_HONEST_REVISION.md says the actual implementation was
11+
# standard Fibonacci search, ~1.44 ·
12+
# log₂(n), and 20% SLOWER than binary.
13+
#
14+
# This experiment settles it by running all three algorithms under the
15+
# same OMC harness and the same random-query workload:
16+
#
17+
# 1. phi_pi_fib_search — original Fibonacci-step search
18+
# 2. phi_pi_fib_search_v2 — F(k)/φ^(π·k) split-point formula
19+
# from PHI_PI_FIB_ALGORITHM.md
20+
# 3. phi_pi_bin_search — standard binary search baseline
21+
#
22+
# All three share the same compare counter (phi_pi_fib_stats), so we
23+
# reset between runs and measure cleanly.
24+
#
25+
# Whichever wins on average comparisons becomes the substrate root
26+
# in the unification refactor that follows.
27+
#
28+
# Run:
29+
# ./target/release/omnimcode-standalone experiments/hybrid_llm/experiment_8_search_bench.omc
30+
# =============================================================================
31+
32+
fn build_sorted_n(n) -> array {
33+
h arr = arr_new(n, 0);
34+
h i = 0;
35+
while i < n {
36+
arr_set(arr, i, i * 2 + 1); # 1, 3, 5, ..., 2n-1
37+
i = i + 1;
38+
}
39+
return arr;
40+
}
41+
42+
# Measure average comparisons for a single search algorithm against
43+
# `arr` with `n_queries` random integer targets in the valid range.
44+
# Returns the avg compares/search.
45+
#
46+
# `which` selects: 0 = phi_pi_fib_search, 1 = phi_pi_fib_search_v2,
47+
# 2 = phi_pi_bin_search.
48+
fn measure_search(arr, n_queries, which) -> float {
49+
h n = arr_len(arr);
50+
h max_target = arr_get(arr, n - 1) + 5;
51+
phi_pi_fib_reset();
52+
h k = 0;
53+
while k < n_queries {
54+
h target = random_int(0, max_target);
55+
h r = 0;
56+
if which == 0 { r = phi_pi_fib_search(arr, target); }
57+
if which == 1 { r = phi_pi_fib_search_v2(arr, target); }
58+
if which == 2 { r = phi_pi_bin_search(arr, target); }
59+
k = k + 1;
60+
}
61+
h s = phi_pi_fib_stats();
62+
return to_float(arr_get(s, 1)) / to_float(arr_get(s, 0));
63+
}
64+
65+
random_seed(42);
66+
67+
print("== Experiment 8: head-to-head search benchmark ==");
68+
print("(random integer queries against sorted [1, 3, 5, ..., 2N-1]; 500 queries per N)");
69+
print("");
70+
print(" N | fib-step | phi-pi-fib-v2 | binary | log2(N) | log_phi_pi_fib(N)");
71+
print(" -------+----------+---------------+--------+---------+------------------");
72+
73+
h sizes = [16, 64, 256, 1024, 4096, 16384, 65536];
74+
h ns = arr_len(sizes);
75+
h si = 0;
76+
while si < ns {
77+
h N = arr_get(sizes, si);
78+
h arr_n = build_sorted_n(N);
79+
h avg_fib = measure_search(arr_n, 500, 0);
80+
h avg_v2 = measure_search(arr_n, 500, 1);
81+
h avg_bin = measure_search(arr_n, 500, 2);
82+
h log2_N = log(to_float(N)) / log(2.0);
83+
h log_ppf_N = log_phi_pi_fibonacci(to_float(N));
84+
print(concat_many(
85+
" ", N,
86+
" | ", avg_fib,
87+
" | ", avg_v2,
88+
" | ", avg_bin,
89+
" | ", log2_N,
90+
" | ", log_ppf_N
91+
));
92+
si = si + 1;
93+
}
94+
print("");
95+
96+
# ---------------------------------------------------------------------------
97+
# Race: total comparisons across the same fixed query set, summed.
98+
# This is the single number that decides which algorithm wins as the
99+
# substrate root.
100+
# ---------------------------------------------------------------------------
101+
102+
fn total_compares(arr, n_queries, seed_offset, which) -> int {
103+
random_seed(100 + seed_offset); # deterministic across algorithms
104+
h n = arr_len(arr);
105+
h max_target = arr_get(arr, n - 1) + 5;
106+
phi_pi_fib_reset();
107+
h k = 0;
108+
while k < n_queries {
109+
h target = random_int(0, max_target);
110+
if which == 0 { h r = phi_pi_fib_search(arr, target); }
111+
if which == 1 { h r = phi_pi_fib_search_v2(arr, target); }
112+
if which == 2 { h r = phi_pi_bin_search(arr, target); }
113+
k = k + 1;
114+
}
115+
h s = phi_pi_fib_stats();
116+
return arr_get(s, 1);
117+
}
118+
119+
print("== Race over a 65,536-element array, 2,000 random queries each ==");
120+
print("(same seeded query stream for all three algorithms — fair fight)");
121+
print("");
122+
123+
h big = build_sorted_n(65536);
124+
h fib_total = total_compares(big, 2000, 1, 0);
125+
h v2_total = total_compares(big, 2000, 1, 1);
126+
h bin_total = total_compares(big, 2000, 1, 2);
127+
128+
print(concat_many(" fib-step total compares: ", fib_total));
129+
print(concat_many(" phi-pi-fib-v2 total compares: ", v2_total));
130+
print(concat_many(" binary search total compares: ", bin_total));
131+
print("");
132+
133+
# Show pairwise ratios so the result is unambiguous.
134+
fn ratio(a, b) -> float {
135+
return to_float(a) / to_float(b);
136+
}
137+
138+
print("Pairwise ratios (lower = better for left side):");
139+
print(concat_many(" v2 / binary = ", ratio(v2_total, bin_total)));
140+
print(concat_many(" fib / binary = ", ratio(fib_total, bin_total)));
141+
print(concat_many(" v2 / fib = ", ratio(v2_total, fib_total)));
142+
print("");
143+
144+
# ---------------------------------------------------------------------------
145+
# Verdict.
146+
# ---------------------------------------------------------------------------
147+
148+
print("== Verdict (read the numbers above, no spin) ==");
149+
print("");
150+
print("If v2 < binary: PHI_PI_FIB_ALGORITHM.md was right and v2 should");
151+
print(" become the substrate root.");
152+
print("");
153+
print("If v2 ≈ binary but cheaper than fib-step: v2 is still a net win");
154+
print(" over the current substrate; promote it.");
155+
print("");
156+
print("If binary < v2 and binary < fib-step: TIER_4_HONEST_REVISION.md");
157+
print(" was right. Use binary_search (renamed appropriately) as the");
158+
print(" substrate root, and the phi-pi-fibonacci framework is");
159+
print(" architectural naming over a binary-search implementation.");
160+
print("== End ==");

omnimcode-core/src/interpreter.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,6 +1663,9 @@ impl Interpreter {
16631663
// Phi-Pi-Fib search (Fibonacci-step binary search variant)
16641664
| "phi_pi_fib_search" | "phi_pi_fib_nearest"
16651665
| "phi_pi_fib_stats" | "phi_pi_fib_reset"
1666+
// Phi-Pi-Fib search v2 + binary baseline + theoretical bound
1667+
| "phi_pi_fib_search_v2" | "phi_pi_fib_nearest_v2"
1668+
| "phi_pi_bin_search" | "log_phi_pi_fibonacci"
16661669
// Self-healing
16671670
| "safe_divide" | "safe_arr_get" | "safe_arr_set"
16681671
| "safe_add" | "safe_sub" | "safe_mul" | "resolve_singularity"
@@ -4027,6 +4030,109 @@ impl Interpreter {
40274030
crate::phi_pi_fib::reset_search_stats();
40284031
Ok(Value::Null)
40294032
}
4033+
// phi_pi_fib_search_v2(sorted_arr, target) -> int
4034+
// F(k)/φ^(π·k) split-point search. Same return convention
4035+
// as phi_pi_fib_search (exact match index, or -(insert+1)).
4036+
// Comparison counts are folded into the shared counters so
4037+
// phi_pi_fib_stats() reports both algorithms' totals — call
4038+
// phi_pi_fib_reset between runs when measuring head-to-head.
4039+
"phi_pi_fib_search_v2" => {
4040+
if args.len() < 2 {
4041+
return Err("phi_pi_fib_search_v2 requires (sorted_array, target)".to_string());
4042+
}
4043+
let arr_v = self.eval_expr(&args[0])?;
4044+
let target = self.eval_expr(&args[1])?.to_int();
4045+
if let Value::Array(arr) = arr_v {
4046+
let items_b = arr.items.borrow();
4047+
let ints: Vec<i64> = items_b.iter().map(|v| v.to_int()).collect();
4048+
let r = crate::phi_pi_fib::phi_pi_fib_search_v2(
4049+
&ints,
4050+
&target,
4051+
|a, b| if a < b { -1 } else if a > b { 1 } else { 0 },
4052+
);
4053+
Ok(Value::HInt(HInt::new(match r {
4054+
Ok(i) => i as i64,
4055+
Err(insert_pos) => -(insert_pos as i64 + 1),
4056+
})))
4057+
} else {
4058+
Err("phi_pi_fib_search_v2: first argument must be an array".to_string())
4059+
}
4060+
}
4061+
// phi_pi_fib_nearest_v2(sorted_arr, target) -> int
4062+
// Always-valid nearest-index variant of phi_pi_fib_search_v2.
4063+
"phi_pi_fib_nearest_v2" => {
4064+
if args.len() < 2 {
4065+
return Err("phi_pi_fib_nearest_v2 requires (sorted_array, target)".to_string());
4066+
}
4067+
let arr_v = self.eval_expr(&args[0])?;
4068+
let target = self.eval_expr(&args[1])?.to_int();
4069+
if let Value::Array(arr) = arr_v {
4070+
let items_b = arr.items.borrow();
4071+
let ints: Vec<i64> = items_b.iter().map(|v| v.to_int()).collect();
4072+
if ints.is_empty() {
4073+
return Ok(Value::HInt(HInt::new(-1)));
4074+
}
4075+
let r = crate::phi_pi_fib::phi_pi_fib_search_v2(
4076+
&ints,
4077+
&target,
4078+
|a, b| if a < b { -1 } else if a > b { 1 } else { 0 },
4079+
);
4080+
let idx: usize = match r {
4081+
Ok(i) => i,
4082+
Err(insert_pos) => {
4083+
let n = ints.len();
4084+
if insert_pos == 0 {
4085+
0
4086+
} else if insert_pos >= n {
4087+
n - 1
4088+
} else {
4089+
let left = (target - ints[insert_pos - 1]).abs();
4090+
let right = (ints[insert_pos] - target).abs();
4091+
if right < left { insert_pos } else { insert_pos - 1 }
4092+
}
4093+
}
4094+
};
4095+
Ok(Value::HInt(HInt::new(idx as i64)))
4096+
} else {
4097+
Err("phi_pi_fib_nearest_v2: first argument must be an array".to_string())
4098+
}
4099+
}
4100+
// phi_pi_bin_search(sorted_arr, target) -> int
4101+
// Standard binary search baseline. Same return convention as
4102+
// the phi_pi_fib_search variants. Shares the global compare
4103+
// counter so head-to-head benches see all three algorithms.
4104+
"phi_pi_bin_search" => {
4105+
if args.len() < 2 {
4106+
return Err("phi_pi_bin_search requires (sorted_array, target)".to_string());
4107+
}
4108+
let arr_v = self.eval_expr(&args[0])?;
4109+
let target = self.eval_expr(&args[1])?.to_int();
4110+
if let Value::Array(arr) = arr_v {
4111+
let items_b = arr.items.borrow();
4112+
let ints: Vec<i64> = items_b.iter().map(|v| v.to_int()).collect();
4113+
let r = crate::phi_pi_fib::binary_search(
4114+
&ints,
4115+
&target,
4116+
|a, b| if a < b { -1 } else if a > b { 1 } else { 0 },
4117+
);
4118+
Ok(Value::HInt(HInt::new(match r {
4119+
Ok(i) => i as i64,
4120+
Err(insert_pos) => -(insert_pos as i64 + 1),
4121+
})))
4122+
} else {
4123+
Err("phi_pi_bin_search: first argument must be an array".to_string())
4124+
}
4125+
}
4126+
// log_phi_pi_fibonacci(n) -> float
4127+
// The theoretical compare-count bound for phi_pi_fib_search_v2.
4128+
// Equals ln(n) / (π · ln(φ)) ≈ 0.459 · log₂(n).
4129+
"log_phi_pi_fibonacci" => {
4130+
if args.is_empty() {
4131+
return Err("log_phi_pi_fibonacci requires (n)".to_string());
4132+
}
4133+
let n = self.eval_expr(&args[0])?.to_float();
4134+
Ok(Value::HFloat(crate::phi_pi_fib::log_phi_pi_fibonacci(n)))
4135+
}
40304136
"arr_slice" => {
40314137
if args.len() < 3 {
40324138
return Err("arr_slice requires (array, start, end)".to_string());
@@ -4994,6 +5100,8 @@ pub(crate) const HEAL_BUILTIN_NAMES: &[&str] = &[
49945100
// Phi-Pi-Fib search
49955101
"phi_pi_fib_search", "phi_pi_fib_nearest",
49965102
"phi_pi_fib_stats", "phi_pi_fib_reset",
5103+
"phi_pi_fib_search_v2", "phi_pi_fib_nearest_v2",
5104+
"phi_pi_bin_search", "log_phi_pi_fibonacci",
49975105
// Self-healing
49985106
"safe_divide", "safe_arr_get", "safe_arr_set",
49995107
"safe_add", "safe_sub", "safe_mul", "resolve_singularity",

0 commit comments

Comments
 (0)