All Downloads are FREE. Search and download functionalities are using the official Maven repository.

mgo.evolution.algorithm.HDOSE.scala Maven / Gradle / Ivy

The newest version!
package mgo.evolution.algorithm

/*
 * Copyright (C) 2024 Romain Reuillon
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see .
 */

import cats.implicits.*
import mgo.evolution.*
import mgo.evolution.algorithm.GenomeVectorDouble.*
import mgo.evolution.breeding.*
import mgo.evolution.elitism.*
import mgo.evolution.ranking.*
import monocle.*
import monocle.syntax.all.*

import java.util
import scala.collection.mutable.ArrayBuffer
import scala.reflect.ClassTag
import mgo.tools.*

object HDOSE:

  import CDGenome._
  import DeterministicIndividual._
  import monocle._

  case class StateType[P](archive: Archive[Individual[P]], distance: Double)
  type HDOSEState[P] = EvolutionState[StateType[P]]

  def archiveLens[P]: Lens[HDOSEState[P], Archive[Individual[P]]] = Focus[HDOSEState[P]](_.s.archive)
  def distanceLens[P]: Lens[HDOSEState[P], Double] = Focus[HDOSEState[P]](_.s.distance)

  def initialState[P](distance: Double = 1.0): HDOSEState[P] = EvolutionState(s = StateType(Archive.empty, distance))

  def initialGenomes(lambda: Int, continuous: Vector[C], discrete: Vector[D], reject: Option[Genome => Boolean], rng: scala.util.Random): Vector[Genome] =
    CDGenome.initialGenomes(lambda, continuous, discrete, reject, rng)

  def adaptiveBreeding[P](
    lambda: Int,
    operatorExploration: Double,
    continuous: Vector[C],
    discrete: Vector[D],
    weightC: Vector[Double],
    weightD: Vector[Double],
    fitness: P => Vector[Double],
    reject: Option[Genome => Boolean]): Breeding[HDOSEState[P], Individual[P], Genome] =
    HDOSEOperation.adaptiveBreeding[HDOSEState[P], Individual[P], Genome](
      individualFitness(fitness),
      Focus[Individual[P]](_.genome).get,
      continuousValues(continuous).get,
      continuousOperator.get,
      discreteValues(discrete).get,
      discreteOperator.get,
      discrete,
      tooCloseByComponent(weightC, weightD, discrete),
      distanceLens.get,
      buildGenome(discrete),
      logOfPopulationSize,
      lambda,
      reject,
      operatorExploration,
      archiveLens[P].get)

  def expression[P](fitness: (IArray[Double], IArray[Int]) => P, components: Vector[C], discrete: Vector[D]): (Genome, Long, Boolean) => Individual[P] =
    DeterministicIndividual.expression(fitness, components, discrete)

  def elitism[P](
    mu: Int,
    limit: Vector[Double],
    weightC: Vector[Double],
    weightD: Vector[Double],
    archiveSize: Int,
    continuous: Vector[C],
    discrete: Vector[D],
    fitness: P => Vector[Double],
    precision: Double): Elitism[HDOSEState[P], Individual[P]] =
    HDOSEOperation.elitism[HDOSEState[P], Individual[P], Genome](
      individualFitness(fitness),
      limit,
      scaledValues(continuous, discrete),
      mu,
      archiveLens[P],
      tooCloseByComponent(weightC, weightD, discrete),
      precision,
      distanceLens,
      archiveSize,
      continuousValues(continuous).get,
      discreteValues(discrete).get,
      Focus[Individual[P]](_.genome).get
    )

  case class Result[P](continuous: Vector[Double], discrete: Vector[Int], fitness: Vector[Double], individual: Individual[P], archive: Boolean)

  def result[P](state: HDOSEState[P], population: Vector[Individual[P]], continuous: Vector[C], discrete: Vector[D], fitness: P => Vector[Double], keepAll: Boolean): Vector[Result[P]] =
    def individualToResult(i: Individual[P], archive: Boolean) =
      Result(scaleContinuousVectorValues(continuousVectorValues(continuous).get(i.genome), continuous), i.focus(_.genome) andThen discreteVectorValues(discrete) get, DeterministicIndividual.individualFitness(fitness)(i), i, archive)

    val goodIndividuals =
      if keepAll
      then population.map(i => individualToResult(i, false))
      else Seq()

    val archiveIndividuals =
      archiveLens.get(state).toVector.map(i => individualToResult(i, true))

    archiveIndividuals ++ goodIndividuals

  def result(t: HDOSE, state: HDOSEState[Vector[Double]], population: Vector[Individual[Vector[Double]]]): Vector[Result[Vector[Double]]] =
    result[Vector[Double]](state = state, continuous = t.continuous, discrete = t.discrete, fitness = identity, population = population, keepAll = false)

  def reject(t: HDOSE): Option[Genome => Boolean] = NSGA2.reject(t.reject, t.continuous, t.discrete)

  def tooCloseByComponent(weightC: Vector[Double], weightD: Vector[Double], discrete: Vector[D]): HDOSEOperation.TooClose = (g1, g2, d) =>
    val (c1, d1) = g1
    val (c2, d2) = g2

    var sum = 0.0

    var ic = 0
    val cSize = c1.size

    while ic < cSize && sum < d
    do
      sum += Math.abs(c1(ic) - c2(ic)) * weightC(ic)
      ic += 1


    var id = 0
    val dSize = d1.size

    while id < dSize && sum < d
    do
      val d = discrete(id)
      def scale(x: Int) =
        changeScale(x, d.low, d.high, 0.0, 1.0)

      sum += Math.abs(scale(d1(id)) - scale(d2(id))) * weightD(id)
      id += 1

    sum < d

  given Algorithm[HDOSE, Individual[Vector[Double]], Genome, HDOSEState[Vector[Double]]] with
    override def initialState(t: HDOSE, rng: scala.util.Random) = HDOSE.initialState(t.distance)
    override def initialPopulation(t: HDOSE, rng: scala.util.Random, parallel: Algorithm.ParallelContext) =
      deterministic.initialPopulation[Genome, Individual[Vector[Double]]](
        HDOSE.initialGenomes(t.lambda, t.continuous, t.discrete, reject(t), rng),
        HDOSE.expression(t.fitness, t.continuous, t.discrete),
        parallel)

    def step(t: HDOSE) =
      val sC = t.weightC.getOrElse(Vector.fill(t.continuous.size)(1.0))
      val sD = t.weightD.getOrElse(Vector.fill(t.discrete.size)(1.0))

      deterministic.step[HDOSEState[Vector[Double]], Individual[Vector[Double]], Genome](
        HDOSE.adaptiveBreeding[Vector[Double]](t.lambda, t.operatorExploration, t.continuous, t.discrete, sC, sD, identity, reject(t)),
        HDOSE.expression(t.fitness, t.continuous, t.discrete),
        HDOSE.elitism(t.mu, t.limit, sC, sD, t.archiveSize, t.continuous, t.discrete, identity, t.distance),
        Focus[HDOSEState[Vector[Double]]](_.generation),
        Focus[HDOSEState[Vector[Double]]](_.evaluated))


