6
\$\begingroup\$

Setup

The following code implements the algorithm described in this and this paper. The first paper describes how the evolution of a fish population can be simulated, while the second paper introdues the phenomenon of 'fishing' into the simulation. The end goal of both papers is to show that one can have a stable fish population, introduce "responsible" fishing such that the total number of fish in the population decreases, but remains stable. The last step is then to add "irresponsible" fishing and to show that small changes in the fishing rate (fished fish per year) can have drastical results on the amount of fish that survive.


Code

The code is a reimplementation int Kotlin of C++ code I wrote some time ago. I picked up Kotlin just recently and I was struggeling with how inheritance worked here and how exactly to deal with static variables. It works as intended.

The code consists of three classes genome, animal and population (as well as a derivied class) and the main function that actually performs the simulation (this plot summerizes the results).

genome.kt

package penna

import java.util.*

typealias age_t = Int

class Genome{
    /* Genome Class for the Penna simulation.
     * The genome class has two private members:
     *   1) 'genome_size_' is of type 'age_t' and static. It represents the length of the
     *      genome and therefore later the maximum age of the animal in question.
     *      'agt_t' is set to 'int' since it needs to be bigger than 0 and an
     *      element of the whole numbers.
     *   2) The actual genome is represented by a bitset called 'genome_' of length
     *      'genome_size_'.
     */

    private var genes = BitSet(genome_size)
    init { genes.set(0, genome_size, false) }

    /*  PRE:  'this' needs to be a valid Genome instance.
     *  POST: switch exactly 'mutation_rate_' many instances of
     *        of the child's genome_.
     */
    fun mutate(){
        val indices: MutableList<Int> = (0..genome_size).toMutableList()
        indices.shuffle()

        for(k in 0..mutation_rate_){
            genes.flip(indices[k])
        }
    }

    /*  PRE:  'this' is a valid genome instance and 'age' is smaller or equal to genome_size
     *  POST: Counts all the "bad genes" in genome_ up to the 'age'-th entry.
     *        A gene is bad if the entry in the BitSet is set to 'true'.
     */
    fun countBad(age: age_t): Int {
        return genes.get(0, age).cardinality()
    }

    companion object{
        var genome_size: Int = 64
        fun setMutationRate(age: age_t) { mutation_rate_ = age }
        private var mutation_rate_: age_t = 0
    }
}

animal.kt

package penna

import kotlin.random.Random.Default.nextDouble

class Animal(){
    /* Animal class for the Penna simulation.
     * The Animal class has several private members:
     *    1) 'mutation_rate_', 'reproduction_age_' and 'threshold_' are all parameters
     *        that stay constant for all animals of a population.
     *        The respective values can all be retrieved and set with the corresponding
     *        get and set functions.
     *    2) 'age_' represents the current age of the animal. By default construction it is set to 0.
     *    3) 'genome_' is a Genome class instance in which we will save the genome of an animal.
     *       When constructed all genes are set to be good (aka false).
     *    4) 'pregnant_' is a variable of type bool and tells you if the animal is currently pregnant.
     *       The status of each animal can be retrieved via the member function isPregnant().
     */

    // Default constructor
    private var age = 0
    private var genome: Genome = Genome()
    private var pregnant: Boolean = false

    constructor(mum_genes: Genome): this(){
        age = 0
        genome = mum_genes
        pregnant = false
    }

    fun isPregnant(): Boolean { return pregnant }

    fun age(): Int {
        return age
    }

    /* PRE:  'this' is a valid animal instance.
     * POST: Returns true if the animal is dead, otherwise false.
     *       An animal is dead if:
     *           1) age_ > max_age
     *           2) count_bad(age_) > threshold_
     */
    fun isDead(): Boolean { return age > max_age || genome.countBad(age) > threshold }

    /* PRE:  'mother' is pregnant.
     * POST: The following things are done in this order:
     *          1) set the mothers pregnancy to false.
     *          2) create an Animal instance with the mothers genome_
     *          3) 'mutate' the child's genome.
     */
    fun giveBirth(): Animal {
        assert(pregnant)
        pregnant = false
        val childGenome = genome
        childGenome.mutate()
        return Animal(childGenome)
    }

    /* PRE:  'this' has to be a valid Animal instance
     * POST: Grow the animal by one year:
     *          1) age_++
     *          2) set pregnant_ to true with probability_to_get_pregnant_.
     */
    fun grow() {
        assert(!this.isDead())
        age++
        if (age > reproductionAge && !pregnant){
            if(nextDouble(0.0,1.0) <= probabilityToGetPregnant){
                pregnant = true
            }
        }
    }

