├── .gitignore ├── project ├── build.properties ├── plugins.sbt ├── CatsEffectForkPlugin.scala ├── LintingPlugin.scala └── ProjectPlugin.scala ├── version.sbt ├── spawning-pool-core-alpha └── src │ ├── main │ └── scala │ │ └── com │ │ └── htmlism │ │ └── spawningpool │ │ ├── evolution │ │ └── Evolution.scala │ │ ├── generation │ │ └── Generation.scala │ │ ├── package.scala │ │ └── fitness │ │ └── Fitness.scala │ └── test │ └── scala │ └── com │ └── htmlism │ └── spawningpool │ └── fitness │ └── FitnessSpec.scala ├── spawning-pool-core └── src │ ├── main │ └── scala │ │ └── com │ │ └── htmlism │ │ └── spawningpool │ │ ├── RandomIndexProvider.scala │ │ ├── ChromosomeGenerator.scala │ │ ├── combinatorics │ │ ├── DiscreteAlleleGenerator.scala │ │ ├── DiscreteFixedLengthCombinator.scala │ │ ├── DiscreteVariableLengthCombinator.scala │ │ ├── package.scala │ │ ├── specialized │ │ │ ├── DiscreteFixedLengthCombinator.scala │ │ │ ├── DiscreteVariableLengthCombinator.scala │ │ │ └── HomogenousCombinator.scala │ │ ├── GeneIndexProvider.scala │ │ ├── AlleleIndexProvider.scala │ │ ├── LengthProvider.scala │ │ ├── CrossoverParentProvider.scala │ │ ├── MutationMethodProvider.scala │ │ ├── VariationProvider.scala │ │ ├── FixedLengthCombinator.scala │ │ ├── AlleleGenerator.scala │ │ ├── DefaultRandomProvider.scala │ │ ├── HomogenousCombinator.scala │ │ └── VariableLengthCombinator.scala │ │ ├── generation │ │ ├── package.scala │ │ └── generators.scala │ │ ├── PositiveCount.scala │ │ ├── Crossover.scala │ │ ├── package.scala │ │ ├── Mutation.scala │ │ ├── Evolver.scala │ │ ├── Generator.scala │ │ ├── SolutionContext.scala │ │ └── Solver.scala │ └── test │ └── scala │ └── com │ └── htmlism │ └── spawningpool │ ├── GeneratorSpec.scala │ ├── SolverSpec.scala │ └── combinatorics │ ├── DiscreteAlleleGeneratorSpec.scala │ ├── FixedLengthCombinatorSpec.scala │ └── VariableLengthCombinatorSpec.scala ├── .scalafix.conf ├── RELEASES.md ├── .github └── workflows │ └── ci.yml ├── benchmark └── src │ └── main │ └── scala │ └── com │ └── htmlism │ └── spawningpool │ ├── Benchmark.scala │ └── VectorVsArray.scala ├── LICENSE ├── README.md ├── .scalafmt.conf └── TODO.md /.gitignore: -------------------------------------------------------------------------------- 1 | .bsp 2 | .idea 3 | target 4 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.7 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "0.100.1-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /spawning-pool-core-alpha/src/main/scala/com/htmlism/spawningpool/evolution/Evolution.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.evolution 2 | 3 | trait Evolution[A] {} 4 | -------------------------------------------------------------------------------- /spawning-pool-core-alpha/src/main/scala/com/htmlism/spawningpool/generation/Generation.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.generation 2 | 3 | trait Generation[A] {} 4 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/RandomIndexProvider.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | trait RandomIndexProvider { 4 | def randomIndex(size: Int): Int 5 | } 6 | -------------------------------------------------------------------------------- /.scalafix.conf: -------------------------------------------------------------------------------- 1 | rules = [ 2 | OrganizeImports 3 | ] 4 | 5 | OrganizeImports { 6 | removeUnused = true 7 | expandRelative = true 8 | groups = [ 9 | "java", 10 | "scala.", 11 | "*", 12 | "com.htmlism" 13 | ] 14 | targetDialect = Scala3 15 | } 16 | -------------------------------------------------------------------------------- /spawning-pool-core-alpha/src/main/scala/com/htmlism/spawningpool/package.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism 2 | 3 | package object spawningpool { 4 | type Generation[A] = generation.Generation[A] 5 | type Evolution[A] = evolution.Evolution[A] 6 | type Fitness[A] = fitness.Fitness[A] 7 | } 8 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/ChromosomeGenerator.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | /** 4 | * A base trait for sources that can generate chromosomes 5 | * 6 | * @tparam A 7 | * The type of the chromosome 8 | */ 9 | trait ChromosomeGenerator[A] { 10 | def generateChromosome: A 11 | } 12 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/DiscreteAlleleGenerator.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | trait DiscreteAlleleGenerator[A] extends AlleleGenerator[A] with AlleleIndexProvider { 4 | def alleles: Seq[A] 5 | 6 | def generateAllele: A = alleles(nextAlleleIndex(alleles.size)) 7 | } 8 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/DiscreteFixedLengthCombinator.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | class DiscreteFixedLengthCombinator[A](val alleles: Seq[A], val size: Int) 4 | extends FixedLengthCombinator[A] 5 | with DiscreteAlleleGenerator[A] 6 | with DefaultRandomProvider 7 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/DiscreteVariableLengthCombinator.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | class DiscreteVariableLengthCombinator[A](val alleles: Seq[A], val initialSize: Int) 4 | extends VariableLengthCombinator[A] 5 | with DiscreteAlleleGenerator[A] 6 | with DefaultRandomProvider 7 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.5") 2 | addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.2") 3 | addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.4.1") 4 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") 5 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.3") 6 | addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") 7 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/generation/package.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | import scala.util.Random 4 | 5 | package object generation { 6 | private[generation] val DEFAULT_ARRAY_LENGTH = 100 7 | 8 | private[generation] val rngInt = () => Random.nextInt() 9 | private[generation] val rngDouble = () => Random.nextDouble() 10 | 11 | private[generation] val rngLength = (max: Int) => Random.nextInt(max) 12 | } 13 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/package.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | /** 4 | * Chromosomes are often implemented as collections of some type. Included in this package are helper traits that 5 | * leverage this pattern. 6 | * 7 | * The element type of the chromosome can either be continuous (e.g. `Int`, `Double`) or discrete (i.e. elements from a 8 | * sequence). 9 | */ 10 | package object combinatorics 11 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/specialized/DiscreteFixedLengthCombinator.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics.specialized 2 | 3 | import scala.reflect.ClassTag 4 | 5 | import com.htmlism.spawningpool.combinatorics 6 | 7 | class DiscreteFixedLengthCombinator[A](alleles: Seq[A], size: Int)(implicit val classTag: ClassTag[A]) 8 | extends combinatorics.DiscreteFixedLengthCombinator(alleles, size) 9 | with HomogenousCombinator[A] 10 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/specialized/DiscreteVariableLengthCombinator.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics.specialized 2 | 3 | import scala.reflect.ClassTag 4 | 5 | import com.htmlism.spawningpool.combinatorics 6 | 7 | class DiscreteVariableLengthCombinator[A](alleles: Seq[A], size: Int)(implicit val classTag: ClassTag[A]) 8 | extends combinatorics.DiscreteVariableLengthCombinator(alleles, size) 9 | with HomogenousCombinator[A] 10 | -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | ## 0.100.1 2 | 3 | * added missng scalaz resolver 4 | * added scoverage support 5 | * added specialized combinators 6 | 7 | ## 0.100.0 8 | 9 | * homogenous mutation is a function of the allele being mutated 10 | * allowed maximum generations to be dynamic 11 | * dropped deprecated classes 12 | 13 | ## 0.0.2 14 | 15 | * added variation provider 16 | * unhid working variable crossover implementation 17 | * implemented add gene mutation 18 | * implemented remove gene mutation 19 | * deprecated old world chromosome model 20 | 21 | ## 0.0.1 22 | 23 | * initial release 24 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/specialized/HomogenousCombinator.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics.specialized 2 | 3 | import scala.collection.immutable.ArraySeq 4 | import scala.reflect.ClassTag 5 | 6 | import com.htmlism.spawningpool.combinatorics 7 | 8 | trait HomogenousCombinator[A] extends combinatorics.HomogenousCombinator[A] { 9 | implicit def classTag: ClassTag[A] 10 | 11 | override def fill(size: Int): (=> A) => Seq[A] = { fill => 12 | ArraySeq.unsafeWrapArray(Array.fill[A](size)(fill)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /spawning-pool-core/src/test/scala/com/htmlism/spawningpool/GeneratorSpec.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | import org.specs2.mutable.Specification 4 | 5 | class GeneratorSpec extends Specification { 6 | "Generation" should { 7 | "provide default implementations" in { 8 | findGenerator[Int].generate 9 | findGenerator[Double].generate 10 | findGenerator[Array[Int]].generate 11 | findGenerator[Array[Double]].generate 12 | 13 | true 14 | } 15 | } 16 | 17 | private def findGenerator[A](implicit generator: Generator[A]) = generator 18 | } 19 | -------------------------------------------------------------------------------- /spawning-pool-core/src/test/scala/com/htmlism/spawningpool/SolverSpec.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | import org.specs2.mutable.Specification 4 | 5 | import com.htmlism.spawningpool.Solver.* 6 | 7 | class SolverSpec extends Specification { 8 | "The solver" should { 9 | "select individuals at random" in { 10 | val rig = new RandomIndexProvider { 11 | val iterator = Iterable(0).iterator 12 | 13 | def randomIndex(size: Int) = iterator.next() 14 | } 15 | 16 | randomIndividual(Seq("arthas"))(rig) === "arthas" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /project/CatsEffectForkPlugin.scala: -------------------------------------------------------------------------------- 1 | import sbt.* 2 | import sbt.Keys.* 3 | 4 | /** 5 | * Automatically enriches projects with the following settings (despite the word "override"). 6 | */ 7 | object CatsEffectForkPlugin extends AutoPlugin { 8 | 9 | /** 10 | * Thus plug-in will automatically be enabled; it has no requirements. 11 | */ 12 | override def trigger: PluginTrigger = 13 | AllRequirements 14 | 15 | // cats-effect prefers to run in its own main thread 16 | // https://github.com/typelevel/cats-effect/pull/3774 17 | override val buildSettings: Seq[Setting[?]] = Seq( 18 | fork := true 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: 4 | push: 5 | branches: ['**'] 6 | 7 | jobs: 8 | lint-and-test: 9 | name: Unit test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - uses: actions/setup-java@v4 15 | with: 16 | distribution: temurin 17 | java-version: '23' 18 | cache: sbt 19 | 20 | - uses: sbt/setup-sbt@v1 21 | 22 | - name: Check linting and formatting 23 | run: sbt 'scalafixAll --check' scalafmtSbtCheck scalafmtCheck 24 | 25 | - name: Unit test 26 | run: sbt +test 27 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/GeneIndexProvider.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | /** 4 | * This trait describes the source from which evolvers determine where spot mutation occurs 5 | * 6 | * Concrete classes will typically mix in `DefaultRandomProvider`. 7 | */ 8 | trait GeneIndexProvider { 9 | 10 | /** 11 | * Returns the next gene index from this provider 12 | * 13 | * @param size 14 | * The length of the chromosome where this index will be used 15 | * @return 16 | * An index 17 | */ 18 | def nextGeneIndex(size: Int): Int 19 | } 20 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/AlleleIndexProvider.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | /** 4 | * This trait describes the source from which discrete allele generators get their indices. 5 | * 6 | * Concrete classes will typically mix in `DefaultRandomProvider`. 7 | */ 8 | trait AlleleIndexProvider { 9 | 10 | /** 11 | * Returns the next allele index from this provider 12 | * 13 | * @param size 14 | * The size of the collection where the index will be used 15 | * @return 16 | * An index 17 | */ 18 | def nextAlleleIndex(size: Int): Int 19 | } 20 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/LengthProvider.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | /** 4 | * This trait describes the source from which variable-length generators will determine how big the next chromosome 5 | * will be 6 | * 7 | * Concrete classes will typically mix in `DefaultRandomProvider`. 8 | */ 9 | trait LengthProvider { 10 | 11 | /** 12 | * Returns the next length from this provider 13 | * 14 | * @param maximum 15 | * The maximum possible length (inclusive) 16 | * @return 17 | * A length 18 | */ 19 | def nextLength(maximum: Int): Int 20 | } 21 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/CrossoverParentProvider.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | /** 4 | * This trait describes the source from which variable-length evolvers determine which allele's parents to select 5 | * during crossover. 6 | * 7 | * Concrete classes will typically mix in `DefaultRandomProvider`. 8 | */ 9 | trait CrossoverParentProvider { 10 | 11 | /** 12 | * Returns the next boolean from this provider 13 | * 14 | * @return 15 | * `true` if the first parent should be selected, `false` otherwise 16 | */ 17 | def nextUseFirstParent: Boolean 18 | } 19 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/PositiveCount.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | object PositiveCount { 4 | def apply(count: Int): PositiveCount = 5 | if (count > 0) 6 | new PositiveCount(count) 7 | else 8 | throw new IllegalArgumentException(s"$count is not positive") 9 | } 10 | 11 | class PositiveCount private (val count: Int) extends AnyVal { 12 | 13 | /** 14 | * Returns a new positive count with one less. Could throw an exception if the count is already 1. 15 | * 16 | * @return 17 | * A positive count 18 | */ 19 | def minusOne: PositiveCount = PositiveCount(count - 1) 20 | } 21 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/MutationMethodProvider.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | /** 4 | * This trait describes the source from which variable-length evolvers will determine which mutation method to use 5 | * 6 | * Concrete classes will typically mix in `DefaultRandomProvider`. 7 | */ 8 | trait MutationMethodProvider { 9 | 10 | /** 11 | * Returns the next mutation method from this provider. The value will be one of `MutateGene`, `AddGene`, and 12 | * `RemoveGene`. 13 | * 14 | * @return 15 | * A mutation method 16 | */ 17 | def nextMutationMethod: MutationMethod 18 | } 19 | -------------------------------------------------------------------------------- /spawning-pool-core/src/test/scala/com/htmlism/spawningpool/combinatorics/DiscreteAlleleGeneratorSpec.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | import org.specs2.mutable.Specification 4 | 5 | class DiscreteAlleleGeneratorSpec extends Specification { 6 | "A discrete allele generator" should { 7 | "return the expected values" in { 8 | 9 | val generator = new DiscreteAlleleGenerator[String] with AlleleIndexProvider { 10 | private val rng = Iterable(2, 0, 1).iterator 11 | 12 | val alleles = Seq("alpha", "beta", "gamma") 13 | 14 | def nextAlleleIndex(size: Int) = rng.next() 15 | } 16 | 17 | generator.generateAllele === "gamma" 18 | generator.generateAllele === "alpha" 19 | generator.generateAllele === "beta" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /project/LintingPlugin.scala: -------------------------------------------------------------------------------- 1 | import sbt.Keys.* 2 | import sbt.* 3 | import scalafix.sbt.ScalafixPlugin.autoImport.* 4 | import org.typelevel.sbt.tpolecat.TpolecatPlugin.autoImport.tpolecatExcludeOptions 5 | import org.typelevel.scalacoptions.ScalacOptions 6 | import wartremover.Wart 7 | import wartremover.WartRemover.autoImport.* 8 | 9 | object LintingPlugin extends AutoPlugin { 10 | override def trigger = 11 | allRequirements 12 | 13 | override val globalSettings = 14 | addCommandAlias("fmt", "; scalafmtSbt; scalafmtAll") ++ 15 | addCommandAlias("fix", "scalafixAll") 16 | 17 | override val buildSettings = 18 | Seq( 19 | tpolecatExcludeOptions += ScalacOptions.fatalWarnings, 20 | wartremoverWarnings ++= Warts.unsafe diff List(Wart.Any), 21 | semanticdbEnabled := true, 22 | semanticdbVersion := scalafixSemanticdb.revision 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /benchmark/src/main/scala/com/htmlism/spawningpool/Benchmark.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | object Benchmark { 4 | private def toDurations(times: Int)(f: () => Unit): Seq[Long] = 5 | for (n <- 1 to times) yield { 6 | println() 7 | println(s"Running $n...") 8 | 9 | val start = compat.Platform.currentTime 10 | 11 | f() 12 | 13 | val duration = compat.Platform.currentTime - start 14 | val duratinInSeconds = duration / 1000 15 | 16 | duratinInSeconds 17 | } 18 | 19 | def apply[A](fs: Map[A, () => Unit], times: Int): Unit = { 20 | val durations = fs 21 | .mapValues(toDurations(times)) 22 | .map(identity) 23 | 24 | println("Durations:") 25 | durations.foreach(println) 26 | 27 | val averages = durations 28 | .mapValues(_.sum / times) 29 | 30 | println("Average duration:") 31 | averages.foreach(println) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/Crossover.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | /** 4 | * A typeclass that represents a crossover operation for chromosomes. 5 | * 6 | * This operation can accept an arbitrary number of chromosomes, though genetic crossover typically involves only two 7 | * parents. 8 | * 9 | * @tparam A 10 | * The type of the chromosomes 11 | */ 12 | trait Crossover[A] { 13 | 14 | /** 15 | * Returns a new chromosome given a collection of parent chromosomes. 16 | * 17 | * @param xs 18 | * The parent chromosomes 19 | * @return 20 | * A chromosome 21 | */ 22 | def crossover(xs: List[A]): A 23 | } 24 | 25 | object IntUniformCrossover extends Crossover[Int] { 26 | def crossover(xs: List[Int]): Int = ??? 27 | } 28 | 29 | object DoubleUniformCrossover extends Crossover[Double] { 30 | def crossover(xs: List[Double]): Double = ??? 31 | } 32 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/VariationProvider.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | /** 4 | * This trait describes the source from which variable-length evolvers will determine the mechanics of mutation 5 | * 6 | * Concrete classes will typically mix in `DefaultRandomProvider`. 7 | */ 8 | trait VariationProvider { 9 | 10 | /** 11 | * Returns the next gene index for insertion from this provider 12 | * 13 | * @param size 14 | * The size of the chromosome to mutate 15 | * @return 16 | * An index 17 | */ 18 | def nextGeneIndexForInsertion(size: Int): Int 19 | 20 | /** 21 | * Returns the next gene index for removal from this provider 22 | * 23 | * @param size 24 | * The size of the chromosome to mutate 25 | * @return 26 | * An index 27 | */ 28 | def nextGeneIndexForRemoval(size: Int): Int 29 | } 30 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/FixedLengthCombinator.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | /** 4 | * The base trait for combinators that create and manipulate chromosomes of a fixed length. 5 | * 6 | * @tparam A 7 | * The type of each gene in the chromosome 8 | */ 9 | trait FixedLengthCombinator[A] extends HomogenousCombinator[A] { 10 | 11 | /** 12 | * The number of elements for every chromosome generated. 13 | * 14 | * @return 15 | * A number 16 | */ 17 | def size: Int 18 | 19 | def generateChromosome: B = fill(size)(generateAllele) 20 | 21 | override def crossover(firstParent: B, secondParent: B): B = 22 | (0 until size).flatMap { i => 23 | val parent = if (nextUseFirstParent) firstParent else secondParent 24 | 25 | if (parent.isDefinedAt(i)) 26 | parent(i) :: Nil 27 | else 28 | Nil 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/AlleleGenerator.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | /** 4 | * A base trait for sources that can generate alleles, the individual parts of a chromosome 5 | * 6 | * @tparam A 7 | * The type of the allele 8 | */ 9 | trait AlleleGenerator[A] { 10 | 11 | /** 12 | * Generates an allele. 13 | * 14 | * This form of generation is used during chromosome generation. 15 | * 16 | * @return 17 | * An allele 18 | */ 19 | def generateAllele: A 20 | 21 | /** 22 | * Generates an allele given an existing allele. 23 | * 24 | * This form of generation is used during mutation. 25 | * 26 | * @param allele 27 | * The current allele being replaced 28 | * @return 29 | * An allele 30 | */ 31 | def generateAllele(allele: A): A = { 32 | val _ = allele 33 | generateAllele 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/package.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism 2 | 3 | /** 4 | * The `spawning-pool` framework enables the search and generation of solutions encoded as a chromosome over a fitness 5 | * landscape, all in a generic manner typical of Scala libraries. 6 | * 7 | * Because genetic algorithms are computationally intensive, this framework uses futures to perform evolution 8 | * concurrently. 9 | */ 10 | package object spawningpool { 11 | implicit object DefaultRandomIndexProvider extends RandomIndexProvider { 12 | private val rng = new util.Random 13 | 14 | def randomIndex(size: Int): Int = rng.nextInt(size) 15 | } 16 | 17 | implicit def countToInt(count: PositiveCount): Int = count.count 18 | 19 | implicit val asdf: Mutation[Int] = IntMutation 20 | 21 | implicit class MutationOps[A: Mutation](x: A) { 22 | def mutate: A = implicitly[Mutation[A]].mutate(x) 23 | } 24 | 25 | def muz[A: Mutation](x: A): A = implicitly[Mutation[A]].mutate(x) 26 | } 27 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/Mutation.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | /** 6 | * A typeclass that represents a unary mutation operation for a chromosome. 7 | * 8 | * @tparam A 9 | * The type of the chromosome 10 | */ 11 | 12 | @implicitNotFound("Please define mutation support for type ${A}") 13 | trait Mutation[A] { 14 | 15 | /** 16 | * Returns a mutated version of a chromosome. 17 | * 18 | * @param x 19 | * A chromosome 20 | * 21 | * @return 22 | * A chromosome 23 | */ 24 | def mutate(x: A): A 25 | } 26 | 27 | object IntMutation extends Mutation[Int] { 28 | def mutate(x: Int): Int = x + 1 29 | } 30 | 31 | object DoubleMutation extends Mutation[Double] { 32 | def mutate(x: Double): Double = ??? 33 | } 34 | 35 | object FixedArrayIntMutation extends Mutation[Array[Int]] { 36 | def mutate(x: Array[Int]): Array[Int] = ??? 37 | } 38 | 39 | object FixedDoubleMutation extends Mutation[Array[Double]] { 40 | def mutate(x: Array[Double]): Array[Double] = ??? 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mark Canlas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/Evolver.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | /** 4 | * A base trait for genetic operators 5 | * 6 | * @tparam A 7 | * The type of the chromosome 8 | */ 9 | trait Evolver[A] { 10 | 11 | /** 12 | * Mutates a given chromosome 13 | * 14 | * The intent of mutation is to provide a small, random change to an existing chromosome and yield a new chromosome 15 | * whose fitness is somewhat similar to the original 16 | * 17 | * @param chromosome 18 | * The chromosome to mutate 19 | * @return 20 | * A new chromosome 21 | */ 22 | def mutate(chromosome: A): A 23 | 24 | /** 25 | * Combines two parent chromosomes to yield a child chromosome 26 | * 27 | * The intent of combination is to produce a chromosome whose fitness reflects the fitness of parts of its parents 28 | * 29 | * @param firstParent 30 | * A parent chromosome 31 | * @param secondParent 32 | * Another parent chromosome 33 | * @return 34 | * A new chromosome 35 | */ 36 | def crossover(firstParent: A, secondParent: A): A 37 | } 38 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/Generator.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | import com.htmlism.spawningpool.generation.* 4 | 5 | /** 6 | * A trait that represents a nullary operation for generating chromosomes of `A`. 7 | * 8 | * Chromosomes are typically generated randomly. 9 | * 10 | * @tparam A 11 | * The type of the chromosome 12 | */ 13 | trait Generator[A] { 14 | 15 | /** 16 | * Generates a chromosome. 17 | * 18 | * @return 19 | * A chromosome 20 | */ 21 | def generate: A 22 | } 23 | 24 | object Generator { 25 | implicit val intGenerator: Generator[Int] = IntGenerator 26 | implicit val doubleGenerator: Generator[Double] = DoubleGenerator 27 | 28 | implicit val intArrayGenerator: Generator[Array[Int]] = FixedIntArrayGenerator 29 | implicit val doubleArrayGenerator: Generator[Array[Double]] = 30 | FixedDoubleArrayGenerator 31 | 32 | object VariableLength { 33 | implicit val intArrayGenerator: Generator[Array[Int]] = 34 | VariableIntArrayGenerator 35 | implicit val doubleArrayGenerator: Generator[Array[Double]] = 36 | VariableDoubleArrayGenerator 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | spawning-pool 2 | ============= 3 | 4 | A framework for **genetic algorithms** in Scala. 5 | 6 | This framework enables the search and generation of solutions encoded as a chromosome over a fitness landscape, all in a generic manner typical of Scala libraries. 7 | 8 | Because genetic algorithms are computationally intensive, this framework uses *futures* to perform evolution concurrently. 9 | 10 | This framework also features helper classes for chromosomes that are homogenous collections of some type (e.g. a string of characters or a tour of cities). 11 | 12 | Testing 13 | ------- 14 | 15 | sbt clean "project solver" coverage test 16 | 17 | References 18 | ---------- 19 | * [Essentials of Metaheuristics](http://cs.gmu.edu/~sean/book/metaheuristics/) by Sean Luke 20 | * [Watchmaker Framework](http://watchmaker.uncommons.org/) (Java) 21 | * [Jenetics](https://github.com/jenetics/jenetics) (Java) 22 | * [GeneticSharp](https://github.com/giacomelli/GeneticSharp) (C#) 23 | 24 | Colophon 25 | -------- 26 | 27 | The *StarCraft* video game series features the Zerg, an alien species adept at rapid evolution and genetic manipulation. One of the foundational structures for the Zerg is the **Spawning Pool**. 28 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/DefaultRandomProvider.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | /** 4 | * This trait implements all of the combinatorial providers using Scala's random number generator. 5 | */ 6 | trait DefaultRandomProvider 7 | extends AlleleIndexProvider 8 | with CrossoverParentProvider 9 | with GeneIndexProvider 10 | with LengthProvider 11 | with VariationProvider 12 | with MutationMethodProvider { 13 | private val rng = new util.Random 14 | private val mutations = IndexedSeq(MutateGene, AddGene, RemoveGene) 15 | 16 | def nextAlleleIndex(size: Int): Int = guardedRandom(size) 17 | 18 | def nextGeneIndex(size: Int): Int = guardedRandom(size) 19 | 20 | def nextUseFirstParent: Boolean = rng.nextBoolean() 21 | 22 | def nextMutationMethod: MutationMethod = 23 | mutations(rng.nextInt(mutations.size)) 24 | 25 | def nextLength(size: Int): Int = guardedRandom(size + 1) 26 | 27 | // variation 28 | def nextGeneIndexForInsertion(size: Int): Int = guardedRandom(size + 1) 29 | def nextGeneIndexForRemoval(size: Int): Int = guardedRandom(size) 30 | 31 | private def guardedRandom(n: Int) = { 32 | assert(n != 0) 33 | 34 | rng.nextInt(n) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /spawning-pool-core-alpha/src/test/scala/com/htmlism/spawningpool/fitness/FitnessSpec.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.fitness 2 | 3 | import org.specs2.mutable.Specification 4 | 5 | class FitnessSpec extends Specification { 6 | "fitness by ordering" should { 7 | "summon the implicit ordering" in { 8 | val fitness = new OrdinalFitness[(String, Int)] 9 | 10 | fitness.compare("a" -> 1, "a" -> 2) === -1 11 | fitness.compare("a" -> 1, "a" -> 1) === 0 12 | fitness.compare("b" -> 1, "a" -> 2) === 1 13 | } 14 | } 15 | 16 | "ratio fitness" should { 17 | "summon the implicit numeric" in { 18 | val fitness = new RatioFitness((s: String) => s.length) 19 | 20 | fitness.compare("short", "longest") === -1 21 | fitness.compare("equal", "apple") === 0 22 | fitness.compare("longest", "short") === 1 23 | } 24 | } 25 | 26 | "fitness" should { 27 | "support minimization" in { 28 | val fitness = new OrdinalFitness[Int] 29 | 30 | fitness.compare(123, 45) === 1 31 | fitness.minimize.compare(123, 45) === -1 32 | } 33 | 34 | "support chaining" in { 35 | val fitness1 = new OrdinalFitness[Int] 36 | val fitness2 = new OrdinalFitness[Double] 37 | val totalFitness = fitness1 andThen fitness2 38 | 39 | totalFitness.compare((1, 1d), (1, 2d)) === -1 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.9.9" 2 | 3 | runner.dialect = "scala213source3" 4 | 5 | fileOverride { 6 | "glob:**.sbt" { 7 | runner.dialect = scala212source3 8 | } 9 | 10 | "glob:**/project/**.*" { 11 | runner.dialect = scala212source3 12 | } 13 | } 14 | 15 | align.preset = more 16 | 17 | align.tokens = [ 18 | { code = "=" }, 19 | 20 | { code = "extends" }, 21 | 22 | { code = "//" }, 23 | 24 | { code = "<-", owners = ["Enumerator.Generator"] }, 25 | 26 | { code = "=", owners = ["(Enumerator.Val|Defn.(Va(l|r)|GivenAlias|Def|Type))"] }, 27 | 28 | { code = "=>", owners = ["Case"] }, 29 | 30 | { code = "->", owners = ["Term.ApplyInfix"] }, 31 | 32 | { code = ":=", owners = ["Term.ApplyInfix"] }, 33 | { code = "%", owners = ["Term.ApplyInfix"] }, 34 | { code = "%%", owners = ["Term.ApplyInfix"] } 35 | ] 36 | 37 | align.allowOverflow = true, 38 | 39 | align.tokenCategory = { 40 | Equals = Assign, 41 | LeftArrow = Assign 42 | } 43 | 44 | maxColumn = 120 45 | 46 | docstrings.style = SpaceAsterisk 47 | docstrings.blankFirstLine = yes 48 | docstrings.wrap = yes 49 | 50 | includeNoParensInSelectChains = true 51 | optIn.breakChainOnFirstMethodDot = true 52 | 53 | rewrite.rules = [RedundantBraces] 54 | rewrite.redundantBraces.ifElseExpressions = true 55 | rewrite.redundantBraces.stringInterpolation = true 56 | 57 | rewrite.scala3.convertToNewSyntax = true 58 | rewrite.scala3.removeOptionalBraces = true 59 | 60 | rewrite.trailingCommas.style = keep 61 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/generation/generators.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | package generation 3 | 4 | import scala.util.Random 5 | 6 | object IntGenerator extends Generator[Int] { 7 | def generate: Int = Random.nextInt() 8 | } 9 | 10 | object DoubleGenerator extends Generator[Double] { 11 | def generate: Double = Random.nextDouble() 12 | } 13 | 14 | object FixedIntArrayGenerator extends FixedIntArrayGenerator(DEFAULT_ARRAY_LENGTH, rngInt) 15 | object FixedDoubleArrayGenerator extends FixedDoubleArrayGenerator(DEFAULT_ARRAY_LENGTH, rngDouble) 16 | object VariableIntArrayGenerator extends VariableIntArrayGenerator(DEFAULT_ARRAY_LENGTH, rngInt, rngLength) 17 | object VariableDoubleArrayGenerator extends VariableDoubleArrayGenerator(DEFAULT_ARRAY_LENGTH, rngDouble, rngLength) 18 | 19 | // fixed 20 | 21 | class FixedIntArrayGenerator(length: Int, nextGene: () => Int) extends Generator[Array[Int]] { 22 | def generate: Array[Int] = Array.fill(length)(nextGene()) 23 | } 24 | 25 | class FixedDoubleArrayGenerator(length: Int, nextGene: () => Double) extends Generator[Array[Double]] { 26 | def generate: Array[Double] = Array.fill(length)(nextGene()) 27 | } 28 | 29 | // variable 30 | 31 | class VariableIntArrayGenerator(max: Int, nextGene: () => Int, nextLength: Int => Int) extends Generator[Array[Int]] { 32 | def generate: Array[Int] = Array.fill(nextLength(max))(nextGene()) 33 | } 34 | 35 | class VariableDoubleArrayGenerator(max: Int, nextGene: () => Double, nextLength: Int => Int) 36 | extends Generator[Array[Double]] { 37 | def generate: Array[Double] = Array.fill(nextLength(max))(nextGene()) 38 | } 39 | -------------------------------------------------------------------------------- /project/ProjectPlugin.scala: -------------------------------------------------------------------------------- 1 | import sbt.Keys.* 2 | import sbt.* 3 | 4 | /** 5 | * Automatically enriches projects with the following settings (despite the word "override"). 6 | */ 7 | object ProjectPlugin extends AutoPlugin { 8 | 9 | /** 10 | * Defines what members will be imported to the `build.sbt` scope. 11 | */ 12 | val autoImport = ThingsToAutoImport 13 | 14 | /** 15 | * Thus plug-in will automatically be enabled; it has no requirements. 16 | */ 17 | override def trigger: PluginTrigger = AllRequirements 18 | 19 | object ThingsToAutoImport { 20 | private def jarName(s: String) = 21 | "spawning-pool-" + s 22 | 23 | def module(s: String): Project = 24 | Project(s, file(jarName(s))) 25 | .settings(name := jarName(s)) 26 | 27 | implicit class ProjectOps(p: Project) { 28 | def withCats: Project = 29 | p 30 | .settings(libraryDependencies += "org.typelevel" %% "cats-core" % "2.13.0") 31 | 32 | def withEffectMonad: Project = 33 | p.settings(libraryDependencies += "org.typelevel" %% "cats-effect" % "3.6.3") 34 | 35 | def withTesting: Project = { 36 | val weaverVersion = 37 | "0.10.1" 38 | 39 | p.settings( 40 | libraryDependencies ++= Seq( 41 | "org.typelevel" %% "weaver-cats" % weaverVersion % Test, 42 | "org.typelevel" %% "weaver-scalacheck" % weaverVersion % Test 43 | ) 44 | ) 45 | } 46 | 47 | def withYaml: Project = 48 | p.settings( 49 | libraryDependencies ++= Seq( 50 | "io.circe" %% "circe-yaml" % "0.15.1" 51 | ) 52 | ) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /spawning-pool-core/src/test/scala/com/htmlism/spawningpool/combinatorics/FixedLengthCombinatorSpec.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | import org.specs2.mutable.Specification 4 | 5 | class FixedLengthCombinatorSpec extends Specification { 6 | "A fixed-length combinator" should { 7 | val size = 3 8 | 9 | val combinator = new FixedTestCombinator(size) 10 | 11 | val firstChromosome = combinator.generateChromosome 12 | val secondChromosome = combinator.generateChromosome 13 | 14 | val mutatedChromosome = combinator.mutate(firstChromosome) 15 | 16 | "generate chromosomes of a fixed length" in { 17 | firstChromosome.length === size 18 | secondChromosome.length === size 19 | } 20 | 21 | "generate the expected values" in { 22 | firstChromosome === Seq("luigi", "bowser", "peach") 23 | secondChromosome === Seq("mario", "luigi", "bowser") 24 | } 25 | 26 | "support spot mutation" in { 27 | mutatedChromosome === Seq("luigi", "mario", "peach") 28 | } 29 | 30 | "support crossover" in { 31 | combinator.crossover(firstChromosome, secondChromosome) === Seq("mario", "luigi", "peach") 32 | } 33 | } 34 | } 35 | 36 | class FixedTestCombinator(val size: Int) extends FixedLengthCombinator[String] with DiscreteAlleleGenerator[String] { 37 | private val alleleIndexes = Iterable(1, 3, 2, 0, 1, 3, 0).iterator 38 | private val parents = Iterable(false, false, true).iterator 39 | 40 | def alleles: Seq[String] = Seq("mario", "luigi", "peach", "bowser") 41 | 42 | def nextAlleleIndex(size: Int) = alleleIndexes.next() 43 | 44 | def nextGeneIndex(unused: Int) = 1 45 | 46 | def nextUseFirstParent = parents.next() 47 | 48 | override def fill(unused: Int): String => List[String] = 49 | List.fill(size)(_) 50 | } 51 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/HomogenousCombinator.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | import com.htmlism.spawningpool.ChromosomeGenerator 4 | import com.htmlism.spawningpool.Evolver 5 | 6 | /** 7 | * A trait for combinators that create and manipulate chromosomes backed by a sequence. 8 | * 9 | * This trait provides a default implementation for mutation. Mutation occurs by replacing a random gene with a 10 | * randomly generated allele. 11 | * 12 | * @tparam A 13 | * The type of each gene in the chromosome 14 | */ 15 | trait HomogenousCombinator[A] 16 | extends AlleleGenerator[A] 17 | with ChromosomeGenerator[Seq[A]] 18 | with Evolver[Seq[A]] 19 | with GeneIndexProvider 20 | with CrossoverParentProvider { 21 | 22 | /** The type of the chromosome */ 23 | type B = Seq[A] 24 | 25 | def mutate(chromosome: B): B = 26 | if (chromosome.isEmpty) 27 | chromosome 28 | else { 29 | val index = nextGeneIndex(chromosome.size) 30 | val alleleToUpdate = chromosome(index) 31 | 32 | chromosome.updated(index, generateAllele(alleleToUpdate)) 33 | } 34 | 35 | def crossover(firstParent: B, secondParent: B): B = { 36 | val size = Math.max(firstParent.size, secondParent.size) 37 | 38 | (0 until size).flatMap { i => 39 | val parent = if (nextUseFirstParent) firstParent else secondParent 40 | 41 | if (parent.isDefinedAt(i)) 42 | parent(i) :: Nil 43 | else 44 | Nil 45 | } 46 | } 47 | 48 | /** 49 | * Generates a chromosome of a given size. 50 | * 51 | * @param size 52 | * The number of genes in the chromosome 53 | * @return 54 | * A chromosome 55 | */ 56 | def fill(size: Int): (=> A) => Seq[A] = Vector.fill[A](size) 57 | } 58 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/combinatorics/VariableLengthCombinator.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | /** 4 | * The base trait for combinators that create and manipulate chromosomes of a variable length. 5 | * 6 | * This trait augments the default mutation operator defined in [[HomogenousCombinator]] with two other alternatives. 7 | * Mutation occurs by randomly choosing one of these three operations: 8 | * 9 | * - Add a randomly generated gene in a random location 10 | * - Delete a random gene 11 | * - Mutate a random gene (via [[HomogenousCombinator]]) 12 | * 13 | * @tparam A 14 | * The type of each gene in the chromosome 15 | */ 16 | trait VariableLengthCombinator[A] 17 | extends HomogenousCombinator[A] 18 | with VariationProvider 19 | with LengthProvider 20 | with MutationMethodProvider { 21 | 22 | /** 23 | * The initial number of elements for every chromosome generated. 24 | * 25 | * @return 26 | * A number 27 | */ 28 | def initialSize: Int 29 | 30 | def generateChromosome: B = fill(nextLength(initialSize))(generateAllele) 31 | 32 | override def mutate(chromosome: B): B = 33 | if (chromosome.isEmpty) 34 | addGene(chromosome) 35 | else 36 | nextMutationMethod match { 37 | case MutateGene => super.mutate(chromosome) 38 | case AddGene => addGene(chromosome) 39 | case RemoveGene => 40 | chromosome.patch(nextGeneIndexForRemoval(chromosome.size), Nil, 1) 41 | } 42 | 43 | private def addGene(chromosome: B) = 44 | chromosome.patch(nextGeneIndexForInsertion(chromosome.size), Seq(generateAllele), 0) 45 | } 46 | 47 | /** 48 | * An algebra for different types of mutation. 49 | */ 50 | sealed trait MutationMethod 51 | 52 | case object MutateGene extends MutationMethod 53 | case object AddGene extends MutationMethod 54 | case object RemoveGene extends MutationMethod 55 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * use fs2 2 | * describe a population to population evolver 3 | * mutation + crossover is slightly prescriptive 4 | * callbacks for islands/aggregation 5 | * case class vs Ints/better primitives 6 | * array vs vector as default Seq implementation 7 | * add healing period for "invalid" (?) solutions 8 | * default value is none/identity 9 | * demonstrate with healing salesman (chromosome is sequence with duplicates, heals to unique sequence) 10 | * determinisitically with canonical sequence (slower?) 11 | * or recklessly with canonical set (faster?) 12 | * describe base hierarchy 13 | * describe combinatorics hierarchy 14 | * doc primary methods and overrides by traits 15 | * consider external operators so that value classes need not extend a trait 16 | * gut chromosome methods 17 | * explore spontaneous generation and how it links to fixed/variable length homogenous chromosomes 18 | * need drosophilia/helloworld 19 | * sodoku?? 20 | * support arbitrary types? 21 | * support arbitary fitness? 22 | * fitness min/maxer (implies ordered?) 23 | * sample generator 24 | * mutation/combination operators (provided as list?) 25 | * implemented as seq yields seq? 26 | * selection operators 27 | * proportional to fitness? 28 | * select random parents? 29 | * elitism? 30 | * supported chromosome types 31 | * fixed length homogenous (Seq[T]) 32 | * cross over, spot mutation (requires () => T, or Chromosome => T) 33 | * variable length homogenous (Stack[T]) 34 | * termination conditions 35 | * time elapsed 36 | * generations elapsed 37 | * state fitness improvmeent 38 | * target fitness reached 39 | * and/or 40 | 41 | ## Redesign 42 | 43 | population evolver. does not specify mutation or crossover. input Seq[A] output Seq[A]. if A itself is also a Seq, can support by default mutation and popular crossover operations 44 | 45 | population manager? used to control islands? 46 | 47 | population evolver itself has termination conditions 48 | 49 | better, earlier support for Array[Int/Double] as chromosomes 50 | 51 | framework for working with Int over discrete collections/as chromosomes 52 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/SolutionContext.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | object SolutionContext { 4 | 5 | /** 6 | * A factory method for solution contexts 7 | * 8 | * @param islandId 9 | * A nominal identifier 10 | * @param fitness 11 | * A fitness function 12 | * @param evolver 13 | * An evolver for solutions 14 | * @param mutationRate 15 | * The rate of mutation from 0 to 1 16 | * @param population 17 | * A collection of solutions 18 | * @param ordering 19 | * An ordering for fitness 20 | * 21 | * @tparam A 22 | * The type of candidate solutions 23 | * @tparam B 24 | * The type of fitness score 25 | * 26 | * @return 27 | * A solution context 28 | */ 29 | def apply[A, B](islandId: Int, fitness: A => B, evolver: Evolver[A], mutationRate: Double, population: Seq[A])( 30 | implicit ordering: Ordering[B] 31 | ): SolutionContext[A, B] = 32 | new SolutionContext(islandId, fitness, evolver, mutationRate, population, 0) 33 | } 34 | 35 | /** 36 | * A bundle of parameters and values for evolution 37 | * 38 | * @param islandId 39 | * A nominal identifier 40 | * @param fitness 41 | * A fitness function 42 | * @param evolver 43 | * An evolver for solutions 44 | * @param mutationRate 45 | * The rate of mutation from 0 to 1 46 | * @param population 47 | * A collection of solutions 48 | * @param generations 49 | * A zero-based ordinal for generations 50 | * @param ordering 51 | * An ordering for fitness 52 | * @tparam A 53 | * The type of candidate solutions 54 | * @tparam B 55 | * The type of fitness score 56 | */ 57 | case class SolutionContext[A, B]( 58 | islandId: Int, 59 | fitness: A => B, 60 | evolver: Evolver[A], 61 | mutationRate: Double, 62 | population: Seq[A], 63 | generations: Int 64 | )(implicit val ordering: Ordering[B]) { 65 | def increment(newPopulation: Seq[A]): SolutionContext[A, B] = 66 | copy(population = newPopulation, generations = generations + 1) 67 | } 68 | -------------------------------------------------------------------------------- /benchmark/src/main/scala/com/htmlism/spawningpool/VectorVsArray.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | import scala.concurrent.ExecutionContext.Implicits.global 4 | 5 | import com.htmlism.spawningpool.combinatorics.* 6 | 7 | object VectorVsArray extends App { 8 | Benchmark( 9 | Map( 10 | "vector of ref" -> withVector _, 11 | "vector of int" -> withVectorInt _, 12 | "array of ref" -> withArray _, 13 | "array of int" -> withArrayInt _ 14 | ), 15 | 10 16 | ) 17 | 18 | def withVectorInt(): Unit = { 19 | implicit val combinator = new DiscreteFixedLengthCombinator(1 to 100, 100) 20 | 21 | val solver = new Solver[Seq[Int], Int]( 22 | { _ => 23 | util.Random.nextInt() 24 | }, 25 | islandCount = PositiveCount(15), 26 | populationSize = PositiveCount(1000), 27 | generations = PositiveCount(40) 28 | ) 29 | 30 | val _ = solver.solveNow 31 | } 32 | 33 | def withArrayInt(): Unit = { 34 | implicit val combinator = 35 | new specialized.DiscreteFixedLengthCombinator(1 to 100, 100) 36 | 37 | val solver = new Solver[Seq[Int], Int]( 38 | { _ => 39 | util.Random.nextInt() 40 | }, 41 | islandCount = PositiveCount(15), 42 | populationSize = PositiveCount(1000), 43 | generations = PositiveCount(40) 44 | ) 45 | 46 | val _ = solver.solveNow 47 | } 48 | 49 | def withVector(): Unit = { 50 | implicit val combinator = 51 | new DiscreteFixedLengthCombinator((1 to 100).map(WrappedInt), 100) 52 | 53 | val solver = new Solver[Seq[WrappedInt], Int]( 54 | { _ => 55 | util.Random.nextInt() 56 | }, 57 | islandCount = PositiveCount(15), 58 | populationSize = PositiveCount(1000), 59 | generations = PositiveCount(40) 60 | ) 61 | 62 | val _ = solver.solveNow 63 | } 64 | 65 | def withArray(): Unit = { 66 | implicit val combinator = new specialized.DiscreteFixedLengthCombinator((1 to 100).map(WrappedInt), 100) 67 | 68 | val solver = new Solver[Seq[WrappedInt], Int]( 69 | { _ => 70 | util.Random.nextInt() 71 | }, 72 | islandCount = PositiveCount(15), 73 | populationSize = PositiveCount(1000), 74 | generations = PositiveCount(40) 75 | ) 76 | 77 | val _ = solver.solveNow 78 | } 79 | } 80 | 81 | case class WrappedInt(a: Int) 82 | -------------------------------------------------------------------------------- /spawning-pool-core-alpha/src/main/scala/com/htmlism/spawningpool/fitness/Fitness.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.fitness 2 | 3 | /** 4 | * A typeclass for describing fitness for some type of solution 5 | * 6 | * @tparam A 7 | * The solution type whose fitness is being evaluated 8 | */ 9 | trait Fitness[A] { 10 | self => 11 | 12 | /** 13 | * Given two solutions, find out how they relate. 14 | * 15 | * - negative if x is less than (worse than) y 16 | * - positive if x is greater than (better than) y 17 | * - zero if x equals y 18 | * 19 | * @param x 20 | * The first solution 21 | * @param y 22 | * The second solution 23 | * @return 24 | * An integer 25 | */ 26 | def compare(x: A, y: A): Int 27 | 28 | def minimize: Fitness[A] = 29 | new Fitness[A] { 30 | def compare(x: A, y: A) = self.compare(x, y) * -1 31 | } 32 | 33 | def andThen[B](that: Fitness[B]): Fitness[(A, B)] = 34 | new Fitness[(A, B)] { 35 | def compare(x: (A, B), y: (A, B)): Int = { 36 | val score = self.compare(x._1, y._1) 37 | 38 | if (score == 0) 39 | that.compare(x._2, y._2) 40 | else 41 | score 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Fitness that is satisfied using an implicit ordering in the solution space. Scala already provides ordering for 48 | * common types like `Int` and `Double` as well as tuples of those types 49 | * 50 | * @tparam A 51 | * The solution type whose fitness is being evaluated 52 | */ 53 | class OrdinalFitness[A: Ordering] extends Fitness[A] { 54 | def compare(x: A, y: A): Int = implicitly[Ordering[A]].compare(x, y) 55 | } 56 | 57 | /** 58 | * Fitness that is satisfied using a fitness evaluation function and an implicit numeric for the fitness score. 59 | * 60 | * This is the most common way to provide fitness to a genetic algorithm. 61 | * 62 | * @param f 63 | * A function for converting a solution to a fitness score. This score type must support numeric operations. Scala 64 | * already provides numeric support for common types like `Int` and `Double` 65 | * @tparam A 66 | * The solution type whose fitness is being evaluated 67 | * @tparam B 68 | * A numeric type for scoring 69 | */ 70 | class RatioFitness[A, B: Numeric](f: A => B) extends Fitness[A] { 71 | def compare(x: A, y: A): Int = implicitly[Numeric[B]].compare(f(x), f(y)) 72 | } 73 | -------------------------------------------------------------------------------- /spawning-pool-core/src/test/scala/com/htmlism/spawningpool/combinatorics/VariableLengthCombinatorSpec.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool.combinatorics 2 | 3 | import org.specs2.mutable.Specification 4 | 5 | class VariableLengthCombinatorSpec extends Specification { 6 | "A variable-length generator" should { 7 | val combinator = new VariableTestCombinator 8 | 9 | val firstChromosome = combinator.generateChromosome 10 | val secondChromosome = combinator.generateChromosome 11 | val thirdChromosome = combinator.generateChromosome 12 | 13 | val firstMutation = combinator.mutate(firstChromosome) 14 | val secondMutation = combinator.mutate(secondChromosome) 15 | val thirdMutation = combinator.mutate(thirdChromosome) 16 | 17 | val child = combinator.crossover(secondChromosome, firstChromosome) 18 | 19 | "generate chromosomes of differing lengths" in { 20 | firstChromosome.length === 1 21 | secondChromosome.length === 3 22 | thirdChromosome.length === 5 23 | } 24 | 25 | "generate chromosomes of the provided alleles" in { 26 | firstChromosome === Seq("GoGo") 27 | secondChromosome === Seq("HoneyLemon", "Baymax", "Fred") 28 | thirdChromosome === Seq("Hiro", "Wasabi", "GoGo", "Baymax", "HoneyLemon") 29 | } 30 | 31 | "support spot mutation" in { 32 | secondMutation === Seq("HoneyLemon", "Hiro", "Fred") 33 | } 34 | 35 | "support insertion mutation" in { 36 | thirdMutation === Seq("Wasabi", "Hiro", "Wasabi", "GoGo", "Baymax", "HoneyLemon") 37 | } 38 | 39 | "support removal mutation" in { 40 | firstMutation === Seq.empty 41 | } 42 | 43 | "support crossover" in { 44 | child === Seq("GoGo", "Fred") 45 | } 46 | } 47 | } 48 | 49 | class VariableTestCombinator extends VariableLengthCombinator[String] with DiscreteAlleleGenerator[String] { 50 | val initialSize = 11 51 | val alleles = Seq("Hiro", "Baymax", "Fred", "GoGo", "Wasabi", "HoneyLemon") 52 | 53 | private val mutationMethods = 54 | Iterable(RemoveGene, MutateGene, AddGene).iterator 55 | def nextMutationMethod: MutationMethod = mutationMethods.next() 56 | 57 | private val alleleIndexes = Iterable(3, 5, 1, 2, 0, 4, 3, 1, 5, 0, 4).iterator 58 | def nextAlleleIndex(size: Int) = alleleIndexes.next() 59 | 60 | private val parents = Iterable(false, false, true).iterator 61 | def nextUseFirstParent: Boolean = parents.next() 62 | 63 | def nextGeneIndex(size: Int): Int = 1 64 | 65 | private val lengths = Iterable(1, 3, 5).iterator 66 | def nextLength(maximum: Int) = lengths.next() 67 | 68 | // variation 69 | def nextGeneIndexForInsertion(size: Int): Int = 0 70 | def nextGeneIndexForRemoval(size: Int): Int = 0 71 | } 72 | -------------------------------------------------------------------------------- /spawning-pool-core/src/main/scala/com/htmlism/spawningpool/Solver.scala: -------------------------------------------------------------------------------- 1 | package com.htmlism.spawningpool 2 | 3 | import scala.annotation.tailrec 4 | import scala.concurrent.* 5 | import scala.concurrent.duration.* 6 | 7 | object Solver { 8 | val DEFAULT_POPULATION_SIZE = PositiveCount(50) 9 | val DEFAULT_ISLAND_COUNT = PositiveCount(4) 10 | val DEFAULT_GENERATION_COUNT = PositiveCount(20) 11 | val DEFAULT_MUTATION_RATE = .01 12 | 13 | def randomIndividual[A](population: Seq[A])(implicit rig: RandomIndexProvider): A = 14 | population(rig.randomIndex(population.size)) 15 | 16 | def evolvePopulation[A, B](implicit ctx: SolutionContext[A, B]): SolutionContext[A, B] = { 17 | println(s"island ${ctx.islandId} generating children for generation ${ctx.generations}") 18 | 19 | val newPopulation = Vector.fill(ctx.population.size)(bearChild) 20 | 21 | ctx.increment(newPopulation) 22 | } 23 | 24 | def tournamentSelect[A, B](size: PositiveCount)(implicit ctx: SolutionContext[A, B]): A = 25 | tournamentSelect(size, randomIndividual(ctx.population)) 26 | 27 | @tailrec 28 | private def tournamentSelect[A, B](size: PositiveCount, champion: A)(implicit ctx: SolutionContext[A, B]): A = 29 | if (size == PositiveCount(1)) 30 | champion 31 | else { 32 | val challenger = randomIndividual(ctx.population) 33 | val compare = 34 | ctx.ordering.compare(ctx.fitness(champion), ctx.fitness(challenger)) 35 | 36 | val nextChampion = if (compare < 0) challenger else champion 37 | 38 | tournamentSelect(size.minusOne, nextChampion) 39 | } 40 | 41 | def bearChild[A, B](implicit ctx: SolutionContext[A, B]): A = { 42 | val child = ctx.evolver.crossover(tournamentSelect(PositiveCount(2)), tournamentSelect(PositiveCount(2))) 43 | 44 | if ((new scala.util.Random).nextDouble() < ctx.mutationRate) 45 | ctx.evolver.mutate(child) 46 | else 47 | child 48 | } 49 | 50 | def awaitResult[A](future: Future[A]): A = Await.result(future, Duration.Inf) 51 | } 52 | 53 | /** 54 | * A generator of solutions configured with tuning parameters. 55 | * 56 | * @param fitness 57 | * A function for determining the fitness score B of candidate solutions A 58 | * @param populationSize 59 | * The number of solutions in each population 60 | * @param islandCount 61 | * The number of islands in each 62 | * @param mutationRate 63 | * The rate of mutation from 0 to 1 64 | * @param generations 65 | * The number of generations to evolve 66 | * @param evolver 67 | * The mechanism responsible for evolving candidate solutions 68 | * @param ordering 69 | * An ordering for values of B 70 | * @param rig 71 | * The source of randomness 72 | * 73 | * @tparam A 74 | * The type of the candidate solutions 75 | * @tparam B 76 | * The type of the fitness score 77 | */ 78 | class Solver[A, B]( 79 | fitness: A => B, 80 | populationSize: PositiveCount = Solver.DEFAULT_POPULATION_SIZE, 81 | islandCount: PositiveCount = Solver.DEFAULT_ISLAND_COUNT, 82 | mutationRate: Double = Solver.DEFAULT_MUTATION_RATE, 83 | generations: PositiveCount = Solver.DEFAULT_GENERATION_COUNT 84 | )(implicit evolver: Evolver[A], ordering: Ordering[B]) { 85 | import com.htmlism.spawningpool.Solver.* 86 | 87 | type Population = Vector[A] 88 | type Solutions = Set[A] 89 | 90 | def solve(implicit src: ChromosomeGenerator[A], ec: ExecutionContext): Future[Solutions] = 91 | Future { 92 | evolveFrom(Vector.fill(populationSize)(src.generateChromosome)) 93 | } 94 | 95 | def solve(seed: List[A])(implicit ec: ExecutionContext): Future[Solutions] = 96 | Future { 97 | if (seed.isEmpty) 98 | throw new IllegalArgumentException("must provide a non-empty collection as a seed") 99 | else 100 | evolveFrom { 101 | seed.toVector 102 | } 103 | } 104 | 105 | def solveNow(implicit src: ChromosomeGenerator[A], ec: ExecutionContext): Solutions = awaitResult(solve) 106 | 107 | def solveNow(seed: List[A])(implicit ec: ExecutionContext): Solutions = 108 | awaitResult(solve(seed)) 109 | 110 | private def evolveFrom(seeding: => Population)(implicit ec: ExecutionContext) = { 111 | val islands = generateIslands(seeding) 112 | 113 | val baseFutures = islands.zipWithIndex.map { case (p, i) => 114 | Future { 115 | SolutionContext(i, fitness, evolver, mutationRate, p) 116 | } 117 | } 118 | 119 | val endFutures = evolveIslands(baseFutures, generations) 120 | 121 | val evolvedIslands = endFutures.map { f => 122 | f.map { ctx => 123 | val byFitness = ctx.population.groupBy(ctx.fitness) 124 | 125 | byFitness(byFitness.keys.max) 126 | } 127 | } 128 | 129 | evolvedIslands.foldLeft(Set.empty[A])((acc, sols) => acc ++ awaitResult(sols)) 130 | } 131 | 132 | @tailrec 133 | private def evolveIslands( 134 | islands: Iterable[Future[SolutionContext[A, B]]], 135 | generations: Int 136 | )(implicit ec: ExecutionContext): Iterable[Future[SolutionContext[A, B]]] = 137 | generations match { 138 | case 0 => islands 139 | case _ => 140 | val newIslands = islands.map { f => 141 | f.map(evolvePopulation(_)) // type inference with implicits is hard 142 | } 143 | 144 | evolveIslands(newIslands, generations - 1) 145 | } 146 | 147 | private def generateIslands(f: => Population) = Iterable.fill(islandCount)(f) 148 | } 149 | --------------------------------------------------------------------------------