davidrmiller / biosim4

Biological evolution simulator
Other
3.21k stars 460 forks source link

How are colors calculated? #16

Open rick2047 opened 3 years ago

rick2047 commented 3 years ago

I have very little knowledge of C++, but I find the project fascinating. But I can't figure out how a genome is converted into colors. Looking at the code, it seems like the function to convert a genome https://github.com/davidrmiller/biosim4/blob/9d9bb2706f0212946f8c654ddb1f65862a652fb7/src/imageWriter.cpp#L98-L108

It seems like only the first and last gene is being used to convert to a color. Which seems to be unpacked and plotted here

https://github.com/davidrmiller/biosim4/blob/9d9bb2706f0212946f8c654ddb1f65862a652fb7/src/imageWriter.cpp#L56-L58

I have couple questions

  1. The first and last genes are not in any ways special, then why only use those two? This can give an illusion of two very different genomes looking very similar.
  2. Why unpack a uint8 to three uint8s, only for visualization?
davidrmiller commented 3 years ago

Ah colors. @rick2047, you ask an interesting question. The repository code happened to capture one of many awkward ways I tried to convert a genome, which could be hundreds of bits, into a few bits of color. The draw_circle() function takes a pointer to an array of three unsigned 8-bit integers for red, green, and blue for a 24-bit color space. But I found that very light colors are hard to see against a white canvas, so I limited each color channel to six bits of the darkest colors. There's no way to map hundreds of bits of genome into an 18-bit color space in a way that shows all genomic differences. Half of the bits of a genome specify connection weights, but I intuitively felt that two genomes were more "different" if they differed in connection topology than if they differed in connection weights. That's why I preferentially used bits from neuron identifiers to map to the color space instead of using bits from connection weights. Since genomes could be very short or very long, I arbitrarily used bits from the beginning and end of the genome. It's a bit lazy but worked well enough in practice for the intended purpose. There are many other ways to do this mapping.

rick2047 commented 3 years ago

Ok, nice to know we are not completely off :D. If I understand it correctly, the 6 bit of colors are basically mapping the sink/source type and ID for the first and last genes. I was wondering if you considered using dimensionality reduction techniques based on projection like PCA. The biggest problem I see is it would require us to learn the color space each iteration, and it would not be stable between iterations as the creatures mutate.

Dragon-GCS commented 3 years ago

i'm trying to write this by python(well, I like python and i dont want change my system to Linux). I use a 24bit to express a gene, 8 for sink and Red value, 8 for source and Green value, 8 for weight and Blue value, chosse the first gene to represent the genome's color, or maybe consider use the average value of all gene's RGB

keaton-codes-tech commented 2 years ago

Hello fellow programmers. I remade this project in JavaScript but tried to reverse engineer as much of it as I could without reading the source code, so my project ended up working quite differently. The way I generated colours was as follows:

I labelled the Input, Hidden and Output neurons with different categories: pheromone = 0, internal = 1, environment = 2, social = 3, hidden = 4 (space for 2 more types if needed/wanted)

Then I created the colour channels as follows:

Red channel Bit 1 = Source Layer (input, hidden) Bit 2-4 = Source Category (allows for 7 different types) Bit 5-8 = Source TypeID (4 bits allows for 15 different IDs per category)

Green channel Bit 1 = Sink Layer (hidden, output) Bit 2-4 = Sink Category Bit 5-8 = Sink TypeID

Blue channel Connection weight that is between -4 and 4 is mapped to range 0 - 255

The colour is the average of these values for each connection between neurons.

The functions that I use to map the range:

    // returns the value between two numbers at a specified decimal midpoint
    lerp(x, y, a) {
        return (x * (1 - a) + y * a);
    },
    // give it a number and then a minimum & maximum. If your number falls within the bounds of the min & max, it’ll return it. 
    // If not, it’ll return either the minimum if it’s smaller, or the maximum if it’s bigger
    clamp(a, min = 0, max = 1) {
        return Math.min(max, Math.max(min, a));
    },
    // Inverted LERP: you pass any value, and it’ll return that decimal, wherever it falls on that spectrum
    invlerp(x, y, a) {
        return clamp((a - x) / (y - x));
    },
    // converts a value from one data range to another
    range(x1, y1, x2, y2, a) {
        return lerp(x2, y2, invlerp(x1, y1, a));
    },

Let me know if any of this sounds dumb :)