Skip to content

Commit

Permalink
Improve documentation for LinearCrossover operator.
Browse files Browse the repository at this point in the history
  • Loading branch information
JedS6391 committed Sep 7, 2019
1 parent 65d4862 commit 55c6c10
Showing 1 changed file with 96 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ class LinearCrossover<TProgram, TOutput : Output<TProgram>, TTarget : Target<TPr

/**
* Combines the two individuals given by exchanging two segments of instructions.
*
* For details of the algorithm, see page 89 here:
* https://web.archive.org/web/20190905005257/https://pdfs.semanticscholar.org/31c8/a5e106b80c07c1c0f74bcf42de6d24de2bf1.pdf
*/
override fun combine(mother: Program<TProgram, TOutput>, father: Program<TProgram, TOutput>) {
// For details of the algorithm, see page 89 here:
// https://web.archive.org/web/20190905005257/https://pdfs.semanticscholar.org/31c8/a5e106b80c07c1c0f74bcf42de6d24de2bf1.pdf

// Ensure that we are not trying to combine two individuals that don't have a valid length.
if (!this.programLengthIsValid(mother) || !this.programLengthIsValid(father)) {
throw IllegalArgumentException(
Expand All @@ -59,7 +59,8 @@ class LinearCrossover<TProgram, TOutput : Output<TProgram>, TTarget : Target<TPr
))

// First make sure that the mother is shorter than the father, since we are treating the mother as gp[1]
// and the father as gp[2]. We also take a copy to ensure that the mother and father stay unmodified until the end of the operation.
// and the father as gp[2]. We also take a copy to ensure that the mother and father stay unmodified
// until the end of the operation.
var firstIndividual = mother.instructions.copy()
var secondIndividual = father.instructions.copy()

Expand All @@ -69,14 +70,6 @@ class LinearCrossover<TProgram, TOutput : Output<TProgram>, TTarget : Target<TPr
secondIndividual = temp
}

// We take a copy of the original individuals after swapping them
// so that we have a reference to the full set of instructions without
// having to determine if the mother or father is first/second.
val originalFirstIndividual = firstIndividual.toMutableList()
val originalSecondIndividual = secondIndividual.toMutableList()

// 1. Randomly select an instruction position i[k] (crossover point) in program gp[k] (k in {1, 2})
// with len(gp[1]) <= len(gp[2]) and distance |i[1] - i[2]| <= min(len(gp[1]) -1, dc_max)
val crossoverPoints = this.determineCrossoverPoints(firstIndividual, secondIndividual) ?: return

val segments = this.determineSegments(
Expand All @@ -85,18 +78,14 @@ class LinearCrossover<TProgram, TOutput : Output<TProgram>, TTarget : Target<TPr
crossoverPoints
) ?: return

// 6. Exchange segment s1 in program gp[1] by segment s2 from program gp[2] and vice versa.
val (firstNewIndividual, secondNewIndividual) = this.buildNewIndividuals(
firstIndividual,
secondIndividual,
originalFirstIndividual,
originalSecondIndividual,
crossoverPoints,
segments
)

// Replace the instructions of the original individuals to reflect the
// changes made using linear crossover.
// Replace the instructions of the original individuals to reflect the changes made using linear crossover.
mother.instructions = firstNewIndividual
father.instructions = secondNewIndividual

Expand All @@ -108,10 +97,24 @@ class LinearCrossover<TProgram, TOutput : Output<TProgram>, TTarget : Target<TPr
))
}

