generated from michael/webpack-base
identify an especially tricky bug
This commit is contained in:
parent
9e624f225e
commit
c7abcbb03a
@ -25,10 +25,10 @@ const FEATURES = {
|
||||
HEAD_Y: 'HEAD_Y',
|
||||
APPLE_REL_X: 'APPLE_REL_X',
|
||||
APPLE_REL_Y: 'APPLE_REL_Y',
|
||||
TAIL_ABOVE: 'TAIL_ABOVE',
|
||||
TAIL_BELOW: 'TAIL_BELOW',
|
||||
TAIL_LEFT: 'TAIL_LEFT',
|
||||
TAIL_RIGHT: 'TAIL_RIGHT',
|
||||
// TAIL_ABOVE: 'TAIL_ABOVE',
|
||||
// TAIL_BELOW: 'TAIL_BELOW',
|
||||
// TAIL_LEFT: 'TAIL_LEFT',
|
||||
// TAIL_RIGHT: 'TAIL_RIGHT',
|
||||
};
|
||||
const OUTPUTS = {
|
||||
U: 'U',
|
||||
@ -43,10 +43,10 @@ const _BASE_GENOME_SNAKE_BRAIN_NEAT_EDGES = completeBipartiteEdges(
|
||||
FEATURES.HEAD_Y,
|
||||
FEATURES.APPLE_REL_X,
|
||||
FEATURES.APPLE_REL_Y,
|
||||
FEATURES.TAIL_ABOVE,
|
||||
FEATURES.TAIL_BELOW,
|
||||
FEATURES.TAIL_LEFT,
|
||||
FEATURES.TAIL_RIGHT,
|
||||
// FEATURES.TAIL_ABOVE,
|
||||
// FEATURES.TAIL_BELOW,
|
||||
// FEATURES.TAIL_LEFT,
|
||||
// FEATURES.TAIL_RIGHT,
|
||||
],
|
||||
[OUTPUTS.U, OUTPUTS.D, OUTPUTS.L, OUTPUTS.R],
|
||||
);
|
||||
@ -108,10 +108,10 @@ export class NEATSnakeBrain {
|
||||
act.set(FEATURES.HEAD_Y, head.y);
|
||||
act.set(FEATURES.APPLE_REL_X, appleRel.x);
|
||||
act.set(FEATURES.APPLE_REL_Y, appleRel.y);
|
||||
act.set(FEATURES.TAIL_ABOVE, Math.max(...above, BOARD_SQUARES));
|
||||
act.set(FEATURES.TAIL_BELOW, Math.max(...below, BOARD_SQUARES));
|
||||
act.set(FEATURES.TAIL_LEFT, Math.max(...left, BOARD_SQUARES));
|
||||
act.set(FEATURES.TAIL_RIGHT, Math.max(...right, BOARD_SQUARES));
|
||||
// act.set(FEATURES.TAIL_ABOVE, Math.max(...above, BOARD_SQUARES));
|
||||
// act.set(FEATURES.TAIL_BELOW, Math.max(...below, BOARD_SQUARES));
|
||||
// act.set(FEATURES.TAIL_LEFT, Math.max(...left, BOARD_SQUARES));
|
||||
// act.set(FEATURES.TAIL_RIGHT, Math.max(...right, BOARD_SQUARES));
|
||||
|
||||
this.brain.think(act);
|
||||
|
||||
|
@ -228,7 +228,7 @@
|
||||
* - Effectively pass data from inputs, through hidden nodes, to outputs
|
||||
*/
|
||||
|
||||
import { edgesToNodes, NodeID, traceParents, RawEdge } from './network';
|
||||
import { edgesToNodes, NodeID, traceParents, RawEdge, traceChildren } from './network';
|
||||
import { keyMax, mapInvert, mapMap, randchoice, randint, randomNegPos, setDifference, setMap } from './util';
|
||||
|
||||
export interface GeneData {
|
||||
@ -258,6 +258,33 @@ let g_node_id = 1;
|
||||
let g_innovation_number = 1;
|
||||
let g_species_id = 1;
|
||||
|
||||
function hashGenome(genome: Genome) {
|
||||
const cyrb53 = (str: string, seed = 0) => {
|
||||
let h1 = 0xdeadbeef ^ seed;
|
||||
let h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
||||
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
||||
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
};
|
||||
let hash = 0;
|
||||
for (const gene of genome) {
|
||||
hash += cyrb53(gene.src_id);
|
||||
hash += cyrb53(gene.dst_id);
|
||||
hash += gene.data.weight;
|
||||
hash += gene.data.enabled ? 1 : 0;
|
||||
hash += gene.data.innovation;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function resetGlobalIDs(config: { node_id: number; innovation_number: number; species_id: number }) {
|
||||
g_node_id = config.node_id;
|
||||
g_innovation_number = config.innovation_number;
|
||||
@ -508,7 +535,8 @@ export function mutateNewConn(newConn: { src_id: NodeID; dst_id: NodeID }, newWe
|
||||
};
|
||||
}
|
||||
|
||||
export function findAcyclicNewConns<DataT>(rawEdges: RawEdge<DataT>[]): { src_id: NodeID; dst_id: NodeID }[] {
|
||||
// TODO: improve name since also excluding source nodes
|
||||
export function findAcyclicNonSourceNewConns<DataT>(rawEdges: RawEdge<DataT>[]): { src_id: NodeID; dst_id: NodeID }[] {
|
||||
// finds potential new connections that are acyclic and are not already connected
|
||||
|
||||
// NOTE: there's some performance stuff here that could definitely be improved
|
||||
@ -518,12 +546,22 @@ export function findAcyclicNewConns<DataT>(rawEdges: RawEdge<DataT>[]): { src_id
|
||||
// a node that is connected to one of its parents creates a cycle
|
||||
const allNodeIDs = new Set(nodes.keys());
|
||||
const parents = traceParents(nodes);
|
||||
const children = traceChildren(nodes);
|
||||
|
||||
// TODO: think on this more:
|
||||
// adding a new connection needs to not connect a source -> source or a sink -> sink
|
||||
// or else the innovation number system gets messed up
|
||||
const sources = new Set(Array.from(parents.entries()).flatMap(([k, v]) => (v.size === 0 ? [k] : [])));
|
||||
const sinks = new Set(Array.from(children.entries()).flatMap(([k, v]) => (v.size === 0 ? [k] : [])));
|
||||
console.log({ sources, sinks });
|
||||
|
||||
const acyclic = new Map<NodeID, Set<NodeID>>();
|
||||
for (const [nodeID, nodeParents] of parents.entries()) {
|
||||
const nodeParentIDs = setMap(nodeParents, n => n.id);
|
||||
const nodeIDsAcyclic = setDifference(allNodeIDs, nodeParentIDs);
|
||||
nodeIDsAcyclic.delete(nodeID);
|
||||
acyclic.set(nodeID, nodeIDsAcyclic);
|
||||
const nodeIDsAcyclicNonSource = setDifference(nodeIDsAcyclic, sources);
|
||||
acyclic.set(nodeID, nodeIDsAcyclicNonSource);
|
||||
}
|
||||
|
||||
// flatten
|
||||
@ -552,6 +590,9 @@ export interface MutateConfig {
|
||||
export function mutate(genome: Genome, config: MutateConfig): Genome {
|
||||
const { mutate_rate, assign_rate, assign_mag, perturb_mag, new_node_rate, new_connection_rate } = config;
|
||||
|
||||
console.log('mutating: ', hashGenome(genome), genome);
|
||||
findAcyclicNonSourceNewConns(genome);
|
||||
|
||||
const newGenome = genome.map(gene => {
|
||||
if (Math.random() >= mutate_rate) return gene; // this connection should not be mutated
|
||||
if (Math.random() < assign_rate) {
|
||||
@ -574,8 +615,10 @@ export function mutate(genome: Genome, config: MutateConfig): Genome {
|
||||
}
|
||||
|
||||
if (Math.random() < new_connection_rate) {
|
||||
// TODO: figure out why we're not hitting a
|
||||
// TODO: disallow "source" nodes from being new connection destinations
|
||||
// create a new connection between two *previously unconnected* nodes
|
||||
const options = findAcyclicNewConns(newGenome);
|
||||
const options = findAcyclicNonSourceNewConns(newGenome);
|
||||
if (options.length === 0) {
|
||||
// TODO: remove this warn once this starts working
|
||||
// this is mostly a sanity check / useful for metrics
|
||||
@ -583,10 +626,17 @@ export function mutate(genome: Genome, config: MutateConfig): Genome {
|
||||
} else {
|
||||
// choose a random connection
|
||||
const newConn = randchoice(options);
|
||||
console.log('adding new connection...', hashGenome(genome), genome, newConn);
|
||||
if (new Set(['U', 'D', 'L', 'R']).has(newConn.dst_id)) {
|
||||
debugger;
|
||||
}
|
||||
const newGene = mutateNewConn(newConn, assign_mag * randomNegPos());
|
||||
newGenome.push(newGene);
|
||||
}
|
||||
|
||||
findAcyclicNonSourceNewConns(newGenome);
|
||||
}
|
||||
console.log('created: ', hashGenome(newGenome), newGenome);
|
||||
|
||||
return newGenome;
|
||||
}
|
||||
@ -619,6 +669,7 @@ export function computeNextGeneration(
|
||||
const mom = randchoice(winners);
|
||||
const dad = chooseMate(mom, winnersPopulation, mcc);
|
||||
const crossed = crossGenomes(mom, dad, fitness, cc);
|
||||
// TODO: crossed has cycles!
|
||||
const baby = mutate(crossed, mc);
|
||||
|
||||
// assign to a species + add to next generation
|
||||
|
@ -80,7 +80,7 @@ export function traceParents<DataT>(nodes: Network<Node<DataT>>) {
|
||||
function traceNodeParents(node: Node<DataT>, depth: number): Set<Node<DataT>> {
|
||||
console.log('tracing: ', node.id, depth);
|
||||
// TODO: there's a particularly nasty issue where maybe nodes are not just from their own genome?
|
||||
if (depth > 1000) {
|
||||
if (depth > 20) {
|
||||
debugger;
|
||||
}
|
||||
if (parents.has(node.id)) return parents.get(node.id)!;
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
tournamentSelectionWithChampions,
|
||||
compatibilityDistance,
|
||||
crossGenomes,
|
||||
findAcyclicNewConns,
|
||||
findAcyclicNonSourceNewConns,
|
||||
Genome,
|
||||
mutateAssign,
|
||||
mutateNewConn,
|
||||
@ -357,7 +357,7 @@ function testMutateNewConn() {
|
||||
}
|
||||
addTest(testMutateNewConn);
|
||||
|
||||
function testFindAcyclicNewConns() {
|
||||
function testFindAcyclicNonSourceNewConns() {
|
||||
/*
|
||||
* all edges pointing down
|
||||
*
|
||||
@ -377,22 +377,22 @@ function testFindAcyclicNewConns() {
|
||||
{ src_id: 'D', dst_id: 'F', data: null },
|
||||
];
|
||||
|
||||
const options = findAcyclicNewConns(edges);
|
||||
const options = findAcyclicNonSourceNewConns(edges);
|
||||
const expected = [
|
||||
{ src_id: 'A', dst_id: 'B' },
|
||||
// not A -> B - B is a source
|
||||
{ src_id: 'A', dst_id: 'F' },
|
||||
{ src_id: 'B', dst_id: 'A' },
|
||||
// not B -> A - A is a source
|
||||
{ src_id: 'B', dst_id: 'C' },
|
||||
{ src_id: 'B', dst_id: 'E' },
|
||||
{ src_id: 'B', dst_id: 'F' },
|
||||
{ src_id: 'C', dst_id: 'B' },
|
||||
// not C -> B - B is a source
|
||||
{ src_id: 'C', dst_id: 'D' },
|
||||
{ src_id: 'C', dst_id: 'F' },
|
||||
{ src_id: 'D', dst_id: 'C' },
|
||||
// not E -> B
|
||||
// not E -> B - B is a parent of E
|
||||
{ src_id: 'E', dst_id: 'F' },
|
||||
// not F -> A
|
||||
// not F -> B
|
||||
// not F -> A - A is a parent of F
|
||||
// not F -> B - B is a parent of F
|
||||
{ src_id: 'F', dst_id: 'C' },
|
||||
{ src_id: 'F', dst_id: 'E' },
|
||||
];
|
||||
@ -406,7 +406,7 @@ function testFindAcyclicNewConns() {
|
||||
|
||||
assertDeepEqual(options, expected);
|
||||
}
|
||||
addTest(testFindAcyclicNewConns);
|
||||
addTest(testFindAcyclicNonSourceNewConns);
|
||||
|
||||
function testComputeNextGeneration() {
|
||||
function makeGenome(weight: number) {
|
||||
|
Loading…
Reference in New Issue
Block a user