    companion object{
        private var probabilityToGetPregnant: Double = 0.0
        private var reproductionAge: age_t = 0                  // Age at which Animals start reproduction
        private var threshold: age_t = 0                        // More than this many mutations kills the Animal
        var max_age: age_t = Genome.genome_size

        fun setReproductionAge(num: age_t){ reproductionAge = num }
        fun setThreshold(num: age_t){ threshold = num }
        fun setProbabilityToGetPregnant(num: Double){ probabilityToGetPregnant = num }
    }
}

population.kt

package penna

import kotlin.random.Random.Default.nextDouble

open class Population(private var nMax: Int, nZero: Int) {
    /* Class to simulate a population of Animal objects.
     *      nMax:  The upper limit of the population size
     *      nZero: The starting number of the population
     */
    protected var population: MutableList<Animal> = ArrayList()

    init {
        for(k in 0 until nZero){
            population.add(Animal())
        }
    }

    fun size(): Int {
        return population.size
    }

    /* PRE:  ---
     * POST: Performs one step in the simulation:
     *          1) Age all animals by calling Animal::grow()
     *          2) Remove all animals that:
     *              2.1) are dead ( by using Animal::isDead() )
     *              2.2) if there are more than nMax many Animals in the population
     *              2.3) regardless of the above, kills an animal with probability population.size()/nMax
     *          3) Generate offspring by calling Animal::give_birth on the pregnant Animals in population and
     *             appending it to population.
     */
    open fun step() {
        // Age all animals
        population.forEach { it.grow() }

        // Remove dead ones
        population.removeIf{ this.size() / nMax.toDouble() >= 1.0 ||
                             nextDouble(0.0,1.0) < this.size() / nMax.toDouble() ||
                             it.isDead()
        }

        // Generate offspring
        val parents: MutableList<Animal> = population.filter { it.isPregnant() }.toMutableList()

        val babies : MutableList<Animal> = ArrayList()
        for(animal in parents){
            babies.add(animal.giveBirth())
        }

        population.addAll(babies)

    }
}

class FishingPopulation(nMax: Int, nZero: Int, fishingProb: Double, fishingAge: Int) : Population(nMax, nZero) {
    /* Derived class of Population to realize the Fishing aspect of the Discussion.
     *      fishingProb:    is the probability with which one fish will die due to fishing
     *      fishingAge:     the age at which a fish can die due to fishing
     */
    private var fishProb: Double = 0.0
    private var fishAge: Int = 0

    init {
        fishProb = fishingProb
        fishAge = fishingAge
    }

    // Change the two Parameters on the fly when necessary
    fun changeFishing(fishingProb: Double, fishingAge: Int){
        fishProb = fishingProb
        fishAge = fishingAge
    }

    /* Essentially the same function as Population::step(). We only perform the fishing in addition by removing
     * fish with the specified probability.
     */
    override fun step() {
        super.step()
        super.population.removeIf { it.age() > fishAge && nextDouble(0.0,1.0) < fishProb }
    }
}

main.kt

package penna

import java.io.File

fun main(){

    // Set the parameters for the simulation.
    Genome.genome_size = 64                     // Determines the maximal age of the Animal
    Genome.setMutationRate(2)                   // How many mutations per year can happen in the worst case
    Animal.setReproductionAge(6)                // Age at which Animals start reproduction
    Animal.setThreshold(8)                      // More than this many mutations kills the Animal
    Animal.setProbabilityToGetPregnant(1.0)     // Animal generate offspring every year


    val fish = FishingPopulation(10000, 1000, 0.0, 0)

    val popSizes: MutableList<Int> = ArrayList()

    for(generation in 0 until 5000){
        popSizes.add(fish.size())
        fish.step()
        if(generation == 500) {
            fish.changeFishing(0.19, 8)
        }
        if(generation == 3500){
            fish.changeFishing(0.22,0)
        }
    }

    File("data.txt").writeText(popSizes.toString())
}

As I said above, I'm a complete beginner when it comes to Kotlin and I have never coded in Java either, so it's very well possible that I approached the problem here completely worng... Any feedback is recommended.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ Does your code already work like you want it to? \$\endgroup\$
    – Phrancis
    Apr 8, 2020 at 20:42
  • 1
    \$\begingroup\$ @Phrancis It does, the results are the same I get from the c++ version. \$\endgroup\$
    – Sito
    Apr 8, 2020 at 20:44
  • \$\begingroup\$ If there is a way how I can improve the question, please let me know. \$\endgroup\$
    – Sito
    Apr 8, 2020 at 21:21
  • 3
    \$\begingroup\$ I reworded the title a bit to make it more clear, and added some tags. Hope you get some good answers. \$\endgroup\$
    – Phrancis
    Apr 8, 2020 at 22:54
  • \$\begingroup\$ @Phrancis Thank you very much for the help! \$\endgroup\$
    – Sito
    Apr 8, 2020 at 22:56

