Volkan36's picture
undefined - Initial Deployment
e25139f verified
Raw
History Blame Contribute Delete
21.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SSOA — Agent‑Based Simulation (HTML+JS)</title>
<style>
:root{--bg:#0b0e16;--fg:#e6edf3;--muted:#8892a6;--accent:#7aa2f7;--good:#3fb950;--warn:#f2cc60;--bad:#f85149;}
html,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;background:var(--bg);color:var(--fg);}
.row{display:flex;gap:16px;align-items:stretch}
.col{display:flex;flex-direction:column;gap:12px}
.card{background:#11162a;border:1px solid #1e263d;border-radius:14px;padding:12px}
h1{font-size:20px;margin:8px 0 4px}
h2{font-size:16px;margin:8px 0}
label{font-size:12px;color:var(--muted)}
input[type="range"]{width:100%}
input[type="number"],select{background:#0f1425;color:var(--fg);border:1px solid #1e263d;border-radius:8px;padding:6px}
button{background:#182040;color:var(--fg);border:1px solid #243056;border-radius:10px;padding:8px 10px;cursor:pointer}
button:hover{background:#1a274d}
.metrics{display:flex;gap:10px;flex-wrap:wrap;font-size:12px;color:#cdd6e4}
.badge{background:#0f1425;border:1px solid #1e263d;border-radius:999px;padding:4px 8px}
canvas{background:#0a0f1f;border:1px solid #1e263d;border-radius:14px}
.grid{display:grid;grid-template-columns:repeat(2,minmax(180px,1fr));gap:12px}
.hint{font-size:12px;color:var(--muted)}
.small{font-size:11px;color:var(--muted)}
.mono{font-family:ui-monospace,SFMono-Regular,Consolas,Monaco,monospace}
</style>
</head>
<body>
<div class="row" style="padding:12px">
<div class="col" style="flex:1 1 340px;max-width:430px">
<div class="card">
<h1>Secret Societies Optimisation Algorithm (SSOA)</h1>
<div class="hint">Agent‑based simulation with hierarchical lodges → cells, secrecy‑masked reporting, grand directives, covert exchange, infiltration, purge, and oath.</div>
</div>
<div class="card">
<h2>Controls</h2>
<div class="grid">
<div>
<label>Objective</label>
<select id="objective">
<option value="sphere">Sphere (2‑D)</option>
<option value="rastrigin">Rastrigin (2‑D)</option>
<option value="ackley">Ackley (2‑D)</option>
</select>
</div>
<div>
<label>Bounds</label>
<select id="bounds">
<option value="sphere">[-5, 5]</option>
<option value="wide">[-10, 10]</option>
<option value="narrow">[-3, 3]</option>
</select>
</div>
<div>
<label>Population N</label>
<input id="N" type="number" min="12" max="400" step="1" value="90" />
</div>
<div>
<label>Lodges L</label>
<input id="L" type="number" min="1" max="9" step="1" value="3" />
</div>
<div>
<label>Cells per Lodge C</label>
<input id="C" type="number" min="1" max="8" step="1" value="3" />
</div>
<div>
<label>Agents per Cell m</label>
<input id="m" type="number" min="2" max="12" step="1" value="10" />
</div>
<div>
<label>Secrecy s</label>
<input id="s" type="range" min="0" max="1" step="0.05" value="0.35" />
<span class="small mono" id="sVal">0.35</span>
</div>
<div>
<label>Directive α (start→end)</label>
<input id="alpha0" type="number" min="0" max="1" step="0.05" value="0.12" />
<input id="alpha1" type="number" min="0" max="1" step="0.05" value="0.6" />
</div>
<div>
<label>Drift ρ₀</label>
<input id="rho0" type="number" min="0.01" max="2" step="0.01" value="0.4" />
</div>
<div>
<label>Decay γ</label>
<input id="gamma" type="number" min="0.90" max="0.999" step="0.001" value="0.985" />
</div>
<div>
<label>Covert exchange p<sub>cx</sub></label>
<input id="pcx" type="number" min="0" max="0.5" step="0.01" value="0.06" />
</div>
<div>
<label>Infiltration p<sub>inf</sub></label>
<input id="pinf" type="number" min="0" max="0.5" step="0.01" value="0.12" />
</div>
<div>
<label>Diversity threshold</label>
<input id="divThresh" type="number" min="0" max="2" step="0.01" value="0.12" />
</div>
<div>
<label>Purge ratio π</label>
<input id="pi" type="number" min="0" max="0.5" step="0.01" value="0.08" />
</div>
<div>
<label>Epochs per sec</label>
<input id="speed" type="number" min="1" max="240" step="1" value="60" />
</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px">
<button id="startBtn">Start</button>
<button id="pauseBtn">Pause</button>
<button id="stepBtn">Step</button>
<button id="resetBtn">Reset</button>
<button id="seedBtn">New Seed</button>
</div>
<div class="metrics" id="metrics" style="margin-top:8px"></div>
</div>
<div class="card">
<h2>Notes</h2>
<div class="hint">Visual pane maps 2‑D decision space. Colour indicates lodge membership; subtle inner ring indicates cell leaders. Chart shows best objective value across epochs. Projection preserves bounds. Diversity metric uses median pairwise distance normalised by domain width.</div>
</div>
</div>
<div class="col" style="flex:2 1 680px">
<div class="card">
<h2>Search Space</h2>
<canvas id="field" width="800" height="520"></canvas>
</div>
<div class="card">
<h2>Best Fitness</h2>
<canvas id="chart" width="800" height="220"></canvas>
</div>
</div>
</div>
<script>
(function(){
// ---------- Utilities ----------
const RNG = (seed)=>{ // mulberry32
let t = seed >>> 0;
return ()=>{
t += 0x6D2B79F5;
let r = Math.imul(t ^ t >>> 15, 1 | t);
r ^= r + Math.imul(r ^ r >>> 7, 61 | r);
return ((r ^ r >>> 14) >>> 0) / 4294967296;
}
};
const choice = (rng, arr)=>arr[(rng()*arr.length)|0];
const clamp = (x,a,b)=>Math.max(a,Math.min(b,x));
const projBox = (x, lo, hi)=>x.map((v,i)=>clamp(v,lo[i],hi[i]));
const add = (a,b)=>[a[0]+b[0], a[1]+b[1]];
const sub = (a,b)=>[a[0]-b[0], a[1]-b[1]];
const mul = (a,s)=>[a[0]*s, a[1]*s];
const had = (a,b)=>[a[0]*b[0], a[1]*b[1]];
const norm = (a)=>Math.hypot(a[0],a[1]);
const randn = (rng)=>{ // Box–Muller
let u = 1 - rng();
let v = 1 - rng();
return Math.sqrt(-2*Math.log(u)) * Math.cos(2*Math.PI*v);
};
// ---------- Objectives ----------
function makeObjective(name){
if(name==='sphere'){
return {
f:(x)=>x[0]*x[0]+x[1]*x[1],
label:'Sphere'
};
}
if(name==='rastrigin'){
return {
f:(x)=>20 + (x[0]*x[0]-10*Math.cos(2*Math.PI*x[0])) + (x[1]*x[1]-10*Math.cos(2*Math.PI*x[1])),
label:'Rastrigin'
};
}
// Ackley
return {
f:(x)=>{
const a=20,b=0.2,c=2*Math.PI;
const s = 0.5*(x[0]*x[0]+x[1]*x[1]);
const q = 0.5*(Math.cos(c*x[0])+Math.cos(c*x[1]));
return -a*Math.exp(-b*Math.sqrt(s)) - Math.exp(q) + a + Math.E;
},
label:'Ackley'
};
}
// ---------- SSOA Core ----------
class SSOA {
constructor(cfg){
Object.assign(this, cfg);
this.dim = 2;
this.rng = RNG(cfg.seed||123456);
this.obj = makeObjective(cfg.objective);
this.bounds = cfg.bounds;
this.epoch = 0;
this.makePopulation();
this.best = {x:[...this.pop[0].x], f:Infinity};
this.evaluateAll();
this.initDiversity = this.diversity();
this.history = [];
}
makePopulation(){
const [lo,hi] = this.bounds;
const N = this.L*this.C*this.m;
this.pop = Array.from({length:N}, (_,i)=>({
x:[ lo[0] + (hi[0]-lo[0])*this.rng(), lo[1] + (hi[1]-lo[1])*this.rng() ],
f:Infinity,
lodge: Math.floor(i/(this.C*this.m)),
cell: Math.floor((i/this.m)%this.C)
}));
}
evaluateAll(){
for(const a of this.pop){ a.f = this.obj.f(a.x); }
this.updateBest();
}
updateBest(){
for(const a of this.pop){ if(a.f < this.best.f){ this.best = {x:[...a.x], f:a.f}; } }
}
lodgeMembers(L){ return this.pop.filter(a=>a.lodge===L); }
cellMembers(L,C){ return this.pop.filter(a=>a.lodge===L && a.cell===C); }
leaders(){
// cell leaders then lodge champions
const cellBest = [];
for(let L=0; L<this.L; L++){
for(let C=0; C<this.C; C++){
const mem = this.cellMembers(L,C);
let b = mem[0];
for(const a of mem){ if(a.f<b.f) b=a; }
cellBest.push(b);
}
}
const lodgeBest = [];
for(let L=0; L<this.L; L++){
const mem = this.lodgeMembers(L);
let b = mem[0];
for(const a of mem){ if(a.f<b.f) b=a; }
lodgeBest.push(b);
}
return {cellBest, lodgeBest};
}
mask(x){
// secrecy fraction s: choose s*dim coordinates to disclose
const m = [0,0];
let k = (this.s*this.dim)|0; if(k<1) k=1; // at least one coordinate
const idx = [0,1];
for(let i=0;i<k;i++) m[idx.splice((this.rng()*idx.length)|0,1)[0]] = 1;
return [x[0]*m[0], x[1]*m[1]];
}
aggregateBeacon(){
const {lodgeBest} = this.leaders();
// average masked coordinates from lodge champions
let acc=[0,0], cnt=[0,0];
for(const a of lodgeBest){
const M = this.mask(a.x);
if(M[0]!==0){ acc[0]+=M[0]; cnt[0]++; }
if(M[1]!==0){ acc[1]+=M[1]; cnt[1]++; }
}
const g=[0,0];
g[0]=cnt[0]? acc[0]/cnt[0] : 0;
g[1]=cnt[1]? acc[1]/cnt[1] : 0;
return g;
}
recruitment(){
// scouts = lodge champions; spawn around with uniform jitter scaled by rt
const [lo,hi] = this.bounds;
const width=[hi[0]-lo[0], hi[1]-lo[1]];
const rt = this.rho0*Math.pow(this.gamma, this.epoch);
const {lodgeBest} = this.leaders();
const recruits=[];
for(const s of lodgeBest){
const jitter=[(this.rng()*2-1)*rt*width[0], (this.rng()*2-1)*rt*width[1]];
const cand = projBox(add(s.x, jitter), lo, hi);
recruits.push({x:cand, f: this.obj.f(cand)});
}
return recruits;
}
initiation(recruits){
// duel against random member of each cell
const pool=[];
for(const r of recruits){ pool.push(r); }
// replace worst in random cells if recruit wins
for(let L=0; L<this.L; L++){
for(let C=0; C<this.C; C++){
const mem = this.cellMembers(L,C);
const challenger = choice(this.rng, pool);
// incumbent random
const idx = (this.rng()*mem.length)|0;
const inc = mem[idx];
const winnerBetter = challenger.f < inc.f;
if(winnerBetter){
inc.x = [...challenger.x]; inc.f = challenger.f;
}
}
}
}
ritualDrift(){
const [lo,hi] = this.bounds; const width=[hi[0]-lo[0], hi[1]-lo[1]];
const rt = this.rho0*Math.pow(this.gamma, this.epoch);
const {cellBest} = this.leaders();
for(const a of this.pop){
const leader = cellBest.find(b=>b.lodge===a.lodge && b.cell===a.cell) || a;
const noise=[randn(this.rng)*rt*width[0], randn(this.rng)*rt*width[1]];
const pull= mul(sub(leader.x, a.x), this.beta);
const x1 = projBox(add(add(a.x, noise), pull), lo, hi);
a.x = x1; a.f = this.obj.f(a.x);
}
}
directives(){
const g = this.aggregateBeacon();
const [lo,hi] = this.bounds;
const a = this.alphaSchedule();
for(const x of this.pop){
const step = mul(sub(g, x.x), a);
const y = projBox(add(x.x, step), lo, hi);
x.x = y; x.f = this.obj.f(y);
}
}
covertExchange(){
if(this.rng() > this.pcx) return;
if(this.L<2) return;
const L1 = (this.rng()*this.L)|0; let L2 = (this.rng()*this.L)|0; if(L2===L1) L2=(L2+1)%this.L;
const A = this.lodgeMembers(L1), B = this.lodgeMembers(L2);
const a = choice(this.rng,A), b = choice(this.rng,B);
// masked substring swap: choose one coordinate index
const i = (this.rng()*2)|0;
const tmp = a.x[i]; a.x[i]=b.x[i]; b.x[i]=tmp;
a.f=this.obj.f(a.x); b.f=this.obj.f(b.x);
}
infiltration(){
// trigger when diversity below threshold
const d = this.diversity();
if(d >= this.divThresh) return;
if(this.rng() > this.pinf) return;
const [lo,hi] = this.bounds;
// pick random cell, replace worst with recruit near a random scout
const L = (this.rng()*this.L)|0, C=(this.rng()*this.C)|0;
const mem = this.cellMembers(L,C);
let worst = mem[0];
for(const a of mem){ if(a.f > worst.f) worst=a; }
const {lodgeBest} = this.leaders();
const s = choice(this.rng, lodgeBest);
const rt = this.rho0*Math.pow(this.gamma, this.epoch);
const jitter=[(this.rng()*2-1)*rt, (this.rng()*2-1)*rt];
const cand = projBox(add(s.x, jitter), lo, hi);
worst.x = cand; worst.f = this.obj.f(cand);
}
purge(){
const M = Math.max(1, Math.floor(this.pi * this.pop.length));
const sorted = [...this.pop].sort((a,b)=>a.f-b.f);
const survivors = sorted.slice(0, this.pop.length - M);
const purged = sorted.slice(this.pop.length - M);
// refill: half recruits, half refined champions
const {lodgeBest} = this.leaders();
const [lo,hi] = this.bounds; const width=[hi[0]-lo[0], hi[1]-lo[1]];
const rt = 0.3*this.rho0*Math.pow(this.gamma, this.epoch);
for(const _ of purged){
if(this.rng()<0.5){
const s = choice(this.rng, lodgeBest);
const jitter=[randn(this.rng)*rt*width[0], randn(this.rng)*rt*width[1]];
survivors.push({x:projBox(add(s.x,jitter), lo, hi), f:Infinity, lodge:0, cell:0});
} else {
// random recruit within bounds
survivors.push({x:[ lo[0]+(hi[0]-lo[0])*this.rng(), lo[1]+(hi[1]-lo[1])*this.rng() ], f:Infinity, lodge:0, cell:0});
}
}
// reassign structure
const N = survivors.length; // unchanged
for(let i=0;i<N;i++){ survivors[i].lodge = Math.floor(i/(this.C*this.m)); survivors[i].cell = Math.floor((i/this.m)%this.C); }
this.pop = survivors;
this.evaluateAll();
}
oath(){ this.updateBest(); }
alphaSchedule(){
// cosine ramp from alpha0 to alpha1 using variance proxy
const t = Math.min(1, this.epoch/1000);
const w = 0.5 - 0.5*Math.cos(Math.PI*t);
return this.alpha0*(1-w) + this.alpha1*w;
}
diversity(){
// median pairwise distance / domain width
const [lo,hi]=this.bounds; const width = Math.hypot(hi[0]-lo[0], hi[1]-lo[1]);
const pts = this.pop.map(a=>a.x);
const dists=[]; const M=pts.length;
for(let i=0;i<M;i++) for(let j=i+1;j<M;j++) dists.push(Math.hypot(pts[i][0]-pts[j][0], pts[i][1]-pts[j][1]));
dists.sort((a,b)=>a-b);
const med = dists.length? dists[(dists.length/2)|0] : 0;
return med/width;
}
step(){
this.epoch++;
const recruits = this.recruitment();
this.initiation(recruits);
this.ritualDrift();
this.directives();
this.covertExchange();
this.infiltration();
this.purge();
this.oath();
this.history.push(this.best.f);
if(this.history.length>800) this.history.shift();
}
}
// ---------- Rendering ----------
const field = document.getElementById('field');
const chart = document.getElementById('chart');
const ctx = field.getContext('2d');
const gtx = chart.getContext('2d');
function render(sim){
// field
ctx.clearRect(0,0,field.width,field.height);
// background contours via sampling grid (fast coarse shading)
const W=field.width, H=field.height;
const cells=40; const cw=W/cells, ch=H/cells;
const [lo,hi]=sim.bounds;
for(let i=0;i<cells;i++){
for(let j=0;j<cells;j++){
const x=lo[0] + (i+0.5)/cells*(hi[0]-lo[0]);
const y=lo[1] + (j+0.5)/cells*(hi[1]-lo[1]);
const v = sim.obj.f([x,y]);
const shade = Math.max(0, Math.min(1, 1 - 1/(1+v)));
ctx.fillStyle = `rgba(122,162,247,${0.06*shade})`;
ctx.fillRect(i*cw, H-(j+1)*ch, cw, ch);
}
}
// axes
ctx.strokeStyle='#243056'; ctx.lineWidth=1;
ctx.strokeRect(0,0,W,H);
const colours=['#7aa2f7','#9ece6a','#f7768e','#bb9af7','#e0af68','#2ac3de','#f7768e'];
const [loB,hiB]=sim.bounds; const sx=W/(hiB[0]-loB[0]), sy=H/(hiB[1]-loB[1]);
const X=(p)=> (p[0]-loB[0])*sx; const Y=(p)=> H - (p[1]-loB[1])*sy;
// draw agents by lodge
for(let L=0; L<sim.L; L++){
const col = colours[L%colours.length];
ctx.fillStyle=col; ctx.strokeStyle=col;
// cell leaders ring cache
const {cellBest} = sim.leaders();
const leaders = cellBest.filter(a=>a.lodge===L);
// members
const mem = sim.lodgeMembers(L);
for(const a of mem){
const r=3.2;
ctx.beginPath(); ctx.arc(X(a.x), Y(a.x), r, 0, Math.PI*2); ctx.fill();
}
// leaders as ring
ctx.strokeStyle='#e6edf3';
for(const b of leaders){
ctx.beginPath(); ctx.arc(X(b.x), Y(b.x), 6, 0, Math.PI*2); ctx.stroke();
}
}
// global best star
ctx.fillStyle= '#3fb950';
star(ctx, X(sim.best.x), Y(sim.best.x), 6, 12, 5);
// chart: best history
gtx.clearRect(0,0,chart.width,chart.height);
gtx.strokeStyle = '#7aa2f7'; gtx.lineWidth=2;
const arr = sim.history; if(arr.length>1){
const min = Math.min(...arr), max = Math.max(...arr);
const pad = 0.001;
gtx.beginPath();
for(let i=0;i<arr.length;i++){
const x = i/(arr.length-1)*chart.width;
const y = chart.height - ((arr[i]-min)/Math.max(pad,(max-min))) * chart.height;
if(i===0) gtx.moveTo(x,y); else gtx.lineTo(x,y);
}
gtx.stroke();
}
}
function star(g,x,y,r1,r2,points){
g.beginPath();
for(let i=0;i<points*2;i++){
const r = i%2? r1: r2;
const a = i*Math.PI/points;
const px = x + Math.cos(a)*r;
const py = y + Math.sin(a)*r;
if(i===0) g.moveTo(px,py); else g.lineTo(px,py);
}
g.closePath(); g.fill();
}
// ---------- UI & Loop ----------
const $ = (id)=>document.getElementById(id);
const sVal = $('sVal');
$('s').addEventListener('input', e=>{ sVal.textContent=Number(e.target.value).toFixed(2); });
let sim = null; let running=false; let last=0; let acc=0;
function cfgFromUI(seed){
const boundsSel = $('bounds').value;
let bounds = {
'sphere':[[-5, -5],[5,5]], 'wide':[[-10,-10],[10,10]], 'narrow':[[-3,-3],[3,3]]
}[boundsSel];
return {
objective: $('objective').value,
bounds: bounds,
L: Number($('L').value|0),
C: Number($('C').value|0),
m: Number($('m').value|0),
rho0: Number($('rho0').value),
gamma: Number($('gamma').value),
s: Number($('s').value),
alpha0: Number($('alpha0').value),
alpha1: Number($('alpha1').value),
pcx: Number($('pcx').value),
pinf: Number($('pinf').value),
divThresh: Number($('divThresh').value),
pi: Number($('pi').value),
seed: seed ?? Math.floor(Math.random()*1e9)
};
}
function reset(seed){
// ensure N consistency = L*C*m
const Nreq = Number($('L').value)*Number($('C').value)*Number($('m').value);
$('N').value = Nreq;
sim = new SSOA(cfgFromUI(seed));
render(sim);
updateMetrics();
}
function updateMetrics(){
const m = document.getElementById('metrics');
const div = sim.diversity();
m.innerHTML = `
<span class="badge">epoch <span class="mono">${sim.epoch}</span></span>
<span class="badge">best f <span class="mono">${sim.best.f.toExponential(3)}</span></span>
<span class="badge">diversity <span class="mono">${div.toFixed(3)}</span></span>
<span class="badge">α <span class="mono">${sim.alphaSchedule().toFixed(3)}</span></span>
<span class="badge">seed <span class="mono">${sim.rng().toString().slice(2)}</span></span>
`;
}
$('startBtn').onclick=()=>{ running=true; };
$('pauseBtn').onclick=()=>{ running=false; };
$('stepBtn').onclick=()=>{ running=false; tick(1/60,true); };
$('resetBtn').onclick=()=>{ running=false; reset(); };
$('seedBtn').onclick=()=>{ reset(Math.floor(Math.random()*1e9)); };
function tick(dt,force){
const targetHz = Number($('speed').value);
acc += dt*targetHz;
while(acc>=1 || force){
sim.step();
acc-=1; if(force) break;
}
render(sim);
updateMetrics();
}
function loop(ts){
const dt = Math.min(0.1, (ts-last)/1000 || 0); last=ts;
if(running){ tick(dt,false); }
requestAnimationFrame(loop);
}
reset(123456789);
requestAnimationFrame(loop);
})();
</script>
</body>
</html>