Skip to content

Commit

Permalink
Speciation v2 (#371)
Browse files Browse the repository at this point in the history
* Refactor import paths in DeDuplicator.ts, CreatureUUID.ts, Breed.ts, ExperimentStore.ts, and Creature.ts

* Refactor sortCreaturesByScore function and add verbose logging in ElitismUtils.ts

* Refactor checkAndAdd function in Neat.ts to improve readability and fix error handling

* Refactor checkAndAdd function in Neat.ts to improve readability and error handling

* Refactor Genus class to add findClosestMatchingSpecies method

* Refactor Genus class to add findClosestMatchingSpecies method

* Fix error handling in Creature.ts and CreatureState.ts

* Fix bias range check in Propagate/STEP.ts and add missing synapses in FineTune.ts

* Fix formatting in Creature.ts and FineTune.ts

* Refactor import paths in DeDuplicator.ts, CreatureUUID.ts, Breed.ts, ExperimentStore.ts, and Creature.ts

* Refactor error handling and import paths in Genus.ts, Neat.ts, CreatureUUID.ts, Breed.ts, ExperimentStore.ts, and Creature.ts

---------

Co-authored-by: Nigel Leck <[email protected]>
  • Loading branch information
nleck and nigelleck authored May 5, 2024
1 parent feb1fe3 commit b631781
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 256 deletions.
2 changes: 1 addition & 1 deletion src/Creature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ export class Creature implements CreatureInternal {
) {
let avgTxt = "";
if (Number.isFinite(result.averageScore)) {
avgTxt = ` (avg: ${yellow(result.averageScore.toFixed(4))})`;
avgTxt = `(avg: ${yellow(result.averageScore.toFixed(4))})`;
}
console.log(
"Generation",
Expand Down
155 changes: 155 additions & 0 deletions src/NEAT/Breed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { Creature, Selection } from "../../mod.ts";
import { Offspring } from "../architecture/Offspring.ts";
import { NeatConfig } from "../config/NeatConfig.ts";
import { Genus } from "./Genus.ts";

export class Breed {
private genus: Genus;
readonly config: NeatConfig;

constructor(genus: Genus, config: NeatConfig) {
this.genus = genus;
this.config = config;
}
/**
* Breeds two parents into an offspring, population MUST be sorted
*/
breed(): Creature | undefined {
const mum = this.getParent(this.genus.population);

if (mum === undefined) {
console.warn(
"No mother found",
this.config.selection.name,
this.genus.population.length,
);

return;
}

const dad = this.getDad(mum);
if (dad === undefined) {
console.warn(
"No father found",
);

return;
}

const creature = Offspring.breed(
mum,
dad,
);

return creature;
}

getDad(mum: Creature): Creature {
if (mum.uuid === undefined) throw new Error(`mum.uuid is undefined`);

const species = this.genus.findSpeciesByCreatureUUID(mum.uuid);

let possibleFathers = species.creatures.filter((creature) =>
creature.uuid !== mum.uuid
);

if (possibleFathers.length === 0) {
const closestSpecies = this.genus.findClosestMatchingSpecies(mum);
if (closestSpecies) {
possibleFathers = closestSpecies.creatures;
}
}

return this.getParent(possibleFathers);
}

/**
* Gets a parent based on the selection function
* @return {Creature} parent
*/
getParent(population: Creature[]): Creature {
switch (this.config.selection) {
case Selection.POWER: {
const r = Math.random();
const index = Math.floor(
Math.pow(r, Selection.POWER.power) *
population.length,
);

return population[index];
}
case Selection.FITNESS_PROPORTIONATE: {
/**
* As negative fitnesses are possible
* https://stackoverflow.com/questions/16186686/genetic-algorithm-handling-negative-fitness-values
* this is unnecessarily run for every individual, should be changed
*/

let totalFitness = 0;
let minimalFitness = 0;
for (let i = population.length; i--;) {
const tmpScore = population[i].score;
const score = tmpScore === undefined ? Infinity * -1 : tmpScore;
minimalFitness = score < minimalFitness ? score : minimalFitness;
totalFitness += score;
}

const adjustFitness = Math.abs(minimalFitness);
totalFitness += adjustFitness * population.length;

const random = Math.random() * totalFitness;
let value = 0;

for (let i = 0; i < population.length; i++) {
const genome = population[i];
if (genome.score !== undefined) {
value += genome.score + adjustFitness;
if (random < value) {
return genome;
}
}
}

// if all scores equal, return random genome
return population[
Math.floor(Math.random() * population.length)
];
}
case Selection.TOURNAMENT: {
if (Selection.TOURNAMENT.size > this.config.populationSize) {
throw new Error(
"Your tournament size should be lower than the population size, please change Selection.TOURNAMENT.size",
);
}

// Create a tournament
const individuals = new Array(Selection.TOURNAMENT.size);
for (let i = 0; i < Selection.TOURNAMENT.size; i++) {
const random = population[
Math.floor(Math.random() * population.length)
];
individuals[i] = random;
}

// Sort the tournament individuals by score
individuals.sort(function (a, b) {
return b.score - a.score;
});

// Select an individual
for (let i = 0; i < Selection.TOURNAMENT.size; i++) {
if (
Math.random() < Selection.TOURNAMENT.probability ||
i === Selection.TOURNAMENT.size - 1
) {
return individuals[i];
}
}
throw new Error(`No parent found in tournament`);
}
default: {
throw new Error(`Unknown selection: ${this.config.selection}`);
}
}
}
}
8 changes: 5 additions & 3 deletions src/NEAT/Genus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ export class Genus {
readonly speciesMap: Map<string, Species>;
readonly creatureToSpeciesMap: Map<string, string>;

readonly population: Creature[] = [];

constructor() {
this.speciesMap = new Map();
this.creatureToSpeciesMap = new Map();
}

async addCreature(creature: Creature): Promise<Species> {
if (creature === undefined || creature.uuid === undefined) {
throw new Error(`creature ${creature.uuid} is undefined`);
}
assert(creature, "No creature provided");
assert(creature.uuid, "No creature UUID");

this.population.push(creature);
const key = await Species.calculateKey(creature);

let species = this.speciesMap.get(key);
Expand Down
72 changes: 72 additions & 0 deletions src/NEAT/Mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { removeTag } from "https://deno.land/x/[email protected]/mod.ts";
import { creatureValidate } from "../architecture/CreatureValidate.ts";
import { Creature, Mutation } from "../../mod.ts";
import { CreatureInternal } from "../architecture/CreatureInterfaces.ts";
import { NeatConfig } from "../config/NeatConfig.ts";

export class Mutator {
private config: NeatConfig;
constructor(config: NeatConfig) {
this.config = config;
}

/**
* Mutates the given (or current) population
*/
mutate(creatures: Creature[]): void {
for (let i = creatures.length; i--;) {
if (Math.random() <= this.config.mutationRate) {
const creature = creatures[i];
if (this.config.debug) {
creatureValidate(creature);
}
for (let j = this.config.mutationAmount; j--;) {
const mutationMethod = this.selectMutationMethod(creature);

creature.mutate(
mutationMethod,
Math.random() < this.config.focusRate
? this.config.focusList
: undefined,
);
}

if (this.config.debug) {
creatureValidate(creature);
}

removeTag(creature, "approach");
}
}
}

/**
* Selects a random mutation method for a genome according to the parameters
*/
private selectMutationMethod(creature: CreatureInternal) {
const mutationMethods = this.config
.mutation;

for (let attempts = 0; true; attempts++) {
const mutationMethod = mutationMethods[
Math.floor(Math.random() * this.config.mutation.length)
];

if (
mutationMethod === Mutation.ADD_NODE &&
creature.neurons.length >= this.config.maximumNumberOfNodes
) {
continue;
}

if (
mutationMethod === Mutation.ADD_CONN &&
creature.synapses.length >= this.config.maxConns
) {
continue;
}

return mutationMethod;
}
}
}
Loading

0 comments on commit b631781

Please sign in to comment.