source: cpp/frams/genetics/f9/f9_conv.cpp @ 1337

Last change on this file since 1337 was 1337, checked in by Maciej Komosinski, 2 days ago

Added a comment discussing details of f9 deterministic "body noise"

  • Property svn:eol-style set to native
File size: 12.6 KB
Line 
1// This file is a part of Framsticks SDK.  http://www.framsticks.com/
2// Copyright (C) 1999-2025  Maciej Komosinski and Szymon Ulatowski.
3// See LICENSE.txt for details.
4
5#include "f9_conv.h"
6#include <frams/model/model.h>
7#include <string.h>
8
9#define APPLY_DETERMINISTIC_BODY_NOISE //this genetic representation easily produces perfectly vertical sticks that would stay upright forever in simulation. In most cases such infinite perfection is not desired, so we make the construct less perfect by perturbing its coordinates.
10
11GenoConv_f90::GenoConv_f90()
12{
13        name = "Turtle3D-ortho encoding";
14        in_format = '9';
15        out_format = '0';
16        mapsupport = 1;
17}
18
19
20const char* turtle_commands_f9 = "LRBFDU";
21int turtle_commands_f9_count = 6; //keep in sync, must equal strlen(turtle_commands_f9)
22
23//const char* turtle_commandsX_f9="-+0000";
24//const char* turtle_commandsY_f9="00-+00";
25//const char* turtle_commandsZ_f9="0000-+";
26
27SString GenoConv_f90::convert(SString &in, MultiMap *map, bool using_checkpoints)
28{
29        vector<XYZ_LOC> vertices;
30        XYZ_LOC current;
31        Model m;
32        m.open(using_checkpoints);
33        int recently_added = addSegment(m, 0, vertices, current, 0xDead);
34        for (int i = 0; i < in.length(); i++)
35        {
36                char command = in[i];
37                char *ptr = strchr((char*)turtle_commands_f9, command);
38                if (ptr)
39                {
40                        int delta[] = { 0, 0, 0 };
41                        int pos = ptr - turtle_commands_f9;
42                        int axis = pos / 2;
43                        int dir = pos % 2;
44                        (*(delta + axis)) += dir * 2 - 1; //+1 or -1 in the given axis
45                        current.add(delta);
46                        recently_added = addSegment(m, i, vertices, current, recently_added);
47                        m.checkpoint();
48                }
49        }
50#ifdef APPLY_DETERMINISTIC_BODY_NOISE
51        perturbPartLocations(m);
52#endif
53        setColors(m, recently_added);
54        m.close();
55        if (m.getPartCount() < 2) //only one part <=> there were no valid turtle commands in the input genotype
56                return ""; //so we return an invalid f0 genotype
57        if (map != NULL)
58                m.getCurrentToF0Map(*map);
59        return m.getF0Geno().getGenes();
60}
61
62int GenoConv_f90::addSegment(Model &m, int genenr, vector<XYZ_LOC> &vertices, const XYZ_LOC &new_vertex, int recently_added)
63{
64        if (vertices.size() < 1) //empty model?
65        {
66                return addNewVertex(m, vertices, new_vertex);
67        }
68        else
69        {
70                int vertex_here = findVertexAt(vertices, new_vertex);
71                if (vertex_here < 0) //need to create a new Part
72                {
73                        vertex_here = addNewVertex(m, vertices, new_vertex);
74                } //else there already exists a Part in new_vertex; new Joint may or may not be needed
75                Part *p1 = m.getPart(recently_added);
76                Part *p2 = m.getPart(vertex_here);
77                p1->addMapping(MultiRange(genenr, genenr));
78                p2->addMapping(MultiRange(genenr, genenr));
79
80                int j12 = m.findJoint(p1, p2);
81                int j21 = m.findJoint(p2, p1);
82                if (j12 >= 0)
83                        m.getJoint(j12)->addMapping(MultiRange(genenr, genenr));
84                else if (j21 >= 0)
85                        m.getJoint(j21)->addMapping(MultiRange(genenr, genenr));
86                else //both j12<0 and j21<0. New Joint needed. Should always happen if we just created a new Part (vertex_here was <0)
87                        m.addNewJoint(p1, p2)->addMapping(MultiRange(genenr, genenr));
88                return vertex_here;
89        }
90}
91
92int GenoConv_f90::findVertexAt(vector<XYZ_LOC> &vertices, const XYZ_LOC &vertex)
93{
94        for (int i = 0; i < (int)vertices.size(); i++)
95                if (vertices[i].same_coordinates(vertex)) return i;
96        return -1;
97}
98
99
100int GenoConv_f90::addNewVertex(Model &m, vector<XYZ_LOC> &vertices, const XYZ_LOC &new_vertex)
101{
102        Part *p = new Part;
103        p->p.x = new_vertex.x;
104        p->p.y = new_vertex.y;
105        p->p.z = new_vertex.z;
106        m.addPart(p);
107
108        vertices.push_back(new_vertex);
109        return int(vertices.size()) - 1;
110}
111
112double mix(int *colortab, int maxind, double ind)
113{
114        int indpre = (int)ind;
115        int indpost = indpre + 1;
116        if (indpost > maxind) indpost = maxind;
117        int v1 = colortab[indpre];
118        int v2 = colortab[indpost];
119        double d1 = ind - indpre;
120        double d2 = indpost - ind;
121        double v = indpre == indpost ? v1 : d2 * v1 + d1 * v2; //d1+d2==1
122        return v;
123}
124
125void GenoConv_f90::setColors(Model &m, int last_added_part) //sets fixed (independent from genes) colors and widths on a model, purely for aesthetic purposes
126{
127        static const bool OLD = false; //old "rainbow" hue gradient
128        if (OLD)
129        {
130                //a rainbow on Joints: from the first one red, through middle green, to blue or violet - last
131                static int r[] = { 1, 1, 0, 0, 0, 1 };
132                static int g[] = { 0, 1, 1, 1, 0, 0 };
133                static int b[] = { 0, 0, 0, 1, 1, 1 };
134                int maxind = int(std::size(r)) - 1;
135
136                int joints_count = m.getJointCount();
137                for (int i = 0; i < joints_count; i++)
138                {
139                        Joint *j = m.getJoint(i);
140                        double x = joints_count < 2 ? 0 : (double)i / (joints_count - 1); //0..1, position in the rainbow
141                        double ind = x * maxind;
142                        j->vcolor.x = mix(r, maxind, ind);
143                        j->vcolor.y = mix(g, maxind, ind);
144                        j->vcolor.z = mix(b, maxind, ind);
145                }
146        }
147        else
148        {
149                int joints_count = m.getJointCount();
150                for (int i = 0; i < joints_count; i++)
151                {
152                        Joint *j = m.getJoint(i);
153                        Pt3D d = j->part2->p - j->part1->p; //dx,dy,dz
154                        double ax = fabs(d.x), ay = fabs(d.y), az = fabs(d.z);
155                        // Pairs of colors should be easy to associate as "one family" at a glance, but different so that we use all main six parts of the spectrum.
156                        // Colors are slightly brightened to make them more pastel/plastic; extreme saturation pure red=1,0,0 or blue 0,0,1 do not look good.
157                        // Find the longest axis (i.e., recover the information from the genotype: was this joint created by LR, BF, or DU?)
158                        if ((ax > ay) && (ax > az)) // x
159                        {
160                                j->vcolor = d.x < 0 ? Pt3D(1, 0.2, 0.2) : Pt3D(1, 0.2, 0.6); // red, purple red
161                        }
162                        else
163                                if ((ay > ax) && (ay > az)) // y
164                                {
165                                        j->vcolor = d.y < 0 ? Pt3D(0.2, 1, 0.2) : Pt3D(0.7, 1, 0.2); //green, yellowish green
166                                }
167                                else // z
168                                {
169                                        j->vcolor = d.z < 0 ? Pt3D(0.2, 0.2, 1) : Pt3D(0.4, 0.9, 1); //blue, cyanish blue
170                                }
171                }
172        }
173
174        int parts_count = m.getPartCount();
175        if (OLD)
176        {
177                SList jlist;
178                for (int i = 0; i < parts_count; i++)
179                {
180                        Part *p = m.getPart(i);
181                        jlist.clear();
182                        int count = m.findJoints(jlist, p);
183                        Pt3D averagecolor(0, 0, 0); //Parts will get averaged colors from all attached Joints
184                        FOREACH(Joint*, j, jlist)
185                                averagecolor += j->vcolor;
186                        p->vcolor = averagecolor / count;
187                }
188        }
189        else
190        {
191                //Parts will get gray colors from darker to brighter, according to their order of creation.
192                for (int i = 0; i < parts_count; i++)
193                {
194                        Part *p = m.getPart(i);
195                        p->vcolor.x = p->vcolor.y = p->vcolor.z = 0.3 + 0.4 * i / (parts_count - 1); //0.3..0.7, so first (black) and last (white) stand out more
196                }
197        }
198        //The first Part will be black, the last Part will be white - a visual aid for easier matching of the genotype and the corresponding phenotype.
199        if (!OLD)
200                m.getPart(0)->vcolor = Pt3D(0, 0, 0); //mark first Part black - not attractive in OLD sweet and positive color scheme
201        m.getPart(last_added_part)->vcolor = Pt3D(1, 1, 1); //mark last Part white
202}
203
204void GenoConv_f90::perturbPartLocations(Model &m) //deterministic "body noise", see APPLY_DETERMINISTIC_BODY_NOISE
205{
206        // If you want to know why some structures that have different f9 genotypes, but "look the same", still get different fitness values, read on!
207        //
208        // For the purpose of the following discussion, consider 4 genotypes and the resulting structures as examples:
209        // 1. square                        /*9*/LFRB
210        // 2. square overlapping twice      /*9*/LFRBLFRB
211        // 3. reverse square                /*9*/LRFLB
212        // 4. cyclic permutation square     /*9*/FRBL
213        //
214        // All these structures "look the same" (i.e., the phenotypes are squares and a human does not distinguish any differences).
215        // However, due to noise added to location of Parts based on the consecutive number of a Part,
216        // only the phenotypes of 1. and 2. are truly, perfectly identical.
217        //
218        // Since the noise is determined by consecutive Part numbers, it is possible to create structures that "look the same",
219        // but since Parts in them are created in different order, the structures would have slightly differently disturbed
220        // coordinates (and thus fitness).
221        //
222        // To avoid this effect, the noise could instead be generated from x,y,z (i.e., XYZ_LOC) of Parts so that structures that
223        // "look the same" have exactly the same noise added to each Part (and thus identical fitness), but then the structures that are
224        // shifted, rotated, or mirrored would still have different noise added to each Part, even though they could also be considered as
225        // "looking the same".
226        // In that approach, 3. would be the same as 1. and 2., because it is specifically constructed to use the same x,y,z Part coordinates as 1.
227        // However, 4. would still be different from 1. and 2., because it is a rotated version of 1, and coordinates of Parts are different.
228        //
229        // So there is no quick and simple way to add exactly the same noise to structures that "look the same" independently of
230        // various potential transformations. Also, what is considered "the same" may vary: is the structure (e.g. a tetrahedron) turned
231        // upside-down "the same" and do we require the same noise disturbance? It depends on the context and what we use this structure for.
232        // So that's the question of identifying transformation-invariant features in the structure that would become the source of the noise.
233        //
234        // With the current approach, structures that have precisely the same randomly perturbed Part coordinates (and thus fitness) are
235        // those that have the same Part coordinates before random perturbations AND the order of creating Parts is the same (their genotypes
236        // may still be different though, like 1. and 2.). Structures which have the same order of created Parts and are only shifted
237        // relative to each other (i.e., all Parts uniformly shifted) can also be considered precisely the same (ignoring floating point
238        // differences). There is no such example provided here because it is impossible to make in f9, because we always start
239        // "drawing" Part #0 from (0,0,0) - it is not possible in f9 to start "drawing" Part #0 from another initial point in space.
240        //
241        // A simple alternative (quite opposite to the current approach) could be to add the same amount of shift (noise) to each Part
242        // depending only on their vertical coordinate - then only vertical transformations would influence that shift, while Part creation
243        // order and all horizontal shifts, rotations, and reflections would not matter.
244        // Another determinant of randomness could be the structure of the phenotype graph, traversed depth-first, from some specific Part
245        // (i.e., closest to the centroid).
246        // Another determinant of randomness could be the integer (avoiding floating point operations if possible) squared distance from
247        // the structure center/centroid of Parts (likely displaced by some constant to avoid symmetry and ties in distances, e.g. for
248        // a vertical stick).
249        // Another (best?) approach: [start] calculate centroid of current structure, calculate distances from centroid to all Parts, take closest Part, add noise to its coordinates based on its distance from centroid and stddev(x),stddev(y),stddev(z) for all Parts in the entire phenotype (this ensures that noise depends on the orientation of the entire phenotype, i.e., a "horizontal square" and a "vertical square", after added noise, still "look the same"), then go back to [start]. In case many Parts have identical distance, take Part with lowest number (id) - i.e., use the same number as we currently use as the only source of noise, assuming that the same distance from centroid for many Parts means "any of them could be taken as first, it does not matter which one we take". Note that this approach starts to resemble some mechanisms used in the similarity estimation measures - PCA to align/"normalize" the phenotype in 3D etc.
250
251        // For now, the source of the noise stays as it is because:
252        // 1) the source is integer (consecutive number of a Part), not distances or other stats prone to floating point inaccuracies
253        // 2) the idea is extremely simple
254        // 3) "complaints/surprises" detailed above are only due to the fact that we know what is the phenotype (solution) and we have some specific expectations (like "rotated or mirrored or shifted or alternatively encoded structures should be equivalent, should behave the same way in simulation, and thus should have the same fitness"). For the optimization algorithm it does not matter and it has no such assumptions, because genotypes for "rotated or mirrored or shifted or alternatively encoded structures" are actually different, so since they are formally different solutions, it is perfectly fine that they may get a different fitness value. Also, while they are extremely close in the phenotype space, they are not neccessarily close in the f9 genotype space where the search takes place.
255        for (int i = 0; i < m.getPartCount(); i++)
256        {
257                Part *p = m.getPart(i);
258                Pt3D noise(
259                        ((i + 1) % 10) - 4.5,
260                        ((3 * i + 5) % 10) - 4.5,
261                        ((7 * i + 2) % 10) - 4.5
262                ); //-4.5 .. 4.5 in each axis
263                p->p += noise / 1000;
264        }
265}
Note: See TracBrowser for help on using the repository browser.