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

Last change on this file was 1258, checked in by Maciej Komosinski, 9 months ago

f1->f0 conversion settings: modifier compatibility, 'C' and 'Q' modifier influence, bending muscle default range

  • Property svn:eol-style set to native
File size: 18.7 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 1 (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        sprintf(tmp, "p1=%d,p2=%d,dx=%lg,rx=%lg,ry=%lg,rz=%lg,stam=%lg,vr=%g,vg=%g,vb=%g",
334                part1, part2, len, angle.x, angle.y, angle.z, c.stamina, c.cred, c.cgreen, c.cblue);
335        lastjoint_muscle_power = c.muscle_power;
336        return model.addFromString(Model::JointType, tmp, makeRange(g));
337}
338
339int Builder::growPart(GeneProps &c, const char *g)
340{
341        sprintf(tmp, "dn=%lg,fr=%lg,ing=%lg,as=%lg,vr=%g,vg=%g,vb=%g",
342                1.0 / c.weight, c.friction, c.ingestion, c.assimilation, c.cred, c.cgreen, c.cblue);
343        return model.addFromString(Model::PartType, tmp, makeRange(g));
344}
345
346const char *Builder::skipNeuro(const char *z)
347{
348        for (; *z; z++) if ((*z == ']') || (*z == ')')) break;
349        return z - 1;
350}
351
352const char* Builder::growNeuro(const char* t, GeneProps& props, int &hasmuscles)
353{
354        const char*neuroend = skipNeuro(t);
355        last_f1_neuro = model.addNewNeuro();
356        neuro_cls_param = NULL;
357        last_f1_neuro->attachToPart(getLastPart());
358        const MultiRange *mr = makeRange(t - 1, neuroend + 1);
359        if (mr) last_f1_neuro->addMapping(*mr);
360        neuro_f1_to_f0 += last_f1_neuro;
361
362        SString clsname;
363        bool haveclass = 0;
364        while (*t && *t <= ' ') t++;
365        const char* next = (*t) ? (t + 1) : t;
366        while (*next && *next <= ' ') next++;
367        if (*t && *next != ',' && *next != ']') // old style muscles [|rest] or [@rest]
368                switch (*t)
369                {
370                case '@': if (t[1] == ':') break;
371                        haveclass = 1;
372                        //              if (!(hasmuscles&1))
373                        {
374                                hasmuscles |= 1;
375                                Neuro *muscle = model.addNewNeuro();
376                                sprintf(tmp, "@:p=%lg", lastjoint_muscle_power);
377                                muscle->addInput(last_f1_neuro);
378                                muscle->setDetails(tmp);
379                                muscle->attachToJoint(getLastJoint());
380                                if (usemapping) muscle->addMapping(*makeRange(t));
381                        }
382                        t++;
383                        break;
384                case '|': if (t[1] == ':') break;
385                        haveclass = 1;
386                        //              if (!(hasmuscles&2))
387                        {
388                                hasmuscles |= 2;
389                                Neuro *muscle = model.addNewNeuro();
390                                sprintf(tmp, "|:p=%lg,r=%lg", lastjoint_muscle_power, props.muscle_bend_range);
391                                muscle->addInput(last_f1_neuro);
392                                muscle->setDetails(tmp);
393                                muscle->attachToJoint(getLastJoint());
394                                if (usemapping) muscle->addMapping(*makeRange(t));
395                        }
396                        t++;
397                        break;
398                }
399        while (*t && *t <= ' ') t++;
400        bool finished = 0;
401        const char *begin = t;
402        const char* colon = 0;
403        SString classparams;
404        while (!finished)
405        {
406                switch (*t)
407                {
408                case ':': colon = t; break;
409                case 0: case ']': case ')': finished = 1;
410                        // NO break!
411                case ',':
412                        if (!haveclass && !colon && t > begin)
413                        {
414                                haveclass = 1;
415                                SString clsname(begin, t - begin);
416                                clsname = trim(clsname);
417                                last_f1_neuro->setClassName(clsname);
418                                NeuroClass *cls = last_f1_neuro->getClass();
419                                if (cls)
420                                {
421                                        if (cls->getPreferredLocation() == 2)
422                                                last_f1_neuro->attachToJoint(getLastJoint());
423                                        else if (cls->getPreferredLocation() == 1)
424                                                last_f1_neuro->attachToPart(getLastPart());
425
426                                        lastNeuroClassParam();
427                                        //special handling: muscle properties (can be overwritten by subsequent property assignments)
428                                        if (!strcmp(cls->getName().c_str(), "|"))
429                                        {
430                                                neuro_cls_param->setDoubleById("p", lastjoint_muscle_power);
431                                                neuro_cls_param->setDoubleById("r", props.muscle_bend_range);
432                                        }
433                                        else if (!strcmp(cls->getName().c_str(), "@"))
434                                        {
435                                                neuro_cls_param->setDoubleById("p", lastjoint_muscle_power);
436                                        }
437                                }
438                        }
439                        else if (colon && (colon > begin) && (t > colon))
440                                growConnection(begin, colon, t, props);
441                        if (t[0] != ',') t--;
442                        begin = t + 1; colon = 0;
443                        break;
444                }
445                t++;
446        }
447        SAFEDELETE(neuro_cls_param);
448        return t;
449}
450void Builder::growConnection(const char* begin, const char* colon, const char* end, GeneProps& props)
451{
452        while (*begin && *begin <= ' ') begin++;
453        int i;
454        if (isdigit(begin[0]) || (begin[0] == '-'))
455        {
456                double conn_weight = ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str());
457                paInt relative = ExtValue::getInt(trim(SString(begin, colon - begin)).c_str(), false);
458                int this_refno = neuro_f1_to_f0.size() - 1;
459                addOrRememberInput(this_refno, this_refno + relative, conn_weight);
460        }
461        else if ((i = last_f1_neuro->extraProperties().findIdn(begin, colon - begin)) >= 0)
462        {
463                last_f1_neuro->extraProperties().setFromString(i, colon + 1);
464        }
465        else if (isupper(begin[0]) || strchr("*|@", begin[0]))
466        {
467                SString clsname(begin, colon - begin);
468                trim(clsname);
469                Neuro *receptor = model.addNewNeuro();
470                receptor->setClassName(clsname);
471                NeuroClass *cls = receptor->getClass();
472                if (cls)
473                {
474                        if (cls->getPreferredLocation() == 2) receptor->attachToJoint(getLastJoint());
475                        else if (cls->getPreferredLocation() == 1) receptor->attachToPart(getLastPart());
476                }
477                last_f1_neuro->addInput(receptor, ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str()));
478                if (usemapping) receptor->addMapping(*makeRange(begin, end - 1));
479        }
480        else if ((begin[0] == '>') && (begin[1]))
481        {
482                Neuro *out = model.addNewNeuro();
483                out->addInput(last_f1_neuro, ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str()));
484                out->setClassName(SString(begin + 1, end - colon - 1));
485                if (begin[1] == '@')
486                {
487                        sprintf(tmp, "p=%lg", lastjoint_muscle_power);
488                        out->setClassParams(tmp);
489                }
490                else if (begin[1] == '|')
491                {
492                        sprintf(tmp, "p=%lg,r=%lg", lastjoint_muscle_power, props.muscle_bend_range);
493                        out->setClassParams(tmp);
494                }
495                NeuroClass *cls = out->getClass();
496                if (cls)
497                {
498                        if (cls->getPreferredLocation() == 2) out->attachToJoint(getLastJoint());
499                        else if (cls->getPreferredLocation() == 1) out->attachToPart(getLastPart());
500                }
501                if (usemapping) out->addMapping(*makeRange(begin, end - 1));
502        }
503        else if (*begin == '!') addClassParam("fo", ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str()));
504        else if (*begin == '=') addClassParam("in", ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str()));
505        else if (*begin == '/') addClassParam("si", ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str()));
506        else if (islower(begin[0]))
507        {
508                SString name(begin, colon - begin);
509                SString value(colon + 1, end - (colon + 1));
510                addClassParam(name.c_str(), value.c_str());
511        }
512}
Note: See TracBrowser for help on using the repository browser.