Tab/space alignment consistency from running gofmt on all packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
181 lines
5.3 KiB
Go
181 lines
5.3 KiB
Go
// Package mapelites implements a 4-D MAP-Elites behavior grid for diversity
|
||
// maintenance in the evolution pipeline (plan §10.2).
|
||
//
|
||
// The four behavior dimensions are:
|
||
//
|
||
// X axis – Aggression (0.0 = pacifist … 1.0 = full aggressor)
|
||
// Y axis – Economy (0.0 = ignores energy … 1.0 = perfect economy)
|
||
// Z axis – Exploration (0.0 = stays near core … 1.0 = covers >80% of map)
|
||
// W axis – Formation (0.0 = units scattered … 1.0 = units always grouped)
|
||
//
|
||
// Each dimension is binned into Size levels, producing Size⁴ cells.
|
||
// Plan §10.2 specifies Size=3 → 3⁴ = 81 cells.
|
||
package mapelites
|
||
|
||
import "math"
|
||
|
||
// NumDims is the number of behavior dimensions.
|
||
const NumDims = 4
|
||
|
||
// Grid is a 4-D MAP-Elites behavior grid.
|
||
type Grid struct {
|
||
size int
|
||
cells map[[NumDims]int]Cell
|
||
}
|
||
|
||
// Cell is a single niche in the grid.
|
||
type Cell struct {
|
||
ProgramID int64
|
||
Fitness float64
|
||
Occupied bool
|
||
}
|
||
|
||
// Placement records which grid cell a program was placed into.
|
||
type Placement struct {
|
||
X, Y, Z, W int
|
||
}
|
||
|
||
// Key returns the [4]int array key for this placement.
|
||
func (p Placement) Key() [NumDims]int {
|
||
return [NumDims]int{p.X, p.Y, p.Z, p.W}
|
||
}
|
||
|
||
// New creates an empty Grid with the given side length per dimension.
|
||
// Total cells = size⁴. Use size=3 for the 81-cell grid per §10.2.
|
||
func New(size int) *Grid {
|
||
return &Grid{size: size, cells: make(map[[NumDims]int]Cell)}
|
||
}
|
||
|
||
// Size returns the side length of the grid (per dimension).
|
||
func (g *Grid) Size() int {
|
||
return g.size
|
||
}
|
||
|
||
// TotalCells returns size⁴.
|
||
func (g *Grid) TotalCells() int {
|
||
return g.size * g.size * g.size * g.size
|
||
}
|
||
|
||
// dimBin converts a continuous [0, 1] value to a discrete bin in [0, size-1].
|
||
func (g *Grid) dimBin(v float64) int {
|
||
return int(math.Min(math.Floor(v*float64(g.size)), float64(g.size-1)))
|
||
}
|
||
|
||
// BehaviorToCell converts continuous behavior values (each in [0, 1]) to
|
||
// discrete grid coordinates clamped to [0, size-1].
|
||
func (g *Grid) BehaviorToCell(aggression, economy, exploration, formation float64) (x, y, z, w int) {
|
||
return g.dimBin(aggression), g.dimBin(economy), g.dimBin(exploration), g.dimBin(formation)
|
||
}
|
||
|
||
// TryPlace attempts to place a program in the cell determined by its behavior
|
||
// vector. The cell is updated only when it is empty or the new program has
|
||
// strictly higher fitness than the incumbent.
|
||
// Returns the target cell coordinates and whether the cell was updated.
|
||
func (g *Grid) TryPlace(id int64, fitness, aggression, economy, exploration, formation float64) (Placement, bool) {
|
||
x, y, z, w := g.BehaviorToCell(aggression, economy, exploration, formation)
|
||
key := [NumDims]int{x, y, z, w}
|
||
cell, exists := g.cells[key]
|
||
|
||
if !exists || fitness > cell.Fitness {
|
||
g.cells[key] = Cell{ProgramID: id, Fitness: fitness, Occupied: true}
|
||
return Placement{X: x, Y: y, Z: z, W: w}, true
|
||
}
|
||
return Placement{X: x, Y: y, Z: z, W: w}, false
|
||
}
|
||
|
||
// Get returns the cell at grid coordinates (x, y, z, w).
|
||
func (g *Grid) Get(x, y, z, w int) Cell {
|
||
return g.cells[[NumDims]int{x, y, z, w}]
|
||
}
|
||
|
||
// OccupiedCount returns the number of filled cells.
|
||
func (g *Grid) OccupiedCount() int {
|
||
return len(g.cells)
|
||
}
|
||
|
||
// Elite returns the cell with the highest fitness in the grid.
|
||
// Returns (zero Cell, false) when the grid is empty.
|
||
func (g *Grid) Elite() (Cell, bool) {
|
||
var best Cell
|
||
found := false
|
||
for _, c := range g.cells {
|
||
if c.Occupied && (!found || c.Fitness > best.Fitness) {
|
||
best = c
|
||
found = true
|
||
}
|
||
}
|
||
return best, found
|
||
}
|
||
|
||
// AllElites returns a flat slice of every occupied cell.
|
||
func (g *Grid) AllElites() []Cell {
|
||
out := make([]Cell, 0, len(g.cells))
|
||
for _, c := range g.cells {
|
||
if c.Occupied {
|
||
out = append(out, c)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// Slice returns a 2-D snapshot of the grid by fixing two dimensions.
|
||
// For example, to view the aggression×economy plane at exploration=1, formation=1:
|
||
//
|
||
// slice := grid.Slice(2, 1, 3, 1) // fix dim 2 (z) to 1, dim 3 (w) to 1
|
||
//
|
||
// Returns a size×size grid of cells.
|
||
func (g *Grid) Slice(fixedDim1, fixedVal1, fixedDim2, fixedVal2 int) [][]Cell {
|
||
result := make([][]Cell, g.size)
|
||
for i := range result {
|
||
result[i] = make([]Cell, g.size)
|
||
}
|
||
|
||
// Determine the two free dimensions (the ones not fixed)
|
||
free := [2]int{-1, -1}
|
||
fi := 0
|
||
for d := 0; d < NumDims; d++ {
|
||
if d != fixedDim1 && d != fixedDim2 {
|
||
free[fi] = d
|
||
fi++
|
||
}
|
||
}
|
||
|
||
for key, cell := range g.cells {
|
||
if key[fixedDim1] == fixedVal1 && key[fixedDim2] == fixedVal2 {
|
||
result[key[free[0]]][key[free[1]]] = cell
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// GridSnapshot is a JSON-serializable snapshot of the grid for the dashboard.
|
||
type GridSnapshot struct {
|
||
Size int `json:"size"`
|
||
DimNames [NumDims]string `json:"dim_names"`
|
||
Cells []CellSnapshot `json:"cells"`
|
||
}
|
||
|
||
// CellSnapshot is one occupied cell in the grid snapshot.
|
||
type CellSnapshot struct {
|
||
Pos [NumDims]int `json:"pos"`
|
||
Program int64 `json:"program_id"`
|
||
Fitness float64 `json:"fitness"`
|
||
}
|
||
|
||
// Snapshot returns a JSON-serializable representation of the grid.
|
||
func (g *Grid) Snapshot() GridSnapshot {
|
||
snap := GridSnapshot{
|
||
Size: g.size,
|
||
DimNames: [NumDims]string{"aggression", "economy", "exploration", "formation"},
|
||
}
|
||
for key, cell := range g.cells {
|
||
if cell.Occupied {
|
||
snap.Cells = append(snap.Cells, CellSnapshot{
|
||
Pos: key,
|
||
Program: cell.ProgramID,
|
||
Fitness: math.Round(cell.Fitness*1000) / 1000,
|
||
})
|
||
}
|
||
}
|
||
return snap
|
||
}
|