// This file is a part of Framsticks SDK. http://www.framsticks.com/ // Copyright (C) 1999-2023 Maciej Komosinski and Szymon Ulatowski. // See LICENSE.txt for details. #include "f1_conv.h" #include #include #include #include #include #include #include //#define v1f1COMPATIBLE //as in ancient Framsticks 1.x #define FIELDSTRUCT GenoConv_f1 static ParamEntry f1_conv_paramtab[] = { { "Genetics: Converters: f1 to f0", 1, 3, }, { "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." }, { "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." }, { "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" "- Full/NumberOfBranches - a compromise between the two other settings.\n" "- 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" "- 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."}, { 0, }, }; #undef FIELDSTRUCT class Builder { public: 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) { switch (settings.modifier_compatibility) { case GenoConv_f1_Settings::MODIF_COMPAT_LEGACY: gp_ops = GeneProps::getLegacyOps(); break; case GenoConv_f1_Settings::MODIF_COMPAT_ALL_CHANGE_05: gp_ops = GeneProps::getAllChange05Ops(); break; } } ~Builder() { if (own_first_part_mapping) SAFEDELETE(first_part_mapping); } GenoConv_f1_Settings& settings; GenePropsOps* gp_ops; char tmp[222]; bool invalid; Model model; const char *genbegin; SList neuro_f1_to_f0; // neuro_f1_to_f0(f1_refno) = actual neuro pointer Neuro *last_f1_neuro; SyntParam *neuro_cls_param; struct Connection { int n1, n2; double w; Connection(int _n1, int _n2, double _w) :n1(_n1), n2(_n2), w(_w) {} }; SListTempl connections; int usemapping; MultiRange range; MultiRange *first_part_mapping; bool own_first_part_mapping; double lastjoint_muscle_power; double model_energy; int model_energy_count; void grow(int part1, const char*g, Pt3D k, GeneProps c, int branching_part); void setPartMapping(int p, const char* g); int growJoint(int part1, int part2, const Pt3D &angle, const GeneProps &c, const char *g); int growPart(GeneProps &c, const char *g); const char *skipNeuro(const char *z); const char* growNeuro(const char* t, GeneProps &c, int&); void growConnection(const char* begin, const char* colon, const char* end, GeneProps& props); int countBranches(const char*g, SList &out); SyntParam* lastNeuroClassParam(); void addClassParam(const char* name, double value); void addClassParam(const char* name, const char* value); const MultiRange* makeRange(const char*g) { return makeRange(g, g); } const MultiRange* makeRange(const char*g, const char*g2); Part *getLastPart() { return getLastJoint()->part2; } Neuro *getLastNeuro() { return model.getNeuro(model.getNeuroCount() - 1); } Joint *getLastJoint() { return model.getJoint(model.getJointCount() - 1); } void addOrRememberInput(int n1, int n2, double w) { //if (!addInput(n1,n2,w,false)) connections += Connection(n1, n2, w); } bool addInput(int n1, int n2, double w, bool final) { if ((n1 < 0) || (n2 < 0) || (n1 >= neuro_f1_to_f0.size()) || (n2 >= neuro_f1_to_f0.size())) { if (final) logPrintf("GenoConvF1", "addInput", LOG_WARN, "illegal neuron connection %d <- %d (ignored)", n1, n2); return 0; } Neuro *neuro = (Neuro*)neuro_f1_to_f0(n1); Neuro *input = (Neuro*)neuro_f1_to_f0(n2); neuro->addInput(input, w); return 1; } void addPendingInputs() { for (int i = 0; i < connections.size(); i++) { Connection *c = &connections(i); addInput(c->n1, c->n2, c->w, true); } } }; const MultiRange* Builder::makeRange(const char*g, const char*g2) { if (!usemapping) return 0; range.clear(); range.add(g - genbegin, g2 - genbegin); return ⦥ } GenoConv_f1::GenoConv_f1() { name = "Recursive encoding"; in_format = '1'; mapsupport = 1; param.setParamTab(f1_conv_paramtab); param.select(this); param.setDefault(); } /** main conversion function - with conversion map support */ SString GenoConv_f1::convert(SString &i, MultiMap *map, bool using_checkpoints) { const char* g = i.c_str(); Builder builder(settings, g, map ? 1 : 0); builder.model.open(using_checkpoints); builder.grow(-1, g, Pt3D_0, GeneProps::standard_values, -1); // uses Model::addFromString() to create model elements if (builder.invalid) return SString(); builder.addPendingInputs(); builder.model.startenergy = (builder.model_energy_count > 0) ? (builder.model_energy / builder.model_energy_count) : 1.0; builder.model.close(); // model is ready to use now if (map) builder.model.getCurrentToF0Map(*map); // generate f1-to-f0 conversion map return builder.model.getF0Geno().getGenes(); } void Builder::setPartMapping(int p, const char* g) { if (!usemapping) return; const MultiRange *r = makeRange(g); if (p < 0) { //special case: mapping the part which is not yet created if (first_part_mapping) first_part_mapping->add(*r); else { first_part_mapping = new MultiRange(*r); own_first_part_mapping = true; } } else model.getPart(p)->addMapping(*r); } void Builder::grow(int part1, const char*g, Pt3D k, GeneProps c, int branching_part) { int hasmuscles = 0; if (settings.cq_mod_influence == settings.CQ_INFLUENCE_OLD) k += Pt3D(c.twist, 0, c.curvedness); while (1) { if (c.executeModifier(*g, gp_ops) == 0) { setPartMapping(part1, g); } else { switch (*g) { case 0: return; case ',': case ')': setPartMapping(branching_part, g); return; case 'R': k.x += 0.7853; setPartMapping(part1, g); break; // 45 degrees = pi/4 like in f4 case 'r': k.x -= 0.7853; setPartMapping(part1, g); break; case '[': //neuron // setdebug(g-(char*)geny,DEBUGNEURO | !l_neu); if (model.getJointCount()) g = growNeuro(g + 1, c, hasmuscles); else { logMessage("GenoConv_F1", "grow", 1, "Illegal neuron position (ignored)"); g = skipNeuro(g + 1); } break; case 'X': { int freshpart = 0; //setdebug(g-(char*)geny,DEBUGEST | !l_est); if (part1 < 0) //initial grow { if (model.getPartCount() > 0) part1 = 0; else { part1 = growPart(c, g); freshpart = 1; if (first_part_mapping) { //mapping was defined before creating this initial Part -> put it into the Part assert(own_first_part_mapping); model.getPart(part1)->setMapping(*first_part_mapping); delete first_part_mapping; //first_part_mapping can be still used later but from now on it references the internal Part mapping first_part_mapping = model.getPart(part1)->getMapping(); own_first_part_mapping = false; } } } if (!freshpart) { Part *part = model.getPart(part1); part->density = ((part->mass * part->density) + 1.0 / c.weight) / (part->mass + 1.0); // v=m*d // part->volume+=1.0/c.weight; part->mass += 1.0; } if (settings.modifier_compatibility == settings.MODIF_COMPAT_LEGACY) model_energy += 0.9 * c.energy + 0.1; else model_energy += c.energy; model_energy_count++; int part2 = growPart(c, g); Pt3D new_k; switch (settings.cq_mod_influence) { case GenoConv_f1_Settings::CQ_INFLUENCE_OLD: new_k = k; break; case GenoConv_f1_Settings::CQ_INFLUENCE_NEW: new_k = k + Pt3D(c.twist, 0, c.curvedness); break; } growJoint(part1, part2, new_k, c, g); // est* e = new est(*s,*s2,k,c,zz,this); // attenuate properties as they are propagated along the structure c.propagateAlong(true); model.checkpoint(); grow(part2, g + 1, Pt3D_0, c, branching_part); return; } case '(': { setPartMapping(part1, g); SList ga; int count = countBranches(g + 1, ga); c.muscle_reset_range = false; switch (settings.branch_muscle_range) { case GenoConv_f1_Settings::MUSCLE_RANGE_ORIGINAL: c.muscle_bend_range = 1.0 / count; break; case GenoConv_f1_Settings::MUSCLE_RANGE_STRICT: c.muscle_bend_range = 1.0 / (count + 1); break; case GenoConv_f1_Settings::MUSCLE_RANGE_UNLIMITED: c.muscle_bend_range = 1.0; break; } for (int i = 0; i < count; i++) grow(part1, (char*)ga(i), k + Pt3D(0, 0, -M_PI + (i + 1) * (2 * M_PI / (count + 1))), c, part1); return; } case ' ': case '\t': case '\n': case '\r': break; default: invalid = 1; return; } } g++; } } SyntParam* Builder::lastNeuroClassParam() { if (!neuro_cls_param) { NeuroClass *cls = last_f1_neuro->getClass(); if (cls) { neuro_cls_param = new SyntParam(last_f1_neuro->classProperties()); // this is equivalent to: // SyntParam tmp=last_f1_neuro->classProperties(); // neuro_cls_param=new SyntParam(tmp); // interestingly, some compilers eliminate the call to new SyntParam, // realizing that a copy constructor is redundant when the original object is // temporary. there are no side effect of such optimization, as long as the // copy-constructed object is exact equivalent of the original. } } return neuro_cls_param; } void Builder::addClassParam(const char* name, double value) { lastNeuroClassParam(); if (neuro_cls_param) neuro_cls_param->setDoubleById(name, value); } void Builder::addClassParam(const char* name, const char* value) { lastNeuroClassParam(); if (neuro_cls_param) { ExtValue e(value); const ExtValue &re(e); neuro_cls_param->setById(name, re); } } int Builder::countBranches(const char*g, SList &out) { int gl = 0; out += (void*)g; while (gl >= 0) { switch (*g) { case 0: gl = -1; break; case '(': case '[': ++gl; break; case ')': case ']': --gl; break; case ',': if (!gl) out += (void*)(g + 1); } g++; } return out.size(); } int Builder::growJoint(int part1, int part2, const Pt3D &angle, const GeneProps &c, const char *g) { double len = std::min(2.0, c.length); sprintf(tmp, "p1=%d,p2=%d,dx=%lg,rx=%lg,ry=%lg,rz=%lg,stam=%lg,vr=%g,vg=%g,vb=%g", part1, part2, len, angle.x, angle.y, angle.z, c.stamina, c.cred, c.cgreen, c.cblue); lastjoint_muscle_power = c.muscle_power; return model.addFromString(Model::JointType, tmp, makeRange(g)); } int Builder::growPart(GeneProps &c, const char *g) { sprintf(tmp, "dn=%lg,fr=%lg,ing=%lg,as=%lg,vr=%g,vg=%g,vb=%g", 1.0 / c.weight, c.friction, c.ingestion, c.assimilation, c.cred, c.cgreen, c.cblue); return model.addFromString(Model::PartType, tmp, makeRange(g)); } const char *Builder::skipNeuro(const char *z) { for (; *z; z++) if ((*z == ']') || (*z == ')')) break; return z - 1; } const char* Builder::growNeuro(const char* t, GeneProps& props, int &hasmuscles) { const char*neuroend = skipNeuro(t); last_f1_neuro = model.addNewNeuro(); neuro_cls_param = NULL; last_f1_neuro->attachToPart(getLastPart()); const MultiRange *mr = makeRange(t - 1, neuroend + 1); if (mr) last_f1_neuro->addMapping(*mr); neuro_f1_to_f0 += last_f1_neuro; SString clsname; bool haveclass = 0; while (*t && *t <= ' ') t++; const char* next = (*t) ? (t + 1) : t; while (*next && *next <= ' ') next++; if (*t && *next != ',' && *next != ']') // old style muscles [|rest] or [@rest] switch (*t) { case '@': if (t[1] == ':') break; haveclass = 1; // if (!(hasmuscles&1)) { hasmuscles |= 1; Neuro *muscle = model.addNewNeuro(); sprintf(tmp, "@:p=%lg", lastjoint_muscle_power); muscle->addInput(last_f1_neuro); muscle->setDetails(tmp); muscle->attachToJoint(getLastJoint()); if (usemapping) muscle->addMapping(*makeRange(t)); } t++; break; case '|': if (t[1] == ':') break; haveclass = 1; // if (!(hasmuscles&2)) { hasmuscles |= 2; Neuro *muscle = model.addNewNeuro(); sprintf(tmp, "|:p=%lg,r=%lg", lastjoint_muscle_power, props.muscle_bend_range); muscle->addInput(last_f1_neuro); muscle->setDetails(tmp); muscle->attachToJoint(getLastJoint()); if (usemapping) muscle->addMapping(*makeRange(t)); } t++; break; } while (*t && *t <= ' ') t++; bool finished = 0; const char *begin = t; const char* colon = 0; SString classparams; while (!finished) { switch (*t) { case ':': colon = t; break; case 0: case ']': case ')': finished = 1; // NO break! case ',': if (!haveclass && !colon && t > begin) { haveclass = 1; SString clsname(begin, t - begin); clsname = trim(clsname); last_f1_neuro->setClassName(clsname); NeuroClass *cls = last_f1_neuro->getClass(); if (cls) { if (cls->getPreferredLocation() == 2) last_f1_neuro->attachToJoint(getLastJoint()); else if (cls->getPreferredLocation() == 1) last_f1_neuro->attachToPart(getLastPart()); lastNeuroClassParam(); //special handling: muscle properties (can be overwritten by subsequent property assignments) if (!strcmp(cls->getName().c_str(), "|")) { neuro_cls_param->setDoubleById("p", lastjoint_muscle_power); neuro_cls_param->setDoubleById("r", props.muscle_bend_range); } else if (!strcmp(cls->getName().c_str(), "@")) { neuro_cls_param->setDoubleById("p", lastjoint_muscle_power); } } } else if (colon && (colon > begin) && (t > colon)) growConnection(begin, colon, t, props); if (t[0] != ',') t--; begin = t + 1; colon = 0; break; } t++; } SAFEDELETE(neuro_cls_param); return t; } void Builder::growConnection(const char* begin, const char* colon, const char* end, GeneProps& props) { while (*begin && *begin <= ' ') begin++; int i; if (isdigit(begin[0]) || (begin[0] == '-')) { double conn_weight = ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str()); paInt relative = ExtValue::getInt(trim(SString(begin, colon - begin)).c_str(), false); int this_refno = neuro_f1_to_f0.size() - 1; addOrRememberInput(this_refno, this_refno + relative, conn_weight); } else if ((i = last_f1_neuro->extraProperties().findIdn(begin, colon - begin)) >= 0) { last_f1_neuro->extraProperties().setFromString(i, colon + 1); } else if (isupper(begin[0]) || strchr("*|@", begin[0])) { SString clsname(begin, colon - begin); trim(clsname); Neuro *receptor = model.addNewNeuro(); receptor->setClassName(clsname); NeuroClass *cls = receptor->getClass(); if (cls) { if (cls->getPreferredLocation() == 2) receptor->attachToJoint(getLastJoint()); else if (cls->getPreferredLocation() == 1) receptor->attachToPart(getLastPart()); } last_f1_neuro->addInput(receptor, ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str())); if (usemapping) receptor->addMapping(*makeRange(begin, end - 1)); } else if ((begin[0] == '>') && (begin[1])) { Neuro *out = model.addNewNeuro(); out->addInput(last_f1_neuro, ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str())); out->setClassName(SString(begin + 1, end - colon - 1)); if (begin[1] == '@') { sprintf(tmp, "p=%lg", lastjoint_muscle_power); out->setClassParams(tmp); } else if (begin[1] == '|') { sprintf(tmp, "p=%lg,r=%lg", lastjoint_muscle_power, props.muscle_bend_range); out->setClassParams(tmp); } NeuroClass *cls = out->getClass(); if (cls) { if (cls->getPreferredLocation() == 2) out->attachToJoint(getLastJoint()); else if (cls->getPreferredLocation() == 1) out->attachToPart(getLastPart()); } if (usemapping) out->addMapping(*makeRange(begin, end - 1)); } else if (*begin == '!') addClassParam("fo", ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str())); else if (*begin == '=') addClassParam("in", ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str())); else if (*begin == '/') addClassParam("si", ExtValue::getDouble(trim(SString(colon + 1, end - (colon + 1))).c_str())); else if (islower(begin[0])) { SString name(begin, colon - begin); SString value(colon + 1, end - (colon + 1)); addClassParam(name.c_str(), value.c_str()); } }