/**
* Determines a crossover point for each of [firstIndividual] and [secondIndividual] with a distance
* difference that satisfies the supplied [maximumCrossoverDistance].
*
* *Note that the search for crossover points is bounded by [MAX_ITERATIONS]. If [MAX_ITERATIONS] is exceeded,
* no segments will be returned (indicated by a null return).*
*
* @param firstIndividual The first individual to determine a crossover point for.
* @param secondIndividual The second individual to determine a crossover point for.
* @return A pair of [CrossoverPoint], one for [firstIndividual] and one for [secondIndividual].
*/
private fun determineCrossoverPoints(
firstIndividual: MutableList<Instruction<TProgram>>,
secondIndividual: MutableList<Instruction<TProgram>>
): Pair<CrossoverPoint, CrossoverPoint>? {
// 1. Randomly select an instruction position i[k] (crossover point) in program gp[k] (k in {1, 2})
// with len(gp[1]) <= len(gp[2]) and distance |i[1] - i[2]| <= min(len(gp[1]) -1, dc_max)

// Randomly select some initial crossover points for each individual.
var firstCrossoverPoint = random.nextInt(firstIndividual.size)
var secondCrossoverPoint = random.nextInt(secondIndividual.size)
Expand All @@ -132,6 +135,21 @@ class LinearCrossover<TProgram, TOutput : Output<TProgram>, TTarget : Target<TPr
}
}