1 Answer 1

3
\$\begingroup\$

remark: I focused on syntax//features, not on the program itself.

shuffled

I'm taking the explanatory route
also

val indices: MutableList<Int> = (0..genome_size).toMutableList()
indices.shuffle()

This function can be rewritten with also:

val indices = (0..genome_size).toMutableList()
    .also{ it.shuffle() }

The object on which an extension-function is called is called receiver.
also allows you to provide a lambda in which you can access the receiver using it.
also returns the receiver itself.

apply
The next step from also is apply:
apply is the same as also, but you use this to refer to the receiver.
This means the code can be rewritten as:

val indices = (0..genome_size).toMutableList()
    .apply { this.shuffle() }

and because you can skip this to refer to something, you can use:

val indices = (0..genome_size).toMutableList()
    .apply { shuffle() }

Why did I tell you this?
There is already a function that does .toMutableList().apply { shuffle() }, named shuffled. Therefor, you can rewrite this function with:

val indices: List<Int> = (0..genome_size).shuffled()

oneliner function

You can simplify functions which start with return:

fun countBad(age: age_t): Int {
    return genes.get(0, age).cardinality()
}

This can be simplified to:

fun countBad(age: age_t): Int = genes.get(0, age).cardinality()
//or to 
fun countBad(age: age_t) = genes.get(0, age).cardinality()

constructors

In kotlin, constructors can define properties and default constructor parameters.

class FishingPopulation(
    nMax: Int, 
    nZero: Int, 
    fishingProb: Double, 
    fishingAge: Int
) : Population(nMax, nZero) {
    private var fishProb: Double = 0.0
    private var fishAge: Int = 0

    init {
        fishProb = fishingProb
        fishAge = fishingAge
    }
}

can be rewritten to:

class FishingPopulation(
    nMax: Int, 
    nZero: Int, 
    private var fishProb: Double = 0.0, 
    private var fishAge: Int = 0
) : Population(nMax, nZero)

There is one small difference between this code and the previous code:
fishProb and fishAge now have default-params, which means that they don't have to be specified during construction:

FishingPopulation(1, 2) is now the same as FishingPopulation(1, 2, 0, 0)
Also FishingPopulation(1, 2, fishAge = 1) is the same as FishingPopulation(1, 2, 0, 1)

List

MutableList vs ArrayList

In your code you use the following:

protected var population: MutableList<Animal> = ArrayList()

This is perfectly fine, if it must be an ArrayList.
If this is not required, you could better create the list by it's interface:

protected var population: MutableList<Animal> = mutableListOf()
//or
protected var population = mutableListOf<Animal>()

List vs MutableList

List doesn't allow mutation whereas MutableList does.
When you have code which requires that you mutate a particular list for example if the list is being observed actively for changes, then you need MutableList.
In every other case it's probably enough to have a normal List.

For example, the code where you create your parents (inside population) doesn't mutate at all, so copying it to a MutableList is unnecesary.

val parents: MutableList<Animal> = 
    population.filter { it.isPregnant() }
        .toMutableList()

transformation-operations

The code

val parents: MutableList<Animal> = population
    .filter { it.isPregnant() }
val babies : MutableList<Animal> = ArrayList()
for(animal in parents){
    babies.add(animal.giveBirth())
}

can be simplified using map:

val babies = population
    .filter { it.isPregnant() }
    .map{ it.giveBirth() }

You add the babies afterwards to a bigger list.
Using mapTo, you can add it to the bigger list immediately:

val babies = population
    .filter { it.isPregnant() }
    .mapTo(population){ it.giveBirth() }

Both access Population, but it will work because transformation-operations will work with a pattern:

  1. create a new collection
  2. process the items and add to the collection when needed.
  3. return the new collection.

Therefor, after the filter-function, population is not accessed anymore.
This also means that it isn't very performant... When you don't want to create a new list every time, you should use sequences.

see Kotlin Koans if you want to learn more about filter, map, zip, window, etc.

small remarks

  • nextDouble(0.0,1.0) is the same as nextDouble(1.0)
  • removeIf is from Java. use removeAll instead
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.