case class HDOSE(
  mu: Int,
  lambda: Int,
  fitness: (IArray[Double], IArray[Int]) => Vector[Double],
  limit: Vector[Double],
  archiveSize: Int = 1000,
  continuous: Vector[C] = Vector.empty,
  discrete: Vector[D] = Vector.empty,
  weightC: Option[Vector[Double]] = None,
  weightD: Option[Vector[Double]] = None,
  operatorExploration: Double = 0.1,
  reject: Option[(IArray[Double], IArray[Int]) => Boolean] = None,
  distance: Double = 1.0)

object HDOSEOperation:

  type GenomeValue =  (IArray[Double], IArray[Int])
  type TooClose = (GenomeValue, GenomeValue, Double) => Boolean

  def isTooCloseFromArchive[I](
    tooClose: TooClose,
    archive: Archive[I],
    genomeValues: I => GenomeValue,
    diversityDistance: Double)(g: GenomeValue): Boolean =
    import mgo.tools.loop
    import scala.util.boundary
    import scala.jdk.CollectionConverters.*

    val size = archive.size

    boundary[Boolean]:
      loop(0, _ < size, _ + 1): i =>
        val individual = archive(i)

        if tooClose(genomeValues(individual), g, diversityDistance)
        then boundary.break(true)

      false


  def shrinkArchive[I: ClassTag](
    tooClose: TooClose,
    archive: Archive[I],
    genomeValues: I => GenomeValue,
    diversityDistance: Double): Archive[I] =

    def isTooClose(archive: Archive[I], i: I) =
      isTooCloseFromArchive(tooClose, archive, genomeValues, diversityDistance)(genomeValues(i))

    val newArchive = new ArrayBuffer[I](archive.size)
    newArchive.addOne(archive.head)

    for
      i <- archive.tail
      if !isTooClose(IArray.unsafeFromArray(newArchive.toArray), i)
    do newArchive.addOne(i)

    IArray.unsafeFromArray(newArchive.toArray)

  def computeDistance[I: ClassTag](
    distance: TooClose,
    archive: Archive[I],
    genomeValues: I => GenomeValue,
    targetSize: Int,
    currentDistance: Double,
    precision: Double) =
    def computeSize(d: Double) =
      shrinkArchive(distance, archive, genomeValues, d).size.toDouble

    val newDistance =
      mgo.tools.findFirstUnder(
        targetSize,
        computeSize,
        currentDistance,
        precision
      )

    newDistance


  def adaptiveBreeding[S, I, G](
    fitness: I => Vector[Double],
    genome: I => G,
    continuousValues: G => IArray[Double],
    continuousOperator: G => Option[Int],
    discreteValues: G => IArray[Int],
    discreteOperator: G => Option[Int],
    discrete: Vector[D],
    distance: TooClose,
    diversityDistance: S => Double,
    buildGenome: (IArray[Double], Option[Int], IArray[Int], Option[Int]) => G,
    tournamentRounds: Int => Int,
    lambda: Int,
    reject: Option[G => Boolean],
    operatorExploration: Double,
    archive: S => Archive[I]): Breeding[S, I, G] =
    (s, population, rng) =>
      val archivedPopulation = archive(s)
      val ranks = ranking.paretoRankingMinAndCrowdingDiversity[I](population, fitness, rng)
      val allRanks = ranks ++ Vector.fill(archivedPopulation.size)(worstParetoRanking)
      val continuousOperatorStatistics = operatorProportions(genome andThen continuousOperator, population)
      val discreteOperatorStatistics = operatorProportions(genome andThen discreteOperator, population)

      val genomeValue = (genome andThen (continuousValues, discreteValues).tupled).memoized

      val breeding: Breeding[S, I, G] =
        (s, pop, rng) =>
          val newGs =
            applyDynamicOperators[S, I, G](
              tournament(allRanks, tournamentRounds),
              genomeValue,
              continuousOperatorStatistics,
              discreteOperatorStatistics,
              discrete,
              operatorExploration,
              buildGenome)(s, pop, rng)
          newGs.filterNot: g =>
            val values = (continuousValues(g), discreteValues(g))
            isTooCloseFromArchive(
              distance,
              archivedPopulation,
              genomeValue,
              diversityDistance(s))(values)

      val offspring = breed[S, I, G](breeding, lambda, reject)(s, population ++ archivedPopulation, rng)
      randomTake(offspring, lambda, rng)


  def elitism[S, I: ClassTag, G](
    fitness: I => Vector[Double],
    limit: Vector[Double],
    scaledValues: G => (IArray[Double], IArray[Int]),
    mu: Int,
    archive: monocle.Lens[S, Archive[I]],
    distance: TooClose,
    precision: Double,
    diversityDistance: Lens[S, Double],
    archiveSize: Int,
    continuousValues: G => IArray[Double],
    discreteValues: G => IArray[Int],
    genome: I => G): Elitism[S, I] =
    (s1, population, candidates, rng) =>
      val memoizedFitness = fitness.memoized
      val cloneRemoved = filterNaN(keepFirst(genome andThen scaledValues)(population, candidates), memoizedFitness)

      // FIXME individuals can be close to each other but yet added to the archive
      def newlyReaching = candidates.filter(c => OSEOperation.patternIsReached(memoizedFitness(c), limit))

      val s2 = archive.modify(_ ++ newlyReaching)(s1)

      val genomeValue = (genome andThen (continuousValues, discreteValues).tupled).memoized

      val s3 =
        if archive.get(s2).size <= archiveSize
        then s2
        else
          val newDiversityDistance =
            computeDistance(
              distance,
              archive.get(s2),
              genomeValue,
              archiveSize,
              diversityDistance.get(s2),
              precision
            )

          val newArchive =
            shrinkArchive(
              distance,
              archive.get(s2),
              genomeValue,
              newDiversityDistance
            )

          (archive.replace(newArchive) andThen diversityDistance.replace(newDiversityDistance))(s2)

      val filteredPopulation =
        cloneRemoved.filterNot: i =>
          isTooCloseFromArchive(
            distance,
            archive.get(s3),
            genomeValue,
            diversityDistance.get(s3))(genomeValue(i))

      NSGA2Operations.elitism(memoizedFitness, genome andThen scaledValues, mu)(s3, filteredPopulation, Vector.empty, rng)







© 2015 - 2025 Weber Informatics LLC | Privacy Policy