OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / basal-agent / belief_field_agent.c

143 lines · c ref:
/* belief_field_agent.c — the SEED: a world model that lives as a belief-FIELD over coupled units,
 * infers by belief-mating, and drives a DECISION toward a setpoint. The base case of the design.
 *
 * Architecture (each piece traces to a validated result of the arc):
 *   SUBSTRATE : N units on a ring, each holding a belief vector b_i in R^3 (the "cells").
 *   FIELD     : the collective belief across units = the world model (pattern memory). Units are
 *               gap-junction-coupled (diffusion) -> a COHERENCE DOMAIN = the individual (the boundary).
 *   INFERENCE : fast loop = BELIEF-MATING (predict via the transition operator, correct via emission),
 *               exactly the mess3 filter (mess3.c) distributed over the field.
 *   DECISION  : a setpoint g (goal state); the effector picks the action that its belief predicts will
 *               drive the world toward g. Active-inference flavored: act to reach the setpoint.
 *   LEARNING  : (slow loop) the operators are learned by method-of-moments (mom.c, the validated
 *               valley-crosser). This SEED is handed the operators to isolate the NEW parts the arc
 *               never built -- the distributed FIELD and the closed perception->decision->action LOOP.
 *
 * Environment: a CONTROLLABLE 3-cause POMDP. Hidden s in {0,1,2}; action a shifts it (next=(s+a)%3
 * w.p. 1-tn, else uniform) so the effect depends on WHERE you are -> you must INFER s to act well;
 * emission x is noisy (correct w.p. 1-en). Goal: hold state g. A no-model agent (acts on raw x) must
 * lose to a belief agent. Validation: FIELD goal-occupancy ~ OPTIMAL filter >> NO-MODEL; and the field's
 * collective belief tracks the exact filter (inference is correct) while staying coherent (boundary holds).
 *
 * build:  cc -O2 -Wall -std=c11 -o belief_field_agent belief_field_agent.c -lm
 */
#include <math.h>
#include <stdio.h>

#define K   3          /* causes / symbols / actions */
#define N   12         /* field units (the coherence domain) */
#define TICKS 300000
#define TN  0.15       /* transition noise */
#define EN  0.60       /* emission noise: raw x is right only 60% -> reactive control fails, inference wins */
#define DIFF 0.30      /* gap-junction diffusion (keeps the coherence domain together) */
#define UNITNOISE 0.01 /* per-unit corruption (the field must denoise via coupling) */
#define GOAL 0

static unsigned long long RNG;
static unsigned long long xs(void){ RNG^=RNG<<13; RNG^=RNG>>7; RNG^=RNG<<17; return RNG; }
static double urand(void){ return (double)(xs()>>11)*(1.0/9007199254740992.0); }
static int    rk(void){ return (int)(xs()%K); }

/* --- environment operators (the agent's model = these; learning them = mom.c) --- */
static double Ptrans(int j,int i,int a){ return (1.0-TN)*((j==(i+a)%K)?1.0:0.0) + TN/K; } /* P(next=j|s=i,a) */
static double Pemit (int x,int s){ return (1.0-EN)*((x==s)?1.0:0.0) + EN/K; }             /* P(x|s) */
static int step_env(int s,int a){ return (urand()<1.0-TN)? (s+a)%K : rk(); }
static int emit_env(int s){ return (urand()<1.0-EN)? s : rk(); }

static void norm(double*b){ double z=0; for(int j=0;j<K;j++) z+=b[j]; if(z<1e-300)z=1e-300; for(int j=0;j<K;j++) b[j]/=z; }

/* belief-mating step (predict via transition op given action a, correct via emission of x) */
static void mate(double*b,int a,int x){
	double bp[K]={0,0,0};
	for(int j=0;j<K;j++){ double s=0; for(int i=0;i<K;i++) s+=b[i]*Ptrans(j,i,a); bp[j]=s; }   /* predict */
	for(int j=0;j<K;j++) bp[j]*=Pemit(x,j);                                                    /* correct */
	norm(bp); for(int j=0;j<K;j++) b[j]=bp[j];
}
/* decision: pick the action whose predicted next-belief puts most mass on the goal g */
static int decide(const double*b){
	int best=0; double bv=-1;
	for(int a=0;a<K;a++){ double m=0; for(int i=0;i<K;i++) m+=b[i]*Ptrans(GOAL,i,a); if(m>bv){bv=m;best=a;} }
	return best;
}

enum { OPTIMAL, FIELD, NOMODEL, RANDOM };

/* run one controller for TICKS; returns goal-occupancy. For FIELD also writes belief-divergence vs the
   exact filter and inter-unit coherence. */
