Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
5521 lines (5150 sloc) 190 KB
/*
MIT License
Copyright (c) 2019 문동선 (NaniteFactory)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package naneat
import (
"encoding/json"
"errors"
"fmt"
"image"
"image/color"
"io/ioutil"
"log"
"math"
"math/rand"
"os"
"reflect"
"runtime/debug"
"sort"
"strconv"
"sync"
"syscall"
"time"
"github.com/campoy/unique"
uuid "github.com/satori/go.uuid"
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/simple"
"gonum.org/v1/gonum/graph/topo"
)
// ----------------------------------------------------------------------------
// Package
// Initialization of this package.
func init() {
syscall.Write(syscall.Handle(os.Stdout.Fd()), nil) // The standard out is flushed.
rand.Seed(time.Now().UTC().UnixNano()) // Seed for random mutations etc.
}
// ----------------------------------------------------------------------------
// Experimenter
// Experimenter exposes a set of user-friendly methods.
// Facade of this library.
type Experimenter interface {
Self() *Universe
Status() UniverseStatusGetter
RegisterMeasurer(agent Measurer)
UnregisterMeasurer(agent Measurer)
Run() error
Shutdown()
IsPumping() bool
}
// UniverseStatusGetter exposes a set of thread-safe universe state getters.
type UniverseStatusGetter interface {
Info() (
conf Configuration, topFitness float64,
population, numSpecies, generation, innovation, nicheCount int,
)
Measurers() (ret map[Measurer]*Subscribed)
Breeds() []*Species
Organisms() []*Organism
BiasGenes() []*NodeGene
NonBiasInputGenes() []*NodeGene
GetInputGenes() []*NodeGene
GetOutputGenes() []*NodeGene
}
// New is where everything starts.
//
// These two are the same:
// - func New(config *Configuration) Experimenter
// - func (config *Configuration) New() Experimenter
//
func New(config *Configuration) Experimenter {
return config.New()
}
// ----------------------------------------------------------------------------
// Measurer
// Measurer is a consumer interface for what measures an organism's fitness.
// It could be an emulator thread or a network server or any test case.
// Implement it however you'd like.
type Measurer interface {
// Notice the side effect of this method as an impure function that
// with fitness it returns it might also update the state of our NN.
MeasureAsync(organism []*NeuralNetwork) (postman Swallow, err error)
String() string
}
// Swallow is a bird that will deliver our fortune.
// Or it's a channel our organism's fitness is transferred over.
type Swallow <-chan float64
// Agent is a Measurer: Agent implements Measurer interface.
type Agent struct {
name string
chReceive chan []*NeuralNetwork
chSend chan float64
isWorking bool
}
// NewAgent is a constructor.
func NewAgent() *Agent {
return &Agent{
name: "Agent",
chReceive: make(chan []*NeuralNetwork, 1),
chSend: make(chan float64),
isWorking: false,
}
}
// String callback of Agent implements Measurer.
func (a *Agent) String() string {
return a.name
}
// Close all channels of this agent.
//
// Effects of closing channels:
// - Channels are closed.
// - The other goroutine is notified of the fact that the data flow was shut quite tough.
// - Possible memory leaks are prevented in case our GC is working so lazy.
//
func (a *Agent) Close() {
close(a.chReceive)
close(a.chSend)
}
// Send blocks the thread.
// The supplier uses this method.
func (a *Agent) Send(fitness float64) {
a.chSend <- fitness
a.isWorking = false
}
// Receive blocks the thread.
// The supplier uses this method.
func (a *Agent) Receive() (brains []*NeuralNetwork) {
a.isWorking = true
brains = <-a.chReceive
return brains
}
// IsWorking when this agent as a supplier thinks it is working,
// not when the consumer thinks it is.
// The consumer might use this method.
func (a *Agent) IsWorking() bool {
return a.isWorking
}
// Measure blocks the thread.
// The consumer might use this method.
func (a *Agent) Measure(brains []*NeuralNetwork) (fitness float64) {
a.chReceive <- brains
return <-a.chSend
}
// MeasureAsync does not block the thread.
// The consumer uses this method.
func (a *Agent) MeasureAsync(brains []*NeuralNetwork) (messenger Swallow, err error) {
select {
case a.chReceive <- brains:
return a.chSend, nil
default:
return nil, errors.New("failed to make an order: this agent has already been ordered")
}
}
// ----------------------------------------------------------------------------
// Configuration
// Configuration stores the universe's constants. A set of hyper parameters.
type Configuration struct {
// Label for the universe
ExperimentName string
// Max number of creatures
SizePopulation int
// The percentage bottom which percentage of organisms in a species is culled off every epoch.
// The range of this value is [0, 1].
PercentageCulling float64
// The Nth generation the stagnant species is going to face its extermination. xD
// The species not improving over this much of generations will get penalized.
// Negative value for this means infinity.
MaxCntArmageddon int // Age threshold to dropoff. Rotten species are not allowed to reproduce.
// If the top fitness of the entire universe does not improve for more than this much of generations,
// only the top two species are allowed to reproduce, refocusing the search into the most promising spaces.
// Negative value for this means infinity.
MaxCntApocalypse int // Threshold to the universe's stagnancy.
// If false, entire population of a species besides the champion is replaced by their offsprings at each reproduce procedure.
// If true, there are un-culled organisms in species: reproducible organisms are saved and lives on to next generation.
IsProtectiveElitism bool
IsFitnessRemeasurable bool // If true, the fitness of every organism that's already measured gets remeasured in the measuring stage of each generation.
// Ratio of different reproduction methods performed upon breeding species
ScaleCrossoverMultipointRnd int // Sexual reproduction 1
ScaleCrossoverMultipointAvg int // Sexual reproduction 2
ScaleCrossoverSinglepointRnd int // Sexual reproduction 3
ScaleCrossoverSinglepointAvg int // Sexual reproduction 4
ScaleFission int // Asexual breeding (Binary fission)
// Base of M-Rate (ChanceGene)
ChanceIsMutational bool // Genetic(dynamic) or constant.
ChanceAddNode float64 // Add-Node mutation chance.
ChanceAddLink float64 // Add-Link mutation chance only for non-bias nodes.
ChanceAddBias float64 // Add-Link mutation chance only for bias.
ChancePerturbWeight float64 // Synaptic weight mutation chance.
ChanceNullifyWeight float64 // Mutation chance of synaptic weight becoming zero.
ChanceTurnOn float64 // Enable mutation chance.
ChanceTurnOff float64 // Disable mutation chance.
ChanceBump float64 // Bump mutation chance.
// What defines the synaptic weight mutation.
// This is a size factor determining the deviation of a Gaussian distribution.
TendencyPerturbWeight float64 // The mean(offset). Set to 0.0 by default.
TendencyPerturbDice float64 // The mean(offset). Set by default to a very small positive value.
StrengthPerturbWeight float64 // Adjusted deviation. Synaptic weight is perturbed by this percent of it.
StrengthPerturbDice float64 // Adjusted deviation. Mutational rate is perturbed by this percent of it.
// Measuring the compatibility distance
CompatThreshold float64 // The line separating species.
CompatIsNormalizedForSize bool // The characterizing variables of disjointedness and excess get expressed in normalized percent, or, rather in the absolute which ignores the size of two genetic encoders.
CompatCoeffDisjoint float64 // The maximum disjointedness when expressed in normalized percent or simply a multiplier to the number of disjoint genes a chromosome has.
CompatCoeffExcess float64 // The maximum excessiveness when expressed in normalized percent or simply a multiplier to the number of excessive genes a chromosome has.
CompatCoeffWeight float64 // The maximum or simply a multiplier regarding the mutational difference of weights.
CompatCoeffChance float64 // The maximum or simply a multiplier regarding the mutational difference of chance gene's.
// Seeds of genetics (Genome/Chromosome)
NumChromosomes int // The number of Chromosome-s for a single Genome. The number of NNs a single Organism consists of.
NumBiases int // The number of biases.
NumNonBiasInputs int // The number of input nodes. This doesn't count for biases.
NumOutputs int // The number of output nodes.
}
// NewConfiguration is a constructor.
// This simply creates an empty object.
// Feel free to edit what's returned however you'd like.
func NewConfiguration() *Configuration {
return &Configuration{}
}
// NewConfigurationSimple is a constructor.
// It returns what's filled up with a default setting.
//
// A set of params I used to test with:
// - nNet = 2
// - nIn = 38 * 28
// - nOut = 7
//
func NewConfigurationSimple(nNet, nIn, nOut int) *Configuration {
return &Configuration{
ExperimentName: "UU",
SizePopulation: 400,
PercentageCulling: 0.7,
MaxCntArmageddon: 8,
MaxCntApocalypse: 20,
IsProtectiveElitism: true,
IsFitnessRemeasurable: false,
//
ScaleCrossoverMultipointRnd: 8, // 80%
ScaleCrossoverMultipointAvg: 1, // 10%
ScaleCrossoverSinglepointRnd: 0, // 0%
ScaleCrossoverSinglepointAvg: 0, // 0%
ScaleFission: 1, // 10%
//
ChanceIsMutational: true,
ChanceAddNode: 0.5, // 50%
ChanceAddLink: 4.0, // 400%
ChanceAddBias: 0.9, // 90%
ChancePerturbWeight: 1.0, // 100%
ChanceNullifyWeight: 0.0, // 0%
ChanceTurnOn: 0.1, // 10%
ChanceTurnOff: 0.4, // 40%
ChanceBump: 0.1, // 10%
TendencyPerturbWeight: 0.0, // 0%p (val offset +)
TendencyPerturbDice: 0.001, // 0.1%p (val offset +)
StrengthPerturbWeight: 0.15, // 15% of 50%
StrengthPerturbDice: 0.15, // 15%
//
CompatThreshold: 10.0,
CompatIsNormalizedForSize: true,
CompatCoeffDisjoint: 20.0,
CompatCoeffExcess: 20.0,
CompatCoeffWeight: 10.0,
CompatCoeffChance: 10.0,
//
NumChromosomes: nNet,
NumBiases: 1,
NumNonBiasInputs: nIn,
NumOutputs: nOut,
}
}
// BirthRatioSimplified returns the breeding methods ratio in smallest integers.
func (config *Configuration) BirthRatioSimplified() (
weightCrossoverMultipointRnd,
weightCrossoverMultipointAvg,
weightCrossoverSinglepointRnd,
weightCrossoverSinglepointAvg,
weightFission,
weightSterile float64,
) {
r1 := config.ScaleCrossoverMultipointRnd
r2 := config.ScaleCrossoverMultipointAvg
r3 := config.ScaleCrossoverSinglepointRnd
r4 := config.ScaleCrossoverSinglepointAvg
r5 := config.ScaleFission
gcd := GCD(r1, r2, r3, r4, r5)
weightCrossoverMultipointRnd = float64(r1 / gcd)
weightCrossoverMultipointAvg = float64(r2 / gcd)
weightCrossoverSinglepointRnd = float64(r3 / gcd)
weightCrossoverSinglepointAvg = float64(r4 / gcd)
weightFission = float64(r5 / gcd)
return
}
// NewUniverse is a constructor.
func (config *Configuration) NewUniverse() *Universe {
retUniv := &Universe{
Config: *config,
//
ChStopSign: make(chan chan struct{}),
IsRunning: false,
MutexRun: sync.Mutex{},
//
Agents: map[Measurer]*Subscribed{},
MutexAgents: sync.Mutex{},
//
Livings: map[*Organism]struct{}{},
Classes: []*Species{},
MutexLivings: sync.Mutex{},
MutexClasses: sync.Mutex{},
TopFitness: 0,
Generation: 0,
Innovation: 0,
NicheCount: 0,
//
InputGenes: make([]*NodeGene, config.NumBiases+config.NumNonBiasInputs),
OutputGenes: make([]*NodeGene, config.NumOutputs),
}
// sow
{ // inputs
nBias := config.NumBiases
for i := 0; i < nBias; i++ {
retUniv.InputGenes[i] = NewNodeGene("bias_"+strconv.Itoa(i), InputNodeBias, i)
}
for i := nBias; i < len(retUniv.InputGenes); i++ {
retUniv.InputGenes[i] = NewNodeGene("primal_in_"+strconv.Itoa(i-nBias), InputNodeNotBias, i)
}
}
for i := 0; i < len(retUniv.OutputGenes); i++ { // outputs
retUniv.OutputGenes[i] = NewNodeGene("primal_out_"+strconv.Itoa(i), OutputNode, i)
}
// creation + speciation
for len(retUniv.Livings) < retUniv.Config.SizePopulation {
newLife, err := retUniv.NewOrganismBasic()
if err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
if err := retUniv.AddOrganism(newLife); err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
if err := retUniv.Speciate(newLife); err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
}
// The initial generation doesn't get more than a chance.
for _, s := range retUniv.Classes {
s.Stagnancy = retUniv.Config.MaxCntArmageddon
}
return retUniv
}
// New is where everything starts.
//
// These two are the same:
// - func New(config *Configuration) Experimenter
// - func (config *Configuration) New() Experimenter
//
func (config *Configuration) New() Experimenter {
return config.NewUniverse()
}
// ----------------------------------------------------------------------------
// Ark (Import)
// Ark JSON export of Universe. Backup state data.
type Ark struct {
ReferenceByUUID map[uuid.UUID]*NodeGene
Classes []*Species
TopFitness float64
Generation int
Innovation int
NicheCount int
Config Configuration
InputGenes []*NodeGene
OutputGenes []*NodeGene
}
// NewArkFromFile loads one universe state from a JSON file.
// Data in.
func NewArkFromFile(filepath string) (*Ark, error) {
jsonRaw, err := ioutil.ReadFile(filepath)
if err != nil {
return nil, err
}
var glove Ark
err = json.Unmarshal(jsonRaw, &glove)
if err != nil {
return nil, err
}
// log.Println(glove) // debug //
return &glove, nil
}
// New is the coolest constructor where everything starts.
func (pack *Ark) New() (ret Experimenter, err error) {
return pack.NewUniverse()
}
// NewUniverse is a constructor.
// Data out.
func (pack *Ark) NewUniverse() (retUniv *Universe, err error) {
retUniv = &Universe{
Config: pack.Config,
//
ChStopSign: make(chan chan struct{}),
IsRunning: false,
MutexRun: sync.Mutex{},
//
Agents: map[Measurer]*Subscribed{},
MutexAgents: sync.Mutex{},
//
Livings: map[*Organism]struct{}{},
Classes: pack.Classes,
TopFitness: pack.TopFitness,
Generation: pack.Generation,
Innovation: pack.Innovation,
NicheCount: pack.NicheCount,
//
InputGenes: pack.InputGenes,
OutputGenes: pack.OutputGenes,
}
// IO nodes
for i, nodeGene := range retUniv.InputGenes { // NodeGenesFromUUID
retUniv.InputGenes[i] = pack.ReferenceByUUID[nodeGene.UUID]
}
for i, nodeGene := range retUniv.OutputGenes { // NodeGenesFromUUID
retUniv.OutputGenes[i] = pack.ReferenceByUUID[nodeGene.UUID]
}
ioNodeGenes := func() []*NodeGene {
ret := append([]*NodeGene{}, retUniv.InputGenes...)
ret = append(ret, retUniv.OutputGenes...)
return ret
}()
// Links & Hidden nodes
for _, species := range retUniv.Classes {
for _, organism := range species.Livings {
// Genome (chrome)
for _, chrome := range organism.GenoType().Chromosomes() {
chrome.IONodeGenes = append([]*NodeGene{}, ioNodeGenes...)
for i, linkGene := range chrome.LinkGenes {
chrome.LinkGenes[i].Topo.From = pack.ReferenceByUUID[linkGene.Topo.From.UUID]
chrome.LinkGenes[i].Topo.To = pack.ReferenceByUUID[linkGene.Topo.To.UUID]
}
chrome.Sort()
}
// Phenome
phenotype := make([]*NeuralNetwork, organism.GenoType().Length())
for iChrome, chromosome := range organism.GenoType().Chromosomes() {
phenotype[iChrome], err = chromosome.NewNeuralNetwork()
if err != nil {
return nil, err
}
}
organism.Phenotype = phenotype
// After that...
organism.Breed = species // Speciate
retUniv.Livings[organism] = struct{}{} // Populate
}
}
return retUniv, nil
}
// ----------------------------------------------------------------------------
// Universe
// Universe is NEAT context in the highest level,
// which gets you an access to all available training data of a training session.
// It is strongly recommended not to access any of these members directly outside this package,
// unless you understand what those really are.
// Otherwise use this struct's methods and its constructor instead.
type Universe struct {
// Static info relates to all creations and destructions.
Config Configuration // Constants.
// Trivial runtime settings.
ChStopSign chan chan struct{} // Private used only for the Shutdown().
IsRunning bool // What tells whether this universe is actually running or not.
MutexRun sync.Mutex // Sync tool used only for Experimenter implementations. For IsRunning bool.
// Interface to talk to other possible modules.
Agents map[Measurer]*Subscribed // A set of agents measuring fitnesses of our creatures.
MutexAgents sync.Mutex // Sync tool used only for Experimenter implementations.
// Regarding space we have in this universe.
Livings map[*Organism]struct{} // A set of all creatures available in this universe.
Classes []*Species // Biological category of creatures.
TopFitness float64 // Of an organism in this universe.
MutexLivings sync.Mutex // Sync tool used only for Experimenter implementations.
MutexClasses sync.Mutex // Sync tool used only for Experimenter implementations.
// Regarding time how far we've been through.
// These can only go forward and there is no way for them to be winded back besides resetting the whole universe.
// So what's not necessary at all: Any method that would decrement any of these global (innovation/historical) number.
Generation int // The Nth generation. We could say the age of this universe is N generation old.
Innovation int // The innovation number, which should be/is global throughout this universe.
NicheCount int // This is a counter that tells exactly how many of niches have appeared since the creation of this universe. Niche is an identifier historical and unique for each of species in this universe.
// The ancestor of all creatures born and raised in this universe.
InputGenes []*NodeGene
OutputGenes []*NodeGene
}
// ----------------------------------------------------------------------------
// Universe - Subscribed (Component)
// Subscribed of a Measurer.
type Subscribed struct {
// Measurer receives an NN to be evaluated.
S Swallow // Channel assigned to a Measurer.
O *Organism // The creature being measured by a Measurer.
}
// NewSubscribed is a constructor.
func NewSubscribed(mailFrom Swallow, mailTo *Organism) *Subscribed {
return &Subscribed{
S: mailFrom,
O: mailTo,
}
}
// String callback of this.
func (subbed *Subscribed) String() string {
return fmt.Sprint(*subbed)
}
// ----------------------------------------------------------------------------
// Universe - Export
// NewArk of this universe.
func (univ *Universe) NewArk() *Ark {
return &Ark{
ReferenceByUUID: univ.NodeGenesByUUID(nil),
Classes: univ.Classes,
TopFitness: univ.TopFitness,
Generation: univ.Generation,
Innovation: univ.Innovation,
NicheCount: univ.NicheCount,
Config: univ.Config,
InputGenes: univ.InputGenes,
OutputGenes: univ.OutputGenes,
}
}
// NodeGenesByUUID returns a map of all node genes available in this Universe.
// The parameter can be nil.
func (univ *Universe) NodeGenesByUUID(fillMe map[uuid.UUID]*NodeGene) (filledOut map[uuid.UUID]*NodeGene) {
if fillMe == nil {
filledOut = map[uuid.UUID]*NodeGene{}
} else {
filledOut = fillMe
}
for _, species := range univ.Classes {
for _, organism := range species.Livings {
filledOut = organism.GenoType().NodeGenesByUUID(filledOut)
}
}
for _, nodeGene := range univ.InputGenes {
filledOut[nodeGene.UUID] = nodeGene
}
for _, nodeGene := range univ.OutputGenes {
filledOut[nodeGene.UUID] = nodeGene
}
return filledOut
}
// Save this universe to a JSON file.
func (univ *Universe) Save(filename string) (err error) {
// copy
jsonRawBackup, err := json.Marshal(univ.NewArk())
if err != nil {
return err
}
// dump
err = ioutil.WriteFile(filename, jsonRawBackup, 0644)
if err != nil {
return err
}
return nil
}
// ----------------------------------------------------------------------------
// Universe - Run (EP)
// Self implements the Experimenter interface.
func (univ *Universe) Self() *Universe {
return univ
}
// Status implements the Experimenter interface.
func (univ *Universe) Status() UniverseStatusGetter {
return univ
}
// RegisterMeasurer to this universe.
func (univ *Universe) RegisterMeasurer(agent Measurer) {
univ.MutexAgents.Lock()
defer univ.MutexAgents.Unlock()
univ.Agents[agent] = nil
}
// UnregisterMeasurer to this universe.
func (univ *Universe) UnregisterMeasurer(agent Measurer) {
univ.MutexAgents.Lock()
defer univ.MutexAgents.Unlock()
delete(univ.Agents, agent)
}
// Run this universe. Entry point to our GA.
// *This* procedure of *this* universe can be run only one at a time.
// Otherwise it meets the only condition this can error.
// Give this blocking synchronous function a thread or a goroutine.
func (univ *Universe) Run() error {
// init
univ.MutexRun.Lock()
if univ.IsRunning {
univ.MutexRun.Unlock()
return errors.New("already running")
}
univ.IsRunning = true
univ.MutexRun.Unlock()
// clean up
defer func() {
univ.MutexRun.Lock()
defer univ.MutexRun.Unlock()
univ.IsRunning = false
}()
// handled error
type shutdownError struct {
error
chReply chan struct{}
}
newShutdownError := func(chReply chan struct{}) *shutdownError {
return &shutdownError{errors.New("stop signed"), chReply}
}
// Def.
measure := func(univ *Universe) error {
// Note: univ.Agents map[Measurer]Subscribed // 1 // We know by this, the number of Measurers and their work info.
mapSetMeasuringBirds := map[Swallow]Measurer{} // 2 // Messengers in a set. So we get an idea about the number of students being evaluated(mailed).
setOrgFroshesYetMeasured := map[*Organism]struct{}{} // 3 // A set temporarily storing newbs to be evaluated. (So we can count them.)
// Set newbs.
univ.MutexLivings.Lock()
for organism := range univ.Livings {
if univ.Config.IsFitnessRemeasurable || !organism.IsFitnessMeasured() {
setOrgFroshesYetMeasured[organism] = struct{}{}
}
}
univ.MutexLivings.Unlock()
for len(mapSetMeasuringBirds) > 0 || len(setOrgFroshesYetMeasured) > 0 {
select {
case chReply := <-univ.ChStopSign: // don't reply and throw that to other routine
return newShutdownError(chReply) // Stop running this measure().
default:
// Noop. Do nothing.
}
{ // 1. Send enough
univ.MutexAgents.Lock()
for measurer, postman := range univ.Agents {
if postman != nil {
continue
}
for newbie := range setOrgFroshesYetMeasured {
bird, err := measurer.MeasureAsync(newbie.Phenotype[:])
if err != nil { // 'This agent is already working' error.
// log.Println("fatal:", err) // debug //
panic(err)
}
log.Println("started measuring:", newbie) //
// 123
univ.Agents[measurer] = NewSubscribed(bird, newbie) // 1
mapSetMeasuringBirds[bird] = measurer // 2
delete(setOrgFroshesYetMeasured, newbie) // 3
//
break // Only a single item is retrieved from this map.
}
}
univ.MutexAgents.Unlock()
}
{ // 2. Recv one
if !(len(mapSetMeasuringBirds) > 0) {
continue
}
birdCases := make([]reflect.SelectCase, len(mapSetMeasuringBirds)+1)
{ // dynamic N cases
birdCases[0] = reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(univ.ChStopSign),
}
i := 1
for messenger := range mapSetMeasuringBirds {
birdCases[i] = reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(messenger),
}
i++
}
}
// select case of dynamic N cases
if iMessenger, itfMessageFitness, notClosed := reflect.Select(birdCases); notClosed { // This blocks the thread.
if iMessenger == 0 { // if univ.ChStopSign
chReply := itfMessageFitness.Interface().(chan struct{}) // don't reply and throw that to other routine
return newShutdownError(chReply) // Stop running this measure().
}
univ.MutexAgents.Lock()
// set vars
messenger := birdCases[iMessenger].Chan.Interface().(Swallow)
messageFitness := itfMessageFitness.Float()
measurer := mapSetMeasuringBirds[messenger]
organism := univ.Agents[measurer].O
// update
organism.UpdateFitness(messageFitness)
univ.TopFitness = math.Max(univ.TopFitness, messageFitness)
log.Println("measured:", organism) //
// 123
univ.Agents[measurer] = nil // 1
delete(mapSetMeasuringBirds, messenger) // 2
// 3 // 3 is already done.
univ.MutexAgents.Unlock()
} else { // This must be errornous case - a channel close.
if iMessenger == 0 { // if univ.ChStopSign
panic(fmt.Sprint(birdCases[iMessenger], " must not be closed"))
}
univ.MutexAgents.Lock()
log.Println("warning: a closed channel is selected") //
messenger := birdCases[iMessenger].Chan.Interface().(Swallow)
measurer := mapSetMeasuringBirds[messenger]
delete(univ.Agents, measurer) // 1 // unregister measurer
delete(mapSetMeasuringBirds, messenger) // 2
// 3 // 3 is already done.
// End of abnormal event handler.
univ.MutexAgents.Unlock()
}
}
// spin loop
// for messenger, organism := range mapSetMeasuringBirds {
// select {
// case fitness := <-messenger:
// organism.UpdateFitness(fitness)
// delete(mapSetMeasuringBirds, messenger)
// default:
// // Noop. Do nothing.
// }
// }
} // for
univ.Save("akashic." + univ.Config.ExperimentName + "." + strconv.Itoa(univ.Generation) + ".measured.json")
return nil
}
extinctify := func(univ *Universe) {
// Update species.
univ.MutexClasses.Lock()
for _, group := range univ.Classes {
// Check stagnancy.
_ /*stagnancy*/, _ /*topFitnessOfSpecies*/, err := group.EvaluateGeneration() // update stagnancy and topFitness
if err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
}
univ.MutexClasses.Unlock()
// Presidential election.
boss, inPlaceOfBoss := func(univ *Universe) (immuneToExtinction *Species, vicePresident *Species) { // get one leading species
univ.MutexClasses.Lock()
sortByProminence, err := univ.GetSpeciesOrderedByProminence()
univ.MutexClasses.Unlock()
if err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
if len(sortByProminence) >= 2 {
return sortByProminence[0], sortByProminence[1]
}
return sortByProminence[0], nil // assuming at least one species is there in univ.
}(univ)
log.Println("top leading species:", boss) //
if inPlaceOfBoss != nil {
log.Println("second leading species:", inPlaceOfBoss) //
}
// The black spot distribution.
const (
_ = iota
extinctionReasonOverStagnantGlobal
extinctionReasonOverStagnantLocal
extinctionReasonTooWeakAvgFitness
extinctionReasonTooWeakTopFitness
extinctionReasonTooWeakHomeAlone
)
sumFitAvgAdj, fitAvgsAdj := univ.AdjAvgFitnessesOfSpecies() // reads
sumFitTopAdj, fitTopsAdj := univ.AdjTopFitnessesOfSpecies() // reads
indicesSpeciesExtinct := []int{}
reasonsSpeciesExtinct := map[*Species]int{}
univ.MutexClasses.Lock()
for iGroup, group := range univ.Classes { // univ.Classes because of univ.AverageFitnessesOfSpecies()
// Blackspot because RemoveSpecies() modifies `univ.Classes` thus can't be called while iterating over it.
if group == boss { // the boss is immune to any of these death conditions
continue
}
if univ.Config.MaxCntApocalypse >= 0 && boss.Stagnancy > univ.Config.MaxCntApocalypse { // Apocalypse not Armageddon
if group != inPlaceOfBoss {
indicesSpeciesExtinct = append(indicesSpeciesExtinct, iGroup)
reasonsSpeciesExtinct[group] = extinctionReasonOverStagnantGlobal
}
continue
}
if univ.Config.MaxCntArmageddon >= 0 && group.Stagnancy > univ.Config.MaxCntArmageddon { // general over-stagnant
indicesSpeciesExtinct = append(indicesSpeciesExtinct, iGroup)
reasonsSpeciesExtinct[group] = extinctionReasonOverStagnantLocal
continue
}
if 1 > int(math.Floor((fitAvgsAdj[iGroup]/sumFitAvgAdj)*float64(univ.Config.SizePopulation))) { // too weak in avg-fitness to even become a species with at least 1 population
indicesSpeciesExtinct = append(indicesSpeciesExtinct, iGroup)
reasonsSpeciesExtinct[group] = extinctionReasonTooWeakAvgFitness
continue
}
if 1 > int(math.Floor((fitTopsAdj[iGroup]/sumFitTopAdj)*float64(univ.Config.SizePopulation))) { // too weak in top-fitness to even become a species with at least 1 population
indicesSpeciesExtinct = append(indicesSpeciesExtinct, iGroup)
reasonsSpeciesExtinct[group] = extinctionReasonTooWeakTopFitness
continue
}
if group.Size() < 2 && group.Stagnancy > 0 { // species with only a single population doesn't need to get more than one chance
indicesSpeciesExtinct = append(indicesSpeciesExtinct, iGroup)
reasonsSpeciesExtinct[group] = extinctionReasonTooWeakHomeAlone
continue
}
}
univ.MutexClasses.Unlock()
// Extinction.
classesRemovedAndEmpty, expelledOrgsByClasses, err := univ.RemoveClasses(indicesSpeciesExtinct...) // should be called outside the univ.Classes iteration.
if err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
for iClass, expelledOrgs := range expelledOrgsByClasses {
classEmptied := classesRemovedAndEmpty[iClass]
for _, organismExpelled := range expelledOrgs {
if err := univ.RemoveOrganism(organismExpelled); err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
}
switch reasonsSpeciesExtinct[classEmptied] {
case extinctionReasonOverStagnantGlobal:
log.Println(len(expelledOrgs), "creature(s) of", classEmptied.Niche(), "got annihilated for this entire universe being stagnant over", univ.Config.MaxCntApocalypse, "generation(s) (extinction)") //
case extinctionReasonOverStagnantLocal:
log.Println(len(expelledOrgs), "creature(s) of", classEmptied.Niche(), "got annihilated for being locally stagnant over", univ.Config.MaxCntArmageddon, "generation(s) (extinction)") //
case extinctionReasonTooWeakAvgFitness:
log.Println(len(expelledOrgs), "creature(s) of", classEmptied.Niche(), "got annihilated for being too weak in avg-fitness (extinction)") //
case extinctionReasonTooWeakTopFitness:
log.Println(len(expelledOrgs), "creature(s) of", classEmptied.Niche(), "got annihilated for being too weak in top-fitness (extinction)") //
case extinctionReasonTooWeakHomeAlone:
log.Println(len(expelledOrgs), "creature(s) of", classEmptied.Niche(), "got annihilated for being too weak with only a single population (extinction)") //
default:
panic("extinction reason unjustified")
}
}
log.Println(len(classesRemovedAndEmpty), "classification(s) got annihilated for being stagnant or being too weak (extinction)") //
}
cull := func(univ *Universe) {
// Wiki: In biology, culling is the process of segregating organisms from
// a group according to desired or undesired characteristics.
univ.MutexClasses.Lock()
for _, zoo := range univ.Classes {
eliminated := zoo.Cull(univ.Config.PercentageCulling)
log.Println(len(eliminated), "creature(s) got culled off of", zoo.Niche()) //
for _, organism := range eliminated {
univ.RemoveOrganism(organism)
}
}
univ.MutexClasses.Unlock()
}
reproduce := func(univ *Universe) (orphans []*Organism) {
// fitnesses
sumFitAvgAdj, fitAvgsAdj := univ.AdjAvgFitnessesOfSpecies()
// orphans
orphans = []*Organism{}
nSizeOrphanageForNewbs := func() int {
// assuming this is after the culling and the extinction.
if univ.Config.IsProtectiveElitism {
return univ.Config.SizePopulation - len(univ.Livings)
}
return univ.Config.SizePopulation - len(univ.Classes)
}()
// orphanage 1
univ.MutexClasses.Lock()
for i, s := range univ.Classes {
if nOffsprings := int(math.Floor((fitAvgsAdj[i] / sumFitAvgAdj) * float64(nSizeOrphanageForNewbs))); nOffsprings > 0 {
babies, err := univ.NewOrganismBrood(s, nOffsprings)
if err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
orphans = append(orphans, babies...)
}
}
univ.MutexClasses.Unlock()
log.Println("reproduced", len(orphans), "new offspring(s)", "and", nSizeOrphanageForNewbs-len(orphans), "filler(s) will be") //
// orphanage 2
for len(orphans) < nSizeOrphanageForNewbs {
newFiller, err := univ.SpeciesRandom().Champion().GenoType().Copy()
if err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
univ.Mutate(newFiller)
o, err := newFiller.NewOrganismSimple()
if err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
orphans = append(orphans, o)
}
log.Println("total of", len(orphans), "creature(s) reproduced") //
if !univ.Config.IsProtectiveElitism {
univ.MutexClasses.Lock()
for _, zoo := range univ.Classes { // re-culling process in reproduce method
eliminated := zoo.CullToSinglePopulation()
log.Println(len(eliminated), "elite creature(s) of", zoo.Niche(), "are retired") //
for _, organism := range eliminated {
univ.RemoveOrganism(organism)
}
}
univ.MutexClasses.Unlock()
}
// test
if nSizeOrphanageForNewbs != len(orphans) {
panic(fmt.Sprint(
fmt.Sprintln("fatal miscalculation: your math is way off"),
fmt.Sprintln(
"nSizeOrphanageForNewbs ->", nSizeOrphanageForNewbs,
"len(orphans) ->", len(orphans),
),
fmt.Sprintln(
"sumFitAvgAdj ->", sumFitAvgAdj,
"fitAvgsAdj ->", fitAvgsAdj,
),
))
}
// return
return orphans
}
speciate := func(univ *Universe, orphans ...*Organism) {
for _, orphan := range orphans {
if orphan == nil {
continue
}
if err := univ.AddOrganism(orphan); err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
if err := univ.Speciate(orphan); err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
// log.Println("speciated:", orphan) //
}
}
forwardGeneration := func(univ *Universe) {
univ.Generation++
univ.Save("akashic." + univ.Config.ExperimentName + "." + strconv.Itoa(univ.Generation) + ".json")
// test
if univ.Config.SizePopulation != len(univ.Livings) {
panic(fmt.Sprint(
fmt.Sprintln("fatal miscalculation: your math is way off"),
fmt.Sprintln(
"univ.Config.SizePopulation ->", univ.Config.SizePopulation,
"len(univ.Livings) ->", len(univ.Livings),
),
))
}
}
epoch := func(univ *Universe) error {
defer func() {
if r := recover(); r != nil { //
log.Println("epoch(): panic recovered")
log.Println("reason for panic:", r)
log.Println("stacktrace from panic: \r\n" + string(debug.Stack()))
}
}()
var chReplyOnHold chan struct{}
holdStopSign := func(chReplyOnHold chan struct{}) { // arg there to make this function look more explicit
if chReplyOnHold == nil {
select {
case chReplyOnHold = <-univ.ChStopSign:
// don't reply and throw that to outside procedure
log.Println("")
log.Println("NaNEAT Main: Shutting down... Please wait until this generation gets saved.")
log.Println("")
default:
// Noop. Do nothing.
}
}
}
unholdStopSign := func(chReplyOnHold chan struct{}, msg string) error { // arg there to make this function look more explicit
if chReplyOnHold != nil {
log.Println("")
log.Println(msg)
log.Println("")
// don't reply and throw that to other routine
return newShutdownError(chReplyOnHold)
}
return nil
}
_, topFitness, population, numSpecies, generation, innovation, nicheCount := univ.Info()
log.Println("")
log.Println("NaNEAT Main:",
"Gen", generation, "Population", population,
"TopFitness", topFitness, "Innovation", innovation,
"NicheCount", nicheCount, "Num.Species", numSpecies,
) //
log.Println("")
if err := unholdStopSign(
chReplyOnHold, "NaNEAT Main: Run stops. All the work is save to a json file.",
); err != nil {
return err // Stop running this epoch.
}
select { // resolve stop sign // neither hold nor unhold
case chReply := <-univ.ChStopSign:
log.Println("")
log.Println("NaNEAT Main: Run interrupted and just stopped. All the work is save to a json file if Gen > 0.")
log.Println("")
// don't reply and throw that to other routine
return newShutdownError(chReply) // Stop running this epoch.
default:
// Noop. Do nothing.
}
log.Println("")
log.Println("NaNEAT Main: 1. Measure", univ.Generation) //
log.Println("")
if err := measure(univ); err != nil { // Measure fitnesses of our creatures.
switch err.(type) { // resolve stop sign // neither hold nor unhold
case *shutdownError: // catch thrown channel
univ.Save("akashic." + univ.Config.ExperimentName + "." + strconv.Itoa(univ.Generation) + ".measuring.json")
log.Println("")
log.Println("NaNEAT Main: Run interrupted and just stopped. All the work is saved to a json file.")
log.Println("")
// don't reply and throw that to other routine
return err // Stop running this epoch.
default: // panic for unhandled errors
panic(err)
}
}
holdStopSign(chReplyOnHold)
log.Println("")
log.Println("NaNEAT Main: 2. Extinctify", univ.Generation) //
log.Println("")
extinctify(univ) // Stagnant species get obliterated and face their extinction. xD
holdStopSign(chReplyOnHold)
log.Println("")
log.Println("NaNEAT Main: 3. Cull", univ.Generation) //
log.Println("")
cull(univ) // Cull off the weakers in each species.
holdStopSign(chReplyOnHold)
log.Println("")
log.Println("NaNEAT Main: 4. Reproduce", univ.Generation) //
log.Println("")
kids := reproduce(univ) // Breed alive creatures in their mating season, involving genetic mutation and crossover.
holdStopSign(chReplyOnHold)
log.Println("")
log.Println("NaNEAT Main: 5. Speciate", univ.Generation) //
log.Println("")
speciate(univ, kids...) // Classify all children of this new generation individually. Plus respeciate previous ones.
holdStopSign(chReplyOnHold)
log.Println("")
log.Println("NaNEAT Main: 6. ForwardGeneration", univ.Generation) //
log.Println("")
forwardGeneration(univ)
return nil
} // func literal
// Init cycles.
for {
if err := epoch(univ); err != nil {
defer func() { // in case another stop sign came in while doing all these things
select {
case chReply := <-univ.ChStopSign:
chReply <- struct{}{}
default:
// Noop. Do nothing.
}
}()
switch vErr := err.(type) {
case *shutdownError: // Reply at the end of Run() to prevent a new stop sign to be received while handling one of these cases.
vErr.chReply <- struct{}{}
return nil // Successfully stopping running this universe.
default: // panic for unhandled errors
panic(err)
}
}
} // for
// return nil // unreachable
}
// IsPumping thread-safely tells if this universe is running or not.
func (univ *Universe) IsPumping() bool {
univ.MutexRun.Lock()
defer univ.MutexRun.Unlock()
return univ.IsRunning
}
// Shutdown stops running this universe.
// This blocks the routine this method is being called upon until the universe stops.
// Falls through if the universe isn't currently running (at the time this function gets called).
func (univ *Universe) Shutdown() {
if univ.IsPumping() {
chReplyWhenStopped := make(chan struct{})
univ.ChStopSign <- chReplyWhenStopped
<-chReplyWhenStopped
}
}
// ----------------------------------------------------------------------------
// Universe - Getter
//
// Thread-safely get a universe state.
//
// Info peeks this universe's basic status. This should be thread-safe.
// The return is a nice summarized set of basic informative infomations.
//
// Usage:
// _, topFitness, population, numSpecies, generation, innovation, nicheCount := univ.Info()
//
func (univ *Universe) Info() (
conf Configuration, topFitness float64,
population, numSpecies, generation, innovation, nicheCount int,
) {
return univ.Config, univ.TopFitness,
len(univ.Livings), len(univ.Classes),
univ.Generation, univ.Innovation, univ.NicheCount
}
// Measurers thread-safely returns all registered measurers and their work details we might want to take a look at.
// The returned map is a copy but its contents are pointers to original objects.
func (univ *Universe) Measurers() (ret map[Measurer]*Subscribed) {
univ.MutexAgents.Lock()
defer univ.MutexAgents.Unlock()
ret = map[Measurer]*Subscribed{}
for measurer, subbed := range univ.Agents {
ret[measurer] = subbed
}
return ret
}
// Breeds is a getter thread-safely returning all species present in this universe.
// The returned slice is newly constructed and, comes with its contents that reference to original objects.
func (univ *Universe) Breeds() []*Species {
univ.MutexClasses.Lock()
defer univ.MutexClasses.Unlock()
return append([]*Species{}, univ.Classes...)
}
// Organisms is a getter thread-safely returning all organisms present in this universe.
// The returned slice is newly constructed and, comes with its contents that reference to original objects.
func (univ *Universe) Organisms() []*Organism {
univ.MutexLivings.Lock()
defer univ.MutexLivings.Unlock()
ret := make([]*Organism, len(univ.Livings))
i := 0
for creature := range univ.Livings {
ret[i] = creature
i++
}
return ret
}
// BiasGenes returns genes for bias input nodes. This should be thread-safe.
// The returned slice is newly constructed and, comes with its contents that reference to original objects.
func (univ *Universe) BiasGenes() []*NodeGene {
return append([]*NodeGene{}, univ.InputGenes[:univ.Config.NumBiases]...)
}
// NonBiasInputGenes returns genes for non-bias input nodes. This should be thread-safe.
// The returned slice is newly constructed and, comes with its contents that reference to original objects.
func (univ *Universe) NonBiasInputGenes() []*NodeGene {
return append([]*NodeGene{}, univ.InputGenes[univ.Config.NumBiases:]...)
}
// GetInputGenes is a getter. This should be thread-safe.
// The returned slice is newly constructed and, comes with its contents that reference to original objects.
func (univ *Universe) GetInputGenes() []*NodeGene {
return append([]*NodeGene{}, univ.InputGenes...)
}
// GetOutputGenes is a getter. This should be thread-safe.
// The returned slice is newly constructed and, comes with its contents that reference to original objects.
func (univ *Universe) GetOutputGenes() []*NodeGene {
return append([]*NodeGene{}, univ.OutputGenes...)
}
// ----------------------------------------------------------------------------
// Universe - Creator
//
// Note:
// What New~Basic() would return are so pre-defined, (all filled up with hypers)
// thus those functions do not take any argument other than the method's receiver.
//
// AncestorGenes is a getter returning a copy slice of all ancestral node-genes in this universe.
// The slices returned are new; newly constructed with the contents that reference to those original objects.
func (univ *Universe) AncestorGenes() (inputs []*NodeGene, outputs []*NodeGene, err error) {
inputs = make([]*NodeGene, len(univ.InputGenes))
if copy(inputs, univ.InputGenes) != len(inputs) {
return nil, nil, errors.New("failed to copy those inputs node-genes")
}
outputs = make([]*NodeGene, len(univ.OutputGenes))
if copy(outputs, univ.OutputGenes) != len(outputs) {
return nil, nil, errors.New("failed to copy those outputs node-genes")
}
return inputs, outputs, nil
}
// NewChanceGeneBasic is a constructor.
func (univ *Universe) NewChanceGeneBasic() *ChanceGene {
enabled := univ.Config.ChanceIsMutational
addNode := univ.Config.ChanceAddNode
addLink := univ.Config.ChanceAddLink
addBias := univ.Config.ChanceAddBias
perturbWeight := univ.Config.ChancePerturbWeight
nullifyWeight := univ.Config.ChanceNullifyWeight
turnOn := univ.Config.ChanceTurnOn
turnOff := univ.Config.ChanceTurnOff
bump := univ.Config.ChanceBump
return NewChanceGene(enabled, addNode, addLink, addBias, perturbWeight, nullifyWeight, turnOn, turnOff, bump)
}
// NewChanceGeneMix is a constructor.
// All parameters are mandatory and should not be nil. This function panics otherwise.
// The return is the result of a crossover. nil is returned if there is a logical error.
func (univ *Universe) NewChanceGeneMix(vParent1, vParent2 ChanceGene, isAvgNotRnd bool) *ChanceGene {
if vParent1.IsEnabled() && vParent2.IsEnabled() {
const left, right int = 0, 1
pair := [2][]float64{
func(n ...float64) []float64 { return n }(vParent1.Unpack()), // left
func(n ...float64) []float64 { return n }(vParent2.Unpack()), // right
}
if isAvgNotRnd {
return NewChanceGeneEnabled(
(pair[left][0]+pair[right][0])/2, // addNode
(pair[left][1]+pair[right][1])/2, // addLink
(pair[left][2]+pair[right][2])/2, // addBias
(pair[left][3]+pair[right][3])/2, // perturbWeight
(pair[left][4]+pair[right][4])/2, // nullifyWeight
(pair[left][5]+pair[right][5])/2, // turnOn
(pair[left][6]+pair[right][6])/2, // turnOff
(pair[left][7]+pair[right][7])/2, // bump
) // averaged
}
// Rnd
return NewChanceGeneEnabled(
pair[rand.Intn(2)][0], // addNode
pair[rand.Intn(2)][1], // addLink
pair[rand.Intn(2)][2], // addBias
pair[rand.Intn(2)][3], // perturbWeight
pair[rand.Intn(2)][4], // nullifyWeight
pair[rand.Intn(2)][5], // turnOn
pair[rand.Intn(2)][6], // turnOff
pair[rand.Intn(2)][7], // bump
) // 50% chance each
} else if !vParent1.IsEnabled() && !vParent2.IsEnabled() {
return univ.NewChanceGeneBasic()
} else if vParent1.IsEnabled() && !vParent2.IsEnabled() {
return vParent1.Copy()
} else if !vParent1.IsEnabled() && vParent2.IsEnabled() {
return vParent2.Copy()
}
return nil
}
// NewInnovation pulls out a new historical marker from this (current) NEAT context.
// The return is an identifiable innovation number, which is unique in this universe and is considered a component of the LinkGene.
func (univ *Universe) NewInnovation() (innovation *Innovation) {
v := Innovation(univ.Innovation)
innovation = &v
univ.Innovation++
return innovation
}
// NewLinkGene is a constructor.
// The link gene comes with a historical marker unique in this universe. (an identifiable innovation number)
func (univ *Universe) NewLinkGene(from, to *NodeGene, weight float64, enabled bool) *LinkGene {
return NewLinkGene(*univ.NewInnovation(), from, to, weight, enabled)
}
// NewLinkGeneSimple is a constructor.
// The link gene comes with an identifiable innovation number.
// A perturbed random weight is assigned to the gene created.
func (univ *Universe) NewLinkGeneSimple(from, to *NodeGene, enabled bool) *LinkGene {
ret := NewLinkGene(*univ.NewInnovation(), from, to, float64(NeuralValueMid), enabled)
univ.PerturbLinkGene(ret)
return ret
}
// NewLinkGeneFromLinkGene is a constructor.
// The link gene comes with an identifiable innovation number.
// The rest of its fields are filled up with the parent's.
//
// Parameter:
// - `vParent`: The parent as value.
//
func (univ *Universe) NewLinkGeneFromLinkGene(vParent LinkGene) (daughter *LinkGene) {
ret := NewLinkGene(*univ.NewInnovation(), vParent.Topo.From, vParent.Topo.To, vParent.Weight, vParent.Enabled)
return ret
}
// NewLinkGeneSimpleFromLinkGene is a constructor.
// The link gene comes with an identifiable innovation number.
// A perturbed random weight is assigned to the gene created.
// The rest of its fields are filled up with the parent's.
//
// Parameter:
// - `vParent`: The parent as value.
//
func (univ *Universe) NewLinkGeneSimpleFromLinkGene(vParent LinkGene) (daughter *LinkGene) {
ret := univ.NewLinkGeneFromLinkGene(vParent)
univ.PerturbLinkGene(ret)
return ret
}
// NewNodeGeneBasic is a constructor.
// The return is a new unique identifiable hidden node.
func (univ *Universe) NewNodeGeneBasic() *NodeGene {
return NewNodeGene("", HiddenNode, -1)
}
// NewChromosome is a constructor.
func (univ *Universe) NewChromosome(linkGenes []*LinkGene) (*Chromosome, error) {
input, output, err := univ.AncestorGenes()
if err != nil {
return nil, err
}
return NewChromosome(linkGenes, append(input, output...), univ.NewChanceGeneBasic())
}
// NewChromosomeBasic is a constructor.
func (univ *Universe) NewChromosomeBasic() (*Chromosome, error) {
return univ.NewChromosome(nil)
}
// NewGenomeBasic is a constructor.
func (univ *Universe) NewGenomeBasic() (g *Genome, err error) {
chromes := make([]*Chromosome, univ.Config.NumChromosomes)
for i := range chromes {
chromes[i], err = univ.NewChromosomeBasic()
if err != nil {
return nil, err
}
}
return NewGenome(chromes)
}
// NewOrganismBasic is a constructor.
// This returns a basic mutant that's unmeasured and unrelated with any species.
func (univ *Universe) NewOrganismBasic() (o *Organism, err error) {
g, err := univ.NewGenomeBasic()
if err != nil {
return nil, err
}
univ.Mutate(g)
return g.NewOrganismSimple()
}
// NewOrganismBrood returns N new offsprings (re)produced from a designated species,
// where N is given by the parameter `nChildren` >= 1.
// The hermaphroditism is strongly avoided unless there is only one creature left in the species.
// This function can return error for bad genetics issue. Panics upon runtime violation.
func (univ *Universe) NewOrganismBrood(s *Species, nChildren int) (children []*Organism, err error) {
if s == nil {
return nil, errors.New("nil species")
}
if s.Size() < 1 {
return nil, errors.New("empty species")
}
if nChildren < 1 {
return nil, errors.New("nChildren < 1")
}
const (
CrossoverMultipointRnd = iota // Sexual reproduction 1
CrossoverMultipointAvg // Sexual reproduction 2
CrossoverSinglepointRnd // Sexual reproduction 3
CrossoverSinglepointAvg // Sexual reproduction 4
Fission // Asexual breeding (Binary fission)
Sterile // Birth control (Contraception)
)
// local return
children = make([]*Organism, nChildren)
populate := func(iBaby int, genomeBaby *Genome) error {
if genomeBaby == nil {
children[iBaby] = nil
}
univ.Mutate(genomeBaby)
children[iBaby], err = genomeBaby.NewOrganismSimple()
return err
}
for i := range children {
// 1. get newGenomeBaby
var (
newGenomeBaby *Genome
mom, dad *Organism
)
if nCreatures := s.Size(); nCreatures == 1 {
mom = s.OrganismRandom()
dad = mom // hermaphrodite
} else if nCreatures > 1 {
parents := s.RandomOrganisms(2)
mom = parents[0]
dad = parents[1]
} else {
panic("runtime violation - empty species")
}
switch Roulette(univ.Config.BirthRatioSimplified()) {
case CrossoverMultipointRnd: // 1
if mom == dad { // hermaphrodite
newGenomeBaby, err = mom.GenoType().Copy()
} else { // sexual
newGenomeBaby, err = univ.CrossoverMultipoint(mom, dad, false)
}
if err != nil {
return nil, err
}
case CrossoverMultipointAvg: // 2
if mom == dad { // hermaphrodite
newGenomeBaby, err = mom.GenoType().Copy()
} else { // sexual
newGenomeBaby, err = univ.CrossoverMultipoint(mom, dad, true)
}
if err != nil {
return nil, err
}
case CrossoverSinglepointRnd: // 3
if mom == dad { // hermaphrodite
newGenomeBaby, err = mom.GenoType().Copy()
} else { // sexual
newGenomeBaby, err = univ.CrossoverSinglepoint(mom.GenoType(), dad.GenoType(), false)
}
if err != nil {
return nil, err
}
case CrossoverSinglepointAvg: // 4
if mom == dad { // hermaphrodite
newGenomeBaby, err = mom.GenoType().Copy()
} else { // sexual
newGenomeBaby, err = univ.CrossoverSinglepoint(mom.GenoType(), dad.GenoType(), true)
}
if err != nil {
return nil, err
}
case Fission: // 5
newGenomeBaby, err = mom.GenoType().Copy()
if err != nil {
return nil, err
}
default: // This can't be taken lightly. xD
return nil, errors.New("undefined reproduction method")
} // switch case
//
// 2. populate newGenomeBaby
if err := populate(i, newGenomeBaby); err != nil {
return nil, err
}
} // for
return children, nil
}
// NewNiche returns an identifiable niche that's unique in this universe.
// Niche is considered a component of the Species.
func (univ *Universe) NewNiche() (habitat *Niche) {
v := Niche(univ.NicheCount)
habitat = &v
univ.NicheCount++
return habitat
}
// NewSpecies is a constructor. This creates a species with its unique identifier.
func (univ *Universe) NewSpecies(livings []*Organism) *Species {
return NewSpecies(*univ.NewNiche(), livings)
}
// NewSpeciesBasic is a constructor. This simply creates an empty species with its own niche.
func (univ *Universe) NewSpeciesBasic() *Species {
return univ.NewSpecies(nil)
}
// ----------------------------------------------------------------------------
// Universe - Modifier
// PerturbChanceGene perturbs the weight of a dice-gene, in a strength constant and statically defined in this universe.
func (univ *Universe) PerturbChanceGene(perturbed *ChanceGene) {
perturbed.Perturb(
univ.Config.TendencyPerturbDice,
univ.Config.StrengthPerturbDice,
)
}
// PerturbLinkGene perturbs the weight of a link-gene, in a strength defined in the dice.
// Argument dice is allowed to be nil. The other arguments are not.
func (univ *Universe) PerturbLinkGene(perturbed *LinkGene) {
perturbed.Perturb(
univ.Config.TendencyPerturbWeight,
univ.Config.StrengthPerturbWeight,
)
}
// ----------------------------------------------------------------------------
// Universe - Mutator
// Mutate a genome under this universe's context.
// To avoid confusion make sure that you don't mutate an existing organism's genome.
// It is strongly recommended that you get a copy of a genome you want to mutate before this.
func (univ *Universe) Mutate(g *Genome) {
for _, chrome := range g.Chromosomes() {
univ.MutateDice(chrome)
univ.MutatePerturbWeight(chrome)
univ.MutateNullifyWeight(chrome)
if err := univ.MutateAddLinkNoBias(chrome); err != nil {
switch err.Error() {
case "nowhere": // handeled error
// log.Println("Cannot MutateAddLink() with this network. It might be fully connected.") // debug //
default: // unhandled errors
panic(err)
// log.Println("fatal:", err) // debug //
}
}
if err := univ.MutateAddBias(chrome); err != nil {
switch err.Error() {
case "nowhere": // handeled error
// log.Println("Cannot mutate add bias: either bias is not present or fully connected") // debug //
default: // unhandled errors
panic(err)
// log.Println("fatal:", err) // debug //
}
}
univ.MutateAddNode(chrome)
univ.MutateBump(chrome)
univ.MutateEnableOnOff(chrome)
}
}
// MutateDice noises all the dynamic chances of a genetic encoder Gaussian distributed.
// The way this works is similar to the weight mutation.
func (univ *Universe) MutateDice(chrome *Chromosome) {
if chrome.IsDiceEnabled() {
// Chance(probability) for mutating a dice is fixed to a constant 100%.
univ.PerturbChanceGene(chrome.DiceGene)
}
}
// MutatePerturbWeight noises all the weights of a genetic encoder Gaussian distributed.
func (univ *Universe) MutatePerturbWeight(chrome *Chromosome) {
for _, linkGene := range chrome.LinkGenes {
for chance := func() float64 { // the initial value
p := 0.0
if chrome.IsDiceEnabled() {
p = chrome.DiceGene.PerturbWeight
} else if !chrome.IsDiceEnabled() {
p = univ.Config.ChancePerturbWeight
} else {
panic("runtime violation")
}
return p
}(); chance > 0.0; chance -= 1.0 {
if rand.Float64() < chance {
univ.PerturbLinkGene(linkGene)
}
}
}
}
// MutateNullifyWeight gives a chance for synaptic weights to become zero.
func (univ *Universe) MutateNullifyWeight(chrome *Chromosome) {
for _, linkGene := range chrome.LinkGenes {
for chance := func() float64 { // the initial value
p := 0.0
if chrome.IsDiceEnabled() {
p = chrome.DiceGene.NullifyWeight
} else if !chrome.IsDiceEnabled() {
p = univ.Config.ChanceNullifyWeight
} else {
panic("runtime violation")
}
return p
}(); chance > 0.0; chance -= 1.0 {
if rand.Float64() < chance {
linkGene.Weight = 0.0
}
}
}
}
// MutateAddLinkNoBias creates a structural innovation between existing non-bias nodes.
func (univ *Universe) MutateAddLinkNoBias(chrome *Chromosome) error {
if chrome == nil {
return errors.New("nil chromosome")
}
for chance := func() float64 { // the initial value
p := 0.0
if chrome.IsDiceEnabled() {
p = chrome.DiceGene.AddLink
} else if !chrome.IsDiceEnabled() {
p = univ.Config.ChanceAddLink
} else {
panic("runtime violation")
}
return p
}(); chance > 0.0; chance -= 1.0 {
if rand.Float64() < chance {
proto, err := chrome.NewNeuralNetworkProto(true, true, false)
if err != nil {
return err
}
from, to, err := proto.RandomNicePlaceForNewAcyclicLink()
if err != nil {
return err
}
// mutate genetics
newLinkGene := univ.NewLinkGeneSimple(from.Gene(), to.Gene(), true)
chrome.AddLinkGenes(newLinkGene) // This will/must not create any cycle in the graph.
}
}
return nil
}
// MutateAddBias creates a link that connects to a bias.
func (univ *Universe) MutateAddBias(chrome *Chromosome) error {
if chrome == nil {
return errors.New("nil chromosome")
}
for chance := func() float64 { // the initial value
p := 0.0
if chrome.IsDiceEnabled() {
p = chrome.DiceGene.AddBias
} else if !chrome.IsDiceEnabled() {
p = univ.Config.ChanceAddBias
} else {
panic("runtime violation")
}
return p
}(); chance > 0.0; chance -= 1.0 {
if rand.Float64() < chance {
proto, err := chrome.NewNeuralNetworkProto(true, false, true)
if err != nil {
return err
}
nodes, err := proto.Sort()
if err != nil {
return err
}
from, to := func() (fromNode, toNode *NeuralNode) {
if biases := proto.GetInputNodes(); len(biases) > 0 {
fromNode = biases[rand.Intn(len(biases))]
toNode = proto.FromTo(nodes, fromNode)
return fromNode, toNode
}
return nil, nil
}()
if from == nil || to == nil {
return errors.New("nowhere") // cannot mutate add bias: either bias is not present or fully connected
}
// mutate genetics
newLinkGene := univ.NewLinkGeneSimple(from.Gene(), to.Gene(), true)
chrome.AddLinkGenes(newLinkGene) // This will/must not create any cycle in the graph.
}
}
return nil
}
// MutateAddNode of a genetic encoder creates a structural innovation.
// This splits a link resulting in two links and a node.
// The function will panic if nil parameter is provided.
// In theory this shouldn't raise any error as long as the Mutate-Add-Link operation works fine.
func (univ *Universe) MutateAddNode(chrome *Chromosome) {
// if chrome == nil {
// return errors.New("nil chromosome")
// }
MutateAddNodeLoop:
for chance := func() float64 { // the initial value
p := 0.0
if chrome.IsDiceEnabled() {
p = chrome.DiceGene.AddNode
} else if !chrome.IsDiceEnabled() {
p = univ.Config.ChanceAddNode
} else {
panic("runtime violation")
}
return p
}(); chance > 0.0; chance -= 1.0 {
links := func(chrome *Chromosome) (enabledLinks []*LinkGene) {
enabledLinks = []*LinkGene{}
for _, linkGene := range chrome.LinkGenes {
if linkGene.Enabled {
enabledLinks = append(enabledLinks, linkGene)
}
}
return enabledLinks
}(chrome)
if len(links) <= 0 {
break MutateAddNodeLoop
}
if rand.Float64() < chance {
linkSplit := func(enabledLinks []*LinkGene) (randomSplittableLink *LinkGene) {
return enabledLinks[rand.Intn(len(enabledLinks))]
}(links)
// Do not ruin the order below.
vParentFrom := *linkSplit
vParentTo := *linkSplit
if NeuralValue(vParentFrom.Weight).Kind() == NVKindNegative &&
NeuralValue(vParentTo.Weight).Kind() == NVKindNegative {
vParentTo.Weight *= -1
}
linkSplit.Enabled = false
newNodeGene := univ.NewNodeGeneBasic()
vParentFrom.Topo.To = newNodeGene
vParentTo.Topo.From = newNodeGene
chrome.AddLinkGenes(
univ.NewLinkGeneSimpleFromLinkGene(vParentFrom),
univ.NewLinkGeneSimpleFromLinkGene(vParentTo),
)
// Do not ruin the order above.
}
} // for
// Noop. Do nothing.
return // Return nothing.
}
// MutateBump re-enables the oldest disabled gene.
// This method is only an alias for `(*Chromosome).Bump()`.
//
// Mutations that fiddle around with the enable flag of genes:
// 1. Re-enable the oldest disabled gene. (BUMP)
// 2. Toggle on or off randomly at a rate.
//
func (univ *Universe) MutateBump(chrome *Chromosome) {
for chance := func() float64 { // the initial value
p := 0.0
if chrome.IsDiceEnabled() {
p = chrome.DiceGene.Bump
} else if !chrome.IsDiceEnabled() {
p = univ.Config.ChanceBump
} else {
panic("runtime violation")
}
return p
}(); chance > 0.0; chance -= 1.0 {
if rand.Float64() < chance {
chrome.Bump()
}
}
}
// MutateEnableOnOff of a genetic encoder mutates(toggles) the link-enable flag of one randomly chosen gene.
// Toggle genes from enable on to enable off or vice versa.
//
// Mutations that fiddle around with the enable flag of genes:
// 1. Re-enable the oldest disabled gene. (BUMP)
// 2. Toggle on or off randomly at a rate.
//
func (univ *Universe) MutateEnableOnOff(chrome *Chromosome) {
enabled, disabled := []*LinkGene{}, []*LinkGene{}
for _, linkGene := range chrome.LinkGenes {
if linkGene.Enabled {
enabled = append(enabled, linkGene)
} else if !linkGene.Enabled {
disabled = append(disabled, linkGene)
} else {
panic("runtime violation")
}
}
// ON
for chance := func() float64 { // the initial value
p := 0.0
if chrome.IsDiceEnabled() {
p = chrome.DiceGene.TurnOn
} else if !chrome.IsDiceEnabled() {
p = univ.Config.ChanceTurnOn
} else {
panic("runtime violation")
}
return p
}(); chance > 0.0 && len(disabled) > 0; chance -= 1.0 {
if rand.Float64() < chance {
i := rand.Int63n(int64(len(disabled)))
disabled[i].Enabled = true
disabled = append(disabled[:i], disabled[i+1:]...)
}
}
// OFF
for chance := func() float64 { // the initial value
p := 0.0
if chrome.IsDiceEnabled() {
p = chrome.DiceGene.TurnOff
} else if !chrome.IsDiceEnabled() {
p = univ.Config.ChanceTurnOff
} else {
panic("runtime violation")
}
return p
}(); chance > 0.0 && len(enabled) > 0; chance -= 1.0 {
if rand.Float64() < chance {
i := rand.Int63n(int64(len(enabled)))
enabled[i].Enabled = false
enabled = append(enabled[:i], enabled[i+1:]...)
}
}
}
// ----------------------------------------------------------------------------
// Universe - Crossover
// CrossoverMultipoint mates two fitness-measured organisms.
// The return is their new obvious child, with mixed up genes, inheriting all
// the structural innovation from the dominant(more-fit) of either parents.
// This allows the topologies to grow incrementally maintaining their minimal
// structure, helping those evolving structures to find out a better solution
// whilst keeping themselves as small as possible.
// This operation alone can interbreed two organisms of different species,
// as it doesn't care the compatibility distance of them.
//
// - - -
//
// Parameters:
// - `o1`, `o2`: The parents. (can be given in any order)
// - `isAvgNotRnd`: This option determines the mating method either
// the matching genes are average weighted or are randomly chosen from either of the parents.
//
// Returns:
// - `offspring`: The baby as a genetic encoder.
// - `err`: Not nil if any kind of invalid value was found.
//
// - - -
//
// Multipoint mating methods:
//
// 1. `Rnd`;
// For every point in each chromosome where each chromosome shares the innovation number,
// the gene is chosen randomly from either parent.
// If one parent has an innovation absent in the other,
// the baby always inherits the innovation from the more fit parent.
//
// 2. `Avg`;
// Instead of selecting one or the other when the innovation numbers match,
// this averages the weight at each matching gene.
//
// - - -
//
func (univ *Universe) CrossoverMultipoint(o1, o2 *Organism, isAvgNotRnd bool) (offspring *Genome, err error) {
// CrossoverableO checks if two chosen organisms are okay to be crossovered,
// without considering their compatibility.
// The return provides the reason why the two are unable to reproduce.
if err = func(o1, o2 *Organism, isFitnessRequired bool) (err error) { // CrossoverableO
if o1 == nil {
return errors.New(fmt.Sprint("nil organism: ", o1))
}
if o2 == nil {
return errors.New(fmt.Sprint("nil organism: ", o2))
}
if isFitnessRequired {
if !o1.IsFitnessMeasured() {
return errors.New(fmt.Sprint("fitness unmeasured: ", o1))
}
if !o2.IsFitnessMeasured() {
return errors.New(fmt.Sprint("fitness unmeasured: ", o2))
}
}
if o1.GenoType().Length() != o2.GenoType().Length() {
return errors.New(fmt.Sprint("the number of chromosome(s) didn't match: ", o1, o2))
}
return nil
}(o1, o2, true); err != nil {
return nil, err
}
fitness1, err := o1.GetFitness()
if err != nil {
return nil, err
}
fitness2, err := o2.GetFitness()
if err != nil {
return nil, err
}
// type local; a full set of what (*Chromosome).Genetics() returns.
type genetics struct {
*Chromosome // inheritance
mapLinkGenes map[Innovation]*LinkGene
nLinkGenes int
historical Innovation
}
// recombination method
recombination := /*switch case*/ func() func(superior, inferior /*in*/ genetics, newChromeBeingConstructed /*out*/ *Chromosome) {
if isAvgNotRnd {
return func(superior, inferior genetics, newChrome *Chromosome) {
for innovation, linkGeneBetter := range superior.mapLinkGenes { // for genes
if linkGenePoorer, isGeneMatched := inferior.mapLinkGenes[innovation]; isGeneMatched {
// When matched, the weight is averaged.
newChrome.AddLinkGenes(func() *LinkGene {
newLinkGeneAvg := linkGenePoorer.Copy()
newLinkGeneAvg.Weight = (linkGenePoorer.Weight + linkGeneBetter.Weight) / 2
return newLinkGeneAvg
}())
} else { // The rest are from the fitter.
newChrome.AddLinkGenes(linkGeneBetter.Copy())
}
}
}
}
return func(superior, inferior genetics, newChrome *Chromosome) {
for innovation, linkGeneBetter := range superior.mapLinkGenes { // for genes
if linkGenePoorer, isGeneMatched := inferior.mapLinkGenes[innovation]; isGeneMatched &&
linkGenePoorer.Enabled && rand.Float64() < 0.5 {
// When matched, enabled inferior genes have 50% chance of replacing the opposite for each.
newChrome.AddLinkGenes(linkGenePoorer.Copy())
} else { // The rest are from the fitter.
newChrome.AddLinkGenes(linkGeneBetter.Copy())
}
}
}
}() // end of switch case
// create & fill newChromes
newChromes := make([]*Chromosome, o1.GenoType().Length())
if err = o1.GenoType().ForEachMatchingChromosome(o2.GenoType(), func(
i int, // iChrome
// 1
chrome1 *Chromosome,
linkGenes1 map[Innovation]*LinkGene,
nLinkGenes1 int,
historical1 Innovation,
// 2
chrome2 *Chromosome,
linkGenes2 map[Innovation]*LinkGene,
nLinkGenes2 int,
historical2 Innovation,
) error {
// tell structural baggage
superior, inferior, err := func() (topologicalDominant, topologicalRecessive genetics, err error) {
// second closure
genetics1 := genetics{chrome1, linkGenes1, nLinkGenes1, historical1}
genetics2 := genetics{chrome2, linkGenes2, nLinkGenes2, historical2}
if fitness1 > fitness2 {
topologicalDominant = genetics1
topologicalRecessive = genetics2
} else if fitness1 < fitness2 {
topologicalDominant = genetics2
topologicalRecessive = genetics1
} else if fitness1 == fitness2 {
if nLinkGenes1 >= nLinkGenes2 {
topologicalDominant = genetics2
topologicalRecessive = genetics1
} else if nLinkGenes1 < nLinkGenes2 {
topologicalDominant = genetics1
topologicalRecessive = genetics2
} else {
panic("runtime violation")
}
} else {
panic("runtime violation")
}
return topologicalDominant, topologicalRecessive, nil
}() // closure call
if err != nil {
return err
}
// fill me
newChrome, err := univ.NewChromosomeBasic()
if err != nil {
return err
}
// for range genes
recombination(superior, inferior, newChrome)
// dice gene by val
*newChrome.DiceGene = *univ.NewChanceGeneMix(
*superior.DiceGene, *inferior.DiceGene, isAvgNotRnd,
)
// local return
newChromes[i] = newChrome
return nil
}); err != nil { // This is after the closure above returned.
return nil, err
}
return NewGenome(newChromes)
}
// CrossoverSinglepoint mates two organisms.
// This method does not require the fitnesses to be measured. The return is their new child.
// A single point is chosen in the smaller chromosome to split and cross with the bigger one.
// Genes to the left of that point in the smaller chromosome is connected to the other where
// every single gene is taken from the larger chromosome.
// This operation alone can interbreed two organisms of different species,
// as it doesn't care the compatibility distance of the parents.
//
// - - -
//
// Singlepoint mating methods:
// 1. `Avg`; The gene at crosspoint is averaged with the matching gene from the larger, iif there is one.
// 2. `Rnd`; The gene is randomly chosen from either of the parents' that's not absent.
//
// Parameters:
// - `g1`, `g2`: The parents. (can be given in any order)
// - `isAvgNotRnd`: This option determines the mating method either
// the crosspoint gene is average weighted or is randomly chosen from either of the parents.
//
// Returns:
// - `offspring`: The baby genome.
// - `err`: Not nil if any kind of invalid value was found.
//
// - - -
//
func (univ *Universe) CrossoverSinglepoint(g1, g2 *Genome, isAvgNotRnd bool) (offspring *Genome, err error) {
// CrossoverableG checks if two chosen organisms are okay to be crossovered,
// without considering their compatibility.
// The return provides the reason why the two are unable to reproduce.
if err = func(g1, g2 *Genome) (err error) { // CrossoverableG
if g1 == nil {
return errors.New(fmt.Sprint("nil genome: ", g1))
}
if g2 == nil {
return errors.New(fmt.Sprint("nil genome: ", g2))
}
if g1.Length() != g2.Length() {
return errors.New(fmt.Sprint("the number of chromosome(s) didn't match: ", g1, g2))
}
return nil
}(g1, g2); err != nil {
return nil, err
}
// create & fill newChromes
newChromes := make([]*Chromosome, g1.Length())
if err = g1.ForEachMatchingChromosome(g2, func(
i int, // iChrome
// 1
chrome1 *Chromosome,
linkGenes1 map[Innovation]*LinkGene,
nLinkGenes1 int,
historical1 Innovation,
// 2
chrome2 *Chromosome,
linkGenes2 map[Innovation]*LinkGene,
nLinkGenes2 int,
historical2 Innovation,
) error {
// type local; a full set of what (*Chromosome).Genetics() returns.
type genetics struct {
*Chromosome // inheritance
mapLinkGenes map[Innovation]*LinkGene
nLinkGenes int
historical Innovation
}
// play a role
smaller, bigger, err := func() (shorter, longer genetics, err error) {
// second closure
genetics1 := genetics{chrome1, linkGenes1, nLinkGenes1, historical1}
genetics2 := genetics{chrome2, linkGenes2, nLinkGenes2, historical2}
if nLinkGenes1 > nLinkGenes2 {
shorter = genetics2
longer = genetics1
} else if nLinkGenes1 < nLinkGenes2 {
shorter = genetics1
longer = genetics2
} else if nLinkGenes1 == nLinkGenes2 {
switch rand.Intn(2) { // just flip a two-sided coin
case 0:
shorter = genetics2
longer = genetics1
case 1:
shorter = genetics1
longer = genetics2
default:
panic("runtime violation")
}
// or decided depending on the innovation number
// if historical1 >= historical2 {
// shorter = genetics2
// longer = genetics1
// } else if historical1 < historical2 {
// shorter = genetics1
// longer = genetics2
// } else {
// panic("runtime violation")
// }
} else {
panic("runtime violation")
}
return shorter, longer, nil
}() // closure call
if err != nil {
return err
}
// fill me
newChrome, err := univ.NewChromosomeBasic()
if err != nil {
return err
}
// cross
iCrosspoint := rand.Intn(smaller.nLinkGenes) - 1 // where the last element of the left is in
// -1 can be returned for sizeBuilding 0 chrome
if iCrosspoint != -1 {
{ // left
smaller.Sort()
for iLeft := 0; iLeft < iCrosspoint; iLeft++ { // excluding the crosspoint
newChrome.AddLinkGenes(smaller.LinkGenes[iLeft].Copy())
}
}
{ // crosspoint
linkGeneLeft := smaller.LinkGenes[iCrosspoint]
if linkGeneRight, isGeneMatched := bigger.mapLinkGenes[linkGeneLeft.Topo.Innov]; isGeneMatched {
linkGeneMix := linkGeneLeft.Copy()
if isAvgNotRnd {
linkGeneMix.Weight = (linkGeneLeft.Weight + linkGeneRight.Weight) / 2
newChrome.AddLinkGenes(linkGeneMix.Copy())
} else { // toss a coin
if rand.Float64() < 0.5 { // head
newChrome.AddLinkGenes(linkGeneLeft.Copy())
} else { // tail
newChrome.AddLinkGenes(linkGeneRight.Copy())
}
}
} else {
newChrome.AddLinkGenes(linkGeneLeft.Copy())
}
}
{ // right
bigger.Sort()
if iLower := sort.Search(bigger.nLinkGenes, func(i int) bool {
return bigger.LinkGenes[i].Topo.Innov > smaller.historical
}); iLower != -1 {
for iRight := iLower; iRight < bigger.nLinkGenes; iRight++ {
newChrome.AddLinkGenes(bigger.LinkGenes[iRight].Copy())
}
}
}
} else { // right
for _, linkGeneRight := range bigger.mapLinkGenes {
newChrome.AddLinkGenes(linkGeneRight.Copy())
}
}
// dice gene by val
*newChrome.DiceGene = *univ.NewChanceGeneMix(
*smaller.DiceGene, *bigger.DiceGene, isAvgNotRnd,
)
// local return
newChromes[i] = newChrome
return nil
}); err != nil { // This is after the closure above returned.
return nil, err
}
return NewGenome(newChromes)
}
// ----------------------------------------------------------------------------
// Universe - Genetic Analysis
// ChanceDiff returns mutational differences between two die.
func (univ *Universe) ChanceDiff(dice1, dice2 *ChanceGene) (
addNode, addLink, addBias, perturbWeight, nullifyWeight, turnOn, turnOff, bump float64,
) {
if dice1 == nil || dice1.Enabled == false {
dice1 = univ.NewChanceGeneBasic()
}
if dice2 == nil || dice2.Enabled == false {
dice2 = univ.NewChanceGeneBasic()
}
// Unpack
addNode1, addLink1, addBias1, perturbWeight1, nullifyWeight1, turnOn1, turnOff1, bump1 := dice1.Unpack()
addNode2, addLink2, addBias2, perturbWeight2, nullifyWeight2, turnOn2, turnOff2, bump2 := dice2.Unpack()
// Return
addNode = math.Abs(addNode1 - addNode2)
addLink = math.Abs(addLink1 - addLink2)
addBias = math.Abs(addBias1 - addBias2)
perturbWeight = math.Abs(perturbWeight1 - perturbWeight2)
nullifyWeight = math.Abs(nullifyWeight1 - nullifyWeight2)
turnOn = math.Abs(turnOn1 - turnOn2)
turnOff = math.Abs(turnOff1 - turnOff2)
bump = math.Abs(bump1 - bump2)
return
}
// ChanceDiffInPercent returns mutational differences between two die, normalized to [0, 1].
func (univ *Universe) ChanceDiffInPercent(dice1, dice2 *ChanceGene) (
addNode, addLink, addBias, perturbWeight, nullifyWeight, turnOn, turnOff, bump float64,
) {
if dice1 == nil || dice1.Enabled == false {
dice1 = univ.NewChanceGeneBasic()
}
if dice2 == nil || dice2.Enabled == false {
dice2 = univ.NewChanceGeneBasic()
}
// Unpack
addNode1, addLink1, addBias1, perturbWeight1, nullifyWeight1, turnOn1, turnOff1, bump1 := dice1.Unpack()
addNode2, addLink2, addBias2, perturbWeight2, nullifyWeight2, turnOn2, turnOff2, bump2 := dice2.Unpack()
// Return
addNode = RelativeDifferenceNormalized(addNode1, addNode2)
addLink = RelativeDifferenceNormalized(addLink1, addLink2)
addBias = RelativeDifferenceNormalized(addBias1, addBias2)
perturbWeight = RelativeDifferenceNormalized(perturbWeight1, perturbWeight2)
nullifyWeight = RelativeDifferenceNormalized(nullifyWeight1, nullifyWeight2)
turnOn = RelativeDifferenceNormalized(turnOn1, turnOn2)
turnOff = RelativeDifferenceNormalized(turnOff1, turnOff2)
bump = RelativeDifferenceNormalized(bump1, bump2)
return
}
// Compatible checks if two genomes are compatible for crossover and in addition returns a measure of
// how distant two genomes are. Those genome parameters can be given in any order.
// This is where the compatibility distance gets in in order to speciate different organisms.
//
// The compatibility distance gives a measure of compatibility between two Genomes,
// by computing a linear combination of 3 characterizing variables of their compatibility.
// The 3 variables represent:
// PERCENT DISJOINT GENES, PERCENT EXCESS GENES, and MUTATIONAL DIFFERENCE WITHIN MATCHING GENES.
// So the formula for compatibility is: `th <= c1*pdg + c2*peg + c3*mdmg`.
// The 3 coefficients are global system parameters. (NEAT context of Universe here.)
// For the mutational difference, the weight of a gene can give a rough sense of
// how much mutation the gene has experienced since it originally appeared.
//
// This function returns an error if given parameters are not valid.
//
func (univ *Universe) Compatible(g1 *Genome, g2 *Genome) (isFertile bool, incompatibility float64, err error) {
// vars
nChromes := g1.Length()
analysis1 := make([]bool, nChromes) // isFertile for each chromosome
analysis2 := make([]float64, nChromes) // incompatibility for each chromosome
// iterate over
if err = g1.ForEachMatchingChromosome(g2, func(
i int, // iChrome
// 1
chrome1 *Chromosome,
linkGenes1 map[Innovation]*LinkGene,
nLinkGenes1 int,
historical1 Innovation,
// 2
chrome2 *Chromosome,
linkGenes2 map[Innovation]*LinkGene,
nLinkGenes2 int,
historical2 Innovation,
) error {
var ( // Divide two chosen chromosomes into Seme and Uke.
mapExcessive, mapDeficient map[Innovation]*LinkGene
minHistorical Innovation
) // Play your role...
switch maxHistorical := math.Max(float64(historical1), float64(historical2)); maxHistorical {
case float64(historical1):
mapExcessive, mapDeficient = linkGenes1, linkGenes2
minHistorical = historical2
case float64(historical2):
mapExcessive, mapDeficient = linkGenes2, linkGenes1
minHistorical = historical1
default:
panic("runtime violation found in switch case")
}
// Two chosen chromosomes: Calculating the characterizing variables.
var (
nMatched, nDisjoint, nExcess int
sumPercentWeightDiff, sumPercentChanceDiff float64 // sum of those normalized
sumWeightDiff, sumChanceDiff float64 // sum of raw value differences
)
if univ.Config.CompatIsNormalizedForSize {
sumPercentChanceDiff = Sum(univ.ChanceDiffInPercent(chrome1.DiceGene, chrome2.DiceGene))
for innovation, geneFrom := range mapExcessive {
if geneTo, available := mapDeficient[innovation]; available { // 1. matching gene
nMatched++
sumPercentWeightDiff += RelativeDifferenceNormalized(geneFrom.Weight, geneTo.Weight)
} else if innovation <= minHistorical { // 2. disjoint gene
nDisjoint++
} else if innovation > minHistorical { // 3. excess gene
nExcess++
} else {
panic("runtime violation found in an excessive chromosome")
}
}
} else if !univ.Config.CompatIsNormalizedForSize {
sumChanceDiff = Sum(univ.ChanceDiff(chrome1.DiceGene, chrome2.DiceGene))
for innovation, geneFrom := range mapExcessive {
if geneTo, available := mapDeficient[innovation]; available { // 1. matching gene
nMatched++
sumWeightDiff += math.Abs(geneFrom.Weight - geneTo.Weight)
} else if innovation <= minHistorical { // 2. disjoint gene
nDisjoint++
} else if innovation > minHistorical { // 3. excess gene
nExcess++
} else {
panic("runtime violation found in an excessive chromosome")
}
}
} else {
panic("runtime violation in hyper param")
}
for innovation /*, geneFrom*/ := range mapDeficient {
if _, available := mapExcessive[innovation]; available { // 1. matching gene
// Noop. Do nothing.
} else if innovation <= minHistorical { // 2. disjoint gene
nDisjoint++
} else if innovation > minHistorical { // 3. excess gene
panic("an excess gene was found in a deficient chromosome")
} else {
panic("runtime violation found in a deficient chromosome")
}
}
// Local return of this chromosome.
compatDistChromosome := func() (retCompatDistChromosome float64) {
// coefficients
c1 := univ.Config.CompatCoeffDisjoint
c2 := univ.Config.CompatCoeffExcess
c3 := univ.Config.CompatCoeffWeight
c4 := univ.Config.CompatCoeffChance
if univ.Config.CompatIsNormalizedForSize { // Normalize each of the characteristics to [0, 1].
var (
normalizeFactor, percentDisjoint, percentExcess float64
avgWeightDiffInPercent, avgChanceDiffInPercent float64
)
// pdg and peg
normalizeFactor = Sum(float64(nLinkGenes1), float64(nLinkGenes2))
// normalizeFactor = math.Max(float64(nLinkGenes1), float64(nLinkGenes2)) // normalize for size bigger of either
if normalizeFactor != 0.0 { // handles divide by zero
percentDisjoint = float64(nDisjoint) / normalizeFactor // in percatage
percentExcess = float64(nExcess) / normalizeFactor // in percatage
}
// mdmg
if float64(nMatched) != 0.0 { // handles divide by zero
avgWeightDiffInPercent = sumPercentWeightDiff / float64(nMatched) // in percatage
}
if float64(NumKindChance) != 0.0 { // handles divide by zero
avgChanceDiffInPercent = sumPercentChanceDiff / float64(NumKindChance) // in percatage
}
// The compatibility formula. (for normalized characteristics)
retCompatDistChromosome = (c1 * percentDisjoint) + (c2 * percentExcess) + (c3 * avgWeightDiffInPercent) + (c4 * avgChanceDiffInPercent)
// if retCompatDistChromosome > c1+c2+c3+c4 || retCompatDistChromosome < 0 { // debug //
// log.Println("len(mapExcessive)", len(mapExcessive)) // debug //
// log.Println("len(mapDeficient)", len(mapDeficient)) // debug //
// log.Println("minHistorical", minHistorical) // debug //
// log.Println("/") // debug //
// log.Println("nDisjoint", nDisjoint) // debug //
// log.Println("nExcess", nExcess) // debug //
// log.Println("nMatched", nMatched) // debug //
// log.Println("NumKindChance", NumKindChance) // debug //
// log.Println("sumWeightDiff", sumWeightDiff) // debug //
// log.Println("sumChanceDiff", sumChanceDiff) // debug //
// log.Println("normalizeFactor", normalizeFactor) // debug //
// log.Println("/") // debug //
// log.Println("percentDisjoint", percentDisjoint) // debug //
// log.Println("percentExcess", percentExcess) // debug //
// log.Println("avgWeightDiffInPercent", avgWeightDiffInPercent) // debug //
// log.Println("avgChanceDiffInPercent", avgChanceDiffInPercent) // debug //
// log.Println("/") // debug //
// log.Println("retCompatDistChromosome", retCompatDistChromosome) // debug //
// log.Println() // debug //
// } // debug //
return retCompatDistChromosome
} else if !univ.Config.CompatIsNormalizedForSize {
// mdmg
var avgWeightDiff, avgChanceDiff float64
if float64(nMatched) != 0.0 { // handles divide by zero
avgWeightDiff = sumWeightDiff / float64(nMatched) // in percatage
}
if float64(NumKindChance) != 0.0 { // handles divide by zero
avgChanceDiff = sumChanceDiff / float64(NumKindChance) // in percatage
}
// The compatibility formula. (for absolute characteristics)
retCompatDistChromosome = (c1 * float64(nDisjoint)) + (c2 * float64(nExcess)) + (c3 * avgWeightDiff) + (c4 * avgChanceDiff)
// log.Println("len(mapExcessive)", len(mapExcessive)) // debug //
// log.Println("len(mapDeficient)", len(mapDeficient)) // debug //
// log.Println("minHistorical", minHistorical) // debug //
// log.Println("/") // debug //
// log.Println("nMatched", nMatched) // debug //
// log.Println("NumKindChance", NumKindChance) // debug //
// log.Println("sumWeightDiff", sumWeightDiff) // debug //
// log.Println("sumChanceDiff", sumChanceDiff) // debug //
// log.Println("/") // debug //
// log.Println("nDisjoint", nDisjoint) // debug //
// log.Println("nExcess", nExcess) // debug //
// log.Println("avgWeightDiff", avgWeightDiff) // debug //
// log.Println("avgChanceDiff", avgChanceDiff) // debug //
// log.Println("/") // debug //
// log.Println("retCompatDistChromosome", retCompatDistChromosome) // debug //
// log.Println() // debug //
return retCompatDistChromosome
}
panic("runtime violation in hyper param")
}()
isChromosomeFertile := (compatDistChromosome <= univ.Config.CompatThreshold)
// Local return.
analysis1[i] = isChromosomeFertile
analysis2[i] = compatDistChromosome
// if i == nChromes-1 { // debug //
// log.Println("univ.Compatible(g1, g2):", i+1, "matching chromosomes were found in this operation.") // debug //
// log.Println("isFertile", analysis1) // debug //
// log.Println("incompatibility", analysis2) // debug //
// log.Println() // debug //
// } // debug //
return nil
}); err != nil {
return false, math.NaN(), err
} // for
// Return
isFertile = And(analysis1...) // important
incompatibility = Sum(analysis2...) / float64(nChromes) // important
err = nil // nah
return isFertile, incompatibility, err
}
// ----------------------------------------------------------------------------
// Universe - Speciation
//
// These methods does not care the thread-safety. You might want to use `univ.MutexClasses` with these.
//
// GetSpeciesOrderedByProminence returns a copy slice of all available classes in this universe.
// This method is used to tell the champion species of this universe.
// The returned slice here is in...
// - 1) descending order of the top fitness of each species,
// - 2) ascending order of the stagnancy of each species,
// - 3) descending order of the average fitness of each species,
// - 4) and ascending order of the number that denotes how early the niche of each species has appeared in this universe.
// `univ.MutexClasses` is required in order to get this operation guarantee the thread-safety as it accesses `univ.Classes` directly.
// This function panics if there are one or more organisms have not been measured.
func (univ *Universe) GetSpeciesOrderedByProminence() (sortByProminence []*Species, err error) {
sortByProminence = make([]*Species, len(univ.Classes))
if nCopied := copy(sortByProminence, univ.Classes); nCopied != len(univ.Classes) {
return nil, errors.New("failed to copy given slice")
}
sort.Slice(sortByProminence, func(i, j int) bool {
if sortByProminence[i].TopFitness != sortByProminence[j].TopFitness {
return sortByProminence[i].TopFitness > sortByProminence[j].TopFitness // descending
}
if sortByProminence[i].Stagnancy != sortByProminence[j].Stagnancy {
return sortByProminence[i].Stagnancy < sortByProminence[j].Stagnancy // ascending
}
{ // elif
avgFitness1, err1 := sortByProminence[i].AverageFitness()
if err1 != nil {
panic(err1)
}
avgFitness2, err2 := sortByProminence[j].AverageFitness()
if err2 != nil {
panic(err2)
}
if avgFitness1 != avgFitness2 {
return avgFitness1 > avgFitness2 // descending
}
}
return sortByProminence[i].Niche() < sortByProminence[j].Niche() // ascending
})
return sortByProminence, nil
}
// AdjAvgFitnessesOfSpecies evaluates adjusted average-fitnesses of all competing species.
// Adjusted here only means that those values are made non-negative. The order for iSpecies is preserved.
func (univ *Universe) AdjAvgFitnessesOfSpecies() (sumFitAvgAdj float64, fitAvgsAdj []float64) {
return func(sumFitAvg float64, fitAvgs []float64) (
sumFitAvgAdj float64, fitAvgsAdj []float64,
) {
fitAvgsAdj = make([]float64, len(fitAvgs))
minFitAvg := 0.0 // won't be bigger than 0
for i, fitAvg := range fitAvgs {
fitAvgsAdj[i] = fitAvg // copy
minFitAvg = math.Min(minFitAvg, fitAvg) // find min
}
offset := math.Abs(minFitAvg)
for i, fitAvg := range fitAvgsAdj {
fitAvgsAdj[i] = fitAvg + offset // adjust
if fitAvgsAdj[i] < 0 {
panic(fmt.Sprint(
fmt.Sprintln("terrible miscalculation:"),
fmt.Sprintln(
"sumFitAvg ->", sumFitAvg,
"fitAvgs ->", fitAvgs,
),
fmt.Sprintln(
"sumFitAvgAdj ->", sumFitAvgAdj,
"fitAvgsAdj ->", fitAvgsAdj,
),
))
}
}
sumFitAvgAdj = sumFitAvg + (offset * float64(len(fitAvgs)))
return sumFitAvgAdj, fitAvgsAdj
}(func(company []*Species) (sum float64, fits []float64) {
fits = make([]float64, len(company))
for i, team := range company {
fitAvg, err := team.AverageFitness()
if err != nil {
// log.Println("fatal:", err) // debug //
panic(err)
}
sum += fitAvg
fits[i] = fitAvg
}
return sum, fits
}(univ.Classes))
}
// AdjTopFitnessesOfSpecies evaluates adjusted top-fitnesses of all competing species.
// Adjusted here only means that those values are made non-negative. The order for iSpecies is preserved.
func (univ *Universe) AdjTopFitnessesOfSpecies() (sumFitTopAdj float64, fitTopsAdj []float64) {
return func(sumFitTop float64, fitTops []float64) (
sumFitTopAdj float64, fitTopsAdj []float64,
) {
fitTopsAdj = make([]float64, len(fitTops))
minFitTop := 0.0 // won't be bigger than 0
for i, fitTop := range fitTops {
fitTopsAdj[i] = fitTop // copy
minFitTop = math.Min(minFitTop, fitTop) // find min
}
offset := math.Abs(minFitTop)
for i, fitTop := range fitTopsAdj {
fitTopsAdj[i] = fitTop + offset // adjust
if fitTopsAdj[i] < 0 {
panic(fmt.Sprint(
fmt.Sprintln("terrible miscalculation:"),
fmt.Sprintln(
"sumFitTop ->", sumFitTop,
"fitTops ->", fitTops,
),
fmt.Sprintln(
"sumFitTopAdj ->", sumFitTopAdj,
"fitTopsAdj ->", fitTopsAdj,
),
))
}
}
sumFitTopAdj = sumFitTop + (offset * float64(len(fitTops)))
return sumFitTopAdj, fitTopsAdj
}(func(company []*Species) (sum float64, fits []float64) {
fits = make([]float64, len(company))
for i, team := range company {
fitTop := team.TopFitness
sum += fitTop
fits[i] = fitTop
}
return sum, fits
}(univ.Classes))
}
// SortSpecies of this universe.
func (univ *Universe) SortSpecies() bool {
sort.Slice(univ.Classes, func(i, j int) bool {
return univ.Classes[i].Niche() < univ.Classes[j].Niche()
})
return false
}
// HasSpecies in this universe.
// This takes linear time O(N) searching for a species in a list.
func (univ *Universe) HasSpecies(s *Species) bool {
if s == nil {
return false
}
for _, species := range univ.Classes {
if species == s {
return true
}
}
return false
}
// AddSpecies to this universe.
// The parameter should not be nil.
func (univ *Universe) AddSpecies(s *Species) error {
if s == nil {
return errors.New("nil species")
// s = NewSpecies(nil) // empty species // This creates a default object (empty species) when the parameter is nil.
}
univ.Classes = append(univ.Classes, s)
return nil
}
// RemoveClasses of this universe. It means extinction of species.
// This unspeciates all creatures of those species taking only constant time for each,
// and returns them (unspeciated) so they can be killed or respeciated or recycled however etc.
func (univ *Universe) RemoveClasses(indicesSpecies ...int) (
removed []*Species, segregated [][]*Organism, err error,
) {
// Just in case...
unique.Slice(&indicesSpecies, func(i, j int) bool { return indicesSpecies[i] < indicesSpecies[j] })
for _, argIndex := range indicesSpecies {
if argIndex < 0 || argIndex >= len(univ.Classes) {
return nil, nil, errors.New("index out of bound: " + strconv.Itoa(argIndex))
}
}
// After those arguments are set...
nRemoved := len(indicesSpecies)
copyClasses := make([]*Species, len(univ.Classes)) // tmp copy
if nCopied := copy(copyClasses, univ.Classes); nCopied != len(univ.Classes) {
return nil, nil, errors.New("failed to copy univ species")
}
for i := 0; i < nRemoved; i++ { // move them to front
j := indicesSpecies[i]
copyClasses[i], copyClasses[j] = copyClasses[j], copyClasses[i]
}
removed = make([]*Species, nRemoved)
segregated = make([][]*Organism, nRemoved)
for iSpecies, s := range copyClasses[:nRemoved] {
removed[iSpecies] = s
segregated[iSpecies] = s.Cull(1.00)
}
// Notice that univ.Classes is updated in this function without mutex or any sync tool.
univ.Classes = copyClasses[nRemoved:]
univ.SortSpecies()
return removed, segregated, nil
}
// SpeciesRandom returns a random species in this universe.
func (univ *Universe) SpeciesRandom() *Species {
if len(univ.Classes) < 1 {
return nil
}
return univ.Classes[rand.Intn(len(univ.Classes))]
}
// TellSpecies of an organism without touching it. This returns the closest species of that organism.
// If it turns out that there are two or more species of the same compatibility distance available
// in this universe, the return is the more earlier in the list in which species are sorted.
// This function returns error if the organism's genome is told corrupted.
func (univ *Universe) TellSpecies(o /*read-only*/ *Organism) (ret *Species, err error) {
min := math.MaxFloat64
for _, species := range univ.Classes {
if president := species.Champion(); president != nil {
if isFertile, incompatibility, err := univ.Compatible(o.GenoType(), president.GenoType()); err != nil {
return nil, err
} else if isFertile && incompatibility < min {
ret = species
min = incompatibility
} else {
// Noop. Do nothing.
}
continue
}
}
return ret, nil
}
// Speciate an unspeciated organism.
// This relates both organism and species.
func (univ *Universe) Speciate(o *Organism) (err error) {
if o == nil {
return errors.New("nil organism")
}
if o.IsSpeciated() {
return errors.New("you have to unspeciate this organism")
}
if o.Breed, err = univ.TellSpecies(o); err != nil {
return err
}
if !univ.HasSpecies(o.Species()) {
newClass := univ.NewSpeciesBasic()
if err = univ.AddSpecies(newClass); err != nil {
return err
}
newClass.AddOrganisms(o)
} else {
o.Species().AddOrganisms(o)
}
return nil
}
// AddOrganism to this universe. The organism does not get speciated here.
// The parameter should not be nil and it should be new in this universe.
func (univ *Universe) AddOrganism(o *Organism) (err error) {
if o == nil {
// o = univ.NewOrganismBasic() // This; AddOrganism, creates a default organism when the parameter is nil.
return errors.New("nil organism")
}
if _, isAlreadyThere := univ.Livings[o]; isAlreadyThere {
return errors.New("the organism already exists")
}
univ.Livings[o] = struct{}{}
return nil
}
// RemoveOrganism of this universe. This unspeciates the organism.
// The time complexity is O(1), except for the case the organism is speciated,
// for which this operation takes linear time O(N) searching it as an element in a species in order to unspeciate it.
func (univ *Universe) RemoveOrganism(o *Organism) error {
if o == nil {
return errors.New("cannot remove nil organism")
}
if o.IsSpeciated() {
if err := o.Unspeciate(); err != nil {
return err
}
}
delete(univ.Livings, o)
return nil
}
// ----------------------------------------------------------------------------
// Species
// Niche is an identifier and a historical marker for species.
// This is unique for each of species under a NEAT context.
// And the bigger the number is, the later the Niche is.
// It basically is considered an integer starting from zero
// but could be used in other way depending on the context.
type Niche int64
// String callback of Niche.
func (niche /* not ptr */ Niche) String() string {
return "<" + strconv.Itoa(int(niche)) + ">"
}
// Species is a classification of living organisms in our simulated world.
type Species struct {
ID Niche // Identifier of the habitat of this species.
Livings []*Organism // Creatures of this species.
// What tracks for the N how long this species has been stagnant.
// This species has been stagnant over N generations in a row.
// It's named stagnancy instead of stagnation for it sounding not like an economics vocabulary.
Stagnancy int
// We keep track of this so we can evaluate Stagnancy of this Species in each epoch.
TopFitness float64
}
// NewSpecies is a constructor.
// Arg livings can be nil to set by default.
func NewSpecies(id Niche, livings []*Organism) *Species {
if livings == nil {
livings = []*Organism{}
}
return &Species{
ID: id,
Livings: livings,
Stagnancy: 0,
TopFitness: 0,
}
}
// String callback of Species.
func (s *Species) String() string {
return fmt.Sprint(
"{",
"Species ", s.ID, " of ", len(s.Livings), " Organisms", "/",
"Stagnancy:", s.Stagnancy, "/",
"TopFitness:", s.TopFitness,
"}",
)
}
// Niche is a getter returning this species' identifier
// that could also be considered the habitat of creatures of this species.
func (s *Species) Niche() Niche {
return s.ID
}
// Size returns the number of organisms in this species.
// Population of this species.
func (s *Species) Size() int {
return len(s.Livings)
}
// Sort sorts organisms of this species by their fitness, in descending order.
func (s *Species) Sort() {
sort.Slice(s.Livings, func(i, j int) bool {
return s.Livings[i].Fitness > s.Livings[j].Fitness
})
}
// AddOrganisms (organism) in this Species.
// This updates the organism(s) added as well.
func (s *Species) AddOrganisms(orgs ...*Organism) {
s.Livings = append(s.Livings, orgs...)
for _, o := range orgs {
if o.Species() != s {
o.Breed = s
}
}
s.Sort()
}
// Cull off bottom `percentage` percent of organisms.
// The range of `percentage` is [0, 1].
// This function returns all organisms culled off of this zoo.
func (s *Species) Cull(percentage float64) (segregated []*Organism) {
s.Sort()
percentageFromTop := 1 - percentage
percentileFrom := int(math.Ceil(float64(s.Size()) * percentageFromTop))
segregated = s.Livings[percentileFrom:] // Panics if s[len(s)+1:] but s[len(s):] will give an empty slice without any error.
s.Livings = s.Livings[:percentileFrom]
for _, o := range segregated {
o.Unbreed()
}
return segregated
}
// CullToSinglePopulation culls off all creatures but the champion of this species.
// The species must consist of one population at least. Otherwise this function panics.
func (s *Species) CullToSinglePopulation() (segregated []*Organism) {
s.Sort()
segregated = s.Livings[1:]
s.Livings = s.Livings[:1]
for _, o := range segregated {
o.Unbreed()
}
return segregated
}
// Champion of this species.
// Returns nil if there is no organism in this species.
func (s *Species) Champion() *Organism {
s.Sort()
if len(s.Livings) > 0 {
return s.Livings[0]
}
return nil
}
// EvaluateGeneration checks and updates the stagnancy of this species.
// This returns the updated stagnancy, and the top fitness of this species evaluated.
// An error is returned when the fitness of the champion is yet evaluated.
func (s *Species) EvaluateGeneration() (stagnancy int, topFitness float64, err error) {
fitness, err := s.Champion().GetFitness()
if err != nil {
return s.Stagnancy, topFitness, err
}
if fitness > s.TopFitness {
s.TopFitness = fitness
s.Stagnancy = 0
return s.Stagnancy, s.TopFitness, nil
}
s.Stagnancy++
return s.Stagnancy, s.TopFitness, nil
}
// Unspeciate kicks out only a single organism of this species.
// Consider using (*Species).Cull() instead if you want a massacre.
// This operation takes O(N) time searching a given organism.
// The return is about whether it was successful or not.
func (s *Species) Unspeciate(o *Organism) (expelled bool) {
for i, found := range s.Livings {
if found == o {
found.Unbreed()
s.Livings = append(s.Livings[:i], s.Livings[i+1:]...)
return true
}
}
return false
}
// AverageFitness of this species.
// This returns an error when it turns out that there is at least one
// organism with its fitness unevaluated in this species.
func (s *Species) AverageFitness() (averageFitness float64, err error) {
sum := 0.0
for _, organism := range s.Livings {
fitness, err := organism.GetFitness()
if err != nil {
return averageFitness,
errors.New("unable to calculate the average fitness of this species: " + err.Error())
}
sum += fitness
}
return sum / float64(len(s.Livings)), nil
}
// OrganismRandom returns an organism chosen randomly of this species.
// The return won't and shouldn't be nil.
func (s *Species) OrganismRandom() *Organism {
return s.Livings[rand.Intn(len(s.Livings))]
}
// RandomOrganisms returns N organisms randomly chosen from this species.
// The given parameter N must be less than or equal to the number of
// organisms of this species. Otherwise this function returns nil.
func (s *Species) RandomOrganisms(n int) (creatures []*Organism) {
if 0 > n || n > len(s.Livings) {
return nil
}
creatures = make([]*Organism, n)
for i, v := range rand.Perm(len(s.Livings))[:n] {
creatures[i] = s.Livings[v]
}
return creatures
}
// ----------------------------------------------------------------------------
// Organism
// Organism is an individual living thing born to compete with others.
type Organism struct {
Genotype *Genome
Phenotype []*NeuralNetwork
Breed *Species
Fitness float64 // private. Do not access this directly without a public method.
IsMeasured bool // private. Do not access this directly without a public method.
}
// String callback of Organism.
func (o *Organism) String() string {
return fmt.Sprint(
"{",
"Organism", "/",
// "Genotype:", o.Genotype, "/",
// "Phenotype:", o.Phenotype, "/",
"Breed:", o.Breed, "/",
"Fitness:", o.Fitness, "/",
"IsMeasured:", o.IsMeasured,
"}",
)
}
// IsFitnessMeasured returns true iff an organism is already measured, no not for being measured.
// Returns false when this organism needs its fitness evaluated.
func (o *Organism) IsFitnessMeasured() bool {
return o.IsMeasured
}
// UpdateFitness is a setter with a positive side effect.
func (o *Organism) UpdateFitness(fitness float64) {
o.Fitness = fitness
o.IsMeasured = true
}
// GetFitness is a getter.
// Returns error when the fitness of this organism is yet measured.
func (o *Organism) GetFitness() (fitness float64, err error) {
if o.IsFitnessMeasured() {
return o.Fitness, nil
}
return o.Fitness, errors.New("fitness of this organism needs to be measured")
}
// GenoType returns the genotype of this organism.
// Uppercase T of this function name is to avoid naming conflicts. :P
func (o *Organism) GenoType() *Genome {
return o.Genotype
}
// PhenoType returns the phenotype of this organism.
// Uppercase T of this function name is to avoid naming conflicts. :P
func (o *Organism) PhenoType() []*NeuralNetwork {
return o.Phenotype
}
// Species returns the breed of this organism.
// Returns nil when this creature is yet classified.
func (o *Organism) Species() *Species {
return o.Breed
}
// IsSpeciated of this organism.
func (o *Organism) IsSpeciated() bool {
return o.Breed != nil
}
// Unbreed this organism. This method is different from Unspeciate().
// - Unspeciate() **calls** the species the organism is in to dislink(unassociate;unrelate) the two. `o.Species().Unspeciate(o)`
// - Unbreed() is **called** by the species the organism is in to safely access the struct's field. `o.Breed = nil`
// This function is a callback. Generally you don't want to use this.
func (o *Organism) Unbreed() {
o.Breed = nil
}
// Unspeciate this single organism. This method is different from Unbreed().
// - Unspeciate() **calls** the species the organism is in to dislink(unassociate;unrelate) the two. `o.Species().Unspeciate(o)`
// - Unbreed() is **called** by the species the organism is in to safely access the struct's field. `o.Breed = nil`
// This function results in calling Unbreed() and so is preferred over the other most of the times.
// Also this operation may take up to O(N) time searching a given organism.
// Consider using (*Species).Cull() or (*Universe).RemoveSpecies() instead if you just want a massacre.
func (o *Organism) Unspeciate() error {
if o.Species() == nil {
return errors.New("cannot unspeciate an unspeciated organism")
}
if !o.Species().Unspeciate(o) {
return errors.New("the species couldn't kick this guy out")
}
return nil
}
// Copy is a constructor. Notice: this method is no use in GA.
// The returned daughter is a genetic copy of this organism unspeciated, unevaluated, and unmeasured.
// This does not mutate the organism so you might want to use (*Genome).Copy() instead.
func (o *Organism) Copy() (daughter *Organism, err error) {
newGenome, err := o.GenoType().Copy()
if err != nil {
return nil, err
}
return newGenome.NewOrganismSimple()
}
// OrganismMarshal is JSON export of Organism.
type OrganismMarshal struct {
Genotype *Genome
Fitness float64
IsMeasured bool
}
// MarshalJSON of json.Marshaler interface.
func (o *Organism) MarshalJSON() ([]byte, error) {
return json.Marshal(&OrganismMarshal{
Genotype: o.Genotype,
Fitness: o.Fitness,
IsMeasured: o.IsMeasured,
})
}
// UnmarshalJSON of json.Unmarshaler interface.
// The organism is unspeciated.
func (o *Organism) UnmarshalJSON(jsonOrganismMarshal []byte) (err error) {
om := &OrganismMarshal{}
if err = json.Unmarshal(jsonOrganismMarshal, om); err != nil {
return err
}
//
phenome, err := om.Genotype.NewPhenotype()
if err != nil {
return err
}
*o = Organism{
Genotype: om.Genotype,
Phenotype: phenome,
Breed: nil,
Fitness: om.Fitness,
IsMeasured: om.IsMeasured,
}
return nil
}
// ----------------------------------------------------------------------------
// Genome
// Genome represents a genotype of a single living organism, from which, in general meaning,
// we can derive a neural network as a phenotype.
// This Genome designed here in specific can encode of multiple
// neural networks taking a separate set of inputs for each.
type Genome struct { // This would be a factory for an organism.
Chromes []*Chromosome // Each of these chromosomes is for a network.
}
// NewGenome is a constructor.
// Arg chromes can be nil to set by default.
func NewGenome(chromes []*Chromosome) (g *Genome, err error) {
g = &Genome{}
//
if chromes == nil {
chrome, err := NewChromosome(nil, nil, nil)
if err != nil {
return nil, err
}
g.Chromes = []*Chromosome{chrome}
return g, nil
}
//
if len(chromes) < 1 {
return nil, errors.New("the number of chromosome(s) should be at least one or more")
}
//
g.Chromes = make([]*Chromosome, len(chromes))
for iChrome, chromosome := range chromes {
if chromosome == nil {
g.Chromes[iChrome], err = NewChromosome(nil, nil, nil)
if err != nil {
return nil, err
}
continue
}
g.Chromes[iChrome] = chromosome
}
//
return g, nil
}
// String of Genome.
func (g *Genome) String() string {
const left, right = 0, 1
switch len(g.Chromes) {
case 0:
return "{Genome empty}"
case 1:
return "{Genome of " + g.Chromes[left].String() + "}"
case 2:
return "{Genome of Left: " + g.Chromes[left].String() + " and Right: " + g.Chromes[right].String() + "}"
default:
return "{Genome of " + strconv.Itoa(len(g.Chromes)) + " Chromosomes with Left: " + g.Chromes[left].String() + " and Right: " + g.Chromes[right].String() + "}"
}
// return "{Genome unknown}" // unreachable
}
// Length returns the number of chromosomes this genome consist of.
func (g *Genome) Length() int {
return len(g.Chromes)
}
// Chromosomes returns a list of all chromosomes of this genome.
// The slice returned is an original reference not a copy.
func (g *Genome) Chromosomes() []*Chromosome {
return g.Chromes
}
// IsValid tests this genome.
func (g *Genome) IsValid() bool {
if g.Chromosomes() == nil {
return false
}
for _, chromosome := range g.Chromosomes() {
if chromosome == nil {
return false
}
}
return true
}
// NodeGenesByUUID returns a map of all node genes available in this Genome.
// The parameter can be nil.
func (g *Genome) NodeGenesByUUID(fillMe map[uuid.UUID]*NodeGene) (filledOut map[uuid.UUID]*NodeGene) {
if fillMe == nil {
filledOut = map[uuid.UUID]*NodeGene{}
} else {
filledOut = fillMe
}
for _, chromosome := range g.Chromosomes() {
if chromosome != nil {
filledOut = chromosome.NodeGenesByUUID(filledOut)
}
}
return filledOut
}
// ForEachMatchingChromosome of two genomes.
// Use this method to line up chromosomes of a genome beside one another.
// Genetic recombination may happen upon each genetic analysis of homologous chromosomes.
// The callback provides parental chromosomes that pair up with each other.
// This function returns error if each genome has a distinct number of chromosomes.
// Mandatory: All arguments are required and cannot be nil parameter.
func (g *Genome) ForEachMatchingChromosome(
matchedPartner *Genome,
forEachGeneticAnalysis func(
i int, // iChrome
chrome1 *Chromosome,
linkGenes1 map[Innovation]*LinkGene,
nLinkGenes1 int, historical1 Innovation,
chrome2 *Chromosome,
linkGenes2 map[Innovation]*LinkGene,
nLinkGenes2 int, historical2 Innovation,
) error,
) error {
// mate g1 and g2
if g1, g2 := g, matchedPartner; g1 != nil && g2 != nil && forEachGeneticAnalysis != nil {
if chromes1, chromes2 := g1.Chromosomes(), g2.Chromosomes(); g1.Length() == g2.Length() {
for i, chrome1 := range chromes1 { // for each matching chromosomes
chrome2 := chromes2[i] // depicted chrome1 and chrome2
links1, size1, innov1 := chrome1.Genetics()
links2, size2, innov2 := chrome2.Genetics()
if err := forEachGeneticAnalysis(
i,
chrome1, links1, size1, innov1,
chrome2, links2, size2, innov2,
); err != nil {
return err
}
}
} else {
return errors.New("bad genome: the number of chromosome(s) does not match")
}
} else {
return errors.New("nil parameter")
}
return nil
}
// NewPhenotype from this genotype.
func (g *Genome) NewPhenotype() (phenome []*NeuralNetwork, err error) {
phenome = make([]*NeuralNetwork, g.Length())
for i, chrome := range g.Chromosomes() {
phenome[i], err = chrome.NewNeuralNetwork()
if err != nil {
return nil, err
}
}
return phenome, nil
}
// NewOrganism is born. A constructor.
// The returned organism inherits the reference of this genome.
func (g *Genome) NewOrganism(breed *Species, fitness float64, isMeasured bool) (o *Organism, err error) {
phenome, err := g.NewPhenotype()
if err != nil {
return nil, err
}
return &Organism{
Genotype: g,
Phenotype: phenome,
Breed: breed,
Fitness: fitness,
IsMeasured: isMeasured,
}, nil
}
// NewOrganismSimple is a constructor.
// The return is a default organism unmeasured and nil breed.
func (g *Genome) NewOrganismSimple() (o *Organism, err error) {
return g.NewOrganism(nil, 0, false)
}
// Copy is a constructor.
func (g *Genome) Copy() (ret *Genome, err error) {
newChromes := make([]*Chromosome, len(g.Chromosomes()))
for i, chrome := range g.Chromosomes() {
newChromes[i], err = chrome.Copy()
if err != nil {
return nil, err
}
}
return NewGenome(newChromes)
}
// ----------------------------------------------------------------------------
// Chromosome
// Chromosome is a part of Genome the struct type.
// This encodes an NN and thus is a factory of a neural network.
// Chromosome may implement the genetic scheme of NEAT; a linear representation of
// network connectivity where its sequence alignment is significant.
type Chromosome struct {
LinkGenes []*LinkGene // Hidden nodes are inferred out of this. Sort by innov.
IONodeGenes []*NodeGene `json:"-"` // This does not contain hidden nodes. Inputs and Outputs only.
DiceGene *ChanceGene
}
// NewChromosome is a constructor.
// Arg links, nodes, and dice can be nil to set them empty by default.
//
// Parameters
// - `links`: Link-genes. The innovation number must not overlap with any. A link may be connected to the Input, Hidden, or Output nodes.
// - `nodes`: Node-genes. Only Input/Output nodes are allowed. Hidden nodes etc. are forbidden.
// - `dice`: A set of mutation probabilities.
//
func NewChromosome(links []*LinkGene, ioNodes []*NodeGene, dice *ChanceGene) (chrome *Chromosome, err error) {
chrome = &Chromosome{}
// 1. LinkGenes
if links == nil { // new empty
chrome.LinkGenes = []*LinkGene{}
} else { // or copy
chrome.LinkGenes = make([]*LinkGene, len(links))
if len(chrome.LinkGenes) != copy(chrome.LinkGenes, links) {
return nil, errors.New("failed to copy link-genes while constructing chromosome")
}
}
// 2. NodeGenes
if ioNodes == nil { // new empty
chrome.IONodeGenes = []*NodeGene{}
} else { // or copy
chrome.IONodeGenes = func(nodes []*NodeGene) (filtered []*NodeGene) {
filtered = []*NodeGene{} // nodeGenesWithoutHidden
for _, nodeGene := range nodes {
switch nodeGene.TypeInInt() {
case InputNodeBias:
fallthrough
case InputNodeNotBias:
fallthrough
case OutputNode:
filtered = append(filtered, nodeGene)
}
}
return filtered
}(ioNodes)
} // NodeGenes are copied only for safety.
// 3. DiceGene
if dice == nil { // new empty
chrome.DiceGene = NewChanceGeneDisabled()
} else { // or copy
chrome.DiceGene = func(dice *ChanceGene) (copy *ChanceGene) {
v := *dice
return &v
}(dice)
}
// Finalize
if !chrome.IsValid() {
return nil, errors.New("GIGO: bad chromosome is being constructed")
}
chrome.Sort()
return chrome, nil
}
// IsValid tests this chromosome.
func (chrome *Chromosome) IsValid() bool {
if !chrome.IsValidForLinks() || !chrome.IsValidForNodes() || !chrome.IsValidForDice() {
return false
}
return true
}
// IsValidForLinks tests links of this chromosome.
func (chrome *Chromosome) IsValidForLinks() bool {
if chrome.LinkGenes == nil {
return false
}
{ // check dup innovations in LinkGenes
innovations := map[int]struct{}{}
for _, linkGene := range chrome.LinkGenes {
if _, isDup := innovations[int(linkGene.Topo.Innov)]; isDup {
return false
}
innovations[int(linkGene.Topo.Innov)] = struct{}{}
}
}
return true
}
// IsValidForNodes tests nodes of this chromosome.
func (chrome *Chromosome) IsValidForNodes() bool {
if chrome.IONodeGenes == nil {
return false
}
{ // check NodeGenes
if !(len(chrome.IONodeGenes) > 0) {
return false
}
hasInputNode, hasOutputNode := false, false
for _, nodeGene := range chrome.IONodeGenes {
if (nodeGene.TypeInInt() != InputNodeBias &&
nodeGene.TypeInInt() != InputNodeNotBias &&
nodeGene.TypeInInt() != OutputNode) ||
!nodeGene.IsValid() {
return false
}
{ // check has
if !hasInputNode && nodeGene.TypeInInt() == InputNodeNotBias {
hasInputNode = true
}
if !hasOutputNode && nodeGene.TypeInInt() == OutputNode {
hasOutputNode = true
}
}
}
if !hasInputNode && !hasOutputNode {
return false
}
}
return true
}
// IsValidForDice tests the dice of this chromosome.
func (chrome *Chromosome) IsValidForDice() bool {
if chrome.DiceGene == nil {
return false
}
return true
}
// Sort of Chromosome sorts slice(s) of a chromosome itself.
// For nodes, the order is important for inputs & outputs.
// The link genes are sorted in ascending order of the innovation number.
func (chrome *Chromosome) Sort() {
// Sort LinkGenes.
sort.Slice(chrome.LinkGenes, func(i, j int) bool {
return chrome.LinkGenes[i].Topo.Innov < chrome.LinkGenes[j].Topo.Innov
})
// Sort NodeGenes.
sort.Slice(chrome.IONodeGenes, func(i, j int) bool {
if chrome.IONodeGenes[i].TypeInInt() != chrome.IONodeGenes[j].TypeInInt() {
return chrome.IONodeGenes[i].TypeInInt() < chrome.IONodeGenes[j].Type
}
return chrome.IONodeGenes[i].Idx < chrome.IONodeGenes[j].Idx
})
}
// AddLinkGenes (gene) to this Chromosome.
// This does not verify the added gene(s) for its efficiency.
// You could test it with (*Chromosome).IsValidLinks().
func (chrome *Chromosome) AddLinkGenes(pleasePassInDeepCopiesHere ...*LinkGene) {
chrome.LinkGenes = append(chrome.LinkGenes, pleasePassInDeepCopiesHere...)
// chrome.Sort() // Lazy. Not necessary I think.
}
// Bump re-enables the oldest disabled structure a single gene represents.
// (It's like bringing up a post in message board terms.) Time complexity is O(N).
// If there ain't one disabled then this operation does nothing other than consuming some time searching for it.
func (chrome *Chromosome) Bump() {
chrome.Sort()
for _, linkGene := range chrome.LinkGenes {
if !linkGene.Enabled {
linkGene.Enabled = true
return
}
}
}
// String callback of Chromosome.
func (chrome *Chromosome) String() string {
const tooBig = 5
if chrome.SizeBuilding() < tooBig {
return fmt.Sprint(
"{Chromosome of ",
len(chrome.IONodeGenes), " NodeGenes", "/",
"LinkGenes:", chrome.LinkGenes, "/",
"DiceGene:", chrome.DiceGene, "}")
}
return fmt.Sprint(
"{Chromosome of ",
len(chrome.IONodeGenes), " NodeGenes and ",
chrome.SizeBuilding(), " LinkGenes and ",
chrome.DiceGene, "}")
}
// IsDiceEnabled tells if this chromosome has its own dynamic mutation rate other than the fixed probability.
func (chrome *Chromosome) IsDiceEnabled() bool {
// DiceGene is not nil-able and it shouldn't. This function panics when it is nil.
return chrome.DiceGene.Enabled
}
// NodeGenesByUUID returns a map of all node genes available in this Chromosome.
// The parameter can be nil.
func (chrome *Chromosome) NodeGenesByUUID(fillMe map[uuid.UUID]*NodeGene) (filledOut map[uuid.UUID]*NodeGene) {
if fillMe == nil {
filledOut = map[uuid.UUID]*NodeGene{}
} else {
filledOut = fillMe
}
for _, linkGene := range chrome.LinkGenes {
if from := linkGene.Topo.From; from != nil {
filledOut[from.UUID] = from
}
if to := linkGene.Topo.To; to != nil {
filledOut[to.UUID] = to
}
}
return filledOut
}
// Genetics provides the essentials of this chromosome's genetics; all the link-gene stuff that is.
//
// Returns:
// - `linkGenesByInnovation`: A map of all link genes available in this Chromosome.
// - `sizeLinkGenes`: The normalize size factor for the compatibility distance measure. Refer to (*Chromosome).SizeBuilding().
// - `innovationLatest`: The biggest (latest) innovation number this chromosome has. This is a historical bound used to divide unmatched genes into disjoint and excess.
//
func (chrome *Chromosome) Genetics() (linkGenesByInnovation map[Innovation]*LinkGene, sizeLinkGenes int, innovationLatest Innovation) {
linkGenesByInnovation = map[Innovation]*LinkGene{}
for _, linkGene := range chrome.LinkGenes {
linkGenesByInnovation[linkGene.Topo.Innov] = linkGene
}
sizeLinkGenes = chrome.SizeBuilding()
if sizeLinkGenes > 0 {
innovationLatest = chrome.LinkGenes[len(chrome.LinkGenes)-1].Topo.Innov
}
return
}
// SizeBuilding returns the number of all link genes this genetic encoder has.
// This allows us to measure the scale of structural innovations a topology gets made up with.
func (chrome *Chromosome) SizeBuilding() (sizeTopologicalInnovation int) {
return len(chrome.LinkGenes)
}
// NewNeuralNetwork (verbed) from a Chromosome.
func (chrome *Chromosome) NewNeuralNetwork() (network *NeuralNetwork, err error) {
// What we create and return here.
network = &NeuralNetwork{
DirectedGraph: simple.NewDirectedGraph(),
InputNodes: []*NeuralNode{},
OutputNodes: []*NeuralNode{},
NodeByGene: map[*NodeGene]*NeuralNode{},
EdgeByGene: map[*LinkGene]*NeuralEdge{},
NumBiasNodes: 0,
NumInputNodes: 0,
NumHiddenNodes: 0,
NumOutputNodes: 0,
NumLayers: 0,
IsEvaluatedWell: false,
}
// Not needed since we've probably already sorted this,
// but just to make sure...
chrome.Sort()
// Decode NodeGene(s) of a Chromosome.
for _, verticeGene := range chrome.IONodeGenes {
network.AddNewNeuralNode(verticeGene)
}
// Decode LinkGene(s) of a Chromosome.
for _, edgeGene := range chrome.LinkGenes {
if !edgeGene.Enabled {
continue
}
// I play Pot of Greed.
nodeFrom := network.FindNodeByGene(edgeGene.Topo.From)
if nodeFrom == nil {
nodeFrom = network.AddNewNeuralNode(edgeGene.Topo.From)
}
nodeTo := network.FindNodeByGene(edgeGene.Topo.To)
if nodeTo == nil {
nodeTo = network.AddNewNeuralNode(edgeGene.Topo.To)
}
// This allows me to draw 2 cards from my deck and add them to my hand.
err := network.AddNewNeuralEdge(nodeFrom, nodeTo, edgeGene)
if err != nil {
return nil, errors.New("bad chromosome: " + err.Error())
}
}
// Validate this network's nodes.
{ // Validate forward.
visitedForward := map[*NeuralNode]struct{}{}
for _, inputNode := range network.GetInputNodes() {
// Validates nodes in this network floodfilling forward in DFS order.
network.TraverseForward(inputNode, func(onNodeVisit *NeuralNode) {
if !onNodeVisit.IsValidForward {
onNodeVisit.IsValidForward = true
}
}, visitedForward)
}
}
{ // Validate backward.
visitedBackward := map[*NeuralNode]struct{}{}
for _, outputNode := range network.GetOutputNodes() {
// Validates nodes in this network floodfilling backward in DFS order.
network.TraverseBackward(outputNode, func(onNodeVisit *NeuralNode) {
if !onNodeVisit.IsValidBackward {
onNodeVisit.IsValidBackward = true
}
}, visitedBackward)
}
}
{ // Then cull off disconnected (invalid) nodes, as well as any edges attached to it.
nodes, err := network.Sort()