Skip to content

Commit 2d5c9bb

Browse files
authored
Merge pull request #221 from AdaWorldAPI/claude/jirak-math-theorems-harvest-rfii13
feat(entropy_ladder): HHTL fork ladder — orthogonal leaf residue → Friston domain fork
2 parents bf606eb + 786110a commit 2d5c9bb

2 files changed

Lines changed: 241 additions & 0 deletions

File tree

.claude/blackboard.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,45 @@
33
> **Read this first.** The "Polyglot Notebook" architecture below is a
44
> separate/older program, not the current epoch.
55
6+
## 2026-06-17 — DECISION: HHTL fork ladder coded in `hpc::entropy_ladder` (CONJECTURE)
7+
8+
Reified the operator's standing idea — *if the orthogonal (helix/CAM-PQ)
9+
leaf residue is strong enough, free energy forks into another domain
10+
(HHTL shift = new exploration)* — as pure functions beside the existing
11+
entropy/quadrant code. Unifies four vocabularies as one 2-axis structure:
12+
`entropy_ladder::Quadrant(entropy,energy)` ≡ `lance-graph-contract::mul::
13+
FlowState(challenge,skill)` (Csikszentmihalyi) ≡ Friston model-vs-surprise
14+
≡ Staunen↔Wisdom.
15+
16+
- `residue_surprise(mag, noise_floor, sigma_k) → [0,1]` — orthogonal residue
17+
magnitude (prediction error the in-domain centroid codebook fails to
18+
explain) → challenge axis. Below floor = quantization (≈0); linear ramp
19+
over `sigma_k·noise_floor`. Threshold provenance per `I-NOISE-FLOOR-JIRAK`
20+
(Berry-Esseen wrong under CAM-PQ weak dependence); ramp is an honest proxy
21+
pending Jirak calibration, **not** a claimed bound.
22+
- `ForkAction {Commit, DescendDeeper, ForkBasin, ForkDomain}` + `fork_decision`
23+
— bands challenge−skill on the shipped `mul::flow_state_from` boundaries
24+
(Anxiety δ>0.2, Boredom δ<-0.2; the matched middle |δ|≤0.2, which
25+
flow_state_from splits into Flow/Transition, collapses to one in-domain
26+
branch here); HHTL depth decides descend-vs-
27+
fork. `ForkDomain` (mint a new classid = the Friston model-switch) requires
28+
BOTH leaf depth AND challenge≫skill — the "strong enough AT THE LEAF" invariant.
29+
30+
Layering kept honest: the `FlowState` *assessment* stays in lance-graph
31+
(thinking); the fork *math* lives here in ndarray (substrate, where residue +
32+
energy physically are) — per the Architecture Rule. Pure fns + one enum, no
33+
struct/layer, composes with `Quadrant`. 5 lib + 2 doctests, clippy clean.
34+
Branch `claude/jirak-math-theorems-harvest-rfii13`.
35+
36+
**Loose ends (CONJECTURE → gated):** (a) feed the *real* `edge_codec::
37+
CoarseResidue` magnitude from the live codec into `fork_decision` (currently
38+
caller-supplied); (b) `ForkDomain` vs `ForkBasin` should be arbitrated by
39+
residue *orthogonality* (⊥ all in-domain centroids = genuinely new), not the
40+
depth+delta proxy; (c) Jirak-derived σ threshold to replace the `sigma_k`
41+
proxy. This driver-side wire merges with lance-graph `materialize`'s
42+
`ThoughtCtx::from_live` step (same call-site).
43+
44+
645
## 2026-06-10 — DECISION: GUID prefix→shape routing crystallized (docs-only)
746

