diff --git a/README.md b/README.md index 67fecf7..5a0951f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![version](https://img.shields.io/github/v/tag/yaricom/goNEAT.svg?sort=semver)](https://github.com/yaricom/goNEAT/releases/latest) [![Build Status](https://travis-ci.org/yaricom/goNEAT.svg?branch=master)](https://travis-ci.org/yaricom/goNEAT) [![GoDoc](https://godoc.org/github.com/yaricom/goNEAT/neat?status.svg)](https://godoc.org/github.com/yaricom/goNEAT/neat) -[![Go version](https://img.shields.io/badge/go-1.17-blue.svg)](https://github.com/moovweb/gvm) +[![Go version](https://img.shields.io/badge/go-1.18-blue.svg)](https://github.com/moovweb/gvm) [![license](https://img.shields.io/github/license/yaricom/goNEAT.svg)](https://github.com/yaricom/goNEAT/blob/master/LICENSE) [![yaricom/goNEAT](https://tokei.rs/b1/github/yaricom/goNEAT?category=lines)](https://github.com/yaricom/goNEAT) [![Sourcegraph](https://sourcegraph.com/github.com/yaricom/goNEAT/-/badge.svg)](https://sourcegraph.com/github.com/yaricom/goNEAT?badge) @@ -26,7 +26,7 @@ and their interconnections). ## Minimum requirements | Requirement | Notes | |-------------|------------------| -| Go version | Go1.17 or higher | +| Go version | Go1.18 or higher | ## Releases diff --git a/examples/performance/performance_test.go b/examples/performance/performance_test.go new file mode 100644 index 0000000..ea95207 --- /dev/null +++ b/examples/performance/performance_test.go @@ -0,0 +1,155 @@ +package performance + +import ( + "bytes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yaricom/goNEAT/v4/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat/network" + "testing" +) + +const genomeStr = `genomestart 0 +trait 1 0.1 0 0 0 0 0 0 0 +trait 2 0.2 0 0 0 0 0 0 0 +trait 3 0.3 0 0 0 0 0 0 0 +node 1 1 1 1 SigmoidSteepenedActivation +node 2 2 1 1 SigmoidSteepenedActivation +node 3 1 1 1 SigmoidSteepenedActivation +node 4 1 1 1 SigmoidSteepenedActivation +node 5 1 1 1 SigmoidSteepenedActivation +node 6 3 1 1 SigmoidSteepenedActivation +node 7 1 1 3 SigmoidSteepenedActivation +node 8 1 0 2 SigmoidSteepenedActivation +node 30 3 0 0 SigmoidSteepenedActivation +node 71 1 0 0 SigmoidSteepenedActivation +node 266 1 0 0 SigmoidSteepenedActivation +node 1685 1 0 0 SigmoidSteepenedActivation +node 1811 1 0 0 SigmoidSteepenedActivation +node 3436 1 0 0 SigmoidSteepenedActivation +node 3627 1 0 0 SigmoidSteepenedActivation +node 4077 1 0 0 SigmoidSteepenedActivation +node 6640 2 0 0 SigmoidSteepenedActivation +gene 1 1 8 -4.08386891981225 false 1 -4.08386891981225 true +gene 2 2 8 -2.1690052056140163 false 2 -2.1690052056140163 true +gene 3 3 8 -10.699778103607123 false 3 -10.699778103607123 true +gene 1 4 8 -9.342647981011918 false 4 -9.342647981011918 true +gene 2 5 8 6.996472488106343 false 5 6.996472488106343 true +gene 2 6 8 2.2286271196270824 false 6 2.2286271196270824 true +gene 2 7 8 5.305934250174099 false 7 5.305934250174099 true +gene 1 1 30 5.327875888469306 false 121 5.327875888469306 true +gene 1 30 8 5.684209149585454 false 122 5.684209149585454 true +gene 3 3 30 0.9475646340769113 false 271 0.9475646340769113 true +gene 3 5 30 1.2529446872848236 false 439 1.2529446872848236 true +gene 1 1 71 2.0097007140389365 false 583 2.0097007140389365 true +gene 1 71 8 -2.6692540269378506 false 584 -2.6692540269378506 true +gene 1 3 71 -4.073066558696308 false 834 -4.073066558696308 true +gene 3 2 71 2.0095928118285507 false 1223 2.0095928118285507 true +gene 3 2 266 3.4187907757610367 false 1627 3.4187907757610367 true +gene 3 266 71 -3.2830938391621856 false 1628 -3.2830938391621856 true +gene 1 6 71 -3.463234141766563 false 2528 -3.463234141766563 true +gene 3 3 266 -2.281968137235494 false 3056 -2.281968137235494 true +gene 2 4 266 3.497559408686352 false 4212 3.497559408686352 true +gene 3 30 71 -2.8430156534468214 false 4678 -2.8430156534468214 true +gene 1 7 71 -3.45518023982917 false 6526 -3.45518023982917 true +gene 1 71 1685 2.338576667094656 false 7008 2.338576667094656 true +gene 1 1685 8 -0.2433441365216774 false 7009 -0.2433441365216774 true +gene 1 30 1811 -1.1814519616174946 false 7485 -1.1814519616174946 true +gene 1 1811 8 -0.9537417818325291 false 7486 -0.9537417818325291 true +gene 2 7 266 2.748821688559035 false 9914 2.748821688559035 true +gene 2 1811 266 3.908454872776794 false 10435 3.908454872776794 true +gene 3 6 266 7.145812334508661 false 12872 7.145812334508661 true +gene 1 71 3436 -0.09282935033826101 false 13446 -0.09282935033826101 true +gene 1 3436 1685 2.021217174697969 false 13447 2.021217174697969 true +gene 2 1811 3627 -3.0538854223916507 false 14092 -3.0538854223916507 true +gene 2 3627 266 -3.0405574668478694 false 14093 -3.0405574668478694 true +gene 1 71 4077 6.692035802696705 false 15777 6.692035802696705 true +gene 1 4077 1685 -3.413636512977078 false 15778 -3.413636512977078 true +gene 1 3627 30 1.11052014349801 true 16364 1.11052014349801 true +gene 2 1811 4077 5.8503253679873435 false 17006 5.8503253679873435 true +gene 2 2 30 1.6963498266122063 false 17650 1.6963498266122063 true +gene 1 2 3627 -6.386739930810184 false 18335 -6.386739930810184 true +gene 1 266 4077 -3.4520706013634195 false 22326 -3.4520706013634195 true +gene 1 4077 3436 4.013733453814073 false 24107 4.013733453814073 true +gene 1 3 6640 -0.4098657935148693 false 24713 -0.4098657935148693 true +gene 1 6640 71 -1.753803731332276 false 24714 -1.753803731332276 true +gene 2 3627 4077 -0.28877942085982367 false 25611 -0.28877942085982367 true +genomeend 0` + +const genomeStrSimple = `genomestart 131 +trait 1 0.1 0 0 0 0 0 0 0 +trait 2 0.2 0 0 0 0 0 0 0 +trait 3 0.3 0 0 0 0 0 0 0 +node 1 1 1 3 SigmoidSteepenedActivation +node 2 1 1 1 SigmoidSteepenedActivation +node 3 1 1 1 SigmoidSteepenedActivation +node 4 1 1 1 SigmoidSteepenedActivation +node 5 1 1 1 SigmoidSteepenedActivation +node 6 3 0 2 SigmoidSteepenedActivation +node 7 1 0 2 SigmoidSteepenedActivation +node 9 1 0 0 SigmoidSteepenedActivation +gene 1 1 6 2.2319059895867506 false 1 2.2319059895867506 true +gene 2 2 6 -1.2321454922952315 false 2 -1.2321454922952315 true +gene 3 3 6 -0.49043752407567043 false 3 -0.49043752407567043 true +gene 1 4 6 0.46891774508325124 false 4 0.46891774508325124 true +gene 2 5 6 -2.4300089957414337 false 5 -2.4300089957414337 true +gene 3 1 7 -1.6431550383146705 false 6 -1.6431550383146705 true +gene 1 2 7 0.5398285078832726 false 7 0.5398285078832726 true +gene 1 3 7 0.13384471791198438 false 8 0.13384471791198438 true +gene 3 4 7 2.6321796987645034 false 9 2.6321796987645034 true +gene 1 5 7 -0.09060533978387858 false 10 -0.09060533978387858 true +gene 3 3 9 -3.089340847206661 false 13 -3.089340847206661 true +gene 3 9 6 0.5616266601637229 false 14 0.5616266601637229 true +genomeend 131` + +func buildNetworkFromGenome(str string) (*network.Network, error) { + buf := bytes.NewBufferString(str) + reader, err := genetics.NewGenomeReader(buf, genetics.PlainGenomeEncoding) + if err != nil { + return nil, err + } + if genome, err := reader.Read(); err != nil { + return nil, err + } else { + return genome.Genesis(0) + } +} + +func TestNetwork_MaxActivationDepth_FromGenome(t *testing.T) { + net, err := buildNetworkFromGenome(genomeStr) + require.NoError(t, err) + + str := logNetworkActivationPath(net, t) + t.Log(str) + + depth, err := net.MaxActivationDepth() + assert.NoError(t, err, "failed to calculate max depth") + assert.Equal(t, 9, depth) +} + +func TestNetwork_MaxActivationDepthCap_FromGenome(t *testing.T) { + net, err := buildNetworkFromGenome(genomeStr) + require.NoError(t, err) + + limit := 5 + depth, err := net.MaxActivationDepthWithCap(limit) + require.EqualError(t, err, network.ErrMaximalNetDepthExceeded.Error()) + assert.Equal(t, limit, depth) +} + +func Test_PrintAllActivationDepthPaths_simple(t *testing.T) { + net, err := buildNetworkFromGenome(genomeStrSimple) + require.NoError(t, err) + + str := logNetworkActivationPath(net, t) + t.Log(str) + expected := "1 -> 6\n2 -> 6\n3 -> 6\n4 -> 6\n5 -> 6\n3 -> 9 -> 6\n1 -> 7\n2 -> 7\n3 -> 7\n4 -> 7\n5 -> 7\n" + assert.Equal(t, expected, str) +} + +func logNetworkActivationPath(net *network.Network, t *testing.T) string { + buf := bytes.NewBufferString("") + err := network.PrintAllActivationDepthPaths(net, buf) + require.NoError(t, err) + return buf.String() +} diff --git a/examples/pole/cart2pole.go b/examples/pole/cart2pole.go index 3b63a30..1457423 100644 --- a/examples/pole/cart2pole.go +++ b/examples/pole/cart2pole.go @@ -3,11 +3,11 @@ package pole import ( "context" "fmt" - "github.com/yaricom/goNEAT/v3/experiment" - "github.com/yaricom/goNEAT/v3/experiment/utils" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/genetics" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/experiment" + "github.com/yaricom/goNEAT/v4/experiment/utils" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat/network" "math" "sort" ) @@ -140,6 +140,10 @@ func (e *cartDoublePoleGenerationEvaluator) GenerationEvaluate(ctx context.Conte // the organism champion champion := currSpecies.FindChampion() championFitness := champion.Fitness + championPhenotype, err := champion.Phenotype() + if err != nil { + return err + } // Now check to make sure the champion can do 100'000 evaluations cartPole.nonMarkovLong = true @@ -174,7 +178,7 @@ func (e *cartDoublePoleGenerationEvaluator) GenerationEvaluate(ctx context.Conte // The champion needs to be flushed here because it may have // leftover activation from its last test run that could affect // its recurrent memory - if _, err = champion.Phenotype.Flush(); err != nil { + if _, err = championPhenotype.Flush(); err != nil { return err } @@ -231,12 +235,9 @@ func (e *cartDoublePoleGenerationEvaluator) GenerationEvaluate(ctx context.Conte } if epoch.Solved { - // print winner organism + // print winner organism's statistics org := epoch.Champion - // The max depth of the network to be activated - if depth, err := org.Phenotype.MaxActivationDepthFast(0); err == nil { - neat.InfoLog(fmt.Sprintf("Activation depth of the winner: %d\n", depth)) - } + utils.PrintActivationDepth(org, true) genomeFile := "pole2_winner_genome" // Prints the winner organism to file! @@ -246,7 +247,7 @@ func (e *cartDoublePoleGenerationEvaluator) GenerationEvaluate(ctx context.Conte neat.InfoLog(fmt.Sprintf("Generation #%d winner's genome dumped to: %s\n", epoch.Id, orgPath)) } - // Prints the winner organism's Phenotype to the Cytoscape JSON file! + // Prints the winner organism's phenotype to the Cytoscape JSON file! if orgPath, err := utils.WriteGenomeCytoscapeJSON(genomeFile, e.OutputPath, org, epoch); err != nil { neat.ErrorLog(fmt.Sprintf("Failed to dump winner organism's phenome Cytoscape JSON graph, reason: %s\n", err)) } else { @@ -261,7 +262,11 @@ func (e *cartDoublePoleGenerationEvaluator) GenerationEvaluate(ctx context.Conte // orgEvaluate method evaluates fitness of the organism for cart double pole-balancing task func (e *cartDoublePoleGenerationEvaluator) orgEvaluate(organism *genetics.Organism, cartPole *CartPole) (winner bool, err error) { // Try to balance a pole now - organism.Fitness, err = cartPole.evalNet(organism.Phenotype, e.ActionType) + phenotype, err := organism.Phenotype() + if err != nil { + return false, err + } + organism.Fitness, err = cartPole.evalNet(phenotype, e.ActionType) if err != nil { return false, err } @@ -316,7 +321,7 @@ func (p *CartPole) evalNet(net *network.Network, actionType ActionType) (steps f p.resetState() - netDepth, err := net.MaxActivationDepthFast(0) // The max depth of the network to be activated + netDepth, err := net.MaxActivationDepthWithCap(0) // The max depth of the network to be activated if err != nil { neat.WarnLog(fmt.Sprintf( "Failed to estimate activation depth of the network, skipping evaluation: %s", err)) diff --git a/examples/pole/cart2pole_test.go b/examples/pole/cart2pole_test.go index 7331a6f..0752992 100644 --- a/examples/pole/cart2pole_test.go +++ b/examples/pole/cart2pole_test.go @@ -4,9 +4,9 @@ import ( "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/examples/utils" - experiment2 "github.com/yaricom/goNEAT/v3/experiment" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/examples/utils" + experiment2 "github.com/yaricom/goNEAT/v4/experiment" + "github.com/yaricom/goNEAT/v4/neat" "math/rand" "testing" ) diff --git a/examples/pole/cartpole.go b/examples/pole/cartpole.go index 979d410..23c252d 100644 --- a/examples/pole/cartpole.go +++ b/examples/pole/cartpole.go @@ -3,11 +3,11 @@ package pole import ( "context" "fmt" - "github.com/yaricom/goNEAT/v3/experiment" - "github.com/yaricom/goNEAT/v3/experiment/utils" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/genetics" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/experiment" + "github.com/yaricom/goNEAT/v4/experiment/utils" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat/network" "math" "math/rand" ) @@ -75,11 +75,9 @@ func (e *cartPoleGenerationEvaluator) GenerationEvaluate(ctx context.Context, po } if epoch.Solved { - // print winner organism + // print winner organism's statistics org := epoch.Champion - if depth, err := org.Phenotype.MaxActivationDepthFast(0); err == nil { - neat.InfoLog(fmt.Sprintf("Activation depth of the winner: %d\n", depth)) - } + utils.PrintActivationDepth(org, true) genomeFile := "pole1_winner_genome" // Prints the winner organism to file! @@ -89,7 +87,7 @@ func (e *cartPoleGenerationEvaluator) GenerationEvaluate(ctx context.Context, po neat.InfoLog(fmt.Sprintf("Generation #%d winner's genome dumped to: %s\n", epoch.Id, orgPath)) } - // Prints the winner organism's Phenotype to the Cytoscape JSON file! + // Prints the winner organism's phenotype to the Cytoscape JSON file! if orgPath, err := utils.WriteGenomeCytoscapeJSON(genomeFile, e.OutputPath, org, epoch); err != nil { neat.ErrorLog(fmt.Sprintf("Failed to dump winner organism's phenome Cytoscape JSON graph, reason: %s\n", err)) } else { @@ -103,8 +101,13 @@ func (e *cartPoleGenerationEvaluator) GenerationEvaluate(ctx context.Context, po // orgEvaluate evaluates provided organism for cart pole balancing task func (e *cartPoleGenerationEvaluator) orgEvaluate(organism *genetics.Organism) (bool, error) { + phenotype, err := organism.Phenotype() + if err != nil { + return false, err + } + // Try to balance a pole now - if fitness, err := e.runCart(organism.Phenotype); err != nil { + if fitness, err := e.runCart(phenotype); err != nil { return false, nil } else { organism.Fitness = float64(fitness) @@ -150,7 +153,7 @@ func (e *cartPoleGenerationEvaluator) runCart(net *network.Network) (steps int, thetaDot = float64(rand.Int31()%3000)/1000.0 - 1.5 } - netDepth, err := net.MaxActivationDepthFast(0) // The max depth of the network to be activated + netDepth, err := net.MaxActivationDepthWithCap(0) // The max depth of the network to be activated if err != nil { neat.WarnLog(fmt.Sprintf( "Failed to estimate maximal depth of the network with loop.\nUsing default depth: %d", netDepth)) diff --git a/examples/pole/cartpole_test.go b/examples/pole/cartpole_test.go index 419e8f5..2fa14b5 100644 --- a/examples/pole/cartpole_test.go +++ b/examples/pole/cartpole_test.go @@ -4,9 +4,9 @@ import ( "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/examples/utils" - experiment2 "github.com/yaricom/goNEAT/v3/experiment" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/examples/utils" + experiment2 "github.com/yaricom/goNEAT/v4/experiment" + "github.com/yaricom/goNEAT/v4/neat" "math/rand" "testing" "time" diff --git a/examples/utils/utils.go b/examples/utils/utils.go index 2ae6475..d3d8913 100644 --- a/examples/utils/utils.go +++ b/examples/utils/utils.go @@ -2,8 +2,8 @@ package utils import ( "github.com/pkg/errors" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/genetics" "os" ) diff --git a/examples/xor/XOR.go b/examples/xor/XOR.go index f82131c..cd84f6c 100644 --- a/examples/xor/XOR.go +++ b/examples/xor/XOR.go @@ -9,10 +9,10 @@ package xor import ( "context" "fmt" - "github.com/yaricom/goNEAT/v3/experiment" - "github.com/yaricom/goNEAT/v3/experiment/utils" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/genetics" + "github.com/yaricom/goNEAT/v4/experiment" + "github.com/yaricom/goNEAT/v4/experiment/utils" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/genetics" "math" ) @@ -82,9 +82,7 @@ func (e *xorGenerationEvaluator) GenerationEvaluate(ctx context.Context, pop *ge if epoch.Solved { // print winner organism org := epoch.Champion - if depth, err := org.Phenotype.MaxActivationDepthFast(0); err == nil { - neat.InfoLog(fmt.Sprintf("Activation depth of the winner: %d\n", depth)) - } + utils.PrintActivationDepth(org, true) genomeFile := "xor_winner_genome" // Prints the winner organism's Genome to the file! @@ -94,7 +92,7 @@ func (e *xorGenerationEvaluator) GenerationEvaluate(ctx context.Context, pop *ge neat.InfoLog(fmt.Sprintf("Generation #%d winner's genome dumped to: %s\n", epoch.Id, orgPath)) } - // Prints the winner organism's Phenotype to the DOT file! + // Prints the winner organism's phenotype to the DOT file! if orgPath, err := utils.WriteGenomeDOT(genomeFile, e.OutputPath, org, epoch); err != nil { neat.ErrorLog(fmt.Sprintf("Failed to dump winner organism's phenome DOT graph, reason: %s\n", err)) } else { @@ -102,7 +100,7 @@ func (e *xorGenerationEvaluator) GenerationEvaluate(ctx context.Context, pop *ge epoch.Id, orgPath)) } - // Prints the winner organism's Phenotype to the Cytoscape JSON file! + // Prints the winner organism's phenotype to the Cytoscape JSON file! if orgPath, err := utils.WriteGenomeCytoscapeJSON(genomeFile, e.OutputPath, org, epoch); err != nil { neat.ErrorLog(fmt.Sprintf("Failed to dump winner organism's phenome Cytoscape JSON graph, reason: %s\n", err)) } else { @@ -124,7 +122,12 @@ func (e *xorGenerationEvaluator) orgEvaluate(organism *genetics.Organism) (bool, {1.0, 1.0, 0.0}, {1.0, 1.0, 1.0}} - netDepth, err := organism.Phenotype.MaxActivationDepthFast(0) // The max depth of the network to be activated + phenotype, err := organism.Phenotype() + if err != nil { + return false, err + } + + netDepth, err := phenotype.MaxActivationDepthWithCap(0) // The max depth of the network to be activated if err != nil { neat.WarnLog(fmt.Sprintf( "Failed to estimate maximal depth of the network with loop:\n%s\nUsing default depth: %d", @@ -141,20 +144,20 @@ func (e *xorGenerationEvaluator) orgEvaluate(organism *genetics.Organism) (bool, // Load and activate the network on each input for count := 0; count < 4; count++ { - if err = organism.Phenotype.LoadSensors(in[count]); err != nil { + if err = phenotype.LoadSensors(in[count]); err != nil { neat.ErrorLog(fmt.Sprintf("Failed to load sensors: %s", err)) return false, err } // Use depth to ensure full relaxation - if success, err = organism.Phenotype.ForwardSteps(netDepth); err != nil { + if success, err = phenotype.ForwardSteps(netDepth); err != nil { neat.ErrorLog(fmt.Sprintf("Failed to activate network: %s", err)) return false, err } - out[count] = organism.Phenotype.Outputs[0].Activation + out[count] = phenotype.Outputs[0].Activation // Flush network for subsequent use - if _, err = organism.Phenotype.Flush(); err != nil { + if _, err = phenotype.Flush(); err != nil { neat.ErrorLog(fmt.Sprintf("Failed to flush network: %s", err)) return false, err } diff --git a/examples/xor/XOR_test.go b/examples/xor/XOR_test.go index dc85bea..9d7e694 100644 --- a/examples/xor/XOR_test.go +++ b/examples/xor/XOR_test.go @@ -4,9 +4,9 @@ import ( "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/examples/utils" - experiment2 "github.com/yaricom/goNEAT/v3/experiment" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/examples/utils" + experiment2 "github.com/yaricom/goNEAT/v4/experiment" + "github.com/yaricom/goNEAT/v4/neat" "math/rand" "testing" "time" diff --git a/executor.go b/executor.go index dc1bc0b..4194254 100644 --- a/executor.go +++ b/executor.go @@ -4,11 +4,11 @@ import ( "context" "flag" "fmt" - "github.com/yaricom/goNEAT/v3/examples/pole" - "github.com/yaricom/goNEAT/v3/examples/xor" - "github.com/yaricom/goNEAT/v3/experiment" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/genetics" + "github.com/yaricom/goNEAT/v4/examples/pole" + "github.com/yaricom/goNEAT/v4/examples/xor" + "github.com/yaricom/goNEAT/v4/experiment" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/genetics" "log" "math/rand" "os" @@ -25,12 +25,16 @@ func main() { var experimentName = flag.String("experiment", "XOR", "The name of experiment to run. [XOR, cart_pole, cart_2pole_markov, cart_2pole_non-markov]") var trialsCount = flag.Int("trials", 0, "The number of trials for experiment. Overrides the one set in configuration.") var logLevel = flag.String("log_level", "", "The logger level to be used. Overrides the one set in configuration.") + var randSeed = flag.Int64("seed", 0, "The seed for random number generator") flag.Parse() // Seed the random-number generator with current time so that // the numbers will be different every time we run. seed := time.Now().Unix() + if randSeed != nil { + seed = *randSeed + } rand.Seed(seed) // Load NEAT options diff --git a/experiment/common.go b/experiment/common.go index 23bfcd1..fd0a8f8 100644 --- a/experiment/common.go +++ b/experiment/common.go @@ -4,8 +4,10 @@ package experiment import ( "context" "errors" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/genetics" + "fmt" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/genetics" + "math" "time" ) @@ -44,3 +46,18 @@ func epochExecutorForContext(ctx context.Context) (genetics.PopulationEpochExecu return nil, errors.New("unsupported epoch executor type requested") } } + +// organismComplexity is to get complexity of the given organism. If error happens during complexity evaluation or +// provided nil the math.MaxInt will be returned. +func organismComplexity(organism *genetics.Organism) int { + if organism == nil { + neat.WarnLog("Can not estimate complexity of the organism. Nil provided.") + return math.MaxInt + } + if phenotype, err := organism.Phenotype(); err != nil { + neat.WarnLog(fmt.Sprintf("Failed to get phenotype of the organism, reason: %s", err)) + return math.MaxInt + } else { + return phenotype.Complexity() + } +} diff --git a/experiment/common_test.go b/experiment/common_test.go index 4e3e98f..efd9545 100644 --- a/experiment/common_test.go +++ b/experiment/common_test.go @@ -4,8 +4,8 @@ import ( "context" "errors" "github.com/stretchr/testify/assert" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/genetics" "testing" ) diff --git a/experiment/experiment.go b/experiment/experiment.go index 5711bfd..768b7a4 100644 --- a/experiment/experiment.go +++ b/experiment/experiment.go @@ -4,7 +4,7 @@ import ( "encoding/gob" "fmt" "github.com/sbinet/npyio/npz" - "github.com/yaricom/goNEAT/v3/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat/genetics" "gonum.org/v1/gonum/mat" "io" "math" @@ -144,7 +144,7 @@ func (e *Experiment) BestComplexity() Floats { var x Floats = make([]float64, len(e.Trials)) for i, t := range e.Trials { if org, ok := t.BestOrganism(false); ok { - x[i] = float64(org.Phenotype.Complexity()) + x[i] = float64(organismComplexity(org)) } } return x @@ -234,7 +234,7 @@ func (e *Experiment) EfficiencyScore() float64 { t.WinnerStatistics() } - meanComplexity += float64(t.WinnerGeneration.Champion.Phenotype.Complexity()) + meanComplexity += float64(t.WinnerGeneration.ChampionComplexity()) meanFitness += t.WinnerGeneration.Champion.Fitness count++ @@ -277,7 +277,7 @@ func (e *Experiment) PrintStatistics() { fmt.Printf("\nChampion found in %d trial run\n\tNodes:\t\t\t%d\n\tGenes:\t\t\t%d\n\tEvaluations:\t\t%d\n\n\tDiversity:\t\t%d", trid, nodes, genes, evals, divers) fmt.Printf("\n\tComplexity:\t\t%d\n\tAge:\t\t\t%d\n\tFitness:\t\t%f\n", - org.Phenotype.Complexity(), org.Species.Age, org.Fitness) + organismComplexity(org), org.Species.Age, org.Fitness) } else { fmt.Println("\nNo winner found in the experiment!!!") } @@ -296,7 +296,7 @@ func (e *Experiment) PrintStatistics() { avgDivers += float64(diversity) avgGenerations += float64(len(t.Generations)) - meanComplexity += float64(t.WinnerGeneration.Champion.Phenotype.Complexity()) + meanComplexity += float64(t.WinnerGeneration.ChampionComplexity()) meanAge += float64(t.WinnerGeneration.Champion.Species.Age) meanFitness += t.WinnerGeneration.Champion.Fitness diff --git a/experiment/experiment_execute.go b/experiment/experiment_execute.go index d8bca95..072b074 100644 --- a/experiment/experiment_execute.go +++ b/experiment/experiment_execute.go @@ -3,8 +3,8 @@ package experiment import ( "context" "fmt" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/genetics" "time" ) diff --git a/experiment/experiment_execute_test.go b/experiment/experiment_execute_test.go index d76dfe8..798f16c 100644 --- a/experiment/experiment_execute_test.go +++ b/experiment/experiment_execute_test.go @@ -6,8 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/genetics" "testing" ) diff --git a/experiment/experiment_test.go b/experiment/experiment_test.go index 1613062..f9a863b 100644 --- a/experiment/experiment_test.go +++ b/experiment/experiment_test.go @@ -6,7 +6,7 @@ import ( "github.com/sbinet/npyio/npz" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat/genetics" "gonum.org/v1/gonum/mat" "math" "testing" diff --git a/experiment/generation.go b/experiment/generation.go index 20e2eca..f3b28b5 100644 --- a/experiment/generation.go +++ b/experiment/generation.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/gob" "github.com/pkg/errors" - "github.com/yaricom/goNEAT/v3/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat/genetics" "math" "reflect" "sort" @@ -56,7 +56,7 @@ func (g *Generation) FillPopulationStatistics(pop *genetics.Population) { g.Age[i] = float64(currSpecies.Age) // sort organisms from current species by fitness to have most fit first sort.Sort(sort.Reverse(currSpecies.Organisms)) - g.Complexity[i] = float64(currSpecies.Organisms[0].Phenotype.Complexity()) + g.Complexity[i] = float64(organismComplexity(currSpecies.Organisms[0])) g.Fitness[i] = currSpecies.Organisms[0].Fitness // finds the best organism in epoch if not solved @@ -78,6 +78,15 @@ func (g *Generation) Average() (fitness, age, complexity float64) { return fitness, age, complexity } +// ChampionComplexity is to get complexity of the champion organism. If failed to calculate complexity +// due to error or missing champion the math.MaxInt value returned. +func (g *Generation) ChampionComplexity() int { + if g.Champion == nil { + return math.MaxInt + } + return organismComplexity(g.Champion) +} + // Encode is to encode the generation with provided GOB encoder func (g *Generation) Encode(enc *gob.Encoder) error { if err := enc.EncodeValue(reflect.ValueOf(g.Id)); err != nil { diff --git a/experiment/generation_test.go b/experiment/generation_test.go index d6c672a..19ef239 100644 --- a/experiment/generation_test.go +++ b/experiment/generation_test.go @@ -5,10 +5,11 @@ import ( "encoding/gob" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/genetics" - "github.com/yaricom/goNEAT/v3/neat/math" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/genetics" + neatMath "github.com/yaricom/goNEAT/v4/neat/math" + "github.com/yaricom/goNEAT/v4/neat/network" + "math" "math/rand" "testing" "time" @@ -27,32 +28,7 @@ func TestGeneration_Average(t *testing.T) { func TestGeneration_FillPopulationStatistics(t *testing.T) { rand.Seed(42) - in, out, nmax := 3, 2, 5 - linkProb := 0.9 - conf := neat.Options{ - DisjointCoeff: 0.5, - ExcessCoeff: 0.5, - MutdiffCoeff: 0.5, - CompatThreshold: 5.0, - PopSize: 100, - } - pop, err := genetics.NewPopulationRandom(in, out, nmax, false, linkProb, &conf) - require.NoError(t, err, "failed to create population") - require.NotNil(t, pop, "population expected") - require.Len(t, pop.Organisms, conf.PopSize, "wrong population size") - assert.True(t, len(pop.Species) > 0, "population has no species") - - maxFitness := -1.0 - for i := range pop.Species { - for j := range pop.Species[i].Organisms { - fitness := rand.Float64() * 100 - pop.Species[i].Organisms[j].Fitness = fitness - if fitness > maxFitness { - maxFitness = fitness - } - } - } - + pop, maxFitness := buildTestPopulation(t) gen := Generation{ Id: 1, TrialId: 1, @@ -101,6 +77,24 @@ func TestGeneration_Encode_Decode(t *testing.T) { assert.EqualValues(t, gen, dgen) } +func TestGeneration_ChampionComplexity(t *testing.T) { + rand.Seed(42) + pop, maxFitness := buildTestPopulation(t) + gen := Generation{ + Id: 1, + TrialId: 1, + } + gen.FillPopulationStatistics(pop) + assert.NotNil(t, gen.Champion) + assert.Equal(t, maxFitness, gen.Champion.Fitness) + + assert.Equal(t, 32, gen.ChampionComplexity()) + + // remove champion and check that proper value returned + gen.Champion = nil + assert.Equal(t, math.MaxInt, gen.ChampionComplexity()) +} + const ( testDiversity = 32 testWinnerEvals = 12423 @@ -114,6 +108,37 @@ var ( testFitness = Floats{10.0, 30.0, 40.0} ) +func buildTestPopulation(t *testing.T) (*genetics.Population, float64) { + in, out, maxHidden := 3, 2, 5 + linkProb := 0.9 + conf := neat.Options{ + DisjointCoeff: 0.5, + ExcessCoeff: 0.5, + MutdiffCoeff: 0.5, + CompatThreshold: 5.0, + PopSize: 100, + NodeActivators: []neatMath.NodeActivationType{neatMath.GaussianBipolarActivation}, + NodeActivatorsProb: []float64{1.0}, + } + pop, err := genetics.NewPopulationRandom(in, out, maxHidden, false, linkProb, &conf) + require.NoError(t, err, "failed to create population") + require.NotNil(t, pop, "population expected") + require.Len(t, pop.Organisms, conf.PopSize, "wrong population size") + assert.True(t, len(pop.Species) > 0, "population has no species") + + maxFitness := -1.0 + for i := range pop.Species { + for j := range pop.Species[i].Organisms { + fitness := rand.Float64() * 100 + pop.Species[i].Organisms[j].Fitness = fitness + if fitness > maxFitness { + maxFitness = fitness + } + } + } + return pop, maxFitness +} + func buildTestGeneration(genId int, fitness float64) *Generation { duration := time.Duration(rand.Float32()*10) * time.Second return buildTestGenerationWithDuration(genId, fitness, duration) @@ -156,10 +181,10 @@ func buildTestGenome(id int) *genetics.Genome { } nodes := []*network.NNode{ - {Id: 1, NeuronType: network.InputNeuron, ActivationType: math.NullActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)}, - {Id: 2, NeuronType: network.InputNeuron, ActivationType: math.NullActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)}, - {Id: 3, NeuronType: network.BiasNeuron, ActivationType: math.SigmoidSteepenedActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)}, - {Id: 4, NeuronType: network.OutputNeuron, ActivationType: math.SigmoidSteepenedActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)}, + {Id: 1, NeuronType: network.InputNeuron, ActivationType: neatMath.NullActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)}, + {Id: 2, NeuronType: network.InputNeuron, ActivationType: neatMath.NullActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)}, + {Id: 3, NeuronType: network.BiasNeuron, ActivationType: neatMath.SigmoidSteepenedActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)}, + {Id: 4, NeuronType: network.OutputNeuron, ActivationType: neatMath.SigmoidSteepenedActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)}, } genes := []*genetics.Gene{ diff --git a/experiment/trial.go b/experiment/trial.go index 69a9499..9550655 100644 --- a/experiment/trial.go +++ b/experiment/trial.go @@ -2,7 +2,8 @@ package experiment import ( "encoding/gob" - "github.com/yaricom/goNEAT/v3/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat/genetics" + "math" "sort" "time" ) @@ -100,8 +101,8 @@ func (t *Trial) ChampionSpeciesAges() Floats { func (t *Trial) ChampionsComplexities() Floats { var x Floats = make([]float64, len(t.Generations)) for i, e := range t.Generations { - if e.Champion != nil && e.Champion.Phenotype != nil { - x[i] = float64(e.Champion.Phenotype.Complexity()) + if complexity := e.ChampionComplexity(); complexity != math.MaxInt { + x[i] = float64(complexity) } } return x diff --git a/experiment/trial_test.go b/experiment/trial_test.go index db0a02a..dea96e0 100644 --- a/experiment/trial_test.go +++ b/experiment/trial_test.go @@ -3,9 +3,11 @@ package experiment import ( "bytes" "encoding/gob" + "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/genetics" "math" "math/rand" "testing" @@ -235,9 +237,8 @@ func buildTestTrialWithBestOrganismGenesis(id, numGenerations int) *Trial { trial := buildTestTrial(id, numGenerations) // do genesis of best organisms for i := range trial.Generations { - org := trial.Generations[i].Champion - if phenotype, err := org.Genotype.Genesis(org.Genotype.Id); err == nil { - trial.Generations[i].Champion.Phenotype = phenotype + if err := trial.Generations[i].Champion.UpdatePhenotype(); err != nil { + neat.WarnLog(fmt.Sprintf("Failed to crete phenotype, reason: %s", err)) } } return trial diff --git a/experiment/utils/utils.go b/experiment/utils/utils.go index 8046629..73825d6 100644 --- a/experiment/utils/utils.go +++ b/experiment/utils/utils.go @@ -2,10 +2,13 @@ package utils import ( + "bytes" "fmt" - "github.com/yaricom/goNEAT/v3/experiment" - "github.com/yaricom/goNEAT/v3/neat/genetics" - "github.com/yaricom/goNEAT/v3/neat/network/formats" + "github.com/yaricom/goNEAT/v4/experiment" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/genetics" + "github.com/yaricom/goNEAT/v4/neat/network" + "github.com/yaricom/goNEAT/v4/neat/network/formats" "log" "os" ) @@ -13,8 +16,12 @@ import ( // WriteGenomePlain is to write genome of the organism to the genomeFile in the outDir directory using plain encoding. // The method return path to the file if successful or error if failed. func WriteGenomePlain(genomeFile, outDir string, org *genetics.Organism, epoch *experiment.Generation) (string, error) { + phenotype, err := org.Phenotype() + if err != nil { + return "", err + } orgPath := fmt.Sprintf("%s/%s_%d-%d", CreateOutDirForTrial(outDir, epoch.TrialId), - genomeFile, org.Phenotype.NodeCount(), org.Phenotype.LinkCount()) + genomeFile, phenotype.NodeCount(), phenotype.LinkCount()) if file, err := os.Create(orgPath); err != nil { return "", err } else if err = org.Genotype.Write(file); err != nil { @@ -26,11 +33,15 @@ func WriteGenomePlain(genomeFile, outDir string, org *genetics.Organism, epoch * // WriteGenomeDOT is to write genome of the organism to the genomeFile in the outDir directory using DOT encoding. // The method return path to the file if successful or error if failed. func WriteGenomeDOT(genomeFile, outDir string, org *genetics.Organism, epoch *experiment.Generation) (string, error) { + phenotype, err := org.Phenotype() + if err != nil { + return "", err + } orgPath := fmt.Sprintf("%s/%s_%d-%d.dot", CreateOutDirForTrial(outDir, epoch.TrialId), - genomeFile, org.Phenotype.NodeCount(), org.Phenotype.LinkCount()) + genomeFile, phenotype.NodeCount(), phenotype.LinkCount()) if file, err := os.Create(orgPath); err != nil { return "", err - } else if err = formats.WriteDOT(file, org.Phenotype); err != nil { + } else if err = formats.WriteDOT(file, phenotype); err != nil { return "", err } return orgPath, nil @@ -39,11 +50,15 @@ func WriteGenomeDOT(genomeFile, outDir string, org *genetics.Organism, epoch *ex // WriteGenomeCytoscapeJSON is to write genome of the organism to the genomeFile in the outDir directory using Cytoscape JSON encoding. // The method return path to the file if successful or error if failed. func WriteGenomeCytoscapeJSON(genomeFile, outDir string, org *genetics.Organism, epoch *experiment.Generation) (string, error) { + phenotype, err := org.Phenotype() + if err != nil { + return "", err + } orgPath := fmt.Sprintf("%s/%s_%d-%d.cyjs", CreateOutDirForTrial(outDir, epoch.TrialId), - genomeFile, org.Phenotype.NodeCount(), org.Phenotype.LinkCount()) + genomeFile, phenotype.NodeCount(), phenotype.LinkCount()) if file, err := os.Create(orgPath); err != nil { return "", err - } else if err = formats.WriteCytoscapeJSON(file, org.Phenotype); err != nil { + } else if err = formats.WriteCytoscapeJSON(file, phenotype); err != nil { return "", err } return orgPath, nil @@ -72,3 +87,20 @@ func CreateOutDirForTrial(outDir string, trialID int) string { } return dir } + +// PrintActivationDepth is to print maximal activation depth of phenotype network of the organism +func PrintActivationDepth(organism *genetics.Organism, printActivationPath bool) { + phenotype, err := organism.Phenotype() + if err != nil { + return + } + if depth, err := phenotype.MaxActivationDepthWithCap(0); err == nil { + neat.InfoLog(fmt.Sprintf("Activation depth of the winner: %d\n", depth)) + } + if printActivationPath { + buf := bytes.NewBufferString("Activation paths of the winner:\n") + if err = network.PrintAllActivationDepthPaths(phenotype, buf); err == nil { + neat.InfoLog(buf.String()) + } + } +} diff --git a/go.mod b/go.mod index bdb6112..25270ca 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,19 @@ -module github.com/yaricom/goNEAT/v3 +module github.com/yaricom/goNEAT/v4 go 1.17 require ( github.com/pkg/errors v0.9.1 - github.com/sbinet/npyio v0.6.0 - github.com/spf13/cast v1.5.0 - github.com/stretchr/testify v1.7.1 - gonum.org/v1/gonum v0.11.0 + github.com/sbinet/npyio v0.7.0 + github.com/spf13/cast v1.5.1 + github.com/stretchr/testify v1.8.4 + gonum.org/v1/gonum v0.13.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/davecgh/go-spew v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.1.0 // indirect - golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 // indirect + github.com/stretchr/objx v0.5.0 // indirect + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect ) diff --git a/go.sum b/go.sum index 8a34c3c..aea837a 100644 --- a/go.sum +++ b/go.sum @@ -1,54 +1,88 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/latin-modern v0.3.0/go.mod h1:ysEQXnuT/sCDOAONxC7ImeEDVINbltClhasMAqEtRK0= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.3.0/go.mod h1:jdJ+cqF+F4SUL2V+qxBth8fvBpBDS7yloUL5Fi8GTGY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= +github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9/go.mod h1:gWuR/CrFDDeVRFQwHPvsv9soJVB/iqymhuZQuJ3a9OM= +github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/sbinet/npyio v0.6.0 h1:IyqqQIzRjDym9xnIXsToCKei/qCzxDP+Y74KoMlMgXo= -github.com/sbinet/npyio v0.6.0/go.mod h1:/q3BNr6dJOy+t6h7RZchTJ0nwRJO52mivaem29WE1j8= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/sbinet/npyio v0.7.0 h1:KH8n5VrI1O2FeNAHwa0WmC1f9nGNtXNzQHBkyoU8tuE= +github.com/sbinet/npyio v0.7.0/go.mod h1:4jmxspVr/RFRPc6zSGR/8FP6nb9m7EpypUXrU/cf/nU= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 h1:n9HxLrNxWWtEb1cA950nuEEj3QnKbtsCJ6KjcgisNUs= golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -58,34 +92,82 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= +golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= -gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +gonum.org/v1/gonum v0.13.0 h1:a0T3bh+7fhRyqeNbiC3qVHYmkiQgit3wnNan/2c0HMM= +gonum.org/v1/gonum v0.13.0/go.mod h1:/WPYRckkfWrhWefxyYTfrTtQR0KH4iyHNuzxqXAKyAU= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/neat/genetics/common.go b/neat/genetics/common.go index 4d3db0c..04a2a0e 100644 --- a/neat/genetics/common.go +++ b/neat/genetics/common.go @@ -3,8 +3,8 @@ package genetics import ( "errors" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/network" "strings" ) diff --git a/neat/genetics/gene.go b/neat/genetics/gene.go index 43ced41..72a45e7 100644 --- a/neat/genetics/gene.go +++ b/neat/genetics/gene.go @@ -2,8 +2,8 @@ package genetics import ( "fmt" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/network" ) // The Gene type in this system specifies a "Connection Gene." diff --git a/neat/genetics/gene_test.go b/neat/genetics/gene_test.go index 9e0fa86..23258f4 100644 --- a/neat/genetics/gene_test.go +++ b/neat/genetics/gene_test.go @@ -2,9 +2,9 @@ package genetics import ( "github.com/stretchr/testify/assert" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/math" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" + "github.com/yaricom/goNEAT/v4/neat/network" "testing" ) diff --git a/neat/genetics/genome.go b/neat/genetics/genome.go index 62a74b2..549ba5a 100644 --- a/neat/genetics/genome.go +++ b/neat/genetics/genome.go @@ -3,9 +3,9 @@ package genetics import ( "errors" "fmt" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/math" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" + "github.com/yaricom/goNEAT/v4/neat/network" "io" "math/rand" "reflect" @@ -40,33 +40,45 @@ type Genome struct { // Allows Genome to be matched with its Network Phenotype *network.Network `yaml:""` + + // nodeByIdMap allows to fast lookup if node with specific ID belongs to this Genome + nodeByIdMap map[int]*network.NNode } // NewGenome Constructor which takes full genome specs and puts them into the new one func NewGenome(id int, t []*neat.Trait, n []*network.NNode, g []*Gene) *Genome { - return &Genome{ - Id: id, - Traits: t, - Nodes: n, - Genes: g, - } + return newGenome(id, t, n, g, nil) } // NewModularGenome Constructs new modular genome func NewModularGenome(id int, t []*neat.Trait, n []*network.NNode, g []*Gene, mimoG []*MIMOControlGene) *Genome { + return newGenome(id, t, n, g, mimoG) +} + +func newGenome(id int, traits []*neat.Trait, nodes []*network.NNode, genes []*Gene, mimoG []*MIMOControlGene) *Genome { + nodeByIdMap := make(map[int]*network.NNode) + for _, n := range nodes { + nodeByIdMap[n.Id] = n + } + return newGenomeWithNodeIdMap(id, traits, nodes, genes, mimoG, nodeByIdMap) +} + +func newGenomeWithNodeIdMap(id int, traits []*neat.Trait, nodes []*network.NNode, genes []*Gene, + mimoG []*MIMOControlGene, nodeByIdMap map[int]*network.NNode) *Genome { return &Genome{ Id: id, - Traits: t, - Nodes: n, - Genes: g, + Traits: traits, + Nodes: nodes, + Genes: genes, ControlGenes: mimoG, + nodeByIdMap: nodeByIdMap, } } // This special constructor creates a Genome with in inputs, out outputs, n out of maxHidden hidden units, and random // connectivity. If rec is true then recurrent connections will be included. The last input is a bias // link_prob is the probability of a link. The created genome is not modular. -func newGenomeRand(newId, in, out, n, maxHidden int, recurrent bool, linkProb float64) *Genome { +func newGenomeRand(newId, in, out, n, maxHidden int, recurrent bool, linkProb float64, opts *neat.Options) (*Genome, error) { totalNodes := in + out + maxHidden matrixDim := totalNodes * totalNodes // The connection matrix which will be randomized @@ -83,10 +95,11 @@ func newGenomeRand(newId, in, out, n, maxHidden int, recurrent bool, linkProb fl // Create empty genome gnome := Genome{ - Id: newId, - Traits: []*neat.Trait{newTrait}, - Nodes: make([]*network.NNode, 0), - Genes: make([]*Gene, 0), + Id: newId, + Traits: []*neat.Trait{newTrait}, + Nodes: make([]*network.NNode, 0), + Genes: make([]*Gene, 0), + nodeByIdMap: make(map[int]*network.NNode), } // Step through the connection matrix, randomly assigning bits @@ -96,28 +109,29 @@ func newGenomeRand(newId, in, out, n, maxHidden int, recurrent bool, linkProb fl // Build the input nodes for i := 1; i <= in; i++ { - var newNode *network.NNode - if i < in { - newNode = network.NewNNode(i, network.InputNeuron) - } else { - newNode = network.NewNNode(i, network.BiasNeuron) - } + bias := i == in // the last input node + newNode := network.NewSensorNode(i, bias) newNode.Trait = newTrait - gnome.Nodes = append(gnome.Nodes, newNode) + gnome.addNode(newNode) } // Build the hidden nodes for i := in + 1; i <= in+n; i++ { newNode := network.NewNNode(i, network.HiddenNeuron) + if activationType, err := opts.RandomNodeActivationType(); err != nil { + return nil, err + } else { + newNode.ActivationType = activationType + } newNode.Trait = newTrait - gnome.Nodes = append(gnome.Nodes, newNode) + gnome.addNode(newNode) } // Build the output nodes for i := firstOutput; i <= totalNodes; i++ { newNode := network.NewNNode(i, network.OutputNeuron) newNode.Trait = newTrait - gnome.Nodes = append(gnome.Nodes, newNode) + gnome.addNode(newNode) } // @@ -183,7 +197,7 @@ func newGenomeRand(newId, in, out, n, maxHidden int, recurrent bool, linkProb fl inNode, outNode = nil, nil } } - return &gnome + return &gnome, nil } // ReadGenome reads Genome from reader @@ -335,24 +349,37 @@ func (g *Genome) getNextGeneInnovNum() (int64, error) { return innNum + int64(1), nil } -// Returns true if this Genome already includes provided node -func (g *Genome) hasNode(node *network.NNode) bool { - if node == nil { - return false - } - if id, _ := g.getLastNodeId(); node.Id > id { - return false // not found +func (g *Genome) addNode(node *network.NNode) { + g.Nodes = append(g.Nodes, node) + g.mapNodeId(node) +} + +func (g *Genome) addNodes(nodes []*network.NNode) { + for i := 0; i < len(nodes); i++ { + g.addNode(nodes[i]) } - for _, n := range g.Nodes { - if n.Id == node.Id { - return true - } +} + +func (g *Genome) mapNodeId(node *network.NNode) { + g.nodeByIdMap[node.Id] = node +} + +func (g *Genome) NodeWithId(nodeId int) *network.NNode { + if node, ok := g.nodeByIdMap[nodeId]; ok { + return node + } else { + return nil } - return false +} + +// Returns true if this Genome already includes provided node +func (g *Genome) haveNode(nodeId int) bool { + _, ok := g.nodeByIdMap[nodeId] + return ok } // Returns true if this Genome already includes provided gene -func (g *Genome) hasGene(gene *Gene) bool { +func (g *Genome) haveGene(gene *Gene) bool { if inn, _ := g.getNextGeneInnovNum(); gene.InnovationNum >= inn { // The gene has innovation number higher that not assigned yet innovation number for this genome. This means // that this is gene not from this genome lineage. @@ -360,8 +387,8 @@ func (g *Genome) hasGene(gene *Gene) bool { } // Find genetically equal link in this genome to the provided gene - for _, g := range g.Genes { - if g.Link.IsEqualGenetically(gene.Link) { + for _, gn := range g.Genes { + if gn.Link.IsEqualGenetically(gene.Link) { return true } } @@ -478,27 +505,79 @@ func (g *Genome) duplicate(newId int) (*Genome, error) { } // Duplicate NNodes - nodesDup := make([]*network.NNode, len(g.Nodes)) - for i, nd := range g.Nodes { - // First, find the duplicate of the trait that this node points to - assocTrait := nd.Trait + nodesDup, nodeIdMap := g.duplicateNodes(traitsDup) + + // Duplicate Genes + genesDup, err := g.duplicateGenes(traitsDup, nodeIdMap) + if err != nil { + return nil, err + } + + // If no MIMO control genes return plain genome + // + if len(g.ControlGenes) == 0 { + return newGenomeWithNodeIdMap(newId, traitsDup, nodesDup, genesDup, nil, nodeIdMap), nil + } + + // Duplicate MIMO Control Genes and return modular genome + // + controlGenesDup, err := g.duplicateControlGenes(traitsDup, nodeIdMap) + if err != nil { + return nil, err + } + return newGenomeWithNodeIdMap(newId, traitsDup, nodesDup, genesDup, controlGenesDup, nodeIdMap), nil +} + +func (g *Genome) duplicateControlGenes(traits []*neat.Trait, nodeIdMap map[int]*network.NNode) ([]*MIMOControlGene, error) { + controlGenesDup := make([]*MIMOControlGene, len(g.ControlGenes)) + for i, cg := range g.ControlGenes { + // duplicate control node + controlNode := cg.ControlNode + // find duplicate of trait associated with control node + assocTrait := controlNode.Trait if assocTrait != nil { - assocTrait = TraitWithId(assocTrait.Id, traitsDup) + assocTrait = TraitWithId(assocTrait.Id, traits) + } + nodeCopy := network.NewNNodeCopy(controlNode, assocTrait) + // add incoming links + for _, l := range controlNode.Incoming { + inNode, ok := nodeIdMap[l.InNode.Id] + if !ok { + return nil, fmt.Errorf("incoming node: %d not found for control node: %d", + l.InNode.Id, controlNode.Id) + } + newInLink := network.NewLinkCopy(l, inNode, nodeCopy) + nodeCopy.Incoming = append(nodeCopy.Incoming, newInLink) + } + + // add outgoing links + for _, l := range controlNode.Outgoing { + outNode, ok := nodeIdMap[l.OutNode.Id] + if !ok { + return nil, fmt.Errorf("outgoing node: %d not found for control node: %d", + l.InNode.Id, controlNode.Id) + } + newOutLink := network.NewLinkCopy(l, nodeCopy, outNode) + nodeCopy.Outgoing = append(nodeCopy.Outgoing, newOutLink) } - nodesDup[i] = network.NewNNodeCopy(nd, assocTrait) + + // add MIMO control gene + controlGenesDup[i] = NewMIMOGeneCopy(cg, nodeCopy) } + return controlGenesDup, nil +} - // Duplicate Genes +func (g *Genome) duplicateGenes(traits []*neat.Trait, nodeIdMap map[int]*network.NNode) ([]*Gene, error) { genesDup := make([]*Gene, len(g.Genes)) for i, gn := range g.Genes { // First find the nodes connected by the gene's link - inNode := NodeWithId(gn.Link.InNode.Id, nodesDup) - if inNode == nil { + inNode, ok := nodeIdMap[gn.Link.InNode.Id] + if !ok { return nil, fmt.Errorf("incoming node: %d not found for gene %s", gn.Link.InNode.Id, gn.String()) } - outNode := NodeWithId(gn.Link.OutNode.Id, nodesDup) - if outNode == nil { + outNode, ok := nodeIdMap[gn.Link.OutNode.Id] + if !ok { return nil, fmt.Errorf("outgoing node: %d not found for gene %s", gn.Link.OutNode.Id, gn.String()) } @@ -506,55 +585,28 @@ func (g *Genome) duplicate(newId int) (*Genome, error) { // Find the duplicate of trait associated with this gene assocTrait := gn.Link.Trait if assocTrait != nil { - assocTrait = TraitWithId(assocTrait.Id, traitsDup) + assocTrait = TraitWithId(assocTrait.Id, traits) } genesDup[i] = NewGeneCopy(gn, assocTrait, inNode, outNode) } + return genesDup, nil +} - if len(g.ControlGenes) == 0 { - // If no MIMO control genes return plain genome - return NewGenome(newId, traitsDup, nodesDup, genesDup), nil - } else { - // Duplicate MIMO Control Genes and build modular genome - controlGenesDup := make([]*MIMOControlGene, len(g.ControlGenes)) - for i, cg := range g.ControlGenes { - // duplicate control node - controlNode := cg.ControlNode - // find duplicate of trait associated with control node - assocTrait := controlNode.Trait - if assocTrait != nil { - assocTrait = TraitWithId(assocTrait.Id, traitsDup) - } - nodeCopy := network.NewNNodeCopy(controlNode, assocTrait) - // add incoming links - for _, l := range controlNode.Incoming { - inNode := NodeWithId(l.InNode.Id, nodesDup) - if inNode == nil { - return nil, fmt.Errorf("incoming node: %d not found for control node: %d", - l.InNode.Id, controlNode.Id) - } - newInLink := network.NewLinkCopy(l, inNode, nodeCopy) - nodeCopy.Incoming = append(nodeCopy.Incoming, newInLink) - } - - // add outgoing links - for _, l := range controlNode.Outgoing { - outNode := NodeWithId(l.OutNode.Id, nodesDup) - if outNode == nil { - return nil, fmt.Errorf("outgoing node: %d not found for control node: %d", - l.InNode.Id, controlNode.Id) - } - newOutLink := network.NewLinkCopy(l, nodeCopy, outNode) - nodeCopy.Outgoing = append(nodeCopy.Outgoing, newOutLink) - } - - // add MIMO control gene - controlGenesDup[i] = NewMIMOGeneCopy(cg, nodeCopy) +func (g *Genome) duplicateNodes(traits []*neat.Trait) ([]*network.NNode, map[int]*network.NNode) { + nodesDup := make([]*network.NNode, len(g.Nodes)) + nodeIdMap := make(map[int]*network.NNode) + for i, nd := range g.Nodes { + // First, find the duplicate of the trait that this node points to + assocTrait := nd.Trait + if assocTrait != nil { + assocTrait = TraitWithId(assocTrait.Id, traits) } - - return NewModularGenome(newId, traitsDup, nodesDup, genesDup, controlGenesDup), nil + node := network.NewNNodeCopy(nd, assocTrait) + nodesDup[i] = node + nodeIdMap[node.Id] = node } + return nodesDup, nodeIdMap } // For debugging: A number of tests can be run on a genome to check its integrity. @@ -624,6 +676,15 @@ func (g *Genome) verify() (bool, error) { return true, nil } +func (g *Genome) nodeInsert(node *network.NNode) { + g.Nodes = nodeInsert(g.Nodes, node) + g.mapNodeId(node) +} + +func (g *Genome) geneInsert(gene *Gene) { + g.Genes = geneInsert(g.Genes, gene) +} + // Inserts a NNode into a given ordered list of NNodes in ascending order by NNode ID func nodeInsert(nodes []*network.NNode, n *network.NNode) []*network.NNode { if n == nil { diff --git a/neat/genetics/genome_compatibility.go b/neat/genetics/genome_compatibility.go index a524ae3..02f00ef 100644 --- a/neat/genetics/genome_compatibility.go +++ b/neat/genetics/genome_compatibility.go @@ -1,7 +1,7 @@ package genetics import ( - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/neat" "math" ) diff --git a/neat/genetics/genome_compatibility_test.go b/neat/genetics/genome_compatibility_test.go index 8a737c4..2ae102f 100644 --- a/neat/genetics/genome_compatibility_test.go +++ b/neat/genetics/genome_compatibility_test.go @@ -3,8 +3,8 @@ package genetics import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/network" "testing" ) diff --git a/neat/genetics/genome_mutate.go b/neat/genetics/genome_mutate.go index e317822..03fef18 100644 --- a/neat/genetics/genome_mutate.go +++ b/neat/genetics/genome_mutate.go @@ -1,11 +1,11 @@ package genetics import ( - "errors" "fmt" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/math" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/pkg/errors" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" + "github.com/yaricom/goNEAT/v4/neat/network" "math/rand" ) @@ -107,7 +107,7 @@ func (g *Genome) mutateConnectSensors(innovations InnovationsObserver, _ *neat.O newInnovation := NewInnovationForLink(sensor.Id, output.Id, nextInnovId, newWeight, traitNum) innovations.StoreInnovation(*newInnovation) - } else if gene != nil && g.hasGene(gene) { + } else if gene != nil && g.haveGene(gene) { // The gene for already occurred innovation already in this genome. // This may happen as result of parent genome mutation in current epoch which is // repeated in the child after parent's genome transferred to child during mating @@ -119,7 +119,7 @@ func (g *Genome) mutateConnectSensors(innovations InnovationsObserver, _ *neat.O // Now add the new Gene to the Genome if gene != nil { - g.Genes = geneInsert(g.Genes, gene) + g.geneInsert(gene) linkAdded = true } } @@ -129,11 +129,13 @@ func (g *Genome) mutateConnectSensors(innovations InnovationsObserver, _ *neat.O // Mutate the genome by adding a new link between two random NNodes, // if NNodes are already connected, keep trying conf.NewLinkTries times -func (g *Genome) mutateAddLink(innovations InnovationsObserver, opts *neat.Options) (bool, error) { +func (g *Genome) mutateAddLink(innovations InnovationsObserver, generation int, opts *neat.Options) (bool, error) { // If the phenotype does not exist, exit on false, print error // Note: This should never happen - if it does there is a bug if g.Phenotype == nil { - return false, errors.New("attempt to add link to genome with no phenotype") + if _, err := g.Genesis(generation); err != nil { + return false, errors.Wrap(err, "genesis failed while trying to add link") + } } else if len(g.Nodes) == 0 { return false, errors.New("genome has no nodes to be connected by new link") } @@ -275,7 +277,7 @@ func (g *Genome) mutateAddLink(innovations InnovationsObserver, opts *neat.Optio innovation := NewInnovationForRecurrentLink(node1.Id, node2.Id, nextInnovId, newWeight, traitNum, doRecur) innovations.StoreInnovation(*innovation) - } else if gene != nil && g.hasGene(gene) { + } else if gene != nil && g.haveGene(gene) { // The gene for already occurred innovation already in this genome. // This may happen as result of parent genome mutation in current epoch which is // repeated in the child after parent's genome transferred to child during mating @@ -293,7 +295,7 @@ func (g *Genome) mutateAddLink(innovations InnovationsObserver, opts *neat.Optio // Now add the new Gene to the Genome if gene != nil { - g.Genes = geneInsert(g.Genes, gene) + g.geneInsert(gene) } } @@ -367,7 +369,7 @@ func (g *Genome) mutateAddNode(innovations InnovationsObserver, nodeIdGenerator -A new node -Stuck between the same nodes as were chosen for this mutation -Splitting the same gene as chosen for this mutation - If so, we know this mutation is not a novel innovation in this generation + If so, we know this mutation is not a novel innovation in this generation, so we make it match the original, identical mutation which occurred elsewhere in the population by coincidence */ if inn.innovationType == newNodeInnType && @@ -418,7 +420,7 @@ func (g *Genome) mutateAddNode(innovations InnovationsObserver, nodeIdGenerator // Store innovation innovation := NewInnovationForNode(inNode.Id, outNode.Id, gene1Innovation, gene2Innovation, node.Id, gene.InnovationNum) innovations.StoreInnovation(*innovation) - } else if node != nil && g.hasNode(node) { + } else if node != nil && g.haveNode(node.Id) { // The same add node innovation occurred in the same genome (parent) - just skip. // This may happen when parent of this organism experienced the same mutation in current epoch earlier // and after that parent's genome was duplicated to child by mating and the same mutation parameters @@ -433,9 +435,9 @@ func (g *Genome) mutateAddNode(innovations InnovationsObserver, nodeIdGenerator // Now add the new NNode and new Genes to the Genome if node != nil && gene1 != nil && gene2 != nil { - g.Genes = geneInsert(g.Genes, gene1) - g.Genes = geneInsert(g.Genes, gene2) - g.Nodes = nodeInsert(g.Nodes, node) + g.geneInsert(gene1) + g.geneInsert(gene2) + g.nodeInsert(node) return true, nil } // failed to create node or connecting genes diff --git a/neat/genetics/genome_mutate_test.go b/neat/genetics/genome_mutate_test.go index 8b0b83a..6849607 100644 --- a/neat/genetics/genome_mutate_test.go +++ b/neat/genetics/genome_mutate_test.go @@ -3,9 +3,9 @@ package genetics import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/math" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" + "github.com/yaricom/goNEAT/v4/neat/network" "math/rand" "testing" ) @@ -29,7 +29,7 @@ func TestGenome_mutateAddLink(t *testing.T) { _, err = gnome1.Genesis(1) require.NoError(t, err, "genesis failed") - res, err := gnome1.mutateAddLink(pop, context) + res, err := gnome1.mutateAddLink(pop, 1, context) require.NoError(t, err, "failed to add link") require.True(t, res, "New link not added") @@ -48,11 +48,11 @@ func TestGenome_mutateAddLink(t *testing.T) { {Id: 5, NeuronType: network.HiddenNeuron, ActivationType: math.SigmoidSteepenedActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)}, {Id: 6, NeuronType: network.InputNeuron, ActivationType: math.SigmoidSteepenedActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)}, } - gnome1.Nodes = append(gnome1.Nodes, nodes...) + gnome1.addNodes(nodes) _, err = gnome1.Genesis(1) // do network genesis with new nodes added require.NoError(t, err, "genesis failed") - res, err = gnome1.mutateAddLink(pop, context) + res, err = gnome1.mutateAddLink(pop, 1, context) require.NoError(t, err, "failed to add link") require.True(t, res, "New link not added") @@ -93,7 +93,7 @@ func TestGenome_mutateConnectSensors(t *testing.T) { ActivationType: math.SigmoidSteepenedActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)} - gnome1.Nodes = append(gnome1.Nodes, node) + gnome1.addNode(node) // Create gnome phenotype _, err = gnome1.Genesis(1) require.NoError(t, err, "genesis failed") diff --git a/neat/genetics/genome_reader.go b/neat/genetics/genome_reader.go index 3b0a4c9..92d2333 100644 --- a/neat/genetics/genome_reader.go +++ b/neat/genetics/genome_reader.go @@ -5,9 +5,9 @@ import ( "errors" "fmt" "github.com/spf13/cast" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/math" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" + "github.com/yaricom/goNEAT/v4/neat/network" "gopkg.in/yaml.v3" "io" "os" @@ -55,11 +55,7 @@ func (r *plainGenomeReader) Encoding() GenomeEncoding { } func (r *plainGenomeReader) Read() (*Genome, error) { - gnome := Genome{ - Traits: make([]*neat.Trait, 0), - Nodes: make([]*network.NNode, 0), - Genes: make([]*Gene, 0), - } + gnome := newGenome(0, make([]*neat.Trait, 0), make([]*network.NNode, 0), make([]*Gene, 0), nil) var gId int // Loop until file is finished, parsing each line @@ -93,10 +89,10 @@ func (r *plainGenomeReader) Read() (*Genome, error) { return nil, err } // check that node ID is unique - if prevNode := NodeWithId(newNode.Id, gnome.Nodes); prevNode != nil { + if gnome.haveNode(newNode.Id) { return nil, fmt.Errorf("node ID: %d is not unique", newNode.Id) } - gnome.Nodes = append(gnome.Nodes, newNode) + gnome.addNode(newNode) case "gene": // Read a Gene @@ -123,7 +119,7 @@ func (r *plainGenomeReader) Read() (*Genome, error) { if err := scanner.Err(); err != nil { return nil, err } - return &gnome, nil + return gnome, nil } // The method to read Trait in plain text format @@ -144,7 +140,7 @@ func readPlainTrait(r io.Reader) (*neat.Trait, error) { // Read a Network Node from specified Reader in plain text format // and applies corresponding trait to it from a list of traits provided func readPlainNetworkNode(r io.Reader, traits []*neat.Trait) (*network.NNode, error) { - n := network.NewNetworkNode() + node := network.NewNetworkNode() buff := bufio.NewReader(r) line, _, err := buff.ReadLine() if err != nil { @@ -157,25 +153,25 @@ func readPlainNetworkNode(r io.Reader, traits []*neat.Trait) (*network.NNode, er if nId, err := strconv.ParseInt(parts[0], 10, 32); err != nil { return nil, err } else { - n.Id = int(nId) + node.Id = int(nId) } if traitId, err := strconv.ParseInt(parts[1], 10, 32); err != nil { return nil, err } else { - n.Trait = TraitWithId(int(traitId), traits) + node.Trait = TraitWithId(int(traitId), traits) } if neuronType, err := strconv.ParseInt(parts[3], 10, 8); err != nil { return nil, err } else { - n.NeuronType = network.NodeNeuronType(neuronType) + node.NeuronType = network.NodeNeuronType(neuronType) } if len(parts) == 5 { - n.ActivationType, err = math.NodeActivators.ActivationTypeFromName(parts[4]) + node.ActivationType, err = math.NodeActivators.ActivationTypeFromName(parts[4]) } - return n, err + return node, err } // Reads Gene from reader in plain text format @@ -234,13 +230,7 @@ func (r *yamlGenomeReader) Read() (*Genome, error) { if err != nil { return nil, err } - gnome := &Genome{ - Id: genId, - Traits: make([]*neat.Trait, 0), - Nodes: make([]*network.NNode, 0), - Genes: make([]*Gene, 0), - ControlGenes: make([]*MIMOControlGene, 0), - } + gnome := newGenome(genId, make([]*neat.Trait, 0), make([]*network.NNode, 0), make([]*Gene, 0), make([]*MIMOControlGene, 0)) // read traits traits := gm["traits"].([]interface{}) @@ -264,10 +254,10 @@ func (r *yamlGenomeReader) Read() (*Genome, error) { return nil, err } // check that node ID is unique - if prevNode := NodeWithId(node.Id, gnome.Nodes); prevNode != nil { + if gnome.haveNode(node.Id) { return nil, fmt.Errorf("node ID: %d is not unique", node.Id) } - gnome.Nodes = append(gnome.Nodes, node) + gnome.addNode(node) } // read Genes @@ -289,7 +279,7 @@ func (r *yamlGenomeReader) Read() (*Genome, error) { return nil, err } // check that control node ID is unique - if prevNode := NodeWithId(mGene.ControlNode.Id, gnome.Nodes); prevNode != nil { + if gnome.haveNode(mGene.ControlNode.Id) { return nil, fmt.Errorf("control node ID: %d is not unique", mGene.ControlNode.Id) } gnome.ControlGenes = append(gnome.ControlGenes, mGene) @@ -423,19 +413,19 @@ func readMIMOControlGene(conf map[string]interface{}, traits []*neat.Trait, node // Reads NNode configuration func readNNode(conf map[string]interface{}, traits []*neat.Trait) (*network.NNode, error) { - nd := network.NewNetworkNode() - nd.Id = conf["id"].(int) + node := network.NewNetworkNode() + node.Id = conf["id"].(int) traitId := conf["trait_id"].(int) - nd.Trait = TraitWithId(traitId, traits) + node.Trait = TraitWithId(traitId, traits) typeName := conf["type"].(string) var err error - nd.NeuronType, err = network.NeuronTypeByName(typeName) + node.NeuronType, err = network.NeuronTypeByName(typeName) if err != nil { return nil, err } activation := conf["activation"].(string) - nd.ActivationType, err = math.NodeActivators.ActivationTypeFromName(activation) - return nd, err + node.ActivationType, err = math.NodeActivators.ActivationTypeFromName(activation) + return node, err } // Reads Trait configuration diff --git a/neat/genetics/genome_reader_test.go b/neat/genetics/genome_reader_test.go index b8dd213..ea5ebbf 100644 --- a/neat/genetics/genome_reader_test.go +++ b/neat/genetics/genome_reader_test.go @@ -4,9 +4,9 @@ import ( "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/math" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" + "github.com/yaricom/goNEAT/v4/neat/network" "os" "strings" "testing" diff --git a/neat/genetics/genome_reproduce.go b/neat/genetics/genome_reproduce.go index cf91014..b2fa677 100644 --- a/neat/genetics/genome_reproduce.go +++ b/neat/genetics/genome_reproduce.go @@ -2,8 +2,8 @@ package genetics import ( "fmt" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/network" "math/rand" ) @@ -101,13 +101,13 @@ func (g *Genome) mateMultipoint(og *Genome, genomeId int, fitness1, fitness2 flo chosenGene = p1gene i1++ if !p1better { - skip = true // Skip excess from the worse genome + skip = true // Skip disjoint from the worse genome } } else { chosenGene = p2gene i2++ if p1better { - skip = true // Skip excess from the worse genome + skip = true // Skip disjoint from the worse genome } } } @@ -116,10 +116,12 @@ func (g *Genome) mateMultipoint(og *Genome, genomeId int, fitness1, fitness2 flo // skip=false // Check to see if the chosen gene conflicts with an already chosen gene i.e. do they represent the same link - for _, gene := range newGenes { - if gene.Link.IsEqualGenetically(chosenGene.Link) { - skip = true - break + if !skip { + for _, gene := range newGenes { + if gene.Link.IsEqualGenetically(chosenGene.Link) { + skip = true + break + } } } @@ -315,13 +317,13 @@ func (g *Genome) mateMultipointAvg(og *Genome, genomeId int, fitness1, fitness2 chosenGene = p1gene i1++ if !p1better { - skip = true // Skip excess from the worse genome + skip = true // Skip disjoint from the worse genome } } else { chosenGene = p2gene i2++ if p1better { - skip = true // Skip excess from the worse genome + skip = true // Skip disjoint from the worse genome } } } @@ -330,10 +332,12 @@ func (g *Genome) mateMultipointAvg(og *Genome, genomeId int, fitness1, fitness2 // skip=false // Check to see if the chosen gene conflicts with an already chosen gene i.e. do they represent the same link - for _, gene := range newGenes { - if gene.Link.IsEqualGenetically(chosenGene.Link) { - skip = true - break + if !skip { + for _, gene := range newGenes { + if gene.Link.IsEqualGenetically(chosenGene.Link) { + skip = true + break + } } } @@ -557,10 +561,12 @@ func (g *Genome) mateSinglePoint(og *Genome, genomeId int) (*Genome, error) { } // Check to see if the chosen gene conflicts with an already chosen gene i.e. do they represent the same link - for _, gene := range newGenes { - if gene.Link.IsEqualGenetically(chosenGene.Link) { - skip = true - break + if !skip { + for _, gene := range newGenes { + if gene.Link.IsEqualGenetically(chosenGene.Link) { + skip = true + break + } } } diff --git a/neat/genetics/genome_reproduce_test.go b/neat/genetics/genome_reproduce_test.go index 47138da..929a7b5 100644 --- a/neat/genetics/genome_reproduce_test.go +++ b/neat/genetics/genome_reproduce_test.go @@ -3,7 +3,7 @@ package genetics import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat/network" "math/rand" "testing" ) diff --git a/neat/genetics/genome_test.go b/neat/genetics/genome_test.go index 5956da5..cc794a8 100644 --- a/neat/genetics/genome_test.go +++ b/neat/genetics/genome_test.go @@ -3,9 +3,9 @@ package genetics import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/math" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" + "github.com/yaricom/goNEAT/v4/neat/network" "math/rand" "testing" ) @@ -55,7 +55,7 @@ func buildTestModularGenome(id int) *Genome { {Id: 6, NeuronType: network.HiddenNeuron, ActivationType: math.LinearActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)}, {Id: 7, NeuronType: network.HiddenNeuron, ActivationType: math.NullActivation, Incoming: make([]*network.Link, 0), Outgoing: make([]*network.Link, 0)}, } - gnome.Nodes = append(gnome.Nodes, ioNodes...) + gnome.addNodes(ioNodes) // connect added nodes ioConnGenes := []*Gene{ @@ -85,9 +85,16 @@ func buildTestModularGenome(id int) *Genome { func TestGenome_NewGenomeRand(t *testing.T) { rand.Seed(42) newId, in, out, n := 1, 3, 2, 2 + opts := &neat.Options{ + CompatThreshold: 0.5, + PopSize: 10, + NodeActivators: []math.NodeActivationType{math.GaussianBipolarActivation}, + NodeActivatorsProb: []float64{1.0}, + } - gnome := newGenomeRand(newId, in, out, n, 5, false, 0.5) - require.NotNil(t, gnome, "Failed to create random genome") + gnome, err := newGenomeRand(newId, in, out, n, 5, false, 0.5, opts) + require.NoError(t, err, "failed to create random genome") + require.NotNil(t, gnome, "random genome expected") assert.Len(t, gnome.Nodes, in+n+out, "failed to create nodes") assert.True(t, len(gnome.Genes) >= in+n+out, "Failed to create genes") } @@ -220,13 +227,10 @@ func TestGene_Verify(t *testing.T) { func TestGenome_geneInsert(t *testing.T) { gnome := buildTestGenome(1) gnome.Genes = append(gnome.Genes, NewConnectionGene(network.NewLinkWithTrait(gnome.Traits[2], 5.5, gnome.Nodes[2], gnome.Nodes[3], false), 5, 0, false)) - genes := geneInsert(gnome.Genes, NewConnectionGene(network.NewLinkWithTrait(gnome.Traits[2], 5.5, gnome.Nodes[2], gnome.Nodes[3], false), 4, 0, false)) - if len(genes) != 5 { - t.Error("len(genes) != 5", len(genes)) - return - } + gnome.geneInsert(NewConnectionGene(network.NewLinkWithTrait(gnome.Traits[2], 5.5, gnome.Nodes[2], gnome.Nodes[3], false), 4, 0, false)) + require.Equal(t, 5, len(gnome.Genes), "wrong genes number") - for i, g := range genes { + for i, g := range gnome.Genes { if g.InnovationNum != int64(i+1) { t.Error("(g.InnovationNum != i + 1)", g.InnovationNum, i+1) } diff --git a/neat/genetics/genome_writer.go b/neat/genetics/genome_writer.go index dfaaccf..fd588e5 100644 --- a/neat/genetics/genome_writer.go +++ b/neat/genetics/genome_writer.go @@ -3,9 +3,9 @@ package genetics import ( "bufio" "fmt" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/math" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" + "github.com/yaricom/goNEAT/v4/neat/network" "gopkg.in/yaml.v3" "io" ) diff --git a/neat/genetics/genome_writer_test.go b/neat/genetics/genome_writer_test.go index 49c3599..6fcbe3c 100644 --- a/neat/genetics/genome_writer_test.go +++ b/neat/genetics/genome_writer_test.go @@ -6,8 +6,8 @@ import ( "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/network" "strings" "testing" ) diff --git a/neat/genetics/mimo_gene.go b/neat/genetics/mimo_gene.go index 0fadd44..5137ae4 100644 --- a/neat/genetics/mimo_gene.go +++ b/neat/genetics/mimo_gene.go @@ -2,7 +2,7 @@ package genetics import ( "fmt" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat/network" ) // MIMOControlGene The Multiple-Input Multiple-Output (MIMO) control Gene allows creating modular genomes, in which several groups of genes diff --git a/neat/genetics/organism.go b/neat/genetics/organism.go index daa4b17..b4464cb 100644 --- a/neat/genetics/organism.go +++ b/neat/genetics/organism.go @@ -3,7 +3,7 @@ package genetics import ( "bytes" "fmt" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat/network" ) // Organisms represents sortable list of organisms by fitness @@ -25,8 +25,6 @@ type Organism struct { // Win marker (if needed for a particular task) IsWinner bool - // The Organism's phenotype - Phenotype *network.Network // The Organism's genotype Genotype *Genome // The Species of the Organism @@ -41,6 +39,9 @@ type Organism struct { // Implemented as ANY to allow implementation specific objects. Data *OrganismData + // The Organism's phenotype + orgPhenotype *network.Network + // A fitness measure that won't change during fitness adjustments of population's epoch evaluation originalFitness float64 @@ -69,29 +70,35 @@ type Organism struct { // NewOrganism Creates new organism with specified genome, fitness and given generation number func NewOrganism(fit float64, g *Genome, generation int) (org *Organism, err error) { - phenotype := g.Phenotype - if phenotype == nil { - phenotype, err = g.Genesis(g.Id) + org = &Organism{ + Fitness: fit, + Genotype: g, + orgPhenotype: g.Phenotype, + Generation: generation, + } + return org, nil +} + +// Phenotype is to get phenotype of this organism. If phenotype was not built yet, then it would be created first +// as a result of this method invocation +func (o *Organism) Phenotype() (*network.Network, error) { + if o.orgPhenotype == nil { + phenotype, err := o.Genotype.Genesis(o.Genotype.Id) if err != nil { return nil, err } + o.orgPhenotype = phenotype } - org = &Organism{ - Fitness: fit, - Genotype: g, - Phenotype: phenotype, - Generation: generation, - } - return org, nil + return o.orgPhenotype, nil } // UpdatePhenotype Regenerate the underlying network graph based on a change in the genotype func (o *Organism) UpdatePhenotype() (err error) { // First, delete the old phenotype (net) - o.Phenotype = nil + o.orgPhenotype = nil // Now, recreate the phenotype off the new genotype - o.Phenotype, err = o.Genotype.Genesis(o.Genotype.Id) + o.orgPhenotype, err = o.Genotype.Genesis(o.Genotype.Id) return err } @@ -124,8 +131,6 @@ func (o *Organism) UnmarshalBinary(data []byte) error { return err } else if o.Genotype, err = ReadGenome(b, genotypeId); err != nil { return err - } else if o.Phenotype, err = o.Genotype.Genesis(genotypeId); err != nil { - return err } return nil @@ -150,12 +155,12 @@ func (o *Organism) Dump() string { _, _ = fmt.Fprintln(b, "Fitness: ", o.Fitness) _, _ = fmt.Fprintln(b, "Error: ", o.Error) _, _ = fmt.Fprintln(b, "IsWinner: ", o.IsWinner) - _, _ = fmt.Fprintln(b, "Phenotype: ", o.Phenotype) + _, _ = fmt.Fprintln(b, "Phenotype: ", o.orgPhenotype) _, _ = fmt.Fprintln(b, "Genotype: ", o.Genotype) _, _ = fmt.Fprintln(b, "Species: ", o.Species) _, _ = fmt.Fprintln(b, "ExpectedOffspring: ", o.ExpectedOffspring) _, _ = fmt.Fprintln(b, "Data: ", o.Data) - _, _ = fmt.Fprintln(b, "Phenotype: ", o.Phenotype) + _, _ = fmt.Fprintln(b, "Phenotype: ", o.orgPhenotype) _, _ = fmt.Fprintln(b, "originalFitness: ", o.originalFitness) _, _ = fmt.Fprintln(b, "toEliminate: ", o.toEliminate) _, _ = fmt.Fprintln(b, "isChampion: ", o.isChampion) @@ -183,14 +188,8 @@ func (f Organisms) Less(i, j int) bool { // try to promote most fit organisms return true // lower fitness is less } else if f[i].Fitness == f[j].Fitness { - // try to promote less complex organisms - ci := f[i].Phenotype.Complexity() - cj := f[j].Phenotype.Complexity() - if ci > cj { - return true // higher complexity is less - } else if ci == cj { - return f[i].Genotype.Id < f[j].Genotype.Id // least recent (older) is less - } + // try to promote children of the population champion + return f[i].highestFitness < f[j].highestFitness } return false } diff --git a/neat/genetics/organism_test.go b/neat/genetics/organism_test.go index eb553c2..3653c46 100644 --- a/neat/genetics/organism_test.go +++ b/neat/genetics/organism_test.go @@ -45,6 +45,24 @@ func TestOrganisms(t *testing.T) { } } +func TestOrganism_Phenotype(t *testing.T) { + gnome := buildTestGenome(1) + organism, err := NewOrganism(rand.Float64(), gnome, 1) + require.NoError(t, err) + + phenotype, err := organism.Phenotype() + require.NoError(t, err) + require.NotNil(t, phenotype) + + assert.Equal(t, 4, phenotype.NodeCount(), "wrong nodes count") + assert.Equal(t, 3, phenotype.LinkCount(), "wrong links count") + + // check that phenotype not created twice + other, err := organism.Phenotype() + require.NoError(t, err) + assert.True(t, phenotype == other, "must be the same pointer") +} + func TestOrganism_MarshalBinary(t *testing.T) { gnome := buildTestGenome(1) org, err := NewOrganism(rand.Float64(), gnome, 1) @@ -95,10 +113,10 @@ func TestOrganism_UpdatePhenotype(t *testing.T) { org, err := NewOrganism(rand.Float64(), gnome, 1) require.NoError(t, err, "failed to create organism") - org.Phenotype = nil - assert.Nil(t, org.Phenotype, "no phenotype expected") + org.orgPhenotype = nil + assert.Nil(t, org.orgPhenotype, "no phenotype expected") err = org.UpdatePhenotype() require.NoError(t, err, "failed to recreate phenotype") - assert.NotNil(t, org.Phenotype) + assert.NotNil(t, org.orgPhenotype) } diff --git a/neat/genetics/population.go b/neat/genetics/population.go index 41b8a87..5025d8d 100644 --- a/neat/genetics/population.go +++ b/neat/genetics/population.go @@ -2,9 +2,9 @@ package genetics import ( "context" - "errors" "fmt" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/pkg/errors" + "github.com/yaricom/goNEAT/v4/neat" "math" "math/rand" "sync" @@ -78,7 +78,10 @@ func NewPopulationRandom(in, out, maxHidden int, recurrent bool, linkProb float6 pop := newPopulation() for count := 0; count < opts.PopSize; count++ { - gen := newGenomeRand(count, in, out, rand.Intn(maxHidden), maxHidden, recurrent, linkProb) + gen, err := newGenomeRand(count, in, out, rand.Intn(maxHidden), maxHidden, recurrent, linkProb, opts) + if err != nil { + return nil, errors.Wrap(err, "failed to create random population") + } org, err := NewOrganism(0.0, gen, 1) if err != nil { return nil, err @@ -139,7 +142,7 @@ func (p *Population) Innovations() []Innovation { return p.innovations } -// Create a population from Genome g. The new Population will have the same topology as g +// spawn creates a population from Genome g. The new Population will have the same topology as g // with link weights slightly perturbed from g's func (p *Population) spawn(g *Genome, opts *neat.Options) (err error) { for count := 0; count < opts.PopSize; count++ { diff --git a/neat/genetics/population_epoch.go b/neat/genetics/population_epoch.go index 2b11a15..66cf838 100644 --- a/neat/genetics/population_epoch.go +++ b/neat/genetics/population_epoch.go @@ -5,7 +5,7 @@ import ( "context" "encoding/gob" "fmt" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/neat" "sort" "sync" ) diff --git a/neat/genetics/population_epoch_test.go b/neat/genetics/population_epoch_test.go index 2b2367a..9518555 100644 --- a/neat/genetics/population_epoch_test.go +++ b/neat/genetics/population_epoch_test.go @@ -4,7 +4,8 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" "math/rand" "testing" ) @@ -36,26 +37,30 @@ func parallelExecutorNextEpoch(pop *Population, opts *neat.Options) error { func TestPopulationEpochExecutor_NextEpoch(t *testing.T) { rand.Seed(42) - in, out, nmax, n := 3, 2, 15, 3 + in, out, maxHidden, n := 3, 2, 15, 3 linkProb := 0.8 - conf := neat.Options{ - CompatThreshold: 0.5, - DropOffAge: 1, - PopSize: 30, - BabiesStolen: 10, - RecurOnlyProb: 0.2, + conf := &neat.Options{ + CompatThreshold: 0.5, + DropOffAge: 1, + PopSize: 30, + BabiesStolen: 10, + RecurOnlyProb: 0.2, + NodeActivators: []math.NodeActivationType{math.GaussianBipolarActivation}, + NodeActivatorsProb: []float64{1.0}, } neat.LogLevel = neat.LogLevelInfo - gen := newGenomeRand(1, in, out, n, nmax, false, linkProb) - pop, err := NewPopulation(gen, &conf) + gen, err := newGenomeRand(1, in, out, n, maxHidden, false, linkProb, conf) + require.NoError(t, err, "failed to create random genome") + + pop, err := NewPopulation(gen, conf) require.NoError(t, err, "failed to create population") require.NotNil(t, pop, "population expected") // test sequential executor - err = sequentialExecutorNextEpoch(pop, &conf) + err = sequentialExecutorNextEpoch(pop, conf) assert.NoError(t, err, "failed to run sequential epoch executor") // test parallel executor - err = parallelExecutorNextEpoch(pop, &conf) + err = parallelExecutorNextEpoch(pop, conf) assert.NoError(t, err, "failed to run parallel epoch executor") } diff --git a/neat/genetics/population_io.go b/neat/genetics/population_io.go index 787e68c..4b96c8b 100644 --- a/neat/genetics/population_io.go +++ b/neat/genetics/population_io.go @@ -4,7 +4,7 @@ import ( "bufio" "bytes" "fmt" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/neat" "io" "strconv" "strings" diff --git a/neat/genetics/population_io_test.go b/neat/genetics/population_io_test.go index 30d7ad9..6f4b2bb 100644 --- a/neat/genetics/population_io_test.go +++ b/neat/genetics/population_io_test.go @@ -5,7 +5,7 @@ import ( "bytes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/neat" "strings" "testing" ) diff --git a/neat/genetics/population_test.go b/neat/genetics/population_test.go index 7695215..6bb500d 100644 --- a/neat/genetics/population_test.go +++ b/neat/genetics/population_test.go @@ -3,7 +3,8 @@ package genetics import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" "math/rand" "strings" "testing" @@ -11,13 +12,15 @@ import ( func TestNewPopulationRandom(t *testing.T) { rand.Seed(42) - in, out, nmax := 3, 2, 5 + in, out, maxHidden := 3, 2, 5 linkProb := 0.5 conf := neat.Options{ - CompatThreshold: 0.5, - PopSize: 10, + CompatThreshold: 0.5, + PopSize: 10, + NodeActivators: []math.NodeActivationType{math.GaussianBipolarActivation}, + NodeActivatorsProb: []float64{1.0}, } - pop, err := NewPopulationRandom(in, out, nmax, false, linkProb, &conf) + pop, err := NewPopulationRandom(in, out, maxHidden, false, linkProb, &conf) require.NoError(t, err, "failed to create population") require.NotNil(t, pop, "population expected") require.Len(t, pop.Organisms, conf.PopSize, "wrong population size") @@ -29,7 +32,6 @@ func TestNewPopulationRandom(t *testing.T) { assert.True(t, len(org.Genotype.Genes) > 0, "organism has no genes at: %d", i) assert.True(t, len(org.Genotype.Nodes) > 0, "organism has no nodes at: %d", i) assert.True(t, len(org.Genotype.Traits) > 0, "organism has no traits at: %d", i) - assert.NotNil(t, org.Genotype.Phenotype, "organism has no phenotype") } } @@ -37,13 +39,16 @@ func TestNewPopulation(t *testing.T) { rand.Seed(42) in, out, nmax, n := 3, 2, 5, 3 linkProb := 0.5 - conf := neat.Options{ - CompatThreshold: 0.5, - PopSize: 10, + conf := &neat.Options{ + CompatThreshold: 0.5, + PopSize: 10, + NodeActivators: []math.NodeActivationType{math.GaussianBipolarActivation}, + NodeActivatorsProb: []float64{1.0}, } - gen := newGenomeRand(1, in, out, n, nmax, false, linkProb) + gen, err := newGenomeRand(1, in, out, n, nmax, false, linkProb, conf) + require.NoError(t, err, "failed to create random genome") - pop, err := NewPopulation(gen, &conf) + pop, err := NewPopulation(gen, conf) require.NoError(t, err, "failed to create population") require.NotNil(t, pop, "population expected") require.Len(t, pop.Organisms, conf.PopSize, "wrong population size") @@ -60,13 +65,12 @@ func TestNewPopulation(t *testing.T) { assert.True(t, len(org.Genotype.Genes) > 0, "organism has no genes at: %d", i) assert.True(t, len(org.Genotype.Nodes) > 0, "organism has no nodes at: %d", i) assert.True(t, len(org.Genotype.Traits) > 0, "organism has no traits at: %d", i) - assert.NotNil(t, org.Genotype.Phenotype, "organism has no phenotype") } } func TestPopulation_verify(t *testing.T) { // first create population - popStr := "genomestart 1\n" + + genomeStr := "genomestart 1\n" + "trait 1 0.1 0 0 0 0 0 0 0\n" + "trait 2 0.2 0 0 0 0 0 0 0\n" + "trait 3 0.3 0 0 0 0 0 0 0\n" + @@ -93,7 +97,7 @@ func TestPopulation_verify(t *testing.T) { conf := neat.Options{ CompatThreshold: 0.5, } - pop, err := ReadPopulation(strings.NewReader(popStr), &conf) + pop, err := ReadPopulation(strings.NewReader(genomeStr), &conf) require.NoError(t, err, "failed to create population") require.NotNil(t, pop, "population expected") diff --git a/neat/genetics/species.go b/neat/genetics/species.go index 2e1fc3b..310d93c 100644 --- a/neat/genetics/species.go +++ b/neat/genetics/species.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/neat" "io" "math" "math/rand" @@ -315,10 +315,7 @@ func (s *Species) reproduce(ctx context.Context, generation int, pop *Population } } else { // Sometimes we add a link to a superchamp - if _, err = newGenome.Genesis(generation); err != nil { - return nil, err - } - if _, err = newGenome.mutateAddLink(pop, opts); err != nil { + if _, err = newGenome.mutateAddLink(pop, generation, opts); err != nil { return nil, err } mutStructBaby = true @@ -380,11 +377,7 @@ func (s *Species) reproduce(ctx context.Context, generation int, pop *Population } else if rand.Float64() < opts.MutateAddLinkProb { neat.DebugLog("SPECIES: ---> mutateAddLink") - // Mutate add link - if _, err = newGenome.Genesis(generation); err != nil { - return nil, err - } - if _, err = newGenome.mutateAddLink(pop, opts); err != nil { + if _, err = newGenome.mutateAddLink(pop, generation, opts); err != nil { return nil, err } mutStructBaby = true @@ -495,11 +488,7 @@ func (s *Species) reproduce(ctx context.Context, generation int, pop *Population } else if rand.Float64() < opts.MutateAddLinkProb { neat.DebugLog("SPECIES: ---------> mutateAddLink") - // mutate_add_link - if _, err = newGenome.Genesis(generation); err != nil { - return nil, err - } - if _, err = newGenome.mutateAddLink(pop, opts); err != nil { + if _, err = newGenome.mutateAddLink(pop, generation, opts); err != nil { return nil, err } mutStructBaby = true @@ -581,15 +570,8 @@ func (f byOrganismOrigFitness) Less(i, j int) bool { // try to promote most fit species return true // Lower fitness is less } else if org1.originalFitness == org2.originalFitness { - // try to promote less complex species - c1 := org1.Phenotype.Complexity() - c2 := org2.Phenotype.Complexity() - if c1 > c2 { - return true // Higher complexity is "less" - } else if c1 == c2 { - // try to promote younger species - return f[i].Age > f[j].Age // Higher Age is Less - } + // try to promote younger species + return f[i].Age > f[j].Age // Higher Age is Less } return false } diff --git a/neat/genetics/species_test.go b/neat/genetics/species_test.go index a9b3ed2..9af7291 100644 --- a/neat/genetics/species_test.go +++ b/neat/genetics/species_test.go @@ -6,7 +6,8 @@ import ( "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" "math/rand" "sort" "testing" @@ -205,21 +206,27 @@ func TestSpecies_reproduce_fail(t *testing.T) { // Tests Species reproduce success func TestSpecies_reproduce(t *testing.T) { rand.Seed(42) - in, out, nmax, n := 3, 2, 15, 3 + in, out, maxHidden, n := 3, 2, 15, 3 linkProb := 0.8 // Configuration - opts := neat.Options{ - DropOffAge: 5, - SurvivalThresh: 0.5, - AgeSignificance: 0.5, - PopSize: 30, - CompatThreshold: 0.6, + opts := &neat.Options{ + DropOffAge: 5, + SurvivalThresh: 0.5, + AgeSignificance: 0.5, + PopSize: 30, + CompatThreshold: 0.6, + MutateAddLinkProb: 0.9, + MutateAddNodeProb: 0.9, + NodeActivators: []math.NodeActivationType{math.SigmoidApproximationActivation, math.SigmoidBipolarActivation}, + NodeActivatorsProb: []float64{0.5, 0.5}, } neat.LogLevel = neat.LogLevelInfo - gen := newGenomeRand(1, in, out, n, nmax, false, linkProb) - pop, err := NewPopulation(gen, &opts) + gen, err := newGenomeRand(1, in, out, n, maxHidden, false, linkProb, opts) + require.NoError(t, err, "failed to create random genome") + + pop, err := NewPopulation(gen, opts) require.NoError(t, err, "failed to create population") require.NotNil(t, pop, "population expected") diff --git a/neat/neat.go b/neat/neat.go index cadc2cc..e671c03 100644 --- a/neat/neat.go +++ b/neat/neat.go @@ -6,14 +6,12 @@ import ( "context" "fmt" "github.com/pkg/errors" - "github.com/spf13/cast" - "github.com/yaricom/goNEAT/v3/neat/math" - "gopkg.in/yaml.v3" - "io" - "io/ioutil" - "os" - "strconv" - "strings" + "github.com/yaricom/goNEAT/v4/neat/math" +) + +var ( + ErrNoActivatorsRegistered = errors.New("no node activators registered with NEAT options, please assign at least one to NodeActivators") + ErrActivatorsProbabilitiesNumberMismatch = errors.New("number of node activator probabilities doesn't match number of activators") ) // GenomeCompatibilityMethod defines the method to calculate genomes compatibility @@ -142,11 +140,18 @@ type Options struct { // RandomNodeActivationType Returns next random node activation type among registered with this context func (c *Options) RandomNodeActivationType() (math.NodeActivationType, error) { + if len(c.NodeActivators) == 0 { + return 0, ErrNoActivatorsRegistered + } // quick check for the most cases if len(c.NodeActivators) == 1 { return c.NodeActivators[0], nil } - // find next random + + // find random activator + if len(c.NodeActivators) != len(c.NodeActivatorsProb) { + return 0, ErrActivatorsProbabilitiesNumberMismatch + } index := math.SingleRouletteThrow(c.NodeActivatorsProb) if index < 0 || index >= len(c.NodeActivators) { return 0, fmt.Errorf("unexpected error when trying to find random node activator, activator index: %d", index) @@ -154,31 +159,6 @@ func (c *Options) RandomNodeActivationType() (math.NodeActivationType, error) { return c.NodeActivators[index], nil } -// set default values for activator type and its probability of selection -func (c *Options) initNodeActivators() (err error) { - if len(c.NodeActivatorsWithProbs) == 0 { - c.NodeActivators = []math.NodeActivationType{math.SigmoidSteepenedActivation} - c.NodeActivatorsProb = []float64{1.0} - return nil - } - // create activators - actFns := c.NodeActivatorsWithProbs - c.NodeActivators = make([]math.NodeActivationType, len(actFns)) - c.NodeActivatorsProb = make([]float64, len(actFns)) - for i, line := range actFns { - fields := strings.Fields(line) - if c.NodeActivators[i], err = math.NodeActivators.ActivationTypeFromName(fields[0]); err != nil { - return err - } - if prob, err := strconv.ParseFloat(fields[1], 64); err != nil { - return err - } else { - c.NodeActivatorsProb[i] = prob - } - } - return nil -} - // Validate is to validate that this options has valid values func (c *Options) Validate() error { if err := c.EpochExecutorType.Validate(); err != nil { @@ -188,156 +168,19 @@ func (c *Options) Validate() error { if err := c.GenCompatMethod.Validate(); err != nil { return err } - return nil -} -// NeatContext is to get Context which carries NEAT options inside to be propagated -func (c *Options) NeatContext() context.Context { - return NewContext(context.Background(), c) -} - -// LoadYAMLOptions is to load NEAT options encoded as YAML file -func LoadYAMLOptions(r io.Reader) (*Options, error) { - content, err := ioutil.ReadAll(r) - if err != nil { - return nil, err + // check activators + if len(c.NodeActivators) == 0 { + return ErrNoActivatorsRegistered } - // read options - var opts Options - if err = yaml.Unmarshal(content, &opts); err != nil { - return nil, errors.Wrap(err, "failed to decode NEAT options from YAML") - } - - // initialize logger - if err = InitLogger(opts.LogLevel); err != nil { - return nil, errors.Wrap(err, "failed to initialize logger") + if len(c.NodeActivators) != len(c.NodeActivatorsProb) { + return ErrActivatorsProbabilitiesNumberMismatch } - // read node activators - if err = opts.initNodeActivators(); err != nil { - return nil, errors.Wrap(err, "failed to read node activators") - } - - if err = opts.Validate(); err != nil { - return nil, errors.Wrap(err, "invalid NEAT options") - } - - return &opts, nil -} - -// LoadNeatOptions Loads NEAT options configuration from provided reader encode in plain text format (.neat) -func LoadNeatOptions(r io.Reader) (*Options, error) { - c := &Options{} - // read configuration - var name string - var param string - for { - _, err := fmt.Fscanf(r, "%s %v\n", &name, ¶m) - if err == io.EOF { - break - } else if err != nil { - return nil, err - } - switch name { - case "trait_param_mut_prob": - c.TraitParamMutProb = cast.ToFloat64(param) - case "trait_mutation_power": - c.TraitMutationPower = cast.ToFloat64(param) - case "weight_mut_power": - c.WeightMutPower = cast.ToFloat64(param) - case "disjoint_coeff": - c.DisjointCoeff = cast.ToFloat64(param) - case "excess_coeff": - c.ExcessCoeff = cast.ToFloat64(param) - case "mutdiff_coeff": - c.MutdiffCoeff = cast.ToFloat64(param) - case "compat_threshold": - c.CompatThreshold = cast.ToFloat64(param) - case "age_significance": - c.AgeSignificance = cast.ToFloat64(param) - case "survival_thresh": - c.SurvivalThresh = cast.ToFloat64(param) - case "mutate_only_prob": - c.MutateOnlyProb = cast.ToFloat64(param) - case "mutate_random_trait_prob": - c.MutateRandomTraitProb = cast.ToFloat64(param) - case "mutate_link_trait_prob": - c.MutateLinkTraitProb = cast.ToFloat64(param) - case "mutate_node_trait_prob": - c.MutateNodeTraitProb = cast.ToFloat64(param) - case "mutate_link_weights_prob": - c.MutateLinkWeightsProb = cast.ToFloat64(param) - case "mutate_toggle_enable_prob": - c.MutateToggleEnableProb = cast.ToFloat64(param) - case "mutate_gene_reenable_prob": - c.MutateGeneReenableProb = cast.ToFloat64(param) - case "mutate_add_node_prob": - c.MutateAddNodeProb = cast.ToFloat64(param) - case "mutate_add_link_prob": - c.MutateAddLinkProb = cast.ToFloat64(param) - case "mutate_connect_sensors": - c.MutateConnectSensors = cast.ToFloat64(param) - case "interspecies_mate_rate": - c.InterspeciesMateRate = cast.ToFloat64(param) - case "mate_multipoint_prob": - c.MateMultipointProb = cast.ToFloat64(param) - case "mate_multipoint_avg_prob": - c.MateMultipointAvgProb = cast.ToFloat64(param) - case "mate_singlepoint_prob": - c.MateSinglepointProb = cast.ToFloat64(param) - case "mate_only_prob": - c.MateOnlyProb = cast.ToFloat64(param) - case "recur_only_prob": - c.RecurOnlyProb = cast.ToFloat64(param) - case "pop_size": - c.PopSize = cast.ToInt(param) - case "dropoff_age": - c.DropOffAge = cast.ToInt(param) - case "newlink_tries": - c.NewLinkTries = cast.ToInt(param) - case "print_every": - c.PrintEvery = cast.ToInt(param) - case "babies_stolen": - c.BabiesStolen = cast.ToInt(param) - case "num_runs": - c.NumRuns = cast.ToInt(param) - case "num_generations": - c.NumGenerations = cast.ToInt(param) - case "epoch_executor": - c.EpochExecutorType = EpochExecutorType(param) - case "genome_compat_method": - c.GenCompatMethod = GenomeCompatibilityMethod(param) - case "log_level": - c.LogLevel = param - default: - return nil, errors.Errorf("unknown configuration parameter found: %s = %s", name, param) - } - } - // initialize logger - if err := InitLogger(c.LogLevel); err != nil { - return nil, errors.Wrap(err, "failed to initialize logger") - } - - if err := c.initNodeActivators(); err != nil { - return nil, err - } - if err := c.Validate(); err != nil { - return nil, err - } - - return c, nil + return nil } -// ReadNeatOptionsFromFile reads NEAT options from specified configFilePath automatically resolving config file encoding. -func ReadNeatOptionsFromFile(configFilePath string) (*Options, error) { - configFile, err := os.Open(configFilePath) - if err != nil { - return nil, errors.Wrap(err, "failed to open config file") - } - fileName := configFile.Name() - if strings.HasSuffix(fileName, "yml") || strings.HasSuffix(fileName, "yaml") { - return LoadYAMLOptions(configFile) - } else { - return LoadNeatOptions(configFile) - } +// NeatContext is to get Context which carries NEAT options inside to be propagated +func (c *Options) NeatContext() context.Context { + return NewContext(context.Background(), c) } diff --git a/neat/neat_options_readers.go b/neat/neat_options_readers.go new file mode 100644 index 0000000..b327c7c --- /dev/null +++ b/neat/neat_options_readers.go @@ -0,0 +1,183 @@ +package neat + +import ( + "fmt" + "github.com/pkg/errors" + "github.com/spf13/cast" + "github.com/yaricom/goNEAT/v4/neat/math" + "gopkg.in/yaml.v3" + "io" + "io/ioutil" + "os" + "strconv" + "strings" +) + +// LoadYAMLOptions is to load NEAT options encoded as YAML file +func LoadYAMLOptions(r io.Reader) (*Options, error) { + content, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + // read options + var opts Options + if err = yaml.Unmarshal(content, &opts); err != nil { + return nil, errors.Wrap(err, "failed to decode NEAT options from YAML") + } + + // initialize logger + if err = InitLogger(opts.LogLevel); err != nil { + return nil, errors.Wrap(err, "failed to initialize logger") + } + + // read node activators + if err = opts.initNodeActivators(); err != nil { + return nil, errors.Wrap(err, "failed to read node activators") + } + + if err = opts.Validate(); err != nil { + return nil, errors.Wrap(err, "invalid NEAT options") + } + + return &opts, nil +} + +// LoadNeatOptions Loads NEAT options configuration from provided reader encode in plain text format (.neat) +func LoadNeatOptions(r io.Reader) (*Options, error) { + c := &Options{} + // read configuration + var name string + var param string + for { + _, err := fmt.Fscanf(r, "%s %v\n", &name, ¶m) + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + switch name { + case "trait_param_mut_prob": + c.TraitParamMutProb = cast.ToFloat64(param) + case "trait_mutation_power": + c.TraitMutationPower = cast.ToFloat64(param) + case "weight_mut_power": + c.WeightMutPower = cast.ToFloat64(param) + case "disjoint_coeff": + c.DisjointCoeff = cast.ToFloat64(param) + case "excess_coeff": + c.ExcessCoeff = cast.ToFloat64(param) + case "mutdiff_coeff": + c.MutdiffCoeff = cast.ToFloat64(param) + case "compat_threshold": + c.CompatThreshold = cast.ToFloat64(param) + case "age_significance": + c.AgeSignificance = cast.ToFloat64(param) + case "survival_thresh": + c.SurvivalThresh = cast.ToFloat64(param) + case "mutate_only_prob": + c.MutateOnlyProb = cast.ToFloat64(param) + case "mutate_random_trait_prob": + c.MutateRandomTraitProb = cast.ToFloat64(param) + case "mutate_link_trait_prob": + c.MutateLinkTraitProb = cast.ToFloat64(param) + case "mutate_node_trait_prob": + c.MutateNodeTraitProb = cast.ToFloat64(param) + case "mutate_link_weights_prob": + c.MutateLinkWeightsProb = cast.ToFloat64(param) + case "mutate_toggle_enable_prob": + c.MutateToggleEnableProb = cast.ToFloat64(param) + case "mutate_gene_reenable_prob": + c.MutateGeneReenableProb = cast.ToFloat64(param) + case "mutate_add_node_prob": + c.MutateAddNodeProb = cast.ToFloat64(param) + case "mutate_add_link_prob": + c.MutateAddLinkProb = cast.ToFloat64(param) + case "mutate_connect_sensors": + c.MutateConnectSensors = cast.ToFloat64(param) + case "interspecies_mate_rate": + c.InterspeciesMateRate = cast.ToFloat64(param) + case "mate_multipoint_prob": + c.MateMultipointProb = cast.ToFloat64(param) + case "mate_multipoint_avg_prob": + c.MateMultipointAvgProb = cast.ToFloat64(param) + case "mate_singlepoint_prob": + c.MateSinglepointProb = cast.ToFloat64(param) + case "mate_only_prob": + c.MateOnlyProb = cast.ToFloat64(param) + case "recur_only_prob": + c.RecurOnlyProb = cast.ToFloat64(param) + case "pop_size": + c.PopSize = cast.ToInt(param) + case "dropoff_age": + c.DropOffAge = cast.ToInt(param) + case "newlink_tries": + c.NewLinkTries = cast.ToInt(param) + case "print_every": + c.PrintEvery = cast.ToInt(param) + case "babies_stolen": + c.BabiesStolen = cast.ToInt(param) + case "num_runs": + c.NumRuns = cast.ToInt(param) + case "num_generations": + c.NumGenerations = cast.ToInt(param) + case "epoch_executor": + c.EpochExecutorType = EpochExecutorType(param) + case "genome_compat_method": + c.GenCompatMethod = GenomeCompatibilityMethod(param) + case "log_level": + c.LogLevel = param + default: + return nil, errors.Errorf("unknown configuration parameter found: %s = %s", name, param) + } + } + // initialize logger + if err := InitLogger(c.LogLevel); err != nil { + return nil, errors.Wrap(err, "failed to initialize logger") + } + + if err := c.initNodeActivators(); err != nil { + return nil, err + } + if err := c.Validate(); err != nil { + return nil, err + } + + return c, nil +} + +// ReadNeatOptionsFromFile reads NEAT options from specified configFilePath automatically resolving config file encoding. +func ReadNeatOptionsFromFile(configFilePath string) (*Options, error) { + configFile, err := os.Open(configFilePath) + if err != nil { + return nil, errors.Wrap(err, "failed to open config file") + } + fileName := configFile.Name() + if strings.HasSuffix(fileName, "yml") || strings.HasSuffix(fileName, "yaml") { + return LoadYAMLOptions(configFile) + } else { + return LoadNeatOptions(configFile) + } +} + +// set default values for activator type and its probability of selection +func (c *Options) initNodeActivators() (err error) { + if len(c.NodeActivatorsWithProbs) == 0 { + c.NodeActivators = []math.NodeActivationType{math.SigmoidSteepenedActivation} + c.NodeActivatorsProb = []float64{1.0} + return nil + } + // create activators + actFns := c.NodeActivatorsWithProbs + c.NodeActivators = make([]math.NodeActivationType, len(actFns)) + c.NodeActivatorsProb = make([]float64, len(actFns)) + for i, line := range actFns { + fields := strings.Fields(line) + if c.NodeActivators[i], err = math.NodeActivators.ActivationTypeFromName(fields[0]); err != nil { + return err + } + if c.NodeActivatorsProb[i], err = strconv.ParseFloat(fields[1], 64); err != nil { + return err + } + } + return nil +} diff --git a/neat/neat_options_readers_test.go b/neat/neat_options_readers_test.go new file mode 100644 index 0000000..0dcc5f7 --- /dev/null +++ b/neat/neat_options_readers_test.go @@ -0,0 +1,123 @@ +package neat + +import ( + "errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yaricom/goNEAT/v4/neat/math" + "os" + "testing" +) + +const ( + alwaysErrorText = "always be failing" + xorOptionsFilePlain = "../data/xor_test.neat" + xorOptionsFileYaml = "../data/xor_test.neat.yml" +) + +var errFoo = errors.New(alwaysErrorText) + +type ErrorReader int + +func (e ErrorReader) Read(_ []byte) (n int, err error) { + return 0, errFoo +} + +func TestLoadNeatOptions(t *testing.T) { + config, err := os.Open(xorOptionsFilePlain) + require.NoError(t, err) + + // Load Neat Context + opts, err := LoadNeatOptions(config) + require.NoError(t, err) + checkNeatOptions(opts, t) +} + +func TestLoadNeatOptions_readError(t *testing.T) { + errorReader := ErrorReader(1) + opts, err := LoadNeatOptions(&errorReader) + assert.EqualError(t, err, alwaysErrorText) + assert.Nil(t, opts) +} + +func TestLoadYAMLOptions(t *testing.T) { + config, err := os.Open(xorOptionsFileYaml) + require.NoError(t, err) + + // Load YAML context + opts, err := LoadYAMLOptions(config) + require.NoError(t, err, "failed to load options") + + checkNeatOptions(opts, t) + + // check activators + require.Len(t, opts.NodeActivators, 4, "wrong node activators size") + activators := []math.NodeActivationType{math.SigmoidBipolarActivation, + math.GaussianBipolarActivation, math.LinearAbsActivation, math.SineActivation} + probs := []float64{0.25, 0.35, 0.15, 0.25} + for i, a := range activators { + assert.Equal(t, a, opts.NodeActivators[i], "wrong node activator type at: %d", i) + assert.Equal(t, probs[i], opts.NodeActivatorsProb[i], "wrong probability at: %d", i) + + } +} + +func TestLoadYAMLOptions_readError(t *testing.T) { + errorReader := ErrorReader(1) + opts, err := LoadYAMLOptions(&errorReader) + assert.EqualError(t, err, alwaysErrorText) + assert.Nil(t, opts) +} + +func TestReadNeatOptionsFromFile(t *testing.T) { + opts, err := ReadNeatOptionsFromFile(xorOptionsFilePlain) + require.NoError(t, err, "failed to read NEAT options with PLAIN encoding") + assert.NotNil(t, opts) + + opts, err = ReadNeatOptionsFromFile(xorOptionsFileYaml) + require.NoError(t, err, "failed to read NEAT options with YAML encoding") + assert.NotNil(t, opts) +} + +func TestReadNeatOptionsFromFile_error(t *testing.T) { + opts, err := ReadNeatOptionsFromFile("file doesnt exist") + assert.Error(t, err) + assert.Nil(t, opts) +} + +func checkNeatOptions(nc *Options, t *testing.T) { + assert.Equal(t, 0.5, nc.TraitParamMutProb) + assert.Equal(t, 1.0, nc.TraitMutationPower) + assert.Equal(t, 2.5, nc.WeightMutPower) + assert.Equal(t, 1.0, nc.DisjointCoeff) + assert.Equal(t, 1.0, nc.ExcessCoeff) + assert.Equal(t, 0.4, nc.MutdiffCoeff) + assert.Equal(t, 3.0, nc.CompatThreshold) + assert.Equal(t, 1.0, nc.AgeSignificance) + assert.Equal(t, 0.2, nc.SurvivalThresh) + assert.Equal(t, 0.25, nc.MutateOnlyProb) + assert.Equal(t, 0.1, nc.MutateRandomTraitProb) + assert.Equal(t, 0.1, nc.MutateLinkTraitProb) + assert.Equal(t, 0.1, nc.MutateNodeTraitProb) + assert.Equal(t, 0.9, nc.MutateLinkWeightsProb) + assert.Equal(t, 0.0, nc.MutateToggleEnableProb) + assert.Equal(t, 0.0, nc.MutateGeneReenableProb) + assert.Equal(t, 0.03, nc.MutateAddNodeProb) + assert.Equal(t, 0.08, nc.MutateAddLinkProb) + assert.Equal(t, 0.5, nc.MutateConnectSensors) + assert.Equal(t, 0.001, nc.InterspeciesMateRate) + assert.Equal(t, 0.3, nc.MateMultipointProb) + assert.Equal(t, 0.3, nc.MateMultipointAvgProb) + assert.Equal(t, 0.3, nc.MateSinglepointProb) + assert.Equal(t, 0.2, nc.MateOnlyProb) + assert.Equal(t, 0.0, nc.RecurOnlyProb) + assert.Equal(t, 200, nc.PopSize) + assert.Equal(t, 50, nc.DropOffAge) + assert.Equal(t, 50, nc.NewLinkTries) + assert.Equal(t, 10, nc.PrintEvery) + assert.Equal(t, 0, nc.BabiesStolen) + assert.Equal(t, 100, nc.NumRuns) + assert.Equal(t, 100, nc.NumGenerations) + assert.Equal(t, EpochExecutorTypeSequential, nc.EpochExecutorType) + assert.Equal(t, GenomeCompatibilityMethodFast, nc.GenCompatMethod) +} diff --git a/neat/neat_test.go b/neat/neat_test.go index eab7245..35ff83c 100644 --- a/neat/neat_test.go +++ b/neat/neat_test.go @@ -1,81 +1,19 @@ package neat import ( - "errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat/math" - "os" + "github.com/yaricom/goNEAT/v4/neat/math" "testing" ) -const ( - alwaysErrorText = "always be failing" - xorOptionsFilePlain = "../data/xor_test.neat" - xorOptionsFileYaml = "../data/xor_test.neat.yml" -) - -var errFoo = errors.New(alwaysErrorText) - -type ErrorReader int - -func (e ErrorReader) Read(_ []byte) (n int, err error) { - return 0, errFoo -} - -func TestLoadNeatOptions(t *testing.T) { - config, err := os.Open(xorOptionsFilePlain) - require.NoError(t, err) - - // Load Neat Context - opts, err := LoadNeatOptions(config) - require.NoError(t, err) - checkNeatOptions(opts, t) -} - -func TestLoadNeatOptions_readError(t *testing.T) { - errorReader := ErrorReader(1) - opts, err := LoadNeatOptions(&errorReader) - assert.EqualError(t, err, alwaysErrorText) - assert.Nil(t, opts) -} - -func TestLoadYAMLOptions(t *testing.T) { - config, err := os.Open(xorOptionsFileYaml) - require.NoError(t, err) - - // Load YAML context - opts, err := LoadYAMLOptions(config) - require.NoError(t, err, "failed to load options") - - checkNeatOptions(opts, t) - - // check activators - require.Len(t, opts.NodeActivators, 4, "wrong node activators size") - activators := []math.NodeActivationType{math.SigmoidBipolarActivation, - math.GaussianBipolarActivation, math.LinearAbsActivation, math.SineActivation} - probs := []float64{0.25, 0.35, 0.15, 0.25} - for i, a := range activators { - assert.Equal(t, a, opts.NodeActivators[i], "wrong node activator type at: %d", i) - assert.Equal(t, probs[i], opts.NodeActivatorsProb[i], "wrong probability at: %d", i) - - } -} - -func TestLoadYAMLOptions_readError(t *testing.T) { - errorReader := ErrorReader(1) - opts, err := LoadYAMLOptions(&errorReader) - assert.EqualError(t, err, alwaysErrorText) - assert.Nil(t, opts) -} - func TestOptions_NeatContext(t *testing.T) { - config, err := os.Open(xorOptionsFileYaml) - require.NoError(t, err) - - // Load YAML context - opts, err := LoadYAMLOptions(config) - require.NoError(t, err, "failed to load options") + opts := &Options{ + CompatThreshold: 0.5, + PopSize: 10, + NodeActivators: []math.NodeActivationType{math.GaussianBipolarActivation}, + NodeActivatorsProb: []float64{1.0}, + } // check that NEAT context has options inside ctx := opts.NeatContext() @@ -83,57 +21,46 @@ func TestOptions_NeatContext(t *testing.T) { nOpts, ok := FromContext(ctx) require.True(t, ok, "options not found") assert.NotNil(t, nOpts) + assert.EqualValues(t, opts, nOpts) } -func TestReadNeatOptionsFromFile(t *testing.T) { - opts, err := ReadNeatOptionsFromFile(xorOptionsFilePlain) - require.NoError(t, err, "failed to read NEAT options with PLAIN encoding") - assert.NotNil(t, opts) +func TestOptions_RandomNodeActivationType_noActivators(t *testing.T) { + opts := &Options{ + CompatThreshold: 0.5, + PopSize: 10, + } + activator, err := opts.RandomNodeActivationType() + assert.EqualError(t, err, ErrNoActivatorsRegistered.Error()) + assert.EqualValues(t, 0, activator) +} - opts, err = ReadNeatOptionsFromFile(xorOptionsFileYaml) - require.NoError(t, err, "failed to read NEAT options with YAML encoding") - assert.NotNil(t, opts) +func TestOptions_RandomNodeActivationType_activatorsProbabilitiesNumberMismatch(t *testing.T) { + opts := &Options{ + NodeActivators: []math.NodeActivationType{math.SigmoidApproximationActivation, math.SigmoidBipolarActivation}, + NodeActivatorsProb: []float64{0.5}, + } + activator, err := opts.RandomNodeActivationType() + assert.EqualError(t, err, ErrActivatorsProbabilitiesNumberMismatch.Error()) + assert.EqualValues(t, 0, activator) } -func TestReadNeatOptionsFromFile_error(t *testing.T) { - opts, err := ReadNeatOptionsFromFile("file doesnt exist") - assert.Error(t, err) - assert.Nil(t, opts) +func TestOptions_RandomNodeActivationType_singleValue(t *testing.T) { + opts := &Options{ + NodeActivators: []math.NodeActivationType{math.GaussianBipolarActivation}, + NodeActivatorsProb: []float64{1.0}, + } + activator, err := opts.RandomNodeActivationType() + require.NoError(t, err) + assert.Equal(t, math.GaussianBipolarActivation, activator) } -func checkNeatOptions(nc *Options, t *testing.T) { - assert.Equal(t, 0.5, nc.TraitParamMutProb) - assert.Equal(t, 1.0, nc.TraitMutationPower) - assert.Equal(t, 2.5, nc.WeightMutPower) - assert.Equal(t, 1.0, nc.DisjointCoeff) - assert.Equal(t, 1.0, nc.ExcessCoeff) - assert.Equal(t, 0.4, nc.MutdiffCoeff) - assert.Equal(t, 3.0, nc.CompatThreshold) - assert.Equal(t, 1.0, nc.AgeSignificance) - assert.Equal(t, 0.2, nc.SurvivalThresh) - assert.Equal(t, 0.25, nc.MutateOnlyProb) - assert.Equal(t, 0.1, nc.MutateRandomTraitProb) - assert.Equal(t, 0.1, nc.MutateLinkTraitProb) - assert.Equal(t, 0.1, nc.MutateNodeTraitProb) - assert.Equal(t, 0.9, nc.MutateLinkWeightsProb) - assert.Equal(t, 0.0, nc.MutateToggleEnableProb) - assert.Equal(t, 0.0, nc.MutateGeneReenableProb) - assert.Equal(t, 0.03, nc.MutateAddNodeProb) - assert.Equal(t, 0.08, nc.MutateAddLinkProb) - assert.Equal(t, 0.5, nc.MutateConnectSensors) - assert.Equal(t, 0.001, nc.InterspeciesMateRate) - assert.Equal(t, 0.3, nc.MateMultipointProb) - assert.Equal(t, 0.3, nc.MateMultipointAvgProb) - assert.Equal(t, 0.3, nc.MateSinglepointProb) - assert.Equal(t, 0.2, nc.MateOnlyProb) - assert.Equal(t, 0.0, nc.RecurOnlyProb) - assert.Equal(t, 200, nc.PopSize) - assert.Equal(t, 50, nc.DropOffAge) - assert.Equal(t, 50, nc.NewLinkTries) - assert.Equal(t, 10, nc.PrintEvery) - assert.Equal(t, 0, nc.BabiesStolen) - assert.Equal(t, 100, nc.NumRuns) - assert.Equal(t, 100, nc.NumGenerations) - assert.Equal(t, EpochExecutorTypeSequential, nc.EpochExecutorType) - assert.Equal(t, GenomeCompatibilityMethodFast, nc.GenCompatMethod) +func TestOptions_RandomNodeActivationType(t *testing.T) { + opts := &Options{ + NodeActivators: []math.NodeActivationType{math.SigmoidApproximationActivation, math.SigmoidBipolarActivation}, + NodeActivatorsProb: []float64{0.5, 0.5}, + } + activator, err := opts.RandomNodeActivationType() + require.NoError(t, err) + res := activator == math.SigmoidApproximationActivation || activator == math.SigmoidBipolarActivation + assert.True(t, res) } diff --git a/neat/network/common.go b/neat/network/common.go index 0364c1e..9923cd5 100644 --- a/neat/network/common.go +++ b/neat/network/common.go @@ -4,7 +4,7 @@ package network import ( "errors" "fmt" - neatmath "github.com/yaricom/goNEAT/v3/neat/math" + neatmath "github.com/yaricom/goNEAT/v4/neat/math" "math" ) diff --git a/neat/network/common_test.go b/neat/network/common_test.go index 1bf7d0d..d3a0cf0 100644 --- a/neat/network/common_test.go +++ b/neat/network/common_test.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" "github.com/stretchr/testify/assert" - "github.com/yaricom/goNEAT/v3/neat/math" + "github.com/yaricom/goNEAT/v4/neat/math" "testing" ) diff --git a/neat/network/fast_network.go b/neat/network/fast_network.go index ed809c5..b67b331 100644 --- a/neat/network/fast_network.go +++ b/neat/network/fast_network.go @@ -3,7 +3,7 @@ package network import ( "errors" "fmt" - neatmath "github.com/yaricom/goNEAT/v3/neat/math" + neatmath "github.com/yaricom/goNEAT/v4/neat/math" "math" ) diff --git a/neat/network/formats/common_test.go b/neat/network/formats/common_test.go index b82ce4f..c2f4083 100644 --- a/neat/network/formats/common_test.go +++ b/neat/network/formats/common_test.go @@ -2,8 +2,8 @@ package formats import ( "errors" - "github.com/yaricom/goNEAT/v3/neat/math" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat/math" + "github.com/yaricom/goNEAT/v4/neat/network" ) const alwaysErrorText = "always be failing" diff --git a/neat/network/formats/network_graph_cytoscapejs.go b/neat/network/formats/network_graph_cytoscapejs.go index 5daadf7..606c7aa 100644 --- a/neat/network/formats/network_graph_cytoscapejs.go +++ b/neat/network/formats/network_graph_cytoscapejs.go @@ -3,8 +3,8 @@ package formats import ( "encoding/json" "fmt" - "github.com/yaricom/goNEAT/v3/neat/math" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat/math" + "github.com/yaricom/goNEAT/v4/neat/network" "gonum.org/v1/gonum/graph/formats/cytoscapejs" "io" ) diff --git a/neat/network/formats/network_graph_cytoscapejs_test.go b/neat/network/formats/network_graph_cytoscapejs_test.go index 832caac..0e3fea6 100644 --- a/neat/network/formats/network_graph_cytoscapejs_test.go +++ b/neat/network/formats/network_graph_cytoscapejs_test.go @@ -4,9 +4,9 @@ import ( "bytes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/math" - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" + "github.com/yaricom/goNEAT/v4/neat/network" "testing" ) diff --git a/neat/network/formats/network_graph_dot.go b/neat/network/formats/network_graph_dot.go index 3fca0d8..d86b0d3 100644 --- a/neat/network/formats/network_graph_dot.go +++ b/neat/network/formats/network_graph_dot.go @@ -1,7 +1,7 @@ package formats import ( - "github.com/yaricom/goNEAT/v3/neat/network" + "github.com/yaricom/goNEAT/v4/neat/network" "gonum.org/v1/gonum/graph/encoding/dot" "io" ) diff --git a/neat/network/link.go b/neat/network/link.go index 4377be8..1e76410 100644 --- a/neat/network/link.go +++ b/neat/network/link.go @@ -2,7 +2,7 @@ package network import ( "fmt" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/neat" ) // Link is a connection from one node to another with an associated weight. diff --git a/neat/network/link_graph_test.go b/neat/network/link_graph_test.go index a5cb1cb..43867db 100644 --- a/neat/network/link_graph_test.go +++ b/neat/network/link_graph_test.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/neat" "testing" ) diff --git a/neat/network/link_test.go b/neat/network/link_test.go index 6ded85b..eeb0a78 100644 --- a/neat/network/link_test.go +++ b/neat/network/link_test.go @@ -2,7 +2,7 @@ package network import ( "github.com/stretchr/testify/assert" - "github.com/yaricom/goNEAT/v3/neat" + "github.com/yaricom/goNEAT/v4/neat" "testing" ) diff --git a/neat/network/network.go b/neat/network/network.go index 99be624..9c9b0c9 100644 --- a/neat/network/network.go +++ b/neat/network/network.go @@ -4,7 +4,7 @@ import ( "bytes" "errors" "fmt" - "github.com/yaricom/goNEAT/v3/neat/math" + "github.com/yaricom/goNEAT/v4/neat/math" "gonum.org/v1/gonum/graph/path" "io" ) @@ -235,7 +235,7 @@ func (n *Network) OutputIsOff() bool { // ActivateSteps Attempts to activate the network given number of steps before returning error. // Normally the maxSteps should be equal to the maximal activation depth of the network as returned by -// MaxActivationDepth() or MaxActivationDepthFast() +// MaxActivationDepth() or MaxActivationDepthWithCap() func (n *Network) ActivateSteps(maxSteps int) (bool, error) { if maxSteps == 0 { return false, ErrZeroActivationStepsRequested @@ -327,7 +327,7 @@ func (n *Network) ForwardSteps(steps int) (res bool, err error) { } func (n *Network) RecursiveSteps() (bool, error) { - netDepth, err := n.MaxActivationDepthFast(0) + netDepth, err := n.MaxActivationDepthWithCap(0) if err != nil { return false, err } @@ -428,22 +428,20 @@ func (n *Network) IsRecurrent(inNode, outNode *NNode, count *int, thresh int) bo // MaxActivationDepth is to find the maximum number of neuron layers to be activated between an output and an input layers. func (n *Network) MaxActivationDepth() (int, error) { - // The quick case when there are no hidden nodes or control - if len(n.allNodes) == len(n.inputs)+len(n.Outputs) && len(n.controlNodes) == 0 { - return 1, nil // just one layer depth + if len(n.controlNodes) == 0 { + return n.MaxActivationDepthWithCap(0) + } else { + return n.maxActivationDepthModular(nil) } - - return n.maxActivationDepth(nil) } -// MaxActivationDepthFast is to find the maximum number of neuron layers to be activated between an output and an input layers. -// This is the fastest version of depth calculation but only suitable for simple networks. If current network is modular -// the error will be raised. -// It is possible to limit the maximal depth value by setting the maxDepth value greater than zero. +// MaxActivationDepthWithCap is to find the maximum number of neuron layers to be activated between an output and an input layers. +// It is possible to limit the maximal depth value by setting the maxDepthCap value greater than zero. // If network depth exceeds provided maxDepth value this value will be returned along with ErrMaximalNetDepthExceeded // to indicate that calculation stopped. -// If maxDepth is less or equal to zero no maximal depth limitation will be set. -func (n *Network) MaxActivationDepthFast(maxDepth int) (int, error) { +// If maxDepthCap is less or equal to zero no maximal depth limitation will be set. +// Unsupported for modular networks. +func (n *Network) MaxActivationDepthWithCap(maxDepthCap int) (int, error) { if len(n.controlNodes) > 0 { return -1, errors.New("unsupported for modular networks") } @@ -455,7 +453,7 @@ func (n *Network) MaxActivationDepthFast(maxDepth int) (int, error) { max := 0 // The max depth for _, node := range n.Outputs { - currDepth, err := node.Depth(1, maxDepth) // 1 is to include this layer + currDepth, err := node.Depth(0, maxDepthCap) if err != nil { return currDepth, err } @@ -482,9 +480,9 @@ func (n *Network) BaseNodes() []*NNode { return n.allNodes } -// maxActivationDepth calculates maximal activation depth and optionally prints the examined activation paths to the -// provided writer. -func (n *Network) maxActivationDepth(w io.Writer) (int, error) { +// maxActivationDepthModular calculates maximal activation depth and optionally prints the examined activation paths +// to the provided writer. It is intended only for modular networks. +func (n *Network) maxActivationDepthModular(w io.Writer) (int, error) { allPaths, ok := path.JohnsonAllPaths(n) if !ok { // negative cycle detected - fallback to FloydWarshall @@ -501,7 +499,7 @@ func (n *Network) maxActivationDepth(w io.Writer) (int, error) { } // iterate over returned paths and find the one with maximal length for _, p := range paths { - l := len(p) + l := len(p) - 1 // to exclude input node if l > max { max = l } diff --git a/neat/network/network_test.go b/neat/network/network_test.go index 2a9d423..ea47f37 100644 --- a/neat/network/network_test.go +++ b/neat/network/network_test.go @@ -3,7 +3,7 @@ package network import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat/math" + "github.com/yaricom/goNEAT/v4/neat/math" "testing" ) @@ -70,12 +70,12 @@ func buildNetwork() *Network { func buildModularNetwork() *Network { allNodes := []*NNode{ - NewNNode(1, InputNeuron), - NewNNode(2, InputNeuron), - NewNNode(3, BiasNeuron), - NewNNode(4, HiddenNeuron), - NewNNode(5, HiddenNeuron), - NewNNode(7, HiddenNeuron), + NewNNode(1, InputNeuron), // INPUT 1 + NewNNode(2, InputNeuron), // INPUT 2 + NewNNode(3, BiasNeuron), // BIAS + NewNNode(4, HiddenNeuron), // HIDDEN 4 + NewNNode(5, HiddenNeuron), // HIDDEN 5 + NewNNode(7, HiddenNeuron), // HIDDEN 7 NewNNode(8, OutputNeuron), NewNNode(9, OutputNeuron), } @@ -84,27 +84,27 @@ func buildModularNetwork() *Network { } // HIDDEN 6 - control node controlNodes[0].ActivationType = math.MultiplyModuleActivation - controlNodes[0].AddIncoming(allNodes[3], 1.0) - controlNodes[0].AddIncoming(allNodes[4], 1.0) - controlNodes[0].AddOutgoing(allNodes[5], 1.0) + controlNodes[0].AddIncoming(allNodes[3], 1.0) // <- HIDDEN 4 + controlNodes[0].AddIncoming(allNodes[4], 1.0) // <- HIDDEN 5 + controlNodes[0].AddOutgoing(allNodes[5], 1.0) // -> HIDDEN 7 // HIDDEN 4 allNodes[3].ActivationType = math.LinearActivation - allNodes[3].ConnectFrom(allNodes[0], 15.0) - allNodes[3].ConnectFrom(allNodes[2], 10.0) + allNodes[3].ConnectFrom(allNodes[0], 15.0) // <- INPUT 1 + allNodes[3].ConnectFrom(allNodes[2], 10.0) // <- BIAS // HIDDEN 5 allNodes[4].ActivationType = math.LinearActivation - allNodes[4].ConnectFrom(allNodes[1], 5.0) - allNodes[4].ConnectFrom(allNodes[2], 1.0) + allNodes[4].ConnectFrom(allNodes[1], 5.0) // <- INPUT 2 + allNodes[4].ConnectFrom(allNodes[2], 1.0) // <- BIAS // HIDDEN 7 - allNodes[5].ActivationType = math.NullActivation + allNodes[5].ActivationType = math.NullActivation // <- CONTROL // OUTPUT 8 - allNodes[6].ConnectFrom(allNodes[5], 4.5) + allNodes[6].ConnectFrom(allNodes[5], 4.5) // <- HIDDEN 7 allNodes[6].ActivationType = math.LinearActivation // OUTPUT 9 - allNodes[7].ConnectFrom(allNodes[5], 13.0) + allNodes[7].ConnectFrom(allNodes[5], 13.0) // <- HIDDEN 7 allNodes[7].ActivationType = math.LinearActivation return NewModularNetwork(allNodes[0:3], allNodes[6:8], allNodes, controlNodes, 0) @@ -132,7 +132,7 @@ func TestNetwork_MaxActivationDepth_Simple(t *testing.T) { depth, err := net.MaxActivationDepth() assert.NoError(t, err, "failed to calculate max depth") - assert.Equal(t, 4, depth) + assert.Equal(t, 3, depth) logNetworkActivationPath(net, t) } @@ -142,7 +142,7 @@ func TestNetwork_MaxActivationDepth_Modular(t *testing.T) { depth, err := net.MaxActivationDepth() assert.NoError(t, err, "failed to calculate max depth") - assert.Equal(t, 5, depth) + assert.Equal(t, 4, depth) logNetworkActivationPath(net, t) } @@ -157,9 +157,9 @@ func TestNetwork_MaxActivationDepth_No_Hidden_or_Control(t *testing.T) { func TestNetwork_MaxActivationDepthFast_Simple(t *testing.T) { net := buildNetwork() - depth, err := net.MaxActivationDepthFast(0) + depth, err := net.MaxActivationDepthWithCap(0) assert.NoError(t, err, "failed to calculate max depth") - assert.Equal(t, 4, depth) + assert.Equal(t, 3, depth) logNetworkActivationPath(net, t) } @@ -168,7 +168,7 @@ func TestNetwork_MaxActivationDepthFast_Simple_WithMaxLimitError(t *testing.T) { net := buildNetwork() maxDepth := 2 - depth, err := net.MaxActivationDepthFast(2) + depth, err := net.MaxActivationDepthWithCap(2) assert.EqualError(t, err, ErrMaximalNetDepthExceeded.Error()) assert.Equal(t, maxDepth, depth) } @@ -176,7 +176,7 @@ func TestNetwork_MaxActivationDepthFast_Simple_WithMaxLimitError(t *testing.T) { func TestNetwork_MaxActivationDepthFast_Modular(t *testing.T) { net := buildModularNetwork() - _, err := net.MaxActivationDepthFast(0) + _, err := net.MaxActivationDepthWithCap(0) assert.Error(t, err, "error expected") } @@ -356,36 +356,6 @@ func TestNetwork_ActivateSteps_ErrNetExceededMaxActivationAttempts(t *testing.T) assert.False(t, res) } -func TestNetwork_ActivateSteps_maxActivationDepth_disconnected(t *testing.T) { - net := buildDisconnectedNetwork() - - depth, err := net.maxActivationDepth(nil) - assert.NoError(t, err) - assert.Equal(t, 0, depth) -} - -func TestNetwork_ActivateSteps_maxActivationDepth_negative_cycle(t *testing.T) { - net := buildNetwork() - - // create negative cycle - net.allNodes[1].ConnectFrom(net.allNodes[7], -130.0) - - depth, err := net.maxActivationDepth(nil) - assert.NoError(t, err) - assert.Equal(t, 3, depth) - - logNetworkActivationPath(net, t) -} - -func TestNetwork_ActivateSteps_maxActivationDepth_writeError(t *testing.T) { - net := buildNetwork() - - errWriter := ErrorWriter(1) - depth, err := net.maxActivationDepth(&errWriter) - assert.EqualError(t, err, alwaysErrorText) - assert.Equal(t, 0, depth) -} - func TestModularNetwork_ControlNodes(t *testing.T) { net := buildModularNetwork() diff --git a/neat/network/nnode.go b/neat/network/nnode.go index 1bfef08..2e13dd8 100644 --- a/neat/network/nnode.go +++ b/neat/network/nnode.go @@ -3,8 +3,9 @@ package network import ( "bytes" "fmt" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/math" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" + "io" ) // NNode is either a NEURON or a SENSOR. @@ -74,6 +75,19 @@ func NewNNodeCopy(n *NNode, t *neat.Trait) *NNode { return node } +// NewSensorNode is to create sensor node +func NewSensorNode(nodeId int, bias bool) *NNode { + n := NewNetworkNode() + n.ActivationType = math.NullActivation + n.Id = nodeId + if bias { + n.NeuronType = BiasNeuron + } else { + n.NeuronType = InputNeuron + } + return n +} + // NewNetworkNode The default constructor func NewNetworkNode() *NNode { return &NNode{ @@ -198,36 +212,78 @@ func (n *NNode) FlushbackCheck() error { return nil } -// Depth Find the greatest depth starting from this neuron at depth d. If maxDepth > 0 it can be used to stop early in +// Depth Find the greatest depth starting from this neuron at depth d. If maxDepthCap > 0 it can be used to stop early in // case if very deep network detected -func (n *NNode) Depth(d int, maxDepth int) (int, error) { - if maxDepth > 0 && d > maxDepth { +func (n *NNode) Depth(d int, maxDepthCap int) (int, error) { + if maxDepthCap > 0 && d > maxDepthCap { // to avoid very deep network traversing - return maxDepth, ErrMaximalNetDepthExceeded + return maxDepthCap, ErrMaximalNetDepthExceeded } - n.visited = true - // Base Case + + // The end reached if n.IsSensor() { return d, nil + } + + n.visited = true + + // visit parent nodes + max := d + for _, l := range n.Incoming { + if l.InNode.visited { + // was already visited (loop detected) - skipping + continue + } + if curDepth, err := l.InNode.Depth(d+1, maxDepthCap); err != nil { + return curDepth, err + } else if curDepth > max { + max = curDepth + } + } + + n.visited = false + return max, nil + +} + +func (n *NNode) printDepthPaths(path []int, pathIndex *int, w io.Writer) error { + // Mark the current node and store it in path[] + n.visited = true + path[*pathIndex] = n.Id + *pathIndex++ + + if n.IsSensor() { + // destination reached + for i := *pathIndex - 1; i >= 0; i-- { + if i > 0 { + if _, err := fmt.Fprintf(w, "%d -> ", path[i]); err != nil { + return err + } + } else { + if _, err := fmt.Fprintf(w, "%d", path[i]); err != nil { + return err + } + } + } + if _, err := fmt.Fprintln(w); err != nil { + return err + } } else { - // recursion - max := d // The max depth + // visit parent nodes for _, l := range n.Incoming { if l.InNode.visited { // was already visited (loop detected) - skipping continue } - curDepth, err := l.InNode.Depth(d+1, maxDepth) - if err != nil { - return curDepth, err - } - if curDepth > max { - max = curDepth + if err := l.InNode.printDepthPaths(path, pathIndex, w); err != nil { + return err } } - return max, nil } - + // Remove current vertex from path[] and mark it as unvisited + *pathIndex-- + n.visited = false + return nil } // NodeType Convenient method to check network's node type (SENSOR, NEURON) diff --git a/neat/network/nnode_graph.go b/neat/network/nnode_graph.go index 28238af..b8a64ed 100644 --- a/neat/network/nnode_graph.go +++ b/neat/network/nnode_graph.go @@ -2,7 +2,7 @@ package network import ( "fmt" - "github.com/yaricom/goNEAT/v3/neat/math" + "github.com/yaricom/goNEAT/v4/neat/math" "gonum.org/v1/gonum/graph/encoding" ) diff --git a/neat/network/nnode_test.go b/neat/network/nnode_test.go index 0dcfc85..1c2afad 100644 --- a/neat/network/nnode_test.go +++ b/neat/network/nnode_test.go @@ -3,8 +3,8 @@ package network import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yaricom/goNEAT/v3/neat" - "github.com/yaricom/goNEAT/v3/neat/math" + "github.com/yaricom/goNEAT/v4/neat" + "github.com/yaricom/goNEAT/v4/neat/math" "testing" ) diff --git a/neat/network/utils.go b/neat/network/utils.go index f36936b..5d7583f 100644 --- a/neat/network/utils.go +++ b/neat/network/utils.go @@ -9,8 +9,19 @@ import ( // PrintAllActivationDepthPaths is to print all paths used to find the maximal activation depth of the network func PrintAllActivationDepthPaths(n *Network, w io.Writer) error { - _, err := n.maxActivationDepth(w) - return err + if len(n.controlNodes) > 0 { + _, err := n.maxActivationDepthModular(w) + return err + } + + for _, node := range n.Outputs { + path := make([]int, n.NodeCount()) + pathIndex := 0 + if err := node.printDepthPaths(path, &pathIndex, w); err != nil { + return err + } + } + return nil } // PrintPath is to print the given paths into specified writer diff --git a/neat/network/utils_test.go b/neat/network/utils_test.go index dcc799e..96a6e49 100644 --- a/neat/network/utils_test.go +++ b/neat/network/utils_test.go @@ -13,7 +13,7 @@ func TestPrintAllActivationDepthPaths_Simple(t *testing.T) { net := buildNetwork() actual := logNetworkActivationPath(net, t) - expected := "\n1 -> 4 -> 7\n---------------\n2 -> 4 -> 7\n2 -> 5 -> 6 -> 8\n---------------\n3 -> 5 -> 6 -> 7\n3 -> 5 -> 6 -> 8\n---------------\n" + expected := "\n1 -> 4 -> 7\n2 -> 4 -> 7\n2 -> 5 -> 6 -> 7\n3 -> 5 -> 6 -> 7\n2 -> 5 -> 6 -> 8\n3 -> 5 -> 6 -> 8\n" assert.Equal(t, expected, actual) } @@ -25,7 +25,7 @@ func TestPrintAllActivationDepthPaths_Modular(t *testing.T) { assert.Equal(t, expected, actual) } -func TestPrintAllActivationDepthPaths_writeError(t *testing.T) { +func TestPrintAllActivationDepthPaths_modular_writeError(t *testing.T) { net := buildModularNetwork() errWriter := ErrorWriter(1) @@ -33,6 +33,14 @@ func TestPrintAllActivationDepthPaths_writeError(t *testing.T) { assert.EqualError(t, err, alwaysErrorText) } +func TestPrintAllActivationDepthPaths_plain_writeError(t *testing.T) { + net := buildNetwork() + + errWriter := ErrorWriter(1) + err := PrintAllActivationDepthPaths(net, &errWriter) + assert.EqualError(t, err, alwaysErrorText) +} + func TestPrintPath(t *testing.T) { net := buildNetwork() diff --git a/neat/trait.go b/neat/trait.go index 95ef170..d7441d6 100644 --- a/neat/trait.go +++ b/neat/trait.go @@ -3,7 +3,7 @@ package neat import ( "errors" "fmt" - "github.com/yaricom/goNEAT/v3/neat/math" + "github.com/yaricom/goNEAT/v4/neat/math" "math/rand" )