From a506d45e16922bbda123f92dad55959694ccc2df Mon Sep 17 00:00:00 2001 From: sanine Date: Wed, 29 Nov 2023 02:59:51 -0600 Subject: implement individual gene mutation functions --- nerine.cabal | 5 ++++ package.yaml | 1 + src/Genome.hs | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++ test/GenomeTest.hs | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/Main.hs | 2 ++ 5 files changed, 149 insertions(+) create mode 100644 src/Genome.hs create mode 100644 test/GenomeTest.hs diff --git a/nerine.cabal b/nerine.cabal index 52b1c4a..7592604 100644 --- a/nerine.cabal +++ b/nerine.cabal @@ -25,6 +25,7 @@ source-repository head library exposed-modules: + Genome Lib Mind other-modules: @@ -36,6 +37,7 @@ library ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints build-depends: base >=4.7 && <5 + , random , tasty , tasty-hunit default-language: Haskell2010 @@ -52,6 +54,7 @@ executable nerine-exe build-depends: base >=4.7 && <5 , nerine + , random , tasty , tasty-hunit default-language: Haskell2010 @@ -60,6 +63,7 @@ test-suite nerine-test type: exitcode-stdio-1.0 main-is: Main.hs other-modules: + GenomeTest MindTest Paths_nerine autogen-modules: @@ -70,6 +74,7 @@ test-suite nerine-test build-depends: base >=4.7 && <5 , nerine + , random , tasty , tasty-hunit default-language: Haskell2010 diff --git a/package.yaml b/package.yaml index bd8c0f2..f0d605b 100644 --- a/package.yaml +++ b/package.yaml @@ -23,6 +23,7 @@ dependencies: - base >= 4.7 && < 5 - tasty - tasty-hunit +- random ghc-options: - -Wall diff --git a/src/Genome.hs b/src/Genome.hs new file mode 100644 index 0000000..4fb063c --- /dev/null +++ b/src/Genome.hs @@ -0,0 +1,69 @@ +module Genome + ( Gene (..) + , Genome (..) + , mutateGeneSource + , mutateGeneSink + , mutateGeneWeight + , mutateGene + ) where + + +import Mind (NeuronIndex (..)) +import System.Random + + +data Gene = Gene { source :: NeuronIndex, sink :: NeuronIndex, weight :: Float } deriving (Eq, Show) +data Genome = Genome { numInput :: Int, numInternal :: Int, numOutput :: Int, genes :: [Gene] } deriving (Eq, Show) + + +-- choose a random list element +randomChoice :: RandomGen a => [b] -> a -> (b, a) +randomChoice xs r = + let (index, r') = randomR (0, length xs - 1) r + in (xs !! index, r') + + +-- pick a new random source for the gene +mutateGeneSource :: RandomGen a => Genome -> Gene -> a -> (Gene, a) +mutateGeneSource genome g r = + let + nInput = numInput genome + nInternal = numInternal genome + index :: Int + (index, r') = randomR (0, nInput+nInternal-1) r + source' = + if index < nInput + then Input index + else Internal $ index - nInput + in ( g { source = source' }, r') + + +-- pick a new random sink for the gene +mutateGeneSink :: RandomGen a => Genome -> Gene -> a -> (Gene, a) +mutateGeneSink genome g r = + let + nInternal = numInternal genome + nOutput = numOutput genome + index :: Int + (index, r') = randomR (0, nInternal+nOutput-1) r + sink' = if index < nInternal + then Internal index + else Output $ index - nInternal + in ( g { sink = sink' }, r' ) + + +-- pick a new random weight, in the range (-w, w) +-- where w is twice the absolute value of the current weight +mutateGeneWeight :: RandomGen a => Genome -> Gene -> a -> (Gene, a) +mutateGeneWeight _ g r = + let + w = 2 * (abs $ weight g) + (weight', r') = randomR (negate w, w) r + in ( g { weight = weight' }, r' ) + + +-- randomly mutate gene (modify source, sink, or weight +mutateGene :: RandomGen a => Genome -> Gene -> a -> (Gene, a) +mutateGene genome g r = + let (f, r') = randomChoice [mutateGeneSource, mutateGeneSink, mutateGeneWeight] r + in f genome g r' diff --git a/test/GenomeTest.hs b/test/GenomeTest.hs new file mode 100644 index 0000000..0a27a94 --- /dev/null +++ b/test/GenomeTest.hs @@ -0,0 +1,72 @@ +module GenomeTest (suite) where + +import Test.Tasty +import Test.Tasty.HUnit +import qualified Mind as M +import Genome +import System.Random + +suite :: TestTree +suite = testGroup "genome tests" $ + [ mutationTests + ] + + +mutationTests :: TestTree +mutationTests = testGroup "mutations" $ + [ testCase "mutating the source of a gene" $ + let + rand = mkStdGen 1 -- randomR generates sequence (1, 2, 0, ...) + genome = Genome { numInput = 2, numInternal = 1, numOutput = 5, genes = [] } + sourceGene = Gene { source = M.Input 0, sink = M.Output 1, weight = 4 } + mutatedGenes = fst $ foldl + (\(list, r) g -> + let (g', r') = mutateGeneSource genome g r + in (list ++ [g'], r') + ) + ([], rand) + (replicate 3 sourceGene) + in + mutatedGenes @?= + [ Gene { source = M.Input 1, sink = M.Output 1, weight = 4 } + , Gene { source = M.Internal 0, sink = M.Output 1, weight = 4 } + , Gene { source = M.Input 0, sink = M.Output 1, weight = 4 } + ] + , testCase "mutating the sink of a gene" $ + let + rand = mkStdGen 1 -- randomR generates sequence (1, 2, 0, ...) + genome = Genome { numInput = 2, numInternal = 1, numOutput = 2, genes = [] } + sourceGene = Gene { source = M.Input 0, sink = M.Output 1, weight = 4 } + mutatedGenes = fst $ foldl + (\(list, r) g -> + let (g', r') = mutateGeneSink genome g r + in (list ++ [g'], r') + ) + ([], rand) + (replicate 3 sourceGene) + in + mutatedGenes @?= + [ Gene { source = M.Input 0, sink = M.Output 0, weight = 4 } + , Gene { source = M.Input 0, sink = M.Output 1, weight = 4 } + , Gene { source = M.Input 0, sink = M.Internal 0, weight = 4 } + ] + , testCase "mutating the weight of a gene" $ + let + rand = mkStdGen 0 -- randomR generates sequence (7.572357,-1.4116564,-7.2413177, ...) + genome = Genome { numInput = 2, numInternal = 1, numOutput = 2, genes = [] } + sourceGene = Gene { source = M.Input 0, sink = M.Output 1, weight = 4 } + mutatedGenes = fst $ foldl + (\(list, r) g -> + let (g', r') = mutateGeneWeight genome g r + in (list ++ [g'], r') + ) + ([], rand) + (replicate 3 sourceGene) + in + mutatedGenes @?= + [ Gene { source = M.Input 0, sink = M.Output 1, weight = 7.572357} + , Gene { source = M.Input 0, sink = M.Output 1, weight = -1.4116564 } + , Gene { source = M.Input 0, sink = M.Internal 1, weight = -7.2413177 } + ] + + ] diff --git a/test/Main.hs b/test/Main.hs index 8847a07..907bbdd 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -3,8 +3,10 @@ module Main (main) where import Test.Tasty import qualified MindTest +import qualified GenomeTest main :: IO () main = defaultMain $ testGroup "all tests" $ [ MindTest.suite + , GenomeTest.suite ] -- cgit v1.2.1