847
The operator-pinned canonical GUID (`OGAR/CLAUDE.md`: hex dash-groups =

src/hpc/entropy_ladder.rs

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,134 @@ pub fn entropy_class(h: f64) -> u8 {
210210
((h * 4.0) as u8).min(3)
211211
}
212212

213+
// ── HHTL fork ladder — orthogonal leaf residue → Friston domain fork ──────────
214+
//
215+
// The unification (board canon): the entropy×energy `Quadrant`, Csikszentmihalyi's
216+
// flow channel (`lance_graph_contract::mul::FlowState`), Friston free-energy
217+
// minimization, and the Staunen↔Wisdom ladder are ONE 2-axis structure. The
218+
// CHALLENGE axis is surprise = the magnitude of the orthogonal helix/CAM-PQ leaf
219+
// residue the current domain's centroid codebook fails to explain; the SKILL axis
220+
// is the in-domain codebook's remaining resolving capacity. The fork rule is the
221+
// "anxiety escape": when challenge ≫ skill *at leaf depth*, the model cannot
222+
// minimize free energy in-domain, so it switches the model — mint a new classid
223+
// domain (an HHTL shift into a new exploration). This is the reduction-to-practice
224+
// of "if the orthogonal leaf residue is strong enough, free energy forks into
225+
// another domain."
226+
227+
/// Map an orthogonal-residue magnitude to the surprise/challenge axis `[0, 1]`,
228+
/// relative to the substrate noise floor.
229+
///
230+
/// The helix/CAM-PQ leaf residue is the component left after the assigned centroid
231+
/// (the "place") is subtracted — geometrically orthogonal to that centroid, so its
232+
/// magnitude is the prediction error the current domain fails to explain. Below the
233+
/// noise floor it is mere quantization (≈0 surprise); the excess scales linearly to
234+
/// saturation over `sigma_k · noise_floor`.
235+
///
236+
/// **Threshold provenance (`I-NOISE-FLOOR-JIRAK`):** `noise_floor` should be the
237+
/// Berry-Esseen/Jirak weak-dependence bound (CAM-PQ contamination makes classic IID
238+
/// Berry-Esseen wrong), and `sigma_k` the σ-multiple deemed "genuinely new". The
239+
/// linear ramp here is an honest proxy pending a Jirak-derived calibration, not a
240+
/// claimed bound.
241+
///
242+
/// # Examples
243+
/// ```
244+
/// use ndarray::hpc::entropy_ladder::residue_surprise;
245+
/// assert!(residue_surprise(0.001, 0.004, 6.0) < 1e-12); // below floor → no surprise
246+
/// assert!((residue_surprise(1.0, 0.004, 6.0) - 1.0).abs() < 1e-12); // saturated
247+
/// let mid = residue_surprise(0.016, 0.004, 6.0); // excess 0.012 over span 0.024
248+
/// assert!((mid - 0.5).abs() < 1e-9);
249+
/// ```
250+
#[inline]
251+
pub fn residue_surprise(residue_mag: f64, noise_floor: f64, sigma_k: f64) -> f64 {
252+
let nf = noise_floor.max(f64::MIN_POSITIVE);
253+
// Guard the span AFTER the multiply: `MIN_POSITIVE * MIN_POSITIVE` underflows to
254+
// 0.0, so a degenerate (zero/negative) calibration would make `excess / span`
255+
// evaluate `0.0 / 0.0 = NaN` and defeat the `fork_decision` bands. Floor the
256+
// product itself so the result is always finite and in `[0, 1]`.
257+
let span = (sigma_k.max(f64::MIN_POSITIVE) * nf).max(f64::MIN_POSITIVE);
258+
let excess = (residue_mag - nf).max(0.0);
259+
(excess / span).clamp(0.0, 1.0)
260+
}
261+
262+
/// What to do with a leaf residue, governed by the Csikszentmihalyi flow channel
263+
/// (challenge = residue surprise, skill = in-domain codebook capacity). Mirrors
264+
/// `lance_graph_contract::mul::FlowState`, projected onto the HHTL cascade.
265+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
266+
#[repr(u8)]
267+
pub enum ForkAction {
268+
/// **Boredom** — challenge ≪ skill: the domain over-explains. Commit (and the
269+
/// caller may coarsen the address).
270+
Commit = 0,
271+
/// **Flow/Transition with depth remaining** — challenge ≈ skill: the codebook can
272+
/// still reach the point; descend HEEL→HIP→TWIG→LEAF one tier.
273+
DescendDeeper = 1,
274+
/// **Flow/Transition at leaf depth** — resolvable, but no finer tier remains: a
275+
/// sibling basin within the SAME classid codebook.
276+
ForkBasin = 2,
277+
/// **Anxiety at leaf depth** — challenge ≫ skill and irreducible in-domain: mint a
278+
/// NEW classid domain (HHTL shift = new exploration). Friston: switch the
279+
/// generative model when free energy can't be minimized within it.
280+
ForkDomain = 3,
281+
}
282+
283+
/// The fork decision. `challenge = residue_surprise(residue_mag, noise_floor,
284+
/// sigma_k)`; `in_domain_skill ∈ [0,1]` is the codebook's remaining resolving
285+
/// capacity. The challenge↔skill delta `δ` is banded on the shipped
286+
/// `flow_state_from` boundaries — **Anxiety `δ>0.2`** and **Boredom `δ<-0.2`** —
287+
/// then HHTL depth decides descend-vs-fork. The matched middle (`-0.2 ≤ δ ≤ 0.2`,
288+
/// which `flow_state_from` further splits into Flow `|δ|<0.15` and Transition) is
289+
/// uniformly "resolvable in-domain" here, so this ladder collapses Flow+Transition
290+
/// into one branch — the Flow/Transition distinction does not change the action.
291+
///
292+
/// * **Boredom** (`δ<-0.2`) → [`ForkAction::Commit`].
293+
/// * **Anxiety** (`δ>0.2`), `depth < max` → [`ForkAction::DescendDeeper`] (apply skill
294+
/// at a finer tier before declaring the residue irreducible — fork is a *leaf*
295+
/// condition).
296+
/// * **Anxiety** (`δ>0.2`), `depth == max` → [`ForkAction::ForkDomain`] (the orthogonal
297+
/// leaf residue is strong enough: free energy forks into a new domain).
298+
/// * **Flow/Transition** (`|δ|≤0.2`) → [`ForkAction::DescendDeeper`] while
299+
/// `depth < max`, else [`ForkAction::ForkBasin`].
300+
///
301+
/// # Examples
302+
/// ```
303+
/// use ndarray::hpc::entropy_ladder::{fork_decision, ForkAction};
304+
/// // Huge residue, low in-domain skill, already at the leaf → fork to a new domain.
305+
/// let a = fork_decision(1.0, 0.1, 3, 3, 0.004, 6.0);
306+
/// assert_eq!(a, ForkAction::ForkDomain);
307+
/// // Same surprise but a coarse tier remains → descend first, don't fork yet.
308+
/// assert_eq!(fork_decision(1.0, 0.1, 1, 3, 0.004, 6.0), ForkAction::DescendDeeper);
309+
/// // Tiny residue → the domain over-explains → commit.
310+
/// assert_eq!(fork_decision(0.002, 0.5, 3, 3, 0.004, 6.0), ForkAction::Commit);
311+
/// ```
312+
pub fn fork_decision(
313+
residue_mag: f64, in_domain_skill: f64, depth: u8, max_depth: u8, noise_floor: f64, sigma_k: f64,
314+
) -> ForkAction {
315+
let challenge = residue_surprise(residue_mag, noise_floor, sigma_k);
316+
let skill = in_domain_skill.clamp(0.0, 1.0);
317+
let delta = challenge - skill;
318+
let at_leaf = depth >= max_depth;
319+
if delta < -0.2 {
320+
// Boredom — skill over-covers the challenge.
321+
ForkAction::Commit
322+
} else if delta > 0.2 {
323+
// Anxiety — challenge exceeds skill. Fork only once the residue is a *leaf*
324+
// residue; otherwise a finer tier may still resolve it.
325+
if at_leaf {
326+
ForkAction::ForkDomain
327+
} else {
328+
ForkAction::DescendDeeper
329+
}
330+
} else {
331+
// Flow / Transition — matched. Resolve in-domain: descend if we can, else a
332+
// sibling basin in the same codebook.
333+
if at_leaf {
334+
ForkAction::ForkBasin
335+
} else {
336+
ForkAction::DescendDeeper
337+
}
338+
}
339+
}
340+
213341
#[cfg(test)]
214342
mod tests {
215343
use super::*;
@@ -271,6 +399,80 @@ mod tests {
271399
assert_eq!(entropy_class(0.99), 3);
272400
}
273401

402+
#[test]
403+
fn residue_surprise_floor_ramp_saturation() {
404+
// Below the noise floor → quantization only → zero surprise.
405+
assert!(residue_surprise(0.003, 0.004, 6.0) < 1e-12);
406+
assert!(residue_surprise(0.004, 0.004, 6.0) < 1e-12);
407+
// Linear ramp: excess / (sigma_k · nf). nf=0.004, span=0.024.
408+
assert!((residue_surprise(0.016, 0.004, 6.0) - 0.5).abs() < 1e-9);
409+
// Saturates at 1.
410+
assert!((residue_surprise(0.028, 0.004, 6.0) - 1.0).abs() < 1e-9);
411+
assert!((residue_surprise(10.0, 0.004, 6.0) - 1.0).abs() < 1e-9);
412+
// Monotone in residue magnitude.
413+
assert!(residue_surprise(0.01, 0.004, 6.0) < residue_surprise(0.02, 0.004, 6.0));
414+
}
415+
416+
#[test]
417+
fn residue_surprise_degenerate_calibration_is_finite() {
418+
// Codex #221: zero/negative calibration must not yield NaN (MIN_POSITIVE²
419+
// underflows to 0.0 → the span guard must run AFTER the multiply). Every
420+
// result stays finite and in [0, 1], so fork_decision's bands stay valid.
421+
for &(mag, nf, k) in &[(0.0, 0.0, 0.0), (0.5, 0.0, 0.0), (0.0, -1.0, -1.0), (1.0, 0.0, 6.0), (0.0, 0.004, 0.0)]
422+
{
423+
let s = residue_surprise(mag, nf, k);
424+
assert!(s.is_finite() && (0.0..=1.0).contains(&s), "got {s} for ({mag},{nf},{k})");
425+
}
426+
// And the downstream decision still lands in a real band (not the NaN path).
427+
let a = fork_decision(0.0, 0.5, 3, 3, 0.0, 0.0);
428+
assert!(matches!(a, ForkAction::Commit | ForkAction::ForkBasin | ForkAction::DescendDeeper));
429+
}
430+
431+
#[test]
432+
fn fork_ladder_four_actions() {
433+
let (nf, k) = (0.004, 6.0);
434+
// Boredom: tiny residue, ample skill → commit.
435+
assert_eq!(fork_decision(0.002, 0.6, 3, 3, nf, k), ForkAction::Commit);
436+
// Anxiety at leaf: strong orthogonal leaf residue, low skill → fork domain.
437+
assert_eq!(fork_decision(1.0, 0.1, 3, 3, nf, k), ForkAction::ForkDomain);
438+
// Anxiety but a coarse tier remains → descend before forking (leaf condition).
439+
assert_eq!(fork_decision(1.0, 0.1, 1, 3, nf, k), ForkAction::DescendDeeper);
440+
// Flow at leaf (challenge ≈ skill): resolvable in-domain → sibling basin.
441+
// challenge=0.5 (residue 0.016), skill=0.5 → delta 0 → Flow.
442+
assert_eq!(fork_decision(0.016, 0.5, 3, 3, nf, k), ForkAction::ForkBasin);
443+
// Flow with depth remaining → descend.
444+
assert_eq!(fork_decision(0.016, 0.5, 0, 3, nf, k), ForkAction::DescendDeeper);
445+
}
446+
447+
#[test]
448+
fn fork_domain_only_when_residue_is_strong_at_leaf() {
449+
let (nf, k) = (0.004, 6.0);
450+
// The operator's invariant: ForkDomain requires BOTH (a) leaf depth AND
451+
// (b) a residue strong enough that challenge ≫ skill. Weaken either and the
452+
// domain must NOT fork.
453+
assert_eq!(fork_decision(1.0, 0.1, 3, 3, nf, k), ForkAction::ForkDomain);
454+
// (a) not at leaf → descend, never fork.
455+
assert_ne!(fork_decision(1.0, 0.1, 2, 3, nf, k), ForkAction::ForkDomain);
456+
// (b) skill matches the (saturated) challenge → Flow, not Anxiety → basin.
457+
assert_ne!(fork_decision(1.0, 0.9, 3, 3, nf, k), ForkAction::ForkDomain);
458+
}
459+
460+
#[test]
461+
fn fork_anxiety_aligns_with_high_surprise_quadrant() {
462+
// Cross-check the unification: an Anxiety/ForkDomain residue is high-challenge,
463+
// so on the entropy×energy plane (challenge as entropy) it lands in the
464+
// high-entropy half (Staunen at low energy / Confusion at high energy) — never
465+
// Boredom/Wisdom. This ties ForkAction to the shipped Quadrant.
466+
let challenge = residue_surprise(1.0, 0.004, 6.0); // saturated → 1.0
467+
assert!(challenge >= 0.5);
468+
assert_eq!(Quadrant::classify(challenge, 0.1), Quadrant::Staunen);
469+
assert_eq!(Quadrant::classify(challenge, 0.9), Quadrant::Confusion);
470+
// And a Boredom/Commit residue is low-challenge → low-entropy half.
471+
let calm = residue_surprise(0.002, 0.004, 6.0); // 0.0
472+
assert_eq!(Quadrant::classify(calm, 0.1), Quadrant::Boredom);
473+
assert_eq!(Quadrant::classify(calm, 0.9), Quadrant::Wisdom);
474+
}
475+
274476
/// Validation: entropy is a reliability proxy. Build a population of edges
275477
/// whose belief `(f, c)` is estimated from `n_obs` Bernoulli(p) draws, then
276478
/// measure each edge's empirical prediction accuracy against fresh draws.

0 commit comments

Comments
 (0)