static double run(int type,double*field_kl,double*field_coh){
	RNG=0x9e3779b97f4a7c15ULL;
	int s=rk(); long hits=0;
	double bf[K]={1.0/K,1.0/K,1.0/K};                 /* single exact belief (OPTIMAL / shadow for FIELD) */
	double u[N][K]; for(int n=0;n<N;n++) for(int j=0;j<K;j++) u[n][j]=1.0/K;   /* the FIELD */
	double klsum=0, cohsum=0; long m=0;

	for(long t=0;t<TICKS;t++){
		/* collective belief of the field */
		double c[K]={0,0,0}; for(int n=0;n<N;n++) for(int j=0;j<K;j++) c[j]+=u[n][j]/N;
		/* DECISION */
		int a;
		if(type==OPTIMAL)    a=decide(bf);
		else if(type==FIELD) a=decide(c);
		else                 a=rk();

		/* world steps */
		s=step_env(s,a); int x=emit_env(s); if(s==GOAL) hits++;

		/* INFERENCE */
		if(type==OPTIMAL||type==FIELD) mate(bf,a,x);          /* exact filter (also shadow for FIELD) */
		if(type==FIELD){
			/* each unit belief-mates, is corrupted, then diffuses toward its neighbours (gap junctions) */
			double nu[N][K];
			for(int n=0;n<N;n++){ double b[K]; for(int j=0;j<K;j++) b[j]=u[n][j];
				mate(b,a,x);
				for(int j=0;j<K;j++) b[j]*= (1.0 + UNITNOISE*(urand()*2-1));
				norm(b);
				int L=(n-1+N)%N,R=(n+1)%N;
				for(int j=0;j<K;j++) nu[n][j]=(1-DIFF)*b[j]+DIFF*0.5*(u[L][j]+u[R][j]);
				norm(nu[n]); }
			for(int n=0;n<N;n++) for(int j=0;j<K;j++) u[n][j]=nu[n][j];
			/* validation: field collective vs exact filter (KL), and inter-unit coherence */
			double cc[K]={0,0,0}; for(int n=0;n<N;n++) for(int j=0;j<K;j++) cc[j]+=u[n][j]/N;
			double kl=0; for(int j=0;j<K;j++){ double p=cc[j]<1e-9?1e-9:cc[j], q=bf[j]<1e-9?1e-9:bf[j]; kl+=p*log(p/q); }
			double coh=0; for(int n=0;n<N;n++){ double d=0; for(int j=0;j<K;j++) d+=fabs(u[n][j]-cc[j]); coh+=1.0-d/2; }
			klsum+=kl; cohsum+=coh/N; m++;
		}
	}
	if(type==FIELD){ *field_kl=klsum/m; *field_coh=cohsum/m; }
	return (double)hits/TICKS;
}

/* NOMODEL needs to remember last obs; simplest: re-run a dedicated loop */
static double run_nomodel(void){
	RNG=0x9e3779b97f4a7c15ULL; int s=rk(),lastx=0; long hits=0;
	for(long t=0;t<TICKS;t++){ int a=((GOAL-lastx)%K+K)%K; s=step_env(s,a); int x=emit_env(s); if(s==GOAL)hits++; lastx=x; }
	return (double)hits/TICKS;
}

int main(void){
	double kl=0,coh=0,dummy;
	double opt = run(OPTIMAL,&dummy,&dummy);
	double fld = run(FIELD,&kl,&coh);
	double nom = run_nomodel();
	double rnd = run(RANDOM,&dummy,&dummy);

	printf("=== Belief-field agent (SEED) on a controllable 3-cause POMDP ===\n");
	printf("  N=%d field units, goal=state %d, emission noise=%.2f, transition noise=%.2f.\n",N,GOAL,EN,TN);
	printf("  Decision = act toward the setpoint using the field's belief. Chance occupancy = %.3f.\n\n",1.0/K);
	printf("  controller            | goal-occupancy\n");
	printf("  ----------------------+---------------\n");
	printf("  OPTIMAL exact filter  |     %.3f\n", opt);
	printf("  FIELD (belief-field)  |     %.3f\n", fld);
	printf("  NO-MODEL (raw obs)    |     %.3f\n", nom);
	printf("  RANDOM                |     %.3f\n", rnd);
	printf("\n");
	printf("  field inference fidelity: KL(field collective || exact filter) = %.4f nats  (->0 = correct)\n", kl);
	printf("  coherence domain: mean inter-unit agreement = %.3f  (->1 = the field holds together = one individual)\n", coh);
	printf("\n");
	if(fld > nom+0.04 && fld > opt-0.03 && coh > 0.95 && kl < 0.06)
		printf("  => SEED WORKS. The world model living as a distributed belief-FIELD infers correctly (tracks the\n     exact filter) while staying coherent (one individual), and DRIVES DECISIONS that reach the setpoint\n     ~as well as the optimal controller and far better than acting on raw observation. Perception->belief\n     ->decision->action is closed. Fast loop = belief-mating (mess3); boundary = the coherence domain;\n     goal = the setpoint. Learning the operators = mom.c plugs into the slow loop. Base case complete.\n");
	else
		printf("  => Not yet: FIELD did not clearly beat NO-MODEL and match OPTIMAL with faithful inference.\n     Read the numbers; adjust before claiming the seed works.\n");
	return 0;
}