/**
* Determines a segment in each of [firstIndividual] and [secondIndividual] that begin at the provided [crossoverPoints].
*
* The segments computed will be no longer than [maximumSegmentLength] and the difference in length between the
* two segments no greater than [maximumSegmentLengthDifference]. It is also ensured that the first segment
* will be no longer than the second segment.
*
* *Note that the search for segments is bounded by [MAX_ITERATIONS]. If [MAX_ITERATIONS] is exceeded,
* no segments will be returned (indicated by a null return).*
*
* @param firstIndividual The first individual to determine a segment for.
* @param secondIndividual The second individual to determine a segment for.
* @param crossoverPoints A pair of crossover points (one for [firstIndividual] and one for [secondIndividual]).
* @return A pair of [Segment], one for [firstIndividual] and one for [secondIndividual].
*/
private fun determineSegments(
firstIndividual: MutableList<Instruction<TProgram>>,
secondIndividual: MutableList<Instruction<TProgram>>,
Expand All @@ -153,7 +171,7 @@ class LinearCrossover<TProgram, TOutput : Output<TProgram>, TTarget : Target<TPr
// to avoid a potentially infinite loop.
var iterations = 0

while (iterations++ < MAX_ITERATIONS && !this.segmentsAreValid(firstSegment.size, secondSegment.size)) {
while (iterations++ < MAX_ITERATIONS && !this.segmentSizesAreValid(firstSegment.size, secondSegment.size)) {
firstSegmentLength = random.randInt(1, min(firstIndividual.size - firstCrossoverPoint, this.maximumSegmentLength))
secondSegmentLength = random.randInt(1, min(secondIndividual.size - secondCrossoverPoint, this.maximumSegmentLength))

Expand Down Expand Up @@ -190,20 +208,37 @@ class LinearCrossover<TProgram, TOutput : Output<TProgram>, TTarget : Target<TPr
return Pair(firstSegment, secondSegment)
}

/**
* Constructs two new individuals based on the original [firstIndividual] and [secondIndividual] using
* the [crossoverPoints] and [segments] previously determined.
*
* @param firstIndividual The first individual.
* @param secondIndividual The second individual.
* @param crossoverPoints Previously determined crossover points.
* @param segments Previously determined segments to exchange.
* @return A pair of new individuals, after exchanging the provided segments in each.
*/
private fun buildNewIndividuals(
firstIndividual: MutableList<Instruction<TProgram>>,
secondIndividual: MutableList<Instruction<TProgram>>,
originalFirstIndividual: MutableList<Instruction<TProgram>>,
originalSecondIndividual: MutableList<Instruction<TProgram>>,
crossoverPoints: Pair<CrossoverPoint, CrossoverPoint>,
segments: Pair<Segment<TProgram>, Segment<TProgram>>
): Pair<MutableList<Instruction<TProgram>>, MutableList<Instruction<TProgram>>> {
// 6. Exchange segment s1 in program gp[1] by segment s2 from program gp[2] and vice versa.

// We take a copy of the original individuals so that we have a reference to the full set of instructions.
val originalFirstIndividual = firstIndividual.toMutableList()
val originalSecondIndividual = secondIndividual.toMutableList()

val (firstCrossoverPoint, secondCrossoverPoint) = crossoverPoints
val (firstSegment, secondSegment) = segments

val firstSegmentEnd = firstCrossoverPoint + firstSegment.size
val secondSegmentEnd = secondCrossoverPoint + secondSegment.size

// First we remove the segments from each individual.
firstIndividual.removeRange(firstCrossoverPoint until (firstCrossoverPoint + firstSegment.size))
secondIndividual.removeRange(secondCrossoverPoint until (secondCrossoverPoint + secondSegment.size))
firstIndividual.removeRange(firstCrossoverPoint until firstSegmentEnd)
secondIndividual.removeRange(secondCrossoverPoint until secondSegmentEnd)

// Then start constructing a new set of instructions for each program, up to the start of each segment.
val firstNewIndividual = originalFirstIndividual.slice(0 until firstCrossoverPoint).toMutableList()
Expand All @@ -216,32 +251,59 @@ class LinearCrossover<TProgram, TOutput : Output<TProgram>, TTarget : Target<TPr

// Add everything from the original individuals that comes after the end of each segment.
firstNewIndividual.addAll(
originalFirstIndividual.slice((firstCrossoverPoint + firstSegment.size) until originalFirstIndividual.size)
originalFirstIndividual.slice(firstSegmentEnd until originalFirstIndividual.size)
)
secondNewIndividual.addAll(
originalSecondIndividual.slice((secondCrossoverPoint + secondSegment.size) until originalSecondIndividual.size)
originalSecondIndividual.slice(secondSegmentEnd until originalSecondIndividual.size)
)

return Pair(firstNewIndividual, secondNewIndividual)
}

/**
* Determines if the length of the supplied [program] is valid or not.
*
* @param program A program to check the length validity of.
* @return Whether or not the program length is valid or not.
*/
private fun programLengthIsValid(program: Program<TProgram, TOutput>): Boolean {
// Program length should always fall within these bounds.
// If it doesn't then something has gone wrong somewhere.
return (
program.instructions.size >= this.minimumProgramLength &&
program.instructions.size <= this.maximumProgramLength
)
}

/**
* Determines if the given crossover points are valid.
*
* The distance between two crossover points must not be greater than the [maximumCrossoverDistance].
*
* @param firstCrossoverPoint The first crossover point.
* @param secondCrossoverPoint The second crossover point.
* @param firstIndividualSize The size of the first individual that crossover is being performed on.
* @return Whether or not the crossover points are valid.
*/
private fun crossoverPointsAreValid(
firstCrossoverPoint: CrossoverPoint,
secondCrossoverPoint: CrossoverPoint,
firstIndividualSize: Int
): Boolean {
// Make sure the points chosen are close enough to each other to satisfy the maximum crossover distance constraint.
// The points chosen must be close enough to each other to satisfy the maximum crossover distance constraint.
return abs(firstCrossoverPoint - secondCrossoverPoint) <= min(firstIndividualSize - 1, this.maximumCrossoverDistance)
}

private fun segmentsAreValid(firstSegmentSize: Int, secondSegmentSize: Int): Boolean {
/**
* Determines if the given segment sizes are valid.
*
* The difference in length between two segments must not be greater than the [maximumSegmentLengthDifference],
* as well as the length of the first segment being no greater than the length of the second segment.
*
* @param firstSegmentSize The size of the first segment.
* @param secondSegmentSize The size of the second segment.
*/
private fun segmentSizesAreValid(firstSegmentSize: Int, secondSegmentSize: Int): Boolean {
return (
abs(firstSegmentSize - secondSegmentSize) <= this.maximumSegmentLengthDifference &&
firstSegmentSize <= secondSegmentSize
Expand All @@ -252,6 +314,14 @@ class LinearCrossover<TProgram, TOutput : Output<TProgram>, TTarget : Target<TPr
}

// These aliases are used to make the code clearer to read.

/**
* A point in a [Program] at which crossover will occur.
*/
private typealias CrossoverPoint = Int

/**
* A segment of instructions in a [Program] that will be exchanged during crossover.
*/
private typealias Segment<T> = List<Instruction<T>>

0 comments on commit 55c6c10

Please sign in to comment.