source: cpp/frams/genetics/f1/f1_conv.cpp @ 1304

Last change on this file since 1304 was 1302, checked in by Maciej Komosinski, 7 months ago

Cosmetic / minor fixes

  • Property svn:eol-style set to native
File size: 18.8 KB
Line 
1// This file is a part of Framsticks SDK.  http://www.framsticks.com/
2// Copyright (C) 1999-2023  Maciej Komosinski and Szymon Ulatowski.
3// See LICENSE.txt for details.
4
5#include "f1_conv.h"
6#include <common/log.h>
7#include <frams/util/multirange.h>
8#include <frams/util/multimap.h>
9#include <frams/genetics/geneprops.h>
10#include <ctype.h>
11#include <assert.h>
12#include <algorithm>
13
14//#define v1f1COMPATIBLE //as in ancient Framsticks 1.x
15
16#define FIELDSTRUCT GenoConv_f1
17static ParamEntry f1_conv_paramtab[] =
18{
19        { "Genetics: Converters: f1 to f0", 1, 3, },
20        { "conv_f1_f0_modcompat", 1, 0, "Modifier compatibility", "d 0 1 1 ~Before June 2023~Modern", FIELD(settings.modifier_compatibility), "The modern implementation makes the influence of modifiers more consistent and uniform, and the extreme property values are easier to reach with a lower number of characters, which improves the topology for evolutionary search.\nPrevious implementation can be enabled for compatibility, for example when you want to test old genotypes." },
21        { "conv_f1_f0_cq_influence", 1, 0, "'C' and 'Q' modifier influence", "d 0 1 1 ~Before June 2023~Modern", FIELD(settings.cq_mod_influence), "'C' and 'Q' modifier semantics was changed in June 2023. Previously they did not affect the stick immediately following the current sequence of modifiers. In the modern implementation, all modifiers consistently start their influence at the very next stick that is being created in the current branch.\nExample:\nIn the old interpretation of 'XcXX', only the last stick is rotated, because 'c' starts its influence at the stick that occurs after the current stick. In the modern implementation, the same effect is achieved with 'XXcX', where 'c' immediately bends the first 'X' that appears after it.\nPrevious implementation can be enabled for compatibility, for example when you want to test old genotypes." },
22        { "conv_f1_f0_branch_muscle_range", 1, 0, "Bending muscle default range", "d 0 2 0 ~Full/NumberOfBranches~Full/(NumberOfBranches+1)~Full", FIELD(settings.branch_muscle_range), "Determines how the bending muscle default turning range is limited when the muscle is controlling a stick growing from a branching point that has 'NumberOfBranches' sticks separated by commas. The motivation of the limited range is to keep the neighboring sticks from intersecting when they are bent by muscles. This constraint may degrade the performance (e.g. velocity) of creatures, but this default value can be overridden by providing a specific range property value for the '|' muscle neuron in the genotype.\n"
23          "- Full/NumberOfBranches - a compromise between the two other settings.\n"
24          "- Full/(NumberOfBranches+1) - because the originating stick also counts as a branch. This setting guarantees that in the worst case, when at least two neighboring branches have sticks controlled by bending muscles and their controlling signals are at extreme values, the sticks can touch and overlap, but will not intersect. This setting is in most cases too strict because (1) all branches are very rarely controlled by muscles, (2) there are often 'empty' branches - multiple commas with no sticks in-between, and (3) the share of the originating stick is effectively wasted because this stick itself has no muscle at the branching point so it will not bend; the muscle bending range is symmetrical and the default range is equal for all muscles in a branching, but the sticks equipped with muscles in a branching are rarely evenly spaced.\n"
25          "- Full: always the complete angle - because we do not have to care about the physical plausibility and avoid intersecting sticks, and other genetic representations do not impose such constraints, so this full angle setting can be useful as the default bending range when comparing the performance of various genetic encodings."},
26
27        { 0, },
28};
29#undef FIELDSTRUCT
30
31class Builder
32{
33public:
34        Builder(GenoConv_f1_Settings& _settings, const char*g, int mapping = 0) :settings(_settings), invalid(0), genbegin(g), usemapping(mapping), first_part_mapping(NULL), own_first_part_mapping(true), model_energy(0), model_energy_count(0)
35        {
36                switch (settings.modifier_compatibility)
37                {
38                case GenoConv_f1_Settings::MODIF_COMPAT_LEGACY: gp_ops = GeneProps::getLegacyOps(); break;
39                case GenoConv_f1_Settings::MODIF_COMPAT_ALL_CHANGE_05: gp_ops = GeneProps::getAllChange05Ops(); break;
40                }
41        }
42        ~Builder() { if (own_first_part_mapping) SAFEDELETE(first_part_mapping); }
43
44        GenoConv_f1_Settings& settings;
45        GenePropsOps* gp_ops;
46        char tmp[222];
47        bool invalid;
48        Model model;
49        const char *genbegin;
50        SList neuro_f1_to_f0; // neuro_f1_to_f0(f1_refno) = actual neuro pointer
51        Neuro *last_f1_neuro;
52        SyntParam *neuro_cls_param;
53
54        struct Connection
55        {
56                int n1, n2; double w;
57                Connection(int _n1, int _n2, double _w) :n1(_n1), n2(_n2), w(_w) {}
58        };
59
60        SListTempl<Connection> connections;
61        int usemapping;
62        MultiRange range;
63        MultiRange *first_part_mapping;
64        bool own_first_part_mapping;
65        double lastjoint_muscle_power;
66        double model_energy;
67        int model_energy_count;
68        void grow(int part1, const char*g, Pt3D k, GeneProps c, int branching_part);
69        void setPartMapping(int p, const char* g);
70        int growJoint(int part1, int part2, const Pt3D &angle, const GeneProps &c, const char *g);
71        int growPart(GeneProps &c, const char *g);
72        const char *skipNeuro(const char *z);
73        const char* growNeuro(const char* t, GeneProps &c, int&);
74        void growConnection(const char* begin, const char* colon, const char* end, GeneProps& props);
75        int countBranches(const char*g, SList &out);
76        SyntParam* lastNeuroClassParam();
77        void addClassParam(const char* name, double value);
78        void addClassParam(const char* name, const char* value);
79
80        const MultiRange* makeRange(const char*g) { return makeRange(g, g); }
81        const MultiRange* makeRange(const char*g, const char*g2);
82        Part *getLastPart() { return getLastJoint()->part2; }
83        Neuro *getLastNeuro() { return model.getNeuro(model.getNeuroCount() - 1); }
84        Joint *getLastJoint() { return model.getJoint(model.getJointCount() - 1); }
85        void addOrRememberInput(int n1, int n2, double w)
86        {
87                //if (!addInput(n1,n2,w,false))
88                connections += Connection(n1, n2, w);
89        }
90        bool addInput(int n1, int n2, double w, bool final)
91        {
92                if ((n1 < 0) || (n2 < 0) || (n1 >= neuro_f1_to_f0.size()) || (n2 >= neuro_f1_to_f0.size()))
93                {
94                        if (final) logPrintf("GenoConvF1", "addInput", LOG_WARN,
95                                "illegal neuron connection %d <- %d (ignored)", n1, n2);
96                        return 0;
97                }
98                Neuro *neuro = (Neuro*)neuro_f1_to_f0(n1);
99                Neuro *input = (Neuro*)neuro_f1_to_f0(n2);
100                neuro->addInput(input, w);
101                return 1;
102        }
103        void addPendingInputs()
104        {
105                for (int i = 0; i < connections.size(); i++)
106                {
107                        Connection *c = &connections(i);
108                        addInput(c->n1, c->n2, c->w, true);
109                }
110        }
111};
112
113const MultiRange* Builder::makeRange(const char*g, const char*g2)
114{
115        if (!usemapping) return 0;
116        range.clear();
117        range.add(g - genbegin, g2 - genbegin);
118        return &range;
119}
120
121GenoConv_f1::GenoConv_f1()
122{
123        name = "Recursive encoding";
124        in_format = '1';
125        mapsupport = 1;
126
127        param.setParamTab(f1_conv_paramtab);
128        param.select(this);
129        param.setDefault();
130}
131
132/** main conversion function - with conversion map support */
133SString GenoConv_f1::convert(SString &i, MultiMap *map, bool using_checkpoints)
134{
135        const char* g = i.c_str();
136        Builder builder(settings, g, map ? 1 : 0);
137        builder.model.open(using_checkpoints);
138        builder.grow(-1, g, Pt3D_0, GeneProps::standard_values, -1); // uses Model::addFromString() to create model elements
139        if (builder.invalid) return SString();
140        builder.addPendingInputs();
141        builder.model.startenergy = (builder.model_energy_count > 0) ? (builder.model_energy / builder.model_energy_count) : 1.0;
142        builder.model.close(); // model is ready to use now
143        if (map) builder.model.getCurrentToF0Map(*map); // generate f1-to-f0 conversion map
144        return builder.model.getF0Geno().getGenes();
145}
146
147void Builder::setPartMapping(int p, const char* g)
148{
149        if (!usemapping) return;
150        const MultiRange *r = makeRange(g);
151        if (p < 0)
152        { //special case: mapping the part which is not yet created
153                if (first_part_mapping) first_part_mapping->add(*r);
154                else { first_part_mapping = new MultiRange(*r); own_first_part_mapping = true; }
155        }
156        else
157                model.getPart(p)->addMapping(*r);
158}
159
160void Builder::grow(int part1, const char*g, Pt3D k, GeneProps c, int branching_part)
161{
162        int hasmuscles = 0;
163        if (settings.cq_mod_influence == settings.CQ_INFLUENCE_OLD)
164                k += Pt3D(c.twist, 0, c.curvedness);
165        while (1)
166        {
167                if (c.executeModifier(*g, gp_ops) == 0)
168                {
169                        setPartMapping(part1, g);
170                }
171                else
172                {
173                        switch (*g)
174                        {
175                        case 0: return;
176                        case ',': case ')': setPartMapping(branching_part, g); return;
177                        case 'R': k.x += 0.7853; setPartMapping(part1, g); break; // 45 degrees = pi/4 like in f4
178                        case 'r': k.x -= 0.7853; setPartMapping(part1, g); break;
179                        case '[': //neuron
180                                //              setdebug(g-(char*)geny,DEBUGNEURO | !l_neu);
181                                if (model.getJointCount())
182                                        g = growNeuro(g + 1, c, hasmuscles);
183                                else
184                                {
185                                        logMessage("GenoConv_F1", "grow", 1, "Illegal neuron position (ignored)");
186                                        g = skipNeuro(g + 1);
187                                }
188                                break;
189                        case 'X':
190                        {
191                                int freshpart = 0;
192                                //setdebug(g-(char*)geny,DEBUGEST | !l_est);
193                                if (part1 < 0) //initial grow
194                                {
195                                        if (model.getPartCount() > 0)
196                                                part1 = 0;
197                                        else
198                                        {
199                                                part1 = growPart(c, g);
200                                                freshpart = 1;
201                                                if (first_part_mapping)
202                                                {
203                                                        //mapping was defined before creating this initial Part -> put it into the Part
204                                                        assert(own_first_part_mapping);
205                                                        model.getPart(part1)->setMapping(*first_part_mapping);
206                                                        delete first_part_mapping;
207                                                        //first_part_mapping can be still used later but from now on it references the internal Part mapping
208                                                        first_part_mapping = model.getPart(part1)->getMapping();
209                                                        own_first_part_mapping = false;
210                                                }
211                                        }
212                                }
213                                if (!freshpart)
214                                {
215                                        Part *part = model.getPart(part1);
216                                        part->density = ((part->mass * part->density) + 1.0 / c.weight) / (part->mass + 1.0); // v=m*d
217                                        //                      part->volume+=1.0/c.weight;
218                                        part->mass += 1.0;
219                                }
220
221                                if (settings.modifier_compatibility == settings.MODIF_COMPAT_LEGACY)
222                                        model_energy += 0.9 * c.energy + 0.1;
223                                else
224                                        model_energy += c.energy;
225                                model_energy_count++;
226
227                                int part2 = growPart(c, g);
228                                Pt3D new_k;
229                                switch (settings.cq_mod_influence)
230                                {
231                                case GenoConv_f1_Settings::CQ_INFLUENCE_OLD: new_k = k; break;
232                                case GenoConv_f1_Settings::CQ_INFLUENCE_NEW: new_k = k + Pt3D(c.twist, 0, c.curvedness); break;
233                                }
234                                growJoint(part1, part2, new_k, c, g);
235                                //              est* e = new est(*s,*s2,k,c,zz,this);
236
237                                // attenuate properties as they are propagated along the structure
238                                c.propagateAlong(true);
239
240                                model.checkpoint();
241                                grow(part2, g + 1, Pt3D_0, c, branching_part);
242                                return;
243                        }
244                        case '(':
245                        {
246                                setPartMapping(part1, g);
247                                SList ga;
248                                int count = countBranches(g + 1, ga);
249                                c.muscle_reset_range = false;
250                                switch (settings.branch_muscle_range)
251                                {
252                                case GenoConv_f1_Settings::MUSCLE_RANGE_ORIGINAL:
253                                        c.muscle_bend_range = 1.0 / count;
254                                        break;
255                                case GenoConv_f1_Settings::MUSCLE_RANGE_STRICT:
256                                        c.muscle_bend_range = 1.0 / (count + 1);
257                                        break;
258                                case GenoConv_f1_Settings::MUSCLE_RANGE_UNLIMITED:
259                                        c.muscle_bend_range = 1.0;
260                                        break;
261                                }
262                                for (int i = 0; i < count; i++)
263                                        grow(part1, (char*)ga(i), k + Pt3D(0, 0, -M_PI + (i + 1) * (2 * M_PI / (count + 1))), c, part1);
264                                return;
265                        }
266                        case ' ': case '\t': case '\n': case '\r': break;
267                        default: invalid = 1; return;
268                        }
269                }
270                g++;
271        }
272}
273
274SyntParam* Builder::lastNeuroClassParam()
275{
276        if (!neuro_cls_param)
277        {
278                NeuroClass *cls = last_f1_neuro->getClass();
279                if (cls)
280                {
281                        neuro_cls_param = new SyntParam(last_f1_neuro->classProperties());
282                        // this is equivalent to:
283                        //              SyntParam tmp=last_f1_neuro->classProperties();
284                        //              neuro_cls_param=new SyntParam(tmp);
285                        // interestingly, some compilers eliminate the call to new SyntParam,
286                        // realizing that a copy constructor is redundant when the original object is
287                        // temporary. there are no side effect of such optimization, as long as the
288                        // copy-constructed object is exact equivalent of the original.
289                }
290        }
291        return neuro_cls_param;
292}
293
294void Builder::addClassParam(const char* name, double value)
295{
296        lastNeuroClassParam();
297        if (neuro_cls_param)
298                neuro_cls_param->setDoubleById(name, value);
299}
300
301void Builder::addClassParam(const char* name, const char* value)
302{
303        lastNeuroClassParam();
304        if (neuro_cls_param)
305        {
306                ExtValue e(value);
307                const ExtValue &re(e);
308                neuro_cls_param->setById(name, re);
309        }
310}
311
312int Builder::countBranches(const char*g, SList &out)
313{
314        int gl = 0;
315        out += (void*)g;
316        while (gl >= 0)
317        {
318                switch (*g)
319                {
320                case 0: gl = -1; break;
321                case '(': case '[': ++gl; break;
322                case ')': case ']': --gl; break;
323                case ',': if (!gl) out += (void*)(g + 1);
324                }
325                g++;
326        }
327        return out.size();
328}
329
330int Builder::growJoint(int part1, int part2, const Pt3D &angle, const GeneProps &c, const char *g)
331{
332        double len = std::min(2.0, c.length);
333        Part *p1 = model.getPart(part1);
334        Part *p2 = model.getPart(part2);
335        sprintf(tmp, "p1=%d,p2=%d,dx=%lg,rx=%lg,ry=%lg,rz=%lg,stam=%lg,vr=%g,vg=%g,vb=%g",
336                part1, part2, len, angle.x, angle.y, angle.z, c.stamina, (p1->vcolor.x+p2->vcolor.x)/2, (p1->vcolor.y+p2->vcolor.y)/2, (p1->vcolor.z+p2->vcolor.z)/2);
337        lastjoint_muscle_power = c.muscle_power;
338        return model.addFromString(Model::JointType, tmp, makeRange(g));
339}
340
341int Builder::growPart(GeneProps &c, const char *g)
342{
343        sprintf(tmp, "dn=%lg,fr=%lg,ing=%lg,as=%lg,vr=%g,vg=%g,vb=%g",
344                1.0 / c.weight, c.friction, c.ingestion, c.assimilation, c.cred, c.cgreen, c.cblue);
345        return model.addFromString(Model::PartType, tmp, makeRange(g));
346}
347
348const char *Builder::skipNeuro(const char *z)
349{
350        for (; *z; z++) if ((*z == ']') || (*z == ')')) break;
351        return z - 1;
352}
353
354const char* Builder::growNeuro(const char* t, GeneProps& props, int &hasmuscles)
355{
356        const char*neuroend = skipNeuro(t);
357        last_f1_neuro = model.addNewNeuro();
358        neuro_cls_param = NULL;
359        last_f1_neuro->attachToPart(getLastPart());
360        const MultiRange *mr = makeRange(t - 1, neuroend + 1);
361        if (mr) last_f1_neuro->addMapping(*mr);
362        neuro_f1_to_f0 += last_f1_neuro;
363
364        SString clsname;
365        bool haveclass = 0;
366        while (*t && *t <= ' ') t++;
367        const char* next = (*t) ? (t + 1) : t;
368        while (*next && *next <= ' ') next++;
369        if (*t && *next != ',' && *next != ']') // old style muscles [|rest] or [@rest]
370                switch (*t)
371                {
372                case '@': if (t[1] == ':') break;
373                        haveclass = 1;
374                        //              if (!(hasmuscles&1))
375                        {
376                                hasmuscles |= 1;
377                                Neuro *muscle = model.addNewNeuro();
378                                sprintf(tmp, "@:p=%lg", lastjoint_muscle_power);
379                                muscle->addInput(last_f1_neuro);
380                                muscle->setDetails(tmp);
381                                muscle->attachToJoint(getLastJoint());
382                                if (usemapping) muscle->addMapping(*makeRange(t));
383                        }
384                        t++;
385                        break;
386                case '|': if (t[1] == ':') break;
387                        haveclass = 1;
388                        //              if (!(hasmuscles&2))
389                        {
390                                hasmuscles |= 2;
391                                Neuro *muscle = model.addNewNeuro();
392                                sprintf(tmp, "|:p=%lg,r=%lg", lastjoint_muscle_power, props.muscle_bend_range);
393                                muscle->addInput(last_f1_neuro);
394                                muscle->setDetails(tmp);
395                                muscle->attachToJoint(getLastJoint());
396                                if (usemapping) muscle->addMapping(*makeRange(t));
397                        }
398                        t++;
399                        break;
400                }
401        while (*t && *t <= ' ') t++;
402        bool finished = 0;
403        const char *begin = t;
404        const char* colon = 0;
405        SString classparams;
406        while (!finished)
407        {
408                switch (*t)
409                {
410                case ':': colon = t; break;
411                case 0: case ']': case ')': finished = 1;
412                        // NO break!
413                case ',':
414                        if (!haveclass && !colon && t > begin)
415                        {
416                                haveclass = 1;
417                                SString clsname(begin, t - begin);
418                                clsname = trim(clsname);
419                                last_f1_neuro->setClassName(clsname);
420                                NeuroClass *cls = last_f1_neuro->getClass();
421                                if (cls)
422                                {
423                                        if (cls->getPreferredLocation() == 2)
424                                                last_f1_neuro->attachToJoint(getLastJoint());
425                                        else if (cls->getPreferredLocation() == 1)
426                                                last_f1_neuro->attachToPart(getLastPart());
427
428                                        lastNeuroClassParam();
429                                        //special handling: muscle properties (can be overwritten by subsequent property assignments)
430                                        if (!strcmp(cls->getName().c_str(), "|"))
431                                        {
432                                                neuro_cls_param->setDoubleById("p", lastjoint_muscle_power);
433                                                neuro_cls_param->setDoubleById("r", props.muscle_bend_range);
434                                        }
435                                        else if (!strcmp(cls->getName().c_str(), "@"))
436                                        {
437                                                neuro_cls_param->setDoubleById("p", lastjoint_muscle_power);
438                                        }
439                                }
440                        }
441                        else if (colon && (colon > begin) && (t > colon))
442                                growConnection(begin, colon, t, props);
443                        if (t[0] != ',') t--;
444                        begin = t + 1; colon = 0;
445                        break;
446                }
447                t++;
448        }
449        SAFEDELETE(neuro_cls_param);
450        return t;
451}
452void Builder::growConnection(const char* begin, const char* colon, const char* end, GeneProps& props)
453{
454        while (*begin && *begin <= ' ') begin++;
455        int i;
456        if (isdigit(begin[0]) || (begin[0] == '-'))
457        {
458                double conn_weight = ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str());
459                paInt relative = ExtValue::getInt(trim(SString(begin, colon - begin)).c_str(), false);
460                int this_refno = neuro_f1_to_f0.size() - 1;
461                addOrRememberInput(this_refno, this_refno + relative, conn_weight);
462        }
463        else if ((i = last_f1_neuro->extraProperties().findIdn(begin, colon - begin)) >= 0)
464        {
465                last_f1_neuro->extraProperties().setFromString(i, colon + 1);
466        }
467        else if (isupper(begin[0]) || strchr("*|@", begin[0]))
468        {
469                SString clsname(begin, colon - begin);
470                trim(clsname);
471                Neuro *receptor = model.addNewNeuro();
472                receptor->setClassName(clsname);
473                NeuroClass *cls = receptor->getClass();
474                if (cls)
475                {
476                        if (cls->getPreferredLocation() == 2) receptor->attachToJoint(getLastJoint());
477                        else if (cls->getPreferredLocation() == 1) receptor->attachToPart(getLastPart());
478                }
479                last_f1_neuro->addInput(receptor, ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str()));
480                if (usemapping) receptor->addMapping(*makeRange(begin, end - 1));
481        }
482        else if ((begin[0] == '>') && (begin[1]))
483        {
484                Neuro *out = model.addNewNeuro();
485                out->addInput(last_f1_neuro, ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str()));
486                out->setClassName(SString(begin + 1, end - colon - 1));
487                if (begin[1] == '@')
488                {
489                        sprintf(tmp, "p=%lg", lastjoint_muscle_power);
490                        out->setClassParams(tmp);
491                }
492                else if (begin[1] == '|')
493                {
494                        sprintf(tmp, "p=%lg,r=%lg", lastjoint_muscle_power, props.muscle_bend_range);
495                        out->setClassParams(tmp);
496                }
497                NeuroClass *cls = out->getClass();
498                if (cls)
499                {
500                        if (cls->getPreferredLocation() == 2) out->attachToJoint(getLastJoint());
501                        else if (cls->getPreferredLocation() == 1) out->attachToPart(getLastPart());
502                }
503                if (usemapping) out->addMapping(*makeRange(begin, end - 1));
504        }
505        else if (*begin == '!') addClassParam("fo", ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str()));
506        else if (*begin == '=') addClassParam("in", ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str()));
507        else if (*begin == '/') addClassParam("si", ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str()));
508        else if (islower(begin[0]))
509        {
510                SString name(begin, colon - begin);
511                SString value(colon + 1, end - (colon + 1));
512                addClassParam(name.c_str(), value.c_str());
513        }
514}
Note: See TracBrowser for help on using the repository browser.